This post is part of How We Built Charlie, a technical series on the runtime and product decisions behind dependable autonomous engineering work. Chapter 1 explained the coordination layer. This chapter examines the durable Task record at its center.
Engineering requests arrive incrementally. An engineer asks Charlie to investigate a checkout regression. A second comment adds guest checkout to the scope. Charlie delegates a focused review of recent payment changes, then incorporates the finding into the parent work.
A chat thread can hold that conversation. The durable Task owns its lifecycle, lineage, later input, and result. That distinction lets the request remain coherent after a model call ends, while delegated work runs, when a system fails, or when someone changes the requirements.
The examples below are simplified architectural traces. They show ownership and failure boundaries, rather than customer incidents or claims about one observed production sequence.

Collaboration surfaces provide signals and context. The Task remains the durable owner while a bounded Run advances it through Turns.
Task, Run, and Turn have different boundaries
The object model is easier to reason about when each term owns a narrow set of facts.
| Object | Durable responsibility |
|---|
| Task | The engineering objective: identity, customer and repository context, lineage, lifecycle, mailbox position, and terminal result |
| Run | Persisted executor state for advancing one Task: phase, transcript, context snapshots, turn index, pending tools, and recovery state |
| Turn | One model iteration and its associated tool-result cycle inside a Run |
A Task exists before and after a particular executor process. A Run is the executor’s bounded attempt and persisted progress for that Task; it is not a second scheduler lifecycle. A Turn begins when the executor assembles the next model input. It includes the model response, any requested tool dispatch, and the resulting tool-output cycle. The Turn ends after those results settle or time out and are persisted or otherwise materialized for the next iteration.
This boundary matters during recovery. Dispatching a tool does not finish a Turn. If a process exits after dispatch, the recovered Run needs to distinguish a pending operation from a settled result. Otherwise it may repeat an external write or continue with a transcript that omits what happened.
Here is schema-shaped pseudocode for the illustrative checkout request:
{
id: "task_example_root",
rootId: "task_example_root",
customerId: "customer_example",
signal: {
id: "signal_example",
charlieUri: "charlie://customer_example/v1/github/repo/example%2Fstorefront/issue/42/comment/99",
requestId: "request_example_checkout",
eventType: "issue_comment",
eventAction: "mentioned"
},
repo: "example/storefront",
message: {
id: "message_example",
createdAt: "2026-07-07T16:20:00Z",
content: [
{ type: "text", text: "Investigate the checkout regression." }
]
},
status: "running",
createdAt: "2026-07-07T16:20:00Z",
startedAt: "2026-07-07T16:20:03Z"
}
A delegated child would add parentId while retaining the same rootId. Terminal success and failure records include a completion time and result; canceled and timed-out Tasks have their own terminal shapes. The example uses fictional public-safe values and omits fields that do not help explain the boundary.
The initial signal and structured message preserve where the request came from and what was requested. Optional repo context identifies the checkout when repository work is involved. Fetched issue history, repository summaries, and other context can change from Turn to Turn without changing the Task’s durable identity.
A Task gives active work a stable address
Once the request has a Task ID, every later component can refer to the same objective. The scheduler can ask whether it still authorizes work. The router can direct a relevant follow-up toward its mailbox. A child investigation can record which parent requested it. The executor can restore persisted Run state. Completion can attach a result to the same owner.
Conversation identifiers remain useful. A GitHub comment ID, Linear issue URL, or Slack thread timestamp tells Charlie where people are collaborating. A Task ID answers a different question: which unit of work is active, and what lifecycle governs it?
That separation supports integrated product behavior without turning a thread into a workflow engine. Charlie can keep an active request open to follow-up, delegate a bounded investigation, recover across surfaces, honor cancellation at scheduler checks, and report completion with durable references. Each experience depends on several components agreeing about the same Task.
Routing still has to interpret intent. Two comments in one issue may change the same objective, or the second comment may request separate work. One Task may also receive relevant context from more than one surface. Thread continuity is evidence for routing, not a rule that every nearby message belongs to the same lifecycle.
The scheduler owns Task lifecycle
A common delivery path is:
scheduled -> queued -> running -> succeeded
-> failed
That line is intentionally incomplete. It is a readable path through the system, while the actual transition rules preserve recovery options:
- Success or failure can be recorded from any non-terminal state.
- Cancellation can terminalize any non-terminal state.
- Timeout applies to a running Task.
- A valid claim can move a scheduled Task directly to running when the ready notification was accepted before the queued view was persisted.
- Once a terminal result is recorded, later result writes are rejected.
The scheduler is the canonical lifecycle authority. The executor persists Run-local progress and reports an outcome. A local process exiting, a queue message arriving, or a model response declaring success does not independently redefine Task status.
Ready delivery and claim handling protect one boundary. A ready notification may be delivered more than once. It permits an executor to attempt a claim against the matching pending-delivery token. One valid claim for that delivery event is accepted at that scheduler boundary. Downstream writes still need their own replay and reconciliation rules; the claim protocol does not make a GitHub comment, branch push, or pull request creation exactly once.
Cancellation is also boundary-specific. The scheduler records a terminal stop condition. At scheduler-check boundaries and immediately before tool dispatch, the executor asks whether the Task or one of its ancestors is terminal. Cancellation is cooperative: an LLM call or other operation already in flight can finish before the next stop check, and an accepted provider write cannot be retracted by changing scheduler state.

The top row is a common delivery path rather than the complete transition graph. Lineage keeps delegated work attributable while stop propagation remains cooperative.
Lineage makes delegation inspectable
When Charlie delegates a focused investigation, the child is a Task with its own identity, bounded input, lifecycle, and result. The parent remains responsible for deciding how that result changes the root objective.
T1 Investigate checkout regression [running]
|
+-- T2 Inspect recent payment changes [succeeded]
result: guest-session token reused in one path
T1 is both the root and parent of T2. The child’s finding returns to the parent as a tool result. Child success does not complete the root: the parent may still need to reproduce the behavior, prepare a patch, run tests, or explain that the evidence does not justify a change. A child failure also does not force root failure when another bounded approach remains available.
The child receives the caller-provided task text and repository context. The parent must include the relevant paths, evidence, constraints, and return contract needed for the assignment; the child does not inherit the parent’s entire transcript. This keeps authority legible and makes it possible to inspect what the worker was asked to do.
Deterministic scheduling can suppress duplicate child creation at the delegation seam. That protection stops at the seam. If the child performs an external write and loses the provider response, its recovery still depends on an operation-specific key, lookup, or explicit uncertainty.
Ancestor stop conditions follow the tree through scheduler checks. Descendants cooperatively observe that an ancestor has become terminal and stop before later work. The scheduler does not eagerly rewrite every descendant as canceled, and lineage does not provide synchronous process termination.
The mailbox lets active work absorb a follow-up
The guest-checkout comment arrives while the investigation is active. Charlie needs to preserve it without injecting new instructions into the middle of a tool operation.
The scheduler maintains a durable mailbox and a per-Task cursor. At a scheduler-check boundary, it returns items after that cursor and advances its own record of the read. The executor maps each message to a deterministic transcript item ID and appends it only when that ID is not already present. If an item is delivered again, transcript deduplication suppresses duplicate insertion.
Those are two separate persistence mechanisms. The scheduler owns the cursor; the executor owns the transcript. Together they make common redelivery cases manageable, but they do not form an acknowledged exactly-once handoff or a universal replay guarantee. In particular, advancing the scheduler cursor does not imply that every subsequent crash will cause the same item to be replayed.
Mailbox storage makes a message eligible for executor retrieval at a later scheduler check. Model delivery is a later event. The executor still has to retrieve the item, append or recognize its transcript identity, and assemble another Turn. Transcript presence can show that the message became model-visible; it cannot prove that the model interpreted it correctly.

A follow-up can join active work at a safe boundary. If routing confirms that the previous Task is terminal, it may schedule a new Task instead of mutating completed history.
Routing policy decides whether the comment changes the active checkout objective. A separate refactor request can become another Task even if it appears in the same thread. If routing confirms that the intended Task is terminal, it may schedule new work with related source context. That is an available policy outcome, rather than an automatic fallback for every late message.
The same distinction supports cross-surface recovery. A request may begin in GitHub, receive a relevant constraint through another supported surface, and complete with a durable artifact elsewhere. The Task supplies continuity; each integration still has its own routing, permissions, and communication rules.
Terminal results support recovery and review
A process ending is an executor event. A Task ending is a scheduler lifecycle event. The executor reports an outcome, and the scheduler records it only while the Task remains eligible for that terminal transition.
For supported entry completions, Charlie can include a structured, bounded self-report with a small list of important durable writes or references. That list may point to a pull request, commit, document, or posted update. It is a recovery aid: another process or person has stable places to inspect after the active Run has ended.
It is not an automatic scheduler-owned effect ledger. It is not exhaustive, and it is not independent verification. The agent reports the important effects it knows about within a bounded result format. Recovery and review still need provider read-back, test output, or other evidence appropriate to the claim.
Patch created
A commit SHA and provider read-back identify a durable revision. They do not establish that the patch is correct.
Pull request exists
A pull request number or URL lets another process retrieve current provider state. The pull request may still target the wrong base or contain an incomplete change.
Tests passed
The exact command, revision, environment, and observed result support a precise test claim. They say nothing about checks that did not run or production behavior outside that environment.
Follow-up incorporated
A mailbox message ID, deterministic transcript item, and resulting code or plan change provide a useful trace. A reviewer may still decide that Charlie misunderstood the instruction.
External writes remain the difficult case. Suppose GitHub accepts a pull request creation, then the response disappears before the number is persisted. Repeating the request may create a duplicate. If Charlie has a stable branch name, recovery can search for an existing pull request and read it back before deciding what to do. Without a supported idempotency key or reliable lookup, the honest outcome preserves uncertainty and asks for review.
This is why evidence-backed completion combines lifecycle status with inspectable references. Status says the scheduler accepted an outcome. Evidence shows what another system currently exposes. Neither substitutes for engineering judgment.
Durable ownership keeps the boundaries honest
The Task model provides a stable work identity, canonical lifecycle, explicit lineage, a place for later input, and a terminal result. Its local protocols are deliberately narrower: ready notifications can repeat, claims are guarded at one scheduler boundary, mailbox insertion is deduplicated in the executor transcript, and cancellation is observed cooperatively. External systems remain separate systems, so effects require their own recovery and verification.
That separation lets Charlie absorb a relevant follow-up, delegate focused work, recover across integrations, stop descendants at safe checkpoints, and return evidence without claiming stronger guarantees than the components provide. When an outcome is ambiguous, the Task can preserve that fact instead of turning it into unsupported success.
The chat still matters. It is where engineers add context, correct assumptions, and decide what to do next. The task gives those conversations a durable object to change.
Continue the series
Previous: How We Built Charlie, Part 1: Why Dependable Agents Need a Coordination Layer. Browse the full How We Built Charlie series.