cogmem 3.7.2 → 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 +6 -0
- package/MEMORY_MODEL.md +1 -1
- package/README.md +5 -4
- package/RELEASE_CHECKLIST.md +5 -5
- 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/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/types/ExtensionPoints.d.ts +3 -1
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 3.7.2
|
|
4
10
|
|
|
5
11
|
- Fixed Memory Atlas `graph-explore` edge projection so selected action, topic, and ordinary memory edges are merged before the final stable edge limit.
|
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
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
package/RELEASE_CHECKLIST.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# cogmem 3.7.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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.
|
|
902
|
+
if (this.ownsDb)
|
|
903
|
+
this.db.close();
|
|
896
904
|
this.closed = true;
|
|
897
905
|
}
|
|
898
906
|
}
|
package/dist/bin/episode.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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, {
|
|
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:
|
|
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
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
record.
|
|
342
|
-
|
|
343
|
-
sourceRef?.
|
|
344
|
-
sourceRef?.
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
&&
|
|
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 {
|
|
@@ -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
|
+
}
|