@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.
Files changed (166) hide show
  1. package/AGENTS.md +74 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +13 -1
  4. package/dist/mcp-server/kobo-tasks-handlers.js +33 -75
  5. package/dist/mcp-server/kobo-tasks-server.js +82 -9
  6. package/dist/server/db/auto-loop-schema.js +44 -0
  7. package/dist/server/db/migrations.js +18 -0
  8. package/dist/server/db/review-return-schema.js +10 -0
  9. package/dist/server/db/schema.js +8 -1
  10. package/dist/server/db/session-handoff-schema.js +24 -0
  11. package/dist/server/index.js +12 -9
  12. package/dist/server/routes/auto-loop-messages.js +51 -0
  13. package/dist/server/routes/documents.js +1 -1
  14. package/dist/server/routes/workspaces.js +119 -145
  15. package/dist/server/services/agent/engines/claude-code/engine.js +16 -0
  16. package/dist/server/services/agent/engines/codex/engine.js +92 -31
  17. package/dist/server/services/agent/orchestrator.js +298 -96
  18. package/dist/server/services/agent/session-controller.js +42 -2
  19. package/dist/server/services/auto-loop-message-service.js +79 -0
  20. package/dist/server/services/auto-loop-service.js +225 -86
  21. package/dist/server/services/auto-loop-state-service.js +30 -0
  22. package/dist/server/services/cron-service.js +30 -16
  23. package/dist/server/services/engine-handoff-service.js +5 -5
  24. package/dist/server/services/quota-backoff-service.js +42 -10
  25. package/dist/server/services/review-return-service.js +64 -0
  26. package/dist/server/services/review-service.js +187 -0
  27. package/dist/server/services/session-activity-service.js +17 -0
  28. package/dist/server/services/session-handoff-runtime.js +26 -0
  29. package/dist/server/services/session-handoff-service.js +479 -0
  30. package/dist/server/services/settings-service.js +3 -3
  31. package/dist/server/services/task-mutations.js +186 -0
  32. package/dist/server/services/wakeup-service.js +24 -1
  33. package/dist/server/services/workspace-message-service.js +16 -2
  34. package/dist/server/services/workspace-service.js +28 -36
  35. package/dist/server/utils/workspace-lifecycle-guard.js +35 -20
  36. package/dist/shared/auto-loop-prompts.js +22 -11
  37. package/dist/shared/auto-loop-types.js +1 -0
  38. package/dist/shared/document-roots.js +2 -0
  39. package/dist/shared/review.js +6 -0
  40. package/dist/shared/session-handoff.js +4 -0
  41. package/dist/shared/task-verification.js +11 -0
  42. package/package.json +12 -12
  43. package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +8 -0
  44. package/src/client/dist/pwa/assets/{ChangelogPage-DX1WGFea.js → ChangelogPage-CnXTnFhe.js} +1 -1
  45. package/src/client/dist/pwa/assets/{ClosePopup-BqW5dy-O.js → ClosePopup-CWjIRHxu.js} +1 -1
  46. package/src/client/dist/pwa/assets/{ComparisonPanel-B4ne55Xj.js → ComparisonPanel-CKylN45q.js} +2 -2
  47. package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +2 -0
  48. package/src/client/dist/pwa/assets/{DashboardPage-Cv6HJEcO.js → DashboardPage-CZQLfoV1.js} +1 -1
  49. package/src/client/dist/pwa/assets/{DiffViewer-COwGxPt6.js → DiffViewer-Csf3WmLc.js} +4 -4
  50. package/src/client/dist/pwa/assets/{DocumentsPanel-DCXpLLb7.js → DocumentsPanel-BeCttB-x.js} +1 -1
  51. package/src/client/dist/pwa/assets/{DrawerToggleButton-DRL3RCkM.js → DrawerToggleButton-BJBBYeOc.js} +1 -1
  52. package/src/client/dist/pwa/assets/GitPanel-B-ZHgghQ.js +3 -0
  53. package/src/client/dist/pwa/assets/{HealthPage-XrOuMoDJ.js → HealthPage-l88TEk-k.js} +1 -1
  54. package/src/client/dist/pwa/assets/MainLayout-BAAQLq95.css +1 -0
  55. package/src/client/dist/pwa/assets/MainLayout-Cf4Q9x1u.js +2 -0
  56. package/src/client/dist/pwa/assets/{QBadge-DbkxGtht.js → QBadge-FWC1zDCq.js} +1 -1
  57. package/src/client/dist/pwa/assets/{QBanner-DhqH95GW.js → QBanner-CcRI_QIy.js} +1 -1
  58. package/src/client/dist/pwa/assets/QBtn-DEdx_Y1V.js +1 -0
  59. package/src/client/dist/pwa/assets/{QBtnDropdown-163oM6T_.js → QBtnDropdown-44Mj-y_1.js} +1 -1
  60. package/src/client/dist/pwa/assets/{QBtnGroup-DELKYo3u.js → QBtnGroup-CAoD6G0Q.js} +1 -1
  61. package/src/client/dist/pwa/assets/{QBtnToggle-BI3k3Ch2.js → QBtnToggle-C_hQ2UCQ.js} +1 -1
  62. package/src/client/dist/pwa/assets/{QChip-owciRuZu.js → QChip-DCPUhqXX.js} +1 -1
  63. package/src/client/dist/pwa/assets/QDrawer-B4qHw8wt.js +1 -0
  64. package/src/client/dist/pwa/assets/QExpansionItem-Bok1m8Wi.js +1 -0
  65. package/src/client/dist/pwa/assets/{QItemSection-D_5_0UrX.js → QItemSection-BhqphaH6.js} +1 -1
  66. package/src/client/dist/pwa/assets/{QList-qupzuEqO.js → QList-CHm4JNP0.js} +1 -1
  67. package/src/client/dist/pwa/assets/{QMarkupTable-B9QuKtK_.js → QMarkupTable-CNpiA9oq.js} +1 -1
  68. package/src/client/dist/pwa/assets/{QPage-DaX9sMQC.js → QPage-BXBw2Alx.js} +1 -1
  69. package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +1 -0
  70. package/src/client/dist/pwa/assets/{QSpinnerDots-DnCV3VzI.js → QSpinnerDots-BkFiCsH3.js} +1 -1
  71. package/src/client/dist/pwa/assets/{QTooltip-BQWY7aeM.js → QTooltip-BDihn2aY.js} +1 -1
  72. package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +1 -0
  73. package/src/client/dist/pwa/assets/{SearchPage-DOHkP5lj.js → SearchPage-BwZ2TX1G.js} +1 -1
  74. package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +9 -0
  75. package/src/client/dist/pwa/assets/{SplitWorkspacePage-DL4kp9Ah.js → SplitWorkspacePage-Bg6nKBcA.js} +1 -1
  76. package/src/client/dist/pwa/assets/{TerminalPanel-DHDnyiYh.js → TerminalPanel-Cq-FR7A7.js} +9 -9
  77. package/src/client/dist/pwa/assets/{TouchPan-BnHXDR6F.js → TouchPan-BQ1cvzRC.js} +1 -1
  78. package/src/client/dist/pwa/assets/{TourReplayButton-ClVcB6mO.js → TourReplayButton-Dz1yEz_z.js} +1 -1
  79. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-Bxe6oIJI.js → WorkspaceAttentionLabels-BCVOP8vp.js} +1 -1
  80. package/src/client/dist/pwa/assets/WorkspacePage-BbUvR1Vf.css +1 -0
  81. package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +4 -0
  82. package/src/client/dist/pwa/assets/{browser-Bkx4fbxl.js → browser-BmWgBx9M.js} +1 -1
  83. package/src/client/dist/pwa/assets/build-path-tree-6EjBmYYY.js +1 -0
  84. package/src/client/dist/pwa/assets/de-COObokpu.js +17 -0
  85. package/src/client/dist/pwa/assets/documents-BZ8bwcjr.js +1 -0
  86. package/src/client/dist/pwa/assets/{editor.api-DreEG0If.js → editor.api-CK7--ckY.js} +1 -1
  87. package/src/client/dist/pwa/assets/editor.worker-iO5iN0DB.js +33 -0
  88. package/src/client/dist/pwa/assets/efforts-Bw0o2Bud.js +9 -0
  89. package/src/client/dist/pwa/assets/engineFeatures-B-9jHocY.css +1 -0
  90. package/src/client/dist/pwa/assets/engineFeatures-DQO9HHXI.js +1 -0
  91. package/src/client/dist/pwa/assets/es-By2Q9Tca.js +17 -0
  92. package/src/client/dist/pwa/assets/expand-template-C8VyjASo.js +1 -0
  93. package/src/client/dist/pwa/assets/{format-B7z3F-Mw.js → format-LtTLLitJ.js} +3 -3
  94. package/src/client/dist/pwa/assets/{formatters-BTBiU0v3.js → formatters-Bl9zA7Ai.js} +1 -1
  95. package/src/client/dist/pwa/assets/fr-D-sd0ddn.js +17 -0
  96. package/src/client/dist/pwa/assets/{freemarker2-BZNTVQ3K.js → freemarker2-DxezMaje.js} +1 -1
  97. package/src/client/dist/pwa/assets/{handlebars-k8tN0dI3.js → handlebars-DK_Bj6Rx.js} +1 -1
  98. package/src/client/dist/pwa/assets/{html-iZL3bbXq.js → html-DFfsfStD.js} +1 -1
  99. package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +1 -0
  100. package/src/client/dist/pwa/assets/index-0wtBh1Xt.css +1 -0
  101. package/src/client/dist/pwa/assets/index-BLd8DAzg.js +20 -0
  102. package/src/client/dist/pwa/assets/it-CeK5lyIA.js +17 -0
  103. package/src/client/dist/pwa/assets/{javascript-TV6gg0MO.js → javascript-DuNbcaL7.js} +1 -1
  104. package/src/client/dist/pwa/assets/{liquid-Cifa5ujF.js → liquid-CM3JKyhv.js} +1 -1
  105. package/src/client/dist/pwa/assets/{mdx-Bnj_8yGE.js → mdx-BG3zYwNF.js} +1 -1
  106. package/src/client/dist/pwa/assets/{monaco-contributions-pEh0n9Wk.js → monaco-contributions-DLscZGH-.js} +1 -1
  107. package/src/client/dist/pwa/assets/monaco.contribution-DnhZHwXT.js +2 -0
  108. package/src/client/dist/pwa/assets/{network-auth-3_5p4fdc.js → network-auth-BJiPbO5l.js} +1 -1
  109. package/src/client/dist/pwa/assets/{notify-theme-D_nf9AeF.js → notify-theme-D4p3qPkp.js} +1 -1
  110. package/src/client/dist/pwa/assets/{pending-diff-open-CUnxC1xw.js → pending-diff-open-dYIFdS4r.js} +1 -1
  111. package/src/client/dist/pwa/assets/permissionModes-B7fuUegU.js +36 -0
  112. package/src/client/dist/pwa/assets/{pinia-BKqmoVru.js → pinia-CTJ3OfP3.js} +1 -1
  113. package/src/client/dist/pwa/assets/{private.use-form-C7umbGO-.js → private.use-form-Bk8ti264.js} +1 -1
  114. package/src/client/dist/pwa/assets/{project-color-CihwpDAA.js → project-color-R_1je4vW.js} +1 -1
  115. package/src/client/dist/pwa/assets/{python-DOoNBSNL.js → python-COs6pHk2.js} +1 -1
  116. package/src/client/dist/pwa/assets/{razor-Bvq5vVpJ.js → razor-Jl1VqMsk.js} +1 -1
  117. package/src/client/dist/pwa/assets/reactivity.esm-bundler-Bo9anJjg.js +1 -0
  118. package/src/client/dist/pwa/assets/render-chat-markdown-AaOo0Tgq.js +67 -0
  119. package/src/client/dist/pwa/assets/{runtime-core.esm-bundler-CDnVUfeZ.js → runtime-core.esm-bundler-CNFKm4ij.js} +1 -1
  120. package/src/client/dist/pwa/assets/{typescript-VVzxoeyM.js → typescript-BU_TUuAM.js} +1 -1
  121. package/src/client/dist/pwa/assets/{use-checkbox-FnauAgTp.js → use-checkbox-CnvHBhSK.js} +1 -1
  122. package/src/client/dist/pwa/assets/{use-id-BIuaWKkB.js → use-id-wXd4c8FA.js} +1 -1
  123. package/src/client/dist/pwa/assets/use-is-mobile-DpKI3XMy.js +1 -0
  124. package/src/client/dist/pwa/assets/{use-search-index-status-DtLzygcy.js → use-search-index-status-Bluf9L99.js} +1 -1
  125. package/src/client/dist/pwa/assets/use-slide-transition-CiaDm-tR.js +1 -0
  126. package/src/client/dist/pwa/assets/use-tours-CVn874kD.js +2 -0
  127. package/src/client/dist/pwa/assets/{workspace-templates-1ui80Kqo.js → workspace-templates-BCKR8TXr.js} +1 -1
  128. package/src/client/dist/pwa/assets/{xml-CzGFIxQa.js → xml-gFFsobxE.js} +1 -1
  129. package/src/client/dist/pwa/assets/{yaml-Bl7nDJeC.js → yaml-DaXaqaGD.js} +1 -1
  130. package/src/client/dist/pwa/index.html +10 -10
  131. package/src/client/dist/pwa/sw.js +1 -1
  132. package/src/mcp-server/README.md +8 -1
  133. package/src/mcp-server/kobo-tasks-handlers.ts +72 -105
  134. package/src/mcp-server/kobo-tasks-server.ts +94 -9
  135. package/src/client/dist/pwa/assets/ActivityFeed-D1e-zV3O.js +0 -8
  136. package/src/client/dist/pwa/assets/CreatePage-Bx_GpUni.js +0 -2
  137. package/src/client/dist/pwa/assets/GitPanel-C9mz4GXF.js +0 -3
  138. package/src/client/dist/pwa/assets/MainLayout-BwV89R-l.css +0 -1
  139. package/src/client/dist/pwa/assets/MainLayout-CwWavQjV.js +0 -2
  140. package/src/client/dist/pwa/assets/QBtn-CZaBclNw.js +0 -1
  141. package/src/client/dist/pwa/assets/QDrawer-DNEj8wLZ.js +0 -1
  142. package/src/client/dist/pwa/assets/QSelect-BUlOQe5k.js +0 -1
  143. package/src/client/dist/pwa/assets/SchedulePanel-Bos94OyR.js +0 -1
  144. package/src/client/dist/pwa/assets/SettingsPage-DtGsd0XH.js +0 -9
  145. package/src/client/dist/pwa/assets/WorkspacePage-Clc4Fl3B.js +0 -4
  146. package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +0 -1
  147. package/src/client/dist/pwa/assets/build-path-tree-BpSY7N5O.js +0 -1
  148. package/src/client/dist/pwa/assets/de-BU0dILBs.js +0 -17
  149. package/src/client/dist/pwa/assets/documents-BIlWh3o-.js +0 -1
  150. package/src/client/dist/pwa/assets/editor.worker-CwQAd_xr.js +0 -30
  151. package/src/client/dist/pwa/assets/efforts-D_yTzymv.js +0 -9
  152. package/src/client/dist/pwa/assets/engineFeatures-C33LTVkK.js +0 -1
  153. package/src/client/dist/pwa/assets/es-CNp0s9na.js +0 -17
  154. package/src/client/dist/pwa/assets/expand-template-wRhAXT-1.js +0 -1
  155. package/src/client/dist/pwa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  156. package/src/client/dist/pwa/assets/fr-RqJyWSdm.js +0 -17
  157. package/src/client/dist/pwa/assets/i18n--PCet00a.js +0 -1
  158. package/src/client/dist/pwa/assets/index-CPfrY38K.js +0 -20
  159. package/src/client/dist/pwa/assets/index-CUoUy6tH.css +0 -1
  160. package/src/client/dist/pwa/assets/it-DSxNuCwL.js +0 -17
  161. package/src/client/dist/pwa/assets/monaco.contribution-D7NmJUJH.js +0 -2
  162. package/src/client/dist/pwa/assets/permissionModes-DC51Pz84.js +0 -36
  163. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DEJj9cFv.js +0 -1
  164. package/src/client/dist/pwa/assets/render-chat-markdown-BQ_saiy5.js +0 -67
  165. package/src/client/dist/pwa/assets/use-is-mobile-CWjqe8gC.js +0 -1
  166. package/src/client/dist/pwa/assets/use-tours-BKeYrJaQ.js +0 -2
package/AGENTS.md CHANGED
@@ -116,6 +116,7 @@ src/
116
116
  | `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
117
117
  | `agent_sessions` | agent-engine sessions: pid where applicable, engine session id, status, timestamps, model, and name |
118
118
  | `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
119
+ | `pending_review_returns` | one-shot return from a review to its original session and LLM configuration (migration v44) |
119
120
  | `pending_wakeups` | one-row-per-workspace scheduler for the `schedule_wakeup` MCP tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
120
121
  | `pending_quota_backoffs` | persisted quota retries and their attempt counts |
121
122
  | `pending_crons` | recurring workspace schedules and their next run |
@@ -135,7 +136,11 @@ src/
135
136
 
136
137
  `worktree_purged_at` + `worktree_purge_restore_data` drive the disk-space purge feature (see [Worktree purge](#worktree-purge) below): when set, the workspace's worktree folder has been removed from disk but the chat history is preserved. `worktree_purge_restore_data` is a JSON blob (`{ prNumber, prUrl, forge, mergeCommitSha, originalWorktreePath, originalSourceBranch, originalWorkingBranch }`) captured at purge time for future "Restore" UX. Both fields are cleared automatically by the pr-watcher when the worktree folder reappears on disk.
137
138
 
138
- `auto_loop` (bool, default 0), `auto_loop_ready` (bool, default 0) and `no_progress_streak` (int, default 0) drive the auto-loop feature: when `auto_loop=1`, `session:ended` triggers `auto-loop-service.onSessionEnded` which either spawns the next iteration via a fresh `startAgent(resume=false)`, disables with `reason='completed'` (no pending tasks), or disables with `reason='stall'` (3 consecutive sessions without a task completed). Archive + delete both auto-disable.
139
+ `auto_loop` preserves the user's execution intent. `auto_loop_runs` (migration v45) stores phase (`grooming | execution | finalization`), operational state (`active | waiting | blocked | completed | stopped`), iteration count and diagnostic recovery. `auto_loop_progress` records monotone task milestones; metadata changes are not progress. After three stagnant iterations, run one diagnostic and two further attempts, then block explicitly without clearing intent. Quotas wait for their actual reset, including multi-day windows; only transient failures use the retry cap. Archived/deleted workspaces disable the loop.
140
+
141
+ Tasks have `role` (`work | finalization`) and structured `verification`. MCP and HTTP share transactional task mutations. An auto-loop task cannot become done without passing reported checks. Finalization runs after all other tasks/criteria, is invalidated by scope changes, and is mandatory before completion. The proof is agent-reported evidence, not server-side execution of arbitrary commands.
142
+
143
+ `auto_loop_messages` durably queues next-iteration instructions, independently of engine session ids. A dispatch interrupted before confirmed completion becomes `unknown`, requiring explicit acknowledgement or retry. Unknown or in-flight instructions prevent completion. Actual engine closure (`EngineProcess.closed`), not a logical end event alone, releases controller ownership and permits the next writer.
139
144
 
140
145
  `brainstorm_model` (nullable, set only at creation) lets a workspace created with `auto_loop=1` run its initial brainstorming session on a different model than the auto-loop iterations that follow, while staying on the same engine. `POST /api/workspaces` uses it for the very first `startAgent` call only; every subsequent auto-loop iteration (`auto-loop-service.ts`'s `spawnNextIteration`) always reads `model`, unchanged. The switch to `model` takes effect once that first live session ends naturally — not at the `[BRAINSTORM_COMPLETE]` marker, since the same session keeps running under `brainstorm_model` until it stops on its own.
141
146
 
@@ -259,6 +264,74 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
259
264
 
260
265
  ## Workspace operations
261
266
 
267
+ ### Fresh sessions and LLM handoffs
268
+
269
+ `session-handoff-service.ts` owns manual transfers within a workspace, including
270
+ same-engine model changes. `session_handoffs` (migration v46) preserves the source,
271
+ target, request idempotency, report and operation state. Lifecycle reservations
272
+ permit only launches carrying their owner token; ordinary chat, reviews and
273
+ automatic writers remain excluded until completion or explicit cancellation.
274
+
275
+ Stop the source immediately and confirm closure before resuming its exact native
276
+ conversation for the optional report-only turn. The final destination always uses
277
+ a fresh conversation. Generation uses a scoped `submit_session_handoff` MCP tool;
278
+ the backend persists the report, publishes `.ai/handoffs/` documents and owns the
279
+ transition. Never treat this generation as auto-loop progress or settle uncertain
280
+ instructions through it. Pending instruction contents stay with the loop dispatcher.
281
+
282
+ `EngineProcess.ready`, when provided, confirms initial native session/turn
283
+ acceptance. A returned process handle alone is insufficient evidence that a
284
+ transfer started successfully. Startup reconciliation retains interrupted transfers
285
+ for an explicit retry/skip/cancel decision and never replays them automatically.
286
+ Stop cancels pending transfers, including while no engine is running. Preserve
287
+ source selection by last use after a review return; loading a historical completed
288
+ transfer must not change the user's currently selected conversation.
289
+ Failed, interrupted and cancelled transfers restore the source conversation as well
290
+ as its configuration once the engine is stopped. `agent_sessions.activation_order`
291
+ (migration v47) records starts, resumes and source restoration independently of real
292
+ start/end timestamps. Backend current-session and implicit-resume selection, and the
293
+ client's current-session helper, use this order with the legacy timestamp fallback.
294
+ Keep failed target sessions in history and do not force a historical handoff's
295
+ selection over a later conversation after reload.
296
+ Rolled-back targets have `activation_order = -1`: exclude them from default
297
+ selection and implicit resume, but retain their native id and execution history.
298
+ An explicit start/resume activates them again. Without a source conversation,
299
+ rollback clears the client selection, including stale persisted preferences,
300
+ so the next ordinary message starts a fresh conversation on the restored engine.
301
+ Pending wakeups pinned to the source move to the destination only after confirmed
302
+ startup, in the same transaction as handoff completion. Preserve their prompt and
303
+ deadline, and never rebind a wakeup belonging to another conversation.
304
+
305
+ ### Review LLM and return to the original session
306
+
307
+ The current conversation is the running session, or the last activated session,
308
+ falling back to `max(startedAt, endedAt)` for legacy rows. Keep the session list in creation order,
309
+ but do not use its first entry to identify the current conversation: a temporary
310
+ review can return to an older session. The stale-session banner and the next
311
+ review's return target must recognize that original session, including on reload.
312
+
313
+ `StartReviewDialog.vue` uses the shared model/effort/permission catalogues. Changed
314
+ settings force a new session on both client and server. `review-service.ts`
315
+ validates before side effects, waits for confirmed shutdown, then applies the
316
+ selected configuration and launches the review. Without a return request, these
317
+ settings remain the workspace defaults. Legacy requests retain their behavior.
318
+
319
+ The optional `returnToSession` flag captures the exact resumable original session.
320
+ `pending_review_returns` persists its configuration and the review session id
321
+ before switching engines. The orchestrator intercepts a successful review end
322
+ (including watchdog closure without a nonzero exit), restores configuration and
323
+ resumes that exact native conversation with the reviewer's final text report.
324
+ Resume selection is scoped to the target engine; never pass a reviewer's native
325
+ conversation id to the original engine, even after cancellation or restart.
326
+ The report reassembles text deltas/final snapshots, is bounded to 24,000 characters,
327
+ and links back to the complete review session through the conversation tool.
328
+ The receiving agent is asked to summarize and await instructions before fixes.
329
+ The intent is consumed before delivery, preventing repeated ends from sending twice.
330
+ Errors restore settings without automatically resuming; explicit stop cancels the
331
+ return. Startup restores interrupted-review settings without replaying a possibly
332
+ already-delivered handoff. Preserve these guards and the auto-loop interception.
333
+ `workspace:configuration` is ephemeral and updates all four LLM settings across tabs.
334
+
262
335
  ### Delete confirmation
263
336
 
264
337
  The workspace deletion dialog requires the exact `workingBranch`, not the editable
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 1.11.45
8
+
9
+ - fix(sessions): harden handoff recovery and highlight transfer progress
10
+ - feat(sessions): add fresh sessions with optional handoffs
11
+
12
+ ## 1.11.44
13
+
14
+ - chore(deps): update packages and node to 24.21.0
15
+ - fix(auto-loop): preserve mission intent until verified completion
16
+ - feat(review): configure review LLM and return to original session
17
+
7
18
  ## 1.11.43
8
19
 
9
20
  - fix(workspaces): refine watchdog stops and deletion confirmation
package/README.md CHANGED
@@ -17,7 +17,7 @@ Kōbō (工房, "workshop") turns *one agent, one terminal* into a real workflow
17
17
 
18
18
  | | |
19
19
  |---|---|
20
- | **🗂️ Isolated worktrees, two engines** | Every workspace is its own git worktree and branch — parallel sessions never collide. Pick Claude Code or OpenAI Codex per workspace, and switch engines later without losing the worktree: Kōbō hands off tasks, git state, and recent context to a fresh session. |
20
+ | **🗂️ Isolated worktrees, two engines** | Every workspace is its own git worktree and branch — parallel sessions never collide. Pick Claude Code or OpenAI Codex per workspace, and [continue in a fresh session](#fresh-sessions-and-llm-handoffs) with the same LLM or a different model/engine, preserving the worktree and mission history. |
21
21
  | **💬 Live chat, real diff review** | Streaming responses, inline Edit/Write diffs, a reasoning panel, and a Monaco diff viewer with **inline file editing**, conflict resolution, and one-click `Sync` / `Push` / `Open PR` / `Merge`. |
22
22
  | **🔁 Auto-loop, cron, wakeups** | Turn an agent loose on the task list: it works through tasks, retries on rate limits, and stops itself when there's nothing left, progress stalls, or it needs you. Cron schedules and one-shot wakeups keep workspaces moving on their own timeline. |
23
23
  | **🔀 Create from a PR/MR, or from a ticket** | Pick an open pull/merge request from GitHub, GitLab, or Bitbucket and Kōbō resolves every local conflict for you before spinning up the workspace. Or start straight from a Notion page or a Sentry issue URL. |
@@ -62,6 +62,18 @@ Kōbō's production build is an installable PWA — use your browser's **Install
62
62
  - **Observability**: a per-session timeline (duration, tools, tokens, errors) and a downloadable redacted diagnostic JSON.
63
63
  - **Optional integrations**: Notion (import missions) and Sentry (fix from issue URL), each independently toggled with a **Test connection** action; local voice transcription via `whisper.cpp`.
64
64
 
65
+ ## Fresh sessions and LLM handoffs
66
+
67
+ Long conversation getting crowded? Use **Continue in a fresh session** at the bottom of the right-hand Tools panel. **Change LLM** uses the same transfer flow and also lets you select another model within the current engine.
68
+
69
+ 1. Choose the destination model, reasoning effort and permission mode. A fresh session starts with the workspace's current configuration selected.
70
+ 2. Leave **Generate a handoff with the current agent** enabled to have the current conversation summarize the objective, constraints, decisions, verified progress, failed approaches and next action. Disable it when the old provider has no quota left: Kōbō builds context from the mission, tasks, Git state and source history without calling that LLM.
71
+ 3. Start the transfer. Kōbō interrupts the current work immediately, waits for confirmed shutdown, prepares the handoff and opens a fresh native conversation that continues the mission automatically.
72
+
73
+ The workspace, worktree, task list and conversation history remain available. The transfer links to its source session and a Markdown report in **Documents** (`.ai/handoffs/`, ignored by Git). Auto-loop keeps its intent and diagnostic budget; queued instructions retain their delivery state. Pending wakeups from the source conversation follow the fresh session once its startup is confirmed.
74
+
75
+ If generation fails, choose **Retry**, **Continue without an LLM summary**, or **Cancel transfer**. Cancel and Stop leave the work stopped and restore the source conversation and LLM configuration, including when the destination failed to start. Failed attempts remain in history without being resumed automatically. If there was no source conversation, the next message starts a fresh one. A server restart preserves the transfer and report for an explicit recovery decision; it never automatically replays a potentially delivered prompt. Transfers are manual—conversation size does not trigger them automatically.
76
+
65
77
  ## Configuration
66
78
 
67
79
  The most common knobs:
@@ -1,12 +1,16 @@
1
+ import { DOCUMENT_DIRS } from '../shared/document-roots.js';
2
+ export { DOCUMENT_DIRS } from '../shared/document-roots.js';
1
3
  import fs from 'node:fs';
2
4
  import path from 'node:path';
3
5
  import { nanoid } from 'nanoid';
4
6
  import * as cronService from '../server/services/cron-service.js';
5
7
  import * as settingsService from '../server/services/settings-service.js';
8
+ import { createTaskRecord, deleteTaskRecord, updateTaskRecord, } from '../server/services/task-mutations.js';
6
9
  import { slugifyProjectName } from '../server/utils/project-slug.js';
7
10
  import { ensureDirectoryInside, resolveExistingPathInside } from '../server/utils/safe-path.js';
8
11
  import { resolveWorkspaceWorktreePath } from '../server/utils/worktree-paths.js';
9
12
  import { MASKED_SECRET, SECRET_GLOBAL_KEYS } from '../shared/consts.js';
13
+ import { parseTaskVerification } from '../shared/task-verification.js';
10
14
  /** Allowed task status values. */
11
15
  export const VALID_TASK_STATUSES = ['pending', 'in_progress', 'done'];
12
16
  function rowToDto(row) {
@@ -15,12 +19,15 @@ function rowToDto(row) {
15
19
  title: row.title,
16
20
  status: row.status,
17
21
  is_acceptance_criterion: row.is_acceptance_criterion === 1,
22
+ sort_order: row.sort_order,
23
+ role: row.role,
24
+ verification: parseTaskVerification(row.verification),
18
25
  };
19
26
  }
20
27
  /** Return all tasks for a workspace, ordered by sort_order. */
21
28
  export function listTasksHandler(db, workspaceId) {
22
29
  const rows = db
23
- .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE workspace_id = ? ORDER BY sort_order ASC')
30
+ .prepare('SELECT * FROM tasks WHERE workspace_id = ? ORDER BY sort_order ASC, rowid ASC')
24
31
  .all(workspaceId);
25
32
  return rows.map(rowToDto);
26
33
  }
@@ -81,86 +88,38 @@ export function setWorkspaceAgentDescriptionHandler(db, workspaceId, args) {
81
88
  }
82
89
  return { ok: true, description: stored };
83
90
  }
84
- /** Set a task's status to "done" and return the updated task. */
85
- export function markTaskDoneHandler(db, workspaceId, taskId) {
86
- const now = new Date().toISOString();
87
- const result = db
88
- .prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ? AND workspace_id = ?')
89
- .run('done', now, taskId, workspaceId);
90
- if (result.changes === 0) {
91
- throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`);
92
- }
93
- const row = db
94
- .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?')
95
- .get(taskId);
91
+ /** Set a task's status to done; auto-loop tasks require successful evidence. */
92
+ export function markTaskDoneHandler(db, workspaceId, taskId, verification) {
93
+ const row = updateTaskRecord(db, workspaceId, taskId, {
94
+ status: 'done',
95
+ ...(verification !== undefined ? { verification } : {}),
96
+ });
96
97
  return { success: true, task: rowToDto(row) };
97
98
  }
98
- /** Create a new task appended at the end of the workspace's task list. */
99
+ /** Create a task at an explicit position or append it to the list. */
99
100
  export function createTaskHandler(db, workspaceId, data) {
100
- if (!data.title?.trim()) {
101
- throw new Error('title is required');
102
- }
103
- // Verify workspace exists
104
- const ws = db.prepare('SELECT id FROM workspaces WHERE id = ?').get(workspaceId);
105
- if (!ws) {
106
- throw new Error(`Workspace '${workspaceId}' not found`);
107
- }
108
- const id = nanoid();
109
- const now = new Date().toISOString();
110
- const isAC = data.is_acceptance_criterion ? 1 : 0;
111
- // Append at the end: max(sort_order) + 1
112
- const maxRow = db
113
- .prepare('SELECT COALESCE(MAX(sort_order), -1) AS max FROM tasks WHERE workspace_id = ?')
114
- .get(workspaceId);
115
- const sortOrder = maxRow.max + 1;
116
- db.prepare('INSERT INTO tasks (id, workspace_id, title, status, is_acceptance_criterion, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(id, workspaceId, data.title.trim(), 'pending', isAC, sortOrder, now, now);
117
- const row = db.prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?').get(id);
118
- return rowToDto(row);
101
+ return rowToDto(createTaskRecord(db, workspaceId, {
102
+ title: data.title,
103
+ isAcceptanceCriterion: data.is_acceptance_criterion,
104
+ sortOrder: data.sort_order,
105
+ afterTaskId: data.after_task_id,
106
+ role: data.role,
107
+ }));
119
108
  }
120
- /** Update one or more fields of an existing task (title, status, or acceptance criterion flag). */
109
+ /** Update task fields atomically, including insertion order and verification. */
121
110
  export function updateTaskHandler(db, workspaceId, taskId, data) {
122
- // Verify task belongs to workspace
123
- const existing = db.prepare('SELECT id FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId);
124
- if (!existing) {
125
- throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`);
126
- }
127
- const sets = [];
128
- const values = [];
129
- if (data.title !== undefined) {
130
- if (!data.title.trim())
131
- throw new Error('title cannot be empty');
132
- sets.push('title = ?');
133
- values.push(data.title.trim());
134
- }
135
- if (data.status !== undefined) {
136
- if (!VALID_TASK_STATUSES.includes(data.status)) {
137
- throw new Error(`Invalid status '${data.status}'. Must be one of: ${VALID_TASK_STATUSES.join(', ')}`);
138
- }
139
- sets.push('status = ?');
140
- values.push(data.status);
141
- }
142
- if (data.is_acceptance_criterion !== undefined) {
143
- sets.push('is_acceptance_criterion = ?');
144
- values.push(data.is_acceptance_criterion ? 1 : 0);
145
- }
146
- if (sets.length === 0) {
147
- throw new Error('No fields to update (provide title, status, or is_acceptance_criterion)');
148
- }
149
- sets.push('updated_at = ?');
150
- values.push(new Date().toISOString());
151
- values.push(taskId);
152
- db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...values);
153
- const row = db
154
- .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?')
155
- .get(taskId);
156
- return rowToDto(row);
111
+ return rowToDto(updateTaskRecord(db, workspaceId, taskId, {
112
+ title: data.title,
113
+ status: data.status,
114
+ isAcceptanceCriterion: data.is_acceptance_criterion,
115
+ sortOrder: data.sort_order,
116
+ afterTaskId: data.after_task_id,
117
+ verification: data.verification,
118
+ }));
157
119
  }
158
- /** Permanently delete a task from a workspace. */
120
+ /** Permanently delete a task from a workspace and invalidate its final review. */
159
121
  export function deleteTaskHandler(db, workspaceId, taskId) {
160
- const result = db.prepare('DELETE FROM tasks WHERE id = ? AND workspace_id = ?').run(taskId, workspaceId);
161
- if (result.changes === 0) {
162
- throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`);
163
- }
122
+ deleteTaskRecord(db, workspaceId, taskId);
164
123
  return { success: true, task_id: taskId };
165
124
  }
166
125
  /** Read the dev-server status for a workspace directly from the database. */
@@ -310,7 +269,6 @@ export function getTicketSourcesHandler(worktreePath) {
310
269
  }
311
270
  // ── Documents ────────────────────────────────────────────────────────────────
312
271
  /** Directories (relative to the worktree root) scanned for AI-generated docs. */
313
- export const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts'];
314
272
  /** Depth cap to keep recursion bounded even on pathological symlink loops. */
315
273
  const DOC_MAX_DEPTH = 8;
316
274
  const DOC_MAX_FILE_BYTES = 1024 * 1024;
@@ -6,6 +6,8 @@ import { getDb } from '../server/db/index.js';
6
6
  import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js';
7
7
  import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, readWorkspaceEventsCsvHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
8
8
  import { callWorkspaceDialogueTool } from './workspace-dialogue-client.js';
9
+ const handoffId = process.env.KOBO_HANDOFF_ID;
10
+ const handoffToken = process.env.KOBO_HANDOFF_TOKEN;
9
11
  const workspaceId = process.env.KOBO_WORKSPACE_ID;
10
12
  const dbPath = process.env.KOBO_DB_PATH;
11
13
  const settingsPath = process.env.KOBO_SETTINGS_PATH;
@@ -113,20 +115,63 @@ async function backendRequest(method, pathname, body) {
113
115
  return text ? JSON.parse(text) : null;
114
116
  }
115
117
  const server = new Server({ name: 'kobo-tasks', version: '1.0.0' }, { capabilities: { tools: {} } });
118
+ const TASK_VERIFICATION_SCHEMA = {
119
+ type: 'object',
120
+ description: 'Required to complete an auto-loop task. Describe checks actually performed; every check must be passed for done. Record failed/not_run checks only on an unfinished task.',
121
+ properties: {
122
+ method: { type: 'string', minLength: 1 },
123
+ summary: { type: 'string', minLength: 1 },
124
+ checks: {
125
+ type: 'array',
126
+ minItems: 1,
127
+ items: {
128
+ type: 'object',
129
+ properties: {
130
+ name: { type: 'string', minLength: 1 },
131
+ status: { type: 'string', enum: ['passed', 'failed', 'not_run'] },
132
+ },
133
+ required: ['name', 'status'],
134
+ },
135
+ },
136
+ },
137
+ required: ['method', 'summary', 'checks'],
138
+ };
139
+ const TASK_ORDER_PROPERTIES = {
140
+ sort_order: {
141
+ type: 'integer',
142
+ minimum: 0,
143
+ description: 'Insert/move at this order, shifting siblings. Omit to append on creation. Mutually exclusive with after_task_id.',
144
+ },
145
+ after_task_id: {
146
+ type: 'string',
147
+ description: 'Insert/move immediately after this task in the same workspace, preserving the remaining order.',
148
+ },
149
+ };
116
150
  const WORKSPACE_SCOPED_TOOLS = [
151
+ {
152
+ name: 'submit_session_handoff',
153
+ description: 'Submit the final Markdown handoff requested by Kōbō for this generation turn, then end your turn. Only available during a backend-owned session transfer; does not launch or stop sessions.',
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: { report: { type: 'string', minLength: 1, maxLength: 24000 } },
157
+ required: ['report'],
158
+ },
159
+ annotations: { destructiveHint: false, openWorldHint: false },
160
+ },
117
161
  {
118
162
  name: 'list_tasks',
119
- description: 'CALL FIRST on any non-trivial turn to know what the user wants done and what is already completed. Returns every task and acceptance criterion for the current workspace with its id and status. Re-call periodically (before marking something done, or after the user asks for a status) to stay in sync with user-added or external updates.',
163
+ description: 'CALL FIRST on any non-trivial turn to know what the user wants done and what is already completed. Returns every task and acceptance criterion for the current workspace with its id, status, sort_order, role, and verification evidence. Re-call periodically (before marking something done, or after the user asks for a status) to stay in sync with user-added or external updates.',
120
164
  inputSchema: { type: 'object', properties: {}, required: [] },
121
165
  annotations: { readOnlyHint: true, openWorldHint: false },
122
166
  },
123
167
  {
124
168
  name: 'mark_task_done',
125
- description: 'CALL AS SOON AS a task or acceptance criterion is finished AND verified (tests pass, feature works, diff committed). Do not wait for the end of the turn — the user watches progress live and marking each item as it completes is the primary signal Kōbō uses to track you.',
169
+ description: 'CALL AS SOON AS a task or acceptance criterion is finished AND verified by checks actually performed. Auto-loop requires structured verification evidence with all checks passed. Never mark done if required checks failed or were not run. Finalization also requires all work tasks and criteria done. Do not wait for the end of the turn — the user watches progress live and marking each item as it completes is the primary signal Kōbō uses to track you.',
126
170
  inputSchema: {
127
171
  type: 'object',
128
172
  properties: {
129
173
  task_id: { type: 'string', description: 'Task id from list_tasks.' },
174
+ verification: TASK_VERIFICATION_SCHEMA,
130
175
  },
131
176
  required: ['task_id'],
132
177
  },
@@ -140,10 +185,16 @@ const WORKSPACE_SCOPED_TOOLS = [
140
185
  },
141
186
  {
142
187
  name: 'create_task',
143
- description: 'CALL WHEN you discover follow-up work that was not in the original list and needs to stick around (e.g. "refactor this helper later", "add a test for edge case"). Appends at the end of the list. Do not use it for ephemeral internal notes — prefer log_thought for those.',
188
+ description: 'CALL WHEN you discover follow-up work that was not in the original list and needs to stick around (e.g. "refactor this helper later", "add a test for edge case"). Appends by default; use after_task_id for a dependent check immediately after its parent. Use role finalization for the final verification gate. Do not use it for ephemeral internal notes — prefer log_thought for those.',
144
189
  inputSchema: {
145
190
  type: 'object',
146
191
  properties: {
192
+ ...TASK_ORDER_PROPERTIES,
193
+ role: {
194
+ type: 'string',
195
+ enum: ['work', 'finalization'],
196
+ description: 'Default work. Finalization is the final verification gate and cannot finish while work/criteria remain open.',
197
+ },
147
198
  title: { type: 'string', description: 'Short, imperative title (e.g. "Add retry to fetchUser").' },
148
199
  is_acceptance_criterion: {
149
200
  type: 'boolean',
@@ -161,6 +212,8 @@ const WORKSPACE_SCOPED_TOOLS = [
161
212
  type: 'object',
162
213
  properties: {
163
214
  task_id: { type: 'string', description: 'Task id from list_tasks.' },
215
+ ...TASK_ORDER_PROPERTIES,
216
+ verification: TASK_VERIFICATION_SCHEMA,
164
217
  title: { type: 'string', description: 'New title (optional).' },
165
218
  status: {
166
219
  type: 'string',
@@ -371,13 +424,13 @@ const WORKSPACE_SCOPED_TOOLS = [
371
424
  // ── Knowledge / context tools ─────────────────────────────────────────────
372
425
  {
373
426
  name: 'list_documents',
374
- description: 'CALL EARLY on a new session to discover plans, specs, and thoughts previously written for this workspace. Recursively lists every .md under docs/plans/, docs/superpowers/, and .ai/thoughts/. Before writing a new plan, check if one already exists.',
427
+ description: 'CALL EARLY on a new session to discover plans, specs, and thoughts previously written for this workspace. Recursively lists every .md under docs/plans/, docs/superpowers/, .ai/thoughts/, and .ai/handoffs/. Before writing a new plan, check if one already exists.',
375
428
  inputSchema: { type: 'object', properties: {}, required: [] },
376
429
  annotations: { readOnlyHint: true, openWorldHint: false },
377
430
  },
378
431
  {
379
432
  name: 'read_document',
380
- description: 'CALL AFTER list_documents when a file title looks relevant to the current task. Returns the full markdown content. Scoped to docs/plans/, docs/superpowers/, .ai/thoughts/ — reject anything else.',
433
+ description: 'CALL AFTER list_documents when a file title looks relevant to the current task. Returns the full markdown content. Scoped to docs/plans/, docs/superpowers/, .ai/thoughts/, .ai/handoffs/ — reject anything else.',
381
434
  inputSchema: {
382
435
  type: 'object',
383
436
  properties: {
@@ -568,9 +621,13 @@ const GLOBAL_TOOLS = [
568
621
  * one of the ~35 workspace-scoped branches.
569
622
  */
570
623
  const GLOBAL_TOOL_NAMES = new Set(GLOBAL_TOOLS.map((t) => t.name));
571
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
572
- tools: workspaceId ? [...WORKSPACE_SCOPED_TOOLS, ...GLOBAL_TOOLS] : GLOBAL_TOOLS,
573
- }));
624
+ function availableTools() {
625
+ const tools = workspaceId ? [...WORKSPACE_SCOPED_TOOLS, ...GLOBAL_TOOLS] : GLOBAL_TOOLS;
626
+ return tools.filter((tool) => handoffId && handoffToken
627
+ ? tool.name === 'submit_session_handoff' || tool.annotations?.readOnlyHint === true
628
+ : tool.name !== 'submit_session_handoff');
629
+ }
630
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: availableTools() }));
574
631
  /** Wrap a successful result as an MCP tool response with JSON text content. */
575
632
  function ok(data) {
576
633
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
@@ -600,6 +657,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
600
657
  return fail(`Tool '${name}' requires a workspace-bound MCP session (KOBO_WORKSPACE_ID not set).`);
601
658
  }
602
659
  try {
660
+ if (handoffId && handoffToken && !availableTools().some((tool) => tool.name === name))
661
+ return fail('This turn is reserved for a handoff report. Only read tools and submit_session_handoff are available.');
662
+ if (name === 'submit_session_handoff') {
663
+ if (!workspaceId || !handoffId || !handoffToken)
664
+ return fail('No handoff generation is active for this MCP invocation.');
665
+ return ok(await backendRequest('POST', `/api/workspaces/${workspaceId}/session-handoffs/${handoffId}/report`, {
666
+ token: handoffToken,
667
+ report: a.report,
668
+ }));
669
+ }
603
670
  if (WORKSPACE_DIALOGUE_TOOLS.some((tool) => tool.name === name)) {
604
671
  if (workspaceId && a.workspace_id === workspaceId && name === 'send_workspace_message')
605
672
  return fail('Use the normal conversation to communicate in your own workspace.');
@@ -612,7 +679,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
612
679
  const taskId = a.task_id;
613
680
  if (!taskId)
614
681
  return fail('task_id parameter is required');
615
- const result = markTaskDoneHandler(db, workspaceId, taskId);
682
+ const result = markTaskDoneHandler(db, workspaceId, taskId, a.verification);
616
683
  void notifyBackend(taskId);
617
684
  return ok(result);
618
685
  }
@@ -627,7 +694,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
627
694
  return fail('title parameter is required');
628
695
  const task = createTaskHandler(db, workspaceId, {
629
696
  title,
697
+ role: a.role,
630
698
  is_acceptance_criterion: a.is_acceptance_criterion,
699
+ sort_order: a.sort_order,
700
+ after_task_id: a.after_task_id,
631
701
  });
632
702
  void notifyTasksUpdated();
633
703
  return ok(task);
@@ -639,7 +709,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
639
709
  const task = updateTaskHandler(db, workspaceId, taskId, {
640
710
  title: a.title,
641
711
  status: a.status,
712
+ verification: a.verification,
642
713
  is_acceptance_criterion: a.is_acceptance_criterion,
714
+ sort_order: a.sort_order,
715
+ after_task_id: a.after_task_id,
643
716
  });
644
717
  void notifyTasksUpdated();
645
718
  return ok(task);
@@ -0,0 +1,44 @@
1
+ /** Shared by fresh installs and the append-only upgrade. */
2
+ export function initAutoLoopSchema(db) {
3
+ const columns = db.prepare('PRAGMA table_info(tasks)').all();
4
+ if (columns.length) {
5
+ if (!columns.some((c) => c.name === 'role'))
6
+ db.exec("ALTER TABLE tasks ADD COLUMN role TEXT NOT NULL DEFAULT 'work'");
7
+ if (!columns.some((c) => c.name === 'verification'))
8
+ db.exec('ALTER TABLE tasks ADD COLUMN verification TEXT');
9
+ if (columns.some((c) => c.name === 'title')) {
10
+ db.exec("UPDATE tasks SET role = 'finalization' WHERE title LIKE '[FINAL] %' AND role = 'work'");
11
+ }
12
+ }
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS auto_loop_runs (
15
+ workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
16
+ phase TEXT NOT NULL DEFAULT 'grooming',
17
+ state TEXT NOT NULL DEFAULT 'waiting',
18
+ reason TEXT,
19
+ iteration INTEGER NOT NULL DEFAULT 0,
20
+ diagnostic_attempts INTEGER NOT NULL DEFAULT 0,
21
+ current_task_id TEXT,
22
+ current_session_id TEXT,
23
+ updated_at TEXT NOT NULL
24
+ );
25
+ CREATE TABLE IF NOT EXISTS auto_loop_progress (
26
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
27
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
28
+ milestone INTEGER NOT NULL DEFAULT 0,
29
+ PRIMARY KEY (workspace_id, task_id)
30
+ );
31
+ CREATE TABLE IF NOT EXISTS auto_loop_messages (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
34
+ client_message_id TEXT NOT NULL,
35
+ content TEXT NOT NULL,
36
+ source TEXT,
37
+ state TEXT NOT NULL DEFAULT 'pending',
38
+ session_id TEXT,
39
+ created_at TEXT NOT NULL,
40
+ UNIQUE (workspace_id, client_message_id)
41
+ );
42
+ CREATE INDEX IF NOT EXISTS idx_auto_loop_messages_pending ON auto_loop_messages(workspace_id, state, id);
43
+ `);
44
+ }
@@ -1,6 +1,9 @@
1
+ import { initAutoLoopSchema } from './auto-loop-schema.js';
1
2
  import { initMcpMessageSchema } from './mcp-message-schema.js';
3
+ import { initReviewReturnSchema } from './review-return-schema.js';
2
4
  import { initSchema } from './schema.js';
3
5
  import { initSearchSchema } from './search-schema.js';
6
+ import { initSessionHandoffSchema } from './session-handoff-schema.js';
4
7
  /** Ordered registry of all schema migrations. Append new entries at the end. */
5
8
  export const migrations = [
6
9
  {
@@ -744,6 +747,21 @@ export const migrations = [
744
747
  },
745
748
  { version: 42, name: 'index-logical-search-messages', migrate: initSearchSchema },
746
749
  { version: 43, name: 'durable-mcp-message-requests', migrate: initMcpMessageSchema },
750
+ { version: 44, name: 'review-session-returns', migrate: initReviewReturnSchema },
751
+ { version: 45, name: 'durable-auto-loop', migrate: initAutoLoopSchema },
752
+ { version: 46, name: 'session-handoffs', migrate: initSessionHandoffSchema },
753
+ {
754
+ version: 47,
755
+ name: 'session-activation-order',
756
+ migrate(db) {
757
+ const table = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'agent_sessions'").get();
758
+ if (!table)
759
+ return;
760
+ const columns = db.prepare('PRAGMA table_info(agent_sessions)').all();
761
+ if (!columns.some((column) => column.name === 'activation_order'))
762
+ db.exec('ALTER TABLE agent_sessions ADD COLUMN activation_order INTEGER NOT NULL DEFAULT 0');
763
+ },
764
+ },
747
765
  ];
748
766
  /** Current schema version — always equals the highest migration version. */
749
767
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -0,0 +1,10 @@
1
+ export function initReviewReturnSchema(db) {
2
+ db.exec(`CREATE TABLE IF NOT EXISTS pending_review_returns (
3
+ workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
4
+ review_session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
5
+ original_session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
6
+ original_configuration TEXT NOT NULL,
7
+ review_configuration TEXT NOT NULL,
8
+ created_at TEXT NOT NULL
9
+ )`);
10
+ }
@@ -1,5 +1,8 @@
1
+ import { initAutoLoopSchema } from './auto-loop-schema.js';
1
2
  import { initMcpMessageSchema } from './mcp-message-schema.js';
3
+ import { initReviewReturnSchema } from './review-return-schema.js';
2
4
  import { initSearchSchema } from './search-schema.js';
5
+ import { initSessionHandoffSchema } from './session-handoff-schema.js';
3
6
  /** Create all tables and indexes for a fresh install. Not used for upgrades -- see migrations.ts. */
4
7
  export function initSchema(db) {
5
8
  db.exec(`
@@ -68,7 +71,8 @@ export function initSchema(db) {
68
71
  ended_at TEXT,
69
72
  name TEXT,
70
73
  task_progress_baseline TEXT,
71
- end_reason TEXT
74
+ end_reason TEXT,
75
+ activation_order INTEGER NOT NULL DEFAULT 0
72
76
  );
73
77
 
74
78
  CREATE TABLE IF NOT EXISTS ws_events (
@@ -225,4 +229,7 @@ export function initSchema(db) {
225
229
  `);
226
230
  initSearchSchema(db);
227
231
  initMcpMessageSchema(db);
232
+ initReviewReturnSchema(db);
233
+ initAutoLoopSchema(db);
234
+ initSessionHandoffSchema(db);
228
235
  }
@@ -0,0 +1,24 @@
1
+ export function initSessionHandoffSchema(db) {
2
+ db.exec(`CREATE TABLE IF NOT EXISTS session_handoffs (
3
+ id TEXT PRIMARY KEY,
4
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
5
+ request_id TEXT NOT NULL,
6
+ request_fingerprint TEXT NOT NULL,
7
+ source_session_id TEXT,
8
+ target_session_id TEXT,
9
+ source_configuration TEXT NOT NULL,
10
+ target_configuration TEXT NOT NULL,
11
+ source_model TEXT,
12
+ generate_summary INTEGER NOT NULL,
13
+ state TEXT NOT NULL,
14
+ report TEXT,
15
+ report_path TEXT,
16
+ generation_token TEXT,
17
+ error TEXT,
18
+ created_at TEXT NOT NULL,
19
+ updated_at TEXT NOT NULL,
20
+ UNIQUE(workspace_id, request_id)
21
+ );
22
+ CREATE UNIQUE INDEX IF NOT EXISTS session_handoffs_pending
23
+ ON session_handoffs(workspace_id) WHERE state NOT IN ('completed', 'cancelled');`);
24
+ }