Skip to main content
Working with an agent? Give them a link to this page as markdown.

Incremental ingest

Use the Watcher API to upload Claude Code or Codex transcripts in chunks. The server stores normalized messages and skips content that it already holds. A successful response identifies both the end of your chunk and the live branch of the session.

Prerequisites​

Deploy server support before updating uploaders. Authenticate with an API key that grants sessions:ingest or sessions:write:any, or with a signed-in user who can write the session. See Authentication and permissions.

Send a chunk​

Send HTTP requests directly or pass the same trajectory fields to the Python SDK ingest method. Your uploader must choose chunks, check their serialized size, and persist acknowledged progress.

Send one Claude Code or Codex trajectory to POST /v1/ingest. Include the same source conversation ID on every request. Put append_after and fork on the item in trajectories, not on the request itself. Omit append_after for an opening chunk. To continue, set it to results[0].chunk_end from the previous response. Using an anchor or fork=true requires exactly one trajectory in the request. Use /v1/ingest for this protocol; the older per-session append route keeps its existing contract.

For example, this opening request uploads one Claude Code message:

{
"source": "my-uploader",
"trajectories": [{
"format": "claude-code-jsonl",
"session_id": "my-conversation",
"timestamp": "2026-10-30T12:00:00Z",
"entries": [{
"type": "user",
"uuid": "message-1",
"parentUuid": null,
"sessionId": "my-conversation",
"timestamp": "2026-10-30T12:00:00Z",
"message": {"role": "user", "content": "Check the test configuration."}
}]
}]
}

For the next chunk, keep the conversation ID and add append_after to the trajectory, using the returned {session_id, trajectory_id} unchanged. Send only the new records and any context needed to parse them independently. Do not split an assistant message or separate a tool result from the call it needs.

Read the response​

The response has one results entry per input, in the same order: results[i] describes trajectories[i]. Duplicate, empty, and skipped inputs keep their slots. Each result contains these fields:

FieldMeaning
chunk_endThe {session_id, trajectory_id} at the end of this chunk. It can lie inside an already-stored chain. Use it as the next request's append_after.
leafThe session's live branch endpoint. It can differ from chunk_end on replay. Use it to read the current trajectory, not to prove this chunk's coverage.
statusstored for new content, already_stored for a replay or message-free input, or partially_stored for an input containing both known and new content.

A chunk with no messages can return a null chunk_end. Track its source position locally and include any context a later chunk needs. There is no server receipt for deferred raw records.

The existing aggregate session_id, trajectory_ids, counts, resolved_tool_calls, and append_anchor keep their batch meanings. append_anchor follows the older continuation rules and can be null for an interior replay. Use chunk_end for incremental upload progress.

Resume, rewind, and retry​

Save each successful chunk's source position and chunk_end together before sending the next one. Keep progress separate for each API URL, capture identity, conversation, and source file. Only reuse an acknowledgement when the corresponding source prefix still matches.

For a rewind, set fork=true on the trajectory. Each branch can start at its shared parent's anchor without resending that parent's raw record. One request may also include both branches whose raw parentUuid identifies the same omitted parent. Without the flag, new content branching from an occupied message is rejected with 409 anchor_conflict. Previously stored branches remain available.

On a lost response or a transport failure, retry the same chunk. Stored overlap is harmless, including a chunk that extends beyond previously stored content. A missing session (404) or unknown_anchor permits replay from an earlier acknowledgement or the start. A content conflict does not prove that earlier content is missing. Correct the refused input before retrying.

Each chunk must parse independently. For Codex, include the session metadata and active instruction context needed by the new records. Start an unanchored chunk at a compaction boundary and include the retained summary and prompts.

Bounds and refusals​

Keep the entire serialized request within 8,388,608 bytes. If no independently parseable chunk fits, stop and reduce the input rather than splitting a message or sending an oversized whole-file fallback.

A 400 content refusal or 409 anchor_conflict does not acknowledge the chunk. Correct the input or anchor before retrying. A 409 session_owner_conflict requires correcting the capture identity or conversation selection. Do not bypass it with a new offset.