From a Tool Call to a Memory Candidate

Claude-mem’s capture problem is deceptively specific: how can a tool action in one agent session become useful historical context without making the user operate a second note-taking workflow? This part of the system answers that question at the boundary between the host session and the memory worker. The important result is not merely that a command or file edit is observed, but that it is assigned to the right project and conversation, filtered when necessary, and placed on an asynchronous path that can interpret it later.

The host session supplies more than one kind of event

A coding agent does not produce one uniform stream. It starts or resumes a conversation, receives prompts, reads files, edits code, runs commands, and eventually stops. Some clients expose those actions through lifecycle hooks. Others write a transcript file whose lines must be interpreted after the fact. Claude-mem supports both shapes, but it does not treat them as two independent memory products. They are capture edges into a shared ingestion path.

The current Claude Code hook manifest makes the division visible. A Setup hook checks the installed version. SessionStart starts or contacts the worker and asks for context. UserPromptSubmit initializes the current prompt and session. PostToolUse sends tool activity. PreToolUse can collect file context before a read. Stop extracts the last assistant response and queues a session summary. The manifest’s event names are operational boundaries: a hook fires at a particular moment, with a particular payload, and the handler decides whether that payload can be handed to the worker.

The summary at Stop is not a replacement for the observations gathered during the session. A tool-use event is close to the action and can describe a discrete discovery or change. A summary is a checkpoint over the broader session: what the user requested, what was investigated, what was learned, what was completed, and what remains in motion. Both are useful because they answer different questions. One helps a later search find a specific piece of work; the other helps a new session recover the shape of a longer effort.

Transcript watching solves a different capture problem. The watcher tails configured JSONL files, remembers byte offsets, handles a file that shrinks or rotates, and discovers new files under watched roots. Its processor resolves a session ID, working directory, project, user and assistant messages, and tool-use/result pairs from each schema-matched event. If a tool-use line arrives before its result, the processor can retain the pending tool information and combine it when the result appears. If a session-end event is represented in the watched schema, the adapter can request a summary and update a context file within an allowed path.

That flexibility does not mean all clients have identical coverage. Different agents name sessions differently and expose different event fields. The transcript watcher is only as complete as its configured schema, readable files, and event payloads. Native hooks may be authoritative for one client, while a transcript adapter is necessary for another. The project even avoids duplicate Codex capture by disabling Codex transcript ingestion when native Codex hooks are authoritative unless the user explicitly opts in. Platform support is therefore an adapter problem: translate each edge into the same semantic event shape, then let the central path own storage and generation.

The shared path also gives capture a useful point of leverage. It can see the working directory at the moment of a tool call, so it can derive a project before the event becomes detached from its host. It can attach a platform source, prompt number, tool-use identifier, and optional subagent identity. Those fields do not make the observation more eloquent, but they make it less ambiguous. A discovery about one repository should not become indistinguishable from a similarly worded discovery in another repository or from an event produced by another client.

Consider a simple session in which a user asks an agent to understand an authentication failure. UserPromptSubmit first gives claude-mem the conversation identity, project location, and cleaned request. The handler creates or reuses the local session record and saves the prompt unless it is a duplicate or entirely private. When the agent then reads a configuration file, PostToolUse supplies the tool name, parameters, result, working directory, and tool-use identifier. Shared ingestion resolves the same session, checks whether the project and tool are eligible, strips memory markers from the payload, and places the event in that session’s buffer. The hook returns; the user can continue asking questions while the observer works in parallel.

If the agent next edits a file and runs a test, those events can join the same live buffer. Their order matters because the observer may combine several tool uses into one meaningful observation rather than creating one note per mechanical action. The working directory and prompt number keep the batch tied to the right project and turn. The tool-use identifier gives the worker a way to suppress an accidental duplicate during its lifetime. None of this decides whether the final observation is a bug fix, discovery, or decision; capture supplies the evidence and context, while the observer makes that interpretation later.

At Stop, the last assistant response enters a separate summary path. The summary can describe the arc of the authentication investigation even if several individual tool events were low signal. A transcript adapter can reach a similar result by recognizing session, user-message, tool-use, tool-result, and session-end events in a JSONL stream. In both cases, the capture layer is preserving temporal and contextual links so a later reducer can see more than an isolated command. It is not trying to write a polished project history at the moment each event occurs.

sequenceDiagram participant P as Primary agent session participant H as Hook or transcript adapter participant I as Shared ingestion participant S as SQLite session store participant B as Per-session live buffer participant O as Observer generator participant R as Response processor P->>H: Tool event with input, outcome, cwd, and session H->>I: Normalize platform and event identity I->>I: Apply project, tool, protocol, and privacy checks I->>S: Resolve session and optionally write raw tool-use index I->>B: Queue admitted observation I-->>P: Return without waiting for compression B->>O: Send synthetic observation prompt O-->>R: Structured response plus observer session identity R->>S: Register memory identity, then store observation or summary

The diagram’s return arrow is as important as its forward arrows. The primary agent does not have to wait for the observer model to finish before continuing with its own work. Ingestion is designed to be a side path. The hook can report that an event was accepted or queued while the actual observation is still being generated. That is why “the event was captured” and “the memory is already searchable” should never be used as synonyms.

Admission and identity decide where an event belongs

Before an event enters the live buffer, shared ingestion makes several decisions. It can skip a project whose path matches the configured exclusion rules. It can skip tools on the configured list, such as routine operations that add little durable signal. It ignores internal protocol payloads rather than treating them as user requests. It also avoids storing metadata reads that target the memory system’s own session files. These filters are not an attempt to understand every tool result; they are an early defense against predictable noise and self-observation.

Privacy filtering happens at this edge as well. Claude-mem recognizes a <private>...</private> convention and strips the marked region before a prompt is stored or an observation is queued. If the resulting user prompt is empty, the turn is treated as intentionally private and generation is suppressed. Tool input and tool output are also stripped before they enter the raw side index or observer queue. The distinction is narrow but important: stripping a marker is an automated operation, while deciding to use the marker remains a user responsibility. Unmarked sensitive content is not made private by the existence of the feature.

The validator avoids another dangerous shortcut. If the expected user_prompts row is absent, it does not conclude that the prompt must have been private. A worker can race its own startup, causing the session-init hook to miss the prompt write. In that case, treating absence as privacy would freeze every later observation for the session. The implementation allows ingestion and records the situation as a likely boot race. A present row that becomes empty after stripping is different: that is evidence of a deliberate private turn. Capture correctness sometimes depends on distinguishing “nothing was recorded” from “the user recorded nothing after redaction.”

Identity is the next boundary. The user-facing conversation has a contentSessionId. The observer has a separate memorySessionId. The first is stable from the host’s point of view; the second comes from the SDK-side processing session and is used to link stored observations and summaries. A local database session is created with the content identity and a NULL memory identity. Only after the observer supplies its own session ID can the worker register that value and store child rows under it.

This separation protects two different relationships. Search and session lookup need to connect a memory record back to the user’s conversation. Foreign-key storage needs a valid observer identity to connect observations to their parent. A user’s conversation ID is not automatically a safe resume token for the observer, and the observer’s identifier is not a substitute for the host’s session identity. Conflating the two could attach records to the wrong session or make a provider resume an unrelated conversation.

The NULL period is meaningful rather than a missing implementation detail. Before the observer has spoken, the database can know that a primary conversation exists but cannot safely attach an observation to the observer-side parent key. Once the first SDK response carries a memory identity, the store registers it before inserting child observations or a summary. Later offered identities are compared with the registered value instead of being allowed to rewrite the relationship. This makes the first successful registration an anchor for the local corpus, while still allowing the host conversation to remain the stable lookup point.

That ordering also explains why the raw tool-use index and the generated observation are separate. The raw side record can be keyed from the host’s tool-use identity as soon as ingestion receives it. The generated observation cannot exist until a provider has interpreted the event and the storage layer has a valid observer relationship. A user who sees a tool-use reference in diagnostics is therefore not necessarily looking at a completed memory; they may be looking at evidence that the event reached an earlier stage of the pipeline.

The current worker is especially deliberate about this distinction after restart. Its active session manager clears a memory identity carried over from an earlier worker instance. The Claude provider starts a fresh no-session-persistence SDK process and currently does not use the stored identity as a resume parameter. When a response later offers an SDK session ID, the database registers it if no identity exists. If a later turn offers a different ID, the store keeps the first registered database identity rather than rewriting all child relationships. This behavior is more conservative than an older session-architecture document that describes ordinary continuation resume. The current provider and store are the stronger evidence for the source snapshot.

Once admission and identity checks pass, the event enters a per-session live buffer. The buffer keeps the tool name, input, outcome, prompt number, working directory, tool-use identifier, and agent metadata until a generator consumes it. A tool-use identifier can suppress a duplicate event during the worker’s lifetime. The event is then presented to the observer as a synthetic message, not as an invitation for the observer to inspect the repository or contact the primary agent. The observer is a one-way recorder: it receives the supplied event and produces a structured response.

A raw tool-use side index is maintained when a concrete tool-use identifier is available. This index is deliberately beside the generation queue, not instead of it. Its purpose is different: it can preserve a reference to original input and output so a later search can disclose literal tool I/O when a compressed observation is not enough. Because the write is best effort, a failure in that backup index should not prevent the observation from being queued. The system is prioritizing the main memory path while retaining a deeper evidence path where it can.

The generator starts only after work is queued, and separate calls that might start it are serialized per session. That prevents two concurrent hook requests from seeing the same “no generator yet” state and launching competing observers for one conversation. The primary session still does not wait for this coordination or for provider completion. It receives the benefit of an invisible side process, but it also inherits that process’s delay and failure boundaries.

This is the essential capture contract: an ordinary event crosses from the host through a platform-specific edge, acquires project and session context, passes selective filters, and becomes live work for a model-assisted reducer. It is not yet a finished memory. The next question is what the reducer is allowed to preserve, how the response is parsed, and why the project stores a structured observation instead of copying the entire event.

References