@remnic/core 9.3.744 → 9.3.745
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 +17 -15
- package/dist/access-boundary.js +14 -15
- package/dist/access-cli.js +92 -33
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +17 -15
- package/dist/access-http.js +20 -21
- package/dist/access-mcp.d.ts +17 -15
- package/dist/access-mcp.js +18 -19
- package/dist/access-operations-batch.js +16 -17
- package/dist/access-operations.d.ts +17 -15
- package/dist/access-operations.js +17 -18
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-OT3Gk79W.d.ts → access-service-CdY88L86.d.ts} +3 -30
- package/dist/access-service.d.ts +7 -5
- package/dist/access-service.js +13 -14
- package/dist/access-surface-catalog.d.ts +17 -15
- 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/{auto-sync-XHSKKQAH.js → auto-sync-IEBKPGAM.js} +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +6 -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-CX0CjOaf.d.ts → catalog-D_ZDxu8Y.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/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-WPLRJ4XM.js → chunk-24YDB5QP.js} +2098 -230
- package/dist/chunk-24YDB5QP.js.map +1 -0
- package/dist/{chunk-6XG3NQRC.js → chunk-3IJGF6K2.js} +2 -2
- package/dist/{chunk-U5WVRMCA.js → chunk-5IN26V6W.js} +2 -2
- package/dist/{chunk-XEADGIAA.js → chunk-DIHCFJVV.js} +7 -7
- package/dist/{chunk-C7LDPZJJ.js → chunk-EHYB325U.js} +227 -227
- package/dist/chunk-EHYB325U.js.map +1 -0
- package/dist/{chunk-E7RGJQWP.js → chunk-HIHCB2S6.js} +2 -2
- package/dist/{chunk-JFCS6PQU.js → chunk-JHQUIH3W.js} +5 -5
- package/dist/{chunk-MXDG6SRH.js → chunk-JRDPT6DA.js} +2 -2
- package/dist/{chunk-BYLC4DKI.js → chunk-KMNYXLPL.js} +2 -2
- package/dist/{chunk-TQPSOTC7.js → chunk-L4N7SVNN.js} +8 -10
- package/dist/{chunk-TQPSOTC7.js.map → chunk-L4N7SVNN.js.map} +1 -1
- package/dist/{chunk-DYK27X7V.js → chunk-LMFS64GY.js} +3 -3
- package/dist/{chunk-FWXLKH2H.js → chunk-NVYF5OB6.js} +3 -3
- package/dist/{chunk-G5FP7VSQ.js → chunk-P4FW3ZU5.js} +58 -60
- package/dist/{chunk-G5FP7VSQ.js.map → chunk-P4FW3ZU5.js.map} +1 -1
- package/dist/{chunk-56F6EAQ6.js → chunk-Q3X7GUAQ.js} +2 -2
- package/dist/{chunk-7N7S2RKF.js → chunk-R2OAN6IP.js} +2 -2
- package/dist/{chunk-CHELXDTO.js → chunk-RB6A7OUN.js} +2 -2
- package/dist/{chunk-3EA5LIFJ.js → chunk-SIV3JZPP.js} +2 -2
- package/dist/{chunk-IP5SANO2.js → chunk-UIKZZ242.js} +2 -2
- package/dist/{chunk-UGFPGWJB.js → chunk-URQLO7BX.js} +2 -2
- package/dist/{chunk-5UNSGRCR.js → chunk-WK3NK6SP.js} +162 -16
- package/dist/chunk-WK3NK6SP.js.map +1 -0
- package/dist/{chunk-VE54E6RP.js → chunk-WWVXXSLF.js} +4 -6
- package/dist/{chunk-VE54E6RP.js.map → chunk-WWVXXSLF.js.map} +1 -1
- package/dist/{chunk-WO22UEM3.js → chunk-XTEWVF3C.js} +2 -2
- package/dist/{chunk-SODQQCAZ.js → chunk-YQX63P3W.js} +18011 -12597
- package/dist/chunk-YQX63P3W.js.map +1 -0
- package/dist/{chunk-FCQ7ZYCQ.js → chunk-ZGAJB74V.js} +2 -2
- package/dist/{chunk-NBP3YHHS.js → chunk-ZGZNLRKF.js} +2 -2
- package/dist/{chunk-C7C5WIIF.js → chunk-ZPZ4VVLT.js} +2 -2
- package/dist/{cli-BQiTZ7Zs.d.ts → cli-BNJtn0R0.d.ts} +3 -3
- package/dist/cli.d.ts +9 -7
- package/dist/cli.js +44 -46
- 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/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/contradiction/index.js +4 -4
- 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 +6 -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/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.js +271 -5362
- package/dist/index.js.map +1 -1
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/mcp-memory-inspector-app.d.ts +16 -14
- 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 +10 -11
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +5 -5
- package/dist/{orchestrator-BPQGHpJz.d.ts → orchestrator-BBGXLd20.d.ts} +169 -7
- package/dist/orchestrator.d.ts +6 -4
- package/dist/orchestrator.js +119 -16
- 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-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-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- 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/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-ByzGwC9c.d.ts → semantic-consolidation-D3aSiu9t.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 +1 -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/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +1 -1
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-5Jik3r4G.d.ts → types-CZm6rx9-.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +3 -3
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/orchestration/extraction-persist.ts +2785 -0
- package/src/orchestration/extraction-run.ts +874 -0
- package/src/orchestrator.ts +167 -3277
- package/dist/chunk-5UNSGRCR.js.map +0 -1
- package/dist/chunk-A2KKLCOS.js +0 -1889
- package/dist/chunk-A2KKLCOS.js.map +0 -1
- package/dist/chunk-AVBT5WEW.js +0 -153
- package/dist/chunk-AVBT5WEW.js.map +0 -1
- package/dist/chunk-C7LDPZJJ.js.map +0 -1
- package/dist/chunk-SODQQCAZ.js.map +0 -1
- package/dist/chunk-WPLRJ4XM.js.map +0 -1
- /package/dist/{auto-sync-XHSKKQAH.js.map → auto-sync-IEBKPGAM.js.map} +0 -0
- /package/dist/{chunk-6XG3NQRC.js.map → chunk-3IJGF6K2.js.map} +0 -0
- /package/dist/{chunk-U5WVRMCA.js.map → chunk-5IN26V6W.js.map} +0 -0
- /package/dist/{chunk-XEADGIAA.js.map → chunk-DIHCFJVV.js.map} +0 -0
- /package/dist/{chunk-E7RGJQWP.js.map → chunk-HIHCB2S6.js.map} +0 -0
- /package/dist/{chunk-JFCS6PQU.js.map → chunk-JHQUIH3W.js.map} +0 -0
- /package/dist/{chunk-MXDG6SRH.js.map → chunk-JRDPT6DA.js.map} +0 -0
- /package/dist/{chunk-BYLC4DKI.js.map → chunk-KMNYXLPL.js.map} +0 -0
- /package/dist/{chunk-DYK27X7V.js.map → chunk-LMFS64GY.js.map} +0 -0
- /package/dist/{chunk-FWXLKH2H.js.map → chunk-NVYF5OB6.js.map} +0 -0
- /package/dist/{chunk-56F6EAQ6.js.map → chunk-Q3X7GUAQ.js.map} +0 -0
- /package/dist/{chunk-7N7S2RKF.js.map → chunk-R2OAN6IP.js.map} +0 -0
- /package/dist/{chunk-CHELXDTO.js.map → chunk-RB6A7OUN.js.map} +0 -0
- /package/dist/{chunk-3EA5LIFJ.js.map → chunk-SIV3JZPP.js.map} +0 -0
- /package/dist/{chunk-IP5SANO2.js.map → chunk-UIKZZ242.js.map} +0 -0
- /package/dist/{chunk-UGFPGWJB.js.map → chunk-URQLO7BX.js.map} +0 -0
- /package/dist/{chunk-WO22UEM3.js.map → chunk-XTEWVF3C.js.map} +0 -0
- /package/dist/{chunk-FCQ7ZYCQ.js.map → chunk-ZGAJB74V.js.map} +0 -0
- /package/dist/{chunk-NBP3YHHS.js.map → chunk-ZGZNLRKF.js.map} +0 -0
- /package/dist/{chunk-C7C5WIIF.js.map → chunk-ZPZ4VVLT.js.map} +0 -0
|
@@ -9,9 +9,21 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
importCapsule
|
|
11
11
|
} from "./chunk-ARV3AUOM.js";
|
|
12
|
+
import {
|
|
13
|
+
getTrustZoneStoreStatus,
|
|
14
|
+
isTrustZoneName,
|
|
15
|
+
listTrustZoneRecords,
|
|
16
|
+
promoteTrustZoneRecord,
|
|
17
|
+
scoreTrustZoneProvenance,
|
|
18
|
+
seedTrustZoneDemoDataset,
|
|
19
|
+
summarizeTrustZonePromotionReadiness
|
|
20
|
+
} from "./chunk-JGSKJHF7.js";
|
|
12
21
|
import {
|
|
13
22
|
planSessionTranscriptMigration
|
|
14
23
|
} from "./chunk-IQ7WCZRW.js";
|
|
24
|
+
import {
|
|
25
|
+
isPathInsideStorageRoot
|
|
26
|
+
} from "./chunk-FF4KLI5W.js";
|
|
15
27
|
import {
|
|
16
28
|
applyTagFilter,
|
|
17
29
|
normalizeTags,
|
|
@@ -23,37 +35,12 @@ import {
|
|
|
23
35
|
import {
|
|
24
36
|
toRecallExplainJson
|
|
25
37
|
} from "./chunk-GKRLPHPU.js";
|
|
26
|
-
import {
|
|
27
|
-
recordObjectiveStateSnapshotsFromObservedMessages
|
|
28
|
-
} from "./chunk-ICP53SKG.js";
|
|
29
|
-
import {
|
|
30
|
-
buildProposedActions,
|
|
31
|
-
buildQualityScore,
|
|
32
|
-
groupActionsByStatus,
|
|
33
|
-
listMemoryGovernanceRuns,
|
|
34
|
-
readMemoryGovernanceRunArtifact,
|
|
35
|
-
runMemoryGovernance
|
|
36
|
-
} from "./chunk-7N7S2RKF.js";
|
|
37
|
-
import {
|
|
38
|
-
clusterByKey,
|
|
39
|
-
createCorrectionService,
|
|
40
|
-
isCorrectionFeatureEnabled
|
|
41
|
-
} from "./chunk-A2KKLCOS.js";
|
|
42
|
-
import {
|
|
43
|
-
getTrustZoneStoreStatus,
|
|
44
|
-
isTrustZoneName,
|
|
45
|
-
listTrustZoneRecords,
|
|
46
|
-
promoteTrustZoneRecord,
|
|
47
|
-
scoreTrustZoneProvenance,
|
|
48
|
-
seedTrustZoneDemoDataset,
|
|
49
|
-
summarizeTrustZonePromotionReadiness
|
|
50
|
-
} from "./chunk-JGSKJHF7.js";
|
|
51
|
-
import {
|
|
52
|
-
isPathInsideStorageRoot
|
|
53
|
-
} from "./chunk-FF4KLI5W.js";
|
|
54
38
|
import {
|
|
55
39
|
estimateRecallTokens
|
|
56
40
|
} from "./chunk-DRUDC2UX.js";
|
|
41
|
+
import {
|
|
42
|
+
recordObjectiveStateSnapshotsFromObservedMessages
|
|
43
|
+
} from "./chunk-ICP53SKG.js";
|
|
57
44
|
import {
|
|
58
45
|
objectiveStateStoreOverrideForNamespace
|
|
59
46
|
} from "./chunk-6HMYUWXR.js";
|
|
@@ -76,6 +63,14 @@ import {
|
|
|
76
63
|
import {
|
|
77
64
|
namespaceIdentityFromToken
|
|
78
65
|
} from "./chunk-ZFXCQPNO.js";
|
|
66
|
+
import {
|
|
67
|
+
buildProposedActions,
|
|
68
|
+
buildQualityScore,
|
|
69
|
+
groupActionsByStatus,
|
|
70
|
+
listMemoryGovernanceRuns,
|
|
71
|
+
readMemoryGovernanceRunArtifact,
|
|
72
|
+
runMemoryGovernance
|
|
73
|
+
} from "./chunk-R2OAN6IP.js";
|
|
79
74
|
import {
|
|
80
75
|
recordMemoryOutcome
|
|
81
76
|
} from "./chunk-EIR5VLIH.js";
|
|
@@ -110,15 +105,22 @@ import {
|
|
|
110
105
|
import {
|
|
111
106
|
FileCalendarSource,
|
|
112
107
|
buildBriefing,
|
|
108
|
+
enqueuePassiveCorrectionNotification,
|
|
113
109
|
parseBriefingFocus,
|
|
114
110
|
parseBriefingWindow
|
|
115
|
-
} from "./chunk-
|
|
111
|
+
} from "./chunk-3IJGF6K2.js";
|
|
116
112
|
import {
|
|
117
113
|
parseEntityFile
|
|
118
|
-
} from "./chunk-
|
|
114
|
+
} from "./chunk-JHQUIH3W.js";
|
|
119
115
|
import {
|
|
120
116
|
stripAttributesSuffix
|
|
121
117
|
} from "./chunk-SVOZFLIQ.js";
|
|
118
|
+
import {
|
|
119
|
+
serializeMutations
|
|
120
|
+
} from "./chunk-LTJAMRGI.js";
|
|
121
|
+
import {
|
|
122
|
+
supersessionKeysForFact
|
|
123
|
+
} from "./chunk-TKZ3AL5V.js";
|
|
122
124
|
import {
|
|
123
125
|
DEFAULT_RECALL_DISCLOSURE,
|
|
124
126
|
isRecallDisclosure
|
|
@@ -140,6 +142,9 @@ import {
|
|
|
140
142
|
import {
|
|
141
143
|
createVersion
|
|
142
144
|
} from "./chunk-VF4XKTX3.js";
|
|
145
|
+
import {
|
|
146
|
+
computeContentHash
|
|
147
|
+
} from "./chunk-W5H6ZRPT.js";
|
|
143
148
|
import {
|
|
144
149
|
exportCapsule
|
|
145
150
|
} from "./chunk-KQAFEZQX.js";
|
|
@@ -181,6 +186,9 @@ import {
|
|
|
181
186
|
resolveRecallEnhancementCapabilities,
|
|
182
187
|
resolveSecurityCapabilities
|
|
183
188
|
} from "./chunk-VSF7AAGE.js";
|
|
189
|
+
import {
|
|
190
|
+
sanitizeMemoryContent
|
|
191
|
+
} from "./chunk-5GKPFL3S.js";
|
|
184
192
|
import {
|
|
185
193
|
expandTildePath
|
|
186
194
|
} from "./chunk-EYIEWJNI.js";
|
|
@@ -203,7 +211,7 @@ import { stat as stat2 } from "fs/promises";
|
|
|
203
211
|
import * as nodeFs from "fs/promises";
|
|
204
212
|
import { constants as fsConstants } from "fs";
|
|
205
213
|
import { readdirSync, unlinkSync } from "fs";
|
|
206
|
-
import { createHash as
|
|
214
|
+
import { createHash as createHash4 } from "crypto";
|
|
207
215
|
import { ZodError } from "zod";
|
|
208
216
|
|
|
209
217
|
// src/coding/decision-records.ts
|
|
@@ -1180,12 +1188,12 @@ async function findArchitectureCardMemory(storage) {
|
|
|
1180
1188
|
});
|
|
1181
1189
|
return cards[0] ?? null;
|
|
1182
1190
|
}
|
|
1183
|
-
function createArchitectureVersioningHook(enabled, maxVersionsPerPage, sidecarDir, memoryDir,
|
|
1191
|
+
function createArchitectureVersioningHook(enabled, maxVersionsPerPage, sidecarDir, memoryDir, readFile5, writeVersion) {
|
|
1184
1192
|
return {
|
|
1185
1193
|
async snapshotIfExists(memory) {
|
|
1186
1194
|
if (!enabled) return;
|
|
1187
1195
|
try {
|
|
1188
|
-
const fullContent = await
|
|
1196
|
+
const fullContent = await readFile5(memory.path);
|
|
1189
1197
|
await writeVersion(
|
|
1190
1198
|
memory.path,
|
|
1191
1199
|
fullContent,
|
|
@@ -1958,219 +1966,2074 @@ function computeSessionDelta(lastSeen, current) {
|
|
|
1958
1966
|
nextState
|
|
1959
1967
|
};
|
|
1960
1968
|
}
|
|
1961
|
-
function capCommits(commits, max) {
|
|
1962
|
-
if (!Number.isFinite(max) || max <= 0) return [];
|
|
1963
|
-
if (commits.length <= max) return commits;
|
|
1964
|
-
return commits.slice(-max);
|
|
1969
|
+
function capCommits(commits, max) {
|
|
1970
|
+
if (!Number.isFinite(max) || max <= 0) return [];
|
|
1971
|
+
if (commits.length <= max) return commits;
|
|
1972
|
+
return commits.slice(-max);
|
|
1973
|
+
}
|
|
1974
|
+
function capFiles(files, max) {
|
|
1975
|
+
if (!Number.isFinite(max) || max <= 0) return [];
|
|
1976
|
+
if (files.length <= max) return files;
|
|
1977
|
+
return files.slice(0, max);
|
|
1978
|
+
}
|
|
1979
|
+
function buildSummaryLine(commitCount, fileCount, sinceIso) {
|
|
1980
|
+
const since = formatSinceDate(sinceIso);
|
|
1981
|
+
const commitWord = commitCount === 1 ? "commit" : "commits";
|
|
1982
|
+
const fileWord = fileCount === 1 ? "file" : "files";
|
|
1983
|
+
return `Since ${since}: ${commitCount} ${commitWord}, ${fileCount} ${fileWord} touched.`;
|
|
1984
|
+
}
|
|
1985
|
+
function formatSinceDate(iso) {
|
|
1986
|
+
const match = /^(\d{4}-\d{2}-\d{2})/.exec(iso);
|
|
1987
|
+
return match ? match[1] : iso;
|
|
1988
|
+
}
|
|
1989
|
+
var CODING_KNOWLEDGE_STATE_DIR = path3.join("state", "coding-knowledge");
|
|
1990
|
+
function sessionDeltaStatePath(memoryDir, namespace) {
|
|
1991
|
+
const safe = sanitizeFragment(namespace) || "default";
|
|
1992
|
+
return path3.join(memoryDir, CODING_KNOWLEDGE_STATE_DIR, `${safe}.json`);
|
|
1993
|
+
}
|
|
1994
|
+
async function readLastSeenState(statePath) {
|
|
1995
|
+
try {
|
|
1996
|
+
const raw = await readFile2(statePath, "utf8");
|
|
1997
|
+
const parsed = JSON.parse(raw);
|
|
1998
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1999
|
+
const obj = parsed;
|
|
2000
|
+
const head = typeof obj.head === "string" ? obj.head : null;
|
|
2001
|
+
const at = typeof obj.at === "string" ? obj.at : null;
|
|
2002
|
+
if (!head || !at) return null;
|
|
2003
|
+
return { head, at };
|
|
2004
|
+
} catch {
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
async function writeLastSeenState(statePath, state) {
|
|
2009
|
+
if (!state.head || !state.at) {
|
|
2010
|
+
throw new Error(
|
|
2011
|
+
`writeLastSeenState: invalid state (head=${JSON.stringify(state.head)}, at=${JSON.stringify(state.at)})`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
const dir = path3.dirname(statePath);
|
|
2015
|
+
await mkdir(dir, { recursive: true });
|
|
2016
|
+
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2017
|
+
await writeFile(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
2018
|
+
await rename(tmp, statePath);
|
|
2019
|
+
}
|
|
2020
|
+
function resolveSlice(repoRoot, sinceHead, invoker) {
|
|
2021
|
+
const headResult = invoker(repoRoot, ["rev-parse", "HEAD"]);
|
|
2022
|
+
if (headResult.exitCode !== 0) {
|
|
2023
|
+
return {
|
|
2024
|
+
ok: false,
|
|
2025
|
+
code: headResult.stdout.trim().length === 0 ? "no_head" : "git_failed",
|
|
2026
|
+
detail: `rev-parse HEAD exited ${headResult.exitCode}`
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
const currentHead = headResult.stdout.trim();
|
|
2030
|
+
if (!currentHead) {
|
|
2031
|
+
return { ok: false, code: "no_head", detail: "rev-parse HEAD returned empty" };
|
|
2032
|
+
}
|
|
2033
|
+
if (!sinceHead) {
|
|
2034
|
+
return { ok: true, slice: { commits: [], touchedFiles: [], currentHead } };
|
|
2035
|
+
}
|
|
2036
|
+
if (sinceHead === currentHead) {
|
|
2037
|
+
return { ok: true, slice: { commits: [], touchedFiles: [], currentHead } };
|
|
2038
|
+
}
|
|
2039
|
+
const SEP = "";
|
|
2040
|
+
const logResult = invoker(repoRoot, [
|
|
2041
|
+
"log",
|
|
2042
|
+
"--reverse",
|
|
2043
|
+
`--pretty=format:%H${SEP}%s`,
|
|
2044
|
+
"--name-only",
|
|
2045
|
+
`${sinceHead}..${currentHead}`
|
|
2046
|
+
]);
|
|
2047
|
+
if (logResult.exitCode !== 0) {
|
|
2048
|
+
if (logResult.exitCode === 128) {
|
|
2049
|
+
return { ok: true, slice: { commits: [], touchedFiles: [], currentHead } };
|
|
2050
|
+
}
|
|
2051
|
+
return {
|
|
2052
|
+
ok: false,
|
|
2053
|
+
code: "git_failed",
|
|
2054
|
+
detail: "git log exited " + logResult.exitCode + " (transient \u2014 state marker preserved)"
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
const { commits, touchedFiles } = parseLogOutput(logResult.stdout, SEP);
|
|
2058
|
+
return { ok: true, slice: { commits, touchedFiles, currentHead } };
|
|
2059
|
+
}
|
|
2060
|
+
function parseLogOutput(stdout, sep2) {
|
|
2061
|
+
const commits = [];
|
|
2062
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
2063
|
+
const blocks = stdout.split(/\n\s*\n/);
|
|
2064
|
+
for (const block of blocks) {
|
|
2065
|
+
const lines = block.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2066
|
+
if (lines.length === 0) continue;
|
|
2067
|
+
const header = lines[0];
|
|
2068
|
+
const sepIdx = header.indexOf(sep2);
|
|
2069
|
+
if (sepIdx >= 0) {
|
|
2070
|
+
const sha = header.slice(0, sepIdx);
|
|
2071
|
+
const subject = header.slice(sepIdx + sep2.length);
|
|
2072
|
+
commits.push({ sha, subject });
|
|
2073
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
2074
|
+
if (lines[i]) fileSet.add(lines[i]);
|
|
2075
|
+
}
|
|
2076
|
+
} else {
|
|
2077
|
+
for (const line of lines) fileSet.add(line);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const touchedFiles = [...fileSet].sort((a, b) => a.localeCompare(b));
|
|
2081
|
+
return { commits, touchedFiles };
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/coding/session-delta-surfaces.ts
|
|
2085
|
+
var DELTA_SUBCOMMANDS = ["get"];
|
|
2086
|
+
function isSessionDeltaSurfaceEnabled(config, codingContext) {
|
|
2087
|
+
return config.enabled === true && config.sessionDelta === true && codingContext !== null && codingContext !== void 0;
|
|
2088
|
+
}
|
|
2089
|
+
async function handleCodingDelta(request, ctx) {
|
|
2090
|
+
const codingContext = request.sessionKey ? ctx.getCodingContext(request.sessionKey) : null;
|
|
2091
|
+
if (codingContext === null || !isSessionDeltaSurfaceEnabled(ctx.codingKnowledge, codingContext)) {
|
|
2092
|
+
ctx.throwInputError(
|
|
2093
|
+
"coding_delta requires codingKnowledge.enabled, codingKnowledge.sessionDelta, and an attached coding context"
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
switch (request.subcommand) {
|
|
2097
|
+
case "get":
|
|
2098
|
+
return deltaGet(request, ctx, codingContext);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
async function deltaGet(request, ctx, codingContext) {
|
|
2102
|
+
const storage = await ctx.resolveStorage(request);
|
|
2103
|
+
const statePath = sessionDeltaStatePath(storage.memoryDir, storage.namespace);
|
|
2104
|
+
const lastSeen = await readLastSeenState(statePath);
|
|
2105
|
+
const repoRoot = codingContext.rootPath ?? codingContext.projectId;
|
|
2106
|
+
const sliceResult = resolveSlice(
|
|
2107
|
+
repoRoot,
|
|
2108
|
+
lastSeen?.head ?? null,
|
|
2109
|
+
ctx.gitInvoker
|
|
2110
|
+
);
|
|
2111
|
+
if (!sliceResult.ok) {
|
|
2112
|
+
log.warn(
|
|
2113
|
+
`coding_delta/get: slice resolution failed (code=${sliceResult.code}): ${sliceResult.detail}`
|
|
2114
|
+
);
|
|
2115
|
+
return {
|
|
2116
|
+
subcommand: "get",
|
|
2117
|
+
ok: false,
|
|
2118
|
+
code: "git_failed",
|
|
2119
|
+
detail: sliceResult.detail
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
const result = computeSessionDelta(lastSeen, sliceResult.slice);
|
|
2123
|
+
const canAdvanceState = storage.canAdvanceState !== false;
|
|
2124
|
+
let nextState = null;
|
|
2125
|
+
if (result.ok) {
|
|
2126
|
+
nextState = result.nextState;
|
|
2127
|
+
} else if (result.code === "unreachable_head") {
|
|
2128
|
+
nextState = result.nextState;
|
|
2129
|
+
}
|
|
2130
|
+
if (nextState && canAdvanceState) {
|
|
2131
|
+
try {
|
|
2132
|
+
await writeLastSeenState(statePath, nextState);
|
|
2133
|
+
} catch (err) {
|
|
2134
|
+
log.warn(
|
|
2135
|
+
`coding_delta/get: state write failed for ${statePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
if (!result.ok) {
|
|
2140
|
+
if (result.code === "unreachable_head") {
|
|
2141
|
+
return {
|
|
2142
|
+
subcommand: "get",
|
|
2143
|
+
ok: false,
|
|
2144
|
+
code: result.code,
|
|
2145
|
+
detail: result.detail,
|
|
2146
|
+
nextState: result.nextState
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
return {
|
|
2150
|
+
subcommand: "get",
|
|
2151
|
+
ok: false,
|
|
2152
|
+
code: result.code,
|
|
2153
|
+
detail: result.detail
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
switch (result.kind) {
|
|
2157
|
+
case "first_run":
|
|
2158
|
+
return { subcommand: "get", ok: true, kind: "first_run", nextState: result.nextState };
|
|
2159
|
+
case "unchanged":
|
|
2160
|
+
return { subcommand: "get", ok: true, kind: "unchanged", nextState: result.nextState };
|
|
2161
|
+
case "changed":
|
|
2162
|
+
return {
|
|
2163
|
+
subcommand: "get",
|
|
2164
|
+
ok: true,
|
|
2165
|
+
kind: "changed",
|
|
2166
|
+
delta: {
|
|
2167
|
+
commits: result.delta.commits,
|
|
2168
|
+
touchedFiles: result.delta.touchedFiles,
|
|
2169
|
+
totalCommits: result.delta.totalCommits,
|
|
2170
|
+
totalTouchedFiles: result.delta.totalTouchedFiles,
|
|
2171
|
+
summaryLine: result.delta.summaryLine
|
|
2172
|
+
},
|
|
2173
|
+
nextState: result.nextState
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/correction/correction-contract.ts
|
|
2179
|
+
var MEMORY_CATEGORIES = [
|
|
2180
|
+
"fact",
|
|
2181
|
+
"preference",
|
|
2182
|
+
"correction",
|
|
2183
|
+
"entity",
|
|
2184
|
+
"decision",
|
|
2185
|
+
"relationship",
|
|
2186
|
+
"principle",
|
|
2187
|
+
"commitment",
|
|
2188
|
+
"moment",
|
|
2189
|
+
"skill",
|
|
2190
|
+
"rule",
|
|
2191
|
+
"procedure",
|
|
2192
|
+
"reasoning_trace"
|
|
2193
|
+
];
|
|
2194
|
+
var CORRECTION_ACTION_KINDS = [
|
|
2195
|
+
"supersede",
|
|
2196
|
+
"edit",
|
|
2197
|
+
"retract",
|
|
2198
|
+
"rescope",
|
|
2199
|
+
"redaction_rule"
|
|
2200
|
+
];
|
|
2201
|
+
var CORRECTION_TEXT_MAX = 1e4;
|
|
2202
|
+
var REDACTION_PATTERN_MAX = 256;
|
|
2203
|
+
function validateCorrectionRequest(request) {
|
|
2204
|
+
if (!request || typeof request !== "object") {
|
|
2205
|
+
throw new CorrectionContractError("CorrectionRequest must be an object.");
|
|
2206
|
+
}
|
|
2207
|
+
const text = typeof request.text === "string" ? request.text.trim() : "";
|
|
2208
|
+
if (text.length === 0) {
|
|
2209
|
+
throw new CorrectionContractError("CorrectionRequest.text is required and must be non-empty.");
|
|
2210
|
+
}
|
|
2211
|
+
if (text.length > CORRECTION_TEXT_MAX) {
|
|
2212
|
+
throw new CorrectionContractError(
|
|
2213
|
+
`CorrectionRequest.text exceeds the ${CORRECTION_TEXT_MAX}-character limit (${text.length}).`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
const targetIds = Array.isArray(request.targetIds) ? request.targetIds.filter((id) => typeof id === "string" && id.length > 0) : void 0;
|
|
2217
|
+
if (targetIds !== void 0 && targetIds.length === 0) {
|
|
2218
|
+
return {
|
|
2219
|
+
text,
|
|
2220
|
+
...request.sessionKey ? { sessionKey: request.sessionKey } : {},
|
|
2221
|
+
...request.principal ? { principal: request.principal } : {},
|
|
2222
|
+
...request.namespace ? { namespace: request.namespace } : {}
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
return {
|
|
2226
|
+
text,
|
|
2227
|
+
...targetIds ? { targetIds } : {},
|
|
2228
|
+
...request.sessionKey ? { sessionKey: request.sessionKey } : {},
|
|
2229
|
+
...request.principal ? { principal: request.principal } : {},
|
|
2230
|
+
...request.namespace ? { namespace: request.namespace } : {}
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
function validateRedactionPattern(pattern) {
|
|
2234
|
+
if (typeof pattern !== "string") {
|
|
2235
|
+
throw new CorrectionContractError("redaction_rule.pattern must be a string.");
|
|
2236
|
+
}
|
|
2237
|
+
const trimmed = pattern.trim();
|
|
2238
|
+
if (trimmed.length === 0) {
|
|
2239
|
+
throw new CorrectionContractError("redaction_rule.pattern is required.");
|
|
2240
|
+
}
|
|
2241
|
+
if (trimmed.length > REDACTION_PATTERN_MAX) {
|
|
2242
|
+
throw new CorrectionContractError(
|
|
2243
|
+
`redaction_rule.pattern exceeds the ${REDACTION_PATTERN_MAX}-character bound.`
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
if (isRegexLike(trimmed) && isUnsafeRedactionRegex(trimmed)) {
|
|
2247
|
+
throw new CorrectionContractError(
|
|
2248
|
+
"redaction_rule.pattern is unsafe \u2014 use a bounded literal or a regex without nested quantifiers / overlapping alternation."
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
if (isRegexLike(trimmed)) {
|
|
2252
|
+
const regexBody = trimmed.slice(1, -1);
|
|
2253
|
+
if (regexBody.length === 0) {
|
|
2254
|
+
throw new CorrectionContractError("redaction_rule.pattern body is empty \u2014 use a non-empty regex.");
|
|
2255
|
+
}
|
|
2256
|
+
try {
|
|
2257
|
+
if (new RegExp(regexBody).test("")) {
|
|
2258
|
+
throw new CorrectionContractError(
|
|
2259
|
+
"redaction_rule.pattern matches the empty string \u2014 it would withhold every fact."
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
} catch (e) {
|
|
2263
|
+
if (e instanceof CorrectionContractError) throw e;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return trimmed;
|
|
2267
|
+
}
|
|
2268
|
+
function isRegexLike(pattern) {
|
|
2269
|
+
return pattern.startsWith("/") && pattern.endsWith("/") && pattern.length >= 2;
|
|
2270
|
+
}
|
|
2271
|
+
function isUnsafeRedactionRegex(pattern) {
|
|
2272
|
+
const body = pattern.startsWith("/") && pattern.endsWith("/") ? pattern.slice(1, -1) : pattern;
|
|
2273
|
+
if (/(?:^|[^\\])\(\.\*\)|(?:^|[^\\])\.\*|(?:^|[^\\])\.\+|^\.([^*+]?)$/.test(body)) {
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
if (body.length > 512) return true;
|
|
2277
|
+
for (let i = 0; i < body.length; i++) {
|
|
2278
|
+
if (body[i] !== "(") continue;
|
|
2279
|
+
let depth = 1;
|
|
2280
|
+
let j = i + 1;
|
|
2281
|
+
while (j < body.length && depth > 0) {
|
|
2282
|
+
if (body[j] === "\\") {
|
|
2283
|
+
j += 2;
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
if (body[j] === "(") depth++;
|
|
2287
|
+
else if (body[j] === ")") depth--;
|
|
2288
|
+
j++;
|
|
2289
|
+
}
|
|
2290
|
+
if (depth !== 0) continue;
|
|
2291
|
+
const afterGroup = body[j];
|
|
2292
|
+
if (afterGroup !== "+" && afterGroup !== "*" && afterGroup !== "{") continue;
|
|
2293
|
+
const groupBody = body.slice(i + 1, j - 1);
|
|
2294
|
+
if (/[+*?]$/.test(groupBody) || /\{\d+,?\d*\}[+*?]?$/.test(groupBody)) {
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
if (groupBody.includes("|")) {
|
|
2298
|
+
const branches = groupBody.split("|");
|
|
2299
|
+
if (branches.length >= 2) {
|
|
2300
|
+
const firstChars = new Set(branches.map((b) => b[0]).filter(Boolean));
|
|
2301
|
+
if (firstChars.size < branches.filter((b) => b.length > 0).length) {
|
|
2302
|
+
return true;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
i = j;
|
|
2307
|
+
}
|
|
2308
|
+
return false;
|
|
2309
|
+
}
|
|
2310
|
+
function validateCorrectionAction(action) {
|
|
2311
|
+
if (!action || typeof action !== "object") {
|
|
2312
|
+
throw new CorrectionContractError("CorrectionAction must be an object.");
|
|
2313
|
+
}
|
|
2314
|
+
const a = action;
|
|
2315
|
+
if (typeof a.kind !== "string" || !CORRECTION_ACTION_KINDS.includes(a.kind)) {
|
|
2316
|
+
throw new CorrectionContractError(
|
|
2317
|
+
`CorrectionAction.kind must be one of: ${CORRECTION_ACTION_KINDS.join(", ")}.`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
switch (a.kind) {
|
|
2321
|
+
case "supersede":
|
|
2322
|
+
if (typeof a.loserId !== "string" || a.loserId.length === 0) {
|
|
2323
|
+
throw new CorrectionContractError("supersede.loserId is required.");
|
|
2324
|
+
}
|
|
2325
|
+
if (a.replacement !== void 0 && a.replacement !== null) {
|
|
2326
|
+
validateMemoryDraft(a.replacement);
|
|
2327
|
+
}
|
|
2328
|
+
break;
|
|
2329
|
+
case "edit":
|
|
2330
|
+
if (typeof a.memoryId !== "string" || a.memoryId.length === 0) {
|
|
2331
|
+
throw new CorrectionContractError("edit.memoryId is required.");
|
|
2332
|
+
}
|
|
2333
|
+
if (typeof a.patch !== "string" || a.patch.length === 0) {
|
|
2334
|
+
throw new CorrectionContractError("edit.patch is required and must be non-empty.");
|
|
2335
|
+
}
|
|
2336
|
+
break;
|
|
2337
|
+
case "retract":
|
|
2338
|
+
if (typeof a.memoryId !== "string" || a.memoryId.length === 0) {
|
|
2339
|
+
throw new CorrectionContractError("retract.memoryId is required.");
|
|
2340
|
+
}
|
|
2341
|
+
break;
|
|
2342
|
+
case "rescope":
|
|
2343
|
+
if (typeof a.memoryId !== "string" || a.memoryId.length === 0) {
|
|
2344
|
+
throw new CorrectionContractError("rescope.memoryId is required.");
|
|
2345
|
+
}
|
|
2346
|
+
if (typeof a.toNamespace !== "string" || a.toNamespace.trim().length === 0) {
|
|
2347
|
+
throw new CorrectionContractError("rescope.toNamespace is required.");
|
|
2348
|
+
}
|
|
2349
|
+
break;
|
|
2350
|
+
case "redaction_rule":
|
|
2351
|
+
if (typeof a.pattern !== "string") {
|
|
2352
|
+
throw new CorrectionContractError("redaction_rule.pattern is required.");
|
|
2353
|
+
}
|
|
2354
|
+
validateRedactionPattern(a.pattern);
|
|
2355
|
+
break;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
function validateMemoryDraft(draft) {
|
|
2359
|
+
if (!draft || typeof draft !== "object") {
|
|
2360
|
+
throw new CorrectionContractError("MemoryDraft must be an object.");
|
|
2361
|
+
}
|
|
2362
|
+
const d = draft;
|
|
2363
|
+
if (typeof d.content !== "string" || d.content.trim().length === 0) {
|
|
2364
|
+
throw new CorrectionContractError("MemoryDraft.content is required and must be non-empty.");
|
|
2365
|
+
}
|
|
2366
|
+
if (d.category !== void 0) {
|
|
2367
|
+
if (typeof d.category !== "string" || !MEMORY_CATEGORIES.includes(d.category)) {
|
|
2368
|
+
throw new CorrectionContractError(
|
|
2369
|
+
`MemoryDraft.category must be one of: ${MEMORY_CATEGORIES.join(", ")}.`
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
if (d.confidence !== void 0 && (typeof d.confidence !== "number" || d.confidence < 0 || d.confidence > 1)) {
|
|
2374
|
+
throw new CorrectionContractError("MemoryDraft.confidence must be a number in [0, 1].");
|
|
2375
|
+
}
|
|
2376
|
+
if (d.tags !== void 0 && !Array.isArray(d.tags)) {
|
|
2377
|
+
throw new CorrectionContractError("MemoryDraft.tags must be an array.");
|
|
2378
|
+
}
|
|
2379
|
+
if (d.structuredAttributes !== void 0 && (typeof d.structuredAttributes !== "object" || d.structuredAttributes === null)) {
|
|
2380
|
+
throw new CorrectionContractError("MemoryDraft.structuredAttributes must be an object.");
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
function deterministicFallbackPlan(args) {
|
|
2384
|
+
return {
|
|
2385
|
+
planId: newPlanId(),
|
|
2386
|
+
request: args.request,
|
|
2387
|
+
namespace: args.namespace,
|
|
2388
|
+
affected: args.affected,
|
|
2389
|
+
classification: "outdated",
|
|
2390
|
+
actions: [],
|
|
2391
|
+
diff: "",
|
|
2392
|
+
confidence: 0,
|
|
2393
|
+
warnings: [...args.warnings, "planner LLM unavailable \u2014 manual action selection required"],
|
|
2394
|
+
createdAt: args.createdAt,
|
|
2395
|
+
expiresAt: args.expiresAt,
|
|
2396
|
+
status: "pending"
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
function newPlanId() {
|
|
2400
|
+
return `corr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2401
|
+
}
|
|
2402
|
+
var CorrectionContractError = class extends Error {
|
|
2403
|
+
constructor(message) {
|
|
2404
|
+
super(message);
|
|
2405
|
+
this.name = "CorrectionContractError";
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// src/correction/correction-planner.ts
|
|
2410
|
+
import { mkdir as mkdir2, readFile as readFile3, rename as rename2, unlink, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
|
|
2411
|
+
import path4 from "path";
|
|
2412
|
+
var REDACTED_REQUEST_TEXT = "[redacted \u2014 never-store/redaction correction text withheld from the pending-plan file]";
|
|
2413
|
+
var CorrectionPlanner = class _CorrectionPlanner {
|
|
2414
|
+
constructor(deps) {
|
|
2415
|
+
this.deps = deps;
|
|
2416
|
+
}
|
|
2417
|
+
deps;
|
|
2418
|
+
/**
|
|
2419
|
+
* Plan a correction. `readableNamespaces` is the AUTHORIZED read scope the
|
|
2420
|
+
* service resolved for this caller; the planner never trusts a caller string.
|
|
2421
|
+
*/
|
|
2422
|
+
async plan(request, readableNamespaces) {
|
|
2423
|
+
const cleaned = validateCorrectionRequest(request);
|
|
2424
|
+
if (cleaned.text.length > CORRECTION_TEXT_MAX) {
|
|
2425
|
+
throw new CorrectionContractError("CorrectionRequest.text too long.");
|
|
2426
|
+
}
|
|
2427
|
+
if (cleaned.targetIds && cleaned.targetIds.length > this.deps.maxAffected) {
|
|
2428
|
+
throw new CorrectionContractError(
|
|
2429
|
+
`Correction target list (${cleaned.targetIds.length}) exceeds maxAffected (${this.deps.maxAffected}) \u2014 narrow the target set.`
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2432
|
+
const namespaces = readableNamespaces;
|
|
2433
|
+
if (namespaces.length === 0) {
|
|
2434
|
+
throw new CorrectionContractError(
|
|
2435
|
+
"Correction requires at least one readable namespace \u2014 principal has no read scope."
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
const located = await this.locateCandidates(cleaned, namespaces);
|
|
2439
|
+
if (located.candidates.length === 0) {
|
|
2440
|
+
return this.persist(
|
|
2441
|
+
this.emptyPlan(cleaned, namespaces[0], "no matching memories found for the correction text")
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
const llm = await this.deps.classifyAndDraft({
|
|
2445
|
+
text: cleaned.text,
|
|
2446
|
+
candidates: located.candidates
|
|
2447
|
+
});
|
|
2448
|
+
for (const action of llm.actions) {
|
|
2449
|
+
validateCorrectionAction(action);
|
|
2450
|
+
if (action.kind === "redaction_rule") {
|
|
2451
|
+
validateRedactionPattern(action.pattern);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
const affected = this.deriveAffected(located.candidates, llm);
|
|
2455
|
+
const touchedCount = new Set(
|
|
2456
|
+
llm.actions.map(
|
|
2457
|
+
(a) => a.kind === "supersede" ? a.loserId : a.kind === "edit" || a.kind === "retract" || a.kind === "rescope" ? a.memoryId : null
|
|
2458
|
+
).filter((id) => id !== null)
|
|
2459
|
+
).size;
|
|
2460
|
+
if (touchedCount > this.deps.maxAffected) {
|
|
2461
|
+
throw new CorrectionContractError(
|
|
2462
|
+
`Correction touches ${touchedCount} memories, exceeding the maxAffected limit of ${this.deps.maxAffected}. Narrow the correction text or supply explicit targetIds.`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
const candidateIds = new Set(located.candidates.map((c) => c.memoryId));
|
|
2466
|
+
if (llm.actions.length > 0 && candidateIds.size > 0) {
|
|
2467
|
+
const filtered = llm.actions.filter((action) => {
|
|
2468
|
+
const id = action.kind === "supersede" ? action.loserId : action.kind === "edit" || action.kind === "retract" || action.kind === "rescope" ? action.memoryId : null;
|
|
2469
|
+
return id === null || candidateIds.has(id);
|
|
2470
|
+
});
|
|
2471
|
+
if (filtered.length < llm.actions.length) {
|
|
2472
|
+
const dropped = llm.actions.length - filtered.length;
|
|
2473
|
+
llm.warnings = [...llm.warnings, `${dropped} action(s) targeted memories outside the located candidate set and were dropped (prompt-injection guard).`];
|
|
2474
|
+
llm.actions = filtered;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
const diff = llm.fallback ? "" : await this.deps.renderDiff({ candidates: located.candidates, actions: llm.actions });
|
|
2478
|
+
const createdAt = this.deps.now().toISOString();
|
|
2479
|
+
const expiresAt = new Date(
|
|
2480
|
+
this.deps.now().getTime() + this.deps.planTtlHours * 60 * 60 * 1e3
|
|
2481
|
+
).toISOString();
|
|
2482
|
+
const plan = llm.fallback ? deterministicFallbackPlan({
|
|
2483
|
+
request: cleaned,
|
|
2484
|
+
namespace: namespaces[0],
|
|
2485
|
+
affected,
|
|
2486
|
+
warnings: llm.warnings,
|
|
2487
|
+
createdAt,
|
|
2488
|
+
expiresAt
|
|
2489
|
+
}) : {
|
|
2490
|
+
planId: newPlanId(),
|
|
2491
|
+
request: cleaned,
|
|
2492
|
+
namespace: namespaces[0],
|
|
2493
|
+
affected,
|
|
2494
|
+
classification: llm.classification,
|
|
2495
|
+
actions: llm.actions,
|
|
2496
|
+
diff,
|
|
2497
|
+
confidence: clampConfidence(llm.confidence),
|
|
2498
|
+
warnings: llm.warnings,
|
|
2499
|
+
createdAt,
|
|
2500
|
+
expiresAt,
|
|
2501
|
+
status: "pending"
|
|
2502
|
+
};
|
|
2503
|
+
return this.persist(plan);
|
|
2504
|
+
}
|
|
2505
|
+
async locateCandidates(request, namespaces) {
|
|
2506
|
+
if (request.targetIds && request.targetIds.length > 0) {
|
|
2507
|
+
const targets = await this.deps.resolveTargets({
|
|
2508
|
+
targetIds: request.targetIds,
|
|
2509
|
+
namespaces
|
|
2510
|
+
});
|
|
2511
|
+
const seedIds2 = targets.map((c) => c.memoryId);
|
|
2512
|
+
const neighbors2 = await this.deps.expandNeighbors({
|
|
2513
|
+
seedIds: seedIds2,
|
|
2514
|
+
namespaces,
|
|
2515
|
+
limit: this.deps.maxAffected
|
|
2516
|
+
});
|
|
2517
|
+
return { candidates: mergeCandidates(targets, neighbors2).slice(0, this.deps.maxAffected) };
|
|
2518
|
+
}
|
|
2519
|
+
const searched = await this.deps.searchCorpus({
|
|
2520
|
+
text: request.text,
|
|
2521
|
+
namespaces,
|
|
2522
|
+
limit: this.deps.maxAffected
|
|
2523
|
+
});
|
|
2524
|
+
if (searched.length === 0) return { candidates: [] };
|
|
2525
|
+
const seedIds = searched.slice(0, Math.min(5, searched.length)).map((c) => c.memoryId);
|
|
2526
|
+
const neighbors = await this.deps.expandNeighbors({
|
|
2527
|
+
seedIds,
|
|
2528
|
+
namespaces,
|
|
2529
|
+
limit: this.deps.maxAffected
|
|
2530
|
+
});
|
|
2531
|
+
return { candidates: mergeCandidates(searched, neighbors).slice(0, this.deps.maxAffected) };
|
|
2532
|
+
}
|
|
2533
|
+
deriveAffected(candidates, llm) {
|
|
2534
|
+
const byId = new Map(candidates.map((c) => [c.memoryId, c]));
|
|
2535
|
+
const out = [];
|
|
2536
|
+
for (const rel of llm.relevance) {
|
|
2537
|
+
const c = byId.get(rel.memoryId);
|
|
2538
|
+
if (!c) continue;
|
|
2539
|
+
out.push({
|
|
2540
|
+
memoryId: c.memoryId,
|
|
2541
|
+
path: c.path,
|
|
2542
|
+
excerpt: c.excerpt,
|
|
2543
|
+
why: rel.why,
|
|
2544
|
+
...c.sourceQuote ? { sourceQuote: c.sourceQuote } : {}
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
const annotated = new Set(out.map((e) => e.memoryId));
|
|
2548
|
+
for (const action of llm.actions) {
|
|
2549
|
+
const id = action.kind === "supersede" ? action.loserId : action.kind === "edit" || action.kind === "retract" || action.kind === "rescope" ? action.memoryId : null;
|
|
2550
|
+
if (id && !annotated.has(id)) {
|
|
2551
|
+
const c = byId.get(id);
|
|
2552
|
+
if (c) {
|
|
2553
|
+
out.push({
|
|
2554
|
+
memoryId: c.memoryId,
|
|
2555
|
+
path: c.path,
|
|
2556
|
+
excerpt: c.excerpt,
|
|
2557
|
+
why: "touched by a drafted action",
|
|
2558
|
+
...c.sourceQuote ? { sourceQuote: c.sourceQuote } : {}
|
|
2559
|
+
});
|
|
2560
|
+
annotated.add(id);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
return out;
|
|
2565
|
+
}
|
|
2566
|
+
emptyPlan(request, namespace, warning) {
|
|
2567
|
+
const createdAt = this.deps.now().toISOString();
|
|
2568
|
+
const expiresAt = new Date(
|
|
2569
|
+
this.deps.now().getTime() + this.deps.planTtlHours * 60 * 60 * 1e3
|
|
2570
|
+
).toISOString();
|
|
2571
|
+
return {
|
|
2572
|
+
planId: newPlanId(),
|
|
2573
|
+
request,
|
|
2574
|
+
namespace,
|
|
2575
|
+
affected: [],
|
|
2576
|
+
classification: "outdated",
|
|
2577
|
+
actions: [],
|
|
2578
|
+
diff: "",
|
|
2579
|
+
confidence: 0,
|
|
2580
|
+
warnings: [warning],
|
|
2581
|
+
createdAt,
|
|
2582
|
+
expiresAt,
|
|
2583
|
+
status: "pending"
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
// -------------------------------------------------------------------------
|
|
2587
|
+
// Plan persistence — atomic write (rule 54), serialized per plan id (rule 40).
|
|
2588
|
+
// -------------------------------------------------------------------------
|
|
2589
|
+
/** Directory holding pending plans for one namespace. */
|
|
2590
|
+
async pendingDir(namespace) {
|
|
2591
|
+
return path4.join(await this.deps.storageDir(namespace), "state", "corrections", "pending");
|
|
2592
|
+
}
|
|
2593
|
+
async persist(plan) {
|
|
2594
|
+
const dir = await this.pendingDir(plan.namespace);
|
|
2595
|
+
const target = path4.join(dir, `${plan.planId}.json`);
|
|
2596
|
+
const sensitive = plan.classification === "never_store" || plan.actions.some((a) => a.kind === "redaction_rule");
|
|
2597
|
+
const persistedPlan = sensitive ? { ...plan, request: { ...plan.request, text: REDACTED_REQUEST_TEXT } } : plan;
|
|
2598
|
+
await serializeMutations(`correction-plan:${target}`, async () => {
|
|
2599
|
+
await mkdir2(dir, { recursive: true });
|
|
2600
|
+
const tmp = `${target}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
2601
|
+
await writeFile2(tmp, `${JSON.stringify(persistedPlan)}
|
|
2602
|
+
`, "utf-8");
|
|
2603
|
+
await rename2(tmp, target);
|
|
2604
|
+
});
|
|
2605
|
+
return plan;
|
|
2606
|
+
}
|
|
2607
|
+
/** Load a pending plan by id (used by the service / executor). */
|
|
2608
|
+
async loadPlan(namespace, planId) {
|
|
2609
|
+
assertSafePlanId(planId);
|
|
2610
|
+
const file = path4.join(await this.pendingDir(namespace), `${planId}.json`);
|
|
2611
|
+
return serializeMutations(`correction-plan:${file}`, async () => {
|
|
2612
|
+
let raw;
|
|
2613
|
+
try {
|
|
2614
|
+
raw = await readFile3(file, "utf-8");
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
const code = err?.code;
|
|
2617
|
+
if (code === "ENOENT") return null;
|
|
2618
|
+
throw err;
|
|
2619
|
+
}
|
|
2620
|
+
return parsePlan(raw);
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
/** List pending plans (newest first), excluding consumed (applied/discarded). */
|
|
2624
|
+
async listPending(namespace) {
|
|
2625
|
+
const dir = await this.pendingDir(namespace);
|
|
2626
|
+
let files;
|
|
2627
|
+
try {
|
|
2628
|
+
files = await readdir2(dir);
|
|
2629
|
+
} catch (err) {
|
|
2630
|
+
const code = err?.code;
|
|
2631
|
+
if (code === "ENOENT") return [];
|
|
2632
|
+
throw err;
|
|
2633
|
+
}
|
|
2634
|
+
const plans = [];
|
|
2635
|
+
for (const f of files) {
|
|
2636
|
+
if (!f.endsWith(".json")) continue;
|
|
2637
|
+
let plan = null;
|
|
2638
|
+
try {
|
|
2639
|
+
plan = await this.loadPlan(namespace, f.replace(/\.json$/, ""));
|
|
2640
|
+
} catch {
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
if (plan && plan.status === "pending") plans.push(plan);
|
|
2644
|
+
}
|
|
2645
|
+
plans.sort((a, b) => a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0);
|
|
2646
|
+
return plans;
|
|
2647
|
+
}
|
|
2648
|
+
async markConsumed(namespace, planId, status) {
|
|
2649
|
+
assertSafePlanId(planId);
|
|
2650
|
+
const file = path4.join(await this.pendingDir(namespace), `${planId}.json`);
|
|
2651
|
+
await serializeMutations(`correction-plan:${file}`, async () => {
|
|
2652
|
+
let raw;
|
|
2653
|
+
try {
|
|
2654
|
+
raw = await readFile3(file, "utf-8");
|
|
2655
|
+
} catch (err) {
|
|
2656
|
+
const code = err?.code;
|
|
2657
|
+
if (code === "ENOENT") return;
|
|
2658
|
+
throw err;
|
|
2659
|
+
}
|
|
2660
|
+
const plan = parsePlan(raw);
|
|
2661
|
+
if (!plan) return;
|
|
2662
|
+
plan.status = status;
|
|
2663
|
+
if (status === "applying") {
|
|
2664
|
+
plan.applyingAt = this.deps.now().toISOString();
|
|
2665
|
+
} else {
|
|
2666
|
+
delete plan.applyingAt;
|
|
2667
|
+
}
|
|
2668
|
+
if (status !== "applying") {
|
|
2669
|
+
plan.actions = plan.actions.map(
|
|
2670
|
+
(a) => a.kind === "redaction_rule" ? { ...a, pattern: "[redacted \u2014 pattern applied]" } : a
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
const tmp = `${file}.${process.pid}.${Date.now().toString(36)}.tmp`;
|
|
2674
|
+
await writeFile2(tmp, `${JSON.stringify(plan)}
|
|
2675
|
+
`, "utf-8");
|
|
2676
|
+
await rename2(tmp, file);
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
async deletePlan(namespace, planId) {
|
|
2680
|
+
assertSafePlanId(planId);
|
|
2681
|
+
const file = path4.join(await this.pendingDir(namespace), `${planId}.json`);
|
|
2682
|
+
await serializeMutations(`correction-plan:${file}`, async () => {
|
|
2683
|
+
try {
|
|
2684
|
+
await unlink(file);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
const code = err?.code;
|
|
2687
|
+
if (code === "ENOENT") return;
|
|
2688
|
+
throw err;
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Stale-applying TTL: a plan in `applying` state longer than this is
|
|
2694
|
+
* assumed to be from a crashed process and is recovered on startup
|
|
2695
|
+
* (#1713 Item 2). 10 minutes is generous for an apply that normally
|
|
2696
|
+
* completes in seconds, yet short enough to avoid leaving stale state.
|
|
2697
|
+
*/
|
|
2698
|
+
static STALE_APPLYING_TTL_MS = 10 * 60 * 1e3;
|
|
2699
|
+
/**
|
|
2700
|
+
* Recover stale `applying` plans past the stale-applying TTL (#1713 Item 2).
|
|
2701
|
+
*
|
|
2702
|
+
* A plan stuck in `applying` means the process died mid-apply. Re-applying
|
|
2703
|
+
* wholesale would duplicate succeeded actions (replacements, tombstones,
|
|
2704
|
+
* audits), so recovery discards + scrubs the plan instead. The operator
|
|
2705
|
+
* inspects the outcome and files a NEW plan for any actions that did not
|
|
2706
|
+
* complete. Plans without `applyingAt` (predating this fix) are recovered
|
|
2707
|
+
* when they are past their `expiresAt` timestamp.
|
|
2708
|
+
*
|
|
2709
|
+
* @returns the ids of recovered (discarded) plans.
|
|
2710
|
+
*/
|
|
2711
|
+
async recoverStaleApplyingPlans(namespace, opts) {
|
|
2712
|
+
const ttl = opts?.ttlMs ?? _CorrectionPlanner.STALE_APPLYING_TTL_MS;
|
|
2713
|
+
const now = (opts?.now ?? this.deps.now()).getTime();
|
|
2714
|
+
const dir = await this.pendingDir(namespace);
|
|
2715
|
+
let files;
|
|
2716
|
+
try {
|
|
2717
|
+
files = await readdir2(dir);
|
|
2718
|
+
} catch (err) {
|
|
2719
|
+
const code = err?.code;
|
|
2720
|
+
if (code === "ENOENT") return [];
|
|
2721
|
+
throw err;
|
|
2722
|
+
}
|
|
2723
|
+
const recovered = [];
|
|
2724
|
+
for (const f of files) {
|
|
2725
|
+
if (!f.endsWith(".json")) continue;
|
|
2726
|
+
const planId = f.replace(/\.json$/, "");
|
|
2727
|
+
let plan;
|
|
2728
|
+
try {
|
|
2729
|
+
plan = await this.loadPlan(namespace, planId);
|
|
2730
|
+
} catch {
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
if (!plan || plan.status !== "applying") continue;
|
|
2734
|
+
const stale = plan.applyingAt ? now - new Date(plan.applyingAt).getTime() >= ttl : now >= new Date(plan.expiresAt).getTime();
|
|
2735
|
+
if (!stale) continue;
|
|
2736
|
+
try {
|
|
2737
|
+
await this.markConsumed(namespace, planId, "discarded");
|
|
2738
|
+
recovered.push(planId);
|
|
2739
|
+
} catch {
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return recovered;
|
|
2743
|
+
}
|
|
2744
|
+
};
|
|
2745
|
+
function assertSafePlanId(planId) {
|
|
2746
|
+
if (typeof planId !== "string" || planId.length === 0 || planId.includes("/") || planId.includes("\\") || planId.includes("\0") || planId === "." || planId === ".." || planId.startsWith(".") || planId.includes("..")) {
|
|
2747
|
+
throw new CorrectionContractError(
|
|
2748
|
+
`Invalid plan id: ${JSON.stringify(planId)} \u2014 must be a bare file basename with no path separators or parent segments.`
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
function clampConfidence(n) {
|
|
2753
|
+
if (!Number.isFinite(n)) return 0;
|
|
2754
|
+
return Math.min(1, Math.max(0, n));
|
|
2755
|
+
}
|
|
2756
|
+
function mergeCandidates(a, b) {
|
|
2757
|
+
const map = /* @__PURE__ */ new Map();
|
|
2758
|
+
for (const c of a) {
|
|
2759
|
+
const existing = map.get(c.memoryId);
|
|
2760
|
+
if (!existing || c.score > existing.score) map.set(c.memoryId, c);
|
|
2761
|
+
}
|
|
2762
|
+
for (const c of b) {
|
|
2763
|
+
const existing = map.get(c.memoryId);
|
|
2764
|
+
if (!existing || c.score > existing.score) map.set(c.memoryId, c);
|
|
2765
|
+
}
|
|
2766
|
+
return [...map.values()].sort((x, y) => y.score - x.score);
|
|
2767
|
+
}
|
|
2768
|
+
function parsePlan(raw) {
|
|
2769
|
+
let parsed;
|
|
2770
|
+
try {
|
|
2771
|
+
parsed = JSON.parse(raw);
|
|
2772
|
+
} catch {
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2776
|
+
const p = parsed;
|
|
2777
|
+
if (typeof p.planId !== "string" || typeof p.namespace !== "string") return null;
|
|
2778
|
+
if (typeof p.createdAt !== "string" || typeof p.expiresAt !== "string") return null;
|
|
2779
|
+
if (!Array.isArray(p.actions) || !Array.isArray(p.affected)) return null;
|
|
2780
|
+
for (const action of p.actions) {
|
|
2781
|
+
try {
|
|
2782
|
+
validateCorrectionAction(action);
|
|
2783
|
+
} catch {
|
|
2784
|
+
return null;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
return parsed;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/correction/correction-executor.ts
|
|
2791
|
+
var CorrectionExecutor = class {
|
|
2792
|
+
constructor(deps, planner) {
|
|
2793
|
+
this.deps = deps;
|
|
2794
|
+
this.planner = planner;
|
|
2795
|
+
}
|
|
2796
|
+
deps;
|
|
2797
|
+
planner;
|
|
2798
|
+
/**
|
|
2799
|
+
* Apply a persisted plan by id. Idempotent-once: a second apply of the same
|
|
2800
|
+
* plan is rejected. Returns the {@link CorrectionOutcome}.
|
|
2801
|
+
*/
|
|
2802
|
+
async apply(namespace, planId, opts) {
|
|
2803
|
+
if (!opts.confirm) {
|
|
2804
|
+
throw new CorrectionContractError(
|
|
2805
|
+
"Correction apply requires explicit confirmation (correction.applyRequiresConfirm)."
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
return serializeMutations(
|
|
2809
|
+
`correction-apply:${namespace}:${planId}`,
|
|
2810
|
+
() => this.applyInternal(namespace, planId, opts.canWriteDestination)
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
async applyInternal(namespace, planId, canWriteDestination) {
|
|
2814
|
+
const plan = await this.planner.loadPlan(namespace, planId);
|
|
2815
|
+
if (!plan) {
|
|
2816
|
+
throw new CorrectionContractError(`Correction plan not found: ${planId}`);
|
|
2817
|
+
}
|
|
2818
|
+
if (plan.namespace !== namespace) {
|
|
2819
|
+
throw new CorrectionContractError(
|
|
2820
|
+
`Correction plan ${planId} belongs to namespace '${plan.namespace}', not '${namespace}'.`
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
if (plan.status === "applied" || plan.status === "partial" || plan.status === "applying") {
|
|
2824
|
+
throw new CorrectionContractError(
|
|
2825
|
+
`Correction plan ${planId} has already been applied or is in progress (status=${plan.status}).`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
if (plan.status === "discarded") {
|
|
2829
|
+
throw new CorrectionContractError(`Correction plan ${planId} has been discarded.`);
|
|
2830
|
+
}
|
|
2831
|
+
if (this.deps.now().getTime() > new Date(plan.expiresAt).getTime()) {
|
|
2832
|
+
await this.planner.markConsumed(namespace, planId, "discarded");
|
|
2833
|
+
throw new CorrectionContractError(
|
|
2834
|
+
`Correction plan ${planId} expired at ${plan.expiresAt} and has been discarded.`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
for (const action of plan.actions) {
|
|
2838
|
+
validateCorrectionAction(action);
|
|
2839
|
+
if (action.kind === "redaction_rule") {
|
|
2840
|
+
validateRedactionPattern(action.pattern);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
try {
|
|
2844
|
+
await this.planner.markConsumed(namespace, planId, "applying");
|
|
2845
|
+
} catch {
|
|
2846
|
+
throw new CorrectionContractError(
|
|
2847
|
+
`Correction plan ${planId}: cannot mark in-progress (filesystem unwritable?) \u2014 aborting before any mutation.`
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
const results = [];
|
|
2851
|
+
const appliedTouched = [];
|
|
2852
|
+
for (const action of plan.actions) {
|
|
2853
|
+
if (action.kind === "supersede" && action.replacement) {
|
|
2854
|
+
const loser = await this.deps.getMemory(namespace, action.loserId);
|
|
2855
|
+
if (!loser) {
|
|
2856
|
+
results.push({
|
|
2857
|
+
action,
|
|
2858
|
+
status: "failed",
|
|
2859
|
+
error: `supersede loser not found: ${action.loserId}`
|
|
2860
|
+
});
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
try {
|
|
2864
|
+
const newId = await this.deps.writeReplacement(namespace, {
|
|
2865
|
+
content: action.replacement.content,
|
|
2866
|
+
...action.replacement.category ? { category: action.replacement.category } : {},
|
|
2867
|
+
...action.replacement.confidence !== void 0 ? { confidence: action.replacement.confidence } : {},
|
|
2868
|
+
...action.replacement.tags ? { tags: action.replacement.tags } : {},
|
|
2869
|
+
...action.replacement.entityRef ? { entityRef: action.replacement.entityRef } : {},
|
|
2870
|
+
...action.replacement.validAt ? { validAt: action.replacement.validAt } : {},
|
|
2871
|
+
...action.replacement.observedAt ? { observedAt: action.replacement.observedAt } : {},
|
|
2872
|
+
...action.replacement.structuredAttributes ? { structuredAttributes: action.replacement.structuredAttributes } : {},
|
|
2873
|
+
supersedes: action.loserId
|
|
2874
|
+
});
|
|
2875
|
+
results.push({ action, status: "applied", memoryId: newId });
|
|
2876
|
+
appliedTouched.push(newId);
|
|
2877
|
+
} catch (err) {
|
|
2878
|
+
results.push({
|
|
2879
|
+
action,
|
|
2880
|
+
status: "failed",
|
|
2881
|
+
error: errMsg(err)
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
} else if (action.kind === "edit") {
|
|
2885
|
+
try {
|
|
2886
|
+
const editedId = await this.deps.applyEdit(namespace, action.memoryId, action.patch);
|
|
2887
|
+
results.push({ action, status: "applied", memoryId: editedId });
|
|
2888
|
+
appliedTouched.push(editedId);
|
|
2889
|
+
} catch (err) {
|
|
2890
|
+
results.push({ action, status: "failed", error: errMsg(err) });
|
|
2891
|
+
}
|
|
2892
|
+
} else if (action.kind === "redaction_rule") {
|
|
2893
|
+
try {
|
|
2894
|
+
await this.deps.registerRedactionRule(namespace, action.pattern);
|
|
2895
|
+
results.push({ action, status: "applied" });
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
results.push({ action, status: "failed", error: errMsg(err) });
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
for (const action of plan.actions) {
|
|
2902
|
+
if (action.kind === "supersede") {
|
|
2903
|
+
const replacementResult = results.find(
|
|
2904
|
+
(r) => r.action === action && r.status === "applied"
|
|
2905
|
+
);
|
|
2906
|
+
if (action.replacement && !replacementResult) {
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
await this.retireAndTombstone(namespace, action, "supersession", results, appliedTouched, {
|
|
2910
|
+
supersededBy: replacementResult?.memoryId
|
|
2911
|
+
});
|
|
2912
|
+
} else if (action.kind === "retract") {
|
|
2913
|
+
await this.retireAndTombstone(namespace, action, "retraction", results, appliedTouched);
|
|
2914
|
+
} else if (action.kind === "rescope") {
|
|
2915
|
+
try {
|
|
2916
|
+
const allowed = canWriteDestination ? await canWriteDestination(action.toNamespace) : true;
|
|
2917
|
+
if (!allowed) {
|
|
2918
|
+
results.push({
|
|
2919
|
+
action,
|
|
2920
|
+
status: "failed",
|
|
2921
|
+
error: `rescope destination namespace not writable: ${action.toNamespace}`
|
|
2922
|
+
});
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
const destId = await this.deps.rescopeMemory(namespace, action.memoryId, action.toNamespace);
|
|
2926
|
+
results.push({ action, status: "applied", memoryId: destId });
|
|
2927
|
+
appliedTouched.push(action.memoryId);
|
|
2928
|
+
try {
|
|
2929
|
+
await this.deps.propagate(action.toNamespace, [destId]);
|
|
2930
|
+
} catch {
|
|
2931
|
+
}
|
|
2932
|
+
} catch (err) {
|
|
2933
|
+
results.push({ action, status: "failed", error: errMsg(err) });
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
const propagationWarnings = [];
|
|
2938
|
+
if (appliedTouched.length > 0) {
|
|
2939
|
+
try {
|
|
2940
|
+
await this.deps.propagate(namespace, appliedTouched);
|
|
2941
|
+
} catch (err) {
|
|
2942
|
+
propagationWarnings.push(`propagation failed (non-fatal): ${errMsg(err)}`);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
const anyFailed = results.some((r) => r.status === "failed");
|
|
2946
|
+
const status = anyFailed ? "partial" : "applied";
|
|
2947
|
+
const appliedAt = this.deps.now().toISOString();
|
|
2948
|
+
const outcome = {
|
|
2949
|
+
planId,
|
|
2950
|
+
status,
|
|
2951
|
+
results,
|
|
2952
|
+
auditMemoryId: "",
|
|
2953
|
+
// filled after the audit write
|
|
2954
|
+
appliedAt
|
|
2955
|
+
};
|
|
2956
|
+
if (propagationWarnings.length > 0) {
|
|
2957
|
+
outcome.warnings = propagationWarnings;
|
|
2958
|
+
}
|
|
2959
|
+
try {
|
|
2960
|
+
const auditId = await this.deps.appendAuditRecord(namespace, {
|
|
2961
|
+
planId,
|
|
2962
|
+
classification: plan.classification,
|
|
2963
|
+
outcome,
|
|
2964
|
+
requestText: plan.request.text
|
|
2965
|
+
});
|
|
2966
|
+
outcome.auditMemoryId = auditId;
|
|
2967
|
+
} catch (err) {
|
|
2968
|
+
outcome.warnings = [
|
|
2969
|
+
...propagationWarnings,
|
|
2970
|
+
`audit record write failed (non-fatal): ${errMsg(err)}`
|
|
2971
|
+
];
|
|
2972
|
+
}
|
|
2973
|
+
try {
|
|
2974
|
+
await this.planner.markConsumed(namespace, planId, status);
|
|
2975
|
+
} catch (err) {
|
|
2976
|
+
const w = outcome;
|
|
2977
|
+
w.warnings = [...w.warnings ?? [], `plan mark-consumed failed (non-fatal): ${errMsg(err)}`];
|
|
2978
|
+
}
|
|
2979
|
+
return outcome;
|
|
2980
|
+
}
|
|
2981
|
+
async retireAndTombstone(namespace, action, reason, results, appliedTouched, opts = {}) {
|
|
2982
|
+
const memoryId = action.kind === "supersede" ? action.loserId : action.kind === "retract" ? action.memoryId : null;
|
|
2983
|
+
if (!memoryId) return;
|
|
2984
|
+
try {
|
|
2985
|
+
const memory = await this.deps.getMemory(namespace, memoryId);
|
|
2986
|
+
if (!memory) {
|
|
2987
|
+
results.push({
|
|
2988
|
+
action,
|
|
2989
|
+
status: "failed",
|
|
2990
|
+
error: `memory not found: ${memoryId}`
|
|
2991
|
+
});
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const validUntil = this.deps.biTemporalEnabled ? this.deps.now().toISOString() : void 0;
|
|
2995
|
+
const tombstoneId = await this.deps.appendTombstone(namespace, {
|
|
2996
|
+
reason,
|
|
2997
|
+
sourceMemoryId: memoryId,
|
|
2998
|
+
rawContent: memory.rawContent,
|
|
2999
|
+
...memory.entityRef ? { entityRef: memory.entityRef } : {},
|
|
3000
|
+
...memory.supersessionKey ? { supersessionKey: memory.supersessionKey } : {},
|
|
3001
|
+
// #1672 item 4: carry the canonical contentHash (exact tier) and the
|
|
3002
|
+
// full supersession-key set (keyed tier) so paraphrased re-observations
|
|
3003
|
+
// of the same supersession identity are blocked, not just the literal
|
|
3004
|
+
// body. Without these the tombstone projected from a structured fact
|
|
3005
|
+
// dropped both fields and the fact could resurrect.
|
|
3006
|
+
...memory.contentHash ? { contentHash: memory.contentHash } : {},
|
|
3007
|
+
...memory.supersessionKeys && memory.supersessionKeys.length > 0 ? { supersessionKeys: memory.supersessionKeys } : {}
|
|
3008
|
+
});
|
|
3009
|
+
await this.deps.retireMemory(namespace, memoryId, {
|
|
3010
|
+
status: reason === "supersession" ? "superseded" : "retracted",
|
|
3011
|
+
...opts.supersededBy ? { supersededBy: opts.supersededBy } : {},
|
|
3012
|
+
...validUntil ? { validUntil } : {}
|
|
3013
|
+
});
|
|
3014
|
+
results.push({
|
|
3015
|
+
action,
|
|
3016
|
+
status: "applied",
|
|
3017
|
+
memoryId,
|
|
3018
|
+
...tombstoneId ? { tombstoneId } : {}
|
|
3019
|
+
});
|
|
3020
|
+
appliedTouched.push(memoryId);
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
results.push({ action, status: "failed", error: errMsg(err) });
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
var CORRECTION_ERROR_MAX = 500;
|
|
3027
|
+
function sanitizeErrorMessage(raw) {
|
|
3028
|
+
const stripped = raw.replace(/(^|[\s:'"(])\/(?:[^\s'">)\\]+\/)+[^\s'">)\\]*/g, "$1<path>").replace(/(^|[\s:'"(])[A-Za-z]:\\[^\s'">)\\]+(?:\\[^\s'">) ]*)*/g, "$1<path>");
|
|
3029
|
+
const trimmed = stripped.trim();
|
|
3030
|
+
return trimmed.length > CORRECTION_ERROR_MAX ? `${trimmed.slice(0, CORRECTION_ERROR_MAX)}\u2026` : trimmed;
|
|
3031
|
+
}
|
|
3032
|
+
function errMsg(err) {
|
|
3033
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
3034
|
+
return sanitizeErrorMessage(raw);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// src/correction/correction-service.ts
|
|
3038
|
+
var CorrectionService = class {
|
|
3039
|
+
constructor(deps) {
|
|
3040
|
+
this.deps = deps;
|
|
3041
|
+
this.planner = new CorrectionPlanner(deps.plannerDeps());
|
|
3042
|
+
this.executor = new CorrectionExecutor(deps.executorDeps(), this.planner);
|
|
3043
|
+
}
|
|
3044
|
+
deps;
|
|
3045
|
+
planner;
|
|
3046
|
+
executor;
|
|
3047
|
+
/**
|
|
3048
|
+
* Plan a correction. Read-only — produces + persists a {@link CorrectionPlan}.
|
|
3049
|
+
* Safe to run with the gate ON (default) since the plan writes no memory state.
|
|
3050
|
+
*
|
|
3051
|
+
* The plan is stored under the AUTHORIZED (writable) namespace — the same
|
|
3052
|
+
* namespace apply/listPending/discard resolve — so the plan is always
|
|
3053
|
+
* found by the caller that created it (review threads: plan-stored-wrong-
|
|
3054
|
+
* namespace + cross-namespace candidate mismatch). Candidates are located
|
|
3055
|
+
* within that single namespace too: a correction can only affect memories
|
|
3056
|
+
* the principal can write, so reading from a non-writable namespace to
|
|
3057
|
+
* draft a correction that can never be applied would be misleading.
|
|
3058
|
+
*/
|
|
3059
|
+
async plan(request) {
|
|
3060
|
+
this.requireEnabled();
|
|
3061
|
+
const authorized = await this.deps.policy.resolveAuthorizedNamespace(request);
|
|
3062
|
+
return this.planner.plan(request, [authorized]);
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Apply a persisted plan by id. Requires `confirm: true` (or interactive
|
|
3066
|
+
* yes at the CLI) when `applyRequiresConfirm` is on (default).
|
|
3067
|
+
*/
|
|
3068
|
+
async apply(planId, opts) {
|
|
3069
|
+
this.requireEnabled();
|
|
3070
|
+
if (this.deps.applyRequiresConfirm() && opts.confirm !== true) {
|
|
3071
|
+
throw new CorrectionContractError(
|
|
3072
|
+
"Correction apply requires explicit confirmation (pass `confirm: true`)."
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
const namespace = await this.deps.policy.resolveAuthorizedNamespace({
|
|
3076
|
+
...opts.namespace ? { namespace: opts.namespace } : {},
|
|
3077
|
+
...opts.sessionKey ? { sessionKey: opts.sessionKey } : {},
|
|
3078
|
+
...opts.principal ? { principal: opts.principal } : {}
|
|
3079
|
+
});
|
|
3080
|
+
return this.executor.apply(namespace, planId, {
|
|
3081
|
+
confirm: true,
|
|
3082
|
+
// Authorize rescope destinations through the namespace policy bound to
|
|
3083
|
+
// THIS caller's principal (review thread: authorize-rescope-destination).
|
|
3084
|
+
canWriteDestination: (dest) => this.deps.policy.canWriteNamespace({
|
|
3085
|
+
namespace: dest,
|
|
3086
|
+
...opts.sessionKey ? { sessionKey: opts.sessionKey } : {},
|
|
3087
|
+
...opts.principal ? { principal: opts.principal } : {}
|
|
3088
|
+
})
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
/** List pending (not-yet-applied) plans for the caller's namespace. */
|
|
3092
|
+
async listPending(opts) {
|
|
3093
|
+
this.requireEnabled();
|
|
3094
|
+
const namespace = await this.deps.policy.resolveAuthorizedNamespace(opts);
|
|
3095
|
+
return this.planner.listPending(namespace);
|
|
3096
|
+
}
|
|
3097
|
+
/** Discard a pending plan. Idempotent. */
|
|
3098
|
+
async discard(planId, opts) {
|
|
3099
|
+
this.requireEnabled();
|
|
3100
|
+
const namespace = await this.deps.policy.resolveAuthorizedNamespace(opts);
|
|
3101
|
+
await this.planner.markConsumed(namespace, planId, "discarded");
|
|
3102
|
+
await this.planner.deletePlan(namespace, planId);
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Recover stale `applying` plans across the given namespaces (#1713 Item 2).
|
|
3106
|
+
*
|
|
3107
|
+
* Startup maintenance: scans each namespace for plans stuck in `applying`
|
|
3108
|
+
* past the stale-applying TTL (process died mid-apply) and discards+scrubs
|
|
3109
|
+
* them. Unlike plan/apply/listPending, this is NOT a per-caller request —
|
|
3110
|
+
* it operates on all supplied namespaces directly because it is an
|
|
3111
|
+
* internal maintenance operation invoked by the orchestrator at init.
|
|
3112
|
+
*
|
|
3113
|
+
* @returns the total count of recovered plans across all namespaces.
|
|
3114
|
+
*/
|
|
3115
|
+
async recoverStaleApplyingPlans(namespaces) {
|
|
3116
|
+
let recovered = 0;
|
|
3117
|
+
for (const namespace of namespaces) {
|
|
3118
|
+
try {
|
|
3119
|
+
const ids = await this.planner.recoverStaleApplyingPlans(namespace);
|
|
3120
|
+
recovered += ids.length;
|
|
3121
|
+
} catch {
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
return recovered;
|
|
3125
|
+
}
|
|
3126
|
+
requireEnabled() {
|
|
3127
|
+
if (!this.deps.isEnabled()) {
|
|
3128
|
+
throw new CorrectionContractError(
|
|
3129
|
+
"Correction Contract is disabled (set correction.enabled = true to enable)."
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
|
|
3135
|
+
// src/correction/correction-access-wiring.ts
|
|
3136
|
+
import path5 from "path";
|
|
3137
|
+
import { createHash as createHash2 } from "crypto";
|
|
3138
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
3139
|
+
function createCorrectionService(wiring) {
|
|
3140
|
+
const cfg = wiring.orchestrator.config;
|
|
3141
|
+
const correctionEnabled = isCorrectionFeatureEnabled(cfg);
|
|
3142
|
+
const applyRequiresConfirm = typeof cfg.correctionApplyRequiresConfirm === "boolean" ? cfg.correctionApplyRequiresConfirm : readCorrectionFlag(cfg, "applyRequiresConfirm", true);
|
|
3143
|
+
const maxAffected = typeof cfg.correctionMaxAffected === "number" && cfg.correctionMaxAffected >= 1 ? Math.floor(cfg.correctionMaxAffected) : readCorrectionNumber(cfg, "maxAffected", 10);
|
|
3144
|
+
const planTtlHours = typeof cfg.correctionPlanTtlHours === "number" && cfg.correctionPlanTtlHours > 0 ? cfg.correctionPlanTtlHours : readCorrectionNumber(cfg, "planTtlHours", 24);
|
|
3145
|
+
const biTemporalEnabled = cfg.temporalBiTemporal === true;
|
|
3146
|
+
const serviceDeps = {
|
|
3147
|
+
policy: {
|
|
3148
|
+
resolveAuthorizedNamespace: (req) => wiring.resolveAuthorizedNamespace(req),
|
|
3149
|
+
canWriteNamespace: (req) => wiring.canWriteNamespace(req),
|
|
3150
|
+
readableNamespaces: (req) => Promise.resolve(wiring.resolveReadableNamespaces(req))
|
|
3151
|
+
},
|
|
3152
|
+
plannerDeps: () => makePlannerDeps(wiring, { maxAffected, planTtlHours }),
|
|
3153
|
+
executorDeps: () => makeExecutorDeps(wiring, { biTemporalEnabled }),
|
|
3154
|
+
isEnabled: () => correctionEnabled,
|
|
3155
|
+
applyRequiresConfirm: () => applyRequiresConfirm
|
|
3156
|
+
};
|
|
3157
|
+
return new CorrectionService(serviceDeps);
|
|
3158
|
+
}
|
|
3159
|
+
function makePlannerDeps(wiring, opts) {
|
|
3160
|
+
return {
|
|
3161
|
+
searchCorpus: async ({ text, namespaces, limit }) => searchMemories(wiring, text, namespaces, limit),
|
|
3162
|
+
resolveTargets: async ({ targetIds, namespaces }) => resolveTargetMemories(wiring, targetIds, namespaces),
|
|
3163
|
+
expandNeighbors: async ({ seedIds, namespaces, limit }) => expandEntityNeighbors(wiring, seedIds, namespaces, limit),
|
|
3164
|
+
classifyAndDraft: async ({ text, candidates }) => classifyAndDraft(wiring, text, candidates),
|
|
3165
|
+
renderDiff: async ({ candidates, actions }) => renderCorrectionDiff(wiring, candidates, actions),
|
|
3166
|
+
storageDir: async (namespace) => (await wiring.orchestrator.getStorage(namespace)).dir,
|
|
3167
|
+
maxAffected: opts.maxAffected,
|
|
3168
|
+
planTtlHours: opts.planTtlHours,
|
|
3169
|
+
now: () => /* @__PURE__ */ new Date()
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
async function searchMemories(wiring, text, namespaces, limit) {
|
|
3173
|
+
const tokens = tokenize(text);
|
|
3174
|
+
if (tokens.length === 0) return [];
|
|
3175
|
+
const scored = [];
|
|
3176
|
+
for (const ns of namespaces) {
|
|
3177
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
3178
|
+
const all = await storage.readAllMemories();
|
|
3179
|
+
for (const m of all) {
|
|
3180
|
+
if (!isEligibleCorrectionCandidate(m)) continue;
|
|
3181
|
+
const hay = `${m.content} ${m.frontmatter.tags?.join(" ") ?? ""}`.toLowerCase();
|
|
3182
|
+
let hits = 0;
|
|
3183
|
+
for (const tok of tokens) {
|
|
3184
|
+
if (hay.includes(tok)) hits++;
|
|
3185
|
+
}
|
|
3186
|
+
if (hits === 0) continue;
|
|
3187
|
+
scored.push({ m, ns, score: hits / tokens.length });
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
scored.sort((a, b) => b.score - a.score);
|
|
3191
|
+
return scored.slice(0, limit).map((s, i) => toCandidate(s.m, s.ns, s.score - i * 0.01));
|
|
3192
|
+
}
|
|
3193
|
+
function tokenize(text) {
|
|
3194
|
+
const STOP = /* @__PURE__ */ new Set([
|
|
3195
|
+
"the",
|
|
3196
|
+
"a",
|
|
3197
|
+
"an",
|
|
3198
|
+
"is",
|
|
3199
|
+
"are",
|
|
3200
|
+
"was",
|
|
3201
|
+
"were",
|
|
3202
|
+
"be",
|
|
3203
|
+
"been",
|
|
3204
|
+
"being",
|
|
3205
|
+
"to",
|
|
3206
|
+
"of",
|
|
3207
|
+
"in",
|
|
3208
|
+
"on",
|
|
3209
|
+
"at",
|
|
3210
|
+
"by",
|
|
3211
|
+
"for",
|
|
3212
|
+
"with",
|
|
3213
|
+
"from",
|
|
3214
|
+
"into",
|
|
3215
|
+
"and",
|
|
3216
|
+
"or",
|
|
3217
|
+
"but",
|
|
3218
|
+
"not",
|
|
3219
|
+
"no",
|
|
3220
|
+
"yes",
|
|
3221
|
+
"this",
|
|
3222
|
+
"that",
|
|
3223
|
+
"these",
|
|
3224
|
+
"those",
|
|
3225
|
+
"it",
|
|
3226
|
+
"its",
|
|
3227
|
+
"we",
|
|
3228
|
+
"you",
|
|
3229
|
+
"i",
|
|
3230
|
+
"he",
|
|
3231
|
+
"she",
|
|
3232
|
+
"they",
|
|
3233
|
+
"them",
|
|
3234
|
+
"our",
|
|
3235
|
+
"your",
|
|
3236
|
+
"my",
|
|
3237
|
+
"his",
|
|
3238
|
+
"her",
|
|
3239
|
+
"their",
|
|
3240
|
+
"as",
|
|
3241
|
+
"so",
|
|
3242
|
+
"if",
|
|
3243
|
+
"then",
|
|
3244
|
+
"than",
|
|
3245
|
+
"too",
|
|
3246
|
+
"very",
|
|
3247
|
+
"can",
|
|
3248
|
+
"will",
|
|
3249
|
+
"just",
|
|
3250
|
+
"should",
|
|
3251
|
+
"now",
|
|
3252
|
+
"has",
|
|
3253
|
+
"have",
|
|
3254
|
+
"had",
|
|
3255
|
+
"do",
|
|
3256
|
+
"does",
|
|
3257
|
+
"did",
|
|
3258
|
+
"about",
|
|
3259
|
+
"which",
|
|
3260
|
+
"what",
|
|
3261
|
+
"who",
|
|
3262
|
+
"when",
|
|
3263
|
+
"where",
|
|
3264
|
+
"why",
|
|
3265
|
+
"how",
|
|
3266
|
+
"all",
|
|
3267
|
+
"each",
|
|
3268
|
+
"every",
|
|
3269
|
+
"both",
|
|
3270
|
+
"few",
|
|
3271
|
+
"more",
|
|
3272
|
+
"most",
|
|
3273
|
+
"other",
|
|
3274
|
+
"some",
|
|
3275
|
+
"such",
|
|
3276
|
+
"only",
|
|
3277
|
+
"own",
|
|
3278
|
+
"same",
|
|
3279
|
+
"up"
|
|
3280
|
+
]);
|
|
3281
|
+
const raw = text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
3282
|
+
const out = raw.filter((w) => w.length >= 3 && !STOP.has(w));
|
|
3283
|
+
return [...new Set(out)];
|
|
3284
|
+
}
|
|
3285
|
+
async function resolveTargetMemories(wiring, targetIds, namespaces) {
|
|
3286
|
+
const out = [];
|
|
3287
|
+
const missing = [];
|
|
3288
|
+
for (const id of targetIds) {
|
|
3289
|
+
let found = null;
|
|
3290
|
+
for (const ns of namespaces) {
|
|
3291
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
3292
|
+
const m = await storage.getMemoryById(id);
|
|
3293
|
+
if (m) {
|
|
3294
|
+
found = toCandidate(m, ns, 1);
|
|
3295
|
+
break;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
if (found) out.push(found);
|
|
3299
|
+
else missing.push(id);
|
|
3300
|
+
}
|
|
3301
|
+
if (missing.length > 0) {
|
|
3302
|
+
throw new CorrectionContractError(`target memory not found: ${missing[0]}`);
|
|
3303
|
+
}
|
|
3304
|
+
return out;
|
|
3305
|
+
}
|
|
3306
|
+
async function expandEntityNeighbors(wiring, seedIds, namespaces, limit) {
|
|
3307
|
+
if (seedIds.length === 0 || limit <= 0 || namespaces.length === 0) return [];
|
|
3308
|
+
const out = [];
|
|
3309
|
+
const seen = new Set(seedIds);
|
|
3310
|
+
const seedRefs = /* @__PURE__ */ new Set();
|
|
3311
|
+
for (const ns of namespaces) {
|
|
3312
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
3313
|
+
const all = await storage.readAllMemories();
|
|
3314
|
+
for (const m of all) {
|
|
3315
|
+
if (seedIds.includes(m.frontmatter.id) && m.frontmatter.entityRef) {
|
|
3316
|
+
seedRefs.add(m.frontmatter.entityRef);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (seedRefs.size === 0) return [];
|
|
3321
|
+
for (const ns of namespaces) {
|
|
3322
|
+
if (out.length >= limit) break;
|
|
3323
|
+
const storage = await wiring.orchestrator.getStorage(ns);
|
|
3324
|
+
const all = await storage.readAllMemories();
|
|
3325
|
+
for (const m of all) {
|
|
3326
|
+
if (out.length >= limit) break;
|
|
3327
|
+
if (seen.has(m.frontmatter.id)) continue;
|
|
3328
|
+
if (!isEligibleCorrectionCandidate(m)) continue;
|
|
3329
|
+
if (m.frontmatter.entityRef && seedRefs.has(m.frontmatter.entityRef)) {
|
|
3330
|
+
out.push(toCandidate(m, ns, 0.5));
|
|
3331
|
+
seen.add(m.frontmatter.id);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return out;
|
|
3336
|
+
}
|
|
3337
|
+
async function classifyAndDraft(wiring, text, candidates) {
|
|
3338
|
+
try {
|
|
3339
|
+
if (!wiring.llmComplete) {
|
|
3340
|
+
return fallbackClassification(candidates, "no LLM client available");
|
|
3341
|
+
}
|
|
3342
|
+
const user = buildClassifyPrompt(text, candidates);
|
|
3343
|
+
const raw = await wiring.llmComplete({ system: CLASSIFY_SYSTEM_PROMPT, user });
|
|
3344
|
+
return parseClassifyResponse(raw, candidates);
|
|
3345
|
+
} catch (err) {
|
|
3346
|
+
return fallbackClassification(candidates, `LLM unavailable: ${errMsg2(err)}`);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
async function renderCorrectionDiff(_wiring, candidates, actions) {
|
|
3350
|
+
const lines = [];
|
|
3351
|
+
for (const action of actions) {
|
|
3352
|
+
const id = action.kind === "supersede" ? action.loserId : action.kind === "edit" || action.kind === "retract" || action.kind === "rescope" ? action.memoryId : null;
|
|
3353
|
+
const candidate = id ? candidates.find((c) => c.memoryId === id) : null;
|
|
3354
|
+
const before = candidate?.content ?? "(new)";
|
|
3355
|
+
const after = describeAfterState(action);
|
|
3356
|
+
lines.push(`--- ${id ?? "redaction-rule"} (${action.kind})`);
|
|
3357
|
+
lines.push(`- ${before.slice(0, 120)}`);
|
|
3358
|
+
lines.push(`+ ${after.slice(0, 120)}`);
|
|
3359
|
+
}
|
|
3360
|
+
return lines.join("\n");
|
|
3361
|
+
}
|
|
3362
|
+
function describeAfterState(action) {
|
|
3363
|
+
switch (action.kind) {
|
|
3364
|
+
case "supersede":
|
|
3365
|
+
return action.replacement?.content ?? "(superseded without replacement)";
|
|
3366
|
+
case "edit":
|
|
3367
|
+
return action.patch;
|
|
3368
|
+
case "retract":
|
|
3369
|
+
return "(retracted + tombstoned)";
|
|
3370
|
+
case "rescope":
|
|
3371
|
+
return `(moved to namespace '${action.toNamespace}')`;
|
|
3372
|
+
case "redaction_rule":
|
|
3373
|
+
return `(redaction rule persisted for pattern '${action.pattern}')`;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
function makeExecutorDeps(wiring, opts) {
|
|
3377
|
+
return {
|
|
3378
|
+
getMemory: async (namespace, memoryId) => getExecutorMemory(wiring, namespace, memoryId),
|
|
3379
|
+
writeReplacement: async (namespace, draft) => writeReplacementMemory(wiring, namespace, draft),
|
|
3380
|
+
applyEdit: async (namespace, memoryId, patch) => applyEditMemory(wiring, namespace, memoryId, patch),
|
|
3381
|
+
retireMemory: async (namespace, memoryId, retireOpts) => retireMemoryFn(wiring, namespace, memoryId, retireOpts),
|
|
3382
|
+
rescopeMemory: async (namespace, memoryId, toNamespace) => rescopeMemoryFn(wiring, namespace, memoryId, toNamespace),
|
|
3383
|
+
appendTombstone: async (namespace, input) => appendTombstoneFn(wiring, namespace, input),
|
|
3384
|
+
registerRedactionRule: async (namespace, pattern) => registerRedactionRuleFn(wiring, namespace, pattern),
|
|
3385
|
+
appendAuditRecord: async (namespace, record) => appendAuditRecordFn(wiring, namespace, record),
|
|
3386
|
+
propagate: async (namespace, touchedMemoryIds) => propagateFn(wiring, namespace, touchedMemoryIds),
|
|
3387
|
+
biTemporalEnabled: opts.biTemporalEnabled,
|
|
3388
|
+
now: () => /* @__PURE__ */ new Date()
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
async function getExecutorMemory(wiring, namespace, memoryId) {
|
|
3392
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3393
|
+
const m = await storage.getMemoryById(memoryId);
|
|
3394
|
+
if (!m) return null;
|
|
3395
|
+
return toExecutorMemory(m);
|
|
3396
|
+
}
|
|
3397
|
+
async function writeReplacementMemory(wiring, namespace, draft) {
|
|
3398
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3399
|
+
const { id, tombstoneBlocked } = await storage.writeMemory(
|
|
3400
|
+
draft.category ?? "fact",
|
|
3401
|
+
draft.content,
|
|
3402
|
+
{
|
|
3403
|
+
source: "correction",
|
|
3404
|
+
confidence: draft.confidence ?? 0.9,
|
|
3405
|
+
tags: draft.tags ?? [],
|
|
3406
|
+
...draft.entityRef ? { entityRef: draft.entityRef } : {},
|
|
3407
|
+
...draft.validAt ? { validAt: draft.validAt } : {},
|
|
3408
|
+
...draft.observedAt ? { observedAt: draft.observedAt } : {},
|
|
3409
|
+
...draft.structuredAttributes ? { structuredAttributes: draft.structuredAttributes } : {},
|
|
3410
|
+
...draft.supersedes ? { supersedes: draft.supersedes } : {}
|
|
3411
|
+
}
|
|
3412
|
+
);
|
|
3413
|
+
if (tombstoneBlocked) {
|
|
3414
|
+
throw new CorrectionContractError(
|
|
3415
|
+
`correction replacement was tombstone-blocked (pending_review ${id}) \u2014 cannot supersede with a non-active replacement`
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3418
|
+
return id;
|
|
3419
|
+
}
|
|
3420
|
+
async function applyEditMemory(wiring, namespace, memoryId, patch) {
|
|
3421
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3422
|
+
const existing = await storage.getMemoryById(memoryId);
|
|
3423
|
+
if (!existing) throw new CorrectionContractError(`memory not found for edit: ${memoryId}`);
|
|
3424
|
+
const contentHashForPatch = existing.frontmatter.category === "fact" ? computeContentHash(sanitizeMemoryContent(patch).text) : void 0;
|
|
3425
|
+
await storage.writeMemoryFrontmatter(
|
|
3426
|
+
{ ...existing, content: patch },
|
|
3427
|
+
{
|
|
3428
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3429
|
+
...contentHashForPatch ? { contentHash: contentHashForPatch } : {}
|
|
3430
|
+
}
|
|
3431
|
+
);
|
|
3432
|
+
return memoryId;
|
|
3433
|
+
}
|
|
3434
|
+
async function retireMemoryFn(wiring, namespace, memoryId, opts) {
|
|
3435
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3436
|
+
const memory = await storage.getMemoryById(memoryId);
|
|
3437
|
+
if (!memory) throw new CorrectionContractError(`memory not found for retire: ${memoryId}`);
|
|
3438
|
+
const storageStatus = opts.status === "retracted" ? "forgotten" : "superseded";
|
|
3439
|
+
await storage.writeMemoryFrontmatter(memory, {
|
|
3440
|
+
status: storageStatus,
|
|
3441
|
+
...opts.supersededBy ? { supersededBy: opts.supersededBy } : {},
|
|
3442
|
+
...opts.status === "superseded" ? { supersededAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
|
|
3443
|
+
// validUntil is the bi-temporal end; absent when the gate is off.
|
|
3444
|
+
...opts.validUntil ? { invalid_at: opts.validUntil } : {}
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
async function rescopeMemoryFn(wiring, namespace, memoryId, toNamespace) {
|
|
3448
|
+
const sourceStorage = await wiring.orchestrator.getStorage(namespace);
|
|
3449
|
+
const memory = await sourceStorage.getMemoryById(memoryId);
|
|
3450
|
+
if (!memory) throw new CorrectionContractError(`memory not found for rescope: ${memoryId}`);
|
|
3451
|
+
if (memory.frontmatter.status && memory.frontmatter.status !== "active") {
|
|
3452
|
+
throw new CorrectionContractError(
|
|
3453
|
+
`cannot rescope memory ${memoryId}: source is ${memory.frontmatter.status}, not active`
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
const destStorage = await wiring.orchestrator.getStorage(toNamespace);
|
|
3457
|
+
const fm = memory.frontmatter;
|
|
3458
|
+
const destContent = fm.structuredAttributes ? stripAttributesSuffix(memory.content) : memory.content;
|
|
3459
|
+
const { id: destId, tombstoneBlocked: destBlocked } = await destStorage.writeMemory(fm.category, destContent, {
|
|
3460
|
+
source: `correction:rescope:${namespace}`,
|
|
3461
|
+
...typeof fm.confidence === "number" ? { confidence: fm.confidence } : {},
|
|
3462
|
+
...Array.isArray(fm.tags) ? { tags: fm.tags } : {},
|
|
3463
|
+
...fm.entityRef ? { entityRef: fm.entityRef } : {},
|
|
3464
|
+
...fm.structuredAttributes ? { structuredAttributes: fm.structuredAttributes } : {},
|
|
3465
|
+
...fm.valid_at ? { validAt: fm.valid_at } : {},
|
|
3466
|
+
...fm.observedAt ? { observedAt: fm.observedAt } : {},
|
|
3467
|
+
...fm.memoryKind ? { memoryKind: fm.memoryKind } : {},
|
|
3468
|
+
...Array.isArray(fm.links) ? { links: fm.links } : {},
|
|
3469
|
+
...fm.intentGoal ? { intentGoal: fm.intentGoal } : {}
|
|
3470
|
+
});
|
|
3471
|
+
if (destBlocked) {
|
|
3472
|
+
throw new CorrectionContractError(
|
|
3473
|
+
`rescope of ${memoryId} into "${toNamespace}" was tombstone-blocked (pending_review ${destId}) \u2014 keeping source active`
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
try {
|
|
3477
|
+
await sourceStorage.writeMemoryFrontmatter(memory, {
|
|
3478
|
+
status: "archived",
|
|
3479
|
+
archivedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3480
|
+
});
|
|
3481
|
+
} catch (err) {
|
|
3482
|
+
try {
|
|
3483
|
+
const destMem = await destStorage.getMemoryById(destId);
|
|
3484
|
+
if (destMem) {
|
|
3485
|
+
await destStorage.writeMemoryFrontmatter(destMem, {
|
|
3486
|
+
status: "archived",
|
|
3487
|
+
archivedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
} catch {
|
|
3491
|
+
}
|
|
3492
|
+
throw err;
|
|
3493
|
+
}
|
|
3494
|
+
return destId;
|
|
3495
|
+
}
|
|
3496
|
+
async function appendTombstoneFn(wiring, namespace, input) {
|
|
3497
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3498
|
+
const enabled = typeof storage.isTombstonesEnabled === "function" ? storage.isTombstonesEnabled() : true;
|
|
3499
|
+
const keys = input.supersessionKeys && input.supersessionKeys.length > 0 ? input.supersessionKeys : input.supersessionKey ? [input.supersessionKey] : [void 0];
|
|
3500
|
+
const writtenIds = [];
|
|
3501
|
+
let firstId = null;
|
|
3502
|
+
for (const key of keys) {
|
|
3503
|
+
const result = await storage.appendTombstone({
|
|
3504
|
+
reason: input.reason,
|
|
3505
|
+
createdBy: "user_correction",
|
|
3506
|
+
sourceMemoryId: input.sourceMemoryId,
|
|
3507
|
+
rawContent: input.rawContent,
|
|
3508
|
+
...input.entityRef ? { entityRef: input.entityRef } : {},
|
|
3509
|
+
...key ? { supersessionKey: key } : {},
|
|
3510
|
+
...input.contentHash ? { contentHash: input.contentHash } : {}
|
|
3511
|
+
});
|
|
3512
|
+
if (result === null && enabled) {
|
|
3513
|
+
for (const id of writtenIds) {
|
|
3514
|
+
try {
|
|
3515
|
+
await storage.revokeTombstone(id, "user_correction");
|
|
3516
|
+
} catch {
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
throw new CorrectionContractError(
|
|
3520
|
+
`tombstone persistence failed for memory ${input.sourceMemoryId} (tombstones enabled but store returned null \u2014 I/O error swallowed)`
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
if (firstId === null) firstId = result;
|
|
3524
|
+
if (result) writtenIds.push(result);
|
|
3525
|
+
}
|
|
3526
|
+
return firstId;
|
|
3527
|
+
}
|
|
3528
|
+
async function registerRedactionRuleFn(wiring, namespace, pattern) {
|
|
3529
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3530
|
+
const dir = path5.join(storage.dir, "state", "corrections", "redaction-rules");
|
|
3531
|
+
await mkdir3(dir, { recursive: true });
|
|
3532
|
+
const slugBase = pattern.replace(/[^a-zA-Z0-9]+/g, "-").slice(0, 48) || "rule";
|
|
3533
|
+
const patternHash = createHash2("sha256").update(pattern).digest("hex").slice(0, 16);
|
|
3534
|
+
const slug = `${slugBase}-${patternHash}`;
|
|
3535
|
+
await writeFile3(
|
|
3536
|
+
path5.join(dir, `${slug}.json`),
|
|
3537
|
+
`${JSON.stringify({ pattern, namespace, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
3538
|
+
`,
|
|
3539
|
+
"utf-8"
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
async function appendAuditRecordFn(wiring, namespace, record) {
|
|
3543
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3544
|
+
const { id } = await storage.writeMemory("correction", buildAuditBody(record), {
|
|
3545
|
+
source: "correction-contract",
|
|
3546
|
+
confidence: 1,
|
|
3547
|
+
tags: ["correction-audit", `plan:${record.planId}`, `classification:${record.classification}`]
|
|
3548
|
+
});
|
|
3549
|
+
return id;
|
|
3550
|
+
}
|
|
3551
|
+
function buildAuditBody(record) {
|
|
3552
|
+
const sensitive = record.classification === "never_store" || record.outcome.results.some((r) => r.action.kind === "redaction_rule");
|
|
3553
|
+
const safeRequest = sensitive ? "[redacted \u2014 never-store/redaction correction text withheld from the audit trail]" : record.requestText.slice(0, 200);
|
|
3554
|
+
const lines = [
|
|
3555
|
+
`Correction plan ${record.planId} applied (${record.outcome.status}).`,
|
|
3556
|
+
"",
|
|
3557
|
+
`Request: ${safeRequest}`,
|
|
3558
|
+
`Classification: ${record.classification}`,
|
|
3559
|
+
`Applied at: ${record.outcome.appliedAt}`,
|
|
3560
|
+
"",
|
|
3561
|
+
"Actions:"
|
|
3562
|
+
];
|
|
3563
|
+
for (const r of record.outcome.results) {
|
|
3564
|
+
const withhold = r.action.kind === "redaction_rule" || record.classification === "never_store";
|
|
3565
|
+
const errPart = r.error ? withhold ? " (error withheld)" : ` (${r.error})` : "";
|
|
3566
|
+
lines.push(` - ${r.action.kind}: ${r.status}${errPart}`);
|
|
3567
|
+
}
|
|
3568
|
+
return lines.join("\n");
|
|
3569
|
+
}
|
|
3570
|
+
async function propagateFn(wiring, namespace, touchedMemoryIds) {
|
|
3571
|
+
for (const id of touchedMemoryIds) {
|
|
3572
|
+
try {
|
|
3573
|
+
const storage = await wiring.orchestrator.getStorage(namespace);
|
|
3574
|
+
const orchestrator = wiring.orchestrator;
|
|
3575
|
+
if (typeof orchestrator.indexPersistedMemory === "function") {
|
|
3576
|
+
await orchestrator.indexPersistedMemory(storage, id);
|
|
3577
|
+
}
|
|
3578
|
+
} catch {
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
1965
3581
|
}
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
3582
|
+
function toCandidate(m, namespace, score) {
|
|
3583
|
+
return {
|
|
3584
|
+
memoryId: m.frontmatter.id,
|
|
3585
|
+
path: m.path,
|
|
3586
|
+
content: m.content,
|
|
3587
|
+
excerpt: m.content.slice(0, 160),
|
|
3588
|
+
...m.frontmatter.entityRef ? { entityRef: m.frontmatter.entityRef } : {},
|
|
3589
|
+
score
|
|
3590
|
+
// namespace is implicit (the planner scopes by namespace); keep it on the
|
|
3591
|
+
// candidate for diff rendering if needed.
|
|
3592
|
+
};
|
|
1970
3593
|
}
|
|
1971
|
-
function
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
3594
|
+
function isEligibleCorrectionCandidate(m) {
|
|
3595
|
+
if (m.frontmatter.status && m.frontmatter.status !== "active") return false;
|
|
3596
|
+
if (typeof m.frontmatter.archivedAt === "string" && m.frontmatter.archivedAt.length > 0) return false;
|
|
3597
|
+
return true;
|
|
3598
|
+
}
|
|
3599
|
+
function toExecutorMemory(m) {
|
|
3600
|
+
const fm = m.frontmatter;
|
|
3601
|
+
const rawBody = fm.structuredAttributes ? stripAttributesSuffix(m.content) : m.content;
|
|
3602
|
+
const supersessionKeys = supersessionKeysForFact({
|
|
3603
|
+
entityRef: fm.entityRef,
|
|
3604
|
+
structuredAttributes: fm.structuredAttributes
|
|
3605
|
+
});
|
|
3606
|
+
return {
|
|
3607
|
+
memoryId: fm.id,
|
|
3608
|
+
content: m.content,
|
|
3609
|
+
category: fm.category,
|
|
3610
|
+
rawContent: rawBody,
|
|
3611
|
+
...fm.entityRef ? { entityRef: fm.entityRef } : {},
|
|
3612
|
+
...supersessionKeys.length > 0 ? { supersessionKeys } : {},
|
|
3613
|
+
...supersessionKeys.length > 0 ? { supersessionKey: supersessionKeys[0] } : {},
|
|
3614
|
+
...typeof fm.contentHash === "string" && fm.contentHash.length > 0 ? { contentHash: fm.contentHash } : {}
|
|
3615
|
+
};
|
|
1976
3616
|
}
|
|
1977
|
-
function
|
|
1978
|
-
|
|
1979
|
-
|
|
3617
|
+
function isCorrectionFeatureEnabled(config) {
|
|
3618
|
+
if (typeof resolveRecallAuxiliaryCapabilities(config).correction === "boolean") return resolveRecallAuxiliaryCapabilities(config).correction;
|
|
3619
|
+
const nested = config.correction;
|
|
3620
|
+
if (nested && typeof nested.enabled === "boolean") return nested.enabled;
|
|
3621
|
+
if (nested && typeof nested.enabled === "string") return nested.enabled === "true" || nested.enabled === "1";
|
|
3622
|
+
return readCorrectionFlag(config, "enabled", true);
|
|
3623
|
+
}
|
|
3624
|
+
function readCorrectionFlag(config, key, fallback) {
|
|
3625
|
+
const nested = config.correction;
|
|
3626
|
+
if (nested && typeof nested[key] === "boolean") return nested[key];
|
|
3627
|
+
if (nested && typeof nested[key] === "string") return nested[key] === "true" || nested[key] === "1";
|
|
3628
|
+
const raw = config[`correction${capitalize(key)}`];
|
|
3629
|
+
if (typeof raw === "boolean") return raw;
|
|
3630
|
+
if (typeof raw === "string") return raw === "true" || raw === "1";
|
|
3631
|
+
return fallback;
|
|
3632
|
+
}
|
|
3633
|
+
function readCorrectionNumber(config, key, fallback) {
|
|
3634
|
+
const nested = config.correction;
|
|
3635
|
+
if (nested) {
|
|
3636
|
+
const nv = nested[key];
|
|
3637
|
+
if (typeof nv === "number" && Number.isFinite(nv)) return nv;
|
|
3638
|
+
if (typeof nv === "string") {
|
|
3639
|
+
const nn = Number(nv);
|
|
3640
|
+
if (Number.isFinite(nn)) return nn;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
const raw = config[`correction${capitalize(key)}`];
|
|
3644
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
3645
|
+
if (typeof raw === "string") {
|
|
3646
|
+
const n = Number(raw);
|
|
3647
|
+
if (Number.isFinite(n)) return n;
|
|
3648
|
+
}
|
|
3649
|
+
return fallback;
|
|
3650
|
+
}
|
|
3651
|
+
function capitalize(s) {
|
|
3652
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3653
|
+
}
|
|
3654
|
+
function errMsg2(err) {
|
|
3655
|
+
if (err instanceof Error) return err.message;
|
|
3656
|
+
return String(err);
|
|
3657
|
+
}
|
|
3658
|
+
var CLASSIFY_SYSTEM_PROMPT = `You classify memory corrections and draft per-memory actions.
|
|
3659
|
+
|
|
3660
|
+
Given a correction statement and candidate memories, respond with a JSON object:
|
|
3661
|
+
{
|
|
3662
|
+
"classification": "wrong" | "outdated" | "incomplete" | "wrong_scope" | "never_store",
|
|
3663
|
+
"confidence": <number 0..1>,
|
|
3664
|
+
"actions": [<one or more correction actions>],
|
|
3665
|
+
"relevance": [{"memoryId": "<id>", "why": "<one short sentence>"}]
|
|
1980
3666
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
3667
|
+
|
|
3668
|
+
Action shapes:
|
|
3669
|
+
- {"kind":"supersede","loserId":"<id>","replacement":{"content":"<new fact>"}}
|
|
3670
|
+
- {"kind":"edit","memoryId":"<id>","patch":"<new full content>"}
|
|
3671
|
+
- {"kind":"retract","memoryId":"<id>"}
|
|
3672
|
+
- {"kind":"rescope","memoryId":"<id>","toNamespace":"<ns>"}
|
|
3673
|
+
- {"kind":"redaction_rule","pattern":"<bounded literal or regex>"}
|
|
3674
|
+
|
|
3675
|
+
Only emit actions you are confident in. If uncertain, return confidence < 0.5 and few actions.`;
|
|
3676
|
+
function buildClassifyPrompt(text, candidates) {
|
|
3677
|
+
const lines = [
|
|
3678
|
+
`Correction: ${text}`,
|
|
3679
|
+
"",
|
|
3680
|
+
"Candidate memories:"
|
|
3681
|
+
];
|
|
3682
|
+
for (const c of candidates.slice(0, 20)) {
|
|
3683
|
+
lines.push(`[${c.memoryId}] ${c.excerpt}`);
|
|
3684
|
+
}
|
|
3685
|
+
lines.push("", "Respond with the JSON object only.");
|
|
3686
|
+
return lines.join("\n");
|
|
1985
3687
|
}
|
|
1986
|
-
|
|
3688
|
+
function parseClassifyResponse(raw, candidates) {
|
|
3689
|
+
let parsed;
|
|
1987
3690
|
try {
|
|
1988
|
-
|
|
1989
|
-
const parsed = JSON.parse(raw);
|
|
1990
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1991
|
-
const obj = parsed;
|
|
1992
|
-
const head = typeof obj.head === "string" ? obj.head : null;
|
|
1993
|
-
const at = typeof obj.at === "string" ? obj.at : null;
|
|
1994
|
-
if (!head || !at) return null;
|
|
1995
|
-
return { head, at };
|
|
3691
|
+
parsed = JSON.parse(raw);
|
|
1996
3692
|
} catch {
|
|
1997
|
-
return
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
async function writeLastSeenState(statePath, state) {
|
|
2001
|
-
if (!state.head || !state.at) {
|
|
2002
|
-
throw new Error(
|
|
2003
|
-
`writeLastSeenState: invalid state (head=${JSON.stringify(state.head)}, at=${JSON.stringify(state.at)})`
|
|
2004
|
-
);
|
|
2005
|
-
}
|
|
2006
|
-
const dir = path3.dirname(statePath);
|
|
2007
|
-
await mkdir(dir, { recursive: true });
|
|
2008
|
-
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2009
|
-
await writeFile(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
2010
|
-
await rename(tmp, statePath);
|
|
2011
|
-
}
|
|
2012
|
-
function resolveSlice(repoRoot, sinceHead, invoker) {
|
|
2013
|
-
const headResult = invoker(repoRoot, ["rev-parse", "HEAD"]);
|
|
2014
|
-
if (headResult.exitCode !== 0) {
|
|
2015
|
-
return {
|
|
2016
|
-
ok: false,
|
|
2017
|
-
code: headResult.stdout.trim().length === 0 ? "no_head" : "git_failed",
|
|
2018
|
-
detail: `rev-parse HEAD exited ${headResult.exitCode}`
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
const currentHead = headResult.stdout.trim();
|
|
2022
|
-
if (!currentHead) {
|
|
2023
|
-
return { ok: false, code: "no_head", detail: "rev-parse HEAD returned empty" };
|
|
2024
|
-
}
|
|
2025
|
-
if (!sinceHead) {
|
|
2026
|
-
return { ok: true, slice: { commits: [], touchedFiles: [], currentHead } };
|
|
3693
|
+
return fallbackClassification(candidates, "LLM returned non-JSON response");
|
|
2027
3694
|
}
|
|
2028
|
-
if (
|
|
2029
|
-
return
|
|
3695
|
+
if (!parsed || typeof parsed !== "object") {
|
|
3696
|
+
return fallbackClassification(candidates, "LLM returned non-object response");
|
|
2030
3697
|
}
|
|
2031
|
-
const
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
3698
|
+
const obj = parsed;
|
|
3699
|
+
const classification = isClassification(obj.classification) ? obj.classification : "outdated";
|
|
3700
|
+
const confidence = typeof obj.confidence === "number" ? Math.min(1, Math.max(0, obj.confidence)) : 0.5;
|
|
3701
|
+
const rawActions = Array.isArray(obj.actions) ? obj.actions : [];
|
|
3702
|
+
const actions = [];
|
|
3703
|
+
const warnings = [];
|
|
3704
|
+
for (const rawAction of rawActions) {
|
|
3705
|
+
try {
|
|
3706
|
+
validateCorrectionAction(rawAction);
|
|
3707
|
+
actions.push(rawAction);
|
|
3708
|
+
} catch (err) {
|
|
3709
|
+
warnings.push(`dropped malformed action: ${errMsg2(err)}`);
|
|
2042
3710
|
}
|
|
2043
|
-
return {
|
|
2044
|
-
ok: false,
|
|
2045
|
-
code: "git_failed",
|
|
2046
|
-
detail: "git log exited " + logResult.exitCode + " (transient \u2014 state marker preserved)"
|
|
2047
|
-
};
|
|
2048
3711
|
}
|
|
2049
|
-
const
|
|
2050
|
-
|
|
3712
|
+
const relevance = Array.isArray(obj.relevance) ? obj.relevance.filter((r) => !!r && typeof r === "object").map((r) => ({
|
|
3713
|
+
memoryId: typeof r.memoryId === "string" ? r.memoryId : "",
|
|
3714
|
+
why: typeof r.why === "string" ? r.why : ""
|
|
3715
|
+
})).filter((r) => r.memoryId.length > 0) : [];
|
|
3716
|
+
return {
|
|
3717
|
+
classification,
|
|
3718
|
+
confidence,
|
|
3719
|
+
actions,
|
|
3720
|
+
relevance,
|
|
3721
|
+
warnings
|
|
3722
|
+
};
|
|
2051
3723
|
}
|
|
2052
|
-
function
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
3724
|
+
function isClassification(value) {
|
|
3725
|
+
return value === "wrong" || value === "outdated" || value === "incomplete" || value === "wrong_scope" || value === "never_store";
|
|
3726
|
+
}
|
|
3727
|
+
function fallbackClassification(candidates, reason) {
|
|
3728
|
+
return {
|
|
3729
|
+
classification: "outdated",
|
|
3730
|
+
confidence: 0,
|
|
3731
|
+
actions: [],
|
|
3732
|
+
relevance: candidates.map((c) => ({ memoryId: c.memoryId, why: "located for review" })),
|
|
3733
|
+
warnings: [reason],
|
|
3734
|
+
fallback: true
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
// src/correction/passive-correction-detector.ts
|
|
3739
|
+
var PATTERNS = [
|
|
3740
|
+
// ── update: new fact replaces / corrects an old one ──────────────────────
|
|
3741
|
+
{
|
|
3742
|
+
polarity: "update",
|
|
3743
|
+
// "actually, it's X not Y" / "actually we use X now"
|
|
3744
|
+
regex: /\bactually[,\s]+(?:it'?s|we|the|i|our|my|\[m:)/i,
|
|
3745
|
+
confidence: 0.85
|
|
3746
|
+
},
|
|
3747
|
+
{
|
|
3748
|
+
polarity: "update",
|
|
3749
|
+
// "we switched/switching/switched to X"
|
|
3750
|
+
regex: /\bwe\b\s*(?:(?:'re|'ve|are|have)\s+)?(?:switch(?:ed|ing)?|migrat(?:ed|ing)?|mov(?:ed|ing)?|chang(?:ed|ing)?)\s+(?:to|over to|from)\b/i,
|
|
3751
|
+
confidence: 0.9
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
polarity: "update",
|
|
3755
|
+
// "we renamed X to Y" / "i renamed"
|
|
3756
|
+
regex: /\b(?:we|i)\s+renam(?:ed|ing)\b/i,
|
|
3757
|
+
confidence: 0.9
|
|
3758
|
+
},
|
|
3759
|
+
{
|
|
3760
|
+
polarity: "update",
|
|
3761
|
+
// "it's now X" / "the deadline moved to X" / "the date changed to X"
|
|
3762
|
+
regex: /\b(?:it'?s now|the\s+(?:deadline|date|meeting|standup|review)\s+(?:moved|chang(?:ed|ing))?\s*to)\b/i,
|
|
3763
|
+
confidence: 0.85
|
|
3764
|
+
},
|
|
3765
|
+
{
|
|
3766
|
+
polarity: "update",
|
|
3767
|
+
// "no, " at the start of a turn or clause — strong correction signal
|
|
3768
|
+
regex: /(?:^|\.\s+|\n)\s*no[,\s]+(?:we|i|it|the|our|my|that)\b/i,
|
|
3769
|
+
confidence: 0.8
|
|
3770
|
+
},
|
|
3771
|
+
{
|
|
3772
|
+
polarity: "update",
|
|
3773
|
+
// "no longer" / "not anymore" + an activity
|
|
3774
|
+
regex: /\b(?:no longer|not anymore)\b/i,
|
|
3775
|
+
confidence: 0.8
|
|
3776
|
+
},
|
|
3777
|
+
{
|
|
3778
|
+
polarity: "update",
|
|
3779
|
+
// Standalone "outdated" — catches "are both outdated", "is outdated", etc.
|
|
3780
|
+
regex: /\boutdated\b/i,
|
|
3781
|
+
confidence: 0.75
|
|
3782
|
+
},
|
|
3783
|
+
{
|
|
3784
|
+
polarity: "update",
|
|
3785
|
+
// "that's outdated" / "that's old"
|
|
3786
|
+
regex: /\bthat'?s\s+(?:outdated|old|no longer (?:true|correct|accurate))\b/i,
|
|
3787
|
+
confidence: 0.85
|
|
3788
|
+
},
|
|
3789
|
+
// ── retract: user says a fact is simply wrong ────────────────────────────
|
|
3790
|
+
{
|
|
3791
|
+
polarity: "retract",
|
|
3792
|
+
regex: /\bthat'?s\s+(?:wrong|not right|incorrect|not true|false)\b/i,
|
|
3793
|
+
confidence: 0.85
|
|
3794
|
+
},
|
|
3795
|
+
{
|
|
3796
|
+
polarity: "retract",
|
|
3797
|
+
// "we don't use X anymore" / "I don't use X anymore"
|
|
3798
|
+
regex: /\b(?:we|i)\s+don'?t\s+use\s+\S+\s+anymore\b/i,
|
|
3799
|
+
confidence: 0.9
|
|
3800
|
+
},
|
|
3801
|
+
{
|
|
3802
|
+
polarity: "retract",
|
|
3803
|
+
regex: /\bforget about\b/i,
|
|
3804
|
+
confidence: 0.8
|
|
3805
|
+
},
|
|
3806
|
+
// ── stop_storing: user wants the agent to stop a behavior ────────────────
|
|
3807
|
+
{
|
|
3808
|
+
polarity: "stop_storing",
|
|
3809
|
+
regex: /\bstop\s+(?:suggesting|bringing\s+up|recommending|reminding\s+me\s+(?:about)?)\b/i,
|
|
3810
|
+
confidence: 0.9
|
|
3811
|
+
},
|
|
3812
|
+
{
|
|
3813
|
+
polarity: "stop_storing",
|
|
3814
|
+
regex: /\b(?:don'?t|never)\s+(?:mention|suggest|recommend|store|bring\s+up)\b/i,
|
|
3815
|
+
confidence: 0.85
|
|
3816
|
+
}
|
|
3817
|
+
];
|
|
3818
|
+
var HYPOTHETICAL_REGEX = /\b(?:if\s+(?:we|you|i|they)\s+(?:ever|were|could|might|should|decide)|what\s+if|suppose|hypothetically|in\s+(?:theory|principle)|let'?s\s+say|imagine\s+if)\b/i;
|
|
3819
|
+
var THIRD_PARTY_REGEX = /\b(?:tell\s+\w+\s+(?:he|she|they|you)'?re?\s+wrong|he'?s\s+wrong\s+about|she'?s\s+wrong\s+about|they'?re\s+wrong\s+about|correct\s+\w+\s+on)\b/i;
|
|
3820
|
+
var TOOL_OUTPUT_REGEX = /\b(?:the\s+(?:output|result|response|error|log)|your\s+(?:output|result|response|code|answer))\s+(?:is\s+)?(?:wrong|incorrect|bad|off)\b/i;
|
|
3821
|
+
var SELF_RESOLVE_REGEX = /(?:actually|wait|no)[,\s]+(?:never\s*mind|forget\s*(?:that|it)|the\s+(?:original|first)\s+(?:was|is)\s+(?:right|correct)|scratch\s+that|disregard)/i;
|
|
3822
|
+
function extractHandles(text) {
|
|
3823
|
+
const matches = text.match(/\[m:[0-9a-fA-F]{4,}\]/g);
|
|
3824
|
+
return matches ?? [];
|
|
3825
|
+
}
|
|
3826
|
+
function extractTargetHint(text, match) {
|
|
3827
|
+
const matchStart = match.index ?? 0;
|
|
3828
|
+
const start = Math.max(0, matchStart - 20);
|
|
3829
|
+
const end = Math.min(text.length, matchStart + match[0].length + 60);
|
|
3830
|
+
const window = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
3831
|
+
return window.length > 80 ? window.slice(0, 77) + "..." : window;
|
|
3832
|
+
}
|
|
3833
|
+
function extractCorrectedAssertion(text, polarity) {
|
|
3834
|
+
if (polarity === "retract") return "";
|
|
3835
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
3836
|
+
return trimmed.length > 200 ? trimmed.slice(0, 197) + "..." : trimmed;
|
|
3837
|
+
}
|
|
3838
|
+
function isWithinQuotes(text, matchIndex) {
|
|
3839
|
+
let inQuote = false;
|
|
3840
|
+
for (let i = 0; i < matchIndex && i < text.length; i++) {
|
|
3841
|
+
const c = text[i];
|
|
3842
|
+
if (c === '"' || c === "\u201C" || c === "\u201D") {
|
|
3843
|
+
inQuote = !inQuote;
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
return inQuote;
|
|
3847
|
+
}
|
|
3848
|
+
function detectAntiFixture(text) {
|
|
3849
|
+
if (SELF_RESOLVE_REGEX.test(text)) return "self_resolving";
|
|
3850
|
+
if (HYPOTHETICAL_REGEX.test(text)) return "hypothetical";
|
|
3851
|
+
if (THIRD_PARTY_REGEX.test(text)) return "third_party";
|
|
3852
|
+
if (TOOL_OUTPUT_REGEX.test(text)) return "tool_output";
|
|
3853
|
+
return null;
|
|
3854
|
+
}
|
|
3855
|
+
function detectPassiveCorrections(turns) {
|
|
3856
|
+
const results = [];
|
|
3857
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3858
|
+
for (let i = 0; i < turns.length; i++) {
|
|
3859
|
+
const turn = turns[i];
|
|
3860
|
+
if (turn.role !== "user") continue;
|
|
3861
|
+
const content = turn.content?.trim();
|
|
3862
|
+
if (!content) continue;
|
|
3863
|
+
const guard = detectAntiFixture(content);
|
|
3864
|
+
if (guard) continue;
|
|
3865
|
+
const handles = extractHandles(content);
|
|
3866
|
+
for (const pattern of PATTERNS) {
|
|
3867
|
+
const match = pattern.regex.exec(content);
|
|
3868
|
+
if (!match) continue;
|
|
3869
|
+
if (match.index !== void 0 && isWithinQuotes(content, match.index)) {
|
|
3870
|
+
continue;
|
|
2067
3871
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
3872
|
+
const targetHint = extractTargetHint(content, match);
|
|
3873
|
+
const correctedAssertion = extractCorrectedAssertion(content, pattern.polarity);
|
|
3874
|
+
const dedupKey = `${i}:${targetHint}`;
|
|
3875
|
+
if (seen.has(dedupKey)) continue;
|
|
3876
|
+
seen.add(dedupKey);
|
|
3877
|
+
results.push({
|
|
3878
|
+
targetHint,
|
|
3879
|
+
correctedAssertion,
|
|
3880
|
+
polarity: pattern.polarity,
|
|
3881
|
+
handles,
|
|
3882
|
+
confidence: pattern.confidence,
|
|
3883
|
+
sourceExcerpt: content.slice(0, 200),
|
|
3884
|
+
turnIndex: i
|
|
3885
|
+
});
|
|
2070
3886
|
}
|
|
2071
3887
|
}
|
|
2072
|
-
|
|
2073
|
-
return { commits, touchedFiles };
|
|
3888
|
+
return results;
|
|
2074
3889
|
}
|
|
2075
3890
|
|
|
2076
|
-
// src/
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
return config.enabled === true && config.sessionDelta === true && codingContext !== null && codingContext !== void 0;
|
|
3891
|
+
// src/correction/passive-capture.ts
|
|
3892
|
+
function emptyTelemetry() {
|
|
3893
|
+
return { detected: 0, queued: 0, autoApplied: 0, suppressedReasons: {} };
|
|
2080
3894
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
3895
|
+
function normalizeForFingerprint(text) {
|
|
3896
|
+
return text.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3897
|
+
}
|
|
3898
|
+
function correctionFingerprint(bufferKey, correction) {
|
|
3899
|
+
return `${bufferKey}::${correction.polarity}::${normalizeForFingerprint(correction.targetHint)}`;
|
|
3900
|
+
}
|
|
3901
|
+
var AUTO_APPLICABLE_ACTION_KINDS = /* @__PURE__ */ new Set(["supersede", "edit", "retract"]);
|
|
3902
|
+
var AUTO_APPLICABLE_CLASSIFICATIONS = /* @__PURE__ */ new Set(["outdated", "wrong"]);
|
|
3903
|
+
function evaluateAutoApplyGuards(plan, config, isLiveSession) {
|
|
3904
|
+
if (!isLiveSession) return "non_live_session";
|
|
3905
|
+
if (plan.confidence < config.confidenceFloor) return "confidence_below_floor";
|
|
3906
|
+
if (!AUTO_APPLICABLE_CLASSIFICATIONS.has(plan.classification)) {
|
|
3907
|
+
return "classification_not_allowed";
|
|
2087
3908
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
return deltaGet(request, ctx, codingContext);
|
|
3909
|
+
if (plan.affected.length > config.autoApplyMaxAffected) {
|
|
3910
|
+
return "affected_too_large";
|
|
2091
3911
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
const storage = await ctx.resolveStorage(request);
|
|
2095
|
-
const statePath = sessionDeltaStatePath(storage.memoryDir, storage.namespace);
|
|
2096
|
-
const lastSeen = await readLastSeenState(statePath);
|
|
2097
|
-
const repoRoot = codingContext.rootPath ?? codingContext.projectId;
|
|
2098
|
-
const sliceResult = resolveSlice(
|
|
2099
|
-
repoRoot,
|
|
2100
|
-
lastSeen?.head ?? null,
|
|
2101
|
-
ctx.gitInvoker
|
|
2102
|
-
);
|
|
2103
|
-
if (!sliceResult.ok) {
|
|
2104
|
-
log.warn(
|
|
2105
|
-
`coding_delta/get: slice resolution failed (code=${sliceResult.code}): ${sliceResult.detail}`
|
|
2106
|
-
);
|
|
2107
|
-
return {
|
|
2108
|
-
subcommand: "get",
|
|
2109
|
-
ok: false,
|
|
2110
|
-
code: "git_failed",
|
|
2111
|
-
detail: sliceResult.detail
|
|
2112
|
-
};
|
|
3912
|
+
if (plan.actions.length === 0) {
|
|
3913
|
+
return "empty_plan";
|
|
2113
3914
|
}
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
nextState = result.nextState;
|
|
2119
|
-
} else if (result.code === "unreachable_head") {
|
|
2120
|
-
nextState = result.nextState;
|
|
3915
|
+
for (const action of plan.actions) {
|
|
3916
|
+
if (!AUTO_APPLICABLE_ACTION_KINDS.has(action.kind)) {
|
|
3917
|
+
return "disallowed_action_kind";
|
|
3918
|
+
}
|
|
2121
3919
|
}
|
|
2122
|
-
|
|
3920
|
+
return null;
|
|
3921
|
+
}
|
|
3922
|
+
async function capturePassiveCorrections(corrections, ctx, config, deps, dedupState) {
|
|
3923
|
+
const telemetry = emptyTelemetry();
|
|
3924
|
+
const plans = [];
|
|
3925
|
+
telemetry.detected = corrections.length;
|
|
3926
|
+
for (const correction of corrections) {
|
|
3927
|
+
const fp = correctionFingerprint(ctx.bufferKey, correction);
|
|
3928
|
+
if (dedupState.has(fp)) {
|
|
3929
|
+
continue;
|
|
3930
|
+
}
|
|
3931
|
+
let targetIds;
|
|
3932
|
+
if (correction.handles.length > 0 && deps.resolveHandle && ctx.sessionKey) {
|
|
3933
|
+
const resolved = [];
|
|
3934
|
+
for (const handle of correction.handles) {
|
|
3935
|
+
const id = deps.resolveHandle(handle, ctx.sessionKey);
|
|
3936
|
+
if (id) resolved.push(id);
|
|
3937
|
+
}
|
|
3938
|
+
if (resolved.length > 0) targetIds = resolved;
|
|
3939
|
+
}
|
|
3940
|
+
const request = {
|
|
3941
|
+
text: correction.correctedAssertion || correction.sourceExcerpt,
|
|
3942
|
+
...targetIds ? { targetIds } : {},
|
|
3943
|
+
...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {},
|
|
3944
|
+
...ctx.principal ? { principal: ctx.principal } : {},
|
|
3945
|
+
namespace: ctx.namespace
|
|
3946
|
+
};
|
|
3947
|
+
let plan;
|
|
2123
3948
|
try {
|
|
2124
|
-
await
|
|
3949
|
+
plan = await deps.planCorrection(request);
|
|
2125
3950
|
} catch (err) {
|
|
2126
3951
|
log.warn(
|
|
2127
|
-
`
|
|
3952
|
+
`passive-correction: planning failed for "${correction.targetHint.slice(0, 60)}": ${err instanceof Error ? err.message : String(err)}`
|
|
2128
3953
|
);
|
|
3954
|
+
continue;
|
|
2129
3955
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
if (
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
3956
|
+
plans.push(plan);
|
|
3957
|
+
dedupState.add(fp);
|
|
3958
|
+
if (config.mode === "queue") {
|
|
3959
|
+
telemetry.queued += 1;
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
const suppression = evaluateAutoApplyGuards(plan, config, ctx.isLiveSession);
|
|
3963
|
+
if (suppression) {
|
|
3964
|
+
telemetry.suppressedReasons[suppression] = (telemetry.suppressedReasons[suppression] ?? 0) + 1;
|
|
3965
|
+
telemetry.queued += 1;
|
|
3966
|
+
continue;
|
|
3967
|
+
}
|
|
3968
|
+
try {
|
|
3969
|
+
const outcome = await deps.applyCorrection(plan.planId, {
|
|
3970
|
+
confirm: true,
|
|
3971
|
+
namespace: ctx.namespace,
|
|
3972
|
+
...ctx.sessionKey ? { sessionKey: ctx.sessionKey } : {},
|
|
3973
|
+
...ctx.principal ? { principal: ctx.principal } : {}
|
|
3974
|
+
});
|
|
3975
|
+
if (outcome.status !== "applied") {
|
|
3976
|
+
log.warn(
|
|
3977
|
+
`passive-correction: plan ${plan.planId} applied partially \u2014 some actions failed (see audit record)`
|
|
3978
|
+
);
|
|
3979
|
+
continue;
|
|
3980
|
+
}
|
|
3981
|
+
telemetry.autoApplied += 1;
|
|
3982
|
+
try {
|
|
3983
|
+
const dir = await deps.storageDir(ctx.namespace);
|
|
3984
|
+
await enqueuePassiveCorrectionNotification(dir, {
|
|
3985
|
+
planId: plan.planId,
|
|
3986
|
+
summary: buildNotificationSummary(plan),
|
|
3987
|
+
undoCommand: `auto-applied (plan ${plan.planId})`,
|
|
3988
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3989
|
+
});
|
|
3990
|
+
} catch (err) {
|
|
3991
|
+
log.debug(`passive-correction: notification enqueue failed (non-fatal): ${err}`);
|
|
3992
|
+
}
|
|
3993
|
+
} catch (err) {
|
|
3994
|
+
log.warn(
|
|
3995
|
+
`passive-correction: auto-apply failed for plan ${plan.planId}: ${err instanceof Error ? err.message : String(err)} \u2014 plan left in queue`
|
|
3996
|
+
);
|
|
3997
|
+
telemetry.queued += 1;
|
|
2140
3998
|
}
|
|
2141
|
-
return {
|
|
2142
|
-
subcommand: "get",
|
|
2143
|
-
ok: false,
|
|
2144
|
-
code: result.code,
|
|
2145
|
-
detail: result.detail
|
|
2146
|
-
};
|
|
2147
3999
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
return {
|
|
2155
|
-
subcommand: "get",
|
|
2156
|
-
ok: true,
|
|
2157
|
-
kind: "changed",
|
|
2158
|
-
delta: {
|
|
2159
|
-
commits: result.delta.commits,
|
|
2160
|
-
touchedFiles: result.delta.touchedFiles,
|
|
2161
|
-
totalCommits: result.delta.totalCommits,
|
|
2162
|
-
totalTouchedFiles: result.delta.totalTouchedFiles,
|
|
2163
|
-
summaryLine: result.delta.summaryLine
|
|
2164
|
-
},
|
|
2165
|
-
nextState: result.nextState
|
|
2166
|
-
};
|
|
4000
|
+
return { telemetry, plans };
|
|
4001
|
+
}
|
|
4002
|
+
function buildNotificationSummary(plan) {
|
|
4003
|
+
const action = plan.actions[0];
|
|
4004
|
+
if (!action) {
|
|
4005
|
+
return plan.request.text.slice(0, 80);
|
|
2167
4006
|
}
|
|
4007
|
+
const target = action.kind === "supersede" ? action.loserId : action.kind === "edit" || action.kind === "retract" || action.kind === "rescope" ? action.memoryId : "pattern";
|
|
4008
|
+
return `${plan.classification}: ${plan.request.text.slice(0, 60)}${plan.request.text.length > 60 ? "..." : ""} (${target})`;
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
// src/procedural/procedure-miner.ts
|
|
4012
|
+
import { createHash as createHash3 } from "crypto";
|
|
4013
|
+
import { mkdir as mkdir4, rm, stat } from "fs/promises";
|
|
4014
|
+
import path6 from "path";
|
|
4015
|
+
|
|
4016
|
+
// src/procedural/reinforcement-core.ts
|
|
4017
|
+
function clusterByKey(items, keyFn) {
|
|
4018
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
4019
|
+
for (const item of items) {
|
|
4020
|
+
const key = keyFn(item);
|
|
4021
|
+
if (typeof key !== "string") {
|
|
4022
|
+
throw new TypeError(
|
|
4023
|
+
`clusterByKey: keyFn must return a string, got ${key === null ? "null" : typeof key}`
|
|
4024
|
+
);
|
|
4025
|
+
}
|
|
4026
|
+
const existing = clusters.get(key);
|
|
4027
|
+
if (existing) {
|
|
4028
|
+
existing.push(item);
|
|
4029
|
+
} else {
|
|
4030
|
+
clusters.set(key, [item]);
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
return clusters;
|
|
2168
4034
|
}
|
|
2169
4035
|
|
|
2170
4036
|
// src/procedural/procedure-miner.ts
|
|
2171
|
-
import { createHash as createHash2 } from "crypto";
|
|
2172
|
-
import { mkdir as mkdir2, rm, stat } from "fs/promises";
|
|
2173
|
-
import path4 from "path";
|
|
2174
4037
|
var PROCEDURE_CLUSTER_ATTR_MAX = 500;
|
|
2175
4038
|
var PROCEDURE_CLUSTER_LOCK_STALE_MS = 12e4;
|
|
2176
4039
|
var PROCEDURE_CLUSTER_LOCK_TIMEOUT_MS = 3e4;
|
|
@@ -2181,7 +4044,7 @@ function clusterKey(record) {
|
|
|
2181
4044
|
return `${goal}|${refs.join(",")}`;
|
|
2182
4045
|
}
|
|
2183
4046
|
function clusterKeyHash(cluster) {
|
|
2184
|
-
return
|
|
4047
|
+
return createHash3("sha256").update(cluster).digest("hex");
|
|
2185
4048
|
}
|
|
2186
4049
|
function isErrnoCode(error, code) {
|
|
2187
4050
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
@@ -2190,14 +4053,14 @@ function sleep(ms) {
|
|
|
2190
4053
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2191
4054
|
}
|
|
2192
4055
|
async function acquireClusterWriteLock(memoryDir, clusterHash) {
|
|
2193
|
-
const lockRoot =
|
|
2194
|
-
const lockDir =
|
|
2195
|
-
await
|
|
4056
|
+
const lockRoot = path6.join(path6.resolve(memoryDir), "state", "procedure-miner-locks");
|
|
4057
|
+
const lockDir = path6.join(lockRoot, `${clusterHash}.lock`);
|
|
4058
|
+
await mkdir4(lockRoot, { recursive: true });
|
|
2196
4059
|
const startedAt = Date.now();
|
|
2197
4060
|
let attempts = 0;
|
|
2198
4061
|
while (true) {
|
|
2199
4062
|
try {
|
|
2200
|
-
await
|
|
4063
|
+
await mkdir4(lockDir);
|
|
2201
4064
|
return async () => {
|
|
2202
4065
|
await rm(lockDir, { recursive: true, force: true });
|
|
2203
4066
|
};
|
|
@@ -2222,7 +4085,7 @@ async function acquireClusterWriteLock(memoryDir, clusterHash) {
|
|
|
2222
4085
|
}
|
|
2223
4086
|
}
|
|
2224
4087
|
async function withClusterWriteLock(memoryDir, clusterHash, fn) {
|
|
2225
|
-
const inProcessKey = `${
|
|
4088
|
+
const inProcessKey = `${path6.resolve(memoryDir)}:${clusterHash}`;
|
|
2226
4089
|
const previous = inProcessClusterWriteLocks.get(inProcessKey) ?? Promise.resolve();
|
|
2227
4090
|
let releaseInProcess;
|
|
2228
4091
|
const current = previous.catch(() => void 0).then(
|
|
@@ -2462,7 +4325,7 @@ function formatProcedureStatsText(report) {
|
|
|
2462
4325
|
}
|
|
2463
4326
|
|
|
2464
4327
|
// src/admin/admin-surfaces.ts
|
|
2465
|
-
import
|
|
4328
|
+
import path7 from "path";
|
|
2466
4329
|
var REDACTED = "<redacted>";
|
|
2467
4330
|
var SENSITIVE_KEY_RE = /(token|secret|password|api[_-]?key|bearer|authorization|credential|private[_-]?key)/i;
|
|
2468
4331
|
var BEARER_VALUE_RE = /^(bearer|sk-|pk-|xox[bp]-|AKIA)/i;
|
|
@@ -2745,7 +4608,7 @@ async function auditTranscripts(memoryDir) {
|
|
|
2745
4608
|
movedEntries: file.groups.reduce((sum, group) => sum + group.entryCount, 0),
|
|
2746
4609
|
unmovableLines: file.unmovableLines
|
|
2747
4610
|
}));
|
|
2748
|
-
const mixedOtherDefault = files.some((file) => file.sourceRelPath.split(
|
|
4611
|
+
const mixedOtherDefault = files.some((file) => file.sourceRelPath.split(path7.sep).includes("other"));
|
|
2749
4612
|
const summary = plan.movedEntries === 0 ? "No mixed-session transcript data detected; nothing to migrate." : `Found ${plan.movedEntries} entries across ${plan.distinctSessions} distinct sessions stranded in shared/legacy directories. Run the CLI migration command to re-home them.`;
|
|
2750
4613
|
return {
|
|
2751
4614
|
generatedAt: plan.generatedAt,
|
|
@@ -3598,7 +5461,7 @@ var EngramAccessService = class {
|
|
|
3598
5461
|
const snapshotForSerialization = {
|
|
3599
5462
|
sessionKey: options.sessionKey ?? "",
|
|
3600
5463
|
recordedAt: new Date(options.snapshot.capturedAt).toISOString(),
|
|
3601
|
-
queryHash:
|
|
5464
|
+
queryHash: createHash4("sha256").update(options.query).digest("hex"),
|
|
3602
5465
|
queryLen: options.query.length,
|
|
3603
5466
|
memoryIds,
|
|
3604
5467
|
namespace,
|
|
@@ -7559,7 +9422,7 @@ ${next}`);
|
|
|
7559
9422
|
sourceSessionKey: request.sessionKey,
|
|
7560
9423
|
inputSummary: inputSummaryParts.length > 0 ? inputSummaryParts.join(" | ").slice(0, 500) : void 0,
|
|
7561
9424
|
dryRun: request.dryRun === true,
|
|
7562
|
-
promptHash: typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0 ?
|
|
9425
|
+
promptHash: typeof request.sourcePrompt === "string" && request.sourcePrompt.length > 0 ? createHash4("sha256").update(request.sourcePrompt).digest("hex") : void 0
|
|
7563
9426
|
};
|
|
7564
9427
|
const preview = this.orchestrator.previewMemoryActionEvent(event);
|
|
7565
9428
|
if (request.dryRun === true) {
|
|
@@ -7572,7 +9435,7 @@ ${next}`);
|
|
|
7572
9435
|
const resolvedNs = this.writableNamespaceFor(request.namespace, request.sessionKey, request.principal);
|
|
7573
9436
|
const storage = await this.orchestrator.getStorage(resolvedNs);
|
|
7574
9437
|
const storageDir = storage.dir;
|
|
7575
|
-
const { writeFile:
|
|
9438
|
+
const { writeFile: writeFile4, mkdir: mkdir5 } = await import("fs/promises");
|
|
7576
9439
|
const { join: join2, resolve: resolve2 } = await import("path");
|
|
7577
9440
|
const safeKey = request.sessionKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
7578
9441
|
if (!safeKey) throw new EngramAccessInputError("sessionKey is required");
|
|
@@ -7581,10 +9444,10 @@ ${next}`);
|
|
|
7581
9444
|
if (!resolved.startsWith(resolve2(storageDir))) {
|
|
7582
9445
|
throw new EngramAccessInputError("Invalid sessionKey");
|
|
7583
9446
|
}
|
|
7584
|
-
await
|
|
9447
|
+
await mkdir5(checkpointDir, { recursive: true });
|
|
7585
9448
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7586
9449
|
const filePath = join2(checkpointDir, `checkpoint-${ts}.md`);
|
|
7587
|
-
await
|
|
9450
|
+
await writeFile4(filePath, request.context, "utf-8");
|
|
7588
9451
|
return { saved: true };
|
|
7589
9452
|
}
|
|
7590
9453
|
async lcmStatus() {
|
|
@@ -8036,7 +9899,7 @@ ${next}`);
|
|
|
8036
9899
|
async offlineSyncSnapshot(options = {}) {
|
|
8037
9900
|
const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
|
|
8038
9901
|
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
8039
|
-
const storageHash =
|
|
9902
|
+
const storageHash = createHash4("sha256").update(storage.dir).digest("hex").slice(0, 16);
|
|
8040
9903
|
const snapshotBuilder = options.includeContent === false && options.baseFiles && options.baseFiles.length > 0 ? buildOfflineSyncSnapshotFromBase : buildOfflineSyncSnapshot;
|
|
8041
9904
|
const snapshot = await snapshotBuilder({
|
|
8042
9905
|
root: storage.dir,
|
|
@@ -8056,7 +9919,7 @@ ${next}`);
|
|
|
8056
9919
|
async offlineSyncSnapshotStream(options = {}) {
|
|
8057
9920
|
const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
|
|
8058
9921
|
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
8059
|
-
const storageHash =
|
|
9922
|
+
const storageHash = createHash4("sha256").update(storage.dir).digest("hex").slice(0, 16);
|
|
8060
9923
|
return {
|
|
8061
9924
|
namespace: resolvedNamespace,
|
|
8062
9925
|
format: OFFLINE_SYNC_SNAPSHOT_FORMAT,
|
|
@@ -8077,7 +9940,7 @@ ${next}`);
|
|
|
8077
9940
|
async offlineSyncFiles(options) {
|
|
8078
9941
|
const resolvedNamespace = this.resolveReadableNamespace(options.namespace, options.principal);
|
|
8079
9942
|
const storage = await this.orchestrator.getStorage(resolvedNamespace);
|
|
8080
|
-
const storageHash =
|
|
9943
|
+
const storageHash = createHash4("sha256").update(storage.dir).digest("hex").slice(0, 16);
|
|
8081
9944
|
try {
|
|
8082
9945
|
const snapshot = await buildOfflineSyncSnapshotForPaths({
|
|
8083
9946
|
root: storage.dir,
|
|
@@ -8483,6 +10346,11 @@ export {
|
|
|
8483
10346
|
DECISION_SUBCOMMANDS,
|
|
8484
10347
|
isCodegraphToolName,
|
|
8485
10348
|
DELTA_SUBCOMMANDS,
|
|
10349
|
+
CorrectionContractError,
|
|
10350
|
+
createCorrectionService,
|
|
10351
|
+
detectPassiveCorrections,
|
|
10352
|
+
capturePassiveCorrections,
|
|
10353
|
+
clusterByKey,
|
|
8486
10354
|
computeProcedureStats,
|
|
8487
10355
|
formatProcedureStatsText,
|
|
8488
10356
|
EngramAccessInputError,
|
|
@@ -8490,4 +10358,4 @@ export {
|
|
|
8490
10358
|
shapeMemorySummary,
|
|
8491
10359
|
EngramAccessService
|
|
8492
10360
|
};
|
|
8493
|
-
//# sourceMappingURL=chunk-
|
|
10361
|
+
//# sourceMappingURL=chunk-24YDB5QP.js.map
|