@remnic/core 9.3.653 → 9.3.654
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/dist/access-cli.js +17 -17
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +10 -10
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +9 -9
- package/dist/access-schema.d.ts +12 -12
- package/dist/{access-service-CdJFd3_b.d.ts → access-service-C8A5hoXJ.d.ts} +11 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +8 -8
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-KJDKZVF3.js → chunk-2DSTAWNZ.js} +3 -3
- package/dist/chunk-3RACUBII.js +212 -0
- package/dist/chunk-3RACUBII.js.map +1 -0
- package/dist/{chunk-Y7NWBBHV.js → chunk-6CVI6BP6.js} +2 -2
- package/dist/{chunk-R3PQUPQ4.js → chunk-6IMKOIZ6.js} +85 -3
- package/dist/chunk-6IMKOIZ6.js.map +1 -0
- package/dist/{chunk-WTI35CVJ.js → chunk-BJA6DQOC.js} +5 -5
- package/dist/{chunk-GI45G4BK.js → chunk-BP2EV6W5.js} +3 -3
- package/dist/{chunk-WLGE6KEO.js → chunk-DBM2BD22.js} +3 -3
- package/dist/{chunk-IENGGY2C.js → chunk-ENV6RDTD.js} +2 -2
- package/dist/{chunk-BEMWL2FZ.js → chunk-FVRBLJP6.js} +2 -2
- package/dist/{chunk-H3PHZLMF.js → chunk-GKKAXVAJ.js} +20 -11
- package/dist/chunk-GKKAXVAJ.js.map +1 -0
- package/dist/{chunk-NOBL7OUP.js → chunk-GPW2E4LN.js} +12 -5
- package/dist/{chunk-NOBL7OUP.js.map → chunk-GPW2E4LN.js.map} +1 -1
- package/dist/{chunk-KWM33SPU.js → chunk-JMQSYGXS.js} +2 -2
- package/dist/{chunk-QQHIQ7JD.js → chunk-JYN7QNTA.js} +87 -18
- package/dist/chunk-JYN7QNTA.js.map +1 -0
- package/dist/{chunk-AJE7FJVE.js → chunk-K6X553JB.js} +2 -2
- package/dist/{chunk-E3J6O6N7.js → chunk-LJCEWTG3.js} +19 -8
- package/dist/{chunk-E3J6O6N7.js.map → chunk-LJCEWTG3.js.map} +1 -1
- package/dist/{chunk-EW52H5EM.js → chunk-NAZWHTYV.js} +12 -5
- package/dist/chunk-NAZWHTYV.js.map +1 -0
- package/dist/{chunk-XMN6MMTU.js → chunk-NCGWXCSW.js} +2 -2
- package/dist/{chunk-C43KEWEV.js → chunk-NE2JBMLN.js} +1 -1
- package/dist/chunk-NE2JBMLN.js.map +1 -0
- package/dist/{chunk-SPMZZUEJ.js → chunk-OL2364SB.js} +2020 -368
- package/dist/chunk-OL2364SB.js.map +1 -0
- package/dist/{chunk-JF7SFXTG.js → chunk-QKK64Z6M.js} +2 -2
- package/dist/{chunk-IVYSVAC6.js → chunk-QW6JZO5P.js} +2 -2
- package/dist/{chunk-EHQLDFSH.js → chunk-RGPUQ66K.js} +2 -2
- package/dist/{chunk-CFOCZPIQ.js → chunk-T2C6QJG2.js} +2 -2
- package/dist/{chunk-V4UDXYGG.js → chunk-XWQ6ERUG.js} +2 -2
- package/dist/{chunk-BNFRL6QW.js → chunk-Y2RIIF6H.js} +2 -2
- package/dist/{chunk-C63WC454.js → chunk-YLZLPVKK.js} +22 -1
- package/dist/chunk-YLZLPVKK.js.map +1 -0
- package/dist/{chunk-RZOBQ23O.js → chunk-Z5MQI7K2.js} +2 -2
- package/dist/{chunk-PRQXUSQV.js → chunk-ZCORQM74.js} +2 -2
- package/dist/{cli-DDo7Qgs-.d.ts → cli-uQgvDFNE.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +22 -22
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +19 -1
- package/dist/contradiction/index.js +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- package/dist/explicit-capture.js +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +30 -28
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +52 -3
- package/dist/namespaces/storage.js +9 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +7 -7
- package/dist/{orchestrator-8fTZsa0y.d.ts → orchestrator-B4Y4sWQH.d.ts} +503 -3
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +13 -13
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/{resolution-3SAP4SH2.js → resolution-IDTEBJFS.js} +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DKdYzQOg.d.ts → semantic-consolidation-BKd0Pype.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/{types-D8yUmSik.d.ts → types-BgChEr0M.d.ts} +11 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +1 -1
- package/src/access-http.ts +7 -0
- package/src/access-mcp.ts +7 -0
- package/src/access-service.ts +12 -0
- package/src/cli.ts +104 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +29 -0
- package/src/contradiction/contradiction.test.ts +284 -0
- package/src/contradiction/resolution.ts +151 -4
- package/src/explicit-capture.ts +31 -10
- package/src/index.ts +10 -0
- package/src/namespaces/catalog.test.ts +3356 -0
- package/src/namespaces/catalog.ts +2123 -0
- package/src/namespaces/storage.ts +210 -30
- package/src/orchestrator-flush.test.ts +300 -0
- package/src/orchestrator.ts +851 -240
- package/src/types.ts +11 -0
- package/dist/chunk-C43KEWEV.js.map +0 -1
- package/dist/chunk-C63WC454.js.map +0 -1
- package/dist/chunk-EW52H5EM.js.map +0 -1
- package/dist/chunk-H3PHZLMF.js.map +0 -1
- package/dist/chunk-ORGWWNJG.js +0 -131
- package/dist/chunk-ORGWWNJG.js.map +0 -1
- package/dist/chunk-QQHIQ7JD.js.map +0 -1
- package/dist/chunk-R3PQUPQ4.js.map +0 -1
- package/dist/chunk-SPMZZUEJ.js.map +0 -1
- /package/dist/{chunk-KJDKZVF3.js.map → chunk-2DSTAWNZ.js.map} +0 -0
- /package/dist/{chunk-Y7NWBBHV.js.map → chunk-6CVI6BP6.js.map} +0 -0
- /package/dist/{chunk-WTI35CVJ.js.map → chunk-BJA6DQOC.js.map} +0 -0
- /package/dist/{chunk-GI45G4BK.js.map → chunk-BP2EV6W5.js.map} +0 -0
- /package/dist/{chunk-WLGE6KEO.js.map → chunk-DBM2BD22.js.map} +0 -0
- /package/dist/{chunk-IENGGY2C.js.map → chunk-ENV6RDTD.js.map} +0 -0
- /package/dist/{chunk-BEMWL2FZ.js.map → chunk-FVRBLJP6.js.map} +0 -0
- /package/dist/{chunk-KWM33SPU.js.map → chunk-JMQSYGXS.js.map} +0 -0
- /package/dist/{chunk-AJE7FJVE.js.map → chunk-K6X553JB.js.map} +0 -0
- /package/dist/{chunk-XMN6MMTU.js.map → chunk-NCGWXCSW.js.map} +0 -0
- /package/dist/{chunk-JF7SFXTG.js.map → chunk-QKK64Z6M.js.map} +0 -0
- /package/dist/{chunk-IVYSVAC6.js.map → chunk-QW6JZO5P.js.map} +0 -0
- /package/dist/{chunk-EHQLDFSH.js.map → chunk-RGPUQ66K.js.map} +0 -0
- /package/dist/{chunk-CFOCZPIQ.js.map → chunk-T2C6QJG2.js.map} +0 -0
- /package/dist/{chunk-V4UDXYGG.js.map → chunk-XWQ6ERUG.js.map} +0 -0
- /package/dist/{chunk-BNFRL6QW.js.map → chunk-Y2RIIF6H.js.map} +0 -0
- /package/dist/{chunk-RZOBQ23O.js.map → chunk-Z5MQI7K2.js.map} +0 -0
- /package/dist/{chunk-PRQXUSQV.js.map → chunk-ZCORQM74.js.map} +0 -0
- /package/dist/{resolution-3SAP4SH2.js.map → resolution-IDTEBJFS.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { createHash, randomBytes } from "node:crypto";
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import {
|
|
11
11
|
mkdir,
|
|
12
12
|
readdir,
|
|
@@ -297,8 +297,20 @@ import {
|
|
|
297
297
|
type ConversationIndexBackendInspection,
|
|
298
298
|
type ConversationQmdRuntime,
|
|
299
299
|
} from "./conversation-index/backend.js";
|
|
300
|
-
import {
|
|
301
|
-
|
|
300
|
+
import {
|
|
301
|
+
NamespaceStorageRouter,
|
|
302
|
+
resolveNamespaceStorageRoot,
|
|
303
|
+
} from "./namespaces/storage.js";
|
|
304
|
+
import {
|
|
305
|
+
NamespaceCatalog,
|
|
306
|
+
hasMemoryData,
|
|
307
|
+
type NamespaceRecord,
|
|
308
|
+
} from "./namespaces/catalog.js";
|
|
309
|
+
import {
|
|
310
|
+
namespaceIdentityFromToken,
|
|
311
|
+
namespaceIdentityToken,
|
|
312
|
+
normalizeNamespaceIdentity,
|
|
313
|
+
} from "./namespaces/identity.js";
|
|
302
314
|
import {
|
|
303
315
|
canReadNamespace,
|
|
304
316
|
defaultNamespaceForPrincipal,
|
|
@@ -327,6 +339,7 @@ import { parseFlexibleIsoTimestamp } from "./utils/iso-timestamp.js";
|
|
|
327
339
|
import { TierMigrationExecutor } from "./tier-migration.js";
|
|
328
340
|
import { decideTierTransition, type MemoryTier } from "./tier-routing.js";
|
|
329
341
|
import {
|
|
342
|
+
isSafeRouteNamespace,
|
|
330
343
|
selectRouteRule,
|
|
331
344
|
type RouteRule,
|
|
332
345
|
type RoutingEngineOptions,
|
|
@@ -1757,6 +1770,10 @@ export function resolvePersistedMemoryRelativePath(options: {
|
|
|
1757
1770
|
export class Orchestrator {
|
|
1758
1771
|
readonly storage: StorageManager;
|
|
1759
1772
|
private readonly storageRouter: NamespaceStorageRouter;
|
|
1773
|
+
/** Rebuildable namespace catalog (issue #1499). Inert unless namespaces enabled. */
|
|
1774
|
+
readonly namespaceCatalog: NamespaceCatalog;
|
|
1775
|
+
private readonly namespaceStorageDirHints = new Map<string, Set<string>>();
|
|
1776
|
+
private namespaceStorageDirHintsLoaded = false;
|
|
1760
1777
|
private readonly namespaceSearchRouter: NamespaceSearchRouter;
|
|
1761
1778
|
qmd: SearchBackend;
|
|
1762
1779
|
private readonly conversationQmd?: ConversationQmdRuntime;
|
|
@@ -2245,6 +2262,220 @@ export class Orchestrator {
|
|
|
2245
2262
|
);
|
|
2246
2263
|
}
|
|
2247
2264
|
|
|
2265
|
+
private rememberNamespaceStorageDirHint(namespace: string, storageDir?: string): void {
|
|
2266
|
+
if (!this.config.namespacesEnabled || !storageDir) return;
|
|
2267
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
2268
|
+
if (!ns) return;
|
|
2269
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
2270
|
+
if (ns !== defaultNs && !isSafeRouteNamespace(ns)) return;
|
|
2271
|
+
|
|
2272
|
+
if (!this.storageDirMatchesNamespaceHint(ns, storageDir)) return;
|
|
2273
|
+
|
|
2274
|
+
const resolvedStorageDir = path.resolve(storageDir);
|
|
2275
|
+
let hints = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
2276
|
+
if (!hints) {
|
|
2277
|
+
hints = new Set<string>();
|
|
2278
|
+
this.namespaceStorageDirHints.set(resolvedStorageDir, hints);
|
|
2279
|
+
}
|
|
2280
|
+
hints.add(ns);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
private storageDirMatchesNamespaceHint(namespace: string, storageDir: string): boolean {
|
|
2284
|
+
const ns = normalizeNamespaceIdentity(namespace);
|
|
2285
|
+
if (!ns) return false;
|
|
2286
|
+
|
|
2287
|
+
const resolvedStorageDir = path.resolve(storageDir);
|
|
2288
|
+
const resolvedMemoryDir = path.resolve(this.config.memoryDir);
|
|
2289
|
+
const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
|
|
2290
|
+
if (resolvedStorageDir === resolvedMemoryDir) return ns === defaultNs;
|
|
2291
|
+
|
|
2292
|
+
const resolvedNamespacesDir = path.join(resolvedMemoryDir, "namespaces");
|
|
2293
|
+
if (!isPathInsideStorageRoot(resolvedNamespacesDir, resolvedStorageDir)) return false;
|
|
2294
|
+
|
|
2295
|
+
const rawRoot = path.resolve(resolvedNamespacesDir, ns);
|
|
2296
|
+
const tokenRoot = path.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
|
|
2297
|
+
return resolvedStorageDir === rawRoot || resolvedStorageDir === tokenRoot;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
private namespaceStorageDirHintOwnershipRank(
|
|
2301
|
+
record: { namespace: string },
|
|
2302
|
+
resolvedStorageDir: string,
|
|
2303
|
+
configured: Set<string>,
|
|
2304
|
+
): number {
|
|
2305
|
+
if (resolvedStorageDir === path.resolve(this.config.memoryDir)) {
|
|
2306
|
+
return record.namespace === normalizeNamespaceIdentity(this.config.defaultNamespace)
|
|
2307
|
+
? 0
|
|
2308
|
+
: 3;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
const leaf = path.basename(resolvedStorageDir);
|
|
2312
|
+
const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
|
|
2313
|
+
if (tokenOwnsRoot && configured.has(record.namespace)) return 0;
|
|
2314
|
+
if (record.namespace === leaf) return 1;
|
|
2315
|
+
if (tokenOwnsRoot) return 2;
|
|
2316
|
+
return 3;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
private preferNamespaceStorageDirHintOwner(
|
|
2320
|
+
current: { namespace: string; identityToken: string; storageDir: string },
|
|
2321
|
+
candidate: { namespace: string; identityToken: string; storageDir: string },
|
|
2322
|
+
resolvedStorageDir: string,
|
|
2323
|
+
configured: Set<string>,
|
|
2324
|
+
): { namespace: string; identityToken: string; storageDir: string } {
|
|
2325
|
+
const currentRank = this.namespaceStorageDirHintOwnershipRank(
|
|
2326
|
+
current,
|
|
2327
|
+
resolvedStorageDir,
|
|
2328
|
+
configured,
|
|
2329
|
+
);
|
|
2330
|
+
const candidateRank = this.namespaceStorageDirHintOwnershipRank(
|
|
2331
|
+
candidate,
|
|
2332
|
+
resolvedStorageDir,
|
|
2333
|
+
configured,
|
|
2334
|
+
);
|
|
2335
|
+
if (candidateRank < currentRank) return candidate;
|
|
2336
|
+
if (candidateRank > currentRank) return current;
|
|
2337
|
+
|
|
2338
|
+
const byName = candidate.namespace.localeCompare(current.namespace);
|
|
2339
|
+
if (byName < 0) return candidate;
|
|
2340
|
+
if (byName > 0) return current;
|
|
2341
|
+
return candidate.identityToken.localeCompare(current.identityToken) < 0
|
|
2342
|
+
? candidate
|
|
2343
|
+
: current;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
private loadNamespaceStorageDirHintsFromCatalog(): void {
|
|
2347
|
+
if (this.namespaceStorageDirHintsLoaded || !this.namespaceCatalog.enabled) return;
|
|
2348
|
+
this.namespaceStorageDirHintsLoaded = true;
|
|
2349
|
+
const catalogPath = path.join(this.config.memoryDir, "state", "namespaces.jsonl");
|
|
2350
|
+
if (!existsSync(catalogPath)) return;
|
|
2351
|
+
|
|
2352
|
+
let body: string;
|
|
2353
|
+
try {
|
|
2354
|
+
body = readFileSync(catalogPath, "utf8");
|
|
2355
|
+
} catch {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
const compactedByNamespace = new Map<
|
|
2360
|
+
string,
|
|
2361
|
+
{ namespace: string; identityToken: string; storageDir: string }
|
|
2362
|
+
>();
|
|
2363
|
+
for (const line of body.split(/\r?\n/)) {
|
|
2364
|
+
const trimmed = line.trim();
|
|
2365
|
+
if (!trimmed) continue;
|
|
2366
|
+
try {
|
|
2367
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
2368
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
2369
|
+
const record = parsed as Record<string, unknown>;
|
|
2370
|
+
if (
|
|
2371
|
+
typeof record.namespace !== "string" ||
|
|
2372
|
+
typeof record.storageDir !== "string" ||
|
|
2373
|
+
typeof record.identityToken !== "string"
|
|
2374
|
+
) {
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
const namespace = normalizeNamespaceIdentity(record.namespace);
|
|
2378
|
+
if (!namespace || record.identityToken !== namespaceIdentityToken(namespace)) continue;
|
|
2379
|
+
compactedByNamespace.set(namespace, {
|
|
2380
|
+
namespace,
|
|
2381
|
+
identityToken: record.identityToken,
|
|
2382
|
+
storageDir: record.storageDir,
|
|
2383
|
+
});
|
|
2384
|
+
} catch {
|
|
2385
|
+
// Catalog hints are best-effort. The catalog reader still owns full recovery.
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const configured = new Set(
|
|
2390
|
+
this.configuredNamespaces().map((namespace) => normalizeNamespaceIdentity(namespace)),
|
|
2391
|
+
);
|
|
2392
|
+
const preferredByStorageDir = new Map<
|
|
2393
|
+
string,
|
|
2394
|
+
{ namespace: string; identityToken: string; storageDir: string }
|
|
2395
|
+
>();
|
|
2396
|
+
for (const record of compactedByNamespace.values()) {
|
|
2397
|
+
if (!this.storageDirMatchesNamespaceHint(record.namespace, record.storageDir)) {
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const resolvedStorageDir = path.resolve(record.storageDir);
|
|
2401
|
+
const current = preferredByStorageDir.get(resolvedStorageDir);
|
|
2402
|
+
preferredByStorageDir.set(
|
|
2403
|
+
resolvedStorageDir,
|
|
2404
|
+
current
|
|
2405
|
+
? this.preferNamespaceStorageDirHintOwner(
|
|
2406
|
+
current,
|
|
2407
|
+
record,
|
|
2408
|
+
resolvedStorageDir,
|
|
2409
|
+
configured,
|
|
2410
|
+
)
|
|
2411
|
+
: record,
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
for (const record of preferredByStorageDir.values()) {
|
|
2415
|
+
this.rememberNamespaceStorageDirHint(record.namespace, record.storageDir);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/**
|
|
2420
|
+
* Namespaces that QMD maintenance (update/embed) must cover: the CONFIGURED set
|
|
2421
|
+
* PLUS every dynamic namespace recorded in the catalog (NGnei, codex P2). An
|
|
2422
|
+
* extraction that writes to a coding-scoped/dynamic namespace (not in
|
|
2423
|
+
* defaultNamespace/sharedNamespace/namespacePolicies) is only made discoverable
|
|
2424
|
+
* via the catalog; if maintenance embeds only `configuredNamespaces()`, that
|
|
2425
|
+
* namespace's QMD collection is never updated/embedded after writes and
|
|
2426
|
+
* recall/search stays stale or empty until it is manually configured. We union in
|
|
2427
|
+
* the catalog's namespaces so maintenance keeps dynamic namespaces fresh.
|
|
2428
|
+
* `updateNamespaces`/`embedNamespaces` already trim, dedup, and skip
|
|
2429
|
+
* unavailable/missing collections, so extra names are filtered safely. A catalog
|
|
2430
|
+
* read failure must never break maintenance — fall back to the configured set.
|
|
2431
|
+
*/
|
|
2432
|
+
private async maintenanceNamespaces(): Promise<string[]> {
|
|
2433
|
+
const configured = this.configuredNamespaces();
|
|
2434
|
+
if (!this.namespaceCatalog.enabled) return configured;
|
|
2435
|
+
const configuredSet = new Set(configured);
|
|
2436
|
+
let cataloged: string[] = [];
|
|
2437
|
+
try {
|
|
2438
|
+
const records = await this.namespaceCatalog.listNamespaces();
|
|
2439
|
+
const safeRecords = await Promise.all(
|
|
2440
|
+
records.map(async (record) => {
|
|
2441
|
+
const namespace = record.namespace.trim();
|
|
2442
|
+
if (!namespace || configuredSet.has(namespace)) return null;
|
|
2443
|
+
return (await this.isCatalogedMaintenanceRootLive(record))
|
|
2444
|
+
? namespace
|
|
2445
|
+
: null;
|
|
2446
|
+
}),
|
|
2447
|
+
);
|
|
2448
|
+
cataloged = safeRecords.filter(
|
|
2449
|
+
(namespace): namespace is string => namespace !== null,
|
|
2450
|
+
);
|
|
2451
|
+
} catch {
|
|
2452
|
+
// Best-effort: a catalog read failure must not break QMD maintenance.
|
|
2453
|
+
cataloged = [];
|
|
2454
|
+
}
|
|
2455
|
+
return Array.from(
|
|
2456
|
+
new Set(
|
|
2457
|
+
[...configured, ...cataloged].map((value) => value.trim()).filter(Boolean),
|
|
2458
|
+
),
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
private async isCatalogedMaintenanceRootLive(
|
|
2463
|
+
record: NamespaceRecord,
|
|
2464
|
+
): Promise<boolean> {
|
|
2465
|
+
if (typeof record.storageDir !== "string" || record.storageDir.length === 0) {
|
|
2466
|
+
return false;
|
|
2467
|
+
}
|
|
2468
|
+
try {
|
|
2469
|
+
const liveRoot = await resolveNamespaceStorageRoot(this.config, record.namespace);
|
|
2470
|
+
if (path.resolve(liveRoot) !== path.resolve(record.storageDir)) {
|
|
2471
|
+
return false;
|
|
2472
|
+
}
|
|
2473
|
+
return hasMemoryData(liveRoot);
|
|
2474
|
+
} catch {
|
|
2475
|
+
return false;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2248
2479
|
private buildConfiguredQmdSearchOptions(
|
|
2249
2480
|
queryText: string,
|
|
2250
2481
|
): SearchQueryOptions | undefined {
|
|
@@ -2350,7 +2581,21 @@ export class Orchestrator {
|
|
|
2350
2581
|
storageDir: config.profilingStorageDir || path.join(config.memoryDir, "profiling"),
|
|
2351
2582
|
maxTraces: config.profilingMaxTraces,
|
|
2352
2583
|
});
|
|
2353
|
-
|
|
2584
|
+
// Namespace catalog (issue #1499): downstream, rebuildable metadata index.
|
|
2585
|
+
// Inert unless namespacesEnabled is true. Storage resolution registers
|
|
2586
|
+
// namespaces via the router's onResolve hook; the touch is best-effort and
|
|
2587
|
+
// a catalog write failure never affects storage resolution.
|
|
2588
|
+
this.namespaceCatalog = new NamespaceCatalog(config);
|
|
2589
|
+
this.storageRouter = new NamespaceStorageRouter(config, {
|
|
2590
|
+
// Return the registration promise (round 6, codex P2 — NEFoX) so the
|
|
2591
|
+
// router's resolve-hook dedup only marks a namespace notified when the
|
|
2592
|
+
// catalog actually APPENDED. A dropped append (rebuild-lock timeout) or a
|
|
2593
|
+
// failure resolves to `false`/rejects, so the next `storageFor` retries.
|
|
2594
|
+
onResolve: (namespace, storageDir) => {
|
|
2595
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
2596
|
+
return this.namespaceCatalog.registerResolved(namespace, storageDir);
|
|
2597
|
+
},
|
|
2598
|
+
});
|
|
2354
2599
|
this.namespaceSearchRouter = new NamespaceSearchRouter(
|
|
2355
2600
|
config,
|
|
2356
2601
|
this.storageRouter,
|
|
@@ -2839,6 +3084,14 @@ export class Orchestrator {
|
|
|
2839
3084
|
await sm.ensureDirectories();
|
|
2840
3085
|
await sm.loadAliases().catch(() => undefined);
|
|
2841
3086
|
}
|
|
3087
|
+
// Explicitly seed the catalog with all configured namespaces at startup
|
|
3088
|
+
// (round 6, cursor Medium — NBLlR). The storageFor loop above fires the
|
|
3089
|
+
// router's onResolve hook, but a warm router cache (reused instance
|
|
3090
|
+
// across stop/start) can skip onResolve, leaving policy namespaces absent
|
|
3091
|
+
// from the live catalog until an operator runs `rebuild --apply`. This
|
|
3092
|
+
// call is cheap, idempotent, and best-effort: a catalog failure must
|
|
3093
|
+
// never break initialization (rule #13, #40).
|
|
3094
|
+
await this.namespaceCatalog.registerConfiguredNamespaces().catch(() => undefined);
|
|
2842
3095
|
}
|
|
2843
3096
|
await this.relevance.load();
|
|
2844
3097
|
await this.negatives.load();
|
|
@@ -2907,8 +3160,15 @@ export class Orchestrator {
|
|
|
2907
3160
|
const available = await this.qmd.probe();
|
|
2908
3161
|
if (available) {
|
|
2909
3162
|
log.info(`Search backend: available ${this.qmd.debugStatus()}`);
|
|
3163
|
+
// Ensure collections at startup for the catalog-union namespace set, not
|
|
3164
|
+
// just the configured set (issue #1499 sweep, same class as NHZEV): a
|
|
3165
|
+
// dynamic namespace that exists only in the persisted catalog must have
|
|
3166
|
+
// its QMD collection checked/ensured on boot so recall against it works
|
|
3167
|
+
// after a restart. `registerConfiguredNamespaces()` already seeded the
|
|
3168
|
+
// catalog above, so `maintenanceNamespaces()` is readable here; it falls
|
|
3169
|
+
// back to the configured set on any catalog read failure.
|
|
2910
3170
|
const namespaces = this.config.namespacesEnabled
|
|
2911
|
-
? this.
|
|
3171
|
+
? await this.maintenanceNamespaces()
|
|
2912
3172
|
: [this.config.defaultNamespace];
|
|
2913
3173
|
const states = await Promise.all(
|
|
2914
3174
|
namespaces.map(async (namespace) => {
|
|
@@ -3031,8 +3291,12 @@ export class Orchestrator {
|
|
|
3031
3291
|
try {
|
|
3032
3292
|
log.info("QMD startup sync: updating index to match current disk state");
|
|
3033
3293
|
if (this.config.namespacesEnabled) {
|
|
3294
|
+
// Cover cataloged dynamic namespaces at startup too (NHZEV, codex P2):
|
|
3295
|
+
// a dynamic namespace written before a daemon restart must be synced on
|
|
3296
|
+
// boot, not only by the debounced runQmdMaintenance() path. Same union +
|
|
3297
|
+
// catalog-read-failure fallback as runQmdMaintenance.
|
|
3034
3298
|
await this.namespaceSearchRouter.updateNamespaces(
|
|
3035
|
-
this.
|
|
3299
|
+
await this.maintenanceNamespaces(),
|
|
3036
3300
|
{ signal },
|
|
3037
3301
|
);
|
|
3038
3302
|
} else {
|
|
@@ -3310,9 +3574,16 @@ export class Orchestrator {
|
|
|
3310
3574
|
this.namespaceSearchRouter.clearCache();
|
|
3311
3575
|
}
|
|
3312
3576
|
|
|
3313
|
-
// Ensure collections — namespace-aware when enabled
|
|
3577
|
+
// Ensure collections — namespace-aware when enabled.
|
|
3578
|
+
// Use the catalog-union namespace set (issue #1499 sweep, same class as
|
|
3579
|
+
// NHZEV): this is the QMD startup-recovery sync that ensures collections AND
|
|
3580
|
+
// runs `updateNamespaces(...)` below over the SAME `namespaces` set. A dynamic
|
|
3581
|
+
// namespace that exists only in the persisted catalog must be ensured and
|
|
3582
|
+
// re-synced here too, otherwise after a backend-was-unavailable-at-boot
|
|
3583
|
+
// recovery its collection stays stale. Falls back to the configured set on any
|
|
3584
|
+
// catalog read failure.
|
|
3314
3585
|
const namespaces = this.config.namespacesEnabled
|
|
3315
|
-
? this.
|
|
3586
|
+
? await this.maintenanceNamespaces()
|
|
3316
3587
|
: [this.config.defaultNamespace];
|
|
3317
3588
|
|
|
3318
3589
|
const states = await Promise.all(
|
|
@@ -3934,6 +4205,7 @@ export class Orchestrator {
|
|
|
3934
4205
|
}
|
|
3935
4206
|
|
|
3936
4207
|
for (const cluster of clusters) {
|
|
4208
|
+
let canonicalWriteCompleted = false;
|
|
3937
4209
|
try {
|
|
3938
4210
|
// Operator-aware prompt (issue #561 PR 3): ask the LLM to pick the
|
|
3939
4211
|
// SPLIT/MERGE/UPDATE operator alongside the canonical output. Falls
|
|
@@ -4057,6 +4329,7 @@ export class Orchestrator {
|
|
|
4057
4329
|
derivedVia: operator,
|
|
4058
4330
|
},
|
|
4059
4331
|
);
|
|
4332
|
+
canonicalWriteCompleted = true;
|
|
4060
4333
|
|
|
4061
4334
|
result.memoriesConsolidated++;
|
|
4062
4335
|
|
|
@@ -4090,17 +4363,27 @@ export class Orchestrator {
|
|
|
4090
4363
|
this.contentHashIndex.remove(m.content);
|
|
4091
4364
|
}
|
|
4092
4365
|
}
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
m.path
|
|
4102
|
-
m.frontmatter
|
|
4103
|
-
|
|
4366
|
+
// Best-effort index cleanup: a failure here (e.g. on-disk index save
|
|
4367
|
+
// under disk-full) must NOT abort the archival loop and thereby skip
|
|
4368
|
+
// the catalog write touch below for an already-durable canonical write
|
|
4369
|
+
// (kilo NV0mh).
|
|
4370
|
+
try {
|
|
4371
|
+
await this.embeddingFallback.removeFromIndex(m.frontmatter.id);
|
|
4372
|
+
if (
|
|
4373
|
+
this.config.queryAwareIndexingEnabled &&
|
|
4374
|
+
m.path &&
|
|
4375
|
+
m.frontmatter?.created
|
|
4376
|
+
) {
|
|
4377
|
+
deindexMemory(
|
|
4378
|
+
targetStorage.dir,
|
|
4379
|
+
m.path,
|
|
4380
|
+
m.frontmatter.created,
|
|
4381
|
+
m.frontmatter.tags ?? [],
|
|
4382
|
+
);
|
|
4383
|
+
}
|
|
4384
|
+
} catch (cleanupErr) {
|
|
4385
|
+
log.warn(
|
|
4386
|
+
`[semantic-consolidation] index cleanup failed (non-fatal): ${cleanupErr}`,
|
|
4104
4387
|
);
|
|
4105
4388
|
}
|
|
4106
4389
|
result.memoriesArchived++;
|
|
@@ -4115,6 +4398,21 @@ export class Orchestrator {
|
|
|
4115
4398
|
`[semantic-consolidation] cluster processing failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
4116
4399
|
);
|
|
4117
4400
|
result.errors++;
|
|
4401
|
+
} finally {
|
|
4402
|
+
if (canonicalWriteCompleted) {
|
|
4403
|
+
// Catalog write touch (issue #1499 sweep): record after the canonical
|
|
4404
|
+
// write and, on the happy path, after archival of superseded cluster
|
|
4405
|
+
// memories, so `lastWriteAt` reflects every durable mutation in this
|
|
4406
|
+
// consolidation (cursor NUtCK). The `finally` also covers partial
|
|
4407
|
+
// failures where the canonical memory was written but a later archive
|
|
4408
|
+
// step throws and the cluster catch continues (codex NY-dK).
|
|
4409
|
+
// Best-effort; namespace decoded from the storage dir since this path
|
|
4410
|
+
// has no routed namespace name.
|
|
4411
|
+
this.markCatalogWrite(
|
|
4412
|
+
this.namespaceFromStorageDir(targetStorage.dir),
|
|
4413
|
+
targetStorage.dir,
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4118
4416
|
}
|
|
4119
4417
|
}
|
|
4120
4418
|
|
|
@@ -7194,6 +7492,22 @@ export class Orchestrator {
|
|
|
7194
7492
|
} else {
|
|
7195
7493
|
recallNamespaces = readableRecallNamespaces;
|
|
7196
7494
|
}
|
|
7495
|
+
// Catalog touch (issue #1499): record reads against the recalled namespaces
|
|
7496
|
+
// so the catalog reflects active read scopes. Best-effort, failure-tolerant.
|
|
7497
|
+
// Round 3 (codex P2): gate behind the no_recall guard — when the planner
|
|
7498
|
+
// selects `no_recall` retrieval is skipped entirely (see the early return at
|
|
7499
|
+
// `recallMode === "no_recall"` below), so marking every readable namespace as
|
|
7500
|
+
// read would falsely inflate `lastReadAt` / catalog recency.
|
|
7501
|
+
// Round 4 (codex P2): also skip when the effective memory result limit is
|
|
7502
|
+
// zero (`topK: 0`, a disabled/zero `memories` recall section, etc.). The QMD
|
|
7503
|
+
// path explicitly returns before searching when `recallResultLimit <= 0`, so
|
|
7504
|
+
// no namespace is actually read and the touch would be spurious.
|
|
7505
|
+
// NOTE: the catalog read touch is recorded LATER, immediately after the
|
|
7506
|
+
// Phase 1 `throwIfRecallAborted` gate (round 6, codex P2 / cursor Medium —
|
|
7507
|
+
// NDXHa/NDmle), so it fires only once retrieval is actually about to run.
|
|
7508
|
+
// Recording it here (recall entry) would set `lastReadAt` for recalls that
|
|
7509
|
+
// are aborted, error out, or short-circuit before any QMD/filesystem read.
|
|
7510
|
+
|
|
7197
7511
|
// Effective LCM read NAMESPACE SET (#1505 thread "Include coding fallback
|
|
7198
7512
|
// namespaces in LCM reads"). `observe` archives LCM / structured history
|
|
7199
7513
|
// under `${effectiveNamespace}:${sessionKey}` for whichever namespace was
|
|
@@ -7473,6 +7787,21 @@ export class Orchestrator {
|
|
|
7473
7787
|
// --- Phase 1: Launch ALL independent data fetches in parallel ---
|
|
7474
7788
|
throwIfRecallAborted(options.abortSignal);
|
|
7475
7789
|
|
|
7790
|
+
// Catalog read touch (issue #1499): record reads against the recalled
|
|
7791
|
+
// namespaces HERE — after the abort gate, immediately before retrieval
|
|
7792
|
+
// actually runs — so `lastReadAt` reflects a real read, not a recall that was
|
|
7793
|
+
// aborted/errored/short-circuited before reaching this point (round 3/4/6,
|
|
7794
|
+
// codex/cursor — no_recall, zero-limit, aborted, and pre-read-error cases).
|
|
7795
|
+
// `no_recall` already returned earlier, so it cannot reach here. Best-effort
|
|
7796
|
+
// and failure-tolerant.
|
|
7797
|
+
if (
|
|
7798
|
+
this.namespaceCatalog.enabled &&
|
|
7799
|
+
recallResultLimit > 0 &&
|
|
7800
|
+
!options.abortSignal?.aborted
|
|
7801
|
+
) {
|
|
7802
|
+
for (const ns of recallNamespaces) this.markCatalogRead(ns);
|
|
7803
|
+
}
|
|
7804
|
+
|
|
7476
7805
|
// 0. Shared context (v4.0, optional)
|
|
7477
7806
|
const sharedContextPromise = (async (): Promise<string | null> => {
|
|
7478
7807
|
if (
|
|
@@ -12523,6 +12852,9 @@ export class Orchestrator {
|
|
|
12523
12852
|
storage,
|
|
12524
12853
|
threadIdForExtraction,
|
|
12525
12854
|
{ sessionKey, principal, validAt: sourceValidAt },
|
|
12855
|
+
// Pass the KNOWN base namespace (NHIdx) so the catalog write touch records the
|
|
12856
|
+
// real namespace rather than a guess decoded from the storage dir.
|
|
12857
|
+
selfNamespace,
|
|
12526
12858
|
);
|
|
12527
12859
|
let postPersistMetadataFailed = false;
|
|
12528
12860
|
meta ??= await storage.loadMeta();
|
|
@@ -13004,25 +13336,28 @@ export class Orchestrator {
|
|
|
13004
13336
|
|
|
13005
13337
|
try {
|
|
13006
13338
|
if (this.config.namespacesEnabled) {
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
);
|
|
13339
|
+
// Include cataloged dynamic namespaces, not just the configured set
|
|
13340
|
+
// (NGnei) — resolve once and reuse for both update and embed.
|
|
13341
|
+
const maintenanceNamespaces = await this.maintenanceNamespaces();
|
|
13342
|
+
await this.namespaceSearchRouter.updateNamespaces(maintenanceNamespaces);
|
|
13343
|
+
const now = Date.now();
|
|
13344
|
+
if (
|
|
13345
|
+
this.config.qmdAutoEmbedEnabled &&
|
|
13346
|
+
now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs
|
|
13347
|
+
) {
|
|
13348
|
+
await this.namespaceSearchRouter.embedNamespaces(maintenanceNamespaces);
|
|
13349
|
+
this.lastQmdEmbedAtMs = now;
|
|
13350
|
+
}
|
|
13010
13351
|
} else {
|
|
13011
13352
|
await this.qmd.update();
|
|
13012
|
-
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
) {
|
|
13018
|
-
if (this.config.namespacesEnabled) {
|
|
13019
|
-
await this.namespaceSearchRouter.embedNamespaces(
|
|
13020
|
-
this.configuredNamespaces(),
|
|
13021
|
-
);
|
|
13022
|
-
} else {
|
|
13353
|
+
const now = Date.now();
|
|
13354
|
+
if (
|
|
13355
|
+
this.config.qmdAutoEmbedEnabled &&
|
|
13356
|
+
now - this.lastQmdEmbedAtMs >= this.config.qmdEmbedMinIntervalMs
|
|
13357
|
+
) {
|
|
13023
13358
|
await this.qmd.embed();
|
|
13359
|
+
this.lastQmdEmbedAtMs = now;
|
|
13024
13360
|
}
|
|
13025
|
-
this.lastQmdEmbedAtMs = now;
|
|
13026
13361
|
}
|
|
13027
13362
|
} finally {
|
|
13028
13363
|
this.qmdMaintenanceInFlight = false;
|
|
@@ -13037,6 +13372,7 @@ export class Orchestrator {
|
|
|
13037
13372
|
storage: StorageManager,
|
|
13038
13373
|
threadIdForExtraction?: string | null,
|
|
13039
13374
|
sourceContext?: { sessionKey?: string; principal?: string; validAt?: string },
|
|
13375
|
+
baseNamespace?: string,
|
|
13040
13376
|
): Promise<string[]> {
|
|
13041
13377
|
// Inline source attribution (issue #369). When enabled, every extracted
|
|
13042
13378
|
// fact is rewritten to carry a compact provenance tag inside its body so
|
|
@@ -13315,7 +13651,7 @@ export class Orchestrator {
|
|
|
13315
13651
|
// `createdAt` as the ordering anchor instead of the old fact's
|
|
13316
13652
|
// timestamp, ensuring supersession fires correctly even when
|
|
13317
13653
|
// the matching fact predates conflicting candidates.
|
|
13318
|
-
await applyTemporalSupersession({
|
|
13654
|
+
const hashDedupSupersession = await applyTemporalSupersession({
|
|
13319
13655
|
storage: sharedStorage,
|
|
13320
13656
|
newMemoryId: hashDedupMatchingFact.frontmatter.id,
|
|
13321
13657
|
entityRef: options.entityRef,
|
|
@@ -13324,6 +13660,19 @@ export class Orchestrator {
|
|
|
13324
13660
|
enabled: true,
|
|
13325
13661
|
useCallerTimestamp: true,
|
|
13326
13662
|
});
|
|
13663
|
+
// Catalog touch (issue #1499 — codex P2 NElSf): this dedup branch
|
|
13664
|
+
// returns WITHOUT reaching the post-write `markCatalogWrite` below,
|
|
13665
|
+
// but `applyTemporalSupersession` mutated the shared namespace
|
|
13666
|
+
// (it rewrote frontmatter to retire stale shared facts). When any
|
|
13667
|
+
// ids were actually superseded, the shared namespace changed, so we
|
|
13668
|
+
// must record the write — otherwise the shared record's
|
|
13669
|
+
// `lastWriteAt` stays stale and `writtenSince` maintenance / QMD
|
|
13670
|
+
// fanout skips the namespace after a supersession-only update.
|
|
13671
|
+
// Best-effort and failure-tolerant (markCatalogWrite swallows
|
|
13672
|
+
// errors); only touch when work happened to avoid spurious writes.
|
|
13673
|
+
if (hashDedupSupersession.supersededIds.length > 0) {
|
|
13674
|
+
this.markCatalogWrite(this.config.sharedNamespace, sharedStorage.dir);
|
|
13675
|
+
}
|
|
13327
13676
|
// Active matching fact exists — normal short-circuit is safe.
|
|
13328
13677
|
return;
|
|
13329
13678
|
}
|
|
@@ -13418,6 +13767,16 @@ export class Orchestrator {
|
|
|
13418
13767
|
);
|
|
13419
13768
|
}
|
|
13420
13769
|
}
|
|
13770
|
+
// Catalog touch (issue #1499, Issue B + ordering sweep): a shared-
|
|
13771
|
+
// namespace promotion is the ONLY write the shared namespace receives on
|
|
13772
|
+
// this path, so without this the shared record's lastWriteAt stays stale
|
|
13773
|
+
// and `writtenSince` filters / maintenance fanout skip it. Record AFTER
|
|
13774
|
+
// the promoted write and the shared temporal-supersession attempt so the
|
|
13775
|
+
// catalog timestamp never precedes a later durable frontmatter mutation in
|
|
13776
|
+
// the same promotion pass. The hot-path source-namespace touch uses a
|
|
13777
|
+
// different storage dir, so this does not double-count the source.
|
|
13778
|
+
// Best-effort and failure-tolerant — it must never crash the promotion.
|
|
13779
|
+
this.markCatalogWrite(this.config.sharedNamespace, sharedStorage.dir);
|
|
13421
13780
|
trackPersistedId(sharedStorage, promotedId, {
|
|
13422
13781
|
includeReturnedIds: false,
|
|
13423
13782
|
});
|
|
@@ -13778,6 +14137,19 @@ export class Orchestrator {
|
|
|
13778
14137
|
// affect both the dedup fingerprint and importance (issue #519 procedure routing).
|
|
13779
14138
|
let writeCategory = fact.category;
|
|
13780
14139
|
let targetStorage = storage;
|
|
14140
|
+
// Track the KNOWN target namespace NAME alongside targetStorage (round 6,
|
|
14141
|
+
// codex P2 — NCQI0). Re-deriving it from `targetStorage.dir` mangles a raw
|
|
14142
|
+
// namespace literally named like a canonical token (e.g. `ns-616c706861`
|
|
14143
|
+
// served from its legacy raw dir decodes to `alpha`). We seed it from the
|
|
14144
|
+
// EXPLICIT base namespace the caller used to obtain `storage` (NHIdx, codex
|
|
14145
|
+
// P2) — `selfNamespace`/`writeNamespaceOverride` — so the catalog write touch
|
|
14146
|
+
// records the real namespace, not a guess decoded from the directory. We only
|
|
14147
|
+
// fall back to decoding the dir when no base namespace was passed (legacy
|
|
14148
|
+
// callers). The EXPLICIT routed name (below) still overrides this verbatim.
|
|
14149
|
+
let targetNamespaceName =
|
|
14150
|
+
baseNamespace && baseNamespace.length > 0
|
|
14151
|
+
? baseNamespace
|
|
14152
|
+
: this.namespaceFromStorageDir(targetStorage.dir);
|
|
13781
14153
|
let routedRuleId: string | undefined;
|
|
13782
14154
|
let routedNamespaceExplicit = false;
|
|
13783
14155
|
if (routeRules.length > 0) {
|
|
@@ -13794,6 +14166,7 @@ export class Orchestrator {
|
|
|
13794
14166
|
targetStorage = await this.storageRouter.storageFor(
|
|
13795
14167
|
selected.target.namespace,
|
|
13796
14168
|
);
|
|
14169
|
+
targetNamespaceName = selected.target.namespace;
|
|
13797
14170
|
}
|
|
13798
14171
|
}
|
|
13799
14172
|
} catch (err) {
|
|
@@ -13823,6 +14196,7 @@ export class Orchestrator {
|
|
|
13823
14196
|
targetStorage = await this.storageRouter.storageFor(
|
|
13824
14197
|
this.config.sharedNamespace,
|
|
13825
14198
|
);
|
|
14199
|
+
targetNamespaceName = this.config.sharedNamespace;
|
|
13826
14200
|
log.debug(
|
|
13827
14201
|
`scope-routing: fact "${fact.content.slice(0, 60)}…" routed to shared namespace (scope=global)`,
|
|
13828
14202
|
);
|
|
@@ -14198,41 +14572,49 @@ export class Orchestrator {
|
|
|
14198
14572
|
contentHashSource: rawChunkedContent,
|
|
14199
14573
|
},
|
|
14200
14574
|
);
|
|
14575
|
+
try {
|
|
14576
|
+
// Write individual chunks with parent reference
|
|
14577
|
+
for (const chunk of chunkResult.chunks) {
|
|
14578
|
+
// Score each chunk's importance separately
|
|
14579
|
+
const chunkImportance = scoreImportance(
|
|
14580
|
+
chunk.content,
|
|
14581
|
+
writeCategory,
|
|
14582
|
+
fact.tags,
|
|
14583
|
+
);
|
|
14584
|
+
const chunkWriteSource =
|
|
14585
|
+
(fact as any).source === "proactive"
|
|
14586
|
+
? "chunking-proactive"
|
|
14587
|
+
: "chunking";
|
|
14201
14588
|
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
intentEntityTypes: inferredIntent?.entityTypes,
|
|
14232
|
-
memoryKind,
|
|
14233
|
-
validAt: sourceContext?.validAt,
|
|
14234
|
-
},
|
|
14235
|
-
);
|
|
14589
|
+
await targetStorage.writeChunk(
|
|
14590
|
+
parentId,
|
|
14591
|
+
chunk.index,
|
|
14592
|
+
chunkResult.chunks.length,
|
|
14593
|
+
writeCategory,
|
|
14594
|
+
// Each chunk carries its own inline citation so provenance
|
|
14595
|
+
// survives when a single chunk is quoted in isolation.
|
|
14596
|
+
applyInlineCitation(chunk.content),
|
|
14597
|
+
{
|
|
14598
|
+
confidence: fact.confidence,
|
|
14599
|
+
tags: fact.tags,
|
|
14600
|
+
entityRef: fact.entityRef,
|
|
14601
|
+
source: chunkWriteSource,
|
|
14602
|
+
importance: chunkImportance,
|
|
14603
|
+
intentGoal: inferredIntent?.goal,
|
|
14604
|
+
intentActionType: inferredIntent?.actionType,
|
|
14605
|
+
intentEntityTypes: inferredIntent?.entityTypes,
|
|
14606
|
+
memoryKind,
|
|
14607
|
+
validAt: sourceContext?.validAt,
|
|
14608
|
+
},
|
|
14609
|
+
);
|
|
14610
|
+
}
|
|
14611
|
+
} finally {
|
|
14612
|
+
// The parent memory is durable once writeMemory returns `parentId`.
|
|
14613
|
+
// Touch immediately around the chunk-write loop so a later chunk
|
|
14614
|
+
// failure still surfaces the partially durable parent/chunk files to
|
|
14615
|
+
// catalog-driven `writtenSince` maintenance. The final touch below
|
|
14616
|
+
// still refreshes `lastWriteAt` after later durable writes on success.
|
|
14617
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
14236
14618
|
}
|
|
14237
14619
|
|
|
14238
14620
|
if (routedRuleId) {
|
|
@@ -14308,62 +14690,71 @@ export class Orchestrator {
|
|
|
14308
14690
|
// directly for embedding-fallback sync of each chunk document.
|
|
14309
14691
|
await this.indexPersistedMemory(targetStorage, chunkId);
|
|
14310
14692
|
}
|
|
14311
|
-
|
|
14312
|
-
|
|
14313
|
-
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
14319
|
-
|
|
14320
|
-
|
|
14321
|
-
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
}
|
|
14328
|
-
// v8.2: graph edge building for chunked memories
|
|
14329
|
-
if (this.config.multiGraphMemoryEnabled) {
|
|
14330
|
-
try {
|
|
14331
|
-
const graphContext = await ensureGraphContext(targetStorage);
|
|
14332
|
-
const entityRef =
|
|
14333
|
-
typeof (fact as any).entityRef === "string"
|
|
14334
|
-
? (fact as any).entityRef
|
|
14335
|
-
: undefined;
|
|
14336
|
-
const parentRelPath = resolvePersistedMemoryRelativePath({
|
|
14337
|
-
memoryId: parentId,
|
|
14338
|
-
pathById: graphContext.memoryPathById,
|
|
14339
|
-
category: writeCategory,
|
|
14340
|
-
});
|
|
14341
|
-
graphContext.memoryPathById.set(parentId, parentRelPath);
|
|
14342
|
-
appendMemoryToGraphContext({
|
|
14343
|
-
allMemsForGraph: graphContext.allMemsForGraph,
|
|
14344
|
-
storageDir: targetStorage.dir,
|
|
14345
|
-
memoryRelPath: parentRelPath,
|
|
14346
|
-
memoryId: parentId,
|
|
14347
|
-
category: writeCategory,
|
|
14348
|
-
content: fact.content ?? "",
|
|
14349
|
-
entityRef,
|
|
14693
|
+
try {
|
|
14694
|
+
if (
|
|
14695
|
+
this.config.verbatimArtifactsEnabled &&
|
|
14696
|
+
this.config.verbatimArtifactCategories.includes(writeCategory) &&
|
|
14697
|
+
fact.confidence >= this.config.verbatimArtifactsMinConfidence
|
|
14698
|
+
) {
|
|
14699
|
+
// Reuse citedChunkedContent so the artifact carries the same citation
|
|
14700
|
+
// timestamp as the parent memory write above (Fix #3 — duplicate-citation).
|
|
14701
|
+
await targetStorage.writeArtifact(citedChunkedContent, {
|
|
14702
|
+
confidence: fact.confidence,
|
|
14703
|
+
tags: [...fact.tags, "artifact", "chunked-parent"],
|
|
14704
|
+
artifactType: this.artifactTypeForCategory(writeCategory),
|
|
14705
|
+
sourceMemoryId: parentId,
|
|
14706
|
+
intentGoal: inferredIntent?.goal,
|
|
14707
|
+
intentActionType: inferredIntent?.actionType,
|
|
14708
|
+
intentEntityTypes: inferredIntent?.entityTypes,
|
|
14350
14709
|
});
|
|
14351
|
-
await this.buildGraphEdge(
|
|
14352
|
-
targetStorage,
|
|
14353
|
-
parentRelPath,
|
|
14354
|
-
entityRef,
|
|
14355
|
-
parentId,
|
|
14356
|
-
fact.content ?? "",
|
|
14357
|
-
graphContext.allMemsForGraph,
|
|
14358
|
-
graphContext.memoryPathById,
|
|
14359
|
-
threadIdForExtraction ?? undefined,
|
|
14360
|
-
threadEpisodeIdsForGraph,
|
|
14361
|
-
graphContext.previousPersistedRelPath,
|
|
14362
|
-
);
|
|
14363
|
-
graphContext.previousPersistedRelPath = parentRelPath;
|
|
14364
|
-
} catch {
|
|
14365
|
-
/* fail-open */
|
|
14366
14710
|
}
|
|
14711
|
+
// v8.2: graph edge building for chunked memories
|
|
14712
|
+
if (this.config.multiGraphMemoryEnabled) {
|
|
14713
|
+
try {
|
|
14714
|
+
const graphContext = await ensureGraphContext(targetStorage);
|
|
14715
|
+
const entityRef =
|
|
14716
|
+
typeof (fact as any).entityRef === "string"
|
|
14717
|
+
? (fact as any).entityRef
|
|
14718
|
+
: undefined;
|
|
14719
|
+
const parentRelPath = resolvePersistedMemoryRelativePath({
|
|
14720
|
+
memoryId: parentId,
|
|
14721
|
+
pathById: graphContext.memoryPathById,
|
|
14722
|
+
category: writeCategory,
|
|
14723
|
+
});
|
|
14724
|
+
graphContext.memoryPathById.set(parentId, parentRelPath);
|
|
14725
|
+
appendMemoryToGraphContext({
|
|
14726
|
+
allMemsForGraph: graphContext.allMemsForGraph,
|
|
14727
|
+
storageDir: targetStorage.dir,
|
|
14728
|
+
memoryRelPath: parentRelPath,
|
|
14729
|
+
memoryId: parentId,
|
|
14730
|
+
category: writeCategory,
|
|
14731
|
+
content: fact.content ?? "",
|
|
14732
|
+
entityRef,
|
|
14733
|
+
});
|
|
14734
|
+
await this.buildGraphEdge(
|
|
14735
|
+
targetStorage,
|
|
14736
|
+
parentRelPath,
|
|
14737
|
+
entityRef,
|
|
14738
|
+
parentId,
|
|
14739
|
+
fact.content ?? "",
|
|
14740
|
+
graphContext.allMemsForGraph,
|
|
14741
|
+
graphContext.memoryPathById,
|
|
14742
|
+
threadIdForExtraction ?? undefined,
|
|
14743
|
+
threadEpisodeIdsForGraph,
|
|
14744
|
+
graphContext.previousPersistedRelPath,
|
|
14745
|
+
);
|
|
14746
|
+
graphContext.previousPersistedRelPath = parentRelPath;
|
|
14747
|
+
} catch {
|
|
14748
|
+
/* fail-open */
|
|
14749
|
+
}
|
|
14750
|
+
}
|
|
14751
|
+
} finally {
|
|
14752
|
+
// Catalog touch (issue #1499): refresh AFTER later chunked
|
|
14753
|
+
// source-namespace durable mutations — temporal supersession, shared
|
|
14754
|
+
// promotion, optional artifact writes, and graph-edge writes — so
|
|
14755
|
+
// `lastWriteAt` cannot precede later file changes on successful
|
|
14756
|
+
// completion. Use the KNOWN routed name, not a dir-decoded guess.
|
|
14757
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
14367
14758
|
}
|
|
14368
14759
|
trackBehaviorSignals(
|
|
14369
14760
|
targetStorage,
|
|
@@ -14469,120 +14860,150 @@ export class Orchestrator {
|
|
|
14469
14860
|
} catch (err) {
|
|
14470
14861
|
log.warn(`temporal-supersession: unexpected error: ${err}`);
|
|
14471
14862
|
}
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14863
|
+
try {
|
|
14864
|
+
trackBehaviorSignals(
|
|
14865
|
+
targetStorage,
|
|
14866
|
+
buildBehaviorSignalsForMemory({
|
|
14867
|
+
memoryId,
|
|
14868
|
+
category: writeCategory,
|
|
14869
|
+
content: fact.content,
|
|
14870
|
+
namespace: this.namespaceFromStorageDir(targetStorage.dir),
|
|
14871
|
+
confidence: fact.confidence,
|
|
14872
|
+
source: "extraction",
|
|
14873
|
+
}),
|
|
14874
|
+
);
|
|
14875
|
+
trackPersistedId(targetStorage, memoryId);
|
|
14876
|
+
if (
|
|
14877
|
+
threadEpisodeIdsForGraph &&
|
|
14878
|
+
!threadEpisodeIdsForGraph.includes(memoryId)
|
|
14879
|
+
) {
|
|
14880
|
+
threadEpisodeIdsForGraph.push(memoryId);
|
|
14881
|
+
}
|
|
14882
|
+
await this.indexPersistedMemory(targetStorage, memoryId);
|
|
14883
|
+
await promoteMemoryToShared({
|
|
14884
|
+
sourceStorage: targetStorage,
|
|
14476
14885
|
category: writeCategory,
|
|
14477
14886
|
content: fact.content,
|
|
14478
|
-
namespace: this.namespaceFromStorageDir(targetStorage.dir),
|
|
14479
14887
|
confidence: fact.confidence,
|
|
14480
|
-
|
|
14481
|
-
|
|
14482
|
-
);
|
|
14483
|
-
trackPersistedId(targetStorage, memoryId);
|
|
14484
|
-
if (
|
|
14485
|
-
threadEpisodeIdsForGraph &&
|
|
14486
|
-
!threadEpisodeIdsForGraph.includes(memoryId)
|
|
14487
|
-
) {
|
|
14488
|
-
threadEpisodeIdsForGraph.push(memoryId);
|
|
14489
|
-
}
|
|
14490
|
-
await this.indexPersistedMemory(targetStorage, memoryId);
|
|
14491
|
-
await promoteMemoryToShared({
|
|
14492
|
-
sourceStorage: targetStorage,
|
|
14493
|
-
category: writeCategory,
|
|
14494
|
-
content: fact.content,
|
|
14495
|
-
confidence: fact.confidence,
|
|
14496
|
-
tags: fact.tags,
|
|
14497
|
-
entityRef:
|
|
14498
|
-
typeof (fact as any).entityRef === "string"
|
|
14499
|
-
? (fact as any).entityRef
|
|
14500
|
-
: undefined,
|
|
14501
|
-
structuredAttributes: fact.structuredAttributes,
|
|
14502
|
-
sourceMemoryId: memoryId,
|
|
14503
|
-
importance,
|
|
14504
|
-
intentGoal: inferredIntent?.goal,
|
|
14505
|
-
intentActionType: inferredIntent?.actionType,
|
|
14506
|
-
intentEntityTypes: inferredIntent?.entityTypes,
|
|
14507
|
-
memoryKind,
|
|
14508
|
-
validAt: sourceContext?.validAt,
|
|
14509
|
-
source: extractionWriteSource,
|
|
14510
|
-
});
|
|
14511
|
-
// v8.2: graph edge building (fail-open — errors caught inside GraphIndex)
|
|
14512
|
-
if (this.config.multiGraphMemoryEnabled) {
|
|
14513
|
-
try {
|
|
14514
|
-
const graphContext = await ensureGraphContext(targetStorage);
|
|
14515
|
-
const entityRef =
|
|
14888
|
+
tags: fact.tags,
|
|
14889
|
+
entityRef:
|
|
14516
14890
|
typeof (fact as any).entityRef === "string"
|
|
14517
14891
|
? (fact as any).entityRef
|
|
14518
|
-
: undefined
|
|
14519
|
-
|
|
14520
|
-
memoryId,
|
|
14521
|
-
pathById: graphContext.memoryPathById,
|
|
14522
|
-
category: writeCategory,
|
|
14523
|
-
});
|
|
14524
|
-
graphContext.memoryPathById.set(memoryId, memoryRelPath);
|
|
14525
|
-
appendMemoryToGraphContext({
|
|
14526
|
-
allMemsForGraph: graphContext.allMemsForGraph,
|
|
14527
|
-
storageDir: targetStorage.dir,
|
|
14528
|
-
memoryRelPath: memoryRelPath,
|
|
14529
|
-
memoryId,
|
|
14530
|
-
category: writeCategory,
|
|
14531
|
-
content: fact.content ?? "",
|
|
14532
|
-
entityRef,
|
|
14533
|
-
});
|
|
14534
|
-
await this.buildGraphEdge(
|
|
14535
|
-
targetStorage,
|
|
14536
|
-
memoryRelPath,
|
|
14537
|
-
entityRef,
|
|
14538
|
-
memoryId,
|
|
14539
|
-
fact.content ?? "",
|
|
14540
|
-
graphContext.allMemsForGraph,
|
|
14541
|
-
graphContext.memoryPathById,
|
|
14542
|
-
threadIdForExtraction ?? undefined,
|
|
14543
|
-
threadEpisodeIdsForGraph,
|
|
14544
|
-
graphContext.previousPersistedRelPath,
|
|
14545
|
-
);
|
|
14546
|
-
graphContext.previousPersistedRelPath = memoryRelPath;
|
|
14547
|
-
} catch {
|
|
14548
|
-
/* fail-open */
|
|
14549
|
-
}
|
|
14550
|
-
}
|
|
14551
|
-
if (
|
|
14552
|
-
this.config.verbatimArtifactsEnabled &&
|
|
14553
|
-
this.config.verbatimArtifactCategories.includes(writeCategory) &&
|
|
14554
|
-
fact.confidence >= this.config.verbatimArtifactsMinConfidence
|
|
14555
|
-
) {
|
|
14556
|
-
// Reuse citedFactContent so the artifact carries the same citation
|
|
14557
|
-
// timestamp as the memory write above (Fix #3 — duplicate-citation).
|
|
14558
|
-
await targetStorage.writeArtifact(citedFactContent, {
|
|
14559
|
-
confidence: fact.confidence,
|
|
14560
|
-
tags: [...fact.tags, "artifact"],
|
|
14561
|
-
artifactType: this.artifactTypeForCategory(writeCategory),
|
|
14892
|
+
: undefined,
|
|
14893
|
+
structuredAttributes: fact.structuredAttributes,
|
|
14562
14894
|
sourceMemoryId: memoryId,
|
|
14895
|
+
importance,
|
|
14563
14896
|
intentGoal: inferredIntent?.goal,
|
|
14564
14897
|
intentActionType: inferredIntent?.actionType,
|
|
14565
14898
|
intentEntityTypes: inferredIntent?.entityTypes,
|
|
14899
|
+
memoryKind,
|
|
14900
|
+
validAt: sourceContext?.validAt,
|
|
14901
|
+
source: extractionWriteSource,
|
|
14566
14902
|
});
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
|
|
14903
|
+
// v8.2: graph edge building (fail-open — errors caught inside GraphIndex)
|
|
14904
|
+
if (this.config.multiGraphMemoryEnabled) {
|
|
14905
|
+
try {
|
|
14906
|
+
const graphContext = await ensureGraphContext(targetStorage);
|
|
14907
|
+
const entityRef =
|
|
14908
|
+
typeof (fact as any).entityRef === "string"
|
|
14909
|
+
? (fact as any).entityRef
|
|
14910
|
+
: undefined;
|
|
14911
|
+
const memoryRelPath = resolvePersistedMemoryRelativePath({
|
|
14912
|
+
memoryId,
|
|
14913
|
+
pathById: graphContext.memoryPathById,
|
|
14914
|
+
category: writeCategory,
|
|
14915
|
+
});
|
|
14916
|
+
graphContext.memoryPathById.set(memoryId, memoryRelPath);
|
|
14917
|
+
appendMemoryToGraphContext({
|
|
14918
|
+
allMemsForGraph: graphContext.allMemsForGraph,
|
|
14919
|
+
storageDir: targetStorage.dir,
|
|
14920
|
+
memoryRelPath: memoryRelPath,
|
|
14921
|
+
memoryId,
|
|
14922
|
+
category: writeCategory,
|
|
14923
|
+
content: fact.content ?? "",
|
|
14924
|
+
entityRef,
|
|
14925
|
+
});
|
|
14926
|
+
await this.buildGraphEdge(
|
|
14927
|
+
targetStorage,
|
|
14928
|
+
memoryRelPath,
|
|
14929
|
+
entityRef,
|
|
14930
|
+
memoryId,
|
|
14931
|
+
fact.content ?? "",
|
|
14932
|
+
graphContext.allMemsForGraph,
|
|
14933
|
+
graphContext.memoryPathById,
|
|
14934
|
+
threadIdForExtraction ?? undefined,
|
|
14935
|
+
threadEpisodeIdsForGraph,
|
|
14936
|
+
graphContext.previousPersistedRelPath,
|
|
14937
|
+
);
|
|
14938
|
+
graphContext.previousPersistedRelPath = memoryRelPath;
|
|
14939
|
+
} catch {
|
|
14940
|
+
/* fail-open */
|
|
14941
|
+
}
|
|
14942
|
+
}
|
|
14943
|
+
if (
|
|
14944
|
+
this.config.verbatimArtifactsEnabled &&
|
|
14945
|
+
this.config.verbatimArtifactCategories.includes(writeCategory) &&
|
|
14946
|
+
fact.confidence >= this.config.verbatimArtifactsMinConfidence
|
|
14947
|
+
) {
|
|
14948
|
+
// Reuse citedFactContent so the artifact carries the same citation
|
|
14949
|
+
// timestamp as the memory write above (Fix #3 — duplicate-citation).
|
|
14950
|
+
await targetStorage.writeArtifact(citedFactContent, {
|
|
14951
|
+
confidence: fact.confidence,
|
|
14952
|
+
tags: [...fact.tags, "artifact"],
|
|
14953
|
+
artifactType: this.artifactTypeForCategory(writeCategory),
|
|
14954
|
+
sourceMemoryId: memoryId,
|
|
14955
|
+
intentGoal: inferredIntent?.goal,
|
|
14956
|
+
intentActionType: inferredIntent?.actionType,
|
|
14957
|
+
intentEntityTypes: inferredIntent?.entityTypes,
|
|
14958
|
+
});
|
|
14959
|
+
}
|
|
14960
|
+
// Register in content-hash index after successful write.
|
|
14961
|
+
// Thread 3 fix: canonicalize by stripping any pre-existing citation so
|
|
14962
|
+
// the stored hash matches what the dedup check computes via
|
|
14963
|
+
// stripCitationForTemplate before calling contentHashIndex.has().
|
|
14964
|
+
if (this.contentHashIndex) {
|
|
14965
|
+
const canonicalFactContent =
|
|
14966
|
+
citationEnabled &&
|
|
14967
|
+
hasCitationForTemplate(fact.content, citationTemplate)
|
|
14968
|
+
? stripCitationForTemplate(fact.content, citationTemplate)
|
|
14969
|
+
: fact.content;
|
|
14970
|
+
const hashRegisterKey =
|
|
14971
|
+
writeCategory === "procedure"
|
|
14972
|
+
? buildProcedurePersistBody(fact.content, fact.procedureSteps)
|
|
14973
|
+
: canonicalFactContent;
|
|
14974
|
+
this.contentHashIndex.add(hashRegisterKey);
|
|
14975
|
+
}
|
|
14976
|
+
} finally {
|
|
14977
|
+
// Catalog touch (issue #1499): record AFTER every synchronous
|
|
14978
|
+
// source-namespace mutation in the non-chunked path: writeMemory,
|
|
14979
|
+
// temporal supersession, graph edges, and optional verbatim artifacts.
|
|
14980
|
+
// The `finally` preserves the write touch when post-write indexing or
|
|
14981
|
+
// promotion fails after the canonical memory is already durable. Use the
|
|
14982
|
+
// KNOWN routed name, not a dir-decoded guess (NCQI0).
|
|
14983
|
+
this.markCatalogWrite(targetNamespaceName, targetStorage.dir);
|
|
14984
|
+
}
|
|
14985
|
+
}
|
|
14986
|
+
|
|
14987
|
+
// Tracks whether THIS extraction persisted any durable, non-fact output to the
|
|
14988
|
+
// BASE namespace's storage (entity / relationship / profile / question). The
|
|
14989
|
+
// per-fact `markCatalogWrite` only fires inside the fact write loop, so a
|
|
14990
|
+
// fact-less extraction that still persists durable data must record exactly one
|
|
14991
|
+
// base-namespace catalog touch after all writes complete (NHZEZ, codex P2).
|
|
14992
|
+
let durableNonFactWritten = false;
|
|
14993
|
+
let durableNonFactTouchRecorded = false;
|
|
14994
|
+
const touchBaseNonFactNamespace = () => {
|
|
14995
|
+
const baseTouchNamespace =
|
|
14996
|
+
baseNamespace && baseNamespace.length > 0
|
|
14997
|
+
? baseNamespace
|
|
14998
|
+
: this.namespaceFromStorageDir(storage.dir);
|
|
14999
|
+
this.markCatalogWrite(baseTouchNamespace, storage.dir);
|
|
15000
|
+
};
|
|
15001
|
+
const recordDurableNonFactWrite = () => {
|
|
15002
|
+
durableNonFactWritten = true;
|
|
15003
|
+
if (durableNonFactTouchRecorded) return;
|
|
15004
|
+
durableNonFactTouchRecorded = true;
|
|
15005
|
+
touchBaseNonFactNamespace();
|
|
15006
|
+
};
|
|
14586
15007
|
for (const entity of entities) {
|
|
14587
15008
|
try {
|
|
14588
15009
|
const name = (entity as any)?.name;
|
|
@@ -14607,7 +15028,10 @@ export class Orchestrator {
|
|
|
14607
15028
|
? (entity as any).structuredSections
|
|
14608
15029
|
: undefined,
|
|
14609
15030
|
});
|
|
14610
|
-
if (id)
|
|
15031
|
+
if (id) {
|
|
15032
|
+
trackPersistedId(storage, id);
|
|
15033
|
+
recordDurableNonFactWrite();
|
|
15034
|
+
}
|
|
14611
15035
|
} catch (err) {
|
|
14612
15036
|
log.warn(`persistExtraction: entity write failed: ${err}`);
|
|
14613
15037
|
}
|
|
@@ -14626,10 +15050,12 @@ export class Orchestrator {
|
|
|
14626
15050
|
target: rel.target,
|
|
14627
15051
|
label: rel.label,
|
|
14628
15052
|
});
|
|
15053
|
+
recordDurableNonFactWrite();
|
|
14629
15054
|
await storage.addEntityRelationship(rel.target, {
|
|
14630
15055
|
target: rel.source,
|
|
14631
15056
|
label: `${rel.label} (reverse)`,
|
|
14632
15057
|
});
|
|
15058
|
+
recordDurableNonFactWrite();
|
|
14633
15059
|
} catch (err) {
|
|
14634
15060
|
log.debug(`relationship persist failed: ${err}`);
|
|
14635
15061
|
}
|
|
@@ -14658,23 +15084,49 @@ export class Orchestrator {
|
|
|
14658
15084
|
|
|
14659
15085
|
if (profileUpdates.length > 0) {
|
|
14660
15086
|
await storage.appendToProfile(profileUpdates);
|
|
15087
|
+
recordDurableNonFactWrite();
|
|
14661
15088
|
}
|
|
14662
15089
|
|
|
14663
15090
|
// Persist questions
|
|
14664
15091
|
for (const q of questions) {
|
|
14665
15092
|
const id = await storage.writeQuestion(q.question, q.context, q.priority);
|
|
14666
|
-
if (id)
|
|
15093
|
+
if (id) {
|
|
15094
|
+
trackPersistedId(storage, id);
|
|
15095
|
+
recordDurableNonFactWrite();
|
|
15096
|
+
}
|
|
14667
15097
|
}
|
|
14668
15098
|
|
|
14669
|
-
// Persist identity reflection
|
|
15099
|
+
// Persist identity reflection. This writes durable namespace-local state, so
|
|
15100
|
+
// an identity-ONLY extraction (no facts/entities/profile/questions) still
|
|
15101
|
+
// counts as a durable non-fact write for the catalog touch below (NIIly).
|
|
15102
|
+
// Only count it when the write actually succeeds (best-effort write); the
|
|
15103
|
+
// touch is recorded AFTER this so a rolled-back/failed write never touches.
|
|
14670
15104
|
if (this.config.identityEnabled && result.identityReflection) {
|
|
14671
15105
|
try {
|
|
14672
15106
|
await storage.appendIdentityReflection(result.identityReflection);
|
|
15107
|
+
recordDurableNonFactWrite();
|
|
14673
15108
|
} catch (err) {
|
|
14674
15109
|
log.debug(`identity reflection write failed: ${err}`);
|
|
14675
15110
|
}
|
|
14676
15111
|
}
|
|
14677
15112
|
|
|
15113
|
+
// Catalog touch for durable NON-FACT outputs (NHZEZ / NIIly, codex P2). The
|
|
15114
|
+
// per-fact `markCatalogWrite` above only fires inside the fact write loop, so
|
|
15115
|
+
// an extraction that persists ONLY entities, relationships, profile updates,
|
|
15116
|
+
// questions, or an identity reflection (no facts) would record durable data to
|
|
15117
|
+
// the BASE namespace's storage without ever touching the catalog — leaving that
|
|
15118
|
+
// namespace's `lastWriteAt` stale so `listNamespaces({writtenSince})` /
|
|
15119
|
+
// write-recency QMD maintenance miss the write. All of these are written to the
|
|
15120
|
+
// BASE `storage` (not the per-fact routed `targetStorage`), so we record ONE
|
|
15121
|
+
// base-namespace touch here, AFTER every non-fact write completes. Use the
|
|
15122
|
+
// KNOWN base namespace name, not a dir-decoded guess (NCQI0). One touch per
|
|
15123
|
+
// namespace per extraction — `markWrite` is idempotent, so if the fact path
|
|
15124
|
+
// already touched the base namespace this only refreshes `lastWriteAt`.
|
|
15125
|
+
// Best-effort and failure-tolerant (markCatalogWrite swallows errors).
|
|
15126
|
+
if (durableNonFactWritten) {
|
|
15127
|
+
touchBaseNonFactNamespace();
|
|
15128
|
+
}
|
|
15129
|
+
|
|
14678
15130
|
// Save content-hash index after batch
|
|
14679
15131
|
if (this.contentHashIndex) {
|
|
14680
15132
|
await this.contentHashIndex
|
|
@@ -14912,6 +15364,11 @@ export class Orchestrator {
|
|
|
14912
15364
|
log.info("running consolidation pass");
|
|
14913
15365
|
let merged = 0;
|
|
14914
15366
|
let invalidated = 0;
|
|
15367
|
+
// Tracks whether any consolidation memory-item action (UPDATE / MERGE /
|
|
15368
|
+
// INVALIDATE) durably rewrote memory state. A consolidation pass that only
|
|
15369
|
+
// mutates memory items (no profile/entity updates) still changes the default
|
|
15370
|
+
// namespace's data, so its catalog `lastWriteAt` must refresh too (NIBOi).
|
|
15371
|
+
let memoryItemMutated = false;
|
|
14915
15372
|
|
|
14916
15373
|
// Flush access tracking buffer first
|
|
14917
15374
|
if (this.accessTrackingBuffer.size > 0) {
|
|
@@ -14955,6 +15412,7 @@ export class Orchestrator {
|
|
|
14955
15412
|
: null;
|
|
14956
15413
|
if (await this.storage.invalidateMemory(item.existingId)) {
|
|
14957
15414
|
invalidated += 1;
|
|
15415
|
+
memoryItemMutated = true;
|
|
14958
15416
|
await this.embeddingFallback.removeFromIndex(item.existingId);
|
|
14959
15417
|
if (toInvalidate?.path && toInvalidate.frontmatter?.created) {
|
|
14960
15418
|
deindexMemory(
|
|
@@ -14976,6 +15434,7 @@ export class Orchestrator {
|
|
|
14976
15434
|
lineage: [item.existingId],
|
|
14977
15435
|
},
|
|
14978
15436
|
);
|
|
15437
|
+
memoryItemMutated = true;
|
|
14979
15438
|
await this.indexPersistedMemory(this.storage, item.existingId);
|
|
14980
15439
|
// updateMemory() only changes content/updated/lineage — path, created, and tags
|
|
14981
15440
|
// are preserved, so the temporal/tag index entry is already correct; no reindex needed.
|
|
@@ -14991,6 +15450,7 @@ export class Orchestrator {
|
|
|
14991
15450
|
lineage: [item.existingId, item.mergeWith],
|
|
14992
15451
|
},
|
|
14993
15452
|
);
|
|
15453
|
+
memoryItemMutated = true;
|
|
14994
15454
|
await this.indexPersistedMemory(this.storage, item.existingId);
|
|
14995
15455
|
// updateMemory() only changes content/updated/supersedes/lineage — path, created, and tags
|
|
14996
15456
|
// are preserved, so the temporal/tag index entry for the survivor is already correct.
|
|
@@ -15035,9 +15495,24 @@ export class Orchestrator {
|
|
|
15035
15495
|
});
|
|
15036
15496
|
}
|
|
15037
15497
|
|
|
15498
|
+
// Catalog write touch accounting (issue #1499 sweep): consolidation persists
|
|
15499
|
+
// durable mutations directly to the default-namespace `this.storage`, bypassing
|
|
15500
|
+
// the extraction write path. We do NOT touch here — later maintenance steps in
|
|
15501
|
+
// this same function (entity-file merges, expired-commitment / TTL cleanup,
|
|
15502
|
+
// fact archival) can ALSO mutate the namespace on a run with no LLM outputs
|
|
15503
|
+
// (NIjwl). So we accumulate every durable mutation into `memoryItemMutated` and
|
|
15504
|
+
// record ONE consolidated touch AFTER all mutation-producing steps complete,
|
|
15505
|
+
// just before returning (rule #25: touch after the write commits). LLM
|
|
15506
|
+
// profile/entity updates and memory-item actions (UPDATE / MERGE / INVALIDATE)
|
|
15507
|
+
// count here (NIBOi).
|
|
15508
|
+
if (result.profileUpdates.length > 0 || result.entityUpdates.length > 0) {
|
|
15509
|
+
memoryItemMutated = true;
|
|
15510
|
+
}
|
|
15511
|
+
|
|
15038
15512
|
// Merge fragmented entity files
|
|
15039
15513
|
const entitiesMerged = await this.storage.mergeFragmentedEntities();
|
|
15040
15514
|
if (entitiesMerged > 0) {
|
|
15515
|
+
memoryItemMutated = true;
|
|
15041
15516
|
log.info(`merged ${entitiesMerged} fragmented entity files`);
|
|
15042
15517
|
}
|
|
15043
15518
|
|
|
@@ -15048,6 +15523,10 @@ export class Orchestrator {
|
|
|
15048
15523
|
5,
|
|
15049
15524
|
);
|
|
15050
15525
|
if (synthesized > 0) {
|
|
15526
|
+
// Entity synthesis rewrites entity files — a durable namespace mutation,
|
|
15527
|
+
// so record it for the catalog touch even when it is the only change in
|
|
15528
|
+
// the pass (codex). Otherwise lastWriteAt goes stale.
|
|
15529
|
+
memoryItemMutated = true;
|
|
15051
15530
|
log.info(`refreshed ${synthesized} entity syntheses`);
|
|
15052
15531
|
}
|
|
15053
15532
|
} catch (err) {
|
|
@@ -15060,6 +15539,7 @@ export class Orchestrator {
|
|
|
15060
15539
|
this.config.commitmentDecayDays,
|
|
15061
15540
|
);
|
|
15062
15541
|
if (deletedCommitments.length > 0) {
|
|
15542
|
+
memoryItemMutated = true;
|
|
15063
15543
|
log.info(`cleaned ${deletedCommitments.length} expired commitments`);
|
|
15064
15544
|
if (this.config.queryAwareIndexingEnabled) {
|
|
15065
15545
|
for (const m of deletedCommitments) {
|
|
@@ -15089,6 +15569,7 @@ export class Orchestrator {
|
|
|
15089
15569
|
lifecycle.transitionedToExpired.length > 0 ||
|
|
15090
15570
|
lifecycle.deletedResolved.length > 0
|
|
15091
15571
|
) {
|
|
15572
|
+
memoryItemMutated = true;
|
|
15092
15573
|
log.info(
|
|
15093
15574
|
`commitment ledger lifecycle: expired ${lifecycle.transitionedToExpired.length}, cleaned ${lifecycle.deletedResolved.length}`,
|
|
15094
15575
|
);
|
|
@@ -15101,6 +15582,7 @@ export class Orchestrator {
|
|
|
15101
15582
|
// Clean memories past their TTL (speculative memories auto-expire)
|
|
15102
15583
|
const deletedTTL = await this.storage.cleanExpiredTTL();
|
|
15103
15584
|
if (deletedTTL.length > 0) {
|
|
15585
|
+
memoryItemMutated = true;
|
|
15104
15586
|
log.info(`cleaned ${deletedTTL.length} TTL-expired memories`);
|
|
15105
15587
|
if (this.config.queryAwareIndexingEnabled) {
|
|
15106
15588
|
for (const m of deletedTTL) {
|
|
@@ -15119,7 +15601,12 @@ export class Orchestrator {
|
|
|
15119
15601
|
try {
|
|
15120
15602
|
const lightSleepStartedAt = new Date().toISOString();
|
|
15121
15603
|
const lifecycleCorpus = await this.storage.readAllMemories();
|
|
15122
|
-
|
|
15604
|
+
// Lifecycle frontmatter writes count as durable mutations for the catalog
|
|
15605
|
+
// touch below (codex NR-tS), even when no other consolidation step set
|
|
15606
|
+
// memoryItemMutated.
|
|
15607
|
+
if ((await this.runLifecyclePolicyPass(lifecycleCorpus)) > 0) {
|
|
15608
|
+
memoryItemMutated = true;
|
|
15609
|
+
}
|
|
15123
15610
|
await this.recordScheduledDreamsPhaseRun(
|
|
15124
15611
|
"lightSleep",
|
|
15125
15612
|
lifecycleCorpus.length,
|
|
@@ -15139,13 +15626,17 @@ export class Orchestrator {
|
|
|
15139
15626
|
|
|
15140
15627
|
try {
|
|
15141
15628
|
const deepSleepStartedAt = new Date().toISOString();
|
|
15142
|
-
|
|
15629
|
+
// Tier migrations move/rewrite memory files; count them as durable
|
|
15630
|
+
// mutations for the catalog touch below (codex NThSW).
|
|
15631
|
+
const tierMigration = await this.runTierMigrationCycle(this.storage, "maintenance");
|
|
15632
|
+
if (tierMigration.migrated > 0) memoryItemMutated = true;
|
|
15143
15633
|
allMemories = await this.storage.readAllMemories();
|
|
15144
15634
|
|
|
15145
15635
|
// Fact archival pass (v6.0) — move old, low-importance, rarely-accessed facts to archive/
|
|
15146
15636
|
if (this.config.factArchivalEnabled) {
|
|
15147
15637
|
const archived = await this.runFactArchival(allMemories);
|
|
15148
15638
|
if (archived > 0) {
|
|
15639
|
+
memoryItemMutated = true;
|
|
15149
15640
|
log.info(`archived ${archived} old low-importance facts`);
|
|
15150
15641
|
}
|
|
15151
15642
|
}
|
|
@@ -15268,6 +15759,10 @@ export class Orchestrator {
|
|
|
15268
15759
|
);
|
|
15269
15760
|
if (profileResult) {
|
|
15270
15761
|
await this.storage.writeProfile(profileResult.consolidatedProfile);
|
|
15762
|
+
// Profile consolidation rewrites profile.md — a durable namespace
|
|
15763
|
+
// mutation; record it for the catalog touch even when it is the only
|
|
15764
|
+
// change in the pass (codex). Otherwise lastWriteAt goes stale.
|
|
15765
|
+
memoryItemMutated = true;
|
|
15271
15766
|
log.info(
|
|
15272
15767
|
`profile.md consolidated: removed ${profileResult.removedCount} items — ${profileResult.summary}`,
|
|
15273
15768
|
);
|
|
@@ -15352,6 +15847,21 @@ export class Orchestrator {
|
|
|
15352
15847
|
}
|
|
15353
15848
|
}
|
|
15354
15849
|
|
|
15850
|
+
// Consolidated catalog write touch (issue #1499 sweep; NIBOi + NIjwl). One
|
|
15851
|
+
// touch covering EVERY durable namespace mutation this pass made — LLM
|
|
15852
|
+
// profile/entity/memory-item actions AND cleanup-only maintenance (entity-file
|
|
15853
|
+
// merges, expired-commitment / ledger-lifecycle / TTL cleanup, fact archival).
|
|
15854
|
+
// Recorded here, after all mutation-producing steps, so a cleanup-only run that
|
|
15855
|
+
// rewrote the store still refreshes `lastWriteAt` (rule #25). The default
|
|
15856
|
+
// namespace is always configured/cataloged; `markWrite` is idempotent so this
|
|
15857
|
+
// only refreshes recency. Best-effort and failure-tolerant.
|
|
15858
|
+
if (memoryItemMutated) {
|
|
15859
|
+
this.markCatalogWrite(
|
|
15860
|
+
this.namespaceFromStorageDir(this.storage.dir),
|
|
15861
|
+
this.storage.dir,
|
|
15862
|
+
);
|
|
15863
|
+
}
|
|
15864
|
+
|
|
15355
15865
|
log.info("consolidation complete");
|
|
15356
15866
|
return { memoriesProcessed: allMemories.length, merged, invalidated };
|
|
15357
15867
|
}
|
|
@@ -15801,14 +16311,17 @@ export class Orchestrator {
|
|
|
15801
16311
|
|
|
15802
16312
|
async runLifecyclePolicyNow(storage: StorageManager = this.storage): Promise<{ memoriesAssessed: number }> {
|
|
15803
16313
|
const lifecycleCorpus = await storage.readAllMemories();
|
|
15804
|
-
|
|
16314
|
+
// Record the catalog write when the pass rewrote any frontmatter (codex NR-tS).
|
|
16315
|
+
if ((await this.runLifecyclePolicyPass(lifecycleCorpus, storage)) > 0) {
|
|
16316
|
+
this.markCatalogWrite(this.namespaceFromStorageDir(storage.dir), storage.dir);
|
|
16317
|
+
}
|
|
15805
16318
|
return { memoriesAssessed: lifecycleCorpus.length };
|
|
15806
16319
|
}
|
|
15807
16320
|
|
|
15808
16321
|
private async runLifecyclePolicyPass(
|
|
15809
16322
|
allMemories: MemoryFile[],
|
|
15810
16323
|
storage: StorageManager = this.storage,
|
|
15811
|
-
): Promise<
|
|
16324
|
+
): Promise<number> {
|
|
15812
16325
|
const now = new Date();
|
|
15813
16326
|
const nowIso = now.toISOString();
|
|
15814
16327
|
const countsByState: Record<LifecycleState, number> = {
|
|
@@ -15885,7 +16398,9 @@ export class Orchestrator {
|
|
|
15885
16398
|
if (wrote) updatedCount += 1;
|
|
15886
16399
|
}
|
|
15887
16400
|
|
|
15888
|
-
|
|
16401
|
+
// Report how many memories had frontmatter rewritten so callers can record a
|
|
16402
|
+
// catalog write touch for lifecycle-only passes (codex NR-tS).
|
|
16403
|
+
if (!this.config.lifecycleMetricsEnabled) return updatedCount;
|
|
15889
16404
|
|
|
15890
16405
|
const total = evaluatedCount;
|
|
15891
16406
|
const metrics = {
|
|
@@ -15910,6 +16425,7 @@ export class Orchestrator {
|
|
|
15910
16425
|
);
|
|
15911
16426
|
await mkdir(path.dirname(metricsPath), { recursive: true });
|
|
15912
16427
|
await writeFile(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
|
|
16428
|
+
return updatedCount;
|
|
15913
16429
|
}
|
|
15914
16430
|
|
|
15915
16431
|
/**
|
|
@@ -16030,9 +16546,10 @@ export class Orchestrator {
|
|
|
16030
16546
|
new Date(b.frontmatter.created).getTime(),
|
|
16031
16547
|
);
|
|
16032
16548
|
|
|
16033
|
-
// Keep recent memories
|
|
16034
|
-
|
|
16035
|
-
const
|
|
16549
|
+
// Keep recent memories, with explicit zero handling so `slice(-0)` does not
|
|
16550
|
+
// accidentally keep every memory out of the summarization candidate set.
|
|
16551
|
+
const recentToKeep = Math.max(0, this.config.summarizationRecentToKeep);
|
|
16552
|
+
const toSummarize = recentToKeep > 0 ? sorted.slice(0, -recentToKeep) : sorted;
|
|
16036
16553
|
|
|
16037
16554
|
// Filter candidates for summarization
|
|
16038
16555
|
const candidates = toSummarize.filter((m) => {
|
|
@@ -16093,6 +16610,15 @@ export class Orchestrator {
|
|
|
16093
16610
|
summary.id,
|
|
16094
16611
|
);
|
|
16095
16612
|
|
|
16613
|
+
// Catalog write touch (issue #1499 sweep): summarization writes a durable
|
|
16614
|
+
// summary and then rewrites source-memory archive status, bypassing the
|
|
16615
|
+
// extraction write path. Record the touch after both mutations complete so
|
|
16616
|
+
// `lastWriteAt` covers the final archived-state write.
|
|
16617
|
+
this.markCatalogWrite(
|
|
16618
|
+
this.namespaceFromStorageDir(this.storage.dir),
|
|
16619
|
+
this.storage.dir,
|
|
16620
|
+
);
|
|
16621
|
+
|
|
16096
16622
|
log.info(
|
|
16097
16623
|
`created summary ${summary.id} from ${batch.length} memories, archived ${archived}`,
|
|
16098
16624
|
);
|
|
@@ -16127,8 +16653,12 @@ export class Orchestrator {
|
|
|
16127
16653
|
private static readonly IDENTITY_CONSOLIDATE_THRESHOLD = 8_000;
|
|
16128
16654
|
|
|
16129
16655
|
private async autoConsolidateIdentity(): Promise<void> {
|
|
16656
|
+
// Fan out over the catalog-union namespace set (issue #1499 sweep): a dynamic
|
|
16657
|
+
// namespace that accumulated IDENTITY.md reflections must also be eligible for
|
|
16658
|
+
// auto-consolidation, otherwise its identity file grows unbounded and is never
|
|
16659
|
+
// consolidated. Falls back to the configured set on any catalog read failure.
|
|
16130
16660
|
const namespaces = this.config.namespacesEnabled
|
|
16131
|
-
? this.
|
|
16661
|
+
? await this.maintenanceNamespaces()
|
|
16132
16662
|
: [this.config.defaultNamespace];
|
|
16133
16663
|
|
|
16134
16664
|
for (const namespace of namespaces) {
|
|
@@ -16190,6 +16720,19 @@ export class Orchestrator {
|
|
|
16190
16720
|
identityNamespace,
|
|
16191
16721
|
);
|
|
16192
16722
|
await storage.writeIdentityReflections("");
|
|
16723
|
+
// NRcCL (codex P2): record a per-namespace catalog write for THIS namespace
|
|
16724
|
+
// after the identity files are updated. This fan-out can mutate a dynamic
|
|
16725
|
+
// namespace via `writeIdentity`/`writeIdentityReflections`, but the
|
|
16726
|
+
// consolidation pass's only consolidated touch covers `this.storage` (the
|
|
16727
|
+
// default) and only fires when `memoryItemMutated` was set by OTHER work — so
|
|
16728
|
+
// a namespace whose sole mutation in the pass is identity consolidation would
|
|
16729
|
+
// otherwise keep a stale `lastWriteAt`, making `listNamespaces({ writtenSince })`
|
|
16730
|
+
// and catalog-recency consumers miss the write. Best-effort and
|
|
16731
|
+
// failure-tolerant (`markCatalogWrite` swallows errors, never crashing the
|
|
16732
|
+
// consolidation; gotcha #13, rule #40). No double-count with the consolidated
|
|
16733
|
+
// touch above: that one is gated on `memoryItemMutated` (which identity
|
|
16734
|
+
// consolidation does not set), and `markWrite` is idempotent regardless.
|
|
16735
|
+
this.markCatalogWrite(namespace, storage.dir);
|
|
16193
16736
|
log.info(
|
|
16194
16737
|
`IDENTITY(${namespace}) consolidated: ${identityContent.length} → ${newContent.length} chars, ${result.learnedPatterns.length} patterns`,
|
|
16195
16738
|
);
|
|
@@ -18538,7 +19081,75 @@ export class Orchestrator {
|
|
|
18538
19081
|
return this.config.defaultNamespace;
|
|
18539
19082
|
const m = resolvedStorageDir.match(/[\\/]namespaces[\\/]([^\\/]+)$/);
|
|
18540
19083
|
if (!m?.[1]) return this.config.defaultNamespace;
|
|
18541
|
-
|
|
19084
|
+
const dirName = m[1];
|
|
19085
|
+
// Token-shaped raw names (round 6, codex P2 — NBsFz): a dir name might be a
|
|
19086
|
+
// tokenized identity OR a literal raw namespace name that merely LOOKS like a
|
|
19087
|
+
// token (e.g. a configured or dynamic name `ns-616c706861`). The round-trip check below
|
|
19088
|
+
// (`namespaceIdentityToken(decoded) === dirName`) is TAUTOLOGICAL for a
|
|
19089
|
+
// canonical token string, so it cannot tell a tokenized dir for `alpha` apart
|
|
19090
|
+
// from the legacy raw root of a namespace literally named `ns-616c706861`
|
|
19091
|
+
// (codex NRCve). A dir name that is itself a KNOWN namespace (configured or
|
|
19092
|
+
// catalog-owned at this exact storage root) is therefore preserved as the
|
|
19093
|
+
// literal namespace BEFORE attempting to decode it.
|
|
19094
|
+
if (this.configuredNamespaces().includes(dirName)) {
|
|
19095
|
+
return dirName;
|
|
19096
|
+
}
|
|
19097
|
+
this.loadNamespaceStorageDirHintsFromCatalog();
|
|
19098
|
+
const hintedNamespaces = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
19099
|
+
if (hintedNamespaces?.has(dirName)) {
|
|
19100
|
+
return dirName;
|
|
19101
|
+
}
|
|
19102
|
+
if (hintedNamespaces?.size === 1) {
|
|
19103
|
+
const [hintedNamespace] = hintedNamespaces;
|
|
19104
|
+
if (hintedNamespace) return hintedNamespace;
|
|
19105
|
+
}
|
|
19106
|
+
const decoded = namespaceIdentityFromToken(dirName);
|
|
19107
|
+
if (decoded && namespaceIdentityToken(decoded) === dirName) {
|
|
19108
|
+
return decoded;
|
|
19109
|
+
}
|
|
19110
|
+
return dirName;
|
|
19111
|
+
}
|
|
19112
|
+
|
|
19113
|
+
/**
|
|
19114
|
+
* Record a namespace write in the catalog (issue #1499). Best-effort and
|
|
19115
|
+
* failure-tolerant: a catalog write error MUST NOT crash the primary memory
|
|
19116
|
+
* write (CLAUDE.md gotcha #13, rule #40). Fire-and-forget by design.
|
|
19117
|
+
*/
|
|
19118
|
+
private markCatalogWrite(namespace: string, storageDir?: string): void {
|
|
19119
|
+
if (!this.namespaceCatalog.enabled) return;
|
|
19120
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
19121
|
+
void this.namespaceCatalog
|
|
19122
|
+
.markWrite(namespace, { discoveredBy: "write", storageDir })
|
|
19123
|
+
.catch(() => undefined);
|
|
19124
|
+
}
|
|
19125
|
+
|
|
19126
|
+
/**
|
|
19127
|
+
* Public best-effort catalog write touch (issue #1499). User-facing explicit
|
|
19128
|
+
* captures (`memory_store`) and review-queue approvals persist via
|
|
19129
|
+
* `persistExplicitCapture()` → `storage.writeMemory()`, which bypasses the
|
|
19130
|
+
* extraction write path that calls `markCatalogWrite`. Without this their
|
|
19131
|
+
* namespaces never record `lastWriteAt`, so the catalog under-reports write
|
|
19132
|
+
* recency (round 5, codex P2). Fire-and-forget and failure-tolerant — a
|
|
19133
|
+
* catalog error must never affect the explicit write (gotcha #13, rule #40).
|
|
19134
|
+
*
|
|
19135
|
+
* An undefined/empty `namespace` means the write targeted the DEFAULT namespace
|
|
19136
|
+
* (`getStorage(undefined)` routes there), so we record it under the configured
|
|
19137
|
+
* default rather than skipping it (round 6, codex P2 — default `memory_store`
|
|
19138
|
+
* and inline-note writes were missing from `writtenSince`/maintenance).
|
|
19139
|
+
*/
|
|
19140
|
+
recordCatalogWrite(namespace?: string, storageDir?: string): void {
|
|
19141
|
+
const ns = namespace && namespace.trim().length > 0 ? namespace : this.config.defaultNamespace;
|
|
19142
|
+
if (!ns) return;
|
|
19143
|
+
this.markCatalogWrite(ns, storageDir);
|
|
19144
|
+
}
|
|
19145
|
+
|
|
19146
|
+
/** Record a namespace read in the catalog. Best-effort, failure-tolerant. */
|
|
19147
|
+
private markCatalogRead(namespace: string, storageDir?: string): void {
|
|
19148
|
+
if (!this.namespaceCatalog.enabled) return;
|
|
19149
|
+
this.rememberNamespaceStorageDirHint(namespace, storageDir);
|
|
19150
|
+
void this.namespaceCatalog
|
|
19151
|
+
.markRead(namespace, { discoveredBy: "read", storageDir })
|
|
19152
|
+
.catch(() => undefined);
|
|
18542
19153
|
}
|
|
18543
19154
|
|
|
18544
19155
|
private async readAllMemoriesForNamespaces(
|