@loicngr/kobo 2.1.7 → 2.1.10

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 (194) hide show
  1. package/AGENTS.md +18 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CONFIGURATION.md +44 -1
  4. package/DESIGN.md +11 -1
  5. package/README.md +4 -0
  6. package/dist/mcp-server/kobo-tasks-server.js +33 -1
  7. package/dist/mcp-server/memory-client.js +39 -0
  8. package/dist/mcp-server/workspace-dialogue-client.js +4 -1
  9. package/dist/server/db/memory-schema.js +185 -0
  10. package/dist/server/db/migrations.js +6 -0
  11. package/dist/server/db/schema.js +2 -0
  12. package/dist/server/index.js +4 -0
  13. package/dist/server/routes/mcp.js +31 -2
  14. package/dist/server/routes/memory.js +510 -0
  15. package/dist/server/routes/settings.js +3 -0
  16. package/dist/server/routes/workspaces.js +2 -0
  17. package/dist/server/services/agent/engines/claude-code/engine.js +7 -3
  18. package/dist/server/services/agent/engines/claude-code/event-mapper.js +23 -3
  19. package/dist/server/services/agent/engines/codex/event-mapper.js +2 -2
  20. package/dist/server/services/agent/orchestrator.js +110 -6
  21. package/dist/server/services/memory-agent-runtime.js +65 -0
  22. package/dist/server/services/memory-budget-service.js +205 -0
  23. package/dist/server/services/memory-context-service.js +306 -0
  24. package/dist/server/services/memory-mcp-service.js +310 -0
  25. package/dist/server/services/memory-service.js +891 -0
  26. package/dist/server/services/settings-defaults.js +2 -2
  27. package/dist/server/services/settings-service.js +21 -2
  28. package/dist/server/utils/memory-project-identity.js +17 -0
  29. package/dist/server/utils/memory-token-budget.js +226 -0
  30. package/dist/shared/codex-models.js +24 -0
  31. package/dist/shared/memory-prompts.js +17 -0
  32. package/dist/shared/memory-tools.js +215 -0
  33. package/dist/shared/memory.js +81 -0
  34. package/dist/shared/models.js +42 -0
  35. package/dist/shared/subagent-classification.js +5 -0
  36. package/package.json +1 -1
  37. package/src/client/dist/pwa/assets/ActivityFeed-CkiYsNd5.js +2 -0
  38. package/src/client/dist/pwa/assets/{ActivityFeed-BfMUdSXQ.css → ActivityFeed-DdJvMzRI.css} +1 -1
  39. package/src/client/dist/pwa/assets/ChangelogPage-DkoB2IEa.js +1 -0
  40. package/src/client/dist/pwa/assets/ClosePopup-CRV2hxHS.js +1 -0
  41. package/src/client/dist/pwa/assets/ComparisonPanel-DH9Q0XNK.js +2 -0
  42. package/src/client/dist/pwa/assets/CreatePage-Com5PQPV.js +2 -0
  43. package/src/client/dist/pwa/assets/DashboardPage-DOeZUleS.js +1 -0
  44. package/src/client/dist/pwa/assets/DiffViewer-Bke40k8T.js +8 -0
  45. package/src/client/dist/pwa/assets/DocumentsPanel-CO16-Q1X.js +1 -0
  46. package/src/client/dist/pwa/assets/{DrawerToggleButton-CQVSfKpY.js → DrawerToggleButton-fjWJjVF_.js} +1 -1
  47. package/src/client/dist/pwa/assets/GitPanel-BAgnXzXP.js +3 -0
  48. package/src/client/dist/pwa/assets/HealthPage-DR-ywiB2.js +1 -0
  49. package/src/client/dist/pwa/assets/MainLayout-B1suu_Nn.js +2 -0
  50. package/src/client/dist/pwa/assets/{MainLayout-CFLswVqP.css → MainLayout-DAcSwgUw.css} +1 -1
  51. package/src/client/dist/pwa/assets/MemoryPanel-Ch3qeyQw.css +1 -0
  52. package/src/client/dist/pwa/assets/MemoryPanel-DKp9mn5X.js +1 -0
  53. package/src/client/dist/pwa/assets/MemoryProposalList-B3v7marz.js +1 -0
  54. package/src/client/dist/pwa/assets/MemoryProposalList-OmDpl3o5.css +1 -0
  55. package/src/client/dist/pwa/assets/QBadge-CW3Lw-kL.js +1 -0
  56. package/src/client/dist/pwa/assets/QBanner-BT2ZemYu.js +1 -0
  57. package/src/client/dist/pwa/assets/QBtn-BUSdPaf4.js +1 -0
  58. package/src/client/dist/pwa/assets/QBtnDropdown-a6q0PVGm.js +1 -0
  59. package/src/client/dist/pwa/assets/QBtnGroup-CnM4vSJA.js +1 -0
  60. package/src/client/dist/pwa/assets/QBtnToggle-DccOOtbh.js +1 -0
  61. package/src/client/dist/pwa/assets/QChip-Be16M4Ig.js +1 -0
  62. package/src/client/dist/pwa/assets/QExpansionItem-2fylQBjy.js +1 -0
  63. package/src/client/dist/pwa/assets/QItemSection-MdJ1_ZK7.js +1 -0
  64. package/src/client/dist/pwa/assets/QList-Cx1UfaUm.js +1 -0
  65. package/src/client/dist/pwa/assets/QMarkupTable-UhP7fVUp.js +1 -0
  66. package/src/client/dist/pwa/assets/QPage-CqkGHokx.js +1 -0
  67. package/src/client/dist/pwa/assets/QScrollArea-I9pplcfM.js +1 -0
  68. package/src/client/dist/pwa/assets/QSelect-CWXNtFAO.js +1 -0
  69. package/src/client/dist/pwa/assets/QSpinnerDots-xdWyaE1i.js +1 -0
  70. package/src/client/dist/pwa/assets/QTooltip-fzzd3anS.js +1 -0
  71. package/src/client/dist/pwa/assets/SchedulePanel-4KNBIQvY.js +1 -0
  72. package/src/client/dist/pwa/assets/SearchPage-CGul0ui9.js +1 -0
  73. package/src/client/dist/pwa/assets/SettingsPage-C1dcMqCO.css +1 -0
  74. package/src/client/dist/pwa/assets/SettingsPage-_h4xQlof.js +9 -0
  75. package/src/client/dist/pwa/assets/SplitWorkspacePage-DxifLUAa.js +1 -0
  76. package/src/client/dist/pwa/assets/{TerminalPanel-auHWDK9i.js → TerminalPanel-CRo3ETg_.js} +10 -10
  77. package/src/client/dist/pwa/assets/ToolCallItem-i60zEGsY.js +8 -0
  78. package/src/client/dist/pwa/assets/{ToolCallItem-DhjdG8vp.css → ToolCallItem-m-YnWEbv.css} +1 -1
  79. package/src/client/dist/pwa/assets/TouchPan-DjFq7Kjf.js +1 -0
  80. package/src/client/dist/pwa/assets/{TourReplayButton-uCZpvYlj.js → TourReplayButton-DmEgJ_5R.js} +1 -1
  81. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-C7MLWyKy.js +1 -0
  82. package/src/client/dist/pwa/assets/WorkspacePage-Bw7yTZ3h.js +4 -0
  83. package/src/client/dist/pwa/assets/{browser-CU7taghA.js → browser-3unJ6AQX.js} +1 -1
  84. package/src/client/dist/pwa/assets/build-path-tree-CPo0G1mj.js +1 -0
  85. package/src/client/dist/pwa/assets/custom-sounds-C5fGFFdT.js +1 -0
  86. package/src/client/dist/pwa/assets/de-B3oUHpbN.js +17 -0
  87. package/src/client/dist/pwa/assets/detached-fullscreen-Du3TcYdX.js +1 -0
  88. package/src/client/dist/pwa/assets/dom-BtYTvLeN.js +2 -0
  89. package/src/client/dist/pwa/assets/{editor.api-CBaHnF7Q.js → editor.api-CMBx0cvf.js} +1 -1
  90. package/src/client/dist/pwa/assets/efforts-Bjrcpo2R.js +9 -0
  91. package/src/client/dist/pwa/assets/es-X1ki-Z2w.js +17 -0
  92. package/src/client/dist/pwa/assets/expand-template-cR43uBa5.js +1 -0
  93. package/src/client/dist/pwa/assets/{format-MtzcnR3z.js → format-B1k_qdxW.js} +1 -1
  94. package/src/client/dist/pwa/assets/formatters-gz0Rg7Rw.js +1 -0
  95. package/src/client/dist/pwa/assets/fr-nkT5LuOx.js +17 -0
  96. package/src/client/dist/pwa/assets/{freemarker2-BtVwKuc0.js → freemarker2-C8DHgbX4.js} +1 -1
  97. package/src/client/dist/pwa/assets/{handlebars-Bhl5yJrV.js → handlebars-BmEGWkcn.js} +1 -1
  98. package/src/client/dist/pwa/assets/{html-DKXLlkkM.js → html-giJc-yQc.js} +1 -1
  99. package/src/client/dist/pwa/assets/i18n-PAAy4Hkn.js +1 -0
  100. package/src/client/dist/pwa/assets/index-BjqJ45nb.js +20 -0
  101. package/src/client/dist/pwa/assets/it-Bfp_J8p0.js +17 -0
  102. package/src/client/dist/pwa/assets/{javascript-Cdfy_faM.js → javascript-CwQBOtjT.js} +1 -1
  103. package/src/client/dist/pwa/assets/{liquid-0NoEATfB.js → liquid-BPEVDB8V.js} +1 -1
  104. package/src/client/dist/pwa/assets/{mdx-hvP0EAwa.js → mdx-DAa4McnC.js} +1 -1
  105. package/src/client/dist/pwa/assets/{monaco-contributions-Be_o6s3m.js → monaco-contributions-Ct4RhitT.js} +1 -1
  106. package/src/client/dist/pwa/assets/monaco.contribution-BKnd0ZwH.js +2 -0
  107. package/src/client/dist/pwa/assets/network-auth-S-KP7qhA.js +1 -0
  108. package/src/client/dist/pwa/assets/{notify-theme-I_sWGwy6.js → notify-theme-DIqeoflM.js} +1 -1
  109. package/src/client/dist/pwa/assets/{permissionModes-BDi77lhg.js → permissionModes-DLWiBfdb.js} +1 -1
  110. package/src/client/dist/pwa/assets/{private.use-form-vxvbh_VN.js → private.use-form-DY1BkePL.js} +1 -1
  111. package/src/client/dist/pwa/assets/{project-color-i2bg2TfP.js → project-color-LZ5CKuTW.js} +1 -1
  112. package/src/client/dist/pwa/assets/{python-Bz-bbYMw.js → python-D8xYERv8.js} +1 -1
  113. package/src/client/dist/pwa/assets/{razor-CyafkF-1.js → razor-BnFyr836.js} +1 -1
  114. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DY1JkAEA.js +1 -0
  115. package/src/client/dist/pwa/assets/runtime-core.esm-bundler-Bqg71Vll.js +1 -0
  116. package/src/client/dist/pwa/assets/{typescript-BSLweq4l.js → typescript-DW45V56c.js} +1 -1
  117. package/src/client/dist/pwa/assets/use-checkbox-v4aAbE6K.js +1 -0
  118. package/src/client/dist/pwa/assets/use-dark-BHObfh0O.js +1 -0
  119. package/src/client/dist/pwa/assets/use-id-BrdYXce0.js +1 -0
  120. package/src/client/dist/pwa/assets/use-is-mobile-CDB_rzM9.js +1 -0
  121. package/src/client/dist/pwa/assets/use-search-index-status-DzlqDFPy.js +1 -0
  122. package/src/client/dist/pwa/assets/{use-slide-transition-C5eDGOmu.js → use-slide-transition-B_Nu6u-t.js} +1 -1
  123. package/src/client/dist/pwa/assets/use-tours-CErX5nL7.js +2 -0
  124. package/src/client/dist/pwa/assets/use-worktree-restore-BpaEGfds.js +1 -0
  125. package/src/client/dist/pwa/assets/workspace-templates-CKduJ2zA.js +1 -0
  126. package/src/client/dist/pwa/assets/{xml-DOY7F605.js → xml-gQTv5O31.js} +1 -1
  127. package/src/client/dist/pwa/assets/{yaml-CnWF0ZRJ.js → yaml-B4TEYUEa.js} +1 -1
  128. package/src/client/dist/pwa/index.html +10 -10
  129. package/src/client/dist/pwa/sw.js +1 -1
  130. package/src/mcp-server/README.md +25 -0
  131. package/src/mcp-server/kobo-tasks-server.ts +38 -1
  132. package/src/mcp-server/memory-client.ts +52 -0
  133. package/src/mcp-server/workspace-dialogue-client.ts +4 -1
  134. package/src/client/dist/pwa/assets/ActivityFeed-B8tJgXwD.js +0 -2
  135. package/src/client/dist/pwa/assets/ChangelogPage-YjWYsB_s.js +0 -1
  136. package/src/client/dist/pwa/assets/ClosePopup-C1a82pL9.js +0 -1
  137. package/src/client/dist/pwa/assets/ComparisonPanel-azVvpfGW.js +0 -2
  138. package/src/client/dist/pwa/assets/CreatePage-BVNtCTdA.js +0 -2
  139. package/src/client/dist/pwa/assets/DashboardPage-DydscFLA.js +0 -1
  140. package/src/client/dist/pwa/assets/DiffViewer-DfMqxOYw.js +0 -8
  141. package/src/client/dist/pwa/assets/DocumentsPanel-DCGgj9pc.js +0 -1
  142. package/src/client/dist/pwa/assets/GitPanel-D6ZCZPbj.js +0 -3
  143. package/src/client/dist/pwa/assets/HealthPage-D_A6DTiH.js +0 -1
  144. package/src/client/dist/pwa/assets/MainLayout-CeHg73-f.js +0 -2
  145. package/src/client/dist/pwa/assets/QBadge-rGILsUYl.js +0 -1
  146. package/src/client/dist/pwa/assets/QBanner-CtppfNDn.js +0 -1
  147. package/src/client/dist/pwa/assets/QBtn-b_baQtAt.js +0 -1
  148. package/src/client/dist/pwa/assets/QBtnDropdown-D8I04HPE.js +0 -1
  149. package/src/client/dist/pwa/assets/QBtnGroup-CUmAgBV8.js +0 -1
  150. package/src/client/dist/pwa/assets/QBtnToggle-COL46j6W.js +0 -1
  151. package/src/client/dist/pwa/assets/QChip-Btg1Lp_6.js +0 -1
  152. package/src/client/dist/pwa/assets/QExpansionItem-DmzCAbka.js +0 -1
  153. package/src/client/dist/pwa/assets/QItemSection-BhGnulVQ.js +0 -1
  154. package/src/client/dist/pwa/assets/QList-Cw2xs69W.js +0 -1
  155. package/src/client/dist/pwa/assets/QMarkupTable-Ck2E2TaI.js +0 -1
  156. package/src/client/dist/pwa/assets/QPage-BCDXjfVj.js +0 -1
  157. package/src/client/dist/pwa/assets/QScrollArea-BmBuij9J.js +0 -1
  158. package/src/client/dist/pwa/assets/QSelect-DUuWu79L.js +0 -1
  159. package/src/client/dist/pwa/assets/QSpinnerDots-BsKmccpX.js +0 -1
  160. package/src/client/dist/pwa/assets/QTooltip-Dhy3hNTO.js +0 -1
  161. package/src/client/dist/pwa/assets/SchedulePanel-BCiV6rMr.js +0 -1
  162. package/src/client/dist/pwa/assets/SearchPage-of0gPqV4.js +0 -1
  163. package/src/client/dist/pwa/assets/SettingsPage-BPg3-Lhb.js +0 -9
  164. package/src/client/dist/pwa/assets/SettingsPage-CpykRKq2.css +0 -1
  165. package/src/client/dist/pwa/assets/SplitWorkspacePage-BcdPVE9B.js +0 -1
  166. package/src/client/dist/pwa/assets/ToolCallItem-BQmqNcfF.js +0 -8
  167. package/src/client/dist/pwa/assets/TouchPan-DKpZbMAf.js +0 -1
  168. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-jDKgkf3G.js +0 -1
  169. package/src/client/dist/pwa/assets/WorkspacePage-DQ-jvLEj.js +0 -4
  170. package/src/client/dist/pwa/assets/build-path-tree-fw6KlTB4.js +0 -1
  171. package/src/client/dist/pwa/assets/custom-sounds-Dfi-grUv.js +0 -1
  172. package/src/client/dist/pwa/assets/de-C_20Oxyv.js +0 -17
  173. package/src/client/dist/pwa/assets/efforts-CNJXztO0.js +0 -9
  174. package/src/client/dist/pwa/assets/es-B3_j4CWr.js +0 -17
  175. package/src/client/dist/pwa/assets/expand-template-BtpC0GKE.js +0 -1
  176. package/src/client/dist/pwa/assets/formatters-BZm4POkA.js +0 -1
  177. package/src/client/dist/pwa/assets/fr-DbPOG-xd.js +0 -17
  178. package/src/client/dist/pwa/assets/i18n-B9eF3pgw.js +0 -1
  179. package/src/client/dist/pwa/assets/i18n-CPFleoLe.js +0 -19
  180. package/src/client/dist/pwa/assets/index-BVJWwOvK.js +0 -2
  181. package/src/client/dist/pwa/assets/it--6niM3qU.js +0 -17
  182. package/src/client/dist/pwa/assets/latest-request-7rgsEfKZ.js +0 -1
  183. package/src/client/dist/pwa/assets/monaco.contribution-DRQLCBp8.js +0 -2
  184. package/src/client/dist/pwa/assets/network-auth-DVXmkvPH.js +0 -1
  185. package/src/client/dist/pwa/assets/preload-helper-DjzSQ1t3.js +0 -1
  186. package/src/client/dist/pwa/assets/reactivity.esm-bundler-tuEvQ47f.js +0 -1
  187. package/src/client/dist/pwa/assets/runtime-core.esm-bundler-DOiZcasU.js +0 -1
  188. package/src/client/dist/pwa/assets/use-checkbox-BAt9fjK-.js +0 -1
  189. package/src/client/dist/pwa/assets/use-id-b4A2I8LB.js +0 -1
  190. package/src/client/dist/pwa/assets/use-is-mobile-DLIZxEo9.js +0 -1
  191. package/src/client/dist/pwa/assets/use-search-index-status-BzsGSHGh.js +0 -1
  192. package/src/client/dist/pwa/assets/use-tours-CM4E53aB.js +0 -2
  193. package/src/client/dist/pwa/assets/use-worktree-restore-C__ResAR.js +0 -1
  194. package/src/client/dist/pwa/assets/workspace-templates-TOa0yVyr.js +0 -1
@@ -11,6 +11,8 @@ import * as cleanupScriptService from '../cleanup-script-service.js';
11
11
  import * as cronService from '../cron-service.js';
12
12
  import { resolveForge } from '../forge/resolve.js';
13
13
  import * as lifecycleHookService from '../lifecycle-hook-service.js';
14
+ import { advanceMemoryConversationEpoch, allocateMemoryConversationKey, bindMemoryNativeConversation, createMemoryCapability, revokeMemoryCapability, } from '../memory-agent-runtime.js';
15
+ import { buildMemoryContext, markMemoryContextFailed, markMemoryContextInitialized, markMemoryContextSubmitted, } from '../memory-context-service.js';
14
16
  import * as quotaBackoffService from '../quota-backoff-service.js';
15
17
  import { buildReviewReturnPrompt, getReviewReturn, restoreReviewConfiguration } from '../review-return-service.js';
16
18
  import { activateSession, SESSION_RECENCY_ORDER } from '../session-activity-service.js';
@@ -486,7 +488,7 @@ function readEffectiveSettingsSafe(projectPath) {
486
488
  };
487
489
  }
488
490
  }
489
- function buildMcpServers(workspaceId, extraEnv) {
491
+ function buildMcpServers(workspaceId, extraEnv, memoryCapability) {
490
492
  const mcpServerCompiled = getCompiledMcpServerPath();
491
493
  const mcpServerSource = getMcpServerSourcePath();
492
494
  return [
@@ -497,6 +499,7 @@ function buildMcpServers(workspaceId, extraEnv) {
497
499
  env: {
498
500
  KOBO_WORKSPACE_ID: workspaceId,
499
501
  ...extraEnv,
502
+ ...(memoryCapability ? { KOBO_MEMORY_SESSION_TOKEN: memoryCapability } : {}),
500
503
  KOBO_DB_PATH: getDbPath(),
501
504
  KOBO_SETTINGS_PATH: getSettingsPath(),
502
505
  KOBO_BACKEND_URL: `http://127.0.0.1:${backendPort}`,
@@ -842,6 +845,13 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
842
845
  // stop — see `sourceControllerIsStopping` further down.
843
846
  if (sourceNoLongerOwnsWorkspace && ev.kind !== 'session:ended')
844
847
  return;
848
+ if (sourceController && ev.kind === 'session:started') {
849
+ const conversationKey = controllerMemoryConversationKeys.get(sourceController);
850
+ if (conversationKey)
851
+ bindMemoryNativeConversation(conversationKey, ev.engineSessionId);
852
+ }
853
+ if (sourceController && ev.kind === 'session:ended' && terminalAfterClose)
854
+ revokeControllerMemoryCapability(sourceController);
845
855
  // A controller already told to stop (D1) keeps emitting real events until it
846
856
  // actually dies — they are persisted and broadcast like any other (see
847
857
  // `routeEvent` below, unconditional). But a handful of side effects here
@@ -861,6 +871,34 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev, terminal
861
871
  if (launch?.handoffGeneration &&
862
872
  !['session:started', 'session:ended', 'session:user-input-requested'].includes(ev.kind))
863
873
  return;
874
+ if (sourceController &&
875
+ !sourceControllerIsStopping &&
876
+ controllers.get(workspaceId) === sourceController &&
877
+ pendingStarts.get(workspaceId) === undefined) {
878
+ const compaction = memoryCompactionStates.get(sourceController) ?? { active: false, compactedWithoutId: false };
879
+ if (ev.kind === 'session:compacting' && ev.active) {
880
+ compaction.active = true;
881
+ compaction.compactedWithoutId = false;
882
+ if (ev.compactionId)
883
+ compaction.activeId = ev.compactionId;
884
+ memoryCompactionStates.set(sourceController, compaction);
885
+ }
886
+ else if (ev.kind === 'session:compacted') {
887
+ const duplicateId = ev.compactionId !== undefined && ev.compactionId === compaction.lastId;
888
+ const hasDistinctSignal = ev.compactionId !== undefined ? !duplicateId : compaction.active || !compaction.compactedWithoutId;
889
+ if (hasDistinctSignal) {
890
+ const conversationKey = controllerMemoryConversationKeys.get(sourceController);
891
+ if (conversationKey)
892
+ advanceMemoryConversationEpoch(conversationKey);
893
+ if (ev.compactionId)
894
+ compaction.lastId = ev.compactionId;
895
+ compaction.compactedWithoutId = true;
896
+ }
897
+ compaction.active = false;
898
+ compaction.activeId = undefined;
899
+ memoryCompactionStates.set(sourceController, compaction);
900
+ }
901
+ }
864
902
  if (sourceController && !sourceControllerIsStopping) {
865
903
  if (ev.kind === 'session:compacting' && ev.active) {
866
904
  const status = getWs(workspaceId)?.status;
@@ -1298,6 +1336,17 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
1298
1336
  return true;
1299
1337
  }
1300
1338
  const launchOptions = new WeakMap();
1339
+ const controllerMemoryCapabilities = new WeakMap();
1340
+ const controllerMemoryConversationKeys = new WeakMap();
1341
+ const controllerMemoryContextIds = new WeakMap();
1342
+ const memoryCompactionStates = new WeakMap();
1343
+ function revokeControllerMemoryCapability(controller) {
1344
+ const token = controllerMemoryCapabilities.get(controller);
1345
+ if (!token)
1346
+ return;
1347
+ revokeMemoryCapability(token);
1348
+ controllerMemoryCapabilities.delete(controller);
1349
+ }
1301
1350
  // ── Public API ────────────────────────────────────────────────────────────────
1302
1351
  /**
1303
1352
  * Spawn an agent (via the resolved engine) for a workspace. Returns
@@ -1384,6 +1433,22 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
1384
1433
  // A review whose report returns to the original session must end by itself:
1385
1434
  // enforce read-only plan mode here, not only through its prompt.
1386
1435
  const readOnly = getReviewReturn(workspaceId, agentSessionId) !== null;
1436
+ const effectivePermissionMode = readOnly ? 'plan' : (agentPermissionMode ?? ws?.agentPermissionMode ?? 'bypass');
1437
+ const memoryDispatchId = nanoid();
1438
+ const memoryCapability = createMemoryCapability({
1439
+ dispatchId: memoryDispatchId,
1440
+ workspaceId,
1441
+ sessionId: agentSessionId,
1442
+ engine: engine.id,
1443
+ // A resume continues its prior native conversation; a fresh dispatch starts
1444
+ // a separate context even if application session selection is reused.
1445
+ conversationKey: allocateMemoryConversationKey({
1446
+ sessionId: agentSessionId,
1447
+ engine: engine.id,
1448
+ ...(resumeFromEngineSessionId ? { nativeConversationId: resumeFromEngineSessionId } : {}),
1449
+ }),
1450
+ readOnly: effectivePermissionMode === 'plan' || !!launch?.handoffGeneration,
1451
+ });
1387
1452
  const unavailableIntegrations = [];
1388
1453
  const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), engine.id, (name) => unavailableIntegrations.push(name));
1389
1454
  const options = {
@@ -1393,7 +1458,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
1393
1458
  model,
1394
1459
  effort: reasoningEffort,
1395
1460
  // Cascade: explicit caller override → workspace setting → 'bypass'.
1396
- agentPermissionMode: readOnly ? 'plan' : (agentPermissionMode ?? ws?.agentPermissionMode ?? 'bypass'),
1461
+ agentPermissionMode: effectivePermissionMode,
1397
1462
  readOnly,
1398
1463
  resumeFromEngineSessionId,
1399
1464
  backendUrl: `http://127.0.0.1:${backendPort}`,
@@ -1406,12 +1471,23 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
1406
1471
  }
1407
1472
  })(),
1408
1473
  settings,
1409
- mcpServers: [...buildMcpServers(workspaceId, launch?.mcpEnv), ...integrationServers],
1474
+ mcpServers: [...buildMcpServers(workspaceId, launch?.mcpEnv, memoryCapability.token), ...integrationServers],
1410
1475
  env: ws ? buildAgentEnv(ws.projectPath) : undefined,
1411
1476
  };
1412
- beforeDispatch?.();
1477
+ try {
1478
+ beforeDispatch?.();
1479
+ }
1480
+ catch (error) {
1481
+ revokeMemoryCapability(memoryCapability.token);
1482
+ throw error;
1483
+ }
1413
1484
  let controller;
1414
1485
  controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, controller, ev));
1486
+ controllerMemoryCapabilities.set(controller, memoryCapability.token);
1487
+ controllerMemoryConversationKeys.set(controller, memoryCapability.conversationKey);
1488
+ // This promise settles only after the controller confirms actual process
1489
+ // closure, covering direct stop() paths that do not emit session:ended.
1490
+ void controller.closed.then(() => revokeControllerMemoryCapability(controller));
1415
1491
  if (launch)
1416
1492
  launchOptions.set(controller, launch);
1417
1493
  registerSessionLifecycleOwner(workspaceId, agentSessionId, controller);
@@ -1438,11 +1514,33 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
1438
1514
  if (registered && registered !== controller)
1439
1515
  throw new Error('Agent is still stopping');
1440
1516
  controllers.set(workspaceId, controller);
1517
+ const memoryContext = launch?.handoffGeneration
1518
+ ? undefined
1519
+ : buildMemoryContext({
1520
+ workspaceId,
1521
+ sessionId: agentSessionId,
1522
+ engine: engine.id,
1523
+ dispatchId: memoryDispatchId,
1524
+ conversationKey: memoryCapability.conversationKey,
1525
+ resume: Boolean(resumeFromEngineSessionId),
1526
+ readOnly: effectivePermissionMode === 'plan' || !!launch?.handoffGeneration,
1527
+ mode: getGlobalSettings().memoryMode,
1528
+ });
1529
+ if (memoryContext) {
1530
+ controllerMemoryContextIds.set(controller, memoryContext.recordId);
1531
+ options.prompt = `${options.prompt}\n\n${memoryContext.prompt}`;
1532
+ markMemoryContextSubmitted(memoryContext.recordId);
1533
+ }
1441
1534
  return controller.start(options);
1442
1535
  })
1443
1536
  .then(async () => {
1444
- if (launch?.onStarted)
1445
- await controller.engineProcess?.ready;
1537
+ const ready = controller.engineProcess?.ready;
1538
+ const contextId = controllerMemoryContextIds.get(controller);
1539
+ if (ready && contextId) {
1540
+ void ready.then(() => markMemoryContextInitialized(contextId), () => markMemoryContextFailed(contextId));
1541
+ }
1542
+ if (launch?.onStarted && ready)
1543
+ await ready;
1446
1544
  if (controller.status !== 'stopping')
1447
1545
  launch?.onStarted?.();
1448
1546
  const pid = controller.pid;
@@ -1457,6 +1555,10 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
1457
1555
  }
1458
1556
  })
1459
1557
  .catch((err) => {
1558
+ revokeControllerMemoryCapability(controller);
1559
+ const contextId = controllerMemoryContextIds.get(controller);
1560
+ if (contextId)
1561
+ markMemoryContextFailed(contextId);
1460
1562
  if (existingCtrl && controller.status === 'stopping')
1461
1563
  return;
1462
1564
  console.error('[orchestrator] engine.start failed:', err);
@@ -1532,6 +1634,7 @@ export function stopSubagents(workspaceId, id) {
1532
1634
  /** Bound the caller's wait, retaining ownership until actual engine shutdown. */
1533
1635
  export const STOP_AGENT_TIMEOUT_MS = 15_000;
1534
1636
  async function stopController(workspaceId, ctrl, cause = 'user') {
1637
+ revokeControllerMemoryCapability(ctrl);
1535
1638
  ctrl.stopCause = cause;
1536
1639
  restoreReviewConfiguration(workspaceId, ctrl.agentSessionId);
1537
1640
  preCompactionStatus.delete(ctrl);
@@ -1615,6 +1718,7 @@ cause = 'user') {
1615
1718
  let pendingStop = Promise.resolve();
1616
1719
  if (pending) {
1617
1720
  pendingStarts.delete(workspaceId);
1721
+ revokeControllerMemoryCapability(pending);
1618
1722
  pending.stopCause = cause;
1619
1723
  pendingStop = pending.stop().then(() => {
1620
1724
  handleEvent(workspaceId, pending.agentSessionId, pending, {
@@ -0,0 +1,65 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { nanoid } from 'nanoid';
3
+ import { getDb } from '../db/index.js';
4
+ const activeCapabilities = new Map();
5
+ /** Persist an opaque conversation key independently from a controller/token generation. */
6
+ export function allocateMemoryConversationKey(input) {
7
+ const db = getDb();
8
+ if (input.nativeConversationId) {
9
+ const existing = db
10
+ .prepare(`SELECT conversation_key FROM memory_budget_contexts
11
+ WHERE kind = 'internal' AND session_id = ? AND engine = ? AND native_conversation_id = ?
12
+ ORDER BY updated_at DESC LIMIT 1`)
13
+ .get(input.sessionId, input.engine, input.nativeConversationId);
14
+ if (existing)
15
+ return existing.conversation_key;
16
+ }
17
+ const key = nanoid();
18
+ const timestamp = new Date().toISOString();
19
+ db.prepare(`INSERT INTO memory_budget_contexts
20
+ (id, kind, session_id, engine, conversation_key, native_conversation_id, epoch,
21
+ cumulative_estimated_tokens, delivered_json, created_at, updated_at)
22
+ VALUES (?, 'internal', ?, ?, ?, ?, 0, 0, '[]', ?, ?)`).run(nanoid(), input.sessionId, input.engine, key, input.nativeConversationId ?? null, timestamp, timestamp);
23
+ return key;
24
+ }
25
+ /** Bind a fresh conversation's native provider id once the engine confirms it. */
26
+ export function bindMemoryNativeConversation(key, nativeConversationId) {
27
+ const timestamp = new Date().toISOString();
28
+ getDb()
29
+ .prepare(`UPDATE memory_budget_contexts SET native_conversation_id = ?, updated_at = ?
30
+ WHERE kind = 'internal' AND conversation_key = ? AND epoch = 0 AND native_conversation_id IS NULL`)
31
+ .run(nativeConversationId, timestamp, key);
32
+ }
33
+ /** Start a new metadata-only ledger epoch after the owning controller confirms compaction. */
34
+ export function advanceMemoryConversationEpoch(key) {
35
+ const db = getDb();
36
+ const current = db
37
+ .prepare(`SELECT id, session_id, engine, native_conversation_id, epoch FROM memory_budget_contexts
38
+ WHERE kind = 'internal' AND conversation_key = ? ORDER BY epoch DESC LIMIT 1`)
39
+ .get(key);
40
+ if (!current)
41
+ return undefined;
42
+ const id = nanoid();
43
+ const epoch = current.epoch + 1;
44
+ const timestamp = new Date().toISOString();
45
+ db.prepare(`INSERT INTO memory_budget_contexts
46
+ (id, kind, session_id, engine, conversation_key, native_conversation_id, epoch,
47
+ cumulative_estimated_tokens, delivered_json, created_at, updated_at)
48
+ VALUES (?, 'internal', ?, ?, ?, ?, ?, 0, '[]', ?, ?)`).run(id, current.session_id, current.engine, key, current.native_conversation_id, epoch, timestamp, timestamp);
49
+ return { id, epoch };
50
+ }
51
+ /** Issue an unguessable, launch-specific capability. The raw value is process env only. */
52
+ export function createMemoryCapability(descriptor) {
53
+ const token = randomBytes(32).toString('hex');
54
+ activeCapabilities.set(token, Object.freeze({ ...descriptor }));
55
+ return { token, conversationKey: descriptor.conversationKey };
56
+ }
57
+ export function getMemoryCapability(token) {
58
+ if (!token || !/^[a-f0-9]{64}$/.test(token))
59
+ return undefined;
60
+ return activeCapabilities.get(token);
61
+ }
62
+ /** Revokes only this launch secret, never another generation sharing its session id. */
63
+ export function revokeMemoryCapability(token) {
64
+ activeCapabilities.delete(token);
65
+ }
@@ -0,0 +1,205 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { MEMORY_CONVERSATION_BUDGET_TOKENS } from '../../shared/memory.js';
3
+ import { getDb } from '../db/index.js';
4
+ /** Metadata only: fingerprints never retain note bodies or prompt text. */
5
+ export function getMemoryBootstrapFingerprint(budgetContextId) {
6
+ const row = getDb().prepare('SELECT delivered_json FROM memory_budget_contexts WHERE id = ?').get(budgetContextId);
7
+ if (!row)
8
+ return undefined;
9
+ try {
10
+ const records = JSON.parse(row.delivered_json);
11
+ if (!Array.isArray(records))
12
+ return undefined;
13
+ const record = records.reverse().find((item) => item?.kind === 'bootstrap');
14
+ if (typeof record?.guidanceHash === 'string' && typeof record?.snapshotHash === 'string')
15
+ return { guidanceHash: record.guidanceHash, snapshotHash: record.snapshotHash };
16
+ }
17
+ catch {
18
+ /* Legacy or malformed metadata cannot prove prior delivery. */
19
+ }
20
+ return undefined;
21
+ }
22
+ export function getInternalMemoryBudgetContext(conversationKey) {
23
+ const row = getDb()
24
+ .prepare(`SELECT id, epoch, cumulative_estimated_tokens AS cumulativeEstimatedTokens
25
+ FROM memory_budget_contexts WHERE kind = 'internal' AND conversation_key = ?
26
+ ORDER BY epoch DESC LIMIT 1`)
27
+ .get(conversationKey);
28
+ if (!row)
29
+ throw new TypeError('Internal memory budget ledger not found');
30
+ return row;
31
+ }
32
+ const MEMORY_DENIAL_RESERVE_TOKENS = 500;
33
+ const MEMORY_REGULAR_OUTPUT_LIMIT = MEMORY_CONVERSATION_BUDGET_TOKENS - MEMORY_DENIAL_RESERVE_TOKENS;
34
+ const BUDGET_DENIAL_MARKER = 'budget-denial';
35
+ function now() {
36
+ return new Date().toISOString();
37
+ }
38
+ /** Create a new external handle or resolve one previously returned to the same client/transport. */
39
+ export function createExternalMemoryBudgetContext(requestedId, clientName, transport) {
40
+ const db = getDb();
41
+ if (requestedId !== undefined) {
42
+ if (requestedId.length < 12 || requestedId.length > 128)
43
+ throw new TypeError('Invalid memory_context_id');
44
+ const row = db
45
+ .prepare(`SELECT id, kind, cumulative_estimated_tokens, client_name, transport
46
+ FROM memory_budget_contexts WHERE external_context_id = ? OR id = ?`)
47
+ .get(requestedId, requestedId);
48
+ if (row?.kind !== 'external' || row.id !== requestedId) {
49
+ throw new TypeError('Unknown external memory_context_id');
50
+ }
51
+ if (row.client_name !== clientName || row.transport !== transport) {
52
+ throw new TypeError('memory_context_id belongs to another external client context');
53
+ }
54
+ return { id: row.id, cumulativeEstimatedTokens: row.cumulative_estimated_tokens };
55
+ }
56
+ const id = nanoid(32);
57
+ const timestamp = now();
58
+ db.prepare(`INSERT INTO memory_budget_contexts
59
+ (id, kind, external_context_id, epoch, cumulative_estimated_tokens, delivered_json, client_name, transport, created_at, updated_at)
60
+ VALUES (?, 'external', ?, 0, 0, '[]', ?, ?, ?, ?)`).run(id, id, clientName.slice(0, 120), transport, timestamp, timestamp);
61
+ return { id, cumulativeEstimatedTokens: 0 };
62
+ }
63
+ /**
64
+ * Charges a native-conversation bootstrap or an MCP response using an immediate
65
+ * SQLite transaction, so concurrent sub-agents cannot spend the same allowance.
66
+ * Unknown/failed delivery is intentionally still charged.
67
+ */
68
+ export function reserveMemoryBudget(input) {
69
+ if (!Number.isSafeInteger(input.estimatedTokens) || input.estimatedTokens < 1)
70
+ throw new TypeError('Memory budget charge must be a positive integer');
71
+ const responseLimitTokens = input.responseLimitTokens;
72
+ if (responseLimitTokens !== undefined && input.estimatedTokens > responseLimitTokens) {
73
+ return {
74
+ accepted: false,
75
+ budgetContextId: input.budgetContextId,
76
+ estimatedTokens: input.estimatedTokens,
77
+ remainingTokens: 0,
78
+ limitTokens: responseLimitTokens,
79
+ reason: 'response-limit',
80
+ };
81
+ }
82
+ const db = getDb();
83
+ return db
84
+ .transaction(() => {
85
+ const row = db
86
+ .prepare(`SELECT id, kind, cumulative_estimated_tokens, delivered_json
87
+ FROM memory_budget_contexts WHERE id = ?`)
88
+ .get(input.budgetContextId);
89
+ if (!row)
90
+ throw new TypeError('Memory budget ledger not found');
91
+ let delivered;
92
+ try {
93
+ const parsed = JSON.parse(row.delivered_json);
94
+ delivered = Array.isArray(parsed) ? parsed : [];
95
+ }
96
+ catch {
97
+ delivered = [];
98
+ }
99
+ const denialAlreadyUsed = delivered.some((item) => item && typeof item === 'object' && item.kind === BUDGET_DENIAL_MARKER);
100
+ const remaining = denialAlreadyUsed
101
+ ? 0
102
+ : Math.max(0, MEMORY_REGULAR_OUTPUT_LIMIT - row.cumulative_estimated_tokens);
103
+ if (input.estimatedTokens > remaining) {
104
+ return {
105
+ accepted: false,
106
+ budgetContextId: row.id,
107
+ estimatedTokens: input.estimatedTokens,
108
+ remainingTokens: remaining,
109
+ limitTokens: MEMORY_REGULAR_OUTPUT_LIMIT,
110
+ reason: 'conversation-limit',
111
+ };
112
+ }
113
+ const newDeliveries = [...(input.deliveries ?? []), ...(input.delivery ? [input.delivery] : [])];
114
+ if (input.bootstrap) {
115
+ delivered = delivered.filter((item) => !item || typeof item !== 'object' || item.kind !== 'bootstrap');
116
+ delivered.push({ kind: 'bootstrap', ...input.bootstrap });
117
+ }
118
+ if (newDeliveries.length > 0) {
119
+ delivered.push(...newDeliveries);
120
+ if (delivered.length > 2_000)
121
+ delivered = delivered.slice(-2_000);
122
+ }
123
+ const cumulative = row.cumulative_estimated_tokens + input.estimatedTokens;
124
+ db.prepare(`UPDATE memory_budget_contexts SET cumulative_estimated_tokens = ?, delivered_json = ?, updated_at = ?
125
+ WHERE id = ?`).run(cumulative, JSON.stringify(delivered), now(), row.id);
126
+ return {
127
+ accepted: true,
128
+ budgetContextId: row.id,
129
+ estimatedTokens: input.estimatedTokens,
130
+ remainingTokens: MEMORY_REGULAR_OUTPUT_LIMIT - cumulative,
131
+ limitTokens: MEMORY_REGULAR_OUTPUT_LIMIT,
132
+ };
133
+ })
134
+ .immediate();
135
+ }
136
+ /** Spend one reserved terminal receipt; later exhausted calls are suppressed without output. */
137
+ export function reserveMemoryBudgetDenial(budgetContextId, estimatedTokens) {
138
+ if (!Number.isSafeInteger(estimatedTokens) || estimatedTokens < 1 || estimatedTokens > MEMORY_DENIAL_RESERVE_TOKENS)
139
+ throw new TypeError('Memory budget denial receipt exceeds its reserved allowance');
140
+ const db = getDb();
141
+ return db
142
+ .transaction(() => {
143
+ const row = db
144
+ .prepare(`SELECT id, cumulative_estimated_tokens, delivered_json
145
+ FROM memory_budget_contexts WHERE id = ?`)
146
+ .get(budgetContextId);
147
+ if (!row)
148
+ throw new TypeError('Memory budget ledger not found');
149
+ let delivered;
150
+ try {
151
+ const parsed = JSON.parse(row.delivered_json);
152
+ delivered = Array.isArray(parsed) ? parsed : [];
153
+ }
154
+ catch {
155
+ delivered = [];
156
+ }
157
+ const alreadyUsed = delivered.some((item) => item && typeof item === 'object' && item.kind === BUDGET_DENIAL_MARKER);
158
+ if (alreadyUsed || row.cumulative_estimated_tokens + estimatedTokens > MEMORY_CONVERSATION_BUDGET_TOKENS) {
159
+ return {
160
+ responseAllowed: false,
161
+ budgetContextId: row.id,
162
+ estimatedTokens: 0,
163
+ remainingTokens: Math.max(0, MEMORY_CONVERSATION_BUDGET_TOKENS - row.cumulative_estimated_tokens),
164
+ limitTokens: MEMORY_CONVERSATION_BUDGET_TOKENS,
165
+ };
166
+ }
167
+ delivered.push({ kind: BUDGET_DENIAL_MARKER, estimatedTokens });
168
+ const cumulative = row.cumulative_estimated_tokens + estimatedTokens;
169
+ db.prepare(`UPDATE memory_budget_contexts
170
+ SET cumulative_estimated_tokens = ?, delivered_json = ?, updated_at = ? WHERE id = ?`).run(cumulative, JSON.stringify(delivered), now(), row.id);
171
+ return {
172
+ responseAllowed: true,
173
+ budgetContextId: row.id,
174
+ estimatedTokens,
175
+ remainingTokens: MEMORY_CONVERSATION_BUDGET_TOKENS - cumulative,
176
+ limitTokens: MEMORY_CONVERSATION_BUDGET_TOKENS,
177
+ };
178
+ })
179
+ .immediate();
180
+ }
181
+ /** Returns delivery metadata only; persisted note bodies are never stored in this ledger. */
182
+ export function wasMemoryFragmentDelivered(budgetContextId, delivery) {
183
+ const row = getDb()
184
+ .prepare('SELECT kind, delivered_json FROM memory_budget_contexts WHERE id = ?')
185
+ .get(budgetContextId);
186
+ if (!row)
187
+ return false;
188
+ try {
189
+ const items = JSON.parse(row.delivered_json);
190
+ return (Array.isArray(items) &&
191
+ items.some((item) => {
192
+ if (!item || typeof item !== 'object')
193
+ return false;
194
+ const candidate = item;
195
+ return (candidate.entryId === delivery.entryId &&
196
+ candidate.revision === delivery.revision &&
197
+ candidate.kind === delivery.kind &&
198
+ candidate.start === delivery.start &&
199
+ candidate.end === delivery.end);
200
+ }));
201
+ }
202
+ catch {
203
+ return false;
204
+ }
205
+ }