@loicngr/kobo 2.1.10 → 2.1.11
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 +22 -3
- package/CHANGELOG.md +4 -0
- package/CONFIGURATION.md +28 -3
- package/dist/server/services/agent/engines/claude-code/engine.js +130 -43
- 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,7 @@ 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`.
|
|
260
271
|
|
|
261
272
|
**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
273
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
@@ -438,6 +449,14 @@ One task, two engines, two sibling worktrees. `POST /api/workspaces` accepts a `
|
|
|
438
449
|
|
|
439
450
|
### Workspace templates and duplication
|
|
440
451
|
|
|
452
|
+
Workspace template presets include optional `tags`. New form captures always
|
|
453
|
+
store the selection (including `[]`); legacy omission preserves the current
|
|
454
|
+
form selection. Server reads/writes filter tags against the current global
|
|
455
|
+
catalogue. Catalogue updates and config imports prune saved template references
|
|
456
|
+
before saving settings, so removed tags cannot reappear when re-added later.
|
|
457
|
+
Only affected presets are rewritten; existing workspace tags are untouched.
|
|
458
|
+
A corrupt template file aborts catalogue cleanup without replacing that file.
|
|
459
|
+
|
|
441
460
|
`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
461
|
|
|
443
462
|
### Onboarding tours
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ 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.11
|
|
8
|
+
|
|
9
|
+
- fix: harden agent lifecycle and workspace workflows
|
|
10
|
+
|
|
7
11
|
## 2.1.10
|
|
8
12
|
|
|
9
13
|
- 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,23 @@ 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 15 seconds for
|
|
1277
|
+
SDK cleanup. Metadata alone cannot extend this cleanup deadline.
|
|
1278
|
+
|
|
1279
|
+
Silence limits are 2 minutes without a pending tool, 10 minutes for background
|
|
1280
|
+
agents, and 30 minutes for tools (including background Bash, workflow and MCP
|
|
1281
|
+
jobs). A compaction has a separate absolute 10-minute limit. Waiting for your
|
|
1282
|
+
answer or permission pauses the inactivity watchdog.
|
|
1283
|
+
|
|
1284
|
+
Watchdog notices remain in conversation history, but do not display a red error
|
|
1285
|
+
banner. Other failures retain their banners. Watchdog shutdown requests both
|
|
1286
|
+
cancellation and closure of the SDK transport. Kōbō still requires confirmed
|
|
1287
|
+
closure before another agent can write in the same workspace; a runtime that
|
|
1288
|
+
refuses to stop is not treated as safely terminated.
|
|
1289
|
+
|
|
1265
1290
|
### OpenAI Codex
|
|
1266
1291
|
|
|
1267
1292
|
> Experimental. The Claude Code engine remains the primary, battle-tested path.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
3
|
import { nanoid } from 'nanoid';
|
|
3
4
|
import { isSubagentTask } from '../../../../../shared/subagent-classification.js';
|
|
@@ -35,8 +36,7 @@ export const CLAUDE_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
|
35
36
|
* stream is still reaped once this longer ceiling elapses.
|
|
36
37
|
*/
|
|
37
38
|
export const CLAUDE_TOOL_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
38
|
-
/**
|
|
39
|
-
* liveness deadline take over instead of pausing forever. */
|
|
39
|
+
/** Absolute ceiling on a context compaction, independent of tool idle time. */
|
|
40
40
|
export const COMPACTION_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
41
41
|
// Safety net for a subagent whose terminal `task_notification` carries a
|
|
42
42
|
// status outside the mapper's known-terminal set (SDK schema drift) — it
|
|
@@ -73,7 +73,7 @@ class ClaudeInputStream {
|
|
|
73
73
|
waiting;
|
|
74
74
|
closed = false;
|
|
75
75
|
queuedForcedMessages = 0;
|
|
76
|
-
|
|
76
|
+
unansweredMessages = new Set();
|
|
77
77
|
constructor(initialPrompt) {
|
|
78
78
|
this.messages = [{ message: this.toUserMessage(initialPrompt), forced: false }];
|
|
79
79
|
}
|
|
@@ -95,16 +95,43 @@ class ClaudeInputStream {
|
|
|
95
95
|
this.waiting = undefined;
|
|
96
96
|
wake?.();
|
|
97
97
|
}
|
|
98
|
-
hasUnansweredInput(
|
|
99
|
-
return this.queuedForcedMessages > 0 || this.
|
|
98
|
+
hasUnansweredInput() {
|
|
99
|
+
return this.queuedForcedMessages > 0 || this.unansweredMessages.size > 0;
|
|
100
|
+
}
|
|
101
|
+
acknowledgeResult(result) {
|
|
102
|
+
// One Claude turn can consume several queued prompts. A result counts
|
|
103
|
+
// turns, not messages: only retire the sends explicitly covered by it.
|
|
104
|
+
const ids = [...(result.user_message_uuids ?? []), ...(result.user_message_uuid ? [result.user_message_uuid] : [])];
|
|
105
|
+
if (result.user_message_uuids !== undefined || result.user_message_uuid !== undefined) {
|
|
106
|
+
for (const id of ids)
|
|
107
|
+
this.unansweredMessages.delete(id);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// A legacy result can acknowledge an already delivered input, never a
|
|
111
|
+
// future one (e.g. an interrupted turn's result on resume).
|
|
112
|
+
const oldest = this.unansweredMessages.values().next().value;
|
|
113
|
+
if (oldest)
|
|
114
|
+
this.unansweredMessages.delete(oldest);
|
|
115
|
+
}
|
|
116
|
+
const wake = this.waiting;
|
|
117
|
+
this.waiting = undefined;
|
|
118
|
+
wake?.();
|
|
100
119
|
}
|
|
101
120
|
async *[Symbol.asyncIterator]() {
|
|
102
121
|
while (!this.closed || this.messages.length > 0) {
|
|
122
|
+
// The SDK echoes at most 64 consumed UUIDs per result. Backpressure
|
|
123
|
+
// prevents losing correlation without guessing which inputs were read.
|
|
124
|
+
if (!this.closed && this.unansweredMessages.size >= 64) {
|
|
125
|
+
await new Promise((resolve) => {
|
|
126
|
+
this.waiting = resolve;
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
103
130
|
const next = this.messages.shift();
|
|
104
131
|
if (next) {
|
|
105
132
|
if (next.forced)
|
|
106
133
|
this.queuedForcedMessages--;
|
|
107
|
-
this.
|
|
134
|
+
this.unansweredMessages.add(next.message.uuid);
|
|
108
135
|
yield next.message;
|
|
109
136
|
continue;
|
|
110
137
|
}
|
|
@@ -114,7 +141,7 @@ class ClaudeInputStream {
|
|
|
114
141
|
}
|
|
115
142
|
}
|
|
116
143
|
toUserMessage(text) {
|
|
117
|
-
return { type: 'user', message: { role: 'user', content: text }, parent_tool_use_id: null };
|
|
144
|
+
return { type: 'user', uuid: randomUUID(), message: { role: 'user', content: text }, parent_tool_use_id: null };
|
|
118
145
|
}
|
|
119
146
|
}
|
|
120
147
|
export function createClaudeCodeEngine() {
|
|
@@ -210,6 +237,9 @@ export function createClaudeCodeEngine() {
|
|
|
210
237
|
const onAbort = () => {
|
|
211
238
|
if (pendingResolvers.get(toolCallId) === resolver) {
|
|
212
239
|
pendingResolvers.delete(toolCallId);
|
|
240
|
+
reevaluateLivenessPause();
|
|
241
|
+
if (settlementPending)
|
|
242
|
+
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
213
243
|
const abortError = new Error('Pending user input aborted');
|
|
214
244
|
abortError.name = 'AbortError';
|
|
215
245
|
reject(abortError);
|
|
@@ -304,6 +334,27 @@ export function createClaudeCodeEngine() {
|
|
|
304
334
|
sdkOptions.pathToClaudeCodeExecutable = explicitBinary;
|
|
305
335
|
const inputStream = new ClaudeInputStream(effectivePrompt);
|
|
306
336
|
const q = query({ prompt: inputStream, options: sdkOptions });
|
|
337
|
+
let queryCloseRequested = false;
|
|
338
|
+
const terminateQuery = () => {
|
|
339
|
+
if (queryCloseRequested)
|
|
340
|
+
return;
|
|
341
|
+
queryCloseRequested = true;
|
|
342
|
+
inputStream.close();
|
|
343
|
+
abortController.abort();
|
|
344
|
+
turnLiveness.stop();
|
|
345
|
+
clearContinuationGrace();
|
|
346
|
+
clearResultDrainWatchdog();
|
|
347
|
+
clearSubagentStallWatchdog();
|
|
348
|
+
clearCompactionStallTimer();
|
|
349
|
+
// Abort alone can leave the SDK iterator parked. close() explicitly
|
|
350
|
+
// tears down its transport/process. Ownership still waits for closed.
|
|
351
|
+
try {
|
|
352
|
+
q.close?.();
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
console.warn('[claude-engine] SDK transport close failed:', error);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
307
358
|
// Best-effort SDK stop of one background task; failures are ignored.
|
|
308
359
|
const requestStopTask = (taskId) => {
|
|
309
360
|
try {
|
|
@@ -356,7 +407,9 @@ export function createClaudeCodeEngine() {
|
|
|
356
407
|
completedResponses === turnCompletedEmittedForResponse ||
|
|
357
408
|
activeSubagentTaskIds.size > 0 ||
|
|
358
409
|
pendingResolvers.size > 0 ||
|
|
359
|
-
|
|
410
|
+
isCompacting ||
|
|
411
|
+
pendingToolCallIds.size > 0 ||
|
|
412
|
+
inputStream.hasUnansweredInput()) {
|
|
360
413
|
return;
|
|
361
414
|
}
|
|
362
415
|
turnCompletedEmittedForResponse = completedResponses;
|
|
@@ -366,6 +419,7 @@ export function createClaudeCodeEngine() {
|
|
|
366
419
|
// catch block) never both emit `session:ended` for the same run.
|
|
367
420
|
let sessionEndedEmitted = false;
|
|
368
421
|
let isCompacting = false;
|
|
422
|
+
let settlementPending = false;
|
|
369
423
|
const emitSessionEnded = (reason, exitCode) => {
|
|
370
424
|
if (sessionEndedEmitted)
|
|
371
425
|
return;
|
|
@@ -384,7 +438,7 @@ export function createClaudeCodeEngine() {
|
|
|
384
438
|
// its task notification and an automatic continuation turn.
|
|
385
439
|
let resultDrainTimer;
|
|
386
440
|
// D2 — one shared liveness module for both engines. Armed before the
|
|
387
|
-
// first SDK message, re-armed on
|
|
441
|
+
// first SDK message, re-armed on meaningful progress, suspended while a human
|
|
388
442
|
// decision or a background subagent is outstanding, stopped in `finally`.
|
|
389
443
|
const turnLiveness = createTurnLiveness({
|
|
390
444
|
timeoutMs: CLAUDE_STREAM_IDLE_TIMEOUT_MS,
|
|
@@ -408,7 +462,7 @@ export function createClaudeCodeEngine() {
|
|
|
408
462
|
});
|
|
409
463
|
emitSessionEnded('watchdog', null);
|
|
410
464
|
}
|
|
411
|
-
|
|
465
|
+
terminateQuery();
|
|
412
466
|
},
|
|
413
467
|
});
|
|
414
468
|
// The single source of truth for "should the idle deadline be paused
|
|
@@ -430,9 +484,8 @@ export function createClaudeCodeEngine() {
|
|
|
430
484
|
// Backstop for a compaction that never reports completion (stuck
|
|
431
485
|
// `session:compacting` with `active: true` forever): `isCompacting`
|
|
432
486
|
// 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`.
|
|
487
|
+
// never trips `isAlive()`'s own sweep either. End it at the absolute
|
|
488
|
+
// ceiling instead of adding another tool-aware idle window afterward.
|
|
436
489
|
let compactionStallTimer;
|
|
437
490
|
const clearCompactionStallTimer = () => {
|
|
438
491
|
if (!compactionStallTimer)
|
|
@@ -441,15 +494,23 @@ export function createClaudeCodeEngine() {
|
|
|
441
494
|
compactionStallTimer = undefined;
|
|
442
495
|
};
|
|
443
496
|
const armCompactionStallTimer = () => {
|
|
444
|
-
|
|
497
|
+
if (compactionStallTimer)
|
|
498
|
+
return;
|
|
445
499
|
compactionStallTimer = setTimeout(() => {
|
|
446
500
|
compactionStallTimer = undefined;
|
|
447
501
|
if (!isCompacting)
|
|
448
502
|
return;
|
|
449
|
-
console.warn(`[claude-engine] Compaction still reported active ${COMPACTION_STALL_TIMEOUT_MS}ms after it started —
|
|
503
|
+
console.warn(`[claude-engine] Compaction still reported active ${COMPACTION_STALL_TIMEOUT_MS}ms after it started — closing the session.`);
|
|
450
504
|
isCompacting = false;
|
|
451
505
|
safeEmit({ kind: 'session:compacting', active: false });
|
|
452
|
-
|
|
506
|
+
safeEmit({
|
|
507
|
+
kind: 'error',
|
|
508
|
+
category: 'other',
|
|
509
|
+
code: 'compaction_stall_timeout',
|
|
510
|
+
message: 'Session force-ended: context compaction stopped reporting completion (watchdog).',
|
|
511
|
+
});
|
|
512
|
+
emitSessionEnded('watchdog', null);
|
|
513
|
+
terminateQuery();
|
|
453
514
|
}, COMPACTION_STALL_TIMEOUT_MS);
|
|
454
515
|
compactionStallTimer.unref?.();
|
|
455
516
|
};
|
|
@@ -471,7 +532,7 @@ export function createClaudeCodeEngine() {
|
|
|
471
532
|
});
|
|
472
533
|
emitSessionEnded('watchdog', null);
|
|
473
534
|
}
|
|
474
|
-
|
|
535
|
+
terminateQuery();
|
|
475
536
|
}, RESULT_DRAIN_TIMEOUT_MS);
|
|
476
537
|
resultDrainTimer.unref?.();
|
|
477
538
|
};
|
|
@@ -498,7 +559,8 @@ export function createClaudeCodeEngine() {
|
|
|
498
559
|
if (activeSubagentTaskIds.size > 0 ||
|
|
499
560
|
pendingResolvers.size > 0 ||
|
|
500
561
|
pendingToolCallIds.size > 0 ||
|
|
501
|
-
|
|
562
|
+
isCompacting ||
|
|
563
|
+
inputStream.hasUnansweredInput())
|
|
502
564
|
return;
|
|
503
565
|
// Settled only now: a result the CLI follows up on (e.g. a resumed
|
|
504
566
|
// turn's empty result) must not hide the busy banner meanwhile.
|
|
@@ -525,19 +587,26 @@ export function createClaudeCodeEngine() {
|
|
|
525
587
|
clearSubagentStallWatchdog();
|
|
526
588
|
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
527
589
|
return;
|
|
590
|
+
const hasAgent = [...activeSubagentTaskIds].some((id) => isSubagentTask({ taskType: mapperState.taskTypes.get(id) }));
|
|
591
|
+
// Shell/workflow/MCP jobs do not emit agent progress during a long
|
|
592
|
+
// command. Give them the same silence allowance as foreground tools.
|
|
593
|
+
const timeoutMs = hasAgent ? SUBAGENT_STALL_TIMEOUT_MS : CLAUDE_TOOL_IDLE_TIMEOUT_MS;
|
|
528
594
|
subagentStallTimer = setTimeout(() => {
|
|
529
595
|
subagentStallTimer = undefined;
|
|
530
596
|
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
531
597
|
return;
|
|
532
|
-
console.warn(`[claude-engine] No
|
|
598
|
+
console.warn(`[claude-engine] No background activity for ${timeoutMs}ms — forcing session:ended`);
|
|
533
599
|
safeEmit({
|
|
534
600
|
kind: 'error',
|
|
535
601
|
category: 'other',
|
|
536
|
-
message:
|
|
602
|
+
message: hasAgent
|
|
603
|
+
? 'Session force-ended: background subagents stopped reporting activity (watchdog).'
|
|
604
|
+
: 'Session force-ended: background tools stopped reporting activity (watchdog).',
|
|
605
|
+
code: hasAgent ? 'subagent_stall_timeout' : 'background_task_stall_timeout',
|
|
537
606
|
});
|
|
538
607
|
emitSessionEnded('watchdog', null);
|
|
539
|
-
|
|
540
|
-
},
|
|
608
|
+
terminateQuery();
|
|
609
|
+
}, timeoutMs);
|
|
541
610
|
subagentStallTimer.unref?.();
|
|
542
611
|
};
|
|
543
612
|
const iteratorPromise = (async () => {
|
|
@@ -545,22 +614,27 @@ export function createClaudeCodeEngine() {
|
|
|
545
614
|
turnLiveness.start();
|
|
546
615
|
try {
|
|
547
616
|
for await (const msg of q) {
|
|
617
|
+
const events = mapSdkMessage(msg, mapperState);
|
|
618
|
+
const isForeground = !('parent_tool_use_id' in msg && msg.parent_tool_use_id != null);
|
|
619
|
+
const foregroundProgress = isForeground &&
|
|
620
|
+
events.some((ev) => ev.kind === 'message:text' ||
|
|
621
|
+
ev.kind === 'message:thinking' ||
|
|
622
|
+
ev.kind === 'tool:call' ||
|
|
623
|
+
ev.kind === 'tool:result');
|
|
548
624
|
// A parent continuation owns the foreground again. Background
|
|
549
625
|
// progress notifications alone do not end the between-turn wait.
|
|
550
|
-
if (
|
|
626
|
+
if (foregroundProgress) {
|
|
551
627
|
waitingForBackground = false;
|
|
628
|
+
settlementPending = false;
|
|
552
629
|
clearContinuationGrace();
|
|
630
|
+
clearResultDrainWatchdog();
|
|
553
631
|
}
|
|
554
632
|
else if (msg.type === 'system' && msg.subtype === 'init') {
|
|
555
633
|
// A new run starts on this stream (e.g. after a resumed turn's empty result).
|
|
556
634
|
clearContinuationGrace();
|
|
635
|
+
settlementPending = false;
|
|
636
|
+
clearResultDrainWatchdog();
|
|
557
637
|
}
|
|
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
638
|
for (const ev of events) {
|
|
565
639
|
if (ev.kind === 'subagent:progress')
|
|
566
640
|
trackSubagentProgress(ev);
|
|
@@ -578,17 +652,26 @@ export function createClaudeCodeEngine() {
|
|
|
578
652
|
armSubagentStallWatchdog();
|
|
579
653
|
for (const ev of events) {
|
|
580
654
|
if (ev.kind === 'session:compacting') {
|
|
655
|
+
const wasCompacting = isCompacting;
|
|
581
656
|
isCompacting = ev.active;
|
|
582
|
-
if (ev.active)
|
|
657
|
+
if (ev.active) {
|
|
658
|
+
clearContinuationGrace();
|
|
659
|
+
clearResultDrainWatchdog();
|
|
583
660
|
armCompactionStallTimer();
|
|
584
|
-
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
585
663
|
clearCompactionStallTimer();
|
|
664
|
+
if (wasCompacting && settlementPending)
|
|
665
|
+
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
666
|
+
}
|
|
586
667
|
}
|
|
587
668
|
// Older SDKs can emit only compact_boundary, without a trailing
|
|
588
669
|
// status update. This boundary marks compaction as complete.
|
|
589
670
|
else if (ev.kind === 'session:compacted') {
|
|
590
671
|
isCompacting = false;
|
|
591
672
|
clearCompactionStallTimer();
|
|
673
|
+
if (settlementPending)
|
|
674
|
+
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
592
675
|
}
|
|
593
676
|
}
|
|
594
677
|
// Actual foreground output proves work resumed even if the SDK
|
|
@@ -602,18 +685,16 @@ export function createClaudeCodeEngine() {
|
|
|
602
685
|
safeEmit({ kind: 'session:compacting', active: false });
|
|
603
686
|
}
|
|
604
687
|
for (const ev of events) {
|
|
605
|
-
if (ev.kind === 'tool:call')
|
|
688
|
+
if (isForeground && ev.kind === 'tool:call')
|
|
606
689
|
pendingToolCallIds.add(ev.toolCallId);
|
|
607
|
-
else if (ev.kind === 'tool:result')
|
|
690
|
+
else if (isForeground && ev.kind === 'tool:result')
|
|
608
691
|
pendingToolCallIds.delete(ev.toolCallId);
|
|
609
692
|
}
|
|
610
693
|
// After a settled result, the last background completion drains
|
|
611
694
|
// the stream only if the parent does not continue (see
|
|
612
695
|
// BACKGROUND_CONTINUATION_GRACE_MS): its continuation still needs
|
|
613
696
|
// the input for permission requests.
|
|
614
|
-
if (subagentStallTimer &&
|
|
615
|
-
activeSubagentTaskIds.size === 0 &&
|
|
616
|
-
!inputStream.hasUnansweredInput(completedResponses)) {
|
|
697
|
+
if (subagentStallTimer && activeSubagentTaskIds.size === 0 && !inputStream.hasUnansweredInput()) {
|
|
617
698
|
clearSubagentStallWatchdog();
|
|
618
699
|
armContinuationGrace(BACKGROUND_CONTINUATION_GRACE_MS);
|
|
619
700
|
}
|
|
@@ -622,9 +703,12 @@ export function createClaudeCodeEngine() {
|
|
|
622
703
|
discoveredSessionId = ev.engineSessionId;
|
|
623
704
|
safeEmit(ev);
|
|
624
705
|
}
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
|
|
706
|
+
// Transport metadata is not forward progress. In particular a
|
|
707
|
+
// stream of trailing notifications must not defeat shutdown.
|
|
708
|
+
if (foregroundProgress ||
|
|
709
|
+
msg.type === 'result' ||
|
|
710
|
+
events.some((ev) => ev.kind === 'subagent:progress' && !ev.ambient))
|
|
711
|
+
turnLiveness.activity();
|
|
628
712
|
// A background subagent or a pending permission card can stay
|
|
629
713
|
// legitimately quiet for minutes; their own dedicated watchdogs
|
|
630
714
|
// own those windows, so suspend the turn deadline meanwhile.
|
|
@@ -634,11 +718,14 @@ export function createClaudeCodeEngine() {
|
|
|
634
718
|
reevaluateLivenessPause();
|
|
635
719
|
if (msg.type === 'result') {
|
|
636
720
|
waitingForBackground = false;
|
|
721
|
+
settlementPending = true;
|
|
637
722
|
clearContinuationGrace();
|
|
638
723
|
pendingToolCallIds.clear();
|
|
724
|
+
turnLiveness.setTimeoutMs(CLAUDE_STREAM_IDLE_TIMEOUT_MS);
|
|
639
725
|
completedResponses++;
|
|
726
|
+
inputStream.acknowledgeResult(msg);
|
|
640
727
|
// A queued forced message starts the next response on this same SDK stream.
|
|
641
|
-
if (!inputStream.hasUnansweredInput(
|
|
728
|
+
if (!inputStream.hasUnansweredInput()) {
|
|
642
729
|
if (activeSubagentTaskIds.size === 0) {
|
|
643
730
|
clearSubagentStallWatchdog();
|
|
644
731
|
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
@@ -721,7 +808,7 @@ export function createClaudeCodeEngine() {
|
|
|
721
808
|
return iteratorRunning;
|
|
722
809
|
},
|
|
723
810
|
sendMessage(text) {
|
|
724
|
-
if (!iteratorRunning)
|
|
811
|
+
if (!iteratorRunning || queryCloseRequested || sessionEndedEmitted)
|
|
725
812
|
throw new Error(`Claude ${AGENT_NO_LONGER_RUNNING_TEXT}`);
|
|
726
813
|
inputStream.send(text);
|
|
727
814
|
armSubagentStallWatchdog();
|
|
@@ -735,7 +822,7 @@ export function createClaudeCodeEngine() {
|
|
|
735
822
|
pendingToolCallIds.size > 0 ||
|
|
736
823
|
pendingResolvers.size > 0 ||
|
|
737
824
|
isCompacting ||
|
|
738
|
-
inputStream.hasUnansweredInput(
|
|
825
|
+
inputStream.hasUnansweredInput())
|
|
739
826
|
return false;
|
|
740
827
|
inputStream.send(text);
|
|
741
828
|
waitingForBackground = false;
|
|
@@ -800,7 +887,7 @@ export function createClaudeCodeEngine() {
|
|
|
800
887
|
}
|
|
801
888
|
},
|
|
802
889
|
async stop() {
|
|
803
|
-
|
|
890
|
+
terminateQuery();
|
|
804
891
|
try {
|
|
805
892
|
await iteratorPromise;
|
|
806
893
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { callMcpTool, initializeMcp, readClaudeMcpEntry, spawnMcpProcess, unwrapMcpResult, } from '../utils/mcp-client.js';
|
|
1
|
+
import { callMcpTool, initializeMcp, listMcpToolNames, readClaudeMcpEntry, spawnMcpProcess, unwrapMcpResult, } from '../utils/mcp-client.js';
|
|
2
2
|
import { stopMcpProcess } from '../utils/mcp-process.js';
|
|
3
3
|
import { getIntegrationConfig } from './integration-config-service.js';
|
|
4
4
|
import { getGlobalSettings } from './settings-service.js';
|
|
@@ -23,7 +23,9 @@ export async function testSentryConnection() {
|
|
|
23
23
|
const mcpProcess = spawnMcpProcess(config.command, config.args, config.env);
|
|
24
24
|
try {
|
|
25
25
|
await initializeMcp(mcpProcess);
|
|
26
|
-
|
|
26
|
+
// whoami is not exposed by all Sentry MCP servers. Organization discovery
|
|
27
|
+
// verifies authenticated read access without requiring an issue or mutating it.
|
|
28
|
+
await callMcpTool(mcpProcess, 'find_organizations', {});
|
|
27
29
|
return {
|
|
28
30
|
ok: true,
|
|
29
31
|
durationMs: Date.now() - startedAt,
|
|
@@ -205,7 +207,15 @@ export async function assignSentryIssueToSelf(issueUrl) {
|
|
|
205
207
|
});
|
|
206
208
|
});
|
|
207
209
|
await initializeMcp(mcpProcess);
|
|
208
|
-
const
|
|
210
|
+
const tools = await listMcpToolNames(mcpProcess);
|
|
211
|
+
if (!tools.includes('whoami') && !tools.includes('execute_sentry_tool')) {
|
|
212
|
+
return { assigned: false, reason: 'Sentry MCP does not expose a current-user lookup tool' };
|
|
213
|
+
}
|
|
214
|
+
// Current Sentry servers expose identity through the tool catalogue;
|
|
215
|
+
// older servers still provide whoami directly. Never retry the write.
|
|
216
|
+
const whoamiRaw = tools.includes('whoami')
|
|
217
|
+
? await callMcpTool(mcpProcess, 'whoami', {})
|
|
218
|
+
: await callMcpTool(mcpProcess, 'execute_sentry_tool', { name: 'whoami', arguments: {} });
|
|
209
219
|
const whoami = unwrapMcpResult(whoamiRaw);
|
|
210
220
|
const userId = extractSentryUserId(whoami);
|
|
211
221
|
if (!userId) {
|
|
@@ -15,6 +15,7 @@ import { LEGACY_AGNOSTIC_AUTO_LOOP_REVIEW_GATE, LEGACY_AGNOSTIC_REVIEW_TEMPLATE,
|
|
|
15
15
|
import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from './review-template-service.js';
|
|
16
16
|
import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT, DEFAULT_MODEL_BY_ENGINE, FRESH_MODEL_BY_ENGINE, } from './settings-defaults.js';
|
|
17
17
|
import { AGNOSTIC_PROMPTS } from './skill-suite-prompts.js';
|
|
18
|
+
import { pruneWorkspaceTemplateTags } from './workspace-template-service.js';
|
|
18
19
|
export const STANDARD_GIT_CONVENTIONS = `Follow the repository's existing Git conventions and the user's instructions.
|
|
19
20
|
Work locally by default. Commit, push, publish comments or open pull requests only when explicitly authorized.
|
|
20
21
|
Inspect git status and all staged, unstaged and untracked changes before staging. Preserve unrelated work.
|
|
@@ -1408,6 +1409,7 @@ export function importConfigBundle(bundle) {
|
|
|
1408
1409
|
for (const key of SECRET_GLOBAL_KEYS) {
|
|
1409
1410
|
migrated.global[key] = current.global[key];
|
|
1410
1411
|
}
|
|
1412
|
+
pruneWorkspaceTemplateTags(migrated.global.tags);
|
|
1411
1413
|
writeSettings(migrated, { backup: true });
|
|
1412
1414
|
}
|
|
1413
1415
|
/** Return only the global settings section. */
|
|
@@ -1727,6 +1729,8 @@ export function updateGlobalSettings(input) {
|
|
|
1727
1729
|
filtered.worktreesPath = validateWorktreesPath(filtered.worktreesPath, { allowEmpty: false });
|
|
1728
1730
|
ensureGlobalWorktreesRootExists(filtered.worktreesPath);
|
|
1729
1731
|
}
|
|
1732
|
+
if (filtered.tags !== undefined)
|
|
1733
|
+
pruneWorkspaceTemplateTags(filtered.tags);
|
|
1730
1734
|
settings.global = { ...settings.global, ...filtered };
|
|
1731
1735
|
writeSettings(settings, { backup: true });
|
|
1732
1736
|
return settings.global;
|
|
@@ -3,6 +3,7 @@ import { nanoid } from 'nanoid';
|
|
|
3
3
|
import { getDb } from '../db/index.js';
|
|
4
4
|
import { writeJsonFileAtomically } from '../utils/atomic-json-file.js';
|
|
5
5
|
import { getWorkspaceTemplatesPath } from '../utils/paths.js';
|
|
6
|
+
import { getGlobalSettings } from './settings-service.js';
|
|
6
7
|
const FILE_VERSION = 1;
|
|
7
8
|
export const MAX_WORKSPACE_TEMPLATES = 100;
|
|
8
9
|
const MAX_NAME_LENGTH = 80;
|
|
@@ -44,6 +45,14 @@ export function sanitizePreset(input) {
|
|
|
44
45
|
if (Array.isArray(raw[key]))
|
|
45
46
|
preset[key] = raw[key].filter((v) => typeof v === 'string');
|
|
46
47
|
}
|
|
48
|
+
if (Array.isArray(raw.tags)) {
|
|
49
|
+
preset.tags = [
|
|
50
|
+
...new Set(raw.tags
|
|
51
|
+
.filter((tag) => typeof tag === 'string')
|
|
52
|
+
.map((tag) => tag.trim())
|
|
53
|
+
.filter((tag) => tag.length > 0 && tag.length <= 50)),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
47
56
|
if (typeof raw.agentPermissionMode === 'string' &&
|
|
48
57
|
PERMISSION_MODES.includes(raw.agentPermissionMode)) {
|
|
49
58
|
preset.agentPermissionMode = raw.agentPermissionMode;
|
|
@@ -97,7 +106,32 @@ function writeFile(templates) {
|
|
|
97
106
|
writeJsonFileAtomically(getWorkspaceTemplatesPath(), file);
|
|
98
107
|
}
|
|
99
108
|
export function listWorkspaceTemplates() {
|
|
100
|
-
return readFile();
|
|
109
|
+
return readFile().map((template) => ({ ...template, preset: sanitizeTemplatePreset(template.preset) }));
|
|
110
|
+
}
|
|
111
|
+
/** Guard writes from stale clients as well as reads of hand-edited/older files. */
|
|
112
|
+
function sanitizeTemplatePreset(input) {
|
|
113
|
+
const preset = sanitizePreset(input);
|
|
114
|
+
if (preset.tags !== undefined) {
|
|
115
|
+
const allowed = new Set(getGlobalSettings().tags);
|
|
116
|
+
preset.tags = preset.tags.filter((tag) => allowed.has(tag));
|
|
117
|
+
}
|
|
118
|
+
return preset;
|
|
119
|
+
}
|
|
120
|
+
/** Called before persisting a changed catalogue; a corrupt file aborts deletion. */
|
|
121
|
+
export function pruneWorkspaceTemplateTags(allowedTags) {
|
|
122
|
+
const allowed = new Set(allowedTags);
|
|
123
|
+
let changed = false;
|
|
124
|
+
const templates = readFile().map((template) => {
|
|
125
|
+
if (template.preset.tags === undefined)
|
|
126
|
+
return template;
|
|
127
|
+
const tags = template.preset.tags.filter((tag) => allowed.has(tag));
|
|
128
|
+
if (tags.length === template.preset.tags.length)
|
|
129
|
+
return template;
|
|
130
|
+
changed = true;
|
|
131
|
+
return { ...template, preset: { ...template.preset, tags }, updatedAt: new Date().toISOString() };
|
|
132
|
+
});
|
|
133
|
+
if (changed)
|
|
134
|
+
writeFile(templates);
|
|
101
135
|
}
|
|
102
136
|
/** Throws `Invalid template name`, `already exists` or `Too many templates`. */
|
|
103
137
|
export function createWorkspaceTemplate(input) {
|
|
@@ -116,7 +150,7 @@ export function createWorkspaceTemplate(input) {
|
|
|
116
150
|
name,
|
|
117
151
|
createdAt: now,
|
|
118
152
|
updatedAt: now,
|
|
119
|
-
preset:
|
|
153
|
+
preset: sanitizeTemplatePreset(input.preset),
|
|
120
154
|
};
|
|
121
155
|
writeFile([...templates, template]);
|
|
122
156
|
return template;
|
|
@@ -139,7 +173,7 @@ export function updateWorkspaceTemplate(id, updates) {
|
|
|
139
173
|
const updated = {
|
|
140
174
|
...current,
|
|
141
175
|
name,
|
|
142
|
-
preset: updates.preset !== undefined ?
|
|
176
|
+
preset: sanitizeTemplatePreset(updates.preset !== undefined ? updates.preset : current.preset),
|
|
143
177
|
updatedAt: new Date().toISOString(),
|
|
144
178
|
};
|
|
145
179
|
templates[index] = updated;
|