@loicngr/kobo 2.1.6 → 2.1.9

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 (196) hide show
  1. package/AGENTS.md +16 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CONFIGURATION.md +39 -0
  4. package/DESIGN.md +11 -1
  5. package/README.md +4 -0
  6. package/dist/mcp-server/kobo-tasks-handlers.js +17 -4
  7. package/dist/mcp-server/kobo-tasks-server.js +47 -3
  8. package/dist/mcp-server/memory-client.js +39 -0
  9. package/dist/mcp-server/workspace-dialogue-client.js +4 -1
  10. package/dist/server/db/memory-schema.js +185 -0
  11. package/dist/server/db/migrations.js +6 -0
  12. package/dist/server/db/schema.js +2 -0
  13. package/dist/server/index.js +4 -0
  14. package/dist/server/routes/mcp.js +31 -2
  15. package/dist/server/routes/memory.js +525 -0
  16. package/dist/server/routes/settings.js +3 -0
  17. package/dist/server/routes/workspaces.js +4 -1
  18. package/dist/server/services/agent/engines/claude-code/event-mapper.js +13 -2
  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/dev-server-service.js +7 -1
  22. package/dist/server/services/memory-agent-runtime.js +65 -0
  23. package/dist/server/services/memory-budget-service.js +183 -0
  24. package/dist/server/services/memory-context-service.js +287 -0
  25. package/dist/server/services/memory-mcp-service.js +339 -0
  26. package/dist/server/services/memory-service.js +891 -0
  27. package/dist/server/services/settings-service.js +19 -0
  28. package/dist/server/utils/memory-project-identity.js +17 -0
  29. package/dist/server/utils/memory-token-budget.js +221 -0
  30. package/dist/shared/codex-models.js +0 -24
  31. package/dist/shared/memory-prompts.js +18 -0
  32. package/dist/shared/memory-tools.js +215 -0
  33. package/dist/shared/memory.js +81 -0
  34. package/dist/shared/models.js +35 -41
  35. package/package.json +1 -1
  36. package/src/client/dist/pwa/assets/ActivityFeed-DdJvMzRI.css +1 -0
  37. package/src/client/dist/pwa/assets/ActivityFeed-w8TNfMKd.js +2 -0
  38. package/src/client/dist/pwa/assets/ChangelogPage-B1rkKskD.js +1 -0
  39. package/src/client/dist/pwa/assets/ClosePopup-CRV2hxHS.js +1 -0
  40. package/src/client/dist/pwa/assets/{ComparisonPanel-CfmJHeH_.js → ComparisonPanel-kMWS9il3.js} +2 -2
  41. package/src/client/dist/pwa/assets/CreatePage-C7fLOpai.css +1 -0
  42. package/src/client/dist/pwa/assets/CreatePage-DOamHqeH.js +2 -0
  43. package/src/client/dist/pwa/assets/DashboardPage-Ds6Y8xlV.js +1 -0
  44. package/src/client/dist/pwa/assets/DiffViewer-j7rwPKxU.js +8 -0
  45. package/src/client/dist/pwa/assets/DocumentsPanel-BTp3J6Rq.js +1 -0
  46. package/src/client/dist/pwa/assets/{DrawerToggleButton-DKytIBlk.js → DrawerToggleButton-DcVhAbjN.js} +1 -1
  47. package/src/client/dist/pwa/assets/GitPanel-DOugIJUS.js +3 -0
  48. package/src/client/dist/pwa/assets/HealthPage-BSrTmp1k.js +1 -0
  49. package/src/client/dist/pwa/assets/{MainLayout-DkSavoiV.css → MainLayout-DAcSwgUw.css} +1 -1
  50. package/src/client/dist/pwa/assets/MainLayout-DWzXL1KW.js +2 -0
  51. package/src/client/dist/pwa/assets/MemoryPanel-BivQoMQ6.js +1 -0
  52. package/src/client/dist/pwa/assets/MemoryPanel-Ch3qeyQw.css +1 -0
  53. package/src/client/dist/pwa/assets/MemoryProposalList-BnoVbFsj.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-B9xNOKD0.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-BBTTV_37.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-bm6nYMnB.js +1 -0
  68. package/src/client/dist/pwa/assets/QSelect-CPE9LsIn.js +1 -0
  69. package/src/client/dist/pwa/assets/QSpinnerDots-xdWyaE1i.js +1 -0
  70. package/src/client/dist/pwa/assets/QTooltip-CgVCYatx.js +1 -0
  71. package/src/client/dist/pwa/assets/SchedulePanel-DNwt3V1a.js +1 -0
  72. package/src/client/dist/pwa/assets/SearchPage-Dr43rGji.js +1 -0
  73. package/src/client/dist/pwa/assets/SettingsPage-C1dcMqCO.css +1 -0
  74. package/src/client/dist/pwa/assets/SettingsPage-oSGNwimG.js +9 -0
  75. package/src/client/dist/pwa/assets/SplitWorkspacePage-Cjd_Lq-0.js +1 -0
  76. package/src/client/dist/pwa/assets/{TerminalPanel-C4dYabx6.js → TerminalPanel-Bsrg96VZ.js} +9 -9
  77. package/src/client/dist/pwa/assets/ToolCallItem-M3CSguac.js +8 -0
  78. package/src/client/dist/pwa/assets/TouchPan-CxThkotY.js +1 -0
  79. package/src/client/dist/pwa/assets/{TourReplayButton-DhbNI6O5.js → TourReplayButton-COI4vR7q.js} +1 -1
  80. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CqlhQOqG.js +1 -0
  81. package/src/client/dist/pwa/assets/{WorkspacePage-BnW-gp3u.css → WorkspacePage-Ba0jAHhn.css} +1 -1
  82. package/src/client/dist/pwa/assets/WorkspacePage-C-JXKt-S.js +4 -0
  83. package/src/client/dist/pwa/assets/{browser-CTkGHrN5.js → browser-DuFAaYHh.js} +1 -1
  84. package/src/client/dist/pwa/assets/build-path-tree-DFwB0SSs.js +1 -0
  85. package/src/client/dist/pwa/assets/custom-sounds-DTV-xHVv.js +1 -0
  86. package/src/client/dist/pwa/assets/de-YFD4X2nQ.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-CKcTKXcM.js → editor.api-DDwuA_C9.js} +1 -1
  90. package/src/client/dist/pwa/assets/efforts-DU9dTnaN.js +9 -0
  91. package/src/client/dist/pwa/assets/es-BvRPXBHq.js +17 -0
  92. package/src/client/dist/pwa/assets/expand-template-LaRhdYUp.js +1 -0
  93. package/src/client/dist/pwa/assets/{format-MtzcnR3z.js → format-BUmAn8W2.js} +1 -1
  94. package/src/client/dist/pwa/assets/formatters-DfLD95iT.js +1 -0
  95. package/src/client/dist/pwa/assets/fr-CZhjMFZl.js +17 -0
  96. package/src/client/dist/pwa/assets/{freemarker2-JGUTjYvR.js → freemarker2-DVQM2t3e.js} +1 -1
  97. package/src/client/dist/pwa/assets/{handlebars-BWStl2Hc.js → handlebars-ChiPNy9j.js} +1 -1
  98. package/src/client/dist/pwa/assets/{html-CjGCxUEe.js → html-CnGW31DH.js} +1 -1
  99. package/src/client/dist/pwa/assets/i18n-v1pgIO4z.js +1 -0
  100. package/src/client/dist/pwa/assets/index-DPN7dSDH.js +20 -0
  101. package/src/client/dist/pwa/assets/it-B4rz5PZO.js +17 -0
  102. package/src/client/dist/pwa/assets/{javascript-C_jd0j2i.js → javascript-BmeNra1c.js} +1 -1
  103. package/src/client/dist/pwa/assets/{liquid-DMyQVGkp.js → liquid-C5qQFvIu.js} +1 -1
  104. package/src/client/dist/pwa/assets/{mdx-DeW5qzOU.js → mdx-TsWwhlKp.js} +1 -1
  105. package/src/client/dist/pwa/assets/{monaco-contributions-Be_o6s3m.js → monaco-contributions-SMzL7GRn.js} +1 -1
  106. package/src/client/dist/pwa/assets/monaco.contribution-sN2BZtSW.js +2 -0
  107. package/src/client/dist/pwa/assets/network-auth-DXsDKXrM.js +1 -0
  108. package/src/client/dist/pwa/assets/{notify-theme-uhSG2r09.js → notify-theme-BXJypfo4.js} +1 -1
  109. package/src/client/dist/pwa/assets/{permissionModes-Cc5VaOdC.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-BQbOnYBN.js → project-color-CBLQt6bM.js} +1 -1
  112. package/src/client/dist/pwa/assets/{python-C1v3VovI.js → python-CKHf2s-D.js} +1 -1
  113. package/src/client/dist/pwa/assets/{razor-DMEmKSdO.js → razor-D_5X4O5M.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-rKyiSQID.js → typescript-D62emDGo.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-bIKbEP5q.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-CTNYIX-Q.js +2 -0
  124. package/src/client/dist/pwa/assets/use-worktree-restore-D69uzYON.js +1 -0
  125. package/src/client/dist/pwa/assets/workspace-templates-CSwgMrUf.js +1 -0
  126. package/src/client/dist/pwa/assets/{xml-U_pDcywM.js → xml-Kyg_K_5X.js} +1 -1
  127. package/src/client/dist/pwa/assets/{yaml-CRCd_ET9.js → yaml-DC__PKjj.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 +23 -0
  131. package/src/mcp-server/kobo-tasks-handlers.ts +24 -9
  132. package/src/mcp-server/kobo-tasks-server.ts +52 -3
  133. package/src/mcp-server/memory-client.ts +52 -0
  134. package/src/mcp-server/workspace-dialogue-client.ts +4 -1
  135. package/src/client/dist/pwa/assets/ActivityFeed-BO1mKMJ0.js +0 -1
  136. package/src/client/dist/pwa/assets/ActivityFeed-ZTxXVmNM.css +0 -1
  137. package/src/client/dist/pwa/assets/ChangelogPage-BXNz7PGb.js +0 -1
  138. package/src/client/dist/pwa/assets/ClosePopup-C1a82pL9.js +0 -1
  139. package/src/client/dist/pwa/assets/CreatePage-6_AP5zso.js +0 -2
  140. package/src/client/dist/pwa/assets/CreatePage-CMyl2j8Z.css +0 -1
  141. package/src/client/dist/pwa/assets/DashboardPage-BKCrV6u0.js +0 -1
  142. package/src/client/dist/pwa/assets/DiffViewer-BMzrb9fB.js +0 -8
  143. package/src/client/dist/pwa/assets/DocumentsPanel-CqGg38Oi.js +0 -1
  144. package/src/client/dist/pwa/assets/GitPanel-DRzhrs_V.js +0 -3
  145. package/src/client/dist/pwa/assets/HealthPage-DWotmfkh.js +0 -1
  146. package/src/client/dist/pwa/assets/MainLayout-fRVZ01LN.js +0 -2
  147. package/src/client/dist/pwa/assets/QBadge-rGILsUYl.js +0 -1
  148. package/src/client/dist/pwa/assets/QBanner-mzrnn82R.js +0 -1
  149. package/src/client/dist/pwa/assets/QBtn-b_baQtAt.js +0 -1
  150. package/src/client/dist/pwa/assets/QBtnDropdown-BGGvQIvH.js +0 -1
  151. package/src/client/dist/pwa/assets/QBtnGroup-CUmAgBV8.js +0 -1
  152. package/src/client/dist/pwa/assets/QBtnToggle-COL46j6W.js +0 -1
  153. package/src/client/dist/pwa/assets/QChip-Chu7r6RZ.js +0 -1
  154. package/src/client/dist/pwa/assets/QExpansionItem-C2XXHOHk.js +0 -1
  155. package/src/client/dist/pwa/assets/QItemSection-CR3Kfibl.js +0 -1
  156. package/src/client/dist/pwa/assets/QList-DnezW6gW.js +0 -1
  157. package/src/client/dist/pwa/assets/QMarkupTable-wuDf50Oh.js +0 -1
  158. package/src/client/dist/pwa/assets/QPage-BCDXjfVj.js +0 -1
  159. package/src/client/dist/pwa/assets/QScrollArea-BnmQ9US7.js +0 -1
  160. package/src/client/dist/pwa/assets/QSelect-BskTzVCM.js +0 -1
  161. package/src/client/dist/pwa/assets/QSpinnerDots-BsKmccpX.js +0 -1
  162. package/src/client/dist/pwa/assets/QTooltip-CO84H0cq.js +0 -1
  163. package/src/client/dist/pwa/assets/SchedulePanel-CPirTYw5.js +0 -1
  164. package/src/client/dist/pwa/assets/SearchPage-CnT0-Hpe.js +0 -1
  165. package/src/client/dist/pwa/assets/SettingsPage-CpykRKq2.css +0 -1
  166. package/src/client/dist/pwa/assets/SettingsPage-DCMZL7GO.js +0 -9
  167. package/src/client/dist/pwa/assets/SplitWorkspacePage-DwWsD7f5.js +0 -1
  168. package/src/client/dist/pwa/assets/ToolCallItem-CjXWiKJc.js +0 -8
  169. package/src/client/dist/pwa/assets/TouchPan-6fI8xFPq.js +0 -1
  170. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CZxW7QSq.js +0 -1
  171. package/src/client/dist/pwa/assets/WorkspacePage-B0wCQdev.js +0 -4
  172. package/src/client/dist/pwa/assets/build-path-tree-Bd50EZMW.js +0 -1
  173. package/src/client/dist/pwa/assets/custom-sounds-DoPDmcY5.js +0 -1
  174. package/src/client/dist/pwa/assets/de-BPtzBQu0.js +0 -17
  175. package/src/client/dist/pwa/assets/efforts-CDsBbk8o.js +0 -9
  176. package/src/client/dist/pwa/assets/es-BoNOfG0w.js +0 -17
  177. package/src/client/dist/pwa/assets/expand-template-D6zFPzzB.js +0 -1
  178. package/src/client/dist/pwa/assets/formatters-DCjBZ47F.js +0 -1
  179. package/src/client/dist/pwa/assets/fr-Bu92fK5U.js +0 -17
  180. package/src/client/dist/pwa/assets/i18n-DKj-umtn.js +0 -19
  181. package/src/client/dist/pwa/assets/i18n-DtmbeJu8.js +0 -1
  182. package/src/client/dist/pwa/assets/index-BPmUmc6D.js +0 -2
  183. package/src/client/dist/pwa/assets/it-CrA4Q7oc.js +0 -17
  184. package/src/client/dist/pwa/assets/latest-request-CBPb8UWQ.js +0 -1
  185. package/src/client/dist/pwa/assets/monaco.contribution-VUGNbVME.js +0 -2
  186. package/src/client/dist/pwa/assets/network-auth-CSx0jS5X.js +0 -1
  187. package/src/client/dist/pwa/assets/preload-helper-DjzSQ1t3.js +0 -1
  188. package/src/client/dist/pwa/assets/reactivity.esm-bundler-tuEvQ47f.js +0 -1
  189. package/src/client/dist/pwa/assets/runtime-core.esm-bundler-DOiZcasU.js +0 -1
  190. package/src/client/dist/pwa/assets/use-checkbox-BqDMQ6x7.js +0 -1
  191. package/src/client/dist/pwa/assets/use-id-b4A2I8LB.js +0 -1
  192. package/src/client/dist/pwa/assets/use-is-mobile-DLIZxEo9.js +0 -1
  193. package/src/client/dist/pwa/assets/use-search-index-status-BL3cb04x.js +0 -1
  194. package/src/client/dist/pwa/assets/use-tours-Dum-bOyQ.js +0 -2
  195. package/src/client/dist/pwa/assets/use-worktree-restore-BQyoz3Qa.js +0 -1
  196. package/src/client/dist/pwa/assets/workspace-templates-YRwFjkUh.js +0 -1
@@ -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) }],
@@ -0,0 +1,525 @@
1
+ import { Hono } from 'hono';
2
+ import { bodyLimit } from 'hono/body-limit';
3
+ import { MEMORY_BODY_MAX_CHARS, MEMORY_PAGE_SIZE_MAX, MEMORY_TITLE_MAX_CHARS } from '../../shared/memory.js';
4
+ import { isMemoryToolName, validateMemoryToolArguments } from '../../shared/memory-tools.js';
5
+ import { getMemoryCapability } from '../services/memory-agent-runtime.js';
6
+ import { getInternalMemoryBudgetContext, reserveMemoryBudget, reserveMemoryBudgetDenial, wasMemoryFragmentDelivered, } from '../services/memory-budget-service.js';
7
+ import { listMemoryContextRecords } from '../services/memory-context-service.js';
8
+ import { approveMemoryProposal, clearMemoryScope, createMemory, deleteMemory, listMemories, listMemoryOperations, listMemoryProposals, listMemoryScopes, listWorkspaceMemoryOperations, MemoryConflictError, MemoryNotFoundError, previewMemoryClear, promoteMemory, readMemory, rejectMemoryProposal, remember, searchMemories, updateMemory, } from '../services/memory-service.js';
9
+ import { prepareMemoryToolEnvelope, sliceMemoryBodyFragment } from '../utils/memory-token-budget.js';
10
+ const app = new Hono();
11
+ const human = { kind: 'human' };
12
+ const MAX_BODY_BYTES = 16 * 1024;
13
+ function isObject(value) {
14
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
15
+ }
16
+ async function readJson(c) {
17
+ let value;
18
+ try {
19
+ value = await c.req.json();
20
+ }
21
+ catch {
22
+ throw new TypeError('Request body must be valid JSON');
23
+ }
24
+ if (!isObject(value))
25
+ throw new TypeError('Request body must be a JSON object');
26
+ return value;
27
+ }
28
+ function requiredString(value, name, max = 512) {
29
+ if (typeof value !== 'string' || !value.trim() || value.length > max)
30
+ throw new TypeError(`Invalid ${name}`);
31
+ return value.trim();
32
+ }
33
+ function requiredRevision(value, name = 'revision', minimum = 1) {
34
+ if (!Number.isSafeInteger(value) || value < minimum)
35
+ throw new TypeError(`Invalid ${name}`);
36
+ return value;
37
+ }
38
+ function parseLimit(value) {
39
+ if (value === undefined)
40
+ return undefined;
41
+ if (!/^\d+$/.test(value))
42
+ throw new TypeError('Invalid page size');
43
+ const limit = Number(value);
44
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MEMORY_PAGE_SIZE_MAX)
45
+ throw new TypeError('Invalid page size');
46
+ return limit;
47
+ }
48
+ function parseCursor(value) {
49
+ if (value === undefined)
50
+ return undefined;
51
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
52
+ throw new TypeError('Invalid cursor');
53
+ return value;
54
+ }
55
+ function parseEntry(value, partial = false) {
56
+ const result = {};
57
+ if (!partial || value.key !== undefined)
58
+ result.key = requiredString(value.key, 'key', 80).toLowerCase();
59
+ if (!partial || value.title !== undefined)
60
+ result.title = requiredString(value.title, 'title', MEMORY_TITLE_MAX_CHARS);
61
+ if (!partial || value.body !== undefined) {
62
+ if (typeof value.body === 'string' && value.body.length > MEMORY_BODY_MAX_CHARS)
63
+ throw new PayloadTooLargeError('Memory body exceeds the character limit');
64
+ result.body = requiredString(value.body, 'body', MEMORY_BODY_MAX_CHARS);
65
+ }
66
+ if (partial && Object.keys(result).length === 0)
67
+ throw new TypeError('At least one memory field is required');
68
+ return result;
69
+ }
70
+ class PayloadTooLargeError extends Error {
71
+ }
72
+ function handleError(c, error) {
73
+ if (error instanceof MemoryNotFoundError)
74
+ return c.json({ error: error.message }, 404);
75
+ if (error instanceof MemoryConflictError)
76
+ return c.json({ error: error.message }, 409);
77
+ if (error instanceof PayloadTooLargeError)
78
+ return c.json({ error: error.message }, 413);
79
+ if (error instanceof TypeError)
80
+ return c.json({ error: error.message }, 400);
81
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
82
+ }
83
+ function boundedAgentReply(c, capability, value, status = 200, deliveries = [], preReserved, pageOffset) {
84
+ let ledger;
85
+ try {
86
+ ledger = getInternalMemoryBudgetContext(capability.conversationKey);
87
+ }
88
+ catch (error) {
89
+ // Capabilities are normally created alongside their ledger. If stale or
90
+ // malformed runtime state breaks that invariant, preserve the validation
91
+ // status without turning a client error into a 500 or doing any write.
92
+ const bounded = prepareMemoryToolEnvelope(value, { targetTokens: 1_000, transport: 'internal' });
93
+ const statusCode = error instanceof TypeError ? status : 500;
94
+ return c.json(bounded.data, { status: statusCode });
95
+ }
96
+ const placeholder = {
97
+ ...value,
98
+ budget: { estimatedTokens: 1_000, remainingTokens: 6_000, exhausted: false },
99
+ };
100
+ const sized = prepareMemoryToolEnvelope(placeholder, { targetTokens: 1_000, transport: 'internal', pageOffset });
101
+ const emittedItems = Array.isArray(sized.data.items) ? sized.data.items : [];
102
+ const emittedIds = new Set(emittedItems
103
+ .filter((item) => !!item && typeof item === 'object' && !Array.isArray(item))
104
+ .map((item) => item.id));
105
+ const emittedDeliveries = deliveries.filter((delivery) => {
106
+ if (delivery.kind === 'metadata' || delivery.kind === 'excerpt')
107
+ return emittedIds.has(delivery.entryId);
108
+ const body = sized.data.entry?.body;
109
+ if (typeof body !== 'string')
110
+ return false;
111
+ delivery.end = delivery.start + [...body].length;
112
+ return true;
113
+ });
114
+ const reservation = preReserved ??
115
+ reserveMemoryBudget({
116
+ budgetContextId: ledger.id,
117
+ estimatedTokens: sized.estimatedTokens,
118
+ responseLimitTokens: 1_000,
119
+ deliveries: emittedDeliveries,
120
+ });
121
+ if (!reservation.accepted) {
122
+ return exhaustedAgentReply(c, capability);
123
+ }
124
+ const result = {
125
+ ...sized.data,
126
+ budget: {
127
+ estimatedTokens: reservation.estimatedTokens,
128
+ remainingTokens: reservation.remainingTokens,
129
+ exhausted: reservation.remainingTokens <= 0,
130
+ },
131
+ };
132
+ const bounded = prepareMemoryToolEnvelope(result, { targetTokens: 1_000, transport: 'internal' });
133
+ return c.json(bounded.data, { status });
134
+ }
135
+ function exhaustedAgentReply(c, capability, message = 'Budget mémoire épuisé; ne répète pas cette lecture dans la conversation courante.') {
136
+ const ledger = getInternalMemoryBudgetContext(capability.conversationKey);
137
+ const denial = prepareMemoryToolEnvelope({
138
+ budgetExhausted: true,
139
+ message,
140
+ remainingTokens: 0,
141
+ limitTokens: 6_000,
142
+ }, { targetTokens: 1_000, transport: 'internal' });
143
+ const reservation = reserveMemoryBudgetDenial(ledger.id, denial.estimatedTokens);
144
+ return c.json(reservation.responseAllowed ? denial.data : { budgetExhausted: true, memoryOutputSuppressed: true }, 200);
145
+ }
146
+ function reserveAgentWrite(capability) {
147
+ const ledger = getInternalMemoryBudgetContext(capability.conversationKey);
148
+ // Reserve the full output ceiling before mutating. Sizing a synthetic large
149
+ // receipt can collapse it to a small fallback and undercharge the real result.
150
+ return reserveMemoryBudget({
151
+ budgetContextId: ledger.id,
152
+ estimatedTokens: 1_000,
153
+ responseLimitTokens: 1_000,
154
+ });
155
+ }
156
+ app.use('*', bodyLimit({ maxSize: MAX_BODY_BYTES, onError: (c) => c.json({ error: 'Request body is too large' }, 413) }));
157
+ app.get('/scopes', (c) => {
158
+ try {
159
+ const workspaceId = c.req.query('workspaceId');
160
+ return c.json(listMemoryScopes({
161
+ ...(workspaceId ? { workspaceId } : {}),
162
+ cursor: parseCursor(c.req.query('cursor')),
163
+ limit: parseLimit(c.req.query('limit')),
164
+ }));
165
+ }
166
+ catch (error) {
167
+ return handleError(c, error);
168
+ }
169
+ });
170
+ app.get('/entries', (c) => {
171
+ try {
172
+ const scopeId = c.req.query('scopeId');
173
+ const workspaceId = c.req.query('workspaceId');
174
+ if (Boolean(scopeId) === Boolean(workspaceId))
175
+ throw new TypeError('Specify exactly one scopeId or workspaceId');
176
+ const query = c.req.query('query');
177
+ if (query !== undefined && (!query.trim() || query.length > 200))
178
+ throw new TypeError('Invalid search query');
179
+ return c.json(listMemories({
180
+ ...(scopeId ? { scopeId } : { workspaceId }),
181
+ cursor: parseCursor(c.req.query('cursor')),
182
+ limit: parseLimit(c.req.query('limit')),
183
+ ...(query !== undefined ? { query } : {}),
184
+ }));
185
+ }
186
+ catch (error) {
187
+ return handleError(c, error);
188
+ }
189
+ });
190
+ app.post('/entries', async (c) => {
191
+ try {
192
+ const value = await readJson(c);
193
+ const scopeId = requiredString(value.scopeId, 'scopeId');
194
+ const input = parseEntry(value);
195
+ const created = createMemory({ scopeId, ...input, actor: human });
196
+ return c.json(created, 201);
197
+ }
198
+ catch (error) {
199
+ return handleError(c, error);
200
+ }
201
+ });
202
+ app.patch('/entries/:entryId', async (c) => {
203
+ try {
204
+ const value = await readJson(c);
205
+ const updated = updateMemory({
206
+ scopeId: requiredString(value.scopeId, 'scopeId'),
207
+ entryId: requiredString(c.req.param('entryId'), 'entryId'),
208
+ expectedRevision: requiredRevision(value.expectedRevision),
209
+ ...parseEntry(value, true),
210
+ actor: human,
211
+ });
212
+ return c.json(updated);
213
+ }
214
+ catch (error) {
215
+ return handleError(c, error);
216
+ }
217
+ });
218
+ app.delete('/entries/:entryId', async (c) => {
219
+ try {
220
+ const value = await readJson(c);
221
+ deleteMemory({
222
+ scopeId: requiredString(value.scopeId, 'scopeId'),
223
+ entryId: requiredString(c.req.param('entryId'), 'entryId'),
224
+ expectedRevision: requiredRevision(value.expectedRevision),
225
+ actor: human,
226
+ });
227
+ return c.body(null, 204);
228
+ }
229
+ catch (error) {
230
+ return handleError(c, error);
231
+ }
232
+ });
233
+ app.post('/entries/:entryId/promote', async (c) => {
234
+ try {
235
+ const value = await readJson(c);
236
+ const promoted = promoteMemory({
237
+ sourceScopeId: requiredString(value.sourceScopeId, 'sourceScopeId'),
238
+ entryId: requiredString(c.req.param('entryId'), 'entryId'),
239
+ targetScopeId: requiredString(value.targetScopeId, 'targetScopeId'),
240
+ actor: human,
241
+ });
242
+ return c.json(promoted, 201);
243
+ }
244
+ catch (error) {
245
+ return handleError(c, error);
246
+ }
247
+ });
248
+ app.get('/proposals', (c) => {
249
+ try {
250
+ const scopeId = requiredString(c.req.query('scopeId'), 'scopeId');
251
+ return c.json({ items: listMemoryProposals(scopeId) });
252
+ }
253
+ catch (error) {
254
+ return handleError(c, error);
255
+ }
256
+ });
257
+ app.post('/proposals/:proposalId/approve', (c) => {
258
+ try {
259
+ return c.json(approveMemoryProposal(requiredString(c.req.param('proposalId'), 'proposalId'), human));
260
+ }
261
+ catch (error) {
262
+ return handleError(c, error);
263
+ }
264
+ });
265
+ app.post('/proposals/:proposalId/reject', (c) => {
266
+ try {
267
+ rejectMemoryProposal(requiredString(c.req.param('proposalId'), 'proposalId'), human);
268
+ return c.body(null, 204);
269
+ }
270
+ catch (error) {
271
+ return handleError(c, error);
272
+ }
273
+ });
274
+ app.get('/scopes/:scopeId/clear-preview', (c) => {
275
+ try {
276
+ return c.json(previewMemoryClear(requiredString(c.req.param('scopeId'), 'scopeId'), human));
277
+ }
278
+ catch (error) {
279
+ return handleError(c, error);
280
+ }
281
+ });
282
+ app.post('/scopes/:scopeId/clear', async (c) => {
283
+ try {
284
+ const value = await readJson(c);
285
+ return c.json(clearMemoryScope({
286
+ scopeId: requiredString(c.req.param('scopeId'), 'scopeId'),
287
+ expectedRevision: requiredRevision(value.expectedRevision, 'preview revision', 0),
288
+ actor: human,
289
+ }));
290
+ }
291
+ catch (error) {
292
+ return handleError(c, error);
293
+ }
294
+ });
295
+ app.get('/operations', (c) => {
296
+ try {
297
+ const workspaceId = c.req.query('workspaceId');
298
+ const cursor = parseCursor(c.req.query('afterCursor'));
299
+ const limit = parseLimit(c.req.query('limit'));
300
+ if (workspaceId && !c.req.query('scopeId')) {
301
+ return c.json(listWorkspaceMemoryOperations(requiredString(workspaceId, 'workspaceId'), { cursor, limit }));
302
+ }
303
+ return c.json(listMemoryOperations({
304
+ scopeId: requiredString(c.req.query('scopeId'), 'scopeId'),
305
+ ...(workspaceId ? { workspaceId: requiredString(workspaceId, 'workspaceId') } : {}),
306
+ cursor,
307
+ limit,
308
+ }));
309
+ }
310
+ catch (error) {
311
+ return handleError(c, error);
312
+ }
313
+ });
314
+ app.get('/workspaces/:workspaceId/view', (c) => {
315
+ try {
316
+ const workspaceId = requiredString(c.req.param('workspaceId'), 'workspaceId');
317
+ const scopePage = listMemoryScopes({ workspaceId });
318
+ const scopes = scopePage.items;
319
+ const entries = listMemories({ workspaceId });
320
+ const proposals = scopes.flatMap((scope) => listMemoryProposals(scope.id));
321
+ // Context persistence is populated by the session-injection phase. Returning an
322
+ // honest empty collection is preferable to inventing delivery history here.
323
+ const sessionId = c.req.query('sessionId');
324
+ return c.json({
325
+ scopes,
326
+ entries: entries.items,
327
+ entriesNextCursor: entries.nextCursor,
328
+ entriesTotalCount: entries.totalCount,
329
+ proposals,
330
+ contexts: sessionId ? listMemoryContextRecords(workspaceId, sessionId) : [],
331
+ ...(sessionId ? { sessionId } : {}),
332
+ });
333
+ }
334
+ catch (error) {
335
+ return handleError(c, error);
336
+ }
337
+ });
338
+ /** Workspace MCP capability boundary. Provenance and scope are derived server-side. */
339
+ app.post('/agent/:toolName', async (c) => {
340
+ const token = c.req.header('X-Kobo-Memory-Session');
341
+ const capability = getMemoryCapability(token);
342
+ if (!capability)
343
+ return c.json({ error: 'Invalid or expired memory capability' }, 401);
344
+ const toolName = c.req.param('toolName');
345
+ if (!isMemoryToolName(toolName))
346
+ return c.json({ error: 'Unknown memory tool' }, 404);
347
+ let writeReservation;
348
+ try {
349
+ const input = await readJson(c);
350
+ // Stop may revoke the launch while the request body is still arriving.
351
+ // No asynchronous work follows this check before service mutations.
352
+ if (getMemoryCapability(token) !== capability)
353
+ return c.json({ error: 'Invalid or expired memory capability' }, 401);
354
+ const args = validateMemoryToolArguments(toolName, input);
355
+ const actor = {
356
+ kind: 'internal-agent',
357
+ workspaceId: capability.workspaceId,
358
+ sessionId: capability.sessionId,
359
+ engine: capability.engine,
360
+ };
361
+ // Capability-only fields are never forwarded as memory arguments.
362
+ const scopeId = typeof args.scope_id === 'string' ? args.scope_id : undefined;
363
+ if (toolName === 'list_memory_scopes') {
364
+ const page = listMemoryScopes({
365
+ workspaceId: capability.workspaceId,
366
+ actor,
367
+ ...(typeof args.cursor === 'string' ? { cursor: args.cursor } : {}),
368
+ ...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
369
+ });
370
+ return boundedAgentReply(c, capability, {
371
+ ...page,
372
+ items: page.items.map(({ id, level, generation, revision }) => ({ id, level, generation, revision })),
373
+ }, 200, [], undefined, Number(args.cursor ?? 0));
374
+ }
375
+ if (toolName === 'list_memories') {
376
+ const page = listMemories({
377
+ ...(scopeId ? { scopeId } : { workspaceId: capability.workspaceId }),
378
+ actor,
379
+ ...(typeof args.cursor === 'string' ? { cursor: args.cursor } : {}),
380
+ ...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
381
+ });
382
+ const ledgerId = getInternalMemoryBudgetContext(capability.conversationKey).id;
383
+ const freshDeliveries = [];
384
+ const items = page.items.map((entry) => {
385
+ const delivery = { entryId: entry.id, revision: entry.revision, kind: 'metadata' };
386
+ if (wasMemoryFragmentDelivered(ledgerId, delivery))
387
+ return { id: entry.id, revision: entry.revision, alreadyDelivered: true };
388
+ freshDeliveries.push(delivery);
389
+ return {
390
+ id: entry.id,
391
+ scopeId: entry.scopeId,
392
+ key: entry.key,
393
+ title: entry.title,
394
+ revision: entry.revision,
395
+ actor: entry.actor,
396
+ createdAt: entry.createdAt,
397
+ updatedAt: entry.updatedAt,
398
+ };
399
+ });
400
+ return boundedAgentReply(c, capability, { ...page, items }, 200, freshDeliveries, undefined, Number(args.cursor ?? 0));
401
+ }
402
+ if (toolName === 'read_memory') {
403
+ const entry = readMemory({ scopeId: scopeId, entryId: args.entry_id, actor });
404
+ const cursor = args.cursor;
405
+ const offset = cursor ? Number(cursor.split('.')[1]) : 0;
406
+ const delivery = {
407
+ entryId: entry.id,
408
+ revision: entry.revision,
409
+ kind: 'body-fragment',
410
+ start: offset,
411
+ end: offset + 40,
412
+ };
413
+ const fragment = sliceMemoryBodyFragment({
414
+ body: entry.body,
415
+ revision: entry.revision,
416
+ ...(cursor ? { cursor } : {}),
417
+ alreadyDelivered: wasMemoryFragmentDelivered(getInternalMemoryBudgetContext(capability.conversationKey).id, delivery),
418
+ repeat: args.repeat === true,
419
+ });
420
+ return boundedAgentReply(c, capability, {
421
+ entry: {
422
+ id: entry.id,
423
+ key: entry.key,
424
+ title: entry.title,
425
+ revision: entry.revision,
426
+ ...(fragment.text !== undefined ? { body: fragment.text } : {}),
427
+ offset: fragment.offset,
428
+ totalCodePoints: fragment.totalCodePoints,
429
+ truncated: fragment.truncated,
430
+ ...(fragment.nextCursor ? { nextCursor: fragment.nextCursor } : {}),
431
+ ...(fragment.alreadyDelivered ? { alreadyDelivered: true } : {}),
432
+ },
433
+ }, 200, fragment.text !== undefined ? [{ ...delivery, end: offset + [...fragment.text].length }] : []);
434
+ }
435
+ if (toolName === 'search_memories') {
436
+ const page = searchMemories({
437
+ scopeId: scopeId,
438
+ query: args.query,
439
+ actor,
440
+ ...(typeof args.cursor === 'string' ? { cursor: args.cursor } : {}),
441
+ ...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
442
+ });
443
+ const ledgerId = getInternalMemoryBudgetContext(capability.conversationKey).id;
444
+ const freshDeliveries = [];
445
+ const items = page.items.map((entry) => {
446
+ const delivery = { entryId: entry.id, revision: entry.revision, kind: 'metadata' };
447
+ if (wasMemoryFragmentDelivered(ledgerId, delivery))
448
+ return { id: entry.id, revision: entry.revision, alreadyDelivered: true };
449
+ freshDeliveries.push(delivery);
450
+ return {
451
+ id: entry.id,
452
+ scopeId: entry.scopeId,
453
+ key: entry.key,
454
+ title: entry.title,
455
+ revision: entry.revision,
456
+ actor: entry.actor,
457
+ createdAt: entry.createdAt,
458
+ updatedAt: entry.updatedAt,
459
+ };
460
+ });
461
+ return boundedAgentReply(c, capability, { ...page, items }, 200, freshDeliveries, undefined, Number(args.cursor ?? 0));
462
+ }
463
+ if (toolName === 'list_memory_operations') {
464
+ return boundedAgentReply(c, capability, listMemoryOperations({
465
+ scopeId: scopeId,
466
+ workspaceId: capability.workspaceId,
467
+ actor,
468
+ ...(typeof args.cursor === 'string' ? { cursor: args.cursor } : {}),
469
+ ...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
470
+ }));
471
+ }
472
+ if (capability.readOnly)
473
+ return boundedAgentReply(c, capability, { error: 'This agent launch is read-only for memory' }, 403);
474
+ writeReservation = reserveAgentWrite(capability);
475
+ if (!writeReservation.accepted) {
476
+ return exhaustedAgentReply(c, capability, 'Budget mémoire épuisé; aucune écriture mémoire n’a été effectuée.');
477
+ }
478
+ const result = remember({
479
+ scopeId: scopeId,
480
+ expectedGeneration: args.expected_generation,
481
+ key: args.key,
482
+ title: args.title,
483
+ body: args.body,
484
+ ...(typeof args.entry_id === 'string' ? { targetEntryId: args.entry_id } : {}),
485
+ ...(typeof args.expected_revision === 'number' ? { expectedRevision: args.expected_revision } : {}),
486
+ actor,
487
+ });
488
+ const receipt = result.status === 'applied'
489
+ ? {
490
+ status: result.status,
491
+ entry: {
492
+ id: result.entry.id,
493
+ scopeId: result.entry.scopeId,
494
+ key: result.entry.key,
495
+ revision: result.entry.revision,
496
+ updatedAt: result.entry.updatedAt,
497
+ },
498
+ }
499
+ : result.status === 'proposed'
500
+ ? {
501
+ status: result.status,
502
+ proposal: {
503
+ id: result.proposal.id,
504
+ scopeId: result.proposal.scopeId,
505
+ generation: result.proposal.generation,
506
+ },
507
+ }
508
+ : { status: result.status, reason: result.reason };
509
+ return boundedAgentReply(c, capability, receipt, 200, [], writeReservation);
510
+ }
511
+ catch (error) {
512
+ const status = error instanceof MemoryNotFoundError
513
+ ? 404
514
+ : error instanceof MemoryConflictError
515
+ ? 409
516
+ : error instanceof TypeError
517
+ ? 400
518
+ : 500;
519
+ const message = error instanceof Error ? error.message : String(error);
520
+ if (writeReservation?.accepted)
521
+ return boundedAgentReply(c, capability, { error: message }, status, [], writeReservation);
522
+ return boundedAgentReply(c, capability, { error: message }, status);
523
+ }
524
+ });
525
+ export default app;
@@ -1,4 +1,5 @@
1
1
  import { Hono } from 'hono';
2
+ import { isMemoryMode } from '../../shared/memory.js';
2
3
  import { isValidSkillSuite } from '../../shared/skill-suite-prompts.js';
3
4
  import { isWorkflowPolicy } from '../../shared/workflow-policy.js';
4
5
  import { getDb } from '../db/index.js';
@@ -164,6 +165,8 @@ app.put('/global', async (c) => {
164
165
  return c.json({ error: 'Invalid JSON body' }, 400);
165
166
  if (body.workflowPolicy !== undefined && !isWorkflowPolicy(body.workflowPolicy))
166
167
  return c.json({ error: 'Invalid workflowPolicy' }, 400);
168
+ if (body.memoryMode !== undefined && !isMemoryMode(body.memoryMode))
169
+ return c.json({ error: 'Invalid memoryMode' }, 400);
167
170
  const updated = settingsService.updateGlobalSettings(body);
168
171
  // The client assigns this response straight into its store, so echoing the
169
172
  // real credentials back would undo the masking on GET the first time
@@ -492,6 +492,9 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
492
492
  if (body.comparisonId !== undefined && typeof body.comparisonId !== 'string') {
493
493
  return c.json({ error: 'comparisonId must be a string' }, 400);
494
494
  }
495
+ if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== 'string'))) {
496
+ return c.json({ error: 'tags must be an array of strings' }, 400);
497
+ }
495
498
  creationId = typeof body.creationId === 'string' && body.creationId.length > 0 ? body.creationId : undefined;
496
499
  currentStep = 'validate';
497
500
  emitCreateProgress(creationId, currentStep);
@@ -841,7 +844,7 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
841
844
  // Skip any tag the user has removed from the catalogue so we respect
842
845
  // their choice (they may have pruned "notion"/"sentry" on purpose).
843
846
  const catalogTags = new Set(globalSettings.tags ?? []);
844
- const autoTags = [];
847
+ const autoTags = [...(body.tags ?? [])];
845
848
  if (body.notionUrl && catalogTags.has('notion'))
846
849
  autoTags.push('notion');
847
850
  if (body.sentryUrl && catalogTags.has('sentry'))
@@ -179,6 +179,10 @@ export function mapSdkMessage(msg, state) {
179
179
  return event;
180
180
  });
181
181
  }
182
+ function sdkCompactionId(msg) {
183
+ const uuid = msg.uuid;
184
+ return typeof uuid === 'string' && uuid.length > 0 ? uuid : undefined;
185
+ }
182
186
  function mapSdkMessagePayload(msg, state) {
183
187
  // Treat as a generic record — the SDK discriminated union is too broad to
184
188
  // narrow per branch here.
@@ -230,7 +234,10 @@ function mapSdkMessagePayload(msg, state) {
230
234
  }
231
235
  if (type === 'system') {
232
236
  if (subtype === 'compact' || subtype === 'compact_boundary') {
233
- events.push({ kind: 'session:compacted' });
237
+ events.push({
238
+ kind: 'session:compacted',
239
+ ...(sdkCompactionId(msg) ? { compactionId: sdkCompactionId(msg) } : {}),
240
+ });
234
241
  return events;
235
242
  }
236
243
  // Live status message: the SDK announces it is compacting context before the
@@ -238,7 +245,11 @@ function mapSdkMessagePayload(msg, state) {
238
245
  // UI can tell the user "compacting…" instead of looking frozen.
239
246
  if (subtype === 'status') {
240
247
  const sdkStatus = typeof parsed.status === 'string' ? parsed.status : null;
241
- events.push({ kind: 'session:compacting', active: sdkStatus === 'compacting' });
248
+ events.push({
249
+ kind: 'session:compacting',
250
+ active: sdkStatus === 'compacting',
251
+ ...(sdkCompactionId(msg) ? { compactionId: sdkCompactionId(msg) } : {}),
252
+ });
242
253
  return events;
243
254
  }
244
255
  if (subtype === 'task_started' ||
@@ -182,7 +182,7 @@ export function handleItemStarted(item, state) {
182
182
  return events;
183
183
  }
184
184
  if (item.type === 'contextCompaction') {
185
- events.push({ kind: 'session:compacting', active: true });
185
+ events.push({ kind: 'session:compacting', active: true, compactionId: item.id });
186
186
  return events;
187
187
  }
188
188
  return events;
@@ -193,7 +193,7 @@ export function handleItemStarted(item, state) {
193
193
  export function handleItemCompleted(item, state) {
194
194
  const events = [];
195
195
  if (item.type === 'contextCompaction')
196
- return [{ kind: 'session:compacted' }];
196
+ return [{ kind: 'session:compacted', compactionId: item.id }];
197
197
  if (item.type === 'agentMessage') {
198
198
  const text = item.text ?? '';
199
199
  const messageId = scopedId(state, item.id);