@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
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extraction-run coordinator — extracted from the orchestrator
|
|
3
|
+
* (issue #1526, seam 15).
|
|
4
|
+
*
|
|
5
|
+
* Owns the buffer→extract→persist pipeline orchestration:
|
|
6
|
+
* - `runExtraction` — the main extraction pipeline coordinator
|
|
7
|
+
* (normalize → threshold gate → namespace resolve → dedupe
|
|
8
|
+
* fingerprint → extract → validate → persist → post-write hooks)
|
|
9
|
+
* - dedupe support: `shouldQueueExtraction`,
|
|
10
|
+
* `buildExtractionFingerprint`, `normalizeExtractionFingerprintTurns`
|
|
11
|
+
* - `recordProcessedExtractionFingerprint` — meta recording
|
|
12
|
+
*
|
|
13
|
+
* The orchestrator constructs one instance and delegates the extraction
|
|
14
|
+
* run to it. `persistExtraction`, `maybeCapturePassiveCorrections`, and
|
|
15
|
+
* all post-write side-effect helpers arrive as injectable delegates so
|
|
16
|
+
* the orchestrator keeps ownership of cross-subsystem routing and the
|
|
17
|
+
* behavior-critical chokepoints it hosts.
|
|
18
|
+
*
|
|
19
|
+
* Behavior-preserving move from orchestrator.ts. No logic changes — the
|
|
20
|
+
* orchestrator keeps thin delegating methods so existing call sites
|
|
21
|
+
* and tests that exercise the public API continue to work.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import type { StorageManager } from "../index.js";
|
|
26
|
+
import { NamespaceStorageRouter } from "../namespaces/storage.js";
|
|
27
|
+
import type { ExtractionEngine } from "../extraction.js";
|
|
28
|
+
import type { SmartBuffer } from "../buffer.js";
|
|
29
|
+
import type { ThreadingManager } from "../threading.js";
|
|
30
|
+
import type { BoxBuilder } from "../boxes.js";
|
|
31
|
+
import {
|
|
32
|
+
resolvePrincipal,
|
|
33
|
+
defaultNamespaceForPrincipal,
|
|
34
|
+
} from "../namespaces/principal.js";
|
|
35
|
+
import {
|
|
36
|
+
resolveScopeProfilePlan,
|
|
37
|
+
type ResolvedScopeProfilePlan,
|
|
38
|
+
} from "../namespaces/scope-profiles.js";
|
|
39
|
+
import { resolveCodingNamespaceOverlay } from "../coding/coding-namespace.js";
|
|
40
|
+
import type { CodingContext } from "../types.js";
|
|
41
|
+
import {
|
|
42
|
+
resolvePresentationCapabilities,
|
|
43
|
+
resolveMemoryLifecycleCapabilities,
|
|
44
|
+
type GraphConstructionCapabilitySet,
|
|
45
|
+
type MemoryLifecycleCapabilitySet,
|
|
46
|
+
} from "../capabilities.js";
|
|
47
|
+
import { throwIfAborted as sharedThrowIfAborted, abortError as sharedAbortError } from "../abort-error.js";
|
|
48
|
+
import { parseFlexibleIsoTimestamp } from "../utils/iso-timestamp.js";
|
|
49
|
+
import { log } from "../logger.js";
|
|
50
|
+
import type {
|
|
51
|
+
PluginConfig,
|
|
52
|
+
BufferTurn,
|
|
53
|
+
ExtractionResult,
|
|
54
|
+
} from "../types.js";
|
|
55
|
+
import type { TierMigrationCycleSummary } from "../recall-state.js";
|
|
56
|
+
|
|
57
|
+
export interface ExtractionRunResult {
|
|
58
|
+
status: "completed" | "skipped";
|
|
59
|
+
reason?: string;
|
|
60
|
+
persistedCount: number;
|
|
61
|
+
durableOutputCount: number;
|
|
62
|
+
postPersistMetadataFailed?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Dependencies injected by the orchestrator. All stable references or
|
|
66
|
+
* live accessors — lazy getters for anything tests reassign
|
|
67
|
+
* post-construction (buffer, extraction, storageRouter, threading). */
|
|
68
|
+
export interface ExtractionRunCoordinatorDeps {
|
|
69
|
+
config: PluginConfig;
|
|
70
|
+
getBuffer: () => SmartBuffer;
|
|
71
|
+
getExtraction: () => ExtractionEngine;
|
|
72
|
+
getStorageRouter: () => NamespaceStorageRouter;
|
|
73
|
+
getThreading: () => ThreadingManager;
|
|
74
|
+
|
|
75
|
+
persistExtraction: (
|
|
76
|
+
result: ExtractionResult,
|
|
77
|
+
storage: StorageManager,
|
|
78
|
+
threadIdForExtraction?: string | null,
|
|
79
|
+
sourceContext?: { sessionKey?: string; principal?: string; validAt?: string },
|
|
80
|
+
baseNamespace?: string,
|
|
81
|
+
scopeProfileWritePlan?: ResolvedScopeProfilePlan | null,
|
|
82
|
+
sourceText?: string,
|
|
83
|
+
graphCaps?: GraphConstructionCapabilitySet,
|
|
84
|
+
lifecycleCaps?: MemoryLifecycleCapabilitySet,
|
|
85
|
+
) => Promise<string[]>;
|
|
86
|
+
|
|
87
|
+
maybeCapturePassiveCorrections: (
|
|
88
|
+
turns: readonly BufferTurn[],
|
|
89
|
+
opts: {
|
|
90
|
+
sessionKey: string;
|
|
91
|
+
principal?: string;
|
|
92
|
+
namespace: string;
|
|
93
|
+
bufferKey: string;
|
|
94
|
+
isLiveSession: boolean;
|
|
95
|
+
},
|
|
96
|
+
) => Promise<void>;
|
|
97
|
+
|
|
98
|
+
resolveSelfNamespace: (sessionKey?: string) => string;
|
|
99
|
+
getCodingContextForSession: (sessionKey: string) => CodingContext | null;
|
|
100
|
+
applyCodingNamespaceOverlay: (sessionKey: string, namespace: string) => string;
|
|
101
|
+
boxBuilderFor: (storage: StorageManager) => BoxBuilder;
|
|
102
|
+
appendPersistedThreadEpisodes: (
|
|
103
|
+
threadId: string,
|
|
104
|
+
ids: string[],
|
|
105
|
+
) => Promise<void>;
|
|
106
|
+
maybeScheduleConsolidation: (nonZero: boolean) => void;
|
|
107
|
+
requestQmdMaintenance: () => void;
|
|
108
|
+
runTierMigrationCycle: (
|
|
109
|
+
storage: StorageManager,
|
|
110
|
+
trigger: "extraction" | "maintenance" | "manual",
|
|
111
|
+
options?: {
|
|
112
|
+
dryRun?: boolean;
|
|
113
|
+
limitOverride?: number;
|
|
114
|
+
force?: boolean;
|
|
115
|
+
},
|
|
116
|
+
) => Promise<TierMigrationCycleSummary>;
|
|
117
|
+
getLastPersistExtractionDeferredCount: () => number;
|
|
118
|
+
recordProcessedExtractionFingerprint: (
|
|
119
|
+
storage: StorageManager,
|
|
120
|
+
fingerprint: string,
|
|
121
|
+
preloadedMeta?: Awaited<ReturnType<StorageManager["loadMeta"]>>,
|
|
122
|
+
) => Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Recall/extraction-specific abort helpers. Thin wrappers over the shared
|
|
126
|
+
// `abort-error.ts` module.
|
|
127
|
+
const abortRecallError = sharedAbortError;
|
|
128
|
+
|
|
129
|
+
function throwIfRecallAborted(
|
|
130
|
+
signal?: AbortSignal,
|
|
131
|
+
message = "recall aborted",
|
|
132
|
+
): void {
|
|
133
|
+
sharedThrowIfAborted(signal, message);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function raceRecallAbort<T>(
|
|
137
|
+
promise: Promise<T>,
|
|
138
|
+
signal?: AbortSignal,
|
|
139
|
+
message = "recall aborted",
|
|
140
|
+
): Promise<T> {
|
|
141
|
+
throwIfRecallAborted(signal, message);
|
|
142
|
+
if (!signal) return promise;
|
|
143
|
+
|
|
144
|
+
let onAbort: (() => void) | null = null;
|
|
145
|
+
const abortPromise = new Promise<T>((_resolve, reject) => {
|
|
146
|
+
onAbort = () => reject(abortRecallError(message));
|
|
147
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
return await Promise.race([promise, abortPromise]);
|
|
152
|
+
} finally {
|
|
153
|
+
if (onAbort) {
|
|
154
|
+
signal.removeEventListener("abort", onAbort);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function deriveTopicsFromExtraction(result: ExtractionResult): string[] {
|
|
160
|
+
const topics = new Set<string>();
|
|
161
|
+
for (const fact of result.facts ?? []) {
|
|
162
|
+
for (const tag of fact.tags ?? []) {
|
|
163
|
+
if (tag && tag.length >= 2) topics.add(tag.toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
if (fact.entityRef) topics.add(fact.entityRef.toLowerCase());
|
|
166
|
+
if (fact.category) topics.add(fact.category);
|
|
167
|
+
}
|
|
168
|
+
for (const entity of (result as any).entities ?? []) {
|
|
169
|
+
if (typeof entity.name === "string" && entity.name.length >= 2) {
|
|
170
|
+
topics.add(entity.name.toLowerCase());
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [...topics].slice(0, 16);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Coordinates the extraction run pipeline. Holds the dedupe fingerprint
|
|
178
|
+
* cache (`recentExtractionFingerprints`) and delegates all side effects
|
|
179
|
+
* to injected orchestrator methods.
|
|
180
|
+
*/
|
|
181
|
+
export class ExtractionRunCoordinator {
|
|
182
|
+
private readonly recentExtractionFingerprints = new Map<string, number>();
|
|
183
|
+
|
|
184
|
+
constructor(
|
|
185
|
+
private readonly deps: ExtractionRunCoordinatorDeps,
|
|
186
|
+
) {}
|
|
187
|
+
|
|
188
|
+
private get config(): PluginConfig {
|
|
189
|
+
return this.deps.config;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
// Source-valid-at helpers (moved from orchestrator.ts module scope)
|
|
194
|
+
// -------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
private latestSourceValidAtFromTurns(turns: readonly BufferTurn[]): string | undefined {
|
|
197
|
+
let latestMs: number | null = null;
|
|
198
|
+
for (const turn of turns) {
|
|
199
|
+
if (turn.extractionContextOnly === true) continue;
|
|
200
|
+
if (typeof turn.sourceValidAt !== "string") continue;
|
|
201
|
+
const parsed = parseFlexibleIsoTimestamp(turn.sourceValidAt.trim());
|
|
202
|
+
if (parsed === null) continue;
|
|
203
|
+
if (latestMs === null || parsed > latestMs) {
|
|
204
|
+
latestMs = parsed;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return latestMs === null ? undefined : new Date(latestMs).toISOString();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
// -------------------------------------------------------------------------
|
|
212
|
+
// Extraction dedupe fingerprint helpers
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
normalizeExtractionFingerprintTurns(turns: BufferTurn[]): string[] {
|
|
216
|
+
if (!Array.isArray(turns) || turns.length === 0) return [];
|
|
217
|
+
return turns
|
|
218
|
+
.filter((turn) => turn.role === "user" || turn.role === "assistant")
|
|
219
|
+
.map((turn) => {
|
|
220
|
+
if (
|
|
221
|
+
typeof turn.turnFingerprint === "string" &&
|
|
222
|
+
turn.turnFingerprint.length > 0
|
|
223
|
+
) {
|
|
224
|
+
return `fp:${turn.turnFingerprint}`;
|
|
225
|
+
}
|
|
226
|
+
return `${turn.role}:${(turn.content ?? "").replace(/\s+/g, " ").trim().slice(0, this.config.extractionMaxTurnChars)}`;
|
|
227
|
+
})
|
|
228
|
+
.filter((value) => value.length > 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
buildExtractionFingerprint(
|
|
233
|
+
turns: BufferTurn[],
|
|
234
|
+
bufferKey: string,
|
|
235
|
+
): string | null {
|
|
236
|
+
const normalized = this.normalizeExtractionFingerprintTurns(turns).join("\n");
|
|
237
|
+
if (!normalized) return null;
|
|
238
|
+
return createHash("sha256")
|
|
239
|
+
.update(`${bufferKey}\n${normalized}`)
|
|
240
|
+
.digest("hex");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
shouldQueueExtraction(
|
|
246
|
+
turns: BufferTurn[],
|
|
247
|
+
options: { commit?: boolean; bufferKey?: string } = {},
|
|
248
|
+
): boolean {
|
|
249
|
+
const lifecycleCaps = resolveMemoryLifecycleCapabilities(this.config);
|
|
250
|
+
if (!lifecycleCaps.extractionDedupe) return true;
|
|
251
|
+
if (!Array.isArray(turns) || turns.length === 0) return false;
|
|
252
|
+
|
|
253
|
+
const bufferKey = options.bufferKey ?? turns[0]?.sessionKey ?? "default";
|
|
254
|
+
const fingerprint = this.buildExtractionFingerprint(turns, bufferKey);
|
|
255
|
+
if (!fingerprint) return false;
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const seenAt = this.recentExtractionFingerprints.get(fingerprint);
|
|
258
|
+
if (seenAt && now - seenAt < this.config.extractionDedupeWindowMs) {
|
|
259
|
+
log.debug("extraction dedupe: skipped duplicate buffered turn set");
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.commit !== false) {
|
|
264
|
+
this.recentExtractionFingerprints.set(fingerprint, now);
|
|
265
|
+
}
|
|
266
|
+
// Keep this cache bounded to avoid unbounded growth.
|
|
267
|
+
if (
|
|
268
|
+
options.commit !== false &&
|
|
269
|
+
this.recentExtractionFingerprints.size > 200
|
|
270
|
+
) {
|
|
271
|
+
const entries = Array.from(
|
|
272
|
+
this.recentExtractionFingerprints.entries(),
|
|
273
|
+
).sort((a, b) => a[1] - b[1]);
|
|
274
|
+
for (const [key] of entries.slice(0, entries.length - 200)) {
|
|
275
|
+
this.recentExtractionFingerprints.delete(key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
// -------------------------------------------------------------------------
|
|
284
|
+
// Processed-fingerprint recording
|
|
285
|
+
// -------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
async recordProcessedExtractionFingerprint(
|
|
288
|
+
storage: StorageManager,
|
|
289
|
+
fingerprint: string,
|
|
290
|
+
preloadedMeta?: Awaited<ReturnType<StorageManager["loadMeta"]>>,
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
const meta = preloadedMeta ?? (await storage.loadMeta());
|
|
293
|
+
const observedAt = new Date().toISOString();
|
|
294
|
+
const seen = new Map(
|
|
295
|
+
(meta.processedExtractionFingerprints ?? []).map((entry) => [
|
|
296
|
+
entry.fingerprint,
|
|
297
|
+
entry.observedAt,
|
|
298
|
+
]),
|
|
299
|
+
);
|
|
300
|
+
seen.set(fingerprint, observedAt);
|
|
301
|
+
meta.processedExtractionFingerprints = Array.from(seen.entries())
|
|
302
|
+
.map(([value, at]) => ({ fingerprint: value, observedAt: at }))
|
|
303
|
+
.sort((left, right) => left.observedAt.localeCompare(right.observedAt))
|
|
304
|
+
.slice(-500);
|
|
305
|
+
if (!preloadedMeta) {
|
|
306
|
+
await storage.saveMeta(meta);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
// -------------------------------------------------------------------------
|
|
312
|
+
// Main extraction pipeline
|
|
313
|
+
// -------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
async runExtraction(
|
|
316
|
+
turns: BufferTurn[],
|
|
317
|
+
options: {
|
|
318
|
+
clearBufferAfterExtraction?: boolean;
|
|
319
|
+
skipCharThreshold?: boolean;
|
|
320
|
+
skipUserTurnThreshold?: boolean;
|
|
321
|
+
deadlineMs?: number;
|
|
322
|
+
bufferKey?: string;
|
|
323
|
+
abortSignal?: AbortSignal;
|
|
324
|
+
failOnExtractionFailure?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Explicit namespace override for the write path (#460). When set,
|
|
327
|
+
* extraction writes go to this namespace instead of the one derived
|
|
328
|
+
* from `defaultNamespaceForPrincipal(resolvePrincipal(sessionKey))`.
|
|
329
|
+
* The resolved `principal` is still threaded into memory metadata
|
|
330
|
+
* for provenance; only the storage target is overridden.
|
|
331
|
+
*/
|
|
332
|
+
writeNamespaceOverride?: string;
|
|
333
|
+
/**
|
|
334
|
+
* Pin the provenance principal instead of deriving it from
|
|
335
|
+
* `resolvePrincipal(sessionKey)` (#1495 thread 1). When set, this is the
|
|
336
|
+
* identity an access surface already authenticated; used so observed-turn
|
|
337
|
+
* provenance is correct even though `turn.sessionKey` is the ORIGINAL
|
|
338
|
+
* (un-prefixed) key and storage is pinned via `writeNamespaceOverride`.
|
|
339
|
+
*/
|
|
340
|
+
principalOverride?: string;
|
|
341
|
+
} = {},
|
|
342
|
+
): Promise<ExtractionRunResult> {
|
|
343
|
+
log.debug(`running extraction on ${turns.length} turns`);
|
|
344
|
+
const clearBufferAfterExtraction =
|
|
345
|
+
options.clearBufferAfterExtraction ?? true;
|
|
346
|
+
const skipCharThreshold = options.skipCharThreshold ?? false;
|
|
347
|
+
const skipUserTurnThreshold = options.skipUserTurnThreshold ?? false;
|
|
348
|
+
const deadlineMs =
|
|
349
|
+
typeof options.deadlineMs === "number" &&
|
|
350
|
+
Number.isFinite(options.deadlineMs)
|
|
351
|
+
? options.deadlineMs
|
|
352
|
+
: undefined;
|
|
353
|
+
const bufferKey = options.bufferKey ?? turns[0]?.sessionKey ?? "default";
|
|
354
|
+
const throwIfDeadlineExceeded = (stage: string): void => {
|
|
355
|
+
if (typeof deadlineMs === "number" && Date.now() > deadlineMs) {
|
|
356
|
+
throw new Error(`replay extraction deadline exceeded (${stage})`);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const throwIfAborted = (stage: string): void => {
|
|
360
|
+
throwIfRecallAborted(options.abortSignal, `extraction aborted (${stage})`);
|
|
361
|
+
};
|
|
362
|
+
const clearBuffer = async (options?: { ignoreAbort?: boolean }) => {
|
|
363
|
+
if (options?.ignoreAbort !== true) {
|
|
364
|
+
throwIfAborted("before_clear_buffer");
|
|
365
|
+
}
|
|
366
|
+
if (clearBufferAfterExtraction) {
|
|
367
|
+
await this.deps.getBuffer().clearAfterExtraction(bufferKey, turns);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Skip extraction for cron job sessions - these are system operations, not user conversations
|
|
372
|
+
const sessionKey = turns[0]?.sessionKey ?? "";
|
|
373
|
+
if (sessionKey.includes(":cron:")) {
|
|
374
|
+
log.debug(`skipping extraction for cron session: ${sessionKey}`);
|
|
375
|
+
await clearBuffer();
|
|
376
|
+
return {
|
|
377
|
+
status: "skipped",
|
|
378
|
+
reason: "cron_session",
|
|
379
|
+
persistedCount: 0,
|
|
380
|
+
durableOutputCount: 0,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const normalizedTurns = turns
|
|
385
|
+
.filter(
|
|
386
|
+
(t) =>
|
|
387
|
+
(t.role === "user" || t.role === "assistant") &&
|
|
388
|
+
typeof t.content === "string",
|
|
389
|
+
)
|
|
390
|
+
.map((t) => ({
|
|
391
|
+
...t,
|
|
392
|
+
content: t.content.trim().slice(0, this.config.extractionMaxTurnChars),
|
|
393
|
+
}))
|
|
394
|
+
.filter((t) => t.content.length > 0);
|
|
395
|
+
const targetTurns = normalizedTurns.filter(
|
|
396
|
+
(turn) => turn.extractionContextOnly !== true,
|
|
397
|
+
);
|
|
398
|
+
if (targetTurns.length === 0) {
|
|
399
|
+
log.debug("skipping extraction: no non-context turns after normalization");
|
|
400
|
+
// Context-only turns may still contain corrections (review: "context-only
|
|
401
|
+
// turns skip capture"). Scan normalizedTurns before clearing the buffer.
|
|
402
|
+
if (normalizedTurns.length > 0) {
|
|
403
|
+
await this.deps.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], { sessionKey, principal: resolvePrincipal(sessionKey, this.config), namespace: this.deps.resolveSelfNamespace(sessionKey), bufferKey, isLiveSession: clearBufferAfterExtraction });
|
|
404
|
+
}
|
|
405
|
+
await clearBuffer();
|
|
406
|
+
return { status: "skipped", reason: "empty_normalized_turns", persistedCount: 0, durableOutputCount: 0 };
|
|
407
|
+
}
|
|
408
|
+
const sourceValidAt = this.latestSourceValidAtFromTurns(targetTurns);
|
|
409
|
+
throwIfDeadlineExceeded("before_extract");
|
|
410
|
+
throwIfAborted("before_extract");
|
|
411
|
+
|
|
412
|
+
const userTurns = targetTurns.filter((t) => t.role === "user");
|
|
413
|
+
const totalChars = targetTurns.reduce(
|
|
414
|
+
(sum, t) => sum + t.content.length,
|
|
415
|
+
0,
|
|
416
|
+
);
|
|
417
|
+
const belowCharThreshold = totalChars < this.config.extractionMinChars;
|
|
418
|
+
const belowUserTurnThreshold =
|
|
419
|
+
!skipUserTurnThreshold &&
|
|
420
|
+
userTurns.length < this.config.extractionMinUserTurns;
|
|
421
|
+
if ((!skipCharThreshold && belowCharThreshold) || belowUserTurnThreshold) {
|
|
422
|
+
log.debug(
|
|
423
|
+
`skipping extraction: below threshold (totalChars=${totalChars}, userTurns=${userTurns.length})`,
|
|
424
|
+
);
|
|
425
|
+
await clearBuffer();
|
|
426
|
+
// Passive correction capture runs even when extraction is skipped for
|
|
427
|
+
// being below the char/user-turn threshold (review: "skipped extraction
|
|
428
|
+
// skips capture"). A short correction like "stop using Vim" is under
|
|
429
|
+
// extractionMinChars but is exactly the high-value case this feature
|
|
430
|
+
// exists for. The write namespace resolves through the SAME scope-profile
|
|
431
|
+
// plan as the full extraction path so corrections target the active
|
|
432
|
+
// profile's write layer, not just resolveSelfNamespace (review:
|
|
433
|
+
// "below-threshold wrong namespace"). The ACL in passiveCorrectionService
|
|
434
|
+
// authorizes the actual plan/apply.
|
|
435
|
+
{
|
|
436
|
+
const capturePrincipal =
|
|
437
|
+
typeof options.principalOverride === "string" && options.principalOverride.length > 0
|
|
438
|
+
? options.principalOverride
|
|
439
|
+
: resolvePrincipal(sessionKey, this.config);
|
|
440
|
+
const captureWO = typeof options.writeNamespaceOverride === "string" && options.writeNamespaceOverride.length > 0
|
|
441
|
+
? options.writeNamespaceOverride : undefined;
|
|
442
|
+
const captureCodingCtx = sessionKey ? this.deps.getCodingContextForSession(sessionKey) : null;
|
|
443
|
+
const captureCodingOv = resolveCodingNamespaceOverlay(captureCodingCtx, this.config.codingMode, this.config.defaultNamespace);
|
|
444
|
+
const captureScopePlan = resolveScopeProfilePlan({ config: this.config, principal: capturePrincipal, codingContext: captureCodingCtx, codingOverlay: captureCodingOv });
|
|
445
|
+
const captureNamespace = captureWO ?? captureScopePlan?.writeNamespace ?? this.deps.resolveSelfNamespace(sessionKey);
|
|
446
|
+
await this.deps.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], {
|
|
447
|
+
sessionKey, principal: capturePrincipal, namespace: captureNamespace, bufferKey, isLiveSession: clearBufferAfterExtraction,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
status: "skipped",
|
|
452
|
+
reason: "below_threshold",
|
|
453
|
+
persistedCount: 0,
|
|
454
|
+
durableOutputCount: 0,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Provenance principal honours the access-surface override (#1495 thread 1,
|
|
459
|
+
// mirroring the recall path's `principalOverride`, issue #570 PR 4). Access
|
|
460
|
+
// surfaces that authenticated the caller at the transport layer pass their
|
|
461
|
+
// resolved principal so provenance uses the SAME identity the surface
|
|
462
|
+
// authorized, instead of `resolvePrincipal(sessionKey)` — which on a
|
|
463
|
+
// namespace-prefixed key would collapse to `default`. The ORIGINAL,
|
|
464
|
+
// un-prefixed session key still drives threading.
|
|
465
|
+
const principal =
|
|
466
|
+
typeof options.principalOverride === "string" &&
|
|
467
|
+
options.principalOverride.length > 0
|
|
468
|
+
? options.principalOverride
|
|
469
|
+
: resolvePrincipal(sessionKey, this.config);
|
|
470
|
+
// Write path — explicit callers still win. Otherwise, an active hosted
|
|
471
|
+
// scope profile owns the extraction write target so hook-captured turns land
|
|
472
|
+
// in the same layer that profile recall searches. Without a profile, preserve
|
|
473
|
+
// the existing coding-agent overlay behavior (issue #569).
|
|
474
|
+
const explicitWriteNamespace =
|
|
475
|
+
typeof options.writeNamespaceOverride === "string" &&
|
|
476
|
+
options.writeNamespaceOverride.length > 0
|
|
477
|
+
? options.writeNamespaceOverride
|
|
478
|
+
: undefined;
|
|
479
|
+
const codingContextForWrite = sessionKey
|
|
480
|
+
? this.deps.getCodingContextForSession(sessionKey)
|
|
481
|
+
: null;
|
|
482
|
+
const codingOverlayForWrite = resolveCodingNamespaceOverlay(
|
|
483
|
+
codingContextForWrite,
|
|
484
|
+
this.config.codingMode,
|
|
485
|
+
this.config.defaultNamespace,
|
|
486
|
+
);
|
|
487
|
+
const scopeProfileGatePlan = resolveScopeProfilePlan({
|
|
488
|
+
config: this.config,
|
|
489
|
+
principal,
|
|
490
|
+
codingContext: codingContextForWrite,
|
|
491
|
+
codingOverlay: codingOverlayForWrite,
|
|
492
|
+
});
|
|
493
|
+
const scopeProfileWritePlan = explicitWriteNamespace ? null : scopeProfileGatePlan;
|
|
494
|
+
if (scopeProfileWritePlan) {
|
|
495
|
+
const selectedLayer = scopeProfileWritePlan.layers.find(
|
|
496
|
+
(layer) => layer.id === scopeProfileWritePlan.writeLayer,
|
|
497
|
+
);
|
|
498
|
+
const writeNamespaceReadable = scopeProfileWritePlan.readNamespaces.includes(
|
|
499
|
+
scopeProfileWritePlan.writeNamespace,
|
|
500
|
+
);
|
|
501
|
+
if (!selectedLayer?.writable || !writeNamespaceReadable) {
|
|
502
|
+
log.warn(
|
|
503
|
+
`runExtraction: skipping scope profile ${scopeProfileWritePlan.profileId} because write layer ${scopeProfileWritePlan.writeLayer} is not writable inside the profile read stack`,
|
|
504
|
+
);
|
|
505
|
+
await clearBuffer();
|
|
506
|
+
return {
|
|
507
|
+
status: "skipped",
|
|
508
|
+
reason: "scope_profile_no_writable_layer",
|
|
509
|
+
persistedCount: 0,
|
|
510
|
+
durableOutputCount: 0,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const selfNamespace =
|
|
515
|
+
explicitWriteNamespace ??
|
|
516
|
+
scopeProfileWritePlan?.writeNamespace ??
|
|
517
|
+
this.deps.applyCodingNamespaceOverlay(
|
|
518
|
+
sessionKey,
|
|
519
|
+
defaultNamespaceForPrincipal(principal, this.config),
|
|
520
|
+
);
|
|
521
|
+
const storage = await this.deps.getStorageRouter().storageFor(selfNamespace);
|
|
522
|
+
const shouldPersistProcessedFingerprint = targetTurns.some(
|
|
523
|
+
(turn) => turn.persistProcessedFingerprint === true,
|
|
524
|
+
);
|
|
525
|
+
const extractionFingerprint = this.buildExtractionFingerprint(
|
|
526
|
+
targetTurns,
|
|
527
|
+
bufferKey,
|
|
528
|
+
);
|
|
529
|
+
let meta =
|
|
530
|
+
extractionFingerprint && shouldPersistProcessedFingerprint
|
|
531
|
+
? await storage.loadMeta()
|
|
532
|
+
: null;
|
|
533
|
+
if (
|
|
534
|
+
extractionFingerprint &&
|
|
535
|
+
shouldPersistProcessedFingerprint &&
|
|
536
|
+
(meta?.processedExtractionFingerprints ?? []).some(
|
|
537
|
+
(entry) => entry.fingerprint === extractionFingerprint,
|
|
538
|
+
)
|
|
539
|
+
) {
|
|
540
|
+
log.debug(
|
|
541
|
+
`runExtraction: skipping already-processed extraction fingerprint for ${bufferKey}`,
|
|
542
|
+
);
|
|
543
|
+
await clearBuffer();
|
|
544
|
+
return {
|
|
545
|
+
status: "skipped",
|
|
546
|
+
reason: "processed_fingerprint",
|
|
547
|
+
persistedCount: 0,
|
|
548
|
+
durableOutputCount: 0,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Pass existing entity names so the LLM can reuse them instead of inventing variants
|
|
553
|
+
const existingEntities = await storage.listEntityNames();
|
|
554
|
+
const result = await raceRecallAbort(
|
|
555
|
+
this.deps.getExtraction().extract(
|
|
556
|
+
normalizedTurns,
|
|
557
|
+
existingEntities,
|
|
558
|
+
),
|
|
559
|
+
options.abortSignal,
|
|
560
|
+
"extraction aborted (during_extract)",
|
|
561
|
+
);
|
|
562
|
+
throwIfDeadlineExceeded("before_persist");
|
|
563
|
+
throwIfAborted("before_persist");
|
|
564
|
+
|
|
565
|
+
// Defensive: validate extraction result before processing. Explicit
|
|
566
|
+
// fail-closed callers, such as flush-plan import, must not observe
|
|
567
|
+
// malformed extractor output as a successful skip.
|
|
568
|
+
if (!result) {
|
|
569
|
+
log.warn("runExtraction: extraction returned null/undefined");
|
|
570
|
+
if (options.failOnExtractionFailure) {
|
|
571
|
+
throw new Error("extraction failed: invalid_extraction_result");
|
|
572
|
+
}
|
|
573
|
+
await clearBuffer();
|
|
574
|
+
return {
|
|
575
|
+
status: "skipped",
|
|
576
|
+
reason: "invalid_extraction_result",
|
|
577
|
+
persistedCount: 0,
|
|
578
|
+
durableOutputCount: 0,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const invalidExtractionResultFields = [
|
|
582
|
+
["facts", result.facts],
|
|
583
|
+
["entities", result.entities],
|
|
584
|
+
["questions", result.questions],
|
|
585
|
+
["profileUpdates", result.profileUpdates],
|
|
586
|
+
]
|
|
587
|
+
.filter(([, value]) => !Array.isArray(value))
|
|
588
|
+
.map(([field]) => field);
|
|
589
|
+
if (invalidExtractionResultFields.length > 0) {
|
|
590
|
+
log.warn(
|
|
591
|
+
"runExtraction: extraction returned invalid collection fields",
|
|
592
|
+
{
|
|
593
|
+
invalidFields: invalidExtractionResultFields,
|
|
594
|
+
resultKeys:
|
|
595
|
+
typeof result === "object" && result !== null
|
|
596
|
+
? Object.keys(result)
|
|
597
|
+
: [],
|
|
598
|
+
},
|
|
599
|
+
);
|
|
600
|
+
if (options.failOnExtractionFailure) {
|
|
601
|
+
throw new Error("extraction failed: invalid_extraction_result");
|
|
602
|
+
}
|
|
603
|
+
await clearBuffer();
|
|
604
|
+
return {
|
|
605
|
+
status: "skipped",
|
|
606
|
+
reason: "invalid_extraction_result",
|
|
607
|
+
persistedCount: 0,
|
|
608
|
+
durableOutputCount: 0,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const extractionFailure =
|
|
612
|
+
typeof result.extractionFailure === "string" &&
|
|
613
|
+
result.extractionFailure.trim().length > 0
|
|
614
|
+
? result.extractionFailure
|
|
615
|
+
: undefined;
|
|
616
|
+
if (options.failOnExtractionFailure && extractionFailure) {
|
|
617
|
+
throw new Error(`extraction failed: ${extractionFailure}`);
|
|
618
|
+
}
|
|
619
|
+
if (
|
|
620
|
+
result.facts.length === 0 &&
|
|
621
|
+
result.entities.length === 0 &&
|
|
622
|
+
result.questions.length === 0 &&
|
|
623
|
+
result.profileUpdates.length === 0
|
|
624
|
+
) {
|
|
625
|
+
log.debug(
|
|
626
|
+
"runExtraction: extraction produced no durable outputs; skipping persistence",
|
|
627
|
+
);
|
|
628
|
+
if (extractionFailure) {
|
|
629
|
+
log.warn(
|
|
630
|
+
"runExtraction: extraction reported failure with no durable outputs; not marking fingerprint processed",
|
|
631
|
+
{ extractionFailure },
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (
|
|
635
|
+
extractionFingerprint &&
|
|
636
|
+
shouldPersistProcessedFingerprint &&
|
|
637
|
+
!extractionFailure
|
|
638
|
+
) {
|
|
639
|
+
meta ??= await storage.loadMeta();
|
|
640
|
+
await this.deps.recordProcessedExtractionFingerprint(
|
|
641
|
+
storage,
|
|
642
|
+
extractionFingerprint,
|
|
643
|
+
meta,
|
|
644
|
+
);
|
|
645
|
+
meta.extractionCount += 1;
|
|
646
|
+
meta.lastExtractionAt = new Date().toISOString();
|
|
647
|
+
await storage.saveMeta(meta);
|
|
648
|
+
}
|
|
649
|
+
// Correction-only turns that meet char/user-turn thresholds but yield
|
|
650
|
+
// zero facts still need passive capture (review: "empty extraction skips
|
|
651
|
+
// capture"). selfNamespace/principal already resolved above.
|
|
652
|
+
await this.deps.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], { sessionKey, principal, namespace: selfNamespace, bufferKey, isLiveSession: clearBufferAfterExtraction });
|
|
653
|
+
await clearBuffer();
|
|
654
|
+
return { status: "skipped", reason: "empty_extraction_result", persistedCount: 0, durableOutputCount: 0 };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
let threadIdForExtraction: string | null = null;
|
|
658
|
+
if (resolvePresentationCapabilities(this.config).threading && turns.length > 0) {
|
|
659
|
+
const lastTurn = turns[turns.length - 1];
|
|
660
|
+
try {
|
|
661
|
+
threadIdForExtraction = await this.deps.getThreading().processTurn(lastTurn, []);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
// Fail-open: threading errors must not block memory persistence.
|
|
664
|
+
log.warn(
|
|
665
|
+
"[threading] processTurn failed before persistence (non-fatal)",
|
|
666
|
+
err,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const persistedIds = await this.deps.persistExtraction(
|
|
672
|
+
result,
|
|
673
|
+
storage,
|
|
674
|
+
threadIdForExtraction,
|
|
675
|
+
{ sessionKey, principal, validAt: sourceValidAt },
|
|
676
|
+
// Pass the KNOWN base namespace (NHIdx) so the catalog write touch records the
|
|
677
|
+
// real namespace rather than a guess decoded from the storage dir.
|
|
678
|
+
selfNamespace,
|
|
679
|
+
scopeProfileGatePlan,
|
|
680
|
+
// Verbatim source turns for the faithfulness gate (#1576) so it can
|
|
681
|
+
// locate a quote per fact when #1575 spans are absent.
|
|
682
|
+
normalizedTurns.map((t) => t.content).join("\n\n"),
|
|
683
|
+
);
|
|
684
|
+
let postPersistMetadataFailed = false;
|
|
685
|
+
meta ??= await storage.loadMeta();
|
|
686
|
+
if (extractionFingerprint && shouldPersistProcessedFingerprint) {
|
|
687
|
+
try {
|
|
688
|
+
await this.deps.recordProcessedExtractionFingerprint(
|
|
689
|
+
storage,
|
|
690
|
+
extractionFingerprint,
|
|
691
|
+
meta,
|
|
692
|
+
);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
log.warn(
|
|
695
|
+
"runExtraction: failed to persist processed extraction fingerprint; continuing with buffer clear",
|
|
696
|
+
error,
|
|
697
|
+
);
|
|
698
|
+
postPersistMetadataFailed = true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Persist extraction counters and processed fingerprints before running
|
|
702
|
+
// follow-on helpers so replay dedupe survives any later non-essential
|
|
703
|
+
// failure. If this aggregate meta write fails, still clear the buffer:
|
|
704
|
+
// the durable memories are already written and replaying the same turns
|
|
705
|
+
// would duplicate them.
|
|
706
|
+
meta.extractionCount += 1;
|
|
707
|
+
meta.lastExtractionAt = new Date().toISOString();
|
|
708
|
+
meta.totalMemories += Array.isArray(result?.facts)
|
|
709
|
+
? result.facts.length
|
|
710
|
+
: 0;
|
|
711
|
+
meta.totalEntities += Array.isArray(result?.entities)
|
|
712
|
+
? result.entities.length
|
|
713
|
+
: 0;
|
|
714
|
+
try {
|
|
715
|
+
await storage.saveMeta(meta);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
log.warn(
|
|
718
|
+
"runExtraction: failed to save extraction metadata after durable persistence; continuing with buffer clear",
|
|
719
|
+
error,
|
|
720
|
+
);
|
|
721
|
+
postPersistMetadataFailed = true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const durableOutputCount =
|
|
725
|
+
result.facts.length +
|
|
726
|
+
result.entities.length +
|
|
727
|
+
result.questions.length +
|
|
728
|
+
result.profileUpdates.length;
|
|
729
|
+
|
|
730
|
+
// Buffer retention for defer verdicts (issue #562, PR 2). When the judge
|
|
731
|
+
// deferred at least one candidate, retain the tail of the current turn
|
|
732
|
+
// window so the next extraction pass has the surrounding context that
|
|
733
|
+
// may disambiguate the deferred fact. Non-defer runs clear the slot.
|
|
734
|
+
//
|
|
735
|
+
// Gated on:
|
|
736
|
+
// - `clearBufferAfterExtraction` — replay / bulk-import paths call
|
|
737
|
+
// `runExtraction` with this false and do not operate on live buffer
|
|
738
|
+
// state. Writing retention there would create synthetic buffer
|
|
739
|
+
// entries and cross-contaminate future live extractions.
|
|
740
|
+
// - NOT `extractionJudgeShadow` — in shadow mode the judge is only
|
|
741
|
+
// advisory; facts are still persisted regardless of verdict, so
|
|
742
|
+
// retaining the turn window on top of a persisted write would both
|
|
743
|
+
// waste buffer space and cause the same facts to re-enter the
|
|
744
|
+
// pipeline on the next pass.
|
|
745
|
+
try {
|
|
746
|
+
if (
|
|
747
|
+
clearBufferAfterExtraction &&
|
|
748
|
+
!this.config.extractionJudgeShadow
|
|
749
|
+
) {
|
|
750
|
+
const deferredCount = this.deps.getLastPersistExtractionDeferredCount();
|
|
751
|
+
if (deferredCount > 0 && normalizedTurns.length > 0) {
|
|
752
|
+
await this.deps.getBuffer().retainDeferredTurns(
|
|
753
|
+
bufferKey,
|
|
754
|
+
normalizedTurns as BufferTurn[],
|
|
755
|
+
10,
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
await this.deps.getBuffer().retainDeferredTurns(bufferKey, [], 0);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} catch (err) {
|
|
762
|
+
// Fail-open: retention is a nice-to-have. If it fails the judge will
|
|
763
|
+
// still cap deferrals and convert to reject on the next pass.
|
|
764
|
+
log.debug(
|
|
765
|
+
`extraction-judge: defer retention failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
await clearBuffer({ ignoreAbort: true });
|
|
770
|
+
|
|
771
|
+
// Passive correction capture (issue #1581) — detect corrections expressed
|
|
772
|
+
// passively in the extracted turns and route to the Correction Contract.
|
|
773
|
+
// Runs AFTER persistence + buffer clear so a capture failure never blocks
|
|
774
|
+
// the extraction return. `clearBufferAfterExtraction` gates live-session
|
|
775
|
+
// auto-apply (replay/import → queue-only).
|
|
776
|
+
await this.deps.maybeCapturePassiveCorrections(normalizedTurns as BufferTurn[], {
|
|
777
|
+
sessionKey,
|
|
778
|
+
principal,
|
|
779
|
+
namespace: selfNamespace,
|
|
780
|
+
bufferKey,
|
|
781
|
+
isLiveSession: clearBufferAfterExtraction,
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// Build memory box from this extraction (v8.0 Phase 2A)
|
|
785
|
+
// Topics are derived from the current extraction's facts and entities only —
|
|
786
|
+
// not from readAllMemories() — so box topics accurately reflect the current
|
|
787
|
+
// session window and the call is free of expensive full-corpus I/O.
|
|
788
|
+
if (resolvePresentationCapabilities(this.config).memoryBoxes && persistedIds.length > 0) {
|
|
789
|
+
const extractionTopics = deriveTopicsFromExtraction(result);
|
|
790
|
+
// Derive episodic metadata from buffer turns (REMem-inspired)
|
|
791
|
+
const firstUserTurn = turns.find((t) => t.role === "user");
|
|
792
|
+
const boxGoal =
|
|
793
|
+
firstUserTurn?.content?.slice(0, 100)?.trim() || undefined;
|
|
794
|
+
await this.deps.boxBuilderFor(storage)
|
|
795
|
+
.onExtraction({
|
|
796
|
+
topics: extractionTopics,
|
|
797
|
+
memoryIds: persistedIds,
|
|
798
|
+
timestamp: new Date().toISOString(),
|
|
799
|
+
goal: boxGoal,
|
|
800
|
+
})
|
|
801
|
+
.catch((err) =>
|
|
802
|
+
log.warn("[boxes] onExtraction failed (non-fatal)", err),
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Batch-append persisted IDs so non-fact memories (entities/questions) are
|
|
807
|
+
// always attached to the thread. The helper excludes pending_review ids (#1635).
|
|
808
|
+
if (
|
|
809
|
+
resolvePresentationCapabilities(this.config).threading &&
|
|
810
|
+
threadIdForExtraction &&
|
|
811
|
+
persistedIds.length > 0
|
|
812
|
+
) {
|
|
813
|
+
await this.deps.appendPersistedThreadEpisodes(
|
|
814
|
+
threadIdForExtraction,
|
|
815
|
+
persistedIds,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Thread title update for the already-established thread context.
|
|
820
|
+
if (resolvePresentationCapabilities(this.config).threading && threadIdForExtraction) {
|
|
821
|
+
const conversationContent = turns.map((t) => t.content).join(" ");
|
|
822
|
+
try {
|
|
823
|
+
await this.deps.getThreading().updateThreadTitle(
|
|
824
|
+
threadIdForExtraction,
|
|
825
|
+
conversationContent,
|
|
826
|
+
);
|
|
827
|
+
} catch (err) {
|
|
828
|
+
log.warn(
|
|
829
|
+
"[threading] updateThreadTitle failed after persistence (non-fatal)",
|
|
830
|
+
err,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Check if consolidation is needed (debounced + non-zero gated).
|
|
836
|
+
const nonZeroExtraction = durableOutputCount > 0;
|
|
837
|
+
try {
|
|
838
|
+
// The increment of nonZeroExtractionsSinceConsolidation moved into
|
|
839
|
+
// the scheduler with the rest of the cadence state (issue #1526 PR1).
|
|
840
|
+
this.deps.maybeScheduleConsolidation(nonZeroExtraction);
|
|
841
|
+
} catch (err) {
|
|
842
|
+
log.warn(
|
|
843
|
+
"runExtraction: consolidation scheduling failed after persistence (non-fatal)",
|
|
844
|
+
err,
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
try {
|
|
849
|
+
this.deps.requestQmdMaintenance();
|
|
850
|
+
} catch (err) {
|
|
851
|
+
log.warn(
|
|
852
|
+
"runExtraction: QMD maintenance scheduling failed after persistence (non-fatal)",
|
|
853
|
+
err,
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
try {
|
|
858
|
+
await this.deps.runTierMigrationCycle(storage, "extraction");
|
|
859
|
+
} catch (err) {
|
|
860
|
+
log.warn(
|
|
861
|
+
"runExtraction: tier migration failed after persistence (non-fatal)",
|
|
862
|
+
err,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
status: "completed",
|
|
868
|
+
persistedCount: persistedIds.length,
|
|
869
|
+
durableOutputCount,
|
|
870
|
+
postPersistMetadataFailed,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|