Skip to content

Task notes

Task.status.notes is an append-only journal. It IS the continuation state.

There is no session resume, no conversation object key, no handoff preamble, and no chat service. A pod's turn-0 render is identical every time - the same context bundle, built the same way, with no resume mode and no special first-turn preamble. What carries context forward from one pod to the next is the journal.

That is a deliberate simplification. A pod is cheap and disposable; the Task is the durable object. Everything the next pod needs to know has to be written down on the Task, by the pod that knew it, before that pod goes away.


The journal

type Note struct {
    At    metav1.Time `json:"at"`
    Agent string      `json:"agent"`  // brainstorm|incident|clarify|refine|review|documentation|implement|operator
    Kind  string      `json:"kind"`   // note|plan|handoff
    Body  string      `json:"body"`   // MaxLength=4096
}
Field Rule
agent Stamped by the operator's REST layer from Task.status.agentKind. It is not a request-body key, and there is no MCP argument for it. An agent can therefore never produce agent="operator": when status.agentKind is empty the write returns 409, and it is never defaulted. The only writer of agent="operator" is the operator itself, in-process
kind note (an observation), plan (the approach), handoff (what the next pod needs)
body Truncated to 4096 bytes, on a rune boundary

An agent writes one with task_note(kind, body). It reads them back in its context bundle, or - when the bundle says some were elided - with task_context(notes=all).

The writer is stamped, never claimed

agent is the one field an agent might want to lie about, so it is the one field an agent cannot set. A note attributed to the operator carries the operator's authority in a bundle that is otherwise untrusted content; that attribution has to be unforgeable, and the cheapest way to make it unforgeable is to never accept it from the wire.


Caps and spilling

At 50 notes the oldest is spilled to tatara-memory (stats.notesSpilled increments, and the returned track id is appended to stats.notesSpilledRefs) and dropped from the CR. MaxItems=60 on the CRD is a backstop only.

There is no 409-on-cap. An agent must always be able to write its handoff. A note write that cannot be made to fit even after eviction does not fail the agent: it fails the Task, loudly, at stageReason=object-too-large - see the etcd object budget.

Spilled notes are readable again. task_context(notes=all) rehydrates the full history out of tatara-memory from stats.notesSpilledRefs and renders it in order. Notes are the continuation state, so a spilled note that could not be read back would be continuity silently lost - which is why the <notes> element always carries an elision marker naming the exact call that retrieves the rest.


What replaced what

Was Is
Subtask CRs, fed to a running agent turn by turn a plan note
tatara-chat rooms, one per implementation stream note and handoff notes on the Task
chat_handoffs, HANDOFF_KEY, handover, sessionID, conversationObjectKey, --resume a handoff note

Both of those mechanisms are gone. The Subtask CRD is deleted, along with its reconciler, its three MCP tools and its three REST endpoints. The chat service is archived and its release is removed from the cluster.


Notes in the bundle

Notes render inside the context bundle, wrapped as untrusted content like everything else in it:

<notes total="62" rendered="50" elided="12" fetch="task_context(task=tatara-implement-2026-07-12-m4z8q, notes=all)">
  <note agent="implement" at="2026-07-12T10:31Z" kind="handoff" source="agent">Scope locked. 3 repos.</note>
  <note agent="implement" at="2026-07-12T11:02Z" kind="plan" source="agent">Guard the reaper on podStartedAt plus a live-turn probe.</note>
  <note agent="operator" at="2026-07-12T13:00Z" kind="handoff" source="operator">TTL stop. Last turn's final text: ... Repos pushed: ... No agent handoff was captured.</note>
</notes>

source is a render attribute, derived from the writer: source="agent" is stamped for every note whose agent != "operator", and only the operator's in-process writer produces source="operator".

total / rendered / elided are present unconditionally, even when nothing was elided. An agent must never have to infer completeness from the absence of an attribute.


The operator writes notes too

The operator writes a note whenever it knows something the next pod will need and no agent is in a position to write it down.

The clearest case is the pod TTL stop. When a pod hits podStartedAt + agentPodTTLSeconds, the operator stops admitting normal turns to it, waits for the in-flight turn's callback, and then submits exactly one final turn: "Your pod is being stopped. Call task_note(kind=handoff) with everything the next pod needs, then stop." The pod is asked for its own handoff, because it is the only thing that knows what it was doing.

If that turn does not land - the hard cap at t0 + 2 * turnTimeoutSeconds + 60s expires, or the wrapper returns a 410, 409 or 5xx - the operator writes a synthetic handoff note in-process instead, from the last turn's final text and the list of repos that were pushed, and force-deletes the pod.

Task.status.notes is therefore never empty after a TTL stop. Either the agent wrote a handoff, or the operator wrote one for it.


Audit

Agent pods are not Loki-scraped. The operator is.

So the operator logs every note write to its own stdout at INFO (action=task_note, with the Task, the agent kind, the note kind and the byte count). The journal therefore has an audit trail at Loki's retention rather than at the Task CR's 48h delivered-TTL - the notes outlive the object they were written on.


See also