@remnic/core 9.7.11 → 9.7.12
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 +6 -6
- 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-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-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-W6R43NLA.js → chunk-AOWPZ7TM.js} +17 -17
- 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-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-7XRUYC6G.js → chunk-TITSEN2M.js} +3 -3
- 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-QHT4W3RU.js → chunk-ZASJ53ML.js} +6 -6
- 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 +14 -14
- 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/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 +1 -1
- 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/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/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/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-JZM5QUTE.js.map +0 -1
- package/dist/chunk-KXLRMM35.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-W6R43NLA.js.map → chunk-AOWPZ7TM.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-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-7XRUYC6G.js.map → chunk-TITSEN2M.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-QHT4W3RU.js.map → chunk-ZASJ53ML.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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
stat,
|
|
13
13
|
writeFile,
|
|
14
14
|
} from "node:fs/promises";
|
|
15
|
+
import { StringDecoder } from "node:string_decoder";
|
|
15
16
|
import type { PluginConfig } from "../types.js";
|
|
16
17
|
import {
|
|
17
18
|
MutationSerializer,
|
|
@@ -497,12 +498,46 @@ export class NamespaceCatalog {
|
|
|
497
498
|
// against this normalized form everywhere instead.
|
|
498
499
|
private readonly defaultNamespaceIdentity: string;
|
|
499
500
|
|
|
501
|
+
// Issue #1903 — touch-path performance knobs, resolved with the documented
|
|
502
|
+
// defaults when an externally-built PluginConfig omits them (so a partial
|
|
503
|
+
// config object still yields the production behavior). `0` is a real disable
|
|
504
|
+
// switch for each and is preserved by `??` (0 is not null/undefined).
|
|
505
|
+
private readonly compactBytesLimit: number;
|
|
506
|
+
private readonly readCoalesceMs: number;
|
|
507
|
+
private readonly writeCoalesceMs: number;
|
|
508
|
+
// Size after the most recent auto-compaction (0 = none yet). Hysteresis for
|
|
509
|
+
// maybeAutoCompact: a folded catalog that is itself above the limit (many
|
|
510
|
+
// distinct namespaces) would otherwise be re-folded + re-rewritten on every
|
|
511
|
+
// touch. We only compact again once the log has grown by >= one limit-worth
|
|
512
|
+
// since the last compaction (#1903, Codex).
|
|
513
|
+
private lastCompactedSize = 0;
|
|
514
|
+
|
|
515
|
+
// Issue #1903 — per-(namespace, kind) coalescing buffer. Keyed `${kind}:${ns}`.
|
|
516
|
+
// Holds the latest buffered timestamp/metadata for a pure-timestamp touch on
|
|
517
|
+
// an already-known record; a `.unref()`'d timer flushes it once per window so
|
|
518
|
+
// the append-only log does not grow one line per repeat touch. Semantically
|
|
519
|
+
// observable touches (first sight, provenance/field change) bypass this buffer
|
|
520
|
+
// and flush immediately (see `coalesceTouch`).
|
|
521
|
+
private readonly pendingTouches = new Map<
|
|
522
|
+
string,
|
|
523
|
+
{
|
|
524
|
+
namespace: string;
|
|
525
|
+
kind: "read" | "write";
|
|
526
|
+
metadata?: NamespaceTouchMetadata;
|
|
527
|
+
at: Date;
|
|
528
|
+
timer: NodeJS.Timeout;
|
|
529
|
+
}
|
|
530
|
+
>();
|
|
531
|
+
|
|
500
532
|
constructor(private readonly config: PluginConfig) {
|
|
501
533
|
this.memoryDir = config.memoryDir;
|
|
502
534
|
this.stateDir = path.join(this.memoryDir, STATE_DIR);
|
|
503
535
|
this.catalogPath = path.join(this.stateDir, CATALOG_FILE);
|
|
504
536
|
this.rebuildLockPath = path.join(this.stateDir, REBUILD_LOCK_FILE);
|
|
505
537
|
this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);
|
|
538
|
+
this.compactBytesLimit = config.namespacesCatalogCompactBytes ?? 16 * 1024 * 1024;
|
|
539
|
+
this.readCoalesceMs = config.namespacesCatalogReadTouchCoalesceMs ?? 60_000;
|
|
540
|
+
this.writeCoalesceMs = config.namespacesCatalogWriteTouchCoalesceMs ?? 1_000;
|
|
506
541
|
}
|
|
507
542
|
|
|
508
543
|
/** Whether the catalog is active (namespaces enabled and catalog not opted out). */
|
|
@@ -663,11 +698,137 @@ export class NamespaceCatalog {
|
|
|
663
698
|
// ── Touch API (cheap, failure-tolerant) ─────────────────────────────────
|
|
664
699
|
|
|
665
700
|
async markRead(namespace: string, metadata?: NamespaceTouchMetadata): Promise<void> {
|
|
666
|
-
await this.
|
|
701
|
+
await this.coalesceTouch(namespace, "read", metadata);
|
|
667
702
|
}
|
|
668
703
|
|
|
669
704
|
async markWrite(namespace: string, metadata?: NamespaceTouchMetadata): Promise<void> {
|
|
670
|
-
await this.
|
|
705
|
+
await this.coalesceTouch(namespace, "write", metadata);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Route a read/write touch through the per-(namespace, kind) coalescing buffer
|
|
710
|
+
* (issue #1903). A touch flushes IMMEDIATELY (bypassing the buffer) whenever it
|
|
711
|
+
* is semantically observable to an in-process reader — first sight of the
|
|
712
|
+
* namespace, a `config`→`write` provenance upgrade, the first time a touch
|
|
713
|
+
* field is set (so `listNamespaces({ writtenSince })` and record presence stay
|
|
714
|
+
* correct), or a change to `kind`/`principal`/`projectId`/`branch`/
|
|
715
|
+
* `parentNamespace`. A pure timestamp refresh on an already-known record is
|
|
716
|
+
* DEFERRED and coalesced: the newest buffered `at`/metadata wins and a single
|
|
717
|
+
* `touch` runs when the window elapses (or on `flushPendingTouches`). Window
|
|
718
|
+
* `0` skips the buffer entirely (the pre-#1903 immediate-append behavior).
|
|
719
|
+
* Best-effort: coalescing decisions read only the warm `compactedCache`, never
|
|
720
|
+
* forcing a disk parse; a cold cache flushes immediately (and re-warms it).
|
|
721
|
+
*/
|
|
722
|
+
private async coalesceTouch(
|
|
723
|
+
namespace: string,
|
|
724
|
+
kind: "read" | "write",
|
|
725
|
+
metadata?: NamespaceTouchMetadata,
|
|
726
|
+
): Promise<void> {
|
|
727
|
+
if (!this.enabled) return;
|
|
728
|
+
const window = kind === "read" ? this.readCoalesceMs : this.writeCoalesceMs;
|
|
729
|
+
if (window <= 0) {
|
|
730
|
+
await this.touch(namespace, kind, metadata);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
// Validate up front so an unsafe namespace rejects deterministically, exactly
|
|
734
|
+
// as the immediate-append path does (touch validates too).
|
|
735
|
+
const ns = this.validateNamespace(namespace);
|
|
736
|
+
const cached = this.compactedCache?.records.get(ns);
|
|
737
|
+
if (cached === undefined || this.touchMustFlushImmediately(kind, metadata, cached)) {
|
|
738
|
+
// Semantically observable in-process → flush now (also warms the cache).
|
|
739
|
+
// Any older buffered touch for this key is now redundant (this newer touch
|
|
740
|
+
// updates the same field with a newer timestamp): drop it so its stale
|
|
741
|
+
// timer cannot regress the timestamp after this flush.
|
|
742
|
+
const stale = this.pendingTouches.get(`${kind}:${ns}`);
|
|
743
|
+
if (stale) {
|
|
744
|
+
clearTimeout(stale.timer);
|
|
745
|
+
this.pendingTouches.delete(`${kind}:${ns}`);
|
|
746
|
+
}
|
|
747
|
+
await this.touch(namespace, kind, metadata);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
// Pure timestamp refresh on a known record → coalesce within a FIXED window.
|
|
751
|
+
const key = `${kind}:${ns}`;
|
|
752
|
+
const at = metadata?.at ?? new Date();
|
|
753
|
+
const existing = this.pendingTouches.get(key);
|
|
754
|
+
if (existing) {
|
|
755
|
+
// Keep the ORIGINAL timer and only refresh the buffered payload (#1903,
|
|
756
|
+
// Codex). Re-arming on every touch would make this a debounce that never
|
|
757
|
+
// flushes under continuous traffic (e.g. a sustained import), leaving
|
|
758
|
+
// lastWriteAt stale and writtenSince/maintenance consumers blind to an
|
|
759
|
+
// actively-written namespace. With a fixed window, each window flushes on
|
|
760
|
+
// schedule carrying the newest buffered timestamp.
|
|
761
|
+
existing.metadata = metadata;
|
|
762
|
+
existing.at = at;
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// Cap at Node's maximum setTimeout delay (2^31-1 ms ≈ 24.8 days). Values
|
|
766
|
+
// above that are clamped by Node to 1ms, which would append almost every
|
|
767
|
+
// touch and recreate the churn this coalescing exists to prevent (#1903, Codex).
|
|
768
|
+
const safeWindow = Math.min(window, 2_147_483_647);
|
|
769
|
+
const timer = setTimeout(() => {
|
|
770
|
+
void this.flushPendingTouch(key);
|
|
771
|
+
}, safeWindow);
|
|
772
|
+
timer.unref();
|
|
773
|
+
this.pendingTouches.set(key, { namespace: ns, kind, metadata, at, timer });
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Whether a read/write touch on an EXISTING cached record must flush
|
|
778
|
+
* immediately rather than coalesce. Only pure timestamp refreshes (the value
|
|
779
|
+
* returns false) are safe to defer.
|
|
780
|
+
*/
|
|
781
|
+
private touchMustFlushImmediately(
|
|
782
|
+
kind: "read" | "write",
|
|
783
|
+
metadata: NamespaceTouchMetadata | undefined,
|
|
784
|
+
cached: NamespaceRecord,
|
|
785
|
+
): boolean {
|
|
786
|
+
// A write upgrading a config pre-registration to "write" changes the
|
|
787
|
+
// discoveredBy filter result — must be observable at once (~:1261).
|
|
788
|
+
if (kind === "write" && cached.discoveredBy === "config") return true;
|
|
789
|
+
// The first time a touch field is set makes the namespace newly match
|
|
790
|
+
// presence/`writtenSince` reads, so it must not be deferred.
|
|
791
|
+
if (kind === "write" && cached.lastWriteAt === undefined) return true;
|
|
792
|
+
if (kind === "read" && cached.lastReadAt === undefined) return true;
|
|
793
|
+
if (!metadata) return false;
|
|
794
|
+
if (metadata.kind !== undefined && metadata.kind !== cached.kind) return true;
|
|
795
|
+
if (metadata.principal !== undefined && metadata.principal !== cached.principal) return true;
|
|
796
|
+
if (metadata.projectId !== undefined && metadata.projectId !== cached.projectId) return true;
|
|
797
|
+
if (metadata.branch !== undefined && metadata.branch !== cached.branch) return true;
|
|
798
|
+
if (metadata.parentNamespace !== undefined && metadata.parentNamespace !== cached.parentNamespace) {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
// A changed storageDir repoints routing/containment for this namespace —
|
|
802
|
+
// must be observable at once, not held stale by the coalesce window (#1903, Cursor).
|
|
803
|
+
if (metadata.storageDir !== undefined && metadata.storageDir !== cached.storageDir) return true;
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Fire a single buffered touch when its coalescing window elapses. */
|
|
808
|
+
private async flushPendingTouch(key: string): Promise<void> {
|
|
809
|
+
const pending = this.pendingTouches.get(key);
|
|
810
|
+
if (!pending) return;
|
|
811
|
+
this.pendingTouches.delete(key);
|
|
812
|
+
clearTimeout(pending.timer);
|
|
813
|
+
await this.touch(pending.namespace, pending.kind, { ...pending.metadata, at: pending.at }).catch(
|
|
814
|
+
() => undefined,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Flush every buffered coalesced touch now (issue #1903). Called from the
|
|
820
|
+
* router/orchestrator shutdown hook so a long-lived host does not drop
|
|
821
|
+
* buffered read/write timestamps on teardown, and by tests to make coalesced
|
|
822
|
+
* touches deterministically observable. Best-effort: a failed flush never
|
|
823
|
+
* throws (each touch is `.catch()`-guarded).
|
|
824
|
+
*/
|
|
825
|
+
async flushPendingTouches(): Promise<void> {
|
|
826
|
+
const pending = [...this.pendingTouches.values()];
|
|
827
|
+
this.pendingTouches.clear();
|
|
828
|
+
for (const p of pending) {
|
|
829
|
+
clearTimeout(p.timer);
|
|
830
|
+
await this.touch(p.namespace, p.kind, { ...p.metadata, at: p.at }).catch(() => undefined);
|
|
831
|
+
}
|
|
671
832
|
}
|
|
672
833
|
|
|
673
834
|
async markMaintenance(namespace: string, jobName: string, at?: Date): Promise<void> {
|
|
@@ -1269,11 +1430,48 @@ export class NamespaceCatalog {
|
|
|
1269
1430
|
}
|
|
1270
1431
|
|
|
1271
1432
|
await this.appendUnchained(record);
|
|
1433
|
+
// Size-triggered auto-compaction (issue #1903), still inside the held
|
|
1434
|
+
// cross-process lock + queueCritical turn so the fold→rename is atomic
|
|
1435
|
+
// against concurrent touches and cross-process rebuilds.
|
|
1436
|
+
await this.maybeAutoCompact();
|
|
1272
1437
|
return true;
|
|
1273
1438
|
}),
|
|
1274
1439
|
);
|
|
1275
1440
|
}
|
|
1276
1441
|
|
|
1442
|
+
/**
|
|
1443
|
+
* Fold-and-rewrite the JSONL log when it exceeds `compactBytesLimit`
|
|
1444
|
+
* (issue #1903). MUST be called only from inside `touch`'s held-lock +
|
|
1445
|
+
* `queueCritical` critical section — it reuses `loadCompacted` (last-record-
|
|
1446
|
+
* wins fold; every namespace row survives) and `rewriteUnchained` (atomic
|
|
1447
|
+
* temp-file + rename), so the collapse is cross-process safe and never
|
|
1448
|
+
* invokes the disk-scan purge in `finishRebuild`. Best-effort and fail-open
|
|
1449
|
+
* (rule #40): the append already succeeded and is durable, so a failed
|
|
1450
|
+
* compaction must not fail the touch — the next over-limit touch retries.
|
|
1451
|
+
*/
|
|
1452
|
+
private async maybeAutoCompact(): Promise<void> {
|
|
1453
|
+
try {
|
|
1454
|
+
const limit = this.compactBytesLimit; // 0 => auto-compaction disabled
|
|
1455
|
+
if (limit <= 0) return;
|
|
1456
|
+
// Exact post-append size when the cache is warm; skip this touch's check
|
|
1457
|
+
// when the cache is cold (a cross-process append invalidated it) — the
|
|
1458
|
+
// next touch re-warms and re-checks.
|
|
1459
|
+
const size = this.compactedCache?.identity.size;
|
|
1460
|
+
if (size === undefined || size <= limit) return;
|
|
1461
|
+
// Hysteresis (issue #1903, Codex): a folded catalog whose deduped state is
|
|
1462
|
+
// itself above the limit (many distinct namespaces) would otherwise be
|
|
1463
|
+
// re-folded + re-rewritten on EVERY touch — O(catalog) per touch. Only
|
|
1464
|
+
// compact again once the log has grown by >= one limit-worth since the last
|
|
1465
|
+
// compaction; by then the new appends are duplicates the fold can collapse.
|
|
1466
|
+
if (this.lastCompactedSize > 0 && size - this.lastCompactedSize < limit) return;
|
|
1467
|
+
const folded = await this.loadCompacted();
|
|
1468
|
+
await this.rewriteUnchained([...folded.values()]);
|
|
1469
|
+
this.lastCompactedSize = this.compactedCache?.identity.size ?? size;
|
|
1470
|
+
} catch {
|
|
1471
|
+
// Fail-open: compaction is best-effort maintenance on already-durable data.
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1277
1475
|
// ── Rebuild from disk ────────────────────────────────────────────────────
|
|
1278
1476
|
|
|
1279
1477
|
async rebuildFromDisk(
|
|
@@ -1957,25 +2155,52 @@ export class NamespaceCatalog {
|
|
|
1957
2155
|
|
|
1958
2156
|
try {
|
|
1959
2157
|
this.onCatalogReadForTest?.();
|
|
1960
|
-
|
|
1961
|
-
|
|
2158
|
+
// Streaming chunked parse (issue #1903): the log can reach 100+ MB in
|
|
2159
|
+
// production, so read it in bounded chunks and yield to the event loop
|
|
2160
|
+
// every ~5000 lines instead of buffering the whole file and blocking on a
|
|
2161
|
+
// single giant `split("\n")`. The fold is unchanged (last-record-wins with
|
|
2162
|
+
// field-level touch merge across cross-process full-snapshot appends).
|
|
2163
|
+
const CHUNK_BYTES = 1 << 16; // 64 KiB
|
|
2164
|
+
const YIELD_EVERY_LINES = 5000;
|
|
2165
|
+
const chunk = Buffer.allocUnsafe(CHUNK_BYTES);
|
|
2166
|
+
// StringDecoder buffers partial multi-byte UTF-8 sequences that straddle a
|
|
2167
|
+
// chunk boundary, so a namespace/principal with non-ASCII bytes never
|
|
2168
|
+
// corrupts at the seam.
|
|
2169
|
+
const decoder = new StringDecoder("utf8");
|
|
2170
|
+
let leftover = "";
|
|
2171
|
+
let linesSinceYield = 0;
|
|
2172
|
+
const foldLine = (line: string): void => {
|
|
1962
2173
|
const trimmed = line.trim();
|
|
1963
|
-
if (trimmed.length === 0)
|
|
2174
|
+
if (trimmed.length === 0) return;
|
|
1964
2175
|
let parsed: unknown;
|
|
1965
2176
|
try {
|
|
1966
2177
|
parsed = JSON.parse(trimmed);
|
|
1967
2178
|
} catch {
|
|
1968
2179
|
// Skip corrupt lines (CLAUDE.md rule #18 robustness).
|
|
1969
|
-
|
|
2180
|
+
return;
|
|
1970
2181
|
}
|
|
1971
2182
|
const record = coerceRecord(parsed);
|
|
1972
|
-
if (!record)
|
|
1973
|
-
// Preserve each touch field across cross-process full-snapshot appends.
|
|
1974
|
-
// Field-level touch merge preserves the newest value for every touch
|
|
1975
|
-
// field when full-snapshot appends from separate processes interleave.
|
|
2183
|
+
if (!record) return;
|
|
1976
2184
|
const prior = records.get(record.namespace);
|
|
1977
2185
|
records.set(record.namespace, prior ? mergeNewerTouchFields(record, prior) : record);
|
|
2186
|
+
};
|
|
2187
|
+
for (;;) {
|
|
2188
|
+
const { bytesRead } = await handle.read(chunk, 0, CHUNK_BYTES, null);
|
|
2189
|
+
if (bytesRead === 0) break;
|
|
2190
|
+
leftover += decoder.write(chunk.subarray(0, bytesRead));
|
|
2191
|
+
let newlineIndex = leftover.indexOf("\n");
|
|
2192
|
+
while (newlineIndex !== -1) {
|
|
2193
|
+
foldLine(leftover.slice(0, newlineIndex));
|
|
2194
|
+
leftover = leftover.slice(newlineIndex + 1);
|
|
2195
|
+
if (++linesSinceYield >= YIELD_EVERY_LINES) {
|
|
2196
|
+
linesSinceYield = 0;
|
|
2197
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
2198
|
+
}
|
|
2199
|
+
newlineIndex = leftover.indexOf("\n");
|
|
2200
|
+
}
|
|
1978
2201
|
}
|
|
2202
|
+
leftover += decoder.end();
|
|
2203
|
+
foldLine(leftover);
|
|
1979
2204
|
} catch {
|
|
1980
2205
|
this.compactedCache = undefined;
|
|
1981
2206
|
return new Map();
|
|
@@ -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) {
|
|
@@ -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;
|