35 KiB
Terminal Lifecycle Transition Recovery — Tech Spec
Linear: REMOTE-1973
Behavioral guarantees
- Correctly ordered lifecycle events retain their existing user-visible behavior, block sequence, prompt rendering, exit-code semantics, and downstream notifications.
- Missing, duplicated, stale, or out-of-order lifecycle evidence must not panic, corrupt a finished block or prompt grid, lose preserved command/output, create phantom blocks, repeat once-per-block side effects, or leave Warp permanently stuck in an executing state.
- When completion metadata supplied by
Precmdpermits recovery, Warp preserves the shell-provided exit code and exact next-block identity while applying prompt metadata only to the corresponding fresh prompt block. - A novel completion is treated as evidence that the active command executed, even when Warp missed submission or
Preexecevidence. - Duplicate, colliding, malformed, or otherwise unsupported lifecycle evidence is handled conservatively without overwriting trustworthy state or fabricating completion evidence. In particular, any correlated or prompt-only
Precmdreceived after the active block has already reachedAtPromptis ignored with diagnostics. - Version-skewed shared sessions preserve normal prompt flow when possible; a prompt-only or malformed
Precmdmust never complete or advance a block or fall through to the existing unsafe mutation path.
Context
Warp currently validates that lifecycle hooks come from registered shell sessions, then immediately dispatches them to TerminalModel. TerminalModel performs model-wide cleanup and delegates block mutations through BlockList to the active Block. There is no central transition check before a hook mutates the active block or prompt grid:
app/src/terminal/model/ansi/mod.rs (552-609) @ 73858e86validates registered sessions and directly dispatches decoded hooks.app/src/terminal/model/terminal_model.rs (2700-2780) @ 73858e86handles completion, prompt-ready, and execution-start hooks, including alt-screen/controller-facing side effects.app/src/terminal/model/blocks.rs (3715-3795) @ 73858e86finalizes/advances blocks and applies prompt metadata.app/src/terminal/model/block.rs (3329-3400) @ 73858e86reparses prompt grids and mutates block lifecycle state.
Command starts enter the model through a separate path:
app/src/terminal/model/terminal_model.rs (1639-1695) @ 73858e86starts local, shared-session, environment-collection, and AI-associated commands without returning an acceptance disposition.app/src/terminal/model/blocks.rs (2801-2840) @ 73858e86caches prompt state and starts normal/in-band blocks.app/src/terminal/writeable_pty/pty_controller.rs (492-610) @ 73858e86attaches start state and writes queued commands.
Completion is currently represented as an always-present exit code. Block::finish seals grids, stores the exit code, changes block state, and emits BlockCompleted; BlockList separately advances to the next block and later emits AfterBlockCompleted from Precmd:
app/src/terminal/model/block.rs (1560-1620) @ 73858e86stores a concrete exit code and emits completion.app/src/terminal/model/blocks.rs (2975-3055) @ 73858e86advances the block list and handles background/bootstrap state.app/src/terminal/model/block/serialized_block.rs (160-365) @ 73858e86serializes completed block state.
Shared sessions send ordered lifecycle events separately from raw PTY bytes. The sharer emits the completion's next-block identity while parsing the hook and sends the raw PTY chunk afterward, but the viewer currently does not use the ordered completion event to coordinate raw-hook recovery:
app/src/terminal/model/terminal_model.rs (3010-3050) @ 73858e86sends raw PTY bytes after processing a chunk.app/src/terminal/shared_session/sharer/network.rs (1518-1595) @ 73858e86preserves ordered event delivery.app/src/terminal/shared_session/viewer/event_loop.rs (180-305) @ 73858e86applies shared events and PTY bytes.
Every supported shell already captures the previous command's exit code at the beginning of the same shell function that emits CommandFinished and then Precmd: zsh_body.sh:301 @ 73858e86, bash_body.sh:432 @ 73858e86, fish.sh:267 @ 73858e86, and pwsh.ps1:442 @ 73858e86. The same function also allocates the next block ID. Repeating both values in Precmd lets it reconcile a lost CommandFinished without inventing an exit code or block identity.
The existing cursor-clamping changes remain defense in depth. This spec adds completion metadata shared by CommandFinished and Precmd to the existing shell protocol; a fuller prompt-cycle identity, enforceable frozen-grid semantics, and PTY-reader teardown remain follow-ups.
Proposed changes
1. Add a private block-lifecycle module owned by TerminalModel
Declare a private mod lifecycle; from app/src/terminal/model/mod.rs. This is an ordinary Rust helper module, not a WarpUI entity or model. Keep its API private to terminal::model, exposing only the pub(super) types and entry points needed by TerminalModel.
Organize new lifecycle code under app/src/terminal/model/lifecycle/:
mod.rsis the small internal facade and ownsBlockLifecycleCoordinator.transition.rsowns the pure phase/input/snapshot/action types and exhaustive transition planner.telemetry.rsowns recovery records and per-terminal rate limiting.mod_test.rsowns the table-driven transition and rate-limiting tests.
TerminalModel owns one BlockLifecycleCoordinator because it is the existing layer that observes local/shared/in-band command starts, every lifecycle hook, alt-screen and bracketed-paste cleanup, handler-event emission, shared-session ordering, scrollback loading, and terminal exit. Keep changes in terminal_model.rs to thin integration: gather live evidence, call the lifecycle facade, and execute the returned action through small application helpers.
BlockList and Block expose recovery-capable mutation primitives, but they do not decide whether a lifecycle transition is valid. All production command starts and lifecycle hooks must be planned by the coordinator before those primitives run.
The private lifecycle module owns these core types:
LifecyclePhase:AwaitingPrecmd,AtPrompt,Submitted,Executing,Unknown, andTerminated.CommandStartKind: distinguishes user/queued/shared-session, in-band, and bootstrap starts.NextBlockIdDisposition:Novel,ActiveDuplicate, orExistingCollision.PreexecObservation: classifies whether a repeated execution-start command agrees with the active command without retaining command text.LifecycleInput: payload-light evidence forStartCommand,Preexec,CommandFinished,PrecmdWithCompletionMetadata,PromptOnlyPrecmd,InitShell, and terminal exit.LifecycleSnapshot: immutable live evidence including active block ID/session, block state, started/finished/received-Precmd/in-band flags, bootstrap stage, alt-screen state, and hook session.LifecycleAction: exhaustive planned operations:StartActiveBlock,ApplyPreexec,AcceptCommandFinished,ReconcileCompletionThenApplyPrecmd,ApplyPrecmd,BeginEpoch,Terminate, andIgnore.LifecycleTransition: previous phase, next phase, action, and optional recovery record.LifecycleRecoveryRecord: structured non-UGC diagnostic data for a non-normal transition, including whether completion evidence came fromCommandFinishedorPrecmd.BlockLifecycleCoordinator: current phase, lifecycle epoch, and per-terminal transition rate limiter.StartCommandOutcome:Accepted,Coalesced,RejectedExecuting, orIgnoredTerminated.
Planning is a pure exhaustive match over phase, snapshot, and input. TerminalModel applies the planned action and commits the next phase only after the action succeeds. Before each plan, the coordinator reconciles its phase with the live snapshot; an impossible combination degrades to Unknown and recovers from the incoming evidence.
Completion identity is evaluated before the phase-specific matrix:
CommandFinishedwith a next block ID equal to the active block is a duplicate and is ignored.Precmdwith a next block ID equal to the active block applies prompt metadata only when the block is fresh and awaiting its first prompt. If the active block already receivedPrecmd, ignore the repeated evidence. If its exit code disagrees with the already-completed previous block, record the mismatch without re-finishing or overwriting the block.- Completion metadata whose next block ID belongs to a non-active existing block is stale/colliding and is ignored.
- Completion metadata with a novel next block ID treats an unfinished active block as having executed and completes it using the provided exit code, or advances past an already-finished active block without re-finishing it. Completion evidence is sufficient to choose
DoneWithExecutioneven when Warp did not observe submission orPreexec. It creates that exact next block and—when the evidence came fromPrecmd—applies prompt metadata to it.
The remaining phase-specific policy is:
AwaitingPrecmd:StartCommandorPreexecaccepts the new command and records the missing prompt.AtPrompt:StartCommandand missing-local-startPreexecare accepted.Submitted: repeatedStartCommandcoalesces andPreexecexecutes normally.Executing:StartCommandis rejected before bytes are written. RepeatedPreexecis ignored, with a differing command recorded separately.Unknown:StartCommandestablishesSubmitted;Preexecensures start and establishesExecuting; completion metadata supplied byPrecmdreconciles block identity before applying the phase result.Terminated: ignore all command lifecycle inputs.
TerminalModel::new_internal, restored/imported state, and shared-session scrollback begin at Unknown. InitShell starts a new epoch and moves to bootstrap Submitted after the bootstrap block exists. Bootstrapped changes metadata only. Terminal exit moves to Terminated.
2. Share flattened completion metadata between CommandFinished and Precmd
Define CompletionMetadata in app/src/terminal/model/ansi/dcs_hooks.rs:
pub struct CompletionMetadata {
pub exit_code: ExitCode,
pub next_block_id: BlockId,
}
The current PrecmdValue is also constructed internally for session restoration and tests, where no command-completion evidence exists. Split its existing prompt/session/in-band fields into a reusable PromptMetadata struct, retaining their current field-level serde attributes and default behavior:
pub struct PromptMetadata {
pub pwd: Option<String>,
pub ps1: Option<String>,
pub ps1_is_encoded: Option<bool>,
pub honor_ps1: Option<bool>,
pub rprompt: Option<String>,
pub git_head: Option<String>,
pub git_branch: Option<String>,
pub virtual_env: Option<String>,
pub conda_env: Option<String>,
pub node_version: Option<String>,
pub kube_config: Option<String>,
pub session_id: HookSessionId,
pub is_after_in_band_command: bool,
}
The ANSI parser constructs full canonical PrecmdValue for hooks with completion metadata, and TerminalModel is the only lifecycle layer that consumes it. BlockList, Block, HeaderGrid, cached in-band prompt data, and restoration paths consume only PromptMetadata. Update hook session extraction to read PrecmdValue.prompt_metadata.session_id.
Embed CompletionMetadata non-optionally in both hook payloads and flatten both components of PrecmdValue:
pub struct CommandFinishedValue {
#[serde(flatten)]
pub completion_metadata: CompletionMetadata,
#[serde(default)]
pub session_id: HookSessionId,
}
pub struct PrecmdValue {
#[serde(flatten)]
pub completion_metadata: CompletionMetadata,
#[serde(flatten)]
pub prompt_metadata: PromptMetadata,
}
#[serde(flatten)] keeps every JSON field at its current top level. CommandFinished retains its existing flat exit_code and next_block_id fields, while Precmd gains the same two top-level fields without nesting its existing prompt fields. Keep session_id in PromptMetadata, outside CompletionMetadata: it belongs to the prompt hook's session semantics, while the shared completion struct represents only command-result and block-identity evidence.
Each shell emitter captures the exit code first, allocates the next block ID into a local variable, and sends the same pair in both hooks. Zsh and Bash currently allocate/increment the next ID inline while constructing CommandFinished; move that allocation into a local before sending either hook. Fish and PowerShell already retain equivalent locals.
CommandFinished remains the early, low-latency completion signal. Precmd is the authoritative reconciliation barrier: when the early event was lost, it supplies the real exit code and exact next block ID needed to perform normal completion exactly once. This design preserves the existing non-optional Block.exit_code, serialization, persistence, history, styling, AI results, and downstream result semantics.
Do not derive or apply a meaningful default for incomplete CompletionMetadata. PromptMetadata may retain Default for internal prompt-only construction. Refactor the key-value decoder's current DProtoHook::default_from_name plus incremental populate_field path for completion-bearing hooks to collect partial completion fields and construct canonical CompletionMetadata only after both fields are present. Replace production/test uses of default full PrecmdValue or CommandFinishedValue with explicit helpers for hooks with completion metadata where necessary.
At the parser boundary, classify both JSON and key-value Precmd payloads:
- Both completion fields produce canonical
PrecmdValue. - Neither completion field produces a distinct internal prompt-only
Precmdevent carrying onlyPromptMetadata. - Exactly one completion field is malformed and is rejected.
Keep CompletionMetadata non-optional inside canonical PrecmdValue. Add a separate prompt-only handler callback for the internal prompt-only event, and extract its registered session from PromptMetadata.session_id.
Prompt-only compatibility exists specifically for meaningful client-version skew, primarily a newer shared-session viewer parsing raw PTY hooks from an older sharer. A prompt-only Precmd may apply prompt metadata to a fresh block in AwaitingPrecmd after an accepted CommandFinished. In AtPrompt, Submitted, Executing, or Unknown, it is ignored with diagnostics because it cannot prove completion, identify the next block, or safely identify prompt content that should be replaced. It never completes or advances a block. This preserves normal cross-version prompt flow without attempting to emulate unsafe older behavior during the rare overlap of version skew and an exceptional lifecycle sequence.
Keep HandlerEvent::Precmd as a normal once-per-block prompt event. Ignored repeated Precmd evidence must not emit another downstream prompt event.
3. Centralize lifecycle application in TerminalModel
Add private helpers near the ANSI handler implementation:
lifecycle_snapshotconstructs live evidence.plan_lifecycle_transitionasks the coordinator for an action.apply_lifecycle_transitionexhaustively executes the action, emits recovery diagnostics, and commits the next phase.complete_commandis the only finalization pipeline and consumesCompletionMetadatafrom either hook.apply_precmd_to_fresh_blockpasses onlyPromptMetadatainto the normal prompt-ready path.
Rewrite TerminalModel's command_finished, precmd_with_completion_metadata, prompt_only_precmd, and preexec handlers to plan before mutation. Accepted lifecycle hooks always target BlockList rather than using delegate!, even when the alternate screen is active; unrelated ANSI behavior continues using existing delegation.
complete_command performs shared completion behavior whether the first accepted evidence came from CommandFinished or Precmd:
- Unset bracketed paste and exit the alternate screen.
- Capture command type, bootstrap stage, in-band state, and the next block ID.
- If the unfinished active block is not executing, move it through a minimal preexec-equivalent execution transition without replacing its command text, so
Block::finishproducesDoneWithExecution. - Complete/advance the block list through one primitive.
- Emit the ordered shared-session completion event.
- Emit
HandlerEvent::CommandFinishedso controller state clears.
When Precmd.completion_metadata.next_block_id is novel, run complete_command with its real completion metadata, then apply the received prompt metadata to the newly created block. When its next block ID is already active, apply the prompt only if the active block is still awaiting its first prompt; otherwise ignore the repeated evidence without completing or mutating prompt state. This ordering prevents the received prompt from touching the completed block.
Change all TerminalModel::start_command_execution* variants to return StartCommandOutcome. Attach AI/environment/shared metadata, emit ordered shared-session start events, set controller executing state, and write PTY bytes only for Accepted. Route the in-band before-write callback through a lifecycle-aware TerminalModel::start_in_band_command_execution instead of calling BlockList::start_active_block_for_in_band_command directly.
4. Add focused BlockList and Block primitives
Replace BlockList::finalize_block_and_advance_list(CommandFinishedValue) with complete_active_block_and_advance(CompletionMetadata). This primitive preserves existing bootstrap progression, background-block finalization, selection/height changes, in-band decrement, latest-finish timing, and next-block creation regardless of which hook supplied the completion metadata.
Add:
classify_next_block_idfor the global duplicate/collision rule.ensure_active_block_started(CommandStartKind)for missing-start/Preexecrecovery while preserving prompt caching and early-output reset.ensure_active_block_executing_for_completionfor the minimal preexec-equivalent transition required before completing a submitted/unknown unfinished block asDoneWithExecution.advance_from_finished_activefor a novel completion that must not finish the old block twice.apply_precmd_to_active(PromptMetadata)for the normal once-per-block path, including cached populated in-band prompt-metadata substitution.
Do not add prompt-refresh mutation primitives for repeated Precmd. Once the active block has reached AtPrompt, retaining its existing prompt, context, command/input content, and cursor position is safer than attempting to identify and replace a possibly stale prompt region.
Keep ansi::Handler::precmd_with_completion_metadata(PrecmdValue) as the callback for a Precmd with completion metadata and add prompt_only_precmd for the internal prompt-only event; stop delegating either below TerminalModel. Add direct PromptMetadata-accepting helpers for BlockList, Block, and HeaderGrid; change BlockList's cached/last-populated prompt payloads and session-restoration construction to use PromptMetadata. Completion metadata is consumed and removed at the TerminalModel lifecycle boundary before prompt metadata reaches block mutation code.
Keep Block::preexec's defensive start fallback, but production recovery calls BlockList::ensure_active_block_started first so prompt caching and early-output reset occur.
5. Reconcile shared sessions from raw hooks with completion metadata
The shell-generated completion metadata travels in both raw hooks, so sharers and viewers parse the same real exit code and next block ID. A viewer that accepts CommandFinished first advances normally; a viewer that misses it can reconcile from the subsequent raw Precmd without generating an ID or consulting an ordered-event hint.
Keep OrderedTerminalEventType::CommandExecutionFinished as the existing low-latency shared-session signal. It no longer needs to provide a recovery hint. A raw Precmd with completion metadata remains authoritative for exact reconciliation on both sharer and viewer.
Meaningful sharer/viewer version skew can still diverge during exceptional sequences: an old viewer cannot use a new sharer's additional Precmd fields, and a new viewer cannot recover from an old sharer's prompt-only Precmd if CommandFinished was also lost. Accept this rare overlap rather than reproducing unsafe old behavior. The prompt-only path preserves normal old-sharer/new-viewer prompt application.
Reduce the old-sharer population before relying on Precmd completion metadata in production: ship the protocol-only emitter/parser change to stable as soon as possible, then allow two additional stable releases to ship before enabling state-mutating lifecycle recovery in production. The remaining implementation may merge and recovery may be enabled for dev/dogfood during this compatibility soak.
Loading or appending shared-session scrollback resets the coordinator to Unknown so later completion metadata re-establishes the phase. No serialized block, persistence, history, AI-result, or shared-session result schema changes are required.
6. Add structured, rate-limited lifecycle telemetry
Implement the feature-specific event in app/src/terminal/model/lifecycle/telemetry.rs rather than adding a large global telemetry variant. Add an internal Event::LifecycleRecovery(LifecycleRecoveryRecord); ModelEventDispatcher, which owns ModelContext, sends the feature telemetry without exposing a new public ModelEvent.
Rate-limit per terminal and (previous phase, input kind, recovery action) pair. Emit the first event immediately, then at most once per minute with a suppressed-repeat count.
Record phase, input kind, recovery action, active/hook session IDs, active and supplied next block IDs, bootstrap/in-band/start/precmd/preexec evidence, and whether completion evidence came from CommandFinished or Precmd. Never record command text, output, PS1, CWD, or other UGC.
7. Use one lifecycle pipeline and feature-flag only abnormal recovery
Add FeatureFlag::TerminalLifecycleRecovery in crates/warp_features/src/lib.rs. Enable it only for dev/dogfood during the two-release compatibility soak; leave it disabled in production.
Do not maintain separate old and new lifecycle-mutation pipelines. Route every lifecycle input through BlockLifecycleCoordinator and the shared completion/prompt application helpers unconditionally. Normal transitions, duplicate/collision rejection, and the rule that unsupported evidence never reaches the old unsafe mutation path are always enabled.
Use TerminalLifecycleRecovery only when the coordinator selects a non-normal recovery action that would mutate state for a sequence Warp previously mishandled, such as completing from a Precmd with completion metadata. When the flag is disabled, keep the same coordinator and diagnostics but conservatively ignore that recovery action. This keeps the flag localized to action selection rather than duplicating lifecycle logic. Ship the protocol-only first slice to stable as soon as possible. The remaining implementation and dev/dogfood enablement may merge during the compatibility soak, but production recovery remains disabled until two additional stable releases have shipped with Precmd completion metadata. Leave recovery actions dogfood-only for at least one full week before the human DRI considers production enablement; production promotion and eventual flag removal are not implementation slices in this spec.
Deliver in sequential reviewable slices:
- Update all shell emitters to send the same completion pair in both hooks, without restructuring the Rust hook value structs or changing block behavior.
- Add flattened
CompletionMetadata, split reusablePromptMetadatafrom the wirePrecmdValue, classifyWithCompletionMetadata/PromptOnly/malformedPrecmd, and add protocol serialization/parsing tests without changing block behavior. - Centralize normal lifecycle mutations behind focused
TerminalModel,BlockList, andBlockhelpers with normal-flow parity. - Add
BlockLifecycleCoordinator, telemetry, phase reconciliation, start rejection/coalescing, and unconditional safety rules; keep state-mutating recovery disabled. - Add conservative repeated-
Precmdno-op handling and safe prompt-only handling. - Add completion reconciliation from
Precmdwith completion metadata and shared-session recovery; gate state-mutating non-normal recovery actions behindTerminalLifecycleRecovery. - Enable recovery actions for dev/dogfood while keeping them disabled in production.
End-to-end flow
flowchart LR
Hook["Registered lifecycle evidence"] --> Snapshot["TerminalModel builds snapshot"]
Start["Local/shared/in-band start intent"] --> Snapshot
Snapshot --> Plan["BlockLifecycleCoordinator plans transition"]
Plan --> Ignore["Ignore/coalesce safely"]
Plan --> Apply["Apply normal transition"]
Plan --> Recover["Reconcile from Precmd metadata"]
Recover --> Complete["Shared completion pipeline with real exit code"]
Complete --> Advance["Seal old block and create next block"]
Advance --> Prompt["Apply Precmd to fresh block"]
Ignore --> Telemetry["Rate-limited recovery telemetry"]
Recover --> Telemetry
Testing and validation
Pure transition matrix
Add app/src/terminal/model/lifecycle/mod_test.rs with table-driven coverage for every (LifecyclePhase, LifecycleInput) pair. Assert action, next phase, recovery classification, next-block-ID handling from Precmd completion metadata, duplicate/collision handling, Unknown snapshot reconciliation, InitShell epoch reset, Terminated absorption, and rate limiting.
The full matrix must be visible in test cases so a policy change is an explicit review decision.
Model integration
Add TerminalModel regression coverage for:
- Normal command flow with
Preexec,CommandFinished, andPrecmd. - Missing local start and missing prompt metadata.
- Missing-
CommandFinishedrecovery from aPrecmdwith completion metadata, including real exit-code preservation and fresh-block prompt application. - Completion from
CommandFinishedwithout observedPreexecin submitted, unknown, and otherwise-normal flows, including the minimal execution transition and conservativeDoneWithExecutionresult. - Repeated/differing
Preexec, same-ID and novel-IDPrecmd, repeated start, and start while executing. - Duplicate/colliding/novel completion from every phase.
- Restored/unknown state, bootstrap/subshell epochs, registered session switches, and ignored hooks after exit.
- In-band completion and cached prompt payload reuse.
Update out_of_order_precmd_pty_hook_cannot_restore_cursor_outside_finished_header_grid to assert that recovery seals the old block once with the real exit code supplied by Precmd, creates the specified fresh block, applies prompt metadata only to the fresh block, and does not panic.
Capture terminal and handler events to assert exact side effects: one BlockCompleted, one AfterBlockCompleted, one BlockMetadataReceived for the fresh block, one command-finished handler event, one prompt handler event, alt-screen/bracketed-paste cleanup, background finalization, and in-band decrement. Repeated correlated and prompt-only Precmd must leave the active prompt, context, current command/input content, and cursor position unchanged and emit none of the once-per-block events.
Controller, shell protocol, and sharing
- Add
PtyControllertests proving rejected/coalesced starts do not write bytes or leave executing state stuck. - Add
CompletionMetadataserde tests proving both hook payloads use the same flat JSON keys and reject a payload with only one completion field. - Add
PromptMetadatatests proving restoration and prompt-only internal paths never construct or require completion evidence. - Add key-value decoder tests proving it constructs canonical metadata only when both fields were populated and never supplies default completion evidence.
- Add prompt-only
Precmdtests proving normal first-prompt application remains compatible after an acceptedCommandFinished, while repeated prompt-only evidence and prompt-only evidence inSubmitted,Executing, andUnknownnever mutates, completes, or advances a block. - Add shell-emitter tests proving zsh, bash, fish, and PowerShell send identical exit-code/next-block-ID pairs in
CommandFinishedandPrecmd, including in-band commands. - Add shared-session viewer tests proving a raw
Precmdwith completion metadata reconciles to the shell-specified next block ID without an ordered-event hint. - Assert normal persistence, history, styling, automation, and AI-result behavior is unchanged because every completed command still has a real exit code.
- Assert telemetry field allowlisting and rate limiting.
Repository validation
- Run focused
cargo nextestfilters for DCS hooks, block-lifecycle coordinator, terminal model, controller, and shared-session viewer tests. - Run
cargo check -p warp --lib. - Run
./script/format, then the relevant clippy command from./script/presubmit, andgit diff --checkbefore review. - Ask the user to manually validate normal/empty/syntax-error commands, an alt-screen command, an in-band command, prompt repaint, subshell/SSH return, shared-session viewing, and an injected missing-
CommandFinishedsequence. Do not usecargo runon the user's behalf.
Parallelization
Do not parallelize the Warp implementation across child agents. The shared hook schema, shell emitters, transition coordinator, common completion pipeline, and handler events must evolve together; parallel worktrees would make it easy for the duplicated shell payloads and Rust reconciliation contract to drift before either path can be validated end to end.
Implement the slices sequentially. Reviewers can still review the protocol/schema foundation separately from recovery behavior through dependent commits, but one owner should integrate and validate the lifecycle semantics end to end.
Risks and mitigations
- The two shell-emitted metadata copies could diverge. Allocate the exit code and next block ID once per shell prompt cycle, reuse them in both messages, and add emitter/parsing tests. Record mismatches when both events arrive.
- A stale prompt-ready event could be mistaken for completion. Require a registered session and classify the supplied next block ID before mutation. A next block ID belonging to a non-active existing block is stale and ignored.
- Ignoring a repeated
Precmdcould discard newer prompt metadata. Prefer the existing trustworthyAtPromptstate over mutating an ambiguous prompt-and-command header. A later prompt cycle will apply fresh metadata normally. - Recovery could duplicate lifecycle side effects.
CommandFinishedandPrecmdwith completion metadata share one completion pipeline; active-ID and existing-ID cases use dedicated no-refinish paths; integration tests assert exact event counts. - A feature flag could cause old and new lifecycle paths to drift. Always use the new coordinator and shared mutation helpers; the flag changes only whether a selected non-normal recovery action is applied or conservatively ignored.
- Internal prompt-only flows could fabricate completion evidence. Restrict full
PrecmdValueto the parser/TerminalModelboundary and usePromptMetadatafor block mutation, prompt caching, restoration, and prompt-only tests. - Incomplete key-value hooks could appear valid through defaults. Never treat defaulted
CompletionMetadataas evidence; the decoder/builder must require both completion fields together. - Sharer/viewer block sequences could diverge. Both parse the same raw shell-generated next block ID from
Precmdcompletion metadata; neither generates a recovery ID. - Client-version skew could still diverge during an exceptional sequence. Preserve normal prompt application for a new viewer of an old sharer, but never let prompt-only evidence complete or advance a block. Accept the rare exceptional divergence rather than emulating unsafe old behavior.
- Too many old sharers could limit recovery from
Precmdcompletion metadata. Ship the protocol-only change to stable first and wait through two additional stable releases before enabling recovery actions in production, reducing the active old-sharer population before production Warp relies on the new evidence. - Unexpected transition volume could hide a producer regression. Dogfood rollout and rate-limited structured telemetry provide visibility before broader enablement.
Follow-ups
- Add a finished-block or prompt-cycle identity if the next-block identity proves insufficient to reject all stale hooks.
- Enforce explicit receiving/frozen content states for prompt/output grids.
- Make PTY-reader teardown panic-safe and separate abnormal model sealing from normal completion.
- Remove
TerminalLifecycleRecoveryafter stable release rollout.