cogmem 3.7.1 → 3.7.3

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 (87) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MEMORY_ATLAS.md +2 -2
  3. package/MEMORY_MODEL.md +1 -1
  4. package/README.md +6 -5
  5. package/RELEASE_CHECKLIST.md +5 -5
  6. package/dist/atlas/MemoryAtlasService.d.ts +1 -0
  7. package/dist/atlas/MemoryAtlasService.js +36 -4
  8. package/dist/atlas/MemoryAtlasTypes.d.ts +7 -0
  9. package/dist/belief/BeliefStore.d.ts +3 -1
  10. package/dist/belief/BeliefStore.js +10 -2
  11. package/dist/bin/episode.js +107 -16
  12. package/dist/bin/import-support.js +28 -11
  13. package/dist/config/CogmemConfig.d.ts +1 -0
  14. package/dist/config/CogmemConfig.js +45 -0
  15. package/dist/core/MemoryGraph.js +25 -8
  16. package/dist/dream/DreamScheduler.d.ts +6 -1
  17. package/dist/dream/DreamScheduler.js +36 -6
  18. package/dist/engine/CognitiveGraphCompiler.js +1 -1
  19. package/dist/engine/ConsolidationPipeline.js +4 -4
  20. package/dist/engine/DeepWritePromotionPolicy.d.ts +2 -0
  21. package/dist/engine/DeepWritePromotionPolicy.js +25 -4
  22. package/dist/engine/DreamCuratorWorker.d.ts +3 -0
  23. package/dist/engine/DreamCuratorWorker.js +18 -3
  24. package/dist/engine/FactCompiler.js +2 -2
  25. package/dist/entity/EntityGovernanceService.js +1 -1
  26. package/dist/episode/EpisodeActiveScopeGuard.d.ts +3 -0
  27. package/dist/episode/EpisodeActiveScopeGuard.js +46 -0
  28. package/dist/episode/EpisodeAssembler.d.ts +20 -2
  29. package/dist/episode/EpisodeAssembler.js +458 -98
  30. package/dist/episode/EpisodeBoundaryAuditService.d.ts +70 -0
  31. package/dist/episode/EpisodeBoundaryAuditService.js +276 -0
  32. package/dist/episode/EpisodeBoundaryPolicy.d.ts +75 -0
  33. package/dist/episode/EpisodeBoundaryPolicy.js +189 -0
  34. package/dist/episode/EpisodeBoundaryReplayEngine.d.ts +65 -0
  35. package/dist/episode/EpisodeBoundaryReplayEngine.js +272 -0
  36. package/dist/episode/EpisodeImportIdentity.d.ts +1 -0
  37. package/dist/episode/EpisodeImportIdentity.js +1 -0
  38. package/dist/episode/EpisodeInvariantValidator.d.ts +16 -0
  39. package/dist/episode/EpisodeInvariantValidator.js +99 -0
  40. package/dist/episode/EpisodeSplitPlanner.d.ts +89 -0
  41. package/dist/episode/EpisodeSplitPlanner.js +225 -0
  42. package/dist/episode/EpisodeStore.d.ts +67 -2
  43. package/dist/episode/EpisodeStore.js +443 -59
  44. package/dist/episode/EpisodeTypes.d.ts +2 -2
  45. package/dist/episode/TurnRelationClassifier.d.ts +9 -0
  46. package/dist/episode/TurnRelationClassifier.js +42 -12
  47. package/dist/episode/index.d.ts +3 -0
  48. package/dist/episode/index.js +3 -0
  49. package/dist/factory.d.ts +62 -3
  50. package/dist/factory.js +308 -58
  51. package/dist/mcp/CoreMcpTools.js +103 -8
  52. package/dist/mcp/server.js +1 -1
  53. package/dist/migrations/0028_episode_boundary_guardrails.d.ts +3 -0
  54. package/dist/migrations/0028_episode_boundary_guardrails.js +47 -0
  55. package/dist/migrations/0029_episode_active_scope_guard.d.ts +3 -0
  56. package/dist/migrations/0029_episode_active_scope_guard.js +16 -0
  57. package/dist/migrations/0030_memory_event_local_date_source.d.ts +3 -0
  58. package/dist/migrations/0030_memory_event_local_date_source.js +111 -0
  59. package/dist/migrations/0031_episode_boundary_integrity_repair.d.ts +7 -0
  60. package/dist/migrations/0031_episode_boundary_integrity_repair.js +124 -0
  61. package/dist/migrations/SchemaMigrationRunner.d.ts +2 -0
  62. package/dist/migrations/SchemaMigrationRunner.js +46 -15
  63. package/dist/migrations/index.d.ts +6 -1
  64. package/dist/migrations/index.js +15 -1
  65. package/dist/public.d.ts +1 -1
  66. package/dist/public.js +1 -1
  67. package/dist/recall/BrainRecall.js +1 -1
  68. package/dist/store/DeepWriteCandidateStore.d.ts +21 -3
  69. package/dist/store/DeepWriteCandidateStore.js +118 -11
  70. package/dist/store/EntityStore.d.ts +4 -0
  71. package/dist/store/EntityStore.js +24 -12
  72. package/dist/store/EventStore.d.ts +7 -2
  73. package/dist/store/EventStore.js +144 -23
  74. package/dist/store/FactStore.js +6 -5
  75. package/dist/store/MemoryAtlasStore.d.ts +1 -0
  76. package/dist/store/MemoryAtlasStore.js +54 -0
  77. package/dist/types/ExtensionPoints.d.ts +3 -1
  78. package/dist/types/index.d.ts +1 -0
  79. package/examples/hermes-backend/AGENTS.md +1 -1
  80. package/examples/hermes-backend/README.md +1 -1
  81. package/examples/hermes-backend/SKILL.md +2 -2
  82. package/examples/hermes-backend/references/operations.md +2 -2
  83. package/examples/openclaw-backend/AGENTS.md +1 -1
  84. package/examples/openclaw-backend/README.md +1 -1
  85. package/examples/openclaw-backend/SKILL.md +2 -2
  86. package/examples/openclaw-backend/references/operations.md +2 -2
  87. package/package.json +1 -1
@@ -16,6 +16,11 @@ const EPISODE_MESSAGE_SCHEMA = {
16
16
  text: STRING_SCHEMA,
17
17
  externalMessageId: STRING_SCHEMA,
18
18
  timestamp: NUMBER_SCHEMA,
19
+ threadId: STRING_SCHEMA,
20
+ turnId: STRING_SCHEMA,
21
+ turnSeq: NUMBER_SCHEMA,
22
+ localDate: STRING_SCHEMA,
23
+ eventOrdinal: NUMBER_SCHEMA,
19
24
  },
20
25
  required: ['role', 'text'],
21
26
  };
@@ -118,6 +123,7 @@ export function listCogmemMcpTools() {
118
123
  projectId: STRING_SCHEMA, sessionId: STRING_SCHEMA, sourceAgent: STRING_SCHEMA,
119
124
  role: { type: 'string', enum: ['user', 'assistant', 'agent', 'tool', 'system', 'narrator'] },
120
125
  text: STRING_SCHEMA, externalMessageId: STRING_SCHEMA, timestamp: NUMBER_SCHEMA,
126
+ threadId: STRING_SCHEMA, turnId: STRING_SCHEMA, turnSeq: NUMBER_SCHEMA, localDate: STRING_SCHEMA, eventOrdinal: NUMBER_SCHEMA,
121
127
  },
122
128
  required: ['projectId', 'sessionId', 'sourceAgent', 'role', 'text', 'externalMessageId'],
123
129
  },
@@ -144,6 +150,42 @@ export function listCogmemMcpTools() {
144
150
  inputSchema: { type: 'object', properties: { projectId: STRING_SCHEMA, sessionId: STRING_SCHEMA, limit: NUMBER_SCHEMA } },
145
151
  annotations: { title: 'Episode Status', readOnlyHint: true, destructiveHint: false, idempotentHint: true },
146
152
  },
153
+ {
154
+ name: 'cogmem_episode_audit_boundaries',
155
+ description: 'Read-only deterministic audit of episode boundary health. Does not write audit rows, repair, split, Dream, Atlas, or activation telemetry.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ projectId: STRING_SCHEMA, episodeId: STRING_SCHEMA, status: STRING_SCHEMA, limit: NUMBER_SCHEMA, cursor: STRING_SCHEMA,
160
+ maxEvents: NUMBER_SCHEMA, maxDurationMs: NUMBER_SCHEMA, maxIdleGapMs: NUMBER_SCHEMA, timezone: STRING_SCHEMA,
161
+ },
162
+ required: ['projectId'],
163
+ },
164
+ annotations: { title: 'Audit Episode Boundaries', readOnlyHint: true, destructiveHint: false, idempotentHint: true },
165
+ },
166
+ {
167
+ name: 'cogmem_episode_boundary_decisions',
168
+ description: 'Read-only project-scoped trace of episode boundary decisions, including CPU decision, reviewer decision, final decision, guard codes, and warnings.',
169
+ inputSchema: {
170
+ type: 'object',
171
+ properties: { projectId: STRING_SCHEMA, primaryEventId: STRING_SCHEMA, limit: NUMBER_SCHEMA },
172
+ required: ['projectId'],
173
+ },
174
+ annotations: { title: 'Episode Boundary Decisions', readOnlyHint: true, destructiveHint: false, idempotentHint: true },
175
+ },
176
+ {
177
+ name: 'cogmem_episode_split_plan',
178
+ description: 'Read-only deterministic split preview for a single episode. Never calls repair or returns an apply command.',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ projectId: STRING_SCHEMA, episodeId: STRING_SCHEMA, maxEvents: NUMBER_SCHEMA, maxDurationMs: NUMBER_SCHEMA,
183
+ maxIdleGapMs: NUMBER_SCHEMA, timezone: STRING_SCHEMA, includeEventIds: { type: 'boolean' },
184
+ },
185
+ required: ['projectId', 'episodeId'],
186
+ },
187
+ annotations: { title: 'Plan Episode Split', readOnlyHint: true, destructiveHint: false, idempotentHint: true },
188
+ },
147
189
  {
148
190
  name: 'cogmem_topic_list',
149
191
  description: 'List project-scoped user-shaped topic nodes and audited relations. Read-only.',
@@ -186,8 +228,8 @@ export function listCogmemMcpTools() {
186
228
  name: 'cogmem_episode_seal',
187
229
  description: 'Explicitly seal one episode and enqueue it for conditional Dream processing.',
188
230
  inputSchema: {
189
- type: 'object', properties: { episodeId: STRING_SCHEMA, mode: { type: 'string', enum: ['soft', 'hard', 'manual', 'batch'] }, reason: STRING_SCHEMA },
190
- required: ['episodeId'],
231
+ type: 'object', properties: { projectId: STRING_SCHEMA, episodeId: STRING_SCHEMA, mode: { type: 'string', enum: ['soft', 'hard', 'manual', 'batch'] }, reason: STRING_SCHEMA },
232
+ required: ['projectId', 'episodeId'],
191
233
  },
192
234
  annotations: { title: 'Seal Episode', readOnlyHint: false, destructiveHint: false, idempotentHint: true },
193
235
  },
@@ -330,8 +372,9 @@ export function listCogmemMcpTools() {
330
372
  }
331
373
  export async function callCogmemMcpTool(name, args, runtime = {}) {
332
374
  const input = args || {};
333
- const opened = openRuntimeKernel(runtime);
375
+ let opened;
334
376
  try {
377
+ opened = openRuntimeKernel(runtime);
335
378
  switch (name) {
336
379
  case 'cogmem_remember_turn':
337
380
  return await rememberTurn(opened.kernel, input);
@@ -354,6 +397,26 @@ export async function callCogmemMcpTool(name, args, runtime = {}) {
354
397
  return await episodeImport(opened.kernel, input);
355
398
  case 'cogmem_episode_status':
356
399
  return episodeStatus(opened.kernel, input);
400
+ case 'cogmem_episode_audit_boundaries':
401
+ return jsonResult(opened.kernel.auditEpisodeBoundaries({
402
+ projectId: requiredString(input.projectId, 'projectId'), episodeId: optionalString(input.episodeId),
403
+ status: optionalEpisodeStatus(input.status), limit: optionalNumber(input.limit), cursor: optionalString(input.cursor),
404
+ maxEvents: optionalNumber(input.maxEvents), maxDurationMs: optionalNumber(input.maxDurationMs),
405
+ maxIdleGapMs: optionalNumber(input.maxIdleGapMs), timezone: optionalString(input.timezone),
406
+ }));
407
+ case 'cogmem_episode_boundary_decisions':
408
+ return jsonResult({ decisions: opened.kernel.listEpisodeBoundaryDecisions({
409
+ projectId: requiredString(input.projectId, 'projectId'),
410
+ primaryEventId: optionalString(input.primaryEventId),
411
+ limit: optionalNumber(input.limit),
412
+ }) });
413
+ case 'cogmem_episode_split_plan':
414
+ return jsonResult(opened.kernel.planEpisodeSplit({
415
+ projectId: requiredString(input.projectId, 'projectId'), episodeId: requiredString(input.episodeId, 'episodeId'),
416
+ maxEvents: optionalNumber(input.maxEvents), maxDurationMs: optionalNumber(input.maxDurationMs),
417
+ maxIdleGapMs: optionalNumber(input.maxIdleGapMs), timezone: optionalString(input.timezone),
418
+ includeEventIds: input.includeEventIds === true,
419
+ }));
357
420
  case 'cogmem_topic_list': {
358
421
  const projectId = requiredString(input.projectId, 'projectId');
359
422
  return jsonResult({ topics: opened.kernel.userTopicPathRegistry.list(projectId), relations: opened.kernel.topicRelationGraph.list(projectId) });
@@ -372,7 +435,7 @@ export async function callCogmemMcpTool(name, args, runtime = {}) {
372
435
  case 'cogmem_episode_repair':
373
436
  return jsonResult(episodeRepair(opened.kernel, input));
374
437
  case 'cogmem_episode_seal':
375
- return jsonResult(opened.kernel.sealEpisode(requiredString(input.episodeId, 'episodeId'), {
438
+ return jsonResult(sealProjectEpisode(opened.kernel, requiredString(input.projectId, 'projectId'), requiredString(input.episodeId, 'episodeId'), {
376
439
  mode: optionalEpisodeClosureMode(input.mode),
377
440
  reason: optionalString(input.reason) || 'mcp_manual_seal',
378
441
  }));
@@ -449,7 +512,7 @@ export async function callCogmemMcpTool(name, args, runtime = {}) {
449
512
  return jsonResult({ error: error instanceof Error ? error.message : String(error) }, true);
450
513
  }
451
514
  finally {
452
- if (opened.shouldClose)
515
+ if (opened?.shouldClose)
453
516
  opened.kernel.close();
454
517
  }
455
518
  }
@@ -471,6 +534,8 @@ async function episodeAppend(kernel, input) {
471
534
  sourceAgent: requiredString(input.sourceAgent, 'sourceAgent'),
472
535
  role: requiredEpisodeRole(input.role), text,
473
536
  externalMessageId: requiredString(input.externalMessageId, 'externalMessageId'), timestamp: optionalNumber(input.timestamp),
537
+ threadId: optionalString(input.threadId), turnId: optionalString(input.turnId), turnSeq: optionalNumber(input.turnSeq),
538
+ localDate: optionalString(input.localDate), eventOrdinal: optionalNumber(input.eventOrdinal),
474
539
  }));
475
540
  }
476
541
  async function episodeImport(kernel, input) {
@@ -499,8 +564,13 @@ async function episodeImport(kernel, input) {
499
564
  autoIdentityUsed = true;
500
565
  return {
501
566
  role, text, timestamp,
567
+ threadId: optionalString(message.threadId),
568
+ turnId: optionalString(message.turnId),
569
+ turnSeq: optionalNumber(message.turnSeq),
570
+ localDate: optionalString(message.localDate),
571
+ eventOrdinal: optionalNumber(message.eventOrdinal),
502
572
  externalMessageId: suppliedIdentity
503
- || stableIdentity({ role, text, timestamp }),
573
+ || stableIdentity({ role, text, timestamp, sourcePosition: optionalString(message.sourceRange) || optionalString(message.recordId) || index }),
504
574
  };
505
575
  });
506
576
  const results = [];
@@ -590,7 +660,7 @@ function episodeRepair(kernel, input) {
590
660
  if (operation === 'reclassify')
591
661
  return kernel.repairEpisode({
592
662
  operation, projectId, episodeId: requiredString(input.episodeId, 'episodeId'),
593
- episodeType: optionalString(input.episodeType), topicPath: optionalString(input.topicPath), importance: optionalNumber(input.importance),
663
+ episodeType: optionalEpisodeType(input.episodeType), topicPath: optionalString(input.topicPath), importance: optionalNumber(input.importance),
594
664
  });
595
665
  if (operation === 'requeue-dream' || operation === 'invalidate-dream-run')
596
666
  return kernel.repairEpisode({
@@ -598,6 +668,12 @@ function episodeRepair(kernel, input) {
598
668
  });
599
669
  throw new Error(`invalid episode repair operation: ${operation}`);
600
670
  }
671
+ function sealProjectEpisode(kernel, projectId, episodeId, input) {
672
+ const episode = kernel.getEpisode(episodeId);
673
+ if (!episode || episode.projectId !== projectId)
674
+ throw new Error(`episode_project_mismatch:${episodeId}`);
675
+ return kernel.sealEpisode(episodeId, input);
676
+ }
601
677
  function dreamRecommendation(kernel, projectId, requestedMode) {
602
678
  const status = kernel.getEpisodeDreamStatus(projectId);
603
679
  const backlog = status.pending + status.retryScheduled;
@@ -789,7 +865,26 @@ function optionalProspectiveStatuses(value) {
789
865
  return statuses;
790
866
  }
791
867
  function optionalNumber(value) {
792
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
868
+ if (value === undefined || value === null)
869
+ return undefined;
870
+ if (typeof value === 'number' && Number.isFinite(value))
871
+ return value;
872
+ throw new Error('value must be a finite number');
873
+ }
874
+ function optionalEpisodeType(value) {
875
+ if (value === undefined || value === null || value === '')
876
+ return undefined;
877
+ const allowed = new Set(['discussion', 'decision', 'correction', 'preference', 'goal', 'debugging', 'planning', 'prospective', 'general']);
878
+ if (typeof value !== 'string' || !allowed.has(value))
879
+ throw new Error('episodeType must be a valid episode type');
880
+ return value;
881
+ }
882
+ function optionalEpisodeStatus(value) {
883
+ if (value === undefined || value === null || value === '')
884
+ return undefined;
885
+ if (value === 'open' || value === 'soft_sealed' || value === 'sealed')
886
+ return value;
887
+ throw new Error('status must be open, soft_sealed, or sealed');
793
888
  }
794
889
  function optionalTurnIngestMode(value) {
795
890
  if (value === undefined || value === null || value === '')
@@ -5,7 +5,7 @@ import { callCogmemMcpTool, listCogmemMcpTools, } from './CoreMcpTools.js';
5
5
  export function createCogmemMcpServer(runtime = {}) {
6
6
  const server = new Server({
7
7
  name: 'cogmem-core',
8
- version: '3.7.1',
8
+ version: '3.7.2',
9
9
  }, {
10
10
  capabilities: {
11
11
  tools: {},
@@ -0,0 +1,3 @@
1
+ import type { Migration } from '../types/Migration.js';
2
+ export declare const migration_0028: Migration;
3
+ //# sourceMappingURL=0028_episode_boundary_guardrails.d.ts.map
@@ -0,0 +1,47 @@
1
+ export const migration_0028 = {
2
+ version: '0028',
3
+ description: 'episode boundary guardrail decision audit',
4
+ up(db) {
5
+ db.exec(`
6
+ CREATE TABLE IF NOT EXISTS episode_boundary_decisions (
7
+ decision_id TEXT PRIMARY KEY,
8
+ project_id TEXT NOT NULL,
9
+ session_id TEXT NOT NULL,
10
+ source_agent TEXT,
11
+ thread_id TEXT,
12
+ primary_event_id TEXT NOT NULL,
13
+ previous_episode_id TEXT,
14
+ resulting_episode_id TEXT,
15
+ policy_version TEXT NOT NULL,
16
+ mode TEXT NOT NULL,
17
+ guard_action TEXT NOT NULL,
18
+ guard_codes_json TEXT NOT NULL,
19
+ metrics_json TEXT NOT NULL,
20
+ cpu_decision_json TEXT NOT NULL,
21
+ reviewer_invoked INTEGER NOT NULL,
22
+ reviewer_decision_json TEXT,
23
+ final_decision_json TEXT NOT NULL,
24
+ warnings_json TEXT NOT NULL,
25
+ created_at INTEGER NOT NULL,
26
+ UNIQUE(project_id, primary_event_id, policy_version)
27
+ );
28
+ CREATE INDEX IF NOT EXISTS idx_episode_boundary_project_created
29
+ ON episode_boundary_decisions(project_id, created_at);
30
+ CREATE INDEX IF NOT EXISTS idx_episode_boundary_previous
31
+ ON episode_boundary_decisions(previous_episode_id);
32
+ CREATE INDEX IF NOT EXISTS idx_episode_boundary_resulting
33
+ ON episode_boundary_decisions(resulting_episode_id);
34
+ CREATE INDEX IF NOT EXISTS idx_episode_boundary_primary
35
+ ON episode_boundary_decisions(primary_event_id);
36
+ `);
37
+ },
38
+ down(db) {
39
+ db.exec(`
40
+ DROP INDEX IF EXISTS idx_episode_boundary_primary;
41
+ DROP INDEX IF EXISTS idx_episode_boundary_resulting;
42
+ DROP INDEX IF EXISTS idx_episode_boundary_previous;
43
+ DROP INDEX IF EXISTS idx_episode_boundary_project_created;
44
+ DROP TABLE IF EXISTS episode_boundary_decisions;
45
+ `);
46
+ },
47
+ };
@@ -0,0 +1,3 @@
1
+ import type { Migration } from '../types/Migration.js';
2
+ export declare const migration_0029: Migration;
3
+ //# sourceMappingURL=0029_episode_active_scope_guard.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { sealDuplicateOpenEpisodes } from '../episode/EpisodeActiveScopeGuard.js';
2
+ export const migration_0029 = {
3
+ version: '0029',
4
+ description: 'episode active scope uniqueness guard',
5
+ up(db) {
6
+ sealDuplicateOpenEpisodes(db);
7
+ db.exec(`
8
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_episodes_one_active_scope
9
+ ON memory_episodes(project_id, session_id, COALESCE(source_agent, ''), COALESCE(conversation_thread_id, ''))
10
+ WHERE status = 'open';
11
+ `);
12
+ },
13
+ down(db) {
14
+ db.exec(`DROP INDEX IF EXISTS idx_memory_episodes_one_active_scope;`);
15
+ },
16
+ };
@@ -0,0 +1,3 @@
1
+ import type { Migration } from '../types/Migration.js';
2
+ export declare const migration_0030: Migration;
3
+ //# sourceMappingURL=0030_memory_event_local_date_source.d.ts.map
@@ -0,0 +1,111 @@
1
+ export const migration_0030 = {
2
+ version: '0030',
3
+ description: 'memory event local date source provenance',
4
+ up(db) {
5
+ if (db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_events'`).get()) {
6
+ const columns = new Set(db.prepare(`PRAGMA table_info(memory_events)`).all().map((row) => row.name));
7
+ if (!columns.has('local_date_source')) {
8
+ db.exec(`ALTER TABLE memory_events ADD COLUMN local_date_source TEXT NOT NULL DEFAULT 'legacy_unknown';`);
9
+ }
10
+ // 0030 is also responsible for recovering provenance that was already
11
+ // present in the legacy payload before the column existed.
12
+ const eventColumns = new Set(db.prepare(`PRAGMA table_info(memory_events)`).all().map((row) => row.name));
13
+ if (eventColumns.has('payload_json')) {
14
+ db.exec(`
15
+ UPDATE memory_events
16
+ SET local_date_source = CASE
17
+ WHEN json_valid(payload_json) AND json_extract(payload_json, '$.metadata.localDateSource') IN ('event_store_utc_default', 'generated_utc') THEN 'generated_utc'
18
+ WHEN json_valid(payload_json) AND json_extract(payload_json, '$.metadata.localDateSource') = 'explicit' THEN 'explicit'
19
+ WHEN local_date_source IN ('explicit', 'generated_utc') THEN local_date_source
20
+ ELSE 'legacy_unknown'
21
+ END;
22
+ `);
23
+ }
24
+ }
25
+ if (db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_episode_events'`).get()) {
26
+ const links = db.prepare(`
27
+ SELECT episode_id, event_id, position
28
+ FROM memory_episode_events
29
+ ORDER BY episode_id, position, event_id
30
+ `).all();
31
+ const positions = new Map();
32
+ let episodeId;
33
+ let nextPosition = 0;
34
+ for (const link of links) {
35
+ if (link.episode_id !== episodeId) {
36
+ episodeId = link.episode_id;
37
+ nextPosition = 0;
38
+ }
39
+ nextPosition += 1;
40
+ positions.set(`${link.episode_id}\u0000${link.event_id}`, nextPosition);
41
+ }
42
+ // Move through a disjoint temporary range so an existing unique index
43
+ // cannot observe an intermediate duplicate while positions are compacted.
44
+ const temporary = db.prepare(`UPDATE memory_episode_events SET position = ? WHERE episode_id = ? AND event_id = ?`);
45
+ for (const link of links)
46
+ temporary.run(-link.position - 1, link.episode_id, link.event_id);
47
+ for (const link of links)
48
+ temporary.run(positions.get(`${link.episode_id}\u0000${link.event_id}`) ?? 0, link.episode_id, link.event_id);
49
+ const hasEpisodes = Boolean(db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_episodes'`).get());
50
+ const hasEvents = Boolean(db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_events'`).get());
51
+ const eventColumns = hasEvents
52
+ ? new Set(db.prepare(`PRAGMA table_info(memory_events)`).all().map((row) => row.name))
53
+ : new Set();
54
+ const hasGlobalSeq = eventColumns.has('global_seq');
55
+ if (hasEpisodes) {
56
+ const episodes = db.prepare(`SELECT episode_id FROM memory_episodes`).all();
57
+ const first = db.prepare(`
58
+ SELECT event_id FROM memory_episode_events WHERE episode_id = ? ORDER BY position, event_id LIMIT 1
59
+ `);
60
+ const last = db.prepare(`
61
+ SELECT event_id FROM memory_episode_events WHERE episode_id = ? ORDER BY position DESC, event_id DESC LIMIT 1
62
+ `);
63
+ const count = db.prepare(`SELECT COUNT(*) AS count FROM memory_episode_events WHERE episode_id = ?`);
64
+ const rebuild = hasEvents && hasGlobalSeq ? db.prepare(`
65
+ UPDATE memory_episodes SET event_count = ?, start_event_id = COALESCE(?, start_event_id), end_event_id = COALESCE(?, end_event_id),
66
+ start_seq = COALESCE((SELECT global_seq FROM memory_events WHERE event_id = ?), start_seq),
67
+ end_seq = COALESCE((SELECT global_seq FROM memory_events WHERE event_id = ?), end_seq)
68
+ WHERE episode_id = ?
69
+ `) : db.prepare(`
70
+ UPDATE memory_episodes SET event_count = ?, start_event_id = COALESCE(?, start_event_id), end_event_id = COALESCE(?, end_event_id)
71
+ WHERE episode_id = ?
72
+ `);
73
+ for (const episode of episodes) {
74
+ const actualCount = Number(count.get(episode.episode_id).count ?? 0);
75
+ const firstEventId = first.get(episode.episode_id)?.event_id ?? null;
76
+ const lastEventId = last.get(episode.episode_id)?.event_id ?? null;
77
+ if (hasEvents && hasGlobalSeq)
78
+ rebuild.run(actualCount, firstEventId, lastEventId, firstEventId, lastEventId, episode.episode_id);
79
+ else
80
+ rebuild.run(actualCount, firstEventId, lastEventId, episode.episode_id);
81
+ }
82
+ }
83
+ const receiptTable = Boolean(db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'episode_closure_receipts'`).get());
84
+ if (receiptTable) {
85
+ const receipts = db.prepare(`SELECT receipt_id, episode_id, source_event_ids_json FROM episode_closure_receipts`).all();
86
+ const selectPosition = db.prepare(`SELECT event_id, position FROM memory_episode_events WHERE episode_id = ?`);
87
+ const updateReceipt = db.prepare(`UPDATE episode_closure_receipts SET source_event_ids_json = ? WHERE receipt_id = ?`);
88
+ for (const receipt of receipts) {
89
+ let eventIds;
90
+ try {
91
+ const parsed = JSON.parse(receipt.source_event_ids_json);
92
+ eventIds = Array.isArray(parsed) ? parsed.filter((id) => typeof id === 'string') : [];
93
+ }
94
+ catch {
95
+ eventIds = [];
96
+ }
97
+ const positions = new Map(selectPosition.all(receipt.episode_id).map((row) => [row.event_id, row.position]));
98
+ eventIds.sort((left, right) => (positions.get(left) ?? Number.MAX_SAFE_INTEGER) - (positions.get(right) ?? Number.MAX_SAFE_INTEGER));
99
+ updateReceipt.run(JSON.stringify(eventIds), receipt.receipt_id);
100
+ }
101
+ }
102
+ db.exec(`
103
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_episode_events_episode_position_unique
104
+ ON memory_episode_events(episode_id, position);
105
+ `);
106
+ }
107
+ },
108
+ down() {
109
+ // SQLite cannot drop a column without rebuilding the table; keep the compatible additive column.
110
+ },
111
+ };
@@ -0,0 +1,7 @@
1
+ import type { Migration } from '../types/Migration.js';
2
+ /**
3
+ * An immutable repair migration for databases which claimed 0030 through
4
+ * `_meta` while only receiving a subset of its DDL/data work.
5
+ */
6
+ export declare const migration_0031: Migration;
7
+ //# sourceMappingURL=0031_episode_boundary_integrity_repair.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { sealDuplicateOpenEpisodes } from '../episode/EpisodeActiveScopeGuard.js';
2
+ function tableExists(db, name) {
3
+ return Boolean(db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`).get(name));
4
+ }
5
+ function columns(db, table) {
6
+ return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
7
+ }
8
+ /**
9
+ * An immutable repair migration for databases which claimed 0030 through
10
+ * `_meta` while only receiving a subset of its DDL/data work.
11
+ */
12
+ export const migration_0031 = {
13
+ version: '0031',
14
+ description: 'episode boundary integrity repair',
15
+ up(db) {
16
+ db.exec(`
17
+ CREATE TABLE IF NOT EXISTS _episode_integrity_markers (
18
+ marker TEXT PRIMARY KEY,
19
+ applied_at INTEGER NOT NULL
20
+ );
21
+ CREATE TABLE IF NOT EXISTS import_source_anchors (
22
+ project_id TEXT NOT NULL,
23
+ source_id TEXT NOT NULL,
24
+ import_anchor TEXT NOT NULL,
25
+ event_id TEXT NOT NULL UNIQUE,
26
+ content_hash TEXT NOT NULL,
27
+ created_at INTEGER NOT NULL,
28
+ PRIMARY KEY (project_id, source_id, import_anchor)
29
+ );
30
+ `);
31
+ if (tableExists(db, 'memory_events')) {
32
+ if (!columns(db, 'memory_events').has('local_date_source')) {
33
+ db.exec(`ALTER TABLE memory_events ADD COLUMN local_date_source TEXT NOT NULL DEFAULT 'legacy_unknown';`);
34
+ }
35
+ if (columns(db, 'memory_events').has('payload_json')) {
36
+ db.exec(`
37
+ UPDATE memory_events
38
+ SET local_date_source = CASE
39
+ WHEN json_valid(payload_json) AND json_extract(payload_json, '$.metadata.localDateSource') IN ('event_store_utc_default', 'generated_utc') THEN 'generated_utc'
40
+ WHEN json_valid(payload_json) AND json_extract(payload_json, '$.metadata.localDateSource') = 'explicit' THEN 'explicit'
41
+ WHEN local_date_source IN ('explicit', 'generated_utc') THEN local_date_source
42
+ ELSE 'legacy_unknown'
43
+ END;
44
+ `);
45
+ }
46
+ }
47
+ if (tableExists(db, 'memory_episode_events')) {
48
+ const links = db.prepare(`
49
+ SELECT rowid, episode_id, event_id, position
50
+ FROM memory_episode_events
51
+ ORDER BY episode_id, position, event_id, rowid
52
+ `).all();
53
+ const temporary = db.prepare(`UPDATE memory_episode_events SET position = ? WHERE rowid = ?`);
54
+ const finalPosition = db.prepare(`UPDATE memory_episode_events SET position = ? WHERE rowid = ?`);
55
+ const counters = new Map();
56
+ for (const link of links)
57
+ temporary.run(-1_000_000_000 - link.rowid, link.rowid);
58
+ for (const link of links) {
59
+ const next = (counters.get(link.episode_id) ?? 0) + 1;
60
+ counters.set(link.episode_id, next);
61
+ finalPosition.run(next, link.rowid);
62
+ }
63
+ if (tableExists(db, 'memory_episodes')) {
64
+ const eventColumns = tableExists(db, 'memory_events') ? columns(db, 'memory_events') : new Set();
65
+ const hasGlobalSeq = eventColumns.has('global_seq');
66
+ const episodes = db.prepare(`SELECT episode_id FROM memory_episodes`).all();
67
+ const count = db.prepare(`SELECT COUNT(*) AS count FROM memory_episode_events WHERE episode_id = ?`);
68
+ const first = db.prepare(`SELECT event_id FROM memory_episode_events WHERE episode_id = ? ORDER BY position, event_id LIMIT 1`);
69
+ const last = db.prepare(`SELECT event_id FROM memory_episode_events WHERE episode_id = ? ORDER BY position DESC, event_id DESC LIMIT 1`);
70
+ const rebuild = hasGlobalSeq ? db.prepare(`
71
+ UPDATE memory_episodes SET event_count = ?, start_event_id = ?, end_event_id = ?,
72
+ start_seq = (SELECT global_seq FROM memory_events WHERE event_id = ?),
73
+ end_seq = (SELECT global_seq FROM memory_events WHERE event_id = ?)
74
+ WHERE episode_id = ?
75
+ `) : db.prepare(`
76
+ UPDATE memory_episodes SET event_count = ?, start_event_id = ?, end_event_id = ? WHERE episode_id = ?
77
+ `);
78
+ const deleteDependents = (episodeId) => {
79
+ if (tableExists(db, 'episode_closure_receipts'))
80
+ db.prepare(`DELETE FROM episode_closure_receipts WHERE episode_id = ?`).run(episodeId);
81
+ if (tableExists(db, 'episode_dream_jobs'))
82
+ db.prepare(`DELETE FROM episode_dream_jobs WHERE episode_id = ?`).run(episodeId);
83
+ if (tableExists(db, 'episode_cross_refs'))
84
+ db.prepare(`DELETE FROM episode_cross_refs WHERE episode_id = ? OR referenced_episode_id = ?`).run(episodeId, episodeId);
85
+ if (tableExists(db, 'episode_boundary_decisions')) {
86
+ db.prepare(`DELETE FROM episode_boundary_decisions WHERE previous_episode_id = ? OR resulting_episode_id = ?`).run(episodeId, episodeId);
87
+ }
88
+ db.prepare(`DELETE FROM memory_episodes WHERE episode_id = ?`).run(episodeId);
89
+ };
90
+ for (const episode of episodes) {
91
+ const actual = Number(count.get(episode.episode_id).count ?? 0);
92
+ if (actual === 0) {
93
+ // The public schema has non-null pointers. An empty episode cannot
94
+ // represent correct pointer state, so remove it atomically.
95
+ deleteDependents(episode.episode_id);
96
+ continue;
97
+ }
98
+ const firstEventId = first.get(episode.episode_id)?.event_id;
99
+ const lastEventId = last.get(episode.episode_id)?.event_id;
100
+ if (!firstEventId || !lastEventId)
101
+ continue;
102
+ if (hasGlobalSeq)
103
+ rebuild.run(actual, firstEventId, lastEventId, firstEventId, lastEventId, episode.episode_id);
104
+ else
105
+ rebuild.run(actual, firstEventId, lastEventId, episode.episode_id);
106
+ }
107
+ }
108
+ db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_episode_events_episode_position_unique ON memory_episode_events(episode_id, position);`);
109
+ }
110
+ if (tableExists(db, 'memory_episodes')) {
111
+ sealDuplicateOpenEpisodes(db);
112
+ db.exec(`
113
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_episodes_one_active_scope
114
+ ON memory_episodes(project_id, session_id, COALESCE(source_agent, ''), COALESCE(conversation_thread_id, ''))
115
+ WHERE status = 'open';
116
+ `);
117
+ }
118
+ db.prepare(`INSERT OR REPLACE INTO _episode_integrity_markers (marker, applied_at) VALUES ('episode_boundary_integrity_0031', ?)`)
119
+ .run(Date.now());
120
+ },
121
+ down() {
122
+ // Integrity repairs are deliberately forward-only.
123
+ },
124
+ };
@@ -19,11 +19,13 @@ export declare class SchemaMigrationRunner {
19
19
  constructor(db: Database, migrations: Migration[], options?: SchemaMigrationRunnerOptions);
20
20
  plan(): Migration[];
21
21
  run(options?: SchemaMigrationRunOptions): SchemaMigrationResult;
22
+ private ensureMigrationTable;
22
23
  currentVersion(): string | undefined;
23
24
  private appliedVersions;
24
25
  private schemaMigrationsTableExists;
25
26
  private legacySchemaVersion;
26
27
  private legacyCurrentVersion;
27
28
  private adoptLegacyVersion;
29
+ private migrationSchemaSatisfied;
28
30
  }
29
31
  //# sourceMappingURL=SchemaMigrationRunner.d.ts.map
@@ -6,16 +6,8 @@ export class SchemaMigrationRunner {
6
6
  this.db = db;
7
7
  this.migrations = migrations;
8
8
  this.options = options;
9
- if (this.options.readonly)
10
- return;
11
- this.db.exec(`
12
- CREATE TABLE IF NOT EXISTS _schema_migrations (
13
- version TEXT PRIMARY KEY,
14
- description TEXT NOT NULL,
15
- applied_at TEXT NOT NULL
16
- );
17
- `);
18
- this.adoptLegacyVersion();
9
+ // Construction is read-only. Schema bookkeeping is created only when a
10
+ // non-dry run is explicitly requested.
19
11
  }
20
12
  plan() {
21
13
  const applied = this.appliedVersions();
@@ -24,10 +16,13 @@ export class SchemaMigrationRunner {
24
16
  .filter((migration) => !applied.has(migration.version));
25
17
  }
26
18
  run(options = {}) {
27
- const pending = this.plan();
28
- if (options.dryRun) {
19
+ if (options.dryRun || this.options.readonly) {
20
+ const pending = this.plan();
29
21
  return { pending: pending.map((item) => item.version), applied: [], currentVersion: this.currentVersion(), dryRun: true };
30
22
  }
23
+ this.ensureMigrationTable();
24
+ this.adoptLegacyVersion();
25
+ const pending = this.plan();
31
26
  const applied = [];
32
27
  const transaction = this.db.transaction(() => {
33
28
  for (const migration of pending) {
@@ -42,6 +37,15 @@ export class SchemaMigrationRunner {
42
37
  transaction();
43
38
  return { pending: pending.map((item) => item.version), applied, currentVersion: this.currentVersion(), dryRun: false };
44
39
  }
40
+ ensureMigrationTable() {
41
+ this.db.exec(`
42
+ CREATE TABLE IF NOT EXISTS _schema_migrations (
43
+ version TEXT PRIMARY KEY,
44
+ description TEXT NOT NULL,
45
+ applied_at TEXT NOT NULL
46
+ );
47
+ `);
48
+ }
45
49
  currentVersion() {
46
50
  const legacyCurrent = this.legacyCurrentVersion();
47
51
  if (!this.schemaMigrationsTableExists()) {
@@ -56,7 +60,12 @@ export class SchemaMigrationRunner {
56
60
  const applied = new Set();
57
61
  const legacyVersion = this.legacySchemaVersion();
58
62
  for (const migration of this.migrations) {
59
- if (legacyVersion !== undefined && Number.parseInt(migration.version, 10) <= legacyVersion) {
63
+ // `_meta.schema_version` was written by older kernels without a durable
64
+ // migration receipt. It is a hint only: adopting a high legacy version
65
+ // must not hide a partially applied integrity migration.
66
+ if (legacyVersion !== undefined
67
+ && Number.parseInt(migration.version, 10) <= legacyVersion
68
+ && this.migrationSchemaSatisfied(migration.version)) {
60
69
  applied.add(migration.version);
61
70
  }
62
71
  }
@@ -64,7 +73,8 @@ export class SchemaMigrationRunner {
64
73
  return applied;
65
74
  }
66
75
  for (const row of this.db.prepare(`SELECT version FROM _schema_migrations`).all()) {
67
- applied.add(row.version);
76
+ if (this.migrationSchemaSatisfied(row.version))
77
+ applied.add(row.version);
68
78
  }
69
79
  return applied;
70
80
  }
@@ -100,9 +110,30 @@ export class SchemaMigrationRunner {
100
110
  VALUES (?, ?, ?)
101
111
  `);
102
112
  for (const migration of this.migrations) {
103
- if (Number.parseInt(migration.version, 10) <= legacyVersion) {
113
+ if (Number.parseInt(migration.version, 10) <= legacyVersion && this.migrationSchemaSatisfied(migration.version)) {
104
114
  insert.run(migration.version, `adopted: ${migration.description}`, new Date(0).toISOString());
105
115
  }
106
116
  }
107
117
  }
118
+ migrationSchemaSatisfied(version) {
119
+ if (version === '0029') {
120
+ return Boolean(this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_memory_episodes_one_active_scope'`).get());
121
+ }
122
+ if (version === '0030') {
123
+ const eventTable = Boolean(this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_events'`).get());
124
+ const eventColumns = eventTable
125
+ ? new Set(this.db.prepare(`PRAGMA table_info(memory_events)`).all().map((row) => row.name))
126
+ : new Set();
127
+ const positionIndex = Boolean(this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = 'idx_memory_episode_events_episode_position_unique'`).get());
128
+ return (!eventTable || eventColumns.has('local_date_source')) && positionIndex;
129
+ }
130
+ if (version === '0031') {
131
+ return Boolean(this.db.prepare(`
132
+ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_episode_integrity_markers'
133
+ `).get()) && Boolean(this.db.prepare(`
134
+ SELECT 1 FROM _episode_integrity_markers WHERE marker = 'episode_boundary_integrity_0031'
135
+ `).get());
136
+ }
137
+ return true;
138
+ }
108
139
  }