@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
package/AGENTS.md CHANGED
@@ -127,6 +127,11 @@ src/
127
127
  | `schema_migrations` | applied migration versions, names and timestamps |
128
128
  | `workspace_chat_history` | chat-input history per workspace: message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
129
129
  | `workspace_permission_rules` | remembered per-workspace tool approvals, scoped to an exact operation or every invocation of a tool; CASCADE DELETE on workspace |
130
+ | `memory_scopes` / `memory_entries` | global, normalized-project and workspace scopes; active explicit facts with revision and provenance |
131
+ | `memory_proposals` | pending Hybrid project/global writes; body retained only while pending and removed on approval/rejection/clear |
132
+ | `memory_operations` | paginated content-free mutation/read journal with actor, source and affected-count metadata |
133
+ | `memory_contexts` | per-launch receipts with dispatch state, revision refs, omissions and budget metadata; CASCADE DELETE on workspace/session |
134
+ | `memory_budget_contexts` | cumulative native conversation/epoch and external cooperative-context ledgers, including delivered refs and conservative charges |
130
135
 
131
136
  `status` enum: `created | extracting | brainstorming | executing | compacting | awaiting-user | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
132
137
 
@@ -140,6 +145,16 @@ src/
140
145
 
141
146
  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
147
 
148
+ ## Persistent memory
149
+
150
+ Kōbō memory is independent of provider chat history/native memory and shared by Claude Code and Codex. `memoryMode` is a global setting defaulting to `hybrid`: Manual permits reads only; Hybrid applies workspace writes and proposes project/global writes; Automatic applies writes to the explicitly targeted scope. Mode is checked per operation, so a setting change affects existing agent sessions; pending/rejected proposals are never automatically approved or revived. Plan/review/report-only capabilities cannot write.
151
+
152
+ `memory_scopes` identify global, normalized configured project path, or durable workspace id. Project identity is not a display name, branch, or worktree path. Clear is transactional: it deletes entries and pending proposals only from the exact scope, increments generation, and writes content-free journal metadata. Every delayed writer supplies its expected generation. Workspace archive/purge preserves memory; workspace deletion cascades its own scope and launch records while leaving global/project scopes intact. Provenance FKs become null when the source session/workspace is deleted; saved memory remains in its scope until explicitly erased.
153
+
154
+ Agents deliberately save knowledge through shared internal MCP tools; there is no background transcript extraction or extra LLM turn. Workspace-bound tool calls derive actor, scope and write permission from a per-launch capability. It is revoked when stopping begins; bodies cannot forge human identity or widen workspace scope. External clients use the same service through `/api/mcp` HTTP or global stdio, explicitly select scopes, and receive `external-mcp` provenance (display name + transport). They need no native session. They cannot delete, approve, promote or bulk-clear memory.
155
+
156
+ Context injection and every memory-tool response are bounded independently of provider window telemetry. Bootstrap targets 1,000 estimated tokens; normal responses target 1,000 with a hard 1,500-token cap; cumulative native conversation/context epoch retrieval output is capped at 6,000 estimated tokens. Retrieval output stops at 5,500 to preserve a single 500-token terminal denial reserve; later exhausted MCP reads return `isError: true` and empty `content`. Final serialized envelopes are guarded by a 12,000-byte ceiling and 96-byte framing headroom, including escaped and structured MCP duplication. These conservative estimates are not full conversation occupancy. Scope catalogues include generation/revision for CAS writes. Listing/search defaults to compact metadata and explicit body fragments are charged by range; repeated unchanged content is not blindly resent on resume. A confirmed current-controller compaction opens a new budget epoch. Failed/ambiguous delivery is charged conservatively and never replayed automatically. External stateless callers should reuse opaque external `memory_context_id`; it is not authentication and never addresses an internal ledger. See `src/shared/memory-tools.ts`, `services/memory-context-service.ts`, `services/memory-budget-service.ts`, and [CONFIGURATION.md](./CONFIGURATION.md#persistent-memory). Saving (`remember`) and scope discovery (`list_memory_scopes`) are independent of this retrieval budget, including in already-exhausted sessions. Their body-free control replies/errors stay capped at 1,000 estimated tokens per call, with `budget.chargedTokens: 0`; they neither charge nor reset the retrieval ledger. Repeated control replies still occupy model context and must not be polled. Existing permissions, mode and CAS checks remain mandatory.
157
+
143
158
  `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.
144
159
 
145
160
  `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.
@@ -177,10 +192,13 @@ Every feature that touches the schema:
177
192
 
178
193
  - **Run migrations on every backend start.** `src/server/index.ts` opens the connection with `getDb()`, checks for pending migrations, attempts a pre-migration backup, then calls `runMigrations(db)`. `getDb()` only opens/configures SQLite; it does not initialize or migrate the schema. Tests creating their own database must initialize it explicitly.
179
194
  - **Settings migrations are separate.** The JSON settings migration versions in `settings-service.ts` are independent of SQLite's `SCHEMA_VERSION`.
195
+ - **Memory is migration v51** (`persistent-memory-schema`). Keep `initMemorySchema` in `src/server/db/memory-schema.ts` synchronized with fresh-install schema. `memoryMode` is a JSON setting and defaults to Hybrid for missing/invalid legacy values; it is not a SQLite column.
180
196
  - **Test upgrades, not just fresh installs.** The `migrations.test.ts` suite must exercise "old DB → new DB" paths.
181
197
 
182
198
  ## WebSocket protocol
183
199
 
200
+ Claude SDK `task_*` messages cover agents **and** background jobs. Preserve `task_type` by task/tool identity across progress and terminal messages, which often omit it. The existing `subagent:progress` envelope also carries this lifecycle metadata for compatibility; `local_bash`, `local_workflow` and `mcp_task` must be excluded from agent cards, busy-agent counts, chat agent-name resolution and the selective stop-subagents action. Keep tracking these jobs for engine liveness and whole-session interruption. Legacy/unknown classifications and Codex agents remain supported; never classify from a description or from the agent's last tool (`Bash` can be a real agent's current action).
201
+
184
202
  ### External MCP dialogue and update checks
185
203
 
186
204
  `POST /api/mcp` exposes stateless Streamable HTTP discovery/dialogue tools, behind the existing Host/Origin and network-token gates. The global stdio server forwards dialogue calls through this backend so agent runtime ownership remains centralized. `workspace-message-service.ts` shares delivery with WebSocket chat.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ 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
+ ## 2.1.10
8
+
9
+ - fix: correct memory budgets and subagent detection
10
+
11
+ ## 2.1.9
12
+
13
+ - fix: resolve secret scan false positive in memory fixture
14
+
15
+ ## 2.1.8
16
+
17
+ - feat: add persistent scoped memory with MCP access and management UI
18
+ - fix: group subagent activity by task
19
+
7
20
  ## 2.1.7
8
21
 
9
22
  - feat: improve workspace agent visibility
package/CONFIGURATION.md CHANGED
@@ -23,6 +23,7 @@ Reference for Kōbō configuration and external integrations. The tables cover t
23
23
  - [Network access](#network-access)
24
24
  - [Reachable addresses](#reachable-addresses)
25
25
  - [External LLM access through MCP](#external-llm-access-through-mcp)
26
+ - [Persistent memory](#persistent-memory)
26
27
  - [Kōbō update checks](#kōbō-update-checks)
27
28
  - [Docker deployment](#docker-deployment)
28
29
  - [Choosing a compose file](#choosing-a-compose-file)
@@ -134,7 +135,7 @@ settings exports.
134
135
  | Key | Type | Purpose |
135
136
  |---|---|---|
136
137
  | `defaultModel` | `string` | Fallback model when no engine-specific default is set. |
137
- | `defaultModelByEngine` | `Record<engine, string>` | Per-engine default model (`claude-code` / `codex`). Fresh installs use `auto` for both engines: the runtime chooses its default model. Historical migrations and existing user choices are preserved. The actual model is reported by the engine when the session starts; an Auto configuration label is not a model name. |
138
+ | `defaultModelByEngine` | `Record<engine, string>` | Per-engine default model (`claude-code` / `codex`). Fresh installs use Sonnet 5.5 (`claude-sonnet-5-5`) for Claude Code and `auto` for Codex. Historical migrations and existing user choices are preserved. The actual model is reported by the engine when the session starts; an Auto configuration label is not a model name. |
138
139
  | `workflowPolicy` | `{ commit, push, publish }` | Independent `manual` / `automatic` preferences, snapshotted at workspace creation. Fresh defaults are manual. See [Git preferences](#first-run-defaults-and-git-workflow-preferences). |
139
140
  | `skillSuite` | `string` | Standard, an external suite/combination, or custom instructions. Fresh default `standard`; see [Skill suites](#skill-suites). |
140
141
  | `defaultPermissionModeByEngine` | `Record<engine, mode>` | Default permission mode per engine. See [Permission modes](#permission-modes). |
@@ -804,6 +805,48 @@ The **External LLM access (MCP)** panel within global Network access settings pr
804
805
 
805
806
  External senders can supply `idempotency_key` once per intended message and reuse it on retries. The durable receipt survives backend restarts and event retention. An uncertain delivery is explicitly reported and never automatically sent again; see the MCP guide for response codes and limitations.
806
807
 
808
+ ## Persistent memory
809
+
810
+ Kōbō stores deliberate, concise facts in SQLite, independently of Claude Code's native memory and provider chat history. Claude Code and Codex read the same Kōbō scopes, so a fact saved in one engine can be available to the other. There is no background transcript extractor or extra LLM call: agents must deliberately use the memory tools to save useful knowledge.
811
+
812
+ Choose the global behavior in **Settings → Memory**. New and upgraded installations default to **Hybrid**:
813
+
814
+ | Mode | Agent reads | Agent writes |
815
+ |---|---|---|
816
+ | Manual | Reads applicable notes | No writes or proposals |
817
+ | Hybrid | Reads applicable notes | Applies workspace notes; proposes project/global changes for human review |
818
+ | Automatic | Reads applicable notes | Applies changes to the explicitly selected scope |
819
+
820
+ The setting is global, and is checked on every operation; changing it does not delete data, approve pending proposals, or revive rejected proposals. Read-only review and handoff-report launches cannot write even in Automatic mode. Human CRUD, proposal decisions, promotion and clear remain explicit actions.
821
+
822
+ Memory is isolated at three levels:
823
+
824
+ - **Global**: available to all workspaces.
825
+ - **Project**: shared by workspaces using the same normalized configured repository path (not branch, display name or worktree path).
826
+ - **Workspace**: available across that workspace's sessions and retained through archive/worktree purge.
827
+
828
+ Settings can browse and clear each stored scope independently. The workspace drawer shows applicable entries, current-session delivery records, pending proposals and the operation journal; its clear action only clears that workspace scope. Clear removes active entries and proposals in that exact scope, increments its generation, and prevents delayed writes carrying the previous generation. Deleting a workspace deletes its workspace scope and launch records but retains global/project memory; project scopes remain selectable even if no workspace currently uses that path. The journal keeps bounded action/provenance metadata, not deleted note bodies. Clearing memory cannot retract text already sent to an agent or erase provider conversation history.
829
+
830
+ Hybrid changes to project/global memory are proposals until a human approves them. Approval uses revision/generation checks; stale proposals remain reviewable and cannot silently overwrite concurrent edits. Promotion copies a note to a wider scope and preserves the source; conflicts are surfaced rather than overwritten. Changing modes never auto-approves pending proposals.
831
+
832
+ ### Bounded context and retrieval
833
+
834
+ The workspace creation prompt explicitly introduces the memory tools and their scope/mode restrictions. Unchanged resumes send neither repeated guidance nor an empty memory section and consume no bootstrap budget. Content-free fingerprints track guidance and scope/revision changes; a mode/permission change refreshes guidance, and confirmed compaction opens a new epoch. A new user message alone does **not** reset the budget. Previously consumed budget is not refunded. Empty tool errors receive a UI-only explanation without adding repeated text to the model's context.
835
+
836
+ At launch, Kōbō adds a compact ranked index/excerpt within a **1,000 estimated-token** bootstrap target. Individual memory tool responses target **1,000 estimated tokens**, hard-cap at **1,500**, and a native conversation/context epoch has a **6,000 estimated-token** cumulative retrieval-output ceiling. These are conservative memory budgets, not provider context-window sizes or a measurement of total conversation occupancy. Serialized output—including JSON escaping, structured MCP content and framing—is also guarded by byte limits.
837
+
838
+ Saving is independent of retrieval: `remember` and `list_memory_scopes` remain available even in an existing conversation whose retrieval budget is exhausted. Their responses contain only compact receipts/scope metadata, never saved bodies, are capped at 1,000 estimated tokens per call, and report `budget.chargedTokens: 0`. They do not spend or reset the retrieval ledger. This also applies to their validation errors and to external MCP. Manual/read-only restrictions, Hybrid proposals, scope permissions, revocation and revision/generation checks still apply. These control replies are **not** covered by the cumulative retrieval ceiling; repeated calls still add small messages to the model context, so agents should not poll or repeat unchanged writes.
839
+
840
+ The index may omit entries; it reports omission counts and agents can retrieve relevant notes with bounded search/read tools. Large note bodies use explicit character-range fragments and stable cursors. Listing/search returns compact metadata for unchanged entries; explicit full reads/fragments consume budget. Resume does not blindly resend unchanged notes: only new/changed material and a compact reminder may be added within the remaining allowance. A confirmed compaction starts a new memory epoch. Unknown or failed dispatches are charged conservatively; Kōbō does not automatically replay them.
841
+
842
+ The native conversation ledger allows up to **5,500 tokens of regular retrieval output** and retains a **500-token terminal-receipt reserve**, for a hard **6,000-token cumulative retrieval** ceiling. One bounded exhaustion/denial response may use that reserve; later exhausted MCP reads return `isError: true` with an empty `content` array instead of repeating a free error indefinitely. Internal `/api/memory/agent` callers receive a small suppression marker so the workspace bridge can emit the same empty MCP error response. Each final serialized envelope also has a separate **12,000-byte hard ceiling** and includes **96 bytes of framing headroom** in its charge.
843
+
844
+ An external MCP client uses the same memory tools and current database via `/api/mcp` HTTP or the global stdio server. `list_memory_scopes` returns each selectable scope's ID, level, generation and revision; use the current generation in `remember` and rediscover after a clear/conflict. External clients choose their target explicitly and do not need a bound workspace or native engine session. In Manual mode external writes are denied; Hybrid applies workspace writes and proposes broader changes; Automatic applies the requested scope. External provenance records a display client name and HTTP/stdio transport, not an authenticated identity. No delete, approve or clear tool is exposed to MCP clients.
845
+
846
+ Because `/api/mcp` is stateless, an external client should reuse the opaque `memory_context_id` returned by memory calls to carry its cumulative retrieval allowance across tool calls. External retrieval responses share the 5,500-token allowance and a single bounded 500-token denial reserve; after the one terminal denial, later reads return `isError: true` and empty `content`. Scope discovery and saving remain available with bounded control replies. The handle is budget bookkeeping, not authentication; it cannot reference an internal native-conversation ledger. Kōbō cannot observe an external model's full context or compaction, so it cannot infer that a new ID means an empty LLM context. Keep the same ID for one external conversation and start a new one only when appropriate. Mutation calls are not retried automatically; after an uncertain result, inspect the receipt/state before deciding whether to retry.
847
+
848
+ The workspace-scoped MCP capability is read-only for plan/review/report launches and is revoked as soon as stopping begins. Its token limits attribution and scope within Kōbō; it is not an operating-system sandbox for an agent with broader tools. See the [MCP memory tool guide](./src/mcp-server/README.md#persistent-memory-tools) for arguments and result details.
849
+
807
850
  ## Kōbō update checks
808
851
 
809
852
  The backend checks the package's npm `latest` release at startup and then every **20 minutes**, even with no browser open. All connected interfaces receive the result through the existing update banner, without reloading the page. Reconnecting clients retrieve the current snapshot.
package/DESIGN.md CHANGED
@@ -221,7 +221,7 @@ The old three-tab redesign description is no longer the implementation baseline.
221
221
 
222
222
  - Section membership is declared by `navItems` in `SettingsPage.vue`: General,
223
223
  Agents, Skills, Git, Prompts, Scripts, Notion, Sentry, Forge, Voice, Notifications,
224
- Worktrees, Projects, prompt Templates, Workspace templates, and Export.
224
+ Worktrees, Memory, Projects, prompt Templates, Workspace templates, and Export.
225
225
  - Git groups workflow preferences, conventions and branch prefixes.
226
226
  Prompts keeps post-PR/MR, review, CI-fix and finalization instructions. Project overrides use
227
227
  the same Git grouping. Retry limits belong to Agents and reminders to Notifications.
@@ -234,6 +234,16 @@ The old three-tab redesign description is no longer the implementation baseline.
234
234
  and contrast; they complement browser review rather than proving every screen
235
235
  follows this guide.
236
236
 
237
+ ## Memory surfaces
238
+
239
+ Memory has two entry points in the existing settings/drawer navigation, not a separate application shell:
240
+
241
+ - **Settings → Memory** owns the global mode draft/save interaction and the full scope catalogue. Scope selection, CRUD, proposal review and destructive clears are explicit; clearing one scope never implies clearing its parents or children.
242
+ - The workspace right drawer has a **Memory** tab, lazy-loaded like other heavy panels. It is keyed to the selected workspace and current conversation (activation order, with legacy timestamp fallback), not the newest-created session. It shows the memory context that was actually recorded as delivered, including state, budget, omissions, and revision-current text only. Changed/deleted facts are placeholders, never historical body snapshots.
243
+ - The workspace panel keeps the journal and entries readable in the existing narrow/mobile drawer; long IDs, note text and provenance wrap instead of forcing a wider canvas. Archived or purged workspaces retain access to memory controls. The workspace clear affordance names only that scope and links to Settings for global/project management.
244
+
245
+ The shared-memory policy is intentional: Hybrid is the default; workspace facts can be applied while project/global changes await human approval. Proposals and entries use the same compact, flat rows and subtle dividers as other operational panels. Destructive confirmation names the exact entry/scope or clear counts. Memory controls never imply deletion of provider chat history, and a scope clear cannot promise retraction from text already transmitted to an engine.
246
+
237
247
  ## CSS Variables
238
248
 
239
249
  The following core tokens mirror `src/client/src/css/design-tokens.scss`.
package/README.md CHANGED
@@ -66,6 +66,10 @@ Kōbō's production build is an installable PWA — use your browser's **Install
66
66
  - **Observability**: a per-session timeline (duration, tools, tokens, errors) and a downloadable redacted diagnostic JSON.
67
67
  - **Optional integrations**: Notion (import missions) and Sentry (fix from issue URL), usable by both agent engines through saved connections, each independently toggled with a **Test connection** action; local voice transcription via `whisper.cpp`.
68
68
 
69
+ ### Persistent memory across engines
70
+
71
+ Kōbō offers **Manual**, **Hybrid** (the default), and **Automatic** memory behavior in **Settings → Memory**. Global, project, and workspace scopes are shared by Claude Code and Codex but remain independently manageable. Hybrid applies workspace notes while asking for human approval before shared project/global changes. The workspace's right-hand **Memory** tab shows the current conversation's delivered context, pending proposals, and a provenance-aware operation journal. Memory has conservative per-response and per-conversation budgets; it cannot retract facts already transmitted into an agent or provider conversation. See the [persistent memory guide](./CONFIGURATION.md#persistent-memory) and [MCP memory tools](./src/mcp-server/README.md#persistent-memory-tools).
72
+
69
73
  ## Fresh sessions and LLM handoffs
70
74
 
71
75
  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.
@@ -3,8 +3,10 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { getDb } from '../server/db/index.js';
6
+ import { EXTERNAL_MEMORY_TOOL_DEFINITIONS, isMemoryToolName, MEMORY_TOOL_DEFINITIONS } from '../shared/memory-tools.js';
6
7
  import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js';
7
8
  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';
9
+ import { callMemoryTool, isMemoryOutputSuppressed } from './memory-client.js';
8
10
  import { callWorkspaceDialogueTool } from './workspace-dialogue-client.js';
9
11
  const handoffId = process.env.KOBO_HANDOFF_ID;
10
12
  const handoffToken = process.env.KOBO_HANDOFF_TOKEN;
@@ -531,9 +533,11 @@ const WORKSPACE_SCOPED_TOOLS = [
531
533
  inputSchema: { type: 'object', properties: {}, required: [] },
532
534
  annotations: { destructiveHint: false, openWorldHint: false },
533
535
  },
536
+ ...MEMORY_TOOL_DEFINITIONS,
534
537
  ];
535
538
  const GLOBAL_TOOLS = [
536
539
  ...WORKSPACE_DIALOGUE_TOOLS,
540
+ ...EXTERNAL_MEMORY_TOOL_DEFINITIONS,
537
541
  {
538
542
  name: 'list_workspaces',
539
543
  description: 'List Kōbō workspaces with their id, title, status, tags, and creation date. Set tag to return only workspaces carrying that exact tag. Works even when the Kōbō backend server is not running (reads the database directly). Use this to discover existing workspaces before creating one or targeting one with archive_workspace/stop_workspace.',
@@ -632,7 +636,11 @@ const GLOBAL_TOOLS = [
632
636
  */
633
637
  const GLOBAL_TOOL_NAMES = new Set(GLOBAL_TOOLS.map((t) => t.name));
634
638
  function availableTools() {
635
- const tools = workspaceId ? [...WORKSPACE_SCOPED_TOOLS, ...GLOBAL_TOOLS] : GLOBAL_TOOLS;
639
+ // A workspace process exposes the capability-bound memory tools exactly once.
640
+ // Unbound/global stdio exposes the external bridge variant instead.
641
+ const tools = workspaceId
642
+ ? [...WORKSPACE_SCOPED_TOOLS, ...GLOBAL_TOOLS.filter((tool) => !isMemoryToolName(tool.name))]
643
+ : GLOBAL_TOOLS;
636
644
  return tools.filter((tool) => handoffId && handoffToken
637
645
  ? tool.name === 'submit_session_handoff' || tool.annotations?.readOnlyHint === true
638
646
  : tool.name !== 'submit_session_handoff');
@@ -677,6 +685,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
677
685
  report: a.report,
678
686
  }));
679
687
  }
688
+ if (isMemoryToolName(name)) {
689
+ if (!workspaceId) {
690
+ return await callWorkspaceDialogueTool(backendUrl, name, a, process.env.KOBO_NETWORK_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
691
+ }
692
+ // Workspace-bound memory is always dispatched to its launch-scoped capability.
693
+ // It must never fall through to the unrestricted external/global bridge.
694
+ if (!process.env.KOBO_MEMORY_SESSION_TOKEN)
695
+ return fail('Memory tools require the current workspace launch capability.');
696
+ const memoryResult = await callMemoryTool({
697
+ backendUrl,
698
+ name,
699
+ args: a,
700
+ capability: process.env.KOBO_MEMORY_SESSION_TOKEN,
701
+ networkToken: process.env.KOBO_NETWORK_TOKEN,
702
+ });
703
+ if (isMemoryOutputSuppressed(memoryResult))
704
+ return { content: [], isError: true };
705
+ return {
706
+ ...ok(memoryResult),
707
+ ...(memoryResult && typeof memoryResult === 'object' && 'budgetExhausted' in memoryResult
708
+ ? { isError: true }
709
+ : {}),
710
+ };
711
+ }
680
712
  if (WORKSPACE_DIALOGUE_TOOLS.some((tool) => tool.name === name)) {
681
713
  if (workspaceId && a.workspace_id === workspaceId && name === 'send_workspace_message')
682
714
  return fail('Use the normal conversation to communicate in your own workspace.');
@@ -0,0 +1,39 @@
1
+ import { isMemoryToolName, validateMemoryToolArguments } from '../shared/memory-tools.js';
2
+ export function isMemoryOutputSuppressed(value) {
3
+ return (!!value &&
4
+ typeof value === 'object' &&
5
+ !Array.isArray(value) &&
6
+ 'memoryOutputSuppressed' in value &&
7
+ value.memoryOutputSuppressed === true);
8
+ }
9
+ export async function callMemoryTool(input) {
10
+ if (!isMemoryToolName(input.name))
11
+ throw new TypeError('Unknown memory tool');
12
+ if (!input.capability)
13
+ throw new Error('Memory capability is unavailable for this process');
14
+ const args = validateMemoryToolArguments(input.name, input.args);
15
+ const headers = {
16
+ 'Content-Type': 'application/json',
17
+ 'X-Kobo-Memory-Session': input.capability,
18
+ };
19
+ if (input.networkToken)
20
+ headers['X-Kobo-Token'] = input.networkToken;
21
+ const timeoutMs = input.timeoutMs ?? 30_000;
22
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
23
+ throw new TypeError('Memory request timeout must be positive');
24
+ const signal = AbortSignal.timeout(timeoutMs);
25
+ const response = await (input.fetcher ?? fetch)(`${input.backendUrl.replace(/\/$/, '')}/api/memory/agent/${input.name}`, { method: 'POST', headers, body: JSON.stringify(args), signal });
26
+ const raw = await response.text();
27
+ let payload;
28
+ try {
29
+ payload = raw ? JSON.parse(raw) : null;
30
+ }
31
+ catch {
32
+ payload = { error: 'Backend returned invalid JSON' };
33
+ }
34
+ if (!response.ok) {
35
+ const detail = payload && typeof payload === 'object' && 'error' in payload ? String(payload.error) : response.statusText;
36
+ throw new Error(`Memory backend returned ${response.status}: ${detail}`);
37
+ }
38
+ return payload;
39
+ }
@@ -1,10 +1,13 @@
1
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { isMemoryToolName, validateMemoryToolArguments } from '../shared/memory-tools.js';
3
4
  import { validateDialogueArguments } from '../shared/workspace-dialogue-tools.js';
4
5
  import { normalizeClientName } from '../shared/workspace-message-types.js';
5
6
  /** Stateless HTTP bridge: external stdio clients never own agent runtime state. */
6
7
  export async function callWorkspaceDialogueTool(backendUrl, name, args, token, clientName) {
7
- const arguments_ = validateDialogueArguments(name, args);
8
+ const arguments_ = isMemoryToolName(name)
9
+ ? validateMemoryToolArguments(name, args, true)
10
+ : validateDialogueArguments(name, args);
8
11
  const client = new Client({ name: 'kobo-stdio-bridge', version: '1.0.0' });
9
12
  const signal = AbortSignal.timeout(30_000);
10
13
  const transport = new StreamableHTTPClientTransport(new URL(`${backendUrl.replace(/\/$/, '')}/api/mcp`), {
@@ -0,0 +1,185 @@
1
+ /** Shared by fresh installs and the append-only v51 upgrade. */
2
+ export function initMemorySchema(db) {
3
+ db.exec(`
4
+ CREATE TABLE IF NOT EXISTS memory_scopes (
5
+ id TEXT PRIMARY KEY,
6
+ level TEXT NOT NULL CHECK (level IN ('global', 'project', 'workspace')),
7
+ project_path TEXT,
8
+ workspace_id TEXT REFERENCES workspaces(id) ON DELETE CASCADE,
9
+ generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0),
10
+ revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
11
+ created_at TEXT NOT NULL,
12
+ updated_at TEXT NOT NULL,
13
+ CHECK (
14
+ (level = 'global' AND project_path IS NULL AND workspace_id IS NULL) OR
15
+ (level = 'project' AND project_path IS NOT NULL AND workspace_id IS NULL) OR
16
+ (level = 'workspace' AND project_path IS NULL AND workspace_id IS NOT NULL)
17
+ )
18
+ );
19
+
20
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_scopes_global
21
+ ON memory_scopes(level) WHERE level = 'global';
22
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_scopes_project
23
+ ON memory_scopes(project_path) WHERE level = 'project';
24
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_scopes_workspace
25
+ ON memory_scopes(workspace_id) WHERE level = 'workspace';
26
+ CREATE INDEX IF NOT EXISTS idx_memory_scopes_level_id
27
+ ON memory_scopes(level, id);
28
+
29
+ CREATE TABLE IF NOT EXISTS memory_entries (
30
+ id TEXT PRIMARY KEY,
31
+ scope_id TEXT NOT NULL REFERENCES memory_scopes(id) ON DELETE CASCADE,
32
+ memory_key TEXT NOT NULL,
33
+ title TEXT NOT NULL,
34
+ body TEXT NOT NULL,
35
+ revision INTEGER NOT NULL DEFAULT 1 CHECK (revision > 0),
36
+ actor_kind TEXT NOT NULL CHECK (actor_kind IN ('human', 'internal-agent', 'external-mcp')),
37
+ source_workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL,
38
+ source_session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
39
+ source_engine TEXT,
40
+ client_name TEXT,
41
+ transport TEXT CHECK (transport IS NULL OR transport IN ('http', 'stdio')),
42
+ created_at TEXT NOT NULL,
43
+ updated_at TEXT NOT NULL,
44
+ UNIQUE (scope_id, memory_key),
45
+ CHECK (
46
+ (actor_kind = 'human' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NULL AND transport IS NULL) OR
47
+ (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex') AND client_name IS NULL AND transport IS NULL) OR
48
+ (actor_kind = 'external-mcp' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NOT NULL AND transport IN ('http', 'stdio'))
49
+ )
50
+ );
51
+ CREATE INDEX IF NOT EXISTS idx_memory_entries_scope_id
52
+ ON memory_entries(scope_id, id);
53
+ CREATE INDEX IF NOT EXISTS idx_memory_entries_scope_updated
54
+ ON memory_entries(scope_id, updated_at DESC, id DESC);
55
+
56
+ CREATE TABLE IF NOT EXISTS memory_proposals (
57
+ id TEXT PRIMARY KEY,
58
+ scope_id TEXT NOT NULL REFERENCES memory_scopes(id) ON DELETE CASCADE,
59
+ target_entry_id TEXT REFERENCES memory_entries(id) ON DELETE CASCADE,
60
+ base_revision INTEGER,
61
+ memory_key TEXT NOT NULL,
62
+ title TEXT NOT NULL,
63
+ body TEXT NOT NULL,
64
+ generation INTEGER NOT NULL CHECK (generation >= 0),
65
+ actor_kind TEXT NOT NULL CHECK (actor_kind IN ('human', 'internal-agent', 'external-mcp')),
66
+ source_workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL,
67
+ source_session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
68
+ source_engine TEXT,
69
+ client_name TEXT,
70
+ transport TEXT CHECK (transport IS NULL OR transport IN ('http', 'stdio')),
71
+ created_at TEXT NOT NULL,
72
+ updated_at TEXT NOT NULL,
73
+ CHECK ((target_entry_id IS NULL AND base_revision IS NULL) OR (target_entry_id IS NOT NULL AND base_revision IS NOT NULL)),
74
+ CHECK (
75
+ (actor_kind = 'human' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NULL AND transport IS NULL) OR
76
+ (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex') AND client_name IS NULL AND transport IS NULL) OR
77
+ (actor_kind = 'external-mcp' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NOT NULL AND transport IN ('http', 'stdio'))
78
+ )
79
+ );
80
+ CREATE INDEX IF NOT EXISTS idx_memory_proposals_scope_id
81
+ ON memory_proposals(scope_id, id);
82
+ CREATE INDEX IF NOT EXISTS idx_memory_proposals_scope_created
83
+ ON memory_proposals(scope_id, created_at DESC, id DESC);
84
+ CREATE INDEX IF NOT EXISTS idx_memory_proposals_target
85
+ ON memory_proposals(target_entry_id) WHERE target_entry_id IS NOT NULL;
86
+
87
+ CREATE TABLE IF NOT EXISTS memory_operations (
88
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
89
+ scope_id TEXT NOT NULL REFERENCES memory_scopes(id) ON DELETE CASCADE,
90
+ operation TEXT NOT NULL CHECK (operation IN ('created', 'updated', 'deleted', 'proposed', 'approved', 'rejected', 'promoted', 'cleared', 'read')),
91
+ actor_kind TEXT NOT NULL CHECK (actor_kind IN ('human', 'internal-agent', 'external-mcp')),
92
+ source_workspace_id TEXT REFERENCES workspaces(id) ON DELETE SET NULL,
93
+ source_session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
94
+ source_engine TEXT,
95
+ -- Preserve source project attribution when ON DELETE SET NULL clears workspace id.
96
+ source_project_path TEXT,
97
+ client_name TEXT,
98
+ transport TEXT CHECK (transport IS NULL OR transport IN ('http', 'stdio')),
99
+ entry_id TEXT,
100
+ proposal_id TEXT,
101
+ revision INTEGER,
102
+ affected_entries INTEGER,
103
+ affected_proposals INTEGER,
104
+ created_at TEXT NOT NULL,
105
+ CHECK (affected_entries IS NULL OR affected_entries >= 0),
106
+ CHECK (affected_proposals IS NULL OR affected_proposals >= 0),
107
+ CHECK (
108
+ (actor_kind = 'human' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NULL AND transport IS NULL) OR
109
+ (actor_kind = 'internal-agent' AND source_engine IN ('claude-code', 'codex') AND client_name IS NULL AND transport IS NULL) OR
110
+ (actor_kind = 'external-mcp' AND source_workspace_id IS NULL AND source_session_id IS NULL AND source_engine IS NULL AND client_name IS NOT NULL AND transport IN ('http', 'stdio'))
111
+ )
112
+ );
113
+ CREATE INDEX IF NOT EXISTS idx_memory_operations_scope_id
114
+ ON memory_operations(scope_id, id DESC);
115
+ CREATE INDEX IF NOT EXISTS idx_memory_operations_source_workspace_id
116
+ ON memory_operations(source_workspace_id, id DESC);
117
+
118
+ CREATE TABLE IF NOT EXISTS memory_contexts (
119
+ id TEXT PRIMARY KEY,
120
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
121
+ session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
122
+ dispatch_id TEXT NOT NULL UNIQUE,
123
+ budget_context_id TEXT REFERENCES memory_budget_contexts(id) ON DELETE SET NULL,
124
+ budget_epoch INTEGER NOT NULL DEFAULT 0 CHECK (budget_epoch >= 0),
125
+ engine TEXT NOT NULL CHECK (engine IN ('claude-code', 'codex')),
126
+ state TEXT NOT NULL CHECK (state IN ('prepared', 'submitted', 'initialized', 'failed', 'unknown')),
127
+ entry_revisions_json TEXT NOT NULL,
128
+ scope_generations_json TEXT NOT NULL,
129
+ omitted_count INTEGER NOT NULL DEFAULT 0 CHECK (omitted_count >= 0),
130
+ estimated_tokens INTEGER NOT NULL DEFAULT 0 CHECK (estimated_tokens >= 0),
131
+ payload_bytes INTEGER NOT NULL DEFAULT 0 CHECK (payload_bytes >= 0),
132
+ created_at TEXT NOT NULL,
133
+ updated_at TEXT NOT NULL
134
+ );
135
+ CREATE INDEX IF NOT EXISTS idx_memory_contexts_workspace_id
136
+ ON memory_contexts(workspace_id, id DESC);
137
+ CREATE INDEX IF NOT EXISTS idx_memory_contexts_session_id
138
+ ON memory_contexts(session_id, id DESC);
139
+
140
+ -- conversation_key is a backend-owned opaque identity, persisted with each
141
+ -- internal budget ledger. Reuse it for resumes; allocate a new key for a
142
+ -- fresh native conversation, including when the provider ID is not known.
143
+ CREATE TABLE IF NOT EXISTS memory_budget_contexts (
144
+ id TEXT PRIMARY KEY,
145
+ kind TEXT NOT NULL CHECK (kind IN ('internal', 'external')),
146
+ session_id TEXT REFERENCES agent_sessions(id) ON DELETE CASCADE,
147
+ engine TEXT CHECK (engine IS NULL OR engine IN ('claude-code', 'codex')),
148
+ conversation_key TEXT CHECK (conversation_key IS NULL OR length(trim(conversation_key)) > 0),
149
+ native_conversation_id TEXT,
150
+ external_context_id TEXT,
151
+ epoch INTEGER NOT NULL DEFAULT 0 CHECK (epoch >= 0),
152
+ cumulative_estimated_tokens INTEGER NOT NULL DEFAULT 0 CHECK (cumulative_estimated_tokens >= 0),
153
+ delivered_json TEXT NOT NULL DEFAULT '[]',
154
+ client_name TEXT,
155
+ transport TEXT CHECK (transport IS NULL OR transport IN ('http', 'stdio')),
156
+ created_at TEXT NOT NULL,
157
+ updated_at TEXT NOT NULL,
158
+ CHECK (
159
+ (kind = 'internal' AND session_id IS NOT NULL AND engine IS NOT NULL AND conversation_key IS NOT NULL AND external_context_id IS NULL AND client_name IS NULL AND transport IS NULL) OR
160
+ (kind = 'external' AND session_id IS NULL AND engine IS NULL AND conversation_key IS NULL AND external_context_id IS NOT NULL AND client_name IS NOT NULL AND transport IN ('http', 'stdio'))
161
+ )
162
+ );
163
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_budget_internal_conversation_epoch
164
+ ON memory_budget_contexts(conversation_key, epoch) WHERE kind = 'internal';
165
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_budget_external_context
166
+ ON memory_budget_contexts(external_context_id) WHERE kind = 'external';
167
+ CREATE INDEX IF NOT EXISTS idx_memory_budget_session_id
168
+ ON memory_budget_contexts(session_id, id) WHERE session_id IS NOT NULL;
169
+ `);
170
+ }
171
+ /** Apply the current memory schema to an upgraded database, including additive v51 columns. */
172
+ export function migrateMemorySchema(db) {
173
+ initMemorySchema(db);
174
+ const contextColumns = db.prepare('PRAGMA table_info(memory_contexts)').all();
175
+ if (!contextColumns.some((column) => column.name === 'budget_context_id')) {
176
+ db.exec('ALTER TABLE memory_contexts ADD COLUMN budget_context_id TEXT REFERENCES memory_budget_contexts(id) ON DELETE SET NULL');
177
+ }
178
+ if (!contextColumns.some((column) => column.name === 'budget_epoch')) {
179
+ db.exec('ALTER TABLE memory_contexts ADD COLUMN budget_epoch INTEGER NOT NULL DEFAULT 0 CHECK (budget_epoch >= 0)');
180
+ }
181
+ const columns = db.prepare('PRAGMA table_info(memory_operations)').all();
182
+ if (!columns.some((column) => column.name === 'source_project_path')) {
183
+ db.exec('ALTER TABLE memory_operations ADD COLUMN source_project_path TEXT');
184
+ }
185
+ }
@@ -1,5 +1,6 @@
1
1
  import { initAutoLoopSchema } from './auto-loop-schema.js';
2
2
  import { initMcpMessageSchema } from './mcp-message-schema.js';
3
+ import { migrateMemorySchema } from './memory-schema.js';
3
4
  import { initReviewReturnSchema } from './review-return-schema.js';
4
5
  import { initSchema } from './schema.js';
5
6
  import { initSearchSchema } from './search-schema.js';
@@ -793,6 +794,11 @@ export const migrations = [
793
794
  db.exec('CREATE TABLE IF NOT EXISTS reliability_reset (id INTEGER PRIMARY KEY CHECK (id = 1), reset_at TEXT NOT NULL)');
794
795
  },
795
796
  },
797
+ {
798
+ version: 51,
799
+ name: 'persistent-memory-schema',
800
+ migrate: migrateMemorySchema,
801
+ },
796
802
  ];
797
803
  /** Current schema version — always equals the highest migration version. */
798
804
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -1,5 +1,6 @@
1
1
  import { initAutoLoopSchema } from './auto-loop-schema.js';
2
2
  import { initMcpMessageSchema } from './mcp-message-schema.js';
3
+ import { initMemorySchema } from './memory-schema.js';
3
4
  import { initReviewReturnSchema } from './review-return-schema.js';
4
5
  import { initSearchSchema } from './search-schema.js';
5
6
  import { initSessionHandoffSchema } from './session-handoff-schema.js';
@@ -236,4 +237,5 @@ export function initSchema(db) {
236
237
  initReviewReturnSchema(db);
237
238
  initAutoLoopSchema(db);
238
239
  initSessionHandoffSchema(db);
240
+ initMemorySchema(db);
239
241
  }
@@ -21,6 +21,7 @@ import healthRouter from './routes/health.js';
21
21
  import imagesRouter from './routes/images.js';
22
22
  import integrationsRouter from './routes/integrations.js';
23
23
  import mcpRouter from './routes/mcp.js';
24
+ import memoryRouter from './routes/memory.js';
24
25
  import { migrationRouter } from './routes/migration.js';
25
26
  import notionRouter from './routes/notion.js';
26
27
  import pullRequestsRouter from './routes/pull-requests.js';
@@ -41,6 +42,7 @@ import * as cronService from './services/cron-service.js';
41
42
  import { createDailyDbBackupIfNeeded, createPreMigrationBackup, startDailyDbBackupScheduler, } from './services/db-backup-service.js';
42
43
  import { startDevServer, stopAllDevServers, stopDevServer } from './services/dev-server-service.js';
43
44
  import { reconcileMessageRequests } from './services/mcp-message-request-service.js';
45
+ import { reconcileMemoryContextsOnStartup } from './services/memory-context-service.js';
44
46
  import { authorizeWsUpgrade, generateToken, getLanHostnames, getLanUrls, isAllowedOrigin, isAllowedRequestHost, resolveBindHost, resolveDevClientOrigin, resolveNetworkAccessEnvOverrides, resolveProxyHostname, } from './services/network-access-service.js';
45
47
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
46
48
  import * as quotaBackoffService from './services/quota-backoff-service.js';
@@ -82,6 +84,7 @@ catch (err) {
82
84
  }
83
85
  runMigrations(db);
84
86
  reconcileMessageRequests(db);
87
+ reconcileMemoryContextsOnStartup();
85
88
  startSearchIndex();
86
89
  // Event retention. OPT-IN: disabled by default, so this is a no-op until the
87
90
  // user sets a window in Settings → Worktrees. Deliberately not enabled by
@@ -153,6 +156,7 @@ app.get('/api/health', (c) => c.json({ status: 'ok', version: getPackageVersion(
153
156
  // Mount route sub-routers
154
157
  app.route('/api/workspaces', workspacesRouter);
155
158
  app.route('/api/mcp', mcpRouter);
159
+ app.route('/api/memory', memoryRouter);
156
160
  app.route('/api/pull-requests', pullRequestsRouter);
157
161
  app.route('/api/workspaces', imagesRouter);
158
162
  app.route('/api/notion', notionRouter);
@@ -3,9 +3,12 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { Hono } from 'hono';
5
5
  import { bodyLimit } from 'hono/body-limit';
6
+ import { EXTERNAL_MEMORY_TOOL_DEFINITIONS, isMemoryToolName } from '../../shared/memory-tools.js';
6
7
  import { EXTERNAL_MCP_TOOLS } from '../../shared/workspace-dialogue-tools.js';
7
8
  import { createMcpClientContext } from '../services/mcp-client-context.js';
9
+ import { executeMemoryMcpTool } from '../services/memory-mcp-service.js';
8
10
  import { executeWorkspaceDialogueTool } from '../services/workspace-dialogue-service.js';
11
+ import { prepareMemoryToolEnvelope } from '../utils/memory-token-budget.js';
9
12
  const app = new Hono();
10
13
  app.use('*', bodyLimit({ maxSize: 1024 * 1024 }));
11
14
  app.all('/', async (c) => {
@@ -14,11 +17,27 @@ app.all('/', async (c) => {
14
17
  sessionIdGenerator: undefined,
15
18
  enableJsonResponse: true,
16
19
  });
17
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: EXTERNAL_MCP_TOOLS }));
20
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
21
+ tools: [...EXTERNAL_MCP_TOOLS, ...EXTERNAL_MEMORY_TOOL_DEFINITIONS],
22
+ }));
18
23
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
24
+ const isMemory = isMemoryToolName(request.params.name);
19
25
  try {
20
26
  const source = createMcpClientContext(c.req.header('X-Kobo-Client-Name'), c.req.header('X-Kobo-Client-Transport') === 'stdio' ? 'stdio' : 'http', c.req.header('X-Kobo-Client-Name-Encoding'));
21
- const data = await executeWorkspaceDialogueTool(request.params.name, request.params.arguments ?? {}, source);
27
+ const data = isMemory
28
+ ? executeMemoryMcpTool(request.params.name, request.params.arguments ?? {}, source)
29
+ : await executeWorkspaceDialogueTool(request.params.name, request.params.arguments ?? {}, source);
30
+ if (isMemory) {
31
+ const bounded = prepareMemoryToolEnvelope(data, { targetTokens: 1_000, transport: 'external' });
32
+ if (bounded.data.memoryOutputSuppressed === true)
33
+ return { content: [], isError: true };
34
+ const { __mcpError, ...publicData } = bounded.data;
35
+ return {
36
+ content: [{ type: 'text', text: JSON.stringify(publicData) }],
37
+ structuredContent: publicData,
38
+ ...(__mcpError === true || publicData.budgetExhausted === true ? { isError: true } : {}),
39
+ };
40
+ }
22
41
  const structuredContent = data && typeof data === 'object' && !Array.isArray(data) ? data : undefined;
23
42
  return {
24
43
  content: [{ type: 'text', text: JSON.stringify(data) }],
@@ -27,6 +46,16 @@ app.all('/', async (c) => {
27
46
  };
28
47
  }
29
48
  catch (error) {
49
+ if (isMemory) {
50
+ const bounded = prepareMemoryToolEnvelope({
51
+ error: error instanceof Error ? error.message : String(error),
52
+ }, { targetTokens: 1_000, transport: 'external' });
53
+ return {
54
+ isError: true,
55
+ content: [{ type: 'text', text: JSON.stringify(bounded.data) }],
56
+ structuredContent: bounded.data,
57
+ };
58
+ }
30
59
  return {
31
60
  isError: true,
32
61
  content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],