cogmem 3.7.1 → 3.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MEMORY_ATLAS.md +2 -2
  3. package/MEMORY_MODEL.md +1 -1
  4. package/README.md +6 -5
  5. package/RELEASE_CHECKLIST.md +5 -5
  6. package/dist/atlas/MemoryAtlasService.d.ts +1 -0
  7. package/dist/atlas/MemoryAtlasService.js +36 -4
  8. package/dist/atlas/MemoryAtlasTypes.d.ts +7 -0
  9. package/dist/belief/BeliefStore.d.ts +3 -1
  10. package/dist/belief/BeliefStore.js +10 -2
  11. package/dist/bin/episode.js +107 -16
  12. package/dist/bin/import-support.js +28 -11
  13. package/dist/config/CogmemConfig.d.ts +1 -0
  14. package/dist/config/CogmemConfig.js +45 -0
  15. package/dist/core/MemoryGraph.js +25 -8
  16. package/dist/dream/DreamScheduler.d.ts +6 -1
  17. package/dist/dream/DreamScheduler.js +36 -6
  18. package/dist/engine/CognitiveGraphCompiler.js +1 -1
  19. package/dist/engine/ConsolidationPipeline.js +4 -4
  20. package/dist/engine/DeepWritePromotionPolicy.d.ts +2 -0
  21. package/dist/engine/DeepWritePromotionPolicy.js +25 -4
  22. package/dist/engine/DreamCuratorWorker.d.ts +3 -0
  23. package/dist/engine/DreamCuratorWorker.js +18 -3
  24. package/dist/engine/FactCompiler.js +2 -2
  25. package/dist/entity/EntityGovernanceService.js +1 -1
  26. package/dist/episode/EpisodeActiveScopeGuard.d.ts +3 -0
  27. package/dist/episode/EpisodeActiveScopeGuard.js +46 -0
  28. package/dist/episode/EpisodeAssembler.d.ts +20 -2
  29. package/dist/episode/EpisodeAssembler.js +458 -98
  30. package/dist/episode/EpisodeBoundaryAuditService.d.ts +70 -0
  31. package/dist/episode/EpisodeBoundaryAuditService.js +276 -0
  32. package/dist/episode/EpisodeBoundaryPolicy.d.ts +75 -0
  33. package/dist/episode/EpisodeBoundaryPolicy.js +189 -0
  34. package/dist/episode/EpisodeBoundaryReplayEngine.d.ts +65 -0
  35. package/dist/episode/EpisodeBoundaryReplayEngine.js +272 -0
  36. package/dist/episode/EpisodeImportIdentity.d.ts +1 -0
  37. package/dist/episode/EpisodeImportIdentity.js +1 -0
  38. package/dist/episode/EpisodeInvariantValidator.d.ts +16 -0
  39. package/dist/episode/EpisodeInvariantValidator.js +99 -0
  40. package/dist/episode/EpisodeSplitPlanner.d.ts +89 -0
  41. package/dist/episode/EpisodeSplitPlanner.js +225 -0
  42. package/dist/episode/EpisodeStore.d.ts +67 -2
  43. package/dist/episode/EpisodeStore.js +443 -59
  44. package/dist/episode/EpisodeTypes.d.ts +2 -2
  45. package/dist/episode/TurnRelationClassifier.d.ts +9 -0
  46. package/dist/episode/TurnRelationClassifier.js +42 -12
  47. package/dist/episode/index.d.ts +3 -0
  48. package/dist/episode/index.js +3 -0
  49. package/dist/factory.d.ts +62 -3
  50. package/dist/factory.js +308 -58
  51. package/dist/mcp/CoreMcpTools.js +103 -8
  52. package/dist/mcp/server.js +1 -1
  53. package/dist/migrations/0028_episode_boundary_guardrails.d.ts +3 -0
  54. package/dist/migrations/0028_episode_boundary_guardrails.js +47 -0
  55. package/dist/migrations/0029_episode_active_scope_guard.d.ts +3 -0
  56. package/dist/migrations/0029_episode_active_scope_guard.js +16 -0
  57. package/dist/migrations/0030_memory_event_local_date_source.d.ts +3 -0
  58. package/dist/migrations/0030_memory_event_local_date_source.js +111 -0
  59. package/dist/migrations/0031_episode_boundary_integrity_repair.d.ts +7 -0
  60. package/dist/migrations/0031_episode_boundary_integrity_repair.js +124 -0
  61. package/dist/migrations/SchemaMigrationRunner.d.ts +2 -0
  62. package/dist/migrations/SchemaMigrationRunner.js +46 -15
  63. package/dist/migrations/index.d.ts +6 -1
  64. package/dist/migrations/index.js +15 -1
  65. package/dist/public.d.ts +1 -1
  66. package/dist/public.js +1 -1
  67. package/dist/recall/BrainRecall.js +1 -1
  68. package/dist/store/DeepWriteCandidateStore.d.ts +21 -3
  69. package/dist/store/DeepWriteCandidateStore.js +118 -11
  70. package/dist/store/EntityStore.d.ts +4 -0
  71. package/dist/store/EntityStore.js +24 -12
  72. package/dist/store/EventStore.d.ts +7 -2
  73. package/dist/store/EventStore.js +144 -23
  74. package/dist/store/FactStore.js +6 -5
  75. package/dist/store/MemoryAtlasStore.d.ts +1 -0
  76. package/dist/store/MemoryAtlasStore.js +54 -0
  77. package/dist/types/ExtensionPoints.d.ts +3 -1
  78. package/dist/types/index.d.ts +1 -0
  79. package/examples/hermes-backend/AGENTS.md +1 -1
  80. package/examples/hermes-backend/README.md +1 -1
  81. package/examples/hermes-backend/SKILL.md +2 -2
  82. package/examples/hermes-backend/references/operations.md +2 -2
  83. package/examples/openclaw-backend/AGENTS.md +1 -1
  84. package/examples/openclaw-backend/README.md +1 -1
  85. package/examples/openclaw-backend/SKILL.md +2 -2
  86. package/examples/openclaw-backend/references/operations.md +2 -2
  87. package/package.json +1 -1
@@ -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
+ }
@@ -14,15 +14,27 @@ export class MemoryGraph {
14
14
  anchorIndex = new Map();
15
15
  topicReclassifiedListeners = new Set();
16
16
  constructor(dbPath = ':memory:') {
17
- try {
18
- this.db = new Database(dbPath);
19
- this.initializeSchema();
20
- this.rebuildIndexes();
21
- }
22
- catch (error) {
23
- logger.error('Failed to initialize MemoryGraph:', error);
24
- throw new Error(`MemoryGraph initialization failed: ${error}`);
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
- constructor(episodeStore: EpisodeStore, curator: DreamCuratorWorker);
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
- constructor(episodeStore, curator) {
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 startedAt = options.now ?? Date.now();
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: Math.max(5_000, options.leaseMs ?? 5 * 60_000),
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
- this.episodeStore.completeDreamJob(job.episodeId, job.leaseId, ids, startedAt);
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 failure = classifyFailure(message, job.attempts, startedAt);
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.findByEntityId(fact.entityId);
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.findByEntityId(entityId))
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.findByEntityId(entityId)),
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.findByEntityId(item.entityId))
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.findByEntityId(entityId)?.canonicalName || '')
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.evaluateAndApply(candidate));
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.evaluateAndApply(candidate));
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
  }
@@ -26,6 +26,9 @@ export interface DreamCuratorRunOptions {
26
26
  eventId: string;
27
27
  relation: string;
28
28
  }>;
29
+ dreamJobLeaseId?: string;
30
+ leaseUntil?: number;
31
+ attemptGeneration?: number;
29
32
  }
30
33
  export interface DreamCuratorRunResult {
31
34
  runId?: string;
@@ -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) => ({ ...candidate, runId: run.runId, createdAt: now })));
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.findByEntityId(primaryDeviceEntityId)
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.findByEntityId(primaryDeviceEntityId);
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.findByEntityId(entityId);
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,3 @@
1
+ import type Database from 'bun:sqlite';
2
+ export declare function sealDuplicateOpenEpisodes(db: Database): void;
3
+ //# sourceMappingURL=EpisodeActiveScopeGuard.d.ts.map
@@ -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
- constructor(store: EpisodeStore, resolveEvent?: ((eventId: string) => MemoryEvent | null | undefined) | undefined, softReopenWindowMs?: number, reviewer?: TurnRelationAdvisoryReviewer | undefined, resolveTopicContext?: ((primary: MemoryEvent, episode?: MemoryEpisode) => Partial<TurnClassificationContext>) | undefined);
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