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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.7.3
4
+
5
+ - Added enforceable Episode Boundary guardrails with configurable max event, duration, idle-gap, and trusted local-date limits, including best-effort decision audit rows that never roll back successful raw event assignment.
6
+ - Added read-only `cogmem episode audit-boundaries` / MCP `cogmem_episode_audit_boundaries` for bounded episode health diagnostics without Dream, repair, Atlas, binding, activation, or decision-audit writes.
7
+ - Added deterministic read-only `cogmem episode split-plan` / MCP `cogmem_episode_split_plan` previews that preserve logical turns and return `applyableInCurrentVersion=false` with no apply command.
8
+
9
+ ## 3.7.2
10
+
11
+ - Fixed Memory Atlas `graph-explore` edge projection so selected action, topic, and ordinary memory edges are merged before the final stable edge limit.
12
+ - Added direct internal-edge SQL for selected Atlas nodes, including chunked parameters, so bounded graph results are not silently limited to the first 30 node IDs.
13
+ - Added edge truncation metadata and warnings when `graph-explore` omits candidate edges after prioritizing exact matched cards/facets.
14
+
3
15
  ## 3.7.1
4
16
 
5
17
  - Hardened OpenClaw volatile recall, Atlas context, turn bridge, and session-state serialization so historical text, card titles, summaries, source windows, related cards, short-term conclusions, and source anchors cannot close or create `COGMEM_*` prompt blocks.
package/MEMORY_ATLAS.md CHANGED
@@ -10,7 +10,7 @@ Atlas projects existing project-scoped records into these node kinds:
10
10
  - Facet nodes: `time`, `topic`, `issue`, `entity`, `session`, `thread`, `memoryKind`, and `actionKind`.
11
11
  - Action frames are extracted deterministically from raw user evidence and link actor, target, action kind, time, topic, project, and evidence.
12
12
 
13
- Existing memory edges remain canonical. Atlas adds derived navigation edges such as `OCCURRED_ON`, `OCCURRED_IN`, `ABOUT_TOPIC`, `PART_OF_ISSUE`, `INVOLVES_ENTITY`, `IN_SESSION`, `IN_THREAD`, and `HAS_EVIDENCE` without changing the underlying belief or binding graph. The 3.7.1 offline curator automatically proposes only bounded `SAME_ISSUE`, `FOLLOWS_UP`, and weak `RELATED_TO` episode relations; stronger relations such as `REFINES`, `CORRECTS`, `CONTRADICTS`, and `SUPERSEDES` remain reserved for explicit evidence-bearing governance/binding flows. Entity facet node IDs are project-scoped; same-name tools or people in different projects do not share Atlas nodes.
13
+ Existing memory edges remain canonical. Atlas adds derived navigation edges such as `OCCURRED_ON`, `OCCURRED_IN`, `ABOUT_TOPIC`, `PART_OF_ISSUE`, `INVOLVES_ENTITY`, `IN_SESSION`, `IN_THREAD`, and `HAS_EVIDENCE` without changing the underlying belief or binding graph. The 3.7.2 offline curator automatically proposes only bounded `SAME_ISSUE`, `FOLLOWS_UP`, and weak `RELATED_TO` episode relations; stronger relations such as `REFINES`, `CORRECTS`, `CONTRADICTS`, and `SUPERSEDES` remain reserved for explicit evidence-bearing governance/binding flows. Entity facet node IDs are project-scoped; same-name tools or people in different projects do not share Atlas nodes.
14
14
 
15
15
  This is not a copied tree. One episode exists once as `episode:<id>`. A day, topic, issue, entity, session, memory kind, or action kind is only a facet entrance:
16
16
 
@@ -145,4 +145,4 @@ Upgrade an existing 3.5.2 database with:
145
145
  cogmem migrate --yes --backup --json
146
146
  ```
147
147
 
148
- Migration 0025 creates and backfills the disposable projection. Migration 0026 adds exact memory-kind metadata, projection health, and candidate-review audit state. Migration 0027 corrects 3.6.0-upgraded databases by marking Atlas projections dirty until the real action/time rebuild runs. Cogmem 3.7.1 keeps the projection-first schema and adds facet nodes, canonical cards, issue hints, and episode relation edges during Atlas rebuild/maintenance. Projection metadata schema `3.7.2` invalidates earlier 3.7.x Atlas v2 projections so project-scoped entity facets are rebuilt. `cogmem memory tick` refreshes only dirty projects, records rebuild errors, prunes old access telemetry, and decays navigation activation without starting a daemon.
148
+ Migration 0025 creates and backfills the disposable projection. Migration 0026 adds exact memory-kind metadata, projection health, and candidate-review audit state. Migration 0027 corrects 3.6.0-upgraded databases by marking Atlas projections dirty until the real action/time rebuild runs. Cogmem 3.7.2 keeps the projection-first schema and adds facet nodes, canonical cards, issue hints, and episode relation edges during Atlas rebuild/maintenance. Projection metadata schema `3.7.2` invalidates earlier 3.7.x Atlas v2 projections so project-scoped entity facets are rebuilt. `cogmem memory tick` refreshes only dirty projects, records rebuild errors, prunes old access telemetry, and decays navigation activation without starting a daemon.
package/MEMORY_MODEL.md CHANGED
@@ -115,7 +115,7 @@ For pre-answer agent context, prefer `KernelAgentMemoryBackend.recallPack()` whe
115
115
 
116
116
  Collection routing is enforced in compiled and raw fallback paths. Default recall includes untagged and `collection:anchor` memory. Specialized collections such as `collection:theseus` must be requested explicitly with `collection: "theseus"` or `--collection theseus`.
117
117
 
118
- `MemoryKernel.buildMemoryMap()` and `cogmem memory map` expose a static self-map: anatomy, data lanes, hard bounds, counters, and commands. The map includes episode counts, unassigned raw evidence, and the sealed Episode Dream backlog. Core schema version 24 adds project-scoped topic nodes, aliases, relations and operations; episode cross-references and repair audit; ingest lifecycle state; and per-episode Dream failure details. Production initialization runs migrations; `EpisodeStore.initializeSchema` is compatibility bootstrap for direct tests only, with a parity test against migrations 22–24. Use `cogmem migrate --dry-run --json` before an upgrade and `cogmem migrate --yes --backup` to apply it without rewriting Raw Ledger evidence.
118
+ `MemoryKernel.buildMemoryMap()` and `cogmem memory map` expose a static self-map: anatomy, data lanes, hard bounds, counters, and commands. The map includes episode counts, unassigned raw evidence, and the sealed Episode Dream backlog. Core schema version 28 includes project-scoped topic nodes, aliases, relations and operations; episode cross-references and repair audit; ingest lifecycle state; per-episode Dream failure details; and best-effort episode boundary decision audit rows. Production initialization runs migrations; `EpisodeStore.initializeSchema` is compatibility bootstrap for direct tests only, with a parity test against episode migrations 22–28. Use `cogmem migrate --dry-run --json` before an upgrade and `cogmem migrate --yes --backup` to apply it without rewriting Raw Ledger evidence.
119
119
 
120
120
  Graph edge confidence, stability, and activation are separate. Recall and maintenance may decay activation, but they must not decay provenance confidence or the stability of `SUPPORTS`, `CORRECTS`, `CONTRADICTS`, and `SUPERSEDES` evidence. `BrainGraphView` is a bounded read-only traversal surface; it cannot mutate canonical memory.
121
121
 
package/README.md CHANGED
@@ -11,7 +11,7 @@ It is not a knowledge-base app, a note-taking app, a vector RAG wrapper, an Obsi
11
11
 
12
12
  ## Status
13
13
 
14
- Current version: `3.7.1`
14
+ Current version: `3.7.3`
15
15
 
16
16
  Distribution: npm registry. GitHub remains the source mirror and hosts this installer, but package install and upgrade resolve `cogmem` from npm by default.
17
17
 
@@ -229,7 +229,7 @@ cogmem migrate --dry-run --json
229
229
 
230
230
  For a manual migration, run `cogmem migrate --yes --backup`. The migration runner adopts the existing `_meta.schema_version`, applies only later idempotent migrations, preserves Raw Ledger rows, and creates a timestamped, transaction-consistent standalone database backup before changing an on-disk database. The backup includes committed SQLite WAL pages instead of copying only the main database file.
231
231
 
232
- Upgrade a 3.5.2 database, a 3.6.x database, or a pre-release schema-25 test database into the current 3.7.1 schema and projection set with one command:
232
+ Upgrade a 3.5.2 database, a 3.6.x database, or a pre-release schema-25 test database into the current 3.7.3 schema and projection set with one command:
233
233
 
234
234
  ```bash
235
235
  cogmem migrate --yes --backup --json
@@ -278,11 +278,12 @@ cogmem dream tick --project my-agent --mode auto --max-episodes 10 --json
278
278
 
279
279
  Raw events are always written first. `KernelAgentMemoryBackend` and OpenClaw plugin 0.7.1 assemble live turns automatically. The foreground hook uses deterministic rules and previous assistant/user context; background import and repair paths may use the advisory hybrid classifier. Advisory output is allow-listed and cannot directly mutate durable memory. Unknown turns now fail closed as ambiguous. Continuation requires explicit continuation language or project/topic/entity/semantic overlap; Cogmem does not route domains with an expanding hard-coded keyword dictionary.
280
280
 
281
- Hookless MCP agents can call `cogmem_episode_append` or bounded `cogmem_episode_import`. Existing OpenClaw/Hermes import commands use stable content identities and the same episode schema. Low-confidence imported groups soft-seal for review unless an operator explicitly forces sealing. Cogmem skips import batch sealing for empty episode boundaries and skips legacy empty Dream jobs so one bad imported episode cannot block the queue. Episode semantic summaries and closure receipts are control hints, never durable evidence; every Dream candidate must cite a non-empty subset of the episode's raw event IDs and still pass CPU governance.
281
+ Hookless MCP agents can call `cogmem_episode_append` or bounded `cogmem_episode_import`. Existing OpenClaw/Hermes import commands use stable content identities and the same episode schema. Low-confidence imported groups soft-seal for review unless an operator explicitly forces sealing. Episode boundary guardrails can enforce configurable max-event, duration, idle-gap, and trusted local-date limits before advisory review, while decision audit writes remain best-effort. Cogmem skips import batch sealing for empty episode boundaries and skips legacy empty Dream jobs so one bad imported episode cannot block the queue. Episode semantic summaries and closure receipts are control hints, never durable evidence; every Dream candidate must cite a non-empty subset of the episode's raw event IDs and still pass CPU governance.
282
282
 
283
283
  User-shaped topic state is project-scoped. Explicit user operations become active and auditable; model-proposed topics, aliases, and relations remain candidates. Use MCP `cogmem_topic_list` to inspect nodes, `cogmem_topic_operate` to create, rename, alias, move, merge, split, or relate topics, and `cogmem_topic_rollback` to reverse an audited operation. Alias collisions fail closed as `needs_review`; applications can also call `TopicGovernance.rollback()` through the public API.
284
284
 
285
285
  Episode surgery is available through `cogmem episode split|merge|move-event|reclassify|requeue-dream` or MCP `cogmem_episode_repair`. Structural repair recomputes closure receipts, invalidates candidates derived from the old episode boundary, preserves cross-references, requeues eligible sealed episodes, and writes an audit record.
286
+ Use `cogmem episode audit-boundaries` for read-only boundary diagnostics and `cogmem episode split-plan` for deterministic read-only split previews. The split preview does not call repair, does not move events, and returns no executable apply command in 3.7.3.
286
287
 
287
288
  Inspect queue state:
288
289
 
@@ -304,7 +305,7 @@ cogmem memory review --project my-agent --id <candidate-id> --action approve --a
304
305
 
305
306
  `cogmem memory map` remains the system anatomy map. Memory Atlas is the content map. It does not replace recall or create a second fact store; it projects existing topics, entities, clusters, episodes, beliefs, action frames, time buckets, bindings, and evidence into a bounded navigation surface.
306
307
 
307
- In 3.7.1, Atlas is a multi-dimensional navigation graph rather than a folder tree. A remembered episode has one canonical node such as `episode:<id>`. Time, topic, issue, entity, session/thread, memory-kind, and action-kind nodes are facet entrances connected to that canonical episode with typed edges. Results dedupe by `canonicalId` and explain `matchedFacets` plus `matchedPaths`.
308
+ In 3.7.2, Atlas is a multi-dimensional navigation graph rather than a folder tree. A remembered episode has one canonical node such as `episode:<id>`. Time, topic, issue, entity, session/thread, memory-kind, and action-kind nodes are facet entrances connected to that canonical episode with typed edges. Results dedupe by `canonicalId` and explain `matchedFacets` plus `matchedPaths`.
308
309
 
309
310
  ```bash
310
311
  cogmem memory graph --project my-agent --json
@@ -798,7 +799,7 @@ npm pack --dry-run --json
798
799
  npm publish --dry-run --access public
799
800
  ```
800
801
 
801
- Create a GitHub Release from the matching version tag, for example `v3.7.1`. The `.github/workflows/publish.yml` workflow publishes to npm only when the release is published, not when a tag is pushed. The npm Trusted Publisher entry must match repository `liuqin164/cogmem`, workflow file `publish.yml`, and environment `npm publish`.
802
+ Create a GitHub Release from the matching version tag, for example `v3.7.3`. The `.github/workflows/publish.yml` workflow publishes to npm only when the release is published, not when a tag is pushed. The npm Trusted Publisher entry must match repository `liuqin164/cogmem`, workflow file `publish.yml`, and environment `npm publish`.
802
803
 
803
804
  Publish manually only for emergency fallback:
804
805
 
@@ -1,11 +1,11 @@
1
- # cogmem 3.7.1 Release Checklist
1
+ # cogmem 3.7.3 Release Checklist
2
2
 
3
3
  This release is distributed through the npm registry. GitHub remains the source and review mirror.
4
4
 
5
5
  ## Required Metadata
6
6
 
7
7
  - `package.json` name is `cogmem`.
8
- - `package.json` version is `3.7.1`.
8
+ - `package.json` version is `3.7.3`.
9
9
  - `package.json` has `publishConfig.access = public`.
10
10
  - Public export `.` points to `dist/public.js` and `dist/public.d.ts`.
11
11
  - Internal subpath `./internal` exists only as an explicit advanced subpath.
@@ -39,7 +39,7 @@ This release is distributed through the npm registry. GitHub remains the source
39
39
  - `cogmem episode`
40
40
  - `cogmem dream`
41
41
 
42
- MCP `tools/list` includes strategy, episode append/import/status/seal/repair, topic list/operate/rollback, candidate review, conditional Dream tick/status, prospective tools, all seven read-only Memory Atlas queries, and explicit `cogmem_graph_touch`. Atlas queries declare read-only/idempotent semantics and never change activation; only touch records selected-node use. Episode append/import never run Dream. MCP Dream tick requires `maintenanceMode: true` to process work; otherwise it is recommendation-only.
42
+ MCP `tools/list` includes strategy, episode append/import/status/audit-boundaries/split-plan/seal/repair, topic list/operate/rollback, candidate review, conditional Dream tick/status, prospective tools, all seven read-only Memory Atlas queries, and explicit `cogmem_graph_touch`. Atlas queries declare read-only/idempotent semantics and never change activation; only touch records selected-node use. Episode append/import never run Dream. Episode audit-boundaries and split-plan are read-only and never repair, split, Dream, Atlas, bind, or update activation. MCP Dream tick requires `maintenanceMode: true` to process work; otherwise it is recommendation-only.
43
43
 
44
44
  ## Required Documentation
45
45
 
@@ -61,7 +61,7 @@ MCP `tools/list` includes strategy, episode append/import/status/seal/repair, to
61
61
  - README and skills explain Raw Ledger-first episode assembly, soft/hard sealing, explicit conditional Dream ticks, raw-event evidence grounding, repair/retry, and hookless Hermes MCP/import usage.
62
62
  - README and skills give the full post-import maintenance sequence: status, episode status, Dream status, bounded Dream tick, candidate listing, govern candidate, needs-confirmation listing, explicit review, and recall verification.
63
63
  - README and skills explain that 3.6.4+ skips empty imported episode boundaries and legacy empty Dream jobs instead of letting them abort `dream tick`.
64
- - README and skills explain the 3.7.1 agent operations protocol: `memory plan` for next actions, default grouped `memory candidates --json`, `memory list --since/--until/--order`, historical-discussion recall intent, Atlas canonical cards, `matchedFacets`, `relatedButNotSelected`, `relaxationTrace`, Atlas evidence `sourceLocator`, and cursor-based `memory bind`.
64
+ - README and skills explain the 3.7.3 agent operations protocol: `memory plan` for next actions, default grouped `memory candidates --json`, `memory list --since/--until/--order`, historical-discussion recall intent, Atlas canonical cards, `matchedFacets`, `relatedButNotSelected`, `relaxationTrace`, Atlas evidence `sourceLocator`, cursor-based `memory bind`, and read-only episode boundary audit/split planning.
65
65
  - README, `MEMORY_ATLAS.md`, and installed skills explain the multi-dimensional Atlas model: a canonical episode/raw event exists once, facet nodes connect through typed edges, query results intersect facets, and display/injection dedupes by `canonicalId`.
66
66
  - `connect openclaw|hermes --json` documents structured `nextSteps`; `nextCommands` must contain only agent-safe, non-interactive commands and must not include `cogmem init`, `cogmem-init`, gateway restart, or Hermes reload.
67
67
  - README and skills document `cogmem mcp` as the preferred MCP server command for new configs while preserving `cogmem-mcp` as a compatibility bin.
@@ -94,7 +94,7 @@ npm publish --dry-run --access public
94
94
 
95
95
  The pack dry-run must include built public API files, CLI files, examples, docs, and `install.sh`. It must not include local databases or machine-specific files.
96
96
 
97
- After verification, create a GitHub Release from the matching version tag, for example `v3.7.1`. The release workflow publishes through npm Trusted Publishing when the release is published. It must not publish on tag push alone.
97
+ After verification, create a GitHub Release from the matching version tag, for example `v3.7.3`. The release workflow publishes through npm Trusted Publishing when the release is published. It must not publish on tag push alone.
98
98
 
99
99
  Emergency manual fallback:
100
100
 
@@ -22,6 +22,7 @@ export declare class MemoryAtlasService {
22
22
  private attachCardEvidence;
23
23
  private evidence;
24
24
  private edgesFor;
25
+ private edgeProjection;
25
26
  private safeEdges;
26
27
  private adjacentEdges;
27
28
  private directEdgesToTarget;
@@ -57,8 +57,8 @@ export class MemoryAtlasService {
57
57
  nodes = uniqueNodes([...actions.map((action) => this.store.getNode(action.id, projectId)).filter((node) => Boolean(node)), ...nodes]).slice(0, limit);
58
58
  }
59
59
  const nodesWithEvidence = this.attachEvidence(nodes, projectId, options);
60
- const edges = this.edgesFor(nodesWithEvidence, projectId);
61
- const result = slice(projectId, nodesWithEvidence, edges, query);
60
+ const edgeProjection = this.edgeProjection(nodesWithEvidence, projectId, exactMatchedNodeIds(cards, facetResult.plan));
61
+ const result = slice(projectId, nodesWithEvidence, edgeProjection.edges, query);
62
62
  result.facets = {
63
63
  ...facetsForPlan(facetResult.plan),
64
64
  legacy: { time: compiled.range, target: target.labels.join(', ') || compiled.target, memoryKinds: compiled.memoryKinds, keywords: compiled.keywords },
@@ -68,6 +68,10 @@ export class MemoryAtlasService {
68
68
  result.cards = this.attachCardEvidence(cards, projectId, options);
69
69
  if (facetResult.relaxationTrace.length)
70
70
  result.relaxationTrace = facetResult.relaxationTrace;
71
+ if (edgeProjection.truncation) {
72
+ result.edgeTruncation = edgeProjection.truncation;
73
+ result.warnings.push(`edges_truncated:${edgeProjection.truncation.omitted}_omitted`);
74
+ }
71
75
  const hasFacet = Boolean(compiled.range || compiled.target || compiled.memoryKinds.length || compiled.tokens.length);
72
76
  result.coldMemoryResurrected = hasFacet && nodes.some((node) => node.activation <= 0.1);
73
77
  return result;
@@ -248,9 +252,23 @@ export class MemoryAtlasService {
248
252
  });
249
253
  }
250
254
  edgesFor(nodes, projectId) {
255
+ return this.edgeProjection(nodes, projectId).edges;
256
+ }
257
+ edgeProjection(nodes, projectId, priorityIds = new Set()) {
251
258
  const ids = new Set(nodes.map((node) => node.id));
252
- return this.safeEdges(this.store.listEdgesForNodes(projectId, [...ids], 60)
253
- .filter((edge) => ids.has(edge.source) && ids.has(edge.target)).slice(0, 60), projectId);
259
+ const limit = 60;
260
+ const candidates = this.safeEdges(this.store.listEdgesWithinNodes(projectId, [...ids], 4000)
261
+ .filter((edge) => ids.has(edge.source) && ids.has(edge.target)), projectId);
262
+ const sorted = uniqueEdges(candidates).sort((left, right) => edgePriority(right, priorityIds) - edgePriority(left, priorityIds)
263
+ || right.confidence - left.confidence
264
+ || edgeKey(left).localeCompare(edgeKey(right)));
265
+ const edges = sorted.slice(0, limit);
266
+ return {
267
+ edges,
268
+ truncation: sorted.length > edges.length
269
+ ? { limit, returned: edges.length, omitted: sorted.length - edges.length, candidateCount: sorted.length, prioritized: priorityIds.size > 0 }
270
+ : undefined,
271
+ };
254
272
  }
255
273
  safeEdges(edges, projectId) {
256
274
  return edges.map((edge) => ({ ...edge, evidenceEventIds: edge.evidenceEventIds.filter((eventId) => {
@@ -294,6 +312,20 @@ function uniqueIds(ids) { return Array.from(new Set(ids)); }
294
312
  function uniqueEdges(edges) {
295
313
  return Array.from(new Map(edges.map((edge) => [`${edge.source}\0${edge.relation}\0${edge.target}`, edge])).values());
296
314
  }
315
+ function edgeKey(edge) { return `${edge.source}\0${edge.relation}\0${edge.target}`; }
316
+ function edgePriority(edge, priorityIds) {
317
+ return (priorityIds.has(edge.source) ? 1 : 0) + (priorityIds.has(edge.target) ? 1 : 0);
318
+ }
319
+ function exactMatchedNodeIds(cards, plan) {
320
+ return new Set([
321
+ ...cards.flatMap((card) => [
322
+ card.canonicalId,
323
+ ...card.matchedFacets.map((facet) => facet.nodeId),
324
+ ...card.matchedPaths.flatMap((path) => path.via),
325
+ ]),
326
+ ...plan.facets.map((facet) => facet.nodeId).filter((id) => Boolean(id)),
327
+ ]);
328
+ }
297
329
  function edgeTraversalCost(edge) {
298
330
  const confidence = Math.max(0.01, Math.min(1, edge.confidence));
299
331
  const relationPenalty = /^(EVIDENCED_BY|DERIVED_FROM|TARGETS|OCCURRED_IN|SUPPORTS|ABOUT|MENTIONS)$/u.test(edge.relation)
@@ -101,6 +101,13 @@ export interface MemoryAtlasSlice {
101
101
  edges: MemoryAtlasEdge[];
102
102
  nextActions: MemoryAtlasNextAction[];
103
103
  warnings: string[];
104
+ edgeTruncation?: {
105
+ limit: number;
106
+ returned: number;
107
+ omitted: number;
108
+ candidateCount: number;
109
+ prioritized: boolean;
110
+ };
104
111
  facets?: {
105
112
  planner?: {
106
113
  intent: string;
@@ -1,3 +1,4 @@
1
+ import Database from 'bun:sqlite';
1
2
  import type { BeliefCandidate, BeliefConflictCandidate, BeliefEvidenceRecord, BeliefRecord, BeliefRevisionDecision } from '../types/index.js';
2
3
  import { EventStore } from '../store/EventStore.js';
3
4
  import type { PolicyExecutionRecord } from '../store/PolicyExecutionStore.js';
@@ -6,8 +7,9 @@ export declare class BeliefStore {
6
7
  private static readonly SOURCE_TRUST;
7
8
  private static readonly SCOPE_PRIORITY;
8
9
  private db;
10
+ private ownsDb;
9
11
  private closed;
10
- constructor(dbPath?: string, eventStore?: EventStore | undefined);
12
+ constructor(dbPath?: string | Database, eventStore?: EventStore | undefined);
11
13
  private initializeSchema;
12
14
  findByCanonicalKey(canonicalKey: string): BeliefRecord[];
13
15
  countActive(projectId?: string): number;
@@ -20,10 +20,17 @@ export class BeliefStore {
20
20
  global: 1
21
21
  };
22
22
  db;
23
+ ownsDb = true;
23
24
  closed = false;
24
25
  constructor(dbPath = ':memory:', eventStore) {
25
26
  this.eventStore = eventStore;
26
- this.db = new Database(dbPath);
27
+ if (dbPath instanceof Database) {
28
+ this.db = dbPath;
29
+ this.ownsDb = false;
30
+ }
31
+ else {
32
+ this.db = new Database(dbPath);
33
+ }
27
34
  this.initializeSchema();
28
35
  }
29
36
  initializeSchema() {
@@ -892,7 +899,8 @@ export class BeliefStore {
892
899
  close() {
893
900
  if (this.closed)
894
901
  return;
895
- this.db.close();
902
+ if (this.ownsDb)
903
+ this.db.close();
896
904
  this.closed = true;
897
905
  }
898
906
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { createReadStream, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
3
4
  import { createInterface } from 'node:readline';
4
5
  import { createStableImportIdentityFactory } from '../episode/EpisodeImportIdentity.js';
5
6
  import { createMemoryKernel, createMemoryKernelFromConfig } from '../factory.js';
@@ -24,12 +25,15 @@ function parseArgs(argv) {
24
25
  }
25
26
  function usage() {
26
27
  return [
27
- 'Usage: cogmem episode <append|import|list|get|seal|status|repair|split|merge|move-event|reclassify|requeue-dream> [args]',
28
+ 'Usage: cogmem episode <append|import|list|get|seal|status|audit-boundaries|boundary-decisions|split-plan|repair|split|merge|move-event|reclassify|requeue-dream> [args]',
28
29
  ' append --project <id> --session <id> --source-agent <id> --role <role> --text <text>',
29
30
  ' import --project <id> --session <id> --source-agent <id> --format jsonl --file <path> [--seal-batch] [--force-seal] [--chunk-size <n>] [--checkpoint-file <path>] [--resume] [--start-line <n>] [--end-line <n>] [--max-lines <n>] [--skip-errors] [--max-errors <n>]',
30
31
  ' list|status [--project <id>] [--session <id>] [--json]',
31
- ' get --episode <id> [--json]',
32
- ' seal --episode <id> [--mode soft|hard|manual|batch] [--reason <reason>]',
32
+ ' get --project <id> --episode <id> [--json]',
33
+ ' seal --project <id> --episode <id> [--mode soft|hard|manual|batch] [--reason <reason>]',
34
+ ' audit-boundaries --project <id> [--episode <id>] [--status open|soft_sealed|sealed] [--limit <n>] [--cursor <cursor>]',
35
+ ' boundary-decisions --project <id> [--event <eventId>] [--limit <n>]',
36
+ ' split-plan --project <id> --episode <id> [--include-event-ids]',
33
37
  ' repair [--project <id>] [--since <globalSeq>] [--limit <n>]',
34
38
  ' split --project <id> --episode <id> --events <eventId,eventId>',
35
39
  ' merge --project <id> --source-episode <id> --target-episode <id>',
@@ -62,6 +66,8 @@ async function main() {
62
66
  projectId: requiredArg(args, 'project'), sessionId: requiredArg(args, 'session'),
63
67
  sourceAgent: requiredArg(args, 'source-agent'), role: roleArg(args.role), text: requiredArg(args, 'text'),
64
68
  externalMessageId: stringArg(args, 'external-id'), timestamp: numberArg(args, 'timestamp'),
69
+ threadId: stringArg(args, 'thread-id'), turnId: stringArg(args, 'turn-id'), turnSeq: numberArg(args, 'turn-seq'),
70
+ localDate: stringArg(args, 'local-date'), eventOrdinal: numberArg(args, 'event-ordinal'),
65
71
  });
66
72
  }
67
73
  else if (args.command === 'import') {
@@ -74,14 +80,44 @@ async function main() {
74
80
  result = { episodes: kernel.listEpisodes({ projectId, sessionId, limit: numberArg(args, 'limit') }), dream: kernel.getEpisodeDreamStatus(projectId) };
75
81
  }
76
82
  else if (args.command === 'get') {
83
+ const requestedProject = requiredArg(args, 'project');
77
84
  const episodeId = requiredArg(args, 'episode');
78
- result = { episode: kernel.getEpisode(episodeId), events: kernel.listEpisodeEventLinks(episodeId), closureReceipts: kernel.listEpisodeClosureReceipts({ episodeId }) };
85
+ const episode = kernel.getEpisode(episodeId);
86
+ if (!episode || episode.projectId !== requestedProject)
87
+ throw new Error(`episode_project_mismatch:${episodeId}`);
88
+ result = { episode, events: kernel.listEpisodeEventLinks(episodeId), closureReceipts: kernel.listEpisodeClosureReceipts({ episodeId, projectId: requestedProject }) };
79
89
  }
80
90
  else if (args.command === 'seal') {
81
- result = kernel.sealEpisode(requiredArg(args, 'episode'), {
91
+ const episodeId = requiredArg(args, 'episode');
92
+ const requestedProject = requiredArg(args, 'project');
93
+ const episode = kernel.getEpisode(episodeId);
94
+ if (!episode || episode.projectId !== requestedProject)
95
+ throw new Error(`episode_project_mismatch:${episodeId}`);
96
+ result = kernel.sealEpisode(episodeId, {
82
97
  mode: closureModeArg(stringArg(args, 'mode')), reason: stringArg(args, 'reason') || 'cli_manual_seal',
83
98
  });
84
99
  }
100
+ else if (args.command === 'audit-boundaries') {
101
+ result = kernel.auditEpisodeBoundaries({
102
+ projectId: requiredArg(args, 'project'), episodeId: stringArg(args, 'episode'),
103
+ status: statusArg(stringArg(args, 'status')), limit: numberArg(args, 'limit'), cursor: stringArg(args, 'cursor'),
104
+ maxEvents: numberArg(args, 'max-events'), maxDurationMs: numberArg(args, 'max-duration-ms'),
105
+ maxIdleGapMs: numberArg(args, 'max-idle-gap-ms'), timezone: stringArg(args, 'timezone'),
106
+ });
107
+ }
108
+ else if (args.command === 'boundary-decisions') {
109
+ result = { decisions: kernel.listEpisodeBoundaryDecisions({
110
+ projectId: requiredArg(args, 'project'), primaryEventId: stringArg(args, 'event'), limit: numberArg(args, 'limit'),
111
+ }) };
112
+ }
113
+ else if (args.command === 'split-plan') {
114
+ result = kernel.planEpisodeSplit({
115
+ projectId: requiredArg(args, 'project'), episodeId: requiredArg(args, 'episode'),
116
+ maxEvents: numberArg(args, 'max-events'), maxDurationMs: numberArg(args, 'max-duration-ms'),
117
+ maxIdleGapMs: numberArg(args, 'max-idle-gap-ms'), timezone: stringArg(args, 'timezone'),
118
+ includeEventIds: args['include-event-ids'] === true,
119
+ });
120
+ }
85
121
  else if (args.command === 'repair') {
86
122
  result = kernel.repairEpisodes({ projectId, sinceGlobalSeq: numberArg(args, 'since'), limit: numberArg(args, 'limit') });
87
123
  }
@@ -126,9 +162,17 @@ async function importJsonl(kernel, args) {
126
162
  const file = requiredArg(args, 'file');
127
163
  const checkpointFile = stringArg(args, 'checkpoint-file') || `${file}.cogmem-checkpoint.json`;
128
164
  const chunkSize = Math.max(1, Math.min(Math.trunc(numberArg(args, 'chunk-size') ?? 500), 5000));
165
+ const fileHash = createHash('sha256').update(readFileSync(file)).digest('hex');
129
166
  const checkpoint = args.resume === true && existsSync(checkpointFile)
130
167
  ? JSON.parse(readFileSync(checkpointFile, 'utf8'))
131
168
  : {};
169
+ const checkpointPrefixHash = checkpoint.processedLine ? sourcePrefixHash(file, checkpoint.processedLine) : undefined;
170
+ if (args.resume === true && checkpoint.fileHash && ((checkpoint.processedPrefixHash ? checkpoint.processedPrefixHash !== checkpointPrefixHash : checkpoint.fileHash !== fileHash)
171
+ || checkpoint.projectId !== projectId
172
+ || checkpoint.sourceAgent !== sourceAgent
173
+ || checkpoint.sessionId !== sessionId
174
+ || checkpoint.format !== format))
175
+ throw new Error('episode_import_checkpoint_source_mismatch');
132
176
  const resumeAfter = Math.max(0, checkpoint.processedLine || 0);
133
177
  const startLine = Math.max(1, Math.trunc(numberArg(args, 'start-line') ?? 1));
134
178
  const endLine = Math.max(startLine, Math.trunc(numberArg(args, 'end-line') ?? Number.MAX_SAFE_INTEGER));
@@ -151,15 +195,38 @@ async function importJsonl(kernel, args) {
151
195
  let duplicates = 0;
152
196
  let processed = 0;
153
197
  let lineNumber = 0;
198
+ let lastProcessedLine = resumeAfter;
154
199
  let selectedLines = 0;
200
+ let windowStopped = false;
155
201
  const errors = [];
156
202
  const reader = createInterface({ input: createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
157
203
  for await (const rawLine of reader) {
158
204
  lineNumber += 1;
159
205
  if (!rawLine.trim())
160
206
  continue;
161
- if (lineNumber > endLine || selectedLines >= maxLines)
207
+ if (lineNumber > endLine || selectedLines >= maxLines) {
208
+ windowStopped = true;
162
209
  break;
210
+ }
211
+ if (lineNumber <= resumeAfter || lineNumber < startLine) {
212
+ // Rebuild only the deterministic fallback identity state. A malformed
213
+ // skipped line must remain skipped during resume.
214
+ try {
215
+ const prior = JSON.parse(rawLine);
216
+ const priorText = typeof prior.text === 'string' ? prior.text : typeof prior.content === 'string' ? prior.content : undefined;
217
+ const hasExplicitIdentity = typeof prior.externalMessageId === 'string' || typeof prior.id === 'string';
218
+ if (priorText && !hasExplicitIdentity) {
219
+ const priorSession = typeof prior.sessionId === 'string' ? prior.sessionId : sessionId;
220
+ const priorRole = roleValue(prior.role);
221
+ const priorTimestamp = prior.timestamp === undefined ? undefined : timeValue(prior.timestamp);
222
+ getIdentity(priorSession)({ role: priorRole, text: priorText, timestamp: priorTimestamp });
223
+ }
224
+ }
225
+ catch {
226
+ // The checkpoint already records this line as skipped/failed.
227
+ }
228
+ continue;
229
+ }
163
230
  try {
164
231
  const message = JSON.parse(rawLine);
165
232
  const text = typeof message.text === 'string' ? message.text : typeof message.content === 'string' ? message.content : undefined;
@@ -170,21 +237,24 @@ async function importJsonl(kernel, args) {
170
237
  const resolvedSessionId = typeof message.sessionId === 'string' ? message.sessionId : sessionId;
171
238
  const role = roleValue(message.role);
172
239
  const timestamp = timeValue(message.timestamp);
240
+ const threadId = typeof message.threadId === 'string' ? message.threadId : undefined;
241
+ const turnId = typeof message.turnId === 'string' ? message.turnId : undefined;
242
+ const turnSeq = typeof message.turnSeq === 'number' && Number.isFinite(message.turnSeq) ? message.turnSeq : undefined;
243
+ const localDate = typeof message.localDate === 'string' ? message.localDate : undefined;
244
+ const eventOrdinal = typeof message.eventOrdinal === 'number' && Number.isFinite(message.eventOrdinal) ? message.eventOrdinal : undefined;
173
245
  const externalMessageId = typeof message.externalMessageId === 'string'
174
246
  ? message.externalMessageId
175
247
  : typeof message.id === 'string'
176
248
  ? message.id
177
249
  : getIdentity(resolvedSessionId)({ role, text, timestamp });
178
- // Rebuild occurrence counters while streaming past the checkpoint/start line.
179
- if (lineNumber <= resumeAfter || lineNumber < startLine)
180
- continue;
181
250
  selectedLines += 1;
182
251
  const result = await kernel.appendEpisodeMessageAsync({
183
- projectId, sourceAgent, sessionId: resolvedSessionId, role, text, timestamp,
252
+ projectId, sourceAgent, sessionId: resolvedSessionId, role, text, timestamp, threadId, turnId, turnSeq, localDate, eventOrdinal,
184
253
  externalMessageId,
185
254
  metadata: { imported: true, importFormat: format },
186
255
  });
187
256
  processed += 1;
257
+ lastProcessedLine = lineNumber;
188
258
  result.created ? imported += 1 : duplicates += 1;
189
259
  if (result.episodeId)
190
260
  episodeIds.add(result.episodeId);
@@ -193,27 +263,35 @@ async function importJsonl(kernel, args) {
193
263
  if (result.ignored)
194
264
  ignoredEventIds.push(result.eventId);
195
265
  if (processed % chunkSize === 0)
196
- writeCheckpoint(checkpointFile, { processedLine: lineNumber, lastProcessedLine: lineNumber, processed, projectId, sourceAgent, sessionId });
266
+ writeCheckpoint(checkpointFile, {
267
+ processedLine: lineNumber, lastProcessedLine: lineNumber, processed, projectId, sourceAgent, sessionId, format, fileHash,
268
+ processedPrefixHash: sourcePrefixHash(file, lineNumber), fileCompleted: false, windowCompleted: false,
269
+ });
197
270
  }
198
271
  catch (error) {
199
272
  const message = error instanceof Error ? error.message : String(error);
200
273
  errors.push({ line: lineNumber, error: message });
201
274
  writeCheckpoint(checkpointFile, {
202
275
  failedAtLine: lineNumber, error: message, resumeFrom: lineNumber,
203
- lastProcessedLine: Math.max(resumeAfter, lineNumber - 1), processedLine: Math.max(resumeAfter, lineNumber - 1),
204
- processed, projectId, sourceAgent, sessionId,
276
+ lastProcessedLine, processedLine: lastProcessedLine,
277
+ processed, projectId, sourceAgent, sessionId, format, fileHash, processedPrefixHash: sourcePrefixHash(file, lastProcessedLine), fileCompleted: false, windowCompleted: false,
205
278
  });
206
279
  if (!skipErrors || errors.length > maxErrors)
207
280
  throw error;
281
+ lastProcessedLine = lineNumber;
208
282
  }
209
283
  }
210
- writeCheckpoint(checkpointFile, { processedLine: lineNumber, processed, projectId, sourceAgent, sessionId, completed: true });
284
+ writeCheckpoint(checkpointFile, {
285
+ processedLine: lastProcessedLine, processed, projectId, sourceAgent, sessionId, format, fileHash, processedPrefixHash: sourcePrefixHash(file, lastProcessedLine),
286
+ completed: !windowStopped, fileCompleted: !windowStopped, windowCompleted: true,
287
+ hasMore: windowStopped, nextLine: windowStopped ? lineNumber : undefined,
288
+ });
211
289
  const closureReceipts = args['seal-batch'] === true
212
290
  ? [...episodeIds].map((episodeId) => kernel.sealImportedEpisode(episodeId, { reason: 'cli_batch_boundary', force: args['force-seal'] === true }))
213
291
  : [];
214
292
  return {
215
293
  importId: `episode-import:${sourceAgent}:${sessionId}`,
216
- imported, duplicates, processed, errors, resumeFrom: lineNumber + 1, checkpointFile,
294
+ imported, duplicates, processed, errors, resumeFrom: lastProcessedLine + 1, checkpointFile,
217
295
  episodeIds: [...episodeIds], unassignedEventIds, ignoredEventIds, closureReceipts, dreamRan: false,
218
296
  };
219
297
  }
@@ -222,6 +300,10 @@ function writeCheckpoint(path, value) {
222
300
  writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
223
301
  renameSync(temporary, path);
224
302
  }
303
+ function sourcePrefixHash(file, lines) {
304
+ const text = readFileSync(file, 'utf8');
305
+ return createHash('sha256').update(text.split(/\r?\n/u).slice(0, Math.max(0, lines)).join('\n')).digest('hex');
306
+ }
225
307
  function stringArg(args, key) { return typeof args[key] === 'string' && args[key] ? args[key] : undefined; }
226
308
  function requiredArg(args, key) { const value = stringArg(args, key); if (!value)
227
309
  throw new Error(`--${key} is required`); return value; }
@@ -248,6 +330,13 @@ function dreamModeArg(value) {
248
330
  return value;
249
331
  throw new Error('mode must be micro, normal, or deep');
250
332
  }
333
+ function statusArg(value) {
334
+ if (!value)
335
+ return undefined;
336
+ if (value === 'open' || value === 'soft_sealed' || value === 'sealed')
337
+ return value;
338
+ throw new Error('status must be open, soft_sealed, or sealed');
339
+ }
251
340
  function episodeTypeArg(value) {
252
341
  if (!value)
253
342
  return undefined;
@@ -256,6 +345,8 @@ function episodeTypeArg(value) {
256
345
  throw new Error('invalid episode type');
257
346
  }
258
347
  function timeValue(value) {
348
+ if (value === undefined || value === null || value === '')
349
+ return undefined;
259
350
  if (typeof value === 'number' && Number.isFinite(value))
260
351
  return value;
261
352
  if (typeof value === 'string') {
@@ -263,6 +354,6 @@ function timeValue(value) {
263
354
  if (!Number.isNaN(parsed))
264
355
  return parsed;
265
356
  }
266
- return undefined;
357
+ throw new Error('timestamp must be a finite number or parseable date');
267
358
  }
268
359
  main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
@@ -336,22 +336,26 @@ async function recordRawImportedEvidence(kernel, projectId, envelope) {
336
336
  const threadId = sourceRef?.threadId || stringRecordField(metadata.threadId) || record.provenance.sourceId;
337
337
  const sessionId = sourceRef?.sessionId || stringRecordField(metadata.sessionId) || record.provenance.sourceId;
338
338
  const eventOrdinal = sourceRef?.eventOrdinal ?? sourceRef?.sourceOffset;
339
- const importAnchor = [
340
- record.provenance.sourceId,
341
- record.recordId,
342
- sourceRef?.sourcePath,
343
- sourceRef?.lineStart,
344
- sourceRef?.lineEnd,
345
- eventOrdinal,
346
- ].filter((item) => item !== undefined && item !== null && String(item).length > 0).join(':');
339
+ const allowNonUserEpisodeStart = metadata.importedSummarySupport === true || record.provenance.reliabilityClass === 'imported_summary';
340
+ const importAnchor = createHash('sha256').update(JSON.stringify({
341
+ sourceId: record.provenance.sourceId,
342
+ recordId: record.recordId ?? null,
343
+ sourcePath: sourceRef?.sourcePath ?? null,
344
+ lineStart: sourceRef?.lineStart ?? null,
345
+ lineEnd: sourceRef?.lineEnd ?? null,
346
+ eventOrdinal: eventOrdinal ?? null,
347
+ })).digest('hex');
348
+ const contentHash = createHash('sha256').update(record.text).digest('hex');
347
349
  const existing = findImportedRawAnchor(kernel, {
348
350
  projectId,
349
351
  threadId,
350
352
  sourceId: record.provenance.sourceId,
351
353
  importAnchor,
352
- contentHash: createHash('sha256').update(record.text).digest('hex'),
354
+ contentHash,
353
355
  });
354
356
  if (existing) {
357
+ if (existing.contentHash !== contentHash)
358
+ throw new Error(`import_anchor_content_conflict:${importAnchor}`);
355
359
  let link = kernel.episodeStore.getEventLink(existing.eventId);
356
360
  if (!link && !kernel.episodeStore.hasEventDisposition(existing.eventId)) {
357
361
  await kernel.assembleEpisodeTurnAsync([existing], {
@@ -359,6 +363,7 @@ async function recordRawImportedEvidence(kernel, projectId, envelope) {
359
363
  sessionId,
360
364
  sourceAgent: record.provenance.sourceType || record.provenance.sourceId,
361
365
  now: record.timestamp,
366
+ allowNonUserEpisodeStart,
362
367
  });
363
368
  link = kernel.episodeStore.getEventLink(existing.eventId);
364
369
  }
@@ -371,6 +376,8 @@ async function recordRawImportedEvidence(kernel, projectId, envelope) {
371
376
  sessionId,
372
377
  turnId: sourceRef?.turnId || record.turnId || record.recordId,
373
378
  turnSeq: sourceRef?.turnSeq,
379
+ localDate: localDateFromTimestamp(record.timestamp),
380
+ localDateSource: 'generated_utc',
374
381
  role,
375
382
  rawEventType: 'message',
376
383
  content: record.text,
@@ -400,19 +407,29 @@ async function recordRawImportedEvidence(kernel, projectId, envelope) {
400
407
  sessionId,
401
408
  sourceAgent: record.provenance.sourceType || record.provenance.sourceId,
402
409
  now: record.timestamp,
410
+ allowNonUserEpisodeStart,
403
411
  });
404
412
  return { event, created: true, episodeId: assembly.episode?.episodeId };
405
413
  }
406
414
  function findImportedRawAnchor(kernel, input) {
407
- return kernel.getThreadEvents(input.threadId, { projectId: input.projectId }).find((event) => {
415
+ const indexed = kernel.eventStore.findImportedEventAnchor(input.projectId, input.sourceId, input.importAnchor);
416
+ if (indexed)
417
+ return indexed;
418
+ return kernel.getThreadEvents(input.threadId, { projectId: input.projectId, limit: 10_000 }).find((event) => {
408
419
  const payload = event.payload;
409
420
  return event.sourceId === input.sourceId
410
- && (payload.metadata?.importAnchor === input.importAnchor || event.contentHash === input.contentHash);
421
+ && payload.metadata?.importAnchor === input.importAnchor;
411
422
  });
412
423
  }
413
424
  function stringRecordField(value) {
414
425
  return typeof value === 'string' && value.length > 0 ? value : undefined;
415
426
  }
427
+ function localDateFromTimestamp(timestamp) {
428
+ if (typeof timestamp !== 'number' || !Number.isFinite(timestamp))
429
+ return undefined;
430
+ const date = new Date(timestamp);
431
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString().slice(0, 10);
432
+ }
416
433
  function openKernel(args, workspaceRoot) {
417
434
  const explicitDb = stringArg(args, 'db');
418
435
  if (explicitDb) {
@@ -1,6 +1,7 @@
1
1
  import type { MemoryKernelOptions } from '../factory.js';
2
2
  import { ModelRegistry } from '../models/ModelRegistry.js';
3
3
  import { type ConfigDiagnosticLike } from './VectorDimension.js';
4
+ export type { ConfigDiagnosticLike } from './VectorDimension.js';
4
5
  export type CogmemConfigKind = 'toml' | 'missing';
5
6
  export type EnvLike = Record<string, string | undefined>;
6
7
  export interface CogmemConfigResolution {