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
|
@@ -4,6 +4,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
|
4
4
|
import { AesGcmEncryptionProvider } from '../encryption/index.js';
|
|
5
5
|
import { ModelRegistry } from '../models/ModelRegistry.js';
|
|
6
6
|
import { DEFAULT_VECTOR_DIMENSION, addVectorDimensionDiagnostics, parseVectorDimensionValue, } from './VectorDimension.js';
|
|
7
|
+
import { normalizeEpisodeBoundaryConfig } from '../episode/EpisodeBoundaryPolicy.js';
|
|
7
8
|
export function defaultCogmemHome(env = process.env) {
|
|
8
9
|
return join(env.HOME || homedir(), '.cogmem');
|
|
9
10
|
}
|
|
@@ -53,6 +54,7 @@ export function loadCogmemConfig(options = {}) {
|
|
|
53
54
|
const memoryModel = section(root, 'memory_model');
|
|
54
55
|
const reasoningModel = section(root, 'reasoning_model');
|
|
55
56
|
const governance = section(root, 'governance');
|
|
57
|
+
const episodeBoundary = section(root, 'episode_boundary');
|
|
56
58
|
const integrations = section(root, 'integrations');
|
|
57
59
|
const openclaw = section(integrations, 'openclaw');
|
|
58
60
|
const hermes = section(integrations, 'hermes');
|
|
@@ -101,6 +103,25 @@ export function loadCogmemConfig(options = {}) {
|
|
|
101
103
|
}
|
|
102
104
|
if (Object.keys(redactionPolicy).length > 0)
|
|
103
105
|
optionsOut.redactionPolicy = redactionPolicy;
|
|
106
|
+
const boundaryInput = {
|
|
107
|
+
enabled: boundaryBoolean(episodeBoundary.enabled, 'enabled', diagnostics),
|
|
108
|
+
mode: boundaryString(episodeBoundary.mode, 'mode', diagnostics),
|
|
109
|
+
maxEvents: boundaryNumber(episodeBoundary.max_events, 'max_events', diagnostics),
|
|
110
|
+
maxDurationMs: boundaryNumber(episodeBoundary.max_duration_ms, 'max_duration_ms', diagnostics),
|
|
111
|
+
maxIdleGapMs: boundaryNumber(episodeBoundary.max_idle_gap_ms, 'max_idle_gap_ms', diagnostics),
|
|
112
|
+
splitOnTrustedLocalDateChange: boundaryBoolean(episodeBoundary.split_on_trusted_local_date_change, 'split_on_trusted_local_date_change', diagnostics),
|
|
113
|
+
auditDecisions: boundaryBoolean(episodeBoundary.audit_decisions, 'audit_decisions', diagnostics),
|
|
114
|
+
applyToLive: boundaryBoolean(episodeBoundary.apply_to_live, 'apply_to_live', diagnostics),
|
|
115
|
+
applyToImports: boundaryBoolean(episodeBoundary.apply_to_imports, 'apply_to_imports', diagnostics),
|
|
116
|
+
policyVersion: boundaryString(episodeBoundary.policy_version, 'policy_version', diagnostics),
|
|
117
|
+
timezone: boundaryString(episodeBoundary.timezone, 'timezone', diagnostics),
|
|
118
|
+
};
|
|
119
|
+
const configuredBoundary = Object.values(boundaryInput).some((value) => value !== undefined);
|
|
120
|
+
if (configuredBoundary) {
|
|
121
|
+
const normalizedBoundary = normalizeEpisodeBoundaryConfig(boundaryInput);
|
|
122
|
+
optionsOut.episodeBoundary = normalizedBoundary.config;
|
|
123
|
+
diagnostics.push(...normalizedBoundary.diagnostics);
|
|
124
|
+
}
|
|
104
125
|
const encryptionEnabled = booleanValue(governance.encryption) === true;
|
|
105
126
|
const encryptionPassphrase = interpolate(stringValue(governance.encryption_passphrase) || stringValue(governance.passphrase) || '', env, diagnostics);
|
|
106
127
|
if (encryptionEnabled) {
|
|
@@ -245,3 +266,27 @@ function booleanValue(value) {
|
|
|
245
266
|
function numberValue(value) {
|
|
246
267
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
247
268
|
}
|
|
269
|
+
function boundaryBoolean(value, name, diagnostics) {
|
|
270
|
+
if (value === undefined)
|
|
271
|
+
return undefined;
|
|
272
|
+
if (typeof value === 'boolean')
|
|
273
|
+
return value;
|
|
274
|
+
diagnostics.push({ severity: 'warning', code: `invalid_episode_boundary_${name}_type`, message: `episode_boundary.${name} must be a boolean.` });
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
function boundaryNumber(value, name, diagnostics) {
|
|
278
|
+
if (value === undefined)
|
|
279
|
+
return undefined;
|
|
280
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
281
|
+
return value;
|
|
282
|
+
diagnostics.push({ severity: 'warning', code: `invalid_episode_boundary_${name}_type`, message: `episode_boundary.${name} must be a finite number.` });
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
function boundaryString(value, name, diagnostics) {
|
|
286
|
+
if (value === undefined)
|
|
287
|
+
return undefined;
|
|
288
|
+
if (typeof value === 'string')
|
|
289
|
+
return value;
|
|
290
|
+
diagnostics.push({ severity: 'warning', code: `invalid_episode_boundary_${name}_type`, message: `episode_boundary.${name} must be a string.` });
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
package/dist/core/MemoryGraph.js
CHANGED
|
@@ -14,15 +14,27 @@ export class MemoryGraph {
|
|
|
14
14
|
anchorIndex = new Map();
|
|
15
15
|
topicReclassifiedListeners = new Set();
|
|
16
16
|
constructor(dbPath = ':memory:') {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
let lastError;
|
|
18
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
19
|
+
try {
|
|
20
|
+
this.db = new Database(dbPath);
|
|
21
|
+
this.initializeSchema();
|
|
22
|
+
this.rebuildIndexes();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
lastError = error;
|
|
27
|
+
try {
|
|
28
|
+
this.db?.close();
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
if (!isSqliteBusy(error) || attempt === 5)
|
|
32
|
+
break;
|
|
33
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
|
|
34
|
+
}
|
|
25
35
|
}
|
|
36
|
+
logger.error('Failed to initialize MemoryGraph:', lastError);
|
|
37
|
+
throw new Error(`MemoryGraph initialization failed: ${lastError}`);
|
|
26
38
|
}
|
|
27
39
|
initializeSchema() {
|
|
28
40
|
this.db.exec(`
|
|
@@ -858,3 +870,8 @@ export class MemoryGraph {
|
|
|
858
870
|
}
|
|
859
871
|
}
|
|
860
872
|
}
|
|
873
|
+
function isSqliteBusy(error) {
|
|
874
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
875
|
+
const code = error?.code;
|
|
876
|
+
return code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED' || /database is locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(message);
|
|
877
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DreamCuratorWorker } from '../engine/DreamCuratorWorker.js';
|
|
2
2
|
import type { EpisodeStore } from '../episode/EpisodeStore.js';
|
|
3
|
+
import type { DeepWriteCandidateStore } from '../store/DeepWriteCandidateStore.js';
|
|
3
4
|
export type DreamTickMode = 'auto' | 'micro' | 'normal' | 'deep';
|
|
4
5
|
export type SelectedDreamMode = 'none' | 'micro' | 'normal' | 'deep';
|
|
5
6
|
export interface DreamTickOptions {
|
|
@@ -11,6 +12,9 @@ export interface DreamTickOptions {
|
|
|
11
12
|
leaseMs?: number;
|
|
12
13
|
maxAttempts?: number;
|
|
13
14
|
maintenanceReason?: 'daily' | 'upgrade_repair';
|
|
15
|
+
clock?: {
|
|
16
|
+
now(): number;
|
|
17
|
+
};
|
|
14
18
|
}
|
|
15
19
|
export interface DreamTickResult {
|
|
16
20
|
runId: string;
|
|
@@ -40,7 +44,8 @@ export interface DreamTickResult {
|
|
|
40
44
|
export declare class DreamScheduler {
|
|
41
45
|
private readonly episodeStore;
|
|
42
46
|
private readonly curator;
|
|
43
|
-
|
|
47
|
+
private readonly candidateStore;
|
|
48
|
+
constructor(episodeStore: EpisodeStore, curator: DreamCuratorWorker, candidateStore: DeepWriteCandidateStore);
|
|
44
49
|
tick(options?: DreamTickOptions): Promise<DreamTickResult>;
|
|
45
50
|
private recordRun;
|
|
46
51
|
}
|
|
@@ -2,27 +2,35 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
export class DreamScheduler {
|
|
3
3
|
episodeStore;
|
|
4
4
|
curator;
|
|
5
|
-
|
|
5
|
+
candidateStore;
|
|
6
|
+
constructor(episodeStore, curator, candidateStore) {
|
|
6
7
|
this.episodeStore = episodeStore;
|
|
7
8
|
this.curator = curator;
|
|
9
|
+
this.candidateStore = candidateStore;
|
|
8
10
|
}
|
|
9
11
|
async tick(options = {}) {
|
|
10
|
-
const
|
|
12
|
+
const wallStartedAt = Date.now();
|
|
13
|
+
const clock = options.clock ?? {
|
|
14
|
+
now: () => options.now === undefined ? Date.now() : options.now + (Date.now() - wallStartedAt),
|
|
15
|
+
};
|
|
16
|
+
const startedAt = options.now ?? clock.now();
|
|
11
17
|
const requestedMode = options.mode ?? 'auto';
|
|
12
18
|
const runId = `episode-dream-run-${randomUUID()}`;
|
|
13
|
-
const backlogBefore = this.episodeStore.getDreamStatus(options.projectId);
|
|
14
19
|
const graceMs = Math.max(0, options.softSealGraceMs ?? 5 * 60_000);
|
|
20
|
+
const leaseMs = Math.max(5_000, options.leaseMs ?? 5 * 60_000);
|
|
21
|
+
this.candidateStore?.abandonStaleStagedRuns(startedAt - leaseMs, startedAt, options.projectId);
|
|
15
22
|
this.episodeStore.finalizeMatureSoftSeals({
|
|
16
23
|
projectId: options.projectId,
|
|
17
24
|
sealedBefore: startedAt - graceMs,
|
|
18
25
|
now: startedAt,
|
|
19
26
|
});
|
|
27
|
+
const backlogBefore = this.episodeStore.getDreamStatus(options.projectId);
|
|
20
28
|
const maxEpisodes = Math.max(1, Math.min(Math.trunc(options.maxEpisodes ?? modeLimit(requestedMode)), 50));
|
|
21
29
|
const jobs = this.episodeStore.claimDreamJobs({
|
|
22
30
|
projectId: options.projectId,
|
|
23
31
|
limit: maxEpisodes,
|
|
24
32
|
now: startedAt,
|
|
25
|
-
leaseMs
|
|
33
|
+
leaseMs,
|
|
26
34
|
maxAttempts: Math.max(1, options.maxAttempts ?? 3),
|
|
27
35
|
runId,
|
|
28
36
|
});
|
|
@@ -68,16 +76,38 @@ export class DreamScheduler {
|
|
|
68
76
|
episodeRelations: links.map((link) => ({ eventId: link.eventId, relation: link.relation })),
|
|
69
77
|
limit: limits.eventLimit,
|
|
70
78
|
now: startedAt,
|
|
79
|
+
dreamJobLeaseId: job.leaseId,
|
|
80
|
+
leaseUntil: job.leaseUntil,
|
|
81
|
+
attemptGeneration: job.attemptGeneration,
|
|
71
82
|
});
|
|
72
83
|
const ids = run.candidates.map((candidate) => candidate.candidateId);
|
|
73
|
-
|
|
84
|
+
try {
|
|
85
|
+
const commitCandidates = run.runId
|
|
86
|
+
? () => {
|
|
87
|
+
if (this.candidateStore.getDatabase() !== this.episodeStore.getDatabase()) {
|
|
88
|
+
throw new Error('dream_stores_must_share_database');
|
|
89
|
+
}
|
|
90
|
+
this.candidateStore.publishStagedCandidates(run.runId, ids, startedAt);
|
|
91
|
+
this.candidateStore.updateRunStatus(run.runId, 'staged', 'succeeded');
|
|
92
|
+
}
|
|
93
|
+
: undefined;
|
|
94
|
+
const completedAt = clock.now();
|
|
95
|
+
this.episodeStore.completeDreamJob(job.episodeId, job.leaseId, ids, completedAt, commitCandidates);
|
|
96
|
+
}
|
|
97
|
+
catch (completionError) {
|
|
98
|
+
const failedAt = clock.now();
|
|
99
|
+
if (run.runId)
|
|
100
|
+
this.candidateStore.failStagedRun(run.runId, failedAt, 'dream_job_completion_failed');
|
|
101
|
+
throw completionError;
|
|
102
|
+
}
|
|
74
103
|
episodeIds.push(job.episodeId);
|
|
75
104
|
candidateIds.push(...ids);
|
|
76
105
|
}
|
|
77
106
|
catch (error) {
|
|
78
107
|
failures += 1;
|
|
79
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
-
const
|
|
109
|
+
const failureNow = clock.now();
|
|
110
|
+
const failure = classifyFailure(message, job.attempts, failureNow);
|
|
81
111
|
this.episodeStore.failDreamJob(job.episodeId, job.leaseId, message, failure);
|
|
82
112
|
failedEpisodes.push({ episodeId: job.episodeId, error: message, failureCategory: failure.failureCategory, retryAfter: failure.retryAfter });
|
|
83
113
|
}
|
|
@@ -96,7 +96,7 @@ export class CognitiveGraphCompiler {
|
|
|
96
96
|
seedNodeIds.push(factNode.nodeId);
|
|
97
97
|
edgeCount += 1;
|
|
98
98
|
if (fact.entityId) {
|
|
99
|
-
const entity = this.entityStore.
|
|
99
|
+
const entity = this.entityStore.getByEntityId(fact.entityId);
|
|
100
100
|
if (entity) {
|
|
101
101
|
const entityNode = this.store.upsertNode({
|
|
102
102
|
nodeId: `cgnode-${randomUUID()}`,
|
|
@@ -263,7 +263,7 @@ export class ConsolidationPipeline {
|
|
|
263
263
|
}
|
|
264
264
|
buildAsyncEnrichmentEntityBinding(neuron, consolidation) {
|
|
265
265
|
const compiledEntities = consolidation.compiledEntityIds
|
|
266
|
-
.map((entityId) => this.entityStore.
|
|
266
|
+
.map((entityId) => this.entityStore.getByEntityId(entityId))
|
|
267
267
|
.filter((entity) => Boolean(entity));
|
|
268
268
|
const suspiciousReasons = [];
|
|
269
269
|
if (compiledEntities.length > 0) {
|
|
@@ -286,11 +286,11 @@ export class ConsolidationPipeline {
|
|
|
286
286
|
}
|
|
287
287
|
buildAsyncEnrichmentContext(neuron, consolidation) {
|
|
288
288
|
const recentEntities = Array.from(new Map([
|
|
289
|
-
...consolidation.compiledEntityIds.map((entityId) => this.entityStore.
|
|
289
|
+
...consolidation.compiledEntityIds.map((entityId) => this.entityStore.getByEntityId(entityId)),
|
|
290
290
|
...this.entityStore.getEntityTimeline({
|
|
291
291
|
projectId: neuron.metadata.projectId,
|
|
292
292
|
limit: 8
|
|
293
|
-
}).map((item) => this.entityStore.
|
|
293
|
+
}).map((item) => this.entityStore.getByEntityId(item.entityId))
|
|
294
294
|
]
|
|
295
295
|
.filter((entity) => Boolean(entity))
|
|
296
296
|
.map((entity) => [entity.entityId, entity])).values()).slice(0, 6);
|
|
@@ -309,7 +309,7 @@ export class ConsolidationPipeline {
|
|
|
309
309
|
hasSuspiciousUnseenEntityBinding(consolidation) {
|
|
310
310
|
const semanticEntityHints = consolidation.semanticCompilation?.entities.map((entity) => entity.text) || [];
|
|
311
311
|
const compiledEntityNames = consolidation.compiledEntityIds
|
|
312
|
-
.map((entityId) => this.entityStore.
|
|
312
|
+
.map((entityId) => this.entityStore.getByEntityId(entityId)?.canonicalName || '')
|
|
313
313
|
.filter(Boolean);
|
|
314
314
|
const sourceText = consolidation.interactionUnit?.semanticText || '';
|
|
315
315
|
const explicitNamedDevice = sourceText.match(/([A-Za-z][A-Za-z0-9._-]*(?:\s+[A-Za-z0-9._-]+){1,3})/);
|
|
@@ -30,8 +30,10 @@ export interface DeepWriteEvaluationOptions {
|
|
|
30
30
|
export declare class DeepWritePromotionPolicy {
|
|
31
31
|
private readonly deps;
|
|
32
32
|
constructor(deps: DeepWritePromotionPolicyDeps);
|
|
33
|
+
setRelationStore(store: GraphEdgeStoreLike): void;
|
|
33
34
|
promoteRun(runId: string): DeepWritePromotionDecision[];
|
|
34
35
|
promotePending(limit?: number, options?: DeepWritePromotionOptions): DeepWritePromotionDecision[];
|
|
36
|
+
private atomicEvaluate;
|
|
35
37
|
evaluateAndApply(candidate: DeepWriteCandidateRecord, options?: DeepWriteEvaluationOptions): DeepWritePromotionDecision;
|
|
36
38
|
private promoteOrganizationCandidate;
|
|
37
39
|
private promoteFact;
|
|
@@ -134,18 +134,39 @@ export class DeepWritePromotionPolicy {
|
|
|
134
134
|
constructor(deps) {
|
|
135
135
|
this.deps = deps;
|
|
136
136
|
}
|
|
137
|
+
setRelationStore(store) {
|
|
138
|
+
this.deps.relationStore = store;
|
|
139
|
+
}
|
|
137
140
|
promoteRun(runId) {
|
|
138
141
|
const candidates = this.deps.candidateStore.listCandidatesByRun(runId);
|
|
139
|
-
return candidates.map((candidate) => this.
|
|
142
|
+
return candidates.map((candidate) => this.atomicEvaluate(candidate));
|
|
140
143
|
}
|
|
141
144
|
promotePending(limit = 100, options = {}) {
|
|
142
145
|
const candidates = options.projectId
|
|
143
146
|
? this.deps.candidateStore.listCandidates({ statuses: ['candidate'], projectId: options.projectId, limit })
|
|
144
147
|
: this.deps.candidateStore.listCandidatesByStatus(['candidate'], { limit });
|
|
145
|
-
return candidates.map((candidate) => this.
|
|
148
|
+
return candidates.map((candidate) => this.atomicEvaluate(candidate));
|
|
149
|
+
}
|
|
150
|
+
atomicEvaluate(candidate) {
|
|
151
|
+
return this.deps.candidateStore.getDatabase().transaction(() => {
|
|
152
|
+
if (!this.deps.candidateStore.claimCandidate(candidate.candidateId)) {
|
|
153
|
+
return this.keep(candidate, 'candidate_claim_lost');
|
|
154
|
+
}
|
|
155
|
+
const current = this.deps.candidateStore.getCandidate(candidate.candidateId);
|
|
156
|
+
if (!current || current.status !== 'promoting')
|
|
157
|
+
return this.keep(candidate, 'candidate_claim_lost');
|
|
158
|
+
const decision = this.evaluateAndApply(current);
|
|
159
|
+
if (decision.outcome === 'keep_candidate') {
|
|
160
|
+
this.deps.candidateStore.updateCandidateStatus(current.candidateId, 'candidate', {
|
|
161
|
+
reason: decision.reason,
|
|
162
|
+
updatedAt: Date.now(),
|
|
163
|
+
}, 'promoting');
|
|
164
|
+
}
|
|
165
|
+
return decision;
|
|
166
|
+
})();
|
|
146
167
|
}
|
|
147
168
|
evaluateAndApply(candidate, options = {}) {
|
|
148
|
-
if (candidate.status !== 'candidate') {
|
|
169
|
+
if (candidate.status !== 'candidate' && candidate.status !== 'promoting') {
|
|
149
170
|
return this.keep(candidate, `status_${candidate.status}_not_promotable`);
|
|
150
171
|
}
|
|
151
172
|
const content = asRecord(candidate.content);
|
|
@@ -508,7 +529,7 @@ export class DeepWritePromotionPolicy {
|
|
|
508
529
|
type: decision.targetType,
|
|
509
530
|
id: decision.targetId,
|
|
510
531
|
reason: decision.reason,
|
|
511
|
-
});
|
|
532
|
+
}, candidate.status);
|
|
512
533
|
return decision;
|
|
513
534
|
}
|
|
514
535
|
}
|
|
@@ -84,10 +84,22 @@ export class DreamCuratorWorker {
|
|
|
84
84
|
content: candidate.content,
|
|
85
85
|
evidence: candidate.evidence,
|
|
86
86
|
})))),
|
|
87
|
-
status: 'succeeded',
|
|
87
|
+
status: options.sourceEpisodeId ? 'staged' : 'succeeded',
|
|
88
88
|
createdAt: now,
|
|
89
|
+
sourceEpisodeId: options.sourceEpisodeId,
|
|
90
|
+
dreamJobLeaseId: options.dreamJobLeaseId,
|
|
91
|
+
leaseUntil: options.leaseUntil,
|
|
92
|
+
attemptGeneration: options.attemptGeneration,
|
|
89
93
|
});
|
|
90
|
-
const inserted = this.deps.candidateStore.insertCandidates(candidateInputs.map((candidate) => ({
|
|
94
|
+
const inserted = this.deps.candidateStore.insertCandidates(candidateInputs.map((candidate) => ({
|
|
95
|
+
...candidate,
|
|
96
|
+
status: options.sourceEpisodeId && candidate.status !== 'shadow' ? 'staged' : candidate.status,
|
|
97
|
+
publishStatus: options.sourceEpisodeId && candidate.status !== 'shadow'
|
|
98
|
+
? candidate.status
|
|
99
|
+
: undefined,
|
|
100
|
+
runId: run.runId,
|
|
101
|
+
createdAt: now,
|
|
102
|
+
})));
|
|
91
103
|
const status = explicitEpisodeRun
|
|
92
104
|
? before
|
|
93
105
|
: this.deps.dreamLedgerStore.markDreamed(options.projectId, maxGlobalSeq, now);
|
|
@@ -100,7 +112,10 @@ export class DreamCuratorWorker {
|
|
|
100
112
|
candidateCount: inserted.length,
|
|
101
113
|
maxGlobalSeq,
|
|
102
114
|
status,
|
|
103
|
-
candidates: inserted,
|
|
115
|
+
candidates: inserted.map((candidate, index) => ({
|
|
116
|
+
...candidate,
|
|
117
|
+
status: candidateInputs[index]?.status ?? candidate.status,
|
|
118
|
+
})),
|
|
104
119
|
};
|
|
105
120
|
}
|
|
106
121
|
async buildCandidates(events, options, now) {
|
|
@@ -177,7 +177,7 @@ export class FactCompiler {
|
|
|
177
177
|
: null;
|
|
178
178
|
if (issueMatches.length > 0) {
|
|
179
179
|
const resolvedDevice = primaryDeviceEntityId
|
|
180
|
-
? this.entityStore.
|
|
180
|
+
? this.entityStore.getByEntityId(primaryDeviceEntityId)
|
|
181
181
|
: this.resolveImplicitEntity(workingText, 'device', projectId);
|
|
182
182
|
for (const issueMatch of issueMatches) {
|
|
183
183
|
const issueMetadata = this.buildIssueMetadata(issueMatch.issue, issueMatch.issue, sourceText, {
|
|
@@ -266,7 +266,7 @@ export class FactCompiler {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
else if (input.semanticCompilation?.issueHints.length && primaryDeviceEntityId) {
|
|
269
|
-
const resolvedDevice = this.entityStore.
|
|
269
|
+
const resolvedDevice = this.entityStore.getByEntityId(primaryDeviceEntityId);
|
|
270
270
|
for (const issueHint of input.semanticCompilation.issueHints.slice(0, 2)) {
|
|
271
271
|
const issueMetadata = this.buildIssueMetadata(issueHint, issueHint, sourceText);
|
|
272
272
|
factInputs.push({
|
|
@@ -133,7 +133,7 @@ export class EntityGovernanceService {
|
|
|
133
133
|
.map(mapCandidate);
|
|
134
134
|
}
|
|
135
135
|
requireEntity(entityId) {
|
|
136
|
-
const entity = this.entities.
|
|
136
|
+
const entity = this.entities.getByEntityId(entityId);
|
|
137
137
|
if (!entity)
|
|
138
138
|
throw new Error(`Unknown entity: ${entityId}`);
|
|
139
139
|
return entity;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function sealDuplicateOpenEpisodes(db) {
|
|
2
|
+
const duplicates = db.prepare(`
|
|
3
|
+
SELECT project_id, session_id, COALESCE(source_agent, '') AS source_agent_key,
|
|
4
|
+
COALESCE(conversation_thread_id, '') AS thread_key
|
|
5
|
+
FROM memory_episodes
|
|
6
|
+
WHERE status = 'open'
|
|
7
|
+
GROUP BY project_id, session_id, COALESCE(source_agent, ''), COALESCE(conversation_thread_id, '')
|
|
8
|
+
HAVING COUNT(*) > 1
|
|
9
|
+
`).all();
|
|
10
|
+
if (!duplicates.length)
|
|
11
|
+
return;
|
|
12
|
+
const selectEpisodes = db.prepare(`
|
|
13
|
+
SELECT * FROM memory_episodes
|
|
14
|
+
WHERE status = 'open'
|
|
15
|
+
AND project_id = ?
|
|
16
|
+
AND session_id = ?
|
|
17
|
+
AND COALESCE(source_agent, '') = ?
|
|
18
|
+
AND COALESCE(conversation_thread_id, '') = ?
|
|
19
|
+
ORDER BY updated_at DESC, episode_id DESC
|
|
20
|
+
`);
|
|
21
|
+
const selectEventIds = db.prepare(`
|
|
22
|
+
SELECT event_id FROM memory_episode_events WHERE episode_id = ? ORDER BY position
|
|
23
|
+
`);
|
|
24
|
+
const sealEpisode = db.prepare(`
|
|
25
|
+
UPDATE memory_episodes SET status = 'sealed', sealed_at = ?, updated_at = MAX(updated_at, ?)
|
|
26
|
+
WHERE episode_id = ?
|
|
27
|
+
`);
|
|
28
|
+
const insertReceipt = db.prepare(`
|
|
29
|
+
INSERT OR IGNORE INTO episode_closure_receipts (
|
|
30
|
+
receipt_id, episode_id, project_id, closure_mode, closure_reason, source_event_ids_json,
|
|
31
|
+
start_seq, end_seq, topic_path, episode_type, importance, dream_recommended, dream_mode,
|
|
32
|
+
created_at, closure_reason_code, closure_reason_detail, requires_review,
|
|
33
|
+
ignored_nearby_event_ids_json, unassigned_nearby_event_ids_json
|
|
34
|
+
) VALUES (?, ?, ?, 'manual', 'migration_duplicate_open_scope', ?, ?, ?, ?, ?, ?, 0, 'normal', ?,
|
|
35
|
+
'repair', ?, 1, '[]', '[]')
|
|
36
|
+
`);
|
|
37
|
+
for (const scope of duplicates) {
|
|
38
|
+
const rows = selectEpisodes.all(scope.project_id, scope.session_id, scope.source_agent_key, scope.thread_key);
|
|
39
|
+
for (const episode of rows.slice(1)) {
|
|
40
|
+
const eventIds = selectEventIds.all(episode.episode_id).map((row) => row.event_id);
|
|
41
|
+
const now = episode.updated_at;
|
|
42
|
+
sealEpisode.run(now, now, episode.episode_id);
|
|
43
|
+
insertReceipt.run(`migration-0029-${episode.episode_id}`, episode.episode_id, episode.project_id, JSON.stringify(eventIds), episode.start_seq ?? null, episode.end_seq ?? null, episode.topic_path ?? null, episode.episode_type, episode.importance, now, JSON.stringify({ reason: 'migration_duplicate_open_scope', keptOpenEpisodeId: rows[0].episode_id, duplicateScope: scope }));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { MemoryEvent } from '../types/index.js';
|
|
2
|
-
import { type TurnClassificationContext, type TurnRelationAdvisoryReviewer } from './TurnRelationClassifier.js';
|
|
2
|
+
import { type TurnClassificationContext, type TurnRelationAdvisoryReviewer, type TurnRelationReviewStatus } from './TurnRelationClassifier.js';
|
|
3
3
|
import type { EpisodeClosureReceipt, MemoryEpisode } from './EpisodeTypes.js';
|
|
4
4
|
import { EpisodeStore } from './EpisodeStore.js';
|
|
5
|
+
import { EpisodeBoundaryPolicy } from './EpisodeBoundaryPolicy.js';
|
|
5
6
|
export interface EpisodeAssemblyResult {
|
|
6
7
|
episode?: MemoryEpisode;
|
|
7
8
|
assignedEventIds: string[];
|
|
@@ -9,6 +10,17 @@ export interface EpisodeAssemblyResult {
|
|
|
9
10
|
ignoredEventIds: string[];
|
|
10
11
|
closureReceipt?: EpisodeClosureReceipt;
|
|
11
12
|
reopened: boolean;
|
|
13
|
+
boundaryTriggered?: boolean;
|
|
14
|
+
boundaryDetected?: boolean;
|
|
15
|
+
boundaryApplied?: boolean;
|
|
16
|
+
boundaryMode?: string;
|
|
17
|
+
boundaryDecisionId?: string;
|
|
18
|
+
boundaryGuardCodes?: string[];
|
|
19
|
+
boundaryAuditRecorded?: boolean;
|
|
20
|
+
boundaryAuditStatus?: 'disabled' | 'inserted' | 'duplicate' | 'failed' | 'not_applicable';
|
|
21
|
+
previousEpisodeId?: string;
|
|
22
|
+
reviewerRawResultStatus?: TurnRelationReviewStatus;
|
|
23
|
+
warnings?: string[];
|
|
12
24
|
}
|
|
13
25
|
export declare class EpisodeAssembler {
|
|
14
26
|
private readonly store;
|
|
@@ -16,7 +28,8 @@ export declare class EpisodeAssembler {
|
|
|
16
28
|
private readonly softReopenWindowMs;
|
|
17
29
|
private readonly reviewer?;
|
|
18
30
|
private readonly resolveTopicContext?;
|
|
19
|
-
|
|
31
|
+
private readonly boundaryPolicy;
|
|
32
|
+
constructor(store: EpisodeStore, resolveEvent?: ((eventId: string) => MemoryEvent | null | undefined) | undefined, softReopenWindowMs?: number, reviewer?: TurnRelationAdvisoryReviewer | undefined, resolveTopicContext?: ((primary: MemoryEvent, episode?: MemoryEpisode) => Partial<TurnClassificationContext>) | undefined, boundaryPolicy?: EpisodeBoundaryPolicy);
|
|
20
33
|
appendTurn(events: MemoryEvent[], input: {
|
|
21
34
|
projectId: string;
|
|
22
35
|
sessionId: string;
|
|
@@ -25,6 +38,7 @@ export declare class EpisodeAssembler {
|
|
|
25
38
|
now?: number;
|
|
26
39
|
batchSeal?: boolean;
|
|
27
40
|
forceBatchSeal?: boolean;
|
|
41
|
+
allowNonUserEpisodeStart?: boolean;
|
|
28
42
|
}): EpisodeAssemblyResult;
|
|
29
43
|
appendTurnAsync(events: MemoryEvent[], input: {
|
|
30
44
|
projectId: string;
|
|
@@ -34,6 +48,7 @@ export declare class EpisodeAssembler {
|
|
|
34
48
|
now?: number;
|
|
35
49
|
batchSeal?: boolean;
|
|
36
50
|
forceBatchSeal?: boolean;
|
|
51
|
+
allowNonUserEpisodeStart?: boolean;
|
|
37
52
|
}): Promise<EpisodeAssemblyResult>;
|
|
38
53
|
private appendTurnClassified;
|
|
39
54
|
appendEvent(event: MemoryEvent, input: {
|
|
@@ -50,5 +65,8 @@ export declare class EpisodeAssembler {
|
|
|
50
65
|
}): Promise<EpisodeAssemblyResult>;
|
|
51
66
|
private classificationContext;
|
|
52
67
|
private classifyPrimary;
|
|
68
|
+
private appendOrderedEvents;
|
|
69
|
+
private evaluateBoundary;
|
|
70
|
+
private recordBoundaryDecisionSafe;
|
|
53
71
|
}
|
|
54
72
|
//# sourceMappingURL=EpisodeAssembler.d.ts.map
|