@loicngr/kobo 2.1.2 → 2.1.5
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 +12 -0
- package/CONFIGURATION.md +11 -3
- package/dist/server/index.js +24 -91
- package/dist/server/routes/git.js +6 -7
- package/dist/server/routes/settings.js +21 -11
- package/dist/server/routes/workspaces.js +82 -41
- package/dist/server/services/agent/engines/claude-code/engine.js +87 -20
- 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 +27 -5
- package/dist/server/services/agent/engines/codex/options-builder.js +1 -0
- package/dist/server/services/agent/integration-mcp.js +4 -3
- package/dist/server/services/agent/orchestrator.js +36 -1
- package/dist/server/services/agent/session-controller.js +12 -0
- package/dist/server/services/engine-handoff-service.js +120 -24
- package/dist/server/services/pr-checkout-service.js +90 -53
- package/dist/server/services/pr-watcher-service.js +13 -7
- package/dist/server/services/retention/worker.js +40 -0
- package/dist/server/services/review-service.js +8 -4
- package/dist/server/services/session-event-metrics.js +43 -0
- package/dist/server/services/session-handoff-service.js +6 -4
- package/dist/server/services/templates-service.js +67 -49
- package/dist/server/services/terminal-service.js +45 -14
- package/dist/server/services/terminal-websocket-service.js +143 -0
- package/dist/server/services/wakeup-service.js +23 -21
- package/dist/server/services/workspace-service.js +3 -44
- package/dist/server/services/workspace-template-service.js +16 -18
- package/dist/server/services/worktree-purge-service.js +1 -4
- package/dist/server/services/ws-events-retention-service.js +197 -104
- package/dist/server/services/ws-events-retention-worker-service.js +112 -0
- package/dist/server/utils/atomic-json-file.js +23 -0
- package/dist/server/utils/git-ops.js +262 -60
- package/dist/shared/codex-models.js +20 -3
- package/dist/shared/models.js +12 -0
- package/package.json +8 -8
- package/src/client/dist/pwa/assets/{ActivityFeed-DCmiJP-z.css → ActivityFeed-B4lTKMVE.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DXWXioW9.js +8 -0
- package/src/client/dist/pwa/assets/ChangelogPage-BL5Jh8-v.js +1 -0
- package/src/client/dist/pwa/assets/ChangelogPage-DVyx9ZjV.css +1 -0
- package/src/client/dist/pwa/assets/{ClosePopup-BikuzYle.js → ClosePopup-C1a82pL9.js} +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-Bv2SZrVZ.js +2 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-ByVbqYCs.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-CMyl2j8Z.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-d43ITOC1.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-BEXXRguE.js +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-CtaHaVdf.css +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-B92w_jd7.js +8 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-C9JXyqHg.js +1 -0
- package/src/client/dist/pwa/assets/{DrawerToggleButton-Dfvkaro0.js → DrawerToggleButton-Btx8QwHo.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-C4wOcZV2.css → GitPanel-C3blk_Pd.css} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-Cgz9crLN.js +3 -0
- package/src/client/dist/pwa/assets/HealthPage-CDhgJQkL.js +1 -0
- package/src/client/dist/pwa/assets/HealthPage-DT8FGrKA.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-DTTtj9sq.js +2 -0
- package/src/client/dist/pwa/assets/{MainLayout-gHENJEFQ.css → MainLayout-knNNtkre.css} +1 -1
- package/src/client/dist/pwa/assets/QBadge-rGILsUYl.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-7EWRdc4U.js +1 -0
- package/src/client/dist/pwa/assets/QBtn-b_baQtAt.js +1 -0
- package/src/client/dist/pwa/assets/QBtnDropdown-vjat4GZl.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-CG1kWhFm.js +1 -0
- package/src/client/dist/pwa/assets/QExpansionItem-BQuy89vv.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-BrVklYfM.js +1 -0
- package/src/client/dist/pwa/assets/QList-BVX-b8nc.js +1 -0
- package/src/client/dist/pwa/assets/QMarkupTable-B8_FKLvo.js +1 -0
- package/src/client/dist/pwa/assets/QPage-BCDXjfVj.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-DBW_TZri.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-BsKmccpX.js +1 -0
- package/src/client/dist/pwa/assets/QTooltip-DzYbguTR.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BX6iKh6S.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-CBM_GpS3.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-_In2gH2P.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-BqkNYzC5.js +9 -0
- package/src/client/dist/pwa/assets/{SettingsPage-BYdLXJEJ.css → SettingsPage-CpykRKq2.css} +1 -1
- package/src/client/dist/pwa/assets/SplitWorkspacePage-DRoXbSRP.js +1 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-B9LDe7uG.js → TerminalPanel-CfqiuSuk.js} +2 -2
- package/src/client/dist/pwa/assets/{TerminalPanel-DbVZk0vD.css → TerminalPanel-Dz9EE-Ph.css} +1 -1
- package/src/client/dist/pwa/assets/TouchPan-DcOCRf7x.js +1 -0
- package/src/client/dist/pwa/assets/{TourReplayButton-Dtv_UDaY.js → TourReplayButton-gp7m6tSV.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DSw8Upcp.css +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-IcoMGkJh.js +1 -0
- package/src/client/dist/pwa/assets/{WorkspacePage--5HsnMQ1.css → WorkspacePage-BnW-gp3u.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-CR2xAzW_.js +4 -0
- package/src/client/dist/pwa/assets/{browser-B15GGwtW.js → browser-BveHkbe-.js} +1 -1
- package/src/client/dist/pwa/assets/build-path-tree-Dtp7rlxS.js +1 -0
- package/src/client/dist/pwa/assets/custom-sounds-CBoWzJ85.js +1 -0
- package/src/client/dist/pwa/assets/de-BJsWkZro.js +17 -0
- package/src/client/dist/pwa/assets/defineProperty-BbfpZ9Tg.js +1 -0
- package/src/client/dist/pwa/assets/documents-sb2sQ4JC.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-DhaLhNl-.js → editor.api-CbkPxoJf.js} +1 -1
- package/src/client/dist/pwa/assets/efforts-BCli1Unj.js +9 -0
- package/src/client/dist/pwa/assets/es-tNYw7itB.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-CfaEhb2f.js +1 -0
- package/src/client/dist/pwa/assets/{format-2JisC3s1.js → format-BRl2DUlm.js} +7 -7
- package/src/client/dist/pwa/assets/formatters-C5TjPSR_.js +1 -0
- package/src/client/dist/pwa/assets/fr-B5sFski4.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-PYrDqU04.js → freemarker2-CDmvOQyn.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BuiY0fFf.js → handlebars-Bx_w8mx1.js} +1 -1
- package/src/client/dist/pwa/assets/{html-Du3cjdug.js → html-DVRfiRkS.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-Bm-L4nW3.js +1 -0
- package/src/client/dist/pwa/assets/i18n-D21JKsVt.js +19 -0
- package/src/client/dist/pwa/assets/index-C7vFf6p2.js +2 -0
- package/src/client/dist/pwa/assets/index-C8zAZ-k-.css +1 -0
- package/src/client/dist/pwa/assets/it-Bc2FncM9.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-Bb48hPli.js → javascript-CPvttU5J.js} +1 -1
- package/src/client/dist/pwa/assets/latest-request-CN4-B22z.js +1 -0
- package/src/client/dist/pwa/assets/{liquid-Be77iQe3.js → liquid-CI0gQddb.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-CfohNJja.js → mdx-BveBwYJH.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CyM4wsIg.js → monaco-contributions-CHXFh4P2.js} +1 -1
- package/src/client/dist/pwa/assets/monaco.contribution-DOK8t_9B.js +2 -0
- package/src/client/dist/pwa/assets/network-auth-CItnNZDh.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-DvvaSyg1.js → notify-theme-BO6r1Rd7.js} +1 -1
- package/src/client/dist/pwa/assets/pending-diff-open-DpS4iyDM.js +1 -0
- package/src/client/dist/pwa/assets/{permissionModes-B-ciuUbz.js → permissionModes-Cc5VaOdC.js} +1 -1
- package/src/client/dist/pwa/assets/preload-helper-BZ1Pz5am.js +1 -0
- 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-Be0t12-n.js → project-color-Bnv02P9D.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BNemmtx8.js → python-BesLCxpi.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-3NM8iFVf.js → razor-Dv1PMxuY.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-BWJNIDv3.js → render-chat-markdown-CafzCcmu.js} +1 -1
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-DOiZcasU.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-DwyZ4CF7.js → typescript-26L2Xsu9.js} +1 -1
- package/src/client/dist/pwa/assets/use-checkbox-BLeif5-t.js +1 -0
- 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-Mv3pIO6p.js +1 -0
- 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-CjASQcG5.js → use-tours-B5uIqxRS.js} +2 -2
- package/src/client/dist/pwa/assets/use-worktree-restore-BBChALUM.js +1 -0
- package/src/client/dist/pwa/assets/workspace-templates-CDY-MiH2.js +1 -0
- package/src/client/dist/pwa/assets/{xml-B1UujXdS.js → xml-CkRojTcc.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-mHN8kNgU.js → yaml-DKkQHwew.js} +1 -1
- package/src/client/dist/pwa/index.html +12 -9
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CO5rOaa5.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-DKJkFiU_.css +0 -1
- package/src/client/dist/pwa/assets/ChangelogPage-DNRcPRtW.js +0 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-CRRaDs2D.css +0 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-DZoDqgJI.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-wNH35uTG.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DUyy4SzS.js +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-XiCj24i2.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-CZ5QwXqi.js +0 -8
- package/src/client/dist/pwa/assets/DocumentsPanel-sBatgKA-.js +0 -1
- package/src/client/dist/pwa/assets/GitPanel-B8e1olfm.js +0 -3
- package/src/client/dist/pwa/assets/HealthPage-BwXYpczh.js +0 -1
- package/src/client/dist/pwa/assets/HealthPage-CFZ-tdKK.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-DFXIS_wv.js +0 -2
- package/src/client/dist/pwa/assets/QBadge-CQF1smL-.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-bdDA-A5g.js +0 -1
- package/src/client/dist/pwa/assets/QBtn-CcQQrYA_.js +0 -1
- package/src/client/dist/pwa/assets/QBtnDropdown-C-1EhO9J.js +0 -1
- package/src/client/dist/pwa/assets/QBtnGroup-wd5lKRXu.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-BnC8rTql.js +0 -1
- package/src/client/dist/pwa/assets/QChip-D75nKi-L.js +0 -1
- package/src/client/dist/pwa/assets/QExpansionItem-RpMPvfxX.js +0 -1
- package/src/client/dist/pwa/assets/QItemSection-Bt9H35JJ.js +0 -1
- package/src/client/dist/pwa/assets/QList-D5msNbSZ.js +0 -1
- package/src/client/dist/pwa/assets/QMarkupTable-D3REW6RC.js +0 -1
- package/src/client/dist/pwa/assets/QPage-HAaeQ-cv.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-Dpru_T81.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-DDsPDUH9.js +0 -1
- package/src/client/dist/pwa/assets/QTooltip-C31vXtse.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-D6LTiRFg.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-Dj487BdP.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-CJmgFiAa.js +0 -9
- package/src/client/dist/pwa/assets/SplitWorkspacePage-BGSDgGIO.js +0 -1
- package/src/client/dist/pwa/assets/TouchPan-DJqqxDBF.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CrLP_I5S.css +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-VlJCIkwU.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-B_SlX8YB.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-CU6uXunR.js +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-CDJsgChY.js +0 -1
- package/src/client/dist/pwa/assets/de-B_Y4EArT.js +0 -17
- package/src/client/dist/pwa/assets/documents-Cf6nmu8Z.js +0 -1
- package/src/client/dist/pwa/assets/efforts-DoYZi0AZ.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-C9XiCfvv.js +0 -1
- package/src/client/dist/pwa/assets/es-DtsTTShP.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-LDLM-6iN.js +0 -1
- package/src/client/dist/pwa/assets/formatters-ZVNs2VOl.js +0 -1
- package/src/client/dist/pwa/assets/fr-C82MIVrA.js +0 -17
- package/src/client/dist/pwa/assets/i18n-BumeGGx_.js +0 -1
- package/src/client/dist/pwa/assets/index-DolbyIM4.js +0 -20
- package/src/client/dist/pwa/assets/index-VMFLfvp2.css +0 -1
- package/src/client/dist/pwa/assets/it-BJNuTcj2.js +0 -17
- package/src/client/dist/pwa/assets/monaco.contribution-BcIfN6Mi.js +0 -2
- package/src/client/dist/pwa/assets/network-auth-B1xAaHmx.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-B6N0vI5n.js +0 -1
- package/src/client/dist/pwa/assets/pinia-CTJ3OfP3.js +0 -1
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-CNFKm4ij.js +0 -1
- package/src/client/dist/pwa/assets/use-checkbox-2xzX4yYj.js +0 -1
- package/src/client/dist/pwa/assets/use-id-vOr8PRTG.js +0 -1
- package/src/client/dist/pwa/assets/use-is-mobile-CJfSpTHx.js +0 -1
- package/src/client/dist/pwa/assets/use-search-index-status-Bluf9L99.js +0 -1
- package/src/client/dist/pwa/assets/workspace-templates-BxYxJt3S.js +0 -1
- /package/src/client/dist/pwa/assets/{engineFeatures-B-9jHocY.css → use-worktree-restore-B-9jHocY.css} +0 -0
|
@@ -124,9 +124,52 @@ export function createClaudeCodeEngine() {
|
|
|
124
124
|
async start(options, onEvent) {
|
|
125
125
|
const abortController = new AbortController();
|
|
126
126
|
const mapperState = createMapperState();
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
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
|
+
};
|
|
130
173
|
// Foreground tool calls currently awaiting their tool_result. Cleared on
|
|
131
174
|
// every `result` message: a turn cannot end with a tool still in flight.
|
|
132
175
|
const pendingToolCallIds = new Set();
|
|
@@ -260,6 +303,17 @@ export function createClaudeCodeEngine() {
|
|
|
260
303
|
sdkOptions.pathToClaudeCodeExecutable = explicitBinary;
|
|
261
304
|
const inputStream = new ClaudeInputStream(effectivePrompt);
|
|
262
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
|
+
};
|
|
263
317
|
let resolveReady;
|
|
264
318
|
let rejectReady;
|
|
265
319
|
const ready = new Promise((resolve, reject) => {
|
|
@@ -507,15 +561,14 @@ export function createClaudeCodeEngine() {
|
|
|
507
561
|
clearResultDrainWatchdog();
|
|
508
562
|
const events = mapSdkMessage(msg, mapperState);
|
|
509
563
|
for (const ev of events) {
|
|
510
|
-
if (ev.kind
|
|
511
|
-
|
|
512
|
-
if (ev.status === 'running')
|
|
513
|
-
activeSubagentTaskIds.set(ev.toolCallId, ev.taskId ?? ev.toolCallId);
|
|
514
|
-
else
|
|
515
|
-
activeSubagentTaskIds.delete(ev.toolCallId);
|
|
564
|
+
if (ev.kind === 'subagent:progress')
|
|
565
|
+
trackSubagentProgress(ev);
|
|
516
566
|
}
|
|
517
567
|
if (activeSubagentTaskIds.size > 0 &&
|
|
518
|
-
(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) ||
|
|
519
572
|
ev.kind === 'message:text' ||
|
|
520
573
|
ev.kind === 'message:thinking' ||
|
|
521
574
|
ev.kind === 'tool:call' ||
|
|
@@ -689,6 +742,28 @@ export function createClaudeCodeEngine() {
|
|
|
689
742
|
armSubagentStallWatchdog();
|
|
690
743
|
return true;
|
|
691
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
|
+
},
|
|
692
767
|
interrupt() {
|
|
693
768
|
userInterrupted = true;
|
|
694
769
|
// The SDK ends an interrupted run by emitting a `result` with
|
|
@@ -699,16 +774,8 @@ export function createClaudeCodeEngine() {
|
|
|
699
774
|
// background subagent task keeps running unless told to stop via
|
|
700
775
|
// its own API, which would otherwise leave the Stop button
|
|
701
776
|
// appearing to do nothing for up to SUBAGENT_STALL_TIMEOUT_MS.
|
|
702
|
-
for (const taskId of activeSubagentTaskIds
|
|
703
|
-
|
|
704
|
-
void q.stopTask(taskId).catch(() => {
|
|
705
|
-
/* best-effort */
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
catch {
|
|
709
|
-
/* best-effort */
|
|
710
|
-
}
|
|
711
|
-
}
|
|
777
|
+
for (const taskId of [...activeSubagentTaskIds, ...ambientSubagentTaskIds])
|
|
778
|
+
requestStopTask(taskId);
|
|
712
779
|
const qq = q;
|
|
713
780
|
if (typeof qq.interrupt === 'function') {
|
|
714
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
|
|
@@ -340,13 +340,35 @@ export function createCodexEngine() {
|
|
|
340
340
|
if (n.item.type === 'fileChange')
|
|
341
341
|
fileChanges.set(itemKey(n.threadId, n.turnId, n.item.id), n.item);
|
|
342
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
|
+
}
|
|
343
363
|
for (const ev of events)
|
|
344
364
|
safeEmit(ev);
|
|
345
|
-
if (
|
|
346
|
-
const progress =
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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)) {
|
|
350
372
|
activeSubagentThreads.set(threadId, {
|
|
351
373
|
toolCallId: progress.toolCallId,
|
|
352
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;
|
|
@@ -14,10 +14,11 @@ export function hasIntegrationMcpConfig(integration, selectedKey) {
|
|
|
14
14
|
return Boolean(readClaudeMcpEntry((name) => key ? name === key : integration === 'notion' ? name === 'notion' : /sentry/i.test(name)));
|
|
15
15
|
}
|
|
16
16
|
/** Resolve at every launch/resume; never write credentials into a project file. */
|
|
17
|
-
export function buildIntegrationMcpServers(settings, onUnavailable) {
|
|
17
|
+
export function buildIntegrationMcpServers(settings, engineId, onUnavailable) {
|
|
18
18
|
const servers = [];
|
|
19
|
-
//
|
|
20
|
-
|
|
19
|
+
// Codex needs fresh names to avoid recursively merging stale native config.
|
|
20
|
+
// Claude needs stable tool names to preserve its prompt cache across resumes.
|
|
21
|
+
const launch = engineId === 'codex' ? nanoid(12) : 'managed';
|
|
21
22
|
for (const integration of ['notion', 'sentry']) {
|
|
22
23
|
if (!settings[`${integration}Enabled`])
|
|
23
24
|
continue;
|
|
@@ -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;
|
|
@@ -1377,7 +1385,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1377
1385
|
// enforce read-only plan mode here, not only through its prompt.
|
|
1378
1386
|
const readOnly = getReviewReturn(workspaceId, agentSessionId) !== null;
|
|
1379
1387
|
const unavailableIntegrations = [];
|
|
1380
|
-
const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), (name) => unavailableIntegrations.push(name));
|
|
1388
|
+
const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), engine.id, (name) => unavailableIntegrations.push(name));
|
|
1381
1389
|
const options = {
|
|
1382
1390
|
workspaceId,
|
|
1383
1391
|
workingDir,
|
|
@@ -1494,6 +1502,33 @@ export function interruptAgent(workspaceId, options = {}) {
|
|
|
1494
1502
|
autoLoopService.disable(workspaceId, 'user-action');
|
|
1495
1503
|
}
|
|
1496
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
|
+
}
|
|
1497
1532
|
/** Bound the caller's wait, retaining ownership until actual engine shutdown. */
|
|
1498
1533
|
export const STOP_AGENT_TIMEOUT_MS = 15_000;
|
|
1499
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) => {
|
|
@@ -1,41 +1,137 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
1
|
import { getDb } from '../db/index.js';
|
|
3
|
-
|
|
2
|
+
import { getWorkingTreeDiffStatsAsync, getWorkingTreePorcelainAsync } from '../utils/git-ops.js';
|
|
3
|
+
const MAX_RECENT_MESSAGES = 6;
|
|
4
|
+
const MAX_HISTORY_ROWS = 10_000;
|
|
5
|
+
const MAX_MESSAGE_CHARS = 2_000;
|
|
6
|
+
const MAX_CONVERSATION_CHARS = 12_000;
|
|
7
|
+
const TRUNCATED = '[Earlier text omitted]\n';
|
|
8
|
+
/** Normalized text events are deltas; a non-streaming snapshot is complete. */
|
|
9
|
+
function readableFragment(row) {
|
|
4
10
|
try {
|
|
5
|
-
|
|
11
|
+
const payload = JSON.parse(row.payload);
|
|
12
|
+
if (!payload || typeof payload !== 'object')
|
|
13
|
+
return null;
|
|
14
|
+
if (row.type === 'user:message' &&
|
|
15
|
+
typeof payload.content === 'string' &&
|
|
16
|
+
(payload.sender === undefined || payload.sender === 'user')) {
|
|
17
|
+
return { key: `user:${row.id}`, role: 'user', text: payload.content, complete: true };
|
|
18
|
+
}
|
|
19
|
+
if (row.type === 'agent:event' && payload.kind === 'message:text' && typeof payload.text === 'string') {
|
|
20
|
+
return {
|
|
21
|
+
// A resumed/native session may reuse the same message id elsewhere.
|
|
22
|
+
key: JSON.stringify([row.session_id, typeof payload.messageId === 'string' ? payload.messageId : row.id]),
|
|
23
|
+
role: 'assistant',
|
|
24
|
+
text: payload.text,
|
|
25
|
+
complete: payload.streaming === false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
6
28
|
}
|
|
7
29
|
catch {
|
|
8
|
-
|
|
30
|
+
// Historical damaged rows are not evidence of a conversation message.
|
|
9
31
|
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function boundedText(text, limit) {
|
|
35
|
+
if (text.length <= limit)
|
|
36
|
+
return text;
|
|
37
|
+
const marker = '\n[Middle text omitted]\n';
|
|
38
|
+
const available = Math.max(0, limit - marker.length);
|
|
39
|
+
const first = Math.ceil(available / 2);
|
|
40
|
+
return `${text.slice(0, first)}${marker}${text.slice(-(available - first))}`.slice(0, limit);
|
|
10
41
|
}
|
|
11
42
|
function recentConversation(workspaceId, sessionId) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const filter = 'workspace_id = ? AND (? IS NULL OR session_id = ?)';
|
|
45
|
+
const parameters = [workspaceId, sessionId ?? null, sessionId ?? null];
|
|
46
|
+
// Pin the most recent real user message independently of streaming volume.
|
|
47
|
+
const userRows = db
|
|
48
|
+
.prepare(`SELECT rowid AS ordinal, id, type, payload, session_id FROM ws_events
|
|
49
|
+
WHERE ${filter} AND type = 'user:message' ORDER BY rowid DESC`)
|
|
50
|
+
.iterate(...parameters);
|
|
51
|
+
let latestUser;
|
|
52
|
+
for (const row of userRows) {
|
|
53
|
+
const fragment = readableFragment(row);
|
|
54
|
+
if (!fragment?.text)
|
|
55
|
+
continue;
|
|
56
|
+
latestUser = { ...fragment, ordinal: row.ordinal, truncated: false };
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const rows = db
|
|
60
|
+
.prepare(`SELECT rowid AS ordinal, id, type, payload, session_id FROM ws_events
|
|
61
|
+
WHERE ${filter} AND type IN ('user:message', 'agent:event')
|
|
62
|
+
ORDER BY rowid DESC LIMIT ?`)
|
|
63
|
+
.iterate(...parameters, MAX_HISTORY_ROWS + 1);
|
|
64
|
+
const messages = new Map();
|
|
65
|
+
let scanned = 0;
|
|
66
|
+
let historyTruncated = false;
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
if (++scanned > MAX_HISTORY_ROWS) {
|
|
69
|
+
historyTruncated = true;
|
|
70
|
+
break;
|
|
25
71
|
}
|
|
26
|
-
|
|
27
|
-
|
|
72
|
+
const fragment = readableFragment(row);
|
|
73
|
+
if (!fragment?.text)
|
|
74
|
+
continue;
|
|
75
|
+
const existing = messages.get(fragment.key);
|
|
76
|
+
if (existing) {
|
|
77
|
+
existing.ordinal = row.ordinal;
|
|
78
|
+
if (existing.complete)
|
|
79
|
+
continue;
|
|
80
|
+
const merged = fragment.text + existing.text;
|
|
81
|
+
existing.text = merged.slice(-MAX_MESSAGE_CHARS);
|
|
82
|
+
existing.truncated ||= merged.length > MAX_MESSAGE_CHARS;
|
|
83
|
+
existing.complete = fragment.complete;
|
|
28
84
|
}
|
|
29
|
-
|
|
30
|
-
|
|
85
|
+
else if (messages.size < MAX_RECENT_MESSAGES) {
|
|
86
|
+
messages.set(fragment.key, {
|
|
87
|
+
...fragment,
|
|
88
|
+
ordinal: row.ordinal,
|
|
89
|
+
text: fragment.text.slice(-MAX_MESSAGE_CHARS),
|
|
90
|
+
truncated: fragment.text.length > MAX_MESSAGE_CHARS,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const selected = [];
|
|
95
|
+
const warning = historyTruncated
|
|
96
|
+
? '\n\n[Conversation history truncated; recover older context from the source session.]'
|
|
97
|
+
: '';
|
|
98
|
+
let remaining = MAX_CONVERSATION_CHARS - warning.length;
|
|
99
|
+
if (latestUser) {
|
|
100
|
+
const pinned = `Latest user instruction:\n${boundedText(latestUser.text, 6_000)}`;
|
|
101
|
+
selected.push({ ordinal: latestUser.ordinal, text: pinned });
|
|
102
|
+
remaining -= pinned.length + 2;
|
|
103
|
+
messages.delete(latestUser.key);
|
|
104
|
+
}
|
|
105
|
+
const newestFirst = [...messages.values()].sort((a, b) => b.ordinal - a.ordinal);
|
|
106
|
+
for (const message of newestFirst) {
|
|
107
|
+
const label = message.role === 'assistant' ? 'Assistant:\n' : 'User:\n';
|
|
108
|
+
if (remaining <= label.length + TRUNCATED.length + 2)
|
|
109
|
+
break;
|
|
110
|
+
const text = message.truncated ? TRUNCATED + message.text : message.text;
|
|
111
|
+
const rendered = label + boundedText(text, Math.min(MAX_MESSAGE_CHARS, remaining - label.length - 2));
|
|
112
|
+
selected.push({ ordinal: message.ordinal, text: rendered });
|
|
113
|
+
remaining -= rendered.length + 2;
|
|
114
|
+
}
|
|
115
|
+
return (selected
|
|
116
|
+
.sort((a, b) => a.ordinal - b.ordinal)
|
|
117
|
+
.map((message) => message.text)
|
|
118
|
+
.join('\n\n') + warning);
|
|
31
119
|
}
|
|
32
120
|
/** Build a deterministic, secret-free handoff for a fresh session on another engine. */
|
|
33
|
-
export function buildEngineHandoff(workspace, sourceEngine, targetEngine, sourceSessionId) {
|
|
121
|
+
export async function buildEngineHandoff(workspace, sourceEngine, targetEngine, sourceSessionId) {
|
|
34
122
|
const taskLines = workspace.tasks.length
|
|
35
123
|
? workspace.tasks.map((task) => `- [${task.status === 'done' ? 'x' : ' '}] ${task.title}`).join('\n')
|
|
36
124
|
: '- No Kōbō task recorded.';
|
|
37
|
-
const
|
|
38
|
-
|
|
125
|
+
const [status, diff] = await Promise.allSettled([
|
|
126
|
+
getWorkingTreePorcelainAsync(workspace.worktreePath),
|
|
127
|
+
getWorkingTreeDiffStatsAsync(workspace.worktreePath),
|
|
128
|
+
]);
|
|
129
|
+
const changedFiles = status.status === 'fulfilled'
|
|
130
|
+
? status.value.trim() || 'No uncommitted changes detected.'
|
|
131
|
+
: 'Git state unavailable. Verify the working tree before making changes.';
|
|
132
|
+
const diffStat = diff.status === 'fulfilled'
|
|
133
|
+
? diff.value.trim() || 'No uncommitted diff stat available.'
|
|
134
|
+
: 'Git state unavailable. Run git diff --stat to recover the summary.';
|
|
39
135
|
const history = recentConversation(workspace.id, sourceSessionId) || 'No recent conversation message available.';
|
|
40
136
|
return `# Kōbō engine handoff\n\nYou are taking over this workspace from ${sourceEngine} using ${targetEngine}. Work in the existing worktree and verify the repository state before making changes.\n\n## Objective\n${workspace.description ?? workspace.name}\n\n## Tasks\n${taskLines}\n\n## Git state\n- Branch: ${workspace.workingBranch}\n- Base: ${workspace.sourceBranch}\n- Worktree: ${workspace.worktreePath}\n\nChanged files:\n\`\`\`\n${changedFiles}\n\`\`\`\n\nDiff summary:\n\`\`\`\n${diffStat}\n\`\`\`\n\n## Recent conversation\n${history}\n\n## Recover more context\nIf this handoff is insufficient, use \`kobo__read_workspace_events_csv\` to read the previous workspace conversation in pages (optionally filter by \`session_id\`). Use \`kobo__search_codebase\` to find a precise past decision or user request. Read only the context needed, then continue the task.\n`;
|
|
41
137
|
}
|