@remnic/core 9.3.749 → 9.3.751
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 +8 -8
- package/dist/access-boundary.js +15 -15
- package/dist/access-cli.js +39 -39
- package/dist/access-http.d.ts +8 -8
- package/dist/access-http.js +20 -20
- package/dist/access-mcp.d.ts +8 -8
- package/dist/access-mcp.js +19 -19
- package/dist/access-operations-batch.js +16 -16
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +18 -18
- package/dist/access-schema.d.ts +2 -2
- package/dist/access-schema.js +5 -5
- package/dist/{access-service-5-EVpt3v.d.ts → access-service-DmAEOJdJ.d.ts} +11 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +14 -14
- package/dist/access-surface-catalog.d.ts +8 -8
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +8 -1
- package/dist/active-recall.js.map +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +7 -7
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -7
- 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-D_ZDxu8Y.d.ts → catalog-BpOcwHdE.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 +8 -8
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-C5WN74HH.js → chunk-2HAG7JOI.js} +8 -2
- package/dist/chunk-2HAG7JOI.js.map +1 -0
- package/dist/{chunk-GKRLPHPU.js → chunk-2KK5WNQB.js} +2 -2
- package/dist/{chunk-JRDPT6DA.js → chunk-4NWIGAIC.js} +2 -2
- package/dist/{chunk-OBJUKYDO.js → chunk-4XL3CVII.js} +2 -2
- package/dist/{chunk-WWVXXSLF.js → chunk-5PZCDUJ6.js} +4 -4
- package/dist/{chunk-URQLO7BX.js → chunk-6FNZ3NMS.js} +2 -2
- package/dist/{chunk-DRUDC2UX.js → chunk-6MMONPHM.js} +2 -2
- package/dist/{chunk-RB6A7OUN.js → chunk-6SXNQFHB.js} +2 -2
- package/dist/{chunk-ZGZNLRKF.js → chunk-7TWA7DKP.js} +5 -5
- package/dist/{chunk-KMNYXLPL.js → chunk-B35L45HZ.js} +2 -2
- package/dist/{chunk-SIV3JZPP.js → chunk-CGZWGVQF.js} +5 -5
- package/dist/{chunk-CKFLLAIS.js → chunk-DANV2HTH.js} +3 -3
- package/dist/{chunk-NVYF5OB6.js → chunk-DBRVU5PR.js} +3 -3
- package/dist/{chunk-LMFS64GY.js → chunk-E4PFDD3N.js} +4 -4
- package/dist/{chunk-DIHCFJVV.js → chunk-HYI2GP3F.js} +4 -4
- package/dist/{chunk-UIKZZ242.js → chunk-IKHKHADN.js} +2 -2
- package/dist/{chunk-PUR2CKX2.js → chunk-JS2HKKOK.js} +1 -1
- package/dist/{chunk-PUR2CKX2.js.map → chunk-JS2HKKOK.js.map} +1 -1
- package/dist/{chunk-R2OAN6IP.js → chunk-K2JM4DZZ.js} +2 -2
- package/dist/{chunk-3IJGF6K2.js → chunk-K2R3DEEH.js} +2 -2
- package/dist/{chunk-HIHCB2S6.js → chunk-QRRBXD24.js} +2 -2
- package/dist/{chunk-PNUOQYRJ.js → chunk-RWZB6MAY.js} +1953 -2172
- package/dist/chunk-RWZB6MAY.js.map +1 -0
- package/dist/{chunk-ZPZ4VVLT.js → chunk-SD34EK4Z.js} +2 -2
- package/dist/{chunk-WK3NK6SP.js → chunk-SNEXVX3U.js} +6 -6
- package/dist/{chunk-FVZ2ISIX.js → chunk-TPLE6SNY.js} +7 -7
- package/dist/{chunk-24YDB5QP.js → chunk-UETBEHBA.js} +44 -24
- package/dist/chunk-UETBEHBA.js.map +1 -0
- package/dist/{chunk-5IN26V6W.js → chunk-UU32EAIY.js} +5 -5
- package/dist/{chunk-ZGAJB74V.js → chunk-W3BFT5OJ.js} +2 -2
- package/dist/{chunk-L4N7SVNN.js → chunk-WA7FECSG.js} +11 -11
- package/dist/{chunk-Q3X7GUAQ.js → chunk-WOX2UW4K.js} +2 -2
- package/dist/{chunk-XTEWVF3C.js → chunk-WWSXVOGY.js} +2 -2
- package/dist/{chunk-PWWWLD7D.js → chunk-X64YLC24.js} +153 -27
- package/dist/chunk-X64YLC24.js.map +1 -0
- package/dist/{chunk-P4FW3ZU5.js → chunk-YGGXUNS4.js} +20 -20
- package/dist/{chunk-JHQUIH3W.js → chunk-ZBVQ4ZM4.js} +19 -19
- package/dist/{cli-DuWWjaKT.d.ts → cli-lmf5RGyQ.d.ts} +3 -3
- package/dist/cli.d.ts +9 -9
- package/dist/cli.js +33 -33
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -7
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +8 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -7
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +7 -7
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +7 -7
- 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 +10 -10
- package/dist/index.js +76 -72
- 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 +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- 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 +8 -8
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +1 -1
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +7 -7
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync.d.ts +108 -1
- package/dist/offline-sync.js +7 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +15 -12
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/{orchestrator-CZI6hO_R.d.ts → orchestrator-D7bb8ZGp.d.ts} +193 -201
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +51 -41
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +9 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-D3aSiu9t.d.ts → semantic-consolidation-CTfbVwCA.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +7 -7
- 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 +6 -6
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-CZm6rx9-.d.ts → types-D4NYDtXI.d.ts} +7 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +7 -7
- package/package.json +2 -2
- package/src/access-service.ts +22 -0
- package/src/config.ts +5 -0
- package/src/index.ts +3 -1
- package/src/offline-sync.test.ts +354 -52
- package/src/offline-sync.ts +332 -24
- package/src/orchestration/extraction-run.ts +4 -34
- package/src/orchestration/namespace-read-fanout.ts +484 -0
- package/src/orchestration/orchestrator-helpers.ts +1130 -0
- package/src/orchestration/self-deps.ts +50 -0
- package/src/orchestration/workspace-ops.ts +789 -0
- package/src/orchestrator.ts +177 -2296
- package/src/types.ts +9 -0
- package/dist/chunk-24YDB5QP.js.map +0 -1
- package/dist/chunk-C5WN74HH.js.map +0 -1
- package/dist/chunk-PNUOQYRJ.js.map +0 -1
- package/dist/chunk-PWWWLD7D.js.map +0 -1
- /package/dist/{chunk-GKRLPHPU.js.map → chunk-2KK5WNQB.js.map} +0 -0
- /package/dist/{chunk-JRDPT6DA.js.map → chunk-4NWIGAIC.js.map} +0 -0
- /package/dist/{chunk-OBJUKYDO.js.map → chunk-4XL3CVII.js.map} +0 -0
- /package/dist/{chunk-WWVXXSLF.js.map → chunk-5PZCDUJ6.js.map} +0 -0
- /package/dist/{chunk-URQLO7BX.js.map → chunk-6FNZ3NMS.js.map} +0 -0
- /package/dist/{chunk-DRUDC2UX.js.map → chunk-6MMONPHM.js.map} +0 -0
- /package/dist/{chunk-RB6A7OUN.js.map → chunk-6SXNQFHB.js.map} +0 -0
- /package/dist/{chunk-ZGZNLRKF.js.map → chunk-7TWA7DKP.js.map} +0 -0
- /package/dist/{chunk-KMNYXLPL.js.map → chunk-B35L45HZ.js.map} +0 -0
- /package/dist/{chunk-SIV3JZPP.js.map → chunk-CGZWGVQF.js.map} +0 -0
- /package/dist/{chunk-CKFLLAIS.js.map → chunk-DANV2HTH.js.map} +0 -0
- /package/dist/{chunk-NVYF5OB6.js.map → chunk-DBRVU5PR.js.map} +0 -0
- /package/dist/{chunk-LMFS64GY.js.map → chunk-E4PFDD3N.js.map} +0 -0
- /package/dist/{chunk-DIHCFJVV.js.map → chunk-HYI2GP3F.js.map} +0 -0
- /package/dist/{chunk-UIKZZ242.js.map → chunk-IKHKHADN.js.map} +0 -0
- /package/dist/{chunk-R2OAN6IP.js.map → chunk-K2JM4DZZ.js.map} +0 -0
- /package/dist/{chunk-3IJGF6K2.js.map → chunk-K2R3DEEH.js.map} +0 -0
- /package/dist/{chunk-HIHCB2S6.js.map → chunk-QRRBXD24.js.map} +0 -0
- /package/dist/{chunk-ZPZ4VVLT.js.map → chunk-SD34EK4Z.js.map} +0 -0
- /package/dist/{chunk-WK3NK6SP.js.map → chunk-SNEXVX3U.js.map} +0 -0
- /package/dist/{chunk-FVZ2ISIX.js.map → chunk-TPLE6SNY.js.map} +0 -0
- /package/dist/{chunk-5IN26V6W.js.map → chunk-UU32EAIY.js.map} +0 -0
- /package/dist/{chunk-ZGAJB74V.js.map → chunk-W3BFT5OJ.js.map} +0 -0
- /package/dist/{chunk-L4N7SVNN.js.map → chunk-WA7FECSG.js.map} +0 -0
- /package/dist/{chunk-Q3X7GUAQ.js.map → chunk-WOX2UW4K.js.map} +0 -0
- /package/dist/{chunk-XTEWVF3C.js.map → chunk-WWSXVOGY.js.map} +0 -0
- /package/dist/{chunk-P4FW3ZU5.js.map → chunk-YGGXUNS4.js.map} +0 -0
- /package/dist/{chunk-JHQUIH3W.js.map → chunk-ZBVQ4ZM4.js.map} +0 -0
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator module-level helpers — relocated from orchestrator.ts
|
|
3
|
+
* (issue #1526, seam 25).
|
|
4
|
+
*
|
|
5
|
+
* Pure functions, types, and constants that previously lived at module
|
|
6
|
+
* level in orchestrator.ts: recall-mode planning, recall snapshots and
|
|
7
|
+
* parsers, abort/race helpers, artifact recall limits, replay
|
|
8
|
+
* source-time slicing, day-summary date utilities, and QMD startup
|
|
9
|
+
* checks. orchestrator.ts re-exports the previously-public names so
|
|
10
|
+
* every existing importer (coordinators, tests, root shims) keeps
|
|
11
|
+
* working unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Behavior-preserving move — no logic changes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { abortError as sharedAbortError, throwIfAborted as sharedThrowIfAborted } from "../abort-error.js";
|
|
20
|
+
import type { CapabilitySet } from "../capabilities.js";
|
|
21
|
+
import { buildCompressionGuidelinesMarkdown as buildCompressionGuidelinesMarkdownV2 } from "../compression-optimizer.js";
|
|
22
|
+
import { FallbackLlmClient } from "../fallback-llm.js";
|
|
23
|
+
import { hasBroadGraphIntent, planRecallMode } from "../intent.js";
|
|
24
|
+
import { resolveLifecycleState } from "../lifecycle.js";
|
|
25
|
+
import { log } from "../logger.js";
|
|
26
|
+
import type { GraphRecallRankedResult, GraphRecallShadowComparison } from "./graph-recall-coordinator.js";
|
|
27
|
+
import { parseQmdExplain } from "../qmd.js";
|
|
28
|
+
import type { GraphRecallExpandedEntry } from "../recall-state.js";
|
|
29
|
+
import type { RecallXrayServedBy } from "../recall-xray.js";
|
|
30
|
+
import { type BufferTurn, type MemoryActionEvent, type MemoryFile, type MemoryFrontmatter, type MemoryIntent, type PluginConfig, type QmdSearchResult, type RecallPlanMode, confidenceTier } from "../types.js";
|
|
31
|
+
import { categoryDirName } from "../utils/category-dir.js";
|
|
32
|
+
import { parseFlexibleIsoTimestamp } from "../utils/iso-timestamp.js";
|
|
33
|
+
|
|
34
|
+
export interface BulkImportBatchIngestResult {
|
|
35
|
+
attemptedTurnCount: number;
|
|
36
|
+
extractionCount: number;
|
|
37
|
+
persistedCount: number;
|
|
38
|
+
durableOutputCount: number;
|
|
39
|
+
skippedCount: number;
|
|
40
|
+
failedCount: number;
|
|
41
|
+
postPersistMetadataFailureCount: number;
|
|
42
|
+
processedTurnCount: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class BulkImportBatchPartialFailureError extends Error {
|
|
46
|
+
readonly partialResult: BulkImportBatchIngestResult;
|
|
47
|
+
|
|
48
|
+
readonly originalError: unknown;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
message: string,
|
|
52
|
+
partialResult: BulkImportBatchIngestResult,
|
|
53
|
+
originalError: unknown,
|
|
54
|
+
) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "BulkImportBatchPartialFailureError";
|
|
57
|
+
this.partialResult = partialResult;
|
|
58
|
+
this.originalError = originalError;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GraphRecallSnapshot {
|
|
63
|
+
recordedAt: string;
|
|
64
|
+
mode: RecallPlanMode | string;
|
|
65
|
+
queryHash: string;
|
|
66
|
+
queryLength: number;
|
|
67
|
+
namespaces: string[];
|
|
68
|
+
seedCount: number;
|
|
69
|
+
expandedCount: number;
|
|
70
|
+
seeds: string[];
|
|
71
|
+
expanded: GraphRecallExpandedEntry[];
|
|
72
|
+
status?: "completed" | "skipped" | "aborted";
|
|
73
|
+
reason?: string;
|
|
74
|
+
shadowMode?: boolean;
|
|
75
|
+
queryIntent?: MemoryIntent;
|
|
76
|
+
seedResults?: GraphRecallRankedResult[];
|
|
77
|
+
finalResults?: GraphRecallRankedResult[];
|
|
78
|
+
shadowComparison?: GraphRecallShadowComparison;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface IntentDebugSnapshot {
|
|
82
|
+
recordedAt: string;
|
|
83
|
+
promptHash: string;
|
|
84
|
+
promptLength: number;
|
|
85
|
+
retrievalQueryHash: string;
|
|
86
|
+
retrievalQueryLength: number;
|
|
87
|
+
plannerEnabled: boolean;
|
|
88
|
+
plannedMode: RecallPlanMode;
|
|
89
|
+
effectiveMode: RecallPlanMode;
|
|
90
|
+
recallResultLimit: number;
|
|
91
|
+
queryIntent: MemoryIntent;
|
|
92
|
+
graphExpandedIntentDetected: boolean;
|
|
93
|
+
graphDecision: {
|
|
94
|
+
status: "not_requested" | "skipped" | "completed" | "aborted";
|
|
95
|
+
reason?: string;
|
|
96
|
+
shadowMode: boolean;
|
|
97
|
+
qmdAvailable: boolean;
|
|
98
|
+
graphRecallEnabled: boolean;
|
|
99
|
+
multiGraphMemoryEnabled: boolean;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface QmdRecallSnapshot {
|
|
104
|
+
recordedAt: string;
|
|
105
|
+
queryHash: string;
|
|
106
|
+
queryLength: number;
|
|
107
|
+
collection?: string;
|
|
108
|
+
namespaces: string[];
|
|
109
|
+
fetchLimit: number;
|
|
110
|
+
primaryResultCount: number;
|
|
111
|
+
hybridResultCount: number;
|
|
112
|
+
queryAwareSeedCount: number;
|
|
113
|
+
resultCount: number;
|
|
114
|
+
intentHint?: string;
|
|
115
|
+
explainEnabled: boolean;
|
|
116
|
+
hybridTopUpUsed: boolean;
|
|
117
|
+
hybridTopUpSkippedReason?: string;
|
|
118
|
+
results: QmdSearchResult[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface RecallModeDecision {
|
|
122
|
+
plannedMode: RecallPlanMode;
|
|
123
|
+
effectiveMode: RecallPlanMode;
|
|
124
|
+
graphExpandedIntentDetected: boolean;
|
|
125
|
+
graphReason?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Where `plannedMode` came from (issue #1367 / Option C). `"heuristic"` for
|
|
128
|
+
* the regex planner; `"llm"` when the LLM planner classified it; and
|
|
129
|
+
* `"heuristic-fallback"` when the LLM was enabled but errored/timed out and we
|
|
130
|
+
* fell back. Absent on the synchronous heuristic-only path.
|
|
131
|
+
*/
|
|
132
|
+
plannerSource?: "heuristic" | "llm" | "heuristic-fallback";
|
|
133
|
+
/** Short rationale from the planner (for telemetry / x-ray). */
|
|
134
|
+
plannerReason?: string;
|
|
135
|
+
/** Wall-clock spent in the LLM planner call, when one was made. */
|
|
136
|
+
plannerLatencyMs?: number;
|
|
137
|
+
/** True when the LLM planner was enabled but fell back to the heuristic. */
|
|
138
|
+
plannerFallbackUsed?: boolean;
|
|
139
|
+
/** Model that served the LLM classification, when one was used. */
|
|
140
|
+
plannerModelUsed?: string;
|
|
141
|
+
/**
|
|
142
|
+
* The regex-heuristic baseline mode, captured whenever the LLM planner ran
|
|
143
|
+
* (any source). Lets operators compare planned-vs-heuristic during rollout —
|
|
144
|
+
* distinct from `plannedMode`, which on the LLM path is the LLM's choice.
|
|
145
|
+
*/
|
|
146
|
+
plannerHeuristicMode?: RecallPlanMode;
|
|
147
|
+
/**
|
|
148
|
+
* In shadow mode, the mode the LLM *would* have chosen (recorded for
|
|
149
|
+
* comparison) while `effectiveMode` stays on the heuristic decision.
|
|
150
|
+
*/
|
|
151
|
+
shadowLlmMode?: RecallPlanMode;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Map the orchestrator's internal `recallSource` strings to the
|
|
156
|
+
* X-ray `servedBy` vocabulary (issue #570 PR 1). The X-ray tier
|
|
157
|
+
* ladder intentionally flattens QMD / embedding / cold-fallback to
|
|
158
|
+
* the `hybrid` tier because they all materialize through the same
|
|
159
|
+
* hybrid BM25+vector pipeline from the caller's perspective. The
|
|
160
|
+
* `recent_scan` path gets its own dedicated tier because it bypasses
|
|
161
|
+
* the hybrid pipeline entirely. `none` is treated as `hybrid` on the
|
|
162
|
+
* theory that a query that returned nothing still routed through the
|
|
163
|
+
* hybrid pipeline — but callers should normally gate capture on
|
|
164
|
+
* `recalledMemoryIds.length > 0`.
|
|
165
|
+
*/
|
|
166
|
+
export function mapRecallSourceToXrayServedBy(
|
|
167
|
+
source:
|
|
168
|
+
| "none"
|
|
169
|
+
| "hot_qmd"
|
|
170
|
+
| "hot_embedding"
|
|
171
|
+
| "cold_fallback"
|
|
172
|
+
| "recent_scan",
|
|
173
|
+
): RecallXrayServedBy {
|
|
174
|
+
// Exhaustive switch: every current union member is explicitly
|
|
175
|
+
// listed so TypeScript surfaces a compile error if a new source is
|
|
176
|
+
// added without a deliberate mapping. The `never`-typed fallthrough
|
|
177
|
+
// keeps the function total at runtime — if the caller passes an
|
|
178
|
+
// unexpected value that slipped past the type system (e.g. a JSON
|
|
179
|
+
// deserialization), we still fall back to `hybrid`.
|
|
180
|
+
switch (source) {
|
|
181
|
+
case "recent_scan":
|
|
182
|
+
return "recent-scan";
|
|
183
|
+
case "hot_qmd":
|
|
184
|
+
case "hot_embedding":
|
|
185
|
+
case "cold_fallback":
|
|
186
|
+
case "none":
|
|
187
|
+
return "hybrid";
|
|
188
|
+
}
|
|
189
|
+
const _exhaustive: never = source;
|
|
190
|
+
void _exhaustive;
|
|
191
|
+
return "hybrid";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface RecallInvocationOptions {
|
|
195
|
+
namespace?: string;
|
|
196
|
+
topK?: number;
|
|
197
|
+
mode?: RecallPlanMode;
|
|
198
|
+
abortSignal?: AbortSignal;
|
|
199
|
+
/**
|
|
200
|
+
* Capture a `RecallXraySnapshot` for this recall (issue #570). When
|
|
201
|
+
* `true`, the orchestrator builds a snapshot from the data it has
|
|
202
|
+
* already gathered and stashes it in memory, accessible via
|
|
203
|
+
* `getLastXraySnapshot()`. When `false` or absent, nothing is
|
|
204
|
+
* captured and recall behavior is unchanged (schema-only slice).
|
|
205
|
+
*/
|
|
206
|
+
xrayCapture?: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Per-invocation override for `recallBudgetChars` (issue #570 PR 3/4).
|
|
209
|
+
* Flows through `getRecallBudgetChars()` for this recall only — no
|
|
210
|
+
* shared config mutation, so concurrent recalls on the same
|
|
211
|
+
* orchestrator are not affected (CLAUDE.md rule 47: no shared
|
|
212
|
+
* mutable state across async boundaries). Must be a non-negative
|
|
213
|
+
* finite integer; non-conforming values are ignored and the
|
|
214
|
+
* configured budget is used.
|
|
215
|
+
*/
|
|
216
|
+
budgetCharsOverride?: number;
|
|
217
|
+
/**
|
|
218
|
+
* Per-invocation principal override (issue #570 PR 4). When set,
|
|
219
|
+
* the orchestrator uses this principal for ACL / namespace checks
|
|
220
|
+
* instead of `resolvePrincipal(sessionKey, config)`. This is the
|
|
221
|
+
* escape hatch for access surfaces (HTTP / MCP) that have already
|
|
222
|
+
* authenticated the caller upstream — threading an unmapped
|
|
223
|
+
* principal through the session-key-based resolver would otherwise
|
|
224
|
+
* collapse it to `"default"` and produce false denials in
|
|
225
|
+
* namespace-enabled deployments (CLAUDE.md rule 42).
|
|
226
|
+
*/
|
|
227
|
+
principalOverride?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Historical recall point (issue #680). When set, the orchestrator
|
|
230
|
+
* filters out memories whose `valid_at` is after this timestamp OR
|
|
231
|
+
* whose `invalid_at` is at-or-before this timestamp, so callers see
|
|
232
|
+
* the corpus as it existed at `asOf`. ISO 8601 string; comparisons
|
|
233
|
+
* use `Date.parse()` so timezone-aware values round-trip correctly
|
|
234
|
+
* (CLAUDE.md gotcha — never compare ISO strings lexicographically).
|
|
235
|
+
* Invalid values must be rejected at input boundaries (CLAUDE.md
|
|
236
|
+
* rule 51); the orchestrator does NOT silently fall back here.
|
|
237
|
+
*/
|
|
238
|
+
asOf?: string;
|
|
239
|
+
/**
|
|
240
|
+
* Issue #681 — when `true`, bypasses `graphTraversalConfidenceFloor`
|
|
241
|
+
* and includes edges below the floor in graph traversal. Useful for
|
|
242
|
+
* diagnostic recall queries that need to surface results that would
|
|
243
|
+
* normally be pruned by confidence decay. Default `false`.
|
|
244
|
+
*/
|
|
245
|
+
includeLowConfidence?: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* User-aware context scopes active for this recall. Used by X-ray
|
|
248
|
+
* provenance safety checks so boundary-scoped memories are evaluated
|
|
249
|
+
* against the caller's real context.
|
|
250
|
+
*/
|
|
251
|
+
currentContextScopes?: readonly unknown[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type QueryAwarePrefilter = {
|
|
255
|
+
candidatePaths: Set<string> | null;
|
|
256
|
+
temporalFromDate: string | null;
|
|
257
|
+
matchedTags: string[];
|
|
258
|
+
expandedTags: string[];
|
|
259
|
+
combination: "none" | "temporal" | "tag" | "intersection" | "union";
|
|
260
|
+
filteredToFullSearch: boolean;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Recall-specific abort helpers. Thin wrappers over the shared
|
|
264
|
+
// `abort-error.ts` module so every abort in the codebase shares the
|
|
265
|
+
// same `name === "AbortError"` classification contract (`isAbortError`
|
|
266
|
+
// works uniformly). We keep the "recall aborted" default message for
|
|
267
|
+
// back-compat with call-site logs; callers that pass an explicit
|
|
268
|
+
// message (e.g. "extraction aborted (before_extract)") are unaffected.
|
|
269
|
+
export const abortRecallError = sharedAbortError;
|
|
270
|
+
|
|
271
|
+
export function throwIfRecallAborted(
|
|
272
|
+
signal?: AbortSignal,
|
|
273
|
+
message = "recall aborted",
|
|
274
|
+
): void {
|
|
275
|
+
sharedThrowIfAborted(signal, message);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function raceRecallAbort<T>(
|
|
279
|
+
promise: Promise<T>,
|
|
280
|
+
signal?: AbortSignal,
|
|
281
|
+
message = "recall aborted",
|
|
282
|
+
): Promise<T> {
|
|
283
|
+
throwIfRecallAborted(signal, message);
|
|
284
|
+
if (!signal) return promise;
|
|
285
|
+
|
|
286
|
+
let onAbort: (() => void) | null = null;
|
|
287
|
+
const abortPromise = new Promise<T>((_resolve, reject) => {
|
|
288
|
+
onAbort = () => reject(abortRecallError(message));
|
|
289
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
return await Promise.race([promise, abortPromise]);
|
|
294
|
+
} finally {
|
|
295
|
+
if (onAbort) {
|
|
296
|
+
signal.removeEventListener("abort", onAbort);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Maximum age (ms) before a compaction-reset signal file is considered stale and removed. */
|
|
302
|
+
export const COMPACTION_SIGNAL_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
303
|
+
|
|
304
|
+
export const DEFAULT_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS = 10_000;
|
|
305
|
+
|
|
306
|
+
export type DaySummaryGatherOptions = {
|
|
307
|
+
timeZone?: string;
|
|
308
|
+
now?: Date;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export function normalizeIanaTimeZone(value: unknown): string | undefined {
|
|
312
|
+
if (typeof value !== "string") return undefined;
|
|
313
|
+
const trimmed = value.trim();
|
|
314
|
+
if (!trimmed) return undefined;
|
|
315
|
+
try {
|
|
316
|
+
Intl.DateTimeFormat(undefined, { timeZone: trimmed });
|
|
317
|
+
return trimmed;
|
|
318
|
+
} catch {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function formatDateInTimeZone(date: Date, timeZone: string): string {
|
|
324
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
325
|
+
timeZone,
|
|
326
|
+
year: "numeric",
|
|
327
|
+
month: "2-digit",
|
|
328
|
+
day: "2-digit",
|
|
329
|
+
}).formatToParts(date);
|
|
330
|
+
const get = (type: string): string => parts.find((part) => part.type === type)?.value ?? "";
|
|
331
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function utcDateKey(date: Date): string {
|
|
335
|
+
return date.toISOString().slice(0, 10);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function utcDateKeysAround(date: Date): string[] {
|
|
339
|
+
const dayMs = 86_400_000;
|
|
340
|
+
const keys = [
|
|
341
|
+
utcDateKey(new Date(date.getTime() - dayMs)),
|
|
342
|
+
utcDateKey(date),
|
|
343
|
+
utcDateKey(new Date(date.getTime() + dayMs)),
|
|
344
|
+
];
|
|
345
|
+
return keys.filter((value, index, array) => array.indexOf(value) === index);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function utcDateKeysForLocalDay(date: Date, timeZone: string): string[] {
|
|
349
|
+
const targetLocalDate = formatDateInTimeZone(date, timeZone);
|
|
350
|
+
const keys = new Set<string>();
|
|
351
|
+
const hourMs = 3_600_000;
|
|
352
|
+
const scanStart = date.getTime() - 48 * hourMs;
|
|
353
|
+
const scanEnd = date.getTime() + 48 * hourMs;
|
|
354
|
+
for (let ms = scanStart; ms <= scanEnd; ms += hourMs) {
|
|
355
|
+
const candidate = new Date(ms);
|
|
356
|
+
if (formatDateInTimeZone(candidate, timeZone) === targetLocalDate) {
|
|
357
|
+
keys.add(utcDateKey(candidate));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return keys.size > 0 ? [...keys].sort() : utcDateKeysAround(date);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function parseFiniteDate(value: unknown): Date | null {
|
|
364
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
365
|
+
const parsed = new Date(value);
|
|
366
|
+
return Number.isFinite(parsed.getTime()) ? parsed : null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function filterHourlySummaryMarkdownForLocalDay(
|
|
370
|
+
raw: string,
|
|
371
|
+
utcDate: string,
|
|
372
|
+
timeZone: string,
|
|
373
|
+
targetLocalDate: string,
|
|
374
|
+
): string | null {
|
|
375
|
+
const hourHeaderPattern = /^## ([01]\d|2[0-3]):00[ \t]*$/gm;
|
|
376
|
+
const matches = Array.from(raw.matchAll(hourHeaderPattern));
|
|
377
|
+
if (matches.length === 0) return null;
|
|
378
|
+
|
|
379
|
+
const firstSectionStart = matches[0]?.index ?? 0;
|
|
380
|
+
const preamble = raw.slice(0, firstSectionStart).trim();
|
|
381
|
+
const sections: string[] = [];
|
|
382
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
383
|
+
const match = matches[index];
|
|
384
|
+
const hour = match[1];
|
|
385
|
+
if (!hour) continue;
|
|
386
|
+
const sectionTimestamp = parseFiniteDate(`${utcDate}T${hour}:00:00.000Z`);
|
|
387
|
+
if (
|
|
388
|
+
!sectionTimestamp ||
|
|
389
|
+
formatDateInTimeZone(sectionTimestamp, timeZone) !== targetLocalDate
|
|
390
|
+
) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const sectionStart = match.index ?? 0;
|
|
394
|
+
const sectionEnd = matches[index + 1]?.index ?? raw.length;
|
|
395
|
+
const section = raw.slice(sectionStart, sectionEnd).trim();
|
|
396
|
+
if (section.length > 0) sections.push(section);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (sections.length === 0) return null;
|
|
400
|
+
return [preamble, ...sections]
|
|
401
|
+
.filter((section) => section.length > 0)
|
|
402
|
+
.join("\n\n");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export type SearchCollectionState = "present" | "missing" | "unknown" | "skipped";
|
|
406
|
+
|
|
407
|
+
export function qmdStartupCollectionCheckTimeoutMs(): number {
|
|
408
|
+
const raw =
|
|
409
|
+
process.env.REMNIC_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS ??
|
|
410
|
+
process.env.ENGRAM_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS;
|
|
411
|
+
if (raw === undefined) return DEFAULT_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS;
|
|
412
|
+
const parsed = Number(raw);
|
|
413
|
+
return Number.isFinite(parsed) && parsed >= 1_000
|
|
414
|
+
? Math.floor(parsed)
|
|
415
|
+
: DEFAULT_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function qmdStartupCollectionCheckWithTimeout(
|
|
419
|
+
promise: Promise<SearchCollectionState>,
|
|
420
|
+
controller: AbortController,
|
|
421
|
+
label: string,
|
|
422
|
+
): Promise<SearchCollectionState> {
|
|
423
|
+
const timeoutMs = qmdStartupCollectionCheckTimeoutMs();
|
|
424
|
+
let timer: NodeJS.Timeout | undefined;
|
|
425
|
+
let settled = false;
|
|
426
|
+
|
|
427
|
+
const timeoutPromise = new Promise<SearchCollectionState>((resolve) => {
|
|
428
|
+
timer = setTimeout(() => {
|
|
429
|
+
if (settled) return;
|
|
430
|
+
controller.abort();
|
|
431
|
+
log.warn(
|
|
432
|
+
`QMD startup collection check for ${label} timed out after ${timeoutMs}ms; keeping search enabled fail-open`,
|
|
433
|
+
);
|
|
434
|
+
resolve("unknown");
|
|
435
|
+
}, timeoutMs);
|
|
436
|
+
timer.unref?.();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const checkedPromise = promise
|
|
440
|
+
.catch((err): SearchCollectionState => {
|
|
441
|
+
log.warn(
|
|
442
|
+
`QMD startup collection check for ${label} failed; keeping search enabled fail-open: ${err}`,
|
|
443
|
+
);
|
|
444
|
+
return "unknown";
|
|
445
|
+
})
|
|
446
|
+
.finally(() => {
|
|
447
|
+
settled = true;
|
|
448
|
+
if (timer) clearTimeout(timer);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
return await Promise.race([checkedPromise, timeoutPromise]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Default workspace directory when no per-agent or config workspace is available. */
|
|
455
|
+
export function defaultWorkspaceDir(): string {
|
|
456
|
+
return path.join(os.homedir(), ".openclaw", "workspace");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Produce a collision-resistant, filesystem-safe identifier from a session key.
|
|
461
|
+
*
|
|
462
|
+
* Session keys follow colon-delimited forms (e.g., `agent:gpucodebot:main`).
|
|
463
|
+
* A naive replace (`:` → `_`) is lossy: different keys like `agent:alpha` and
|
|
464
|
+
* `agent/alpha` would collide. Instead we append a short SHA-256 hash of the
|
|
465
|
+
* original key to the human-readable sanitized prefix, guaranteeing uniqueness
|
|
466
|
+
* while keeping filenames debuggable.
|
|
467
|
+
*
|
|
468
|
+
* Format: `<sanitized>-<12-char-hex-hash>`
|
|
469
|
+
* Example: `agent:gpucodebot:main` → `agent_gpucodebot_main-a1b2c3d4e5f6`
|
|
470
|
+
*/
|
|
471
|
+
export function sanitizeSessionKeyForFilename(sessionKey: string): string {
|
|
472
|
+
const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
473
|
+
const hash = createHash("sha256")
|
|
474
|
+
.update(sessionKey)
|
|
475
|
+
.digest("hex")
|
|
476
|
+
.slice(0, 12);
|
|
477
|
+
return `${readable}-${hash}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function sourceValidAtMs(turn: BufferTurn): number | null {
|
|
481
|
+
if (typeof turn.sourceValidAt !== "string") return null;
|
|
482
|
+
return parseFlexibleIsoTimestamp(turn.sourceValidAt.trim());
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export const SOURCE_VALID_AT_CONTEXT_TURNS = 2;
|
|
486
|
+
|
|
487
|
+
export function sourceValidAtSliceKey(turn: BufferTurn, index: number): string {
|
|
488
|
+
const validAtMs = sourceValidAtMs(turn);
|
|
489
|
+
return validAtMs === null ? `unknown:${index}` : String(validAtMs);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function asExtractionContextTurn(turn: BufferTurn): BufferTurn {
|
|
493
|
+
return { ...turn, extractionContextOnly: true };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function asExtractionTargetTurn(turn: BufferTurn): BufferTurn {
|
|
497
|
+
const { extractionContextOnly: _contextOnly, ...targetTurn } = turn;
|
|
498
|
+
return targetTurn;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function sourceValidAtContextTurns(
|
|
502
|
+
turns: readonly BufferTurn[],
|
|
503
|
+
targetStart: number,
|
|
504
|
+
targetEnd: number,
|
|
505
|
+
targetValidAtMs: number | null,
|
|
506
|
+
): BufferTurn[] {
|
|
507
|
+
if (targetValidAtMs === null) return [];
|
|
508
|
+
return turns
|
|
509
|
+
.flatMap((turn, index) => {
|
|
510
|
+
if (index >= targetStart && index < targetEnd) return [];
|
|
511
|
+
const contextValidAtMs = sourceValidAtMs(turn);
|
|
512
|
+
if (contextValidAtMs === null || contextValidAtMs > targetValidAtMs) {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
return [{ turn, index, validAtMs: contextValidAtMs }];
|
|
516
|
+
})
|
|
517
|
+
.sort((a, b) => {
|
|
518
|
+
if (a.validAtMs < b.validAtMs) return -1;
|
|
519
|
+
if (a.validAtMs > b.validAtMs) return 1;
|
|
520
|
+
if (a.index === b.index) return 0;
|
|
521
|
+
return a.index < b.index ? -1 : 1;
|
|
522
|
+
})
|
|
523
|
+
.slice(-SOURCE_VALID_AT_CONTEXT_TURNS)
|
|
524
|
+
.map(({ turn }) => asExtractionContextTurn(turn));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function targetSourceValidAtSortMs(turns: readonly BufferTurn[]): number {
|
|
528
|
+
let latestMs: number | null = null;
|
|
529
|
+
for (const turn of turns) {
|
|
530
|
+
if (turn.extractionContextOnly === true) continue;
|
|
531
|
+
const validAtMs = sourceValidAtMs(turn);
|
|
532
|
+
if (validAtMs === null) continue;
|
|
533
|
+
if (latestMs === null || validAtMs > latestMs) {
|
|
534
|
+
latestMs = validAtMs;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return latestMs ?? Number.POSITIVE_INFINITY;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function sortSourceValidAtSlicesChronologically(
|
|
541
|
+
slices: BufferTurn[][],
|
|
542
|
+
): BufferTurn[][] {
|
|
543
|
+
return slices
|
|
544
|
+
.map((turns, order) => ({
|
|
545
|
+
turns,
|
|
546
|
+
order,
|
|
547
|
+
targetValidAtMs: targetSourceValidAtSortMs(turns),
|
|
548
|
+
}))
|
|
549
|
+
.sort((a, b) => {
|
|
550
|
+
if (a.targetValidAtMs < b.targetValidAtMs) return -1;
|
|
551
|
+
if (a.targetValidAtMs > b.targetValidAtMs) return 1;
|
|
552
|
+
if (a.order === b.order) return 0;
|
|
553
|
+
return a.order < b.order ? -1 : 1;
|
|
554
|
+
})
|
|
555
|
+
.map((slice) => slice.turns);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function splitTurnsBySourceValidAt(
|
|
559
|
+
turns: readonly BufferTurn[],
|
|
560
|
+
options: { includeContext?: boolean } = {},
|
|
561
|
+
): BufferTurn[][] {
|
|
562
|
+
if (turns.length === 0) return [];
|
|
563
|
+
if (!turns.some((turn) => sourceValidAtMs(turn) !== null)) {
|
|
564
|
+
return [[...turns]];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const slices: BufferTurn[][] = [];
|
|
568
|
+
let start = 0;
|
|
569
|
+
while (start < turns.length) {
|
|
570
|
+
const targetValidAtMs = sourceValidAtMs(turns[start]);
|
|
571
|
+
const activeKey = sourceValidAtSliceKey(turns[start], start);
|
|
572
|
+
let end = start + 1;
|
|
573
|
+
while (
|
|
574
|
+
end < turns.length &&
|
|
575
|
+
sourceValidAtSliceKey(turns[end], end) === activeKey
|
|
576
|
+
) {
|
|
577
|
+
end += 1;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const contextTurns =
|
|
581
|
+
options.includeContext === false
|
|
582
|
+
? []
|
|
583
|
+
: sourceValidAtContextTurns(turns, start, end, targetValidAtMs);
|
|
584
|
+
slices.push([
|
|
585
|
+
...contextTurns,
|
|
586
|
+
...turns.slice(start, end).map(asExtractionTargetTurn),
|
|
587
|
+
]);
|
|
588
|
+
start = end;
|
|
589
|
+
}
|
|
590
|
+
return sortSourceValidAtSlicesChronologically(slices);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function isArtifactMemoryPath(filePath: string): boolean {
|
|
594
|
+
return /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(filePath);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function buildCompressionGuidelinesMarkdown(
|
|
598
|
+
events: MemoryActionEvent[],
|
|
599
|
+
generatedAtIso: string = new Date().toISOString(),
|
|
600
|
+
): string {
|
|
601
|
+
return buildCompressionGuidelinesMarkdownV2(events, generatedAtIso);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function filterRecallCandidates(
|
|
605
|
+
candidates: QmdSearchResult[],
|
|
606
|
+
options: {
|
|
607
|
+
namespacesEnabled: boolean;
|
|
608
|
+
recallNamespaces: string[];
|
|
609
|
+
resolveNamespace: (path: string) => string;
|
|
610
|
+
limit: number;
|
|
611
|
+
},
|
|
612
|
+
): QmdSearchResult[] {
|
|
613
|
+
const scopedByNamespace = options.namespacesEnabled
|
|
614
|
+
? candidates.filter((r) =>
|
|
615
|
+
options.recallNamespaces.includes(options.resolveNamespace(r.path)),
|
|
616
|
+
)
|
|
617
|
+
: candidates;
|
|
618
|
+
return scopedByNamespace
|
|
619
|
+
.filter((r) => !isArtifactMemoryPath(r.path))
|
|
620
|
+
.slice(0, Math.max(0, options.limit));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function applyQueryAwareCandidateFilter(
|
|
624
|
+
candidates: QmdSearchResult[],
|
|
625
|
+
candidatePaths: Set<string> | null,
|
|
626
|
+
): QmdSearchResult[] {
|
|
627
|
+
if (!candidatePaths) return candidates;
|
|
628
|
+
if (candidatePaths.size === 0) return [];
|
|
629
|
+
const filtered = candidates.filter((candidate) =>
|
|
630
|
+
candidatePaths.has(candidate.path),
|
|
631
|
+
);
|
|
632
|
+
return filtered.length > 0 ? filtered : candidates;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function tokenizeRecallQuery(prompt: string): string[] {
|
|
636
|
+
return prompt
|
|
637
|
+
.toLowerCase()
|
|
638
|
+
.split(/[^a-z0-9]+/i)
|
|
639
|
+
.map((t) => t.trim())
|
|
640
|
+
.filter((t) => t.length >= 3);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function hasLifecycleMetadata(frontmatter: MemoryFrontmatter): boolean {
|
|
644
|
+
return (
|
|
645
|
+
frontmatter.lifecycleState !== undefined ||
|
|
646
|
+
frontmatter.verificationState !== undefined ||
|
|
647
|
+
frontmatter.policyClass !== undefined ||
|
|
648
|
+
frontmatter.lastValidatedAt !== undefined ||
|
|
649
|
+
frontmatter.decayScore !== undefined ||
|
|
650
|
+
frontmatter.heatScore !== undefined
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function shouldFilterLifecycleRecallCandidate(
|
|
655
|
+
frontmatter: MemoryFrontmatter,
|
|
656
|
+
options: {
|
|
657
|
+
lifecyclePolicyEnabled: boolean;
|
|
658
|
+
lifecycleFilterStaleEnabled: boolean;
|
|
659
|
+
},
|
|
660
|
+
): boolean {
|
|
661
|
+
if (!options.lifecyclePolicyEnabled || !options.lifecycleFilterStaleEnabled)
|
|
662
|
+
return false;
|
|
663
|
+
if (!hasLifecycleMetadata(frontmatter)) return false;
|
|
664
|
+
const lifecycleState = resolveLifecycleState(frontmatter);
|
|
665
|
+
return lifecycleState === "stale" || lifecycleState === "archived";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function lifecycleRecallScoreAdjustment(
|
|
669
|
+
frontmatter: MemoryFrontmatter,
|
|
670
|
+
options: {
|
|
671
|
+
lifecyclePolicyEnabled: boolean;
|
|
672
|
+
},
|
|
673
|
+
): number {
|
|
674
|
+
if (!options.lifecyclePolicyEnabled) return 0;
|
|
675
|
+
if (!hasLifecycleMetadata(frontmatter)) return 0;
|
|
676
|
+
|
|
677
|
+
let delta = 0;
|
|
678
|
+
const lifecycleState = resolveLifecycleState(frontmatter);
|
|
679
|
+
switch (lifecycleState) {
|
|
680
|
+
case "active":
|
|
681
|
+
delta += 0.05;
|
|
682
|
+
break;
|
|
683
|
+
case "validated":
|
|
684
|
+
delta += 0.03;
|
|
685
|
+
break;
|
|
686
|
+
case "candidate":
|
|
687
|
+
delta -= 0.01;
|
|
688
|
+
break;
|
|
689
|
+
case "stale":
|
|
690
|
+
delta -= 0.06;
|
|
691
|
+
break;
|
|
692
|
+
case "archived":
|
|
693
|
+
delta -= 0.08;
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
if (frontmatter.verificationState === "disputed") {
|
|
697
|
+
delta -= 0.12;
|
|
698
|
+
}
|
|
699
|
+
return delta;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function computeArtifactRecallLimit(
|
|
703
|
+
recallMode: RecallPlanMode,
|
|
704
|
+
recallResultLimit: number,
|
|
705
|
+
verbatimArtifactsMaxRecall: number,
|
|
706
|
+
): number {
|
|
707
|
+
if (recallMode === "no_recall") return 0;
|
|
708
|
+
if (Math.max(0, recallResultLimit) === 0) return 0;
|
|
709
|
+
const base = Math.max(0, verbatimArtifactsMaxRecall);
|
|
710
|
+
if (recallMode === "minimal") {
|
|
711
|
+
return Math.min(base, Math.max(0, recallResultLimit));
|
|
712
|
+
}
|
|
713
|
+
return base;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function resolveEffectiveRecallMode(options: {
|
|
717
|
+
plannerEnabled: boolean;
|
|
718
|
+
graphRecallEnabled: boolean;
|
|
719
|
+
multiGraphMemoryEnabled: boolean;
|
|
720
|
+
graphExpandedIntentEnabled?: boolean;
|
|
721
|
+
prompt: string;
|
|
722
|
+
}): RecallPlanMode {
|
|
723
|
+
return resolveRecallModeDecision(options).effectiveMode;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export interface RecallModeGraphOptions {
|
|
727
|
+
plannerEnabled: boolean;
|
|
728
|
+
graphRecallEnabled: boolean;
|
|
729
|
+
multiGraphMemoryEnabled: boolean;
|
|
730
|
+
graphExpandedIntentEnabled?: boolean;
|
|
731
|
+
prompt: string;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Apply the graph-mode overlay + gating to a planner-produced mode.
|
|
736
|
+
*
|
|
737
|
+
* Shared by the heuristic ({@link resolveRecallModeDecision}) and LLM
|
|
738
|
+
* ({@link resolveRecallModeDecisionAsync}) paths so graph promotion and the
|
|
739
|
+
* "graph disabled → fall back to full" gating behave identically regardless of
|
|
740
|
+
* which planner produced `plannedModeRaw` (gotcha #39).
|
|
741
|
+
*/
|
|
742
|
+
export function finalizeRecallModeDecision(
|
|
743
|
+
plannedModeRaw: RecallPlanMode,
|
|
744
|
+
options: RecallModeGraphOptions,
|
|
745
|
+
): RecallModeDecision {
|
|
746
|
+
let plannedMode: RecallPlanMode = plannedModeRaw;
|
|
747
|
+
const graphExpandedIntentDetected =
|
|
748
|
+
options.plannerEnabled &&
|
|
749
|
+
options.graphExpandedIntentEnabled === true &&
|
|
750
|
+
hasBroadGraphIntent(options.prompt);
|
|
751
|
+
if (plannedMode !== "graph_mode" && graphExpandedIntentDetected) {
|
|
752
|
+
plannedMode = "graph_mode";
|
|
753
|
+
}
|
|
754
|
+
if (
|
|
755
|
+
plannedMode === "graph_mode" &&
|
|
756
|
+
(!options.graphRecallEnabled || !options.multiGraphMemoryEnabled)
|
|
757
|
+
) {
|
|
758
|
+
return {
|
|
759
|
+
plannedMode,
|
|
760
|
+
effectiveMode: "full",
|
|
761
|
+
graphExpandedIntentDetected,
|
|
762
|
+
graphReason: !options.graphRecallEnabled
|
|
763
|
+
? "graph recall disabled by config"
|
|
764
|
+
: "multi-graph memory disabled by config",
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
plannedMode,
|
|
769
|
+
effectiveMode: plannedMode,
|
|
770
|
+
graphExpandedIntentDetected,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function resolveRecallModeDecision(options: RecallModeGraphOptions): RecallModeDecision {
|
|
775
|
+
const plannedMode: RecallPlanMode = options.plannerEnabled
|
|
776
|
+
? planRecallMode(options.prompt)
|
|
777
|
+
: "full";
|
|
778
|
+
return finalizeRecallModeDecision(plannedMode, options);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Async recall-mode decision with optional LLM-based planning (issue #1367 /
|
|
783
|
+
* Option C). Falls back to the heuristic decision when the LLM planner is
|
|
784
|
+
* disabled, in shadow mode, or unavailable/failed — so this is always safe to
|
|
785
|
+
* await on the recall hot path. Provider-agnostic: the LLM call routes through
|
|
786
|
+
* the gateway/fallback chain.
|
|
787
|
+
*
|
|
788
|
+
* `recallPlannerEnabled === false` keeps the legacy "always full" behavior and
|
|
789
|
+
* skips the LLM entirely (the planner as a whole is off).
|
|
790
|
+
*/
|
|
791
|
+
export async function resolveRecallModeDecisionAsync(
|
|
792
|
+
options: RecallModeGraphOptions & {
|
|
793
|
+
config: PluginConfig;
|
|
794
|
+
/**
|
|
795
|
+
* Recall-operation capability gates (issue #1523). REQUIRED: the recall
|
|
796
|
+
* orchestrator always passes a resolved set — the LLM planner gate reads
|
|
797
|
+
* `caps.recallPlannerLlm`, never re-derives from config.
|
|
798
|
+
*/
|
|
799
|
+
caps: CapabilitySet;
|
|
800
|
+
hints?: string[];
|
|
801
|
+
llm?: FallbackLlmClient;
|
|
802
|
+
signal?: AbortSignal;
|
|
803
|
+
},
|
|
804
|
+
): Promise<RecallModeDecision> {
|
|
805
|
+
const heuristicDecision = resolveRecallModeDecision(options);
|
|
806
|
+
|
|
807
|
+
// Planner globally off, or LLM planning not opted into → heuristic only.
|
|
808
|
+
// Read the resolved capability (issue #1523) — never re-derive from config.
|
|
809
|
+
const plannerLlmEnabled = options.caps.recallPlannerLlm;
|
|
810
|
+
if (!options.plannerEnabled || !plannerLlmEnabled) {
|
|
811
|
+
return heuristicDecision;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const { planRecallModeLLM } = await import("../recall-planner-llm.js");
|
|
815
|
+
const planned = await planRecallModeLLM(
|
|
816
|
+
options.prompt,
|
|
817
|
+
options.hints,
|
|
818
|
+
options.config,
|
|
819
|
+
options.caps,
|
|
820
|
+
options.llm,
|
|
821
|
+
options.signal,
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// Shadow mode: record what the LLM would have chosen but keep the heuristic
|
|
825
|
+
// effective decision (safe rollout / comparison — gotcha #30).
|
|
826
|
+
if (options.config.recallPlannerShadowMode) {
|
|
827
|
+
return {
|
|
828
|
+
...heuristicDecision,
|
|
829
|
+
plannerSource: planned.source,
|
|
830
|
+
plannerReason: `shadow:${planned.reason}`,
|
|
831
|
+
plannerLatencyMs: planned.latencyMs,
|
|
832
|
+
plannerFallbackUsed: planned.fallbackUsed,
|
|
833
|
+
plannerModelUsed: planned.modelUsed,
|
|
834
|
+
plannerHeuristicMode: planned.heuristicMode,
|
|
835
|
+
shadowLlmMode: planned.mode,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const llmDecision = finalizeRecallModeDecision(planned.mode, options);
|
|
840
|
+
return {
|
|
841
|
+
...llmDecision,
|
|
842
|
+
plannerSource: planned.source,
|
|
843
|
+
plannerReason: planned.reason,
|
|
844
|
+
plannerLatencyMs: planned.latencyMs,
|
|
845
|
+
plannerFallbackUsed: planned.fallbackUsed,
|
|
846
|
+
plannerModelUsed: planned.modelUsed,
|
|
847
|
+
plannerHeuristicMode: planned.heuristicMode,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function computeArtifactCandidateFetchLimit(
|
|
852
|
+
targetCount: number,
|
|
853
|
+
): number {
|
|
854
|
+
const cappedTarget = Math.max(0, targetCount);
|
|
855
|
+
if (cappedTarget === 0) return 0;
|
|
856
|
+
const headroom = Math.max(8, cappedTarget * 4);
|
|
857
|
+
return Math.min(200, cappedTarget + headroom);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function computeQmdHybridFetchLimit(
|
|
861
|
+
recallFetchLimit: number,
|
|
862
|
+
artifactsEnabled: boolean,
|
|
863
|
+
maxArtifactRecall: number,
|
|
864
|
+
): number {
|
|
865
|
+
const cappedRecallLimit = Math.max(0, recallFetchLimit);
|
|
866
|
+
if (cappedRecallLimit === 0) return 0;
|
|
867
|
+
if (!artifactsEnabled) return cappedRecallLimit;
|
|
868
|
+
// Overscan when artifacts are enabled, then filter artifact paths before
|
|
869
|
+
// re-applying the recall cap to avoid artifact-dominated top-N starvation.
|
|
870
|
+
const artifactHeadroom = Math.max(20, Math.max(0, maxArtifactRecall) * 8);
|
|
871
|
+
return Math.min(400, cappedRecallLimit + artifactHeadroom);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function summarizeGraphShadowComparison(
|
|
875
|
+
baseline: QmdSearchResult[],
|
|
876
|
+
merged: QmdSearchResult[],
|
|
877
|
+
topN: number,
|
|
878
|
+
): {
|
|
879
|
+
baselineCount: number;
|
|
880
|
+
graphCount: number;
|
|
881
|
+
overlapCount: number;
|
|
882
|
+
overlapRatio: number;
|
|
883
|
+
averageOverlapDelta: number;
|
|
884
|
+
} {
|
|
885
|
+
const limit = Math.max(0, Math.floor(topN));
|
|
886
|
+
const baselineTop = limit > 0 ? baseline.slice(0, limit) : [];
|
|
887
|
+
const graphTop = limit > 0 ? merged.slice(0, limit) : [];
|
|
888
|
+
const baselineByPath = new Map(
|
|
889
|
+
baselineTop.map((item) => [item.path, item.score]),
|
|
890
|
+
);
|
|
891
|
+
const graphByPath = new Map(graphTop.map((item) => [item.path, item.score]));
|
|
892
|
+
|
|
893
|
+
let overlapCount = 0;
|
|
894
|
+
let overlapDeltaSum = 0;
|
|
895
|
+
for (const [p, baselineScore] of baselineByPath.entries()) {
|
|
896
|
+
const graphScore = graphByPath.get(p);
|
|
897
|
+
if (typeof graphScore !== "number") continue;
|
|
898
|
+
overlapCount += 1;
|
|
899
|
+
overlapDeltaSum += graphScore - baselineScore;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const baselineCount = baselineTop.length;
|
|
903
|
+
return {
|
|
904
|
+
baselineCount,
|
|
905
|
+
graphCount: graphTop.length,
|
|
906
|
+
overlapCount,
|
|
907
|
+
overlapRatio: baselineCount > 0 ? overlapCount / baselineCount : 0,
|
|
908
|
+
averageOverlapDelta: overlapCount > 0 ? overlapDeltaSum / overlapCount : 0,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export function parseGraphRecallRankedResults(
|
|
913
|
+
value: unknown,
|
|
914
|
+
): GraphRecallRankedResult[] {
|
|
915
|
+
if (!Array.isArray(value)) return [];
|
|
916
|
+
const parsed: GraphRecallRankedResult[] = [];
|
|
917
|
+
for (const entry of value) {
|
|
918
|
+
if (!entry || typeof entry !== "object") continue;
|
|
919
|
+
const candidate = entry as Partial<GraphRecallRankedResult>;
|
|
920
|
+
if (
|
|
921
|
+
typeof candidate.path !== "string" ||
|
|
922
|
+
typeof candidate.score !== "number"
|
|
923
|
+
)
|
|
924
|
+
continue;
|
|
925
|
+
parsed.push({
|
|
926
|
+
path: candidate.path,
|
|
927
|
+
score: candidate.score,
|
|
928
|
+
docid: typeof candidate.docid === "string" ? candidate.docid : undefined,
|
|
929
|
+
sourceLabels: Array.isArray(candidate.sourceLabels)
|
|
930
|
+
? candidate.sourceLabels.filter(
|
|
931
|
+
(item): item is string => typeof item === "string",
|
|
932
|
+
)
|
|
933
|
+
: [],
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
return parsed.slice(0, 64);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export function parseMemoryIntentSnapshot(value: unknown): MemoryIntent {
|
|
940
|
+
const candidate =
|
|
941
|
+
value && typeof value === "object" ? (value as Partial<MemoryIntent>) : {};
|
|
942
|
+
return {
|
|
943
|
+
goal: typeof candidate.goal === "string" ? candidate.goal : "unknown",
|
|
944
|
+
actionType:
|
|
945
|
+
typeof candidate.actionType === "string"
|
|
946
|
+
? candidate.actionType
|
|
947
|
+
: "unknown",
|
|
948
|
+
entityTypes: Array.isArray(candidate.entityTypes)
|
|
949
|
+
? candidate.entityTypes.filter(
|
|
950
|
+
(item): item is string => typeof item === "string",
|
|
951
|
+
)
|
|
952
|
+
: [],
|
|
953
|
+
taskInitiation: candidate.taskInitiation === true,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
export function buildQmdIntentHint(intent: MemoryIntent): string | undefined {
|
|
958
|
+
const parts: string[] = [];
|
|
959
|
+
if (intent.goal !== "unknown") {
|
|
960
|
+
parts.push(`goal:${intent.goal.replace(/_/g, " ")}`);
|
|
961
|
+
}
|
|
962
|
+
if (intent.actionType !== "unknown") {
|
|
963
|
+
parts.push(`action:${intent.actionType.replace(/_/g, " ")}`);
|
|
964
|
+
}
|
|
965
|
+
if (intent.entityTypes.length > 0) {
|
|
966
|
+
parts.push(`entities:${intent.entityTypes.join(",")}`);
|
|
967
|
+
}
|
|
968
|
+
if (intent.taskInitiation === true) {
|
|
969
|
+
parts.push("task_initiation");
|
|
970
|
+
}
|
|
971
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export function parseQmdRecallResults(value: unknown): QmdSearchResult[] {
|
|
975
|
+
if (!Array.isArray(value)) return [];
|
|
976
|
+
const parsed: QmdSearchResult[] = [];
|
|
977
|
+
for (const entry of value) {
|
|
978
|
+
if (!entry || typeof entry !== "object") continue;
|
|
979
|
+
const candidate = entry as Partial<QmdSearchResult>;
|
|
980
|
+
if (
|
|
981
|
+
typeof candidate.path !== "string" ||
|
|
982
|
+
typeof candidate.score !== "number"
|
|
983
|
+
)
|
|
984
|
+
continue;
|
|
985
|
+
parsed.push({
|
|
986
|
+
docid: typeof candidate.docid === "string" ? candidate.docid : "",
|
|
987
|
+
path: candidate.path,
|
|
988
|
+
snippet: typeof candidate.snippet === "string" ? candidate.snippet : "",
|
|
989
|
+
score: candidate.score,
|
|
990
|
+
explain: parseQmdExplain(candidate.explain),
|
|
991
|
+
transport:
|
|
992
|
+
candidate.transport === "daemon" ||
|
|
993
|
+
candidate.transport === "subprocess" ||
|
|
994
|
+
candidate.transport === "hybrid" ||
|
|
995
|
+
candidate.transport === "scoped_prefilter"
|
|
996
|
+
? candidate.transport
|
|
997
|
+
: undefined,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
return parsed.slice(0, 32);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export function mergeArtifactRecallCandidates(
|
|
1004
|
+
candidatesByNamespace: MemoryFile[][],
|
|
1005
|
+
limit: number,
|
|
1006
|
+
): MemoryFile[] {
|
|
1007
|
+
const cappedLimit = Math.max(0, limit);
|
|
1008
|
+
if (cappedLimit === 0) return [];
|
|
1009
|
+
|
|
1010
|
+
const out: MemoryFile[] = [];
|
|
1011
|
+
const seen = new Set<string>();
|
|
1012
|
+
let offset = 0;
|
|
1013
|
+
while (out.length < cappedLimit) {
|
|
1014
|
+
let hasAnyCandidateAtOffset = false;
|
|
1015
|
+
for (const list of candidatesByNamespace) {
|
|
1016
|
+
if (offset >= list.length) continue;
|
|
1017
|
+
hasAnyCandidateAtOffset = true;
|
|
1018
|
+
const item = list[offset];
|
|
1019
|
+
const dedupeKey = `${item.frontmatter.id}:${item.frontmatter.sourceMemoryId ?? ""}:${item.content}`;
|
|
1020
|
+
if (seen.has(dedupeKey)) continue;
|
|
1021
|
+
seen.add(dedupeKey);
|
|
1022
|
+
out.push(item);
|
|
1023
|
+
if (out.length >= cappedLimit) break;
|
|
1024
|
+
}
|
|
1025
|
+
if (!hasAnyCandidateAtOffset) break;
|
|
1026
|
+
offset += 1;
|
|
1027
|
+
}
|
|
1028
|
+
return out;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export function resolveRecentThreadMemoryPaths(options: {
|
|
1032
|
+
threadEpisodeIds: string[];
|
|
1033
|
+
currentMemoryId: string;
|
|
1034
|
+
allMemsForGraph: MemoryFile[] | null | undefined;
|
|
1035
|
+
pathById?: Map<string, string>;
|
|
1036
|
+
storageDir: string;
|
|
1037
|
+
maxRecent: number;
|
|
1038
|
+
}): string[] {
|
|
1039
|
+
const maxRecent = Math.max(0, options.maxRecent);
|
|
1040
|
+
if (options.threadEpisodeIds.length === 0 || maxRecent === 0) return [];
|
|
1041
|
+
const pathById =
|
|
1042
|
+
options.pathById ??
|
|
1043
|
+
buildMemoryPathById(options.allMemsForGraph, options.storageDir);
|
|
1044
|
+
if (pathById.size === 0) return [];
|
|
1045
|
+
|
|
1046
|
+
// #1635 (defensive): skip pending_review ids from legacy episode sets.
|
|
1047
|
+
const pendingReviewIds = new Set<string>(
|
|
1048
|
+
(options.allMemsForGraph ?? [])
|
|
1049
|
+
.filter((m) => m.frontmatter.status === "pending_review" && m.frontmatter.id)
|
|
1050
|
+
.map((m) => m.frontmatter.id as string),
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
return options.threadEpisodeIds
|
|
1054
|
+
.filter((id) => id !== options.currentMemoryId)
|
|
1055
|
+
.filter((id) => !pendingReviewIds.has(id))
|
|
1056
|
+
.slice(-maxRecent)
|
|
1057
|
+
.map((id) => pathById.get(id))
|
|
1058
|
+
.filter((p): p is string => typeof p === "string" && p.length > 0);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export function buildMemoryPathById(
|
|
1062
|
+
allMemsForGraph: MemoryFile[] | null | undefined,
|
|
1063
|
+
storageDir: string,
|
|
1064
|
+
): Map<string, string> {
|
|
1065
|
+
const pathById = new Map<string, string>();
|
|
1066
|
+
for (const mem of allMemsForGraph ?? []) {
|
|
1067
|
+
const id = mem.frontmatter.id;
|
|
1068
|
+
if (!id) continue;
|
|
1069
|
+
pathById.set(id, path.relative(storageDir, mem.path));
|
|
1070
|
+
}
|
|
1071
|
+
return pathById;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
export function appendMemoryToGraphContext(options: {
|
|
1075
|
+
allMemsForGraph: MemoryFile[] | null | undefined;
|
|
1076
|
+
storageDir: string;
|
|
1077
|
+
memoryRelPath: string;
|
|
1078
|
+
memoryId: string;
|
|
1079
|
+
category: MemoryFile["frontmatter"]["category"];
|
|
1080
|
+
content: string;
|
|
1081
|
+
entityRef: string | undefined;
|
|
1082
|
+
}): void {
|
|
1083
|
+
if (!Array.isArray(options.allMemsForGraph)) return;
|
|
1084
|
+
|
|
1085
|
+
const nowIso = new Date().toISOString();
|
|
1086
|
+
options.allMemsForGraph.push({
|
|
1087
|
+
path: path.join(options.storageDir, options.memoryRelPath),
|
|
1088
|
+
content: options.content,
|
|
1089
|
+
frontmatter: {
|
|
1090
|
+
id: options.memoryId,
|
|
1091
|
+
category: options.category,
|
|
1092
|
+
created: nowIso,
|
|
1093
|
+
updated: nowIso,
|
|
1094
|
+
source: "extraction",
|
|
1095
|
+
confidence: 0.8,
|
|
1096
|
+
confidenceTier: "implied",
|
|
1097
|
+
tags: [],
|
|
1098
|
+
entityRef: options.entityRef,
|
|
1099
|
+
status: "active",
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
export function resolvePersistedMemoryRelativePath(options: {
|
|
1105
|
+
memoryId: string;
|
|
1106
|
+
pathById: Map<string, string>;
|
|
1107
|
+
category: string;
|
|
1108
|
+
}): string {
|
|
1109
|
+
const persisted = options.pathById.get(options.memoryId);
|
|
1110
|
+
if (persisted) return persisted;
|
|
1111
|
+
if (options.category === "correction") {
|
|
1112
|
+
return path.join("corrections", `${options.memoryId}.md`);
|
|
1113
|
+
}
|
|
1114
|
+
// Pick the subtree that matches the StorageManager.writeMemory routing
|
|
1115
|
+
// so fallback paths (used before memoryPathById has seen the fresh
|
|
1116
|
+
// write) agree with where the file actually lives. Routing goes through
|
|
1117
|
+
// the shared categoryDirName() chokepoint (utils/category-dir.ts) so
|
|
1118
|
+
// every category — decisions/, preferences/, reasoning-traces/, ... —
|
|
1119
|
+
// resolves to the same dir the writer used; otherwise graph edges point
|
|
1120
|
+
// at the wrong subtree and graph expansion silently drops those nodes
|
|
1121
|
+
// when readMemoryByPath cannot resolve them (issue #564 PR 3 / #1546).
|
|
1122
|
+
const subtree = categoryDirName(options.category);
|
|
1123
|
+
const idParts = options.memoryId.split("-");
|
|
1124
|
+
const maybeTimestamp = Number(idParts[1]);
|
|
1125
|
+
if (Number.isFinite(maybeTimestamp) && maybeTimestamp > 0) {
|
|
1126
|
+
const day = new Date(maybeTimestamp).toISOString().slice(0, 10);
|
|
1127
|
+
return path.join(subtree, day, `${options.memoryId}.md`);
|
|
1128
|
+
}
|
|
1129
|
+
return path.join(subtree, `${options.memoryId}.md`);
|
|
1130
|
+
}
|