@loicngr/kobo 2.1.13 → 2.1.16
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 +38 -6
- package/CHANGELOG.md +12 -0
- package/CONFIGURATION.md +41 -5
- package/README.md +4 -2
- package/dist/mcp-server/kobo-tasks-server.js +62 -105
- package/dist/mcp-server/workspace-dialogue-client.js +15 -3
- package/dist/server/db/auto-loop-final-review-schema.js +18 -0
- package/dist/server/db/migrations.js +12 -1
- package/dist/server/db/review-return-schema.js +18 -1
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/workspace-message-batch-schema.js +9 -0
- package/dist/server/index.js +8 -1
- package/dist/server/routes/mcp.js +69 -3
- package/dist/server/routes/workspace-messages.js +22 -0
- package/dist/server/routes/workspaces.js +119 -4
- package/dist/server/services/agent/engines/claude-code/engine.js +37 -1
- package/dist/server/services/agent/orchestrator.js +159 -40
- package/dist/server/services/agent/session-controller.js +4 -1
- package/dist/server/services/auto-loop-final-review-service.js +329 -0
- package/dist/server/services/auto-loop-service.js +53 -10
- package/dist/server/services/pr-checkout-rollback-service.js +51 -0
- package/dist/server/services/pr-checkout-service.js +21 -1
- package/dist/server/services/review-launch-runtime.js +17 -0
- package/dist/server/services/review-return-service.js +58 -10
- package/dist/server/services/review-service.js +59 -11
- package/dist/server/services/workspace-creation-mcp-service.js +198 -0
- package/dist/server/services/workspace-group-message-mcp-service.js +58 -0
- package/dist/server/services/workspace-group-message-service.js +182 -0
- package/dist/server/services/workspace-lifecycle-mcp-service.js +72 -0
- package/dist/server/services/workspace-message-service.js +5 -0
- package/dist/server/services/workspace-service.js +11 -0
- package/dist/server/services/workspace-template-service.js +10 -0
- package/dist/shared/auto-loop-review.js +1 -0
- package/dist/shared/pr-checkout-steps.js +86 -0
- package/dist/shared/review-return.js +1 -0
- package/dist/shared/workspace-creation-tools.js +182 -0
- package/dist/shared/workspace-group-message-tools.js +134 -0
- package/dist/shared/workspace-group-messages.js +27 -0
- package/dist/shared/workspace-lifecycle-tools.js +112 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-tJmJhmtl.js +2 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Bbrq02Xt.js +1 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +1 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DD90Nl6c.js → ChangelogPage-BJfkAVYp.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-D_BQFWiT.js → ComparisonPanel-BfP8APKY.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-BbtNfo0x.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +1 -0
- package/src/client/dist/pwa/assets/{DashboardPage-DyvTdmBg.js → DashboardPage-DZYsAh5y.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-4Q-GJgyA.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-bm639sIu.js → DocumentsPanel-BfPlfiwU.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-KfwSzjeY.js → DrawerToggleButton-DG1Qjqk1.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-ECnbb_rx.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-37IHOIW-.js → HealthPage-_VXG1Zeo.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-BH6grTuW.js +2 -0
- package/src/client/dist/pwa/assets/MainLayout-DnmUxNk7.css +1 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-CucZMhjy.js → MemoryPanel-CVlvGZXZ.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-CUQybzVR.js → MemoryProposalList-SJsnzUAM.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-x6qPyl_7.js → QBtnDropdown-DBgGsN-D.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-D33aoRoG.js → QExpansionItem-DKgaMJ_l.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-Di-D6t2u.js → QScrollArea-D3OCsq3g.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-Cb_s_nox.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-BhwG7lp2.js → QTooltip-X5ps8Opw.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-qFi9Wxzh.js → SchedulePanel-Ww_FVlW5.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-DA9nRmsA.js → SearchPage-B87WEHf3.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BAZLGUyg.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-Brw2XsYJ.js → SplitWorkspacePage-B99AJUbG.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DVP18-nk.js → TerminalPanel-DnA1MXwu.js} +9 -9
- package/src/client/dist/pwa/assets/{ToolCallItem-cSHh3p_F.js → ToolCallItem-2vFggaur.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-C6LZZ3rw.js → TouchPan-BSU3z0aO.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Czlqo18H.js → TourReplayButton-DQwS2xdD.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BDYGq0pC.js → WorkspaceAttentionLabels-CM1tGDje.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-B248nKIU.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-CQN6EKIn.js +4 -0
- package/src/client/dist/pwa/assets/{browser-ifCwUGpE.js → browser-DPsupI9Q.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BBkGq3lN.js → build-path-tree-CbIdt-p9.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-DR68eyF8.js +1 -0
- package/src/client/dist/pwa/assets/de-D3Ycvf_F.js +17 -0
- package/src/client/dist/pwa/assets/{editor.api-CV_seGJR.js → editor.api-DvgAtnOj.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-ttw0bL63.js → efforts-nmxfTVQD.js} +1 -1
- package/src/client/dist/pwa/assets/es-TgeS5898.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-C8biMRXo.js → expand-template-Brn5O4a1.js} +1 -1
- package/src/client/dist/pwa/assets/{format-Cu95dbxt.js → format-DfJ3Fv1i.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-C25D1RSd.js → formatters-B30bNFhq.js} +1 -1
- package/src/client/dist/pwa/assets/fr-CJafryNb.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DJEFr74q.js → freemarker2-DnZe74tc.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-C_lzsMjX.js → handlebars-CmuMfxzO.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CWmLFAmY.js → html-BD7OJFGX.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BY4JeBjk.js +1 -0
- package/src/client/dist/pwa/assets/index-C5fAw9ll.js +20 -0
- package/src/client/dist/pwa/assets/it-C5mN8Y61.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-tsuiEUe-.js → javascript-Y-OrgcmW.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-BBr9BvUk.js → liquid-BqOkKePH.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-gjBH1cXG.js → mdx-D-58x2h0.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CvGcBIsb.js → monaco-contributions-BW4ZMKO6.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-Uw-Lfb8Z.js → monaco.contribution-DJWqzNG-.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-BGPRhUa6.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-PMSxqP6t.js → notify-theme-DZDzfxph.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BDQa2TgU.js → project-color-gB5yeZPN.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DlIgD1n6.js → python-BkBj1wDC.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-AraFPlpM.js → razor-DOOJ_0b8.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-CB8UpXHM.js → typescript-Bheo43G5.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CSREHQM4.js → use-search-index-status-CBpnyV-R.js} +1 -1
- package/src/client/dist/pwa/assets/use-tours-BHNasCvg.js +2 -0
- package/src/client/dist/pwa/assets/{use-worktree-restore-BM3Giig1.js → use-worktree-restore-CgABsWno.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-BPMFvTnQ.js → workspace-templates-BnFkigYg.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-uRjL1P43.js → xml-vHt2BD26.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-C9CFR4qV.js → yaml-BvhLPzP9.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 +156 -12
- package/src/mcp-server/kobo-tasks-server.ts +73 -106
- package/src/mcp-server/workspace-dialogue-client.ts +18 -3
- package/src/client/dist/pwa/assets/ActivityFeed-BTRzc60o.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-BZAoCukb.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-CdCs8PRS.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-D0K2PlZD.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-C-Ejlc9_.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BQOOup9o.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-DXQ1ouYl.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-MdPXvwm4.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-DcV6apqE.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-BwUkijpS.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-Bwfv559o.css +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-CeSgjIY7.js +0 -1
- package/src/client/dist/pwa/assets/de-CHY2BAfL.js +0 -17
- package/src/client/dist/pwa/assets/es-DbcSKp38.js +0 -17
- package/src/client/dist/pwa/assets/fr-CyyJ8S2c.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CobVQiTM.js +0 -1
- package/src/client/dist/pwa/assets/index-ImUH7Bbt.js +0 -20
- package/src/client/dist/pwa/assets/it-XQJW2Xkr.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-D0Vm6a09.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-6yxNVmjH.js +0 -2
|
@@ -5,6 +5,7 @@ import { getDb } from '../../db/index.js';
|
|
|
5
5
|
import { assertAgentStopped } from '../../utils/agent-stop-result.js';
|
|
6
6
|
import { ensureKoboHome, getCompiledMcpServerPath, getDbPath, getKoboHome, getMcpServerSourcePath, getSettingsPath, getSkillsPath, } from '../../utils/paths.js';
|
|
7
7
|
import { assertWorkspaceLifecycleAvailable, workspaceLifecycleReason } from '../../utils/workspace-lifecycle-guard.js';
|
|
8
|
+
import { cancelAutoLoopFinalReview, getAutoLoopReviewLaunch, isAutoLoopReview, prepareAutoLoopReviewReturn, retryAutoLoopReviewReturn, } from '../auto-loop-final-review-service.js';
|
|
8
9
|
import { listLoopMessages, settleLoopMessages } from '../auto-loop-message-service.js';
|
|
9
10
|
import * as autoLoopService from '../auto-loop-service.js';
|
|
10
11
|
import * as cleanupScriptService from '../cleanup-script-service.js';
|
|
@@ -14,7 +15,8 @@ import * as lifecycleHookService from '../lifecycle-hook-service.js';
|
|
|
14
15
|
import { advanceMemoryConversationEpoch, allocateMemoryConversationKey, bindMemoryNativeConversation, createMemoryCapability, revokeMemoryCapability, } from '../memory-agent-runtime.js';
|
|
15
16
|
import { buildMemoryContext, markMemoryContextFailed, markMemoryContextInitialized, markMemoryContextSubmitted, } from '../memory-context-service.js';
|
|
16
17
|
import * as quotaBackoffService from '../quota-backoff-service.js';
|
|
17
|
-
import {
|
|
18
|
+
import { cancelReviewLaunch } from '../review-launch-runtime.js';
|
|
19
|
+
import { buildReviewReturnPrompt, claimReviewReturn, completeReviewReturn, getReviewReturn, listReviewReturns, restoreReviewConfiguration, setReviewReturnPhase, } from '../review-return-service.js';
|
|
18
20
|
import { activateSession, SESSION_RECENCY_ORDER } from '../session-activity-service.js';
|
|
19
21
|
import { requestHandoffStop, suspendHandoffTransfers } from '../session-handoff-runtime.js';
|
|
20
22
|
import { getEffectiveSettings, getGlobalSettings } from '../settings-service.js';
|
|
@@ -22,7 +24,7 @@ import { refreshNow } from '../usage/poller.js';
|
|
|
22
24
|
import * as wakeupService from '../wakeup-service.js';
|
|
23
25
|
import { emit, emitEphemeral } from '../websocket-service.js';
|
|
24
26
|
import * as permissionPolicyService from '../workspace-permission-policy-service.js';
|
|
25
|
-
import { getWorkspace as getWs, markWorkspaceUnread, updateWorkspaceStatus, } from '../workspace-service.js';
|
|
27
|
+
import { getWorkspace as getWs, markWorkspaceUnread, updateWorkspaceEngineConfiguration, updateWorkspaceStatus, } from '../workspace-service.js';
|
|
26
28
|
import { resolveEngine } from './engines/registry.js';
|
|
27
29
|
import { AGENT_NO_LONGER_RUNNING_TEXT, } from './engines/types.js';
|
|
28
30
|
import { routeEvent } from './event-router.js';
|
|
@@ -78,6 +80,7 @@ function notifyCapacityAvailable(stoppedWorkspaceId) {
|
|
|
78
80
|
if (shuttingDown)
|
|
79
81
|
return;
|
|
80
82
|
try {
|
|
83
|
+
resumePendingReviewReturns();
|
|
81
84
|
autoLoopService.resumeWaitingWorkspaces(excluded);
|
|
82
85
|
}
|
|
83
86
|
catch (err) {
|
|
@@ -556,9 +559,11 @@ function resolveSessionForResume(workspaceId, existingSessionId, model, engineId
|
|
|
556
559
|
}
|
|
557
560
|
else {
|
|
558
561
|
lastSession = db
|
|
559
|
-
.prepare(`SELECT id, engine_session_id, engine FROM agent_sessions WHERE workspace_id = ? AND COALESCE(engine, 'claude-code') = ? AND
|
|
562
|
+
.prepare(`SELECT id, engine_session_id, engine FROM agent_sessions WHERE workspace_id = ? AND COALESCE(engine, 'claude-code') = ? AND activation_order >= 0 ORDER BY ${SESSION_RECENCY_ORDER} LIMIT 1`)
|
|
560
563
|
.get(workspaceId, engineId);
|
|
561
564
|
}
|
|
565
|
+
// A failed latest conversation must start fresh, not silently fall back to
|
|
566
|
+
// an older, unrelated conversation whose native ID is still valid.
|
|
562
567
|
// A workspace can now contain conversations from multiple engines. The
|
|
563
568
|
// unscoped in-memory ID may belong to the reviewer, so only trust its DB row.
|
|
564
569
|
const engineSessionId = lastSession?.engine_session_id;
|
|
@@ -790,6 +795,14 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
790
795
|
if (ev.kind === 'session:ended') {
|
|
791
796
|
if (pendingTerminalControllers.has(sourceController) || finalizedControllers.has(sourceController))
|
|
792
797
|
return;
|
|
798
|
+
if (controllers.get(workspaceId) === sourceController &&
|
|
799
|
+
getReviewReturn(workspaceId, agentSessionId) &&
|
|
800
|
+
ev.reason === 'completed' &&
|
|
801
|
+
(ev.exitCode === 0 || ev.exitCode === null)) {
|
|
802
|
+
setReviewReturnPhase(workspaceId, 'ready', null, isAutoLoopReview(workspaceId, agentSessionId)
|
|
803
|
+
? undefined
|
|
804
|
+
: buildReviewReturnPrompt(workspaceId, agentSessionId));
|
|
805
|
+
}
|
|
793
806
|
// Real engines may announce their logical result before their writer
|
|
794
807
|
// exits. Delay all terminal effects, including review return and quota
|
|
795
808
|
// retries, until SessionController confirms closure.
|
|
@@ -1019,7 +1032,7 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
1019
1032
|
const transferring = workspaceLifecycleReason(workspaceId) === 'session-handoff';
|
|
1020
1033
|
const returningReview = (ev.kind === 'error' || ev.kind === 'session:ended') && !!getReviewReturn(workspaceId, agentSessionId);
|
|
1021
1034
|
const quotaError = ev.kind === 'error' && (ev.category === 'quota' || /\b429\b/.test(ev.message));
|
|
1022
|
-
if (quotaError && !sourceControllerIsStopping && !
|
|
1035
|
+
if (quotaError && !sourceControllerIsStopping && !transferring) {
|
|
1023
1036
|
void handleQuota(workspaceId, agentSessionId);
|
|
1024
1037
|
}
|
|
1025
1038
|
if (ev.kind === 'error' &&
|
|
@@ -1034,7 +1047,7 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
1034
1047
|
}
|
|
1035
1048
|
if (ev.kind === 'error' && ev.category === 'resume_failed') {
|
|
1036
1049
|
rememberResumeFailed(workspaceId, agentSessionId);
|
|
1037
|
-
clearStaleEngineSessionId(workspaceId);
|
|
1050
|
+
clearStaleEngineSessionId(workspaceId, agentSessionId);
|
|
1038
1051
|
}
|
|
1039
1052
|
if (ev.kind === 'session:ended') {
|
|
1040
1053
|
if (sourceIsSuperseded && sourceController) {
|
|
@@ -1117,29 +1130,28 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
1117
1130
|
return;
|
|
1118
1131
|
const progressDelta = consumeMonotoneTaskProgress(workspaceId, snapshot);
|
|
1119
1132
|
if (returningReview) {
|
|
1120
|
-
const pending =
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
!workspace.worktreePurgedAt &&
|
|
1127
|
-
autoLoopService.getStatus(workspaceId).state !== 'blocked' &&
|
|
1128
|
-
(ev.reason === 'completed' || ev.reason === 'watchdog') &&
|
|
1129
|
-
(ev.exitCode === null || ev.exitCode === 0)) {
|
|
1130
|
-
try {
|
|
1131
|
-
const prompt = buildReviewReturnPrompt(workspaceId, agentSessionId);
|
|
1132
|
-
const original = pending.original;
|
|
1133
|
-
startAgent(workspaceId, workspace.worktreePath, prompt, original.sessionModel ?? original.model, true, original.agentPermissionMode, pending.originalSessionId, original.reasoningEffort);
|
|
1134
|
-
updateWorkspaceStatus(workspaceId, 'executing');
|
|
1135
|
-
emit(workspaceId, 'user:message', { content: prompt, sender: 'system-prompt' }, pending.originalSessionId);
|
|
1133
|
+
const pending = getReviewReturn(workspaceId, agentSessionId);
|
|
1134
|
+
if (pending && sourceController?.stopCause !== 'shutdown' && sourceController?.stopCause !== 'review-recovery') {
|
|
1135
|
+
if (quotaBackoffService.getPending(workspaceId)) {
|
|
1136
|
+
setReviewReturnPhase(workspaceId, 'reviewing');
|
|
1137
|
+
notifyCapacityAvailable(workspaceId);
|
|
1138
|
+
return;
|
|
1136
1139
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1140
|
+
if ((ev.reason === 'completed' || ev.reason === 'watchdog') && (ev.exitCode === null || ev.exitCode === 0)) {
|
|
1141
|
+
try {
|
|
1142
|
+
const prompt = prepareAutoLoopReviewReturn(workspaceId, agentSessionId) ??
|
|
1143
|
+
buildReviewReturnPrompt(workspaceId, agentSessionId);
|
|
1144
|
+
setReviewReturnPhase(workspaceId, 'ready', null, prompt);
|
|
1145
|
+
resumePendingReviewReturns(workspaceId);
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
restoreReviewConfiguration(workspaceId, agentSessionId, true);
|
|
1149
|
+
setReviewReturnPhase(workspaceId, 'blocked', error instanceof Error ? error.message : String(error));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
restoreReviewConfiguration(workspaceId, agentSessionId, true);
|
|
1154
|
+
setReviewReturnPhase(workspaceId, 'blocked', 'The review ended before completion. Retry the review or cancel the return.');
|
|
1143
1155
|
}
|
|
1144
1156
|
}
|
|
1145
1157
|
notifyCapacityAvailable(workspaceId);
|
|
@@ -1157,7 +1169,10 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
|
|
|
1157
1169
|
// disable() clears it, and the cleanup hook needs to know whether this was
|
|
1158
1170
|
// a mid-loop session (never cleans) or a standalone one.
|
|
1159
1171
|
const wasAutoLoop = autoLoopService.getStatus(workspaceId).auto_loop;
|
|
1160
|
-
|
|
1172
|
+
// Durable manual reviews also wait for capacity. Wake them before admitting
|
|
1173
|
+
// another auto-loop iteration now that this controller has actually closed.
|
|
1174
|
+
resumePendingReviewReturns();
|
|
1175
|
+
autoLoopService.onSessionEnded(workspaceId, effectiveReason, progressDelta, instructionIntake, agentSessionId);
|
|
1161
1176
|
// A slot just freed. Any auto-loop workspace parked on the concurrency
|
|
1162
1177
|
// limit has no session of its own to bring it back; this one does it.
|
|
1163
1178
|
autoLoopService.resumeWaitingWorkspaces();
|
|
@@ -1347,6 +1362,101 @@ function revokeControllerMemoryCapability(controller) {
|
|
|
1347
1362
|
revokeMemoryCapability(token);
|
|
1348
1363
|
controllerMemoryCapabilities.delete(controller);
|
|
1349
1364
|
}
|
|
1365
|
+
const reviewReturnDispatchOwners = new Map();
|
|
1366
|
+
/** Resume durable review work only after the previous runtime has closed and admission allows it. */
|
|
1367
|
+
export function resumePendingReviewReturns(onlyWorkspaceId) {
|
|
1368
|
+
if (shuttingDown)
|
|
1369
|
+
return;
|
|
1370
|
+
for (const pending of listReviewReturns()) {
|
|
1371
|
+
const id = pending.workspaceId;
|
|
1372
|
+
if (onlyWorkspaceId && onlyWorkspaceId !== id)
|
|
1373
|
+
continue;
|
|
1374
|
+
if (pending.phase === 'unknown' || pending.phase === 'blocked' || pending.phase === 'dispatching')
|
|
1375
|
+
continue;
|
|
1376
|
+
const workspace = getWs(id);
|
|
1377
|
+
if (!workspace || !autoLoopService.canStartAutomatically(id))
|
|
1378
|
+
continue;
|
|
1379
|
+
try {
|
|
1380
|
+
if (pending.phase === 'ready') {
|
|
1381
|
+
const originalSession = getDb()
|
|
1382
|
+
.prepare('SELECT engine_session_id, engine FROM agent_sessions WHERE id = ? AND workspace_id = ?')
|
|
1383
|
+
.get(pending.originalSessionId, id);
|
|
1384
|
+
if (!originalSession?.engine_session_id ||
|
|
1385
|
+
(originalSession.engine && originalSession.engine !== pending.original.engine))
|
|
1386
|
+
throw new Error('The original session cannot be resumed. Cancel the return and inspect the review report.');
|
|
1387
|
+
const prompt = pending.returnPrompt ??
|
|
1388
|
+
prepareAutoLoopReviewReturn(id, pending.reviewSessionId) ??
|
|
1389
|
+
buildReviewReturnPrompt(id, pending.reviewSessionId);
|
|
1390
|
+
setReviewReturnPhase(id, 'ready', null, prompt);
|
|
1391
|
+
restoreReviewConfiguration(id, undefined, true);
|
|
1392
|
+
const dispatchOwner = Symbol('review-return');
|
|
1393
|
+
reviewReturnDispatchOwners.set(id, dispatchOwner);
|
|
1394
|
+
startAgent(id, workspace.worktreePath, prompt, pending.original.sessionModel ?? pending.original.model, true, pending.original.agentPermissionMode, pending.originalSessionId, pending.original.reasoningEffort, () => {
|
|
1395
|
+
if (!claimReviewReturn(id))
|
|
1396
|
+
throw new Error('The review return is already being dispatched');
|
|
1397
|
+
}, {
|
|
1398
|
+
requireInputAcceptance: true,
|
|
1399
|
+
onStarted: () => {
|
|
1400
|
+
if (reviewReturnDispatchOwners.get(id) !== dispatchOwner)
|
|
1401
|
+
return;
|
|
1402
|
+
reviewReturnDispatchOwners.delete(id);
|
|
1403
|
+
completeReviewReturn(id);
|
|
1404
|
+
if (!controllers.has(id) && !getReviewReturn(id))
|
|
1405
|
+
autoLoopService.resumeWaitingWorkspaces();
|
|
1406
|
+
},
|
|
1407
|
+
onError: (message) => {
|
|
1408
|
+
if (reviewReturnDispatchOwners.get(id) === dispatchOwner)
|
|
1409
|
+
setReviewReturnPhase(id, 'unknown', message);
|
|
1410
|
+
},
|
|
1411
|
+
});
|
|
1412
|
+
updateWorkspaceStatus(id, 'executing');
|
|
1413
|
+
emit(id, 'user:message', { content: prompt, sender: 'system-prompt' }, pending.originalSessionId);
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
const session = getDb()
|
|
1417
|
+
.prepare('SELECT engine_session_id FROM agent_sessions WHERE id = ? AND workspace_id = ?')
|
|
1418
|
+
.get(pending.reviewSessionId, id);
|
|
1419
|
+
if (!session)
|
|
1420
|
+
throw new Error('The review session no longer exists');
|
|
1421
|
+
const extra = getAutoLoopReviewLaunch(id, pending.reviewSessionId);
|
|
1422
|
+
const prompt = session.engine_session_id
|
|
1423
|
+
? 'Kōbō was restarted during this read-only review. Continue the review in this same session and finish with a standalone findings summary. Do not change files or ask questions.' +
|
|
1424
|
+
(extra?.promptSuffix ?? '')
|
|
1425
|
+
: pending.reviewPrompt;
|
|
1426
|
+
if (!prompt)
|
|
1427
|
+
throw new Error('The review was interrupted before its native session was saved and has no durable launch prompt. Cancel and launch a new review.');
|
|
1428
|
+
updateWorkspaceEngineConfiguration(id, pending.review.engine, pending.review.model, pending.review.reasoningEffort, 'plan');
|
|
1429
|
+
emitEphemeral(id, 'workspace:configuration', { ...pending.review, agentPermissionMode: 'plan' });
|
|
1430
|
+
startAgent(id, workspace.worktreePath, prompt, pending.review.model, !!session.engine_session_id, 'plan', pending.reviewSessionId, pending.review.reasoningEffort, undefined, { mcpEnv: extra?.mcpEnv });
|
|
1431
|
+
updateWorkspaceStatus(id, 'executing');
|
|
1432
|
+
emit(id, 'user:message', { content: prompt, sender: 'system-prompt' }, pending.reviewSessionId);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
restoreReviewConfiguration(id, undefined, true);
|
|
1437
|
+
setReviewReturnPhase(id, 'blocked', error instanceof Error ? error.message : String(error));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
export function retryReviewReturn(workspaceId) {
|
|
1442
|
+
if (getAgentStatus(workspaceId))
|
|
1443
|
+
throw new Error('Stop the active agent before retrying the review return');
|
|
1444
|
+
const pending = getReviewReturn(workspaceId);
|
|
1445
|
+
if (!pending)
|
|
1446
|
+
throw new Error('No review return is pending');
|
|
1447
|
+
if (isAutoLoopReview(workspaceId, pending.reviewSessionId))
|
|
1448
|
+
retryAutoLoopReviewReturn(workspaceId, !pending.returnPrompt);
|
|
1449
|
+
setReviewReturnPhase(workspaceId, pending.returnPrompt ? 'ready' : 'reviewing');
|
|
1450
|
+
resumePendingReviewReturns(workspaceId);
|
|
1451
|
+
}
|
|
1452
|
+
export function cancelReviewReturn(workspaceId) {
|
|
1453
|
+
reviewReturnDispatchOwners.delete(workspaceId);
|
|
1454
|
+
cancelReviewLaunch(workspaceId);
|
|
1455
|
+
cancelAutoLoopFinalReview(workspaceId, 'review-return-cancelled');
|
|
1456
|
+
if (!getReviewReturn(workspaceId))
|
|
1457
|
+
return;
|
|
1458
|
+
restoreReviewConfiguration(workspaceId);
|
|
1459
|
+
}
|
|
1350
1460
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
1351
1461
|
/**
|
|
1352
1462
|
* Spawn an agent (via the resolved engine) for a workspace. Returns
|
|
@@ -1539,10 +1649,6 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1539
1649
|
if (ready && contextId) {
|
|
1540
1650
|
void ready.then(() => markMemoryContextInitialized(contextId), () => markMemoryContextFailed(contextId));
|
|
1541
1651
|
}
|
|
1542
|
-
if (launch?.onStarted && ready)
|
|
1543
|
-
await ready;
|
|
1544
|
-
if (controller.status !== 'stopping')
|
|
1545
|
-
launch?.onStarted?.();
|
|
1546
1652
|
const pid = controller.pid;
|
|
1547
1653
|
if (pid !== undefined) {
|
|
1548
1654
|
try {
|
|
@@ -1553,6 +1659,19 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1553
1659
|
console.error('[orchestrator] Failed to update pid:', err);
|
|
1554
1660
|
}
|
|
1555
1661
|
}
|
|
1662
|
+
if (launch?.onStarted && ready)
|
|
1663
|
+
await ready;
|
|
1664
|
+
if (launch?.requireInputAcceptance && controller.engineProcess?.initialPromptAccepted) {
|
|
1665
|
+
try {
|
|
1666
|
+
await controller.engineProcess.initialPromptAccepted;
|
|
1667
|
+
}
|
|
1668
|
+
catch (error) {
|
|
1669
|
+
launch.onError?.(error instanceof Error ? error.message : String(error));
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
if (controller.status !== 'stopping')
|
|
1674
|
+
launch?.onStarted?.();
|
|
1556
1675
|
})
|
|
1557
1676
|
.catch((err) => {
|
|
1558
1677
|
revokeControllerMemoryCapability(controller);
|
|
@@ -1636,7 +1755,7 @@ export const STOP_AGENT_TIMEOUT_MS = 15_000;
|
|
|
1636
1755
|
async function stopController(workspaceId, ctrl, cause = 'user') {
|
|
1637
1756
|
revokeControllerMemoryCapability(ctrl);
|
|
1638
1757
|
ctrl.stopCause = cause;
|
|
1639
|
-
restoreReviewConfiguration(workspaceId, ctrl.agentSessionId);
|
|
1758
|
+
restoreReviewConfiguration(workspaceId, ctrl.agentSessionId, cause === 'shutdown' || cause === 'review-recovery');
|
|
1640
1759
|
preCompactionStatus.delete(ctrl);
|
|
1641
1760
|
// Normalize the state synchronously so callers (archive, delete, manual
|
|
1642
1761
|
// stop) see a clean workspace immediately — without waiting for the async
|
|
@@ -1711,6 +1830,8 @@ async function raceWithTimeout(promise, timeoutMs, onTimeout) {
|
|
|
1711
1830
|
export async function stopAgentAndWait(workspaceId, timeoutMs = STOP_AGENT_TIMEOUT_MS,
|
|
1712
1831
|
/** Only `user` disables auto-loop; technical stops preserve its persisted intent. */
|
|
1713
1832
|
cause = 'user') {
|
|
1833
|
+
if (['user', 'delete', 'purge', 'archive'].includes(cause))
|
|
1834
|
+
cancelReviewReturn(workspaceId);
|
|
1714
1835
|
const finishHandoffStop = ['user', 'delete', 'purge', 'archive'].includes(cause)
|
|
1715
1836
|
? requestHandoffStop(workspaceId)
|
|
1716
1837
|
: undefined;
|
|
@@ -1826,8 +1947,7 @@ export async function sendMessage(workspaceId, content, expectedSessionId, befor
|
|
|
1826
1947
|
throw new Error(`Session '${expectedSessionId}' is not active for workspace '${workspaceId}'`);
|
|
1827
1948
|
}
|
|
1828
1949
|
wakeupService.cancel(workspaceId, 'user-message');
|
|
1829
|
-
beforeDispatch
|
|
1830
|
-
await ctrl.sendMessage(content);
|
|
1950
|
+
await ctrl.sendMessage(content, beforeDispatch);
|
|
1831
1951
|
}
|
|
1832
1952
|
export async function sendMessageForFallback(workspaceId, content) {
|
|
1833
1953
|
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
@@ -2236,14 +2356,13 @@ export function forgetRateLimitInfo(workspaceId) {
|
|
|
2236
2356
|
latestRateLimitInfo.delete(workspaceId);
|
|
2237
2357
|
}
|
|
2238
2358
|
/**
|
|
2239
|
-
*
|
|
2240
|
-
*
|
|
2241
|
-
* future resume cannot fall back to an older stale engine session.
|
|
2359
|
+
* Invalidate only the failed session. Other conversations (especially a
|
|
2360
|
+
* pending review's source) remain valid and must stay explicitly resumable.
|
|
2242
2361
|
*/
|
|
2243
|
-
function clearStaleEngineSessionId(workspaceId) {
|
|
2362
|
+
function clearStaleEngineSessionId(workspaceId, agentSessionId) {
|
|
2244
2363
|
try {
|
|
2245
2364
|
const db = getDb();
|
|
2246
|
-
db.prepare('UPDATE agent_sessions SET engine_session_id = NULL WHERE workspace_id = ?').run(workspaceId);
|
|
2365
|
+
db.prepare('UPDATE agent_sessions SET engine_session_id = NULL WHERE workspace_id = ? AND id = ?').run(workspaceId, agentSessionId);
|
|
2247
2366
|
sessionIds.delete(workspaceId);
|
|
2248
2367
|
}
|
|
2249
2368
|
catch (err) {
|
|
@@ -106,7 +106,7 @@ export class SessionController {
|
|
|
106
106
|
}
|
|
107
107
|
this._status = 'running';
|
|
108
108
|
}
|
|
109
|
-
async sendMessage(content) {
|
|
109
|
+
async sendMessage(content, beforeDispatch) {
|
|
110
110
|
if (!this._startPromise)
|
|
111
111
|
throw new Error('SessionController not started');
|
|
112
112
|
await this._startPromise;
|
|
@@ -114,6 +114,9 @@ export class SessionController {
|
|
|
114
114
|
throw new Error('SessionController is stopping');
|
|
115
115
|
if (!this._engineProcess)
|
|
116
116
|
throw new Error('SessionController not started');
|
|
117
|
+
// Startup may have yielded while the workspace's delivery mode changed.
|
|
118
|
+
// Validate immediately before handing the message to the engine.
|
|
119
|
+
beforeDispatch?.();
|
|
117
120
|
await this._engineProcess.sendMessage(content);
|
|
118
121
|
}
|
|
119
122
|
interrupt() {
|