@loicngr/kobo 2.1.1 → 2.1.4
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/CHANGELOG.md +13 -0
- package/dist/server/routes/workspaces.js +39 -0
- package/dist/server/services/agent/engines/claude-code/engine.js +116 -29
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +88 -22
- package/dist/server/services/agent/engines/claude-code/options-builder.js +1 -0
- package/dist/server/services/agent/engines/codex/engine.js +28 -5
- package/dist/server/services/agent/engines/codex/options-builder.js +7 -1
- package/dist/server/services/agent/orchestrator.js +40 -1
- package/dist/server/services/agent/session-controller.js +12 -0
- package/dist/server/services/review-service.js +6 -2
- package/package.json +13 -13
- package/src/client/dist/pwa/assets/{ActivityFeed-BYR5x7aa.css → ActivityFeed-DCmiJP-z.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DdGARu8o.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-OoOnn4q9.js → ChangelogPage-CGenjiI5.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-CWjIRHxu.js → ClosePopup-C1a82pL9.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-BYpe6Nch.js → ComparisonPanel-aXgjEulK.js} +2 -2
- package/src/client/dist/pwa/assets/{CreatePage-COQwHyqL.js → CreatePage-BaB_30xJ.js} +2 -2
- package/src/client/dist/pwa/assets/{DashboardPage-CZ2036MC.js → DashboardPage-DsbwmO2W.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-CXBmMV9R.js → DiffViewer--dUMf7b3.js} +4 -4
- package/src/client/dist/pwa/assets/{DocumentsPanel-mLGU5aTb.js → DocumentsPanel-DTjEUYUd.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-C39pfWBD.js → DrawerToggleButton-Bh4FXdtJ.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-PUPLj0M8.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-CnJWXfa8.js → HealthPage-BHgucifz.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-Cc4XzCAy.css → MainLayout-CNmwCnDD.css} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-CQVLnTX3.js +2 -0
- package/src/client/dist/pwa/assets/QBadge-rGILsUYl.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-XE0EMe9L.js +1 -0
- package/src/client/dist/pwa/assets/QBtn-b_baQtAt.js +1 -0
- package/src/client/dist/pwa/assets/QBtnDropdown-DVbxGTNb.js +1 -0
- package/src/client/dist/pwa/assets/QBtnGroup-CUmAgBV8.js +1 -0
- package/src/client/dist/pwa/assets/QBtnToggle-COL46j6W.js +1 -0
- package/src/client/dist/pwa/assets/QChip-Bt_JvnHb.js +1 -0
- package/src/client/dist/pwa/assets/QExpansionItem-CxEBY3jY.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-Cf6OZEH4.js +1 -0
- package/src/client/dist/pwa/assets/QList-Z90BspT4.js +1 -0
- package/src/client/dist/pwa/assets/QMarkupTable-Bwjknbmw.js +1 -0
- package/src/client/dist/pwa/assets/QPage-BCDXjfVj.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-CMzJDhi-.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-BsKmccpX.js +1 -0
- package/src/client/dist/pwa/assets/QTooltip-BLhkNT4s.js +1 -0
- package/src/client/dist/pwa/assets/{SchedulePanel-DIMzNIO3.js → SchedulePanel-DTVwe7sX.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-COWnMxRZ.js → SearchPage-CtrR7dNr.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-Bl_RNTGi.js +9 -0
- package/src/client/dist/pwa/assets/SplitWorkspacePage-Bac_Pzmm.js +1 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-C_HNpN1X.js → TerminalPanel-D2j5AA8S.js} +1 -1
- package/src/client/dist/pwa/assets/TouchPan-C0EqFcrr.js +1 -0
- package/src/client/dist/pwa/assets/{TourReplayButton-Cp2UnxWa.js → TourReplayButton-DlMvPblb.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BAk5l4Ik.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-FScR5YAD.js +4 -0
- package/src/client/dist/pwa/assets/{browser-uy3hmwKo.js → browser-DNEbLF-4.js} +1 -1
- package/src/client/dist/pwa/assets/build-path-tree-C0b61nxI.js +1 -0
- package/src/client/dist/pwa/assets/{codicon-Brq4_Ui5.ttf → codicon-DF1abBS2.ttf} +0 -0
- package/src/client/dist/pwa/assets/custom-sounds-DE4JLdGG.js +1 -0
- package/src/client/dist/pwa/assets/{de-CTE3Xb7Z.js → de-DgZ9jKq_.js} +2 -2
- package/src/client/dist/pwa/assets/documents-CALwmao-.js +1 -0
- package/src/client/dist/pwa/assets/editor-CmVjQp1O.css +1 -0
- package/src/client/dist/pwa/assets/editor.api-PENN0io6.js +420 -0
- package/src/client/dist/pwa/assets/editor.worker-BLJF_oQx.js +33 -0
- package/src/client/dist/pwa/assets/efforts-Detdm-FH.js +9 -0
- package/src/client/dist/pwa/assets/{es-CZxg1xjh.js → es-zvlgzw9H.js} +2 -2
- package/src/client/dist/pwa/assets/expand-template-ZIVLPdW2.js +1 -0
- package/src/client/dist/pwa/assets/format-CVI7mr7G.js +489 -0
- package/src/client/dist/pwa/assets/format-DoyK4IAn.css +1 -0
- package/src/client/dist/pwa/assets/{formatters-BtG6lb4d.js → formatters-C_zcfJBt.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-C7HCclxj.js → fr-BICzq59I.js} +2 -2
- package/src/client/dist/pwa/assets/{freemarker2-CDYXi2tK.js → freemarker2-BxN_Aq6C.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-AVkJuN-_.js → handlebars-C9UHXtO1.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CpvEZlmO.js → html-CTH9wSJr.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-DG_vChZ3.js +1 -0
- package/src/client/dist/pwa/assets/index-C8zAZ-k-.css +1 -0
- package/src/client/dist/pwa/assets/index-fuLr-ZVa.js +20 -0
- package/src/client/dist/pwa/assets/{it-DoOWwqZz.js → it-BIGArQrS.js} +2 -2
- package/src/client/dist/pwa/assets/{javascript-BYnnBLH9.js → javascript-WT4BS8p4.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CzqlB3Um.js → liquid-D6dfe-gU.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-hrYFnJVZ.js → mdx-B8pNXNeC.js} +1 -1
- package/src/client/dist/pwa/assets/monaco-contributions-COLfslyi.js +53 -0
- package/src/client/dist/pwa/assets/monaco-contributions-DvnCTJI2.css +1 -0
- package/src/client/dist/pwa/assets/{monaco.contribution-Dl22KqTL.js → monaco.contribution-CDhuQbzi.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-Dq0I5kjv.js → network-auth-QouPCx6A.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-vV3Rm4_v.js → notify-theme-7MsG-MyN.js} +1 -1
- package/src/client/dist/pwa/assets/pending-diff-open-CX24u9gW.js +1 -0
- package/src/client/dist/pwa/assets/{pinia-CTJ3OfP3.js → pinia-BdFjQEqa.js} +1 -1
- package/src/client/dist/pwa/assets/{private.use-form-Bk8ti264.js → private.use-form-vxvbh_VN.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-W5mMgIKK.js → project-color-Bo6iEcqN.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DafgCozG.js → python-D2gb6Jjz.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CWYrTLCm.js → razor-dksrCQF2.js} +1 -1
- package/src/client/dist/pwa/assets/{reactivity.esm-bundler-Bo9anJjg.js → reactivity.esm-bundler-tuEvQ47f.js} +1 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-px_6WDIX.js +69 -0
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-DOiZcasU.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-DLSIuW9u.js → typescript-DTPyRTrW.js} +1 -1
- package/src/client/dist/pwa/assets/{use-checkbox-CnvHBhSK.js → use-checkbox-Bh9EQhkV.js} +1 -1
- package/src/client/dist/pwa/assets/use-id-b4A2I8LB.js +1 -0
- package/src/client/dist/pwa/assets/use-is-mobile-DLIZxEo9.js +1 -0
- package/src/client/dist/pwa/assets/{use-search-index-status-Bluf9L99.js → use-search-index-status-_nCyACc7.js} +1 -1
- package/src/client/dist/pwa/assets/{use-slide-transition-CiaDm-tR.js → use-slide-transition-C5eDGOmu.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-CDdnvIaG.js → use-tours-b00RruX_.js} +1 -1
- package/src/client/dist/pwa/assets/use-worktree-restore-BDOZc9fk.js +1 -0
- package/src/client/dist/pwa/assets/{workspace-templates-C88VmFdQ.js → workspace-templates-DV9Njz0T.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-BAmdYarn.js → xml-mxUZCM4r.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-RQqNMXWh.js → yaml-BuN8sUWC.js} +1 -1
- package/src/client/dist/pwa/index.html +9 -9
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DeJ0V6CW.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-BQk7kJO-.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-D3zfT61Y.js +0 -2
- package/src/client/dist/pwa/assets/QBadge-FWC1zDCq.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-CcRI_QIy.js +0 -1
- package/src/client/dist/pwa/assets/QBtn-DEdx_Y1V.js +0 -1
- package/src/client/dist/pwa/assets/QBtnDropdown-DGLLlBf3.js +0 -1
- package/src/client/dist/pwa/assets/QBtnGroup-CAoD6G0Q.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-C_hQ2UCQ.js +0 -1
- package/src/client/dist/pwa/assets/QChip-DCPUhqXX.js +0 -1
- package/src/client/dist/pwa/assets/QExpansionItem-CQ2LzM8P.js +0 -1
- package/src/client/dist/pwa/assets/QItemSection-BhqphaH6.js +0 -1
- package/src/client/dist/pwa/assets/QList-CHm4JNP0.js +0 -1
- package/src/client/dist/pwa/assets/QMarkupTable-CNpiA9oq.js +0 -1
- package/src/client/dist/pwa/assets/QPage-BXBw2Alx.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-DoTWgQAO.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-BkFiCsH3.js +0 -1
- package/src/client/dist/pwa/assets/QTooltip-DKvUeiR_.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-B9cLhFnO.js +0 -9
- package/src/client/dist/pwa/assets/SplitWorkspacePage-BJKssJyI.js +0 -1
- package/src/client/dist/pwa/assets/TouchPan-CCllyaAF.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B-SuQuS_.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-6lN4iF5W.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-DZrp6Aji.js +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-Du8ReMlU.js +0 -1
- package/src/client/dist/pwa/assets/documents-BtNrB6d0.js +0 -1
- package/src/client/dist/pwa/assets/editor-BJU3Xb3h.css +0 -1
- package/src/client/dist/pwa/assets/editor.api-P-eJRcVK.js +0 -413
- package/src/client/dist/pwa/assets/editor.worker-iO5iN0DB.js +0 -33
- package/src/client/dist/pwa/assets/efforts-ChUC4xtk.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-D-0NPZBE.js +0 -1
- package/src/client/dist/pwa/assets/expand-template-ByoC_kxX.js +0 -1
- package/src/client/dist/pwa/assets/format-CX6Cv1cD.css +0 -1
- package/src/client/dist/pwa/assets/format-CeidAPFn.js +0 -435
- package/src/client/dist/pwa/assets/i18n-KVkTJG-4.js +0 -1
- package/src/client/dist/pwa/assets/index-0wtBh1Xt.css +0 -1
- package/src/client/dist/pwa/assets/index-DsaWUT_Z.js +0 -20
- package/src/client/dist/pwa/assets/monaco-contributions-Cb0afGfW.css +0 -1
- package/src/client/dist/pwa/assets/monaco-contributions-D2YXqNaH.js +0 -51
- package/src/client/dist/pwa/assets/pending-diff-open-BJYqBOUG.js +0 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-CE5RIL0s.js +0 -69
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-CNFKm4ij.js +0 -1
- package/src/client/dist/pwa/assets/use-id-wXd4c8FA.js +0 -1
- package/src/client/dist/pwa/assets/use-is-mobile-DpKI3XMy.js +0 -1
- /package/src/client/dist/pwa/assets/{engineFeatures-B-9jHocY.css → use-worktree-restore-B-9jHocY.css} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.4
|
|
8
|
+
|
|
9
|
+
- chore(deps): update server client and pwa packages
|
|
10
|
+
|
|
11
|
+
## 2.1.3
|
|
12
|
+
|
|
13
|
+
- fix(subagents): reliable lifecycle tracking and a stop button
|
|
14
|
+
|
|
15
|
+
## 2.1.2
|
|
16
|
+
|
|
17
|
+
- fix: read-only returning reviews, resumed-turn stdin, feed and tag filter
|
|
18
|
+
- chore: npm deps update
|
|
19
|
+
|
|
7
20
|
## 2.1.1
|
|
8
21
|
|
|
9
22
|
- feat(dashboard): allow resetting the model reliability table
|
|
@@ -4621,4 +4621,43 @@ app.post('/:id/interrupt', migrationGuard, async (c) => {
|
|
|
4621
4621
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
4622
4622
|
}
|
|
4623
4623
|
});
|
|
4624
|
+
// POST /api/workspaces/:id/subagents/stop - stop one running sub-agent (body
|
|
4625
|
+
// `{ id }`, its canonical card id) or all of them, without interrupting the turn
|
|
4626
|
+
app.post('/:id/subagents/stop', migrationGuard, async (c) => {
|
|
4627
|
+
try {
|
|
4628
|
+
const id = c.req.param('id');
|
|
4629
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
4630
|
+
if (!workspace) {
|
|
4631
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
4632
|
+
}
|
|
4633
|
+
const rawBody = await c.req.text();
|
|
4634
|
+
let body = {};
|
|
4635
|
+
if (rawBody.trim().length > 0) {
|
|
4636
|
+
let parsed;
|
|
4637
|
+
try {
|
|
4638
|
+
parsed = JSON.parse(rawBody);
|
|
4639
|
+
}
|
|
4640
|
+
catch {
|
|
4641
|
+
return c.json({ error: 'Request body must be valid JSON' }, 400);
|
|
4642
|
+
}
|
|
4643
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
4644
|
+
return c.json({ error: 'Request body must be a JSON object' }, 400);
|
|
4645
|
+
}
|
|
4646
|
+
body = parsed;
|
|
4647
|
+
}
|
|
4648
|
+
if (body.id !== undefined && (typeof body.id !== 'string' || body.id.trim().length === 0)) {
|
|
4649
|
+
return c.json({ error: 'id must be a non-empty string when provided' }, 400);
|
|
4650
|
+
}
|
|
4651
|
+
const stopped = agentManager.stopSubagents(id, body.id);
|
|
4652
|
+
return c.json({ stopped });
|
|
4653
|
+
}
|
|
4654
|
+
catch (err) {
|
|
4655
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4656
|
+
if (err instanceof agentManager.StopSubagentsError) {
|
|
4657
|
+
const status = err.code === 'subagent_not_running' ? 404 : 409;
|
|
4658
|
+
return c.json({ error: message, code: err.code }, status);
|
|
4659
|
+
}
|
|
4660
|
+
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
4661
|
+
}
|
|
4662
|
+
});
|
|
4624
4663
|
export default app;
|
|
@@ -48,6 +48,11 @@ export const SUBAGENT_STALL_TIMEOUT_MS = 10 * 60_000;
|
|
|
48
48
|
* every permission request of it fail with "Stream closed", so wait this long
|
|
49
49
|
* for a parent message before draining a stream that did not continue. */
|
|
50
50
|
export const BACKGROUND_CONTINUATION_GRACE_MS = 60_000;
|
|
51
|
+
/** A `result` does not always end the stream: resuming a session killed
|
|
52
|
+
* mid-turn first settles the interrupted turn with an empty result, then
|
|
53
|
+
* runs the new prompt right away. Wait this long for a follow-up before
|
|
54
|
+
* closing the input it still needs for permission requests. */
|
|
55
|
+
export const RESULT_CONTINUATION_GRACE_MS = 3_000;
|
|
51
56
|
const MAX_PENDING_USER_MESSAGES = 20;
|
|
52
57
|
function toMcpServersMap(specs) {
|
|
53
58
|
if (!specs || specs.length === 0)
|
|
@@ -119,9 +124,52 @@ export function createClaudeCodeEngine() {
|
|
|
119
124
|
async start(options, onEvent) {
|
|
120
125
|
const abortController = new AbortController();
|
|
121
126
|
const mapperState = createMapperState();
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
127
|
+
// Active sub-agents keyed by their canonical id (SDK task id when known,
|
|
128
|
+
// else the tool call id), which is also the id passed to `q.stopTask`.
|
|
129
|
+
// One task can surface under both ids (relaunch, kill/restart), so
|
|
130
|
+
// running and terminal events must resolve to the same entry.
|
|
131
|
+
const activeSubagentTaskIds = new Set();
|
|
132
|
+
// tool call id -> SDK task id, learned from events carrying both.
|
|
133
|
+
const subagentTaskIdByToolCallId = new Map();
|
|
134
|
+
// Ambient tasks (SDK `ambient`, e.g. Monitor watchers) are not activity:
|
|
135
|
+
// they never enter `activeSubagentTaskIds`, so they never block
|
|
136
|
+
// turn:completed, keep the input open or arm the stall watchdog. They are
|
|
137
|
+
// tracked apart only so a user interrupt still stops them.
|
|
138
|
+
const ambientSubagentTaskIds = new Set();
|
|
139
|
+
// Canonical ids whose last lifecycle event was terminal: a late
|
|
140
|
+
// `progress` for them is ignored, only a `started` relaunch revives them.
|
|
141
|
+
const terminalSubagentTaskIds = new Set();
|
|
142
|
+
const trackSubagentProgress = (ev) => {
|
|
143
|
+
if (ev.taskId) {
|
|
144
|
+
subagentTaskIdByToolCallId.set(ev.toolCallId, ev.taskId);
|
|
145
|
+
// Merge an entry first tracked under its tool call id alone.
|
|
146
|
+
if (ev.toolCallId !== ev.taskId) {
|
|
147
|
+
activeSubagentTaskIds.delete(ev.toolCallId);
|
|
148
|
+
ambientSubagentTaskIds.delete(ev.toolCallId);
|
|
149
|
+
if (terminalSubagentTaskIds.delete(ev.toolCallId))
|
|
150
|
+
terminalSubagentTaskIds.add(ev.taskId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const taskId = ev.taskId ?? subagentTaskIdByToolCallId.get(ev.toolCallId);
|
|
154
|
+
const key = taskId ?? ev.toolCallId;
|
|
155
|
+
if (ev.status !== 'running') {
|
|
156
|
+
activeSubagentTaskIds.delete(key);
|
|
157
|
+
ambientSubagentTaskIds.delete(key);
|
|
158
|
+
terminalSubagentTaskIds.add(key);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (ev.phase === 'progress' && terminalSubagentTaskIds.has(key))
|
|
162
|
+
return;
|
|
163
|
+
terminalSubagentTaskIds.delete(key);
|
|
164
|
+
if (ev.ambient) {
|
|
165
|
+
activeSubagentTaskIds.delete(key);
|
|
166
|
+
ambientSubagentTaskIds.add(key);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
ambientSubagentTaskIds.delete(key);
|
|
170
|
+
activeSubagentTaskIds.add(key);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
125
173
|
// Foreground tool calls currently awaiting their tool_result. Cleared on
|
|
126
174
|
// every `result` message: a turn cannot end with a tool still in flight.
|
|
127
175
|
const pendingToolCallIds = new Set();
|
|
@@ -130,6 +178,16 @@ export function createClaudeCodeEngine() {
|
|
|
130
178
|
const isInteractive = options.agentPermissionMode === 'interactive';
|
|
131
179
|
const canUseTool = (toolName, input, ctx) => {
|
|
132
180
|
const toolCallId = typeof ctx.toolUseID === 'string' && ctx.toolUseID.length > 0 ? ctx.toolUseID : `tu_${nanoid()}`;
|
|
181
|
+
// Plan mode alone is not read-only here: the non-interactive allow below
|
|
182
|
+
// would approve ExitPlanMode and then every edit. Anything that needs a
|
|
183
|
+
// permission, a question included, is refused outright instead.
|
|
184
|
+
if (options.readOnly) {
|
|
185
|
+
return Promise.resolve({
|
|
186
|
+
behavior: 'deny',
|
|
187
|
+
message: 'This is a read-only review session: do not modify anything or ask the user. Put the finding in your final report instead.',
|
|
188
|
+
interrupt: false,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
133
191
|
// Non-interactive modes: the SDK has already applied its permissionMode
|
|
134
192
|
// rules before reaching us, so allow through unchanged. AskUserQuestion
|
|
135
193
|
// is the exception — it always defers to the user.
|
|
@@ -245,6 +303,17 @@ export function createClaudeCodeEngine() {
|
|
|
245
303
|
sdkOptions.pathToClaudeCodeExecutable = explicitBinary;
|
|
246
304
|
const inputStream = new ClaudeInputStream(effectivePrompt);
|
|
247
305
|
const q = query({ prompt: inputStream, options: sdkOptions });
|
|
306
|
+
// Best-effort SDK stop of one background task; failures are ignored.
|
|
307
|
+
const requestStopTask = (taskId) => {
|
|
308
|
+
try {
|
|
309
|
+
void q.stopTask(taskId).catch(() => {
|
|
310
|
+
/* best-effort */
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
/* best-effort */
|
|
315
|
+
}
|
|
316
|
+
};
|
|
248
317
|
let resolveReady;
|
|
249
318
|
let rejectReady;
|
|
250
319
|
const ready = new Promise((resolve, reject) => {
|
|
@@ -411,9 +480,9 @@ export function createClaudeCodeEngine() {
|
|
|
411
480
|
clearTimeout(resultDrainTimer);
|
|
412
481
|
resultDrainTimer = undefined;
|
|
413
482
|
};
|
|
414
|
-
// Drain for a stream
|
|
415
|
-
//
|
|
416
|
-
// continuation's own `result`
|
|
483
|
+
// Drain for a stream that may still continue: after a result, or after
|
|
484
|
+
// the last background subagent completed. Any new init or parent
|
|
485
|
+
// message cancels it, and that continuation's own `result` re-arms it.
|
|
417
486
|
let continuationGraceTimer;
|
|
418
487
|
const clearContinuationGrace = () => {
|
|
419
488
|
if (!continuationGraceTimer)
|
|
@@ -421,7 +490,7 @@ export function createClaudeCodeEngine() {
|
|
|
421
490
|
clearTimeout(continuationGraceTimer);
|
|
422
491
|
continuationGraceTimer = undefined;
|
|
423
492
|
};
|
|
424
|
-
const armContinuationGrace = () => {
|
|
493
|
+
const armContinuationGrace = (delayMs) => {
|
|
425
494
|
clearContinuationGrace();
|
|
426
495
|
continuationGraceTimer = setTimeout(() => {
|
|
427
496
|
continuationGraceTimer = undefined;
|
|
@@ -430,9 +499,12 @@ export function createClaudeCodeEngine() {
|
|
|
430
499
|
pendingToolCallIds.size > 0 ||
|
|
431
500
|
inputStream.hasUnansweredInput(completedResponses))
|
|
432
501
|
return;
|
|
502
|
+
// Settled only now: a result the CLI follows up on (e.g. a resumed
|
|
503
|
+
// turn's empty result) must not hide the busy banner meanwhile.
|
|
504
|
+
emitTurnCompletedIfSettled();
|
|
433
505
|
inputStream.close();
|
|
434
506
|
armResultDrainWatchdog();
|
|
435
|
-
},
|
|
507
|
+
}, delayMs);
|
|
436
508
|
continuationGraceTimer.unref?.();
|
|
437
509
|
};
|
|
438
510
|
// Safety net for the "waiting on a background subagent" branch below:
|
|
@@ -478,6 +550,10 @@ export function createClaudeCodeEngine() {
|
|
|
478
550
|
waitingForBackground = false;
|
|
479
551
|
clearContinuationGrace();
|
|
480
552
|
}
|
|
553
|
+
else if (msg.type === 'system' && msg.subtype === 'init') {
|
|
554
|
+
// A new run starts on this stream (e.g. after a resumed turn's empty result).
|
|
555
|
+
clearContinuationGrace();
|
|
556
|
+
}
|
|
481
557
|
// This SDK message proves that any drain condition armed by a
|
|
482
558
|
// *previous* message is no longer current. A drain armed while
|
|
483
559
|
// processing this message remains active if the generator then
|
|
@@ -485,15 +561,14 @@ export function createClaudeCodeEngine() {
|
|
|
485
561
|
clearResultDrainWatchdog();
|
|
486
562
|
const events = mapSdkMessage(msg, mapperState);
|
|
487
563
|
for (const ev of events) {
|
|
488
|
-
if (ev.kind
|
|
489
|
-
|
|
490
|
-
if (ev.status === 'running')
|
|
491
|
-
activeSubagentTaskIds.set(ev.toolCallId, ev.taskId ?? ev.toolCallId);
|
|
492
|
-
else
|
|
493
|
-
activeSubagentTaskIds.delete(ev.toolCallId);
|
|
564
|
+
if (ev.kind === 'subagent:progress')
|
|
565
|
+
trackSubagentProgress(ev);
|
|
494
566
|
}
|
|
495
567
|
if (activeSubagentTaskIds.size > 0 &&
|
|
496
|
-
(events.some((ev) =>
|
|
568
|
+
(events.some((ev) =>
|
|
569
|
+
// An ambient watcher's progress is not proof that a real
|
|
570
|
+
// sub-agent advanced: it must not reset the stall bound.
|
|
571
|
+
(ev.kind === 'subagent:progress' && !ev.ambient) ||
|
|
497
572
|
ev.kind === 'message:text' ||
|
|
498
573
|
ev.kind === 'message:thinking' ||
|
|
499
574
|
ev.kind === 'tool:call' ||
|
|
@@ -539,7 +614,7 @@ export function createClaudeCodeEngine() {
|
|
|
539
614
|
activeSubagentTaskIds.size === 0 &&
|
|
540
615
|
!inputStream.hasUnansweredInput(completedResponses)) {
|
|
541
616
|
clearSubagentStallWatchdog();
|
|
542
|
-
armContinuationGrace();
|
|
617
|
+
armContinuationGrace(BACKGROUND_CONTINUATION_GRACE_MS);
|
|
543
618
|
}
|
|
544
619
|
for (const ev of events) {
|
|
545
620
|
if (ev.kind === 'session:started')
|
|
@@ -561,13 +636,11 @@ export function createClaudeCodeEngine() {
|
|
|
561
636
|
clearContinuationGrace();
|
|
562
637
|
pendingToolCallIds.clear();
|
|
563
638
|
completedResponses++;
|
|
564
|
-
emitTurnCompletedIfSettled();
|
|
565
639
|
// A queued forced message starts the next response on this same SDK stream.
|
|
566
640
|
if (!inputStream.hasUnansweredInput(completedResponses)) {
|
|
567
641
|
if (activeSubagentTaskIds.size === 0) {
|
|
568
642
|
clearSubagentStallWatchdog();
|
|
569
|
-
|
|
570
|
-
armResultDrainWatchdog();
|
|
643
|
+
armContinuationGrace(RESULT_CONTINUATION_GRACE_MS);
|
|
571
644
|
}
|
|
572
645
|
else {
|
|
573
646
|
waitingForBackground = true;
|
|
@@ -669,6 +742,28 @@ export function createClaudeCodeEngine() {
|
|
|
669
742
|
armSubagentStallWatchdog();
|
|
670
743
|
return true;
|
|
671
744
|
},
|
|
745
|
+
stopSubagents(ids) {
|
|
746
|
+
const isRunning = (taskId) => activeSubagentTaskIds.has(taskId) || ambientSubagentTaskIds.has(taskId);
|
|
747
|
+
const targets = new Set();
|
|
748
|
+
if (ids === undefined) {
|
|
749
|
+
for (const taskId of [...activeSubagentTaskIds, ...ambientSubagentTaskIds])
|
|
750
|
+
targets.add(taskId);
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
for (const id of ids) {
|
|
754
|
+
// A card id is the SDK task id, or the tool call id when the
|
|
755
|
+
// task id was not known yet: resolve both to the tracked entry.
|
|
756
|
+
const taskId = isRunning(id) ? id : subagentTaskIdByToolCallId.get(id);
|
|
757
|
+
if (taskId && isRunning(taskId))
|
|
758
|
+
targets.add(taskId);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// The main turn keeps running: only the targeted tasks are stopped.
|
|
762
|
+
// Their terminal notification clears the tracking as usual.
|
|
763
|
+
for (const taskId of targets)
|
|
764
|
+
requestStopTask(taskId);
|
|
765
|
+
return targets.size;
|
|
766
|
+
},
|
|
672
767
|
interrupt() {
|
|
673
768
|
userInterrupted = true;
|
|
674
769
|
// The SDK ends an interrupted run by emitting a `result` with
|
|
@@ -679,16 +774,8 @@ export function createClaudeCodeEngine() {
|
|
|
679
774
|
// background subagent task keeps running unless told to stop via
|
|
680
775
|
// its own API, which would otherwise leave the Stop button
|
|
681
776
|
// appearing to do nothing for up to SUBAGENT_STALL_TIMEOUT_MS.
|
|
682
|
-
for (const taskId of activeSubagentTaskIds
|
|
683
|
-
|
|
684
|
-
void q.stopTask(taskId).catch(() => {
|
|
685
|
-
/* best-effort */
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
catch {
|
|
689
|
-
/* best-effort */
|
|
690
|
-
}
|
|
691
|
-
}
|
|
777
|
+
for (const taskId of [...activeSubagentTaskIds, ...ambientSubagentTaskIds])
|
|
778
|
+
requestStopTask(taskId);
|
|
692
779
|
const qq = q;
|
|
693
780
|
if (typeof qq.interrupt === 'function') {
|
|
694
781
|
try {
|
|
@@ -77,6 +77,21 @@ export const QUOTA_PATTERN = /\b429\b|out of extra usage|rate[_ ]limit|usage lim
|
|
|
77
77
|
* of a previous attempt, a sub-agent failure, or application HTTP handling.
|
|
78
78
|
*/
|
|
79
79
|
const ASSISTANT_QUOTA_NOTICE = /^(?:you(?:'|’)re out of extra usage|you(?:'|’)ve hit your (?:session|weekly|usage) limit|claude ai usage limit reached|usage limit reached|quota exceeded|rate limit exceeded)(?:[.!]?(?:[ \t]*[·—-]?[ \t]*resets?\b[^\r\n]*)?)?[.!]?$/i;
|
|
80
|
+
/** SDKTaskNotificationMessage.status -> subagent:progress terminal status. */
|
|
81
|
+
const TASK_NOTIFICATION_STATUS = {
|
|
82
|
+
completed: 'done',
|
|
83
|
+
failed: 'failed',
|
|
84
|
+
stopped: 'stopped',
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* SDKTaskUpdatedMessage.patch.status values that end a task. `pending`,
|
|
88
|
+
* `running` and `paused` are not lifecycle edges and are ignored.
|
|
89
|
+
*/
|
|
90
|
+
const TASK_UPDATED_TERMINAL_STATUS = {
|
|
91
|
+
completed: 'done',
|
|
92
|
+
failed: 'failed',
|
|
93
|
+
killed: 'stopped',
|
|
94
|
+
};
|
|
80
95
|
export function createMapperState() {
|
|
81
96
|
return {
|
|
82
97
|
sessionStartedEmitted: false,
|
|
@@ -84,6 +99,8 @@ export function createMapperState() {
|
|
|
84
99
|
sawErrorResult: false,
|
|
85
100
|
quotaErrorEmitted: false,
|
|
86
101
|
userInterrupted: false,
|
|
102
|
+
taskToolUseIds: new Map(),
|
|
103
|
+
taskFlags: new Map(),
|
|
87
104
|
};
|
|
88
105
|
}
|
|
89
106
|
/** Known SDK `result` subtypes that indicate the run failed. */
|
|
@@ -191,30 +208,79 @@ export function mapSdkMessage(msg, state) {
|
|
|
191
208
|
events.push({ kind: 'session:compacting', active: sdkStatus === 'compacting' });
|
|
192
209
|
return events;
|
|
193
210
|
}
|
|
194
|
-
if (subtype === 'task_started' ||
|
|
211
|
+
if (subtype === 'task_started' ||
|
|
212
|
+
subtype === 'task_progress' ||
|
|
213
|
+
subtype === 'task_notification' ||
|
|
214
|
+
subtype === 'task_updated') {
|
|
195
215
|
const taskId = typeof parsed.task_id === 'string' ? parsed.task_id : undefined;
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
const toolUseId = typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : undefined;
|
|
217
|
+
if (taskId && toolUseId)
|
|
218
|
+
state.taskToolUseIds.set(taskId, toolUseId);
|
|
219
|
+
const toolCallId = toolUseId ?? (taskId ? (state.taskToolUseIds.get(taskId) ?? taskId) : undefined);
|
|
220
|
+
if (!toolCallId)
|
|
221
|
+
return events;
|
|
222
|
+
let status;
|
|
223
|
+
let phase;
|
|
224
|
+
if (subtype === 'task_updated') {
|
|
225
|
+
// Only a terminal patch status is a lifecycle signal; other patches
|
|
226
|
+
// (pending/running/paused, description, is_backgrounded...) are ignored.
|
|
227
|
+
const patch = parsed.patch;
|
|
228
|
+
const patchStatus = TASK_UPDATED_TERMINAL_STATUS[String(patch?.status)];
|
|
229
|
+
if (!patchStatus)
|
|
230
|
+
return events;
|
|
231
|
+
status = patchStatus;
|
|
232
|
+
}
|
|
233
|
+
else if (subtype === 'task_notification') {
|
|
234
|
+
// task_notification is only ever emitted once a task settles: an
|
|
235
|
+
// unknown status value is still terminal (`done`), so a future SDK
|
|
236
|
+
// value never leaves the sub-agent stuck 'running'.
|
|
237
|
+
status = TASK_NOTIFICATION_STATUS[String(parsed.status)] ?? 'done';
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
status = 'running';
|
|
241
|
+
phase = subtype === 'task_started' ? 'started' : 'progress';
|
|
242
|
+
}
|
|
243
|
+
if (taskId) {
|
|
244
|
+
const hasAmbient = typeof parsed.ambient === 'boolean';
|
|
245
|
+
const hasSkip = typeof parsed.skip_transcript === 'boolean';
|
|
246
|
+
if (subtype === 'task_started') {
|
|
247
|
+
// A (re)launch defines the task's classification afresh.
|
|
248
|
+
state.taskFlags.set(taskId, {
|
|
249
|
+
...(hasAmbient ? { ambient: parsed.ambient } : {}),
|
|
250
|
+
...(hasSkip ? { skipTranscript: parsed.skip_transcript } : {}),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else if (hasAmbient || hasSkip) {
|
|
254
|
+
state.taskFlags.set(taskId, {
|
|
255
|
+
...state.taskFlags.get(taskId),
|
|
256
|
+
...(hasAmbient ? { ambient: parsed.ambient } : {}),
|
|
257
|
+
...(hasSkip ? { skipTranscript: parsed.skip_transcript } : {}),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
217
260
|
}
|
|
261
|
+
const flags = taskId ? state.taskFlags.get(taskId) : undefined;
|
|
262
|
+
const usage = parsed.usage;
|
|
263
|
+
const description = typeof parsed.description === 'string'
|
|
264
|
+
? parsed.description
|
|
265
|
+
: subtype === 'task_updated' &&
|
|
266
|
+
typeof parsed.patch?.description === 'string'
|
|
267
|
+
? parsed.patch.description
|
|
268
|
+
: undefined;
|
|
269
|
+
events.push({
|
|
270
|
+
kind: 'subagent:progress',
|
|
271
|
+
toolCallId,
|
|
272
|
+
...(taskId ? { taskId } : {}),
|
|
273
|
+
status,
|
|
274
|
+
...(phase ? { phase } : {}),
|
|
275
|
+
...(flags?.ambient ? { ambient: true } : {}),
|
|
276
|
+
...(flags?.skipTranscript ? { skipTranscript: true } : {}),
|
|
277
|
+
description,
|
|
278
|
+
taskType: typeof parsed.task_type === 'string' ? parsed.task_type : undefined,
|
|
279
|
+
lastToolName: typeof parsed.last_tool_name === 'string' ? parsed.last_tool_name : undefined,
|
|
280
|
+
totalTokens: typeof usage?.total_tokens === 'number' ? usage.total_tokens : undefined,
|
|
281
|
+
toolUses: typeof usage?.tool_uses === 'number' ? usage.tool_uses : undefined,
|
|
282
|
+
durationMs: typeof usage?.duration_ms === 'number' ? usage.duration_ms : undefined,
|
|
283
|
+
});
|
|
218
284
|
return events;
|
|
219
285
|
}
|
|
220
286
|
if (subtype === 'init') {
|
|
@@ -14,6 +14,7 @@ const KOBO_MCP_BRIEF = [
|
|
|
14
14
|
'• `kobo__set_workspace_status` when the mission is done or irrecoverably blocked. Never set `idle` while this session is active; use AskUserQuestion for clarification, or end the turn.',
|
|
15
15
|
'• `kobo__schedule_wakeup` / `kobo__cancel_wakeup` to schedule (or cancel) a follow-up session — prefer these over the built-in `ScheduleWakeup` tool.',
|
|
16
16
|
'Each tool carries its own "WHEN to use" guidance in its description — follow it.',
|
|
17
|
+
'Only one agent edits the worktree at a time: do not run background subagents that modify files while you or another subagent modify files too. Parallel read-only subagents (search, analysis, review) are fine. Wait for a file-editing subagent to finish before editing or running tests yourself, and remember that changes appearing in the worktree may come from your own subagents.',
|
|
17
18
|
].join('\n');
|
|
18
19
|
/**
|
|
19
20
|
* Appended to the Claude Code preset system prompt so the rule is always in
|
|
@@ -77,6 +77,7 @@ export function createCodexEngine() {
|
|
|
77
77
|
model: options.model,
|
|
78
78
|
effort: options.effort,
|
|
79
79
|
agentPermissionMode: options.agentPermissionMode ?? 'bypass',
|
|
80
|
+
readOnly: options.readOnly,
|
|
80
81
|
resumeFromEngineSessionId: options.resumeFromEngineSessionId,
|
|
81
82
|
workingDir: options.workingDir,
|
|
82
83
|
mcpServers: options.mcpServers,
|
|
@@ -339,13 +340,35 @@ export function createCodexEngine() {
|
|
|
339
340
|
if (n.item.type === 'fileChange')
|
|
340
341
|
fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
|
|
341
342
|
const events = mapTurnEvents(n, (state) => handleItemCompleted(n.item, state));
|
|
343
|
+
const collabItem = n.item.type === 'collabAgentToolCall' ? n.item : undefined;
|
|
344
|
+
const isActiveAgent = (status) => status === 'pendingInit' || status === 'running';
|
|
345
|
+
const progressIndex = events.findIndex((event) => event.kind === 'subagent:progress');
|
|
346
|
+
if (collabItem && progressIndex >= 0) {
|
|
347
|
+
// The card that first tracked a thread (normally its spawnAgent)
|
|
348
|
+
// owns it until it finishes. A later wait/sendInput/resumeAgent
|
|
349
|
+
// on an already-owned thread (e.g. a wait that timed out) must
|
|
350
|
+
// not steal it, or the owner card is orphaned in `running`; such
|
|
351
|
+
// a card is done as soon as its own call completes.
|
|
352
|
+
const progress = events[progressIndex];
|
|
353
|
+
const ownsActiveThread = Object.entries(collabItem.agentsStates).some(([threadId, agentState]) => {
|
|
354
|
+
if (!isActiveAgent(agentState.status))
|
|
355
|
+
return false;
|
|
356
|
+
const owner = activeSubagentThreads.get(threadId);
|
|
357
|
+
return !owner || owner.toolCallId === progress.toolCallId;
|
|
358
|
+
});
|
|
359
|
+
if (progress.status === 'running' && !ownsActiveThread) {
|
|
360
|
+
events[progressIndex] = { ...progress, status: 'done' };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
342
363
|
for (const ev of events)
|
|
343
364
|
safeEmit(ev);
|
|
344
|
-
if (
|
|
345
|
-
const progress =
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
365
|
+
if (collabItem) {
|
|
366
|
+
const progress = progressIndex >= 0
|
|
367
|
+
? events[progressIndex]
|
|
368
|
+
: undefined;
|
|
369
|
+
for (const [threadId, agentState] of Object.entries(collabItem.agentsStates)) {
|
|
370
|
+
if (isActiveAgent(agentState.status)) {
|
|
371
|
+
if (progress && !activeSubagentThreads.has(threadId)) {
|
|
349
372
|
activeSubagentThreads.set(threadId, {
|
|
350
373
|
toolCallId: progress.toolCallId,
|
|
351
374
|
description: progress.description,
|
|
@@ -11,6 +11,7 @@ const CODEX_KOBO_MCP_BRIEF = [
|
|
|
11
11
|
'• `mcp__kobo-tasks__set_workspace_status` when the mission is done or irrecoverably blocked. Never set `idle` while this session is active; ask the user or end the turn instead.',
|
|
12
12
|
'• `mcp__kobo-tasks__schedule_wakeup` / `mcp__kobo-tasks__cancel_wakeup` to schedule (or cancel) a follow-up session.',
|
|
13
13
|
'Each tool carries its own "WHEN to use" guidance in its description — follow it.',
|
|
14
|
+
'Only one agent edits the worktree at a time: do not run background subagents that modify files while you or another subagent modify files too. Parallel read-only subagents (search, analysis, review) are fine. Wait for a file-editing subagent to finish before editing or running tests yourself, and remember that changes appearing in the worktree may come from your own subagents.',
|
|
14
15
|
].join('\n');
|
|
15
16
|
export function buildCodexOptions(input) {
|
|
16
17
|
const isResume = input.resumeFromEngineSessionId !== undefined;
|
|
@@ -38,6 +39,10 @@ export function buildCodexOptions(input) {
|
|
|
38
39
|
threadParams.approvalPolicy = 'unless-trusted';
|
|
39
40
|
break;
|
|
40
41
|
}
|
|
42
|
+
if (input.readOnly) {
|
|
43
|
+
threadParams.sandbox = 'read-only';
|
|
44
|
+
threadParams.approvalPolicy = 'never';
|
|
45
|
+
}
|
|
41
46
|
// Model: omit when undefined or 'auto', let Codex use its default
|
|
42
47
|
if (input.model && input.model !== 'auto') {
|
|
43
48
|
threadParams.model = input.model;
|
|
@@ -70,7 +75,8 @@ export function buildCodexOptions(input) {
|
|
|
70
75
|
// model/effort because collaborationMode takes precedence over them. Automatic
|
|
71
76
|
// model selection is filled from thread/start or thread/resume by the engine.
|
|
72
77
|
const collaborationMode = {
|
|
73
|
-
|
|
78
|
+
// A read-only review must not stop on a plan-mode question (see readOnly).
|
|
79
|
+
mode: input.agentPermissionMode === 'plan' && !input.readOnly ? 'plan' : 'default',
|
|
74
80
|
settings: {
|
|
75
81
|
...(threadParams.model ? { model: threadParams.model } : {}),
|
|
76
82
|
reasoning_effort: threadParams.modelReasoningEffort ?? null,
|
|
@@ -34,6 +34,14 @@ export class InterruptAgentError extends Error {
|
|
|
34
34
|
this.name = 'InterruptAgentError';
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
export class StopSubagentsError extends Error {
|
|
38
|
+
code;
|
|
39
|
+
constructor(message, code) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.name = 'StopSubagentsError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
37
45
|
// ── State ──────────────────────────────────────────────────────────────────────
|
|
38
46
|
/** Actual bound port of the running backend — set at startup via setBackendPort() */
|
|
39
47
|
let backendPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000;
|
|
@@ -1373,6 +1381,9 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1373
1381
|
activateSession(workspaceId, agentSessionId);
|
|
1374
1382
|
captureTaskProgressBaseline(workspaceId, agentSessionId);
|
|
1375
1383
|
const settings = ws ? readEffectiveSettingsSafe(ws.projectPath) : readEffectiveSettingsSafe(workingDir);
|
|
1384
|
+
// A review whose report returns to the original session must end by itself:
|
|
1385
|
+
// enforce read-only plan mode here, not only through its prompt.
|
|
1386
|
+
const readOnly = getReviewReturn(workspaceId, agentSessionId) !== null;
|
|
1376
1387
|
const unavailableIntegrations = [];
|
|
1377
1388
|
const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), (name) => unavailableIntegrations.push(name));
|
|
1378
1389
|
const options = {
|
|
@@ -1382,7 +1393,8 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1382
1393
|
model,
|
|
1383
1394
|
effort: reasoningEffort,
|
|
1384
1395
|
// Cascade: explicit caller override → workspace setting → 'bypass'.
|
|
1385
|
-
agentPermissionMode: agentPermissionMode ?? ws?.agentPermissionMode ?? 'bypass',
|
|
1396
|
+
agentPermissionMode: readOnly ? 'plan' : (agentPermissionMode ?? ws?.agentPermissionMode ?? 'bypass'),
|
|
1397
|
+
readOnly,
|
|
1386
1398
|
resumeFromEngineSessionId,
|
|
1387
1399
|
backendUrl: `http://127.0.0.1:${backendPort}`,
|
|
1388
1400
|
koboHome: (() => {
|
|
@@ -1490,6 +1502,33 @@ export function interruptAgent(workspaceId, options = {}) {
|
|
|
1490
1502
|
autoLoopService.disable(workspaceId, 'user-action');
|
|
1491
1503
|
}
|
|
1492
1504
|
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Stop one running sub-agent (by its canonical id: task id or tool call id)
|
|
1507
|
+
* or, without id, every running sub-agent. The main turn keeps running.
|
|
1508
|
+
* Returns the number of sub-agents a stop was requested for.
|
|
1509
|
+
*/
|
|
1510
|
+
export function stopSubagents(workspaceId, id) {
|
|
1511
|
+
const ctrl = controllers.get(workspaceId);
|
|
1512
|
+
if (!ctrl) {
|
|
1513
|
+
throw new StopSubagentsError(`No agent running for workspace '${workspaceId}'`, 'no_agent_running');
|
|
1514
|
+
}
|
|
1515
|
+
let stopped;
|
|
1516
|
+
try {
|
|
1517
|
+
stopped = ctrl.stopSubagents(id === undefined ? undefined : [id]);
|
|
1518
|
+
}
|
|
1519
|
+
catch (err) {
|
|
1520
|
+
// A controller whose engine has not started yet has nothing to stop.
|
|
1521
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1522
|
+
throw new StopSubagentsError(`No agent running for workspace '${workspaceId}': ${message}`, 'no_agent_running');
|
|
1523
|
+
}
|
|
1524
|
+
if (stopped === undefined) {
|
|
1525
|
+
throw new StopSubagentsError(`The agent engine of workspace '${workspaceId}' cannot stop a single sub-agent`, 'unsupported');
|
|
1526
|
+
}
|
|
1527
|
+
if (id !== undefined && stopped === 0) {
|
|
1528
|
+
throw new StopSubagentsError(`Sub-agent '${id}' is not running for workspace '${workspaceId}'`, 'subagent_not_running');
|
|
1529
|
+
}
|
|
1530
|
+
return stopped;
|
|
1531
|
+
}
|
|
1493
1532
|
/** Bound the caller's wait, retaining ownership until actual engine shutdown. */
|
|
1494
1533
|
export const STOP_AGENT_TIMEOUT_MS = 15_000;
|
|
1495
1534
|
async function stopController(workspaceId, ctrl, cause = 'user') {
|
|
@@ -121,6 +121,18 @@ export class SessionController {
|
|
|
121
121
|
throw new Error('SessionController not started');
|
|
122
122
|
this._engineProcess.interrupt();
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Stop running sub-agents without interrupting the main turn. Returns the
|
|
126
|
+
* number of sub-agents a stop was requested for, or `undefined` when the
|
|
127
|
+
* engine cannot stop a single sub-agent.
|
|
128
|
+
*/
|
|
129
|
+
stopSubagents(ids) {
|
|
130
|
+
if (!this._engineProcess)
|
|
131
|
+
throw new Error('SessionController not started');
|
|
132
|
+
if (typeof this._engineProcess.stopSubagents !== 'function')
|
|
133
|
+
return undefined;
|
|
134
|
+
return this._engineProcess.stopSubagents(ids);
|
|
135
|
+
}
|
|
124
136
|
stop() {
|
|
125
137
|
if (!this._stopPromise) {
|
|
126
138
|
this._stopPromise = this.stopEngine().catch((err) => {
|
|
@@ -44,7 +44,8 @@ function resolveConfiguration(workspace, body) {
|
|
|
44
44
|
engine: engineId,
|
|
45
45
|
model: body.model ?? (changedEngine ? 'auto' : workspace.model),
|
|
46
46
|
reasoningEffort: body.reasoningEffort ?? (changedEngine ? 'auto' : workspace.reasoningEffort),
|
|
47
|
-
|
|
47
|
+
// A review that hands its report back must end by itself: always read-only.
|
|
48
|
+
agentPermissionMode: body.returnToSession ? 'plan' : (body.agentPermissionMode ?? workspace.agentPermissionMode),
|
|
48
49
|
};
|
|
49
50
|
// Preserve an existing custom model when unchanged; reject incompatible selections.
|
|
50
51
|
if ((changedEngine || config.model !== workspace.model) &&
|
|
@@ -115,9 +116,12 @@ export async function startWorkspaceReview(id, input) {
|
|
|
115
116
|
baseCommit,
|
|
116
117
|
additionalInstructions: (body.additionalInstructions ?? '').trim(),
|
|
117
118
|
});
|
|
119
|
+
// The return fires only when the review session ends by itself: a reviewer
|
|
120
|
+
// that starts fixing, or waits on a question, never hands back.
|
|
118
121
|
if (body.returnToSession)
|
|
119
122
|
rendered +=
|
|
120
|
-
'\n\
|
|
123
|
+
'\n\nThis is a read-only review session; these rules override any template instruction or skill that suggests fixing issues. Do not modify, create or delete files, and do not commit or push: the original agent session applies the fixes. Running read-only checks such as tests or linters is fine. Do not ask the user questions or wait for input: note any decision that needs the user in your summary instead.' +
|
|
124
|
+
'\n\nFinish with a standalone summary of your review findings, including severity, file/line references, recommended fixes and any remaining uncertainty. This final message will be handed back to the original agent session. Do not rely on earlier messages to explain your findings.';
|
|
121
125
|
assertWorkspaceLifecycleAvailable(id);
|
|
122
126
|
const current = workspaceService.getWorkspace(id);
|
|
123
127
|
if (!current || current.archivedAt || current.worktreePurgedAt || current.status === 'compacting')
|