ds4-context-core 0.1.1 → 0.2.0-beta.1

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 (121) hide show
  1. package/README.md +17 -4
  2. package/dist/adapter/conformance.d.ts +35 -0
  3. package/dist/adapter/conformance.d.ts.map +1 -0
  4. package/dist/adapter/conformance.js +260 -0
  5. package/dist/adapter/conformance.js.map +1 -0
  6. package/dist/adapter/runtime-adapter.d.ts +119 -0
  7. package/dist/adapter/runtime-adapter.d.ts.map +1 -0
  8. package/dist/adapter/runtime-adapter.js +289 -0
  9. package/dist/adapter/runtime-adapter.js.map +1 -0
  10. package/dist/config/config-loader.d.ts +1 -0
  11. package/dist/config/config-loader.d.ts.map +1 -1
  12. package/dist/config/config-loader.js +107 -1
  13. package/dist/config/config-loader.js.map +1 -1
  14. package/dist/config/config.d.ts +45 -0
  15. package/dist/config/config.d.ts.map +1 -1
  16. package/dist/config/config.js +28 -0
  17. package/dist/config/config.js.map +1 -1
  18. package/dist/core/canonical-message.d.ts +2 -1
  19. package/dist/core/canonical-message.d.ts.map +1 -1
  20. package/dist/index.d.ts +12 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +12 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/manifest/context-manifest.d.ts +12 -0
  25. package/dist/manifest/context-manifest.d.ts.map +1 -1
  26. package/dist/memory/memory-manager.d.ts +7 -1
  27. package/dist/memory/memory-manager.d.ts.map +1 -1
  28. package/dist/memory/memory-manager.js +100 -3
  29. package/dist/memory/memory-manager.js.map +1 -1
  30. package/dist/memory/memory-types.d.ts +43 -0
  31. package/dist/memory/memory-types.d.ts.map +1 -1
  32. package/dist/memory/memory-types.js.map +1 -1
  33. package/dist/persistence/migrations.d.ts +2 -1
  34. package/dist/persistence/migrations.d.ts.map +1 -1
  35. package/dist/persistence/migrations.js +155 -25
  36. package/dist/persistence/migrations.js.map +1 -1
  37. package/dist/persistence/repositories/artifact-repository.d.ts +3 -1
  38. package/dist/persistence/repositories/artifact-repository.d.ts.map +1 -1
  39. package/dist/persistence/repositories/artifact-repository.js +16 -27
  40. package/dist/persistence/repositories/artifact-repository.js.map +1 -1
  41. package/dist/persistence/repositories/context-manifest-repository.d.ts +3 -1
  42. package/dist/persistence/repositories/context-manifest-repository.d.ts.map +1 -1
  43. package/dist/persistence/repositories/context-manifest-repository.js +10 -18
  44. package/dist/persistence/repositories/context-manifest-repository.js.map +1 -1
  45. package/dist/persistence/repositories/context-quality-repository.d.ts +21 -0
  46. package/dist/persistence/repositories/context-quality-repository.d.ts.map +1 -0
  47. package/dist/persistence/repositories/context-quality-repository.js +83 -0
  48. package/dist/persistence/repositories/context-quality-repository.js.map +1 -0
  49. package/dist/persistence/repositories/embedding-repository.d.ts +37 -0
  50. package/dist/persistence/repositories/embedding-repository.d.ts.map +1 -0
  51. package/dist/persistence/repositories/embedding-repository.js +142 -0
  52. package/dist/persistence/repositories/embedding-repository.js.map +1 -0
  53. package/dist/persistence/repositories/lease-repository.d.ts +22 -0
  54. package/dist/persistence/repositories/lease-repository.d.ts.map +1 -0
  55. package/dist/persistence/repositories/lease-repository.js +113 -0
  56. package/dist/persistence/repositories/lease-repository.js.map +1 -0
  57. package/dist/persistence/repositories/memory-repository.d.ts +34 -3
  58. package/dist/persistence/repositories/memory-repository.d.ts.map +1 -1
  59. package/dist/persistence/repositories/memory-repository.js +426 -64
  60. package/dist/persistence/repositories/memory-repository.js.map +1 -1
  61. package/dist/persistence/repositories/project-knowledge-repository.d.ts +28 -5
  62. package/dist/persistence/repositories/project-knowledge-repository.d.ts.map +1 -1
  63. package/dist/persistence/repositories/project-knowledge-repository.js +123 -53
  64. package/dist/persistence/repositories/project-knowledge-repository.js.map +1 -1
  65. package/dist/persistence/repositories/session-index-repository.d.ts +9 -1
  66. package/dist/persistence/repositories/session-index-repository.d.ts.map +1 -1
  67. package/dist/persistence/repositories/session-index-repository.js +38 -17
  68. package/dist/persistence/repositories/session-index-repository.js.map +1 -1
  69. package/dist/persistence/repositories/summary-repository.d.ts +3 -1
  70. package/dist/persistence/repositories/summary-repository.d.ts.map +1 -1
  71. package/dist/persistence/repositories/summary-repository.js +35 -31
  72. package/dist/persistence/repositories/summary-repository.js.map +1 -1
  73. package/dist/persistence/sqlite.d.ts +10 -0
  74. package/dist/persistence/sqlite.d.ts.map +1 -1
  75. package/dist/persistence/sqlite.js +41 -12
  76. package/dist/persistence/sqlite.js.map +1 -1
  77. package/dist/persistence/write-coordinator.d.ts +37 -0
  78. package/dist/persistence/write-coordinator.d.ts.map +1 -0
  79. package/dist/persistence/write-coordinator.js +120 -0
  80. package/dist/persistence/write-coordinator.js.map +1 -0
  81. package/dist/planner/context-planner.d.ts +5 -0
  82. package/dist/planner/context-planner.d.ts.map +1 -1
  83. package/dist/planner/context-planner.js +41 -62
  84. package/dist/planner/context-planner.js.map +1 -1
  85. package/dist/project/file-indexer.d.ts +5 -3
  86. package/dist/project/file-indexer.d.ts.map +1 -1
  87. package/dist/project/file-indexer.js +108 -42
  88. package/dist/project/file-indexer.js.map +1 -1
  89. package/dist/project/project-knowledge.d.ts +15 -3
  90. package/dist/project/project-knowledge.d.ts.map +1 -1
  91. package/dist/project/project-knowledge.js +168 -42
  92. package/dist/project/project-knowledge.js.map +1 -1
  93. package/dist/project/symbol-parser.d.ts +48 -0
  94. package/dist/project/symbol-parser.d.ts.map +1 -0
  95. package/dist/project/symbol-parser.js +427 -0
  96. package/dist/project/symbol-parser.js.map +1 -0
  97. package/dist/quality/context-quality.d.ts +172 -0
  98. package/dist/quality/context-quality.d.ts.map +1 -0
  99. package/dist/quality/context-quality.js +624 -0
  100. package/dist/quality/context-quality.js.map +1 -0
  101. package/dist/ranking/learned-ranker.d.ts +174 -0
  102. package/dist/ranking/learned-ranker.d.ts.map +1 -0
  103. package/dist/ranking/learned-ranker.js +782 -0
  104. package/dist/ranking/learned-ranker.js.map +1 -0
  105. package/dist/retrieval/embedding.d.ts +92 -0
  106. package/dist/retrieval/embedding.d.ts.map +1 -0
  107. package/dist/retrieval/embedding.js +56 -0
  108. package/dist/retrieval/embedding.js.map +1 -0
  109. package/dist/retrieval/retrieval-engine.d.ts +9 -1
  110. package/dist/retrieval/retrieval-engine.d.ts.map +1 -1
  111. package/dist/retrieval/retrieval-engine.js +105 -7
  112. package/dist/retrieval/retrieval-engine.js.map +1 -1
  113. package/dist/retrieval/semantic-index.d.ts +30 -0
  114. package/dist/retrieval/semantic-index.d.ts.map +1 -0
  115. package/dist/retrieval/semantic-index.js +284 -0
  116. package/dist/retrieval/semantic-index.js.map +1 -0
  117. package/dist/retrieval/semantic-quality.d.ts +52 -0
  118. package/dist/retrieval/semantic-quality.d.ts.map +1 -0
  119. package/dist/retrieval/semantic-quality.js +117 -0
  120. package/dist/retrieval/semantic-quality.js.map +1 -0
  121. package/package.json +1 -1
@@ -1,17 +1,5 @@
1
+ import { SqliteWriteCoordinator } from "../write-coordinator.js";
1
2
  import { parseMemoryMutation, parsePinMutation, } from "../../memory/memory-types.js";
2
- function runTransaction(database, operation) {
3
- database.exec("BEGIN IMMEDIATE");
4
- try {
5
- const result = operation();
6
- database.exec("COMMIT");
7
- return result;
8
- }
9
- catch (error) {
10
- if (database.isTransaction)
11
- database.exec("ROLLBACK");
12
- throw error;
13
- }
14
- }
15
3
  function parseJson(value) {
16
4
  try {
17
5
  return JSON.parse(value);
@@ -20,17 +8,53 @@ function parseJson(value) {
20
8
  return undefined;
21
9
  }
22
10
  }
23
- function classificationFromMetadata(value) {
24
- const metadata = parseJson(value);
25
- if (!metadata || typeof metadata !== "object" || !("classification" in metadata))
11
+ function stringList(value) {
12
+ return Array.isArray(value) && value.every((item) => typeof item === "string")
13
+ ? [...new Set(value)]
14
+ : undefined;
15
+ }
16
+ function mutationProvenance(value) {
17
+ if (!value || typeof value !== "object")
18
+ return undefined;
19
+ const record = value;
20
+ const sourceEntryIds = stringList(record.sourceEntryIds);
21
+ const contradicts = stringList(record.contradicts);
22
+ if (typeof record.sourceSessionId !== "string"
23
+ || typeof record.mutationEntryId !== "string"
24
+ || !sourceEntryIds
25
+ || !contradicts) {
26
26
  return undefined;
27
- const classification = metadata.classification;
28
- return classification === "normal"
29
- || classification === "internal"
30
- || classification === "sensitive"
31
- || classification === "local-only"
32
- ? classification
27
+ }
28
+ return {
29
+ sourceSessionId: record.sourceSessionId,
30
+ ...(typeof record.sourceSessionFile === "string"
31
+ ? { sourceSessionFile: record.sourceSessionFile }
32
+ : {}),
33
+ mutationEntryId: record.mutationEntryId,
34
+ ...(typeof record.sourceBranchEntryId === "string"
35
+ ? { sourceBranchEntryId: record.sourceBranchEntryId }
36
+ : {}),
37
+ sourceEntryIds,
38
+ ...(typeof record.supersedes === "string" ? { supersedes: record.supersedes } : {}),
39
+ contradicts,
40
+ };
41
+ }
42
+ function metadata(value) {
43
+ const parsed = parseJson(value);
44
+ if (!parsed || typeof parsed !== "object")
45
+ return {};
46
+ const record = parsed;
47
+ const classification = record.classification === "normal"
48
+ || record.classification === "internal"
49
+ || record.classification === "sensitive"
50
+ || record.classification === "local-only"
51
+ ? record.classification
33
52
  : undefined;
53
+ const provenance = mutationProvenance(record.provenance);
54
+ return {
55
+ ...(classification ? { classification } : {}),
56
+ ...(provenance ? { provenance } : {}),
57
+ };
34
58
  }
35
59
  function entryIdFromKey(entryKey) {
36
60
  const separator = entryKey.indexOf(":");
@@ -41,36 +65,53 @@ function identityKey(item) {
41
65
  return undefined;
42
66
  return [item.scope, item.sessionId ?? "", item.projectPath ?? "", item.key].join("\0");
43
67
  }
68
+ function memoryMutationMatchesSource(item, row) {
69
+ return item.scope === "session"
70
+ ? item.sessionId === row.session_id
71
+ : item.projectPath === row.source_project_path;
72
+ }
73
+ function pinMutationMatchesSource(item, row) {
74
+ return item.scope === "project"
75
+ ? item.projectPath === row.source_project_path
76
+ : item.sessionId === row.session_id;
77
+ }
44
78
  function mapMemory(row, sourceEntryIds) {
79
+ const stored = metadata(row.metadata_json);
80
+ const fallbackSessionId = row.origin_session_id ?? row.session_id ?? "legacy";
45
81
  return {
46
82
  id: row.memory_id,
47
83
  scope: row.scope,
48
84
  ...(row.session_id ? { sessionId: row.session_id } : {}),
49
85
  ...(row.project_path ? { projectPath: row.project_path } : {}),
50
86
  ...(row.memory_key ? { key: row.memory_key } : {}),
51
- ...(classificationFromMetadata(row.metadata_json)
52
- ? { classification: classificationFromMetadata(row.metadata_json) }
53
- : {}),
87
+ ...(stored.classification ? { classification: stored.classification } : {}),
54
88
  claim: row.claim,
55
89
  status: row.status,
56
90
  createdAt: row.created_at,
57
91
  updatedAt: row.updated_at,
58
- originSessionId: row.origin_session_id ?? row.session_id ?? "legacy",
92
+ originSessionId: fallbackSessionId,
59
93
  ...(row.superseded_by ? { supersededBy: row.superseded_by } : {}),
60
94
  ...(row.status_reason ? { statusReason: row.status_reason } : {}),
61
95
  sourceEntryIds,
96
+ provenance: stored.provenance ?? {
97
+ sourceSessionId: fallbackSessionId,
98
+ mutationEntryId: sourceEntryIds.at(-1) ?? row.memory_id,
99
+ sourceEntryIds,
100
+ contradicts: [],
101
+ },
62
102
  };
63
103
  }
64
104
  function mapPin(row) {
105
+ const stored = metadata(row.metadata_json);
106
+ const fallbackSessionId = row.session_id ?? "legacy";
107
+ const fallbackEntryId = row.source_entry_id ?? row.pin_id;
65
108
  return {
66
109
  id: row.pin_id,
67
110
  scope: row.scope,
68
111
  ...(row.session_id ? { sessionId: row.session_id } : {}),
69
112
  ...(row.project_path ? { projectPath: row.project_path } : {}),
70
113
  ...(row.branch_leaf_id ? { branchLeafId: row.branch_leaf_id } : {}),
71
- ...(classificationFromMetadata(row.metadata_json)
72
- ? { classification: classificationFromMetadata(row.metadata_json) }
73
- : {}),
114
+ ...(stored.classification ? { classification: stored.classification } : {}),
74
115
  content: row.content,
75
116
  status: row.status,
76
117
  createdAt: row.created_at,
@@ -79,36 +120,145 @@ function mapPin(row) {
79
120
  ...(row.status_reason ? { statusReason: row.status_reason } : {}),
80
121
  ...(row.source_entry_id ? { sourceEntryId: row.source_entry_id } : {}),
81
122
  ...(row.source_file ? { sourceFile: row.source_file } : {}),
123
+ provenance: stored.provenance ?? {
124
+ sourceSessionId: fallbackSessionId,
125
+ mutationEntryId: fallbackEntryId,
126
+ sourceEntryIds: [fallbackEntryId],
127
+ contradicts: [],
128
+ },
82
129
  };
83
130
  }
84
131
  export class MemoryRepository {
85
132
  database;
86
- constructor(database) {
133
+ writes;
134
+ constructor(database, writes = new SqliteWriteCoordinator(database)) {
87
135
  this.database = database;
136
+ this.writes = writes;
88
137
  }
89
138
  reconcileSession(sessionId, memoryMutations, pinMutations) {
90
- return runTransaction(this.database, () => {
139
+ return this.writes.transaction("memory-reconcile-session", () => {
140
+ this.database.prepare(`
141
+ DELETE FROM project_memory_sessions AS source
142
+ WHERE source.session_id = ? AND source.status <> 'ready'
143
+ AND NOT EXISTS (
144
+ SELECT 1 FROM project_memory_source_exclusions AS exclusion
145
+ WHERE exclusion.project_path = source.project_path
146
+ AND exclusion.session_id = source.session_id
147
+ )
148
+ `).run(sessionId);
91
149
  this.database.prepare("DELETE FROM memory_mutations WHERE session_id = ?").run(sessionId);
92
150
  this.database.prepare("DELETE FROM pin_mutations WHERE session_id = ?").run(sessionId);
93
- const insertMemoryMutation = this.database.prepare(`
94
- INSERT INTO memory_mutations(
95
- mutation_key, mutation_id, session_id, created_at, entry_order, payload_json
96
- ) VALUES (?, ?, ?, ?, ?, ?)
97
- `);
98
- const insertPinMutation = this.database.prepare(`
99
- INSERT INTO pin_mutations(
100
- mutation_key, mutation_id, session_id, created_at, entry_order, payload_json
101
- ) VALUES (?, ?, ?, ?, ?, ?)
102
- `);
103
- for (const mutation of memoryMutations) {
104
- insertMemoryMutation.run(mutation.mutationKey, mutation.mutationId, mutation.sessionId, mutation.createdAt, mutation.entryOrder, JSON.stringify(mutation.payload));
151
+ this.insertMemoryMutations(memoryMutations);
152
+ this.insertPinMutations(pinMutations);
153
+ return this.materialize();
154
+ });
155
+ }
156
+ getProjectSourceState(projectPath, sessionId) {
157
+ const row = this.database.prepare(`
158
+ SELECT project_path, session_id, session_file, header_hash, file_size,
159
+ file_mtime_ms, checkpoint_offset, checkpoint_hash_start, checkpoint_hash,
160
+ indexed_records, indexed_mutations, malformed_lines, status, last_error,
161
+ indexed_at, NULL AS excluded_at, NULL AS exclusion_reason
162
+ FROM project_memory_sessions
163
+ WHERE project_path = ? AND session_id = ?
164
+ `).get(projectPath, sessionId);
165
+ return row ? this.mapProjectSourceCheckpoint(row) : undefined;
166
+ }
167
+ replaceProjectSource(checkpoint, memoryMutations, pinMutations) {
168
+ return this.writes.transaction("project-memory-source-rebuild", () => {
169
+ this.database.prepare("DELETE FROM memory_mutations WHERE session_id = ?").run(checkpoint.sessionId);
170
+ this.database.prepare("DELETE FROM pin_mutations WHERE session_id = ?").run(checkpoint.sessionId);
171
+ this.insertMemoryMutations(memoryMutations);
172
+ this.insertPinMutations(pinMutations);
173
+ this.writeProjectSourceState(checkpoint);
174
+ return this.materialize();
175
+ });
176
+ }
177
+ appendProjectSource(checkpoint, memoryMutations, pinMutations) {
178
+ return this.writes.transaction("project-memory-source-append", () => {
179
+ this.insertMemoryMutations(memoryMutations, true);
180
+ this.insertPinMutations(pinMutations, true);
181
+ this.writeProjectSourceState(checkpoint);
182
+ return this.materialize();
183
+ });
184
+ }
185
+ markProjectSourceUnavailable(projectPath, sessionId, status, error, indexedAt) {
186
+ return this.writes.transaction("project-memory-source-unavailable", () => {
187
+ const existing = this.getProjectSourceState(projectPath, sessionId);
188
+ if (!existing)
189
+ return undefined;
190
+ const lastError = error.slice(0, 1_000);
191
+ if (existing.status === status
192
+ && existing.indexedMutations === 0
193
+ && existing.lastError === lastError) {
194
+ return undefined;
105
195
  }
106
- for (const mutation of pinMutations) {
107
- insertPinMutation.run(mutation.mutationKey, mutation.mutationId, mutation.sessionId, mutation.createdAt, mutation.entryOrder, JSON.stringify(mutation.payload));
196
+ this.writeProjectSourceState({
197
+ ...existing,
198
+ indexedMutations: 0,
199
+ status,
200
+ lastError,
201
+ indexedAt,
202
+ });
203
+ return this.materialize();
204
+ });
205
+ }
206
+ setProjectSourceExcluded(projectPath, sessionId, excluded, excludedAt, reason) {
207
+ return this.writes.transaction("project-memory-source-exclusion", () => {
208
+ if (excluded) {
209
+ this.database.prepare(`
210
+ INSERT INTO project_memory_source_exclusions(project_path, session_id, excluded_at, reason)
211
+ VALUES (?, ?, ?, ?)
212
+ ON CONFLICT(project_path, session_id) DO UPDATE SET
213
+ excluded_at = excluded.excluded_at,
214
+ reason = excluded.reason
215
+ `).run(projectPath, sessionId, excludedAt, reason?.slice(0, 500) ?? null);
216
+ }
217
+ else {
218
+ this.database.prepare(`
219
+ DELETE FROM project_memory_source_exclusions
220
+ WHERE project_path = ? AND session_id = ?
221
+ `).run(projectPath, sessionId);
108
222
  }
109
223
  return this.materialize();
110
224
  });
111
225
  }
226
+ listProjectSources(projectPath) {
227
+ const rows = this.database.prepare(`
228
+ SELECT state.project_path, state.session_id, state.session_file, state.header_hash,
229
+ state.file_size, state.file_mtime_ms, state.checkpoint_offset,
230
+ state.checkpoint_hash_start, state.checkpoint_hash, state.indexed_records,
231
+ state.indexed_mutations, state.malformed_lines, state.status, state.last_error,
232
+ state.indexed_at, exclusion.excluded_at, exclusion.reason AS exclusion_reason,
233
+ (SELECT count(*) FROM memory_items AS memory
234
+ WHERE memory.scope = 'project' AND memory.project_path = state.project_path
235
+ AND memory.origin_session_id = state.session_id AND memory.status = 'active')
236
+ AS active_project_memories,
237
+ (SELECT count(*) FROM pins AS pin
238
+ WHERE pin.scope = 'project' AND pin.project_path = state.project_path
239
+ AND pin.session_id = state.session_id AND pin.status = 'active')
240
+ AS active_project_pins
241
+ FROM project_memory_sessions AS state
242
+ LEFT JOIN project_memory_source_exclusions AS exclusion
243
+ ON exclusion.project_path = state.project_path AND exclusion.session_id = state.session_id
244
+ WHERE state.project_path = ?
245
+ ORDER BY state.indexed_at DESC, state.session_id
246
+ `).all(projectPath);
247
+ return rows.map((row) => ({
248
+ projectPath: row.project_path,
249
+ sessionId: row.session_id,
250
+ sessionFile: row.session_file,
251
+ status: row.excluded_at !== null ? "excluded" : row.status,
252
+ indexedRecords: row.indexed_records,
253
+ indexedMutations: row.indexed_mutations,
254
+ malformedLines: row.malformed_lines,
255
+ activeProjectMemories: row.active_project_memories ?? 0,
256
+ activeProjectPins: row.active_project_pins ?? 0,
257
+ indexedAt: row.indexed_at,
258
+ ...(row.last_error ? { lastError: row.last_error } : {}),
259
+ ...(row.exclusion_reason ? { exclusionReason: row.exclusion_reason } : {}),
260
+ }));
261
+ }
112
262
  getMemory(id) {
113
263
  const row = this.database.prepare("SELECT * FROM memory_items WHERE memory_id = ?").get(id);
114
264
  return row ? mapMemory(row, this.memorySourceIds(id)) : undefined;
@@ -119,10 +269,11 @@ export class MemoryRepository {
119
269
  SELECT * FROM memory_items
120
270
  WHERE (
121
271
  (scope = 'session' AND session_id = ?)
122
- OR (scope = 'project' AND project_path = ? AND ? = 1)
272
+ OR (scope = 'project' AND project_path = ? AND ? = 1
273
+ AND (? = 1 OR origin_session_id = ?))
123
274
  ) ${status}
124
275
  ORDER BY created_at DESC, memory_id
125
- `).all(options.sessionId, options.projectPath, options.includeProject ? 1 : 0);
276
+ `).all(options.sessionId, options.projectPath, options.includeProject ? 1 : 0, options.includeCrossSessionProject ? 1 : 0, options.sessionId);
126
277
  const sources = this.memorySources(new Set(rows.map((row) => row.memory_id)));
127
278
  return rows.map((row) => mapMemory(row, sources.get(row.memory_id) ?? []));
128
279
  }
@@ -136,28 +287,30 @@ export class MemoryRepository {
136
287
  SELECT * FROM pins
137
288
  WHERE (
138
289
  (scope IN ('session', 'branch') AND session_id = ?)
139
- OR (scope = 'project' AND project_path = ? AND ? = 1)
290
+ OR (scope = 'project' AND project_path = ? AND ? = 1
291
+ AND (? = 1 OR session_id = ?))
140
292
  ) ${status}
141
293
  ORDER BY created_at ASC, pin_id
142
- `).all(options.sessionId, options.projectPath, options.includeProject ? 1 : 0);
294
+ `).all(options.sessionId, options.projectPath, options.includeProject ? 1 : 0, options.includeCrossSessionProject ? 1 : 0, options.sessionId);
143
295
  return rows.map(mapPin);
144
296
  }
145
- stats(sessionId, projectPath) {
297
+ stats(sessionId, projectPath, includeCrossSessionProject = true) {
146
298
  const memories = this.database.prepare(`
147
299
  SELECT
148
300
  COALESCE(sum(status = 'active'), 0) AS active,
149
301
  COALESCE(sum(status <> 'active'), 0) AS inactive
150
302
  FROM memory_items
151
- WHERE (scope = 'session' AND session_id = ?) OR (scope = 'project' AND project_path = ?)
152
- `).get(sessionId, projectPath);
303
+ WHERE (scope = 'session' AND session_id = ?)
304
+ OR (scope = 'project' AND project_path = ? AND (? = 1 OR origin_session_id = ?))
305
+ `).get(sessionId, projectPath, includeCrossSessionProject ? 1 : 0, sessionId);
153
306
  const pins = this.database.prepare(`
154
307
  SELECT
155
308
  COALESCE(sum(status = 'active'), 0) AS active,
156
309
  COALESCE(sum(status <> 'active'), 0) AS inactive
157
310
  FROM pins
158
311
  WHERE (scope IN ('session', 'branch') AND session_id = ?)
159
- OR (scope = 'project' AND project_path = ?)
160
- `).get(sessionId, projectPath);
312
+ OR (scope = 'project' AND project_path = ? AND (? = 1 OR session_id = ?))
313
+ `).get(sessionId, projectPath, includeCrossSessionProject ? 1 : 0, sessionId);
161
314
  return {
162
315
  activeMemories: memories.active,
163
316
  inactiveMemories: memories.inactive,
@@ -171,8 +324,25 @@ export class MemoryRepository {
171
324
  const memories = new Map();
172
325
  const pins = new Map();
173
326
  const memoryRows = this.database.prepare(`
174
- SELECT mutation_key, mutation_id, session_id, created_at, entry_order, payload_json
175
- FROM memory_mutations ORDER BY created_at, session_id, entry_order, mutation_id, mutation_key
327
+ SELECT mutation.mutation_key, mutation.mutation_id, mutation.session_id,
328
+ session.session_file, session.project_path AS source_project_path,
329
+ mutation.created_at, mutation.entry_order, mutation.source_parent_entry_id,
330
+ CASE
331
+ WHEN exclusion.session_id IS NOT NULL THEN 0
332
+ WHEN source.status IS NOT NULL AND source.status <> 'ready' THEN 0
333
+ ELSE 1
334
+ END AS source_project_enabled,
335
+ mutation.payload_json
336
+ FROM memory_mutations AS mutation
337
+ JOIN sessions AS session ON session.session_id = mutation.session_id
338
+ LEFT JOIN project_memory_sessions AS source
339
+ ON source.project_path = session.project_path
340
+ AND source.session_id = mutation.session_id
341
+ LEFT JOIN project_memory_source_exclusions AS exclusion
342
+ ON exclusion.project_path = session.project_path
343
+ AND exclusion.session_id = mutation.session_id
344
+ ORDER BY mutation.created_at, mutation.session_id, mutation.entry_order,
345
+ mutation.mutation_id, mutation.mutation_key
176
346
  `).all();
177
347
  const memoryOperations = [];
178
348
  for (const row of memoryRows) {
@@ -189,8 +359,25 @@ export class MemoryRepository {
189
359
  this.applyMemoryMutation(memories, mutation, row, warnings, true);
190
360
  }
191
361
  const pinRows = this.database.prepare(`
192
- SELECT mutation_key, mutation_id, session_id, created_at, entry_order, payload_json
193
- FROM pin_mutations ORDER BY created_at, session_id, entry_order, mutation_id, mutation_key
362
+ SELECT mutation.mutation_key, mutation.mutation_id, mutation.session_id,
363
+ session.session_file, session.project_path AS source_project_path,
364
+ mutation.created_at, mutation.entry_order, mutation.source_parent_entry_id,
365
+ CASE
366
+ WHEN exclusion.session_id IS NOT NULL THEN 0
367
+ WHEN source.status IS NOT NULL AND source.status <> 'ready' THEN 0
368
+ ELSE 1
369
+ END AS source_project_enabled,
370
+ mutation.payload_json
371
+ FROM pin_mutations AS mutation
372
+ JOIN sessions AS session ON session.session_id = mutation.session_id
373
+ LEFT JOIN project_memory_sessions AS source
374
+ ON source.project_path = session.project_path
375
+ AND source.session_id = mutation.session_id
376
+ LEFT JOIN project_memory_source_exclusions AS exclusion
377
+ ON exclusion.project_path = session.project_path
378
+ AND exclusion.session_id = mutation.session_id
379
+ ORDER BY mutation.created_at, mutation.session_id, mutation.entry_order,
380
+ mutation.mutation_id, mutation.mutation_key
194
381
  `).all();
195
382
  const pinOperations = [];
196
383
  for (const row of pinRows) {
@@ -219,6 +406,15 @@ export class MemoryRepository {
219
406
  memory.item.status = "invalid";
220
407
  memory.item.statusReason = `Conflicts with active memory ${previousId}`;
221
408
  memory.item.updatedAt = Math.max(memory.item.updatedAt, memory.item.createdAt);
409
+ memory.item.provenance.contradicts = [
410
+ ...new Set([...memory.item.provenance.contradicts, previousId]),
411
+ ];
412
+ const previous = memories.get(previousId);
413
+ if (previous) {
414
+ previous.item.provenance.contradicts = [
415
+ ...new Set([...previous.item.provenance.contradicts, memory.item.id]),
416
+ ];
417
+ }
222
418
  warnings.push(`Memory ${memory.item.id} invalidated because key conflicts with ${previousId}`);
223
419
  }
224
420
  this.database.exec(`
@@ -240,7 +436,10 @@ export class MemoryRepository {
240
436
  const insertMemorySource = this.database.prepare("INSERT OR IGNORE INTO memory_sources(memory_id, entry_key) VALUES (?, ?)");
241
437
  const insertMemoryFts = this.database.prepare("INSERT INTO memory_fts(claim, memory_id) VALUES (?, ?)");
242
438
  for (const { item, sourceKeys } of memories.values()) {
243
- insertMemory.run(item.id, item.scope, item.sessionId ?? null, item.projectPath ?? null, item.claim, item.status, item.createdAt, null, item.key ?? null, item.originSessionId, item.updatedAt, item.statusReason ?? null, JSON.stringify({ ...(item.classification ? { classification: item.classification } : {}) }));
439
+ insertMemory.run(item.id, item.scope, item.sessionId ?? null, item.projectPath ?? null, item.claim, item.status, item.createdAt, null, item.key ?? null, item.originSessionId, item.updatedAt, item.statusReason ?? null, JSON.stringify({
440
+ ...(item.classification ? { classification: item.classification } : {}),
441
+ provenance: item.provenance,
442
+ }));
244
443
  for (const sourceKey of sourceKeys) {
245
444
  if (availableEntryKeys.has(sourceKey))
246
445
  insertMemorySource.run(item.id, sourceKey);
@@ -263,7 +462,10 @@ export class MemoryRepository {
263
462
  const sourceEntryId = item.sourceEntryId ?? item.id;
264
463
  const sourceEntryKey = item.sessionId ? `${item.sessionId}:${sourceEntryId}` : null;
265
464
  const sourceExists = sourceEntryKey ? availableEntryKeys.has(sourceEntryKey) : false;
266
- insertPin.run(item.id, item.scope, item.sessionId ?? null, item.projectPath ?? null, item.content, sourceExists ? sourceEntryKey : null, sourceExists ? sourceEntryId : null, item.sourceFile ?? null, item.status, item.createdAt, item.branchLeafId ?? null, null, item.updatedAt, item.statusReason ?? null, JSON.stringify({ ...(item.classification ? { classification: item.classification } : {}) }));
465
+ insertPin.run(item.id, item.scope, item.sessionId ?? null, item.projectPath ?? null, item.content, sourceExists ? sourceEntryKey : null, sourceExists ? sourceEntryId : null, item.sourceFile ?? null, item.status, item.createdAt, item.branchLeafId ?? null, null, item.updatedAt, item.statusReason ?? null, JSON.stringify({
466
+ ...(item.classification ? { classification: item.classification } : {}),
467
+ provenance: item.provenance,
468
+ }));
267
469
  }
268
470
  const updateSupersededPin = this.database.prepare("UPDATE pins SET superseded_by = ? WHERE pin_id = ?");
269
471
  for (const item of pins.values()) {
@@ -284,9 +486,17 @@ export class MemoryRepository {
284
486
  if (mutation.operation === "status") {
285
487
  const existing = memories.get(mutation.memoryId);
286
488
  if (!existing) {
489
+ if (row.source_project_enabled === 0)
490
+ return;
287
491
  warnings.push(`Memory status mutation ${mutation.mutationId} references missing ${mutation.memoryId}`);
288
492
  return;
289
493
  }
494
+ if (existing.item.scope === "project" && row.source_project_enabled === 0)
495
+ return;
496
+ if (!memoryMutationMatchesSource(existing.item, row)) {
497
+ warnings.push(`Memory status mutation ${mutation.mutationId} crosses its source project/session scope`);
498
+ return;
499
+ }
290
500
  existing.item.status = mutation.status;
291
501
  existing.item.updatedAt = mutation.createdAt;
292
502
  existing.item.statusReason = mutation.reason;
@@ -294,11 +504,25 @@ export class MemoryRepository {
294
504
  return;
295
505
  }
296
506
  if (mutation.operation === "supersede") {
507
+ if (mutation.item.scope === "project" && row.source_project_enabled === 0)
508
+ return;
297
509
  const previous = memories.get(mutation.previousId);
298
510
  if (!previous) {
299
511
  warnings.push(`Memory supersession ${mutation.mutationId} references missing ${mutation.previousId}`);
300
512
  return;
301
513
  }
514
+ const replacement = memories.get(mutation.item.id);
515
+ if (!replacement
516
+ || replacement.item.provenance.sourceSessionId !== row.session_id
517
+ || replacement.item.provenance.mutationEntryId !== entryIdFromKey(row.mutation_key)) {
518
+ warnings.push(`Memory supersession ${mutation.mutationId} has no valid replacement ${mutation.item.id}`);
519
+ return;
520
+ }
521
+ if (previous.item.scope !== replacement.item.scope
522
+ || !memoryMutationMatchesSource(previous.item, row)) {
523
+ warnings.push(`Memory supersession ${mutation.mutationId} crosses its source project/session scope`);
524
+ return;
525
+ }
302
526
  previous.item.status = "superseded";
303
527
  previous.item.supersededBy = mutation.item.id;
304
528
  previous.item.updatedAt = mutation.createdAt;
@@ -309,6 +533,8 @@ export class MemoryRepository {
309
533
  if (mutation.operation === "status")
310
534
  return;
311
535
  const value = mutation.item;
536
+ if (value.scope === "project" && row.source_project_enabled === 0)
537
+ return;
312
538
  const existing = memories.get(value.id);
313
539
  if (existing) {
314
540
  if (existing.item.claim !== value.claim || existing.item.scope !== value.scope) {
@@ -320,10 +546,16 @@ export class MemoryRepository {
320
546
  warnings.push(`Project memory ${value.id} has no project path`);
321
547
  return;
322
548
  }
549
+ if (value.scope === "project" && value.projectPath !== row.source_project_path) {
550
+ warnings.push(`Project memory ${value.id} does not match its canonical source project`);
551
+ return;
552
+ }
323
553
  const sourceKeys = new Set([
324
554
  row.mutation_key,
325
555
  ...value.sourceEntryIds.map((entryId) => `${row.session_id}:${entryId}`),
326
556
  ]);
557
+ const mutationEntryId = entryIdFromKey(row.mutation_key);
558
+ const sourceEntryIds = [...new Set([mutationEntryId, ...value.sourceEntryIds])];
327
559
  const item = {
328
560
  id: value.id,
329
561
  scope: value.scope,
@@ -336,7 +568,18 @@ export class MemoryRepository {
336
568
  createdAt: value.createdAt,
337
569
  updatedAt: mutation.createdAt,
338
570
  originSessionId: row.session_id,
339
- sourceEntryIds: [...new Set([entryIdFromKey(row.mutation_key), ...value.sourceEntryIds])],
571
+ sourceEntryIds,
572
+ provenance: {
573
+ sourceSessionId: row.session_id,
574
+ sourceSessionFile: row.session_file,
575
+ mutationEntryId,
576
+ ...(row.source_parent_entry_id
577
+ ? { sourceBranchEntryId: row.source_parent_entry_id }
578
+ : {}),
579
+ sourceEntryIds,
580
+ ...(mutation.operation === "supersede" ? { supersedes: mutation.previousId } : {}),
581
+ contradicts: [],
582
+ },
340
583
  };
341
584
  memories.set(item.id, { item, sourceKeys });
342
585
  }
@@ -345,9 +588,17 @@ export class MemoryRepository {
345
588
  if (mutation.operation === "status") {
346
589
  const existing = pins.get(mutation.pinId);
347
590
  if (!existing) {
591
+ if (row.source_project_enabled === 0)
592
+ return;
348
593
  warnings.push(`Pin status mutation ${mutation.mutationId} references missing ${mutation.pinId}`);
349
594
  return;
350
595
  }
596
+ if (existing.scope === "project" && row.source_project_enabled === 0)
597
+ return;
598
+ if (!pinMutationMatchesSource(existing, row)) {
599
+ warnings.push(`Pin status mutation ${mutation.mutationId} crosses its source project/session scope`);
600
+ return;
601
+ }
351
602
  existing.status = mutation.status;
352
603
  existing.updatedAt = mutation.createdAt;
353
604
  existing.statusReason = mutation.reason;
@@ -355,11 +606,24 @@ export class MemoryRepository {
355
606
  return;
356
607
  }
357
608
  if (mutation.operation === "supersede") {
609
+ if (mutation.item.scope === "project" && row.source_project_enabled === 0)
610
+ return;
358
611
  const previous = pins.get(mutation.previousId);
359
612
  if (!previous) {
360
613
  warnings.push(`Pin supersession ${mutation.mutationId} references missing ${mutation.previousId}`);
361
614
  return;
362
615
  }
616
+ const replacement = pins.get(mutation.item.id);
617
+ if (!replacement
618
+ || replacement.provenance.sourceSessionId !== row.session_id
619
+ || replacement.provenance.mutationEntryId !== entryIdFromKey(row.mutation_key)) {
620
+ warnings.push(`Pin supersession ${mutation.mutationId} has no valid replacement ${mutation.item.id}`);
621
+ return;
622
+ }
623
+ if (previous.scope !== replacement.scope || !pinMutationMatchesSource(previous, row)) {
624
+ warnings.push(`Pin supersession ${mutation.mutationId} crosses its source project/session scope`);
625
+ return;
626
+ }
363
627
  previous.status = "superseded";
364
628
  previous.supersededBy = mutation.item.id;
365
629
  previous.updatedAt = mutation.createdAt;
@@ -370,6 +634,8 @@ export class MemoryRepository {
370
634
  if (mutation.operation === "status")
371
635
  return;
372
636
  const value = mutation.item;
637
+ if (value.scope === "project" && row.source_project_enabled === 0)
638
+ return;
373
639
  const existing = pins.get(value.id);
374
640
  if (existing) {
375
641
  if (existing.content !== value.content || existing.scope !== value.scope) {
@@ -381,10 +647,16 @@ export class MemoryRepository {
381
647
  warnings.push(`Project pin ${value.id} has no project path`);
382
648
  return;
383
649
  }
650
+ if (value.scope === "project" && value.projectPath !== row.source_project_path) {
651
+ warnings.push(`Project pin ${value.id} does not match its canonical source project`);
652
+ return;
653
+ }
384
654
  if (value.scope === "branch" && !value.branchLeafId) {
385
655
  warnings.push(`Branch pin ${value.id} has no branch leaf`);
386
656
  return;
387
657
  }
658
+ const mutationEntryId = entryIdFromKey(row.mutation_key);
659
+ const sourceEntryId = value.sourceEntryId ?? mutationEntryId;
388
660
  const item = {
389
661
  id: value.id,
390
662
  scope: value.scope,
@@ -396,13 +668,103 @@ export class MemoryRepository {
396
668
  status: "active",
397
669
  createdAt: value.createdAt,
398
670
  updatedAt: mutation.createdAt,
399
- ...(value.sourceEntryId
400
- ? { sourceEntryId: value.sourceEntryId }
401
- : { sourceEntryId: entryIdFromKey(row.mutation_key) }),
671
+ sourceEntryId,
402
672
  ...(value.sourceFile ? { sourceFile: value.sourceFile } : {}),
673
+ provenance: {
674
+ sourceSessionId: row.session_id,
675
+ sourceSessionFile: row.session_file,
676
+ mutationEntryId,
677
+ ...(row.source_parent_entry_id
678
+ ? { sourceBranchEntryId: row.source_parent_entry_id }
679
+ : {}),
680
+ sourceEntryIds: [...new Set([sourceEntryId, mutationEntryId])],
681
+ ...(mutation.operation === "supersede" ? { supersedes: mutation.previousId } : {}),
682
+ contradicts: [],
683
+ },
403
684
  };
404
685
  pins.set(item.id, item);
405
686
  }
687
+ insertMemoryMutations(mutations, ignoreExisting = false) {
688
+ const insert = this.database.prepare(`
689
+ INSERT ${ignoreExisting ? "OR IGNORE" : ""} INTO memory_mutations(
690
+ mutation_key, mutation_id, session_id, created_at, entry_order,
691
+ source_parent_entry_id, payload_json
692
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
693
+ `);
694
+ const existingIds = ignoreExisting
695
+ ? new Set(this.database.prepare("SELECT mutation_id FROM memory_mutations WHERE session_id = ?")
696
+ .all(mutations[0]?.sessionId ?? "")
697
+ .map((row) => row.mutation_id))
698
+ : new Set();
699
+ for (const mutation of mutations) {
700
+ if (existingIds.has(mutation.mutationId))
701
+ continue;
702
+ existingIds.add(mutation.mutationId);
703
+ insert.run(mutation.mutationKey, mutation.mutationId, mutation.sessionId, mutation.createdAt, mutation.entryOrder, mutation.sourceParentEntryId ?? null, JSON.stringify(mutation.payload));
704
+ }
705
+ }
706
+ insertPinMutations(mutations, ignoreExisting = false) {
707
+ const insert = this.database.prepare(`
708
+ INSERT ${ignoreExisting ? "OR IGNORE" : ""} INTO pin_mutations(
709
+ mutation_key, mutation_id, session_id, created_at, entry_order,
710
+ source_parent_entry_id, payload_json
711
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
712
+ `);
713
+ const existingIds = ignoreExisting
714
+ ? new Set(this.database.prepare("SELECT mutation_id FROM pin_mutations WHERE session_id = ?")
715
+ .all(mutations[0]?.sessionId ?? "")
716
+ .map((row) => row.mutation_id))
717
+ : new Set();
718
+ for (const mutation of mutations) {
719
+ if (existingIds.has(mutation.mutationId))
720
+ continue;
721
+ existingIds.add(mutation.mutationId);
722
+ insert.run(mutation.mutationKey, mutation.mutationId, mutation.sessionId, mutation.createdAt, mutation.entryOrder, mutation.sourceParentEntryId ?? null, JSON.stringify(mutation.payload));
723
+ }
724
+ }
725
+ writeProjectSourceState(checkpoint) {
726
+ this.database.prepare(`
727
+ INSERT INTO project_memory_sessions(
728
+ project_path, session_id, session_file, header_hash, file_size,
729
+ file_mtime_ms, checkpoint_offset, checkpoint_hash_start, checkpoint_hash,
730
+ indexed_records, indexed_mutations, malformed_lines, status, last_error,
731
+ indexed_at
732
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
733
+ ON CONFLICT(project_path, session_id) DO UPDATE SET
734
+ session_file = excluded.session_file,
735
+ header_hash = excluded.header_hash,
736
+ file_size = excluded.file_size,
737
+ file_mtime_ms = excluded.file_mtime_ms,
738
+ checkpoint_offset = excluded.checkpoint_offset,
739
+ checkpoint_hash_start = excluded.checkpoint_hash_start,
740
+ checkpoint_hash = excluded.checkpoint_hash,
741
+ indexed_records = excluded.indexed_records,
742
+ indexed_mutations = excluded.indexed_mutations,
743
+ malformed_lines = excluded.malformed_lines,
744
+ status = excluded.status,
745
+ last_error = excluded.last_error,
746
+ indexed_at = excluded.indexed_at
747
+ `).run(checkpoint.projectPath, checkpoint.sessionId, checkpoint.sessionFile, checkpoint.headerHash, checkpoint.fileSize, checkpoint.fileMtimeMs, checkpoint.checkpointOffset, checkpoint.checkpointHashStart, checkpoint.checkpointHash ?? null, checkpoint.indexedRecords, checkpoint.indexedMutations, checkpoint.malformedLines, checkpoint.status, checkpoint.lastError ?? null, checkpoint.indexedAt);
748
+ }
749
+ mapProjectSourceCheckpoint(row) {
750
+ return {
751
+ projectPath: row.project_path,
752
+ sessionId: row.session_id,
753
+ sessionFile: row.session_file,
754
+ headerHash: row.header_hash,
755
+ fileSize: row.file_size,
756
+ fileMtimeMs: row.file_mtime_ms,
757
+ checkpointOffset: row.checkpoint_offset,
758
+ checkpointHashStart: row.checkpoint_hash_start,
759
+ ...(row.checkpoint_hash ? { checkpointHash: row.checkpoint_hash } : {}),
760
+ indexedRecords: row.indexed_records,
761
+ indexedMutations: row.indexed_mutations,
762
+ malformedLines: row.malformed_lines,
763
+ status: row.status,
764
+ ...(row.last_error ? { lastError: row.last_error } : {}),
765
+ indexedAt: row.indexed_at,
766
+ };
767
+ }
406
768
  memorySourceIds(memoryId) {
407
769
  return this.memorySources(new Set([memoryId])).get(memoryId) ?? [];
408
770
  }