PawFlow provides five persistent cognitive systems plus two scoped work-state layers. The distinction matters: durable facts, an agent's experience, unfinished work, and temporary evidence have different owners, lifetimes, and retrieval rules.
The five persistent systems and two work-state layers:
| System | Purpose | Storage |
|---|---|---|
| Memory | Persistent facts, preferences, events | data/memories/{user}.json |
| Knowledge Graph | Entity-relationship triples with temporal validity | data/knowledge_graphs/{user}.json |
| Agent Diary | Per-agent journal of observations and decisions | data/memories/{user}/diary_{agent}.jsonl |
| Project Graph | AST-based code structure graph (17 languages) | data/runtime/graphs/{safe_user}/{safe_relay}/graph.json |
| Project Wiki | LLM-maintained Markdown with source-hash provenance | data/runtime/project_wikis/{safe_user}/{safe_relay}/ |
| Todo List | Authoritative unfinished work for one conversation agent | data/runtime/todolists/todos.sqlite3 |
| Scratchpad | Expiring working evidence and resume notes for one conversation agent | data/runtime/scratchpads/scratchpads.sqlite3 |
| ScratchDir | Relay-backed temporary files scoped to one conversation agent | data/runtime/scratchdirs/scratchdirs.sqlite3 + relay runtime root |
A procedural layer — the skill loop — closes learning into reusable artifacts: agents are instructed (via a ## Skill loop system-prompt block) to crystallize novel multi-step procedures into skills with manage_resource and to fix skills that proved wrong during use; each completed compaction bucket or rollup can produce a structured skill-draft memory; recurrence in a different conversation automatically promotes the procedure to a user skill; the Memories UI still exposes pending drafts for reviewed promotion or deletion; load_skill tracks usage and suggests scope promotion; and the skillCurator flow task produces review-first maintenance reports. See LEARNING_LOOP_PLAN.md.
They are interconnected:
- Memory and diary digests are injected dynamically on API turns and serialized into cold CLI bootstraps. Scratchpad note bodies are never injected; only a count, expiry, and up to five topic labels are shown.
- When a memory is stored, the system cross-checks the knowledge graph for contradictions and warns the agent.
- Auto-extraction triggers periodically pull facts from conversation text into both memory and KG.
- The project graph and project wiki are relay-scoped, refreshed asynchronously for the active relay, and shared by every conversation and agent using that project.
- ScratchDir is the file counterpart of Scratchpad. Use
fs://scratchdir/(or/scratchin shell execution) for temporary files that must survive one tool call. Its UI shows bounded file metadata, quotas, expiry, renewal, exact clear, and explicit promotion to FileStore; it never exposes the relay's physical root. Symbolic links are accepted only when their resolved targets remain inside the same scoped root; escaping or cyclic links fail closed. Filesystem handlers validate a cached scoped ticket against the current store epoch before reuse, soclearfollowed byensureimmediately rebinds instead of retaining a stale facade. If usage is already above quota, status and read-only inspection remain available anddelete_filemay be repeated until compliance is restored; operations that can grow the directory continue to fail closed. Confinement and symbolic-link validation remain enforced during recovery. - ScratchDir metadata is disposable by contract. If SQLite reports a proven
corruption signature, PawFlow renames the metadata database and any WAL/SHM
companions with a
.corrupt-<timestamp>suffix for forensic inspection, then creates an empty ScratchDir store. This recovery is deliberately limited to ScratchDir: durable stores are never discarded or recreated automatically. The ScratchDir manager is opened only when that tool is executed, so damaged temporary metadata cannot prevent unrelated MCP filesystem tools from being wired and used. ScratchDir SQLite connections use WAL journaling, full synchronous durability, and cell-size validation. Startup runs aPRAGMA quick_checkcanary before the store is exposed; WAL moves commit-time page writes into the log but does not by itself identify the source of prior page corruption. See SQLite corruption diagnostics for the opt-in, fail-fast bootstrap canary.
The memory system stores persistent facts per user. Memories survive across conversations and are scoped by visibility, organized by taxonomy, and support temporal validity.
Memories are classified by category:
| Field | Purpose | Values |
|---|---|---|
category |
Memory type | facts, events, discoveries, preferences, advice |
Categories can be filtered directly with recall(category=...).
Every memory has a visibility scope determined by the agent and conversation_id fields:
| Scope | agent |
conversation_id |
Visible to |
|---|---|---|---|
| global | "" |
"" |
All agents in all conversations |
| agent | "coder" |
"" |
Only this agent, in any conversation |
| conversation | "" |
"abc123" |
All agents, but only in this conversation |
| private | "coder" |
"abc123" |
Only this agent in this specific conversation |
When recalling, results are sorted by scope priority: private > conversation > agent > global.
Example -- storing a global memory:
{
"text": "User's timezone is Europe/Paris",
"tags": ["preference", "timezone"],
"scope": "global",
"category": "preferences"
}Memories support time-bounded validity:
valid_from(epoch float): When the fact became true.0means valid since creation.ended(epoch float): When the fact stopped being true.0means still active.
The end_memory method marks a memory as ended without deleting it. The as_of parameter on recall filters to memories valid at a specific point in time.
Example -- ending a memory:
# The user switched from PostgreSQL to SQLite
> end_memory(memory_id="a1b2c3d4e5f6")
# Then store the new fact:
> remember("User switched to SQLite for local storage", tags=["decision"], category="facts")
Memories are extracted automatically in two situations:
-
Periodic auto-save -- Every ~15 user messages, the system extracts key facts from recent conversation text using the
summarizer_serviceLLM. The counter is tracked per-agent via conversation extras (_auto_save_count:{agent}). -
Post-compaction extraction -- When a conversation is compacted, the bucket or rollup summary is fed to
process_post_compaction_learning(). The operation resolves theauto_memoryandskill_learningroles itself; callers cannot inject the active agent client. When both roles resolve to the same live LLM, one ephemeral JSON call returns independentmemoriesandskillfields. Distinct explicit role bindings retain one call per service. Each field is validated and persisted separately, so an invalid or failed memory result does not suppress a valid skill draft, and vice versa. Memory extraction remains conservative: it stores at most two durable memories, rejects ephemeral/current-task state, and recordsimportance,durability,scope, andttl_days. Extracted memories are tagged["auto-extracted", "compaction"].
Compaction auto-extract does not write global permanent memories by default. Only durable high/critical user preferences or advice may become global. Project/debug facts are stored in conversation scope with a TTL unless explicitly classified as durable. Existing stale auto-extracted entries can be marked ended with scripts/memory_gc.py; ended memories remain in the raw JSON audit trail but are ignored by normal recall and the memory panel.
Memory embeddings are optional and use the normal expression cascade. If embedding_llm_service is set to an LLM service that exposes an OpenAI-compatible embeddings endpoint, PawFlow uses that service for remember, semantic_recall, auto-extracted memories, and mirrored Claude Code memories. If the parameter is absent or unusable, PawFlow falls back to the local MiniLM embedder when it is installed; otherwise memories are stored without vectors and remain available through keyword recall.
At every conversation turn, a compact multi-tier digest is built from the user's memories and injected into the system prompt under ## Persistent memory. The tiers are:
| Tier | Source | Max items |
|---|---|---|
| L0 | Identity/profile (tags: identity, profile) |
3 |
| L1 | Key facts (category: facts) |
5 |
| L1 | Preferences (category: preferences) |
3 |
| L2 | Recent events (category: events, sorted by date) |
3 |
| L3 | Active decisions (tags: decision, category: facts) |
3 |
| L4 | Discoveries (category: discoveries) |
3 |
| L4 | Advice (category: advice) |
2 |
| KG | God nodes (most connected entities from Knowledge Graph) | 5 |
The digest is capped at 1200 characters by default. If there are no relevant memories, nothing is injected.
The knowledge graph stores facts as temporal (subject, predicate, object) triples per user. It supports contradiction detection, graph traversal, and community analysis.
A triple represents a single fact:
{
"id": "a1b2c3d4e5f6",
"subject": "PawFlow",
"predicate": "uses",
"object": "tree-sitter",
"valid_from": "2025-06",
"valid_to": "",
"confidence": "EXTRACTED",
"confidence_score": 1.0,
"source": "conversation",
"extracted_at": 1712345678.0
}| Level | Score range | Meaning |
|---|---|---|
| EXTRACTED | >= 0.9 | Directly stated by the user or explicitly observed |
| INFERRED | 0.5 -- 0.89 | Deduced from context or indirect evidence |
| AMBIGUOUS | < 0.5 | Uncertain, possibly contradictory |
Confidence can be provided as a string ("EXTRACTED", "INFERRED", "AMBIGUOUS") or as a numeric score. Numeric scores are automatically mapped to the corresponding label.
When adding a triple, the system checks for active triples with the same subject and predicate but a different object. If found, the response includes a contradictions list:
> kg_add(subject="Quentin", predicate="prefers_editor", object="Neovim")
added: Quentin -> prefers_editor -> Neovim (id: x1y2z3)
Warning: Contradicts active values: VS Code
The old triple is NOT automatically invalidated. The agent must decide whether to call kg_invalidate on the old value.
Each triple has:
valid_from: ISO date string (e.g."2026-01") -- when the fact became true.valid_to: ISO date string -- when the fact expired. Empty string""means still active.
The query_entity method supports an as_of parameter to retrieve only facts valid at a specific date.
The query_graph method traverses the graph starting from entities matching a question:
- BFS (default): Broad context -- explores all seeds in parallel, returning a wide view of connections up to the specified depth.
- DFS: Deep path -- traces a single path from the first matching entity, going deep before wide.
Parameters: question (text to match), mode ("bfs" or "dfs"), depth (default 3), max_results (default 50).
Only active triples (with empty valid_to) are traversed.
Example:
> query_graph(question="authentication", mode="bfs", depth=2)
Graph traversal for 'authentication' (7 connections):
[EXTRACTED] AuthGateway -> supports -> Google
[EXTRACTED] AuthGateway -> supports -> GitHub
[EXTRACTED] AuthGateway -> implements -> OAuth2
[INFERRED] OAuth2 -> used_by -> IdentityService
...
God nodes are the most connected entities in the graph, ranked by degree (number of active triples referencing them as subject or object). Useful for identifying central concepts.
> kg_god_nodes(limit=5)
Most connected entities:
PawFlow (23 connections)
Quentin (15 connections)
AuthGateway (12 connections)
PostgreSQL (8 connections)
Docker (7 connections)
The diary is a per-agent journal that persists across conversations. Unlike memories (facts about the user/project/world), it stores the agent's own first-person decisions, lessons, recurring failure patterns, and reflections. Write after a non-obvious choice or a lesson likely to improve future work, not as a routine turn log. Use the todo list for unfinished work and the scratchpad for temporary evidence.
| Type | When to use |
|---|---|
observation |
A recurring or consequential pattern the agent noticed (default) |
decision |
A choice the agent made and why |
learning |
A lesson learned from experience |
reflection |
Higher-level thinking about patterns |
Example:
> diary_write(
entry="User prefers concise error messages over detailed stack traces in production logs. This seems to be a UX-driven decision.",
type="observation",
tags=["logging", "ux"]
)
The 10 most recent diary entries are built into a compact digest (max 600 characters) and injected into the system prompt under ## Your diary (past observations). Each entry's text is truncated to 100 chars.
| Layer | Put this here | Scope / lifetime | Context behavior |
|---|---|---|---|
| Memory | Durable facts/preferences/events about user, project, or world | Configurable visibility; persistent | Digest plus explicit keyword/semantic recall |
| Diary | Agent's durable decisions, lessons, patterns, reflections | User + agent; persistent across conversations | Last 10 entries injected; older/type-filtered entries via diary_read |
| Todo | Authoritative unfinished work and verification state | User + conversation + agent; durable until completed | Active and recent completed items injected |
| Scratchpad | Temporary evidence, hypotheses, local decisions, resume cues | User + conversation + agent; TTL 1-720 hours | Only topics/count/expiry hinted; note bodies require list or get |
The project graph builds a structural code graph from a codebase using tree-sitter AST extraction. The relay ID is the project identity, so one cached graph is shared across conversations and agents attached to that relay. Extraction runs on the relay where the source files live.
Automatic builds are opt-in. They run only when the variable
PAWFLOW_PROJECT_GRAPH_AUTO is truthy (1, true, yes, on), resolved
from the process environment first, then the conversation → user → global
variable cascade (for example manage_variable at user scope). When opted in,
initial context preparation schedules a background build, and successful
relay writes and shell commands schedule a debounced incremental refresh.
Otherwise project maintenance skips the graph (the wiki scan still runs,
seeded from the existing graph). The manual build action and the UI Build
button always work, for a first build, recovery or an explicit root change.
Automatic maintenance always indexes the relay container, even when a server-local
mutation triggered the refresh; otherwise it would index the deployed runtime
instead of the relay-scoped project. An explicit manual build(local=true)
remains available when the caller intentionally targets the local surface.
Each build runs as a single relay exec; the extraction script travels
base64-encoded in the PAWFLOW_GRAPH_SCRIPT env var and is executed in memory
by a tiny fixed command, without writing a helper file into the source tree.
Nothing sizeable rides in the command line, keeping it under the Windows
cmd.exe 8191-char cap. The script bootstraps sys.path itself before importing
the extractor, trying PAWFLOW_RELAY_CODE_DIR then /opt/pawflow, since the
relay exec env carries no PYTHONPATH. Managed relay runtimes stage the
integrated graphify package alongside the relay handlers and include it in
the runtime source hash, so server upgrades cannot reuse a stale runtime that
lacks the extractor. Desktop and CLI relays bind-mount the same package from
<runtime root>/core/graphify onto /opt/pawflow/graphify; the Relay Desktop
runtime bundle (prepare-runtime.js) ships it. Extraction writes nothing into
the project tree: incremental rebuilds rely on the fingerprint map below, not
on a per-file graphify-out/cache. Small deltas retain
Graphify's normal grouped cross-file resolution. Large
deltas are AST-parsed one file at a time in a memory-bounded sequential pass.
Nodes are compressed as they are produced and edges use an anonymous disk spool,
so the relay never retains a large corpus in RAM. The gzip/base64 delta stays
below the relay's bounded text-output transport; the server validates and decodes
that versioned payload before merging it.
Graph format v2 scopes every internal node ID by its relay-relative source path, so homonymous files and symbols cannot overwrite or inherit each other's edges. Cross-file imports resolve against the complete merged graph, including files unchanged in an incremental build. Original references are retained so target deletion, restoration, or an ambiguous homonym updates existing edges correctly. Ambiguous or unresolved imports use a separate external namespace. Upgrading from an older cache forces one full rebuild. Generated bundles and vendored/minified JavaScript are excluded from discovery and ranking so reports describe application code. External references remain available for traversal but are excluded from god-node rankings in both the report and the prompt digest.
- Server sends the cached
{rel_path: mtime_ns:size}fingerprint map to the relay viaPAWFLOW_GRAPH_KNOWN(gzip+base64, so large maps stay under the ~32K per-variable Windows cap). - Relay walks the workspace tree, skipping standard junk dirs (venv, node_modules, .git, build, dist, pycache, etc.).
- Re-parses only files whose high-resolution fingerprint differs from
known(or are new). Unchanged files keep their cached nodes/edges. - Reports:
parsed_files(re-parsed),removed(inknownbut missing now),mtimes(new map),nodes/edges(just for the re-parsed slice). - Server merges: drops nodes/edges sourced from re-parsed or removed files, appends the new ones.
- No file count cap. Memory cost grows roughly linearly with codebase size. PawFlow consumes Graphify's extracted lists directly instead of materializing a duplicate NetworkX graph on the memory-bounded relay.
- Cache hit: if nothing changed and nothing was removed, the
relay returns
status='unchanged'and no parsing happens server- or relay-side.
Supported languages (17): Python, JavaScript, TypeScript, TSX, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Lua, Zig, PowerShell, Elixir.
Supported file extensions: *.py, *.js, *.ts, *.tsx, *.go, *.rs, *.java, *.c, *.h, *.cpp, *.cc, *.cxx, *.hpp, *.rb, *.cs, *.kt, *.kts, *.scala, *.php, *.swift, *.lua, *.toc, *.zig, *.ps1, *.ex, *.exs.
query: BFS traversal starting from nodes whose label or source path matches the question text. Returns edges with source, target, relation, and confidence.
> project_graph(action="query", question="AuthGateway", depth=3)
Project graph query 'AuthGateway' (12 edges):
[EXTRACTED] AuthGatewayService -> inherits -> BaseService
[EXTRACTED] AuthGatewayService -> calls -> validate_token
...
report: Summary including node/edge counts, confidence breakdown, and high-signal god nodes. Builtin helpers, dangling endpoints, generated bundles, and vendored/minified sources are excluded from the ranking.
node: Details about a specific code entity -- file, location, type, and neighbor edges (up to 20).
The source parameter on the build action specifies which relay/filesystem service to use for fetching code. If omitted, the default relay is used. This must be a relay service (not a filestore) since the code lives on the user's machine.
The project wiki is a persistent set of generated Markdown pages for one
(user, relay) project. Source files remain on the relay. PawFlow stores only
SHA-256 source metadata, generated pages, an index, an append-only activity log,
and exact source provenance for every factual page.
Automatic wiki maintenance resolves the conversation's effective
summarizer_service for every job, then uses only the LLM service configured by
that summarizer. It never reuses the active agent's LLM client and does not fall
back to another LLM when the summarizer binding is unavailable.
Context preparation and successful relay mutations schedule the same coalesced
background worker as the Project Graph. The worker scans source hashes, selects
one batch of at most eight changed high-signal files by default, and makes one
ephemeral LLM call. batch_files=0 now means this safe default, not an unbounded
snapshot; positive configuration is hard-capped at 32 files. Selection and prompt
preparation also enforce UTF-8 byte budgets: 48 KiB per source, 128 KiB for all
source blocks, 24 KiB for the wiki index, and 64 KiB for affected pages. These
provider-independent byte ceilings are a conservative token ceiling and keep a
single maintenance request well below the configured model context.
Large compact JavaScript/CSS bundles are represented by a size/hash marker rather
than their minified body. Identical selected sources are read and hash-checked but
only the first body is sent; later copies cite the canonical path. Ordinary large
text keeps bounded head/tail context and carries explicit truncation metadata.
The source scanner is encoded into the relay command and executed in memory; it
does not create a helper file in the project or on the server-local root.
Wiki scans and updates run only on the relay container surface:
local=true is rejected with a ValueError, because the server/host working
tree is the deployed runtime (app/data/runtime/...), not the project — one
local scan would poison the manifest with thousands of phantom sources that the
next relay scan reports as removed, leading the maintainer LLM to write bogus
"removals" pages. The maintenance worker and the panel refresh action pin
local=False regardless of the surface used for the graph build.
If a manifest was poisoned before this guard existed, acknowledge accepts
glob patterns (app/*, usr/*) that expand against the pending set, so the
phantom backlog can be cleared with a handful of patterns instead of an
exhaustive path list.
The first scan seeds root configuration, architecture documentation, and central
graph files instead of enqueueing an entire large repository. Later additions,
changes, and removals become pending automatically.
Changing the selected project root performs a full derived-state reset: the AST graph is rebuilt, old generated wiki pages and pending entries are removed, and the new root is seeded again from high-signal sources.
The LLM receives untrusted source text and affected existing pages, returns
validated JSON, and may update up to twelve pages. A page is written with the
current SHA-256 digest of every cited source. If any batched source changes while
the LLM call is running, the response is marked superseded, no page is written,
and the newer source remains pending for the next worker run.
An empty, malformed, or structurally invalid LLM response is also fail-closed:
no page or source marker changes, the batch remains pending, and the graph/source
scan portion of project maintenance still completes. Transient failures defer the
exact snapshot for at least 60 seconds so another ready batch can progress. A
deterministic request rejection (context_overflow, caller-invalid 4xx, HTTP 400,
or HTTP 413) marks the unchanged snapshot blocked instead of retrying it forever.
The status surface reports blocked/deferred counts. A new source digest clears the
failure metadata automatically; an operator can also use acknowledge when the
generated or irrelevant source should be removed from the pending backlog.
If an otherwise structured LLM response omits only a page's sources field,
the embedded maintainer conservatively fills it from the non-removed
processed_sources in the exact selected snapshot. When that list is itself
absent or empty, it uses all non-removed sources from the exact selected batch
and records them as processed. It then runs the same strict patch and citation
validation, and never repairs malformed, removed, or out-of-snapshot citations.
For a removed-only batch there is no live citation to infer: uncited page
proposals are discarded, while the declared removals remain processed. PawFlow
never invents a factual page citation from a deleted source.
The Auto Wiki prompt separately budgets the final JSON document. It does not use
that response budget as the provider generation ceiling, because some providers
include internal reasoning in their output-token limit. The provider transport
limit therefore remains controlled by the selected LLM service configuration.
The project_wiki tool provides manual inspection and recovery:
| Action | Purpose |
|---|---|
status |
Show source/page counts, pending changes, scan limits, and stale pages. |
query |
Rank generated pages by full-text relevance. |
page |
Read one page by slug. |
lint |
Report stale pages, missing links/files, orphans, and uncited pages. |
refresh |
Rescan source hashes manually. |
upsert |
Create or replace a page with current source citations. |
acknowledge |
Clear processed sources; stale cited sources are refused. |
Agents receive a compact wiki status digest on each prepared turn. They should query the wiki before broad architectural exploration and validate stale claims against live source files.
The webchat Agent tools menu exposes the cognitive panels. Project Graph,
Project Wiki, and Scratchpad can also be opened with /graph, /wiki, and
/scratchpad; Diary and Memory are available from the same menu and their slash
commands.
| Panel | Available actions |
|---|---|
| Project Graph | Select an explicitly linked relay, automatically load its existing report, build or refresh the derived AST index, search nodes and edges, and inspect a node's source location and neighbors. View opens an interactive force-directed canvas in a tab: it starts from an overview of the most-connected nodes and navigates by capped ego subgraphs (project_graph_ego, ≤300 nodes, 1–2 hops) — click selects a node, double-click/double-tap re-centers on it, edges are colored by confidence, and pan/zoom/pinch work with mouse and touch. The full graph is never rendered at once; the view page requests subgraphs from the panel over postMessage. Stored reports remain readable while that relay is disconnected. The graph is read-only because source code is its source of truth. |
| Project Wiki | Select an explicitly linked relay, list and search pages, render Markdown, create or edit a page, delete a page, refresh source metadata, and run lint checks. |
| Scratchpad | Select a conversation agent, list and search working notes, create or edit a note with tags and a required TTL, delete one note, or clear that agent's notes. |
| Diary | Select a conversation agent, read structured diary entries, filter them by type, and add an entry. |
| Memory | Select all agents, global memory, or a specific conversation agent for filtering and explicit add/edit targeting. |
Project Graph and Project Wiki always send the relay selected in the panel. Scratchpad notes are isolated by user, conversation, and the agent selected in the panel, and expired notes are removed from normal reads automatically.
Use todolist before meaningful multi-step or long-running work and keep its
status authoritative. Use scratchpad only when the work needs transient
evidence or a resume note that should expire.
| Situation | Tool |
|---|---|
| Work must resume after compaction/restart and completion matters | todolist |
| Evidence or a hypothesis is useful only inside the current conversation agent | scratchpad |
| A lesson should improve this agent's work in future conversations | diary_write |
| A fact should be available as durable user/project/world knowledge | remember or kg_add |
Scratchpad bodies are deliberately pull-only. When the injected Scratchpad Hint
names a relevant topic, call scratchpad(action="list", query="...") or
scratchpad(action="get", note_id="..."). Update existing notes rather than
duplicating them, and delete obsolete notes before their TTL when possible.
The _maybe_auto_save_memories method runs after each agent response. It checks if 15 new user messages have accumulated since the last save. When triggered:
- Loads the last 15 messages from the conversation store.
- Concatenates user and assistant message text (first 200 chars each).
- Uses the
summarizer_serviceLLM to extract structured facts. - Stores extracted facts via
auto_extract_memories()with tagauto-extracted.
When a conversation bucket or rollup is completed, process_post_compaction_learning() reads the summary. If automatic memory and skill learning share the same effective LLM, the summary appears once in one prompt and the response has two independently validated fields:
{
"memories": [
{"text": "User prefers JSON over SQLite for storage", "category": "preferences"}
],
"skill": null
}If no maintenance LLM is available, the operation fails closed and stores nothing. When the two roles use distinct explicit bindings, each service retains its own isolated call and output contract.
Periodic auto-save uses the effective summarizer LLM. Post-compaction learning resolves the optional auto_memory and skill_learning bindings first and otherwise falls back to that same summarizer. Client identity, not a service-name string, decides whether the post-compaction analyses can be combined safely.
Every completed compaction bucket and rollup summary is also inspected for one repeatable operational procedure. Coverage requires an existing skill to target the same outcome; a broad skill about the same product or domain does not suppress a release, deployment, migration, validation, or recovery procedure.
The proposer records an INFO outcome for every attempt (created, promoted,
rejected, invalid, duplicate, or skipped) and a WARNING with traceback for error.
Created proposals are conversation-scoped memories tagged skill-draft with a
bounded structured payload. In the Memories panel, Skill drafts filters these
entries. Promote submits the generated instructions through the canonical
skill security review and creates a conversation-scoped skill; PawFlow deletes
the draft only after creation succeeds. The normal delete action rejects a draft.
A first occurrence stays a draft. If the same normalized procedure is extracted
from another conversation, PawFlow creates a user-scoped skill through the
validated ResourceStore path and removes the draft only after creation succeeds.
One conversation cannot confirm its own draft, and validation, name collisions,
or storage failures leave the draft intact.
The diary accepts a reflection entry type that nothing ever asked for: agents
write observations and decisions as they work, and the synthesis across them
never happens. core/reflection_trigger.py injects a Reflection due block
next to the diary digest asking for exactly one — and for a check of whether
the synthesis deserves a kg_add triple or a skill.
It is deliberately silent most of the time. Both conditions must hold:
| Condition | Default | Why |
|---|---|---|
| Diary entries since the last reflection | ≥ 5 (MIN_ENTRIES_SINCE) |
Nothing to synthesize otherwise |
| Time since the last reflection | ≥ 6h (MIN_INTERVAL_S) |
A busy afternoon should not produce three |
A standing "remember to reflect" instruction is one an agent learns to skip; this one appears only when it is actually due, and disappears once the reflection is written.
The static system prompt contains one canonical routing block,
## Tool selection. Mutable data stays out of the cached
prefix: API providers append it to the latest user turn, while a cold CLI session
serializes it into initial_context.md.
Injected under ## Persistent memory. Contains the multi-tier digest (L0-L4 + KG god nodes). Max 1200 characters. Only present if the user has stored memories.
Injected under ## Your diary (past observations). Contains the last 10 diary entries (truncated). Max 600 characters. Only present if the agent has diary entries.
Generated from core/tool_selection.py using the active agent's filtered tool
registry. It names the selection boundary and positive trigger for every
cognitive/work-state route that is actually available, alongside the ambiguous
delegation, orchestration, and waiting families. Families with fewer than two
available routes are omitted from the permanent hint. Use
get_tool_schema(family="cognition") for the complete available comparison;
the individual tool schema remains the source of truth for parameters.
Active todo state is injected automatically. Scratchpad note content is never injected; a non-empty scratchpad contributes only a compact hint with note count, earliest expiry, and up to five topic labels.
All paths are relative to the PawFlow data directory:
| System | Path | Format |
|---|---|---|
| Memory store | data/memories/{user_id}.json |
JSON array of MemoryEntry objects; embeddings as base64 float32 (embedding_f32) |
| Knowledge graph | data/knowledge_graphs/{user_id}.json |
JSON with entities and triples |
| Agent diary | data/memories/{user_id}/diary_{agent_name}.jsonl |
JSONL, one record per line |
| Project graph | data/runtime/graphs/{safe_user}/{safe_relay}/graph.json |
JSON with nodes, edges, metadata |
| Project wiki | data/runtime/project_wikis/{safe_user}/{safe_relay}/ |
Markdown pages plus JSON manifest |
| Conversation index | data/runtime/conversation_index/{user_id}.db |
SQLite FTS5, derived from transcripts |
| Todo list | data/runtime/todolists/todos.sqlite3 |
SQLite, scoped by user/conversation/agent |
| Scratchpad | data/runtime/scratchpads/scratchpads.sqlite3 |
SQLite with TTL, scoped by user/conversation/agent |
All writes use the atomic tmp-then-replace pattern: write to .tmp file first, then replace() to the final path. The conversation index is the exception and deliberately so: it is SQLite (WAL), and it is derived data — deleting the file costs the next search one rebuild and loses nothing.
The runtime registry currently exposes 20 cognitive and work-state tools.
| # | Tool | Parameters | Description |
|---|---|---|---|
| 1 | remember |
text (string, required), tags (string[]), scope (enum: conversation/agent/global/private), category (enum: facts/events/discoveries/preferences/advice), valid_from (number) |
Store a fact in persistent memory |
| 2 | recall |
query (string), tags (string[]), category (enum: facts/events/discoveries/preferences/advice), as_of (number) |
Search memories by text, tags, and category |
| 3 | semantic_recall |
query (string, required), limit (integer), category (enum) |
Search memories by meaning via vector embeddings |
| 4 | forget |
memory_id (string, required) |
Delete a specific memory by ID |
| 5 | check_duplicate |
text (string, required), category (string) |
Check if a similar memory already exists |
| # | Tool | Parameters | Description |
|---|---|---|---|
| 6 | kg_add |
subject, predicate, object (required), valid_from, confidence, source |
Add a fact triple with contradiction detection |
| 7 | kg_query |
entity (required), as_of, direction |
Query all facts about an entity |
| 8 | kg_invalidate |
subject, predicate, object (required), ended |
Mark a fact as no longer valid |
| 9 | kg_timeline |
entity, limit |
Chronological history of facts |
| 10 | kg_stats |
(none) | Summary statistics |
| 11 | query_graph |
question (required), mode, depth, max_results |
BFS/DFS traversal from matching entities |
| 12 | kg_god_nodes |
limit |
Most connected entities |
| # | Tool | Parameters | Description |
|---|---|---|---|
| 13 | diary_write |
entry (required), type, tags |
Write a durable agent-experience entry |
| 14 | diary_read |
limit, type, agents |
Read recent own or explicitly selected same-user agent entries |
| # | Tool | Parameters | Description |
|---|---|---|---|
| 15 | project_graph |
action (build/query/report/node), path, question, depth, source |
Inspect relay-scoped code structure |
Action breakdown:
| Action | Required params | What it does |
|---|---|---|
build |
path (default "."), source (optional relay name) |
Fetch code via relay, run AST extraction, build graph |
query |
question, depth (default 3) |
BFS traversal on the graph |
report |
(none) | Summary with god nodes, stats, confidence breakdown |
node |
question (node label) |
Details about a specific code entity |
| # | Tool | Parameters | Description |
|---|---|---|---|
| 16 | project_wiki |
action (status/pages/query/page/page_data/lint/refresh/upsert/delete/acknowledge), plus action-specific fields |
Query, inspect, refresh, or repair the relay-scoped project wiki |
| # | Tool | Parameters | Description |
|---|---|---|---|
| 17 | todolist |
action (create/update/list/get), plus action-specific fields |
Authoritative durable work state for one conversation agent |
| 18 | scratchpad |
action (create/update/get/list/delete/clear), note fields, TTL, pagination |
Expiring pull-only working notes for one conversation agent |
| # | Tool | Parameters | Description |
|---|---|---|---|
| 19 | learn |
limit |
Extract user preferences and communication patterns from raw messages |
| 20 | conversation_search |
query (required), agent, limit, include_current, summarize |
Search raw text of past conversations |
This is the counterpart of recall, not a duplicate of it. recall searches
memories — what an agent decided at the time was worth keeping. This searches
what was actually said, which is the only way to answer "we solved this
before, where?" when nobody extracted a memory back then. read_history
remains the tool for the current conversation.
Encrypted conversations are never indexed (the index is plaintext, so indexing one would undo the encryption), and only the searching user's own conversations are in their index. See tool_catalog.md for the full behaviour.
- 5 memory tools (
remember,recall,semantic_recall,forget,check_duplicate) - 7 knowledge graph tools (
kg_add,kg_query,kg_invalidate,kg_timeline,kg_stats,query_graph,kg_god_nodes) - 2 diary tools (
diary_write,diary_read) - 1 project graph tool with 4 actions (
project_graph) - 1 project wiki tool with 10 actions (
project_wiki) - 2 work-state tools (
todolist,scratchpad) - 1 learning tool (
learn) - 1 conversation search tool (
conversation_search)
Note on end_memory: Ending a memory (marking it as no longer valid without deleting it) is done via the MemoryStore.end_memory() API method. There is no dedicated tool exposed to agents for this -- agents should use forget to remove obsolete memories or manage temporal validity through the knowledge graph's kg_invalidate instead.