@loicngr/kobo 1.11.44 → 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 +40 -2
- package/CHANGELOG.md +5 -0
- package/README.md +13 -1
- package/dist/mcp-server/kobo-tasks-handlers.js +2 -1
- package/dist/mcp-server/kobo-tasks-server.js +31 -5
- package/dist/server/db/migrations.js +14 -0
- package/dist/server/db/schema.js +4 -1
- package/dist/server/db/session-handoff-schema.js +24 -0
- package/dist/server/index.js +2 -0
- package/dist/server/routes/documents.js +1 -1
- package/dist/server/routes/workspaces.js +73 -9
- package/dist/server/services/agent/engines/claude-code/engine.js +15 -0
- package/dist/server/services/agent/engines/codex/engine.js +1 -0
- package/dist/server/services/agent/orchestrator.js +63 -10
- package/dist/server/services/engine-handoff-service.js +5 -5
- 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/wakeup-service.js +8 -0
- package/dist/server/services/workspace-service.js +5 -2
- package/dist/server/utils/workspace-lifecycle-guard.js +35 -20
- package/dist/shared/document-roots.js +2 -0
- package/dist/shared/session-handoff.js +4 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DSKCvpr2.js → ChangelogPage-CnXTnFhe.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-DDxcsxyC.js → ComparisonPanel-CKylN45q.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +2 -0
- package/src/client/dist/pwa/assets/{DashboardPage-Dg2s9nnv.js → DashboardPage-CZQLfoV1.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-BSzNScIr.js → DiffViewer-Csf3WmLc.js} +4 -4
- package/src/client/dist/pwa/assets/{DocumentsPanel-DfEoI_f7.js → DocumentsPanel-BeCttB-x.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-D85PwhKb.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-CaLCpejR.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/{QBtnDropdown-qJWGcZpC.js → QBtnDropdown-44Mj-y_1.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-DX3PhMqC.js → QDrawer-B4qHw8wt.js} +1 -1
- package/src/client/dist/pwa/assets/QExpansionItem-Bok1m8Wi.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-cDK5AOQ5.js → QTooltip-BDihn2aY.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-BO-J4aNc.js → SearchPage-BwZ2TX1G.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-BIEceNwH.js → SplitWorkspacePage-Bg6nKBcA.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DitOCad1.js → TerminalPanel-Cq-FR7A7.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-DvPAhZsa.js → TouchPan-BQ1cvzRC.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-CJYdH3Jf.js → TourReplayButton-Dz1yEz_z.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CY-9z9nw.js → WorkspaceAttentionLabels-BCVOP8vp.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-DAVo-E4p.css → WorkspacePage-BbUvR1Vf.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +4 -0
- package/src/client/dist/pwa/assets/{browser-BsB6cFv4.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-SCd3w8xV.js → documents-BZ8bwcjr.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-BUdM7I6y.js → editor.api-CK7--ckY.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-DTJQP9or.js → efforts-Bw0o2Bud.js} +2 -2
- 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-BjQxf4pe.js → format-LtTLLitJ.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-BejhmNTV.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-5d5GhyRp.js → freemarker2-DxezMaje.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-DaafEsDK.js → handlebars-DK_Bj6Rx.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CwHBDd2A.js → html-DFfsfStD.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +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-DSNIEEH5.js → javascript-DuNbcaL7.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Dma1UpA1.js → liquid-CM3JKyhv.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-D8rqEAws.js → mdx-BG3zYwNF.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-2E7O2zoi.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-DNmhJTZn.js → network-auth-BJiPbO5l.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-B6Hiwvwb.js → notify-theme-D4p3qPkp.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-CeiVIFZn.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/{project-color-BCp9SWoI.js → project-color-R_1je4vW.js} +1 -1
- package/src/client/dist/pwa/assets/{python-f-9NqRlI.js → python-COs6pHk2.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-DsCmoX0h.js → razor-Jl1VqMsk.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DK5BD9mU.js → render-chat-markdown-AaOo0Tgq.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-cEHg4L10.js → typescript-BU_TUuAM.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-BmTbosmB.js → use-tours-CVn874kD.js} +2 -2
- package/src/client/dist/pwa/assets/{workspace-templates-DUWDLMIX.js → workspace-templates-BCKR8TXr.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-Plt-Y7we.js → xml-gFFsobxE.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-ScEmaAeN.js → yaml-DaXaqaGD.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- 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 +4 -1
- package/src/mcp-server/kobo-tasks-server.ts +38 -5
- package/src/client/dist/pwa/assets/ActivityFeed-D_y4aikp.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-DKVEknbL.js +0 -2
- package/src/client/dist/pwa/assets/GitPanel-CuG8MlgF.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-CDxYIJ_s.js +0 -2
- package/src/client/dist/pwa/assets/MainLayout-DTLNunOi.css +0 -1
- package/src/client/dist/pwa/assets/QSelect-CY4TWW_j.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-LvHiZ6ZH.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Aw89fRSq.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-xH_OyA3r.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-CW-ER1Mt.js +0 -1
- package/src/client/dist/pwa/assets/de-Mu7biceO.js +0 -17
- package/src/client/dist/pwa/assets/engineFeatures-Bj6Ob41d.js +0 -1
- package/src/client/dist/pwa/assets/es-BY5IfNI2.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-SM8vvYyO.js +0 -1
- package/src/client/dist/pwa/assets/fr-BMXCzFCM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CgWW5vfb.js +0 -1
- package/src/client/dist/pwa/assets/index-Di9Sz5E8.js +0 -20
- package/src/client/dist/pwa/assets/it-ByPZQK_K.js +0 -17
- package/src/client/dist/pwa/assets/monaco.contribution-DqOgo72G.js +0 -2
- package/src/client/dist/pwa/assets/permissionModes-DWdtf81n.js +0 -36
|
@@ -3,7 +3,7 @@ import { nanoid } from 'nanoid';
|
|
|
3
3
|
import { getDb } from '../../db/index.js';
|
|
4
4
|
import { assertAgentStopped } from '../../utils/agent-stop-result.js';
|
|
5
5
|
import { ensureKoboHome, getCompiledMcpServerPath, getDbPath, getKoboHome, getMcpServerSourcePath, getSettingsPath, getSkillsPath, } from '../../utils/paths.js';
|
|
6
|
-
import { assertWorkspaceLifecycleAvailable } from '../../utils/workspace-lifecycle-guard.js';
|
|
6
|
+
import { assertWorkspaceLifecycleAvailable, workspaceLifecycleReason } from '../../utils/workspace-lifecycle-guard.js';
|
|
7
7
|
import { listLoopMessages, settleLoopMessages } from '../auto-loop-message-service.js';
|
|
8
8
|
import * as autoLoopService from '../auto-loop-service.js';
|
|
9
9
|
import * as cleanupScriptService from '../cleanup-script-service.js';
|
|
@@ -12,6 +12,8 @@ import { resolveForge } from '../forge/resolve.js';
|
|
|
12
12
|
import * as lifecycleHookService from '../lifecycle-hook-service.js';
|
|
13
13
|
import * as quotaBackoffService from '../quota-backoff-service.js';
|
|
14
14
|
import { buildReviewReturnPrompt, getReviewReturn, restoreReviewConfiguration } from '../review-return-service.js';
|
|
15
|
+
import { activateSession, SESSION_RECENCY_ORDER } from '../session-activity-service.js';
|
|
16
|
+
import { requestHandoffStop, suspendHandoffTransfers } from '../session-handoff-runtime.js';
|
|
15
17
|
import { getEffectiveSettings, getGlobalSettings } from '../settings-service.js';
|
|
16
18
|
import { refreshNow } from '../usage/poller.js';
|
|
17
19
|
import * as wakeupService from '../wakeup-service.js';
|
|
@@ -474,7 +476,7 @@ function readEffectiveSettingsSafe(projectPath) {
|
|
|
474
476
|
};
|
|
475
477
|
}
|
|
476
478
|
}
|
|
477
|
-
function buildMcpServers(workspaceId) {
|
|
479
|
+
function buildMcpServers(workspaceId, extraEnv) {
|
|
478
480
|
const mcpServerCompiled = getCompiledMcpServerPath();
|
|
479
481
|
const mcpServerSource = getMcpServerSourcePath();
|
|
480
482
|
return [
|
|
@@ -484,6 +486,7 @@ function buildMcpServers(workspaceId) {
|
|
|
484
486
|
args: mcpServerCompiled ? [mcpServerCompiled] : ['tsx', mcpServerSource],
|
|
485
487
|
env: {
|
|
486
488
|
KOBO_WORKSPACE_ID: workspaceId,
|
|
489
|
+
...extraEnv,
|
|
487
490
|
KOBO_DB_PATH: getDbPath(),
|
|
488
491
|
KOBO_SETTINGS_PATH: getSettingsPath(),
|
|
489
492
|
KOBO_BACKEND_URL: `http://127.0.0.1:${backendPort}`,
|
|
@@ -540,7 +543,7 @@ function resolveSessionForResume(workspaceId, existingSessionId, model, engineId
|
|
|
540
543
|
}
|
|
541
544
|
else {
|
|
542
545
|
lastSession = db
|
|
543
|
-
.prepare(
|
|
546
|
+
.prepare(`SELECT id, engine_session_id, engine FROM agent_sessions WHERE workspace_id = ? AND COALESCE(engine, 'claude-code') = ? AND engine_session_id IS NOT NULL AND activation_order >= 0 ORDER BY ${SESSION_RECENCY_ORDER} LIMIT 1`)
|
|
544
547
|
.get(workspaceId, engineId);
|
|
545
548
|
}
|
|
546
549
|
// A workspace can now contain conversations from multiple engines. The
|
|
@@ -841,6 +844,13 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
841
844
|
return;
|
|
842
845
|
const routedEvent = ev.kind === 'session:ended' && (hasReplacement || sourceIsSuperseded) ? { ...ev, superseded: true } : ev;
|
|
843
846
|
routeEvent(workspaceId, agentSessionId, routedEvent);
|
|
847
|
+
const launch = sourceController ? launchOptions.get(sourceController) : undefined;
|
|
848
|
+
if (ev.kind === 'error')
|
|
849
|
+
launch?.onError?.(ev.message);
|
|
850
|
+
// Summary turns can read and report, but must not drive loop/quota/scheduler side effects.
|
|
851
|
+
if (launch?.handoffGeneration &&
|
|
852
|
+
!['session:started', 'session:ended', 'session:user-input-requested'].includes(ev.kind))
|
|
853
|
+
return;
|
|
844
854
|
if (sourceController && !sourceControllerIsStopping) {
|
|
845
855
|
if (ev.kind === 'session:compacting' && ev.active) {
|
|
846
856
|
const status = getWs(workspaceId)?.status;
|
|
@@ -958,9 +968,10 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
958
968
|
console.error('[orchestrator] Failed to transition to executing:', err);
|
|
959
969
|
}
|
|
960
970
|
}
|
|
971
|
+
const transferring = workspaceLifecycleReason(workspaceId) === 'session-handoff';
|
|
961
972
|
const returningReview = (ev.kind === 'error' || ev.kind === 'session:ended') && !!getReviewReturn(workspaceId, agentSessionId);
|
|
962
973
|
const quotaError = ev.kind === 'error' && (ev.category === 'quota' || /\b429\b/.test(ev.message));
|
|
963
|
-
if (quotaError && !sourceControllerIsStopping && !returningReview) {
|
|
974
|
+
if (quotaError && !sourceControllerIsStopping && !returningReview && !transferring) {
|
|
964
975
|
void handleQuota(workspaceId, agentSessionId);
|
|
965
976
|
}
|
|
966
977
|
if (ev.kind === 'error' &&
|
|
@@ -968,6 +979,7 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
968
979
|
ev.category !== 'resume_failed' &&
|
|
969
980
|
TRANSIENT_SERVER_ERROR_PATTERN.test(ev.message) &&
|
|
970
981
|
!returningReview &&
|
|
982
|
+
!transferring &&
|
|
971
983
|
getWs(workspaceId)?.autoLoop &&
|
|
972
984
|
!sourceControllerIsStopping) {
|
|
973
985
|
scheduleTransientAutoLoopRecovery(workspaceId, sourceController);
|
|
@@ -989,6 +1001,22 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
989
1001
|
// its persisted prompts too, otherwise a reconnect resurrects a panel
|
|
990
1002
|
// whose backend callback has already been discarded.
|
|
991
1003
|
purgeAllPersistedUserInputRequests(workspaceId, agentSessionId);
|
|
1004
|
+
if (transferring) {
|
|
1005
|
+
onSessionEnded(workspaceId, agentSessionId, sourceController, ev.exitCode, ev.reason, isResumeFailed);
|
|
1006
|
+
launch?.onEnded?.(ev);
|
|
1007
|
+
if (workspaceLifecycleReason(workspaceId) === 'session-handoff') {
|
|
1008
|
+
if (!launch?.handoffGeneration) {
|
|
1009
|
+
settleLoopMessages(workspaceId, agentSessionId, ev.reason === 'completed');
|
|
1010
|
+
if (listLoopMessages(workspaceId).some((message) => message.state === 'unknown'))
|
|
1011
|
+
autoLoopService.block(workspaceId, 'message-delivery-unknown');
|
|
1012
|
+
}
|
|
1013
|
+
notifyCapacityAvailable(workspaceId);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
// A target can finish before its ready/start promise settles. Its callback
|
|
1017
|
+
// completed the transfer and released ownership: process this actual work
|
|
1018
|
+
// session through the ordinary terminal/auto-loop path below exactly once.
|
|
1019
|
+
}
|
|
992
1020
|
// A watchdog end means Kōbō forced the stream closed after the engine
|
|
993
1021
|
// failed to drain. It is neither a clean completion nor evidence of an
|
|
994
1022
|
// agent stall. Move auto-loop work onto the existing persisted, bounded
|
|
@@ -1190,6 +1218,11 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
1190
1218
|
preCompactionStatus.delete(sourceController);
|
|
1191
1219
|
if (isSuperseded)
|
|
1192
1220
|
return false;
|
|
1221
|
+
if (workspaceLifecycleReason(workspaceId) === 'session-handoff') {
|
|
1222
|
+
if (registeredController === sourceController && sourceController?.status !== 'stopping')
|
|
1223
|
+
controllers.delete(workspaceId);
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1193
1226
|
// A superseded end (above) is a session that was replaced, not one that
|
|
1194
1227
|
// ended; everything past this point is a real end the user may want to act
|
|
1195
1228
|
// on, whatever the reason — the script reads KOBO_SESSION_END_REASON.
|
|
@@ -1254,6 +1287,7 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
1254
1287
|
}
|
|
1255
1288
|
return true;
|
|
1256
1289
|
}
|
|
1290
|
+
const launchOptions = new WeakMap();
|
|
1257
1291
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
1258
1292
|
/**
|
|
1259
1293
|
* Spawn an agent (via the resolved engine) for a workspace. Returns
|
|
@@ -1261,10 +1295,10 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
1261
1295
|
* after `engine.start` resolves — callers should subscribe to WS events or
|
|
1262
1296
|
* query the controller via `_getControllers()` for tests.
|
|
1263
1297
|
*/
|
|
1264
|
-
export function startAgent(workspaceId, workingDir, prompt, model, resume = false, agentPermissionMode, existingSessionId, reasoningEffort, beforeDispatch) {
|
|
1298
|
+
export function startAgent(workspaceId, workingDir, prompt, model, resume = false, agentPermissionMode, existingSessionId, reasoningEffort, beforeDispatch, launch) {
|
|
1265
1299
|
if (shuttingDown)
|
|
1266
1300
|
throw new Error('Cannot start an agent while the server is shutting down');
|
|
1267
|
-
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
1301
|
+
assertWorkspaceLifecycleAvailable(workspaceId, launch?.lifecycleOwner);
|
|
1268
1302
|
const workspace = getWs(workspaceId);
|
|
1269
1303
|
if (!workspace)
|
|
1270
1304
|
throw new Error(`Workspace '${workspaceId}' not found`);
|
|
@@ -1334,6 +1368,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1334
1368
|
}
|
|
1335
1369
|
// Persist before the asynchronous engine can emit its first event. This
|
|
1336
1370
|
// survives a Kōbō restart and does not depend on SDK system:init delivery.
|
|
1371
|
+
activateSession(workspaceId, agentSessionId);
|
|
1337
1372
|
captureTaskProgressBaseline(workspaceId, agentSessionId);
|
|
1338
1373
|
const settings = ws ? readEffectiveSettingsSafe(ws.projectPath) : readEffectiveSettingsSafe(workingDir);
|
|
1339
1374
|
const options = {
|
|
@@ -1355,12 +1390,14 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1355
1390
|
}
|
|
1356
1391
|
})(),
|
|
1357
1392
|
settings,
|
|
1358
|
-
mcpServers: buildMcpServers(workspaceId),
|
|
1393
|
+
mcpServers: buildMcpServers(workspaceId, launch?.mcpEnv),
|
|
1359
1394
|
env: ws ? buildAgentEnv(ws.projectPath) : undefined,
|
|
1360
1395
|
};
|
|
1361
1396
|
beforeDispatch?.();
|
|
1362
1397
|
let controller;
|
|
1363
1398
|
controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, controller, ev));
|
|
1399
|
+
if (launch)
|
|
1400
|
+
launchOptions.set(controller, launch);
|
|
1364
1401
|
registerSessionLifecycleOwner(workspaceId, agentSessionId, controller);
|
|
1365
1402
|
if (!existingCtrl)
|
|
1366
1403
|
controllers.set(workspaceId, controller);
|
|
@@ -1377,7 +1414,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1377
1414
|
return;
|
|
1378
1415
|
if (pendingStarts.get(workspaceId) === controller)
|
|
1379
1416
|
pendingStarts.delete(workspaceId);
|
|
1380
|
-
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
1417
|
+
assertWorkspaceLifecycleAvailable(workspaceId, launch?.lifecycleOwner);
|
|
1381
1418
|
const current = getWs(workspaceId);
|
|
1382
1419
|
if (!current || current.archivedAt || current.worktreePurgedAt)
|
|
1383
1420
|
throw new Error('Workspace is no longer available');
|
|
@@ -1387,7 +1424,11 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1387
1424
|
controllers.set(workspaceId, controller);
|
|
1388
1425
|
return controller.start(options);
|
|
1389
1426
|
})
|
|
1390
|
-
.then(() => {
|
|
1427
|
+
.then(async () => {
|
|
1428
|
+
if (launch?.onStarted)
|
|
1429
|
+
await controller.engineProcess?.ready;
|
|
1430
|
+
if (controller.status !== 'stopping')
|
|
1431
|
+
launch?.onStarted?.();
|
|
1391
1432
|
const pid = controller.pid;
|
|
1392
1433
|
if (pid !== undefined) {
|
|
1393
1434
|
try {
|
|
@@ -1524,6 +1565,9 @@ async function raceWithTimeout(promise, timeoutMs, onTimeout) {
|
|
|
1524
1565
|
export async function stopAgentAndWait(workspaceId, timeoutMs = STOP_AGENT_TIMEOUT_MS,
|
|
1525
1566
|
/** Only `user` disables auto-loop; technical stops preserve its persisted intent. */
|
|
1526
1567
|
cause = 'user') {
|
|
1568
|
+
const finishHandoffStop = ['user', 'delete', 'purge', 'archive'].includes(cause)
|
|
1569
|
+
? requestHandoffStop(workspaceId)
|
|
1570
|
+
: undefined;
|
|
1527
1571
|
const pending = pendingStarts.get(workspaceId);
|
|
1528
1572
|
let pendingStop = Promise.resolve();
|
|
1529
1573
|
if (pending) {
|
|
@@ -1544,7 +1588,8 @@ cause = 'user') {
|
|
|
1544
1588
|
// Shutdown only suspends timers: persisted intent belongs to the next boot.
|
|
1545
1589
|
const cancelSchedules = new Promise((resolve) => {
|
|
1546
1590
|
if (cause !== 'shutdown') {
|
|
1547
|
-
|
|
1591
|
+
if (cause !== 'handoff')
|
|
1592
|
+
wakeupService.cancel(workspaceId, 'stopped');
|
|
1548
1593
|
quotaBackoffService.cancel(workspaceId, 'user');
|
|
1549
1594
|
}
|
|
1550
1595
|
resolve();
|
|
@@ -1561,6 +1606,7 @@ cause = 'user') {
|
|
|
1561
1606
|
if (outcome === 'timeout') {
|
|
1562
1607
|
console.error(`[orchestrator] Agent '${workspaceId}' did not stop within ${timeoutMs}ms; retaining its controller`);
|
|
1563
1608
|
}
|
|
1609
|
+
finishHandoffStop?.(outcome);
|
|
1564
1610
|
return outcome;
|
|
1565
1611
|
}
|
|
1566
1612
|
/**
|
|
@@ -1584,6 +1630,7 @@ export async function stopAllAgents(timeoutMs = 3_000) {
|
|
|
1584
1630
|
wakeupService.suspendForShutdown();
|
|
1585
1631
|
cronService.suspendForShutdown();
|
|
1586
1632
|
shuttingDown = true;
|
|
1633
|
+
suspendHandoffTransfers();
|
|
1587
1634
|
if (capacityTimer)
|
|
1588
1635
|
clearTimeout(capacityTimer);
|
|
1589
1636
|
capacityTimer = undefined;
|
|
@@ -1603,6 +1650,8 @@ export function isShuttingDown() {
|
|
|
1603
1650
|
}
|
|
1604
1651
|
/** Deliver a scheduled check without stopping background work or cancelling the wakeup as user input. */
|
|
1605
1652
|
export function sendWakeupIfWaiting(workspaceId, content, expectedSessionId) {
|
|
1653
|
+
if (workspaceLifecycleReason(workspaceId) === 'session-handoff')
|
|
1654
|
+
return false;
|
|
1606
1655
|
const ctrl = controllers.get(workspaceId);
|
|
1607
1656
|
if (ctrl?.status !== 'running')
|
|
1608
1657
|
return false;
|
|
@@ -1620,6 +1669,7 @@ export function sendWakeupIfWaiting(workspaceId, content, expectedSessionId) {
|
|
|
1620
1669
|
}
|
|
1621
1670
|
/** Write a user message to the running agent. */
|
|
1622
1671
|
export async function sendMessage(workspaceId, content, expectedSessionId, beforeDispatch) {
|
|
1672
|
+
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
1623
1673
|
assertWorkspaceNotCompacting(workspaceId);
|
|
1624
1674
|
const ctrl = controllers.get(workspaceId);
|
|
1625
1675
|
if (!ctrl) {
|
|
@@ -1633,6 +1683,7 @@ export async function sendMessage(workspaceId, content, expectedSessionId, befor
|
|
|
1633
1683
|
await ctrl.sendMessage(content);
|
|
1634
1684
|
}
|
|
1635
1685
|
export async function sendMessageForFallback(workspaceId, content) {
|
|
1686
|
+
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
1636
1687
|
assertWorkspaceNotCompacting(workspaceId);
|
|
1637
1688
|
const capturedController = controllers.get(workspaceId);
|
|
1638
1689
|
wakeupService.cancel(workspaceId, 'user-message');
|
|
@@ -1643,6 +1694,7 @@ export async function sendMessageForFallback(workspaceId, content) {
|
|
|
1643
1694
|
return { status: 'sent', sessionId: capturedController.agentSessionId };
|
|
1644
1695
|
}
|
|
1645
1696
|
catch {
|
|
1697
|
+
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
1646
1698
|
assertWorkspaceNotCompacting(workspaceId);
|
|
1647
1699
|
const deadline = Date.now() + FALLBACK_CONTROLLER_TURNOVER_TIMEOUT_MS;
|
|
1648
1700
|
while (controllers.get(workspaceId) === capturedController) {
|
|
@@ -1657,6 +1709,7 @@ export async function sendMessageForFallback(workspaceId, content) {
|
|
|
1657
1709
|
const replacementController = controllers.get(workspaceId);
|
|
1658
1710
|
if (!replacementController)
|
|
1659
1711
|
return { status: 'stopped' };
|
|
1712
|
+
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
1660
1713
|
assertWorkspaceNotCompacting(workspaceId);
|
|
1661
1714
|
await replacementController.sendMessage(content);
|
|
1662
1715
|
return { status: 'sent', sessionId: replacementController.agentSessionId };
|
|
@@ -8,13 +8,13 @@ function runGit(worktreePath, args) {
|
|
|
8
8
|
return '';
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
function recentConversation(workspaceId) {
|
|
11
|
+
function recentConversation(workspaceId, sessionId) {
|
|
12
12
|
const rows = getDb()
|
|
13
13
|
.prepare(`SELECT type, payload FROM ws_events
|
|
14
|
-
WHERE workspace_id = ?
|
|
14
|
+
WHERE workspace_id = ? AND (? IS NULL OR session_id = ?)
|
|
15
15
|
AND (type = 'user:message' OR (type = 'agent:event' AND json_extract(payload, '$.kind') = 'message:text'))
|
|
16
16
|
ORDER BY rowid DESC LIMIT 6`)
|
|
17
|
-
.all(workspaceId);
|
|
17
|
+
.all(workspaceId, sessionId ?? null, sessionId ?? null);
|
|
18
18
|
return rows
|
|
19
19
|
.reverse()
|
|
20
20
|
.flatMap((row) => {
|
|
@@ -30,12 +30,12 @@ function recentConversation(workspaceId) {
|
|
|
30
30
|
.join('\n\n');
|
|
31
31
|
}
|
|
32
32
|
/** Build a deterministic, secret-free handoff for a fresh session on another engine. */
|
|
33
|
-
export function buildEngineHandoff(workspace, sourceEngine, targetEngine) {
|
|
33
|
+
export function buildEngineHandoff(workspace, sourceEngine, targetEngine, sourceSessionId) {
|
|
34
34
|
const taskLines = workspace.tasks.length
|
|
35
35
|
? workspace.tasks.map((task) => `- [${task.status === 'done' ? 'x' : ' '}] ${task.title}`).join('\n')
|
|
36
36
|
: '- No Kōbō task recorded.';
|
|
37
37
|
const changedFiles = runGit(workspace.worktreePath, 'status --short') || 'No uncommitted changes detected.';
|
|
38
38
|
const diffStat = runGit(workspace.worktreePath, 'diff --stat') || 'No uncommitted diff stat available.';
|
|
39
|
-
const history = recentConversation(workspace.id) || 'No recent conversation message available.';
|
|
39
|
+
const history = recentConversation(workspace.id, sourceSessionId) || 'No recent conversation message available.';
|
|
40
40
|
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
41
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getDb } from '../db/index.js';
|
|
2
|
+
// Preserve legacy last-use ordering until a session is started or restored.
|
|
3
|
+
export const SESSION_RECENCY_ORDER = 'activation_order DESC, MAX(started_at, COALESCE(ended_at, started_at)) DESC, started_at DESC';
|
|
4
|
+
/** Make a conversation current without changing its execution history or starting an agent. */
|
|
5
|
+
export function activateSession(workspaceId, sessionId) {
|
|
6
|
+
getDb()
|
|
7
|
+
.prepare(`UPDATE agent_sessions SET activation_order =
|
|
8
|
+
(SELECT COALESCE(MAX(activation_order), 0) + 1 FROM agent_sessions WHERE workspace_id = ? AND activation_order >= 0)
|
|
9
|
+
WHERE workspace_id = ? AND id = ? AND status != 'idle'`)
|
|
10
|
+
.run(workspaceId, workspaceId, sessionId);
|
|
11
|
+
}
|
|
12
|
+
/** Keep a rolled-back target in history without selecting or resuming it implicitly. */
|
|
13
|
+
export function deactivateSession(workspaceId, sessionId) {
|
|
14
|
+
getDb()
|
|
15
|
+
.prepare("UPDATE agent_sessions SET activation_order = -1 WHERE workspace_id = ? AND id = ? AND status != 'running'")
|
|
16
|
+
.run(workspaceId, sessionId);
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Decouples the orchestrator's explicit Stop path from the transfer service. */
|
|
2
|
+
const handlers = new Map();
|
|
3
|
+
export function registerHandoffStopHandler(workspaceId, handler) {
|
|
4
|
+
handlers.set(workspaceId, handler);
|
|
5
|
+
return () => {
|
|
6
|
+
if (handlers.get(workspaceId) === handler)
|
|
7
|
+
handlers.delete(workspaceId);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function requestHandoffStop(workspaceId) {
|
|
11
|
+
const handler = handlers.get(workspaceId);
|
|
12
|
+
handler?.requested();
|
|
13
|
+
if (!handler)
|
|
14
|
+
return undefined;
|
|
15
|
+
let settled = false;
|
|
16
|
+
return (outcome) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
handler.settled(outcome);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function suspendHandoffTransfers() {
|
|
24
|
+
for (const handler of handlers.values())
|
|
25
|
+
handler.shutdown();
|
|
26
|
+
}
|