@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.
Files changed (132) hide show
  1. package/AGENTS.md +38 -6
  2. package/CHANGELOG.md +12 -0
  3. package/CONFIGURATION.md +41 -5
  4. package/README.md +4 -2
  5. package/dist/mcp-server/kobo-tasks-server.js +62 -105
  6. package/dist/mcp-server/workspace-dialogue-client.js +15 -3
  7. package/dist/server/db/auto-loop-final-review-schema.js +18 -0
  8. package/dist/server/db/migrations.js +12 -1
  9. package/dist/server/db/review-return-schema.js +18 -1
  10. package/dist/server/db/schema.js +4 -0
  11. package/dist/server/db/workspace-message-batch-schema.js +9 -0
  12. package/dist/server/index.js +8 -1
  13. package/dist/server/routes/mcp.js +69 -3
  14. package/dist/server/routes/workspace-messages.js +22 -0
  15. package/dist/server/routes/workspaces.js +119 -4
  16. package/dist/server/services/agent/engines/claude-code/engine.js +37 -1
  17. package/dist/server/services/agent/orchestrator.js +159 -40
  18. package/dist/server/services/agent/session-controller.js +4 -1
  19. package/dist/server/services/auto-loop-final-review-service.js +329 -0
  20. package/dist/server/services/auto-loop-service.js +53 -10
  21. package/dist/server/services/pr-checkout-rollback-service.js +51 -0
  22. package/dist/server/services/pr-checkout-service.js +21 -1
  23. package/dist/server/services/review-launch-runtime.js +17 -0
  24. package/dist/server/services/review-return-service.js +58 -10
  25. package/dist/server/services/review-service.js +59 -11
  26. package/dist/server/services/workspace-creation-mcp-service.js +198 -0
  27. package/dist/server/services/workspace-group-message-mcp-service.js +58 -0
  28. package/dist/server/services/workspace-group-message-service.js +182 -0
  29. package/dist/server/services/workspace-lifecycle-mcp-service.js +72 -0
  30. package/dist/server/services/workspace-message-service.js +5 -0
  31. package/dist/server/services/workspace-service.js +11 -0
  32. package/dist/server/services/workspace-template-service.js +10 -0
  33. package/dist/shared/auto-loop-review.js +1 -0
  34. package/dist/shared/pr-checkout-steps.js +86 -0
  35. package/dist/shared/review-return.js +1 -0
  36. package/dist/shared/workspace-creation-tools.js +182 -0
  37. package/dist/shared/workspace-group-message-tools.js +134 -0
  38. package/dist/shared/workspace-group-messages.js +27 -0
  39. package/dist/shared/workspace-lifecycle-tools.js +112 -0
  40. package/package.json +1 -1
  41. package/src/client/dist/pwa/assets/ActivityFeed-tJmJhmtl.js +2 -0
  42. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Bbrq02Xt.js +1 -0
  43. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +1 -0
  44. package/src/client/dist/pwa/assets/{ChangelogPage-DD90Nl6c.js → ChangelogPage-BJfkAVYp.js} +1 -1
  45. package/src/client/dist/pwa/assets/{ComparisonPanel-D_BQFWiT.js → ComparisonPanel-BfP8APKY.js} +1 -1
  46. package/src/client/dist/pwa/assets/CreatePage-BbtNfo0x.js +2 -0
  47. package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +1 -0
  48. package/src/client/dist/pwa/assets/{DashboardPage-DyvTdmBg.js → DashboardPage-DZYsAh5y.js} +1 -1
  49. package/src/client/dist/pwa/assets/DiffViewer-4Q-GJgyA.js +8 -0
  50. package/src/client/dist/pwa/assets/{DocumentsPanel-bm639sIu.js → DocumentsPanel-BfPlfiwU.js} +1 -1
  51. package/src/client/dist/pwa/assets/{DrawerToggleButton-KfwSzjeY.js → DrawerToggleButton-DG1Qjqk1.js} +1 -1
  52. package/src/client/dist/pwa/assets/GitPanel-ECnbb_rx.js +3 -0
  53. package/src/client/dist/pwa/assets/{HealthPage-37IHOIW-.js → HealthPage-_VXG1Zeo.js} +1 -1
  54. package/src/client/dist/pwa/assets/MainLayout-BH6grTuW.js +2 -0
  55. package/src/client/dist/pwa/assets/MainLayout-DnmUxNk7.css +1 -0
  56. package/src/client/dist/pwa/assets/{MemoryPanel-CucZMhjy.js → MemoryPanel-CVlvGZXZ.js} +1 -1
  57. package/src/client/dist/pwa/assets/{MemoryProposalList-CUQybzVR.js → MemoryProposalList-SJsnzUAM.js} +1 -1
  58. package/src/client/dist/pwa/assets/{QBtnDropdown-x6qPyl_7.js → QBtnDropdown-DBgGsN-D.js} +1 -1
  59. package/src/client/dist/pwa/assets/{QExpansionItem-D33aoRoG.js → QExpansionItem-DKgaMJ_l.js} +1 -1
  60. package/src/client/dist/pwa/assets/{QScrollArea-Di-D6t2u.js → QScrollArea-D3OCsq3g.js} +1 -1
  61. package/src/client/dist/pwa/assets/QSelect-Cb_s_nox.js +1 -0
  62. package/src/client/dist/pwa/assets/{QTooltip-BhwG7lp2.js → QTooltip-X5ps8Opw.js} +1 -1
  63. package/src/client/dist/pwa/assets/{SchedulePanel-qFi9Wxzh.js → SchedulePanel-Ww_FVlW5.js} +1 -1
  64. package/src/client/dist/pwa/assets/{SearchPage-DA9nRmsA.js → SearchPage-B87WEHf3.js} +1 -1
  65. package/src/client/dist/pwa/assets/SettingsPage-BAZLGUyg.js +9 -0
  66. package/src/client/dist/pwa/assets/{SplitWorkspacePage-Brw2XsYJ.js → SplitWorkspacePage-B99AJUbG.js} +1 -1
  67. package/src/client/dist/pwa/assets/{TerminalPanel-DVP18-nk.js → TerminalPanel-DnA1MXwu.js} +9 -9
  68. package/src/client/dist/pwa/assets/{ToolCallItem-cSHh3p_F.js → ToolCallItem-2vFggaur.js} +3 -3
  69. package/src/client/dist/pwa/assets/{TouchPan-C6LZZ3rw.js → TouchPan-BSU3z0aO.js} +1 -1
  70. package/src/client/dist/pwa/assets/{TourReplayButton-Czlqo18H.js → TourReplayButton-DQwS2xdD.js} +1 -1
  71. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BDYGq0pC.js → WorkspaceAttentionLabels-CM1tGDje.js} +1 -1
  72. package/src/client/dist/pwa/assets/WorkspacePage-B248nKIU.css +1 -0
  73. package/src/client/dist/pwa/assets/WorkspacePage-CQN6EKIn.js +4 -0
  74. package/src/client/dist/pwa/assets/{browser-ifCwUGpE.js → browser-DPsupI9Q.js} +1 -1
  75. package/src/client/dist/pwa/assets/{build-path-tree-BBkGq3lN.js → build-path-tree-CbIdt-p9.js} +1 -1
  76. package/src/client/dist/pwa/assets/custom-sounds-DR68eyF8.js +1 -0
  77. package/src/client/dist/pwa/assets/de-D3Ycvf_F.js +17 -0
  78. package/src/client/dist/pwa/assets/{editor.api-CV_seGJR.js → editor.api-DvgAtnOj.js} +1 -1
  79. package/src/client/dist/pwa/assets/{efforts-ttw0bL63.js → efforts-nmxfTVQD.js} +1 -1
  80. package/src/client/dist/pwa/assets/es-TgeS5898.js +17 -0
  81. package/src/client/dist/pwa/assets/{expand-template-C8biMRXo.js → expand-template-Brn5O4a1.js} +1 -1
  82. package/src/client/dist/pwa/assets/{format-Cu95dbxt.js → format-DfJ3Fv1i.js} +1 -1
  83. package/src/client/dist/pwa/assets/{formatters-C25D1RSd.js → formatters-B30bNFhq.js} +1 -1
  84. package/src/client/dist/pwa/assets/fr-CJafryNb.js +17 -0
  85. package/src/client/dist/pwa/assets/{freemarker2-DJEFr74q.js → freemarker2-DnZe74tc.js} +1 -1
  86. package/src/client/dist/pwa/assets/{handlebars-C_lzsMjX.js → handlebars-CmuMfxzO.js} +1 -1
  87. package/src/client/dist/pwa/assets/{html-CWmLFAmY.js → html-BD7OJFGX.js} +1 -1
  88. package/src/client/dist/pwa/assets/i18n-BY4JeBjk.js +1 -0
  89. package/src/client/dist/pwa/assets/index-C5fAw9ll.js +20 -0
  90. package/src/client/dist/pwa/assets/it-C5mN8Y61.js +17 -0
  91. package/src/client/dist/pwa/assets/{javascript-tsuiEUe-.js → javascript-Y-OrgcmW.js} +1 -1
  92. package/src/client/dist/pwa/assets/{liquid-BBr9BvUk.js → liquid-BqOkKePH.js} +1 -1
  93. package/src/client/dist/pwa/assets/{mdx-gjBH1cXG.js → mdx-D-58x2h0.js} +1 -1
  94. package/src/client/dist/pwa/assets/{monaco-contributions-CvGcBIsb.js → monaco-contributions-BW4ZMKO6.js} +1 -1
  95. package/src/client/dist/pwa/assets/{monaco.contribution-Uw-Lfb8Z.js → monaco.contribution-DJWqzNG-.js} +2 -2
  96. package/src/client/dist/pwa/assets/network-auth-BGPRhUa6.js +1 -0
  97. package/src/client/dist/pwa/assets/{notify-theme-PMSxqP6t.js → notify-theme-DZDzfxph.js} +1 -1
  98. package/src/client/dist/pwa/assets/{project-color-BDQa2TgU.js → project-color-gB5yeZPN.js} +1 -1
  99. package/src/client/dist/pwa/assets/{python-DlIgD1n6.js → python-BkBj1wDC.js} +1 -1
  100. package/src/client/dist/pwa/assets/{razor-AraFPlpM.js → razor-DOOJ_0b8.js} +1 -1
  101. package/src/client/dist/pwa/assets/{typescript-CB8UpXHM.js → typescript-Bheo43G5.js} +1 -1
  102. package/src/client/dist/pwa/assets/{use-search-index-status-CSREHQM4.js → use-search-index-status-CBpnyV-R.js} +1 -1
  103. package/src/client/dist/pwa/assets/use-tours-BHNasCvg.js +2 -0
  104. package/src/client/dist/pwa/assets/{use-worktree-restore-BM3Giig1.js → use-worktree-restore-CgABsWno.js} +1 -1
  105. package/src/client/dist/pwa/assets/{workspace-templates-BPMFvTnQ.js → workspace-templates-BnFkigYg.js} +1 -1
  106. package/src/client/dist/pwa/assets/{xml-uRjL1P43.js → xml-vHt2BD26.js} +1 -1
  107. package/src/client/dist/pwa/assets/{yaml-C9CFR4qV.js → yaml-BvhLPzP9.js} +1 -1
  108. package/src/client/dist/pwa/index.html +1 -1
  109. package/src/client/dist/pwa/sw.js +1 -1
  110. package/src/mcp-server/README.md +156 -12
  111. package/src/mcp-server/kobo-tasks-server.ts +73 -106
  112. package/src/mcp-server/workspace-dialogue-client.ts +18 -3
  113. package/src/client/dist/pwa/assets/ActivityFeed-BTRzc60o.js +0 -2
  114. package/src/client/dist/pwa/assets/CreatePage-BZAoCukb.js +0 -2
  115. package/src/client/dist/pwa/assets/CreatePage-CdCs8PRS.css +0 -1
  116. package/src/client/dist/pwa/assets/DiffViewer-D0K2PlZD.js +0 -8
  117. package/src/client/dist/pwa/assets/GitPanel-C-Ejlc9_.js +0 -3
  118. package/src/client/dist/pwa/assets/MainLayout-BQOOup9o.css +0 -1
  119. package/src/client/dist/pwa/assets/MainLayout-DXQ1ouYl.js +0 -2
  120. package/src/client/dist/pwa/assets/QSelect-MdPXvwm4.js +0 -1
  121. package/src/client/dist/pwa/assets/SettingsPage-DcV6apqE.js +0 -9
  122. package/src/client/dist/pwa/assets/WorkspacePage-BwUkijpS.js +0 -4
  123. package/src/client/dist/pwa/assets/WorkspacePage-Bwfv559o.css +0 -1
  124. package/src/client/dist/pwa/assets/custom-sounds-CeSgjIY7.js +0 -1
  125. package/src/client/dist/pwa/assets/de-CHY2BAfL.js +0 -17
  126. package/src/client/dist/pwa/assets/es-DbcSKp38.js +0 -17
  127. package/src/client/dist/pwa/assets/fr-CyyJ8S2c.js +0 -17
  128. package/src/client/dist/pwa/assets/i18n-CobVQiTM.js +0 -1
  129. package/src/client/dist/pwa/assets/index-ImUH7Bbt.js +0 -20
  130. package/src/client/dist/pwa/assets/it-XQJW2Xkr.js +0 -17
  131. package/src/client/dist/pwa/assets/network-auth-D0Vm6a09.js +0 -1
  132. 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 { buildReviewReturnPrompt, getReviewReturn, restoreReviewConfiguration } from '../review-return-service.js';
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 engine_session_id IS NOT NULL AND activation_order >= 0 ORDER BY ${SESSION_RECENCY_ORDER} LIMIT 1`)
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 && !returningReview && !transferring) {
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 = restoreReviewConfiguration(workspaceId, agentSessionId);
1121
- const workspace = getWs(workspaceId);
1122
- if (pending &&
1123
- workspace &&
1124
- !shuttingDown &&
1125
- !workspace.archivedAt &&
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
- catch (err) {
1138
- routeEvent(workspaceId, agentSessionId, {
1139
- kind: 'error',
1140
- category: 'other',
1141
- message: `Could not return to the original session: ${err instanceof Error ? err.message : String(err)}`,
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
- autoLoopService.onSessionEnded(workspaceId, effectiveReason, progressDelta, instructionIntake);
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
- * Null out every engine_session_id for the workspace and clear its cache.
2240
- * This runs only while the failing session owns the workspace, ensuring a
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() {