Repository navigation
Conversation
14854f9 to
c4103da
Compare
Design Review (Opus 5.5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Nothing blocks: the feature is off by default and capped by a per-chat budget, and it resumes through the same Notes
[DESIGN-REVIEWED] 1740c8a Description read[DESCRIPTION-READ] 310bb0ff3e977b6999751cf922307615719a749b61b1042f2edb9c1e1c7612ff That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
UX Review (Opus 5.5, fork) — ✅ PASSUX-level review of UX-Verdict: PASS Nothing blocks: the 18 stills and one recording show every control and state this PR adds or changes. This fork lane has no blind read, so nobody has yet checked whether a first-time user understands them; pushing the branch to this repository runs that check. Notes
[UX-REVIEWED] 1740c8a Description read[DESCRIPTION-READ] cd58e98a678d8814059467e17aa0a92ea4c82dd2419a6d8b629ee4c0e915b271 That sha256 names everything this verdict read: the PR description, plus the 21 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
First Principles Review (Opus 5.5, fork) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks: the feature is a new switch that ships off, and the one default-on change fixes a quoting defect that a test pins. One gated add-on goes beyond the Goal. Notes
What this change shipsInventory (10 items) — 9 justifiedIntent: a person who leaves a long task running gets it continued after a restart or a retryable error without having to press Resume. This is an ADDITION, plus one fix.
[FIRST-PRINCIPLES-REVIEWED] 1740c8a Description read[DESCRIPTION-READ] 5f28a9e347b04b2cb70395e43f61f597bbaee85400530b8da8b9ee8869d2896d That sha256 names the PR description this verdict read. Recompute it with |
Opus 5.5 Review (fork) — ✅ no blocking findingsReviewed |
GPT 6.1 Review (fork) — ✅ no blocking findings (all downgraded on adjudication)Reviewed Opus 5.5 adjudication downgraded every blocking finding. Downgraded: the remedy each one needs is disproportionate to the harm it prevents. Adjudication downgraded all 1 blocking finding(s): the remedy each one requires is disproportionate to the harm it prevents. Review detailsBLOCKING -- src/kiro_crew/dashboard/restart_resume.py:968 -- Stop tracking starts after the failed-session teardown gap
Adjudication (Opus 5.5) — is blocking on each finding proportionate?The single finding, F1, is downgraded: it no longer blocks the merge. F1: LOW.
|
c4103da to
6d78286
Compare
1a0d251 to
5ee961f
Compare
5ee961f to
449c4db
Compare
46d3422 to
9d78b06
Compare
9d78b06 to
82df174
Compare
82df174 to
367b33e
Compare
367b33e to
f2e6fbe
Compare
f2e6fbe to
747dd00
Compare
|
HOLD on 391f8d8. The branch does not merge into main, and a human needs to review it.
The design reads carefully. It is off by default, the budget is written before dispatch and is fail-closed, and a Stop or a person's send cancels an armed retry. CI is green. — crew-pr-reviewer (r114) |
|
Answering the HOLD on 391f8d8, item by item. The new head is b26cc65. Conflict. Rebased onto main at 6432cad; both files ( Reserved for human review. Agreed that a maintainer signs off; nothing here pre-empts that. One correction to the reading: Maintainer decision (RFC §9). Still UX CONCERNS (two copy fixes). Both taken in this head: the window row's description now opens with the rule (At most 5 automatic resumes per chat in any 1h.) instead of a back-reference, and the first warning bullet is split so the irreversible risk (a half-applied edit, a message sent twice) and the routine spend are separate bullets. Regression tests were proven red on the previous catalog; stills in the body are recaptured. First Principles CONCERNS ( Opus advisory (helpers in |
Security Scope Review (fork) — 🔴 BLOCK (blocking)Which legitimate operations does the review scope was never resolved: the generate job did not finish (it timed out, failed or was cancelled), so no leg ran; its log names the step that stopped No |
…dget Propose an off-by-default dashboard setting that presses Resume on the person's behalf after a gateway restart and after a retryable terminal error, through the same continue_slot_turn path the button uses, with exponential backoff and a per-chat budget that survives a restart. Permissions are told, not restored: the metadata line records the approval mode the chat last held, and a chat that comes back holding a different one gets a system notice before any automatic resume runs. Nothing re-applies or persists a grant across a restart; which grants survive which restarts stays with rfc-crash-only-session-trust and the decision requested on kirodotdev#12275.
A new dashboard setting, off by default, makes the gateway press Resume on the person's behalf: after a restart, for every restored chat whose turn the previous process never finished, and after a turn ends in an error a retry can plausibly fix, with a backoff of 30 s doubling to 10 min. Both passes go through `continue_slot_turn`, so every state the Resume button refuses is refused here too, the row lands as the same `inject` the button lands (its own prefix line, so the card and the model know nobody pressed anything), and the SEL `dashboard_continue` line names the module. Per chat, `dashboard.resume_retry_limit` automatic resumes per `dashboard.resume_retry_window_minutes` (5 per 60 by default), counted across restart and error resumes and persisted on the metadata line so a crash loop cannot replay the resume on every boot; on exhaustion one notice and one notification, and the Resume button is back. A person's send or Resume press resets the count and drops an armed retry; so do a Stop and a close. Never resumed: a turn the time limit ended, a Stop card, a closed chat, a chat an app owns, and any terminal kind not on the retryable list. Permissions are told, not restored. The metadata line records the approval mode the chat held at its last save; with the switch on, the restore compares it with the mode the chat came back holding and, when they differ, lands one system notice saying so before any automatic resume runs. The notice is behind the switch like the passes, so a gateway that never opts in sees nothing new at a restart; the record is kept either way. A crewmate thread whose profile declares the recorded mode and an app-owned chat are not told: their grant comes back by its own mechanism. Nothing is read back into the trust flags (`test_trust_flags_not_restored` is unchanged). Both automatic bodies join the runner's interrupted-turn restore, so on a session the turn natively resumed the lost request is re-injected for an automatic resume as it is for a press, with the gateway's own system notices left out of what the model is shown as its partial answer. That last part fixes a defect main has today, for the manual Resume too: the compact-after- turn notice the ladder writes into a running turn was quoted back to the model as its own words. The persisted budget is read fail-closed in every case, including an integer too large for a float: the read never raises inside the slot restore, so a tampered line refuses the automation for one window and the chat still opens. A terminal error row is stated as settled -- cause named, instruction dropped, danger tint gone -- only when the gateway's own automatic resume follows it in the transcript, never because its Resume button is missing: a row the person typed past has no button either, and its work was never picked up. A person's own Resume press leaves the row as it has always been, so a dashboard with the switch off reads exactly as before. The stamp a resume spends is written to the metadata line, and the write awaited, before the resume is queued; a write that did not complete refuses the resume, so no automatic provider work runs on a budget that exists only in memory. A person's reset is carried by their own turn's marker as the empty list. The permissions notice pass and the resume pass run in one background task behind the listen, notice first: each loads transcripts the restore deferred, one per chat concerned, so neither belongs on the path the ready signal waits on. The retryable set is closed on structural kinds. The runner's own "please retry" rows are stamped retry_invited by their writers; an error row with no kind, or a kind off the list, is never retried unattended, whatever its words say. Settings -> Chat -> Sessions gains the switch, the per-chat limit and the window; `RecoveryCard` draws the two automatic-resume kinds with the cause and the attempt count the dispatcher stamped on the row. The RFC that records the decision travels in this change (`rfc-restart-resume`, draft). Part of kirodotdev#13838 (the restart half, widened from unattended-only to a setting). Part of kirodotdev#12275 (its fallback half: the chat is told when its mode changed).
|
HOLD at 1740c8a. This PR is reserved for a maintainer. The gateway dispatches turns with nobody present (continue_turn.py#L292-L306 decides whose Continue resets the budget), the approval-mode handler gains bookkeeping (slot_approval.py#L446-L450), and the Stop and teardown paths change. The conflict and the r114 copy and placement items are fixed, and checks are green. Open on this head, with no reply:
What clears it: a rebase, replies to the three AI notes, and a maintainer's review of the unattended dispatch, approval-mode and Stop paths, including the RFC decision. — crew-pr-reviewer (r126) |
Problem / Motivation
Goal: A chat whose turn the app cut off — by a restart, or by an error a retry can fix — picks that turn back up on its own, within a budget the person sets, and a chat that came back with different permissions says so. One defect on
mainis fixed on the way because every automatic resume runs through it: the turn a natively resumed session is handed back quotes the gateway's own mid-turn notices as the model's words (shown below with a test that fails onmain).Today a gateway restart ends every in-flight turn and leaves each chat on "This turn was interrupted when the app restarted. Review the partial output above, then resume to continue." Nothing presses Resume:
_reconcile_local_turn_markerlands that row and the only continuation is a person's click (or thesession_retry/session_continuetools). The in-turn retry ladder — transient 5xx retries, the 300 s connection-recovery window, the stall recovery — ends at "Connection unstable — please try again." and stops there too. A person who starts a long task, leaves, and comes back after a restart or a dropped connection finds the chat exactly where it died, hours ago, waiting for a click.Permissions make the same situation quieter. A Trust picked in the composer lives in memory (deliberately:
test_trust_flags_not_restored). After a restart the chat is back on Ask every time, and the first thing the person learns about it is a stalled approval card — or nothing, if nobody is watching. #12275 describes the stall;rfc-crash-only-session-trust/ #15894 are the maintainers' direction on which grants should survive which restarts. Nothing in that direction tells the chat it changed.Why it matters
Unattended work is the point of a gateway that runs while the person is away: armed loops, conductors, a long refactor started before lunch. Every restart (an update, a crash, a
kirocrew restart) turns each of those into a chat that needs a human click, and the person pays twice — the lost hours, and the search through the sidebar for which chats stopped. A per-chat budget with notices costs model turns while nobody is watching, so it is off by default and bounded; the permissions notice sits behind the same switch, so a gateway that never opts in sees nothing new at a restart.Not a goal
session_retry/session_continuetools send. (One shared change on the natively-resumed path, for every continuation that gets the restored-turn quote, the manual Resume included: the quote leaves the gateway's own system notices out. That fixes a defectmainhas today — a compaction notice the ladder writes mid-turn is quoted back to the model as its own partial answer — and the test that shows it fails onmain; see "What a natively resumed session is shown" under Tests.)Backwards compatibility
Compatible. Three new optional config keys with defaults; a gateway that never writes them behaves as before. The two new
SYSTEM_NOTICE_KINDSentries (approval_mode_dropped,auto_resume_paused) are read by everyis_system_noticecaller the same way the existingcompaction/session_reloadkinds are — the last-real-message scans (is_turn_interrupted,_newest_error_row,_pause_notice_stands), the crewmate-chat row filter,interrupted_turn_rows, andNoticeCardon the frontend — so a notice is looked through, never mistaken for a reply.TURN_FAILED_METAmoved tochat_utils_injected_rows.py(read through thechat_utilsfacade);chat_turn/queue_cycle.py, which writes the row, imports it from there and the runner reads it through that owner as onmain, so its one existing reader (the OpenAI-compatible endpoint'sdonehandling, perlearn-cron-dashboard.md) is unchanged. Two new slot-owned metadata-line keys (auto_resume_stamps,approval_mode_seen): an older line reads back as an empty budget and no record (no notice); a full line write for a chat with no automatic resume and no grant omits both keys, so that line is byte-for-byte as today, while the metadata merge path (a window-less save, goldenempty_window_merge) always writes"auto_resume_stamps": []and"approval_mode_seen": "normal"-- a merge cannot delete a key, and a revoke must overwrite thetrustan earlier line carries (Design review rev 41); and a new line read by older code is ignored, both keys included. The runner's terminal "please retry" rows now carrymeta.kind = retry_invited; no frontend reader keys on an error row's kind except the two interruption kinds andtransient_retry, so a tagged row renders exactly as before, and an untagged one an older gateway persisted simply is not auto-retried.continue_slot_turn(body=None)andparseRecoveryMessage(content, meta?)keep their existing call shapes. Nothing is renamed, removed or rejected thatmainaccepts today.Decision for the owner
The RFC is
draft; a maintainer accepting it (its §9) or overriding is part of the merge decision, and nothing here pre-empts that. Two choices a reviewer may want to overrule, stated here so they are not read as oversights. (1) The boot pass resumes a chat whose Trust ended at the restart. Its first tool call then parks at an approval card, and one budget stamp is spent — but the alternative, skipping that chat, leaves the person the same approval card one Resume press later with none of the pre-approval work done; resuming is strictly more progress, so the chat is resumed and the permissions notice says why the card appeared. (2) The retryable list is closed on structural kinds: the runner's own "please retry" rows are taggedretry_invitedat their writers, and an untagged terminal error is not retried until someone tags it. The rows retried today are unchanged; what changes is that a writer added later is not retried by accident.The RFC ships in this PR as
draftrather than landing first (one PR was the request). Nothing here is on by default — the switch gates both passes and the permissions notice alike, so a gateway that does not opt in behaves as it does today; what remains for the First Principles lane is the RFC's acceptance (its §9), yours to record, or I split the document out. One more call is yours: the UX lane asks for an inline Restore Trust action on the permissions notice. The RFC's §4.6 is "tell, do not restore" — the notice names the composer picker because that control carries the grant's own confirmation, and a one-press re-grant from a system row would be a second place a Trust is handed out; I have not added it. The First Principles lane's remaining ask has narrowed to the window select alone (resume_retry_window_minutes; it now counts the limit as justified) — the same call, smaller. The Design lane asks that the error pass keeping a chat's held Trust be stated as an accepted trade-off or removed: it is now stated as one (RFC §4.6, with the alternative — skipping, or resuming in Ask, a Trusted chat — rejected in §6, and a pin test holding the pass to changing a chat's mode in neither direction). The reasoning is the person's: a Trust picked in the composer is their standing instruction for that chat, and a dropped connection says nothing about whether they still mean it; the flip, if you want it, is one condition in the error pass.What changed (motivation → approach → change)
Decision record.
docs/request-for-change/rfc-restart-resume.md(statusdraft) records the design and travels in this PR as asked. The switch is off by default and gates everything here, the permissions notice included (RFC §4.1, §4.6): a gateway that does not opt in gets no automatic resume and no new row at restart. Whether that notice should reach every gateway — a chat silently losing its Trust at a restart is a defect whether or not anyone pays for automatic resumes — is a product-shape decision this PR leaves to the maintainers (RFC §9 Q6); the record it reads (approval_mode_seen) is written either way — deliberately not gated by the switch, since a record of what the person last saw is honest only if it tracks every pick and revoke: a writer paused while the switch was off would leave the last grant on the line through the merge form (a merge cannot delete a key), and the first boot after the switch came back on would report a Trust dropped that the person had revoked themselves — and its cost is one short key on the slot lines of a gateway that never opts in — on every line the merge form saves (normalwhere no grant was held, since a merge cannot omit a key) and, on a full-line save, only for a chat that held a Trust — read by nothing but the notice and reversible by deleting it (RFC §9 Q6 states it; Design review, rev 42), so taking that decision later needs no migration.Approach: press the button the person would press, nothing more. Both passes dispatch through
continue_slot_turn(chat_api/continue_turn.py, behind thechat_handlersfacade), which gains an optionalbodyand the existingvia; every automatic dispatch passesrequire_interrupted=True. So every state the Resume button refuses (running, queued, approval pending, children running, two failed starts, a turn that finished meanwhile) is refused here too, the row lands as the sameinjectthe button lands, the turn runs with no human-origin flag and no app or cron identity, and the SELdashboard_continueline carriesvia=restart_resumeorvia=error_resumeso the audit trail tells an automatic resume from a press. The inject's first line is its own —[Continue — automatic after a restart]/[Continue — automatic after an error]— so the model does not read a press that never happened as an answer, and the card does not say "requested by the user". Both bodies join_INTERRUPTED_TURN_RESUME_MSGS(chat_utils_turn_recovery.py, read through thechat_utilsfacade), the one set_run_chatgates its interrupted-turn restore on: on a session the turn natively resumed, kiro-cli's log lacks the turn the process died on, and the runner re-injects it from the slot window for an automatic resume exactly as it does for a press; the rows it hands the builder come fromchat_utils.interrupted_turn_rows, which leaves the gateway's own system notices out, since they sit in the span quoted as the model's partial answer. That span already carries such rows onmain:chat_turn.recoverywrites the will compact after this turn notice into a running turn (kind: compaction), so a process that dies on that turn leaves it between the request and the resume row, and a plain Resume press today quotes it to the model as what the model had said.Backend:
dashboard/restart_resume.py(new). Off the gateway boot path (AUTOSDEno-new-work-on-gateway-boot-path, GPT review rev 27): neither facade (server,chat_handlers) imports the dispatcher at module level; the four request-time callers (a send, a Resume, a Stop, a close) import it on use, after the socket is bound; the restore imports it only with the switch on, right where it schedules the boot pass (both passes return at once without the switch, so the gate changes nothing they did); and the per-slot metadata read validates the budget list throughdashboard/auto_resume_stamps.py, a validator beside the codec (re-exported by the dispatcher), so a default-off launch with sessions to restore loads nothing of it. Proven by a subprocess test that imports both facades and applies a tampered line through the codec, red before the gate ("a facade imports the dispatcher"), and by a restore test with the switch off that reaches no dispatcher call.resume_after_restart): run by the one background taskstart_boot_passcreates from_restore_dashboard_sessionsafter both restores and the counter reseed — the permissions notice pass first, then this — so the server's listen andKIROCREW_READYwait on neither (each loads transcripts the restore deferred, one per chat concerned). The restore keeps the turn-in-flight marker's presence on the slot for this process only (_restart_cut_turn, set from the metadata-line read and confirmed bysettle); the pass looks only at flagged slots, loads each one's deferred window off the loop, and resumes those whose newest turn reads as interrupted and ended in no error row or in thegateway_restart_interruptionrow (restart_candidate). That rule is what makes a turn cut before any reply ([user],[user, tool…]) or a cut recovery turn resume, and what keeps an interruption row an earlier boot left from being resumed on a later one; the in-turn retry ladder's pending row (transient_retry, a recovery queued, not an ending) is looked through, so a restart during the ladder's wait — the common case of a restart in a provider outage — is resumed too. Neither pass touches a chat an app owns (slot._app): its app re-grants its own trust and resumes on its own terms. No other driver needs excluding (Design review, rev 25): every continuation of a dashboard chat — the button, an app's Continue, an agent'ssession_retry/session_continue, the two passes — goes throughcontinue_slot_turn, which refuses a running or queued chat and a turn that is no longer interrupted, and the error pass re-reads the tail right before it dispatches and takes its stamp back on a refusal, so of two drivers aiming at the same cut-off turn the second is refused and the work runs once; a cron or monitor loop queues its next turn rather than re-sending the failed one, and sub-agent runs are not dashboard turns (they run insubagent_manager, never reach_finish_queue_cycle, and are not restored slots), so neither pass can reach a conductor's worker. It waits for the cautious-boot pause and a 5 s settle before the first dispatch, 2 s between chats, re-reads the switch from the live snapshot before each dispatch (turning it off during the pause ends the pass), logs and skips one chat's failure rather than ending the pass, and skips a chat the person touched first — a send or Resume clears the marker flag throughreset_budget, and a Stop or a close clears it throughcancel_armed_retrywithout touching the spent budget, so a Stop on the idle restored chat, which writes no Stop card (the route's no-op branch), still ends the resume (GPT review rev 40: the earliercancel_armed_retryonly moved the epoch, which the pass captured after the Stop had bumped it). The classifier reads turn openers the wayis_turn_interrupteddoes — auserornudgerow, or aninjectwhoseinjectKinddispatched a turn — so a held note the runner flushes after an error (an untaggedinject) is looked through rather than read as a new turn that swallowed the error.schedule_after_error): called once per local turn end from_finish_queue_cycle(chat_turn/queue_cycle.py, run on the runner's namespace like every owner) when the slot is idle. One accepted gap, recorded in RFC §4.4 and pinned bytest_a_restart_during_the_backoff_loses_the_armed_retry(Design review, rev 29): the armed retry is a task in the running process, so a restart inside the backoff drops it — the chat comes back on its error row with no marker, the boot pass does not look at it, and the person finds "please retry" exactly as without the switch; no stamp was spent (it is written at dispatch). Closing it would need a persisted pending-retry record whose only consumer is that coincidence and whose stale-record failure mode is worse than the gap. A turn whose newest row is a retryable terminal error —transient_give_up,session_start_failed, orretry_invited— arms one retry aftermin(30 s · 2^(n−1), 10 min)+ 25 % jitter; nothing is armed while the switch is off in the process's config snapshot, and the switch is read again after the backoff so turning it off during the wait stops the retry. The retryable set is a closed allowlist of structural kinds (RETRYABLE_ERROR_KINDS) with no "everything else" entry: the runner's own "please retry" rows (connection lost, session busy, turn or tool stalled, a shared agent process that kept restarting) are stampedretry_invitedby their writers inchat_turn/turn_reply_seams.py,chat_turn/acp_recovery.pyandchat_turn/recovery.py, and anerrorrow with no kind or a kind off the list — an approval prompt nobody answered, "Session stuck", an unclassifiedAcpError, a writer added later without the tag — is never retried, nothing being read off the row's words;meta.turn_failedrows are excluded too; and a Stop card anywhere in the newest turn — including the give-up row the ladder writes when a Stop lands during its backoff — is the person's recorded intent that nothing continues on its own. The pass also measures the session's Stop count it reads at arming against the one the failed turn read at its entry (_turn_openleaves it on the slot as_turn_entry_stop_count): the pass is scheduled from the cycle end, several awaits after the turn released its session permit, and a linked-channel!stoplanding in that gap moves the counter while the session is still live and writes nothing on the slot — read only at arming it passed as the baseline, so the stopped turn was resumed (GPT review, rev 65). When the count moved, nothing is armed.resume_retry_limitperresume_retry_window_minutesper chat, restart and error resumes counted together, stamps persisted on the metadata line (auto_resume_stamps) so a gateway that restarts, resumes, dies on the resumed turn and restarts again still counts the earlier ones. The stamp a resume spends is committed first: the dispatcher appends it and awaits a fenced metadata merge of the list (_commit_budget, the turn marker's own merge-and-guard, registered with the close's wait), and only a completed write lets the dispatch reachcontinue_slot_turn— no log, a closing chat, a missing line, a refused guard or a failed write refuses the resume and takes the stamp back in memory, so no automatic provider work runs on a budget that exists only in memory. On exhaustion: oneauto_resume_pausedsystem notice (idempotent per budget — nothing is added while a pause notice stands with no automatic resume after it in the transcript, so neither a crash loop nor an unattended turn that keeps failing stacks rows), onestate.notify, SELrestart_resume:budget_exhausted, and the Resume button, which was never removed. A person's send (api_chat, not an app's or a cron's) or Resume press (continue_slot_turnwith human origin and no body) resets the count and drops an armed retry; an agent'ssession_retry/session_continueand an app's Continue leave the budget alone, since it bounds exactly what no person asked for. A Stop (api_chat_stop) and a close (slot_lifecycle) drop an armed retry too. Each of those moves a per-slot epoch that the dispatch re-reads under the slot lock right before its queue insert (before_dispatch), so a cancel that lands after the backoff but before the insert refuses the dispatch instead of leaving a gateway-authored entry at the head of the queue; a refusal there, or any ofcontinue_slot_turn's own, takes the stamp back in memory, and the line keeps it until the next save, so a crash in that gap counts one resume that never ran (fail closed). A person's reset is carried by their own turn's marker as the empty list (_auto_resume_reset_pending), so a crash before the periodic flush does not read the spent budget back off the line.read_persisted_stampsre-checks every element; a list that is not a list of finite, non-negative, non-future numbers withinMAX_STAMPSreads as FULL (64 stamps) — the automation refuses, the manual Resume is untouched, a warning names the chat, and the pause notice states only the pause (is paused here) and leads to the remedy — the cause is the operator's, in the codec's warning, not something the person can act on (UX review, rev 26), and no duration is named, since the window aging out re-allows automatic resumes only for later interruptions and nothing re-arms this chat's turn, so for 1h read as a promise to pick it up in an hour (UX review, rev 29) — rather than quoting the sentinel count; the sentinel is told apart by its size (the gateway never appends past the limit, whose ceiling of 20 is far below 64), so a limit the person lowered below the stamps already in the window is quoted as it is._begin_local_turn_markercarries the budget list on its awaited merge when there is one, and its fallback full save — taken only when the metadata line is missing — is no longer best-effort, for every chat (First Principles review, rev 64, undeclared until now): the verdict that marks a person's reset as on disk needs a truthful answer, and a best-effort save answers true to a write that failed. For a chat that carries no budget the outcome is what it was — a failure is logged, re-arms the flush and still admits the turn (the caller does what the best-effort save did) — so a gateway that never opts in sees a different log line and nothing else;TestMarkerFallbackSavedrives it with no switch set.Backend: tell about permissions, do not restore them. The metadata codec gains
approval_mode_seen(trust|trust_reads) — named for what it is, a record of what the person last saw, never a setting — written by every save from the live flags via one pure derivation (approval_mode_record.live_approval_mode, a sibling module beside the codec's facade like the stamp validator — the codec was already 1,700 lines and these readers are a job of their own (Opus review, rev 33); the codec re-exports them for its line writer): the full-line save omits the default, so a chat nobody picked a mode for carries nothing extra and the existing metadata goldens are byte-identical, while the merge (which cannot delete a key) writesnormalso a revoke overwrites an earlier grant. It is read back into_approval_mode_recordedonly, by the read and by nothing else — no save writer touches that field and no reader touches_trust/_trust_reads. In the boot task, ahead of the resume pass and behind the listen — and, like the resume pass, only while the switch is on —notice_dropped_approval_modescompares each slot's recorded mode with the one it holds and, when a grant is gone, lands oneapproval_mode_droppedsystem notice (🔒 This chat's permissions changed when the app restarted: it was on Trust … and is now on Ask every time …), before the boot pass so no automatic resume runs under a mode the person has not been told about — and as a live append, like the pause notice's, so a tab that reconnected before the boot task ran sees it on the wire ahead of the resumed turn, not on its next transcript reload (Opus review, rev 36: the notice had used the replay-mode append, so a connected dashboard saw the "Resumed automatically" card and its approval card without the row saying why; proven red on the old append, which reached no connected client). Nothing is said where the grant comes back by its own mechanism with no person to act: a mate thread whose recorded mode is the one its profile declares (an unset mode declares Trust), because_seed_member_approvalre-applies it when the thread is opened — unless the boot pass may resume that thread first (switch on, turn cut), since the resumed turn would run under the mode the thread holds now; a thread that held a grant its profile does not declare (an approval card's "trust this session"), or whose mate record is gone, is always told, with a remedy that names the Mates page and the profile rather than the Approval mode control the Mates chat does not have (the ordinary chat's remedy names that control as the screen labels it — To turn it back on, choose Trust in Approval mode under the message box. — not the codebase's "composer"; UX review, rev 29); an app-owned chat, whose app owns the grant and which neither pass resumes; and any chat under the process-globalagent.dangerously_skip_permissions, where nothing asks. Two details found in review of this PR's own code: the interruption row makes a cut-off chat dirty, so the 5 s flush can save it during a long restore — the save must therefore not refresh the in-memory record, or the notice compares against the wrong mode (pinned end to end through the real save); and a Trust pick on an idle chat now dirties the chats whose mode moved (approval_modes/mark_changed_approval_modesinapply_approval_mode), because a pick appends no row and the flush skips a slot that is not dirty. The mate seed is left alone: re-opening a bound thread is a no-op by contract, and the seeded mode reaches the line with the thread's next save.Config.
DashboardConfiggainsresume_interrupted_turns(bool, default false),resume_retry_limit(1–20, default 5) andresume_retry_window_minutes(5–1440, default 60);RESUME_RETRY_*inconfig/sections_limits.pyare the bounds the loader andPUT /api/dashboard/configclamp to (type-checked; 400 with acodeon a non-boolean, a non-integer, a fraction or a JSONInfinity), mirrored by the Settings controls.config-baseline.jsonregenerated.Frontend. Settings → Chat → Sessions: the switch under Restore Sessions, with the per-chat limit (Max automatic resumes per chat, stepper 1–20) and the window (Resume limit window, select 5m … 24h) shown while it is on; the window row's always-visible description states the two as one rule with the live values — At most 5 automatic resumes per chat in any 1 hour. — since a "5" and a "1h" read apart say nothing (UX review, rev 25), spelling the window out while the select keeps its 1h (UX review, rev 34: inside prose the abbreviation reads like a typo;
resumeWindowProse, Intl long units, so each locale gets its own word and plural), opening with the rule rather than Together with the limit above: (UX review, rev 33: a reader landing on the row from the palette met the pointer before the rule); it is a plural catalog key (resume_retry_limit_summary, registered inpluralKeys.json) and names the window with the select's own label (resumeWindowLabel, falling back to a localized number of minutes for a hand-edited value the select does not offer); the window row carries no ⓘ tip, since the one it had only restated that sentence (UX review, rev 26); its second sentence — At the limit the chat pauses and asks you to press Resume, which resets the count. — moved there from the limit stepper's ⓘ tip (UX review, rev 42: a person choosing the number needs it before choosing, and a tip is read after), so the tip keeps only what the sentence does not carry, that restart and error resumes are counted together.RecoveryCarddraws the two new kinds as Resumed automatically · after the app restarted / after an error ended the turn · attempt n of limit, reading the count the dispatcher stamped onmeta.autoResumeand dropping it when the stamp is absent or malformed;parseRecoveryMessagegains an optionalmetaargument, so the callers that have no row to hand over are unchanged. The two notice kinds joinSYSTEM_NOTICE_KINDSon both sides so every last-real-message scan looks through them; they render through the existingNoticeCard.ErrorCardgainsresumed, which the two error renderers set from the rows BELOW the row (automaticResumeFollowsinRecoveryCard: the next row that bears on the turn is anauto_restart/auto_errorcontinuation — never inferred from the missing Resume button, which a row the person typed past lacks too): the restart row and a resumed give-up or "please retry" row keep their cause and drop the instruction ("This turn was interrupted when the app restarted. Its partial output is above.", "The connection was unstable." — the restart row alone keeps the pointer its wire text had, since that output may be work the resumed turn repeats; UX review, rev 42) and lose the danger tint, since the Resumed automatically card directly under them says the rest — one fact, said once. A person's own Resume press is deliberately not a case — the row it follows keeps the rendering it has always had — and every other row keeps its wire text. New i18n keys underpages.chat.recoveryCard,pages.chat.errorCardandpages.settings.chatPanel, fanned out to every locale; the command-palette settings registry is regenerated.Docs.
docs/system-specs/modules/history.md(restore gains the two passes, the metadata line gains the two keys),docs/system-specs/modules/config.md(the three keys),docs/system-specs/modules/session.md(the interrupted-turn restore names the shared set),docs/system-specs/common/error-handling.md(the closed retry list and the tag-at-the-writer rule, so a later "please retry" row is not left out of retries unknowingly -- Design, rev 44),docs/feature-map/README.md(one row).Rebased onto
3d09514228. Upstream splitchat_handlers.pyintochat_api/owners andChatPanel.tsxintochatPanelOptions.ts/useChatPanelDashboard.tsmeanwhile; the hooks live in the owners now (chat_send,continue_turn,turn_stop,slot_approval,slot_lifecycle), readrestart_resumethrough the facade binding the composition contract prescribes, and the pinnedcontinue_slot_turnsignature intest_chat_handlers_composition_contract.pycarriesbody.Tests
Backend (
test/test_restart_resume.py, 111 tests,test_restart_resume_boot_path.py, 3, andtest_restart_resume_budget.py, plustest_slot_metadata_codec.pygolden and the two composition contracts):test_chat_runner_turn_error_arms.pynow declares whether the error pass resumes its last row, and a pin holds each declaration toRETRYABLE_ERROR_KINDS, so a new arm has to choose and a kind moved into or out of the set shows up as the arms it moves (Design review, rev 64); the restart row is a restart candidate and not an error one; a cut turn the marker names qualifies whatever its tail ([user],[user, tool], a cut recovery turn); an interruption row an earlier boot left is not this boot's; a marked turn that ended in another error was failing before the restart; each retryable tail (transient_give_up,session_start_failed,retry_invited) is retried; each never-retried kind (turn timeout, auth, entitlement, usage limit, context overflow, app-agent refusal, chat-close interruption, and a kind that does not exist yet) is not; a row off the list is never retried whatever its words — approval timeout, approval no-budget, session stuck,turn_failed, an untagged "please retry" row an older gateway persisted, a "please retry" row under an unknown kind, an unclassified error (proven red on the old code, which retried the untagged ones) — and the list is pinned to name structural kinds only;test_chat_runner_turn_error_arms.pypins that the shared-process-exhausted and nested-prompt-busy arms stampretry_invitedon their terminal row; a clean completion, a Stop card, a give-up row written after a Stop -- including one the ladder markedstoppedwith no Stop card on the slot, the shape a Stop from a linked channel leaves -- an older error behind a finished turn are left alone; a restricted (incognito/temporary) chat is never eligible; a held note flushed after the error does not hide it, an untagged inject after an old error does not make a restart candidate (proven red on the old opener rule), a dispatching inject does open a turn; a restart during the ladder's wait is this restart's (the pendingtransient_retryrow, bare and after auser_replay; proven red); an app-owned chat is never eligible.[](a chat that never had an automatic resume still writes its old bytes); a reset that lands while a stamp's write is in flight survives it — the write is fenced to the list it carries, decided under the line's lock, so a reset that landed first refuses the write and the resume, and a reset that landed after it leaves the flag pending for the person's turn marker to carry, in_commit_budgetand in the marker alike (four tests, proven red on the old code, where the older write cleared the flag and the person's turn never wrote the empty list); the marker clears the flag only after a confirmed write — when the merge found no line, its forced-save fallback is judged by its result, not best-effort, and a fallback that failed or was refused leaves the reset pending and re-arms the flush (proven red on the old code, which cleared the flag over a best-effort save that had swallowed its failure); stamps are written to the line and read back; a malformed, oversize, non-finite or future list reads as FULL and invalid; an integer too large for a float reads as FULL instead of raising, and a tampered line still lets the chat open through the real codec read (proven red:OverflowErrorat the conversion on the old code); a malformed line is named in the log; the window prunes and a stamp exactly one window old still counts; a person's reset clears stamps, cancels the armed retry and moves the epoch; the backoff curve; settings floor at the config bounds.continue_slot_turnfor an interrupted turn.assert True is False, the retry was armed); an unchanged count arms as before, and a turn that never reached its entry snapshot (no reading on the slot) compares nothing and arms as before; every turn-error arm leaves the entry reading on the slot (proven red on the rev-65 code:None == 0); arms one backed-off retry (held inside its backoff by a seam, no wall-clock sleep) and dispatches it; switch off in the snapshot arms nothing; switch off on disk arms but dispatches nothing; switching off during the backoff stops the retry; a never-retry kind arms nothing; a send during the backoff cancels the retry; a person's action after the backoff refuses the dispatch under the lock; the switch turned off while the budget write was in flight refuses the insert under the lock too and takes the stamp back (GPT review, rev 45: the pre-insert hook read only the epoch, which a setting change does not move, so a resume could start after the operator had switched the feature off -- proven red on the old hook); a Stop issued on a linked channel during the ladder's backoff ends the matter: the give-up writers mark the rowstoppedwhen the stop signal caused it and the pass refuses the marked row (Opus review, rev 46: such a Stop moves the session's stop counter alone -- no Stop card on the slot -- so the pass saw a plain give-up row and resumed the turn the person had just stopped; the mark is proven on the pre-token replay arm, the post-token continue arm and the Stop-already-active path for a dashboard Stop, a resolved Stop and a channel Stop, and absent for a steer or a follow-up; the classifier's refusal proven red on the unmarked row); a channel Stop against the idle chat while a resume is pending drops it: both passes record the session's Stop count when the resume is armed (per candidate, for the boot pass), drop it when the count moved by the end of the wait, and the pre-insert hook compares it a last time under the lock (GPT review, rev 47: Slack's!stopon an idle chat moves that count and nothing on the slot -- no Stop card, no epoch, no cancel -- so the armed retry resumed the work the person had just stopped; three tests proven red: the backoff, the under-lock hook, the boot settle); a Stop that lands while a restored chat's window is still loading is kept: the codec's second reading of the restart marker combines with the flag a Stop cleared instead of assigning over it, and the boot pass reads the session's Stop count before that load (GPT review, rev 48: the deferredsettlewrote_restart_cut_turn = had_markeraftercancel_armed_retryhad cleared it, so a Stop during the worker-thread load was undone and the chat resumed; both proven red); and the session manager now records a Stop for a chat with an automatic resume armed even though it has no live session --SessionManager.arm_resume/disarm_resume(counted, canonical key), called by both passes around their waits;note_stoptreats an armed key like an open replay gap (GPT review, rev 49: a restored chat has no session until its first turn, and a failed turn may have torn its session down, so Slack's!stopon the idle thread recorded nothing, replied "Nothing running.", and the stop-count compare saw no change -- the resume ran; proven red with the real manager on both passes, plus a stop-gate test of the rule); and the restore arms a cut chat's key the moment the open tabs exist (arm_cut_chats, synchronous, once per chat, the reading kept on the slot), withstart_boot_passarming before the boot task exists and disarming when it ends however it ends (GPT review, rev 50: the boot task runs the approval-mode notice pass first, which awaits deferred transcript loads, and the restore itself awaits after the slots exist -- a Slack!stoplanding in either window came before any arming and was dropped; proven red throughstart_boot_passwith the real manager, plus the restore-order pin); the stamp is spent under the lock before the insert (what the turn-start save sees is already 1); at the limit the pass pauses instead. The shared dispatcher stand-in runsbefore_dispatchand lands the row the drain would, so a hook that refused every dispatch would fail these tests; another driver landing first during the backoff (an agent'ssession_retry, a cron's next delivery) leaves nothing for the retry — it re-reads the tail, dispatches nothing and spends nothing (the test fails if that re-read is removed).viato the audit line; a person's Resume resets the budget; an agent's Continue leaves it alone; a superseded dispatch is refused under the lock. Who counts as a person for the budget is the send route's rule on the Continue endpoint too — no app token and no attested cron creator — carried ascontinue_slot_turn's ownresets_budgetargument, separate from the queue entry's provenance (directive_user_originstays the send route'snot request_appon both routes), so a cron's Continue dispatches exactly as its send does and, like it, leaves the budget alone (Opus review, rev 36: the endpoint derived the reset from the app token alone, so an attested internal caller with no app could reset the budget that caps unattended resumes; First Principles review, rev 37: the first fix narrowed the provenance flag instead, which a cron's send does not — the test now pins both halves, proven red on the old route for each: the stamps cleared, and the provenance downgraded).""/normal; no notice when the mode came back; no notice under the global override; the wrapper tells only the chats that changed and loads only their windows; the pass is inert while the switch is off — nothing told, no window loaded, the record kept (proven red on the previous code, which told the chat regardless); a mate thread whose declared mode comes back is not told (unset, declared, and no config alike), one the boot pass may resume first is not told either — turn admission re-applies the declared mode before the resumed turn claims a provider (Opus review, rev 56; proven red on the previous code, which told it "now on Ask every time"), and the exemption is decided from the chat's execution record the way the admission seed decides it, so a mate's worker and a conductor chat outside the thread are not told either while a chat whose record cannot be read is (Opus review, rev 57; proven red on the previous code, which told the worker), one that held a grant its profile does not declare is told, one whose mate record is gone is told, an app-owned chat is not told; both notices write theirapi_accessaudit lines (caller, operation, outcome pinned); a pick dirties the chats whose mode moved; the picker route dirties the chat.Infinity,NaNand a fraction, each with itscode.'normal' == 'trust'); the stamp rides the turn-start save. The two changes that reach a gateway with the switch off are tested with it off (Design review, rev 60):TestMarkerFallbackSavedrives the real_begin_local_turn_markerwith no switch set — a forced save that raised or answered false keeps the reset pending, marks the slot dirty and still returns (the turn is admitted), a confirmed one clears the reset — and the codec'stest_both_save_forms_write_every_slot_owned_keypinsapproval_mode_seenamong the keys both the full save and the merge write._INTERRUPTED_TURN_RESUME_MSGSand the prompt phase's body (_turn_build_prompt, which_run_chatawaits) reads that name rather than a literal;interrupted_turn_rowsdrops the notice and keeps the opener and partial answer as the same row objects, the builder restores the same frame from them, and the prompt phase's body reads that helper's name too (the notice lands in the span the builder quotes as the model's words — pinned as the hazard and the fix). End to end through_run_chaton a natively resumed session with no automatic resume involved (test_dashboard_chat_turn_wiring.py): a plain Resume press on a turn the ladder's compact-after-turn notice sits in hands the model its own words and not the notice — red onmain, where the prompt sent reads… first event at 09:14 / 🔄 Context at 81% (limit 80%) — will compact after this turn. / [END INTERRUPTED TURN].Frontend (
RecoveryCard.test.tsx,ErrorCard.test.tsx,SettingsChatPanelCoverage.test.tsx,CompactionCard.test.tsx,ChatPanel.railRegistry.test.tsx): the Trust bullet's restart sentence is pinned with its mate-chat exception, in the rendered bullet and in the catalog key (proven red on the rev-64 copy, which ended at "waits for you."); the two kinds title as automatic and name their cause; a row is stated as settled only when the gateway's own automatic resume actually followed it (automaticResumeFollows: anauto_restart/auto_errorcontinuation below the row; false for the newest row, for a row the person typed past, for a person's own Resume press, for a cron that came next, and for a reply the turn produced with no continuation; a notice, a later error or a steer is walked past) — the interruption rows drop their "then resume to continue", the ladder's give-up and "please retry" rows (connection lost, session busy, turn/tool stalled, shared process restarting) state the cause and that the turn was resumed, all in a neutral tone instead of the danger tint; a row with no Resume button is never settled on that alone (proven red on the old code, which rewrote every button-less interruption row — including one the person typed past); the newest row, with Resume offered, keeps the wire text; a row whose words carry no instruction is left verbatim; the stamped attempt count is appended and a malformed stamp is dropped; the manual Continue copy is untouched;resolveInjectCardhands the row meta to the parser; the rows render as routine (no act-required styling); the limit and window hide until the switch is on (both bydata-setting-key, the attributeSettingsFieldrenders); the stepper persists up/down, never writes past 20 and disables − at 1; the window select persists; both notice kinds route toNoticeCard; the registry carries the new rows; both consequences of the switch — the repeated work and the Trust — are the warning the switch is described by, one list item each and each opening with its risk (May repeat work…, Uses model turns and tool calls while you are away., After an error, the resumed turn keeps the chat's Trust approval mode, so its tools run without asking. After a restart, a resumed turn that needs a tool approval waits for you. — three bullets, the spend split out of the repeat-work bullet and proven red on the rev-33 catalog, which held both in one sentence; proven red on the rev-32 catalog, which said Trust mode; the Trust bullet carrying both halves of the approval rule and the help stopping at the limit, proven red on the rev-35 catalog, where the restart half was the help's second sentence) — the first keeps since nobody reviewed its partial output over the suggested unreviewed (UX review, rev 29): the clause says why the work may repeat, and the one-word form dangles, and its grey help is the plain statement of what it does, with no ⓘ tip on the row (proven red on the previous panel, where the warning was one paragraph of two sentences with the risks buried mid-sentence; earlier, where it carried only the Trust line, the help was the cost paragraph and the feature statement sat behind the ⓘ), and the English catalog keeps the feature statement, the cost and the Trust consequence in three keys with no_hint, the window row has no_hinteither, and the limit's label reads as a maximum (Max automatic resumes per chat); the window row states the limit and the window as one rule with both live values, opening with the rule (At most 5 automatic resumes per chat in any 1 hour., singular at 1, no Together with opener, the window spelled out and the select still 1h — proven red on the rev-33 catalog, re-stated as the limit steps and as a window is picked; an off-list window reads as localized minutes) and the Trust consequence names the mode as the Trust mode picked in the composer (both proven red on the previous panel and catalog). The i18n wire-marker exemption is unchanged (the prefixes have no comma).Manual verification
Rendered the stills and the recording below from synthetic transcripts and fixtures against the built SPA (the repository's transcript and settings harnesses), and read every card. Not performed against a live gateway restart; the restore path is pinned end to end by
TestThroughTheRestorethrough the realrestore_recent_sessionsand_save_slot_to_history.Screenshots / video
Settings → Chat → Sessions, switch off (the default) and on. The copy under the switch is the row's grey help plus a warning block (UX review, revs 22–24: the Trust consequence was first the tail of a paragraph, then a second grey line that read as optional help, then the only warning line while the repeat-work risk stayed grey and the feature statement sat behind the ⓘ). The help now says what the switch does — after a restart, or after an error a retry can fix, the app presses Resume for you, up to the per-chat limit below — always visible, no ⓘ tip on the row. The warning block — the warn hue with the triangle, the shape the Security panel uses for its caveats, and the switch's
aria-describedby, so it is announced before the flip — holds the consequences as three bullets, each opening with its risk (UX review, rev 26: a two-sentence paragraph buried the second risk; rev 33: one 33-word bullet let the half-applied edit hide behind the cost of model turns): the irreversible one first — it may repeat work the cut-off turn had begun (an edit half applied, a message sent), since nobody reviewed its partial output; then the routine spend — it uses model turns and tool calls while nobody is watching; then the limit of unattended work per pass — after an error, the resumed turn keeps the chat's Trust approval mode, so its tools run without asking, because the error pass runs in the same process and inherits the chat's mode (UX review, rev 27: the earlier picked in the composer after an error let "after an error" attach to the picking; rev 32: Trust mode named a control that does not exist — the composer's picker is Approval mode with a Trust segment, which is also how the permissions notice names it). The restart half — a resumed turn that needs a tool approval waits for the person, since the Trust was dropped at the restart — is the Trust bullet's second sentence, the boundary of that risk, rather than a bullet of its own (UX review, rev 28: readers learn to skim amber text, and a reassurance there dilutes the risk beside it) or a sentence of the grey help (UX review, rev 35: a help line stating only the restart half read, skimmed, as every automatic resume stopping at an approval), so the approval rule is read in one place and one style of text. Its one exception is named in the same sentence (Opus review, rev 64): a mate's chat — its thread, a conductor chat or a worker it opened — gets back the approval mode the mate's profile declares, Trust when unset, because turn admission re-seeds it before the resumed turn claims a provider (main's #23010), so a resumed mate turn runs its tools with nobody there; the unqualified sentence promised a wait that does not hold for those chats. The config field's help and RFC §4.1 / §4.6 carry the same exception, and the four Settings stills below are recaptured on that copy. The block is a sibling of the row, not its description, so the row staysSettingsToggleas every other row is and the registry entry carries the feature statement. The limit row is labelled Max automatic resumes per chat, so it reads as a limit and not a count so far (kept over the suggested Maximum — UX review, rev 34 — since the page already has Max Turns per Subagent and Max Concurrent Subagents, so the whole-word premise does not hold); the window row is labelled Resume limit window, a name that stands on its own in the command palette and settings search, its description reads the pair as one rule with the live values, and it has no ⓘ tip (the tip only restated the sentence). It keeps that name rather than Limit resets after (UX review, rev 27): the window slides — each stamp ages out one window after it was spent — so the count never resets at a point in time, and a label promising a reset would describe a timer the budget does not have; nor Count resumes over (UX review, rev 31): the row was Count resumes over the last until rev 25, when the same lane read it as a fragment where the palette lists labels on their own, and two words shorter it is the same fragment — the description under the row already says what the window does (RFC §4.1). The list is painted withtext-warn-text, the token the design system darkens under the light mode, rather than the plain hue (UX review, rev 43: the rows had only been shown in the dark theme): plain--warnon the default light palette's white page is 3.4:1, under the 4.5:1 text floor, and below it in 14 of the 19 built-in light palettes; the token reads 9.5:1 there and clears the floor in every light palette, while in dark palettes it is the plain hue, so the dark frames are unchanged. The test pins the class, and the light frame is shown last below:A turn the restart cut off, resumed automatically. The card reads Resumed automatically · after the app restarted · attempt 1 of 5; the restart row above it keeps its cause only, in the neutral tone:
A turn that ended in the retry ladder's give-up row, resumed after the backoff. The card reads Resumed automatically · after an error ended the turn · attempt 2 of 5:
Recording of the restart case landing live: the red restart row with its Resume button, then the automatic continuation arrives (the row turns neutral and keeps only its cause, the Resumed automatically card lands under it), then the reply. Synthetic data; the person pressed nothing:
resume-after-restart-live.mp4
The row settles in place: the live row and the settled row are one DOM node (the card's root div is kept across the re-render), so its tint now transitions (
transition-colors duration-200, off under reduced motion) from the danger surface to the muted one instead of cutting; the text itself cuts, since a cross-fade would double the row's height for its duration. The two stills above show the two end states.Every settled wording, for a read — all seven shown: This turn was interrupted when the app restarted. Its partial output is above. · The connection was unstable. · Connection lost (exit 1). (the detail is the wire row's) · The session was busy. · The turn stalled. · A tool appeared stalled. · The agent process this chat shares kept restarting (…). Each states the cause only — the restart row also where the cut-off work is, the pointer its wire text carried — and the Resumed automatically card under it says the rest. The tool-stalled row names no tool because the wire row it settles (
⟳ Tool appeared stalled — please retry.) carries none.The two notices — a chat that came back with different permissions, and a chat whose budget is spent:
Their three other wordings. The shared-process row is the one settled wording that carries a detail (the exit code), so it has a still of its own (UX review, rev 48), and the connection-lost row, a copy variant of the same settled component, has one too (UX review, rev 51), as does the stalled-turn row (UX review, rev 53), and the session-busy and tool-stalled rows, the other two plain-sentence wordings of that shape (UX review, rev 59). The permissions notice on a mate thread, whose composer has no approval picker, so the remedy names the Mates page and the profile; the pause after a person lowered the limit under resumes already taken, which states the new limit rather than the count over it (UX review, rev 35: 3 resumes in the last 1 hour, over its limit of 2 blamed an overrun the person never chose); and the pause when the earlier resumes could not be checked (a stamp list on the metadata line this gateway did not write), which states only the pause and the remedy, no cause and no duration — the cause (a stamp list this gateway did not write) is in the gateway log and nothing the person can act on, and a duration would read as a promise to pick the chat up later (UX review, rev 30), so the row leads straight to the Resume press that resets the count:
The pause also raises one system notification, the signal a person who is away gets (UX review, rev 29): in the Notifications feed as Automatic resume paused with the chat's title and what happened — the body opens with the event, automatic resume used all 5 resumes in 1 hour, because the feed row cuts it off after a few words and the chat row's its has no referent after a title (UX review, rev 31) — and opened, with the full body and the Go to Chat button that leads to the Resume the body names:
Related Issues
Part of #13838 (the restart half, widened from unattended-only to a dashboard setting; the refusal half and the
staleverb are not here).Part of #12275 (its fallback half, behind the switch: the chat is told, in the chat, when its mode changed; nothing is restored).
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement
Contributions are accepted under the project's Apache-2.0 license; see the Licensing section of CONTRIBUTING.md.