@remnic/core 9.7.11 → 9.7.13
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-admin-ops-surface.d.ts +6 -6
- package/dist/access-admin-ops-surface.js +7 -7
- package/dist/access-boundary.d.ts +6 -6
- package/dist/access-boundary.js +8 -8
- package/dist/access-cli.js +31 -31
- package/dist/access-http.d.ts +6 -6
- package/dist/access-http.js +12 -12
- package/dist/access-identity-continuity-surface.d.ts +5 -5
- package/dist/access-identity-continuity-surface.js +7 -7
- package/dist/access-lcm-surface.d.ts +6 -6
- package/dist/access-lcm-surface.js +7 -7
- package/dist/access-mcp.d.ts +6 -6
- package/dist/access-mcp.js +11 -11
- package/dist/access-observe-write-surface.d.ts +6 -6
- package/dist/access-observe-write-surface.js +7 -7
- package/dist/access-operations-batch.js +9 -9
- package/dist/access-operations.d.ts +9 -9
- package/dist/access-operations.js +10 -10
- package/dist/access-recall-surface.d.ts +6 -6
- package/dist/access-recall-surface.js +7 -7
- package/dist/access-schema.d.ts +64 -64
- package/dist/{access-service-DSuzikHQ.d.ts → access-service-tHd3eQpd.d.ts} +4 -4
- package/dist/access-service.d.ts +6 -6
- package/dist/access-service.js +7 -7
- package/dist/access-surface-catalog.d.ts +6 -6
- 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 +5 -5
- package/dist/briefing.d.ts +2 -2
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-BNwjACa5.d.ts → catalog-DEMiw6HZ.d.ts} +48 -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/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-U4KHQTJ6.js → chunk-2AL7N3BH.js} +4 -2
- package/dist/{chunk-U4KHQTJ6.js.map → chunk-2AL7N3BH.js.map} +1 -1
- package/dist/{chunk-L4H6MQ7I.js → chunk-2RSL6YRA.js} +2 -2
- package/dist/{chunk-QHT4W3RU.js → chunk-2V5QI6NQ.js} +41 -9
- package/dist/chunk-2V5QI6NQ.js.map +1 -0
- package/dist/{chunk-JYRFO4EH.js → chunk-2ZPQUTMW.js} +4 -4
- package/dist/{chunk-673NPXE3.js → chunk-3SIC3E65.js} +2 -2
- package/dist/{chunk-OVMS3DCB.js → chunk-675YU5IK.js} +2 -2
- package/dist/{chunk-7LMEY5KZ.js → chunk-6N7R7TR2.js} +34 -1
- package/dist/chunk-6N7R7TR2.js.map +1 -0
- package/dist/{chunk-TGBJ2OMK.js → chunk-BQTSGK5Y.js} +2 -2
- package/dist/{chunk-6LM6PPDX.js → chunk-CE3GMHT4.js} +2 -2
- package/dist/{chunk-YXOAQAH2.js → chunk-CLFY35ED.js} +3 -3
- package/dist/{chunk-HQ7QZJM5.js → chunk-D4TLWDYR.js} +2 -2
- package/dist/{chunk-LQKITFGB.js → chunk-FUUF7FMX.js} +2 -2
- package/dist/{chunk-3ZVFLMFQ.js → chunk-G5PVJJRA.js} +1 -1
- package/dist/chunk-G5PVJJRA.js.map +1 -0
- package/dist/{chunk-MXRMNKSY.js → chunk-I3GKTDM2.js} +3 -3
- package/dist/{chunk-EQF5763Z.js → chunk-IQULRIZM.js} +2 -2
- package/dist/{chunk-W6R43NLA.js → chunk-KQMKF4G2.js} +17 -17
- package/dist/{chunk-DJCKXI4M.js → chunk-NVXQTRBL.js} +2 -2
- package/dist/{chunk-GHVJASQ4.js → chunk-OCY3CRPG.js} +3 -3
- package/dist/{chunk-YLTG6BO4.js → chunk-OIFZII6J.js} +2 -2
- package/dist/{chunk-XF4KOVDK.js → chunk-PA6DMYEK.js} +6 -6
- package/dist/{chunk-3OQ4IZJN.js → chunk-QPZCAVDC.js} +7 -7
- package/dist/{chunk-DLWYAN6A.js → chunk-T6U7HDIY.js} +2 -2
- package/dist/{chunk-JZM5QUTE.js → chunk-TMGK6YE7.js} +171 -9
- package/dist/chunk-TMGK6YE7.js.map +1 -0
- package/dist/{chunk-KXLRMM35.js → chunk-TRH533CA.js} +30 -7
- package/dist/chunk-TRH533CA.js.map +1 -0
- package/dist/{chunk-UTDA43TB.js → chunk-TWGGNUWJ.js} +2 -2
- package/dist/{chunk-SBFDL2EQ.js → chunk-UTZW3X2R.js} +2 -2
- package/dist/{chunk-SECMZACR.js → chunk-XUQUBZ7O.js} +2 -2
- package/dist/{chunk-5U65RIVI.js → chunk-YI7XUXLX.js} +16 -15
- package/dist/chunk-YI7XUXLX.js.map +1 -0
- package/dist/{chunk-7XRUYC6G.js → chunk-Z6WN2KSH.js} +19 -5
- package/dist/chunk-Z6WN2KSH.js.map +1 -0
- package/dist/{chunk-WUDBMLC6.js → chunk-ZC3HG22I.js} +2 -2
- package/dist/{chunk-VCEH5UYS.js → chunk-ZRXSHGXT.js} +2 -2
- package/dist/{cli-DACyuK8-.d.ts → cli-D8Tsa4Sh.d.ts} +5 -5
- package/dist/cli.d.ts +8 -8
- package/dist/cli.js +25 -25
- package/dist/compounding/engine.d.ts +2 -2
- 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 +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- 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 +2 -2
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +5 -5
- package/dist/extraction-faithfulness.d.ts +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/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +386 -386
- package/dist/index.js +31 -31
- 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/local-model-endpoint.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 +6 -6
- 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 +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +3 -3
- 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 +3 -3
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +2 -2
- package/dist/operator-toolkit.js +8 -8
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-D83AvrPf.d.ts → orchestrator-B_WXBmuE.d.ts} +4 -4
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +31 -31
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-BSqGpYl1.d.ts → pipeline-Dvtvt141.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-CutnpRVe.d.ts → qmd-DwBLh_hl.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- 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-timings.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/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +2 -2
- 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/schemas.d.ts +74 -74
- 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-btIVRC4l.d.ts → semantic-consolidation-Dia_6FOn.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 +9 -9
- package/dist/signal.d.ts +1 -1
- package/dist/{storage-BVv_Mqa0.d.ts → storage-RshM7i8-.d.ts} +18 -1
- package/dist/storage.d.ts +2 -2
- 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 +2 -2
- package/dist/temporal-timeline-recall.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 +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +66 -66
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-Cmkz7csa.d.ts → types-CHSXOq7s.d.ts} +34 -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 +2 -2
- package/src/access-http.ts +40 -2
- package/src/config.test.ts +44 -0
- package/src/config.ts +43 -0
- package/src/namespaces/catalog.test.ts +258 -0
- package/src/namespaces/catalog.ts +235 -10
- package/src/namespaces/storage.ts +9 -0
- package/src/orchestrator.ts +3 -0
- package/src/relay/mission-access.test.ts +58 -3
- package/src/relay/mission-access.ts +17 -3
- package/src/relay/mission-ui.test.ts +90 -0
- package/src/storage-catalog-touch-gating.test.ts +70 -0
- package/src/storage.ts +31 -5
- package/src/types.ts +34 -0
- package/dist/chunk-3ZVFLMFQ.js.map +0 -1
- package/dist/chunk-5U65RIVI.js.map +0 -1
- package/dist/chunk-7LMEY5KZ.js.map +0 -1
- package/dist/chunk-7XRUYC6G.js.map +0 -1
- package/dist/chunk-JZM5QUTE.js.map +0 -1
- package/dist/chunk-KXLRMM35.js.map +0 -1
- package/dist/chunk-QHT4W3RU.js.map +0 -1
- /package/dist/{chunk-L4H6MQ7I.js.map → chunk-2RSL6YRA.js.map} +0 -0
- /package/dist/{chunk-JYRFO4EH.js.map → chunk-2ZPQUTMW.js.map} +0 -0
- /package/dist/{chunk-673NPXE3.js.map → chunk-3SIC3E65.js.map} +0 -0
- /package/dist/{chunk-OVMS3DCB.js.map → chunk-675YU5IK.js.map} +0 -0
- /package/dist/{chunk-TGBJ2OMK.js.map → chunk-BQTSGK5Y.js.map} +0 -0
- /package/dist/{chunk-6LM6PPDX.js.map → chunk-CE3GMHT4.js.map} +0 -0
- /package/dist/{chunk-YXOAQAH2.js.map → chunk-CLFY35ED.js.map} +0 -0
- /package/dist/{chunk-HQ7QZJM5.js.map → chunk-D4TLWDYR.js.map} +0 -0
- /package/dist/{chunk-LQKITFGB.js.map → chunk-FUUF7FMX.js.map} +0 -0
- /package/dist/{chunk-MXRMNKSY.js.map → chunk-I3GKTDM2.js.map} +0 -0
- /package/dist/{chunk-EQF5763Z.js.map → chunk-IQULRIZM.js.map} +0 -0
- /package/dist/{chunk-W6R43NLA.js.map → chunk-KQMKF4G2.js.map} +0 -0
- /package/dist/{chunk-DJCKXI4M.js.map → chunk-NVXQTRBL.js.map} +0 -0
- /package/dist/{chunk-GHVJASQ4.js.map → chunk-OCY3CRPG.js.map} +0 -0
- /package/dist/{chunk-YLTG6BO4.js.map → chunk-OIFZII6J.js.map} +0 -0
- /package/dist/{chunk-XF4KOVDK.js.map → chunk-PA6DMYEK.js.map} +0 -0
- /package/dist/{chunk-3OQ4IZJN.js.map → chunk-QPZCAVDC.js.map} +0 -0
- /package/dist/{chunk-DLWYAN6A.js.map → chunk-T6U7HDIY.js.map} +0 -0
- /package/dist/{chunk-UTDA43TB.js.map → chunk-TWGGNUWJ.js.map} +0 -0
- /package/dist/{chunk-SBFDL2EQ.js.map → chunk-UTZW3X2R.js.map} +0 -0
- /package/dist/{chunk-SECMZACR.js.map → chunk-XUQUBZ7O.js.map} +0 -0
- /package/dist/{chunk-WUDBMLC6.js.map → chunk-ZC3HG22I.js.map} +0 -0
- /package/dist/{chunk-VCEH5UYS.js.map → chunk-ZRXSHGXT.js.map} +0 -0
|
@@ -414,6 +414,10 @@ export class NamespaceStorageRouter {
|
|
|
414
414
|
* default-namespace storage (`this.storage`) that bypasses the router.
|
|
415
415
|
*/
|
|
416
416
|
bindCatalogWriteHook(sm: StorageManager, namespace: string): void {
|
|
417
|
+
// Issue #1903: pure state-file writes only touch the catalog when explicitly
|
|
418
|
+
// opted in. Router-created storages (storageFor) and the legacy default
|
|
419
|
+
// storage both flow through here, so this is the single wiring point.
|
|
420
|
+
sm.touchStateWrites = this.config.namespacesCatalogTouchStateWrites === true;
|
|
417
421
|
sm.onCatalogWrite = () => this.touchCatalogWrite(namespace, sm.dir);
|
|
418
422
|
}
|
|
419
423
|
|
|
@@ -505,6 +509,11 @@ export class NamespaceStorageRouter {
|
|
|
505
509
|
* moved should await this instead of racing a timer.
|
|
506
510
|
*/
|
|
507
511
|
async whenWriteTouchesSettled(): Promise<void> {
|
|
512
|
+
// Flush any coalesced (buffered) touches first so their appends join the
|
|
513
|
+
// pending set below — otherwise this could resolve while a buffered touch's
|
|
514
|
+
// lastWriteAt is still unwritten on disk (#1903, Cursor). No-op when
|
|
515
|
+
// coalescing is disabled (window 0) or the catalog is inert.
|
|
516
|
+
await this.catalog?.flushPendingTouches().catch(() => undefined);
|
|
508
517
|
while (this.pendingWriteTouches.size > 0) {
|
|
509
518
|
await Promise.allSettled([...this.pendingWriteTouches]);
|
|
510
519
|
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -948,6 +948,9 @@ export class Orchestrator {
|
|
|
948
948
|
}
|
|
949
949
|
this.maintenanceScheduler.dispose();
|
|
950
950
|
await drainRecallWrites(this);
|
|
951
|
+
// Issue #1903: flush any coalesced namespace-catalog touches before teardown
|
|
952
|
+
// so a long-lived host does not drop buffered read/write timestamps.
|
|
953
|
+
await this.namespaceCatalog.flushPendingTouches().catch(() => undefined);
|
|
951
954
|
await this.namespaceSearchRouter.dispose();
|
|
952
955
|
await (this.qmd as { dispose?: () => void | Promise<void> }).dispose?.();
|
|
953
956
|
if (this.conversationQmd && this.conversationQmd !== this.qmd) {
|
|
@@ -60,13 +60,33 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
|
|
|
60
60
|
service: fakeService(root, calls),
|
|
61
61
|
port: 0,
|
|
62
62
|
authToken: "relay-token",
|
|
63
|
-
principal: "
|
|
63
|
+
principal: "operator-build-week",
|
|
64
64
|
adminConsoleEnabled: false,
|
|
65
65
|
});
|
|
66
66
|
const status = await server.start();
|
|
67
67
|
const base = `http://127.0.0.1:${status.port}/engram/v1/relay/missions/${RELAY_DEMO_MISSION_ID}`;
|
|
68
68
|
try {
|
|
69
69
|
const fixture = createRelayMissionFixture();
|
|
70
|
+
const approval = fixture.find((event) => event.payload.kind === "correction_approved");
|
|
71
|
+
assert.ok(approval && approval.payload.kind === "correction_approved");
|
|
72
|
+
const mismatchedApproval = await fetch(`${base}/events`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: authHeaders(),
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
namespace: RELAY_DEMO_NAMESPACE,
|
|
77
|
+
event: {
|
|
78
|
+
...approval,
|
|
79
|
+
idempotencyKey: "mismatched-human-approval",
|
|
80
|
+
payload: {
|
|
81
|
+
...approval.payload,
|
|
82
|
+
approvedBy: { ...approval.payload.approvedBy, id: "different-operator" },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
assert.equal(mismatchedApproval.status, 400);
|
|
88
|
+
assert.equal(((await mismatchedApproval.json()) as { code: string }).code, "input_error");
|
|
89
|
+
|
|
70
90
|
const blankNamespace = await fetch(`${base}/events`, {
|
|
71
91
|
method: "POST",
|
|
72
92
|
headers: authHeaders(),
|
|
@@ -91,7 +111,7 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
|
|
|
91
111
|
assert.equal(response.status, 201, responseBody);
|
|
92
112
|
assert.equal(
|
|
93
113
|
(JSON.parse(responseBody) as { event: { authenticatedPrincipal?: string } }).event.authenticatedPrincipal,
|
|
94
|
-
"
|
|
114
|
+
"operator-build-week"
|
|
95
115
|
);
|
|
96
116
|
}
|
|
97
117
|
|
|
@@ -108,6 +128,7 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
|
|
|
108
128
|
});
|
|
109
129
|
assert.equal(response.status, 200);
|
|
110
130
|
assert.equal(response.headers.get("cache-control"), "no-store");
|
|
131
|
+
assert.equal(response.headers.get("x-remnic-authenticated-principal"), "operator-build-week");
|
|
111
132
|
const snapshot = (await response.json()) as RelayMissionSnapshot;
|
|
112
133
|
assert.equal(snapshot.receipt.complete, true);
|
|
113
134
|
assert.deepEqual(snapshot.receipt.activeDecisionIds, ["decision-refresh-after-expiry"]);
|
|
@@ -115,7 +136,40 @@ test("HTTP fixture endpoint returns one complete, namespace-authorized Relay rec
|
|
|
115
136
|
assert.equal(snapshot.agents.length, 3);
|
|
116
137
|
assert.equal(snapshot.tests.at(-1)?.status, "passed");
|
|
117
138
|
assert.ok(calls.every((call) => call.namespace === RELAY_DEMO_NAMESPACE));
|
|
118
|
-
assert.ok(calls.every((call) => call.principal === "
|
|
139
|
+
assert.ok(calls.every((call) => call.principal === "operator-build-week"));
|
|
140
|
+
} finally {
|
|
141
|
+
await server.stop();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("HTTP Relay rejects human approval without a server-resolved principal before persistence", async () => {
|
|
147
|
+
await withTempRoot(async (root) => {
|
|
148
|
+
const server = new EngramAccessHttpServer({
|
|
149
|
+
service: fakeService(root, []),
|
|
150
|
+
port: 0,
|
|
151
|
+
authToken: "relay-token",
|
|
152
|
+
adminConsoleEnabled: false,
|
|
153
|
+
});
|
|
154
|
+
const status = await server.start();
|
|
155
|
+
const base = `http://127.0.0.1:${status.port}/engram/v1/relay/missions/${RELAY_DEMO_MISSION_ID}`;
|
|
156
|
+
try {
|
|
157
|
+
const approval = createRelayMissionFixture().find((event) => event.payload.kind === "correction_approved");
|
|
158
|
+
assert.ok(approval);
|
|
159
|
+
const rejected = await fetch(`${base}/events`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: authHeaders(),
|
|
162
|
+
body: JSON.stringify({ namespace: RELAY_DEMO_NAMESPACE, event: approval }),
|
|
163
|
+
});
|
|
164
|
+
assert.equal(rejected.status, 400);
|
|
165
|
+
assert.equal(((await rejected.json()) as { code: string }).code, "input_error");
|
|
166
|
+
|
|
167
|
+
const snapshotResponse = await fetch(`${base}?namespace=${RELAY_DEMO_NAMESPACE}`, {
|
|
168
|
+
headers: authHeaders(),
|
|
169
|
+
});
|
|
170
|
+
assert.equal(snapshotResponse.status, 200);
|
|
171
|
+
assert.equal(snapshotResponse.headers.get("x-remnic-authenticated-principal"), null);
|
|
172
|
+
assert.equal(((await snapshotResponse.json()) as RelayMissionSnapshot).found, false);
|
|
119
173
|
} finally {
|
|
120
174
|
await server.stop();
|
|
121
175
|
}
|
|
@@ -150,6 +204,7 @@ test("HTTP Relay surface distinguishes empty, invalid, unauthorized, and backend
|
|
|
150
204
|
|
|
151
205
|
const empty = await fetch(emptyUrl, { headers: authHeaders("read-token") });
|
|
152
206
|
assert.equal(empty.status, 200);
|
|
207
|
+
assert.equal(empty.headers.get("x-remnic-authenticated-principal"), null);
|
|
153
208
|
assert.deepEqual(await empty.json(), assertEmptyMissionShape());
|
|
154
209
|
|
|
155
210
|
const badLimit = await fetch(`${emptyUrl}&limit=501`, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import { type OperationContext, defineOperation } from "../access-boundary.js";
|
|
4
|
-
import type
|
|
4
|
+
import { EngramAccessInputError, type EngramAccessService } from "../access-service.js";
|
|
5
5
|
import {
|
|
6
6
|
RELAY_MISSION_MAX_EVENT_LIMIT,
|
|
7
7
|
RelayIsoDateTimeSchema,
|
|
@@ -76,12 +76,26 @@ export async function appendRelayMissionEvent(
|
|
|
76
76
|
authenticatedPrincipal?: string,
|
|
77
77
|
beforeAppend?: () => void | Promise<void>
|
|
78
78
|
): Promise<RelayMissionAppendResult> {
|
|
79
|
-
const
|
|
79
|
+
const event = RelayMissionEventInputSchema.parse(input.event);
|
|
80
|
+
const approvalPrincipal = authenticatedPrincipal?.trim() || undefined;
|
|
81
|
+
if (event.payload.kind === "correction_approved" && event.payload.approvedBy.kind === "human") {
|
|
82
|
+
if (!approvalPrincipal) {
|
|
83
|
+
throw new EngramAccessInputError(
|
|
84
|
+
"Relay human approval requires a server-resolved authenticated principal"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (event.payload.approvedBy.id !== approvalPrincipal) {
|
|
88
|
+
throw new EngramAccessInputError(
|
|
89
|
+
"Relay human approval identity must match the server-resolved authenticated principal"
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const resolved = await service.getWritableStorageForNamespace(input.namespace ?? undefined, approvalPrincipal);
|
|
80
94
|
const store = new RelayMissionStore({
|
|
81
95
|
rootDir: resolved.storage.dir,
|
|
82
96
|
namespace: resolved.namespace,
|
|
83
97
|
});
|
|
84
|
-
return store.append(input.missionId,
|
|
98
|
+
return store.append(input.missionId, event, { authenticatedPrincipal: approvalPrincipal, beforeAppend });
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
/** Namespace-authorized, bounded Relay snapshot read shared by HTTP and UI. */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { EngramAccessHttpServer } from "../access-http.js";
|
|
9
|
+
import type { EngramAccessService } from "../access-service.js";
|
|
10
|
+
import type { StorageManager } from "../storage.js";
|
|
11
|
+
|
|
12
|
+
const adminConsolePublicDir = path.resolve(
|
|
13
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
14
|
+
"../../../../admin-console/public"
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
async function withTempRoot(run: (root: string) => Promise<void>): Promise<void> {
|
|
18
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-relay-ui-"));
|
|
19
|
+
try {
|
|
20
|
+
await run(root);
|
|
21
|
+
} finally {
|
|
22
|
+
await rm(root, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fakeService(root: string): EngramAccessService {
|
|
27
|
+
const storage = { dir: root } as StorageManager;
|
|
28
|
+
return {
|
|
29
|
+
configRef: { defaultNamespace: "relay-build-week", namespacesEnabled: true },
|
|
30
|
+
getReadableStorageForNamespace: async (namespace?: string) => ({ namespace: namespace ?? "relay-build-week", storage }),
|
|
31
|
+
getWritableStorageForNamespace: async (namespace?: string) => ({ namespace: namespace ?? "relay-build-week", storage }),
|
|
32
|
+
} as unknown as EngramAccessService;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("Relay Mission Control serves both aliases from one exact static allow-list", async () => {
|
|
36
|
+
await withTempRoot(async (root) => {
|
|
37
|
+
const server = new EngramAccessHttpServer({
|
|
38
|
+
service: fakeService(root),
|
|
39
|
+
host: "127.0.0.1",
|
|
40
|
+
port: 0,
|
|
41
|
+
authToken: "relay-token",
|
|
42
|
+
principal: "relay-operator",
|
|
43
|
+
adminConsoleEnabled: true,
|
|
44
|
+
adminConsolePublicDir,
|
|
45
|
+
adminConsolePrefillToken: true,
|
|
46
|
+
});
|
|
47
|
+
const status = await server.start();
|
|
48
|
+
const origin = `http://127.0.0.1:${status.port}`;
|
|
49
|
+
try {
|
|
50
|
+
for (const prefix of ["remnic", "engram"]) {
|
|
51
|
+
const redirect = await fetch(`${origin}/${prefix}/ui/relay`, { redirect: "manual" });
|
|
52
|
+
assert.equal(redirect.status, 301);
|
|
53
|
+
assert.equal(redirect.headers.get("location"), `/${prefix}/ui/relay/`);
|
|
54
|
+
|
|
55
|
+
const shell = await fetch(`${origin}/${prefix}/ui/relay/`);
|
|
56
|
+
assert.equal(shell.status, 200);
|
|
57
|
+
assert.match(shell.headers.get("content-type") ?? "", /^text\/html/);
|
|
58
|
+
assert.doesNotMatch(await shell.text(), /__REMNIC_ADMIN_CONSOLE_PREFILL_TOKEN__/);
|
|
59
|
+
|
|
60
|
+
const authorizedShell = await fetch(`${origin}/${prefix}/ui/relay/`, {
|
|
61
|
+
headers: { authorization: "Bearer relay-token" },
|
|
62
|
+
});
|
|
63
|
+
assert.equal(authorizedShell.status, 200);
|
|
64
|
+
assert.equal(authorizedShell.headers.get("cache-control"), "private, no-store");
|
|
65
|
+
assert.match(await authorizedShell.text(), /__REMNIC_ADMIN_CONSOLE_PREFILL_TOKEN__="relay-token"/);
|
|
66
|
+
|
|
67
|
+
const expectedAssets = new Map([
|
|
68
|
+
["relay.css", /^text\/css/],
|
|
69
|
+
["relay-model.js", /^application\/javascript/],
|
|
70
|
+
["relay.js", /^application\/javascript/],
|
|
71
|
+
["replay.json", /^application\/json/],
|
|
72
|
+
]);
|
|
73
|
+
for (const [fileName, contentType] of expectedAssets) {
|
|
74
|
+
const asset = await fetch(`${origin}/${prefix}/ui/relay/${fileName}`);
|
|
75
|
+
assert.equal(asset.status, 200, `${prefix}/${fileName}`);
|
|
76
|
+
assert.match(asset.headers.get("content-type") ?? "", contentType);
|
|
77
|
+
assert.ok((await asset.text()).length > 100);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const auth = { authorization: "Bearer relay-token" };
|
|
81
|
+
const unknown = await fetch(`${origin}/${prefix}/ui/relay/not-allowed.js`, { headers: auth });
|
|
82
|
+
assert.equal(unknown.status, 404);
|
|
83
|
+
const nested = await fetch(`${origin}/${prefix}/ui/relay/relay.js/extra`, { headers: auth });
|
|
84
|
+
assert.equal(nested.status, 404);
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
await server.stop();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #1903 — state-file writes must not touch the namespace catalog.
|
|
3
|
+
*
|
|
4
|
+
* The post-write catalog hook (`onCatalogWrite`) fires on every successful
|
|
5
|
+
* secure-file write. Pure state files (`state/buffer.json`, ledgers, indexes)
|
|
6
|
+
* are NOT namespace memory data, so by default their writes must be excluded
|
|
7
|
+
* from the catalog touch path (they were the dominant source of catalog churn
|
|
8
|
+
* that grew `state/namespaces.jsonl` unbounded). Namespace data writes
|
|
9
|
+
* (`facts/`, ...) still touch. The `touchStateWrites` opt-in restores the
|
|
10
|
+
* pre-#1903 behavior where every write touched.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
|
|
19
|
+
import { StorageManager } from "./storage.js";
|
|
20
|
+
import type { BufferState } from "./types.js";
|
|
21
|
+
|
|
22
|
+
async function mkDir(): Promise<string> {
|
|
23
|
+
return await mkdtemp(path.join(os.tmpdir(), "remnic-touch-gate-"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function emptyBuffer(): BufferState {
|
|
27
|
+
return { turns: [], lastExtractionAt: null, extractionCount: 0 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("#1903 state-file writes do not touch the catalog by default; data writes do", async () => {
|
|
31
|
+
const dir = await mkDir();
|
|
32
|
+
try {
|
|
33
|
+
const storage = new StorageManager(dir);
|
|
34
|
+
await storage.ensureDirectories();
|
|
35
|
+
let touches = 0;
|
|
36
|
+
storage.onCatalogWrite = () => {
|
|
37
|
+
touches += 1;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// A pure state-file write (state/buffer.json) must NOT touch the catalog.
|
|
41
|
+
await storage.saveBuffer(emptyBuffer());
|
|
42
|
+
assert.equal(touches, 0, "saving buffer.json (a state file) records no catalog touch");
|
|
43
|
+
|
|
44
|
+
// A namespace-data write (facts/) MUST touch exactly once. The lifecycle
|
|
45
|
+
// ledger append it also performs is a state write and stays excluded.
|
|
46
|
+
touches = 0;
|
|
47
|
+
await storage.writeMemory("fact", "The database is PostgreSQL.", { confidence: 0.9 });
|
|
48
|
+
assert.equal(touches, 1, "writing a fact records exactly one catalog touch");
|
|
49
|
+
} finally {
|
|
50
|
+
await rm(dir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("#1903 touchStateWrites=true restores the state-file catalog touch", async () => {
|
|
55
|
+
const dir = await mkDir();
|
|
56
|
+
try {
|
|
57
|
+
const storage = new StorageManager(dir);
|
|
58
|
+
await storage.ensureDirectories();
|
|
59
|
+
storage.touchStateWrites = true;
|
|
60
|
+
let touches = 0;
|
|
61
|
+
storage.onCatalogWrite = () => {
|
|
62
|
+
touches += 1;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
await storage.saveBuffer(emptyBuffer());
|
|
66
|
+
assert.ok(touches >= 1, "with touchStateWrites enabled, saving buffer.json touches the catalog");
|
|
67
|
+
} finally {
|
|
68
|
+
await rm(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -2447,6 +2447,32 @@ export class StorageManager {
|
|
|
2447
2447
|
}
|
|
2448
2448
|
}
|
|
2449
2449
|
|
|
2450
|
+
/**
|
|
2451
|
+
* Whether pure state-file writes touch the namespace catalog (issue #1903).
|
|
2452
|
+
* Set by `NamespaceStorageRouter.bindCatalogWriteHook` from
|
|
2453
|
+
* `namespacesCatalogTouchStateWrites` (default false). State files
|
|
2454
|
+
* (`state/buffer.json`, ledgers, indexes) are not namespace memory data, so by
|
|
2455
|
+
* default their writes do NOT record a catalog touch.
|
|
2456
|
+
*/
|
|
2457
|
+
touchStateWrites = false;
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* Post-write catalog hook, gated by file path (issue #1903). Namespace data
|
|
2461
|
+
* (`facts/`, `cold/`, `entities/`, `artifacts/`, `profile.md`, ...) lives
|
|
2462
|
+
* OUTSIDE `stateDir`, so a data write always touches; a write under `stateDir`
|
|
2463
|
+
* is a pure state write and is skipped unless `touchStateWrites` is enabled.
|
|
2464
|
+
*/
|
|
2465
|
+
private notifyCatalogWriteForPath(filePath: string): void {
|
|
2466
|
+
if (!this.touchStateWrites && this.isStateFilePath(filePath)) return;
|
|
2467
|
+
this.notifyCatalogWrite();
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/** Whether `filePath` resolves to `stateDir` itself or a path contained in it. */
|
|
2471
|
+
private isStateFilePath(filePath: string): boolean {
|
|
2472
|
+
const rel = path.relative(this.stateDir, path.resolve(filePath));
|
|
2473
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2450
2476
|
// ── Tombstone store (issue #1579) ───────────────────────────────────────
|
|
2451
2477
|
// Instance-scoped (rule 11) + namespace-scoped (rule 42) tombstone index.
|
|
2452
2478
|
// Lazily loaded from <stateDir>/tombstones.jsonl; invalidated together with
|
|
@@ -3083,12 +3109,12 @@ export class StorageManager {
|
|
|
3083
3109
|
}
|
|
3084
3110
|
private writeStorageSecureFile(filePath: string, content: string | Buffer): Promise<void> {
|
|
3085
3111
|
return writeMaybeEncryptedFile(filePath, content, this.resolveWriteKey(), {}, this.baseDir).then(() =>
|
|
3086
|
-
this.
|
|
3112
|
+
this.notifyCatalogWriteForPath(filePath)
|
|
3087
3113
|
);
|
|
3088
3114
|
}
|
|
3089
3115
|
private writeStorageSecureFileChunks(filePath: string, chunks: AsyncIterable<Buffer>): Promise<void> {
|
|
3090
3116
|
return writeMaybeEncryptedFileFromChunks(filePath, chunks, this.resolveWriteKey(), {}, this.baseDir).then(() =>
|
|
3091
|
-
this.
|
|
3117
|
+
this.notifyCatalogWriteForPath(filePath)
|
|
3092
3118
|
);
|
|
3093
3119
|
}
|
|
3094
3120
|
|
|
@@ -3340,14 +3366,14 @@ export class StorageManager {
|
|
|
3340
3366
|
if (isEncryptedFile(await readFile(filePath))) {
|
|
3341
3367
|
const existing = await this.readStorageSecureFile(filePath);
|
|
3342
3368
|
await writeMaybeEncryptedFile(filePath, `${existing}${content}`, null, {}, this.baseDir);
|
|
3343
|
-
this.
|
|
3369
|
+
this.notifyCatalogWriteForPath(filePath);
|
|
3344
3370
|
return;
|
|
3345
3371
|
}
|
|
3346
3372
|
} catch (err) {
|
|
3347
3373
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
3348
3374
|
}
|
|
3349
3375
|
await appendFile(filePath, content, "utf-8");
|
|
3350
|
-
this.
|
|
3376
|
+
this.notifyCatalogWriteForPath(filePath);
|
|
3351
3377
|
return;
|
|
3352
3378
|
}
|
|
3353
3379
|
|
|
@@ -3358,7 +3384,7 @@ export class StorageManager {
|
|
|
3358
3384
|
if (!isErrnoCode(err, "ENOENT")) throw err;
|
|
3359
3385
|
}
|
|
3360
3386
|
await writeMaybeEncryptedFile(filePath, `${existing}${content}`, writeKey, {}, this.baseDir);
|
|
3361
|
-
this.
|
|
3387
|
+
this.notifyCatalogWriteForPath(filePath);
|
|
3362
3388
|
}
|
|
3363
3389
|
private get stateDir(): string {
|
|
3364
3390
|
return path.join(this.baseDir, "state");
|
package/src/types.ts
CHANGED
|
@@ -1690,6 +1690,40 @@ export interface PluginConfig {
|
|
|
1690
1690
|
* existing single-namespace behavior is unchanged. Default on.
|
|
1691
1691
|
*/
|
|
1692
1692
|
namespaceCatalogEnabled: boolean;
|
|
1693
|
+
/**
|
|
1694
|
+
* Size (bytes) at which a namespace-catalog touch triggers an in-lock
|
|
1695
|
+
* auto-compaction of `state/namespaces.jsonl` (issue #1903). The append-only
|
|
1696
|
+
* log grows one line per touch; once it exceeds this many bytes the next
|
|
1697
|
+
* touch folds it (last-record-wins, every namespace row preserved) and
|
|
1698
|
+
* atomically rewrites it. Default `16 * 1024 * 1024` (16 MB). `0` disables
|
|
1699
|
+
* auto-compaction (restores the pre-#1903 unbounded-append behavior).
|
|
1700
|
+
*/
|
|
1701
|
+
namespacesCatalogCompactBytes: number;
|
|
1702
|
+
/**
|
|
1703
|
+
* Coalescing window (ms) for pure-timestamp READ touches on an
|
|
1704
|
+
* already-known namespace record (issue #1903). Repeated `markRead` touches
|
|
1705
|
+
* that only refresh `lastReadAt` are buffered per (namespace, kind) and
|
|
1706
|
+
* flushed once per window instead of appending a line each time.
|
|
1707
|
+
* Semantically-observable touches (first sight, provenance/field changes)
|
|
1708
|
+
* always flush immediately. Default `60000`. `0` disables read coalescing
|
|
1709
|
+
* (each touch appends immediately, the pre-#1903 behavior).
|
|
1710
|
+
*/
|
|
1711
|
+
namespacesCatalogReadTouchCoalesceMs: number;
|
|
1712
|
+
/**
|
|
1713
|
+
* Coalescing window (ms) for pure-timestamp WRITE touches on an
|
|
1714
|
+
* already-known namespace record (issue #1903). Mirrors
|
|
1715
|
+
* `namespacesCatalogReadTouchCoalesceMs` for `markWrite`/`lastWriteAt`.
|
|
1716
|
+
* Default `1000`. `0` disables write coalescing.
|
|
1717
|
+
*/
|
|
1718
|
+
namespacesCatalogWriteTouchCoalesceMs: number;
|
|
1719
|
+
/**
|
|
1720
|
+
* Whether pure state-file writes (`state/buffer.json`, ledgers, indexes)
|
|
1721
|
+
* touch the namespace catalog (issue #1903). State files are not namespace
|
|
1722
|
+
* memory data, so by default their writes do NOT record a catalog touch.
|
|
1723
|
+
* Set `true` to restore the pre-#1903 behavior where every secure-file
|
|
1724
|
+
* write touched the catalog. Default `false`.
|
|
1725
|
+
*/
|
|
1726
|
+
namespacesCatalogTouchStateWrites: boolean;
|
|
1693
1727
|
defaultNamespace: string;
|
|
1694
1728
|
sharedNamespace: string;
|
|
1695
1729
|
principalFromSessionKeyMode: PrincipalFromSessionKeyMode;
|