@remnic/core 9.3.708 → 9.3.710
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,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* correction/correction-planner.ts — read-only planner (issue #1580 PR 1).
|
|
3
|
+
*
|
|
4
|
+
* The planner turns a {@link CorrectionRequest} into a {@link CorrectionPlan}:
|
|
5
|
+
*
|
|
6
|
+
* 1. LOCATE. `targetIds` present → resolve directly (not-found → explicit
|
|
7
|
+
* error, rule 34). Else: search via the injected search path scoped to
|
|
8
|
+
* the caller's readable namespaces, plus 1-hop entity-graph neighbors of
|
|
9
|
+
* top hits. Cap candidates at `maxAffected`.
|
|
10
|
+
* 2. CLASSIFY + DRAFT via one LLM call (Responses API only, gotcha 1). On
|
|
11
|
+
* LLM failure → deterministic fallback plan (rule 13): classification
|
|
12
|
+
* `outdated`, `confidence: 0`, empty actions, warning set.
|
|
13
|
+
* 3. RENDER DIFF by materializing what each action would do, using the
|
|
14
|
+
* injected diff renderer (page-versioning — reuse, don't fork).
|
|
15
|
+
* 4. PERSIST the plan atomically under
|
|
16
|
+
* `<memoryDir>/state/corrections/pending/<planId>.json` (rule 54). TTL
|
|
17
|
+
* default 24h; expired plans are rejected at apply with a clear error.
|
|
18
|
+
*
|
|
19
|
+
* The planner NEVER writes memory state — that is the executor's job. The
|
|
20
|
+
* only thing it persists is the plan document itself.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { mkdir, readFile, rename, unlink, readdir, writeFile } from "node:fs/promises";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { serializeMutations } from "../utils/serialize-mutations.js";
|
|
26
|
+
import {
|
|
27
|
+
CORRECTION_TEXT_MAX,
|
|
28
|
+
CorrectionContractError,
|
|
29
|
+
deterministicFallbackPlan,
|
|
30
|
+
newPlanId,
|
|
31
|
+
validateCorrectionAction,
|
|
32
|
+
validateCorrectionRequest,
|
|
33
|
+
validateRedactionPattern,
|
|
34
|
+
type CorrectionAction,
|
|
35
|
+
type CorrectionAffectedEntry,
|
|
36
|
+
type CorrectionClassification,
|
|
37
|
+
type CorrectionPlan,
|
|
38
|
+
type CorrectionRequest,
|
|
39
|
+
type MemoryDraft,
|
|
40
|
+
} from "./correction-contract.js";
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Injected collaborators — kept narrow so the planner is unit-testable.
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** A memory the planner located for the user. */
|
|
47
|
+
export interface PlannerCandidate {
|
|
48
|
+
memoryId: string;
|
|
49
|
+
/** Path relative to the storage dir (for diff rendering). */
|
|
50
|
+
path: string;
|
|
51
|
+
content: string;
|
|
52
|
+
excerpt: string;
|
|
53
|
+
category?: string;
|
|
54
|
+
entityRef?: string;
|
|
55
|
+
/** Provenance source quote (#1575), if available. */
|
|
56
|
+
sourceQuote?: string;
|
|
57
|
+
/** Search relevance / neighbor score, descending. */
|
|
58
|
+
score: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PlannerDeps {
|
|
62
|
+
// readableNamespaces is passed per-call to plan() so a single planner
|
|
63
|
+
// instance serves requests from callers with different read scopes.
|
|
64
|
+
/**
|
|
65
|
+
* Search memories for the correction text. The implementation is supplied
|
|
66
|
+
* by the service and reuses the existing recall/QMD search path scoped to
|
|
67
|
+
* `readableNamespaces`. Returns at most `limit` candidates.
|
|
68
|
+
*/
|
|
69
|
+
searchCorpus(request: {
|
|
70
|
+
text: string;
|
|
71
|
+
namespaces: readonly string[];
|
|
72
|
+
limit: number;
|
|
73
|
+
}): Promise<PlannerCandidate[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve explicit `targetIds` to candidates. Not-found is an explicit
|
|
76
|
+
* error (rule 34): the implementation throws with the missing id.
|
|
77
|
+
*/
|
|
78
|
+
resolveTargets(request: {
|
|
79
|
+
targetIds: readonly string[];
|
|
80
|
+
namespaces: readonly string[];
|
|
81
|
+
}): Promise<PlannerCandidate[]>;
|
|
82
|
+
/**
|
|
83
|
+
* One-hop entity-graph neighbors of the supplied candidates. Returns the
|
|
84
|
+
* union (deduped by memoryId); neighbors not already in `seedIds` only.
|
|
85
|
+
*/
|
|
86
|
+
expandNeighbors(request: {
|
|
87
|
+
seedIds: readonly string[];
|
|
88
|
+
namespaces: readonly string[];
|
|
89
|
+
limit: number;
|
|
90
|
+
}): Promise<PlannerCandidate[]>;
|
|
91
|
+
/**
|
|
92
|
+
* Single LLM call (Responses API). Classifies the correction and drafts
|
|
93
|
+
* per-memory actions. Implementations MUST return a deterministic fallback
|
|
94
|
+
* shape on LLM failure (see {@link LlmClassificationResult.fallback}) so the
|
|
95
|
+
* planner never throws on an LLM outage (rule 13).
|
|
96
|
+
*/
|
|
97
|
+
classifyAndDraft(request: {
|
|
98
|
+
text: string;
|
|
99
|
+
candidates: PlannerCandidate[];
|
|
100
|
+
}): Promise<LlmClassificationResult>;
|
|
101
|
+
/**
|
|
102
|
+
* Render a human-readable diff preview for the planned actions, using
|
|
103
|
+
* page-versioning snapshot/diff (reuse, don't fork). Pure — no side effects.
|
|
104
|
+
*/
|
|
105
|
+
renderDiff(request: {
|
|
106
|
+
candidates: PlannerCandidate[];
|
|
107
|
+
actions: CorrectionAction[];
|
|
108
|
+
}): Promise<string>;
|
|
109
|
+
/** Per-namespace storage dir root (so the plan lands in the right state/). */
|
|
110
|
+
storageDir(namespace: string): Promise<string>;
|
|
111
|
+
/** Max affected memories per plan (issue config: default 10). */
|
|
112
|
+
readonly maxAffected: number;
|
|
113
|
+
/** Plan TTL in hours (issue config: default 24). */
|
|
114
|
+
readonly planTtlHours: number;
|
|
115
|
+
/** Injected clock for deterministic tests. */
|
|
116
|
+
now(): Date;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Result of the single LLM classify+draft call. */
|
|
120
|
+
export interface LlmClassificationResult {
|
|
121
|
+
classification: CorrectionClassification;
|
|
122
|
+
confidence: number;
|
|
123
|
+
/** Per-memory drafted actions (already validated by the adapter). */
|
|
124
|
+
actions: CorrectionAction[];
|
|
125
|
+
/** Per-memory relevance notes — why each affected memory is in the plan. */
|
|
126
|
+
relevance: ReadonlyArray<{ memoryId: string; why: string }>;
|
|
127
|
+
warnings: string[];
|
|
128
|
+
/** True when the adapter fell back due to an LLM outage (rule 13). */
|
|
129
|
+
fallback?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Planner
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
export class CorrectionPlanner {
|
|
137
|
+
constructor(private readonly deps: PlannerDeps) {}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Plan a correction. `readableNamespaces` is the AUTHORIZED read scope the
|
|
141
|
+
* service resolved for this caller; the planner never trusts a caller string.
|
|
142
|
+
*/
|
|
143
|
+
async plan(
|
|
144
|
+
request: CorrectionRequest,
|
|
145
|
+
readableNamespaces: readonly string[],
|
|
146
|
+
): Promise<CorrectionPlan> {
|
|
147
|
+
const cleaned = validateCorrectionRequest(request);
|
|
148
|
+
if (cleaned.text.length > CORRECTION_TEXT_MAX) {
|
|
149
|
+
// Defensive double-check; validateCorrectionRequest already throws.
|
|
150
|
+
throw new CorrectionContractError("CorrectionRequest.text too long.");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (cleaned.targetIds && cleaned.targetIds.length > this.deps.maxAffected) {
|
|
154
|
+
throw new CorrectionContractError(
|
|
155
|
+
`Correction target list (${cleaned.targetIds.length}) exceeds maxAffected (${this.deps.maxAffected}) — narrow the target set.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const namespaces = readableNamespaces;
|
|
160
|
+
if (namespaces.length === 0) {
|
|
161
|
+
throw new CorrectionContractError(
|
|
162
|
+
"Correction requires at least one readable namespace — principal has no read scope.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 1. LOCATE
|
|
167
|
+
const located = await this.locateCandidates(cleaned, namespaces);
|
|
168
|
+
if (located.candidates.length === 0) {
|
|
169
|
+
// No candidates AND no explicit targets → empty plan is valid (the user
|
|
170
|
+
// can still discard). But explicit-target-not-found was already raised
|
|
171
|
+
// inside locateCandidates (rule 34).
|
|
172
|
+
return this.persist(
|
|
173
|
+
this.emptyPlan(cleaned, namespaces[0], "no matching memories found for the correction text"),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. CLASSIFY + DRAFT
|
|
178
|
+
const llm = await this.deps.classifyAndDraft({
|
|
179
|
+
text: cleaned.text,
|
|
180
|
+
candidates: located.candidates,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Validate every action shape before persisting — the adapter is trusted
|
|
184
|
+
// to validate, but a malformed action must never reach the executor
|
|
185
|
+
// (defense in depth, rule 51).
|
|
186
|
+
for (const action of llm.actions) {
|
|
187
|
+
validateCorrectionAction(action);
|
|
188
|
+
if (action.kind === "redaction_rule") {
|
|
189
|
+
validateRedactionPattern(action.pattern);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 3. Map actions → affected entries (only memories the planner located).
|
|
194
|
+
const affected = this.deriveAffected(located.candidates, llm);
|
|
195
|
+
|
|
196
|
+
// Bulk guard (§39): refuse past maxAffected without silent truncation.
|
|
197
|
+
const touchedCount = new Set(
|
|
198
|
+
llm.actions
|
|
199
|
+
.map((a) =>
|
|
200
|
+
a.kind === "supersede"
|
|
201
|
+
? a.loserId
|
|
202
|
+
: a.kind === "edit" || a.kind === "retract" || a.kind === "rescope"
|
|
203
|
+
? a.memoryId
|
|
204
|
+
: null,
|
|
205
|
+
)
|
|
206
|
+
.filter((id): id is string => id !== null),
|
|
207
|
+
).size;
|
|
208
|
+
if (touchedCount > this.deps.maxAffected) {
|
|
209
|
+
throw new CorrectionContractError(
|
|
210
|
+
`Correction touches ${touchedCount} memories, exceeding the maxAffected limit of ${this.deps.maxAffected}. Narrow the correction text or supply explicit targetIds.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Defense in depth (review thread: reject-actions-outside-candidates):
|
|
215
|
+
// an LLM that hallucinates or is prompt-injected must never target a
|
|
216
|
+
// memory the planner did not locate. NOW that the bulk guard has had its
|
|
217
|
+
// chance to reject over-limit plans, drop actions whose target ID is
|
|
218
|
+
// absent from the candidate set and warn.
|
|
219
|
+
const candidateIds = new Set(located.candidates.map((c) => c.memoryId));
|
|
220
|
+
if (llm.actions.length > 0 && candidateIds.size > 0) {
|
|
221
|
+
const filtered = llm.actions.filter((action) => {
|
|
222
|
+
const id =
|
|
223
|
+
action.kind === "supersede"
|
|
224
|
+
? action.loserId
|
|
225
|
+
: action.kind === "edit" || action.kind === "retract" || action.kind === "rescope"
|
|
226
|
+
? action.memoryId
|
|
227
|
+
: null;
|
|
228
|
+
return id === null || candidateIds.has(id);
|
|
229
|
+
});
|
|
230
|
+
if (filtered.length < llm.actions.length) {
|
|
231
|
+
const dropped = llm.actions.length - filtered.length;
|
|
232
|
+
llm.warnings = [...llm.warnings, `${dropped} action(s) targeted memories outside the located candidate set and were dropped (prompt-injection guard).`];
|
|
233
|
+
llm.actions = filtered;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 4. RENDER DIFF
|
|
238
|
+
const diff = llm.fallback
|
|
239
|
+
? ""
|
|
240
|
+
: await this.deps.renderDiff({ candidates: located.candidates, actions: llm.actions });
|
|
241
|
+
|
|
242
|
+
const createdAt = this.deps.now().toISOString();
|
|
243
|
+
const expiresAt = new Date(
|
|
244
|
+
this.deps.now().getTime() + this.deps.planTtlHours * 60 * 60 * 1000,
|
|
245
|
+
).toISOString();
|
|
246
|
+
|
|
247
|
+
const plan: CorrectionPlan = llm.fallback
|
|
248
|
+
? deterministicFallbackPlan({
|
|
249
|
+
request: cleaned,
|
|
250
|
+
namespace: namespaces[0],
|
|
251
|
+
affected,
|
|
252
|
+
warnings: llm.warnings,
|
|
253
|
+
createdAt,
|
|
254
|
+
expiresAt,
|
|
255
|
+
})
|
|
256
|
+
: {
|
|
257
|
+
planId: newPlanId(),
|
|
258
|
+
request: cleaned,
|
|
259
|
+
namespace: namespaces[0],
|
|
260
|
+
affected,
|
|
261
|
+
classification: llm.classification,
|
|
262
|
+
actions: llm.actions,
|
|
263
|
+
diff,
|
|
264
|
+
confidence: clampConfidence(llm.confidence),
|
|
265
|
+
warnings: llm.warnings,
|
|
266
|
+
createdAt,
|
|
267
|
+
expiresAt,
|
|
268
|
+
status: "pending",
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return this.persist(plan);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async locateCandidates(
|
|
275
|
+
request: CorrectionRequest,
|
|
276
|
+
namespaces: readonly string[],
|
|
277
|
+
): Promise<{ candidates: PlannerCandidate[] }> {
|
|
278
|
+
if (request.targetIds && request.targetIds.length > 0) {
|
|
279
|
+
const targets = await this.deps.resolveTargets({
|
|
280
|
+
targetIds: request.targetIds,
|
|
281
|
+
namespaces,
|
|
282
|
+
});
|
|
283
|
+
// Expand 1-hop neighbors and merge (dedup by memoryId, keep highest score).
|
|
284
|
+
const seedIds = targets.map((c) => c.memoryId);
|
|
285
|
+
const neighbors = await this.deps.expandNeighbors({
|
|
286
|
+
seedIds,
|
|
287
|
+
namespaces,
|
|
288
|
+
limit: this.deps.maxAffected,
|
|
289
|
+
});
|
|
290
|
+
return { candidates: mergeCandidates(targets, neighbors).slice(0, this.deps.maxAffected) };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const searched = await this.deps.searchCorpus({
|
|
294
|
+
text: request.text,
|
|
295
|
+
namespaces,
|
|
296
|
+
limit: this.deps.maxAffected,
|
|
297
|
+
});
|
|
298
|
+
if (searched.length === 0) return { candidates: [] };
|
|
299
|
+
const seedIds = searched.slice(0, Math.min(5, searched.length)).map((c) => c.memoryId);
|
|
300
|
+
const neighbors = await this.deps.expandNeighbors({
|
|
301
|
+
seedIds,
|
|
302
|
+
namespaces,
|
|
303
|
+
limit: this.deps.maxAffected,
|
|
304
|
+
});
|
|
305
|
+
return { candidates: mergeCandidates(searched, neighbors).slice(0, this.deps.maxAffected) };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private deriveAffected(
|
|
309
|
+
candidates: PlannerCandidate[],
|
|
310
|
+
llm: LlmClassificationResult,
|
|
311
|
+
): CorrectionAffectedEntry[] {
|
|
312
|
+
const byId = new Map(candidates.map((c) => [c.memoryId, c]));
|
|
313
|
+
const out: CorrectionAffectedEntry[] = [];
|
|
314
|
+
for (const rel of llm.relevance) {
|
|
315
|
+
const c = byId.get(rel.memoryId);
|
|
316
|
+
if (!c) continue;
|
|
317
|
+
out.push({
|
|
318
|
+
memoryId: c.memoryId,
|
|
319
|
+
path: c.path,
|
|
320
|
+
excerpt: c.excerpt,
|
|
321
|
+
why: rel.why,
|
|
322
|
+
...(c.sourceQuote ? { sourceQuote: c.sourceQuote } : {}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// Include touched memories that the LLM did not annotate but whose actions
|
|
326
|
+
// reference them (e.g. an `edit` drafted against a located candidate).
|
|
327
|
+
const annotated = new Set(out.map((e) => e.memoryId));
|
|
328
|
+
for (const action of llm.actions) {
|
|
329
|
+
const id =
|
|
330
|
+
action.kind === "supersede"
|
|
331
|
+
? action.loserId
|
|
332
|
+
: action.kind === "edit" || action.kind === "retract" || action.kind === "rescope"
|
|
333
|
+
? action.memoryId
|
|
334
|
+
: null;
|
|
335
|
+
if (id && !annotated.has(id)) {
|
|
336
|
+
const c = byId.get(id);
|
|
337
|
+
if (c) {
|
|
338
|
+
out.push({
|
|
339
|
+
memoryId: c.memoryId,
|
|
340
|
+
path: c.path,
|
|
341
|
+
excerpt: c.excerpt,
|
|
342
|
+
why: "touched by a drafted action",
|
|
343
|
+
...(c.sourceQuote ? { sourceQuote: c.sourceQuote } : {}),
|
|
344
|
+
});
|
|
345
|
+
annotated.add(id);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private emptyPlan(request: CorrectionRequest, namespace: string, warning: string): CorrectionPlan {
|
|
353
|
+
const createdAt = this.deps.now().toISOString();
|
|
354
|
+
const expiresAt = new Date(
|
|
355
|
+
this.deps.now().getTime() + this.deps.planTtlHours * 60 * 60 * 1000,
|
|
356
|
+
).toISOString();
|
|
357
|
+
return {
|
|
358
|
+
planId: newPlanId(),
|
|
359
|
+
request,
|
|
360
|
+
namespace,
|
|
361
|
+
affected: [],
|
|
362
|
+
classification: "outdated",
|
|
363
|
+
actions: [],
|
|
364
|
+
diff: "",
|
|
365
|
+
confidence: 0,
|
|
366
|
+
warnings: [warning],
|
|
367
|
+
createdAt,
|
|
368
|
+
expiresAt,
|
|
369
|
+
status: "pending",
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// -------------------------------------------------------------------------
|
|
374
|
+
// Plan persistence — atomic write (rule 54), serialized per plan id (rule 40).
|
|
375
|
+
// -------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
/** Directory holding pending plans for one namespace. */
|
|
378
|
+
private async pendingDir(namespace: string): Promise<string> {
|
|
379
|
+
return path.join(await this.deps.storageDir(namespace), "state", "corrections", "pending");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async persist(plan: CorrectionPlan): Promise<CorrectionPlan> {
|
|
383
|
+
const dir = await this.pendingDir(plan.namespace);
|
|
384
|
+
const target = path.join(dir, `${plan.planId}.json`);
|
|
385
|
+
await serializeMutations(`correction-plan:${target}`, async () => {
|
|
386
|
+
await mkdir(dir, { recursive: true });
|
|
387
|
+
const tmp = `${target}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
388
|
+
await writeFile(tmp, `${JSON.stringify(plan)}\n`, "utf-8");
|
|
389
|
+
// rename() is atomic on POSIX for same-filesystem renames (rule 54).
|
|
390
|
+
await rename(tmp, target);
|
|
391
|
+
});
|
|
392
|
+
return plan;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Load a pending plan by id (used by the service / executor). */
|
|
396
|
+
async loadPlan(namespace: string, planId: string): Promise<CorrectionPlan | null> {
|
|
397
|
+
assertSafePlanId(planId);
|
|
398
|
+
const file = path.join(await this.pendingDir(namespace), `${planId}.json`);
|
|
399
|
+
return serializeMutations(`correction-plan:${file}`, async () => {
|
|
400
|
+
let raw: string;
|
|
401
|
+
try {
|
|
402
|
+
raw = await readFile(file, "utf-8");
|
|
403
|
+
} catch (err) {
|
|
404
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
405
|
+
if (code === "ENOENT") return null;
|
|
406
|
+
throw err;
|
|
407
|
+
}
|
|
408
|
+
return parsePlan(raw);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** List pending plans (newest first), excluding consumed (applied/discarded). */
|
|
413
|
+
async listPending(namespace: string): Promise<CorrectionPlan[]> {
|
|
414
|
+
const dir = await this.pendingDir(namespace);
|
|
415
|
+
let files: string[];
|
|
416
|
+
try {
|
|
417
|
+
files = await readdir(dir);
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
420
|
+
if (code === "ENOENT") return [];
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
const plans: CorrectionPlan[] = [];
|
|
424
|
+
for (const f of files) {
|
|
425
|
+
if (!f.endsWith(".json")) continue;
|
|
426
|
+
let plan: CorrectionPlan | null = null;
|
|
427
|
+
try {
|
|
428
|
+
plan = await this.loadPlan(namespace, f.replace(/\.json$/, ""));
|
|
429
|
+
} catch {
|
|
430
|
+
continue; // rule 34 — skip malformed plans with a counter (best-effort).
|
|
431
|
+
}
|
|
432
|
+
if (plan && plan.status === "pending") plans.push(plan);
|
|
433
|
+
}
|
|
434
|
+
plans.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
|
|
435
|
+
return plans;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async markConsumed(namespace: string, planId: string, status: "applying" | "applied" | "discarded" | "partial"): Promise<void> {
|
|
439
|
+
assertSafePlanId(planId);
|
|
440
|
+
const file = path.join(await this.pendingDir(namespace), `${planId}.json`);
|
|
441
|
+
await serializeMutations(`correction-plan:${file}`, async () => {
|
|
442
|
+
let raw: string;
|
|
443
|
+
try {
|
|
444
|
+
raw = await readFile(file, "utf-8");
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
447
|
+
if (code === "ENOENT") return; // idempotent
|
|
448
|
+
throw err;
|
|
449
|
+
}
|
|
450
|
+
const plan = parsePlan(raw);
|
|
451
|
+
if (!plan) return;
|
|
452
|
+
plan.status = status;
|
|
453
|
+
const tmp = `${file}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
454
|
+
await writeFile(tmp, `${JSON.stringify(plan)}\n`, "utf-8");
|
|
455
|
+
await rename(tmp, file);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async deletePlan(namespace: string, planId: string): Promise<void> {
|
|
460
|
+
assertSafePlanId(planId);
|
|
461
|
+
const file = path.join(await this.pendingDir(namespace), `${planId}.json`);
|
|
462
|
+
await serializeMutations(`correction-plan:${file}`, async () => {
|
|
463
|
+
try {
|
|
464
|
+
await unlink(file);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
467
|
+
if (code === "ENOENT") return;
|
|
468
|
+
throw err;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// Helpers
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Reject caller-supplied plan ids that would escape the pending-plan dir
|
|
480
|
+
* (review thread Ug8): `correct --discard --plan-id ../../meta` must not let
|
|
481
|
+
* `path.join(pendingDir, "../../meta.json")` unlink arbitrary `.json` files
|
|
482
|
+
* under the storage root. A safe plan id is a bare basename with no path
|
|
483
|
+
* separators and no parent-segment shape. The canonical format is
|
|
484
|
+
* `corr-<base36>-<base36>` (see {@link newPlanId}); this guard accepts any
|
|
485
|
+
* single-segment id so future formats need not touch it, but blocks every
|
|
486
|
+
* traversal vector (`/`, `\`, `..`, leading dots, NUL).
|
|
487
|
+
*/
|
|
488
|
+
function assertSafePlanId(planId: string): void {
|
|
489
|
+
if (
|
|
490
|
+
typeof planId !== "string" ||
|
|
491
|
+
planId.length === 0 ||
|
|
492
|
+
planId.includes("/") ||
|
|
493
|
+
planId.includes("\\") ||
|
|
494
|
+
planId.includes("\0") ||
|
|
495
|
+
planId === "." ||
|
|
496
|
+
planId === ".." ||
|
|
497
|
+
planId.startsWith(".") ||
|
|
498
|
+
planId.includes("..")
|
|
499
|
+
) {
|
|
500
|
+
throw new CorrectionContractError(
|
|
501
|
+
`Invalid plan id: ${JSON.stringify(planId)} — must be a bare file basename with no path separators or parent segments.`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function clampConfidence(n: number): number {
|
|
506
|
+
if (!Number.isFinite(n)) return 0;
|
|
507
|
+
return Math.min(1, Math.max(0, n));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Merge two candidate lists, deduping by memoryId and keeping the highest score. */
|
|
511
|
+
function mergeCandidates(a: readonly PlannerCandidate[], b: readonly PlannerCandidate[]): PlannerCandidate[] {
|
|
512
|
+
const map = new Map<string, PlannerCandidate>();
|
|
513
|
+
for (const c of a) {
|
|
514
|
+
const existing = map.get(c.memoryId);
|
|
515
|
+
if (!existing || c.score > existing.score) map.set(c.memoryId, c);
|
|
516
|
+
}
|
|
517
|
+
for (const c of b) {
|
|
518
|
+
const existing = map.get(c.memoryId);
|
|
519
|
+
if (!existing || c.score > existing.score) map.set(c.memoryId, c);
|
|
520
|
+
}
|
|
521
|
+
return [...map.values()].sort((x, y) => y.score - x.score);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Parse + structurally validate a plan document. Returns null on malformed. */
|
|
525
|
+
export function parsePlan(raw: string): CorrectionPlan | null {
|
|
526
|
+
let parsed: unknown;
|
|
527
|
+
try {
|
|
528
|
+
parsed = JSON.parse(raw);
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
533
|
+
const p = parsed as Record<string, unknown>;
|
|
534
|
+
if (typeof p.planId !== "string" || typeof p.namespace !== "string") return null;
|
|
535
|
+
if (typeof p.createdAt !== "string" || typeof p.expiresAt !== "string") return null;
|
|
536
|
+
if (!Array.isArray(p.actions) || !Array.isArray(p.affected)) return null;
|
|
537
|
+
// Re-validate each action shape; a single malformed action poisons the plan.
|
|
538
|
+
for (const action of p.actions as unknown[]) {
|
|
539
|
+
try {
|
|
540
|
+
validateCorrectionAction(action);
|
|
541
|
+
} catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return parsed as CorrectionPlan;
|
|
546
|
+
}
|