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
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { sealDuplicateOpenEpisodes } from './EpisodeActiveScopeGuard.js';
|
|
2
3
|
import { summarizeEpisode } from './EpisodeSemanticSummarizer.js';
|
|
4
|
+
import { replayEpisodeBoundaryState } from './EpisodeBoundaryReplayEngine.js';
|
|
3
5
|
export class EpisodeStore {
|
|
4
6
|
db;
|
|
5
7
|
resolveEvent;
|
|
6
8
|
constructor(db, resolveEvent, options = {}) {
|
|
7
9
|
this.db = db;
|
|
8
10
|
this.resolveEvent = resolveEvent;
|
|
11
|
+
this.db.exec('PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;');
|
|
12
|
+
const foreignKeys = this.db.prepare('PRAGMA foreign_keys').get();
|
|
13
|
+
if (foreignKeys?.foreign_keys !== 1)
|
|
14
|
+
throw new Error('episode_store_foreign_keys_disabled');
|
|
9
15
|
if (options.initializeSchemaForTests !== false)
|
|
10
16
|
this.initializeSchema();
|
|
11
17
|
}
|
|
18
|
+
getDatabase() {
|
|
19
|
+
return this.db;
|
|
20
|
+
}
|
|
12
21
|
createEpisode(input) {
|
|
13
22
|
const episodeId = `episode-${randomUUID()}`;
|
|
14
23
|
this.db.prepare(`
|
|
@@ -39,32 +48,25 @@ export class EpisodeStore {
|
|
|
39
48
|
return legacy ? mapEpisode(legacy) : undefined;
|
|
40
49
|
}
|
|
41
50
|
findActiveEpisodeRow(projectId, sessionId, sourceAgent, conversationThreadId) {
|
|
42
|
-
const where = [`project_id = ?`, `session_id = ?`, `status IN ('open', 'soft_sealed')`];
|
|
43
|
-
const params = [projectId, sessionId];
|
|
44
|
-
if (sourceAgent) {
|
|
45
|
-
where.push('source_agent = ?');
|
|
46
|
-
params.push(sourceAgent);
|
|
47
|
-
}
|
|
48
|
-
if (conversationThreadId) {
|
|
49
|
-
where.push('conversation_thread_id = ?');
|
|
50
|
-
params.push(conversationThreadId);
|
|
51
|
-
}
|
|
52
51
|
return this.db.prepare(`
|
|
53
|
-
SELECT * FROM memory_episodes
|
|
52
|
+
SELECT * FROM memory_episodes
|
|
53
|
+
WHERE project_id = ? AND session_id = ? AND status IN ('open', 'soft_sealed')
|
|
54
|
+
AND COALESCE(source_agent, '') = ?
|
|
55
|
+
AND COALESCE(conversation_thread_id, '') = ?
|
|
54
56
|
ORDER BY CASE status WHEN 'open' THEN 0 ELSE 1 END, updated_at DESC LIMIT 1
|
|
55
|
-
`).get(
|
|
57
|
+
`).get(projectId, sessionId, sourceAgent ?? '', conversationThreadId ?? '');
|
|
56
58
|
}
|
|
57
59
|
claimLegacyEpisodeScope(episodeId, sourceAgent, conversationThreadId) {
|
|
58
60
|
if (!sourceAgent && !conversationThreadId)
|
|
59
61
|
return this.getEpisode(episodeId);
|
|
60
62
|
if (this.resolveEvent) {
|
|
61
|
-
const events = this.listEventLinks(episodeId)
|
|
63
|
+
const events = this.listEventLinks(episodeId)
|
|
62
64
|
.map((link) => this.resolveEvent(link.eventId)).filter((event) => Boolean(event));
|
|
63
65
|
const mismatch = events.some((event) => {
|
|
64
66
|
const metadata = event.payload?.metadata;
|
|
65
67
|
const eventSourceAgent = typeof metadata?.sourceAgent === 'string' ? metadata.sourceAgent : undefined;
|
|
66
|
-
return (sourceAgent
|
|
67
|
-
|| (conversationThreadId
|
|
68
|
+
return (sourceAgent !== undefined && eventSourceAgent !== sourceAgent)
|
|
69
|
+
|| (conversationThreadId !== undefined && (event.threadId || '') !== conversationThreadId);
|
|
68
70
|
});
|
|
69
71
|
if (mismatch)
|
|
70
72
|
return undefined;
|
|
@@ -107,15 +109,56 @@ export class EpisodeStore {
|
|
|
107
109
|
`).all(...params, Math.max(1, Math.min(Math.trunc(options.limit ?? 100), 1000)));
|
|
108
110
|
return rows.map(mapEpisode);
|
|
109
111
|
}
|
|
112
|
+
listEpisodesForBoundaryAudit(options) {
|
|
113
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 100), 1000));
|
|
114
|
+
const where = [`project_id = ?`];
|
|
115
|
+
const params = [options.projectId];
|
|
116
|
+
if (options.statuses?.length) {
|
|
117
|
+
where.push(`status IN (${options.statuses.map(() => '?').join(', ')})`);
|
|
118
|
+
params.push(...options.statuses);
|
|
119
|
+
}
|
|
120
|
+
const cursor = parseAuditCursor(options.cursor);
|
|
121
|
+
if (cursor) {
|
|
122
|
+
where.push(`(updated_at < ? OR (updated_at = ? AND episode_id < ?))`);
|
|
123
|
+
params.push(cursor.updatedAt, cursor.updatedAt, cursor.episodeId);
|
|
124
|
+
}
|
|
125
|
+
const rows = this.db.prepare(`
|
|
126
|
+
SELECT * FROM memory_episodes
|
|
127
|
+
WHERE ${where.join(' AND ')}
|
|
128
|
+
ORDER BY updated_at DESC, episode_id DESC
|
|
129
|
+
LIMIT ?
|
|
130
|
+
`).all(...params, limit + 1);
|
|
131
|
+
const selected = rows.slice(0, limit);
|
|
132
|
+
const last = selected.at(-1);
|
|
133
|
+
return {
|
|
134
|
+
episodes: selected.map(mapEpisode),
|
|
135
|
+
nextCursor: rows.length > limit && last ? formatAuditCursor(last.updated_at, last.episode_id) : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
110
138
|
appendEvent(input) {
|
|
111
139
|
const existing = this.getEventLink(input.eventId);
|
|
112
|
-
if (existing)
|
|
113
|
-
|
|
140
|
+
if (existing) {
|
|
141
|
+
if (existing.episodeId === input.episodeId)
|
|
142
|
+
return existing;
|
|
143
|
+
throw new Error(`episode_event_link_conflict:${input.eventId}`);
|
|
144
|
+
}
|
|
114
145
|
const episode = this.getEpisode(input.episodeId);
|
|
115
146
|
if (!episode || episode.status !== 'open')
|
|
116
147
|
throw new Error(`episode_not_open:${input.episodeId}`);
|
|
117
|
-
|
|
148
|
+
let created;
|
|
118
149
|
this.db.transaction(() => {
|
|
150
|
+
const locked = this.db.prepare(`SELECT status FROM memory_episodes WHERE episode_id = ?`).get(input.episodeId);
|
|
151
|
+
if (locked?.status !== 'open')
|
|
152
|
+
throw new Error(`episode_not_open:${input.episodeId}`);
|
|
153
|
+
const positionState = this.db.prepare(`
|
|
154
|
+
SELECT COUNT(*) AS count, MIN(position) AS min_position, MAX(position) AS max_position
|
|
155
|
+
FROM memory_episode_events WHERE episode_id = ?
|
|
156
|
+
`).get(input.episodeId);
|
|
157
|
+
const count = Number(positionState?.count ?? 0);
|
|
158
|
+
if (count > 0 && (positionState?.min_position !== 1 || positionState?.max_position !== count)) {
|
|
159
|
+
throw new Error(`episode_positions_corrupt:${input.episodeId}`);
|
|
160
|
+
}
|
|
161
|
+
const position = count + 1;
|
|
119
162
|
this.db.prepare(`
|
|
120
163
|
INSERT INTO memory_episode_events (episode_id, event_id, position, relation, confidence, created_at)
|
|
121
164
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
@@ -124,14 +167,15 @@ export class EpisodeStore {
|
|
|
124
167
|
const importanceSignals = [...new Set([...episode.importanceSignals, ...(input.importanceSignals || [])])];
|
|
125
168
|
this.db.prepare(`
|
|
126
169
|
UPDATE memory_episodes SET
|
|
127
|
-
end_event_id = ?, end_seq = COALESCE(?, end_seq), event_count = ?, updated_at =
|
|
170
|
+
end_event_id = ?, end_seq = COALESCE(?, end_seq), event_count = ?, updated_at = MAX(updated_at, ?),
|
|
128
171
|
episode_type = COALESCE(?, episode_type), importance = MAX(importance, ?),
|
|
129
172
|
summary = CASE WHEN ? IS NULL OR ? = '' THEN summary ELSE SUBSTR(COALESCE(summary || '\n', '') || ?, 1, 1600) END,
|
|
130
173
|
candidate_types_json = ?, importance_signals_json = ?, importance_reason = COALESCE(?, importance_reason)
|
|
131
|
-
WHERE episode_id = ?
|
|
174
|
+
WHERE episode_id = ? AND status = 'open'
|
|
132
175
|
`).run(input.eventId, input.globalSeq ?? null, position, input.occurredAt, input.episodeType || null, input.importance ?? episode.importance, input.summaryText || null, input.summaryText || '', input.summaryText || '', JSON.stringify(candidateTypes), JSON.stringify(importanceSignals), input.importanceReason || null, input.episodeId);
|
|
176
|
+
created = { episodeId: input.episodeId, eventId: input.eventId, position, relation: input.relation, confidence: input.confidence, createdAt: input.occurredAt };
|
|
133
177
|
})();
|
|
134
|
-
return
|
|
178
|
+
return created;
|
|
135
179
|
}
|
|
136
180
|
getEventLink(eventId) {
|
|
137
181
|
const row = this.db.prepare(`SELECT * FROM memory_episode_events WHERE event_id = ?`).get(eventId);
|
|
@@ -139,9 +183,63 @@ export class EpisodeStore {
|
|
|
139
183
|
}
|
|
140
184
|
listEventLinks(episodeId) {
|
|
141
185
|
return this.db.prepare(`
|
|
142
|
-
SELECT * FROM memory_episode_events WHERE episode_id = ? ORDER BY position
|
|
186
|
+
SELECT * FROM memory_episode_events WHERE episode_id = ? ORDER BY position, event_id
|
|
143
187
|
`).all(episodeId).map(mapEventLink);
|
|
144
188
|
}
|
|
189
|
+
getBoundarySnapshot(episodeId, timezone) {
|
|
190
|
+
const episode = this.getEpisode(episodeId);
|
|
191
|
+
if (!episode)
|
|
192
|
+
throw new Error(`episode_not_found:${episodeId}`);
|
|
193
|
+
if (!this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_events'`).get()) {
|
|
194
|
+
const actualLinkCount = this.listEventLinks(episodeId).length;
|
|
195
|
+
return { eventCount: actualLinkCount, actualLinkCount, storedEventCount: episode.eventCount, eventCountMismatch: actualLinkCount !== episode.eventCount, startedAt: episode.startedAt, updatedAt: episode.updatedAt, trustedLocalDates: [] };
|
|
196
|
+
}
|
|
197
|
+
const actualLinkCount = Number(this.db.prepare(`SELECT COUNT(*) AS count FROM memory_episode_events WHERE episode_id = ?`).get(episodeId)?.count ?? 0);
|
|
198
|
+
const hasLocalDateSource = this.db.prepare(`PRAGMA table_info(memory_events)`).all()
|
|
199
|
+
.some((row) => row.name === 'local_date_source');
|
|
200
|
+
const rows = this.db.prepare(`
|
|
201
|
+
SELECT ee.event_id AS event_id, ee.position AS position, ee.relation AS relation,
|
|
202
|
+
ee.confidence AS confidence, ee.created_at AS created_at, e.role AS role,
|
|
203
|
+
e.local_date AS local_date,
|
|
204
|
+
${hasLocalDateSource ? 'e.local_date_source' : "'legacy_unknown'"} AS local_date_source,
|
|
205
|
+
e.occurred_at AS occurred_at
|
|
206
|
+
FROM memory_episode_events ee
|
|
207
|
+
JOIN memory_events e ON e.event_id = ee.event_id
|
|
208
|
+
WHERE ee.episode_id = ?
|
|
209
|
+
ORDER BY ee.position, ee.event_id
|
|
210
|
+
`).all(episodeId);
|
|
211
|
+
const state = replayEpisodeBoundaryState({
|
|
212
|
+
episode: { startedAt: episode.startedAt },
|
|
213
|
+
timezone,
|
|
214
|
+
pairs: rows.map((row) => ({
|
|
215
|
+
link: { episodeId, eventId: row.event_id, position: row.position, relation: row.relation, confidence: row.confidence, createdAt: row.created_at },
|
|
216
|
+
event: this.resolveEvent?.(row.event_id) || {
|
|
217
|
+
eventId: row.event_id,
|
|
218
|
+
role: row.role,
|
|
219
|
+
localDate: row.local_date ?? undefined,
|
|
220
|
+
localDateSource: row.local_date_source || 'legacy_unknown',
|
|
221
|
+
occurredAt: row.occurred_at ?? undefined,
|
|
222
|
+
payload: {},
|
|
223
|
+
},
|
|
224
|
+
})),
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
eventCount: actualLinkCount,
|
|
228
|
+
actualLinkCount,
|
|
229
|
+
storedEventCount: episode.eventCount,
|
|
230
|
+
eventCountMismatch: actualLinkCount !== episode.eventCount,
|
|
231
|
+
startedAt: episode.startedAt,
|
|
232
|
+
updatedAt: state.lastEventAt ?? episode.updatedAt,
|
|
233
|
+
lastEventAt: state.lastEventAt,
|
|
234
|
+
lastTrustedUserLocalDate: state.lastTrustedUserLocalDate,
|
|
235
|
+
lastTrustedLocalDate: state.lastTrustedUserLocalDate,
|
|
236
|
+
trustedLocalDates: state.trustedLocalDates,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
transaction(fn) {
|
|
240
|
+
const tx = this.db.transaction(fn);
|
|
241
|
+
return typeof tx.immediate === 'function' ? tx.immediate() : tx();
|
|
242
|
+
}
|
|
145
243
|
isEpisodeEmpty(episodeId) {
|
|
146
244
|
const episode = this.getEpisode(episodeId);
|
|
147
245
|
if (!episode)
|
|
@@ -176,6 +274,11 @@ export class EpisodeStore {
|
|
|
176
274
|
throw new Error('episode_not_found');
|
|
177
275
|
if (source.projectId !== target.projectId)
|
|
178
276
|
throw new Error('episode_project_mismatch');
|
|
277
|
+
if (source.sessionId !== target.sessionId
|
|
278
|
+
|| (source.sourceAgent || '') !== (target.sourceAgent || '')
|
|
279
|
+
|| (source.conversationThreadId || '') !== (target.conversationThreadId || '')) {
|
|
280
|
+
throw new Error('episode_repair_scope_mismatch');
|
|
281
|
+
}
|
|
179
282
|
this.db.transaction(() => {
|
|
180
283
|
const nextPosition = this.listEventLinks(targetEpisodeId).length + 1;
|
|
181
284
|
this.db.prepare(`UPDATE memory_episode_events SET episode_id = ?, position = ?, created_at = ? WHERE event_id = ?`)
|
|
@@ -222,6 +325,46 @@ export class EpisodeStore {
|
|
|
222
325
|
`).run(repairId, input.projectId, input.operation, JSON.stringify(input.payload), JSON.stringify(input.before), JSON.stringify(input.after), input.now ?? Date.now());
|
|
223
326
|
return repairId;
|
|
224
327
|
}
|
|
328
|
+
recordBoundaryDecision(input) {
|
|
329
|
+
const decisionId = `episode-boundary-${randomUUID()}`;
|
|
330
|
+
const result = this.db.prepare(`
|
|
331
|
+
INSERT INTO episode_boundary_decisions (
|
|
332
|
+
decision_id, project_id, session_id, source_agent, thread_id, primary_event_id,
|
|
333
|
+
previous_episode_id, resulting_episode_id, policy_version, mode, guard_action,
|
|
334
|
+
guard_codes_json, metrics_json, cpu_decision_json, reviewer_invoked,
|
|
335
|
+
reviewer_decision_json, final_decision_json, warnings_json, created_at
|
|
336
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
337
|
+
ON CONFLICT(project_id, primary_event_id, policy_version) DO NOTHING
|
|
338
|
+
`).run(decisionId, input.projectId, input.sessionId, input.sourceAgent || null, input.threadId || null, input.primaryEventId, input.previousEpisodeId || null, input.resultingEpisodeId || null, input.policyVersion, input.mode, input.guardAction, JSON.stringify(input.guardCodes), JSON.stringify(input.metrics), JSON.stringify(safeDecision(input.cpuDecision)), input.reviewerInvoked ? 1 : 0, input.reviewerDecision ? JSON.stringify(safeDecision(input.reviewerDecision)) : null, JSON.stringify(safeDecision(input.finalDecision)), JSON.stringify(input.warnings), input.createdAt ?? Date.now());
|
|
339
|
+
if (Number(result.changes || 0) > 0)
|
|
340
|
+
return { recorded: true, decisionId, status: 'inserted' };
|
|
341
|
+
const existingRow = this.db.prepare(`
|
|
342
|
+
SELECT * FROM episode_boundary_decisions
|
|
343
|
+
WHERE project_id = ? AND primary_event_id = ? AND policy_version = ?
|
|
344
|
+
LIMIT 1
|
|
345
|
+
`).get(input.projectId, input.primaryEventId, input.policyVersion);
|
|
346
|
+
const existing = existingRow ? mapBoundaryDecision(existingRow) : undefined;
|
|
347
|
+
if (existing && !sameBoundaryDecision(existing, input))
|
|
348
|
+
throw new Error(`episode_boundary_decision_conflict:${input.primaryEventId}`);
|
|
349
|
+
return { recorded: false, decisionId: existing?.decisionId, status: 'duplicate' };
|
|
350
|
+
}
|
|
351
|
+
listBoundaryDecisions(options = {}) {
|
|
352
|
+
const where = [];
|
|
353
|
+
const params = [];
|
|
354
|
+
if (options.projectId) {
|
|
355
|
+
where.push('project_id = ?');
|
|
356
|
+
params.push(options.projectId);
|
|
357
|
+
}
|
|
358
|
+
if (options.primaryEventId) {
|
|
359
|
+
where.push('primary_event_id = ?');
|
|
360
|
+
params.push(options.primaryEventId);
|
|
361
|
+
}
|
|
362
|
+
const rows = this.db.prepare(`
|
|
363
|
+
SELECT * FROM episode_boundary_decisions ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
364
|
+
ORDER BY created_at DESC LIMIT ?
|
|
365
|
+
`).all(...params, Math.max(1, Math.min(Math.trunc(options.limit ?? 100), 1000)));
|
|
366
|
+
return rows.map(mapBoundaryDecision);
|
|
367
|
+
}
|
|
225
368
|
invalidateEpisodeDerivedState(episodeId, now) {
|
|
226
369
|
this.db.prepare(`DELETE FROM episode_dream_jobs WHERE episode_id = ?`).run(episodeId);
|
|
227
370
|
this.db.prepare(`
|
|
@@ -231,17 +374,30 @@ export class EpisodeStore {
|
|
|
231
374
|
}
|
|
232
375
|
resequenceEpisode(episodeId, now) {
|
|
233
376
|
const links = this.listEventLinks(episodeId);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
377
|
+
if (links.length === 0) {
|
|
378
|
+
this.deleteEmptyEpisode(episodeId);
|
|
379
|
+
return;
|
|
237
380
|
}
|
|
238
|
-
const
|
|
381
|
+
const eventById = new Map(links.map((link) => [link.eventId, this.resolveEvent?.(link.eventId)]));
|
|
382
|
+
links.sort((left, right) => canonicalRepairOrder(eventById.get(left.eventId), eventById.get(right.eventId), left, right));
|
|
383
|
+
const updatePosition = this.db.prepare(`UPDATE memory_episode_events SET position = ? WHERE event_id = ? AND episode_id = ?`);
|
|
384
|
+
for (const [index, link] of links.entries())
|
|
385
|
+
updatePosition.run(-1_000_000_000 - index, link.eventId, episodeId);
|
|
386
|
+
for (const [index, link] of links.entries())
|
|
387
|
+
updatePosition.run(index + 1, link.eventId, episodeId);
|
|
388
|
+
const events = links.map((link) => eventById.get(link.eventId)).filter((event) => Boolean(event));
|
|
239
389
|
const first = events[0];
|
|
240
390
|
const last = events.at(-1);
|
|
241
391
|
this.db.prepare(`
|
|
242
|
-
UPDATE memory_episodes SET event_count = ?, start_event_id =
|
|
243
|
-
|
|
244
|
-
`).run(links.length, first?.eventId
|
|
392
|
+
UPDATE memory_episodes SET event_count = ?, start_event_id = ?, end_event_id = ?,
|
|
393
|
+
start_seq = ?, end_seq = ?, updated_at = ? WHERE episode_id = ?
|
|
394
|
+
`).run(links.length, first?.eventId ?? links[0].eventId, last?.eventId ?? links.at(-1).eventId, first?.globalSeq ?? null, last?.globalSeq ?? null, now, episodeId);
|
|
395
|
+
}
|
|
396
|
+
deleteEmptyEpisode(episodeId) {
|
|
397
|
+
this.db.prepare(`DELETE FROM episode_closure_receipts WHERE episode_id = ?`).run(episodeId);
|
|
398
|
+
this.db.prepare(`DELETE FROM episode_dream_jobs WHERE episode_id = ?`).run(episodeId);
|
|
399
|
+
this.db.prepare(`DELETE FROM episode_cross_refs WHERE episode_id = ? OR referenced_episode_id = ?`).run(episodeId, episodeId);
|
|
400
|
+
this.db.prepare(`DELETE FROM memory_episodes WHERE episode_id = ?`).run(episodeId);
|
|
245
401
|
}
|
|
246
402
|
reopenSoftEpisode(episodeId, now) {
|
|
247
403
|
const result = this.db.prepare(`
|
|
@@ -268,9 +424,11 @@ export class EpisodeStore {
|
|
|
268
424
|
const links = this.listEventLinks(episodeId);
|
|
269
425
|
if (links.length === 0 && input.mode !== 'soft')
|
|
270
426
|
throw new Error(`episode_empty:${episodeId}`);
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
427
|
+
const resolvedEvents = links.map((link) => this.resolveEvent?.(link.eventId));
|
|
428
|
+
const missingRawEvidence = Boolean(this.resolveEvent) && resolvedEvents.some((event) => !event);
|
|
429
|
+
const requiresReview = input.requiresReview === true || links.length === 0 || missingRawEvidence;
|
|
430
|
+
const semanticSummary = input.semanticSummary || summarizeEpisode(episode, resolvedEvents.filter((event) => Boolean(event)), links.map((link) => link.eventId));
|
|
431
|
+
const dreamMode = links.length >= 100
|
|
274
432
|
? 'deep'
|
|
275
433
|
: episode.importance >= 0.8 || ['decision', 'correction', 'preference', 'goal', 'prospective'].includes(episode.episodeType)
|
|
276
434
|
? 'micro' : 'normal';
|
|
@@ -311,6 +469,10 @@ export class EpisodeStore {
|
|
|
311
469
|
`).run(receipt.receiptId, episodeId, episode.projectId, input.mode, input.reason, JSON.stringify(receipt.sourceEventIds), receipt.startSeq ?? null, receipt.endSeq ?? null, receipt.topicPath || null, receipt.episodeType, receipt.importance, receipt.dreamRecommended ? 1 : 0, receipt.dreamMode, now, receipt.closureReasonCode, receipt.closureReasonDetail || null, receipt.requiresReview ? 1 : 0, JSON.stringify(receipt.ignoredNearbyEventIds), JSON.stringify(receipt.unassignedNearbyEventIds));
|
|
312
470
|
if (status === 'sealed' && receipt.dreamRecommended && !receipt.requiresReview)
|
|
313
471
|
this.enqueueDreamJob(episode, dreamMode, now);
|
|
472
|
+
if (status !== 'sealed') {
|
|
473
|
+
this.db.prepare(`DELETE FROM episode_dream_jobs WHERE episode_id = ?`).run(episodeId);
|
|
474
|
+
this.db.prepare(`UPDATE memory_episodes SET dream_status = 'none', dream_error = NULL, last_dream_run_id = NULL WHERE episode_id = ?`).run(episodeId);
|
|
475
|
+
}
|
|
314
476
|
})();
|
|
315
477
|
return receipt;
|
|
316
478
|
}
|
|
@@ -400,6 +562,7 @@ export class EpisodeStore {
|
|
|
400
562
|
JOIN memory_episodes e ON e.episode_id = j.episode_id
|
|
401
563
|
WHERE ${where.join(' AND ')}
|
|
402
564
|
AND e.event_count > 0
|
|
565
|
+
AND e.status = 'sealed'
|
|
403
566
|
AND EXISTS (SELECT 1 FROM memory_episode_events ee WHERE ee.episode_id = j.episode_id)
|
|
404
567
|
ORDER BY j.priority DESC, j.created_at LIMIT ?
|
|
405
568
|
`).all(...params, Math.max(1, Math.min(Math.trunc(input.limit), 100)));
|
|
@@ -419,6 +582,7 @@ export class EpisodeStore {
|
|
|
419
582
|
claimed.push({
|
|
420
583
|
episodeId: row.episode_id, projectId: row.project_id, leaseId, modeHint: row.mode_hint,
|
|
421
584
|
attempts: row.attempts + 1, createdAt: row.created_at,
|
|
585
|
+
leaseUntil: input.now + input.leaseMs, attemptGeneration: row.attempts + 1,
|
|
422
586
|
});
|
|
423
587
|
}
|
|
424
588
|
}
|
|
@@ -450,30 +614,38 @@ export class EpisodeStore {
|
|
|
450
614
|
this.markEmptyEpisodeDreamSkippedMany(episodeIds, now, 'episode_empty_skipped_no_raw_evidence');
|
|
451
615
|
return episodeIds.length;
|
|
452
616
|
}
|
|
453
|
-
completeDreamJob(episodeId, leaseId, candidateIds, now) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
617
|
+
completeDreamJob(episodeId, leaseId, candidateIds, now, commitCandidates) {
|
|
618
|
+
this.transaction(() => {
|
|
619
|
+
commitCandidates?.();
|
|
620
|
+
const result = this.db.prepare(`
|
|
621
|
+
UPDATE episode_dream_jobs SET state = 'processed', candidate_ids_json = ?, lease_id = NULL,
|
|
622
|
+
lease_until = NULL, retry_after = NULL, failure_category = NULL, last_error = NULL, updated_at = ?
|
|
623
|
+
WHERE episode_id = ? AND state = 'processing' AND lease_id = ?
|
|
624
|
+
AND lease_until IS NOT NULL AND lease_until >= ?
|
|
625
|
+
`).run(JSON.stringify(candidateIds), now, episodeId, leaseId, now);
|
|
626
|
+
if (!result.changes)
|
|
627
|
+
throw new Error(`episode_dream_lease_lost:${episodeId}`);
|
|
628
|
+
const episode = this.db.prepare(`
|
|
629
|
+
UPDATE memory_episodes SET dream_status = 'processed', last_dreamed_at = ?,
|
|
630
|
+
dream_candidate_count = ?, dream_error = NULL WHERE episode_id = ? AND status = 'sealed'
|
|
631
|
+
`).run(now, candidateIds.length, episodeId);
|
|
632
|
+
if (!episode.changes)
|
|
633
|
+
throw new Error(`episode_dream_episode_not_sealed:${episodeId}`);
|
|
634
|
+
});
|
|
465
635
|
}
|
|
466
636
|
failDreamJob(episodeId, leaseId, error, input) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
.
|
|
637
|
+
this.transaction(() => {
|
|
638
|
+
const state = input.terminal ? 'failed_terminal' : 'failed_retryable';
|
|
639
|
+
const result = this.db.prepare(`
|
|
640
|
+
UPDATE episode_dream_jobs SET state = ?, last_error = ?, failure_category = ?, retry_after = ?,
|
|
641
|
+
lease_id = NULL, lease_until = NULL, updated_at = ?
|
|
642
|
+
WHERE episode_id = ? AND state = 'processing' AND lease_id = ?
|
|
643
|
+
`).run(state, error.slice(0, 2000), input.failureCategory, input.retryAfter ?? null, input.now, episodeId, leaseId);
|
|
644
|
+
if (!result.changes)
|
|
645
|
+
return;
|
|
646
|
+
this.db.prepare(`UPDATE memory_episodes SET dream_status = 'failed', dream_error = ? WHERE episode_id = ?`)
|
|
647
|
+
.run(error.slice(0, 2000), episodeId);
|
|
648
|
+
});
|
|
477
649
|
}
|
|
478
650
|
retryFailed(projectId) {
|
|
479
651
|
const result = projectId
|
|
@@ -504,7 +676,7 @@ export class EpisodeStore {
|
|
|
504
676
|
AND state IN ('pending', 'processing', 'failed_retryable', 'retry_scheduled')
|
|
505
677
|
`).run(reason, now, ...episodeIds);
|
|
506
678
|
this.db.prepare(`
|
|
507
|
-
|
|
679
|
+
UPDATE memory_episodes SET dream_status = 'skipped', dream_error = ?, last_dream_run_id = NULL,
|
|
508
680
|
dream_candidate_count = 0, updated_at = ?
|
|
509
681
|
WHERE episode_id IN (${placeholders})
|
|
510
682
|
`).run(reason, now, ...episodeIds);
|
|
@@ -537,6 +709,10 @@ export class EpisodeStore {
|
|
|
537
709
|
status.failed = status.failedRetryable + status.failedTerminal;
|
|
538
710
|
return status;
|
|
539
711
|
}
|
|
712
|
+
getDreamJobState(episodeId) {
|
|
713
|
+
const row = this.db.prepare(`SELECT state FROM episode_dream_jobs WHERE episode_id = ?`).get(episodeId);
|
|
714
|
+
return row?.state;
|
|
715
|
+
}
|
|
540
716
|
countUnassignedRawEvents(projectId) {
|
|
541
717
|
const row = projectId
|
|
542
718
|
? this.db.prepare(`
|
|
@@ -604,6 +780,9 @@ export class EpisodeStore {
|
|
|
604
780
|
const run = (sql) => { count += Number(this.db.prepare(sql).run(projectId).changes || 0); };
|
|
605
781
|
run(`DELETE FROM episode_dream_runs WHERE project_id = ?`);
|
|
606
782
|
run(`DELETE FROM episode_dream_jobs WHERE project_id = ?`);
|
|
783
|
+
run(`DELETE FROM episode_boundary_decisions WHERE project_id = ?`);
|
|
784
|
+
run(`DELETE FROM episode_cross_refs WHERE project_id = ?`);
|
|
785
|
+
run(`DELETE FROM episode_repair_audit WHERE project_id = ?`);
|
|
607
786
|
run(`DELETE FROM episode_closure_receipts WHERE project_id = ?`);
|
|
608
787
|
run(`DELETE FROM episode_ingest_keys WHERE project_id = ?`);
|
|
609
788
|
run(`DELETE FROM episode_event_dispositions WHERE project_id = ?`);
|
|
@@ -620,12 +799,21 @@ export class EpisodeStore {
|
|
|
620
799
|
this.db.prepare(`
|
|
621
800
|
INSERT INTO episode_dream_jobs (episode_id, project_id, state, priority, mode_hint, created_at, updated_at)
|
|
622
801
|
VALUES (?, ?, 'pending', ?, ?, ?, ?)
|
|
623
|
-
ON CONFLICT(episode_id) DO
|
|
802
|
+
ON CONFLICT(episode_id) DO UPDATE SET
|
|
803
|
+
state = 'pending', mode_hint = excluded.mode_hint, retry_after = NULL, lease_id = NULL, lease_until = NULL,
|
|
804
|
+
failure_category = NULL, last_error = NULL, updated_at = excluded.updated_at
|
|
805
|
+
WHERE episode_dream_jobs.state IN ('pending', 'failed_retryable', 'retry_scheduled')
|
|
624
806
|
`).run(episode.episodeId, episode.projectId, priority, modeHint, now, now);
|
|
625
|
-
this.db.prepare(`
|
|
626
|
-
|
|
807
|
+
const job = this.db.prepare(`SELECT state FROM episode_dream_jobs WHERE episode_id = ?`).get(episode.episodeId);
|
|
808
|
+
if (job?.state === 'pending') {
|
|
809
|
+
this.db.prepare(`UPDATE memory_episodes SET dream_status = 'queued', dream_error = NULL WHERE episode_id = ?`)
|
|
810
|
+
.run(episode.episodeId);
|
|
811
|
+
}
|
|
627
812
|
}
|
|
628
813
|
initializeSchema() {
|
|
814
|
+
this.transaction(() => this.initializeSchemaUnsafe());
|
|
815
|
+
}
|
|
816
|
+
initializeSchemaUnsafe() {
|
|
629
817
|
// Migration 22 is authoritative. This keeps direct store construction compatible in tests and embeddings.
|
|
630
818
|
this.db.exec(`
|
|
631
819
|
CREATE TABLE IF NOT EXISTS memory_episodes (
|
|
@@ -641,7 +829,9 @@ export class EpisodeStore {
|
|
|
641
829
|
CREATE TABLE IF NOT EXISTS memory_episode_events (
|
|
642
830
|
episode_id TEXT NOT NULL, event_id TEXT NOT NULL UNIQUE, position INTEGER NOT NULL,
|
|
643
831
|
relation TEXT NOT NULL, confidence REAL NOT NULL, created_at INTEGER NOT NULL,
|
|
644
|
-
PRIMARY KEY (episode_id, event_id)
|
|
832
|
+
PRIMARY KEY (episode_id, event_id),
|
|
833
|
+
UNIQUE(episode_id, position),
|
|
834
|
+
FOREIGN KEY (episode_id) REFERENCES memory_episodes(episode_id) ON DELETE CASCADE
|
|
645
835
|
);
|
|
646
836
|
CREATE TABLE IF NOT EXISTS episode_closure_receipts (
|
|
647
837
|
receipt_id TEXT PRIMARY KEY, episode_id TEXT NOT NULL, project_id TEXT NOT NULL, closure_mode TEXT NOT NULL,
|
|
@@ -681,8 +871,89 @@ export class EpisodeStore {
|
|
|
681
871
|
repair_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, operation TEXT NOT NULL, payload_json TEXT NOT NULL,
|
|
682
872
|
before_json TEXT NOT NULL, after_json TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
683
873
|
);
|
|
874
|
+
CREATE TABLE IF NOT EXISTS episode_boundary_decisions (
|
|
875
|
+
decision_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, session_id TEXT NOT NULL,
|
|
876
|
+
source_agent TEXT, thread_id TEXT, primary_event_id TEXT NOT NULL,
|
|
877
|
+
previous_episode_id TEXT, resulting_episode_id TEXT, policy_version TEXT NOT NULL,
|
|
878
|
+
mode TEXT NOT NULL, guard_action TEXT NOT NULL, guard_codes_json TEXT NOT NULL,
|
|
879
|
+
metrics_json TEXT NOT NULL, cpu_decision_json TEXT NOT NULL, reviewer_invoked INTEGER NOT NULL,
|
|
880
|
+
reviewer_decision_json TEXT, final_decision_json TEXT NOT NULL, warnings_json TEXT NOT NULL,
|
|
881
|
+
created_at INTEGER NOT NULL, UNIQUE(project_id, primary_event_id, policy_version)
|
|
882
|
+
);
|
|
883
|
+
`);
|
|
884
|
+
this.ensureEpisodeCompatibilityColumns();
|
|
885
|
+
sealDuplicateOpenEpisodes(this.db);
|
|
886
|
+
this.db.exec(`
|
|
887
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_episodes_one_active_scope
|
|
888
|
+
ON memory_episodes(project_id, session_id, COALESCE(source_agent, ''), COALESCE(conversation_thread_id, ''))
|
|
889
|
+
WHERE status = 'open';
|
|
890
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_episode_events_episode_position_unique
|
|
891
|
+
ON memory_episode_events(episode_id, position);
|
|
684
892
|
`);
|
|
685
893
|
}
|
|
894
|
+
ensureEpisodeCompatibilityColumns() {
|
|
895
|
+
const addColumns = (table, definitions) => {
|
|
896
|
+
const exists = this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table);
|
|
897
|
+
if (!exists)
|
|
898
|
+
return;
|
|
899
|
+
const columns = new Set(this.db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
|
|
900
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
901
|
+
if (!columns.has(name))
|
|
902
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`);
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
addColumns('memory_episodes', {
|
|
906
|
+
conversation_thread_id: 'TEXT', semantic_summary_json: 'TEXT', episode_tags_json: "TEXT NOT NULL DEFAULT '[]'",
|
|
907
|
+
candidate_types_json: "TEXT NOT NULL DEFAULT '[]'", importance_signals_json: "TEXT NOT NULL DEFAULT '[]'",
|
|
908
|
+
importance_reason: 'TEXT', linked_episode_id: 'TEXT', dream_status: "TEXT NOT NULL DEFAULT 'none'",
|
|
909
|
+
last_dream_run_id: 'TEXT', last_dreamed_at: 'INTEGER', dream_candidate_count: "INTEGER NOT NULL DEFAULT 0", dream_error: 'TEXT',
|
|
910
|
+
});
|
|
911
|
+
addColumns('episode_closure_receipts', {
|
|
912
|
+
closure_reason_code: "TEXT NOT NULL DEFAULT 'manual'", closure_reason_detail: 'TEXT', requires_review: 'INTEGER NOT NULL DEFAULT 0',
|
|
913
|
+
ignored_nearby_event_ids_json: "TEXT NOT NULL DEFAULT '[]'", unassigned_nearby_event_ids_json: "TEXT NOT NULL DEFAULT '[]'",
|
|
914
|
+
});
|
|
915
|
+
addColumns('episode_dream_jobs', {
|
|
916
|
+
retry_after: 'INTEGER', failure_category: 'TEXT', candidate_ids_json: "TEXT NOT NULL DEFAULT '[]'",
|
|
917
|
+
});
|
|
918
|
+
addColumns('episode_dream_runs', {
|
|
919
|
+
failed_episode_ids_json: "TEXT NOT NULL DEFAULT '[]'", failure_details_json: "TEXT NOT NULL DEFAULT '[]'",
|
|
920
|
+
});
|
|
921
|
+
addColumns('episode_ingest_keys', {
|
|
922
|
+
state: "TEXT NOT NULL DEFAULT 'committed'", updated_at: 'INTEGER', last_error: 'TEXT',
|
|
923
|
+
});
|
|
924
|
+
const episodeLinks = this.db.prepare(`
|
|
925
|
+
SELECT rowid, episode_id, event_id FROM memory_episode_events ORDER BY episode_id, position, event_id
|
|
926
|
+
`).all();
|
|
927
|
+
const movePosition = this.db.prepare(`UPDATE memory_episode_events SET position = ? WHERE rowid = ?`);
|
|
928
|
+
const nextPositions = new Map();
|
|
929
|
+
for (const link of episodeLinks) {
|
|
930
|
+
movePosition.run(-1_000_000_000 - link.rowid, link.rowid);
|
|
931
|
+
const next = (nextPositions.get(link.episode_id) || 0) + 1;
|
|
932
|
+
nextPositions.set(link.episode_id, next);
|
|
933
|
+
movePosition.run(next, link.rowid);
|
|
934
|
+
}
|
|
935
|
+
const eventColumns = Boolean(this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'memory_events'`).get())
|
|
936
|
+
? new Set(this.db.prepare(`PRAGMA table_info(memory_events)`).all().map((row) => row.name))
|
|
937
|
+
: new Set();
|
|
938
|
+
const hasGlobalSeq = eventColumns.has('global_seq');
|
|
939
|
+
const rebuild = hasGlobalSeq ? this.db.prepare(`
|
|
940
|
+
UPDATE memory_episodes SET event_count = (SELECT COUNT(*) FROM memory_episode_events WHERE episode_id = ?),
|
|
941
|
+
start_event_id = COALESCE((SELECT event_id FROM memory_episode_events WHERE episode_id = ? ORDER BY position, event_id LIMIT 1), start_event_id),
|
|
942
|
+
end_event_id = COALESCE((SELECT event_id FROM memory_episode_events WHERE episode_id = ? ORDER BY position DESC, event_id DESC LIMIT 1), end_event_id),
|
|
943
|
+
start_seq = COALESCE((SELECT e.global_seq FROM memory_episode_events ee JOIN memory_events e ON e.event_id = ee.event_id WHERE ee.episode_id = ? ORDER BY ee.position LIMIT 1), start_seq),
|
|
944
|
+
end_seq = COALESCE((SELECT e.global_seq FROM memory_episode_events ee JOIN memory_events e ON e.event_id = ee.event_id WHERE ee.episode_id = ? ORDER BY ee.position DESC LIMIT 1), end_seq)
|
|
945
|
+
WHERE episode_id = ?
|
|
946
|
+
`) : undefined;
|
|
947
|
+
const episodeRows = this.db.prepare(`SELECT episode_id FROM memory_episodes`).all();
|
|
948
|
+
for (const row of episodeRows) {
|
|
949
|
+
if (rebuild)
|
|
950
|
+
rebuild.run(row.episode_id, row.episode_id, row.episode_id, row.episode_id, row.episode_id, row.episode_id);
|
|
951
|
+
else
|
|
952
|
+
this.db.prepare(`UPDATE memory_episodes SET event_count = (SELECT COUNT(*) FROM memory_episode_events WHERE episode_id = ?) WHERE episode_id = ?`).run(row.episode_id, row.episode_id);
|
|
953
|
+
}
|
|
954
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_memory_episode_events_episode ON memory_episode_events(episode_id, position);`);
|
|
955
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_episode_dream_retry ON episode_dream_jobs(state, retry_after, priority DESC, created_at);`);
|
|
956
|
+
}
|
|
686
957
|
}
|
|
687
958
|
function mapEpisode(row) {
|
|
688
959
|
return {
|
|
@@ -719,7 +990,38 @@ function mapClosure(row) {
|
|
|
719
990
|
createdAt: row.created_at,
|
|
720
991
|
};
|
|
721
992
|
}
|
|
993
|
+
function mapBoundaryDecision(row) {
|
|
994
|
+
return {
|
|
995
|
+
decisionId: row.decision_id,
|
|
996
|
+
projectId: row.project_id,
|
|
997
|
+
sessionId: row.session_id,
|
|
998
|
+
sourceAgent: row.source_agent || undefined,
|
|
999
|
+
threadId: row.thread_id || undefined,
|
|
1000
|
+
primaryEventId: row.primary_event_id,
|
|
1001
|
+
previousEpisodeId: row.previous_episode_id || undefined,
|
|
1002
|
+
resultingEpisodeId: row.resulting_episode_id || undefined,
|
|
1003
|
+
policyVersion: row.policy_version,
|
|
1004
|
+
mode: row.mode,
|
|
1005
|
+
guardAction: row.guard_action,
|
|
1006
|
+
guardCodes: parseJson(row.guard_codes_json, []),
|
|
1007
|
+
metrics: parseJson(row.metrics_json, { activeEventCount: 0, trustedLocalDates: [] }),
|
|
1008
|
+
cpuDecision: parseJson(row.cpu_decision_json, {}),
|
|
1009
|
+
reviewerInvoked: row.reviewer_invoked === 1,
|
|
1010
|
+
reviewerDecision: parseJson(row.reviewer_decision_json, undefined),
|
|
1011
|
+
finalDecision: parseJson(row.final_decision_json, {}),
|
|
1012
|
+
warnings: parseJson(row.warnings_json, []),
|
|
1013
|
+
createdAt: row.created_at,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
722
1016
|
function normalizeClosureReasonCode(reason, mode) {
|
|
1017
|
+
if (/event_limit|max_events/u.test(reason))
|
|
1018
|
+
return 'event_limit';
|
|
1019
|
+
if (/duration_limit|max_duration/u.test(reason))
|
|
1020
|
+
return 'duration_limit';
|
|
1021
|
+
if (/idle_gap|max_idle/u.test(reason))
|
|
1022
|
+
return 'idle_gap';
|
|
1023
|
+
if (/local_date|trusted_local_date/u.test(reason))
|
|
1024
|
+
return 'local_date_boundary';
|
|
723
1025
|
if (reason.includes('topic_switch'))
|
|
724
1026
|
return 'topic_switch';
|
|
725
1027
|
if (reason.includes('batch'))
|
|
@@ -734,6 +1036,19 @@ function normalizeClosureReasonCode(reason, mode) {
|
|
|
734
1036
|
return 'explicit_user_closure';
|
|
735
1037
|
return mode === 'manual' ? 'manual' : 'manual';
|
|
736
1038
|
}
|
|
1039
|
+
function formatAuditCursor(updatedAt, episodeId) {
|
|
1040
|
+
return `${updatedAt}:${episodeId}`;
|
|
1041
|
+
}
|
|
1042
|
+
function parseAuditCursor(cursor) {
|
|
1043
|
+
if (!cursor)
|
|
1044
|
+
return undefined;
|
|
1045
|
+
const separator = cursor.indexOf(':');
|
|
1046
|
+
if (separator <= 0)
|
|
1047
|
+
return undefined;
|
|
1048
|
+
const updatedAt = Number(cursor.slice(0, separator));
|
|
1049
|
+
const episodeId = cursor.slice(separator + 1);
|
|
1050
|
+
return Number.isFinite(updatedAt) && episodeId ? { updatedAt, episodeId } : undefined;
|
|
1051
|
+
}
|
|
737
1052
|
function parseJson(value, fallback) {
|
|
738
1053
|
if (!value)
|
|
739
1054
|
return fallback;
|
|
@@ -744,6 +1059,75 @@ function parseJson(value, fallback) {
|
|
|
744
1059
|
return fallback;
|
|
745
1060
|
}
|
|
746
1061
|
}
|
|
1062
|
+
function canonicalRepairOrder(left, right, leftLink, rightLink) {
|
|
1063
|
+
const numeric = (value) => typeof value === 'number' && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
|
|
1064
|
+
const keys = [
|
|
1065
|
+
[numeric(left?.turnSeq), numeric(right?.turnSeq)],
|
|
1066
|
+
[numeric(left?.eventOrdinal), numeric(right?.eventOrdinal)],
|
|
1067
|
+
[numeric(left?.globalSeq), numeric(right?.globalSeq)],
|
|
1068
|
+
[numeric(left?.occurredAt), numeric(right?.occurredAt)],
|
|
1069
|
+
[leftLink.position, rightLink.position],
|
|
1070
|
+
];
|
|
1071
|
+
for (const [a, b] of keys)
|
|
1072
|
+
if (a !== b)
|
|
1073
|
+
return a - b;
|
|
1074
|
+
return leftLink.eventId.localeCompare(rightLink.eventId);
|
|
1075
|
+
}
|
|
1076
|
+
function safeDecision(decision) {
|
|
1077
|
+
return {
|
|
1078
|
+
relation: decision.relation,
|
|
1079
|
+
confidence: decision.confidence,
|
|
1080
|
+
signals: decision.signals?.slice(0, 20),
|
|
1081
|
+
needsLlmReview: decision.needsLlmReview,
|
|
1082
|
+
candidateTypes: decision.candidateTypes?.slice(0, 20),
|
|
1083
|
+
topicPath: decision.topicPath,
|
|
1084
|
+
closureCandidate: decision.closureCandidate,
|
|
1085
|
+
switchKind: decision.switchKind,
|
|
1086
|
+
episodeType: decision.episodeType,
|
|
1087
|
+
importance: decision.importance,
|
|
1088
|
+
importanceSignals: decision.importanceSignals?.slice(0, 20),
|
|
1089
|
+
rationale: decision.rationale?.slice(0, 240),
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function sameBoundaryDecision(existing, input) {
|
|
1093
|
+
return JSON.stringify({
|
|
1094
|
+
projectId: existing.projectId,
|
|
1095
|
+
sessionId: existing.sessionId,
|
|
1096
|
+
sourceAgent: existing.sourceAgent,
|
|
1097
|
+
threadId: existing.threadId,
|
|
1098
|
+
primaryEventId: existing.primaryEventId,
|
|
1099
|
+
previousEpisodeId: existing.previousEpisodeId,
|
|
1100
|
+
resultingEpisodeId: existing.resultingEpisodeId,
|
|
1101
|
+
policyVersion: existing.policyVersion,
|
|
1102
|
+
mode: existing.mode,
|
|
1103
|
+
guardAction: existing.guardAction,
|
|
1104
|
+
guardCodes: existing.guardCodes,
|
|
1105
|
+
metrics: existing.metrics,
|
|
1106
|
+
cpuDecision: safeDecision(existing.cpuDecision),
|
|
1107
|
+
reviewerInvoked: existing.reviewerInvoked,
|
|
1108
|
+
reviewerDecision: existing.reviewerDecision ? safeDecision(existing.reviewerDecision) : undefined,
|
|
1109
|
+
finalDecision: safeDecision(existing.finalDecision),
|
|
1110
|
+
warnings: existing.warnings,
|
|
1111
|
+
}) === JSON.stringify({
|
|
1112
|
+
projectId: input.projectId,
|
|
1113
|
+
sessionId: input.sessionId,
|
|
1114
|
+
sourceAgent: input.sourceAgent,
|
|
1115
|
+
threadId: input.threadId,
|
|
1116
|
+
primaryEventId: input.primaryEventId,
|
|
1117
|
+
previousEpisodeId: input.previousEpisodeId,
|
|
1118
|
+
resultingEpisodeId: input.resultingEpisodeId,
|
|
1119
|
+
policyVersion: input.policyVersion,
|
|
1120
|
+
mode: input.mode,
|
|
1121
|
+
guardAction: input.guardAction,
|
|
1122
|
+
guardCodes: input.guardCodes,
|
|
1123
|
+
metrics: input.metrics,
|
|
1124
|
+
cpuDecision: safeDecision(input.cpuDecision),
|
|
1125
|
+
reviewerInvoked: input.reviewerInvoked,
|
|
1126
|
+
reviewerDecision: input.reviewerDecision ? safeDecision(input.reviewerDecision) : undefined,
|
|
1127
|
+
finalDecision: safeDecision(input.finalDecision),
|
|
1128
|
+
warnings: input.warnings,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
747
1131
|
function retryDelayMs(attempts) {
|
|
748
1132
|
return Math.min(60 * 60_000, 30_000 * 2 ** Math.max(0, attempts - 1));
|
|
749
1133
|
}
|