Skip to content

feat(chat): resume every interrupted session from the sidebar - #21031

Open
jeeshofone wants to merge 2 commits into
kirodotdev:mainfrom
jeeshofone:feat/resume-all-interrupted
Open

jeeshofone wants to merge 2 commits into
kirodotdev:mainfrom
jeeshofone:feat/resume-all-interrupted

Conversation

@jeeshofone

@jeeshofone jeeshofone commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: After a restart or a run of backend errors, one press in the chat sidebar resumes every idle session whose last turn ended without a reply.

A gateway restart, a laptop sleep or a run of backend errors leaves many sessions with a turn that ended without a reply: the composer offers Resume and the sidebar row says "Turn interrupted". Today each needs its tab opened and its own Resume press.

Why it matters

With thirty open sessions that is thirty switches and thirty clicks after every restart, and a missed session sits idle until someone notices.

Not a goal

What changed (motivation → approach → change)

A new sidebar row resumes every interrupted idle session in the background with one press, then says what resumed and what did not.

POST /api/chat/slots/resume-all (owner-only, no body) answers when
202 {ok, total, run_id} a run starts, held as a gateway task in state._background_tasks
200 {ok, total: 0, run_id: ""} nothing is interrupted, and nothing starts
409 {code: "resume_all_running", run_id, total} a run is in flight, and nothing is dispatched
403 owner_only an app token or a non-owner dashboard session, before anything is collected

The outcome {ok, run_id, total, resumed, skipped: [{slot, code}], failed: [{slot, error}], cancelled?} reaches owner sockets as one resume_all_done frame, sent from a finally however the run ends. start_resume_all is the one entry point: the one-run check and the task start have no await between them.

Candidates (collect_interrupted_idle) are the facts the slots row shows: a chat-page session (CHAT_PAGE_MODES) that is not under construction, not a relay archive, not running (its own turn or a linked channel turn), has no live sub-agents, and ends interrupted on the tail the sidebar reads (summary_rows), so nothing is read from disk on the loop. The sidebar counts the same facts, and a fixture both suites read pins its number to the run's total.

Per slot, the window loads off the loop, then continue_slot_turn(require_interrupted=True, via="resume_all") runs. A turn that finished cleanly before its dispatch is skipped as turn_not_failed, every other refusal lands in skipped by code, any other exception (a failed slots push included) lands in failed, and the rest carry on.

Pacing. Every candidate is dispatched at once, in one gather. Each cold start waits for a SessionManager._start_sem permit, which agent.cold_start_concurrency sizes, and is never refused for want of one. Only loop teardown at shutdown cancels the run, and its frame says cancelled: true.

The row leads the search dock's shelf: "N interrupted sessions" and a "Resume N sessions" action, held while offline with the reason in its title and accessible name. A press reads "Resuming…" until the frame with its run_id arrives (the frame can beat the 202, so the hook holds each run's frame until the answer is read), then shows the outcome for six seconds. A 409 says "A resume is already running" and follows that run. Sessions the owner must act on (a repeated start failure, a pending approval, an error) get a "Some sessions were not resumed" notice naming each one and why. Its list scrolls in a six-line box, faded at the bottom while lines sit below. Sessions that moved on by themselves stay in the status line. A failed request, the owner gate included, renders through ErrorNotice with the agent hand-off. The wait is bounded (60 s + 1 s per session) because frames are one-shot.

Why not one long request? The run grows with the session count, and a proxy timeout would show an error for a working run. Why not one Continue per row from the browser? No one-run check across tabs, and no single outcome. Why no status read? Each tab learns the outcome from the frame or the 409, so GET /api/chat/slots/resume-all still reads the session named resume-all.

where what
src/kiro_crew/dashboard/chat_resume_all.py, chat.py, routes/chat.py, ws_event_scope.py the run, the route (registered before every {slot} POST) and the owner-only resume_all_done event
website/src/pages/chat-sidebar/resumeAll.tsx, SearchDock.tsx, scope.ts, pages/ChatSidebar.tsx, components/ErrorNotice.tsx the row, its hook, the shelf that mounts it, and the messageRef prop the problems list's fade measures through
website/src/api/client/chat.ts, lib/resumeAllEvent.ts, hooks/useWebSocket.ts, hooks/websocket/browserEvents.ts chatSlotsResumeAll, and the frame re-broadcast as a window event
website/src/i18n/locales/*.json, i18n/pluralKeys.json, docs/feature-map/README.md, docs/system-specs/modules/history.md the row's copy in every locale, the feature row and the resumeAll.tsx owner row

How this fits #20750

Complementary. #20750 is the automatic, budgeted path, calling continue_slot_turn with a new optional body. This PR calls it by keyword with no body. They share only docs/feature-map/README.md and docs/system-specs/modules/history.md, where each adds its own row.

Backwards compatibility

Compatible: a new route, module, owner frame and sidebar row. The single-slot Continue route is unchanged.

Removes nothing: every change adds a route, an event, a row, copy or a doc row; no existing reader loses anything.

Tests

  • test/test_chat_resume_all.py (30), every run started through start_resume_all and read from its resume_all_done frame: the candidate set (running, finished, empty, relay-archive, sub-agent, under-construction, member DM and channel-turn slots out; a reserved slot counted as the sidebar counts it, then skipped); a restored row judged from its tail with no read, its window loaded off the loop, and a failed read failing that slot only; the pick-up body and via; 40 candidates dispatched in one gather whose cold starts go through a real SessionManager._start_sem (four inside, 36 waiting, none refused); a slot that finished before its dispatch and a stale second press, both turn_not_failed; refusal and failure isolation, a failed slots push included; 202 then the frame; one run per press with the 409 and its total, direct and over HTTP; a cancelled run still sends one frame; an exception outside the per-slot work still sends the outcome; the owner gate; the route order.
  • test/fixtures/resume_all_candidates.json is the count both sides are held to. pytest builds each slot from its listed facts, checks the fixture's rows against serialize_slots, and checks the expected keys against collect_interrupted_idle and start_resume_all's total. vitest (ChatSidebar.resumeAll.test.tsx) feeds the same rows through the chat page's surface filter into the sidebar and checks the count it renders.
  • test/test_slot_running_readers.py lists the collector as a turn_running reader.
  • website/src/test/ChatSidebar.resumeAll.test.tsx (16): the count rule, the outcome text, no row at zero, pending until the matching frame, nothing to resume, the 409 state, the owner-only message with the hand-off, a failed request through ErrorNotice, the problems notice in its scrolling box, that box's six 16 px lines and its bottom fade, which is on only while lines sit below the edge, race-only skips with no notice, the offline hold, the offline reason in the action's title and accessible name, and the two fixture cases. website/src/test/resumeAll.hook.test.tsx (7): a frame before the 202 settles the row, its own run's frame settles it after another run's frame, a 202 read after a second run finished settles from its own run's frame, nothing is kept once the answer is read, another run's early frame does not settle it, a 409 waits with its total, and a 409 whose run already finished settles from the kept frame. The hook keeps frames only while its request is open, one per run id, and empties them when the 202 or 409 is read. That is not the eight-frame buffer removed on dc24629fa5, which kept frames between presses and pruned them by age.
  • Existing tests updated for the new client method and event: ApiClient.refactor.surface.test.ts, ChatSidebar.loopWaiting.test.tsx, ChatSidebar.ownerComposition.test.ts and useWebSocket.frameTrace.test.ts. ErrorNotice.messagePlacement.test.tsx gains two cases: messageRef reaches the message span in the block and the inline variant.

Mutation pins: dropping require_interrupted, the 409 check, a candidate filter or the finally broadcast turns a test red, and a run that paces its own dispatches hangs the 40-candidate test at its barrier. Each of these turns its own side's fixture test red: on the server, dropping the chat-page mode filter, the sub-agent filter, the channel-turn filter or the relay-archive filter, or the slots row no longer reporting a linked channel turn as running; in the browser, dropping the sub-agent or relay-archive filter, or the chat page admitting member threads. Dropping the browser's running filter changes nothing, because the slots row never reports a running session as interrupted. In the hook, one kept frame in place of the map turns the two-runs test red, and keeping frames while no request is open turns the nothing-kept test red. The fade test fails at its six-line check on the code before the problems-list change, and at its fade check when the box is kept but the fade is removed.

Determinism on c5b35b96f7: test/test_chat_resume_all.py passes 20 runs at -n0, 10 at -n 4 --dist loadgroup beside test_slot_running_readers.py and test_chat_slot_continue.py, and 3 seeded shuffled orders (seeds 4127, 23011, 9876). ChatSidebar.resumeAll.test.tsx, resumeAll.hook.test.tsx and ErrorNotice.messagePlacement.test.tsx pass 3 runs with no act() warnings.

Gates on this head (c5b35b96f7, on main at fd2f83c414): every static gate in the prepare-pr profile passes, among them Black, isort, flake8, mypy (0 issues), comment-history, harness parity, feature-map, docs lint, file-line cap, the website build, gen:ui --check, tsc, eslint, i18n:check, i18n:render, phantom classes, restyle ratchet, jscpd and the bundle-size and chunk-cycle checks. The flake-pattern ratchet passes, and semgrep with the repo rules finds nothing. The frontend related set, all four of its batches (1,871 files), passed: 35,964 passed, 2 skipped and 1 expected failure. pytest passes on the 37 backend files that exercise the changed modules (1,474 passed), among them the ws-scoping and error-code contract tests and test_state_parts_dashboard_state.py, whose module main changed under this branch. This head changes no backend file since 8d6053cd30 other than by the rebase. The backend related set last ran on 6d258afb45 (2,412 files then), and none of its failures there was in a file this diff touches.

Manual verification

Built the SPA and drove it against the gateway-free API stub at 1512×862, 2× scale, in both themes: pressed the row, pushed the slots frame and then the outcome event the way the gateway orders them, answered the press with 200, 403, 409 and 500, and refused the socket for the offline state. On 1691f972be the problems list was captured the same way, and the harness read its line height (16 px), its box (96 px) and its fade from the page, at the top of the list and at its end. c5b35b96f7 changes only how the hook keeps early frames, which draws nothing. Not run against a live gateway restart.

Screenshots / video

Twelve states in both themes, captured from the built SPA with placeholder sessions only. The problems list changed on this head, so its before and after come first. The details block holds every other state, the full success and nothing-to-resume lines among them.

The problems list before. The box cut its sixth line ("approval.") and had no edge fade.

Before: some sessions were not resumed, top of the list, its sixth line cut (dark)
Before: some sessions were not resumed, top of the list, its sixth line cut (light)

After, on 1691f972be. Six whole lines, with the bottom edge faded while lines sit below it. Scrolled to its end, the list shows its last line whole and unfaded.

After: top of the list, six whole lines and a faded bottom edge (dark)
After: top of the list, six whole lines and a faded bottom edge (light)
After: scrolled to the end, the last line whole and no fade (dark)
After: scrolled to the end, the last line whole and no fade (light)

The other states the UX lane listed as missing:

Request failed, with the agent hand-off (dark)
Offline, whole window (dark)

Every other state, both themes (18 images)

Three interrupted sessions (dark)
Three interrupted sessions (light)
Pending while the run is in flight (dark)
Pending while the run is in flight (light)
Resumed 2 sessions, 1 not resumed, after the slots push (dark)
Resumed 2 sessions, 1 not resumed, after the slots push (light)
Resumed 3 sessions (dark)
Resumed 3 sessions (light)
Nothing to resume (dark)
Nothing to resume (light)
Request failed, with the agent hand-off (light)
A resume is already running (dark)
A resume is already running (light)
Owner-only refusal, with the agent hand-off (dark)
Owner-only refusal, with the agent hand-off (light)
Offline, whole window (light)
Nothing interrupted: no row (dark)
Nothing interrupted: no row (light)

Related Issues

Closes #21021

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@jeeshofone
jeeshofone requested review from a team and KiroCrewCaptain as code owners October 16, 2026 01:09
@jeeshofone
jeeshofone requested a review from buluoray October 16, 2026 01:09
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Oct 16, 2026
@jeeshofone

Copy link
Copy Markdown
Contributor Author

Intent: One owner press in the chat sidebar resumes every idle session whose last turn ended without a reply, through the existing Continue path, paced and with a per-session outcome.
Not a goal: Automatic resume (that is #20750), any change to what single-session Continue does or refuses, or a config knob for the pacing.

@jeeshofone
jeeshofone force-pushed the feat/resume-all-interrupted branch from b5ab884 to 9b4462f Compare October 16, 2026 03:30
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 16, 2026
@jeeshofone

jeeshofone commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Disposition of the independent review on b5ab884, against head 9b4462f (one commit, rebased onto current main).

M1, a candidate that finished before its wave gets Continue: fixed. Each slot now goes through continue_slot_turn(..., require_interrupted=True), so a slot whose turn ended cleanly between the collect and its wave is reported in skipped as turn_not_failed and is never sent the carry-on body. Both reviewer tests are ported: test_slot_finished_between_waves_is_not_continued and test_second_press_after_turns_finished_fast_does_not_carry_on. Dropping the flag turns both red.

S1 + S2, one long request and a half-queued wave on cancel: fixed with one structural change.

  • The handler answers 202 {ok, total, run_id} at once and runs the waves as a gateway-owned task held in state._background_tasks.
  • Only one run can be in flight. A second press answers 409 resume_all_running with that run's id and dispatches nothing. The check and the start run synchronously with no await in between.
  • Each wave is its own tracked task behind asyncio.shield. Cancelling the run stops it between waves only, so a slot with a queued recovery entry always gets its dispatch.
  • The outcome goes out once as a resume_all_done frame through broadcast_ws_owners, the same owner push pins_changed and dashboard_card use. It is classified owner-only in ws_event_scope, and the socket router re-broadcasts it as a window event that the row matches by run_id.
  • No status route was added, so the route table is unchanged and GET /api/chat/slots/resume-all still reaches a slot named resume-all.
  • Frontend: "Resuming…" stays up until the matching frame arrives, and a 409 shows "A resume is already running" and follows that run. The pending state has a 60 s + 1 s/session ceiling, because a reconnect can miss a one-shot frame.
  • Tests:
    • test_cancelling_the_run_mid_wave_leaves_no_slot_half_queued is your pin flipped. Every slot in waves 1 and 2 is dispatched, waves 3+ are untouched, no slot has queue_depth > 0, and the frame says cancelled: true. Removing the shield turns it red.
    • test_a_second_press_during_a_run_is_refused_and_dispatches_nothing and test_a_double_press_over_http_starts_one_run cover one run per press.
    • test_the_answer_does_not_wait_for_any_dispatch: the 202 arrives while every start is held open, so a short client or proxy timeout no longer matters.

S3, non-owners get a button that always 403s: mapped. The SPA has no view of whether the viewer is the owner (only the server's owner_only refusal says so), so the row stays visible. A 403 owner_only now maps to "Only the dashboard owner can resume every session" with no agent hand-off, following the LeakedRuntimesCard pattern. This has a test and a committed still.

N1, the sidebar and the server counted sub-agent slots differently: fixed on the server. collect_interrupted_idle skips slots whose subagents.running_agents_for(effective_session_key(slot)) is non-empty, which is the fact the slots projection serializes as subagents_running. It also skips slots under construction, which the projection omits. So total now equals the badge. test_slots_with_live_sub_agents_are_not_candidates and test_a_slot_under_construction_is_not_a_candidate each go red without their filter.

N2, a push failure reported a dispatched slot as failed: fixed. continue_slot_turn now logs a failing push_slots_update after a successful dispatch instead of raising it, the same way it already guards the audit sink. test_a_failed_slots_push_after_dispatch_still_counts_as_resumed covers it. The single-slot route gets the same behaviour.

N3, the inter-wave comment: fixed. It now says the pause is fixed pacing and does not wait for any signal.

N4, GET/DELETE reaching the per-slot handlers: no change, as ruled. That behaviour is correct and is what keeps a slot named resume-all reachable.

Also on this head: the screenshots were re-captured for pending, outcome, already running and owner-only (both themes), the PR body was rewritten for the new API shape, and test_slot_running_readers lists the collector as a reservation reader.

Local gates on this head:

  • pytest -n 8: the PR's tests, route_table, slot_ownership_checkpoint, the chat_handlers composition contract, session_control_continue, chat_slot_continue and every ratchet plus the refactor facade all pass.
  • vitest: ChatSidebar*, useWebSocket*, ApiClient* (3090 passed), plus src/i18n/ with the base ref.
  • tsc -b, eslint, i18n:check against the base, the black gate, isort, flake8, mypy on src/kiro_crew (0 issues), and comment-history.

First-pass check: R1-R8 OK.

@github-actions github-actions Bot removed the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 16, 2026
@jeeshofone
jeeshofone force-pushed the feat/resume-all-interrupted branch from 9b4462f to 51e106e Compare October 16, 2026 04:23
@jeeshofone

Copy link
Copy Markdown
Contributor Author

Disposition for the delta review of 9b4462f (APPROVE-WITH-FIXES), against head 51e106e.

F1 (frame before the 202 is dropped): fixed. useResumeAll now listens all the time, not only while it waits on a run. A frame for a run id the row is not waiting on goes into a small kept set: the last 8, each dropped once its run's wait ceiling has passed. onSuccess and the 409 adopt path check that set first and settle at once if the run already finished. The awaited run id is written to the ref where the 202/409 body is read, not on the next render. website/src/test/resumeAll.hook.test.tsx ports the probe: the frame is delivered before the 202 resolves, and the outcome shows with nothing pending. Removing the kept-set lookup turns that test red, along with the "409 whose run already finished" case. A frame for a different run still leaves the row pending.

S1 (shield at shutdown): fixed by rewording, with no shutdown hook. The module docstring, the _run_in_background docstring, the cancel test's docstring and the PR body now say that the shield covers a cancellation of the run task itself. At loop teardown every task is cancelled, the wave included. A slot mid-dispatch keeps its queued entry in memory only, and a restart reloads the queue.

N1 (guard freed before a cancelled run's shielded wave finishes): no change. It is safe under the per-slot lock and the slot_queue_pending refusal, and nothing in production cancels the run task alone.

N2 (exception outside the per-slot work): fixed. The run and wave tasks now take _log_task_exception alongside the strong reference and the discard. The outcome is broadcast from a finally. An exception that escapes the waves sets ok: false, lists every slot not yet recorded in failed with the exception's class name, and is re-raised so it gets logged. Probe P4 is ported as test_an_exception_outside_the_slot_work_still_sends_the_outcome. Dropping the finally broadcast turns it red.

N3 (reserved-admission parity): fixed with a one-line predicate change. collect_interrupted_idle now reads slot.turn_running, the same fact the projection serializes as running. A reserved slot is therefore counted by the server exactly as the badge counts it, and continue_slot_turn reports it as skipped slot_running. A new test covers this. The test_slot_running_readers registration moves to the turn_running readers with that reason.

N4 (adopted 409 used the bare ceiling): fixed. The 409 body carries the in-flight run's total, and the adopting tab uses it for its wait. A hook test checks that the row is still pending past the bare 60 s and settles at the scaled ceiling.

N5 (act warnings in ChatSidebar.resumeAll.test.tsx): no change. These are report-only outside strict mode, and the file's assertions are unaffected.

Local on 51e106e: pytest for the resume-all suite, slot-running readers, the route table and registry, and the chat-handlers, slot-facade, client-route and server composition contracts at -n 8 (464 passed). Vitest over ChatSidebar*, useWebSocket*, ApiClient*, src/i18n and the new hook file (4116 passed). tsc -b, eslint and i18n:check are clean. Black gate, isort, flake8, mypy src/kiro_crew (0 issues), comment-history and semgrep with the repo rules (0) all pass. Still one commit.

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 16, 2026
@iamwhatever
iamwhatever force-pushed the feat/resume-all-interrupted branch from 51e106e to 9f625bb Compare October 23, 2026 06:05
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5, fork) — ✅ PASS

Design-level review of 3aacf7a0c1ecd1185e9e96640eb8196245add829 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

Design-Verdict: PASS

Nothing blocks: the bulk route reuses continue_slot_turn and its refusals for each slot, is owner-gated, allows one run at a time, and takes nothing away.

Notes

[DESIGN-REVIEWED] 3aacf7a

Description read

[DESCRIPTION-READ] f6fb32d0841ae2f072426ffa7d3c5b24e403ecd5e98c3c9b7d3b5e988ec3e433

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 .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5, fork) — ✅ PASS

UX-level review of 3aacf7a0c1ecd1185e9e96640eb8196245add829 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

UX-Verdict: PASS

Nothing blocks. The screenshots cover every new state, but no first-time reader has seen them, because fork PRs get a single review pass. Pushing the branch to this repository would run the cold-read pass.

Notes

  • The partial outcome "Resumed 2 sessions · 1 not resumed" (attachments 13–14) has no reason line for sessions that changed state on their own (e.g. turn_not_failed). Yet "Resume 1 session" is offered again right beside it. Count only problem skips in left, or name the session in the status line.
  • After a partial run with problems (attachments 1–6), the row still offers "Resume 5 sessions" for sessions just reported as waiting on an approval or failing to start. Pressing it again skips them the same way. Leave sessions with problems out of the count, or label the action "Retry".
  • The failed-request notice shows a raw "Internal Server Error" under "Could not resume the sessions" (attachments 7, 19). Show resume_all_failed with a next step instead of errMessage(e) when the server sends back only its generic status text.

[UX-REVIEWED] 3aacf7a

Description read

[DESCRIPTION-READ] b8a05f3c30849a177077c2bff38493a18aac9797ab8f03956b5eda60f3d5c616

That sha256 names everything this verdict read: the PR description, plus the 29 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 .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5, fork) — ✅ PASS

Premise-level review of 3aacf7a0c1ecd1185e9e96640eb8196245add829 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS does not.

First-Principles-Verdict: PASS

Nothing blocks. The one-press bulk resume removes a stated chore, and it reuses continue_slot_turn and the cold-start queue. Two notes: an undeclared request-body flag, and fade machinery the goal does not need.

Notes

  • Item 5 — undeclared: the description's route table says "(owner-only, no body)", but the diff adds _crew_mode_requested and reads body.get("crew_mode") is True. That is a request-body parameter on a public route, and the description never mentions it. It has one consumer (chatSlotsResumeAll(crewModeEnabled())). The flag is per device (readPreviewFlag), so the requirement itself is real; only the framing is missing.
  • Item 7 — oversized: the problems list's bottom fade (useScrollEdgesY, MORE_BELOW_MASK, remeasureList) needs a new ErrorNotice.messageRef prop, and resumeAll is that prop's only consumer. The goal is "says what resumed and what did not", and the max-h-24 overflow-y-auto box alone already keeps Dismiss and the hand-off in view. Subtraction: delete messageRef from ErrorNotice.tsx and the edge-measuring in ResumeAllProblemsNotice, and keep the plain scrolling box. Clears when: the mechanism is removed, or a human amends the Goal.

What this change ships

Inventory (8 items) — 6 justified

Intent: after a restart, resume every interrupted idle session with one press instead of one press per tab. ADDITION.

  1. A sidebar shelf row shows "N interrupted sessions" with a Resume N action whenever N > 0 — justified
  2. POST /api/chat/slots/resume-all runs Continue on every candidate in the background (202) — justified
  3. A second press while a run is going gets 409 and follows that run — justified
  4. An owner-only resume_all_done frame carries the outcome to the pressing tab — justified
  5. An optional {"crew_mode": true} body leaves out the workers of listed Crew Mode roots — undeclared
  6. A "Some sessions were not resumed" notice names each one and why — justified
  7. The problems list fades at the bottom via a new ErrorNotice.messageRef prop — oversized
  8. Copy in 12 locales and the feature-map and history spec rows — justified

[FIRST-PRINCIPLES-REVIEWED] 3aacf7a

Description read

[DESCRIPTION-READ] 87c26e55115e2fe265e8a642407154c299abce0302dc73f47d02296184ab92bb

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review (fork) — ✅ no blocking findings

Reviewed 3aacf7a0c1ecd1185e9e96640eb8196245add829 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 3aacf7a

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review (fork) — ✅ no blocking findings

Reviewed 3aacf7a0c1ecd1185e9e96640eb8196245add829 via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 3aacf7a

@iamwhatever
iamwhatever force-pushed the feat/resume-all-interrupted branch from 9f625bb to 0f376b3 Compare October 23, 2026 06:39
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 23, 2026
@iamwhatever
iamwhatever force-pushed the feat/resume-all-interrupted branch from 0f376b3 to e32755c Compare October 23, 2026 07:07
@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Oct 23, 2026
@jeeshofone
jeeshofone force-pushed the feat/resume-all-interrupted branch from 8d6053c to 1691f97 Compare October 28, 2026 08:00
@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • The outcome frame is one-shot, so a tab that misses it waits out the bound (span=3d98e552d3b6)

Kept: a connected tab always gets the frame. Only a socket that drops during the run misses it, and that is the case the bounded wait exists for.
resume_all_done goes to every owner socket, so a second tab that got the 409 receives the frame of the run it follows. When that run finished before the 409 was read, the hook settles from the frame it kept ("a 409 whose run already finished settles from the kept frame" in resumeAll.hook.test.tsx). A reloaded tab starts with no pending press, so it shows the count from the slots push and never "Resuming…".
A socket that reconnects during the run misses the frame, and the row gives the button back after 60 s plus 1 s per session. The sessions the run did not resume are still interrupted, so the next press counts them. Replaying the last outcome on reconnect would add outcome state on the server and a replay step on every owner connect for this one row. The body's "Why no status read?" makes the same choice for a GET route.

@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • "Resume 5 sessions" under a notice that says those sessions need the owner (span=0d3fb8e3e42e)

Kept, for the reason in the reply to this note on 6d258afb45: the button counts the sessions interrupted now, which is the total the next press acts on.
Whether a press resumes a given session is the server's Continue decision for that session, so counting only the sessions a retry can resume would copy the server's refusal rules into the browser. A second press is safe: a refused session is refused and named again, and the others go through Continue. Hiding the button while the notice is open would put a Dismiss in front of that press.

@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • A failed request shows the server's raw "Internal Server Error" (span=a353922b8935)

Kept, for the reason in the reply to this note on 6d258afb45: the notice already leads with resume_all_failed, "Could not resume the sessions", as its title, and its next step is the "Ask the agent" hand-off.
The message stays the server's own text because ErrorNotice looks that text up in the error journal to hand the endpoint, HTTP status and backend code to the agent. Replacing it would drop that context.

@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • Offline, the reason is only in the button's tooltip (span=5ac31b5d083f)

Kept: offline, the action follows the sidebar's convention for a control that needs the gateway. It spreads offlineProps, as the header's New button does, so its title and its accessible name both give the offline reason.
The window shows the reason without a hover too: while the socket is down, the composer's placeholder starts "Gateway offline", which is why attachment 24 is the whole window. A line of its own in the row would be a second offline notice beside that one.

@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • Item 6: the problems list's fade and messageRef go past the Goal (span=ed06f24f8893)

Rebutted: the fade answers a report on this PR. The UX lane's note on 8d6053cd30 said the list "cuts its last line in half" and "has no edge fade, so the hint that more lines are below is weak", and asked for the repo's edge fade (MORE_BELOW_MASK / useScrollEdges). The maintainer review r131 listed that note as one of the two still open.
Fixing the cut line alone makes the box end on a whole line, so a list longer than six lines would read as complete. The fade is the cue the app already uses for that, on the model lists in ModelEffortDropdown and ChatPanePickerMenus, measured by the existing useScrollEdgesY. messageRef is the one seam it needs, because ErrorNotice renders the span that scrolls and the row cannot measure it otherwise.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 28, 2026
@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • A later run overwrites an outcome needed by a pending request (span=e1be9bc3fecb)

fixed in c5b35b9: the hook keeps the outcome frames it hears while its request is open, one per run_id, and the 202 or 409 settles from its own run's frame.
The map is emptied when the answer is read, whether it is a 202, a 409 or an error, and a frame heard while no request is open is not kept, so nothing carries over to a later press. That is not the eight-frame buffer removed on dc24629fa5 at First Principles' request, which kept frames between presses and pruned them by age. The trigger holds: the server's one-run guard ends when a run's dispatch is done, not when its 202 reaches the tab, so another tab's run can start and finish before a slow 202 is read.
Two hook tests in resumeAll.hook.test.tsx pin it. "a 202 read after a second run finished settles from its own run's frame" is red on 1691f972be. "keeps nothing once the answer is read: a later press does not settle from a frame heard before it" is red on 1691f972be, and red on the fix with its open-request check removed.

@jeeshofone
jeeshofone force-pushed the feat/resume-all-interrupted branch from 1691f97 to c5b35b9 Compare October 28, 2026 09:00
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Oct 28, 2026
@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • One press can leave N resumed turns running at once (span=435eed4594cf)

Acknowledged: the body says so under Not a goal, and the cap belongs at the turn dispatch that every send and Resume reaches, which #25055 tracks.
The press adds no load that N separate Resume presses would not. Each cold start waits for a SessionManager._start_sem permit, so the starts queue at agent.cold_start_concurrency, and no turn a person starts waits on a turn cap. A cap inside this run alone would leave the single-session Resume without one.

@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • A failed request shows the server's raw "Internal Server Error" (span=58cd72a17056)

Kept, for the reason in the reply to this note on 1691f972be: the notice leads with resume_all_failed, "Could not resume the sessions", and its next step is the "Ask the agent" hand-off.
The message stays the server's own text because ErrorNotice looks that text up in the error journal to hand the endpoint, HTTP status and backend code to the agent.

@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • "Resume N sessions" under a notice naming sessions that need the owner (span=1a9b0b89cbf9)

Kept, for the reason in the reply to this note on 1691f972be: the button counts the sessions interrupted now, which is the total the next press acts on.
Counting only the sessions a retry can resume would copy the server's per-session refusal rules into the browser. A second press is safe: a refused session is refused and named again, and the others go through Continue.

@jeeshofone

Copy link
Copy Markdown
Contributor Author
  • Item 5: the one-run guard, the 409 and the follow state (span=20f5f7e689ca)

Kept: the guard is there for the outcome a press reports, not to stop a double dispatch. The note is right that continue_slot_turn's refusals already keep one session from being continued twice.
Without it, a press in a second tab during a run starts a run of its own. The first run has already dispatched most of the sessions, so the second run either finds nothing and says "Nothing to resume", or its Continue on each shared session is refused as slot_running or slot_queue_pending and its row says those sessions were "not resumed", while they are resuming. With the guard, that tab gets the 409, says "A resume is already running", and shows the first run's outcome when its frame arrives.
earlyRef would stay either way: it is what lets a tab settle on its own run's frame when the frame beats the 202, which happens without a second tab.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Oct 28, 2026
@iamwhatever

Copy link
Copy Markdown
Contributor

HOLD at c5b35b9 for human review.

I found no code defect. The owner gate runs before anything is collected, one run at a time holds with no await between check and start, and each session goes through the existing continue_slot_turn with require_interrupted=True. The backend is unchanged since r131. The two notes r131 named are answered: the problems list now shows six whole lines with a fade, and the two-copies candidate rule has a reply. Every current-head lane note has a disposition, CI is green, and the branch merges cleanly.

What still holds it:

  • Sensitive. A new route dispatches agent turns across the install, and a new owner-only socket event carries the outcome. That stays with a human reviewer.
  • Design-doc gate. @bolichen97 asked for explicit design agreement on the bulk action, and none is on record yet.

— crew-pr-reviewer (r133)

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 29, 2026
@iamwhatever
iamwhatever force-pushed the feat/resume-all-interrupted branch from c5b35b9 to 1973320 Compare October 29, 2026 16:46
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Oct 29, 2026
A gateway restart, a laptop sleep or a run of backend errors leaves many
sessions in the same state: the last turn ended without a reply, the
composer offers Resume, and the sidebar flags the row as interrupted.
Recovering them meant opening each tab and pressing Resume, one by one.
On a dashboard with thirty open sessions that is thirty clicks after
every restart.

The sidebar's search dock now leads its shelf with one status row while
any session is interrupted: "N interrupted sessions" and a "Resume N
sessions" action. The press calls the new POST /api/chat/slots/resume-all,
which runs the Continue path (continue_slot_turn) on every candidate.
Candidates are the facts the slots projection serializes for the row:
idle, no live sub-agents, not under construction, not a relay archive,
and is_turn_interrupted on the transcript, so the count on the button is
the total the server answers.

The press is not one long request. The handler collects the candidates
and answers 202 {ok, total, run_id} at once; the waves run as a
gateway-owned task held in state._background_tasks. One run at a time:
a press while a run is in flight answers 409 resume_all_running with
that run's id and total and dispatches nothing. The outcome {run_id,
total, resumed, skipped, failed} reaches owner sockets as one
resume_all_done frame, sent from a finally however the run ends; the
row stays pending until it arrives, then shows it for six seconds. The
frame can beat the 202, so the hook keeps the last few outcomes it
heard and settles from them when the run id is read. A press that got
the 409 follows the running run, with that run's total for its wait,
and says one is already running.

Launches go out four at a time with a fixed 1.5 s pause between waves.
Each wave runs as its own tracked task behind asyncio.shield, so a
cancellation of the run task stops it between waves only: no slot is
left with a queued recovery entry and no runner. At gateway shutdown
loop teardown cancels the wave too; a slot mid-dispatch keeps its
queued entry in memory only, and a restart reloads the queue. Each slot is
continued with require_interrupted=True, so a candidate whose turn ended
cleanly before its wave is skipped as turn_not_failed and never gets the
carry-on body. Every other per-slot Continue refusal is reported in
skipped by code, and an exception from one slot lands in failed without
stopping the wave. A slot counts as resumed once its dispatch succeeded:
a failed slots push afterwards is logged by continue_slot_turn, not
reported.

Owner-only. The single-slot Continue route gates per slot through
deny_app_slot_access; a route that dispatches a turn on every slot is a
different blast radius and takes require_owner_dashboard_request. An
app token and a non-owner dashboard session are both refused before
anything is dispatched; the row maps that owner_only refusal to its own
message without the agent hand-off, which cannot help.

The row sits in the shelf, not as a third button in the search row's
trailing cluster: AUTOSDE max-two-buttons-per-row grandfathers that
cluster but forbids growing it. The route is a literal registered beside
cleanup/model, ahead of every {slot} POST.

Tests: the candidate set (idle + interrupted; running, finished, empty,
relay-archive, sub-agent and under-construction slots out), the pick-up
body on every resumed slot, the wave bound and pause, a slot that
finished between waves and a stale second press both skipped as
turn_not_failed, refusal and failure isolation, a failed push still
counted as resumed, the 202 with the outcome frame, one run per press
(409 for the second, over HTTP too), cancellation mid-wave leaving no
slot half-queued, the answer arriving while every start is still held,
an exception outside the per-slot work still sending the outcome, the
owner gate, and the route order. Frontend: the count rule, the
pending row until the matching frame, a frame that arrives before the
202, the 409 already-running state with its total,
the owner-only message without hand-off, the error notice, the offline
hold, and the frame router trace.

Closes kirodotdev#21021
Resume-all dispatched its candidates four at a time behind asyncio.shield,
with a fixed 1.5 s pause between waves. The agent starts those dispatches
lead to already queue at SessionManager._start_sem, which
agent.cold_start_concurrency sizes and which makes a start beyond its
width wait for a permit rather than refusing it, so the pacer was a second
limit on the same starts that ignored that setting. Every candidate is
dispatched in one gather; the waves, the pause, its _pause seam and the
shield are gone, and the sidebar row's comments no longer describe them.
A burst test sends 40 candidates through a real SessionManager: every
dispatch is in flight at once, four starts run inside the semaphore while
36 wait, and none is refused.

Nothing limits how many resumed turns run at once after their starts,
and the module docstring says so: a run over N candidates can leave N
turns running together, the same load as pressing Resume in each
session, since no turn a person starts waits on a concurrency cap.

continue_turn.py stays as on the base: a failed slots push after a
dispatch raises out of continue_slot_turn as it does for the single-slot
route, and resume-all's _one records it as that slot's failure while the
other slots carry on.

start_resume_all is the module's one entry point. resume_all_interrupted,
which only the tests called and which skipped the one-run guard and the
outcome frame, is gone, and its tests press through start_resume_all and
read the run's resume_all_done frame.

The button's count and the run's total come from one golden fixture,
test/fixtures/resume_all_candidates.json, which both suites read. pytest
builds each slot from its facts, checks the fixture rows against
serialize_slots and the expected keys against collect_interrupted_idle
and start_resume_all's total. vitest feeds the same rows through the chat
page's surface filter into the sidebar and checks the count it renders.

The sidebar hook keeps the outcome frames it hears while its request is
open, one per run id, and empties them once the 202 or 409 is read. The
server's one-run guard ends when a run's dispatch is done, not when its
202 reaches the tab, so another tab's run can start and finish before a
slow 202 is read, and the 202 then settles from its own run's frame.
Nothing is kept between presses, so the eight-frame Map with its trim
loop and age prune is gone.

The event name has one export, in lib/resumeAllEvent, which the row, the
frame's emitter and both tests import. The useWebSocket facade and
browserEvents re-exported it for the tests alone, so those re-exports are
gone and the facade test's name list matches the base again.

Offline, the action's title and accessible name give the reason through
offlineProps and the existing utils.offline.resume_sessions phrase, as the
header's New button does. Online the title is the button's label.

The owner-only refusal keeps the notice's Ask the agent hand-off.
errors-use-error-notice turns the hand-off off only beside an unsaved
draft or when the agent itself cannot run, and neither holds for a
non-owner whose press saved nothing, so the ownerOnly flag that switched
it off is gone and the notice's message alone names the owner gate.

The sidebar row's tests that assert on the first render drain the
providers' pending updates inside act first, so the file emits no act()
warnings.

The resume-all tests await the run task, the gathered background tasks
and the cancelled run through kiro_crew.testing.wait.await_until instead
of asyncio.wait_for with a 10 s ceiling.

The problems list's box holds exactly six lines. leading-4 makes its
lines 16 px, so the edge of its 96 px max-h-24 box falls between two
lines. The inherited 17.05 px lines fit 5.6 to the box, so its edge cut
the sixth. While useScrollEdgesY reports lines below that edge, the box
takes MORE_BELOW_MASK, the fade the model list uses, and the mask comes
off at the end of the list. ErrorNotice takes a messageRef prop, which
hands the row the message span it measures.

With the Crew Mode preview on, a worker whose root is listed stays out of
the run, as it stays out of the button's count: the sidebar counts from
crewModeView, which drops such a worker only while the browser-side
preview flag is on. The press now sends {"crew_mode": true} when the flag
is on, and collect_interrupted_idle then skips a slot whose live crew_root
stamp (crew_mode.live_stamps over the frame's parents) names a listed
chat-page slot. With the flag off, or no body, workers count as plain
sessions on both sides. A worker whose root is not listed still counts.
The shared fixture gains a listed-root case (flag on and off) and a
root-not-listed case; the vitest side sets the flag, counts through
crewModeView and checks what the press sends.
@iamwhatever

Copy link
Copy Markdown
Contributor

Disposition of the Opus BLOCK on 1973320 (chat_resume_all.py:168, Crew Mode workers resumed outside the sidebar count): fixed in 3aacf7a. The same finding was raised by the GPT lane at chat_resume_all.py:131; this fix covers both.

  • collect_interrupted_idle now skips a slot whose live crew_root stamp (crew_mode.live_stamps over _frame_parents, the edges the slot frame reads) names a listed chat-page slot. That is the rule crewModeView applies.
  • On latest main, crewModeView only drops workers while the browser-side Feature Preview mc-preview-crew-mode is on (feat(crew-mode): Ultra Mode Feature Preview (default off) + redesigned Crew page #25872). The server cannot see that flag, so the press now sends {"crew_mode": true} when it is on (api.chatSlotsResumeAll(crewModeEnabled())). With the flag off, no body, or any other body, workers count as plain sessions on both sides.
  • A worker whose root is not listed (closed root) still counts, since its row stays in the sidebar.
  • The shared fixture test/fixtures/resume_all_candidates.json gains a listed-root case (flag on: conductor, plain; flag off: all four) and a root-not-listed case. Both pytest and vitest read it. The listed-root case fails on the old head (['conductor', 'worker', 'grandchild', 'plain']). A new HTTP test pins the body parsing (true only; "yes", no body and non-JSON count workers). The vitest case also checks what the press sends.
  • Rebased onto main 95f7856 (one conflict in useWebSocket.ts: kept main's endSplitPaneFocus export).

@iamwhatever

Copy link
Copy Markdown
Contributor

HOLD at 3aacf7a for human review.

I found no code defect. The owner gate still runs before the body is read or anything is collected, and one run at a time holds with no await between check and start. The new crew_mode flag accepts only a literal true and only narrows the candidate set. The Opus BLOCK on 1973320 is fixed, every lane passes on this head, and CI is green.

What still holds it:

  • Sensitive. A new route dispatches agent turns across the install, and a new owner-only socket event carries the outcome. That stays with a human reviewer.
  • Design-doc gate. @bolichen97 asked for explicit design agreement on the bulk action, and none is on record yet.

Please also refresh the description. The route table still says "owner-only, no body", but the route now reads an optional {"crew_mode": true} (L363-L371), which is the First Principles item 5 note. The Tests section also names c5b35b96f7 as the head.

The other current-head lane notes do not block. The fade/messageRef note and the three UX copy notes repeat classes you answered on earlier heads. The turn-cap note is tracked in #25055. The fixture advice is already followed.

— crew-pr-reviewer (r137)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) readiness: passed Eligible automated validation passed for the current revision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resume every interrupted session from the sidebar in one press

3 participants