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.
- package/CHANGELOG.md +12 -0
- package/MEMORY_ATLAS.md +2 -2
- package/MEMORY_MODEL.md +1 -1
- package/README.md +6 -5
- package/RELEASE_CHECKLIST.md +5 -5
- package/dist/atlas/MemoryAtlasService.d.ts +1 -0
- package/dist/atlas/MemoryAtlasService.js +36 -4
- package/dist/atlas/MemoryAtlasTypes.d.ts +7 -0
- package/dist/belief/BeliefStore.d.ts +3 -1
- package/dist/belief/BeliefStore.js +10 -2
- package/dist/bin/episode.js +107 -16
- package/dist/bin/import-support.js +28 -11
- package/dist/config/CogmemConfig.d.ts +1 -0
- package/dist/config/CogmemConfig.js +45 -0
- package/dist/core/MemoryGraph.js +25 -8
- package/dist/dream/DreamScheduler.d.ts +6 -1
- package/dist/dream/DreamScheduler.js +36 -6
- package/dist/engine/CognitiveGraphCompiler.js +1 -1
- package/dist/engine/ConsolidationPipeline.js +4 -4
- package/dist/engine/DeepWritePromotionPolicy.d.ts +2 -0
- package/dist/engine/DeepWritePromotionPolicy.js +25 -4
- package/dist/engine/DreamCuratorWorker.d.ts +3 -0
- package/dist/engine/DreamCuratorWorker.js +18 -3
- package/dist/engine/FactCompiler.js +2 -2
- package/dist/entity/EntityGovernanceService.js +1 -1
- package/dist/episode/EpisodeActiveScopeGuard.d.ts +3 -0
- package/dist/episode/EpisodeActiveScopeGuard.js +46 -0
- package/dist/episode/EpisodeAssembler.d.ts +20 -2
- package/dist/episode/EpisodeAssembler.js +458 -98
- package/dist/episode/EpisodeBoundaryAuditService.d.ts +70 -0
- package/dist/episode/EpisodeBoundaryAuditService.js +276 -0
- package/dist/episode/EpisodeBoundaryPolicy.d.ts +75 -0
- package/dist/episode/EpisodeBoundaryPolicy.js +189 -0
- package/dist/episode/EpisodeBoundaryReplayEngine.d.ts +65 -0
- package/dist/episode/EpisodeBoundaryReplayEngine.js +272 -0
- package/dist/episode/EpisodeImportIdentity.d.ts +1 -0
- package/dist/episode/EpisodeImportIdentity.js +1 -0
- package/dist/episode/EpisodeInvariantValidator.d.ts +16 -0
- package/dist/episode/EpisodeInvariantValidator.js +99 -0
- package/dist/episode/EpisodeSplitPlanner.d.ts +89 -0
- package/dist/episode/EpisodeSplitPlanner.js +225 -0
- package/dist/episode/EpisodeStore.d.ts +67 -2
- package/dist/episode/EpisodeStore.js +443 -59
- package/dist/episode/EpisodeTypes.d.ts +2 -2
- package/dist/episode/TurnRelationClassifier.d.ts +9 -0
- package/dist/episode/TurnRelationClassifier.js +42 -12
- package/dist/episode/index.d.ts +3 -0
- package/dist/episode/index.js +3 -0
- package/dist/factory.d.ts +62 -3
- package/dist/factory.js +308 -58
- package/dist/mcp/CoreMcpTools.js +103 -8
- package/dist/mcp/server.js +1 -1
- package/dist/migrations/0028_episode_boundary_guardrails.d.ts +3 -0
- package/dist/migrations/0028_episode_boundary_guardrails.js +47 -0
- package/dist/migrations/0029_episode_active_scope_guard.d.ts +3 -0
- package/dist/migrations/0029_episode_active_scope_guard.js +16 -0
- package/dist/migrations/0030_memory_event_local_date_source.d.ts +3 -0
- package/dist/migrations/0030_memory_event_local_date_source.js +111 -0
- package/dist/migrations/0031_episode_boundary_integrity_repair.d.ts +7 -0
- package/dist/migrations/0031_episode_boundary_integrity_repair.js +124 -0
- package/dist/migrations/SchemaMigrationRunner.d.ts +2 -0
- package/dist/migrations/SchemaMigrationRunner.js +46 -15
- package/dist/migrations/index.d.ts +6 -1
- package/dist/migrations/index.js +15 -1
- package/dist/public.d.ts +1 -1
- package/dist/public.js +1 -1
- package/dist/recall/BrainRecall.js +1 -1
- package/dist/store/DeepWriteCandidateStore.d.ts +21 -3
- package/dist/store/DeepWriteCandidateStore.js +118 -11
- package/dist/store/EntityStore.d.ts +4 -0
- package/dist/store/EntityStore.js +24 -12
- package/dist/store/EventStore.d.ts +7 -2
- package/dist/store/EventStore.js +144 -23
- package/dist/store/FactStore.js +6 -5
- package/dist/store/MemoryAtlasStore.d.ts +1 -0
- package/dist/store/MemoryAtlasStore.js +54 -0
- package/dist/types/ExtensionPoints.d.ts +3 -1
- package/dist/types/index.d.ts +1 -0
- package/examples/hermes-backend/AGENTS.md +1 -1
- package/examples/hermes-backend/README.md +1 -1
- package/examples/hermes-backend/SKILL.md +2 -2
- package/examples/hermes-backend/references/operations.md +2 -2
- package/examples/openclaw-backend/AGENTS.md +1 -1
- package/examples/openclaw-backend/README.md +1 -1
- package/examples/openclaw-backend/SKILL.md +2 -2
- package/examples/openclaw-backend/references/operations.md +2 -2
- package/package.json +1 -1
package/dist/mcp/CoreMcpTools.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 === '')
|
package/dist/mcp/server.js
CHANGED
|
@@ -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,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,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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|