1# Feature Lifecycles
2
3This document describes how core user-facing features should work in V2. These flows describe target behavior, not current implementation constraints.
4
5## Creating Threads
6
7Creating an app thread is separate from starting provider work.
8
9```text
10thread.create
11 -> AppThread created
12 -> no provider session required yet
13 -> no provider thread required yet unless eager provider creation is enabled
14```
15
16Provider thread creation may be lazy:
17
18```text
19first run starts
20 -> ensure ProviderSession
21 -> ensure ProviderThread
22 -> send provider turn
23```
24
25If the provider supports empty thread creation, V2 may create a provider thread early. If not, the provider thread is created at first run start.
26
27## Starting A Run
28
29When the user sends a message:
30
31```text
32thread.message.send
33 -> ConversationMessage(role=user)
34 -> Run(status=queued, ordinal=N)
35 -> root ExecutionNode(kind=root_turn, countsForRun=true)
36 -> enqueue provider command
37```
38
39When provider execution begins:
40
41```text
42provider turn accepted/started
43 -> ProviderTurn allocated or correlated
44 -> root node status=running
45 -> run status=running
46```
47
48When the root provider turn completes:
49
50```text
51root provider turn/completed
52 -> finalize assistant streams
53 -> finalize plans/todos/questions
54 -> root node terminal
55 -> capture checkpoint if applicable
56 -> run terminal
57```
58
59Only the root node can complete the run.
60
61## Steering And Queueing Messages
62
63V2 should support three classes of follow-up input:
64
65```text
66steer active run in place
67steer active run by restart
68queue next run
69```
70
71Steering is an app-level intent to change the active run's instructions. Provider-native steering is only one implementation strategy.
72
73If the provider supports active-run steering, the app sends a steer command to the provider and keeps the same run active.
74
75If the provider does not support active-run steering, the app interrupts the active provider turn, finalizes the interrupted attempt, and starts a replacement provider turn for the same app-level steering intent. This may be represented as either:
76
77- the same `RunId` with a new root attempt node, if the UI should treat steering as revising the active run, or
78- a new `RunId` linked by `supersedesRunId`, if preserving the interrupted attempt as a counted turn is important.
79
80The preferred default is the same `RunId` with a new root attempt node until the first attempt has produced a user-meaningful terminal assistant response or checkpoint.
81
82Queueing creates a new run after the active run.
83
84Recommended command shape:
85
86```ts
87type MessageDispatchMode =
88 | { type: "steer_active"; targetRunId: RunId }
89 | { type: "restart_active"; targetRunId: RunId }
90 | { type: "queue_after_active" }
91 | { type: "start_immediately" };
92```
93
94Policy:
95
96- If the provider supports active-run steering, send steer to the provider and attach the message to the active run.
97- If not, interrupt the active provider turn and restart the active run with the steering message included.
98- If the user explicitly requests a new turn, queue even if steering is available.
99- If no run is active, start immediately.
100
101The model supports this because `Run` and `ProviderTurn` are separate. A single app run may have multiple provider-turn attempts when the first attempt was interrupted for steering.
102
103```ts
104type RunAttempt = {
105 id: RunAttemptId;
106 runId: RunId;
107 attemptOrdinal: number;
108 rootNodeId: NodeId;
109 providerThreadId: ProviderThreadId;
110 providerTurnId: ProviderTurnId | null;
111 reason: "initial" | "steering_restart" | "retry" | "provider_recovery";
112 status: "running" | "completed" | "interrupted" | "failed" | "cancelled";
113};
114```
115
116Only the selected/final attempt completes the run and creates the run checkpoint. Earlier interrupted steering attempts remain in the execution graph for audit/debugging.
117
118## Follow-Up Turns
119
120A normal follow-up after a completed run creates a new run.
121
122```text
123previous Run completed
124user sends follow-up
125 -> new Run ordinal=N+1
126 -> new root node
127 -> same AppThread
128 -> same ProviderThread if resumable
129```
130
131If the previous provider thread cannot be resumed due to runtime error, missing native state, or incompatible provider settings, the provider thread is marked unavailable and the app creates a replacement provider thread from reconstructed context.
132
133The replacement path is an error recovery flow, not a provider capability branch.
134
135## Changing Providers Between Runs
136
137Changing providers is a first-class context handoff. It does not fork the app thread and it does not rewrite prior provider history.
138
139```text
140user selects a different provider for next run
141 -> create Run with target provider
142 -> resolve or create target ProviderThread
143 -> create ContextHandoff if target provider thread lacks current app context
144 -> inject handoff summary into provider context
145 -> send user message as the new root turn
146```
147
148Example:
149
150```text
151runs 1-5: Codex provider thread C1
152switch to Claude
153 -> summarize runs 1-5 for Claude
154 -> create Claude provider thread L1
155runs 6-8: Claude provider thread L1
156switch back to Codex
157 -> resume Codex provider thread C1 if possible
158 -> summarize runs 6-8 for Codex
159 -> send run 9 to C1 with the delta handoff
160```
161
162The default policy when returning to a provider is to resume the previous provider thread and bridge only the off-provider delta. This preserves native continuity for that provider's own earlier work while informing it about what happened elsewhere.
163
164Fallback policy: create a fresh provider thread with a full app-thread summary when:
165
166- the previous provider thread cannot be resumed,
167- the provider's context is likely too stale or polluted by repeated handoffs,
168- model/runtime settings changed incompatibly,
169- the user explicitly requests a clean provider context.
170
171The fallback should be explicit in the graph:
172
173```text
174new ProviderThread
175 forked/reconstructed from AppThread summary
176 ContextHandoff(strategy=full_thread_summary)
177```
178
179Provider switching should never concatenate hidden summaries into arbitrary prompts without recording the handoff. The handoff text is part of the run input contract and must be auditable.
180
181## Forking Threads
182
183Forking creates a new app thread from a source point. It should not require provider selection and it should not eagerly create provider runtime state.
184
185```text
186thread.fork
187 -> create target AppThread
188 -> record AppThread lineage
189 -> create ContextTransfer(type=fork, status=pending)
190 -> no ProviderSession required
191 -> no ProviderThread required
192 -> no portable context handoff required
193```
194
195The first run on the fork resolves the pending transfer:
196
197```text
198first message on fork selects provider P
199 -> if source provider is P and native fork is available:
200 resolve by provider-native fork
201 else:
202 materialize portable context
203 -> create/resume target ProviderThread
204 -> send resolved context + user message
205```
206
207The preferred first implementation should fork only from stable source points: completed runs, checkpoints, or idle provider threads with known coverage. Forking from an active run should use the latest completed checkpoint or be rejected until active-run semantics are deliberately designed.
208
209## Merge-Back From Forks
210
211Merge-back brings fork exploration into a source thread without copying the entire fork transcript into the main provider context.
212
213```text
214source thread forked at point S
215fork thread explores through point F
216user sends next source-thread message with merge-back intent
217 -> create ContextTransfer(type=merge_back, basePoint=S, sourcePoint=F)
218 -> build delta context lazily
219 -> inject delta with the next source-thread user message
220```
221
222The delta should describe what changed since the fork point: decisions, files changed, commands/tests run, conclusions, and unresolved issues. It should not include provider protocol noise or full transcript content unless needed.
223
224## Interruption
225
226Interruption targets a run or a node.
227
228```text
229interrupt.run(RunId)
230 -> resolve root node
231 -> resolve provider turn/thread refs
232 -> provider interrupt command if supported
233 -> mark interrupt requested
234```
235
236Terminal state arrives from provider lifecycle:
237
238```text
239provider interrupt request returns
240 -> command acknowledged only
241provider emits root turn/completed status=interrupted
242 -> root node interrupted
243 -> run interrupted
244 -> checkpoint policy runs
245```
246
247The app should not mark the run terminal solely because the interrupt request returned.
248
249If the provider does not support interruption:
250
251- best case: stop provider session and mark run interrupted or cancelled by policy.
252- worst case: mark interrupt unsupported and leave run active until provider exits.
253
254The Stop button sends `run.interrupt` with `holdQueue`. A Stop also holds the queue,
255ends the thread's pull request watches, and drops wakes its delegated tasks still
256owe, so nothing automatic restarts the thread. It then stops every delegated task
257under the thread: a `delegated-tasks.stop` effect sends the internal `thread.stop`
258command to each child thread, depth first. This runs after commit, not inside the
259parent's command, because a command holds only its own thread's lock. `task_cancel`
260sends the same `thread.stop` to its child.
261
262## Resumption
263
264Resumption has three related meanings:
265
2661. Resume an app thread in the UI from persisted projection.
2672. Resume a provider-native conversation/session.
2683. Recover or recreate a live provider runtime process while preserving app and provider-thread continuity.
269
270V2 always supports app-thread resumption from stored events/projections. Provider-thread resumption is a required provider adapter primitive. Every provider harness must expose a cursor/session/thread handle that can be used to resume prior provider state.
271
272Provider runtime recovery is also a core primitive, not a test-only hook. The same recovery path is used when:
273
274- the user restarts the app/server and all provider processes are gone,
275- an idle-session reaper intentionally releases provider processes,
276- a provider process crashes or loses network and policy allows retry.
277
278```text
279open app thread
280 -> load AppThread, Runs, Messages, ExecutionGraph summary
281 -> if provider work must continue, resume the relevant ProviderThread
282```
283
284Provider resume flow:
285
286```text
287resolve activeProviderThreadId
288resolve nativeThreadRef / nativeConversationHeadRef resume cursor
289start ProviderSession
290provider resume
291bind new ProviderSessionId to ProviderThread
292```
293
294Provider runtime recovery flow:
295
296```text
297provider runtime unavailable
298 -> mark ProviderSession stopped/error with reason
299 -> remove live runtime handle from ProviderSessionManager
300 -> keep ProviderThread and nativeThreadRef durable
301 -> on next work, open a new provider runtime
302 -> resume the active ProviderThread before starting/retrying work
303```
304
305Idle cleanup and crash recovery must use production lifecycle APIs. Tests may recreate the outermost layer/runtime or drive the same reaper/recovery service with deterministic clock and replayed provider transport, but they must not use test-only session restart methods.
306
307If provider resume fails:
308
309- preserve historical graph.
310- mark provider thread not loaded or error.
311- create replacement provider thread from reconstructed app context when the user continues.
312- mark pending live requests as not resumable.
313
314Provider resume failure is a runtime/recovery condition. It should not be modeled as "provider does not support resume."
315
316## Forking
317
318Forking creates a new first-class `AppThread` from a stable source.
319
320Supported fork sources:
321
322```ts
323type ForkSource =
324 | { type: "run"; threadId: ThreadId; runId: RunId }
325 | { type: "node"; nodeId: NodeId }
326 | {
327 type: "provider_thread";
328 providerThreadId: ProviderThreadId;
329 providerTurnId?: ProviderTurnId;
330 };
331```
332
333Stable fork boundaries:
334
335- completed run
336- interrupted or failed run with a provider snapshot/checkpoint
337- completed subagent node with a provider thread
338- provider thread snapshot returned by the provider
339- checkpoint boundary
340
341Unstable boundaries:
342
343- streaming message delta
344- active tool call
345- pending approval
346- active subagent turn, unless live fork support is explicitly implemented
347
348Forking from a subagent:
349
350```text
351subagent node selected
352 -> resolve ProviderThread owned by node
353 -> provider fork/resume if supported
354 -> create new AppThread
355 -> attach forked ProviderThread
356 -> copy/link checkpoint baseline
357```
358
359If the provider cannot fork native threads, V2 may synthesize a fork by creating a new app thread with projected messages/context, but that should be marked as lower fidelity by capability.
360
361## Checkpointing
362
363Checkpoints attach to checkpoint scopes. Root runs and child execution nodes can both be checkpointed.
364
365Pre-run baseline:
366
367```text
368before root execution starts
369 -> ensure root checkpoint for current app run ordinal - 1
370```
371
372Post-root-run checkpoint:
373
374```text
375root node terminal
376 -> drain/finalize streams
377 -> capture root checkpoint for app run ordinal
378 -> compute diff from prior root checkpoint
379 -> mark run terminal with checkpoint
380```
381
382Child/subagent checkpoint:
383
384```text
385child checkpointable node starts
386 -> create child CheckpointScope under parent scope
387child node terminal
388 -> drain/finalize child streams/items
389 -> capture child checkpoint for that scope
390 -> compute diff from parent/previous child checkpoint
391 -> mark child node checkpointed
392```
393
394Interrupted/failed runs:
395
396- If filesystem state is meaningful and safe to capture, capture with status reflecting terminal state.
397- If not, store a missing/error checkpoint summary.
398- Child/subagent checkpoints are allowed, but they do not advance the parent app run count.
399- Root run checkpointing should include or reference child checkpoint summaries so the run has a complete audit trail.
400
401Rollback:
402
403```text
404rollback.toRun(threadId, targetRunOrdinal)
405 -> restore filesystem checkpoint targetRunOrdinal
406 -> compute provider turns to roll back if provider supports rollback
407 -> call provider rollback
408 -> reconcile returned provider snapshot if available
409 -> mark later runs rolled_back
410 -> delete or mark stale later checkpoint refs
411```
412
413Codex `thread/rollback` returns an authoritative provider thread snapshot. Other providers may require a synthetic provider thread restart from context.
414
415Nested rollback:
416
417```text
418rollback.node(nodeId, checkpointId)
419 -> restore child checkpoint if scope cwd/ref is available
420 -> roll back provider thread for that child scope if supported
421 -> mark descendant child checkpoints stale/rolled_back
422 -> preserve parent run checkpoint history unless parent filesystem state changed
423```
424
425If a child checkpoint mutates the same workspace as the parent, restoring it may dirty the parent run's workspace state. The UI/API should make that explicit.
426
427## Approvals
428
429Approvals are runtime requests scoped to execution nodes.
430
431```text
432provider requestApproval
433 -> RuntimeRequest(kind=...)
434 -> approval ExecutionNode
435 -> thread pending request projection
436 -> optional thread status waiting
437```
438
439User response:
440
441```text
442approval.respond(RuntimeRequestId)
443 -> resolve live provider callback
444 -> send provider response
445 -> RuntimeRequest resolved
446 -> approval node completed/cancelled
447```
448
449Rules:
450
451- Approval ids are app-owned.
452- Provider request ids are refs.
453- Pending approvals from subagents are still actionable if the provider exposes live response capability.
454- Approval response should fail with `not_resumable` if provider callback state is gone.
455
456## Ask Questions / User Input
457
458Ask-questions is a user-input request, not a plan by itself.
459
460```text
461provider requests user input
462 -> RuntimeRequest(kind=user_input)
463 -> ExecutionNode(kind=user_input_request)
464 -> Question artifact
465 -> run status=waiting
466```
467
468On answer:
469
470```text
471user-input.respond(RuntimeRequestId, answers)
472 -> provider callback
473 -> request resolved
474 -> node completed
475 -> run resumes/runs
476```
477
478If the provider does not support structured questions, the adapter may project a plain assistant message and no respondable request.
479
480## Plans And Todo Lists
481
482V2 separates three concepts:
483
484- Proposed plan: a durable plan artifact the user can accept/implement.
485- Todo list: live execution progress for the current run.
486- Ask questions: structured user-input request.
487
488Codex examples:
489
490```text
491turn/plan/updated
492 -> PlanArtifact(kind=todo_list)
493 -> activity/projection update
494
495item/plan/delta or plan item completed
496 -> PlanArtifact(kind=proposed_plan)
497
498item/tool/requestUserInput
499 -> PlanArtifact(kind=questions)
500 -> RuntimeRequest(kind=user_input)
501```
502
503Rules:
504
505- Root run todo lists update the main plan/progress UI.
506- Child/subagent todo lists are nested under their execution node.
507- Child plans do not replace root plans unless promoted/forked.
508- Proposed plan implementation creates a new run or a new thread depending on user action.
509
510## Activities
511
512Activities are a projection of execution graph events.
513
514Examples:
515
516```text
517tool.started
518approval.requested
519approval.resolved
520plan.updated
521checkpoint.captured
522subagent.started
523subagent.completed
524runtime.warning
525```
526
527Activities should keep `nodeId` and `runId` so the UI can group them correctly.
528
529## Completion Barriers
530
531Before a run is marked terminal, V2 should pass a finalization barrier:
532
533```text
534root provider turn terminal
535 -> flush assistant text buffers
536 -> close active assistant messages
537 -> finalize plan/todo artifacts
538 -> close open non-live child nodes if provider marked them done
539 -> capture checkpoint
540 -> publish run terminal
541```
542
543This prevents early checkpoint capture and incomplete plan state.