@remnic/core 9.3.708 → 9.3.709
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-boundary.d.ts +7 -7
- package/dist/access-boundary.js +8 -8
- package/dist/access-cli.js +111 -24
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +6 -6
- package/dist/access-http.js +12 -12
- package/dist/access-mcp.d.ts +14 -6
- package/dist/access-mcp.js +11 -11
- package/dist/access-operations-batch.js +9 -9
- package/dist/access-operations.d.ts +28 -8
- package/dist/access-operations.js +14 -10
- package/dist/access-schema.d.ts +6 -6
- package/dist/{access-service-j1c1gptF.d.ts → access-service-CoIA0NrG.d.ts} +186 -3
- 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/access-surface-catalog.js +7 -0
- package/dist/access-surface-catalog.js.map +1 -1
- 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 +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +3 -3
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-CKxilpzS.d.ts → catalog-DQCZrBjw.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-PDJQVAGM.js → chunk-2T4TDXPC.js} +14 -14
- package/dist/{chunk-W2WQ4LE7.js → chunk-35YJ6KCV.js} +2 -2
- package/dist/{chunk-C63MK3WL.js → chunk-3TCRU4JA.js} +2 -2
- package/dist/{chunk-WRGPE6AW.js → chunk-3XHD3XGK.js} +44 -1
- package/dist/chunk-3XHD3XGK.js.map +1 -0
- package/dist/{chunk-GKI6LX5L.js → chunk-54TI5GLV.js} +2 -2
- package/dist/{chunk-IVCQW4C4.js → chunk-7IBEWQLG.js} +2 -2
- package/dist/{chunk-Y6YHAGQP.js → chunk-7NDYFAJS.js} +2 -2
- package/dist/{chunk-SPETAWFE.js → chunk-AZVHBFI3.js} +2 -2
- package/dist/{chunk-MLLTU5FX.js → chunk-BOGENF7P.js} +1 -1
- package/dist/chunk-BOGENF7P.js.map +1 -0
- package/dist/{chunk-3PTOZJOI.js → chunk-BQCFXAMN.js} +2 -2
- package/dist/{chunk-4E2QCH46.js → chunk-CNVIWMQI.js} +2 -2
- package/dist/{chunk-4DZATVK5.js → chunk-CXMXAC5R.js} +3 -3
- package/dist/{chunk-STYMKPFT.js → chunk-EVX52NCY.js} +1459 -21
- package/dist/chunk-EVX52NCY.js.map +1 -0
- package/dist/{chunk-W2S3Z5MT.js → chunk-GMRNKPWO.js} +2 -2
- package/dist/{chunk-HIV5E57C.js → chunk-HSDJCT3V.js} +2 -2
- package/dist/{chunk-NM6TSEGQ.js → chunk-JNOYYWCA.js} +2 -2
- package/dist/{chunk-IONFO7UK.js → chunk-JQ7XVM4V.js} +3 -3
- package/dist/{chunk-2VQYHHWB.js → chunk-JSDZMOT7.js} +11 -2
- package/dist/chunk-JSDZMOT7.js.map +1 -0
- package/dist/{chunk-WSWYIKXX.js → chunk-KOEKDZ6A.js} +62 -5
- package/dist/chunk-KOEKDZ6A.js.map +1 -0
- package/dist/{chunk-WQADZ3ZY.js → chunk-NK3SPJLM.js} +20 -19
- package/dist/chunk-NK3SPJLM.js.map +1 -0
- package/dist/{chunk-WDH3KUZU.js → chunk-NXL5CVE7.js} +2 -2
- package/dist/{chunk-BGAHTI4C.js → chunk-OK7FUX6R.js} +6 -6
- package/dist/{chunk-24FGNOQS.js → chunk-P6PRSI3W.js} +85 -4
- package/dist/chunk-P6PRSI3W.js.map +1 -0
- package/dist/{chunk-66TSLESZ.js → chunk-PKE7EJMX.js} +2 -2
- package/dist/chunk-PKE7EJMX.js.map +1 -0
- package/dist/{chunk-MOXFPLD6.js → chunk-QXNFQKWU.js} +2 -2
- package/dist/{chunk-MRX6ZXHZ.js → chunk-S6FQLQGH.js} +2 -2
- package/dist/{chunk-6HPJMR5I.js → chunk-SKQCFAYU.js} +2 -2
- package/dist/{chunk-EJDAXR7O.js → chunk-VWB3HDY6.js} +56 -8
- package/dist/chunk-VWB3HDY6.js.map +1 -0
- package/dist/{chunk-N75N5SNX.js → chunk-XD33EX2F.js} +3 -3
- package/dist/{chunk-ESMY4RJ4.js → chunk-XKU4YE6Z.js} +2 -2
- package/dist/{chunk-XKMDDM7P.js → chunk-Y6PIFKXX.js} +2 -2
- package/dist/{chunk-RH2OSRQY.js → chunk-Z56IHRVV.js} +2 -2
- package/dist/{cli-CbT-pyM4.d.ts → cli-qex-L3GT.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +26 -26
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +3 -3
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +3 -3
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +3 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- 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/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +32 -32
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +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 +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- 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 +2 -2
- package/dist/namespaces/storage.js +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +9 -9
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-D4ovYV3x.d.ts → orchestrator-DsVKLEBk.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +15 -15
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +2 -2
- 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-DyMUCsfN.d.ts → semantic-consolidation-_hVxkTuF.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +8 -1
- package/dist/storage.js +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-Couvz-L3.d.ts → types-DUK4vVnN.d.ts} +27 -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-boundary.ts +2 -0
- package/src/access-cli.ts +107 -2
- package/src/access-http.ts +70 -1
- package/src/access-mcp.ts +65 -0
- package/src/access-operations.ts +122 -0
- package/src/access-service.ts +92 -0
- package/src/access-surface-catalog.test.ts +6 -1
- package/src/access-surface-catalog.ts +7 -0
- package/src/cli.ts +1 -0
- package/src/config.test.ts +10 -0
- package/src/config.ts +43 -0
- package/src/correction/correction-access-wiring.ts +887 -0
- package/src/correction/correction-contract.ts +416 -0
- package/src/correction/correction-executor.test.ts +742 -0
- package/src/correction/correction-executor.ts +473 -0
- package/src/correction/correction-planner.test.ts +446 -0
- package/src/correction/correction-planner.ts +546 -0
- package/src/correction/correction-service.ts +180 -0
- package/src/correction/correction-surfaces.test.ts +245 -0
- package/src/correction/index.ts +43 -0
- package/src/storage.ts +10 -0
- package/src/types.ts +27 -0
- package/dist/chunk-24FGNOQS.js.map +0 -1
- package/dist/chunk-2VQYHHWB.js.map +0 -1
- package/dist/chunk-66TSLESZ.js.map +0 -1
- package/dist/chunk-EJDAXR7O.js.map +0 -1
- package/dist/chunk-MLLTU5FX.js.map +0 -1
- package/dist/chunk-STYMKPFT.js.map +0 -1
- package/dist/chunk-WQADZ3ZY.js.map +0 -1
- package/dist/chunk-WRGPE6AW.js.map +0 -1
- package/dist/chunk-WSWYIKXX.js.map +0 -1
- /package/dist/{chunk-PDJQVAGM.js.map → chunk-2T4TDXPC.js.map} +0 -0
- /package/dist/{chunk-W2WQ4LE7.js.map → chunk-35YJ6KCV.js.map} +0 -0
- /package/dist/{chunk-C63MK3WL.js.map → chunk-3TCRU4JA.js.map} +0 -0
- /package/dist/{chunk-GKI6LX5L.js.map → chunk-54TI5GLV.js.map} +0 -0
- /package/dist/{chunk-IVCQW4C4.js.map → chunk-7IBEWQLG.js.map} +0 -0
- /package/dist/{chunk-Y6YHAGQP.js.map → chunk-7NDYFAJS.js.map} +0 -0
- /package/dist/{chunk-SPETAWFE.js.map → chunk-AZVHBFI3.js.map} +0 -0
- /package/dist/{chunk-3PTOZJOI.js.map → chunk-BQCFXAMN.js.map} +0 -0
- /package/dist/{chunk-4E2QCH46.js.map → chunk-CNVIWMQI.js.map} +0 -0
- /package/dist/{chunk-4DZATVK5.js.map → chunk-CXMXAC5R.js.map} +0 -0
- /package/dist/{chunk-W2S3Z5MT.js.map → chunk-GMRNKPWO.js.map} +0 -0
- /package/dist/{chunk-HIV5E57C.js.map → chunk-HSDJCT3V.js.map} +0 -0
- /package/dist/{chunk-NM6TSEGQ.js.map → chunk-JNOYYWCA.js.map} +0 -0
- /package/dist/{chunk-IONFO7UK.js.map → chunk-JQ7XVM4V.js.map} +0 -0
- /package/dist/{chunk-WDH3KUZU.js.map → chunk-NXL5CVE7.js.map} +0 -0
- /package/dist/{chunk-BGAHTI4C.js.map → chunk-OK7FUX6R.js.map} +0 -0
- /package/dist/{chunk-MOXFPLD6.js.map → chunk-QXNFQKWU.js.map} +0 -0
- /package/dist/{chunk-MRX6ZXHZ.js.map → chunk-S6FQLQGH.js.map} +0 -0
- /package/dist/{chunk-6HPJMR5I.js.map → chunk-SKQCFAYU.js.map} +0 -0
- /package/dist/{chunk-N75N5SNX.js.map → chunk-XD33EX2F.js.map} +0 -0
- /package/dist/{chunk-ESMY4RJ4.js.map → chunk-XKU4YE6Z.js.map} +0 -0
- /package/dist/{chunk-XKMDDM7P.js.map → chunk-Y6PIFKXX.js.map} +0 -0
- /package/dist/{chunk-RH2OSRQY.js.map → chunk-Z56IHRVV.js.map} +0 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* correction/correction-access-wiring.ts — wires the CorrectionService to the
|
|
3
|
+
* orchestrator's storage / tombstone / search surfaces (issue #1580 PR 3).
|
|
4
|
+
*
|
|
5
|
+
* Keeps the access-service god-file thin: the service calls `createCorrectionService`
|
|
6
|
+
* with two namespace-policy callbacks and gets back a fully-wired
|
|
7
|
+
* {@link CorrectionService}. All the heavy lifting (search, LLM classify,
|
|
8
|
+
* tombstone emission, audit-record write, propagation) lives here, in a
|
|
9
|
+
* dedicated module, so the only growth in access-service.ts is the four
|
|
10
|
+
* one-line delegators.
|
|
11
|
+
*
|
|
12
|
+
* Design rules honored:
|
|
13
|
+
* - Caller-supplied namespaces are NEVER trusted raw — the service resolves
|
|
14
|
+
* them through the injected policy (rule 42).
|
|
15
|
+
* - Corrections flow through the existing storage chokepoints: writeMemory
|
|
16
|
+
* (catalog/dedup/reindex fire — rule 43), appendTombstone (#1579),
|
|
17
|
+
* writeMemoryFrontmatter (status flip + validUntil — #1578).
|
|
18
|
+
* - The LLM classify+draft adapter routes through the existing extraction
|
|
19
|
+
* engine (Responses API only, gotcha 1). On any LLM failure the adapter
|
|
20
|
+
* returns a deterministic fallback so the planner never throws (rule 13).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
25
|
+
import type { Orchestrator } from "../orchestrator.js";
|
|
26
|
+
import type { MemoryFile, MemoryStatus, PluginConfig } from "../types.js";
|
|
27
|
+
import { stripAttributesSuffix } from "../structured-attributes.js";
|
|
28
|
+
import {
|
|
29
|
+
CorrectionContractError,
|
|
30
|
+
validateCorrectionAction,
|
|
31
|
+
type CorrectionAction,
|
|
32
|
+
type CorrectionOutcome,
|
|
33
|
+
type CorrectionPlan,
|
|
34
|
+
} from "./correction-contract.js";
|
|
35
|
+
import {
|
|
36
|
+
type LlmClassificationResult,
|
|
37
|
+
type PlannerCandidate,
|
|
38
|
+
type PlannerDeps,
|
|
39
|
+
} from "./correction-planner.js";
|
|
40
|
+
import { type ExecutorDeps, type ExecutorMemory } from "./correction-executor.js";
|
|
41
|
+
import { CorrectionService, type CorrectionServiceDeps } from "./correction-service.js";
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Public entry: build a fully-wired CorrectionService
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The two namespace-policy callbacks the access-service supplies. These are
|
|
49
|
+
* the ONLY surface-area touch-points between the correction modules and the
|
|
50
|
+
* access-service god-file — everything else is wired from the orchestrator.
|
|
51
|
+
*/
|
|
52
|
+
export interface CorrectionAccessWiring {
|
|
53
|
+
orchestrator: Orchestrator;
|
|
54
|
+
/** Resolve the AUTHORIZED namespace for a plan/apply request (write ACL). */
|
|
55
|
+
resolveAuthorizedNamespace(request: {
|
|
56
|
+
namespace?: string;
|
|
57
|
+
sessionKey?: string;
|
|
58
|
+
principal?: string;
|
|
59
|
+
}): Promise<string>;
|
|
60
|
+
/** Resolve the caller's READABLE namespaces (scopes the planner's search). */
|
|
61
|
+
resolveReadableNamespaces(request: {
|
|
62
|
+
namespace?: string;
|
|
63
|
+
sessionKey?: string;
|
|
64
|
+
principal?: string;
|
|
65
|
+
}): readonly string[];
|
|
66
|
+
/** Whether the caller may WRITE a namespace — authorizes rescope destinations. */
|
|
67
|
+
canWriteNamespace(request: {
|
|
68
|
+
namespace: string;
|
|
69
|
+
sessionKey?: string;
|
|
70
|
+
principal?: string;
|
|
71
|
+
}): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Optional LLM-complete callback (Responses API only — gotcha 1). When
|
|
74
|
+
* absent, the planner's classify+draft step falls back to deterministic
|
|
75
|
+
* mode (rule 13). The access-service wires the orchestrator's extraction
|
|
76
|
+
* LLM here once it exposes a public accessor; until then the contract
|
|
77
|
+
* ships with the safe fallback.
|
|
78
|
+
*/
|
|
79
|
+
llmComplete?(request: {
|
|
80
|
+
system: string;
|
|
81
|
+
user: string;
|
|
82
|
+
}): Promise<string>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createCorrectionService(wiring: CorrectionAccessWiring): CorrectionService {
|
|
86
|
+
const cfg = wiring.orchestrator.config;
|
|
87
|
+
// parseConfig now owns these (review thread Txp): the orchestrator's config
|
|
88
|
+
// carries correctionEnabled / correctionApplyRequiresConfirm /
|
|
89
|
+
// correctionMaxAffected / correctionPlanTtlHours as parsed fields. The
|
|
90
|
+
// loose-read helpers stay as a fallback only for tests that construct a
|
|
91
|
+
// PluginConfig-shaped object without running parseConfig.
|
|
92
|
+
const correctionEnabled = isCorrectionFeatureEnabled(cfg);
|
|
93
|
+
const applyRequiresConfirm =
|
|
94
|
+
typeof cfg.correctionApplyRequiresConfirm === "boolean"
|
|
95
|
+
? cfg.correctionApplyRequiresConfirm
|
|
96
|
+
: readCorrectionFlag(cfg, "applyRequiresConfirm", true);
|
|
97
|
+
const maxAffected =
|
|
98
|
+
typeof cfg.correctionMaxAffected === "number" && cfg.correctionMaxAffected >= 1
|
|
99
|
+
? Math.floor(cfg.correctionMaxAffected)
|
|
100
|
+
: readCorrectionNumber(cfg, "maxAffected", 10);
|
|
101
|
+
const planTtlHours =
|
|
102
|
+
typeof cfg.correctionPlanTtlHours === "number" && cfg.correctionPlanTtlHours > 0
|
|
103
|
+
? cfg.correctionPlanTtlHours
|
|
104
|
+
: readCorrectionNumber(cfg, "planTtlHours", 24);
|
|
105
|
+
const biTemporalEnabled = cfg.temporalBiTemporal === true;
|
|
106
|
+
|
|
107
|
+
const serviceDeps: CorrectionServiceDeps = {
|
|
108
|
+
policy: {
|
|
109
|
+
resolveAuthorizedNamespace: (req) => wiring.resolveAuthorizedNamespace(req),
|
|
110
|
+
canWriteNamespace: (req) => wiring.canWriteNamespace(req),
|
|
111
|
+
readableNamespaces: (req) =>
|
|
112
|
+
Promise.resolve(wiring.resolveReadableNamespaces(req)),
|
|
113
|
+
},
|
|
114
|
+
plannerDeps: () =>
|
|
115
|
+
makePlannerDeps(wiring, { maxAffected, planTtlHours }),
|
|
116
|
+
executorDeps: () => makeExecutorDeps(wiring, { biTemporalEnabled }),
|
|
117
|
+
isEnabled: () => correctionEnabled,
|
|
118
|
+
applyRequiresConfirm: () => applyRequiresConfirm,
|
|
119
|
+
};
|
|
120
|
+
return new CorrectionService(serviceDeps);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Planner deps
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
function makePlannerDeps(
|
|
128
|
+
wiring: CorrectionAccessWiring,
|
|
129
|
+
opts: { maxAffected: number; planTtlHours: number },
|
|
130
|
+
): PlannerDeps {
|
|
131
|
+
return {
|
|
132
|
+
searchCorpus: async ({ text, namespaces, limit }) =>
|
|
133
|
+
searchMemories(wiring, text, namespaces, limit),
|
|
134
|
+
resolveTargets: async ({ targetIds, namespaces }) =>
|
|
135
|
+
resolveTargetMemories(wiring, targetIds, namespaces),
|
|
136
|
+
expandNeighbors: async ({ seedIds, namespaces, limit }) =>
|
|
137
|
+
expandEntityNeighbors(wiring, seedIds, namespaces, limit),
|
|
138
|
+
classifyAndDraft: async ({ text, candidates }) =>
|
|
139
|
+
classifyAndDraft(wiring, text, candidates),
|
|
140
|
+
renderDiff: async ({ candidates, actions }) =>
|
|
141
|
+
renderCorrectionDiff(wiring, candidates, actions),
|
|
142
|
+
storageDir: async (namespace) => (await wiring.orchestrator.getStorage(namespace)).dir,
|
|
143
|
+
maxAffected: opts.maxAffected,
|
|
144
|
+
planTtlHours: opts.planTtlHours,
|
|
145
|
+
now: () => new Date(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function searchMemories(
|
|
150
|
+
wiring: CorrectionAccessWiring,
|
|
151
|
+
text: string,
|
|
152
|
+
namespaces: readonly string[],
|
|
153
|
+
limit: number,
|
|
154
|
+
): Promise<PlannerCandidate[]> {
|
|
155
|
+
// Tokenized keyword search (review thread OgIql): natural-language
|
|
156
|
+
// corrections describe the new truth, not quote the old memory verbatim
|
|
157
|
+
// ("we migrated from Postgres to MySQL" vs a memory saying "Postgres is
|
|
158
|
+
// the database"). The old 32-char exact-prefix substring missed these.
|
|
159
|
+
// We tokenize the correction into keywords, score each active memory by
|
|
160
|
+
// keyword overlap, and return memories above a minimum threshold. This is
|
|
161
|
+
// deliberately lightweight (no embedding/QMD dependency) — the planner's
|
|
162
|
+
// job is to LOCATE candidates for the LLM classify+draft, not to rank with
|
|
163
|
+
// the full recall pipeline.
|
|
164
|
+
const tokens = tokenize(text);
|
|
165
|
+
if (tokens.length === 0) return [];
|
|
166
|
+
const scored: Array<{ m: MemoryFile; ns: string; score: number }> = [];
|
|
167
|
+
for (const ns of namespaces) {
|
|
168
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
169
|
+
const all = await storage.readAllMemories();
|
|
170
|
+
for (const m of all) {
|
|
171
|
+
if (m.frontmatter.status && m.frontmatter.status !== "active") continue;
|
|
172
|
+
const hay = `${m.content} ${m.frontmatter.tags?.join(" ") ?? ""}`.toLowerCase();
|
|
173
|
+
let hits = 0;
|
|
174
|
+
for (const tok of tokens) {
|
|
175
|
+
if (hay.includes(tok)) hits++;
|
|
176
|
+
}
|
|
177
|
+
if (hits === 0) continue;
|
|
178
|
+
// Overlap ratio: how many query tokens the memory covers.
|
|
179
|
+
scored.push({ m, ns, score: hits / tokens.length });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
scored.sort((a, b) => b.score - a.score);
|
|
183
|
+
return scored.slice(0, limit).map((s, i) => toCandidate(s.m, s.ns, s.score - i * 0.01));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Tokenize correction text into lowercase search keywords (OgIql). */
|
|
187
|
+
function tokenize(text: string): string[] {
|
|
188
|
+
const STOP = new Set([
|
|
189
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
190
|
+
"to", "of", "in", "on", "at", "by", "for", "with", "from", "into",
|
|
191
|
+
"and", "or", "but", "not", "no", "yes", "this", "that", "these",
|
|
192
|
+
"those", "it", "its", "we", "you", "i", "he", "she", "they", "them",
|
|
193
|
+
"our", "your", "my", "his", "her", "their", "as", "so", "if", "then",
|
|
194
|
+
"than", "too", "very", "can", "will", "just", "should", "now", "has",
|
|
195
|
+
"have", "had", "do", "does", "did", "about", "which", "what", "who",
|
|
196
|
+
"when", "where", "why", "how", "all", "each", "every", "both", "few",
|
|
197
|
+
"more", "most", "other", "some", "such", "only", "own", "same", "up",
|
|
198
|
+
]);
|
|
199
|
+
const raw = text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
200
|
+
const out = raw.filter((w) => w.length >= 3 && !STOP.has(w));
|
|
201
|
+
// De-dup while preserving order.
|
|
202
|
+
return [...new Set(out)];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function resolveTargetMemories(
|
|
206
|
+
wiring: CorrectionAccessWiring,
|
|
207
|
+
targetIds: readonly string[],
|
|
208
|
+
namespaces: readonly string[],
|
|
209
|
+
): Promise<PlannerCandidate[]> {
|
|
210
|
+
const out: PlannerCandidate[] = [];
|
|
211
|
+
const missing: string[] = [];
|
|
212
|
+
for (const id of targetIds) {
|
|
213
|
+
let found: PlannerCandidate | null = null;
|
|
214
|
+
for (const ns of namespaces) {
|
|
215
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
216
|
+
const m = await storage.getMemoryById(id);
|
|
217
|
+
if (m) {
|
|
218
|
+
found = toCandidate(m, ns, 1);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (found) out.push(found);
|
|
223
|
+
else missing.push(id);
|
|
224
|
+
}
|
|
225
|
+
if (missing.length > 0) {
|
|
226
|
+
throw new CorrectionContractError(`target memory not found: ${missing[0]}`);
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function expandEntityNeighbors(
|
|
232
|
+
wiring: CorrectionAccessWiring,
|
|
233
|
+
seedIds: readonly string[],
|
|
234
|
+
namespaces: readonly string[],
|
|
235
|
+
limit: number,
|
|
236
|
+
): Promise<PlannerCandidate[]> {
|
|
237
|
+
// 1-hop entity-graph neighbors via entityRef-tagged siblings. We expand
|
|
238
|
+
// across the AUTHORIZED namespaces the planner supplied (review thread PG8),
|
|
239
|
+
// NOT only `config.defaultNamespace` — a correction planned in a non-default
|
|
240
|
+
// writable namespace must only ever draft siblings from that same authorized
|
|
241
|
+
// scope, or apply will fail as not-found / mutate a same-ID memory in the
|
|
242
|
+
// wrong namespace. The conservative entityRef fallback searches every
|
|
243
|
+
// readable namespace and tags each candidate with the namespace it was read
|
|
244
|
+
// from; a richer graph expansion can ride on top later (rule 57 — additive).
|
|
245
|
+
if (seedIds.length === 0 || limit <= 0 || namespaces.length === 0) return [];
|
|
246
|
+
const out: PlannerCandidate[] = [];
|
|
247
|
+
const seen = new Set<string>(seedIds);
|
|
248
|
+
// Collect the seed entityRefs across every authorized namespace (a seed
|
|
249
|
+
// memory may live in any of them), then surface active siblings sharing one.
|
|
250
|
+
const seedRefs = new Set<string>();
|
|
251
|
+
for (const ns of namespaces) {
|
|
252
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
253
|
+
const all = await storage.readAllMemories();
|
|
254
|
+
for (const m of all) {
|
|
255
|
+
if (seedIds.includes(m.frontmatter.id) && m.frontmatter.entityRef) {
|
|
256
|
+
seedRefs.add(m.frontmatter.entityRef);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (seedRefs.size === 0) return [];
|
|
261
|
+
for (const ns of namespaces) {
|
|
262
|
+
if (out.length >= limit) break;
|
|
263
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
264
|
+
const all = await storage.readAllMemories();
|
|
265
|
+
for (const m of all) {
|
|
266
|
+
if (out.length >= limit) break;
|
|
267
|
+
if (seen.has(m.frontmatter.id)) continue;
|
|
268
|
+
if (m.frontmatter.status && m.frontmatter.status !== "active") continue;
|
|
269
|
+
if (m.frontmatter.entityRef && seedRefs.has(m.frontmatter.entityRef)) {
|
|
270
|
+
out.push(toCandidate(m, ns, 0.5));
|
|
271
|
+
seen.add(m.frontmatter.id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function classifyAndDraft(
|
|
279
|
+
wiring: CorrectionAccessWiring,
|
|
280
|
+
text: string,
|
|
281
|
+
candidates: PlannerCandidate[],
|
|
282
|
+
): Promise<LlmClassificationResult> {
|
|
283
|
+
// Route through the injected LLM callback (Responses API only — gotcha 1).
|
|
284
|
+
// On any failure (or when no callback is wired), return the deterministic
|
|
285
|
+
// fallback (rule 13) — the planner never throws on an LLM outage.
|
|
286
|
+
try {
|
|
287
|
+
if (!wiring.llmComplete) {
|
|
288
|
+
return fallbackClassification(candidates, "no LLM client available");
|
|
289
|
+
}
|
|
290
|
+
const user = buildClassifyPrompt(text, candidates);
|
|
291
|
+
const raw = await wiring.llmComplete({ system: CLASSIFY_SYSTEM_PROMPT, user });
|
|
292
|
+
return parseClassifyResponse(raw, candidates);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return fallbackClassification(candidates, `LLM unavailable: ${errMsg(err)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function renderCorrectionDiff(
|
|
299
|
+
_wiring: CorrectionAccessWiring,
|
|
300
|
+
candidates: PlannerCandidate[],
|
|
301
|
+
actions: CorrectionAction[],
|
|
302
|
+
): Promise<string> {
|
|
303
|
+
// Render a human-readable unified-diff-style preview. We snapshot each
|
|
304
|
+
// affected memory via page-versioning (reuse, don't fork — rule 23) when
|
|
305
|
+
// versioning is configured; otherwise fall back to a textual summary.
|
|
306
|
+
const lines: string[] = [];
|
|
307
|
+
for (const action of actions) {
|
|
308
|
+
const id =
|
|
309
|
+
action.kind === "supersede"
|
|
310
|
+
? action.loserId
|
|
311
|
+
: action.kind === "edit" || action.kind === "retract" || action.kind === "rescope"
|
|
312
|
+
? action.memoryId
|
|
313
|
+
: null;
|
|
314
|
+
const candidate = id ? candidates.find((c) => c.memoryId === id) : null;
|
|
315
|
+
const before = candidate?.content ?? "(new)";
|
|
316
|
+
const after = describeAfterState(action);
|
|
317
|
+
lines.push(`--- ${id ?? "redaction-rule"} (${action.kind})`);
|
|
318
|
+
lines.push(`- ${before.slice(0, 120)}`);
|
|
319
|
+
lines.push(`+ ${after.slice(0, 120)}`);
|
|
320
|
+
}
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function describeAfterState(action: CorrectionAction): string {
|
|
325
|
+
switch (action.kind) {
|
|
326
|
+
case "supersede":
|
|
327
|
+
return action.replacement?.content ?? "(superseded without replacement)";
|
|
328
|
+
case "edit":
|
|
329
|
+
return action.patch;
|
|
330
|
+
case "retract":
|
|
331
|
+
return "(retracted + tombstoned)";
|
|
332
|
+
case "rescope":
|
|
333
|
+
return `(moved to namespace '${action.toNamespace}')`;
|
|
334
|
+
case "redaction_rule":
|
|
335
|
+
return `(redaction rule persisted for pattern '${action.pattern}')`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Executor deps
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
function makeExecutorDeps(
|
|
344
|
+
wiring: CorrectionAccessWiring,
|
|
345
|
+
opts: { biTemporalEnabled: boolean },
|
|
346
|
+
): ExecutorDeps {
|
|
347
|
+
return {
|
|
348
|
+
getMemory: async (namespace, memoryId) => getExecutorMemory(wiring, namespace, memoryId),
|
|
349
|
+
writeReplacement: async (namespace, draft) =>
|
|
350
|
+
writeReplacementMemory(wiring, namespace, draft),
|
|
351
|
+
applyEdit: async (namespace, memoryId, patch) =>
|
|
352
|
+
applyEditMemory(wiring, namespace, memoryId, patch),
|
|
353
|
+
retireMemory: async (namespace, memoryId, retireOpts) =>
|
|
354
|
+
retireMemoryFn(wiring, namespace, memoryId, retireOpts),
|
|
355
|
+
rescopeMemory: async (namespace, memoryId, toNamespace) =>
|
|
356
|
+
rescopeMemoryFn(wiring, namespace, memoryId, toNamespace),
|
|
357
|
+
appendTombstone: async (namespace, input) =>
|
|
358
|
+
appendTombstoneFn(wiring, namespace, input),
|
|
359
|
+
registerRedactionRule: async (namespace, pattern) =>
|
|
360
|
+
registerRedactionRuleFn(wiring, namespace, pattern),
|
|
361
|
+
appendAuditRecord: async (namespace, record) =>
|
|
362
|
+
appendAuditRecordFn(wiring, namespace, record),
|
|
363
|
+
propagate: async (namespace, touchedMemoryIds) =>
|
|
364
|
+
propagateFn(wiring, namespace, touchedMemoryIds),
|
|
365
|
+
biTemporalEnabled: opts.biTemporalEnabled,
|
|
366
|
+
now: () => new Date(),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function getExecutorMemory(
|
|
371
|
+
wiring: CorrectionAccessWiring,
|
|
372
|
+
namespace: string,
|
|
373
|
+
memoryId: string,
|
|
374
|
+
): Promise<ExecutorMemory | null> {
|
|
375
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
376
|
+
const m = await storage.getMemoryById(memoryId);
|
|
377
|
+
if (!m) return null;
|
|
378
|
+
return toExecutorMemory(m);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function writeReplacementMemory(
|
|
382
|
+
wiring: CorrectionAccessWiring,
|
|
383
|
+
namespace: string,
|
|
384
|
+
draft: {
|
|
385
|
+
content: string;
|
|
386
|
+
category?: string;
|
|
387
|
+
confidence?: number;
|
|
388
|
+
tags?: string[];
|
|
389
|
+
entityRef?: string;
|
|
390
|
+
validAt?: string;
|
|
391
|
+
observedAt?: string;
|
|
392
|
+
structuredAttributes?: Record<string, string>;
|
|
393
|
+
supersedes?: string;
|
|
394
|
+
},
|
|
395
|
+
): Promise<string> {
|
|
396
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
397
|
+
// writeMemory is the single storage chokepoint — catalog/dedup/reindex fire
|
|
398
|
+
// here (rule 43). Tombstone blocking also fires here (#1579), so a
|
|
399
|
+
// resurrected fact lands as pending_review rather than silently overwriting.
|
|
400
|
+
const id = await storage.writeMemory(
|
|
401
|
+
(draft.category ?? "fact") as Parameters<typeof storage.writeMemory>[0],
|
|
402
|
+
draft.content,
|
|
403
|
+
{
|
|
404
|
+
source: "correction",
|
|
405
|
+
confidence: draft.confidence ?? 0.9,
|
|
406
|
+
tags: draft.tags ?? [],
|
|
407
|
+
...(draft.entityRef ? { entityRef: draft.entityRef } : {}),
|
|
408
|
+
...(draft.validAt ? { validAt: draft.validAt } : {}),
|
|
409
|
+
...(draft.observedAt ? { observedAt: draft.observedAt } : {}),
|
|
410
|
+
...(draft.structuredAttributes ? { structuredAttributes: draft.structuredAttributes } : {}),
|
|
411
|
+
...(draft.supersedes ? { supersedes: draft.supersedes } : {}),
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
return id;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function applyEditMemory(
|
|
418
|
+
wiring: CorrectionAccessWiring,
|
|
419
|
+
namespace: string,
|
|
420
|
+
memoryId: string,
|
|
421
|
+
patch: string,
|
|
422
|
+
): Promise<string> {
|
|
423
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
424
|
+
const existing = await storage.getMemoryById(memoryId);
|
|
425
|
+
if (!existing) throw new CorrectionContractError(`memory not found for edit: ${memoryId}`);
|
|
426
|
+
// Apply the patch by overwriting content through the storage chokepoint.
|
|
427
|
+
// The StorageManager's writeMemoryFrontmatter snapshots the prior version
|
|
428
|
+
// internally when page-versioning is configured (issue #371), so every edit
|
|
429
|
+
// is revertable without the correction layer forking versioning logic.
|
|
430
|
+
await storage.writeMemoryFrontmatter(
|
|
431
|
+
{ ...existing, content: patch },
|
|
432
|
+
{ updated: new Date().toISOString() },
|
|
433
|
+
);
|
|
434
|
+
return memoryId;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function retireMemoryFn(
|
|
438
|
+
wiring: CorrectionAccessWiring,
|
|
439
|
+
namespace: string,
|
|
440
|
+
memoryId: string,
|
|
441
|
+
opts: { status: "superseded" | "retracted"; supersededBy?: string; validUntil?: string },
|
|
442
|
+
): Promise<void> {
|
|
443
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
444
|
+
const memory = await storage.getMemoryById(memoryId);
|
|
445
|
+
if (!memory) throw new CorrectionContractError(`memory not found for retire: ${memoryId}`);
|
|
446
|
+
// Map the correction-domain status to the storage-domain MemoryStatus.
|
|
447
|
+
// `retracted` (a correction concept) becomes `forgotten` — the soft-delete
|
|
448
|
+
// status that excludes the memory from recall/browse/attribution while
|
|
449
|
+
// keeping a page-version snapshot for reversibility (#686). `superseded`
|
|
450
|
+
// maps to itself.
|
|
451
|
+
const storageStatus: MemoryStatus = opts.status === "retracted" ? "forgotten" : "superseded";
|
|
452
|
+
// Flip status + stamp validUntil (when bi-temporal is on, #1578) +
|
|
453
|
+
// link the superseder. writeMemoryFrontmatter is the chokepoint.
|
|
454
|
+
await storage.writeMemoryFrontmatter(memory, {
|
|
455
|
+
status: storageStatus,
|
|
456
|
+
...(opts.supersededBy ? { supersededBy: opts.supersededBy } : {}),
|
|
457
|
+
...(opts.status === "superseded" ? { supersededAt: new Date().toISOString() } : {}),
|
|
458
|
+
// validUntil is the bi-temporal end; absent when the gate is off.
|
|
459
|
+
...(opts.validUntil ? { invalid_at: opts.validUntil } : {}),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function rescopeMemoryFn(
|
|
464
|
+
wiring: CorrectionAccessWiring,
|
|
465
|
+
namespace: string,
|
|
466
|
+
memoryId: string,
|
|
467
|
+
toNamespace: string,
|
|
468
|
+
): Promise<string> {
|
|
469
|
+
// The destination namespace is re-authorized by the service before the
|
|
470
|
+
// executor runs; here we perform the move atomically (write-then-unlink).
|
|
471
|
+
const sourceStorage = await wiring.orchestrator.getStorage(namespace);
|
|
472
|
+
const memory = await sourceStorage.getMemoryById(memoryId);
|
|
473
|
+
if (!memory) throw new CorrectionContractError(`memory not found for rescope: ${memoryId}`);
|
|
474
|
+
if (memory.frontmatter.status && memory.frontmatter.status !== "active") {
|
|
475
|
+
// Don't copy a stale source: rescoping a superseded/retracted/archived
|
|
476
|
+
// memory duplicates outdated content into the destination (thread Ohjwb).
|
|
477
|
+
throw new CorrectionContractError(
|
|
478
|
+
`cannot rescope memory ${memoryId}: source is ${memory.frontmatter.status}, not active`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const destStorage = await wiring.orchestrator.getStorage(toNamespace);
|
|
482
|
+
const fm = memory.frontmatter;
|
|
483
|
+
// Strip the `[Attributes: …]` suffix writeMemory appended to the source body
|
|
484
|
+
// (review thread Of0p6): we forward `structuredAttributes` separately, so
|
|
485
|
+
// writeMemory will re-append the suffix on the destination. Without this
|
|
486
|
+
// strip the destination would carry the suffix TWICE, producing duplicated
|
|
487
|
+
// attribute text and a different content hash/index entry.
|
|
488
|
+
const destContent = fm.structuredAttributes
|
|
489
|
+
? stripAttributesSuffix(memory.content)
|
|
490
|
+
: memory.content;
|
|
491
|
+
const destId = await destStorage.writeMemory(fm.category, destContent, {
|
|
492
|
+
source: `correction:rescope:${namespace}`,
|
|
493
|
+
...(typeof fm.confidence === "number" ? { confidence: fm.confidence } : {}),
|
|
494
|
+
...(Array.isArray(fm.tags) ? { tags: fm.tags } : {}),
|
|
495
|
+
...(fm.entityRef ? { entityRef: fm.entityRef } : {}),
|
|
496
|
+
...(fm.structuredAttributes ? { structuredAttributes: fm.structuredAttributes } : {}),
|
|
497
|
+
...(fm.valid_at ? { validAt: fm.valid_at } : {}),
|
|
498
|
+
...(fm.observedAt ? { observedAt: fm.observedAt } : {}),
|
|
499
|
+
...(fm.memoryKind ? { memoryKind: fm.memoryKind } : {}),
|
|
500
|
+
...(Array.isArray(fm.links) ? { links: fm.links } : {}),
|
|
501
|
+
...(fm.intentGoal ? { intentGoal: fm.intentGoal } : {}),
|
|
502
|
+
});
|
|
503
|
+
// Unlink the source by archiving (non-destructive — rule 25). If the archive
|
|
504
|
+
// fails AFTER the destination write succeeded, compensate by archiving the
|
|
505
|
+
// destination too so no duplicate ACTIVE fact remains, then re-throw so the
|
|
506
|
+
// executor records the action as failed (review: rescope-duplicates-on-fail).
|
|
507
|
+
try {
|
|
508
|
+
await sourceStorage.writeMemoryFrontmatter(memory, {
|
|
509
|
+
status: "archived",
|
|
510
|
+
archivedAt: new Date().toISOString(),
|
|
511
|
+
});
|
|
512
|
+
} catch (err) {
|
|
513
|
+
try {
|
|
514
|
+
const destMem = await destStorage.getMemoryById(destId);
|
|
515
|
+
if (destMem) {
|
|
516
|
+
await destStorage.writeMemoryFrontmatter(destMem, {
|
|
517
|
+
status: "archived",
|
|
518
|
+
archivedAt: new Date().toISOString(),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
// best-effort compensation
|
|
523
|
+
}
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
return destId;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function appendTombstoneFn(
|
|
530
|
+
wiring: CorrectionAccessWiring,
|
|
531
|
+
namespace: string,
|
|
532
|
+
input: {
|
|
533
|
+
reason: "correction" | "supersession" | "retraction";
|
|
534
|
+
sourceMemoryId: string;
|
|
535
|
+
rawContent: string;
|
|
536
|
+
entityRef?: string;
|
|
537
|
+
supersessionKey?: string;
|
|
538
|
+
},
|
|
539
|
+
): Promise<string | null> {
|
|
540
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
541
|
+
// storage.appendTombstone returns null for TWO reasons: tombstones disabled
|
|
542
|
+
// (off = pre-feature behavior) OR a swallowed store error (it catches I/O
|
|
543
|
+
// failures and returns null — review thread OgIqp). The executor writes the
|
|
544
|
+
// tombstone BEFORE retiring the source (PG9), so when tombstones are
|
|
545
|
+
// enabled a null return means persistence failed and the retire must NOT
|
|
546
|
+
// proceed (no tombstone → resurrection window). Distinguish the two cases
|
|
547
|
+
// via the public isTombstonesEnabled() accessor; when disabled, null is the
|
|
548
|
+
// expected pre-feature return and the action may still succeed.
|
|
549
|
+
const enabled =
|
|
550
|
+
typeof storage.isTombstonesEnabled === "function"
|
|
551
|
+
? storage.isTombstonesEnabled()
|
|
552
|
+
: true;
|
|
553
|
+
const result = await storage.appendTombstone({
|
|
554
|
+
reason: input.reason,
|
|
555
|
+
createdBy: "user_correction",
|
|
556
|
+
sourceMemoryId: input.sourceMemoryId,
|
|
557
|
+
rawContent: input.rawContent,
|
|
558
|
+
...(input.entityRef ? { entityRef: input.entityRef } : {}),
|
|
559
|
+
...(input.supersessionKey ? { supersessionKey: input.supersessionKey } : {}),
|
|
560
|
+
});
|
|
561
|
+
if (result === null && enabled) {
|
|
562
|
+
throw new CorrectionContractError(
|
|
563
|
+
`tombstone persistence failed for memory ${input.sourceMemoryId} (tombstones enabled but store returned null — I/O error swallowed)`,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function registerRedactionRuleFn(
|
|
570
|
+
wiring: CorrectionAccessWiring,
|
|
571
|
+
namespace: string,
|
|
572
|
+
pattern: string,
|
|
573
|
+
): Promise<void> {
|
|
574
|
+
// Persist the redaction rule under state/ so extraction consults it the
|
|
575
|
+
// same way tombstones are consulted (route through the same chokepoint).
|
|
576
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
577
|
+
const dir = path.join(storage.dir, "state", "corrections", "redaction-rules");
|
|
578
|
+
await mkdir(dir, { recursive: true });
|
|
579
|
+
// Idempotent: filename is a slug of the pattern so re-registering the same
|
|
580
|
+
// pattern overwrites rather than duplicates.
|
|
581
|
+
const slug = pattern.replace(/[^a-zA-Z0-9]+/g, "-").slice(0, 64) || "rule";
|
|
582
|
+
await writeFile(
|
|
583
|
+
path.join(dir, `${slug}.json`),
|
|
584
|
+
`${JSON.stringify({ pattern, namespace, createdAt: new Date().toISOString() })}\n`,
|
|
585
|
+
"utf-8",
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function appendAuditRecordFn(
|
|
590
|
+
wiring: CorrectionAccessWiring,
|
|
591
|
+
namespace: string,
|
|
592
|
+
record: {
|
|
593
|
+
planId: string;
|
|
594
|
+
classification: CorrectionPlan["classification"];
|
|
595
|
+
outcome: CorrectionOutcome;
|
|
596
|
+
requestText: string;
|
|
597
|
+
},
|
|
598
|
+
): Promise<string> {
|
|
599
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
600
|
+
// Corrections are themselves memories, searchable and namespaced (issue
|
|
601
|
+
// #1580 design §4). Write a correction-category memory capturing the
|
|
602
|
+
// plan + outcome as the audit trail.
|
|
603
|
+
const id = await storage.writeMemory("correction", buildAuditBody(record), {
|
|
604
|
+
source: "correction-contract",
|
|
605
|
+
confidence: 1.0,
|
|
606
|
+
tags: ["correction-audit", `plan:${record.planId}`, `classification:${record.classification}`],
|
|
607
|
+
});
|
|
608
|
+
return id;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function buildAuditBody(record: {
|
|
612
|
+
planId: string;
|
|
613
|
+
classification: CorrectionPlan["classification"];
|
|
614
|
+
outcome: CorrectionOutcome;
|
|
615
|
+
requestText: string;
|
|
616
|
+
}): string {
|
|
617
|
+
// Never-store / redaction corrections carry the very secret/pattern the user
|
|
618
|
+
// asked Remnic NOT to retain — withhold the request text from the durable
|
|
619
|
+
// audit memory so we don't persist it verbatim (#1580 review, P1).
|
|
620
|
+
const sensitive =
|
|
621
|
+
record.classification === "never_store" ||
|
|
622
|
+
record.outcome.results.some((r) => r.action.kind === "redaction_rule");
|
|
623
|
+
const safeRequest = sensitive
|
|
624
|
+
? "[redacted — never-store/redaction correction text withheld from the audit trail]"
|
|
625
|
+
: record.requestText.slice(0, 200);
|
|
626
|
+
const lines = [
|
|
627
|
+
`Correction plan ${record.planId} applied (${record.outcome.status}).`,
|
|
628
|
+
"",
|
|
629
|
+
`Request: ${safeRequest}`,
|
|
630
|
+
`Classification: ${record.classification}`,
|
|
631
|
+
`Applied at: ${record.outcome.appliedAt}`,
|
|
632
|
+
"",
|
|
633
|
+
"Actions:",
|
|
634
|
+
];
|
|
635
|
+
for (const r of record.outcome.results) {
|
|
636
|
+
// Never-store/redaction action errors can echo the secret/pattern — withhold
|
|
637
|
+
// the error text for those actions (review thread OhjwW).
|
|
638
|
+
const withhold =
|
|
639
|
+
r.action.kind === "redaction_rule" || record.classification === "never_store";
|
|
640
|
+
const errPart = r.error ? (withhold ? " (error withheld)" : ` (${r.error})`) : "";
|
|
641
|
+
lines.push(` - ${r.action.kind}: ${r.status}${errPart}`);
|
|
642
|
+
}
|
|
643
|
+
return lines.join("\n");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function propagateFn(
|
|
647
|
+
wiring: CorrectionAccessWiring,
|
|
648
|
+
namespace: string,
|
|
649
|
+
touchedMemoryIds: readonly string[],
|
|
650
|
+
): Promise<void> {
|
|
651
|
+
// Best-effort post-write propagation. The orchestrator's indexPersistedMemory
|
|
652
|
+
// fires the QMD reindex for a touched file (checklist §31). A failure here
|
|
653
|
+
// is non-fatal — the executor records it as a warning, never a failed action.
|
|
654
|
+
for (const id of touchedMemoryIds) {
|
|
655
|
+
try {
|
|
656
|
+
// indexPersistedMemory is keyed by the namespace's storage, NOT the
|
|
657
|
+
// default namespace (review thread: propagation-hardcodes-default-ns).
|
|
658
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
659
|
+
const orchestrator = wiring.orchestrator as unknown as {
|
|
660
|
+
indexPersistedMemory?(storage: unknown, memoryId: string): Promise<void>;
|
|
661
|
+
};
|
|
662
|
+
if (typeof orchestrator.indexPersistedMemory === "function") {
|
|
663
|
+
await orchestrator.indexPersistedMemory(storage, id);
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
// Swallow — propagation is best-effort.
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
// Helpers
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
function toCandidate(m: MemoryFile, namespace: string, score: number): PlannerCandidate {
|
|
677
|
+
return {
|
|
678
|
+
memoryId: m.frontmatter.id,
|
|
679
|
+
path: m.path,
|
|
680
|
+
content: m.content,
|
|
681
|
+
excerpt: m.content.slice(0, 160),
|
|
682
|
+
...(m.frontmatter.entityRef ? { entityRef: m.frontmatter.entityRef } : {}),
|
|
683
|
+
score,
|
|
684
|
+
// namespace is implicit (the planner scopes by namespace); keep it on the
|
|
685
|
+
// candidate for diff rendering if needed.
|
|
686
|
+
} satisfies PlannerCandidate & { namespace?: string };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function toExecutorMemory(m: MemoryFile): ExecutorMemory {
|
|
690
|
+
const fm = m.frontmatter;
|
|
691
|
+
// The tombstone hash must use the ORIGINAL unsuffixed body: writeMemory
|
|
692
|
+
// appends an `[Attributes: …]` suffix when structuredAttributes are set, so
|
|
693
|
+
// hashing m.content would never match the pre-suffix content hash (thread
|
|
694
|
+
// OhX2N, rule 23). Strip the suffix when attributes are present.
|
|
695
|
+
const rawBody = fm.structuredAttributes ? stripAttributesSuffix(m.content) : m.content;
|
|
696
|
+
return {
|
|
697
|
+
memoryId: fm.id,
|
|
698
|
+
content: m.content,
|
|
699
|
+
category: fm.category,
|
|
700
|
+
rawContent: rawBody,
|
|
701
|
+
...(fm.entityRef ? { entityRef: fm.entityRef } : {}),
|
|
702
|
+
} satisfies ExecutorMemory;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* The single source of truth for whether the Correction Contract feature is
|
|
707
|
+
* enabled. Reads BOTH config shapes so tool visibility and the runtime gate
|
|
708
|
+
* can never drift out of sync (review thread: correction-gate-config-mismatch):
|
|
709
|
+
* - nested: `config.correction.enabled`
|
|
710
|
+
* - flat: the `correctionEnabled` legacy key (ratchet-safe shape)
|
|
711
|
+
* Nested wins when present; both default to `true` (plan is read-only, safe on).
|
|
712
|
+
*/
|
|
713
|
+
export function isCorrectionFeatureEnabled(config: PluginConfig): boolean {
|
|
714
|
+
// parseConfig now resolves this into `config.correctionEnabled` (review
|
|
715
|
+
// thread Txp) — prefer the parsed boolean so operator config actually takes
|
|
716
|
+
// effect. The loose nested/flat read stays as a fallback for PluginConfig-
|
|
717
|
+
// shaped objects built without parseConfig (unit tests).
|
|
718
|
+
if (typeof config.correctionEnabled === "boolean") return config.correctionEnabled;
|
|
719
|
+
const nested = (config as unknown as Record<string, unknown>).correction as
|
|
720
|
+
| Record<string, unknown>
|
|
721
|
+
| undefined;
|
|
722
|
+
if (nested && typeof nested.enabled === "boolean") return nested.enabled;
|
|
723
|
+
if (nested && typeof nested.enabled === "string") return nested.enabled === "true" || nested.enabled === "1";
|
|
724
|
+
return readCorrectionFlag(config, "enabled", true);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Read a boolean correction flag from the loosely-typed config (ratchet-safe). */
|
|
728
|
+
function readCorrectionFlag(config: PluginConfig, key: string, fallback: boolean): boolean {
|
|
729
|
+
// Nested shape wins: config.correction.<key> (review thread: nested-correction-settings).
|
|
730
|
+
const nested = (config as unknown as Record<string, unknown>).correction as
|
|
731
|
+
| Record<string, unknown>
|
|
732
|
+
| undefined;
|
|
733
|
+
if (nested && typeof nested[key] === "boolean") return nested[key] as boolean;
|
|
734
|
+
if (nested && typeof nested[key] === "string") return (nested[key] as string) === "true" || (nested[key] as string) === "1";
|
|
735
|
+
// Flat legacy shape: config.correction<Key>.
|
|
736
|
+
const raw = (config as unknown as Record<string, unknown>)[`correction${capitalize(key)}`];
|
|
737
|
+
if (typeof raw === "boolean") return raw;
|
|
738
|
+
if (typeof raw === "string") return raw === "true" || raw === "1";
|
|
739
|
+
return fallback;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Read a numeric correction value from the loosely-typed config (ratchet-safe). */
|
|
743
|
+
function readCorrectionNumber(config: PluginConfig, key: string, fallback: number): number {
|
|
744
|
+
// Nested shape wins: config.correction.<key> (review thread: nested-correction-settings).
|
|
745
|
+
const nested = (config as unknown as Record<string, unknown>).correction as
|
|
746
|
+
| Record<string, unknown>
|
|
747
|
+
| undefined;
|
|
748
|
+
if (nested) {
|
|
749
|
+
const nv = nested[key];
|
|
750
|
+
if (typeof nv === "number" && Number.isFinite(nv)) return nv;
|
|
751
|
+
if (typeof nv === "string") {
|
|
752
|
+
const nn = Number(nv);
|
|
753
|
+
if (Number.isFinite(nn)) return nn;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const raw = (config as unknown as Record<string, unknown>)[`correction${capitalize(key)}`];
|
|
757
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
758
|
+
if (typeof raw === "string") {
|
|
759
|
+
const n = Number(raw);
|
|
760
|
+
if (Number.isFinite(n)) return n;
|
|
761
|
+
}
|
|
762
|
+
return fallback;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function capitalize(s: string): string {
|
|
766
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function errMsg(err: unknown): string {
|
|
770
|
+
if (err instanceof Error) return err.message;
|
|
771
|
+
return String(err);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
775
|
+
// LLM classify+draft (Responses API only — gotcha 1)
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
|
|
778
|
+
const CLASSIFY_SYSTEM_PROMPT = `You classify memory corrections and draft per-memory actions.
|
|
779
|
+
|
|
780
|
+
Given a correction statement and candidate memories, respond with a JSON object:
|
|
781
|
+
{
|
|
782
|
+
"classification": "wrong" | "outdated" | "incomplete" | "wrong_scope" | "never_store",
|
|
783
|
+
"confidence": <number 0..1>,
|
|
784
|
+
"actions": [<one or more correction actions>],
|
|
785
|
+
"relevance": [{"memoryId": "<id>", "why": "<one short sentence>"}]
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
Action shapes:
|
|
789
|
+
- {"kind":"supersede","loserId":"<id>","replacement":{"content":"<new fact>"}}
|
|
790
|
+
- {"kind":"edit","memoryId":"<id>","patch":"<new full content>"}
|
|
791
|
+
- {"kind":"retract","memoryId":"<id>"}
|
|
792
|
+
- {"kind":"rescope","memoryId":"<id>","toNamespace":"<ns>"}
|
|
793
|
+
- {"kind":"redaction_rule","pattern":"<bounded literal or regex>"}
|
|
794
|
+
|
|
795
|
+
Only emit actions you are confident in. If uncertain, return confidence < 0.5 and few actions.`;
|
|
796
|
+
|
|
797
|
+
function buildClassifyPrompt(text: string, candidates: PlannerCandidate[]): string {
|
|
798
|
+
const lines = [
|
|
799
|
+
`Correction: ${text}`,
|
|
800
|
+
"",
|
|
801
|
+
"Candidate memories:",
|
|
802
|
+
];
|
|
803
|
+
for (const c of candidates.slice(0, 20)) {
|
|
804
|
+
lines.push(`[${c.memoryId}] ${c.excerpt}`);
|
|
805
|
+
}
|
|
806
|
+
lines.push("", "Respond with the JSON object only.");
|
|
807
|
+
return lines.join("\n");
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function parseClassifyResponse(
|
|
811
|
+
raw: string,
|
|
812
|
+
candidates: PlannerCandidate[],
|
|
813
|
+
): LlmClassificationResult {
|
|
814
|
+
let parsed: unknown;
|
|
815
|
+
try {
|
|
816
|
+
parsed = JSON.parse(raw);
|
|
817
|
+
} catch {
|
|
818
|
+
return fallbackClassification(candidates, "LLM returned non-JSON response");
|
|
819
|
+
}
|
|
820
|
+
if (!parsed || typeof parsed !== "object") {
|
|
821
|
+
return fallbackClassification(candidates, "LLM returned non-object response");
|
|
822
|
+
}
|
|
823
|
+
const obj = parsed as Record<string, unknown>;
|
|
824
|
+
const classification = isClassification(obj.classification) ? obj.classification : "outdated";
|
|
825
|
+
const confidence = typeof obj.confidence === "number" ? Math.min(1, Math.max(0, obj.confidence)) : 0.5;
|
|
826
|
+
const rawActions = Array.isArray(obj.actions) ? obj.actions : [];
|
|
827
|
+
const actions: CorrectionAction[] = [];
|
|
828
|
+
const warnings: string[] = [];
|
|
829
|
+
for (const rawAction of rawActions) {
|
|
830
|
+
try {
|
|
831
|
+
validateCorrectionAction(rawAction);
|
|
832
|
+
actions.push(rawAction);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
warnings.push(`dropped malformed action: ${errMsg(err)}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const relevance = Array.isArray(obj.relevance)
|
|
838
|
+
? (obj.relevance as unknown[])
|
|
839
|
+
.filter((r): r is Record<string, unknown> => !!r && typeof r === "object")
|
|
840
|
+
.map((r) => ({
|
|
841
|
+
memoryId: typeof r.memoryId === "string" ? r.memoryId : "",
|
|
842
|
+
why: typeof r.why === "string" ? r.why : "",
|
|
843
|
+
}))
|
|
844
|
+
.filter((r) => r.memoryId.length > 0)
|
|
845
|
+
: [];
|
|
846
|
+
return {
|
|
847
|
+
classification,
|
|
848
|
+
confidence,
|
|
849
|
+
actions,
|
|
850
|
+
relevance,
|
|
851
|
+
warnings,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function isClassification(value: unknown): value is CorrectionPlan["classification"] {
|
|
856
|
+
return (
|
|
857
|
+
value === "wrong" ||
|
|
858
|
+
value === "outdated" ||
|
|
859
|
+
value === "incomplete" ||
|
|
860
|
+
value === "wrong_scope" ||
|
|
861
|
+
value === "never_store"
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function fallbackClassification(
|
|
866
|
+
candidates: PlannerCandidate[],
|
|
867
|
+
reason: string,
|
|
868
|
+
): LlmClassificationResult {
|
|
869
|
+
return {
|
|
870
|
+
classification: "outdated",
|
|
871
|
+
confidence: 0,
|
|
872
|
+
actions: [],
|
|
873
|
+
relevance: candidates.map((c) => ({ memoryId: c.memoryId, why: "located for review" })),
|
|
874
|
+
warnings: [reason],
|
|
875
|
+
fallback: true,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Re-export the local helpers for tests that import this module directly.
|
|
880
|
+
// (deterministicFallbackPlan and newPlanId live in correction-contract.ts and
|
|
881
|
+
// are re-exported by the barrel from there — not duplicated here.)
|
|
882
|
+
export {
|
|
883
|
+
buildAuditBody,
|
|
884
|
+
buildClassifyPrompt,
|
|
885
|
+
CLASSIFY_SYSTEM_PROMPT,
|
|
886
|
+
parseClassifyResponse,
|
|
887
|
+
};
|