@remnic/core 9.3.652 → 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 +11 -11
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +10 -10
- package/dist/access-schema.d.ts +15 -12
- package/dist/access-schema.js +1 -1
- 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-MGGNV3H2.js → chunk-GPW2E4LN.js} +23 -8
- package/dist/chunk-GPW2E4LN.js.map +1 -0
- package/dist/{chunk-KWM33SPU.js → chunk-JMQSYGXS.js} +2 -2
- package/dist/{chunk-WSFNYPAT.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-5V3TAB7D.js → chunk-LJCEWTG3.js} +19 -8
- package/dist/{chunk-5V3TAB7D.js.map → chunk-LJCEWTG3.js.map} +1 -1
- package/dist/{chunk-YOVKPOMD.js → chunk-NAZWHTYV.js} +13 -6
- 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-TCX4WLKK.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-4HYSMH7D.js → chunk-UAU5U5ML.js} +3 -2
- package/dist/chunk-UAU5U5ML.js.map +1 -0
- package/dist/{chunk-V4UDXYGG.js → chunk-XWQ6ERUG.js} +2 -2
- package/dist/{chunk-IJHLC5CH.js → chunk-Y2RIIF6H.js} +32 -22
- package/dist/{chunk-IJHLC5CH.js.map → chunk-Y2RIIF6H.js.map} +1 -1
- 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 +23 -23
- 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 +31 -29
- 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.test.ts +70 -1
- package/src/access-mcp.ts +19 -2
- package/src/access-schema.ts +1 -0
- package/src/access-service.ts +12 -0
- package/src/briefing.test.ts +70 -0
- package/src/briefing.ts +30 -20
- 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-4HYSMH7D.js.map +0 -1
- package/dist/chunk-C43KEWEV.js.map +0 -1
- package/dist/chunk-C63WC454.js.map +0 -1
- package/dist/chunk-H3PHZLMF.js.map +0 -1
- package/dist/chunk-MGGNV3H2.js.map +0 -1
- package/dist/chunk-ORGWWNJG.js +0 -131
- package/dist/chunk-ORGWWNJG.js.map +0 -1
- package/dist/chunk-R3PQUPQ4.js.map +0 -1
- package/dist/chunk-TCX4WLKK.js.map +0 -1
- package/dist/chunk-WSFNYPAT.js.map +0 -1
- package/dist/chunk-YOVKPOMD.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-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
|
@@ -104,52 +104,156 @@ async function hasAnyNamespaceStorageMarker(
|
|
|
104
104
|
* This avoids surprising "lost memories" when an install flips namespaces on without
|
|
105
105
|
* migrating existing data.
|
|
106
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* Optional hooks for the storage router. `onResolve` fires whenever a namespace's
|
|
109
|
+
* storage is resolved/created, so a downstream consumer (e.g. the namespace
|
|
110
|
+
* catalog, issue #1499) can register the namespace. The hook MUST NOT throw into
|
|
111
|
+
* the router; the router invokes it defensively and a hook failure never affects
|
|
112
|
+
* storage resolution.
|
|
113
|
+
*
|
|
114
|
+
* The hook MAY return (or resolve to) a boolean indicating whether the
|
|
115
|
+
* registration actually PERSISTED (round 6, codex P2 — NEFoX). When it resolves
|
|
116
|
+
* to `false` (a dropped/no-op registration), the router does NOT mark the
|
|
117
|
+
* (namespace, storageDir) pair as notified, so the next resolve RETRIES it
|
|
118
|
+
* instead of suppressing it forever. A `void`/`undefined` result is treated as
|
|
119
|
+
* success (legacy hooks).
|
|
120
|
+
*/
|
|
121
|
+
export interface NamespaceStorageRouterHooks {
|
|
122
|
+
onResolve?: (
|
|
123
|
+
namespace: string,
|
|
124
|
+
storageDir: string,
|
|
125
|
+
) => void | boolean | Promise<void | boolean>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the runtime storage root for the configured DEFAULT namespace.
|
|
130
|
+
*
|
|
131
|
+
* Shared between the live router (`NamespaceStorageRouter.defaultNamespaceRoot`)
|
|
132
|
+
* and the rebuildable catalog (`NamespaceCatalog.rebuildFromDisk`) so the two
|
|
133
|
+
* can never diverge (CLAUDE.md rule #22/#42 — read & write paths resolve through
|
|
134
|
+
* the same logic). The contract is: while legacy memory data still lives
|
|
135
|
+
* directly under `memoryDir`, the default root stays `memoryDir`; only once the
|
|
136
|
+
* legacy root is empty and a `namespaces/<default|token>` dir holds data does
|
|
137
|
+
* the default migrate into that tokenized/legacy-named dir.
|
|
138
|
+
*/
|
|
139
|
+
export async function resolveDefaultNamespaceRoot(config: PluginConfig): Promise<string> {
|
|
140
|
+
if (!config.namespacesEnabled) {
|
|
141
|
+
return config.memoryDir;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build the legacy default root from the NORMALIZED (trimmed) name so a
|
|
145
|
+
// whitespace-padded `defaultNamespace` still finds the live `namespaces/default`
|
|
146
|
+
// root (NIabe). `storageFor()` classifies the trimmed value as the default, and
|
|
147
|
+
// the on-disk legacy dir is created under the trimmed name; using the raw spaced
|
|
148
|
+
// name here would look for `namespaces/<spaced>` and miss the real root, falling
|
|
149
|
+
// back to memoryDir/tokenized. `namespaceIdentityToken` already normalizes
|
|
150
|
+
// internally, so the tokenized path is unaffected.
|
|
151
|
+
const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
152
|
+
const legacyNsDir = resolveNamespaceDir(config.memoryDir, defaultIdentity);
|
|
153
|
+
const tokenizedNsDir = resolveNamespaceDir(
|
|
154
|
+
config.memoryDir,
|
|
155
|
+
namespaceIdentityToken(config.defaultNamespace),
|
|
156
|
+
);
|
|
157
|
+
const tokenizedHasData =
|
|
158
|
+
(await exists(tokenizedNsDir)) &&
|
|
159
|
+
(await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));
|
|
160
|
+
const nsDir = tokenizedHasData
|
|
161
|
+
? tokenizedNsDir
|
|
162
|
+
: (await exists(legacyNsDir))
|
|
163
|
+
? legacyNsDir
|
|
164
|
+
: tokenizedNsDir;
|
|
165
|
+
return (await exists(nsDir)) && !(await hasAnyLegacyData(config.memoryDir))
|
|
166
|
+
? nsDir
|
|
167
|
+
: config.memoryDir;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolve the runtime storage root for ANY namespace exactly as the live router
|
|
172
|
+
* would (`NamespaceStorageRouter.namespaceRoot`). Shared so the rebuildable
|
|
173
|
+
* catalog records the SAME on-disk root the router routes to — a recall/read
|
|
174
|
+
* touch must not guess `namespaces/<token>` when the router actually serves a
|
|
175
|
+
* legacy raw-name dir or a migrated default root (CLAUDE.md rule #22/#42; round
|
|
176
|
+
* 4, cursor Medium). The default namespace delegates to `resolveDefaultNamespaceRoot`;
|
|
177
|
+
* every other namespace prefers the tokenized root when it has a storage marker,
|
|
178
|
+
* else a legacy raw-name dir when present, else the tokenized root.
|
|
179
|
+
*/
|
|
180
|
+
export async function resolveNamespaceStorageRoot(
|
|
181
|
+
config: PluginConfig,
|
|
182
|
+
namespace: string,
|
|
183
|
+
): Promise<string> {
|
|
184
|
+
if (!config.namespacesEnabled) return config.memoryDir;
|
|
185
|
+
// Compare on NORMALIZED identity so a whitespace-padded configured default name
|
|
186
|
+
// still routes to the default root rather than a tokenized non-default dir
|
|
187
|
+
// (NH-FH). The catalog keys records by the same normalized identity.
|
|
188
|
+
if (normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(config.defaultNamespace)) {
|
|
189
|
+
return resolveDefaultNamespaceRoot(config);
|
|
190
|
+
}
|
|
191
|
+
const legacyRoot = resolveNamespaceDir(config.memoryDir, namespace);
|
|
192
|
+
const tokenizedRoot = resolveNamespaceDir(config.memoryDir, namespaceIdentityToken(namespace));
|
|
193
|
+
if (
|
|
194
|
+
(await exists(tokenizedRoot)) &&
|
|
195
|
+
(await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))
|
|
196
|
+
) {
|
|
197
|
+
return tokenizedRoot;
|
|
198
|
+
}
|
|
199
|
+
return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;
|
|
200
|
+
}
|
|
201
|
+
|
|
107
202
|
export class NamespaceStorageRouter {
|
|
108
203
|
private readonly cache = new Map<string, StorageManager>();
|
|
109
204
|
private defaultNsRootResolved: string | null = null;
|
|
205
|
+
// Dedup the resolve hook (round 6, cursor Medium — NCNL2). Recall/extraction
|
|
206
|
+
// call `storageFor` repeatedly; firing `onResolve` (→ catalog loadCompacted +
|
|
207
|
+
// append) on every cache hit grows `namespaces.jsonl` without bound between
|
|
208
|
+
// rebuilds. We fire the hook only when the (namespace, storageDir) pair is new
|
|
209
|
+
// or its dir changed, so a steady-state cache hit is a no-op for the catalog.
|
|
210
|
+
private readonly notifiedResolved = new Map<string, string>();
|
|
211
|
+
// In-flight resolve-hook dedup (NFJV-, codex P2). The catalog's `onResolve`
|
|
212
|
+
// hook is ASYNC (it returns `registerResolved(...)`), so `notifiedResolved` is
|
|
213
|
+
// only set after the hook's promise SETTLES. Without tracking the in-flight
|
|
214
|
+
// window, a burst of `storageFor()` cache hits for the SAME namespace before
|
|
215
|
+
// the first registration finishes would each pass the `notifiedResolved` guard
|
|
216
|
+
// and fire their OWN `onResolve` — queueing N duplicate catalog touches + lock
|
|
217
|
+
// acquisitions despite the once-per-namespace intent. We therefore record the
|
|
218
|
+
// (namespace → storageDir) being registered BEFORE awaiting the hook so a
|
|
219
|
+
// concurrent call for the same pair skips firing. On SUCCESS the pair is
|
|
220
|
+
// promoted to `notifiedResolved` (future calls skip permanently); on `false`
|
|
221
|
+
// (dropped touch — e.g. rebuild-lock timeout) OR rejection the in-flight marker
|
|
222
|
+
// is CLEARED so a later `storageFor()` can RETRY the dropped registration. The
|
|
223
|
+
// entry is always removed when the promise settles, so the map cannot grow
|
|
224
|
+
// unbounded (one transient entry per concurrently-resolving namespace).
|
|
225
|
+
private readonly inFlightResolved = new Map<string, string>();
|
|
110
226
|
|
|
111
|
-
|
|
227
|
+
// Normalized (trimmed) default namespace identity (NH-FH). `storageFor`
|
|
228
|
+
// normalizes its input, so default-namespace branches must compare against the
|
|
229
|
+
// normalized config default too — otherwise a whitespace-padded configured
|
|
230
|
+
// default name routes the default namespace to a tokenized non-default root.
|
|
231
|
+
private readonly defaultNamespaceIdentity: string;
|
|
112
232
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
233
|
+
constructor(
|
|
234
|
+
private readonly config: PluginConfig,
|
|
235
|
+
private readonly hooks: NamespaceStorageRouterHooks = {},
|
|
236
|
+
) {
|
|
237
|
+
this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
238
|
+
}
|
|
118
239
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.config.memoryDir,
|
|
122
|
-
namespaceIdentityToken(this.config.defaultNamespace),
|
|
123
|
-
);
|
|
124
|
-
const tokenizedHasData =
|
|
125
|
-
(await exists(tokenizedNsDir)) && (await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));
|
|
126
|
-
const nsDir = tokenizedHasData
|
|
127
|
-
? tokenizedNsDir
|
|
128
|
-
: (await exists(legacyNsDir)) ? legacyNsDir : tokenizedNsDir;
|
|
129
|
-
this.defaultNsRootResolved =
|
|
130
|
-
(await exists(nsDir)) && !(await hasAnyLegacyData(this.config.memoryDir))
|
|
131
|
-
? nsDir
|
|
132
|
-
: this.config.memoryDir;
|
|
240
|
+
private async defaultNamespaceRoot(): Promise<string> {
|
|
241
|
+
this.defaultNsRootResolved = await resolveDefaultNamespaceRoot(this.config);
|
|
133
242
|
return this.defaultNsRootResolved;
|
|
134
243
|
}
|
|
135
244
|
|
|
136
245
|
private async namespaceRoot(namespace: string): Promise<string> {
|
|
137
246
|
// NOTE: only used after defaultNamespaceRoot() resolution.
|
|
138
247
|
if (!this.config.namespacesEnabled) return this.config.memoryDir;
|
|
139
|
-
if (namespace === this.
|
|
248
|
+
if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {
|
|
140
249
|
return this.defaultNsRootResolved ?? this.config.memoryDir;
|
|
141
250
|
}
|
|
142
|
-
|
|
143
|
-
const tokenizedRoot = resolveNamespaceDir(this.config.memoryDir, namespaceIdentityToken(namespace));
|
|
144
|
-
if ((await exists(tokenizedRoot)) && (await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))) {
|
|
145
|
-
return tokenizedRoot;
|
|
146
|
-
}
|
|
147
|
-
return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;
|
|
251
|
+
return resolveNamespaceStorageRoot(this.config, namespace);
|
|
148
252
|
}
|
|
149
253
|
|
|
150
254
|
async storageFor(namespace: string): Promise<StorageManager> {
|
|
151
255
|
const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);
|
|
152
|
-
if (ns !== this.
|
|
256
|
+
if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {
|
|
153
257
|
throw new Error(`unsafe namespace: ${ns}`);
|
|
154
258
|
}
|
|
155
259
|
// Even when the default namespace is exempt from the check above, every
|
|
@@ -158,16 +262,20 @@ export class NamespaceStorageRouter {
|
|
|
158
262
|
// <memoryDir>/namespaces (CodeQL js/path-injection).
|
|
159
263
|
|
|
160
264
|
let root: string;
|
|
161
|
-
if (ns === this.
|
|
265
|
+
if (ns === this.defaultNamespaceIdentity) {
|
|
162
266
|
root = await this.defaultNamespaceRoot();
|
|
163
267
|
const cached = this.cache.get(ns);
|
|
164
268
|
if (cached && cached.dir === root) {
|
|
269
|
+
this.notifyResolved(ns, root);
|
|
165
270
|
return cached;
|
|
166
271
|
}
|
|
167
272
|
} else {
|
|
168
273
|
const cached = this.cache.get(ns);
|
|
169
274
|
root = await this.namespaceRoot(ns);
|
|
170
|
-
if (cached && cached.dir === root)
|
|
275
|
+
if (cached && cached.dir === root) {
|
|
276
|
+
this.notifyResolved(ns, root);
|
|
277
|
+
return cached;
|
|
278
|
+
}
|
|
171
279
|
}
|
|
172
280
|
|
|
173
281
|
const sm = new StorageManager(root, this.config.entitySchemas);
|
|
@@ -176,6 +284,78 @@ export class NamespaceStorageRouter {
|
|
|
176
284
|
// matching the behaviour of the primary this.storage instance in the orchestrator.
|
|
177
285
|
sm.citationTemplate = this.config.inlineSourceAttributionFormat;
|
|
178
286
|
this.cache.set(ns, sm);
|
|
287
|
+
this.notifyResolved(ns, root);
|
|
179
288
|
return sm;
|
|
180
289
|
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Fire the resolve hook defensively. A hook failure (e.g. a catalog write
|
|
293
|
+
* error) MUST NOT crash storage resolution — see CLAUDE.md gotcha #13.
|
|
294
|
+
*/
|
|
295
|
+
private notifyResolved(namespace: string, storageDir: string): void {
|
|
296
|
+
const hook = this.hooks.onResolve;
|
|
297
|
+
if (!hook) return;
|
|
298
|
+
// Skip when we've already SUCCESSFULLY notified this exact (namespace,
|
|
299
|
+
// storageDir) — a steady-state cache hit must not re-append to the catalog
|
|
300
|
+
// log (NCNL2). A changed dir (rare: migration/realignment) still re-fires
|
|
301
|
+
// once. We mark the pair as notified ONLY AFTER the hook succeeds, and CLEAR
|
|
302
|
+
// it on failure, so a dropped registration (e.g. rebuild-lock timeout) is
|
|
303
|
+
// RETRIED on the next cache hit instead of being suppressed forever (round 6,
|
|
304
|
+
// cursor Medium — ND3EJ).
|
|
305
|
+
if (this.notifiedResolved.get(namespace) === storageDir) return;
|
|
306
|
+
// In-flight dedup (NFJV-, codex P2): if a registration for this exact
|
|
307
|
+
// (namespace, storageDir) is already AWAITING its async hook, do not fire a
|
|
308
|
+
// second one. Without this, concurrent cache-hit bursts before the first
|
|
309
|
+
// append settles each pass the `notifiedResolved` guard above and queue
|
|
310
|
+
// duplicate catalog touches/lock acquisitions. A pair with a DIFFERENT
|
|
311
|
+
// in-flight dir (rare mid-migration realignment) still fires once.
|
|
312
|
+
if (this.inFlightResolved.get(namespace) === storageDir) return;
|
|
313
|
+
try {
|
|
314
|
+
// Handle BOTH synchronous throws and asynchronous rejections (round 6,
|
|
315
|
+
// codex P2 — NDo8C). The hook may be `async`; its rejected promise would
|
|
316
|
+
// bypass this try/catch and, where unhandled rejections are fatal, crash
|
|
317
|
+
// storage resolution. Mark the dedup pair as notified ONLY when the hook
|
|
318
|
+
// resolves to a PERSISTED result (round 6, codex P2 — NEFoX): a result of
|
|
319
|
+
// `false` means the registration was dropped/no-op (e.g. rebuild-lock
|
|
320
|
+
// timeout), so we must NOT suppress its retry. `void`/`undefined` is treated
|
|
321
|
+
// as success for legacy hooks. On rejection we leave it un-notified to retry.
|
|
322
|
+
//
|
|
323
|
+
// Record the in-flight marker BEFORE awaiting so concurrent calls for the
|
|
324
|
+
// same pair skip (NFJV-). It is always cleared once the promise settles, so
|
|
325
|
+
// the map holds at most one transient entry per concurrently-resolving
|
|
326
|
+
// namespace and cannot grow unbounded.
|
|
327
|
+
this.inFlightResolved.set(namespace, storageDir);
|
|
328
|
+
Promise.resolve(hook(namespace, storageDir)).then(
|
|
329
|
+
(persisted) => {
|
|
330
|
+
// Clear the in-flight marker ONLY if it is still ours (a newer resolve
|
|
331
|
+
// for a different dir may have replaced it).
|
|
332
|
+
if (this.inFlightResolved.get(namespace) === storageDir) {
|
|
333
|
+
this.inFlightResolved.delete(namespace);
|
|
334
|
+
}
|
|
335
|
+
if (persisted !== false) {
|
|
336
|
+
this.notifiedResolved.set(namespace, storageDir);
|
|
337
|
+
}
|
|
338
|
+
// On `false` (dropped touch) we intentionally do NOT mark notified, so
|
|
339
|
+
// a later `storageFor()` retries the registration. Clearing the
|
|
340
|
+
// in-flight marker above is what re-enables that retry.
|
|
341
|
+
},
|
|
342
|
+
() => {
|
|
343
|
+
// Registration failed — clear in-flight AND do NOT mark as notified, so
|
|
344
|
+
// it is retried on the next cache hit.
|
|
345
|
+
if (this.inFlightResolved.get(namespace) === storageDir) {
|
|
346
|
+
this.inFlightResolved.delete(namespace);
|
|
347
|
+
}
|
|
348
|
+
if (this.notifiedResolved.get(namespace) === storageDir) {
|
|
349
|
+
this.notifiedResolved.delete(namespace);
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
} catch {
|
|
354
|
+
// Synchronous throw: clear any in-flight marker we just set and leave the
|
|
355
|
+
// pair un-notified so a later resolve retries.
|
|
356
|
+
if (this.inFlightResolved.get(namespace) === storageDir) {
|
|
357
|
+
this.inFlightResolved.delete(namespace);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
181
361
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
|
3
6
|
import {
|
|
4
7
|
BulkImportBatchPartialFailureError,
|
|
5
8
|
Orchestrator,
|
|
@@ -7,6 +10,7 @@ import {
|
|
|
7
10
|
import { parseConfig } from "./config.js";
|
|
8
11
|
import type { BufferTurn } from "./types.js";
|
|
9
12
|
import type { ImportTurn } from "./bulk-import/types.js";
|
|
13
|
+
import { namespaceIdentityToken } from "./namespaces/identity.js";
|
|
10
14
|
|
|
11
15
|
function makeTurn(sessionKey: string, content: string): BufferTurn {
|
|
12
16
|
return {
|
|
@@ -1767,3 +1771,299 @@ test("runExtraction still clears the session buffer after persistence even if re
|
|
|
1767
1771
|
"persisted reset flushes must still clear the session buffer even when the reset timeout aborts after persistence",
|
|
1768
1772
|
);
|
|
1769
1773
|
});
|
|
1774
|
+
|
|
1775
|
+
// ── NGnei (codex P2): runQmdMaintenance must cover CATALOGED dynamic namespaces,
|
|
1776
|
+
// not only the configured set. An extraction writing to a coding-scoped/dynamic
|
|
1777
|
+
// namespace is made discoverable via the catalog; if maintenance embeds only
|
|
1778
|
+
// configuredNamespaces(), that namespace's QMD collection stays stale. We stub the
|
|
1779
|
+
// orchestrator internals and assert update/embed receive the UNION of configured +
|
|
1780
|
+
// cataloged namespaces.
|
|
1781
|
+
test("runQmdMaintenance updates and embeds cataloged dynamic namespaces (NGnei)", async () => {
|
|
1782
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1783
|
+
let updateArg: string[] | undefined;
|
|
1784
|
+
let embedArg: string[] | undefined;
|
|
1785
|
+
const memoryDir = path.join(os.tmpdir(), "remnic-qmd-maintenance-ngnei");
|
|
1786
|
+
const dynamicNamespace = "project-origin-dynamic";
|
|
1787
|
+
const dynamicStorageDir = path.join(
|
|
1788
|
+
memoryDir,
|
|
1789
|
+
"namespaces",
|
|
1790
|
+
namespaceIdentityToken(dynamicNamespace),
|
|
1791
|
+
);
|
|
1792
|
+
await mkdir(path.join(dynamicStorageDir, "facts"), { recursive: true });
|
|
1793
|
+
|
|
1794
|
+
orchestrator.config = {
|
|
1795
|
+
memoryDir,
|
|
1796
|
+
namespacesEnabled: true,
|
|
1797
|
+
defaultNamespace: "default",
|
|
1798
|
+
sharedNamespace: "shared",
|
|
1799
|
+
namespacePolicies: [],
|
|
1800
|
+
qmdAutoEmbedEnabled: true,
|
|
1801
|
+
qmdEmbedMinIntervalMs: 0,
|
|
1802
|
+
};
|
|
1803
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
1804
|
+
orchestrator.qmdMaintenancePending = true;
|
|
1805
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
1806
|
+
orchestrator.namespaceCatalog = {
|
|
1807
|
+
enabled: true,
|
|
1808
|
+
async listNamespaces() {
|
|
1809
|
+
return [
|
|
1810
|
+
{ namespace: "default" },
|
|
1811
|
+
{
|
|
1812
|
+
namespace: dynamicNamespace,
|
|
1813
|
+
identityToken: namespaceIdentityToken(dynamicNamespace),
|
|
1814
|
+
kind: "project",
|
|
1815
|
+
createdAt: "2026-04-12T12:00:00.000Z",
|
|
1816
|
+
storageDir: dynamicStorageDir,
|
|
1817
|
+
discoveredBy: "write",
|
|
1818
|
+
}, // dynamic, NOT configured
|
|
1819
|
+
];
|
|
1820
|
+
},
|
|
1821
|
+
};
|
|
1822
|
+
orchestrator.namespaceSearchRouter = {
|
|
1823
|
+
async updateNamespaces(ns: string[]) {
|
|
1824
|
+
updateArg = ns;
|
|
1825
|
+
return ns.length;
|
|
1826
|
+
},
|
|
1827
|
+
async embedNamespaces(ns: string[]) {
|
|
1828
|
+
embedArg = ns;
|
|
1829
|
+
},
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
await orchestrator.runQmdMaintenance();
|
|
1833
|
+
|
|
1834
|
+
assert.ok(updateArg, "updateNamespaces must be called");
|
|
1835
|
+
assert.ok(
|
|
1836
|
+
updateArg!.includes(dynamicNamespace),
|
|
1837
|
+
"QMD update must cover the cataloged dynamic namespace, not just configured ones",
|
|
1838
|
+
);
|
|
1839
|
+
assert.ok(
|
|
1840
|
+
updateArg!.includes("default") && updateArg!.includes("shared"),
|
|
1841
|
+
"configured namespaces remain covered",
|
|
1842
|
+
);
|
|
1843
|
+
assert.ok(
|
|
1844
|
+
embedArg && embedArg.includes(dynamicNamespace),
|
|
1845
|
+
"QMD embed must cover the cataloged dynamic namespace",
|
|
1846
|
+
);
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
test("runQmdMaintenance skips cataloged dynamic namespaces whose live root is unsafe", async () => {
|
|
1850
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1851
|
+
let updateArg: string[] | undefined;
|
|
1852
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-root-"));
|
|
1853
|
+
const outsideDir = await mkdtemp(path.join(os.tmpdir(), "remnic-qmd-unsafe-target-"));
|
|
1854
|
+
try {
|
|
1855
|
+
const dynamicNamespace = "project-origin-symlinked";
|
|
1856
|
+
const liveLegacyRoot = path.join(memoryDir, "namespaces", dynamicNamespace);
|
|
1857
|
+
const catalogSafeRoot = path.join(
|
|
1858
|
+
memoryDir,
|
|
1859
|
+
"namespaces",
|
|
1860
|
+
namespaceIdentityToken(dynamicNamespace),
|
|
1861
|
+
);
|
|
1862
|
+
await mkdir(path.dirname(liveLegacyRoot), { recursive: true });
|
|
1863
|
+
await symlink(outsideDir, liveLegacyRoot, "dir");
|
|
1864
|
+
|
|
1865
|
+
orchestrator.config = {
|
|
1866
|
+
memoryDir,
|
|
1867
|
+
namespacesEnabled: true,
|
|
1868
|
+
defaultNamespace: "default",
|
|
1869
|
+
sharedNamespace: "shared",
|
|
1870
|
+
namespacePolicies: [],
|
|
1871
|
+
qmdAutoEmbedEnabled: false,
|
|
1872
|
+
qmdEmbedMinIntervalMs: 0,
|
|
1873
|
+
};
|
|
1874
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
1875
|
+
orchestrator.qmdMaintenancePending = true;
|
|
1876
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
1877
|
+
orchestrator.namespaceCatalog = {
|
|
1878
|
+
enabled: true,
|
|
1879
|
+
async listNamespaces() {
|
|
1880
|
+
return [
|
|
1881
|
+
{
|
|
1882
|
+
namespace: dynamicNamespace,
|
|
1883
|
+
identityToken: namespaceIdentityToken(dynamicNamespace),
|
|
1884
|
+
kind: "project",
|
|
1885
|
+
createdAt: "2026-04-12T12:00:00.000Z",
|
|
1886
|
+
storageDir: catalogSafeRoot,
|
|
1887
|
+
discoveredBy: "write",
|
|
1888
|
+
},
|
|
1889
|
+
];
|
|
1890
|
+
},
|
|
1891
|
+
};
|
|
1892
|
+
orchestrator.namespaceSearchRouter = {
|
|
1893
|
+
async updateNamespaces(ns: string[]) {
|
|
1894
|
+
updateArg = ns;
|
|
1895
|
+
return ns.length;
|
|
1896
|
+
},
|
|
1897
|
+
async embedNamespaces() {},
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
await orchestrator.runQmdMaintenance();
|
|
1901
|
+
|
|
1902
|
+
assert.ok(updateArg, "updateNamespaces must be called");
|
|
1903
|
+
assert.deepEqual(
|
|
1904
|
+
[...updateArg!].sort(),
|
|
1905
|
+
["default", "shared"],
|
|
1906
|
+
"cataloged dynamic namespaces are skipped when the live router root differs from the catalog-sanitized root",
|
|
1907
|
+
);
|
|
1908
|
+
} finally {
|
|
1909
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
1910
|
+
await rm(outsideDir, { recursive: true, force: true });
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// NGnei fallback: when the catalog is disabled, maintenance covers exactly the
|
|
1915
|
+
// configured set (no catalog read), and a catalog read failure degrades to the
|
|
1916
|
+
// configured set rather than breaking maintenance.
|
|
1917
|
+
test("runQmdMaintenance falls back to configured namespaces when the catalog is disabled (NGnei)", async () => {
|
|
1918
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1919
|
+
let updateArg: string[] | undefined;
|
|
1920
|
+
|
|
1921
|
+
orchestrator.config = {
|
|
1922
|
+
namespacesEnabled: true,
|
|
1923
|
+
defaultNamespace: "default",
|
|
1924
|
+
sharedNamespace: "shared",
|
|
1925
|
+
namespacePolicies: [],
|
|
1926
|
+
qmdAutoEmbedEnabled: false,
|
|
1927
|
+
qmdEmbedMinIntervalMs: 0,
|
|
1928
|
+
};
|
|
1929
|
+
orchestrator.qmdMaintenanceInFlight = false;
|
|
1930
|
+
orchestrator.qmdMaintenancePending = true;
|
|
1931
|
+
orchestrator.lastQmdEmbedAtMs = 0;
|
|
1932
|
+
orchestrator.namespaceCatalog = {
|
|
1933
|
+
enabled: false,
|
|
1934
|
+
async listNamespaces() {
|
|
1935
|
+
throw new Error("catalog disabled — must not be read");
|
|
1936
|
+
},
|
|
1937
|
+
};
|
|
1938
|
+
orchestrator.namespaceSearchRouter = {
|
|
1939
|
+
async updateNamespaces(ns: string[]) {
|
|
1940
|
+
updateArg = ns;
|
|
1941
|
+
return ns.length;
|
|
1942
|
+
},
|
|
1943
|
+
async embedNamespaces() {},
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
await orchestrator.runQmdMaintenance();
|
|
1947
|
+
|
|
1948
|
+
assert.ok(updateArg, "updateNamespaces must be called");
|
|
1949
|
+
assert.deepEqual(
|
|
1950
|
+
[...updateArg!].sort(),
|
|
1951
|
+
["default", "shared"],
|
|
1952
|
+
"a disabled catalog covers exactly the configured set",
|
|
1953
|
+
);
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
// ── NHZEV (codex P2): the QMD STARTUP sync in deferredInitialize() must cover
|
|
1957
|
+
// cataloged dynamic namespaces too, not only configuredNamespaces(). A dynamic
|
|
1958
|
+
// namespace written before a daemon restart exists ONLY in the persisted catalog;
|
|
1959
|
+
// if the boot-time "sync current disk state" pass embeds only the configured set,
|
|
1960
|
+
// that namespace's QMD collection stays stale after restart. We drive
|
|
1961
|
+
// deferredInitialize() with stubbed internals and abort the signal right after the
|
|
1962
|
+
// sync (the next `if (signal.aborted) return;` bails before warmup), then assert the
|
|
1963
|
+
// startup updateNamespaces() received the UNION of configured + cataloged namespaces.
|
|
1964
|
+
test("deferredInitialize startup sync covers cataloged dynamic namespaces (NHZEV)", async () => {
|
|
1965
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
1966
|
+
let updateArg: string[] | undefined;
|
|
1967
|
+
const abortController = new AbortController();
|
|
1968
|
+
const memoryDir = path.join(os.tmpdir(), "remnic-startup-maintenance-nhzev");
|
|
1969
|
+
const dynamicNamespace = "project-origin-dynamic";
|
|
1970
|
+
const dynamicStorageDir = path.join(
|
|
1971
|
+
memoryDir,
|
|
1972
|
+
"namespaces",
|
|
1973
|
+
namespaceIdentityToken(dynamicNamespace),
|
|
1974
|
+
);
|
|
1975
|
+
await mkdir(path.join(dynamicStorageDir, "facts"), { recursive: true });
|
|
1976
|
+
|
|
1977
|
+
orchestrator.config = {
|
|
1978
|
+
memoryDir,
|
|
1979
|
+
namespacesEnabled: true,
|
|
1980
|
+
defaultNamespace: "default",
|
|
1981
|
+
sharedNamespace: "shared",
|
|
1982
|
+
namespacePolicies: [],
|
|
1983
|
+
qmdMaintenanceEnabled: true,
|
|
1984
|
+
};
|
|
1985
|
+
orchestrator.qmd = {
|
|
1986
|
+
isAvailable: () => true,
|
|
1987
|
+
async update() {},
|
|
1988
|
+
};
|
|
1989
|
+
orchestrator.namespaceCatalog = {
|
|
1990
|
+
enabled: true,
|
|
1991
|
+
async listNamespaces() {
|
|
1992
|
+
return [
|
|
1993
|
+
{ namespace: "default" },
|
|
1994
|
+
{
|
|
1995
|
+
namespace: dynamicNamespace,
|
|
1996
|
+
identityToken: namespaceIdentityToken(dynamicNamespace),
|
|
1997
|
+
kind: "project",
|
|
1998
|
+
createdAt: "2026-04-12T12:00:00.000Z",
|
|
1999
|
+
storageDir: dynamicStorageDir,
|
|
2000
|
+
discoveredBy: "write",
|
|
2001
|
+
}, // dynamic, catalog-ONLY, NOT configured
|
|
2002
|
+
];
|
|
2003
|
+
},
|
|
2004
|
+
};
|
|
2005
|
+
orchestrator.namespaceSearchRouter = {
|
|
2006
|
+
async updateNamespaces(ns: string[]) {
|
|
2007
|
+
updateArg = ns;
|
|
2008
|
+
// Abort AFTER the startup sync records its arg so deferredInitialize bails
|
|
2009
|
+
// at the next `if (signal.aborted) return;` before warmup/caches run.
|
|
2010
|
+
abortController.abort();
|
|
2011
|
+
return ns.length;
|
|
2012
|
+
},
|
|
2013
|
+
};
|
|
2014
|
+
|
|
2015
|
+
await orchestrator.deferredInitialize(abortController.signal);
|
|
2016
|
+
|
|
2017
|
+
assert.ok(updateArg, "startup updateNamespaces must be called");
|
|
2018
|
+
assert.ok(
|
|
2019
|
+
updateArg!.includes(dynamicNamespace),
|
|
2020
|
+
"startup sync must cover the cataloged dynamic namespace (NHZEV), not just configured ones",
|
|
2021
|
+
);
|
|
2022
|
+
assert.ok(
|
|
2023
|
+
updateArg!.includes("default") && updateArg!.includes("shared"),
|
|
2024
|
+
"configured namespaces remain covered at startup",
|
|
2025
|
+
);
|
|
2026
|
+
});
|
|
2027
|
+
|
|
2028
|
+
// NHZEV fallback: a catalog read failure during startup sync must degrade to the
|
|
2029
|
+
// configured set rather than breaking deferredInitialize — same failure-tolerance
|
|
2030
|
+
// contract as runQmdMaintenance (maintenanceNamespaces swallows the read error).
|
|
2031
|
+
test("deferredInitialize startup sync falls back to configured set on catalog read failure (NHZEV)", async () => {
|
|
2032
|
+
const orchestrator = Object.create(Orchestrator.prototype) as any;
|
|
2033
|
+
let updateArg: string[] | undefined;
|
|
2034
|
+
const abortController = new AbortController();
|
|
2035
|
+
|
|
2036
|
+
orchestrator.config = {
|
|
2037
|
+
namespacesEnabled: true,
|
|
2038
|
+
defaultNamespace: "default",
|
|
2039
|
+
sharedNamespace: "shared",
|
|
2040
|
+
namespacePolicies: [],
|
|
2041
|
+
qmdMaintenanceEnabled: true,
|
|
2042
|
+
};
|
|
2043
|
+
orchestrator.qmd = {
|
|
2044
|
+
isAvailable: () => true,
|
|
2045
|
+
async update() {},
|
|
2046
|
+
};
|
|
2047
|
+
orchestrator.namespaceCatalog = {
|
|
2048
|
+
enabled: true,
|
|
2049
|
+
async listNamespaces() {
|
|
2050
|
+
throw new Error("catalog read failed");
|
|
2051
|
+
},
|
|
2052
|
+
};
|
|
2053
|
+
orchestrator.namespaceSearchRouter = {
|
|
2054
|
+
async updateNamespaces(ns: string[]) {
|
|
2055
|
+
updateArg = ns;
|
|
2056
|
+
abortController.abort();
|
|
2057
|
+
return ns.length;
|
|
2058
|
+
},
|
|
2059
|
+
};
|
|
2060
|
+
|
|
2061
|
+
await orchestrator.deferredInitialize(abortController.signal);
|
|
2062
|
+
|
|
2063
|
+
assert.ok(updateArg, "startup updateNamespaces must be called");
|
|
2064
|
+
assert.deepEqual(
|
|
2065
|
+
[...updateArg!].sort(),
|
|
2066
|
+
["default", "shared"],
|
|
2067
|
+
"a catalog read failure degrades startup sync to the configured set",
|
|
2068
|
+
);
|
|
2069
|
+
});
|