@loicngr/kobo 2.1.7 → 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 (188) hide show
  1. package/AGENTS.md +16 -0
  2. package/CHANGELOG.md +11 -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-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 +525 -0
  15. package/dist/server/routes/settings.js +3 -0
  16. package/dist/server/services/agent/engines/claude-code/event-mapper.js +13 -2
  17. package/dist/server/services/agent/engines/codex/event-mapper.js +2 -2
  18. package/dist/server/services/agent/orchestrator.js +110 -6
  19. package/dist/server/services/memory-agent-runtime.js +65 -0
  20. package/dist/server/services/memory-budget-service.js +183 -0
  21. package/dist/server/services/memory-context-service.js +287 -0
  22. package/dist/server/services/memory-mcp-service.js +339 -0
  23. package/dist/server/services/memory-service.js +891 -0
  24. package/dist/server/services/settings-service.js +19 -0
  25. package/dist/server/utils/memory-project-identity.js +17 -0
  26. package/dist/server/utils/memory-token-budget.js +221 -0
  27. package/dist/shared/codex-models.js +24 -0
  28. package/dist/shared/memory-prompts.js +18 -0
  29. package/dist/shared/memory-tools.js +215 -0
  30. package/dist/shared/memory.js +81 -0
  31. package/dist/shared/models.js +42 -0
  32. package/package.json +1 -1
  33. package/src/client/dist/pwa/assets/{ActivityFeed-BfMUdSXQ.css → ActivityFeed-DdJvMzRI.css} +1 -1
  34. package/src/client/dist/pwa/assets/ActivityFeed-w8TNfMKd.js +2 -0
  35. package/src/client/dist/pwa/assets/ChangelogPage-B1rkKskD.js +1 -0
  36. package/src/client/dist/pwa/assets/ClosePopup-CRV2hxHS.js +1 -0
  37. package/src/client/dist/pwa/assets/{ComparisonPanel-azVvpfGW.js → ComparisonPanel-kMWS9il3.js} +2 -2
  38. package/src/client/dist/pwa/assets/CreatePage-DOamHqeH.js +2 -0
  39. package/src/client/dist/pwa/assets/DashboardPage-Ds6Y8xlV.js +1 -0
  40. package/src/client/dist/pwa/assets/DiffViewer-j7rwPKxU.js +8 -0
  41. package/src/client/dist/pwa/assets/DocumentsPanel-BTp3J6Rq.js +1 -0
  42. package/src/client/dist/pwa/assets/{DrawerToggleButton-CQVSfKpY.js → DrawerToggleButton-DcVhAbjN.js} +1 -1
  43. package/src/client/dist/pwa/assets/GitPanel-DOugIJUS.js +3 -0
  44. package/src/client/dist/pwa/assets/HealthPage-BSrTmp1k.js +1 -0
  45. package/src/client/dist/pwa/assets/{MainLayout-CFLswVqP.css → MainLayout-DAcSwgUw.css} +1 -1
  46. package/src/client/dist/pwa/assets/MainLayout-DWzXL1KW.js +2 -0
  47. package/src/client/dist/pwa/assets/MemoryPanel-BivQoMQ6.js +1 -0
  48. package/src/client/dist/pwa/assets/MemoryPanel-Ch3qeyQw.css +1 -0
  49. package/src/client/dist/pwa/assets/MemoryProposalList-BnoVbFsj.js +1 -0
  50. package/src/client/dist/pwa/assets/MemoryProposalList-OmDpl3o5.css +1 -0
  51. package/src/client/dist/pwa/assets/QBadge-CW3Lw-kL.js +1 -0
  52. package/src/client/dist/pwa/assets/QBanner-BT2ZemYu.js +1 -0
  53. package/src/client/dist/pwa/assets/QBtn-BUSdPaf4.js +1 -0
  54. package/src/client/dist/pwa/assets/QBtnDropdown-B9xNOKD0.js +1 -0
  55. package/src/client/dist/pwa/assets/QBtnGroup-CnM4vSJA.js +1 -0
  56. package/src/client/dist/pwa/assets/QBtnToggle-DccOOtbh.js +1 -0
  57. package/src/client/dist/pwa/assets/QChip-Be16M4Ig.js +1 -0
  58. package/src/client/dist/pwa/assets/QExpansionItem-BBTTV_37.js +1 -0
  59. package/src/client/dist/pwa/assets/QItemSection-MdJ1_ZK7.js +1 -0
  60. package/src/client/dist/pwa/assets/QList-Cx1UfaUm.js +1 -0
  61. package/src/client/dist/pwa/assets/QMarkupTable-UhP7fVUp.js +1 -0
  62. package/src/client/dist/pwa/assets/QPage-CqkGHokx.js +1 -0
  63. package/src/client/dist/pwa/assets/QScrollArea-bm6nYMnB.js +1 -0
  64. package/src/client/dist/pwa/assets/QSelect-CPE9LsIn.js +1 -0
  65. package/src/client/dist/pwa/assets/QSpinnerDots-xdWyaE1i.js +1 -0
  66. package/src/client/dist/pwa/assets/QTooltip-CgVCYatx.js +1 -0
  67. package/src/client/dist/pwa/assets/SchedulePanel-DNwt3V1a.js +1 -0
  68. package/src/client/dist/pwa/assets/SearchPage-Dr43rGji.js +1 -0
  69. package/src/client/dist/pwa/assets/SettingsPage-C1dcMqCO.css +1 -0
  70. package/src/client/dist/pwa/assets/SettingsPage-oSGNwimG.js +9 -0
  71. package/src/client/dist/pwa/assets/SplitWorkspacePage-Cjd_Lq-0.js +1 -0
  72. package/src/client/dist/pwa/assets/{TerminalPanel-auHWDK9i.js → TerminalPanel-Bsrg96VZ.js} +9 -9
  73. package/src/client/dist/pwa/assets/ToolCallItem-M3CSguac.js +8 -0
  74. package/src/client/dist/pwa/assets/TouchPan-CxThkotY.js +1 -0
  75. package/src/client/dist/pwa/assets/{TourReplayButton-uCZpvYlj.js → TourReplayButton-COI4vR7q.js} +1 -1
  76. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CqlhQOqG.js +1 -0
  77. package/src/client/dist/pwa/assets/WorkspacePage-C-JXKt-S.js +4 -0
  78. package/src/client/dist/pwa/assets/{browser-CU7taghA.js → browser-DuFAaYHh.js} +1 -1
  79. package/src/client/dist/pwa/assets/build-path-tree-DFwB0SSs.js +1 -0
  80. package/src/client/dist/pwa/assets/custom-sounds-DTV-xHVv.js +1 -0
  81. package/src/client/dist/pwa/assets/de-YFD4X2nQ.js +17 -0
  82. package/src/client/dist/pwa/assets/detached-fullscreen-Du3TcYdX.js +1 -0
  83. package/src/client/dist/pwa/assets/dom-BtYTvLeN.js +2 -0
  84. package/src/client/dist/pwa/assets/{editor.api-CBaHnF7Q.js → editor.api-DDwuA_C9.js} +1 -1
  85. package/src/client/dist/pwa/assets/efforts-DU9dTnaN.js +9 -0
  86. package/src/client/dist/pwa/assets/es-BvRPXBHq.js +17 -0
  87. package/src/client/dist/pwa/assets/expand-template-LaRhdYUp.js +1 -0
  88. package/src/client/dist/pwa/assets/{format-MtzcnR3z.js → format-BUmAn8W2.js} +1 -1
  89. package/src/client/dist/pwa/assets/formatters-DfLD95iT.js +1 -0
  90. package/src/client/dist/pwa/assets/fr-CZhjMFZl.js +17 -0
  91. package/src/client/dist/pwa/assets/{freemarker2-BtVwKuc0.js → freemarker2-DVQM2t3e.js} +1 -1
  92. package/src/client/dist/pwa/assets/{handlebars-Bhl5yJrV.js → handlebars-ChiPNy9j.js} +1 -1
  93. package/src/client/dist/pwa/assets/{html-DKXLlkkM.js → html-CnGW31DH.js} +1 -1
  94. package/src/client/dist/pwa/assets/i18n-v1pgIO4z.js +1 -0
  95. package/src/client/dist/pwa/assets/index-DPN7dSDH.js +20 -0
  96. package/src/client/dist/pwa/assets/it-B4rz5PZO.js +17 -0
  97. package/src/client/dist/pwa/assets/{javascript-Cdfy_faM.js → javascript-BmeNra1c.js} +1 -1
  98. package/src/client/dist/pwa/assets/{liquid-0NoEATfB.js → liquid-C5qQFvIu.js} +1 -1
  99. package/src/client/dist/pwa/assets/{mdx-hvP0EAwa.js → mdx-TsWwhlKp.js} +1 -1
  100. package/src/client/dist/pwa/assets/{monaco-contributions-Be_o6s3m.js → monaco-contributions-SMzL7GRn.js} +1 -1
  101. package/src/client/dist/pwa/assets/monaco.contribution-sN2BZtSW.js +2 -0
  102. package/src/client/dist/pwa/assets/network-auth-DXsDKXrM.js +1 -0
  103. package/src/client/dist/pwa/assets/{notify-theme-I_sWGwy6.js → notify-theme-BXJypfo4.js} +1 -1
  104. package/src/client/dist/pwa/assets/{permissionModes-BDi77lhg.js → permissionModes-DLWiBfdb.js} +1 -1
  105. package/src/client/dist/pwa/assets/{private.use-form-vxvbh_VN.js → private.use-form-DY1BkePL.js} +1 -1
  106. package/src/client/dist/pwa/assets/{project-color-i2bg2TfP.js → project-color-CBLQt6bM.js} +1 -1
  107. package/src/client/dist/pwa/assets/{python-Bz-bbYMw.js → python-CKHf2s-D.js} +1 -1
  108. package/src/client/dist/pwa/assets/{razor-CyafkF-1.js → razor-D_5X4O5M.js} +1 -1
  109. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DY1JkAEA.js +1 -0
  110. package/src/client/dist/pwa/assets/runtime-core.esm-bundler-Bqg71Vll.js +1 -0
  111. package/src/client/dist/pwa/assets/{typescript-BSLweq4l.js → typescript-D62emDGo.js} +1 -1
  112. package/src/client/dist/pwa/assets/use-checkbox-v4aAbE6K.js +1 -0
  113. package/src/client/dist/pwa/assets/use-dark-BHObfh0O.js +1 -0
  114. package/src/client/dist/pwa/assets/use-id-BrdYXce0.js +1 -0
  115. package/src/client/dist/pwa/assets/use-is-mobile-CDB_rzM9.js +1 -0
  116. package/src/client/dist/pwa/assets/use-search-index-status-bIKbEP5q.js +1 -0
  117. package/src/client/dist/pwa/assets/{use-slide-transition-C5eDGOmu.js → use-slide-transition-B_Nu6u-t.js} +1 -1
  118. package/src/client/dist/pwa/assets/use-tours-CTNYIX-Q.js +2 -0
  119. package/src/client/dist/pwa/assets/use-worktree-restore-D69uzYON.js +1 -0
  120. package/src/client/dist/pwa/assets/workspace-templates-CSwgMrUf.js +1 -0
  121. package/src/client/dist/pwa/assets/{xml-DOY7F605.js → xml-Kyg_K_5X.js} +1 -1
  122. package/src/client/dist/pwa/assets/{yaml-CnWF0ZRJ.js → yaml-DC__PKjj.js} +1 -1
  123. package/src/client/dist/pwa/index.html +10 -10
  124. package/src/client/dist/pwa/sw.js +1 -1
  125. package/src/mcp-server/README.md +23 -0
  126. package/src/mcp-server/kobo-tasks-server.ts +38 -1
  127. package/src/mcp-server/memory-client.ts +52 -0
  128. package/src/mcp-server/workspace-dialogue-client.ts +4 -1
  129. package/src/client/dist/pwa/assets/ActivityFeed-B8tJgXwD.js +0 -2
  130. package/src/client/dist/pwa/assets/ChangelogPage-YjWYsB_s.js +0 -1
  131. package/src/client/dist/pwa/assets/ClosePopup-C1a82pL9.js +0 -1
  132. package/src/client/dist/pwa/assets/CreatePage-BVNtCTdA.js +0 -2
  133. package/src/client/dist/pwa/assets/DashboardPage-DydscFLA.js +0 -1
  134. package/src/client/dist/pwa/assets/DiffViewer-DfMqxOYw.js +0 -8
  135. package/src/client/dist/pwa/assets/DocumentsPanel-DCGgj9pc.js +0 -1
  136. package/src/client/dist/pwa/assets/GitPanel-D6ZCZPbj.js +0 -3
  137. package/src/client/dist/pwa/assets/HealthPage-D_A6DTiH.js +0 -1
  138. package/src/client/dist/pwa/assets/MainLayout-CeHg73-f.js +0 -2
  139. package/src/client/dist/pwa/assets/QBadge-rGILsUYl.js +0 -1
  140. package/src/client/dist/pwa/assets/QBanner-CtppfNDn.js +0 -1
  141. package/src/client/dist/pwa/assets/QBtn-b_baQtAt.js +0 -1
  142. package/src/client/dist/pwa/assets/QBtnDropdown-D8I04HPE.js +0 -1
  143. package/src/client/dist/pwa/assets/QBtnGroup-CUmAgBV8.js +0 -1
  144. package/src/client/dist/pwa/assets/QBtnToggle-COL46j6W.js +0 -1
  145. package/src/client/dist/pwa/assets/QChip-Btg1Lp_6.js +0 -1
  146. package/src/client/dist/pwa/assets/QExpansionItem-DmzCAbka.js +0 -1
  147. package/src/client/dist/pwa/assets/QItemSection-BhGnulVQ.js +0 -1
  148. package/src/client/dist/pwa/assets/QList-Cw2xs69W.js +0 -1
  149. package/src/client/dist/pwa/assets/QMarkupTable-Ck2E2TaI.js +0 -1
  150. package/src/client/dist/pwa/assets/QPage-BCDXjfVj.js +0 -1
  151. package/src/client/dist/pwa/assets/QScrollArea-BmBuij9J.js +0 -1
  152. package/src/client/dist/pwa/assets/QSelect-DUuWu79L.js +0 -1
  153. package/src/client/dist/pwa/assets/QSpinnerDots-BsKmccpX.js +0 -1
  154. package/src/client/dist/pwa/assets/QTooltip-Dhy3hNTO.js +0 -1
  155. package/src/client/dist/pwa/assets/SchedulePanel-BCiV6rMr.js +0 -1
  156. package/src/client/dist/pwa/assets/SearchPage-of0gPqV4.js +0 -1
  157. package/src/client/dist/pwa/assets/SettingsPage-BPg3-Lhb.js +0 -9
  158. package/src/client/dist/pwa/assets/SettingsPage-CpykRKq2.css +0 -1
  159. package/src/client/dist/pwa/assets/SplitWorkspacePage-BcdPVE9B.js +0 -1
  160. package/src/client/dist/pwa/assets/ToolCallItem-BQmqNcfF.js +0 -8
  161. package/src/client/dist/pwa/assets/TouchPan-DKpZbMAf.js +0 -1
  162. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-jDKgkf3G.js +0 -1
  163. package/src/client/dist/pwa/assets/WorkspacePage-DQ-jvLEj.js +0 -4
  164. package/src/client/dist/pwa/assets/build-path-tree-fw6KlTB4.js +0 -1
  165. package/src/client/dist/pwa/assets/custom-sounds-Dfi-grUv.js +0 -1
  166. package/src/client/dist/pwa/assets/de-C_20Oxyv.js +0 -17
  167. package/src/client/dist/pwa/assets/efforts-CNJXztO0.js +0 -9
  168. package/src/client/dist/pwa/assets/es-B3_j4CWr.js +0 -17
  169. package/src/client/dist/pwa/assets/expand-template-BtpC0GKE.js +0 -1
  170. package/src/client/dist/pwa/assets/formatters-BZm4POkA.js +0 -1
  171. package/src/client/dist/pwa/assets/fr-DbPOG-xd.js +0 -17
  172. package/src/client/dist/pwa/assets/i18n-B9eF3pgw.js +0 -1
  173. package/src/client/dist/pwa/assets/i18n-CPFleoLe.js +0 -19
  174. package/src/client/dist/pwa/assets/index-BVJWwOvK.js +0 -2
  175. package/src/client/dist/pwa/assets/it--6niM3qU.js +0 -17
  176. package/src/client/dist/pwa/assets/latest-request-7rgsEfKZ.js +0 -1
  177. package/src/client/dist/pwa/assets/monaco.contribution-DRQLCBp8.js +0 -2
  178. package/src/client/dist/pwa/assets/network-auth-DVXmkvPH.js +0 -1
  179. package/src/client/dist/pwa/assets/preload-helper-DjzSQ1t3.js +0 -1
  180. package/src/client/dist/pwa/assets/reactivity.esm-bundler-tuEvQ47f.js +0 -1
  181. package/src/client/dist/pwa/assets/runtime-core.esm-bundler-DOiZcasU.js +0 -1
  182. package/src/client/dist/pwa/assets/use-checkbox-BAt9fjK-.js +0 -1
  183. package/src/client/dist/pwa/assets/use-id-b4A2I8LB.js +0 -1
  184. package/src/client/dist/pwa/assets/use-is-mobile-DLIZxEo9.js +0 -1
  185. package/src/client/dist/pwa/assets/use-search-index-status-BzsGSHGh.js +0 -1
  186. package/src/client/dist/pwa/assets/use-tours-CM4E53aB.js +0 -2
  187. package/src/client/dist/pwa/assets/use-worktree-restore-C__ResAR.js +0 -1
  188. package/src/client/dist/pwa/assets/workspace-templates-TOa0yVyr.js +0 -1
@@ -0,0 +1,891 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { deriveMemoryActor, isMemoryScope, MEMORY_BODY_MAX_CHARS, MEMORY_PAGE_SIZE, MEMORY_PAGE_SIZE_MAX, MEMORY_TITLE_MAX_CHARS, memoryWriteDecision, } from '../../shared/memory.js';
3
+ import { getDb } from '../db/index.js';
4
+ import { normalizeMemoryProjectPath } from '../utils/memory-project-identity.js';
5
+ import { getGlobalSettings } from './settings-service.js';
6
+ import { broadcastAll } from './websocket-service.js';
7
+ export class MemoryNotFoundError extends Error {
8
+ constructor(message = 'Memory resource not found') {
9
+ super(message);
10
+ this.name = 'MemoryNotFoundError';
11
+ }
12
+ }
13
+ export class MemoryConflictError extends Error {
14
+ constructor(message = 'Memory changed; reload before retrying') {
15
+ super(message);
16
+ this.name = 'MemoryConflictError';
17
+ }
18
+ }
19
+ function mapScope(row) {
20
+ return {
21
+ id: row.id,
22
+ level: row.level,
23
+ ...(row.project_path ? { projectPath: row.project_path } : {}),
24
+ ...(row.workspace_id ? { workspaceId: row.workspace_id } : {}),
25
+ generation: row.generation,
26
+ revision: row.revision,
27
+ };
28
+ }
29
+ function mapActor(row) {
30
+ if (row.actor_kind === 'human')
31
+ return { kind: 'human' };
32
+ if (row.actor_kind === 'internal-agent') {
33
+ if (!row.source_engine) {
34
+ throw new Error('Invalid persisted internal memory provenance');
35
+ }
36
+ if (!row.source_workspace_id || !row.source_session_id) {
37
+ return {
38
+ kind: 'internal-agent',
39
+ workspaceId: row.source_workspace_id,
40
+ sessionId: row.source_session_id,
41
+ engine: row.source_engine,
42
+ sourceDeleted: true,
43
+ };
44
+ }
45
+ return {
46
+ kind: 'internal-agent',
47
+ workspaceId: row.source_workspace_id,
48
+ sessionId: row.source_session_id,
49
+ engine: row.source_engine,
50
+ };
51
+ }
52
+ if (!row.client_name || !row.transport)
53
+ throw new Error('Invalid persisted external memory provenance');
54
+ return { kind: 'external-mcp', clientName: row.client_name, transport: row.transport };
55
+ }
56
+ function mapEntry(row) {
57
+ return {
58
+ id: row.id,
59
+ scopeId: row.scope_id,
60
+ key: row.memory_key,
61
+ title: row.title,
62
+ body: row.body,
63
+ revision: row.revision,
64
+ actor: mapActor(row),
65
+ createdAt: row.created_at,
66
+ updatedAt: row.updated_at,
67
+ };
68
+ }
69
+ function mapProposal(row) {
70
+ return {
71
+ id: row.id,
72
+ scopeId: row.scope_id,
73
+ ...(row.target_entry_id ? { targetEntryId: row.target_entry_id } : {}),
74
+ ...(row.base_revision !== null ? { baseRevision: row.base_revision } : {}),
75
+ generation: row.generation,
76
+ key: row.memory_key,
77
+ title: row.title,
78
+ body: row.body,
79
+ actor: mapActor(row),
80
+ createdAt: row.created_at,
81
+ };
82
+ }
83
+ function mapOperation(row) {
84
+ const result = {
85
+ id: row.id,
86
+ scopeId: row.scope_id,
87
+ kind: row.operation,
88
+ actor: mapActor(row),
89
+ createdAt: row.created_at,
90
+ ...(row.entry_id ? { entryId: row.entry_id } : {}),
91
+ ...(row.proposal_id ? { proposalId: row.proposal_id } : {}),
92
+ ...(row.revision !== null ? { revision: row.revision } : {}),
93
+ ...(row.affected_entries !== null ? { affectedEntries: row.affected_entries } : {}),
94
+ ...(row.affected_proposals !== null ? { affectedProposals: row.affected_proposals } : {}),
95
+ };
96
+ return result;
97
+ }
98
+ function projectPathForActor(actor) {
99
+ if (actor.kind !== 'internal-agent')
100
+ return null;
101
+ const workspace = getDb().prepare('SELECT project_path FROM workspaces WHERE id = ?').get(actor.workspaceId);
102
+ return workspace ? normalizeMemoryProjectPath(workspace.project_path) : null;
103
+ }
104
+ function now() {
105
+ return new Date().toISOString();
106
+ }
107
+ function requireBoundedPage(limit) {
108
+ const resolved = limit ?? MEMORY_PAGE_SIZE;
109
+ if (!Number.isInteger(resolved) || resolved < 1 || resolved > MEMORY_PAGE_SIZE_MAX) {
110
+ throw new TypeError(`Page size must be between 1 and ${MEMORY_PAGE_SIZE_MAX}`);
111
+ }
112
+ return resolved;
113
+ }
114
+ function requireCursor(cursor) {
115
+ if (cursor === undefined)
116
+ return 0;
117
+ if (!/^\d+$/.test(cursor))
118
+ throw new TypeError('Invalid memory cursor');
119
+ const offset = Number(cursor);
120
+ if (!Number.isSafeInteger(offset))
121
+ throw new TypeError('Invalid memory cursor');
122
+ return offset;
123
+ }
124
+ function requireText(value, name, max) {
125
+ if (typeof value !== 'string')
126
+ throw new TypeError(`${name} must be a string`);
127
+ const trimmed = value.trim();
128
+ if (!trimmed || trimmed.length > max)
129
+ throw new TypeError(`${name} must be 1-${max} characters`);
130
+ return trimmed;
131
+ }
132
+ function requireKey(value) {
133
+ if (typeof value !== 'string')
134
+ throw new TypeError('Memory key must be a string');
135
+ const key = value.trim().toLowerCase();
136
+ if (!/^[a-z0-9][a-z0-9._/-]{0,79}$/.test(key))
137
+ throw new TypeError('Memory key must be a stable lowercase identifier');
138
+ return key;
139
+ }
140
+ function getScopeById(scopeId) {
141
+ const row = getDb().prepare('SELECT * FROM memory_scopes WHERE id = ?').get(scopeId);
142
+ if (!row)
143
+ throw new MemoryNotFoundError('Memory scope not found');
144
+ return row;
145
+ }
146
+ /** Resolve trusted source ownership and enforce the three exact applicable scopes. */
147
+ function assertInternalActorCanAccessScope(actor, scope) {
148
+ if (actor.kind !== 'internal-agent')
149
+ return;
150
+ const source = getDb()
151
+ .prepare(`SELECT s.workspace_id, s.engine, w.project_path
152
+ FROM agent_sessions s JOIN workspaces w ON w.id = s.workspace_id WHERE s.id = ?`)
153
+ .get(actor.sessionId);
154
+ if (!source || source.workspace_id !== actor.workspaceId) {
155
+ throw new MemoryNotFoundError('Agent session is not bound to the stated workspace');
156
+ }
157
+ if (source.engine !== 'claude-code' && source.engine !== 'codex') {
158
+ throw new MemoryNotFoundError('Bound agent session has no valid engine');
159
+ }
160
+ if (source.engine !== actor.engine)
161
+ throw new MemoryNotFoundError('Agent engine does not match the bound session');
162
+ const allowed = scope.level === 'global' ||
163
+ (scope.level === 'workspace' && scope.workspace_id === source.workspace_id) ||
164
+ (scope.level === 'project' &&
165
+ scope.project_path !== null &&
166
+ normalizeMemoryProjectPath(scope.project_path) === normalizeMemoryProjectPath(source.project_path));
167
+ if (!allowed)
168
+ throw new MemoryNotFoundError('Agent cannot access memory outside its applicable scopes');
169
+ }
170
+ function materializeScope(input) {
171
+ const db = getDb();
172
+ const timestamp = now();
173
+ if (input.level === 'global') {
174
+ db.prepare(`INSERT OR IGNORE INTO memory_scopes (id, level, generation, revision, created_at, updated_at)
175
+ VALUES (?, 'global', 0, 0, ?, ?)`).run(nanoid(), timestamp, timestamp);
176
+ const row = db.prepare("SELECT * FROM memory_scopes WHERE level = 'global'").get();
177
+ return mapScope(row);
178
+ }
179
+ if (input.level === 'project') {
180
+ const projectPath = normalizeMemoryProjectPath(input.projectPath);
181
+ db.prepare(`INSERT OR IGNORE INTO memory_scopes (id, level, project_path, generation, revision, created_at, updated_at)
182
+ VALUES (?, 'project', ?, 0, 0, ?, ?)`).run(nanoid(), projectPath, timestamp, timestamp);
183
+ const row = db
184
+ .prepare("SELECT * FROM memory_scopes WHERE level = 'project' AND project_path = ?")
185
+ .get(projectPath);
186
+ return mapScope(row);
187
+ }
188
+ if (input.level === 'workspace') {
189
+ const workspace = db.prepare('SELECT id FROM workspaces WHERE id = ?').get(input.workspaceId);
190
+ if (!workspace)
191
+ throw new MemoryNotFoundError('Workspace not found');
192
+ db.prepare(`INSERT OR IGNORE INTO memory_scopes (id, level, workspace_id, generation, revision, created_at, updated_at)
193
+ VALUES (?, 'workspace', ?, 0, 0, ?, ?)`).run(nanoid(), input.workspaceId, timestamp, timestamp);
194
+ const row = db
195
+ .prepare("SELECT * FROM memory_scopes WHERE level = 'workspace' AND workspace_id = ?")
196
+ .get(input.workspaceId);
197
+ return mapScope(row);
198
+ }
199
+ throw new TypeError('Invalid memory scope');
200
+ }
201
+ /** Resolve/create a durable exact scope; workspace project identity always comes from the workspace row. */
202
+ export function resolveMemoryScope(input) {
203
+ if (!isMemoryScope(input))
204
+ throw new TypeError('Invalid memory scope');
205
+ if (input.level === 'workspace') {
206
+ const workspace = getDb().prepare('SELECT project_path FROM workspaces WHERE id = ?').get(input.workspaceId);
207
+ if (!workspace)
208
+ throw new MemoryNotFoundError('Workspace not found');
209
+ materializeScope({ level: 'project', projectPath: workspace.project_path });
210
+ }
211
+ return materializeScope(input);
212
+ }
213
+ /** Lists persisted scopes; a workspace view always includes the exact applicable ancestors. */
214
+ export function listMemoryScopes(options = {}) {
215
+ const limit = requireBoundedPage(options.limit);
216
+ const offset = requireCursor(options.cursor);
217
+ const db = getDb();
218
+ const actor = options.actor ? deriveMemoryActor(options.actor) : undefined;
219
+ if (actor?.kind === 'internal-agent') {
220
+ if (options.workspaceId && options.workspaceId !== actor.workspaceId) {
221
+ throw new MemoryNotFoundError('Agent cannot list scopes for another workspace');
222
+ }
223
+ assertInternalActorCanAccessScope(actor, getScopeById(materializeScope({ level: 'global' }).id));
224
+ options = { ...options, workspaceId: actor.workspaceId };
225
+ }
226
+ if (options.workspaceId) {
227
+ const workspace = db.prepare('SELECT project_path FROM workspaces WHERE id = ?').get(options.workspaceId);
228
+ if (!workspace)
229
+ throw new MemoryNotFoundError('Workspace not found');
230
+ const scopes = [
231
+ materializeScope({ level: 'global' }),
232
+ materializeScope({ level: 'project', projectPath: workspace.project_path }),
233
+ materializeScope({ level: 'workspace', workspaceId: options.workspaceId }),
234
+ ];
235
+ return {
236
+ items: scopes.slice(offset, offset + limit),
237
+ ...(offset + limit < scopes.length ? { nextCursor: String(offset + limit) } : {}),
238
+ };
239
+ }
240
+ // A global scope is selectable even on a fresh install. Persisted project and
241
+ // workspace scopes remain discoverable after their source directories vanish.
242
+ materializeScope({ level: 'global' });
243
+ const rows = db
244
+ .prepare(`SELECT * FROM memory_scopes
245
+ ORDER BY CASE level WHEN 'global' THEN 0 WHEN 'project' THEN 1 ELSE 2 END, project_path, workspace_id, id
246
+ LIMIT ? OFFSET ?`)
247
+ .all(limit + 1, offset);
248
+ const hasMore = rows.length > limit;
249
+ return { items: rows.slice(0, limit).map(mapScope), ...(hasMore ? { nextCursor: String(offset + limit) } : {}) };
250
+ }
251
+ function writeOperation(scope, actor, operation, entryId, revision, timestamp, proposalId = null, affectedEntries = null, affectedProposals = null) {
252
+ const source = actor.kind === 'internal-agent'
253
+ ? [actor.workspaceId, actor.sessionId, actor.engine, null, null]
254
+ : actor.kind === 'external-mcp'
255
+ ? [null, null, null, actor.clientName, actor.transport]
256
+ : [null, null, null, null, null];
257
+ const sourceProjectPath = projectPathForActor(actor);
258
+ return Number(getDb()
259
+ .prepare(`INSERT INTO memory_operations
260
+ (scope_id, operation, actor_kind, source_workspace_id, source_session_id, source_engine, source_project_path, client_name, transport, entry_id, proposal_id, revision, affected_entries, affected_proposals, created_at)
261
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
262
+ .run(scope.id, operation, actor.kind, ...source.slice(0, 3), sourceProjectPath, ...source.slice(3), entryId, proposalId, revision, affectedEntries, affectedProposals, timestamp).lastInsertRowid);
263
+ }
264
+ function notifyCommitted(scope, operationId, journalOnly = false) {
265
+ broadcastAll('memory:changed', {
266
+ scopeId: scope.id,
267
+ level: scope.level,
268
+ revision: scope.revision,
269
+ generation: scope.generation,
270
+ operationId,
271
+ ...(journalOnly ? { journalOnly: true } : {}),
272
+ });
273
+ }
274
+ /** Create idempotently by exact scope/key, atomically journaling and revising the scope. */
275
+ export function createMemory(input) {
276
+ if (!input || typeof input !== 'object')
277
+ throw new TypeError('Invalid memory input');
278
+ const key = requireKey(input.key);
279
+ const title = requireText(input.title, 'Title', MEMORY_TITLE_MAX_CHARS);
280
+ const body = requireText(input.body, 'Body', MEMORY_BODY_MAX_CHARS);
281
+ const actor = deriveMemoryActor(input.actor);
282
+ const db = getDb();
283
+ let operationId;
284
+ let changedScope;
285
+ const entry = db
286
+ .transaction(() => {
287
+ const scope = getScopeById(input.scopeId);
288
+ assertInternalActorCanAccessScope(actor, scope);
289
+ const existing = db
290
+ .prepare('SELECT * FROM memory_entries WHERE scope_id = ? AND memory_key = ?')
291
+ .get(scope.id, key);
292
+ if (existing) {
293
+ if (existing.title === title && existing.body === body)
294
+ return mapEntry(existing);
295
+ throw new MemoryConflictError('A different memory already uses this key');
296
+ }
297
+ const timestamp = now();
298
+ const id = nanoid();
299
+ const source = actor.kind === 'internal-agent'
300
+ ? [actor.workspaceId, actor.sessionId, actor.engine, null, null]
301
+ : actor.kind === 'external-mcp'
302
+ ? [null, null, null, actor.clientName, actor.transport]
303
+ : [null, null, null, null, null];
304
+ db.prepare(`INSERT INTO memory_entries
305
+ (id, scope_id, memory_key, title, body, revision, actor_kind, source_workspace_id, source_session_id, source_engine, client_name, transport, created_at, updated_at)
306
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, scope.id, key, title, body, actor.kind, ...source, timestamp, timestamp);
307
+ const scopeRevision = db
308
+ .prepare('UPDATE memory_scopes SET revision = revision + 1, updated_at = ? WHERE id = ?')
309
+ .run(timestamp, scope.id);
310
+ if (scopeRevision.changes !== 1)
311
+ throw new MemoryNotFoundError('Memory scope not found');
312
+ const updatedScope = getScopeById(scope.id);
313
+ operationId = writeOperation(updatedScope, actor, 'created', id, 1, timestamp);
314
+ changedScope = updatedScope;
315
+ const row = db.prepare('SELECT * FROM memory_entries WHERE id = ?').get(id);
316
+ return mapEntry(row);
317
+ })
318
+ .immediate();
319
+ if (operationId !== undefined && changedScope)
320
+ notifyCommitted(changedScope, operationId);
321
+ return entry;
322
+ }
323
+ /** Compare-and-swap update; stale revisions conflict and all writes roll back together. */
324
+ export function updateMemory(input) {
325
+ if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1)
326
+ throw new TypeError('Invalid expected revision');
327
+ if (input.key === undefined && input.title === undefined && input.body === undefined)
328
+ throw new TypeError('At least one memory field is required');
329
+ const actor = deriveMemoryActor(input.actor);
330
+ const db = getDb();
331
+ let operationId;
332
+ let changedScope;
333
+ const entry = db
334
+ .transaction(() => {
335
+ const scope = getScopeById(input.scopeId);
336
+ const current = db
337
+ .prepare('SELECT * FROM memory_entries WHERE id = ? AND scope_id = ?')
338
+ .get(input.entryId, scope.id);
339
+ if (!current)
340
+ throw new MemoryNotFoundError('Memory entry not found in the selected scope');
341
+ if (current.revision !== input.expectedRevision)
342
+ throw new MemoryConflictError();
343
+ const key = input.key === undefined ? current.memory_key : requireKey(input.key);
344
+ const title = input.title === undefined ? current.title : requireText(input.title, 'Title', MEMORY_TITLE_MAX_CHARS);
345
+ const body = input.body === undefined ? current.body : requireText(input.body, 'Body', MEMORY_BODY_MAX_CHARS);
346
+ const keyOwner = db
347
+ .prepare('SELECT id FROM memory_entries WHERE scope_id = ? AND memory_key = ?')
348
+ .get(scope.id, key);
349
+ if (keyOwner && keyOwner.id !== current.id)
350
+ throw new MemoryConflictError('A different memory already uses this key');
351
+ assertInternalActorCanAccessScope(actor, scope);
352
+ const timestamp = now();
353
+ const updated = db.prepare(`UPDATE memory_entries SET memory_key = ?, title = ?, body = ?, revision = revision + 1,
354
+ actor_kind = ?, source_workspace_id = ?, source_session_id = ?, source_engine = ?, client_name = ?, transport = ?, updated_at = ?
355
+ WHERE id = ? AND scope_id = ? AND revision = ?`);
356
+ const source = actor.kind === 'internal-agent'
357
+ ? [actor.workspaceId, actor.sessionId, actor.engine, null, null]
358
+ : actor.kind === 'external-mcp'
359
+ ? [null, null, null, actor.clientName, actor.transport]
360
+ : [null, null, null, null, null];
361
+ const result = updated.run(key, title, body, actor.kind, ...source, timestamp, input.entryId, scope.id, input.expectedRevision);
362
+ if (result.changes !== 1)
363
+ throw new MemoryConflictError();
364
+ db.prepare('UPDATE memory_scopes SET revision = revision + 1, updated_at = ? WHERE id = ?').run(timestamp, scope.id);
365
+ const updatedScope = getScopeById(scope.id);
366
+ operationId = writeOperation(updatedScope, actor, 'updated', input.entryId, input.expectedRevision + 1, timestamp);
367
+ changedScope = updatedScope;
368
+ return mapEntry(db.prepare('SELECT * FROM memory_entries WHERE id = ?').get(input.entryId));
369
+ })
370
+ .immediate();
371
+ if (operationId !== undefined && changedScope)
372
+ notifyCommitted(changedScope, operationId);
373
+ return entry;
374
+ }
375
+ function applicableScopes(workspaceId) {
376
+ const workspace = getDb().prepare('SELECT project_path FROM workspaces WHERE id = ?').get(workspaceId);
377
+ if (!workspace)
378
+ throw new MemoryNotFoundError('Workspace not found');
379
+ return [
380
+ materializeScope({ level: 'global' }),
381
+ materializeScope({ level: 'project', projectPath: workspace.project_path }),
382
+ materializeScope({ level: 'workspace', workspaceId }),
383
+ ];
384
+ }
385
+ function listPage(scopeIds, options) {
386
+ const limit = requireBoundedPage(options.limit);
387
+ const offset = requireCursor(options.cursor);
388
+ if (!scopeIds.length)
389
+ return { items: [], totalCount: 0 };
390
+ const db = getDb();
391
+ const placeholders = scopeIds.map(() => '?').join(',');
392
+ let where = `scope_id IN (${placeholders})`;
393
+ const args = [...scopeIds];
394
+ if (options.query !== undefined) {
395
+ if (typeof options.query !== 'string' || options.query.trim().length < 1 || options.query.length > 200)
396
+ throw new TypeError('Search query must be 1-200 characters');
397
+ const escaped = options.query.replace(/[\\%_]/g, '\\$&');
398
+ where += " AND (title LIKE ? ESCAPE '\\' OR body LIKE ? ESCAPE '\\' OR memory_key LIKE ? ESCAPE '\\')";
399
+ const pattern = `%${escaped}%`;
400
+ args.push(pattern, pattern, pattern);
401
+ }
402
+ const totalCount = db.prepare(`SELECT COUNT(*) AS count FROM memory_entries WHERE ${where}`).get(...args).count;
403
+ const rows = db
404
+ .prepare(`SELECT * FROM memory_entries WHERE ${where}
405
+ ORDER BY CASE (SELECT level FROM memory_scopes WHERE id = scope_id) WHEN 'global' THEN 0 WHEN 'project' THEN 1 ELSE 2 END,
406
+ updated_at DESC, id DESC LIMIT ? OFFSET ?`)
407
+ .all(...args, limit + 1, offset);
408
+ const hasMore = rows.length > limit;
409
+ return {
410
+ items: rows.slice(0, limit).map(mapEntry),
411
+ totalCount,
412
+ ...(hasMore ? { nextCursor: String(offset + limit) } : {}),
413
+ };
414
+ }
415
+ export function listMemories(options) {
416
+ const actor = options.actor ? deriveMemoryActor(options.actor) : undefined;
417
+ if (actor?.kind === 'internal-agent') {
418
+ if (options.workspaceId && options.workspaceId !== actor.workspaceId) {
419
+ throw new MemoryNotFoundError('Agent cannot list another workspace memory');
420
+ }
421
+ if (options.scopeId)
422
+ assertInternalActorCanAccessScope(actor, getScopeById(options.scopeId));
423
+ else
424
+ options = { ...options, workspaceId: actor.workspaceId };
425
+ }
426
+ if (Boolean(options.scopeId) === Boolean(options.workspaceId))
427
+ throw new TypeError('Specify exactly one scopeId or workspaceId');
428
+ const scopeIds = options.scopeId
429
+ ? [getScopeById(options.scopeId).id]
430
+ : applicableScopes(options.workspaceId).map((scope) => scope.id);
431
+ const source = actor ?? { kind: 'human' };
432
+ const db = getDb();
433
+ const result = db
434
+ .transaction(() => {
435
+ const page = listPage(scopeIds, options);
436
+ return { page, reads: recordMemoryReads(page.items, source) };
437
+ })
438
+ .immediate();
439
+ for (const [scopeId, operationId] of result.reads)
440
+ notifyCommitted(getScopeById(scopeId), operationId, true);
441
+ return result.page;
442
+ }
443
+ /** Read by explicit scope and id; journal records the retrieval, not model use. */
444
+ export function readMemory(options) {
445
+ const actor = options.actor ? deriveMemoryActor(options.actor) : { kind: 'human' };
446
+ const db = getDb();
447
+ let operationId;
448
+ let changedScope;
449
+ const result = db
450
+ .transaction(() => {
451
+ const scope = getScopeById(options.scopeId);
452
+ assertInternalActorCanAccessScope(actor, scope);
453
+ const entry = db
454
+ .prepare('SELECT * FROM memory_entries WHERE id = ? AND scope_id = ?')
455
+ .get(options.entryId, scope.id);
456
+ if (!entry)
457
+ throw new MemoryNotFoundError('Memory entry not found in the selected scope');
458
+ const timestamp = now();
459
+ const actorSource = actor.kind === 'internal-agent'
460
+ ? [actor.workspaceId, actor.sessionId, actor.engine, null, null]
461
+ : actor.kind === 'external-mcp'
462
+ ? [null, null, null, actor.clientName, actor.transport]
463
+ : [null, null, null, null, null];
464
+ const sourceProjectPath = projectPathForActor(actor);
465
+ operationId = Number(db
466
+ .prepare(`INSERT INTO memory_operations
467
+ (scope_id, operation, actor_kind, source_workspace_id, source_session_id, source_engine, source_project_path, client_name, transport, entry_id, revision, created_at)
468
+ VALUES (?, 'read', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
469
+ .run(scope.id, actor.kind, ...actorSource.slice(0, 3), sourceProjectPath, ...actorSource.slice(3), entry.id, entry.revision, timestamp).lastInsertRowid);
470
+ changedScope = scope;
471
+ return mapEntry(entry);
472
+ })
473
+ .immediate();
474
+ if (operationId !== undefined && changedScope)
475
+ notifyCommitted(changedScope, operationId, true);
476
+ return result;
477
+ }
478
+ export function searchMemories(options) {
479
+ const scope = getScopeById(options.scopeId);
480
+ const actor = options.actor ? deriveMemoryActor(options.actor) : { kind: 'human' };
481
+ assertInternalActorCanAccessScope(actor, scope);
482
+ const db = getDb();
483
+ const result = db
484
+ .transaction(() => {
485
+ const page = listPage([options.scopeId], options);
486
+ return { page, reads: recordMemoryReads(page.items, actor) };
487
+ })
488
+ .immediate();
489
+ for (const [scopeId, operationId] of result.reads)
490
+ notifyCommitted(getScopeById(scopeId), operationId, true);
491
+ return result.page;
492
+ }
493
+ /** Store one content-free journal row per entry actually returned to the caller. */
494
+ function recordMemoryReads(entries, actor) {
495
+ const latestOperationByScope = new Map();
496
+ for (const entry of entries) {
497
+ const scope = getScopeById(entry.scopeId);
498
+ const operationId = writeOperation(scope, actor, 'read', entry.id, entry.revision, now());
499
+ latestOperationByScope.set(scope.id, operationId);
500
+ }
501
+ return latestOperationByScope;
502
+ }
503
+ export function listMemoryOperations(options) {
504
+ const scope = getScopeById(options.scopeId);
505
+ const actor = options.actor ? deriveMemoryActor(options.actor) : undefined;
506
+ if (actor?.kind === 'internal-agent') {
507
+ if (options.workspaceId && options.workspaceId !== actor.workspaceId)
508
+ throw new MemoryNotFoundError('Agent cannot view another workspace operation journal');
509
+ assertInternalActorCanAccessScope(actor, scope);
510
+ }
511
+ const viewWorkspaceId = options.workspaceId ?? (actor?.kind === 'internal-agent' ? actor.workspaceId : undefined);
512
+ let viewProjectPath;
513
+ let sameProjectWorkspaceIds;
514
+ if (viewWorkspaceId) {
515
+ const db = getDb();
516
+ const workspace = db.prepare('SELECT project_path FROM workspaces WHERE id = ?').get(viewWorkspaceId);
517
+ if (!workspace)
518
+ throw new MemoryNotFoundError('Workspace not found');
519
+ viewProjectPath = normalizeMemoryProjectPath(workspace.project_path);
520
+ if (scope.level === 'workspace' && scope.workspace_id !== viewWorkspaceId)
521
+ throw new MemoryNotFoundError('Workspace cannot view another workspace operation journal');
522
+ if (scope.level === 'project' && normalizeMemoryProjectPath(scope.project_path) !== viewProjectPath)
523
+ throw new MemoryNotFoundError('Workspace cannot view another project operation journal');
524
+ if (scope.level !== 'workspace') {
525
+ sameProjectWorkspaceIds = db.prepare('SELECT id, project_path FROM workspaces').all()
526
+ .filter((row) => normalizeMemoryProjectPath(row.project_path) === viewProjectPath)
527
+ .map((row) => row.id);
528
+ }
529
+ }
530
+ const limit = requireBoundedPage(options.limit);
531
+ const offset = requireCursor(options.cursor);
532
+ let sql = 'SELECT * FROM memory_operations WHERE scope_id = ?';
533
+ const args = [options.scopeId];
534
+ if (viewProjectPath && sameProjectWorkspaceIds) {
535
+ const legacySourceWorkspaceCondition = sameProjectWorkspaceIds.length
536
+ ? `source_project_path IS NULL AND source_workspace_id IN (${sameProjectWorkspaceIds.map(() => '?').join(',')})`
537
+ : '0';
538
+ sql += ` AND (actor_kind <> 'internal-agent' OR source_project_path = ? OR (${legacySourceWorkspaceCondition}))`;
539
+ args.push(viewProjectPath, ...sameProjectWorkspaceIds);
540
+ }
541
+ sql += ' ORDER BY id DESC LIMIT ? OFFSET ?';
542
+ args.push(limit + 1, offset);
543
+ const rows = getDb()
544
+ .prepare(sql)
545
+ .all(...args);
546
+ const hasMore = rows.length > limit;
547
+ return { items: rows.slice(0, limit).map(mapOperation), ...(hasMore ? { nextCursor: String(offset + limit) } : {}) };
548
+ }
549
+ /**
550
+ * List the operation journal across a workspace's exact three scopes.
551
+ * The cursor is the last global operation id (exclusive), not an offset per
552
+ * scope, so uneven scope distributions cannot repeat or skip later pages.
553
+ */
554
+ export function listWorkspaceMemoryOperations(workspaceId, options = {}) {
555
+ const limit = requireBoundedPage(options.limit);
556
+ const cursor = options.cursor === undefined ? undefined : requireCursor(options.cursor);
557
+ const db = getDb();
558
+ const workspace = db.prepare('SELECT project_path FROM workspaces WHERE id = ?').get(workspaceId);
559
+ if (!workspace)
560
+ throw new MemoryNotFoundError('Workspace not found');
561
+ const projectPath = normalizeMemoryProjectPath(workspace.project_path);
562
+ const sameProjectWorkspaceIds = db.prepare('SELECT id, project_path FROM workspaces').all()
563
+ .filter((row) => normalizeMemoryProjectPath(row.project_path) === projectPath)
564
+ .map((row) => row.id);
565
+ const scopes = listMemoryScopes({ workspaceId }).items;
566
+ const globalScope = scopes.find((scope) => scope.level === 'global');
567
+ const projectScope = scopes.find((scope) => scope.level === 'project');
568
+ const workspaceScope = scopes.find((scope) => scope.level === 'workspace');
569
+ if (!globalScope || !projectScope || !workspaceScope)
570
+ throw new Error('Applicable memory scopes are incomplete');
571
+ const scopeIds = [globalScope.id, projectScope.id, workspaceScope.id];
572
+ const placeholders = scopeIds.map(() => '?').join(', ');
573
+ const sameProjectPlaceholders = sameProjectWorkspaceIds.map(() => '?').join(', ');
574
+ const legacySourceWorkspaceCondition = sameProjectWorkspaceIds.length
575
+ ? `source_project_path IS NULL AND source_workspace_id IN (${sameProjectPlaceholders})`
576
+ : '0';
577
+ const query = `SELECT * FROM memory_operations
578
+ WHERE scope_id IN (${placeholders})
579
+ AND (scope_id = ? OR actor_kind <> 'internal-agent' OR source_project_path = ? OR (${legacySourceWorkspaceCondition}))
580
+ ${cursor === undefined ? '' : 'AND id < ?'}
581
+ ORDER BY id DESC LIMIT ?`;
582
+ const parameters = [
583
+ ...scopeIds,
584
+ workspaceScope.id,
585
+ projectPath,
586
+ ...sameProjectWorkspaceIds,
587
+ ...(cursor === undefined ? [] : [cursor]),
588
+ limit + 1,
589
+ ];
590
+ const rows = db.prepare(query).all(...parameters);
591
+ const page = rows.slice(0, limit);
592
+ return {
593
+ items: page.map(mapOperation),
594
+ ...(rows.length > limit && page.length ? { nextCursor: String(page.at(-1).id) } : {}),
595
+ };
596
+ }
597
+ function isHuman(actor) {
598
+ return actor.kind === 'human';
599
+ }
600
+ function assertHuman(actor, action) {
601
+ if (!isHuman(actor))
602
+ throw new MemoryNotFoundError(`${action} is available to the human operator only`);
603
+ }
604
+ function insertProposal(scope, actor, key, title, body, generation, targetEntryId, baseRevision) {
605
+ const db = getDb();
606
+ let changedScope;
607
+ let operationId;
608
+ const proposal = db
609
+ .transaction(() => {
610
+ const actorSource = actor.kind === 'internal-agent'
611
+ ? [actor.workspaceId, actor.sessionId, actor.engine, null, null]
612
+ : [null, null, null, actor.clientName, actor.transport];
613
+ const repeated = db
614
+ .prepare(`SELECT * FROM memory_proposals
615
+ WHERE scope_id = ? AND target_entry_id IS ? AND base_revision IS ? AND memory_key = ?
616
+ AND title = ? AND body = ? AND generation = ? AND actor_kind = ?
617
+ AND source_workspace_id IS ? AND source_session_id IS ? AND source_engine IS ?
618
+ AND client_name IS ? AND transport IS ?
619
+ ORDER BY created_at DESC, id DESC LIMIT 1`)
620
+ .get(scope.id, targetEntryId ?? null, baseRevision ?? null, key, title, body, generation, actor.kind, ...actorSource);
621
+ if (repeated)
622
+ return mapProposal(repeated);
623
+ const timestamp = now();
624
+ const id = nanoid();
625
+ db.prepare(`INSERT INTO memory_proposals
626
+ (id, scope_id, target_entry_id, base_revision, memory_key, title, body, generation, actor_kind,
627
+ source_workspace_id, source_session_id, source_engine, client_name, transport, created_at, updated_at)
628
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, scope.id, targetEntryId ?? null, baseRevision ?? null, key, title, body, generation, actor.kind, ...actorSource, timestamp, timestamp);
629
+ db.prepare('UPDATE memory_scopes SET revision = revision + 1, updated_at = ? WHERE id = ?').run(timestamp, scope.id);
630
+ changedScope = getScopeById(scope.id);
631
+ operationId = writeOperation(changedScope, actor, 'proposed', null, changedScope.revision, timestamp, id);
632
+ return mapProposal(db.prepare('SELECT * FROM memory_proposals WHERE id = ?').get(id));
633
+ })
634
+ .immediate();
635
+ if (changedScope && operationId !== undefined)
636
+ notifyCommitted(changedScope, operationId);
637
+ return proposal;
638
+ }
639
+ /** Agent-facing write boundary; reads the operating mode on every invocation. */
640
+ export function remember(input) {
641
+ const key = requireKey(input.key);
642
+ const title = requireText(input.title, 'Title', MEMORY_TITLE_MAX_CHARS);
643
+ const body = requireText(input.body, 'Body', MEMORY_BODY_MAX_CHARS);
644
+ const actor = deriveMemoryActor(input.actor);
645
+ if (actor.kind === 'human')
646
+ throw new TypeError('remember is reserved for agent-originated writes');
647
+ if (!Number.isInteger(input.expectedGeneration) || input.expectedGeneration < 0)
648
+ throw new TypeError('A valid scope generation is required');
649
+ const scope = getScopeById(input.scopeId);
650
+ assertInternalActorCanAccessScope(actor, scope);
651
+ if (scope.generation !== input.expectedGeneration)
652
+ throw new MemoryConflictError('Memory scope generation changed; reread before writing');
653
+ const decision = memoryWriteDecision(getGlobalSettings().memoryMode, scope.level, false);
654
+ if (decision === 'deny')
655
+ return { status: 'denied', reason: 'Memory mode does not allow agent writes' };
656
+ const existing = getDb()
657
+ .prepare('SELECT * FROM memory_entries WHERE scope_id = ? AND memory_key = ?')
658
+ .get(scope.id, key);
659
+ const targetId = input.targetEntryId ?? existing?.id;
660
+ const target = targetId
661
+ ? getDb().prepare('SELECT * FROM memory_entries WHERE id = ? AND scope_id = ?').get(targetId, scope.id)
662
+ : undefined;
663
+ if (targetId && !target)
664
+ throw new MemoryNotFoundError('Target memory entry not found');
665
+ if (target && target.memory_key === key && target.title === title && target.body === body) {
666
+ return { status: 'applied', entry: mapEntry(target) };
667
+ }
668
+ if (target && input.expectedRevision !== target.revision)
669
+ throw new MemoryConflictError('Memory entry revision changed; reread before writing');
670
+ if (!target && input.expectedRevision !== undefined)
671
+ throw new TypeError('expectedRevision requires a target entry');
672
+ if (target && target.memory_key !== key) {
673
+ const owner = existing;
674
+ if (owner && owner.id !== target.id)
675
+ throw new MemoryConflictError('A different memory already uses this key');
676
+ }
677
+ if (decision === 'propose') {
678
+ return {
679
+ status: 'proposed',
680
+ proposal: insertProposal(scope, actor, key, title, body, scope.generation, target?.id, target?.revision),
681
+ };
682
+ }
683
+ if (target) {
684
+ return {
685
+ status: 'applied',
686
+ entry: updateMemory({
687
+ scopeId: scope.id,
688
+ entryId: target.id,
689
+ expectedRevision: target.revision,
690
+ key,
691
+ title,
692
+ body,
693
+ actor,
694
+ }),
695
+ };
696
+ }
697
+ return { status: 'applied', entry: createMemory({ scopeId: scope.id, key, title, body, actor }) };
698
+ }
699
+ export function listMemoryProposals(input) {
700
+ const scopeId = typeof input === 'string' ? input : input.scopeId;
701
+ const source = deriveMemoryActor(typeof input === 'string' ? { kind: 'human' } : (input.actor ?? { kind: 'human' }));
702
+ assertHuman(source, 'Proposal review');
703
+ getScopeById(scopeId);
704
+ return getDb()
705
+ .prepare('SELECT * FROM memory_proposals WHERE scope_id = ? ORDER BY created_at, id')
706
+ .all(scopeId).map(mapProposal);
707
+ }
708
+ function requireProposal(id) {
709
+ const row = getDb().prepare('SELECT * FROM memory_proposals WHERE id = ?').get(id);
710
+ if (!row)
711
+ throw new MemoryNotFoundError('Pending memory proposal not found');
712
+ return row;
713
+ }
714
+ export function approveMemoryProposal(proposalId, actorInput) {
715
+ const actor = deriveMemoryActor(actorInput);
716
+ assertHuman(actor, 'Proposal approval');
717
+ const db = getDb();
718
+ let changedScope;
719
+ let operationId;
720
+ const entry = db
721
+ .transaction(() => {
722
+ const proposal = requireProposal(proposalId);
723
+ const scope = getScopeById(proposal.scope_id);
724
+ if (scope.generation !== proposal.generation)
725
+ throw new MemoryConflictError('Scope was cleared after this proposal was made');
726
+ const target = proposal.target_entry_id
727
+ ? db
728
+ .prepare('SELECT * FROM memory_entries WHERE id = ? AND scope_id = ?')
729
+ .get(proposal.target_entry_id, scope.id)
730
+ : undefined;
731
+ if (proposal.target_entry_id && (!target || target.revision !== proposal.base_revision))
732
+ throw new MemoryConflictError('Proposal target revision changed; proposal remains pending');
733
+ const collision = db
734
+ .prepare('SELECT id FROM memory_entries WHERE scope_id = ? AND memory_key = ? AND id <> ?')
735
+ .get(scope.id, proposal.memory_key, target?.id ?? '');
736
+ if (collision)
737
+ throw new MemoryConflictError('A memory now uses the proposed key; proposal remains pending');
738
+ const timestamp = now();
739
+ const id = target?.id ?? nanoid();
740
+ const newRevision = target ? target.revision + 1 : 1;
741
+ if (target) {
742
+ const updated = db.prepare(`UPDATE memory_entries SET memory_key = ?, title = ?, body = ?, revision = ?,
743
+ actor_kind = ?, source_workspace_id = ?, source_session_id = ?, source_engine = ?, client_name = ?, transport = ?, updated_at = ?
744
+ WHERE id = ? AND scope_id = ? AND revision = ?`);
745
+ const p = proposalSource(proposal);
746
+ if (updated.run(proposal.memory_key, proposal.title, proposal.body, newRevision, proposal.actor_kind, ...p, timestamp, target.id, scope.id, proposal.base_revision).changes !== 1)
747
+ throw new MemoryConflictError();
748
+ }
749
+ else {
750
+ const p = proposalSource(proposal);
751
+ db.prepare(`INSERT INTO memory_entries
752
+ (id, scope_id, memory_key, title, body, revision, actor_kind, source_workspace_id, source_session_id, source_engine, client_name, transport, created_at, updated_at)
753
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, scope.id, proposal.memory_key, proposal.title, proposal.body, proposal.actor_kind, ...p, timestamp, timestamp);
754
+ }
755
+ db.prepare('DELETE FROM memory_proposals WHERE id = ?').run(proposal.id);
756
+ db.prepare('UPDATE memory_scopes SET revision = revision + 1, updated_at = ? WHERE id = ?').run(timestamp, scope.id);
757
+ changedScope = getScopeById(scope.id);
758
+ operationId = writeOperation(changedScope, actor, 'approved', id, newRevision, timestamp, proposal.id);
759
+ return mapEntry(db.prepare('SELECT * FROM memory_entries WHERE id = ?').get(id));
760
+ })
761
+ .immediate();
762
+ if (changedScope && operationId !== undefined)
763
+ notifyCommitted(changedScope, operationId);
764
+ return entry;
765
+ }
766
+ function proposalSource(row) {
767
+ return [row.source_workspace_id, row.source_session_id, row.source_engine, row.client_name, row.transport];
768
+ }
769
+ export function rejectMemoryProposal(proposalId, actorInput) {
770
+ const actor = deriveMemoryActor(actorInput);
771
+ assertHuman(actor, 'Proposal rejection');
772
+ const db = getDb();
773
+ let changedScope;
774
+ let operationId;
775
+ db.transaction(() => {
776
+ const proposal = requireProposal(proposalId);
777
+ const scope = getScopeById(proposal.scope_id);
778
+ const timestamp = now();
779
+ db.prepare('DELETE FROM memory_proposals WHERE id = ?').run(proposal.id);
780
+ db.prepare('UPDATE memory_scopes SET revision = revision + 1, updated_at = ? WHERE id = ?').run(timestamp, scope.id);
781
+ changedScope = getScopeById(scope.id);
782
+ operationId = writeOperation(changedScope, actor, 'rejected', null, changedScope.revision, timestamp, proposal.id);
783
+ }).immediate();
784
+ if (changedScope && operationId !== undefined)
785
+ notifyCommitted(changedScope, operationId);
786
+ }
787
+ export function deleteMemory(input) {
788
+ const actor = deriveMemoryActor(input.actor);
789
+ assertHuman(actor, 'Memory deletion');
790
+ const db = getDb();
791
+ let changedScope;
792
+ let operationId;
793
+ db.transaction(() => {
794
+ const scope = getScopeById(input.scopeId);
795
+ const result = db
796
+ .prepare('DELETE FROM memory_entries WHERE id = ? AND scope_id = ? AND revision = ?')
797
+ .run(input.entryId, scope.id, input.expectedRevision);
798
+ if (result.changes !== 1)
799
+ throw new MemoryConflictError('Memory entry changed or no longer exists');
800
+ const timestamp = now();
801
+ db.prepare('UPDATE memory_scopes SET revision = revision + 1, updated_at = ? WHERE id = ?').run(timestamp, scope.id);
802
+ changedScope = getScopeById(scope.id);
803
+ operationId = writeOperation(changedScope, actor, 'deleted', input.entryId, input.expectedRevision, timestamp);
804
+ }).immediate();
805
+ if (changedScope && operationId !== undefined)
806
+ notifyCommitted(changedScope, operationId);
807
+ }
808
+ export function promoteMemory(input) {
809
+ const actor = deriveMemoryActor(input.actor);
810
+ assertHuman(actor, 'Memory promotion');
811
+ const db = getDb();
812
+ let changedScope;
813
+ let operationId;
814
+ const entry = db
815
+ .transaction(() => {
816
+ const sourceScope = getScopeById(input.sourceScopeId);
817
+ const targetScope = getScopeById(input.targetScopeId);
818
+ const source = db
819
+ .prepare('SELECT * FROM memory_entries WHERE id = ? AND scope_id = ?')
820
+ .get(input.entryId, sourceScope.id);
821
+ if (!source)
822
+ throw new MemoryNotFoundError('Source memory entry not found');
823
+ const collision = db
824
+ .prepare('SELECT id FROM memory_entries WHERE scope_id = ? AND memory_key = ?')
825
+ .get(targetScope.id, source.memory_key);
826
+ if (collision)
827
+ throw new MemoryConflictError('Target scope already has this memory key');
828
+ const timestamp = now();
829
+ const id = nanoid();
830
+ const p = [
831
+ source.source_workspace_id,
832
+ source.source_session_id,
833
+ source.source_engine,
834
+ source.client_name,
835
+ source.transport,
836
+ ];
837
+ db.prepare(`INSERT INTO memory_entries
838
+ (id, scope_id, memory_key, title, body, revision, actor_kind, source_workspace_id, source_session_id, source_engine, client_name, transport, created_at, updated_at)
839
+ VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, targetScope.id, source.memory_key, source.title, source.body, source.actor_kind, ...p, timestamp, timestamp);
840
+ db.prepare('UPDATE memory_scopes SET revision = revision + 1, updated_at = ? WHERE id = ?').run(timestamp, targetScope.id);
841
+ changedScope = getScopeById(targetScope.id);
842
+ operationId = writeOperation(changedScope, actor, 'promoted', id, 1, timestamp);
843
+ return mapEntry(db.prepare('SELECT * FROM memory_entries WHERE id = ?').get(id));
844
+ })
845
+ .immediate();
846
+ if (changedScope && operationId !== undefined)
847
+ notifyCommitted(changedScope, operationId);
848
+ return entry;
849
+ }
850
+ export function previewMemoryClear(scopeId, actorInput = { kind: 'human' }) {
851
+ assertHuman(deriveMemoryActor(actorInput), 'Memory clearing');
852
+ const scope = getScopeById(scopeId);
853
+ const db = getDb();
854
+ const entries = db.prepare('SELECT COUNT(*) AS count FROM memory_entries WHERE scope_id = ?').get(scopeId).count;
855
+ const proposals = db.prepare('SELECT COUNT(*) AS count FROM memory_proposals WHERE scope_id = ?').get(scopeId).count;
856
+ return { scope: mapScope(scope), entries, proposals, revision: scope.revision, generation: scope.generation };
857
+ }
858
+ export function clearMemoryScope(input) {
859
+ const actor = deriveMemoryActor(input.actor);
860
+ assertHuman(actor, 'Memory clearing');
861
+ if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 0)
862
+ throw new TypeError('Invalid preview revision');
863
+ const db = getDb();
864
+ let changedScope;
865
+ let operationId;
866
+ const cleared = db
867
+ .transaction(() => {
868
+ const scope = getScopeById(input.scopeId);
869
+ if (scope.revision !== input.expectedRevision)
870
+ throw new MemoryConflictError('Memory changed since the clear preview');
871
+ const entries = db.prepare('SELECT COUNT(*) AS count FROM memory_entries WHERE scope_id = ?').get(scope.id).count;
872
+ const proposals = db.prepare('SELECT COUNT(*) AS count FROM memory_proposals WHERE scope_id = ?').get(scope.id).count;
873
+ db.prepare('DELETE FROM memory_entries WHERE scope_id = ?').run(scope.id);
874
+ db.prepare('DELETE FROM memory_proposals WHERE scope_id = ?').run(scope.id);
875
+ const timestamp = now();
876
+ db.prepare('UPDATE memory_scopes SET generation = generation + 1, revision = revision + 1, updated_at = ? WHERE id = ?').run(timestamp, scope.id);
877
+ changedScope = getScopeById(scope.id);
878
+ operationId = writeOperation(changedScope, actor, 'cleared', null, changedScope.revision, timestamp, null, entries, proposals);
879
+ return {
880
+ scope: mapScope(changedScope),
881
+ entries,
882
+ proposals,
883
+ revision: changedScope.revision,
884
+ generation: changedScope.generation,
885
+ };
886
+ })
887
+ .immediate();
888
+ if (changedScope && operationId !== undefined)
889
+ notifyCommitted(changedScope, operationId);
890
+ return cleared;
891
+ }