@remnic/core 9.35.1 → 9.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +5 -5
- package/dist/access-admin-ops-surface.js +19 -18
- package/dist/access-authorization-probe.d.ts +5 -5
- package/dist/access-authorization-probe.js +20 -19
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +19 -18
- package/dist/access-cli.js +43 -42
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +5 -5
- package/dist/access-extraction-force-flush.js +19 -18
- package/dist/access-http-lcm-compaction.d.ts +5 -5
- package/dist/access-http-lifecycle-flush.d.ts +5 -5
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +24 -23
- package/dist/access-identity-continuity-surface.d.ts +4 -4
- package/dist/access-identity-continuity-surface.js +19 -18
- package/dist/access-lcm-surface.d.ts +5 -5
- package/dist/access-lcm-surface.js +19 -18
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +22 -21
- package/dist/access-namespace-preflight.js +19 -18
- package/dist/access-observe-write-surface.d.ts +5 -5
- package/dist/access-observe-write-surface.js +19 -18
- package/dist/access-operations-batch.js +20 -19
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +21 -20
- package/dist/access-recall-concurrency.d.ts +5 -5
- package/dist/access-recall-concurrency.js +19 -18
- package/dist/access-recall-response.d.ts +5 -5
- package/dist/access-recall-response.js +19 -18
- package/dist/access-recall-surface.d.ts +5 -5
- package/dist/access-recall-surface.js +19 -18
- package/dist/access-schema.d.ts +88 -88
- package/dist/{access-service-D9zIBlpp.d.ts → access-service-C2WYT1pO.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +5 -5
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +19 -18
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/access-wearables-meetings-surface.d.ts +2 -2
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +10 -9
- package/dist/buffer.d.ts +1 -1
- package/dist/{capsule-merge-2H2IUTBT.js → capsule-merge-IDWJPCWK.js} +77 -8
- package/dist/capsule-merge-IDWJPCWK.js.map +1 -0
- package/dist/causal-consolidation.js +11 -10
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-65ZPVYTA.js → chunk-3CE77VAP.js} +55 -5
- package/dist/chunk-3CE77VAP.js.map +1 -0
- package/dist/chunk-4J7G5SY4.js +338 -0
- package/dist/chunk-4J7G5SY4.js.map +1 -0
- package/dist/{chunk-EO7X2LDK.js → chunk-4TAD3B56.js} +2 -2
- package/dist/{chunk-QHYB4MJV.js → chunk-5ZGHUWTT.js} +2 -2
- package/dist/{chunk-TCIWHK3N.js → chunk-6D5D5A4M.js} +5 -5
- package/dist/{chunk-BIXWNHIL.js → chunk-6TF4M2LB.js} +5 -5
- package/dist/{chunk-2NUC473W.js → chunk-AKELMJO2.js} +220 -48
- package/dist/{chunk-2NUC473W.js.map → chunk-AKELMJO2.js.map} +1 -1
- package/dist/{chunk-FBUE6PD3.js → chunk-BEQOZOYZ.js} +21 -4
- package/dist/chunk-BEQOZOYZ.js.map +1 -0
- package/dist/{chunk-SA3WJJF2.js → chunk-BGLDKAYY.js} +2 -2
- package/dist/{chunk-OPRP6Q4Z.js → chunk-DKKZ6SO2.js} +3 -3
- package/dist/{chunk-6TAUHL33.js → chunk-DLVPUX7U.js} +10 -2
- package/dist/chunk-DLVPUX7U.js.map +1 -0
- package/dist/{chunk-QNWXCJ76.js → chunk-ENTCVTQR.js} +18 -1
- package/dist/{chunk-QNWXCJ76.js.map → chunk-ENTCVTQR.js.map} +1 -1
- package/dist/{chunk-AEPYA55F.js → chunk-FE7MUX7M.js} +2 -2
- package/dist/{chunk-NQUF7XKR.js → chunk-FT4RF53O.js} +2 -2
- package/dist/{chunk-GHR7WHZX.js → chunk-GORHSN5C.js} +2 -2
- package/dist/{chunk-7TEZUUGM.js → chunk-HUS2XN3T.js} +2 -2
- package/dist/{chunk-XWDZQIYU.js → chunk-IJAQCBGV.js} +2 -2
- package/dist/{chunk-TJLLYV2X.js → chunk-IMEXE4XS.js} +7 -7
- package/dist/{chunk-ZCPP5OW6.js → chunk-JQSBRLJ6.js} +31 -27
- package/dist/chunk-JQSBRLJ6.js.map +1 -0
- package/dist/{chunk-IZXTZ7BF.js → chunk-JYLN6KHH.js} +6 -6
- package/dist/{chunk-HEVWWPTL.js → chunk-LH3KXIBJ.js} +17 -17
- package/dist/{chunk-EH5U2YCX.js → chunk-M3DBB2ND.js} +2 -2
- package/dist/{chunk-XABS455Y.js → chunk-NBAEGFXT.js} +5 -5
- package/dist/{chunk-DP7ZV2II.js → chunk-OA6PIPDE.js} +2 -2
- package/dist/{chunk-T4HH5Q3U.js → chunk-OJRLTZQM.js} +3 -3
- package/dist/{chunk-7ZHGPX56.js → chunk-PRP3M5Y3.js} +2 -2
- package/dist/{chunk-Y2ZD2RVG.js → chunk-PYTUBWAS.js} +2 -2
- package/dist/{chunk-4TGZCYHH.js → chunk-Q3CVY7LV.js} +1954 -1642
- package/dist/chunk-Q3CVY7LV.js.map +1 -0
- package/dist/{chunk-PH3ZWL3W.js → chunk-Q53RLEW7.js} +2 -2
- package/dist/{chunk-Y6OWJ3V5.js → chunk-RY2XZTFN.js} +2 -2
- package/dist/{chunk-2SHIWGY7.js → chunk-RY7CHVGE.js} +2 -2
- package/dist/{chunk-7VUAG236.js → chunk-T3FVAHM7.js} +4 -4
- package/dist/{chunk-2LTDWSR6.js → chunk-TSH56VPU.js} +2 -2
- package/dist/{chunk-J6SLNQRO.js → chunk-UBWOCQ3I.js} +6 -2
- package/dist/chunk-UBWOCQ3I.js.map +1 -0
- package/dist/{chunk-5FHE4JI7.js → chunk-VEUVON5I.js} +2 -2
- package/dist/{chunk-KEWUNL7P.js → chunk-WET4M3R6.js} +2 -2
- package/dist/chunk-WKDFJXW5.js +158 -0
- package/dist/chunk-WKDFJXW5.js.map +1 -0
- package/dist/{chunk-SLPLPGJQ.js → chunk-WL2S2UBB.js} +5 -5
- package/dist/{chunk-AHZYNPLX.js → chunk-YVMVVE4B.js} +2 -2
- package/dist/{chunk-5HTAJCTO.js → chunk-ZB3YODID.js} +15 -15
- package/dist/{cli-mFX-QQvx.d.ts → cli-jkmzdT8P.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +34 -33
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +10 -9
- package/dist/connectors/codex-materialize-runner.js +10 -9
- package/dist/connectors/index.js +10 -9
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-provenance-check.js +8 -2
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/consolidation-undo.js +10 -1
- package/dist/consolidation-undo.js.map +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/corpus-watermark.js +12 -11
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +10 -9
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/explicit-capture.js +6 -3
- package/dist/extraction.js +13 -12
- package/dist/{forget-4UY2EOKH.js → forget-DNAJ66ZD.js} +2 -2
- package/dist/index.d.ts +398 -398
- package/dist/index.js +43 -42
- package/dist/local-llm-helpers.d.ts +63 -1
- package/dist/local-llm-helpers.js +10 -1
- package/dist/local-llm.d.ts +4 -1
- package/dist/local-llm.js +2 -2
- package/dist/maintenance/memory-governance.js +10 -9
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -9
- package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-projection.js +11 -10
- package/dist/{maintenance-70Wq118b.d.ts → maintenance-Ha9Dg93M.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +1 -1
- package/dist/namespaces/migrate.js +11 -10
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +10 -9
- package/dist/offline-sync-impression-drain.js +3 -3
- package/dist/operator-doctor-corpus.js +13 -12
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +17 -16
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +15 -14
- package/dist/{orchestrator-BH8LT-NX.d.ts → orchestrator-DV_u9bfx.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +43 -42
- package/dist/page-versioning.js +7 -1
- package/dist/schemas.d.ts +76 -76
- package/dist/semantic-consolidation.js +11 -10
- package/dist/semantic-rule-promotion.js +10 -9
- package/dist/semantic-rule-verifier.js +10 -9
- package/dist/{service-CrxXHz5T.d.ts → service-JxXXY-Eh.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +10 -9
- package/dist/{storage-CyWa1Jm6.d.ts → storage-BbH4HDUS.d.ts} +30 -26
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +9 -8
- package/dist/summarizer.js +3 -3
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/transfer/capsule-import.js +8 -2
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +10 -9
- package/package.json +2 -2
- package/src/binary-lifecycle/pipeline.ts +52 -1
- package/src/consolidation-undo.ts +4 -0
- package/src/curation/index.ts +41 -5
- package/src/lifecycle/tombstones.ts +40 -0
- package/src/local-llm-helpers.ts +171 -0
- package/src/local-llm.ts +34 -30
- package/src/maintenance/memory-governance.ts +4 -0
- package/src/page-versioning.ts +16 -1
- package/src/review/index.ts +97 -13
- package/src/spaces/index.ts +127 -6
- package/src/storage/citation-hash-source.ts +97 -0
- package/src/storage/entity-canonical-id-lock.ts +33 -0
- package/src/storage/entity-canonical-id-migration-adapter.ts +8 -4
- package/src/storage/entity-canonical-id-migration.ts +129 -77
- package/src/storage/entity-canonical-id-references.ts +557 -0
- package/src/storage/entity-ref-repair.ts +171 -0
- package/src/storage/entity-store.ts +163 -2
- package/src/storage/tombstone-blocked-capture-sync.ts +18 -0
- package/src/storage.ts +363 -396
- package/src/transfer/capsule-import.ts +91 -3
- package/src/transfer/capsule-merge.ts +128 -10
- package/dist/capsule-merge-2H2IUTBT.js.map +0 -1
- package/dist/chunk-4RNZMRAV.js +0 -60
- package/dist/chunk-4RNZMRAV.js.map +0 -1
- package/dist/chunk-4TGZCYHH.js.map +0 -1
- package/dist/chunk-65ZPVYTA.js.map +0 -1
- package/dist/chunk-6TAUHL33.js.map +0 -1
- package/dist/chunk-FBUE6PD3.js.map +0 -1
- package/dist/chunk-J6SLNQRO.js.map +0 -1
- package/dist/chunk-ZCPP5OW6.js.map +0 -1
- /package/dist/{chunk-EO7X2LDK.js.map → chunk-4TAD3B56.js.map} +0 -0
- /package/dist/{chunk-QHYB4MJV.js.map → chunk-5ZGHUWTT.js.map} +0 -0
- /package/dist/{chunk-TCIWHK3N.js.map → chunk-6D5D5A4M.js.map} +0 -0
- /package/dist/{chunk-BIXWNHIL.js.map → chunk-6TF4M2LB.js.map} +0 -0
- /package/dist/{chunk-SA3WJJF2.js.map → chunk-BGLDKAYY.js.map} +0 -0
- /package/dist/{chunk-OPRP6Q4Z.js.map → chunk-DKKZ6SO2.js.map} +0 -0
- /package/dist/{chunk-AEPYA55F.js.map → chunk-FE7MUX7M.js.map} +0 -0
- /package/dist/{chunk-NQUF7XKR.js.map → chunk-FT4RF53O.js.map} +0 -0
- /package/dist/{chunk-GHR7WHZX.js.map → chunk-GORHSN5C.js.map} +0 -0
- /package/dist/{chunk-7TEZUUGM.js.map → chunk-HUS2XN3T.js.map} +0 -0
- /package/dist/{chunk-XWDZQIYU.js.map → chunk-IJAQCBGV.js.map} +0 -0
- /package/dist/{chunk-TJLLYV2X.js.map → chunk-IMEXE4XS.js.map} +0 -0
- /package/dist/{chunk-IZXTZ7BF.js.map → chunk-JYLN6KHH.js.map} +0 -0
- /package/dist/{chunk-HEVWWPTL.js.map → chunk-LH3KXIBJ.js.map} +0 -0
- /package/dist/{chunk-EH5U2YCX.js.map → chunk-M3DBB2ND.js.map} +0 -0
- /package/dist/{chunk-XABS455Y.js.map → chunk-NBAEGFXT.js.map} +0 -0
- /package/dist/{chunk-DP7ZV2II.js.map → chunk-OA6PIPDE.js.map} +0 -0
- /package/dist/{chunk-T4HH5Q3U.js.map → chunk-OJRLTZQM.js.map} +0 -0
- /package/dist/{chunk-7ZHGPX56.js.map → chunk-PRP3M5Y3.js.map} +0 -0
- /package/dist/{chunk-Y2ZD2RVG.js.map → chunk-PYTUBWAS.js.map} +0 -0
- /package/dist/{chunk-PH3ZWL3W.js.map → chunk-Q53RLEW7.js.map} +0 -0
- /package/dist/{chunk-Y6OWJ3V5.js.map → chunk-RY2XZTFN.js.map} +0 -0
- /package/dist/{chunk-2SHIWGY7.js.map → chunk-RY7CHVGE.js.map} +0 -0
- /package/dist/{chunk-7VUAG236.js.map → chunk-T3FVAHM7.js.map} +0 -0
- /package/dist/{chunk-2LTDWSR6.js.map → chunk-TSH56VPU.js.map} +0 -0
- /package/dist/{chunk-5FHE4JI7.js.map → chunk-VEUVON5I.js.map} +0 -0
- /package/dist/{chunk-KEWUNL7P.js.map → chunk-WET4M3R6.js.map} +0 -0
- /package/dist/{chunk-SLPLPGJQ.js.map → chunk-WL2S2UBB.js.map} +0 -0
- /package/dist/{chunk-AHZYNPLX.js.map → chunk-YVMVVE4B.js.map} +0 -0
- /package/dist/{chunk-5HTAJCTO.js.map → chunk-ZB3YODID.js.map} +0 -0
- /package/dist/{forget-4UY2EOKH.js.map → forget-DNAJ66ZD.js.map} +0 -0
|
@@ -835,6 +835,46 @@ export function collectRetiredMemoriesForRebuild(
|
|
|
835
835
|
return retired;
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
+
/**
|
|
839
|
+
* One-shot write-time resurrection gate (issue #1579 / #2213). Looks up the
|
|
840
|
+
* fact's identity in the tombstone store and, on a match, downgrades the
|
|
841
|
+
* frontmatter to `pending_review` + `blockedBy` IN PLACE. ADD-ONLY: an
|
|
842
|
+
* already-blocked or terminal-status record is left untouched. Pure over its
|
|
843
|
+
* inputs (no I/O beyond the injected store) so both `writeMemory`'s gate and
|
|
844
|
+
* the chunk-repair re-gate share one lookup + apply.
|
|
845
|
+
*/
|
|
846
|
+
export function applyTombstoneResurrectionGate(
|
|
847
|
+
store: Pick<TombstoneStore, "lookup">,
|
|
848
|
+
fm: {
|
|
849
|
+
id: string;
|
|
850
|
+
status?: string;
|
|
851
|
+
blockedBy?: string;
|
|
852
|
+
tombstoneBlockTier?: TombstoneMatchTier;
|
|
853
|
+
entityRef?: string;
|
|
854
|
+
contentHash?: string;
|
|
855
|
+
},
|
|
856
|
+
query: {
|
|
857
|
+
normalizedText: string;
|
|
858
|
+
supersessionKeys: string[];
|
|
859
|
+
namespace: string;
|
|
860
|
+
},
|
|
861
|
+
): TombstoneMatch | null {
|
|
862
|
+
if (fm.status === "pending_review" && fm.blockedBy) return null;
|
|
863
|
+
if (!(fm.status === undefined || fm.status === "active" || fm.status === "pending_review")) return null;
|
|
864
|
+
const match = store.lookup({
|
|
865
|
+
contentHash: fm.contentHash,
|
|
866
|
+
normalizedText: query.normalizedText,
|
|
867
|
+
...(fm.entityRef ? { entityRef: fm.entityRef } : {}),
|
|
868
|
+
...(query.supersessionKeys.length > 0 ? { supersessionKeys: query.supersessionKeys } : {}),
|
|
869
|
+
namespace: query.namespace,
|
|
870
|
+
});
|
|
871
|
+
if (!match) return null;
|
|
872
|
+
fm.status = "pending_review";
|
|
873
|
+
fm.blockedBy = match.tombstoneId;
|
|
874
|
+
fm.tombstoneBlockTier = match.matchedTier;
|
|
875
|
+
return match;
|
|
876
|
+
}
|
|
877
|
+
|
|
838
878
|
/**
|
|
839
879
|
* Build the live-emission tombstone inputs for a single retired FACT — one
|
|
840
880
|
* input per derived supersession key (or a single keyless record when no
|
package/src/local-llm-helpers.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
1
2
|
export function isAbortError(err: unknown): boolean {
|
|
2
3
|
if (!err || typeof err !== "object") return false;
|
|
3
4
|
const maybe = err as { name?: string; message?: string };
|
|
@@ -56,3 +57,173 @@ export function extractNonRecoverableBackendReasonFromErrorText(errorText: strin
|
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Flatten a fetch rejection into one line, including the `cause` undici hides
|
|
63
|
+
* the real reason behind (`fetch failed` alone says nothing).
|
|
64
|
+
*/
|
|
65
|
+
export function describeFetchFailure(err: unknown): string {
|
|
66
|
+
const name = err instanceof Error ? err.name : "";
|
|
67
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
+
const cause: unknown = err instanceof Error ? (err as Error & { cause?: unknown }).cause : undefined;
|
|
69
|
+
const causeText = cause instanceof Error
|
|
70
|
+
? ` (cause: ${cause.name}: ${cause.message}${
|
|
71
|
+
typeof (cause as Error & { code?: unknown }).code === "string"
|
|
72
|
+
? ` [${String((cause as Error & { code?: unknown }).code)}]`
|
|
73
|
+
: ""
|
|
74
|
+
})`
|
|
75
|
+
: cause === undefined
|
|
76
|
+
? ""
|
|
77
|
+
: ` (cause: ${String(cause)})`;
|
|
78
|
+
return `${name ? `${name}: ` : ""}${message}${causeText}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What to record when every availability probe failed.
|
|
83
|
+
*
|
|
84
|
+
* A probe that TIMED OUT is not evidence the backend is down: the budget is a
|
|
85
|
+
* fixed 2s and a busy event loop can burn it before the socket is scheduled,
|
|
86
|
+
* so the backend looks dead while it answers other callers in single-digit ms.
|
|
87
|
+
* Caching that verdict took extraction offline for a whole health-check
|
|
88
|
+
* interval at a time (issue #2210), so a timeout leaves availability UNKNOWN —
|
|
89
|
+
* this call fails, the next one re-probes instead of reading a stale false.
|
|
90
|
+
*
|
|
91
|
+
* A backend that ANSWERED and said no is a real verdict and stays cached, or
|
|
92
|
+
* the daemon re-probes a known-bad endpoint on every request.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveUnavailableVerdict(args: {
|
|
95
|
+
baseUrl: string;
|
|
96
|
+
sawAbortedProbe: boolean;
|
|
97
|
+
wasAvailable: boolean | null;
|
|
98
|
+
}): { cacheVerdict: boolean; warning: string | null } {
|
|
99
|
+
if (args.sawAbortedProbe) {
|
|
100
|
+
return {
|
|
101
|
+
cacheVerdict: false,
|
|
102
|
+
warning:
|
|
103
|
+
`local LLM availability probe timed out at ${args.baseUrl} (event loop busy?); treating availability as ` +
|
|
104
|
+
"unknown and re-probing on the next request rather than marking the backend down",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
cacheVerdict: true,
|
|
109
|
+
// Surfaced at warn, not debug: extraction stops silently when this flips,
|
|
110
|
+
// and the daemon emits no debug at all unless configured for it.
|
|
111
|
+
warning: args.wasAvailable !== false ? `local LLM became unavailable at ${args.baseUrl}` : null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ProbeFetchResult {
|
|
116
|
+
ok: boolean;
|
|
117
|
+
data: unknown;
|
|
118
|
+
status: number | null;
|
|
119
|
+
/** The probe hit its own budget rather than getting an answer. */
|
|
120
|
+
aborted?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* One health-probe request.
|
|
125
|
+
*
|
|
126
|
+
* Every failure is logged with its cause: a transport error and a 404 both
|
|
127
|
+
* returned `ok: false` with nothing recorded, so an unreachable backend was
|
|
128
|
+
* indistinguishable from a wrong path and an availability failure had no
|
|
129
|
+
* diagnosable reason at all (§22, issue #2210).
|
|
130
|
+
*/
|
|
131
|
+
export async function probeFetch(
|
|
132
|
+
url: string,
|
|
133
|
+
options: { timeoutMs: number; headers: Record<string, string>; signal?: AbortSignal },
|
|
134
|
+
): Promise<ProbeFetchResult> {
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const requestSignal = options.signal
|
|
137
|
+
? AbortSignal.any([options.signal, controller.signal])
|
|
138
|
+
: controller.signal;
|
|
139
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetch(url, { signal: requestSignal, headers: options.headers });
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
log.debug(`local LLM probe: ${url} returned HTTP ${response.status}`);
|
|
145
|
+
return { ok: false, data: null, status: response.status };
|
|
146
|
+
}
|
|
147
|
+
const contentType = response.headers.get("content-type");
|
|
148
|
+
const data = contentType?.includes("application/json") ? await response.json() : await response.text();
|
|
149
|
+
return { ok: true, data, status: response.status };
|
|
150
|
+
} catch (err) {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
const aborted = isAbortError(err);
|
|
153
|
+
log.debug(
|
|
154
|
+
`local LLM probe: ${url} ${aborted ? `timed out after ${options.timeoutMs}ms` : "failed"}: ${describeFetchFailure(err)}`,
|
|
155
|
+
);
|
|
156
|
+
return { ok: false, data: null, status: null, aborted };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface PendingProbe {
|
|
161
|
+
promise: Promise<boolean>;
|
|
162
|
+
controller: AbortController;
|
|
163
|
+
/** Callers still awaiting this sequence; at zero the sequence is aborted. */
|
|
164
|
+
waiters: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Collapses concurrent probe sequences onto one in-flight run.
|
|
169
|
+
*
|
|
170
|
+
* A timed-out availability probe deliberately caches nothing (issue #2210),
|
|
171
|
+
* which removed the false verdict that used to absorb concurrent callers:
|
|
172
|
+
* every queued request would then run its own full probe sequence, on a host
|
|
173
|
+
* already slow enough to blow the probe budget.
|
|
174
|
+
*
|
|
175
|
+
* Cancellation is refcounted rather than shared: one caller walking away must
|
|
176
|
+
* not abort a sequence the others are still waiting on, so the underlying task
|
|
177
|
+
* is aborted only once every waiter has gone.
|
|
178
|
+
*/
|
|
179
|
+
export class SingleFlightProbe {
|
|
180
|
+
private pending: PendingProbe | null = null;
|
|
181
|
+
|
|
182
|
+
run(task: (signal: AbortSignal) => Promise<boolean>, signal?: AbortSignal): Promise<boolean> {
|
|
183
|
+
let active = this.pending;
|
|
184
|
+
if (!active) {
|
|
185
|
+
const controller = new AbortController();
|
|
186
|
+
const created: PendingProbe = { promise: Promise.resolve(false), controller, waiters: 0 };
|
|
187
|
+
created.promise = task(controller.signal).finally(() => {
|
|
188
|
+
if (this.pending === created) this.pending = null;
|
|
189
|
+
});
|
|
190
|
+
this.pending = created;
|
|
191
|
+
active = created;
|
|
192
|
+
}
|
|
193
|
+
const entry = active;
|
|
194
|
+
entry.waiters += 1;
|
|
195
|
+
let released = false;
|
|
196
|
+
const release = () => {
|
|
197
|
+
if (released) return;
|
|
198
|
+
released = true;
|
|
199
|
+
entry.waiters -= 1;
|
|
200
|
+
if (entry.waiters > 0) return;
|
|
201
|
+
// Detach BEFORE aborting. `pending` would otherwise still point at this
|
|
202
|
+
// doomed entry until the task's own `finally` runs a microtask later, and
|
|
203
|
+
// a caller arriving in that window would join an aborted sequence and
|
|
204
|
+
// read its `false` as a verdict.
|
|
205
|
+
if (this.pending === entry) this.pending = null;
|
|
206
|
+
entry.controller.abort();
|
|
207
|
+
};
|
|
208
|
+
if (!signal) return entry.promise.finally(release);
|
|
209
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
210
|
+
const onAbort = () => {
|
|
211
|
+
release();
|
|
212
|
+
resolve(false);
|
|
213
|
+
};
|
|
214
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
215
|
+
entry.promise.then(
|
|
216
|
+
(value) => {
|
|
217
|
+
signal.removeEventListener("abort", onAbort);
|
|
218
|
+
release();
|
|
219
|
+
resolve(value);
|
|
220
|
+
},
|
|
221
|
+
(err: unknown) => {
|
|
222
|
+
signal.removeEventListener("abort", onAbort);
|
|
223
|
+
release();
|
|
224
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
package/src/local-llm.ts
CHANGED
|
@@ -12,8 +12,12 @@ import {
|
|
|
12
12
|
extractNonRecoverableBackendReason,
|
|
13
13
|
extractNonRecoverableBackendReasonFromErrorText,
|
|
14
14
|
isAbortError,
|
|
15
|
+
probeFetch,
|
|
16
|
+
resolveUnavailableVerdict,
|
|
17
|
+
type ProbeFetchResult,
|
|
15
18
|
normalizeBackendTripReason,
|
|
16
19
|
waitForRetryBackoff,
|
|
20
|
+
SingleFlightProbe,
|
|
17
21
|
} from "./local-llm-helpers.js";
|
|
18
22
|
|
|
19
23
|
/** Trim trailing slash characters without backtracking regex. */
|
|
@@ -256,6 +260,7 @@ export class LocalLlmClient {
|
|
|
256
260
|
private config: PluginConfig;
|
|
257
261
|
private isAvailable: boolean | null = null;
|
|
258
262
|
private lastHealthCheck: number = 0;
|
|
263
|
+
private readonly availabilityProbe = new SingleFlightProbe();
|
|
259
264
|
private detectedType: LocalLlmType | null = null;
|
|
260
265
|
private cachedModelInfo: LocalModelInfo | null = null;
|
|
261
266
|
private cachedLmsContext: number | null = null;
|
|
@@ -371,39 +376,20 @@ export class LocalLlmClient {
|
|
|
371
376
|
|
|
372
377
|
|
|
373
378
|
/**
|
|
374
|
-
* Fetch with timeout for health checks
|
|
379
|
+
* Fetch with timeout for health checks. Body lives in the sibling helper so
|
|
380
|
+
* this file stays under its size ceiling (issue #1995).
|
|
375
381
|
*/
|
|
376
382
|
private async fetchWithTimeout(
|
|
377
383
|
url: string,
|
|
378
384
|
timeoutMs: number = 2000,
|
|
379
385
|
headers?: Record<string, string>,
|
|
380
386
|
signal?: AbortSignal,
|
|
381
|
-
): Promise<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const response = await fetch(url, {
|
|
388
|
-
signal: requestSignal,
|
|
389
|
-
headers: this.buildRequestHeaders({ Accept: "application/json", ...(headers ?? {}) }),
|
|
390
|
-
});
|
|
391
|
-
clearTimeout(timeout);
|
|
392
|
-
|
|
393
|
-
if (!response.ok) {
|
|
394
|
-
return { ok: false, data: null, status: response.status };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const contentType = response.headers.get("content-type");
|
|
398
|
-
if (contentType?.includes("application/json")) {
|
|
399
|
-
return { ok: true, data: await response.json(), status: response.status };
|
|
400
|
-
} else {
|
|
401
|
-
return { ok: true, data: await response.text(), status: response.status };
|
|
402
|
-
}
|
|
403
|
-
} catch (err) {
|
|
404
|
-
clearTimeout(timeout);
|
|
405
|
-
return { ok: false, data: null, status: null };
|
|
406
|
-
}
|
|
387
|
+
): Promise<ProbeFetchResult> {
|
|
388
|
+
return await probeFetch(url, {
|
|
389
|
+
timeoutMs,
|
|
390
|
+
headers: this.buildRequestHeaders({ Accept: "application/json", ...(headers ?? {}) }),
|
|
391
|
+
signal,
|
|
392
|
+
});
|
|
407
393
|
}
|
|
408
394
|
|
|
409
395
|
private async probeLmStudioNativeModels(
|
|
@@ -445,8 +431,11 @@ export class LocalLlmClient {
|
|
|
445
431
|
if (this.isAvailable !== null && now - this.lastHealthCheck < LocalLlmClient.HEALTH_CHECK_INTERVAL_MS) {
|
|
446
432
|
return this.isAvailable;
|
|
447
433
|
}
|
|
434
|
+
return await this.availabilityProbe.run((probeSignal) => this.probeAvailability(probeSignal), signal);
|
|
435
|
+
}
|
|
448
436
|
|
|
449
|
-
|
|
437
|
+
private async probeAvailability(signal?: AbortSignal): Promise<boolean> {
|
|
438
|
+
const now = Date.now();
|
|
450
439
|
// Probe server-native endpoints from the server root even when users configure
|
|
451
440
|
// the OpenAI-compatible `/v1` base URL for chat completions.
|
|
452
441
|
const configuredBaseUrl = trimTrailingSlashes(
|
|
@@ -454,6 +443,10 @@ export class LocalLlmClient {
|
|
|
454
443
|
);
|
|
455
444
|
const probeBaseUrl = stripTrailingV1Path(configuredBaseUrl);
|
|
456
445
|
let sawUnauthorizedProbe = false;
|
|
446
|
+
// A probe that TIMED OUT says the event loop was busy, not that the backend
|
|
447
|
+
// is down (issue #2210). Tracked separately so a loaded daemon cannot cache
|
|
448
|
+
// itself into a blackout.
|
|
449
|
+
let sawAbortedProbe = false;
|
|
457
450
|
|
|
458
451
|
// Try to detect which server type is running
|
|
459
452
|
if (signal?.aborted) return false;
|
|
@@ -462,6 +455,7 @@ export class LocalLlmClient {
|
|
|
462
455
|
log.debug(`checking ${serverConfig.type} at ${healthUrl}`);
|
|
463
456
|
|
|
464
457
|
const result = await this.fetchWithTimeout(healthUrl, 2000, undefined, signal);
|
|
458
|
+
if (result.aborted) sawAbortedProbe = true;
|
|
465
459
|
if (signal?.aborted) return false;
|
|
466
460
|
if (result.ok && serverConfig.detectFn(result.data)) {
|
|
467
461
|
if (serverConfig.type === "mlx") {
|
|
@@ -481,6 +475,7 @@ export class LocalLlmClient {
|
|
|
481
475
|
if (serverConfig.type === "llamacpp") {
|
|
482
476
|
let sawLlamaCppSignal = false;
|
|
483
477
|
const propsProbe = await this.fetchWithTimeout(`${probeBaseUrl}/props`, 2000, undefined, signal);
|
|
478
|
+
if (propsProbe.aborted) sawAbortedProbe = true;
|
|
484
479
|
if (signal?.aborted) return false;
|
|
485
480
|
if (propsProbe.ok && isLlamaCppPropsResponse(propsProbe.data)) {
|
|
486
481
|
sawLlamaCppSignal = true;
|
|
@@ -491,6 +486,7 @@ export class LocalLlmClient {
|
|
|
491
486
|
|
|
492
487
|
const modelsUrl = `${probeBaseUrl}${serverConfig.modelsEndpoint}`;
|
|
493
488
|
const modelsProbe = await this.fetchWithTimeout(modelsUrl, 2000, undefined, signal);
|
|
489
|
+
if (modelsProbe.aborted) sawAbortedProbe = true;
|
|
494
490
|
if (signal?.aborted) return false;
|
|
495
491
|
if (modelsProbe.ok && isLlamaCppModelsResponse(modelsProbe.data)) {
|
|
496
492
|
sawLlamaCppSignal = true;
|
|
@@ -523,6 +519,7 @@ export class LocalLlmClient {
|
|
|
523
519
|
try {
|
|
524
520
|
const modelsUrl = `${probeBaseUrl}/v1/models`;
|
|
525
521
|
const result = await this.fetchWithTimeout(modelsUrl, 2000, undefined, signal);
|
|
522
|
+
if (result.aborted) sawAbortedProbe = true;
|
|
526
523
|
if (signal?.aborted) return false;
|
|
527
524
|
if (result.ok) {
|
|
528
525
|
this.isAvailable = true;
|
|
@@ -538,14 +535,21 @@ export class LocalLlmClient {
|
|
|
538
535
|
// Fall through to unavailable
|
|
539
536
|
}
|
|
540
537
|
|
|
541
|
-
this.isAvailable
|
|
538
|
+
const wasAvailable = this.isAvailable;
|
|
542
539
|
this.detectedType = null;
|
|
543
|
-
this.lastHealthCheck = now;
|
|
544
540
|
if (sawUnauthorizedProbe) {
|
|
545
541
|
log.warn(
|
|
546
542
|
`local LLM availability probe was unauthorized at ${configuredBaseUrl}; verify localLlmApiKey and localLlmAuthHeader settings`,
|
|
547
543
|
);
|
|
548
544
|
}
|
|
545
|
+
const verdict = resolveUnavailableVerdict({
|
|
546
|
+
baseUrl: configuredBaseUrl,
|
|
547
|
+
sawAbortedProbe,
|
|
548
|
+
wasAvailable,
|
|
549
|
+
});
|
|
550
|
+
this.isAvailable = verdict.cacheVerdict ? false : null;
|
|
551
|
+
this.lastHealthCheck = verdict.cacheVerdict ? now : 0;
|
|
552
|
+
if (verdict.warning) log.warn(verdict.warning);
|
|
549
553
|
log.debug("local LLM not available at", configuredBaseUrl);
|
|
550
554
|
return false;
|
|
551
555
|
}
|
|
@@ -6,6 +6,7 @@ import type { MemoryFile, MemoryStatus } from "../types.js";
|
|
|
6
6
|
import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
|
|
7
7
|
import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
|
|
8
8
|
import { assertPathInsideRoot } from "../utils/path-containment.js";
|
|
9
|
+
import { requestEntityCanonicalIdReconcile } from "../storage/entity-canonical-id-references.js";
|
|
9
10
|
|
|
10
11
|
export type MemoryGovernanceMode = "shadow" | "apply";
|
|
11
12
|
export type MemoryGovernanceReasonCode =
|
|
@@ -1021,6 +1022,9 @@ export async function restoreMemoryGovernanceRun(
|
|
|
1021
1022
|
// same-process or peer readAllMemories rescans mid-batch and never serves a
|
|
1022
1023
|
// partially-restored corpus (Cursor Medium, #1902).
|
|
1023
1024
|
bumpMemoryCorpusVersionForDir(options.memoryDir);
|
|
1025
|
+
// Restored bytes predate the current migration journal and can carry
|
|
1026
|
+
// legacy entity references (issue #2213) — request one reconcile pass.
|
|
1027
|
+
await requestEntityCanonicalIdReconcile(path.join(options.memoryDir, "state"));
|
|
1024
1028
|
}
|
|
1025
1029
|
|
|
1026
1030
|
return {
|
package/src/page-versioning.ts
CHANGED
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
} from "node:fs/promises";
|
|
28
28
|
import { ALL_CATEGORY_DIRS, RECALL_FALLBACK_DIRS } from "./utils/category-dir.js";
|
|
29
29
|
import { bumpMemoryCorpusVersionForDir } from "./memory-corpus-version.js";
|
|
30
|
+
import {
|
|
31
|
+
pathMayCarryEntityRefs,
|
|
32
|
+
requestEntityCanonicalIdReconcile,
|
|
33
|
+
} from "./storage/entity-canonical-id-references.js";
|
|
30
34
|
|
|
31
35
|
// ---------------------------------------------------------------------------
|
|
32
36
|
// Public interfaces
|
|
@@ -342,9 +346,20 @@ export async function revertToVersion(
|
|
|
342
346
|
// non-memory queue dirs (questions/). Use RECALL_FALLBACK_DIRS — a revert of a
|
|
343
347
|
// questions/ page must NOT bump the recall corpus sentinel and force a rescan.
|
|
344
348
|
const revertedTop = relPath(pagePath, resolvedMemoryDir).split(path.sep)[0];
|
|
345
|
-
|
|
349
|
+
const inRecallTier = (RECALL_FALLBACK_DIRS as readonly string[]).includes(revertedTop);
|
|
350
|
+
if (inRecallTier) {
|
|
346
351
|
bumpMemoryCorpusVersionForDir(resolvedMemoryDir);
|
|
347
352
|
}
|
|
353
|
+
// A pre-migration snapshot can reintroduce a legacy entityRef — or, under
|
|
354
|
+
// entities/, a legacy relationship target rewriteRelationshipTargets had
|
|
355
|
+
// migrated — that the target's completed journal already renamed (issue
|
|
356
|
+
// #2213); the revert keeps the snapshot bytes faithful, so request one
|
|
357
|
+
// bounded reconcile pass instead. pathMayCarryEntityRefs is the migration's
|
|
358
|
+
// own scan scope (hot recall + cold + archive + entities), which is wider
|
|
359
|
+
// than the corpus bump above on purpose.
|
|
360
|
+
if (pathMayCarryEntityRefs(resolvedMemoryDir, pagePath)) {
|
|
361
|
+
await requestEntityCanonicalIdReconcile(path.join(resolvedMemoryDir, "state"));
|
|
362
|
+
}
|
|
348
363
|
log.debug(`page-versioning: reverted ${pagePath} to version ${versionId}`);
|
|
349
364
|
|
|
350
365
|
return version;
|
package/src/review/index.ts
CHANGED
|
@@ -9,6 +9,58 @@ import fs from "node:fs";
|
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { getCategoryDir, ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
|
|
11
11
|
import { bumpMemoryCorpusVersionForDir } from "../memory-corpus-version.js";
|
|
12
|
+
import {
|
|
13
|
+
HistoricalEntityCanonicalIdCache,
|
|
14
|
+
canonicalizeEntityRefFrontmatter,
|
|
15
|
+
repairContentAfterJournalMoveSync,
|
|
16
|
+
} from "../storage/entity-canonical-id-references.js";
|
|
17
|
+
|
|
18
|
+
// Write-boundary journal cache for review-queue promotions (issue #2213).
|
|
19
|
+
const historicalIdCache = new HistoricalEntityCanonicalIdCache();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* In-place raw rewrite under the uniform write-boundary rule (issue #2213):
|
|
23
|
+
* canonicalize the effective entityRef at the write and REPAIR when the
|
|
24
|
+
* journal moved across it — these rewrites re-serialize whole records, so an
|
|
25
|
+
* unrelated approve/dismiss/flag must not write a legacy ref back out. On
|
|
26
|
+
* repair exhaustion the ORIGINAL bytes are restored before the retryable
|
|
27
|
+
* error propagates: performReview bumps the corpus sentinel only after the
|
|
28
|
+
* action returns, so a half-applied rewrite would be invisible to a warm
|
|
29
|
+
* cache while the caller is told the action failed.
|
|
30
|
+
*/
|
|
31
|
+
function writeReviewFileCanonicalized(memoryDir: string, filePath: string, content: string): void {
|
|
32
|
+
const stateDir = path.join(memoryDir, "state");
|
|
33
|
+
const idsAtWrite = historicalIdCache.get(stateDir);
|
|
34
|
+
let originalBytes: string | null = null;
|
|
35
|
+
try {
|
|
36
|
+
originalBytes = fs.readFileSync(filePath, "utf8");
|
|
37
|
+
} catch {
|
|
38
|
+
originalBytes = null; // new file — rollback is removal
|
|
39
|
+
}
|
|
40
|
+
const written = canonicalizeEntityRefFrontmatter(content, idsAtWrite);
|
|
41
|
+
fs.writeFileSync(filePath, written, "utf8");
|
|
42
|
+
try {
|
|
43
|
+
repairContentAfterJournalMoveSync({
|
|
44
|
+
stateDir,
|
|
45
|
+
cache: historicalIdCache,
|
|
46
|
+
idsAtWrite,
|
|
47
|
+
rawContent: content,
|
|
48
|
+
lastWritten: written,
|
|
49
|
+
rewrite: (next) => fs.writeFileSync(filePath, next, "utf8"),
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
try {
|
|
53
|
+
if (originalBytes === null) {
|
|
54
|
+
fs.unlinkSync(filePath);
|
|
55
|
+
} else {
|
|
56
|
+
fs.writeFileSync(filePath, originalBytes, "utf8");
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Best effort — the retryable repair error is the primary signal.
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
12
64
|
|
|
13
65
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
14
66
|
|
|
@@ -295,13 +347,16 @@ function approveItem(
|
|
|
295
347
|
const fm = parseFrontmatter(content);
|
|
296
348
|
if (!fm) return { itemId, action: "approve", message: "Could not parse frontmatter" };
|
|
297
349
|
|
|
298
|
-
// Issue #1579 —
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
350
|
+
// Issue #1579 — approving a tombstone-blocked memory fires the revocation
|
|
351
|
+
// hook so the content is re-allowed (append-only log: a `kind: "revocation"`
|
|
352
|
+
// entry supersedes the tombstone at lookup). Deferred until the approval's
|
|
353
|
+
// writes commit (Codex P2, round 19): revoking before the promote left a
|
|
354
|
+
// failed repair with the non-resurrection guard already destroyed, letting
|
|
355
|
+
// a later extraction resurrect the content. Fire-and-forget once invoked:
|
|
356
|
+
// a hook failure never fails the approval.
|
|
303
357
|
const blockedBy = typeof fm.blockedBy === "string" ? fm.blockedBy : null;
|
|
304
|
-
|
|
358
|
+
const fireBlockedRevocation = (): void => {
|
|
359
|
+
if (!blockedBy || !options.onApproveBlockedMemory) return;
|
|
305
360
|
try {
|
|
306
361
|
const maybe = options.onApproveBlockedMemory(blockedBy, itemId);
|
|
307
362
|
if (maybe && typeof (maybe as Promise<void>).then === "function") {
|
|
@@ -310,7 +365,7 @@ function approveItem(
|
|
|
310
365
|
} catch {
|
|
311
366
|
/* fire-and-forget — never fail the approval (gotcha #13) */
|
|
312
367
|
}
|
|
313
|
-
}
|
|
368
|
+
};
|
|
314
369
|
|
|
315
370
|
const updatedContent = updateFrontmatterFields(content, {
|
|
316
371
|
confidence: "0.9",
|
|
@@ -324,7 +379,8 @@ function approveItem(
|
|
|
324
379
|
});
|
|
325
380
|
|
|
326
381
|
if (found.location === "category") {
|
|
327
|
-
|
|
382
|
+
writeReviewFileCanonicalized(memoryDir, found.filePath, updatedContent);
|
|
383
|
+
fireBlockedRevocation();
|
|
328
384
|
return {
|
|
329
385
|
itemId,
|
|
330
386
|
action: "approve",
|
|
@@ -342,10 +398,38 @@ function approveItem(
|
|
|
342
398
|
const outputPath = path.join(targetDir, dateDir, path.basename(found.filePath));
|
|
343
399
|
|
|
344
400
|
ensureSafeDirectory(rootReal, path.dirname(outputPath));
|
|
345
|
-
|
|
401
|
+
// The suggestions/review queue dirs sit OUTSIDE the migration's reference
|
|
402
|
+
// scan, so a queued pre-migration item can still carry a legacy entityRef
|
|
403
|
+
// (issue #2213): promote it canonicalized against the target journal, and
|
|
404
|
+
// REPAIR when the journal moved across the write.
|
|
405
|
+
const reviewStateDir = path.join(memoryDir, "state");
|
|
406
|
+
const idsAtWrite = historicalIdCache.get(reviewStateDir);
|
|
407
|
+
const promotedWritten = canonicalizeEntityRefFrontmatter(updatedContent, idsAtWrite);
|
|
408
|
+
const promotedPath = writeFileWithoutClobber(outputPath, promotedWritten, itemId);
|
|
409
|
+
try {
|
|
410
|
+
repairContentAfterJournalMoveSync({
|
|
411
|
+
stateDir: reviewStateDir,
|
|
412
|
+
cache: historicalIdCache,
|
|
413
|
+
idsAtWrite,
|
|
414
|
+
rawContent: updatedContent,
|
|
415
|
+
lastWritten: promotedWritten,
|
|
416
|
+
rewrite: (next) => fs.writeFileSync(promotedPath, next, "utf8"),
|
|
417
|
+
});
|
|
418
|
+
} catch (err) {
|
|
419
|
+
// Roll back so a retried approve does not clobber-fail against a partial
|
|
420
|
+
// promotion; the queue entry survives untouched (Bugbot).
|
|
421
|
+
try {
|
|
422
|
+
fs.unlinkSync(promotedPath);
|
|
423
|
+
} catch {
|
|
424
|
+
// Best effort — the retryable repair error below is the primary signal.
|
|
425
|
+
}
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
346
428
|
|
|
347
|
-
// Remove from review
|
|
429
|
+
// Remove from review, THEN revoke: the guard only falls once the approval
|
|
430
|
+
// has fully committed (promoted file repaired + queue entry gone).
|
|
348
431
|
fs.unlinkSync(found.filePath);
|
|
432
|
+
fireBlockedRevocation();
|
|
349
433
|
|
|
350
434
|
return {
|
|
351
435
|
itemId,
|
|
@@ -372,13 +456,13 @@ function dismissItem(
|
|
|
372
456
|
}
|
|
373
457
|
|
|
374
458
|
const content = fs.readFileSync(found.filePath, "utf8");
|
|
375
|
-
|
|
459
|
+
writeReviewFileCanonicalized(
|
|
460
|
+
memoryDir,
|
|
376
461
|
found.filePath,
|
|
377
462
|
updateFrontmatterFields(content, {
|
|
378
463
|
reviewDismissed: "true",
|
|
379
464
|
reviewDismissedAt: new Date().toISOString(),
|
|
380
465
|
}),
|
|
381
|
-
"utf8",
|
|
382
466
|
);
|
|
383
467
|
return {
|
|
384
468
|
itemId,
|
|
@@ -403,7 +487,7 @@ function flagItem(
|
|
|
403
487
|
flagged: "true",
|
|
404
488
|
flaggedAt: new Date().toISOString(),
|
|
405
489
|
});
|
|
406
|
-
|
|
490
|
+
writeReviewFileCanonicalized(memoryDir, found.filePath, fixed);
|
|
407
491
|
return {
|
|
408
492
|
itemId,
|
|
409
493
|
action: "flag",
|