@loicngr/kobo 1.11.43 → 1.11.45
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 +74 -1
- package/CHANGELOG.md +11 -0
- package/README.md +13 -1
- package/dist/mcp-server/kobo-tasks-handlers.js +33 -75
- package/dist/mcp-server/kobo-tasks-server.js +82 -9
- package/dist/server/db/auto-loop-schema.js +44 -0
- package/dist/server/db/migrations.js +18 -0
- package/dist/server/db/review-return-schema.js +10 -0
- package/dist/server/db/schema.js +8 -1
- package/dist/server/db/session-handoff-schema.js +24 -0
- package/dist/server/index.js +12 -9
- package/dist/server/routes/auto-loop-messages.js +51 -0
- package/dist/server/routes/documents.js +1 -1
- package/dist/server/routes/workspaces.js +119 -145
- package/dist/server/services/agent/engines/claude-code/engine.js +16 -0
- package/dist/server/services/agent/engines/codex/engine.js +92 -31
- package/dist/server/services/agent/orchestrator.js +298 -96
- package/dist/server/services/agent/session-controller.js +42 -2
- package/dist/server/services/auto-loop-message-service.js +79 -0
- package/dist/server/services/auto-loop-service.js +225 -86
- package/dist/server/services/auto-loop-state-service.js +30 -0
- package/dist/server/services/cron-service.js +30 -16
- package/dist/server/services/engine-handoff-service.js +5 -5
- package/dist/server/services/quota-backoff-service.js +42 -10
- package/dist/server/services/review-return-service.js +64 -0
- package/dist/server/services/review-service.js +187 -0
- package/dist/server/services/session-activity-service.js +17 -0
- package/dist/server/services/session-handoff-runtime.js +26 -0
- package/dist/server/services/session-handoff-service.js +479 -0
- package/dist/server/services/settings-service.js +3 -3
- package/dist/server/services/task-mutations.js +186 -0
- package/dist/server/services/wakeup-service.js +24 -1
- package/dist/server/services/workspace-message-service.js +16 -2
- package/dist/server/services/workspace-service.js +28 -36
- package/dist/server/utils/workspace-lifecycle-guard.js +35 -20
- package/dist/shared/auto-loop-prompts.js +22 -11
- package/dist/shared/auto-loop-types.js +1 -0
- package/dist/shared/document-roots.js +2 -0
- package/dist/shared/review.js +6 -0
- package/dist/shared/session-handoff.js +4 -0
- package/dist/shared/task-verification.js +11 -0
- package/package.json +12 -12
- package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DX1WGFea.js → ChangelogPage-CnXTnFhe.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-BqW5dy-O.js → ClosePopup-CWjIRHxu.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-B4ne55Xj.js → ComparisonPanel-CKylN45q.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-Cv6HJEcO.js → DashboardPage-CZQLfoV1.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-COwGxPt6.js → DiffViewer-Csf3WmLc.js} +4 -4
- package/src/client/dist/pwa/assets/{DocumentsPanel-DCXpLLb7.js → DocumentsPanel-BeCttB-x.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-DRL3RCkM.js → DrawerToggleButton-BJBBYeOc.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-B-ZHgghQ.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-XrOuMoDJ.js → HealthPage-l88TEk-k.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-BAAQLq95.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-Cf4Q9x1u.js +2 -0
- package/src/client/dist/pwa/assets/{QBadge-DbkxGtht.js → QBadge-FWC1zDCq.js} +1 -1
- package/src/client/dist/pwa/assets/{QBanner-DhqH95GW.js → QBanner-CcRI_QIy.js} +1 -1
- package/src/client/dist/pwa/assets/QBtn-DEdx_Y1V.js +1 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-163oM6T_.js → QBtnDropdown-44Mj-y_1.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnGroup-DELKYo3u.js → QBtnGroup-CAoD6G0Q.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnToggle-BI3k3Ch2.js → QBtnToggle-C_hQ2UCQ.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-owciRuZu.js → QChip-DCPUhqXX.js} +1 -1
- package/src/client/dist/pwa/assets/QDrawer-B4qHw8wt.js +1 -0
- package/src/client/dist/pwa/assets/QExpansionItem-Bok1m8Wi.js +1 -0
- package/src/client/dist/pwa/assets/{QItemSection-D_5_0UrX.js → QItemSection-BhqphaH6.js} +1 -1
- package/src/client/dist/pwa/assets/{QList-qupzuEqO.js → QList-CHm4JNP0.js} +1 -1
- package/src/client/dist/pwa/assets/{QMarkupTable-B9QuKtK_.js → QMarkupTable-CNpiA9oq.js} +1 -1
- package/src/client/dist/pwa/assets/{QPage-DaX9sMQC.js → QPage-BXBw2Alx.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +1 -0
- package/src/client/dist/pwa/assets/{QSpinnerDots-DnCV3VzI.js → QSpinnerDots-BkFiCsH3.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-BQWY7aeM.js → QTooltip-BDihn2aY.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-DOHkP5lj.js → SearchPage-BwZ2TX1G.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-DL4kp9Ah.js → SplitWorkspacePage-Bg6nKBcA.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DHDnyiYh.js → TerminalPanel-Cq-FR7A7.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-BnHXDR6F.js → TouchPan-BQ1cvzRC.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-ClVcB6mO.js → TourReplayButton-Dz1yEz_z.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-Bxe6oIJI.js → WorkspaceAttentionLabels-BCVOP8vp.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BbUvR1Vf.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +4 -0
- package/src/client/dist/pwa/assets/{browser-Bkx4fbxl.js → browser-BmWgBx9M.js} +1 -1
- package/src/client/dist/pwa/assets/build-path-tree-6EjBmYYY.js +1 -0
- package/src/client/dist/pwa/assets/de-COObokpu.js +17 -0
- package/src/client/dist/pwa/assets/documents-BZ8bwcjr.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-DreEG0If.js → editor.api-CK7--ckY.js} +1 -1
- package/src/client/dist/pwa/assets/editor.worker-iO5iN0DB.js +33 -0
- package/src/client/dist/pwa/assets/efforts-Bw0o2Bud.js +9 -0
- package/src/client/dist/pwa/assets/engineFeatures-B-9jHocY.css +1 -0
- package/src/client/dist/pwa/assets/engineFeatures-DQO9HHXI.js +1 -0
- package/src/client/dist/pwa/assets/es-By2Q9Tca.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-C8VyjASo.js +1 -0
- package/src/client/dist/pwa/assets/{format-B7z3F-Mw.js → format-LtTLLitJ.js} +3 -3
- package/src/client/dist/pwa/assets/{formatters-BTBiU0v3.js → formatters-Bl9zA7Ai.js} +1 -1
- package/src/client/dist/pwa/assets/fr-D-sd0ddn.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-BZNTVQ3K.js → freemarker2-DxezMaje.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-k8tN0dI3.js → handlebars-DK_Bj6Rx.js} +1 -1
- package/src/client/dist/pwa/assets/{html-iZL3bbXq.js → html-DFfsfStD.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +1 -0
- package/src/client/dist/pwa/assets/index-0wtBh1Xt.css +1 -0
- package/src/client/dist/pwa/assets/index-BLd8DAzg.js +20 -0
- package/src/client/dist/pwa/assets/it-CeK5lyIA.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-TV6gg0MO.js → javascript-DuNbcaL7.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Cifa5ujF.js → liquid-CM3JKyhv.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-Bnj_8yGE.js → mdx-BG3zYwNF.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-pEh0n9Wk.js → monaco-contributions-DLscZGH-.js} +1 -1
- package/src/client/dist/pwa/assets/monaco.contribution-DnhZHwXT.js +2 -0
- package/src/client/dist/pwa/assets/{network-auth-3_5p4fdc.js → network-auth-BJiPbO5l.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-D_nf9AeF.js → notify-theme-D4p3qPkp.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-CUnxC1xw.js → pending-diff-open-dYIFdS4r.js} +1 -1
- package/src/client/dist/pwa/assets/permissionModes-B7fuUegU.js +36 -0
- package/src/client/dist/pwa/assets/{pinia-BKqmoVru.js → pinia-CTJ3OfP3.js} +1 -1
- package/src/client/dist/pwa/assets/{private.use-form-C7umbGO-.js → private.use-form-Bk8ti264.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-CihwpDAA.js → project-color-R_1je4vW.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DOoNBSNL.js → python-COs6pHk2.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-Bvq5vVpJ.js → razor-Jl1VqMsk.js} +1 -1
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-Bo9anJjg.js +1 -0
- package/src/client/dist/pwa/assets/render-chat-markdown-AaOo0Tgq.js +67 -0
- package/src/client/dist/pwa/assets/{runtime-core.esm-bundler-CDnVUfeZ.js → runtime-core.esm-bundler-CNFKm4ij.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-VVzxoeyM.js → typescript-BU_TUuAM.js} +1 -1
- package/src/client/dist/pwa/assets/{use-checkbox-FnauAgTp.js → use-checkbox-CnvHBhSK.js} +1 -1
- package/src/client/dist/pwa/assets/{use-id-BIuaWKkB.js → use-id-wXd4c8FA.js} +1 -1
- package/src/client/dist/pwa/assets/use-is-mobile-DpKI3XMy.js +1 -0
- package/src/client/dist/pwa/assets/{use-search-index-status-DtLzygcy.js → use-search-index-status-Bluf9L99.js} +1 -1
- package/src/client/dist/pwa/assets/use-slide-transition-CiaDm-tR.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-CVn874kD.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-1ui80Kqo.js → workspace-templates-BCKR8TXr.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-CzGFIxQa.js → xml-gFFsobxE.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-Bl7nDJeC.js → yaml-DaXaqaGD.js} +1 -1
- package/src/client/dist/pwa/index.html +10 -10
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +8 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +72 -105
- package/src/mcp-server/kobo-tasks-server.ts +94 -9
- package/src/client/dist/pwa/assets/ActivityFeed-D1e-zV3O.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-Bx_GpUni.js +0 -2
- package/src/client/dist/pwa/assets/GitPanel-C9mz4GXF.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BwV89R-l.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-CwWavQjV.js +0 -2
- package/src/client/dist/pwa/assets/QBtn-CZaBclNw.js +0 -1
- package/src/client/dist/pwa/assets/QDrawer-DNEj8wLZ.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-BUlOQe5k.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-Bos94OyR.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-DtGsd0XH.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-Clc4Fl3B.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +0 -1
- package/src/client/dist/pwa/assets/build-path-tree-BpSY7N5O.js +0 -1
- package/src/client/dist/pwa/assets/de-BU0dILBs.js +0 -17
- package/src/client/dist/pwa/assets/documents-BIlWh3o-.js +0 -1
- package/src/client/dist/pwa/assets/editor.worker-CwQAd_xr.js +0 -30
- package/src/client/dist/pwa/assets/efforts-D_yTzymv.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-C33LTVkK.js +0 -1
- package/src/client/dist/pwa/assets/es-CNp0s9na.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-wRhAXT-1.js +0 -1
- package/src/client/dist/pwa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- package/src/client/dist/pwa/assets/fr-RqJyWSdm.js +0 -17
- package/src/client/dist/pwa/assets/i18n--PCet00a.js +0 -1
- package/src/client/dist/pwa/assets/index-CPfrY38K.js +0 -20
- package/src/client/dist/pwa/assets/index-CUoUy6tH.css +0 -1
- package/src/client/dist/pwa/assets/it-DSxNuCwL.js +0 -17
- package/src/client/dist/pwa/assets/monaco.contribution-D7NmJUJH.js +0 -2
- package/src/client/dist/pwa/assets/permissionModes-DC51Pz84.js +0 -36
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-DEJj9cFv.js +0 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-BQ_saiy5.js +0 -67
- package/src/client/dist/pwa/assets/use-is-mobile-CWjqe8gC.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-BKeYrJaQ.js +0 -2
package/dist/server/index.js
CHANGED
|
@@ -41,7 +41,9 @@ import { reconcileMessageRequests } from './services/mcp-message-request-service
|
|
|
41
41
|
import { authorizeWsUpgrade, generateToken, getLanHostnames, getLanUrls, isAllowedOrigin, isAllowedRequestHost, resolveBindHost, resolveDevClientOrigin, resolveNetworkAccessEnvOverrides, resolveProxyHostname, } from './services/network-access-service.js';
|
|
42
42
|
import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
|
|
43
43
|
import * as quotaBackoffService from './services/quota-backoff-service.js';
|
|
44
|
+
import { reconcileReviewReturns } from './services/review-return-service.js';
|
|
44
45
|
import { startSearchIndex, stopSearchIndex } from './services/search-service.js';
|
|
46
|
+
import { reconcileSessionHandoffs } from './services/session-handoff-service.js';
|
|
45
47
|
import { getGlobalSettings, updateNetworkAccessSettings } from './services/settings-service.js';
|
|
46
48
|
import { reloadDefaultTemplates } from './services/templates-service.js';
|
|
47
49
|
import { createTerminal, destroyAllTerminals, getTerminal } from './services/terminal-service.js';
|
|
@@ -116,15 +118,8 @@ const dailyBackupScheduler = startDailyDbBackupScheduler(async () => {
|
|
|
116
118
|
});
|
|
117
119
|
// Initialize process cleanup, agent watchdog, PR watcher, and wakeup rehydration
|
|
118
120
|
reconcileOrphanSessions();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
autoLoopService.rehydrate();
|
|
122
|
-
// Restore in-memory retry counts BEFORE re-arming the persisted backoff timers,
|
|
123
|
-
// otherwise the next arm() after restart would compute the next ladder rung
|
|
124
|
-
// from retryCount=0 and undo the progression.
|
|
125
|
-
restoreRetryCountsFromDb();
|
|
126
|
-
quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
|
|
127
|
-
cronService.restoreOnBoot();
|
|
121
|
+
reconcileReviewReturns();
|
|
122
|
+
reconcileSessionHandoffs();
|
|
128
123
|
// Deliver any new default prompt templates to existing installs (seed-once via the
|
|
129
124
|
// seededDefaultSlugs watermark; never overwrites or re-adds deleted defaults).
|
|
130
125
|
try {
|
|
@@ -229,6 +224,14 @@ const server = serve({
|
|
|
229
224
|
hostname: bindHost,
|
|
230
225
|
}, (info) => {
|
|
231
226
|
setBackendPort(info.port);
|
|
227
|
+
// Restored agents receive the actual listening endpoint, including an
|
|
228
|
+
// ephemeral or SERVER_PORT-selected port. Quota ownership comes first.
|
|
229
|
+
restoreRetryCountsFromDb();
|
|
230
|
+
quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
|
|
231
|
+
autoLoopService.rehydrate();
|
|
232
|
+
wakeupService.rehydrate();
|
|
233
|
+
cronService.restoreOnBoot();
|
|
234
|
+
startWatchdog();
|
|
232
235
|
startUpdateChecker();
|
|
233
236
|
stopStartupSpinner();
|
|
234
237
|
const settings = getGlobalSettings();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { listLoopMessages, resolveLoopMessage } from '../services/auto-loop-message-service.js';
|
|
3
|
+
import { deliverWorkspaceMessage } from '../services/workspace-message-service.js';
|
|
4
|
+
import { getWorkspace } from '../services/workspace-service.js';
|
|
5
|
+
import { assertWorkspaceLifecycleAvailable } from '../utils/workspace-lifecycle-guard.js';
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
app.get('/:id/auto-loop/messages', (c) => {
|
|
8
|
+
try {
|
|
9
|
+
const id = c.req.param('id');
|
|
10
|
+
if (!getWorkspace(id))
|
|
11
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
12
|
+
return c.json(listLoopMessages(id));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
app.post('/:id/auto-loop/messages', async (c) => {
|
|
19
|
+
try {
|
|
20
|
+
const body = await c.req.json();
|
|
21
|
+
if (typeof body.content !== 'string' || typeof body.clientMessageId !== 'string')
|
|
22
|
+
return c.json({ error: 'content and clientMessageId are required' }, 400);
|
|
23
|
+
await deliverWorkspaceMessage(c.req.param('id'), {
|
|
24
|
+
content: body.content,
|
|
25
|
+
clientMessageId: body.clientMessageId,
|
|
26
|
+
delivery: 'next_iteration',
|
|
27
|
+
});
|
|
28
|
+
return c.json({ accepted: true });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
app.patch('/:id/auto-loop/messages/:messageId', async (c) => {
|
|
35
|
+
try {
|
|
36
|
+
const id = c.req.param('id');
|
|
37
|
+
assertWorkspaceLifecycleAvailable(id);
|
|
38
|
+
const workspace = getWorkspace(id);
|
|
39
|
+
if (!workspace || workspace.archivedAt || workspace.worktreePurgedAt)
|
|
40
|
+
return c.json({ error: 'Workspace unavailable' }, 409);
|
|
41
|
+
const { action } = await c.req.json();
|
|
42
|
+
if (!['cancel', 'acknowledge', 'retry'].includes(action))
|
|
43
|
+
return c.json({ error: 'Invalid resolution' }, 400);
|
|
44
|
+
resolveLoopMessage(id, Number(c.req.param('messageId')), action);
|
|
45
|
+
return c.json({ ok: true });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
export default app;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
|
+
import { DOCUMENT_DIRS } from '../../shared/document-roots.js';
|
|
4
5
|
import * as workspaceService from '../services/workspace-service.js';
|
|
5
6
|
import { isPathInside, resolveExistingPathInside } from '../utils/safe-path.js';
|
|
6
7
|
/** Hono sub-router for workspace document browsing (read-only). */
|
|
@@ -13,7 +14,6 @@ const app = new Hono();
|
|
|
13
14
|
* Kept intentionally narrow to avoid leaking unrelated project docs
|
|
14
15
|
* (README, product specs, …) into the panel.
|
|
15
16
|
*/
|
|
16
|
-
const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts'];
|
|
17
17
|
/** Only .md files are listed. */
|
|
18
18
|
const MD_EXT = '.md';
|
|
19
19
|
/** Depth cap to keep recursion bounded even on pathological symlink loops. */
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
3
4
|
import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
|
|
5
|
+
import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
|
|
4
6
|
import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
5
7
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
8
|
+
import { workspaceLifecycleReason } from '../utils/workspace-lifecycle-guard.js';
|
|
9
|
+
import autoLoopMessagesRoutes from './auto-loop-messages.js';
|
|
6
10
|
const execFileAsync = promisify(execFileCb);
|
|
7
11
|
import fs from 'node:fs';
|
|
8
12
|
import path from 'node:path';
|
|
@@ -34,11 +38,12 @@ import * as notionService from '../services/notion-service.js';
|
|
|
34
38
|
import { renderPrTemplate } from '../services/pr-template-service.js';
|
|
35
39
|
import { clearPrSnapshotCache, getAllGitStats, getAllPrSnapshots, refreshPrSnapshot, } from '../services/pr-watcher-service.js';
|
|
36
40
|
import * as quotaBackoffService from '../services/quota-backoff-service.js';
|
|
37
|
-
import {
|
|
41
|
+
import { ReviewRequestError, startWorkspaceReview } from '../services/review-service.js';
|
|
38
42
|
import * as sentryService from '../services/sentry-service.js';
|
|
39
43
|
import * as settingsService from '../services/settings-service.js';
|
|
40
44
|
import { runSetupScript } from '../services/setup-script-service.js';
|
|
41
45
|
import { getSuitePrompts } from '../services/skill-suite-prompts.js';
|
|
46
|
+
import { TaskValidationError } from '../services/task-mutations.js';
|
|
42
47
|
import * as terminalService from '../services/terminal-service.js';
|
|
43
48
|
import * as wakeupService from '../services/wakeup-service.js';
|
|
44
49
|
import * as wsService from '../services/websocket-service.js';
|
|
@@ -62,6 +67,28 @@ function workspaceErrorStatus(err) {
|
|
|
62
67
|
}
|
|
63
68
|
/** Hono sub-router for workspace CRUD, tasks, agents, Git and PR creation. */
|
|
64
69
|
const app = new Hono();
|
|
70
|
+
// Transfers reserve the writer across multiple HTTP requests and engine invocations.
|
|
71
|
+
app.use('/:id/*', async (c, next) => {
|
|
72
|
+
const id = c.req.param('id');
|
|
73
|
+
const route = c.req.path.split(`/api/workspaces/${id}/`)[1] ?? '';
|
|
74
|
+
if (!['GET', 'HEAD', 'OPTIONS'].includes(c.req.method) &&
|
|
75
|
+
workspaceLifecycleReason(id) === 'session-handoff' &&
|
|
76
|
+
!route.startsWith('session-handoffs') &&
|
|
77
|
+
![
|
|
78
|
+
'stop',
|
|
79
|
+
'mark-read',
|
|
80
|
+
'chat-history',
|
|
81
|
+
'deferred-tool-use/answer',
|
|
82
|
+
'deferred-tool-use/cancel',
|
|
83
|
+
'deferred-permission/decision',
|
|
84
|
+
].includes(route)) {
|
|
85
|
+
return c.json({
|
|
86
|
+
error: 'A session transfer is pending. Finish or cancel it before changing this workspace.',
|
|
87
|
+
code: 'workspace-busy',
|
|
88
|
+
}, 409);
|
|
89
|
+
}
|
|
90
|
+
await next();
|
|
91
|
+
});
|
|
65
92
|
// These handlers own teardown across awaits; purge/delete/restore own their guard in the service.
|
|
66
93
|
for (const route of ['/:id/archive', '/:id/run-setup-script', '/:id/cancel-source-change', '/:id/rollback-file']) {
|
|
67
94
|
app.use(route, async (c, next) => {
|
|
@@ -295,6 +322,44 @@ app.get('/', (c) => {
|
|
|
295
322
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
296
323
|
}
|
|
297
324
|
});
|
|
325
|
+
function handoffError(c, error) {
|
|
326
|
+
return c.json({ error: error instanceof Error ? error.message : 'Session transfer failed' }, error instanceof SessionHandoffError
|
|
327
|
+
? error.status
|
|
328
|
+
: error instanceof SyntaxError
|
|
329
|
+
? 400
|
|
330
|
+
: workspaceErrorStatus(error));
|
|
331
|
+
}
|
|
332
|
+
app.post('/:id/session-handoffs', migrationGuard, async (c) => {
|
|
333
|
+
try {
|
|
334
|
+
return c.json({ handoff: createSessionHandoff(c.req.param('id'), await c.req.json()) }, 202);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return handoffError(c, error);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
app.get('/:id/session-handoffs/current', (c) => {
|
|
341
|
+
if (!workspaceService.getWorkspace(c.req.param('id')))
|
|
342
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
343
|
+
return c.json({ handoff: getCurrentSessionHandoff(c.req.param('id')) });
|
|
344
|
+
});
|
|
345
|
+
app.post('/:id/session-handoffs/:handoffId/decision', migrationGuard, async (c) => {
|
|
346
|
+
try {
|
|
347
|
+
const body = await c.req.json();
|
|
348
|
+
return c.json({ handoff: await decideSessionHandoff(c.req.param('id'), c.req.param('handoffId'), body.action) });
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return handoffError(c, error);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
app.post('/:id/session-handoffs/:handoffId/report', migrationGuard, async (c) => {
|
|
355
|
+
try {
|
|
356
|
+
const body = await c.req.json();
|
|
357
|
+
return c.json(submitSessionHandoff(c.req.param('id'), c.req.param('handoffId'), body.token, body.report));
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
return handoffError(c, error);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
298
363
|
// POST /api/workspaces/:id/engine-handoff-preview — build an editable, deterministic handoff.
|
|
299
364
|
app.post('/:id/engine-handoff-preview', migrationGuard, async (c) => {
|
|
300
365
|
try {
|
|
@@ -346,15 +411,14 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
|
|
|
346
411
|
error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
347
412
|
}, 409);
|
|
348
413
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return c.json({ workspace: workspaceService.getWorkspace(id), sessionId: session.agentSessionId });
|
|
414
|
+
const operation = createSessionHandoff(id, {
|
|
415
|
+
requestId: nanoid(),
|
|
416
|
+
sourceSessionId: workspaceService.getActiveSession(id)?.id ?? null,
|
|
417
|
+
generateSummary: false,
|
|
418
|
+
target: { engine: body.engine, model, reasoningEffort: effort, agentPermissionMode: body.agentPermissionMode },
|
|
419
|
+
}, handoff);
|
|
420
|
+
const completed = await waitForSessionHandoff(operation.id);
|
|
421
|
+
return c.json({ workspace: workspaceService.getWorkspace(id), sessionId: completed.targetSessionId });
|
|
358
422
|
}
|
|
359
423
|
catch (err) {
|
|
360
424
|
return c.json({ error: err instanceof Error ? err.message : 'Failed to switch engine' }, 500);
|
|
@@ -1601,9 +1665,7 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1601
1665
|
const out = {};
|
|
1602
1666
|
for (const r of rows) {
|
|
1603
1667
|
out[r.id] = {
|
|
1604
|
-
|
|
1605
|
-
auto_loop_ready: r.auto_loop_ready === 1,
|
|
1606
|
-
no_progress_streak: r.no_progress_streak,
|
|
1668
|
+
...autoLoopService.getStatus(r.id),
|
|
1607
1669
|
tasks_done: r.tasks_done ?? 0,
|
|
1608
1670
|
tasks_total: r.tasks_total ?? 0,
|
|
1609
1671
|
crons_count: r.crons_count ?? 0,
|
|
@@ -1616,6 +1678,7 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1616
1678
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
1617
1679
|
}
|
|
1618
1680
|
});
|
|
1681
|
+
app.route('/', autoLoopMessagesRoutes);
|
|
1619
1682
|
// GET /api/workspaces/:id/auto-loop — current auto-loop status for one workspace.
|
|
1620
1683
|
app.get('/:id/auto-loop', (c) => {
|
|
1621
1684
|
try {
|
|
@@ -2011,67 +2074,68 @@ app.delete('/:id/events/:eventId', (c) => {
|
|
|
2011
2074
|
app.post('/:id/tasks', async (c) => {
|
|
2012
2075
|
try {
|
|
2013
2076
|
const id = c.req.param('id');
|
|
2014
|
-
|
|
2015
|
-
if (!workspace) {
|
|
2077
|
+
if (!workspaceService.getWorkspace(id))
|
|
2016
2078
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
.json()
|
|
2020
|
-
.catch(() => ({}));
|
|
2021
|
-
if (!body.title?.trim()) {
|
|
2079
|
+
const body = await c.req.json().catch(() => null);
|
|
2080
|
+
if (!body || typeof body.title !== 'string' || !body.title.trim()) {
|
|
2022
2081
|
return c.json({ error: 'Title is required' }, 400);
|
|
2023
2082
|
}
|
|
2024
|
-
const existing = workspaceService.listTasks(id);
|
|
2025
|
-
const nextSortOrder = existing.length > 0 ? Math.max(...existing.map((t) => t.sortOrder)) + 1 : 0;
|
|
2026
2083
|
const task = workspaceService.createTask(id, {
|
|
2027
2084
|
title: body.title.trim(),
|
|
2028
|
-
isAcceptanceCriterion:
|
|
2029
|
-
sortOrder:
|
|
2085
|
+
isAcceptanceCriterion: body.isAcceptanceCriterion,
|
|
2086
|
+
sortOrder: body.sortOrder,
|
|
2087
|
+
afterTaskId: body.afterTaskId,
|
|
2088
|
+
role: body.role,
|
|
2030
2089
|
});
|
|
2031
2090
|
return c.json(task, 201);
|
|
2032
2091
|
}
|
|
2033
2092
|
catch (err) {
|
|
2034
2093
|
const message = err instanceof Error ? err.message : String(err);
|
|
2035
|
-
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
2094
|
+
return c.json({ error: message }, err instanceof TaskValidationError ? 400 : workspaceErrorStatus(err));
|
|
2036
2095
|
}
|
|
2037
2096
|
});
|
|
2038
|
-
// PATCH /api/workspaces/:id/tasks/:taskId —
|
|
2097
|
+
// PATCH /api/workspaces/:id/tasks/:taskId — validate and apply every field atomically
|
|
2039
2098
|
app.patch('/:id/tasks/:taskId', async (c) => {
|
|
2040
2099
|
try {
|
|
2041
2100
|
const id = c.req.param('id');
|
|
2042
2101
|
const taskId = c.req.param('taskId');
|
|
2043
|
-
|
|
2044
|
-
if (!workspace) {
|
|
2102
|
+
if (!workspaceService.getWorkspace(id))
|
|
2045
2103
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
2046
|
-
|
|
2047
|
-
const task = workspaceService.getTask(taskId, id);
|
|
2048
|
-
if (!task) {
|
|
2104
|
+
if (!workspaceService.getTask(taskId, id)) {
|
|
2049
2105
|
return c.json({ error: `Task '${taskId}' not found in workspace '${id}'` }, 404);
|
|
2050
2106
|
}
|
|
2051
|
-
const body = await c.req
|
|
2052
|
-
|
|
2053
|
-
.
|
|
2054
|
-
if (body.status === undefined && body.title === undefined) {
|
|
2055
|
-
return c.json({ error: 'At least one of status or title is required' }, 400);
|
|
2056
|
-
}
|
|
2057
|
-
if (body.title !== undefined) {
|
|
2058
|
-
if (!body.title.trim()) {
|
|
2059
|
-
return c.json({ error: 'Title cannot be empty' }, 400);
|
|
2060
|
-
}
|
|
2061
|
-
workspaceService.updateTaskTitle(taskId, body.title.trim());
|
|
2062
|
-
}
|
|
2063
|
-
if (body.status !== undefined) {
|
|
2064
|
-
const validStatuses = ['pending', 'in_progress', 'done'];
|
|
2065
|
-
if (!validStatuses.includes(body.status)) {
|
|
2066
|
-
return c.json({ error: `Invalid status. Must be one of: ${validStatuses.join(', ')}` }, 400);
|
|
2067
|
-
}
|
|
2068
|
-
workspaceService.updateTaskStatus(taskId, body.status);
|
|
2107
|
+
const body = await c.req.json().catch(() => null);
|
|
2108
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
2109
|
+
return c.json({ error: 'Request body must be a JSON object' }, 400);
|
|
2069
2110
|
}
|
|
2111
|
+
const fields = {};
|
|
2112
|
+
for (const key of [
|
|
2113
|
+
'title',
|
|
2114
|
+
'status',
|
|
2115
|
+
'isAcceptanceCriterion',
|
|
2116
|
+
'sortOrder',
|
|
2117
|
+
'afterTaskId',
|
|
2118
|
+
'verification',
|
|
2119
|
+
]) {
|
|
2120
|
+
if (body[key] !== undefined)
|
|
2121
|
+
Object.assign(fields, { [key]: body[key] });
|
|
2122
|
+
}
|
|
2123
|
+
if (Object.keys(fields).length === 0)
|
|
2124
|
+
return c.json({ error: 'At least one task field is required' }, 400);
|
|
2125
|
+
if (fields.title !== undefined && (typeof fields.title !== 'string' || !fields.title.trim())) {
|
|
2126
|
+
return c.json({ error: 'Title cannot be empty' }, 400);
|
|
2127
|
+
}
|
|
2128
|
+
if (fields.status !== undefined && !['pending', 'in_progress', 'done'].includes(fields.status)) {
|
|
2129
|
+
return c.json({ error: 'Invalid status. Must be one of: pending, in_progress, done' }, 400);
|
|
2130
|
+
}
|
|
2131
|
+
if (fields.title !== undefined)
|
|
2132
|
+
fields.title = fields.title.trim();
|
|
2133
|
+
workspaceService.updateTask(taskId, fields);
|
|
2070
2134
|
return c.json({ ok: true });
|
|
2071
2135
|
}
|
|
2072
2136
|
catch (err) {
|
|
2073
2137
|
const message = err instanceof Error ? err.message : String(err);
|
|
2074
|
-
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
2138
|
+
return c.json({ error: message }, err instanceof TaskValidationError ? 400 : workspaceErrorStatus(err));
|
|
2075
2139
|
}
|
|
2076
2140
|
});
|
|
2077
2141
|
// DELETE /api/workspaces/:id/tasks/:taskId — delete a task
|
|
@@ -2627,6 +2691,8 @@ app.put('/:id/tags', async (c) => {
|
|
|
2627
2691
|
});
|
|
2628
2692
|
// PATCH /api/workspaces/:id — update workspace fields (status, model, agentPermissionMode, name)
|
|
2629
2693
|
app.patch('/:id', migrationGuard, async (c) => {
|
|
2694
|
+
if (workspaceLifecycleReason(c.req.param('id')) === 'session-handoff')
|
|
2695
|
+
return c.json({ error: 'A session transfer is pending', code: 'workspace-busy' }, 409);
|
|
2630
2696
|
try {
|
|
2631
2697
|
const id = c.req.param('id');
|
|
2632
2698
|
const parsed = await c.req.json();
|
|
@@ -4302,104 +4368,12 @@ app.post('/:id/open-pr', async (c) => {
|
|
|
4302
4368
|
}
|
|
4303
4369
|
});
|
|
4304
4370
|
// POST /api/workspaces/:id/start-review — ask the agent to review committed + uncommitted changes
|
|
4305
|
-
app.post('/:id/start-review', async (c) => {
|
|
4371
|
+
app.post('/:id/start-review', migrationGuard, async (c) => {
|
|
4306
4372
|
try {
|
|
4307
|
-
|
|
4308
|
-
const workspace = workspaceService.getWorkspace(id);
|
|
4309
|
-
if (!workspace) {
|
|
4310
|
-
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
4311
|
-
}
|
|
4312
|
-
const body = await c.req
|
|
4313
|
-
.json()
|
|
4314
|
-
.catch(() => ({}));
|
|
4315
|
-
const additionalInstructions = (body.additionalInstructions ?? '').trim();
|
|
4316
|
-
const newSession = body.newSession === true;
|
|
4317
|
-
const worktreePath = workspace.worktreePath;
|
|
4318
|
-
// Best-effort fetch so the base ref is fresh
|
|
4319
|
-
try {
|
|
4320
|
-
await execFileAsync('git', ['fetch', 'origin', workspace.sourceBranch], { cwd: worktreePath });
|
|
4321
|
-
}
|
|
4322
|
-
catch (err) {
|
|
4323
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4324
|
-
console.warn(`[start-review] git fetch origin ${workspace.sourceBranch} failed: ${msg}`);
|
|
4325
|
-
}
|
|
4326
|
-
// Resolve base commit
|
|
4327
|
-
let baseCommit;
|
|
4328
|
-
try {
|
|
4329
|
-
const { stdout } = await execFileAsync('git', ['rev-parse', `origin/${workspace.sourceBranch}`], {
|
|
4330
|
-
cwd: worktreePath,
|
|
4331
|
-
});
|
|
4332
|
-
baseCommit = stdout.trim();
|
|
4333
|
-
}
|
|
4334
|
-
catch (err) {
|
|
4335
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4336
|
-
return c.json({ error: `Cannot resolve base commit for branch ${workspace.sourceBranch}: ${msg}` }, 500);
|
|
4337
|
-
}
|
|
4338
|
-
// Build context
|
|
4339
|
-
const commits = gitOps.getCommitsBetween(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
4340
|
-
const committedStats = gitOps.getDiffStatsBetween(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
4341
|
-
const workingTreeStats = gitOps.getWorkingTreeDiffStats(worktreePath);
|
|
4342
|
-
const diffStats = workingTreeStats.trim().length > 0
|
|
4343
|
-
? `${committedStats}\n\n— Working tree (uncommitted) —\n${workingTreeStats}`
|
|
4344
|
-
: committedStats;
|
|
4345
|
-
const template = getActiveReviewTemplate();
|
|
4346
|
-
const rendered = renderReviewTemplate(template, {
|
|
4347
|
-
workspace,
|
|
4348
|
-
commits,
|
|
4349
|
-
diffStats,
|
|
4350
|
-
baseCommit,
|
|
4351
|
-
additionalInstructions,
|
|
4352
|
-
});
|
|
4353
|
-
let messageSent = false;
|
|
4354
|
-
let emitSessionId;
|
|
4355
|
-
if (workspaceService.getWorkspace(workspace.id)?.status === 'compacting') {
|
|
4356
|
-
return c.json({
|
|
4357
|
-
code: 'compacting',
|
|
4358
|
-
error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
4359
|
-
}, 409);
|
|
4360
|
-
}
|
|
4361
|
-
if (newSession) {
|
|
4362
|
-
// Stop current agent, wait for it, then start fresh.
|
|
4363
|
-
try {
|
|
4364
|
-
assertAgentStopped(await agentManager.stopAgentAndWait(workspace.id, undefined, 'replacement'));
|
|
4365
|
-
}
|
|
4366
|
-
catch (err) {
|
|
4367
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4368
|
-
console.error(`[start-review] stopAgentAndWait failed (continuing): ${msg}`);
|
|
4369
|
-
throw err;
|
|
4370
|
-
}
|
|
4371
|
-
try {
|
|
4372
|
-
const agent = agentManager.startAgent(workspace.id, worktreePath, rendered, workspace.model, false /* resume */, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
|
|
4373
|
-
workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
|
|
4374
|
-
emitSessionId = agent?.agentSessionId;
|
|
4375
|
-
messageSent = true;
|
|
4376
|
-
}
|
|
4377
|
-
catch (err) {
|
|
4378
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4379
|
-
return c.json({ error: `Failed to start review session: ${msg}` }, 500);
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
else {
|
|
4383
|
-
const session = workspaceService.getActiveSession(workspace.id);
|
|
4384
|
-
emitSessionId = session?.id;
|
|
4385
|
-
try {
|
|
4386
|
-
const delivery = await deliverAgentPrompt(workspace, worktreePath, rendered);
|
|
4387
|
-
emitSessionId = delivery.agentSessionId;
|
|
4388
|
-
messageSent = true;
|
|
4389
|
-
}
|
|
4390
|
-
catch (deliveryErr) {
|
|
4391
|
-
const message = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
|
|
4392
|
-
return c.json({ error: `Failed to dispatch review prompt: ${message}` }, 500);
|
|
4393
|
-
}
|
|
4394
|
-
}
|
|
4395
|
-
// Emit AFTER dispatch so the user:message lands in the correct session id —
|
|
4396
|
-
// for newSession=true that's the freshly created session, not the previous one.
|
|
4397
|
-
wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, emitSessionId);
|
|
4398
|
-
return c.json({ ok: true, messageSent, newSession });
|
|
4373
|
+
return c.json(await startWorkspaceReview(c.req.param('id'), await c.req.json().catch(() => ({}))));
|
|
4399
4374
|
}
|
|
4400
4375
|
catch (err) {
|
|
4401
|
-
|
|
4402
|
-
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
4376
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, err instanceof ReviewRequestError ? err.status : workspaceErrorStatus(err));
|
|
4403
4377
|
}
|
|
4404
4378
|
});
|
|
4405
4379
|
// POST /api/workspaces/:id/start-ci-fix — dispatch the configured CI-fix
|
|
@@ -240,11 +240,25 @@ export function createClaudeCodeEngine() {
|
|
|
240
240
|
sdkOptions.pathToClaudeCodeExecutable = explicitBinary;
|
|
241
241
|
const inputStream = new ClaudeInputStream(effectivePrompt);
|
|
242
242
|
const q = query({ prompt: inputStream, options: sdkOptions });
|
|
243
|
+
let resolveReady;
|
|
244
|
+
let rejectReady;
|
|
245
|
+
const ready = new Promise((resolve, reject) => {
|
|
246
|
+
resolveReady = resolve;
|
|
247
|
+
rejectReady = reject;
|
|
248
|
+
});
|
|
249
|
+
// Legacy callers do not await readiness; still observe an early rejection.
|
|
250
|
+
void ready.catch(() => { });
|
|
243
251
|
let discoveredSessionId;
|
|
244
252
|
// A throwing onEvent handler (e.g. DB query against a closed connection
|
|
245
253
|
// during async test teardown) must not escape as an unhandled rejection.
|
|
246
254
|
const emitDirect = (ev) => {
|
|
247
255
|
try {
|
|
256
|
+
if (ev.kind === 'session:started')
|
|
257
|
+
resolveReady();
|
|
258
|
+
if (ev.kind === 'error')
|
|
259
|
+
rejectReady(new Error(ev.message));
|
|
260
|
+
if (ev.kind === 'session:ended')
|
|
261
|
+
rejectReady(new Error('Claude ended before confirming session initialization'));
|
|
248
262
|
onEvent(ev);
|
|
249
263
|
}
|
|
250
264
|
catch (err) {
|
|
@@ -588,6 +602,8 @@ export function createClaudeCodeEngine() {
|
|
|
588
602
|
}
|
|
589
603
|
})();
|
|
590
604
|
const engineProcess = {
|
|
605
|
+
ready,
|
|
606
|
+
closed: iteratorPromise.then(() => { }),
|
|
591
607
|
get pid() {
|
|
592
608
|
return undefined;
|
|
593
609
|
},
|