| 1 | # Feature Lifecycles |
| 2 | |
| 3 | This 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 | |
| 7 | Creating an app thread is separate from starting provider work. |
| 8 | |
| 9 | ```text |
| 10 | thread.create |
| 11 | -> AppThread created |
| 12 | -> no provider session required yet |
| 13 | -> no provider thread required yet unless eager provider creation is enabled |
| 14 | ``` |
| 15 | |
| 16 | Provider thread creation may be lazy: |
| 17 | |
| 18 | ```text |
| 19 | first run starts |
| 20 | -> ensure ProviderSession |
| 21 | -> ensure ProviderThread |
| 22 | -> send provider turn |
| 23 | ``` |
| 24 | |
| 25 | If 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 | |
| 29 | When the user sends a message: |
| 30 | |
| 31 | ```text |
| 32 | thread.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 | |
| 39 | When provider execution begins: |
| 40 | |
| 41 | ```text |
| 42 | provider turn accepted/started |
| 43 | -> ProviderTurn allocated or correlated |
| 44 | -> root node status=running |
| 45 | -> run status=running |
| 46 | ``` |
| 47 | |
| 48 | When the root provider turn completes: |
| 49 | |
| 50 | ```text |
| 51 | root 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 | |
| 59 | Only the root node can complete the run. |
| 60 | |
| 61 | ## Steering And Queueing Messages |
| 62 | |
| 63 | V2 should support three classes of follow-up input: |
| 64 | |
| 65 | ```text |
| 66 | steer active run in place |
| 67 | steer active run by restart |
| 68 | queue next run |
| 69 | ``` |
| 70 | |
| 71 | Steering is an app-level intent to change the active run's instructions. Provider-native steering is only one implementation strategy. |
| 72 | |
| 73 | If the provider supports active-run steering, the app sends a steer command to the provider and keeps the same run active. |
| 74 | |
| 75 | If 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 | |
| 80 | The 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 | |
| 82 | Queueing creates a new run after the active run. |
| 83 | |
| 84 | Recommended command shape: |
| 85 | |
| 86 | ```ts |
| 87 | type MessageDispatchMode = |
| 88 | | { type: "steer_active"; targetRunId: RunId } |
| 89 | | { type: "restart_active"; targetRunId: RunId } |
| 90 | | { type: "queue_after_active" } |
| 91 | | { type: "start_immediately" }; |
| 92 | ``` |
| 93 | |
| 94 | Policy: |
| 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 | |
| 101 | The 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 |
| 104 | type 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 | |
| 116 | Only 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 | |
| 120 | A normal follow-up after a completed run creates a new run. |
| 121 | |
| 122 | ```text |
| 123 | previous Run completed |
| 124 | user sends follow-up |
| 125 | -> new Run ordinal=N+1 |
| 126 | -> new root node |
| 127 | -> same AppThread |
| 128 | -> same ProviderThread if resumable |
| 129 | ``` |
| 130 | |
| 131 | If 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 | |
| 133 | The replacement path is an error recovery flow, not a provider capability branch. |
| 134 | |
| 135 | ## Changing Providers Between Runs |
| 136 | |
| 137 | Changing 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 |
| 140 | user 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 | |
| 148 | Example: |
| 149 | |
| 150 | ```text |
| 151 | runs 1-5: Codex provider thread C1 |
| 152 | switch to Claude |
| 153 | -> summarize runs 1-5 for Claude |
| 154 | -> create Claude provider thread L1 |
| 155 | runs 6-8: Claude provider thread L1 |
| 156 | switch 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 | |
| 162 | The 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 | |
| 164 | Fallback 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 | |
| 171 | The fallback should be explicit in the graph: |
| 172 | |
| 173 | ```text |
| 174 | new ProviderThread |
| 175 | forked/reconstructed from AppThread summary |
| 176 | ContextHandoff(strategy=full_thread_summary) |
| 177 | ``` |
| 178 | |
| 179 | Provider 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 | |
| 183 | Forking 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 |
| 186 | thread.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 | |
| 195 | The first run on the fork resolves the pending transfer: |
| 196 | |
| 197 | ```text |
| 198 | first 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 | |
| 207 | The 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 | |
| 211 | Merge-back brings fork exploration into a source thread without copying the entire fork transcript into the main provider context. |
| 212 | |
| 213 | ```text |
| 214 | source thread forked at point S |
| 215 | fork thread explores through point F |
| 216 | user 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 | |
| 222 | The 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 | |
| 226 | Interruption targets a run or a node. |
| 227 | |
| 228 | ```text |
| 229 | interrupt.run(RunId) |
| 230 | -> resolve root node |
| 231 | -> resolve provider turn/thread refs |
| 232 | -> provider interrupt command if supported |
| 233 | -> mark interrupt requested |
| 234 | ``` |
| 235 | |
| 236 | Terminal state arrives from provider lifecycle: |
| 237 | |
| 238 | ```text |
| 239 | provider interrupt request returns |
| 240 | -> command acknowledged only |
| 241 | provider emits root turn/completed status=interrupted |
| 242 | -> root node interrupted |
| 243 | -> run interrupted |
| 244 | -> checkpoint policy runs |
| 245 | ``` |
| 246 | |
| 247 | The app should not mark the run terminal solely because the interrupt request returned. |
| 248 | |
| 249 | If 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 | |
| 254 | The Stop button sends `run.interrupt` with `holdQueue`. A Stop also holds the queue, |
| 255 | ends the thread's pull request watches, and drops wakes its delegated tasks still |
| 256 | owe, so nothing automatic restarts the thread. It then stops every delegated task |
| 257 | under the thread: a `delegated-tasks.stop` effect sends the internal `thread.stop` |
| 258 | command to each child thread, depth first. This runs after commit, not inside the |
| 259 | parent's command, because a command holds only its own thread's lock. `task_cancel` |
| 260 | sends the same `thread.stop` to its child. |
| 261 | |
| 262 | ## Resumption |
| 263 | |
| 264 | Resumption has three related meanings: |
| 265 | |
| 266 | 1. Resume an app thread in the UI from persisted projection. |
| 267 | 2. Resume a provider-native conversation/session. |
| 268 | 3. Recover or recreate a live provider runtime process while preserving app and provider-thread continuity. |
| 269 | |
| 270 | V2 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 | |
| 272 | Provider 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 |
| 279 | open app thread |
| 280 | -> load AppThread, Runs, Messages, ExecutionGraph summary |
| 281 | -> if provider work must continue, resume the relevant ProviderThread |
| 282 | ``` |
| 283 | |
| 284 | Provider resume flow: |
| 285 | |
| 286 | ```text |
| 287 | resolve activeProviderThreadId |
| 288 | resolve nativeThreadRef / nativeConversationHeadRef resume cursor |
| 289 | start ProviderSession |
| 290 | provider resume |
| 291 | bind new ProviderSessionId to ProviderThread |
| 292 | ``` |
| 293 | |
| 294 | Provider runtime recovery flow: |
| 295 | |
| 296 | ```text |
| 297 | provider 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 | |
| 305 | Idle 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 | |
| 307 | If 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 | |
| 314 | Provider resume failure is a runtime/recovery condition. It should not be modeled as "provider does not support resume." |
| 315 | |
| 316 | ## Forking |
| 317 | |
| 318 | Forking creates a new first-class `AppThread` from a stable source. |
| 319 | |
| 320 | Supported fork sources: |
| 321 | |
| 322 | ```ts |
| 323 | type 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 | |
| 333 | Stable 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 | |
| 341 | Unstable 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 | |
| 348 | Forking from a subagent: |
| 349 | |
| 350 | ```text |
| 351 | subagent 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 | |
| 359 | If 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 | |
| 363 | Checkpoints attach to checkpoint scopes. Root runs and child execution nodes can both be checkpointed. |
| 364 | |
| 365 | Pre-run baseline: |
| 366 | |
| 367 | ```text |
| 368 | before root execution starts |
| 369 | -> ensure root checkpoint for current app run ordinal - 1 |
| 370 | ``` |
| 371 | |
| 372 | Post-root-run checkpoint: |
| 373 | |
| 374 | ```text |
| 375 | root 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 | |
| 382 | Child/subagent checkpoint: |
| 383 | |
| 384 | ```text |
| 385 | child checkpointable node starts |
| 386 | -> create child CheckpointScope under parent scope |
| 387 | child 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 | |
| 394 | Interrupted/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 | |
| 401 | Rollback: |
| 402 | |
| 403 | ```text |
| 404 | rollback.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 | |
| 413 | Codex `thread/rollback` returns an authoritative provider thread snapshot. Other providers may require a synthetic provider thread restart from context. |
| 414 | |
| 415 | Nested rollback: |
| 416 | |
| 417 | ```text |
| 418 | rollback.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 | |
| 425 | If 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 | |
| 429 | Approvals are runtime requests scoped to execution nodes. |
| 430 | |
| 431 | ```text |
| 432 | provider requestApproval |
| 433 | -> RuntimeRequest(kind=...) |
| 434 | -> approval ExecutionNode |
| 435 | -> thread pending request projection |
| 436 | -> optional thread status waiting |
| 437 | ``` |
| 438 | |
| 439 | User response: |
| 440 | |
| 441 | ```text |
| 442 | approval.respond(RuntimeRequestId) |
| 443 | -> resolve live provider callback |
| 444 | -> send provider response |
| 445 | -> RuntimeRequest resolved |
| 446 | -> approval node completed/cancelled |
| 447 | ``` |
| 448 | |
| 449 | Rules: |
| 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 | |
| 458 | Ask-questions is a user-input request, not a plan by itself. |
| 459 | |
| 460 | ```text |
| 461 | provider requests user input |
| 462 | -> RuntimeRequest(kind=user_input) |
| 463 | -> ExecutionNode(kind=user_input_request) |
| 464 | -> Question artifact |
| 465 | -> run status=waiting |
| 466 | ``` |
| 467 | |
| 468 | On answer: |
| 469 | |
| 470 | ```text |
| 471 | user-input.respond(RuntimeRequestId, answers) |
| 472 | -> provider callback |
| 473 | -> request resolved |
| 474 | -> node completed |
| 475 | -> run resumes/runs |
| 476 | ``` |
| 477 | |
| 478 | If 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 | |
| 482 | V2 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 | |
| 488 | Codex examples: |
| 489 | |
| 490 | ```text |
| 491 | turn/plan/updated |
| 492 | -> PlanArtifact(kind=todo_list) |
| 493 | -> activity/projection update |
| 494 | |
| 495 | item/plan/delta or plan item completed |
| 496 | -> PlanArtifact(kind=proposed_plan) |
| 497 | |
| 498 | item/tool/requestUserInput |
| 499 | -> PlanArtifact(kind=questions) |
| 500 | -> RuntimeRequest(kind=user_input) |
| 501 | ``` |
| 502 | |
| 503 | Rules: |
| 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 | |
| 512 | Activities are a projection of execution graph events. |
| 513 | |
| 514 | Examples: |
| 515 | |
| 516 | ```text |
| 517 | tool.started |
| 518 | approval.requested |
| 519 | approval.resolved |
| 520 | plan.updated |
| 521 | checkpoint.captured |
| 522 | subagent.started |
| 523 | subagent.completed |
| 524 | runtime.warning |
| 525 | ``` |
| 526 | |
| 527 | Activities should keep `nodeId` and `runId` so the UI can group them correctly. |
| 528 | |
| 529 | ## Completion Barriers |
| 530 | |
| 531 | Before a run is marked terminal, V2 should pass a finalization barrier: |
| 532 | |
| 533 | ```text |
| 534 | root 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 | |
| 543 | This prevents early checkpoint capture and incomplete plan state. |