@loicngr/kobo 2.1.10 → 2.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +30 -3
- package/CHANGELOG.md +8 -0
- package/CONFIGURATION.md +32 -3
- package/dist/server/services/agent/engines/claude-code/engine.js +184 -47
- package/dist/server/services/sentry-service.js +13 -3
- package/dist/server/services/settings-service.js +4 -0
- package/dist/server/services/workspace-template-service.js +37 -3
- package/dist/server/utils/mcp-client.js +33 -3
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BTRzc60o.js +2 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DkoB2IEa.js → ChangelogPage-DD90Nl6c.js} +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-D_BQFWiT.js +2 -0
- package/src/client/dist/pwa/assets/{CreatePage-Com5PQPV.js → CreatePage-BZAoCukb.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-CdCs8PRS.css +1 -0
- package/src/client/dist/pwa/assets/{DashboardPage-DOeZUleS.js → DashboardPage-DyvTdmBg.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-Bke40k8T.js → DiffViewer-D0K2PlZD.js} +4 -4
- package/src/client/dist/pwa/assets/{DocumentsPanel-CO16-Q1X.js → DocumentsPanel-bm639sIu.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-fjWJjVF_.js → DrawerToggleButton-KfwSzjeY.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-C-Ejlc9_.js +3 -0
- package/src/client/dist/pwa/assets/{GitPanel-C3blk_Pd.css → GitPanel-CNFtj6k0.css} +1 -1
- package/src/client/dist/pwa/assets/HealthPage-37IHOIW-.js +1 -0
- package/src/client/dist/pwa/assets/HealthPage-Cnlhrn--.css +1 -0
- package/src/client/dist/pwa/assets/{MainLayout-DAcSwgUw.css → MainLayout-BQOOup9o.css} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-DXQ1ouYl.js +2 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-DKp9mn5X.js → MemoryPanel-CucZMhjy.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-B3v7marz.js → MemoryProposalList-CUQybzVR.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-a6q0PVGm.js → QBtnDropdown-x6qPyl_7.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-2fylQBjy.js → QExpansionItem-D33aoRoG.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-I9pplcfM.js → QScrollArea-Di-D6t2u.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-MdPXvwm4.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-fzzd3anS.js → QTooltip-BhwG7lp2.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-AugvL_tp.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-qFi9Wxzh.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-CGul0ui9.js → SearchPage-DA9nRmsA.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-C1dcMqCO.css → SettingsPage-BRvUAZ5r.css} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-DcV6apqE.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-DxifLUAa.js → SplitWorkspacePage-Brw2XsYJ.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CRo3ETg_.js → TerminalPanel-DVP18-nk.js} +9 -9
- package/src/client/dist/pwa/assets/{ToolCallItem-i60zEGsY.js → ToolCallItem-cSHh3p_F.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-DjFq7Kjf.js → TouchPan-C6LZZ3rw.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DmEgJ_5R.js → TourReplayButton-Czlqo18H.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B9LE8HtU.css +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BDYGq0pC.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-BwUkijpS.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-Ba0jAHhn.css → WorkspacePage-Bwfv559o.css} +1 -1
- package/src/client/dist/pwa/assets/{browser-3unJ6AQX.js → browser-ifCwUGpE.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-CPo0G1mj.js → build-path-tree-BBkGq3lN.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-CeSgjIY7.js +1 -0
- package/src/client/dist/pwa/assets/{de-B3oUHpbN.js → de-CHY2BAfL.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-CMBx0cvf.js → editor.api-CV_seGJR.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-Bjrcpo2R.js → efforts-ttw0bL63.js} +2 -2
- package/src/client/dist/pwa/assets/{es-X1ki-Z2w.js → es-DbcSKp38.js} +1 -1
- package/src/client/dist/pwa/assets/{expand-template-cR43uBa5.js → expand-template-C8biMRXo.js} +1 -1
- package/src/client/dist/pwa/assets/{format-B1k_qdxW.js → format-Cu95dbxt.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-gz0Rg7Rw.js → formatters-C25D1RSd.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-nkT5LuOx.js → fr-CyyJ8S2c.js} +1 -1
- package/src/client/dist/pwa/assets/{freemarker2-C8DHgbX4.js → freemarker2-DJEFr74q.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BmEGWkcn.js → handlebars-C_lzsMjX.js} +1 -1
- package/src/client/dist/pwa/assets/{html-giJc-yQc.js → html-CWmLFAmY.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CobVQiTM.js +1 -0
- package/src/client/dist/pwa/assets/{index-BjqJ45nb.js → index-ImUH7Bbt.js} +3 -3
- package/src/client/dist/pwa/assets/{it-Bfp_J8p0.js → it-XQJW2Xkr.js} +1 -1
- package/src/client/dist/pwa/assets/{javascript-CwQBOtjT.js → javascript-tsuiEUe-.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-BPEVDB8V.js → liquid-BBr9BvUk.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DAa4McnC.js → mdx-gjBH1cXG.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-Ct4RhitT.js → monaco-contributions-CvGcBIsb.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-BKnd0ZwH.js → monaco.contribution-Uw-Lfb8Z.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-D0Vm6a09.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-PMSxqP6t.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-LZ5CKuTW.js → project-color-BDQa2TgU.js} +1 -1
- package/src/client/dist/pwa/assets/{python-D8xYERv8.js → python-DlIgD1n6.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BnFyr836.js → razor-AraFPlpM.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-DW45V56c.js → typescript-CB8UpXHM.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-DzlqDFPy.js → use-search-index-status-CSREHQM4.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-CErX5nL7.js → use-tours-6yxNVmjH.js} +2 -2
- package/src/client/dist/pwa/assets/use-worktree-restore-BM3Giig1.js +1 -0
- package/src/client/dist/pwa/assets/{workspace-templates-CKduJ2zA.js → workspace-templates-BPMFvTnQ.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-gQTv5O31.js → xml-uRjL1P43.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-B4TEYUEa.js → yaml-C9CFR4qV.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CkiYsNd5.js +0 -2
- package/src/client/dist/pwa/assets/ComparisonPanel-DH9Q0XNK.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-C7fLOpai.css +0 -1
- package/src/client/dist/pwa/assets/GitPanel-BAgnXzXP.js +0 -3
- package/src/client/dist/pwa/assets/HealthPage-DR-ywiB2.js +0 -1
- package/src/client/dist/pwa/assets/HealthPage-DT8FGrKA.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-B1suu_Nn.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-CWXNtFAO.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-4KNBIQvY.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-CBM_GpS3.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-_h4xQlof.js +0 -9
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-C7MLWyKy.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DSw8Upcp.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-Bw7yTZ3h.js +0 -4
- package/src/client/dist/pwa/assets/custom-sounds-C5fGFFdT.js +0 -1
- package/src/client/dist/pwa/assets/i18n-PAAy4Hkn.js +0 -1
- package/src/client/dist/pwa/assets/network-auth-S-KP7qhA.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-DIqeoflM.js +0 -1
- package/src/client/dist/pwa/assets/use-worktree-restore-BpaEGfds.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -197,6 +197,15 @@ Every feature that touches the schema:
|
|
|
197
197
|
|
|
198
198
|
## WebSocket protocol
|
|
199
199
|
|
|
200
|
+
Claude streaming input is correlated by per-message UUIDs. A result can cover
|
|
201
|
+
several prompts through `user_message_uuids` (messages folded into one turn),
|
|
202
|
+
not one result per sent message. Retire only the matching yielded inputs;
|
|
203
|
+
locally queued or unacknowledged messages still block `turn:completed` and
|
|
204
|
+
input closure. UUID-less legacy results retain a conservative one-input
|
|
205
|
+
fallback for already yielded inputs only; a result never credits a future send.
|
|
206
|
+
Backpressure caps unacknowledged yielded inputs at 64, matching the SDK's result
|
|
207
|
+
UUID list bound. Keep the inactivity watchdog for genuinely unanswered work.
|
|
208
|
+
|
|
200
209
|
Claude SDK `task_*` messages cover agents **and** background jobs. Preserve `task_type` by task/tool identity across progress and terminal messages, which often omit it. The existing `subagent:progress` envelope also carries this lifecycle metadata for compatibility; `local_bash`, `local_workflow` and `mcp_task` must be excluded from agent cards, busy-agent counts, chat agent-name resolution and the selective stop-subagents action. Keep tracking these jobs for engine liveness and whole-session interruption. Legacy/unknown classifications and Codex agents remain supported; never classify from a description or from the agent's last tool (`Bash` can be a real agent's current action).
|
|
201
210
|
|
|
202
211
|
### External MCP dialogue and update checks
|
|
@@ -220,8 +229,10 @@ Clients subscribe to individual workspace ids. The server sends `WsEvent` object
|
|
|
220
229
|
Agent engines emit a normalized `AgentEvent` union, carried by `agent:event`. Common outer types include `user:message`, `task:updated`, `devserver:status`, `workspace:status`, `workspace:archived`, `workspace:unarchived`, and `sync:response`. Legacy `agent:output` rows remain supported through content migration. Keep the backend union in `services/agent/engines/types.ts` and its client mirror in `client/src/types/agent-event.ts` synchronized.
|
|
221
230
|
|
|
222
231
|
Error events may carry a stable `code`. Claude's post-result drain timeout uses
|
|
223
|
-
`result_drain_timeout` and its stream inactivity timeout uses `stream_idle_timeout
|
|
224
|
-
|
|
232
|
+
`result_drain_timeout` and its stream inactivity timeout uses `stream_idle_timeout`.
|
|
233
|
+
Background agents, background tools, and compaction use `subagent_stall_timeout`,
|
|
234
|
+
`background_task_stall_timeout`, and `compaction_stall_timeout`, respectively:
|
|
235
|
+
keep these notices in the chat but exclude them from `AgentErrorBanner`. The selector also
|
|
225
236
|
recognizes their exact legacy messages for persisted history. Other errors keep
|
|
226
237
|
their banners. A `session:ended` with reason `watchdog` leaves a manual workspace
|
|
227
238
|
`idle`, unless accompanied by a nonzero exit code. Preserve the watchdog end reason
|
|
@@ -256,7 +267,15 @@ See the "Notion integration" section of the README for the end-user setup guide.
|
|
|
256
267
|
|
|
257
268
|
Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
|
|
258
269
|
|
|
259
|
-
**Claude Code** (`claude-code/`): consumes the `@anthropic-ai/claude-agent-sdk` async iterator. The SDK manages the Claude runtime; Kōbō does not launch a separate `claude` CLI command. Authentication reuses the user's Claude login or `ANTHROPIC_API_KEY`.
|
|
270
|
+
**Claude Code** (`claude-code/`): consumes the `@anthropic-ai/claude-agent-sdk` async iterator. The SDK manages the Claude runtime; Kōbō does not launch a separate `claude` CLI command. Authentication reuses the user's Claude login or `ANTHROPIC_API_KEY`. A settled result gets a **3 s continuation grace**, then a **15 s result-drain watchdog**. Trailing metadata and child output must not cancel the parent's drain/grace. Silence is bounded at **2 minutes** without tools, **30 minutes** for foreground tools and background shell/workflow/MCP jobs, and **10 minutes** for agents. Compaction has an absolute **10-minute** ceiling; repeated status messages cannot extend it. Human questions/permissions pause inactivity; resolution AND SDK cancellation re-evaluate that pause. Watchdogs request abort plus SDK `query.close()`; neither is proof of closure: controller ownership is released only when `EngineProcess.closed` confirms it. Never fake closure or allow a second writer if the SDK ignores termination. End emission is idempotent and timers are cleared on termination and in `finally`.
|
|
271
|
+
|
|
272
|
+
After the continuation grace, Claude explicitly calls `query.close()`; closing
|
|
273
|
+
the input iterator alone can leave the SDK waiting for an idle notification after
|
|
274
|
+
a successful result. The 15-second drain watchdog is a failed-close backstop,
|
|
275
|
+
not normal cleanup. The SDK still constructs its runtime command; Kōbō's
|
|
276
|
+
`spawnClaudeCodeProcess` hook tracks actual child exit and forwards stderr.
|
|
277
|
+
`EngineProcess.closed` waits for both that exit and iterator completion because
|
|
278
|
+
the SDK's internal cleanup wait is bounded and can return before child exit.
|
|
260
279
|
|
|
261
280
|
**OpenAI Codex** (`codex/`): uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
|
|
262
281
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
@@ -438,6 +457,14 @@ One task, two engines, two sibling worktrees. `POST /api/workspaces` accepts a `
|
|
|
438
457
|
|
|
439
458
|
### Workspace templates and duplication
|
|
440
459
|
|
|
460
|
+
Workspace template presets include optional `tags`. New form captures always
|
|
461
|
+
store the selection (including `[]`); legacy omission preserves the current
|
|
462
|
+
form selection. Server reads/writes filter tags against the current global
|
|
463
|
+
catalogue. Catalogue updates and config imports prune saved template references
|
|
464
|
+
before saving settings, so removed tags cannot reappear when re-added later.
|
|
465
|
+
Only affected presets are rewritten; existing workspace tags are untouched.
|
|
466
|
+
A corrupt template file aborts catalogue cleanup without replacing that file.
|
|
467
|
+
|
|
441
468
|
`workspace-template-service.ts` persists named presets of the create form in `<KOBO_HOME>/workspace-templates.json` (same JSON pattern as `templates.json`, no SQLite migration). `sanitizePreset` keeps known keys with the right type and drops the rest, so a hand-edited file degrades to unset fields rather than errors. `presetFromWorkspace(id)` derives the same shape from an existing workspace (tasks as titles, statuses dropped); `GET /api/workspaces/:id/preset` exposes it read-only and "Duplicate" opens `/create?from=<id>` with it. On the client, `utils/workspace-preset.ts` (`capturePreset` / `applyPreset`, pure, round-trip tested) is the single bridge between the form and a preset; the create page applies the engine first and waits a tick so its engine watchers normalise model / effort / permission mode before the preset's values land. See [CONFIGURATION.md → Workspace templates and duplication](CONFIGURATION.md#workspace-templates-and-duplication).
|
|
442
469
|
|
|
443
470
|
### Onboarding tours
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 2.1.12
|
|
8
|
+
|
|
9
|
+
- fix(claude): close settled SDK sessions without false watchdogs
|
|
10
|
+
|
|
11
|
+
## 2.1.11
|
|
12
|
+
|
|
13
|
+
- fix: harden agent lifecycle and workspace workflows
|
|
14
|
+
|
|
7
15
|
## 2.1.10
|
|
8
16
|
|
|
9
17
|
- fix: correct memory budgets and subagent detection
|
package/CONFIGURATION.md
CHANGED
|
@@ -623,7 +623,7 @@ original agent automatically; review output already recorded remains available.
|
|
|
623
623
|
|
|
624
624
|
A template is a named preset of the create-workspace form: project (optional),
|
|
625
625
|
source branch, branch type, engine, model, reasoning effort, permission mode,
|
|
626
|
-
auto-loop settings, description, tasks
|
|
626
|
+
auto-loop settings, description, tasks, acceptance criteria and tags. The workspace
|
|
627
627
|
name and working branch are never part of it - they are derived at each
|
|
628
628
|
creation - and neither are Notion / Sentry / PR URLs, which are one-off
|
|
629
629
|
context.
|
|
@@ -638,6 +638,14 @@ existing name asks before overwriting. Templates are managed (rename, delete,
|
|
|
638
638
|
inspect) in **Settings → Workspace templates**; to edit one, load it on the
|
|
639
639
|
create page, adjust, and save it again under the same name.
|
|
640
640
|
|
|
641
|
+
Templates save the selected tags, including an explicit empty selection. Older
|
|
642
|
+
templates without a tags field leave the current selection unchanged. Removing
|
|
643
|
+
a tag from the global catalogue also removes it from every saved workspace
|
|
644
|
+
template (including catalogue changes through configuration import). Other
|
|
645
|
+
template fields and tags on existing workspaces are preserved. Stale clients
|
|
646
|
+
cannot save removed tags back into templates. If the template file is corrupt,
|
|
647
|
+
the catalogue change is refused and the file is preserved for recovery.
|
|
648
|
+
|
|
641
649
|
**Duplicate**, in a workspace's context menu, opens the create page prefilled
|
|
642
650
|
from that workspace - its settings, description, tasks and criteria (all reset
|
|
643
651
|
to "to do") - with the name suffixed "(copy)". Nothing is created until you
|
|
@@ -647,8 +655,8 @@ brainstorm effort nor "skip setup script", which are creation-time choices.
|
|
|
647
655
|
Archived workspaces can be duplicated too.
|
|
648
656
|
|
|
649
657
|
Templates live in `<KOBO_HOME>/workspace-templates.json` (100 at most). A
|
|
650
|
-
corrupt file
|
|
651
|
-
|
|
658
|
+
corrupt file produces an error and is preserved; restore a valid file before
|
|
659
|
+
saving templates or changing the tag catalogue.
|
|
652
660
|
|
|
653
661
|
## Auto-purge worktree on PR merged
|
|
654
662
|
|
|
@@ -1262,6 +1270,27 @@ Kōbō talks to the embedded [`@anthropic-ai/claude-agent-sdk`](https://github.c
|
|
|
1262
1270
|
|
|
1263
1271
|
The model selector is populated from [`src/shared/models.ts`](./src/shared/models.ts), also used by backend validation. This checkout includes Fable 5.1, Mythos 5, Opus 5, Opus 4.8, Sonnet 5 and older variants. Pick the default in **Settings → Agents**. Catalogue entries describe what Kōbō offers; provider access still depends on your account. `auto` leaves model selection to the runtime.
|
|
1264
1272
|
|
|
1273
|
+
#### Session watchdogs
|
|
1274
|
+
|
|
1275
|
+
Kōbō distinguishes a finished response from a fully closed SDK session. Once a
|
|
1276
|
+
response is settled, it allows 3 seconds for continuation, then explicitly closes
|
|
1277
|
+
the SDK query. Normal cleanup is reported as completion, not a watchdog failure.
|
|
1278
|
+
A 15-second drain watchdog remains if that close fails to end the iterator.
|
|
1279
|
+
Metadata alone cannot extend this cleanup deadline. Writer ownership waits for
|
|
1280
|
+
both iterator completion and confirmed runtime exit, even if the SDK's own
|
|
1281
|
+
bounded cleanup returns earlier.
|
|
1282
|
+
|
|
1283
|
+
Silence limits are 2 minutes without a pending tool, 10 minutes for background
|
|
1284
|
+
agents, and 30 minutes for tools (including background Bash, workflow and MCP
|
|
1285
|
+
jobs). A compaction has a separate absolute 10-minute limit. Waiting for your
|
|
1286
|
+
answer or permission pauses the inactivity watchdog.
|
|
1287
|
+
|
|
1288
|
+
Watchdog notices remain in conversation history, but do not display a red error
|
|
1289
|
+
banner. Other failures retain their banners. Watchdog shutdown requests both
|
|
1290
|
+
cancellation and closure of the SDK transport. Kōbō still requires confirmed
|
|
1291
|
+
closure before another agent can write in the same workspace; a runtime that
|
|
1292
|
+
refuses to stop is not treated as safely terminated.
|
|
1293
|
+
|
|
1265
1294
|
### OpenAI Codex
|
|
1266
1295
|
|
|
1267
1296
|
> Experimental. The Claude Code engine remains the primary, battle-tested path.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
1
3
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
4
|
import { nanoid } from 'nanoid';
|
|
3
5
|
import { isSubagentTask } from '../../../../../shared/subagent-classification.js';
|
|
@@ -35,8 +37,7 @@ export const CLAUDE_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
|
35
37
|
* stream is still reaped once this longer ceiling elapses.
|
|
36
38
|
*/
|
|
37
39
|
export const CLAUDE_TOOL_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
38
|
-
/**
|
|
39
|
-
* liveness deadline take over instead of pausing forever. */
|
|
40
|
+
/** Absolute ceiling on a context compaction, independent of tool idle time. */
|
|
40
41
|
export const COMPACTION_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
41
42
|
// Safety net for a subagent whose terminal `task_notification` carries a
|
|
42
43
|
// status outside the mapper's known-terminal set (SDK schema drift) — it
|
|
@@ -73,7 +74,7 @@ class ClaudeInputStream {
|
|
|
73
74
|
waiting;
|
|
74
75
|
closed = false;
|
|
75
76
|
queuedForcedMessages = 0;
|
|
76
|
-
|
|
77
|
+
unansweredMessages = new Set();
|
|
77
78
|
constructor(initialPrompt) {
|
|
78
79
|
this.messages = [{ message: this.toUserMessage(initialPrompt), forced: false }];
|
|
79
80
|
}
|
|
@@ -95,16 +96,43 @@ class ClaudeInputStream {
|
|
|
95
96
|
this.waiting = undefined;
|
|
96
97
|
wake?.();
|
|
97
98
|
}
|
|
98
|
-
hasUnansweredInput(
|
|
99
|
-
return this.queuedForcedMessages > 0 || this.
|
|
99
|
+
hasUnansweredInput() {
|
|
100
|
+
return this.queuedForcedMessages > 0 || this.unansweredMessages.size > 0;
|
|
101
|
+
}
|
|
102
|
+
acknowledgeResult(result) {
|
|
103
|
+
// One Claude turn can consume several queued prompts. A result counts
|
|
104
|
+
// turns, not messages: only retire the sends explicitly covered by it.
|
|
105
|
+
const ids = [...(result.user_message_uuids ?? []), ...(result.user_message_uuid ? [result.user_message_uuid] : [])];
|
|
106
|
+
if (result.user_message_uuids !== undefined || result.user_message_uuid !== undefined) {
|
|
107
|
+
for (const id of ids)
|
|
108
|
+
this.unansweredMessages.delete(id);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// A legacy result can acknowledge an already delivered input, never a
|
|
112
|
+
// future one (e.g. an interrupted turn's result on resume).
|
|
113
|
+
const oldest = this.unansweredMessages.values().next().value;
|
|
114
|
+
if (oldest)
|
|
115
|
+
this.unansweredMessages.delete(oldest);
|
|
116
|
+
}
|
|
117
|
+
const wake = this.waiting;
|
|
118
|
+
this.waiting = undefined;
|
|
119
|
+
wake?.();
|
|
100
120
|
}
|
|
101
121
|
async *[Symbol.asyncIterator]() {
|
|
102
122
|
while (!this.closed || this.messages.length > 0) {
|
|
123
|
+
// The SDK echoes at most 64 consumed UUIDs per result. Backpressure
|
|
124
|
+
// prevents losing correlation without guessing which inputs were read.
|
|
125
|
+
if (!this.closed && this.unansweredMessages.size >= 64) {
|
|
126
|
+
await new Promise((resolve) => {
|
|
127
|
+
this.waiting = resolve;
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
103
131
|
const next = this.messages.shift();
|
|
104
132
|
if (next) {
|
|
105
133
|
if (next.forced)
|
|
106
134
|
this.queuedForcedMessages--;
|
|
107
|
-
this.
|
|
135
|
+
this.unansweredMessages.add(next.message.uuid);
|
|
108
136
|
yield next.message;
|
|
109
137
|
continue;
|
|
110
138
|
}
|
|
@@ -114,7 +142,7 @@ class ClaudeInputStream {
|
|
|
114
142
|
}
|
|
115
143
|
}
|
|
116
144
|
toUserMessage(text) {
|
|
117
|
-
return { type: 'user', message: { role: 'user', content: text }, parent_tool_use_id: null };
|
|
145
|
+
return { type: 'user', uuid: randomUUID(), message: { role: 'user', content: text }, parent_tool_use_id: null };
|
|
118
146
|
}
|
|
119
147
|
}
|
|
120
148
|
export function createClaudeCodeEngine() {
|
|
@@ -210,6 +238,9 @@ export function createClaudeCodeEngine() {
|
|
|
210
238
|
const onAbort = () => {
|
|
211
239
|
if (pendingResolvers.get(toolCallId) === resolver) {
|
|
212
240
|
pendingResolvers.delete(toolCallId);
|
|
241
|
+
reevaluateLivenessPause();
|
|
242
|
+
if (settlementPending)
|
|
243
|
+
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
213
244
|
const abortError = new Error('Pending user input aborted');
|
|
214
245
|
abortError.name = 'AbortError';
|
|
215
246
|
reject(abortError);
|
|
@@ -297,6 +328,31 @@ export function createClaudeCodeEngine() {
|
|
|
297
328
|
env: options.env,
|
|
298
329
|
});
|
|
299
330
|
sdkOptions.abortController = abortController;
|
|
331
|
+
// SDK cleanup has its own bounded wait. Iterator completion alone does
|
|
332
|
+
// not prove that its runtime exited: retain writer ownership until both.
|
|
333
|
+
let runtimeRunning = false;
|
|
334
|
+
let runtimeClosed = Promise.resolve();
|
|
335
|
+
sdkOptions.spawnClaudeCodeProcess = ({ command, args, cwd, env, signal }) => {
|
|
336
|
+
const child = spawn(command, args, { cwd, env, signal, windowsHide: true, stdio: 'pipe' });
|
|
337
|
+
runtimeRunning = true;
|
|
338
|
+
runtimeClosed = new Promise((resolve) => {
|
|
339
|
+
const exited = () => {
|
|
340
|
+
runtimeRunning = false;
|
|
341
|
+
resolve();
|
|
342
|
+
};
|
|
343
|
+
child.once('exit', exited);
|
|
344
|
+
child.once('error', () => {
|
|
345
|
+
// A failed spawn has no process; an abort/kill error does not
|
|
346
|
+
// establish that an already spawned runtime has actually exited.
|
|
347
|
+
if (child.pid === undefined)
|
|
348
|
+
exited();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
child.stderr.setEncoding('utf8');
|
|
352
|
+
child.stderr.on('data', (data) => sdkOptions.stderr?.(data));
|
|
353
|
+
child.stderr.on('error', () => { });
|
|
354
|
+
return child;
|
|
355
|
+
};
|
|
300
356
|
// Override the SDK's libc-blind binary resolution on Linux glibc — see
|
|
301
357
|
// resolve-binary.ts for the full rationale. No-op on macOS/Windows/musl.
|
|
302
358
|
const explicitBinary = resolveClaudeBinaryPath();
|
|
@@ -304,6 +360,27 @@ export function createClaudeCodeEngine() {
|
|
|
304
360
|
sdkOptions.pathToClaudeCodeExecutable = explicitBinary;
|
|
305
361
|
const inputStream = new ClaudeInputStream(effectivePrompt);
|
|
306
362
|
const q = query({ prompt: inputStream, options: sdkOptions });
|
|
363
|
+
let queryCloseRequested = false;
|
|
364
|
+
const terminateQuery = () => {
|
|
365
|
+
if (queryCloseRequested)
|
|
366
|
+
return;
|
|
367
|
+
queryCloseRequested = true;
|
|
368
|
+
inputStream.close();
|
|
369
|
+
abortController.abort();
|
|
370
|
+
turnLiveness.stop();
|
|
371
|
+
clearContinuationGrace();
|
|
372
|
+
clearResultDrainWatchdog();
|
|
373
|
+
clearSubagentStallWatchdog();
|
|
374
|
+
clearCompactionStallTimer();
|
|
375
|
+
// Abort alone can leave the SDK iterator parked. close() explicitly
|
|
376
|
+
// tears down its transport/process. Ownership still waits for closed.
|
|
377
|
+
try {
|
|
378
|
+
q.close?.();
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
console.warn('[claude-engine] SDK transport close failed:', error);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
307
384
|
// Best-effort SDK stop of one background task; failures are ignored.
|
|
308
385
|
const requestStopTask = (taskId) => {
|
|
309
386
|
try {
|
|
@@ -344,6 +421,9 @@ export function createClaudeCodeEngine() {
|
|
|
344
421
|
const safeEmit = (ev) => streamingBatcher.push(ev);
|
|
345
422
|
let iteratorRunning = false;
|
|
346
423
|
let userInterrupted = false;
|
|
424
|
+
// Normal post-result disposal is distinct from a watchdog/user abort.
|
|
425
|
+
// The SDK can wait for an `idle` state even after stdin's iterable ends.
|
|
426
|
+
let settledCloseRequested = false;
|
|
347
427
|
let completedResponses = 0;
|
|
348
428
|
let waitingForBackground = false;
|
|
349
429
|
let turnCompletedEmittedForResponse = 0;
|
|
@@ -356,7 +436,9 @@ export function createClaudeCodeEngine() {
|
|
|
356
436
|
completedResponses === turnCompletedEmittedForResponse ||
|
|
357
437
|
activeSubagentTaskIds.size > 0 ||
|
|
358
438
|
pendingResolvers.size > 0 ||
|
|
359
|
-
|
|
439
|
+
isCompacting ||
|
|
440
|
+
pendingToolCallIds.size > 0 ||
|
|
441
|
+
inputStream.hasUnansweredInput()) {
|
|
360
442
|
return;
|
|
361
443
|
}
|
|
362
444
|
turnCompletedEmittedForResponse = completedResponses;
|
|
@@ -366,6 +448,7 @@ export function createClaudeCodeEngine() {
|
|
|
366
448
|
// catch block) never both emit `session:ended` for the same run.
|
|
367
449
|
let sessionEndedEmitted = false;
|
|
368
450
|
let isCompacting = false;
|
|
451
|
+
let settlementPending = false;
|
|
369
452
|
const emitSessionEnded = (reason, exitCode) => {
|
|
370
453
|
if (sessionEndedEmitted)
|
|
371
454
|
return;
|
|
@@ -384,7 +467,7 @@ export function createClaudeCodeEngine() {
|
|
|
384
467
|
// its task notification and an automatic continuation turn.
|
|
385
468
|
let resultDrainTimer;
|
|
386
469
|
// D2 — one shared liveness module for both engines. Armed before the
|
|
387
|
-
// first SDK message, re-armed on
|
|
470
|
+
// first SDK message, re-armed on meaningful progress, suspended while a human
|
|
388
471
|
// decision or a background subagent is outstanding, stopped in `finally`.
|
|
389
472
|
const turnLiveness = createTurnLiveness({
|
|
390
473
|
timeoutMs: CLAUDE_STREAM_IDLE_TIMEOUT_MS,
|
|
@@ -408,7 +491,7 @@ export function createClaudeCodeEngine() {
|
|
|
408
491
|
});
|
|
409
492
|
emitSessionEnded('watchdog', null);
|
|
410
493
|
}
|
|
411
|
-
|
|
494
|
+
terminateQuery();
|
|
412
495
|
},
|
|
413
496
|
});
|
|
414
497
|
// The single source of truth for "should the idle deadline be paused
|
|
@@ -430,9 +513,8 @@ export function createClaudeCodeEngine() {
|
|
|
430
513
|
// Backstop for a compaction that never reports completion (stuck
|
|
431
514
|
// `session:compacting` with `active: true` forever): `isCompacting`
|
|
432
515
|
// pauses `turnLiveness` indefinitely, and a wedged-but-alive generator
|
|
433
|
-
// never trips `isAlive()`'s own sweep either.
|
|
434
|
-
//
|
|
435
|
-
// / short deadline can reap it, mirroring `armSubagentStallWatchdog`.
|
|
516
|
+
// never trips `isAlive()`'s own sweep either. End it at the absolute
|
|
517
|
+
// ceiling instead of adding another tool-aware idle window afterward.
|
|
436
518
|
let compactionStallTimer;
|
|
437
519
|
const clearCompactionStallTimer = () => {
|
|
438
520
|
if (!compactionStallTimer)
|
|
@@ -441,15 +523,23 @@ export function createClaudeCodeEngine() {
|
|
|
441
523
|
compactionStallTimer = undefined;
|
|
442
524
|
};
|
|
443
525
|
const armCompactionStallTimer = () => {
|
|
444
|
-
|
|
526
|
+
if (compactionStallTimer)
|
|
527
|
+
return;
|
|
445
528
|
compactionStallTimer = setTimeout(() => {
|
|
446
529
|
compactionStallTimer = undefined;
|
|
447
530
|
if (!isCompacting)
|
|
448
531
|
return;
|
|
449
|
-
console.warn(`[claude-engine] Compaction still reported active ${COMPACTION_STALL_TIMEOUT_MS}ms after it started —
|
|
532
|
+
console.warn(`[claude-engine] Compaction still reported active ${COMPACTION_STALL_TIMEOUT_MS}ms after it started — closing the session.`);
|
|
450
533
|
isCompacting = false;
|
|
451
534
|
safeEmit({ kind: 'session:compacting', active: false });
|
|
452
|
-
|
|
535
|
+
safeEmit({
|
|
536
|
+
kind: 'error',
|
|
537
|
+
category: 'other',
|
|
538
|
+
code: 'compaction_stall_timeout',
|
|
539
|
+
message: 'Session force-ended: context compaction stopped reporting completion (watchdog).',
|
|
540
|
+
});
|
|
541
|
+
emitSessionEnded('watchdog', null);
|
|
542
|
+
terminateQuery();
|
|
453
543
|
}, COMPACTION_STALL_TIMEOUT_MS);
|
|
454
544
|
compactionStallTimer.unref?.();
|
|
455
545
|
};
|
|
@@ -471,7 +561,7 @@ export function createClaudeCodeEngine() {
|
|
|
471
561
|
});
|
|
472
562
|
emitSessionEnded('watchdog', null);
|
|
473
563
|
}
|
|
474
|
-
|
|
564
|
+
terminateQuery();
|
|
475
565
|
}, RESULT_DRAIN_TIMEOUT_MS);
|
|
476
566
|
resultDrainTimer.unref?.();
|
|
477
567
|
};
|
|
@@ -495,16 +585,33 @@ export function createClaudeCodeEngine() {
|
|
|
495
585
|
clearContinuationGrace();
|
|
496
586
|
continuationGraceTimer = setTimeout(() => {
|
|
497
587
|
continuationGraceTimer = undefined;
|
|
498
|
-
if (
|
|
588
|
+
if (completedResponses === 0 ||
|
|
589
|
+
!settlementPending ||
|
|
590
|
+
activeSubagentTaskIds.size > 0 ||
|
|
499
591
|
pendingResolvers.size > 0 ||
|
|
500
592
|
pendingToolCallIds.size > 0 ||
|
|
501
|
-
|
|
593
|
+
isCompacting ||
|
|
594
|
+
inputStream.hasUnansweredInput())
|
|
502
595
|
return;
|
|
503
596
|
// Settled only now: a result the CLI follows up on (e.g. a resumed
|
|
504
597
|
// turn's empty result) must not hide the busy banner meanwhile.
|
|
505
598
|
emitTurnCompletedIfSettled();
|
|
506
599
|
inputStream.close();
|
|
507
600
|
armResultDrainWatchdog();
|
|
601
|
+
// Closing the input iterable alone leaves the SDK in waitForRunEnd
|
|
602
|
+
// when its last session state is still running. Dispose explicitly,
|
|
603
|
+
// but let the iterator confirm actual closure before session:ended.
|
|
604
|
+
// The drain watchdog now guards a FAILED close, not normal cleanup.
|
|
605
|
+
if (!settledCloseRequested && typeof q.close === 'function') {
|
|
606
|
+
settledCloseRequested = true;
|
|
607
|
+
try {
|
|
608
|
+
q.close();
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
settledCloseRequested = false;
|
|
612
|
+
console.warn('[claude-engine] Post-result SDK close failed:', error);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
508
615
|
}, delayMs);
|
|
509
616
|
continuationGraceTimer.unref?.();
|
|
510
617
|
};
|
|
@@ -525,19 +632,26 @@ export function createClaudeCodeEngine() {
|
|
|
525
632
|
clearSubagentStallWatchdog();
|
|
526
633
|
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
527
634
|
return;
|
|
635
|
+
const hasAgent = [...activeSubagentTaskIds].some((id) => isSubagentTask({ taskType: mapperState.taskTypes.get(id) }));
|
|
636
|
+
// Shell/workflow/MCP jobs do not emit agent progress during a long
|
|
637
|
+
// command. Give them the same silence allowance as foreground tools.
|
|
638
|
+
const timeoutMs = hasAgent ? SUBAGENT_STALL_TIMEOUT_MS : CLAUDE_TOOL_IDLE_TIMEOUT_MS;
|
|
528
639
|
subagentStallTimer = setTimeout(() => {
|
|
529
640
|
subagentStallTimer = undefined;
|
|
530
641
|
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
531
642
|
return;
|
|
532
|
-
console.warn(`[claude-engine] No
|
|
643
|
+
console.warn(`[claude-engine] No background activity for ${timeoutMs}ms — forcing session:ended`);
|
|
533
644
|
safeEmit({
|
|
534
645
|
kind: 'error',
|
|
535
646
|
category: 'other',
|
|
536
|
-
message:
|
|
647
|
+
message: hasAgent
|
|
648
|
+
? 'Session force-ended: background subagents stopped reporting activity (watchdog).'
|
|
649
|
+
: 'Session force-ended: background tools stopped reporting activity (watchdog).',
|
|
650
|
+
code: hasAgent ? 'subagent_stall_timeout' : 'background_task_stall_timeout',
|
|
537
651
|
});
|
|
538
652
|
emitSessionEnded('watchdog', null);
|
|
539
|
-
|
|
540
|
-
},
|
|
653
|
+
terminateQuery();
|
|
654
|
+
}, timeoutMs);
|
|
541
655
|
subagentStallTimer.unref?.();
|
|
542
656
|
};
|
|
543
657
|
const iteratorPromise = (async () => {
|
|
@@ -545,22 +659,27 @@ export function createClaudeCodeEngine() {
|
|
|
545
659
|
turnLiveness.start();
|
|
546
660
|
try {
|
|
547
661
|
for await (const msg of q) {
|
|
662
|
+
const events = mapSdkMessage(msg, mapperState);
|
|
663
|
+
const isForeground = !('parent_tool_use_id' in msg && msg.parent_tool_use_id != null);
|
|
664
|
+
const foregroundProgress = isForeground &&
|
|
665
|
+
events.some((ev) => ev.kind === 'message:text' ||
|
|
666
|
+
ev.kind === 'message:thinking' ||
|
|
667
|
+
ev.kind === 'tool:call' ||
|
|
668
|
+
ev.kind === 'tool:result');
|
|
548
669
|
// A parent continuation owns the foreground again. Background
|
|
549
670
|
// progress notifications alone do not end the between-turn wait.
|
|
550
|
-
if (
|
|
671
|
+
if (foregroundProgress) {
|
|
551
672
|
waitingForBackground = false;
|
|
673
|
+
settlementPending = false;
|
|
552
674
|
clearContinuationGrace();
|
|
675
|
+
clearResultDrainWatchdog();
|
|
553
676
|
}
|
|
554
677
|
else if (msg.type === 'system' && msg.subtype === 'init') {
|
|
555
678
|
// A new run starts on this stream (e.g. after a resumed turn's empty result).
|
|
556
679
|
clearContinuationGrace();
|
|
680
|
+
settlementPending = false;
|
|
681
|
+
clearResultDrainWatchdog();
|
|
557
682
|
}
|
|
558
|
-
// This SDK message proves that any drain condition armed by a
|
|
559
|
-
// *previous* message is no longer current. A drain armed while
|
|
560
|
-
// processing this message remains active if the generator then
|
|
561
|
-
// goes silent.
|
|
562
|
-
clearResultDrainWatchdog();
|
|
563
|
-
const events = mapSdkMessage(msg, mapperState);
|
|
564
683
|
for (const ev of events) {
|
|
565
684
|
if (ev.kind === 'subagent:progress')
|
|
566
685
|
trackSubagentProgress(ev);
|
|
@@ -578,17 +697,26 @@ export function createClaudeCodeEngine() {
|
|
|
578
697
|
armSubagentStallWatchdog();
|
|
579
698
|
for (const ev of events) {
|
|
580
699
|
if (ev.kind === 'session:compacting') {
|
|
700
|
+
const wasCompacting = isCompacting;
|
|
581
701
|
isCompacting = ev.active;
|
|
582
|
-
if (ev.active)
|
|
702
|
+
if (ev.active) {
|
|
703
|
+
clearContinuationGrace();
|
|
704
|
+
clearResultDrainWatchdog();
|
|
583
705
|
armCompactionStallTimer();
|
|
584
|
-
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
585
708
|
clearCompactionStallTimer();
|
|
709
|
+
if (wasCompacting && settlementPending)
|
|
710
|
+
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
711
|
+
}
|
|
586
712
|
}
|
|
587
713
|
// Older SDKs can emit only compact_boundary, without a trailing
|
|
588
714
|
// status update. This boundary marks compaction as complete.
|
|
589
715
|
else if (ev.kind === 'session:compacted') {
|
|
590
716
|
isCompacting = false;
|
|
591
717
|
clearCompactionStallTimer();
|
|
718
|
+
if (settlementPending)
|
|
719
|
+
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
592
720
|
}
|
|
593
721
|
}
|
|
594
722
|
// Actual foreground output proves work resumed even if the SDK
|
|
@@ -602,18 +730,16 @@ export function createClaudeCodeEngine() {
|
|
|
602
730
|
safeEmit({ kind: 'session:compacting', active: false });
|
|
603
731
|
}
|
|
604
732
|
for (const ev of events) {
|
|
605
|
-
if (ev.kind === 'tool:call')
|
|
733
|
+
if (isForeground && ev.kind === 'tool:call')
|
|
606
734
|
pendingToolCallIds.add(ev.toolCallId);
|
|
607
|
-
else if (ev.kind === 'tool:result')
|
|
735
|
+
else if (isForeground && ev.kind === 'tool:result')
|
|
608
736
|
pendingToolCallIds.delete(ev.toolCallId);
|
|
609
737
|
}
|
|
610
738
|
// After a settled result, the last background completion drains
|
|
611
739
|
// the stream only if the parent does not continue (see
|
|
612
740
|
// BACKGROUND_CONTINUATION_GRACE_MS): its continuation still needs
|
|
613
741
|
// the input for permission requests.
|
|
614
|
-
if (subagentStallTimer &&
|
|
615
|
-
activeSubagentTaskIds.size === 0 &&
|
|
616
|
-
!inputStream.hasUnansweredInput(completedResponses)) {
|
|
742
|
+
if (subagentStallTimer && activeSubagentTaskIds.size === 0 && !inputStream.hasUnansweredInput()) {
|
|
617
743
|
clearSubagentStallWatchdog();
|
|
618
744
|
armContinuationGrace(BACKGROUND_CONTINUATION_GRACE_MS);
|
|
619
745
|
}
|
|
@@ -622,9 +748,12 @@ export function createClaudeCodeEngine() {
|
|
|
622
748
|
discoveredSessionId = ev.engineSessionId;
|
|
623
749
|
safeEmit(ev);
|
|
624
750
|
}
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
|
|
751
|
+
// Transport metadata is not forward progress. In particular a
|
|
752
|
+
// stream of trailing notifications must not defeat shutdown.
|
|
753
|
+
if (foregroundProgress ||
|
|
754
|
+
msg.type === 'result' ||
|
|
755
|
+
events.some((ev) => ev.kind === 'subagent:progress' && !ev.ambient))
|
|
756
|
+
turnLiveness.activity();
|
|
628
757
|
// A background subagent or a pending permission card can stay
|
|
629
758
|
// legitimately quiet for minutes; their own dedicated watchdogs
|
|
630
759
|
// own those windows, so suspend the turn deadline meanwhile.
|
|
@@ -634,11 +763,14 @@ export function createClaudeCodeEngine() {
|
|
|
634
763
|
reevaluateLivenessPause();
|
|
635
764
|
if (msg.type === 'result') {
|
|
636
765
|
waitingForBackground = false;
|
|
766
|
+
settlementPending = true;
|
|
637
767
|
clearContinuationGrace();
|
|
638
768
|
pendingToolCallIds.clear();
|
|
769
|
+
turnLiveness.setTimeoutMs(CLAUDE_STREAM_IDLE_TIMEOUT_MS);
|
|
639
770
|
completedResponses++;
|
|
771
|
+
inputStream.acknowledgeResult(msg);
|
|
640
772
|
// A queued forced message starts the next response on this same SDK stream.
|
|
641
|
-
if (!inputStream.hasUnansweredInput(
|
|
773
|
+
if (!inputStream.hasUnansweredInput()) {
|
|
642
774
|
if (activeSubagentTaskIds.size === 0) {
|
|
643
775
|
clearSubagentStallWatchdog();
|
|
644
776
|
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
@@ -670,7 +802,12 @@ export function createClaudeCodeEngine() {
|
|
|
670
802
|
abortController.signal.aborted ||
|
|
671
803
|
/aborted by user|process aborted|abortError|ede_diagnostic/i.test(error.message ?? '');
|
|
672
804
|
if (isAbort) {
|
|
673
|
-
|
|
805
|
+
if (settledCloseRequested && !userInterrupted && !abortController.signal.aborted) {
|
|
806
|
+
const reason = mapperState.sawErrorResult ? 'error' : 'completed';
|
|
807
|
+
emitSessionEnded(reason, reason === 'completed' ? 0 : null);
|
|
808
|
+
}
|
|
809
|
+
else
|
|
810
|
+
emitSessionEnded('killed', null);
|
|
674
811
|
}
|
|
675
812
|
else {
|
|
676
813
|
safeEmit({
|
|
@@ -710,7 +847,7 @@ export function createClaudeCodeEngine() {
|
|
|
710
847
|
})();
|
|
711
848
|
const engineProcess = {
|
|
712
849
|
ready,
|
|
713
|
-
closed: iteratorPromise.then(() =>
|
|
850
|
+
closed: iteratorPromise.then(() => runtimeClosed),
|
|
714
851
|
get pid() {
|
|
715
852
|
return undefined;
|
|
716
853
|
},
|
|
@@ -718,10 +855,10 @@ export function createClaudeCodeEngine() {
|
|
|
718
855
|
return discoveredSessionId;
|
|
719
856
|
},
|
|
720
857
|
isAlive() {
|
|
721
|
-
return iteratorRunning;
|
|
858
|
+
return iteratorRunning || runtimeRunning;
|
|
722
859
|
},
|
|
723
860
|
sendMessage(text) {
|
|
724
|
-
if (!iteratorRunning)
|
|
861
|
+
if (!iteratorRunning || queryCloseRequested || settledCloseRequested || sessionEndedEmitted)
|
|
725
862
|
throw new Error(`Claude ${AGENT_NO_LONGER_RUNNING_TEXT}`);
|
|
726
863
|
inputStream.send(text);
|
|
727
864
|
armSubagentStallWatchdog();
|
|
@@ -735,7 +872,7 @@ export function createClaudeCodeEngine() {
|
|
|
735
872
|
pendingToolCallIds.size > 0 ||
|
|
736
873
|
pendingResolvers.size > 0 ||
|
|
737
874
|
isCompacting ||
|
|
738
|
-
inputStream.hasUnansweredInput(
|
|
875
|
+
inputStream.hasUnansweredInput())
|
|
739
876
|
return false;
|
|
740
877
|
inputStream.send(text);
|
|
741
878
|
waitingForBackground = false;
|
|
@@ -800,7 +937,7 @@ export function createClaudeCodeEngine() {
|
|
|
800
937
|
}
|
|
801
938
|
},
|
|
802
939
|
async stop() {
|
|
803
|
-
|
|
940
|
+
terminateQuery();
|
|
804
941
|
try {
|
|
805
942
|
await iteratorPromise;
|
|
806
943
|
}
|