@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
package/src/orchestrator.ts
CHANGED
|
@@ -115,6 +115,75 @@ import { TurnIngestionCoordinator } from "./orchestration/turn-ingestion.js";
|
|
|
115
115
|
import { RecallIntrospectionCoordinator } from "./orchestration/recall-introspection.js";
|
|
116
116
|
import { OrchestratorInitCoordinator } from "./orchestration/orchestrator-init.js";
|
|
117
117
|
import { PersistenceIndexCoordinator } from "./orchestration/persistence-index.js";
|
|
118
|
+
import { WorkspaceOpsCoordinator } from "./orchestration/workspace-ops.js";
|
|
119
|
+
import { NamespaceReadFanoutCoordinator } from "./orchestration/namespace-read-fanout.js";
|
|
120
|
+
import { selfDeps } from "./orchestration/self-deps.js";
|
|
121
|
+
import {
|
|
122
|
+
abortRecallError,
|
|
123
|
+
buildCompressionGuidelinesMarkdown,
|
|
124
|
+
buildQmdIntentHint,
|
|
125
|
+
isArtifactMemoryPath,
|
|
126
|
+
mergeArtifactRecallCandidates,
|
|
127
|
+
tokenizeRecallQuery,
|
|
128
|
+
type BulkImportBatchIngestResult,
|
|
129
|
+
type DaySummaryGatherOptions,
|
|
130
|
+
type GraphRecallSnapshot,
|
|
131
|
+
type IntentDebugSnapshot,
|
|
132
|
+
type QmdRecallSnapshot,
|
|
133
|
+
type QueryAwarePrefilter,
|
|
134
|
+
type RecallInvocationOptions,
|
|
135
|
+
} from "./orchestration/orchestrator-helpers.js";
|
|
136
|
+
// Issue #1526 seam 25: module-level helpers moved to
|
|
137
|
+
// orchestration/orchestrator-helpers.ts; re-exported here so existing
|
|
138
|
+
// importers (coordinators, tests, root shims) keep working.
|
|
139
|
+
export {
|
|
140
|
+
BulkImportBatchPartialFailureError,
|
|
141
|
+
COMPACTION_SIGNAL_MAX_AGE_MS,
|
|
142
|
+
appendMemoryToGraphContext,
|
|
143
|
+
applyQueryAwareCandidateFilter,
|
|
144
|
+
buildCompressionGuidelinesMarkdown,
|
|
145
|
+
buildMemoryPathById,
|
|
146
|
+
computeArtifactCandidateFetchLimit,
|
|
147
|
+
computeArtifactRecallLimit,
|
|
148
|
+
computeQmdHybridFetchLimit,
|
|
149
|
+
defaultWorkspaceDir,
|
|
150
|
+
filterHourlySummaryMarkdownForLocalDay,
|
|
151
|
+
filterRecallCandidates,
|
|
152
|
+
formatDateInTimeZone,
|
|
153
|
+
isArtifactMemoryPath,
|
|
154
|
+
lifecycleRecallScoreAdjustment,
|
|
155
|
+
mapRecallSourceToXrayServedBy,
|
|
156
|
+
mergeArtifactRecallCandidates,
|
|
157
|
+
normalizeIanaTimeZone,
|
|
158
|
+
parseFiniteDate,
|
|
159
|
+
parseGraphRecallRankedResults,
|
|
160
|
+
parseMemoryIntentSnapshot,
|
|
161
|
+
parseQmdRecallResults,
|
|
162
|
+
qmdStartupCollectionCheckWithTimeout,
|
|
163
|
+
raceRecallAbort,
|
|
164
|
+
resolveEffectiveRecallMode,
|
|
165
|
+
resolvePersistedMemoryRelativePath,
|
|
166
|
+
resolveRecallModeDecision,
|
|
167
|
+
resolveRecallModeDecisionAsync,
|
|
168
|
+
resolveRecentThreadMemoryPaths,
|
|
169
|
+
sanitizeSessionKeyForFilename,
|
|
170
|
+
shouldFilterLifecycleRecallCandidate,
|
|
171
|
+
splitTurnsBySourceValidAt,
|
|
172
|
+
summarizeGraphShadowComparison,
|
|
173
|
+
targetSourceValidAtSortMs,
|
|
174
|
+
throwIfRecallAborted,
|
|
175
|
+
tokenizeRecallQuery,
|
|
176
|
+
utcDateKeysForLocalDay,
|
|
177
|
+
type BulkImportBatchIngestResult,
|
|
178
|
+
type DaySummaryGatherOptions,
|
|
179
|
+
type GraphRecallSnapshot,
|
|
180
|
+
type IntentDebugSnapshot,
|
|
181
|
+
type QmdRecallSnapshot,
|
|
182
|
+
type QueryAwarePrefilter,
|
|
183
|
+
type RecallInvocationOptions,
|
|
184
|
+
type RecallModeDecision,
|
|
185
|
+
} from "./orchestration/orchestrator-helpers.js";
|
|
186
|
+
|
|
118
187
|
export { hasIdentityRecoveryIntent, resolveEffectiveIdentityInjectionMode } from "./orchestration/recall-result-formatter.js";
|
|
119
188
|
import {
|
|
120
189
|
GraphRecallCoordinator,
|
|
@@ -469,1105 +538,14 @@ import type {
|
|
|
469
538
|
} from "./types.js";
|
|
470
539
|
import { disposeDefaultArchiveScoring, getDefaultArchiveScoring, memoryFileToScoreItem } from "./recall/archive-scoring.js";
|
|
471
540
|
|
|
472
|
-
export interface BulkImportBatchIngestResult {
|
|
473
|
-
attemptedTurnCount: number;
|
|
474
|
-
extractionCount: number;
|
|
475
|
-
persistedCount: number;
|
|
476
|
-
durableOutputCount: number;
|
|
477
|
-
skippedCount: number;
|
|
478
|
-
failedCount: number;
|
|
479
|
-
postPersistMetadataFailureCount: number;
|
|
480
|
-
processedTurnCount: number;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export class BulkImportBatchPartialFailureError extends Error {
|
|
484
|
-
readonly partialResult: BulkImportBatchIngestResult;
|
|
485
|
-
|
|
486
|
-
readonly originalError: unknown;
|
|
487
|
-
|
|
488
|
-
constructor(
|
|
489
|
-
message: string,
|
|
490
|
-
partialResult: BulkImportBatchIngestResult,
|
|
491
|
-
originalError: unknown,
|
|
492
|
-
) {
|
|
493
|
-
super(message);
|
|
494
|
-
this.name = "BulkImportBatchPartialFailureError";
|
|
495
|
-
this.partialResult = partialResult;
|
|
496
|
-
this.originalError = originalError;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
541
|
|
|
500
542
|
// Issue #1526 seam 15: ExtractionRunResult moved to orchestration/extraction-run.ts.
|
|
501
543
|
export type { ExtractionRunResult } from "./orchestration/extraction-run.js";
|
|
502
544
|
|
|
503
|
-
export interface GraphRecallSnapshot {
|
|
504
|
-
recordedAt: string;
|
|
505
|
-
mode: RecallPlanMode | string;
|
|
506
|
-
queryHash: string;
|
|
507
|
-
queryLength: number;
|
|
508
|
-
namespaces: string[];
|
|
509
|
-
seedCount: number;
|
|
510
|
-
expandedCount: number;
|
|
511
|
-
seeds: string[];
|
|
512
|
-
expanded: GraphRecallExpandedEntry[];
|
|
513
|
-
status?: "completed" | "skipped" | "aborted";
|
|
514
|
-
reason?: string;
|
|
515
|
-
shadowMode?: boolean;
|
|
516
|
-
queryIntent?: MemoryIntent;
|
|
517
|
-
seedResults?: GraphRecallRankedResult[];
|
|
518
|
-
finalResults?: GraphRecallRankedResult[];
|
|
519
|
-
shadowComparison?: GraphRecallShadowComparison;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
export interface IntentDebugSnapshot {
|
|
523
|
-
recordedAt: string;
|
|
524
|
-
promptHash: string;
|
|
525
|
-
promptLength: number;
|
|
526
|
-
retrievalQueryHash: string;
|
|
527
|
-
retrievalQueryLength: number;
|
|
528
|
-
plannerEnabled: boolean;
|
|
529
|
-
plannedMode: RecallPlanMode;
|
|
530
|
-
effectiveMode: RecallPlanMode;
|
|
531
|
-
recallResultLimit: number;
|
|
532
|
-
queryIntent: MemoryIntent;
|
|
533
|
-
graphExpandedIntentDetected: boolean;
|
|
534
|
-
graphDecision: {
|
|
535
|
-
status: "not_requested" | "skipped" | "completed" | "aborted";
|
|
536
|
-
reason?: string;
|
|
537
|
-
shadowMode: boolean;
|
|
538
|
-
qmdAvailable: boolean;
|
|
539
|
-
graphRecallEnabled: boolean;
|
|
540
|
-
multiGraphMemoryEnabled: boolean;
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export interface QmdRecallSnapshot {
|
|
545
|
-
recordedAt: string;
|
|
546
|
-
queryHash: string;
|
|
547
|
-
queryLength: number;
|
|
548
|
-
collection?: string;
|
|
549
|
-
namespaces: string[];
|
|
550
|
-
fetchLimit: number;
|
|
551
|
-
primaryResultCount: number;
|
|
552
|
-
hybridResultCount: number;
|
|
553
|
-
queryAwareSeedCount: number;
|
|
554
|
-
resultCount: number;
|
|
555
|
-
intentHint?: string;
|
|
556
|
-
explainEnabled: boolean;
|
|
557
|
-
hybridTopUpUsed: boolean;
|
|
558
|
-
hybridTopUpSkippedReason?: string;
|
|
559
|
-
results: QmdSearchResult[];
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
export interface RecallModeDecision {
|
|
563
|
-
plannedMode: RecallPlanMode;
|
|
564
|
-
effectiveMode: RecallPlanMode;
|
|
565
|
-
graphExpandedIntentDetected: boolean;
|
|
566
|
-
graphReason?: string;
|
|
567
|
-
/**
|
|
568
|
-
* Where `plannedMode` came from (issue #1367 / Option C). `"heuristic"` for
|
|
569
|
-
* the regex planner; `"llm"` when the LLM planner classified it; and
|
|
570
|
-
* `"heuristic-fallback"` when the LLM was enabled but errored/timed out and we
|
|
571
|
-
* fell back. Absent on the synchronous heuristic-only path.
|
|
572
|
-
*/
|
|
573
|
-
plannerSource?: "heuristic" | "llm" | "heuristic-fallback";
|
|
574
|
-
/** Short rationale from the planner (for telemetry / x-ray). */
|
|
575
|
-
plannerReason?: string;
|
|
576
|
-
/** Wall-clock spent in the LLM planner call, when one was made. */
|
|
577
|
-
plannerLatencyMs?: number;
|
|
578
|
-
/** True when the LLM planner was enabled but fell back to the heuristic. */
|
|
579
|
-
plannerFallbackUsed?: boolean;
|
|
580
|
-
/** Model that served the LLM classification, when one was used. */
|
|
581
|
-
plannerModelUsed?: string;
|
|
582
|
-
/**
|
|
583
|
-
* The regex-heuristic baseline mode, captured whenever the LLM planner ran
|
|
584
|
-
* (any source). Lets operators compare planned-vs-heuristic during rollout —
|
|
585
|
-
* distinct from `plannedMode`, which on the LLM path is the LLM's choice.
|
|
586
|
-
*/
|
|
587
|
-
plannerHeuristicMode?: RecallPlanMode;
|
|
588
|
-
/**
|
|
589
|
-
* In shadow mode, the mode the LLM *would* have chosen (recorded for
|
|
590
|
-
* comparison) while `effectiveMode` stays on the heuristic decision.
|
|
591
|
-
*/
|
|
592
|
-
shadowLlmMode?: RecallPlanMode;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/**
|
|
596
|
-
* Map the orchestrator's internal `recallSource` strings to the
|
|
597
|
-
* X-ray `servedBy` vocabulary (issue #570 PR 1). The X-ray tier
|
|
598
|
-
* ladder intentionally flattens QMD / embedding / cold-fallback to
|
|
599
|
-
* the `hybrid` tier because they all materialize through the same
|
|
600
|
-
* hybrid BM25+vector pipeline from the caller's perspective. The
|
|
601
|
-
* `recent_scan` path gets its own dedicated tier because it bypasses
|
|
602
|
-
* the hybrid pipeline entirely. `none` is treated as `hybrid` on the
|
|
603
|
-
* theory that a query that returned nothing still routed through the
|
|
604
|
-
* hybrid pipeline — but callers should normally gate capture on
|
|
605
|
-
* `recalledMemoryIds.length > 0`.
|
|
606
|
-
*/
|
|
607
|
-
export function mapRecallSourceToXrayServedBy(
|
|
608
|
-
source:
|
|
609
|
-
| "none"
|
|
610
|
-
| "hot_qmd"
|
|
611
|
-
| "hot_embedding"
|
|
612
|
-
| "cold_fallback"
|
|
613
|
-
| "recent_scan",
|
|
614
|
-
): RecallXrayServedBy {
|
|
615
|
-
// Exhaustive switch: every current union member is explicitly
|
|
616
|
-
// listed so TypeScript surfaces a compile error if a new source is
|
|
617
|
-
// added without a deliberate mapping. The `never`-typed fallthrough
|
|
618
|
-
// keeps the function total at runtime — if the caller passes an
|
|
619
|
-
// unexpected value that slipped past the type system (e.g. a JSON
|
|
620
|
-
// deserialization), we still fall back to `hybrid`.
|
|
621
|
-
switch (source) {
|
|
622
|
-
case "recent_scan":
|
|
623
|
-
return "recent-scan";
|
|
624
|
-
case "hot_qmd":
|
|
625
|
-
case "hot_embedding":
|
|
626
|
-
case "cold_fallback":
|
|
627
|
-
case "none":
|
|
628
|
-
return "hybrid";
|
|
629
|
-
}
|
|
630
|
-
const _exhaustive: never = source;
|
|
631
|
-
void _exhaustive;
|
|
632
|
-
return "hybrid";
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
export interface RecallInvocationOptions {
|
|
636
|
-
namespace?: string;
|
|
637
|
-
topK?: number;
|
|
638
|
-
mode?: RecallPlanMode;
|
|
639
|
-
abortSignal?: AbortSignal;
|
|
640
|
-
/**
|
|
641
|
-
* Capture a `RecallXraySnapshot` for this recall (issue #570). When
|
|
642
|
-
* `true`, the orchestrator builds a snapshot from the data it has
|
|
643
|
-
* already gathered and stashes it in memory, accessible via
|
|
644
|
-
* `getLastXraySnapshot()`. When `false` or absent, nothing is
|
|
645
|
-
* captured and recall behavior is unchanged (schema-only slice).
|
|
646
|
-
*/
|
|
647
|
-
xrayCapture?: boolean;
|
|
648
|
-
/**
|
|
649
|
-
* Per-invocation override for `recallBudgetChars` (issue #570 PR 3/4).
|
|
650
|
-
* Flows through `getRecallBudgetChars()` for this recall only — no
|
|
651
|
-
* shared config mutation, so concurrent recalls on the same
|
|
652
|
-
* orchestrator are not affected (CLAUDE.md rule 47: no shared
|
|
653
|
-
* mutable state across async boundaries). Must be a non-negative
|
|
654
|
-
* finite integer; non-conforming values are ignored and the
|
|
655
|
-
* configured budget is used.
|
|
656
|
-
*/
|
|
657
|
-
budgetCharsOverride?: number;
|
|
658
|
-
/**
|
|
659
|
-
* Per-invocation principal override (issue #570 PR 4). When set,
|
|
660
|
-
* the orchestrator uses this principal for ACL / namespace checks
|
|
661
|
-
* instead of `resolvePrincipal(sessionKey, config)`. This is the
|
|
662
|
-
* escape hatch for access surfaces (HTTP / MCP) that have already
|
|
663
|
-
* authenticated the caller upstream — threading an unmapped
|
|
664
|
-
* principal through the session-key-based resolver would otherwise
|
|
665
|
-
* collapse it to `"default"` and produce false denials in
|
|
666
|
-
* namespace-enabled deployments (CLAUDE.md rule 42).
|
|
667
|
-
*/
|
|
668
|
-
principalOverride?: string;
|
|
669
|
-
/**
|
|
670
|
-
* Historical recall point (issue #680). When set, the orchestrator
|
|
671
|
-
* filters out memories whose `valid_at` is after this timestamp OR
|
|
672
|
-
* whose `invalid_at` is at-or-before this timestamp, so callers see
|
|
673
|
-
* the corpus as it existed at `asOf`. ISO 8601 string; comparisons
|
|
674
|
-
* use `Date.parse()` so timezone-aware values round-trip correctly
|
|
675
|
-
* (CLAUDE.md gotcha — never compare ISO strings lexicographically).
|
|
676
|
-
* Invalid values must be rejected at input boundaries (CLAUDE.md
|
|
677
|
-
* rule 51); the orchestrator does NOT silently fall back here.
|
|
678
|
-
*/
|
|
679
|
-
asOf?: string;
|
|
680
|
-
/**
|
|
681
|
-
* Issue #681 — when `true`, bypasses `graphTraversalConfidenceFloor`
|
|
682
|
-
* and includes edges below the floor in graph traversal. Useful for
|
|
683
|
-
* diagnostic recall queries that need to surface results that would
|
|
684
|
-
* normally be pruned by confidence decay. Default `false`.
|
|
685
|
-
*/
|
|
686
|
-
includeLowConfidence?: boolean;
|
|
687
|
-
/**
|
|
688
|
-
* User-aware context scopes active for this recall. Used by X-ray
|
|
689
|
-
* provenance safety checks so boundary-scoped memories are evaluated
|
|
690
|
-
* against the caller's real context.
|
|
691
|
-
*/
|
|
692
|
-
currentContextScopes?: readonly unknown[];
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
export type QueryAwarePrefilter = {
|
|
696
|
-
candidatePaths: Set<string> | null;
|
|
697
|
-
temporalFromDate: string | null;
|
|
698
|
-
matchedTags: string[];
|
|
699
|
-
expandedTags: string[];
|
|
700
|
-
combination: "none" | "temporal" | "tag" | "intersection" | "union";
|
|
701
|
-
filteredToFullSearch: boolean;
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
// Recall-specific abort helpers. Thin wrappers over the shared
|
|
705
|
-
// `abort-error.ts` module so every abort in the codebase shares the
|
|
706
|
-
// same `name === "AbortError"` classification contract (`isAbortError`
|
|
707
|
-
// works uniformly). We keep the "recall aborted" default message for
|
|
708
|
-
// back-compat with call-site logs; callers that pass an explicit
|
|
709
|
-
// message (e.g. "extraction aborted (before_extract)") are unaffected.
|
|
710
|
-
const abortRecallError = sharedAbortError;
|
|
711
|
-
|
|
712
|
-
export function throwIfRecallAborted(
|
|
713
|
-
signal?: AbortSignal,
|
|
714
|
-
message = "recall aborted",
|
|
715
|
-
): void {
|
|
716
|
-
sharedThrowIfAborted(signal, message);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
export async function raceRecallAbort<T>(
|
|
720
|
-
promise: Promise<T>,
|
|
721
|
-
signal?: AbortSignal,
|
|
722
|
-
message = "recall aborted",
|
|
723
|
-
): Promise<T> {
|
|
724
|
-
throwIfRecallAborted(signal, message);
|
|
725
|
-
if (!signal) return promise;
|
|
726
|
-
|
|
727
|
-
let onAbort: (() => void) | null = null;
|
|
728
|
-
const abortPromise = new Promise<T>((_resolve, reject) => {
|
|
729
|
-
onAbort = () => reject(abortRecallError(message));
|
|
730
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
try {
|
|
734
|
-
return await Promise.race([promise, abortPromise]);
|
|
735
|
-
} finally {
|
|
736
|
-
if (onAbort) {
|
|
737
|
-
signal.removeEventListener("abort", onAbort);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/** Maximum age (ms) before a compaction-reset signal file is considered stale and removed. */
|
|
743
|
-
export const COMPACTION_SIGNAL_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
744
|
-
const DEFAULT_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS = 10_000;
|
|
745
|
-
|
|
746
|
-
type DaySummaryGatherOptions = {
|
|
747
|
-
timeZone?: string;
|
|
748
|
-
now?: Date;
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
function normalizeIanaTimeZone(value: unknown): string | undefined {
|
|
752
|
-
if (typeof value !== "string") return undefined;
|
|
753
|
-
const trimmed = value.trim();
|
|
754
|
-
if (!trimmed) return undefined;
|
|
755
|
-
try {
|
|
756
|
-
Intl.DateTimeFormat(undefined, { timeZone: trimmed });
|
|
757
|
-
return trimmed;
|
|
758
|
-
} catch {
|
|
759
|
-
return undefined;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function formatDateInTimeZone(date: Date, timeZone: string): string {
|
|
764
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
765
|
-
timeZone,
|
|
766
|
-
year: "numeric",
|
|
767
|
-
month: "2-digit",
|
|
768
|
-
day: "2-digit",
|
|
769
|
-
}).formatToParts(date);
|
|
770
|
-
const get = (type: string): string => parts.find((part) => part.type === type)?.value ?? "";
|
|
771
|
-
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function utcDateKey(date: Date): string {
|
|
775
|
-
return date.toISOString().slice(0, 10);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function utcDateKeysAround(date: Date): string[] {
|
|
779
|
-
const dayMs = 86_400_000;
|
|
780
|
-
const keys = [
|
|
781
|
-
utcDateKey(new Date(date.getTime() - dayMs)),
|
|
782
|
-
utcDateKey(date),
|
|
783
|
-
utcDateKey(new Date(date.getTime() + dayMs)),
|
|
784
|
-
];
|
|
785
|
-
return keys.filter((value, index, array) => array.indexOf(value) === index);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function utcDateKeysForLocalDay(date: Date, timeZone: string): string[] {
|
|
789
|
-
const targetLocalDate = formatDateInTimeZone(date, timeZone);
|
|
790
|
-
const keys = new Set<string>();
|
|
791
|
-
const hourMs = 3_600_000;
|
|
792
|
-
const scanStart = date.getTime() - 48 * hourMs;
|
|
793
|
-
const scanEnd = date.getTime() + 48 * hourMs;
|
|
794
|
-
for (let ms = scanStart; ms <= scanEnd; ms += hourMs) {
|
|
795
|
-
const candidate = new Date(ms);
|
|
796
|
-
if (formatDateInTimeZone(candidate, timeZone) === targetLocalDate) {
|
|
797
|
-
keys.add(utcDateKey(candidate));
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
return keys.size > 0 ? [...keys].sort() : utcDateKeysAround(date);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function parseFiniteDate(value: unknown): Date | null {
|
|
804
|
-
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
805
|
-
const parsed = new Date(value);
|
|
806
|
-
return Number.isFinite(parsed.getTime()) ? parsed : null;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function filterHourlySummaryMarkdownForLocalDay(
|
|
810
|
-
raw: string,
|
|
811
|
-
utcDate: string,
|
|
812
|
-
timeZone: string,
|
|
813
|
-
targetLocalDate: string,
|
|
814
|
-
): string | null {
|
|
815
|
-
const hourHeaderPattern = /^## ([01]\d|2[0-3]):00[ \t]*$/gm;
|
|
816
|
-
const matches = Array.from(raw.matchAll(hourHeaderPattern));
|
|
817
|
-
if (matches.length === 0) return null;
|
|
818
|
-
|
|
819
|
-
const firstSectionStart = matches[0]?.index ?? 0;
|
|
820
|
-
const preamble = raw.slice(0, firstSectionStart).trim();
|
|
821
|
-
const sections: string[] = [];
|
|
822
|
-
for (let index = 0; index < matches.length; index += 1) {
|
|
823
|
-
const match = matches[index];
|
|
824
|
-
const hour = match[1];
|
|
825
|
-
if (!hour) continue;
|
|
826
|
-
const sectionTimestamp = parseFiniteDate(`${utcDate}T${hour}:00:00.000Z`);
|
|
827
|
-
if (
|
|
828
|
-
!sectionTimestamp ||
|
|
829
|
-
formatDateInTimeZone(sectionTimestamp, timeZone) !== targetLocalDate
|
|
830
|
-
) {
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
const sectionStart = match.index ?? 0;
|
|
834
|
-
const sectionEnd = matches[index + 1]?.index ?? raw.length;
|
|
835
|
-
const section = raw.slice(sectionStart, sectionEnd).trim();
|
|
836
|
-
if (section.length > 0) sections.push(section);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
if (sections.length === 0) return null;
|
|
840
|
-
return [preamble, ...sections]
|
|
841
|
-
.filter((section) => section.length > 0)
|
|
842
|
-
.join("\n\n");
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
type SearchCollectionState = "present" | "missing" | "unknown" | "skipped";
|
|
846
|
-
|
|
847
|
-
function qmdStartupCollectionCheckTimeoutMs(): number {
|
|
848
|
-
const raw =
|
|
849
|
-
process.env.REMNIC_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS ??
|
|
850
|
-
process.env.ENGRAM_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS;
|
|
851
|
-
if (raw === undefined) return DEFAULT_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS;
|
|
852
|
-
const parsed = Number(raw);
|
|
853
|
-
return Number.isFinite(parsed) && parsed >= 1_000
|
|
854
|
-
? Math.floor(parsed)
|
|
855
|
-
: DEFAULT_QMD_STARTUP_COLLECTION_CHECK_TIMEOUT_MS;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
export async function qmdStartupCollectionCheckWithTimeout(
|
|
859
|
-
promise: Promise<SearchCollectionState>,
|
|
860
|
-
controller: AbortController,
|
|
861
|
-
label: string,
|
|
862
|
-
): Promise<SearchCollectionState> {
|
|
863
|
-
const timeoutMs = qmdStartupCollectionCheckTimeoutMs();
|
|
864
|
-
let timer: NodeJS.Timeout | undefined;
|
|
865
|
-
let settled = false;
|
|
866
|
-
|
|
867
|
-
const timeoutPromise = new Promise<SearchCollectionState>((resolve) => {
|
|
868
|
-
timer = setTimeout(() => {
|
|
869
|
-
if (settled) return;
|
|
870
|
-
controller.abort();
|
|
871
|
-
log.warn(
|
|
872
|
-
`QMD startup collection check for ${label} timed out after ${timeoutMs}ms; keeping search enabled fail-open`,
|
|
873
|
-
);
|
|
874
|
-
resolve("unknown");
|
|
875
|
-
}, timeoutMs);
|
|
876
|
-
timer.unref?.();
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
const checkedPromise = promise
|
|
880
|
-
.catch((err): SearchCollectionState => {
|
|
881
|
-
log.warn(
|
|
882
|
-
`QMD startup collection check for ${label} failed; keeping search enabled fail-open: ${err}`,
|
|
883
|
-
);
|
|
884
|
-
return "unknown";
|
|
885
|
-
})
|
|
886
|
-
.finally(() => {
|
|
887
|
-
settled = true;
|
|
888
|
-
if (timer) clearTimeout(timer);
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
return await Promise.race([checkedPromise, timeoutPromise]);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/** Default workspace directory when no per-agent or config workspace is available. */
|
|
895
|
-
export function defaultWorkspaceDir(): string {
|
|
896
|
-
return path.join(os.homedir(), ".openclaw", "workspace");
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/**
|
|
900
|
-
* Produce a collision-resistant, filesystem-safe identifier from a session key.
|
|
901
|
-
*
|
|
902
|
-
* Session keys follow colon-delimited forms (e.g., `agent:gpucodebot:main`).
|
|
903
|
-
* A naive replace (`:` → `_`) is lossy: different keys like `agent:alpha` and
|
|
904
|
-
* `agent/alpha` would collide. Instead we append a short SHA-256 hash of the
|
|
905
|
-
* original key to the human-readable sanitized prefix, guaranteeing uniqueness
|
|
906
|
-
* while keeping filenames debuggable.
|
|
907
|
-
*
|
|
908
|
-
* Format: `<sanitized>-<12-char-hex-hash>`
|
|
909
|
-
* Example: `agent:gpucodebot:main` → `agent_gpucodebot_main-a1b2c3d4e5f6`
|
|
910
|
-
*/
|
|
911
|
-
export function sanitizeSessionKeyForFilename(sessionKey: string): string {
|
|
912
|
-
const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
913
|
-
const hash = createHash("sha256")
|
|
914
|
-
.update(sessionKey)
|
|
915
|
-
.digest("hex")
|
|
916
|
-
.slice(0, 12);
|
|
917
|
-
return `${readable}-${hash}`;
|
|
918
|
-
}
|
|
919
545
|
|
|
920
|
-
function sourceValidAtMs(turn: BufferTurn): number | null {
|
|
921
|
-
if (typeof turn.sourceValidAt !== "string") return null;
|
|
922
|
-
return parseFlexibleIsoTimestamp(turn.sourceValidAt.trim());
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
const SOURCE_VALID_AT_CONTEXT_TURNS = 2;
|
|
926
|
-
|
|
927
|
-
function sourceValidAtSliceKey(turn: BufferTurn, index: number): string {
|
|
928
|
-
const validAtMs = sourceValidAtMs(turn);
|
|
929
|
-
return validAtMs === null ? `unknown:${index}` : String(validAtMs);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function asExtractionContextTurn(turn: BufferTurn): BufferTurn {
|
|
933
|
-
return { ...turn, extractionContextOnly: true };
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function asExtractionTargetTurn(turn: BufferTurn): BufferTurn {
|
|
937
|
-
const { extractionContextOnly: _contextOnly, ...targetTurn } = turn;
|
|
938
|
-
return targetTurn;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function sourceValidAtContextTurns(
|
|
942
|
-
turns: readonly BufferTurn[],
|
|
943
|
-
targetStart: number,
|
|
944
|
-
targetEnd: number,
|
|
945
|
-
targetValidAtMs: number | null,
|
|
946
|
-
): BufferTurn[] {
|
|
947
|
-
if (targetValidAtMs === null) return [];
|
|
948
|
-
return turns
|
|
949
|
-
.flatMap((turn, index) => {
|
|
950
|
-
if (index >= targetStart && index < targetEnd) return [];
|
|
951
|
-
const contextValidAtMs = sourceValidAtMs(turn);
|
|
952
|
-
if (contextValidAtMs === null || contextValidAtMs > targetValidAtMs) {
|
|
953
|
-
return [];
|
|
954
|
-
}
|
|
955
|
-
return [{ turn, index, validAtMs: contextValidAtMs }];
|
|
956
|
-
})
|
|
957
|
-
.sort((a, b) => {
|
|
958
|
-
if (a.validAtMs < b.validAtMs) return -1;
|
|
959
|
-
if (a.validAtMs > b.validAtMs) return 1;
|
|
960
|
-
if (a.index === b.index) return 0;
|
|
961
|
-
return a.index < b.index ? -1 : 1;
|
|
962
|
-
})
|
|
963
|
-
.slice(-SOURCE_VALID_AT_CONTEXT_TURNS)
|
|
964
|
-
.map(({ turn }) => asExtractionContextTurn(turn));
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
export function targetSourceValidAtSortMs(turns: readonly BufferTurn[]): number {
|
|
968
|
-
let latestMs: number | null = null;
|
|
969
|
-
for (const turn of turns) {
|
|
970
|
-
if (turn.extractionContextOnly === true) continue;
|
|
971
|
-
const validAtMs = sourceValidAtMs(turn);
|
|
972
|
-
if (validAtMs === null) continue;
|
|
973
|
-
if (latestMs === null || validAtMs > latestMs) {
|
|
974
|
-
latestMs = validAtMs;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
return latestMs ?? Number.POSITIVE_INFINITY;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function sortSourceValidAtSlicesChronologically(
|
|
981
|
-
slices: BufferTurn[][],
|
|
982
|
-
): BufferTurn[][] {
|
|
983
|
-
return slices
|
|
984
|
-
.map((turns, order) => ({
|
|
985
|
-
turns,
|
|
986
|
-
order,
|
|
987
|
-
targetValidAtMs: targetSourceValidAtSortMs(turns),
|
|
988
|
-
}))
|
|
989
|
-
.sort((a, b) => {
|
|
990
|
-
if (a.targetValidAtMs < b.targetValidAtMs) return -1;
|
|
991
|
-
if (a.targetValidAtMs > b.targetValidAtMs) return 1;
|
|
992
|
-
if (a.order === b.order) return 0;
|
|
993
|
-
return a.order < b.order ? -1 : 1;
|
|
994
|
-
})
|
|
995
|
-
.map((slice) => slice.turns);
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
export function splitTurnsBySourceValidAt(
|
|
999
|
-
turns: readonly BufferTurn[],
|
|
1000
|
-
options: { includeContext?: boolean } = {},
|
|
1001
|
-
): BufferTurn[][] {
|
|
1002
|
-
if (turns.length === 0) return [];
|
|
1003
|
-
if (!turns.some((turn) => sourceValidAtMs(turn) !== null)) {
|
|
1004
|
-
return [[...turns]];
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const slices: BufferTurn[][] = [];
|
|
1008
|
-
let start = 0;
|
|
1009
|
-
while (start < turns.length) {
|
|
1010
|
-
const targetValidAtMs = sourceValidAtMs(turns[start]);
|
|
1011
|
-
const activeKey = sourceValidAtSliceKey(turns[start], start);
|
|
1012
|
-
let end = start + 1;
|
|
1013
|
-
while (
|
|
1014
|
-
end < turns.length &&
|
|
1015
|
-
sourceValidAtSliceKey(turns[end], end) === activeKey
|
|
1016
|
-
) {
|
|
1017
|
-
end += 1;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const contextTurns =
|
|
1021
|
-
options.includeContext === false
|
|
1022
|
-
? []
|
|
1023
|
-
: sourceValidAtContextTurns(turns, start, end, targetValidAtMs);
|
|
1024
|
-
slices.push([
|
|
1025
|
-
...contextTurns,
|
|
1026
|
-
...turns.slice(start, end).map(asExtractionTargetTurn),
|
|
1027
|
-
]);
|
|
1028
|
-
start = end;
|
|
1029
|
-
}
|
|
1030
|
-
return sortSourceValidAtSlicesChronologically(slices);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
export function isArtifactMemoryPath(filePath: string): boolean {
|
|
1034
|
-
return /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(filePath);
|
|
1035
|
-
}
|
|
1036
546
|
// Issue #1526 seam 15: deriveTopicsFromExtraction moved to orchestration/extraction-run.ts.
|
|
1037
547
|
export { deriveTopicsFromExtraction } from "./orchestration/extraction-run.js";
|
|
1038
|
-
export function buildCompressionGuidelinesMarkdown(
|
|
1039
|
-
events: MemoryActionEvent[],
|
|
1040
|
-
generatedAtIso: string = new Date().toISOString(),
|
|
1041
|
-
): string {
|
|
1042
|
-
return buildCompressionGuidelinesMarkdownV2(events, generatedAtIso);
|
|
1043
|
-
}
|
|
1044
548
|
|
|
1045
|
-
export function filterRecallCandidates(
|
|
1046
|
-
candidates: QmdSearchResult[],
|
|
1047
|
-
options: {
|
|
1048
|
-
namespacesEnabled: boolean;
|
|
1049
|
-
recallNamespaces: string[];
|
|
1050
|
-
resolveNamespace: (path: string) => string;
|
|
1051
|
-
limit: number;
|
|
1052
|
-
},
|
|
1053
|
-
): QmdSearchResult[] {
|
|
1054
|
-
const scopedByNamespace = options.namespacesEnabled
|
|
1055
|
-
? candidates.filter((r) =>
|
|
1056
|
-
options.recallNamespaces.includes(options.resolveNamespace(r.path)),
|
|
1057
|
-
)
|
|
1058
|
-
: candidates;
|
|
1059
|
-
return scopedByNamespace
|
|
1060
|
-
.filter((r) => !isArtifactMemoryPath(r.path))
|
|
1061
|
-
.slice(0, Math.max(0, options.limit));
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
export function applyQueryAwareCandidateFilter(
|
|
1065
|
-
candidates: QmdSearchResult[],
|
|
1066
|
-
candidatePaths: Set<string> | null,
|
|
1067
|
-
): QmdSearchResult[] {
|
|
1068
|
-
if (!candidatePaths) return candidates;
|
|
1069
|
-
if (candidatePaths.size === 0) return [];
|
|
1070
|
-
const filtered = candidates.filter((candidate) =>
|
|
1071
|
-
candidatePaths.has(candidate.path),
|
|
1072
|
-
);
|
|
1073
|
-
return filtered.length > 0 ? filtered : candidates;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
export function tokenizeRecallQuery(prompt: string): string[] {
|
|
1077
|
-
return prompt
|
|
1078
|
-
.toLowerCase()
|
|
1079
|
-
.split(/[^a-z0-9]+/i)
|
|
1080
|
-
.map((t) => t.trim())
|
|
1081
|
-
.filter((t) => t.length >= 3);
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function hasLifecycleMetadata(frontmatter: MemoryFrontmatter): boolean {
|
|
1085
|
-
return (
|
|
1086
|
-
frontmatter.lifecycleState !== undefined ||
|
|
1087
|
-
frontmatter.verificationState !== undefined ||
|
|
1088
|
-
frontmatter.policyClass !== undefined ||
|
|
1089
|
-
frontmatter.lastValidatedAt !== undefined ||
|
|
1090
|
-
frontmatter.decayScore !== undefined ||
|
|
1091
|
-
frontmatter.heatScore !== undefined
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
export function shouldFilterLifecycleRecallCandidate(
|
|
1096
|
-
frontmatter: MemoryFrontmatter,
|
|
1097
|
-
options: {
|
|
1098
|
-
lifecyclePolicyEnabled: boolean;
|
|
1099
|
-
lifecycleFilterStaleEnabled: boolean;
|
|
1100
|
-
},
|
|
1101
|
-
): boolean {
|
|
1102
|
-
if (!options.lifecyclePolicyEnabled || !options.lifecycleFilterStaleEnabled)
|
|
1103
|
-
return false;
|
|
1104
|
-
if (!hasLifecycleMetadata(frontmatter)) return false;
|
|
1105
|
-
const lifecycleState = resolveLifecycleState(frontmatter);
|
|
1106
|
-
return lifecycleState === "stale" || lifecycleState === "archived";
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
export function lifecycleRecallScoreAdjustment(
|
|
1110
|
-
frontmatter: MemoryFrontmatter,
|
|
1111
|
-
options: {
|
|
1112
|
-
lifecyclePolicyEnabled: boolean;
|
|
1113
|
-
},
|
|
1114
|
-
): number {
|
|
1115
|
-
if (!options.lifecyclePolicyEnabled) return 0;
|
|
1116
|
-
if (!hasLifecycleMetadata(frontmatter)) return 0;
|
|
1117
|
-
|
|
1118
|
-
let delta = 0;
|
|
1119
|
-
const lifecycleState = resolveLifecycleState(frontmatter);
|
|
1120
|
-
switch (lifecycleState) {
|
|
1121
|
-
case "active":
|
|
1122
|
-
delta += 0.05;
|
|
1123
|
-
break;
|
|
1124
|
-
case "validated":
|
|
1125
|
-
delta += 0.03;
|
|
1126
|
-
break;
|
|
1127
|
-
case "candidate":
|
|
1128
|
-
delta -= 0.01;
|
|
1129
|
-
break;
|
|
1130
|
-
case "stale":
|
|
1131
|
-
delta -= 0.06;
|
|
1132
|
-
break;
|
|
1133
|
-
case "archived":
|
|
1134
|
-
delta -= 0.08;
|
|
1135
|
-
break;
|
|
1136
|
-
}
|
|
1137
|
-
if (frontmatter.verificationState === "disputed") {
|
|
1138
|
-
delta -= 0.12;
|
|
1139
|
-
}
|
|
1140
|
-
return delta;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
export function computeArtifactRecallLimit(
|
|
1144
|
-
recallMode: RecallPlanMode,
|
|
1145
|
-
recallResultLimit: number,
|
|
1146
|
-
verbatimArtifactsMaxRecall: number,
|
|
1147
|
-
): number {
|
|
1148
|
-
if (recallMode === "no_recall") return 0;
|
|
1149
|
-
if (Math.max(0, recallResultLimit) === 0) return 0;
|
|
1150
|
-
const base = Math.max(0, verbatimArtifactsMaxRecall);
|
|
1151
|
-
if (recallMode === "minimal") {
|
|
1152
|
-
return Math.min(base, Math.max(0, recallResultLimit));
|
|
1153
|
-
}
|
|
1154
|
-
return base;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
export function resolveEffectiveRecallMode(options: {
|
|
1158
|
-
plannerEnabled: boolean;
|
|
1159
|
-
graphRecallEnabled: boolean;
|
|
1160
|
-
multiGraphMemoryEnabled: boolean;
|
|
1161
|
-
graphExpandedIntentEnabled?: boolean;
|
|
1162
|
-
prompt: string;
|
|
1163
|
-
}): RecallPlanMode {
|
|
1164
|
-
return resolveRecallModeDecision(options).effectiveMode;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
interface RecallModeGraphOptions {
|
|
1168
|
-
plannerEnabled: boolean;
|
|
1169
|
-
graphRecallEnabled: boolean;
|
|
1170
|
-
multiGraphMemoryEnabled: boolean;
|
|
1171
|
-
graphExpandedIntentEnabled?: boolean;
|
|
1172
|
-
prompt: string;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
/**
|
|
1176
|
-
* Apply the graph-mode overlay + gating to a planner-produced mode.
|
|
1177
|
-
*
|
|
1178
|
-
* Shared by the heuristic ({@link resolveRecallModeDecision}) and LLM
|
|
1179
|
-
* ({@link resolveRecallModeDecisionAsync}) paths so graph promotion and the
|
|
1180
|
-
* "graph disabled → fall back to full" gating behave identically regardless of
|
|
1181
|
-
* which planner produced `plannedModeRaw` (gotcha #39).
|
|
1182
|
-
*/
|
|
1183
|
-
function finalizeRecallModeDecision(
|
|
1184
|
-
plannedModeRaw: RecallPlanMode,
|
|
1185
|
-
options: RecallModeGraphOptions,
|
|
1186
|
-
): RecallModeDecision {
|
|
1187
|
-
let plannedMode: RecallPlanMode = plannedModeRaw;
|
|
1188
|
-
const graphExpandedIntentDetected =
|
|
1189
|
-
options.plannerEnabled &&
|
|
1190
|
-
options.graphExpandedIntentEnabled === true &&
|
|
1191
|
-
hasBroadGraphIntent(options.prompt);
|
|
1192
|
-
if (plannedMode !== "graph_mode" && graphExpandedIntentDetected) {
|
|
1193
|
-
plannedMode = "graph_mode";
|
|
1194
|
-
}
|
|
1195
|
-
if (
|
|
1196
|
-
plannedMode === "graph_mode" &&
|
|
1197
|
-
(!options.graphRecallEnabled || !options.multiGraphMemoryEnabled)
|
|
1198
|
-
) {
|
|
1199
|
-
return {
|
|
1200
|
-
plannedMode,
|
|
1201
|
-
effectiveMode: "full",
|
|
1202
|
-
graphExpandedIntentDetected,
|
|
1203
|
-
graphReason: !options.graphRecallEnabled
|
|
1204
|
-
? "graph recall disabled by config"
|
|
1205
|
-
: "multi-graph memory disabled by config",
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
return {
|
|
1209
|
-
plannedMode,
|
|
1210
|
-
effectiveMode: plannedMode,
|
|
1211
|
-
graphExpandedIntentDetected,
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
export function resolveRecallModeDecision(options: RecallModeGraphOptions): RecallModeDecision {
|
|
1216
|
-
const plannedMode: RecallPlanMode = options.plannerEnabled
|
|
1217
|
-
? planRecallMode(options.prompt)
|
|
1218
|
-
: "full";
|
|
1219
|
-
return finalizeRecallModeDecision(plannedMode, options);
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
/**
|
|
1223
|
-
* Async recall-mode decision with optional LLM-based planning (issue #1367 /
|
|
1224
|
-
* Option C). Falls back to the heuristic decision when the LLM planner is
|
|
1225
|
-
* disabled, in shadow mode, or unavailable/failed — so this is always safe to
|
|
1226
|
-
* await on the recall hot path. Provider-agnostic: the LLM call routes through
|
|
1227
|
-
* the gateway/fallback chain.
|
|
1228
|
-
*
|
|
1229
|
-
* `recallPlannerEnabled === false` keeps the legacy "always full" behavior and
|
|
1230
|
-
* skips the LLM entirely (the planner as a whole is off).
|
|
1231
|
-
*/
|
|
1232
|
-
export async function resolveRecallModeDecisionAsync(
|
|
1233
|
-
options: RecallModeGraphOptions & {
|
|
1234
|
-
config: PluginConfig;
|
|
1235
|
-
/**
|
|
1236
|
-
* Recall-operation capability gates (issue #1523). REQUIRED: the recall
|
|
1237
|
-
* orchestrator always passes a resolved set — the LLM planner gate reads
|
|
1238
|
-
* `caps.recallPlannerLlm`, never re-derives from config.
|
|
1239
|
-
*/
|
|
1240
|
-
caps: CapabilitySet;
|
|
1241
|
-
hints?: string[];
|
|
1242
|
-
llm?: FallbackLlmClient;
|
|
1243
|
-
signal?: AbortSignal;
|
|
1244
|
-
},
|
|
1245
|
-
): Promise<RecallModeDecision> {
|
|
1246
|
-
const heuristicDecision = resolveRecallModeDecision(options);
|
|
1247
|
-
|
|
1248
|
-
// Planner globally off, or LLM planning not opted into → heuristic only.
|
|
1249
|
-
// Read the resolved capability (issue #1523) — never re-derive from config.
|
|
1250
|
-
const plannerLlmEnabled = options.caps.recallPlannerLlm;
|
|
1251
|
-
if (!options.plannerEnabled || !plannerLlmEnabled) {
|
|
1252
|
-
return heuristicDecision;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
const { planRecallModeLLM } = await import("./recall-planner-llm.js");
|
|
1256
|
-
const planned = await planRecallModeLLM(
|
|
1257
|
-
options.prompt,
|
|
1258
|
-
options.hints,
|
|
1259
|
-
options.config,
|
|
1260
|
-
options.caps,
|
|
1261
|
-
options.llm,
|
|
1262
|
-
options.signal,
|
|
1263
|
-
);
|
|
1264
|
-
|
|
1265
|
-
// Shadow mode: record what the LLM would have chosen but keep the heuristic
|
|
1266
|
-
// effective decision (safe rollout / comparison — gotcha #30).
|
|
1267
|
-
if (options.config.recallPlannerShadowMode) {
|
|
1268
|
-
return {
|
|
1269
|
-
...heuristicDecision,
|
|
1270
|
-
plannerSource: planned.source,
|
|
1271
|
-
plannerReason: `shadow:${planned.reason}`,
|
|
1272
|
-
plannerLatencyMs: planned.latencyMs,
|
|
1273
|
-
plannerFallbackUsed: planned.fallbackUsed,
|
|
1274
|
-
plannerModelUsed: planned.modelUsed,
|
|
1275
|
-
plannerHeuristicMode: planned.heuristicMode,
|
|
1276
|
-
shadowLlmMode: planned.mode,
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
const llmDecision = finalizeRecallModeDecision(planned.mode, options);
|
|
1281
|
-
return {
|
|
1282
|
-
...llmDecision,
|
|
1283
|
-
plannerSource: planned.source,
|
|
1284
|
-
plannerReason: planned.reason,
|
|
1285
|
-
plannerLatencyMs: planned.latencyMs,
|
|
1286
|
-
plannerFallbackUsed: planned.fallbackUsed,
|
|
1287
|
-
plannerModelUsed: planned.modelUsed,
|
|
1288
|
-
plannerHeuristicMode: planned.heuristicMode,
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
export function computeArtifactCandidateFetchLimit(
|
|
1293
|
-
targetCount: number,
|
|
1294
|
-
): number {
|
|
1295
|
-
const cappedTarget = Math.max(0, targetCount);
|
|
1296
|
-
if (cappedTarget === 0) return 0;
|
|
1297
|
-
const headroom = Math.max(8, cappedTarget * 4);
|
|
1298
|
-
return Math.min(200, cappedTarget + headroom);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
export function computeQmdHybridFetchLimit(
|
|
1302
|
-
recallFetchLimit: number,
|
|
1303
|
-
artifactsEnabled: boolean,
|
|
1304
|
-
maxArtifactRecall: number,
|
|
1305
|
-
): number {
|
|
1306
|
-
const cappedRecallLimit = Math.max(0, recallFetchLimit);
|
|
1307
|
-
if (cappedRecallLimit === 0) return 0;
|
|
1308
|
-
if (!artifactsEnabled) return cappedRecallLimit;
|
|
1309
|
-
// Overscan when artifacts are enabled, then filter artifact paths before
|
|
1310
|
-
// re-applying the recall cap to avoid artifact-dominated top-N starvation.
|
|
1311
|
-
const artifactHeadroom = Math.max(20, Math.max(0, maxArtifactRecall) * 8);
|
|
1312
|
-
return Math.min(400, cappedRecallLimit + artifactHeadroom);
|
|
1313
|
-
}
|
|
1314
|
-
export function summarizeGraphShadowComparison(
|
|
1315
|
-
baseline: QmdSearchResult[],
|
|
1316
|
-
merged: QmdSearchResult[],
|
|
1317
|
-
topN: number,
|
|
1318
|
-
): {
|
|
1319
|
-
baselineCount: number;
|
|
1320
|
-
graphCount: number;
|
|
1321
|
-
overlapCount: number;
|
|
1322
|
-
overlapRatio: number;
|
|
1323
|
-
averageOverlapDelta: number;
|
|
1324
|
-
} {
|
|
1325
|
-
const limit = Math.max(0, Math.floor(topN));
|
|
1326
|
-
const baselineTop = limit > 0 ? baseline.slice(0, limit) : [];
|
|
1327
|
-
const graphTop = limit > 0 ? merged.slice(0, limit) : [];
|
|
1328
|
-
const baselineByPath = new Map(
|
|
1329
|
-
baselineTop.map((item) => [item.path, item.score]),
|
|
1330
|
-
);
|
|
1331
|
-
const graphByPath = new Map(graphTop.map((item) => [item.path, item.score]));
|
|
1332
|
-
|
|
1333
|
-
let overlapCount = 0;
|
|
1334
|
-
let overlapDeltaSum = 0;
|
|
1335
|
-
for (const [p, baselineScore] of baselineByPath.entries()) {
|
|
1336
|
-
const graphScore = graphByPath.get(p);
|
|
1337
|
-
if (typeof graphScore !== "number") continue;
|
|
1338
|
-
overlapCount += 1;
|
|
1339
|
-
overlapDeltaSum += graphScore - baselineScore;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
const baselineCount = baselineTop.length;
|
|
1343
|
-
return {
|
|
1344
|
-
baselineCount,
|
|
1345
|
-
graphCount: graphTop.length,
|
|
1346
|
-
overlapCount,
|
|
1347
|
-
overlapRatio: baselineCount > 0 ? overlapCount / baselineCount : 0,
|
|
1348
|
-
averageOverlapDelta: overlapCount > 0 ? overlapDeltaSum / overlapCount : 0,
|
|
1349
|
-
};
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
export function parseGraphRecallRankedResults(
|
|
1353
|
-
value: unknown,
|
|
1354
|
-
): GraphRecallRankedResult[] {
|
|
1355
|
-
if (!Array.isArray(value)) return [];
|
|
1356
|
-
const parsed: GraphRecallRankedResult[] = [];
|
|
1357
|
-
for (const entry of value) {
|
|
1358
|
-
if (!entry || typeof entry !== "object") continue;
|
|
1359
|
-
const candidate = entry as Partial<GraphRecallRankedResult>;
|
|
1360
|
-
if (
|
|
1361
|
-
typeof candidate.path !== "string" ||
|
|
1362
|
-
typeof candidate.score !== "number"
|
|
1363
|
-
)
|
|
1364
|
-
continue;
|
|
1365
|
-
parsed.push({
|
|
1366
|
-
path: candidate.path,
|
|
1367
|
-
score: candidate.score,
|
|
1368
|
-
docid: typeof candidate.docid === "string" ? candidate.docid : undefined,
|
|
1369
|
-
sourceLabels: Array.isArray(candidate.sourceLabels)
|
|
1370
|
-
? candidate.sourceLabels.filter(
|
|
1371
|
-
(item): item is string => typeof item === "string",
|
|
1372
|
-
)
|
|
1373
|
-
: [],
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
return parsed.slice(0, 64);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
export function parseMemoryIntentSnapshot(value: unknown): MemoryIntent {
|
|
1380
|
-
const candidate =
|
|
1381
|
-
value && typeof value === "object" ? (value as Partial<MemoryIntent>) : {};
|
|
1382
|
-
return {
|
|
1383
|
-
goal: typeof candidate.goal === "string" ? candidate.goal : "unknown",
|
|
1384
|
-
actionType:
|
|
1385
|
-
typeof candidate.actionType === "string"
|
|
1386
|
-
? candidate.actionType
|
|
1387
|
-
: "unknown",
|
|
1388
|
-
entityTypes: Array.isArray(candidate.entityTypes)
|
|
1389
|
-
? candidate.entityTypes.filter(
|
|
1390
|
-
(item): item is string => typeof item === "string",
|
|
1391
|
-
)
|
|
1392
|
-
: [],
|
|
1393
|
-
taskInitiation: candidate.taskInitiation === true,
|
|
1394
|
-
};
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
function buildQmdIntentHint(intent: MemoryIntent): string | undefined {
|
|
1398
|
-
const parts: string[] = [];
|
|
1399
|
-
if (intent.goal !== "unknown") {
|
|
1400
|
-
parts.push(`goal:${intent.goal.replace(/_/g, " ")}`);
|
|
1401
|
-
}
|
|
1402
|
-
if (intent.actionType !== "unknown") {
|
|
1403
|
-
parts.push(`action:${intent.actionType.replace(/_/g, " ")}`);
|
|
1404
|
-
}
|
|
1405
|
-
if (intent.entityTypes.length > 0) {
|
|
1406
|
-
parts.push(`entities:${intent.entityTypes.join(",")}`);
|
|
1407
|
-
}
|
|
1408
|
-
if (intent.taskInitiation === true) {
|
|
1409
|
-
parts.push("task_initiation");
|
|
1410
|
-
}
|
|
1411
|
-
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
export function parseQmdRecallResults(value: unknown): QmdSearchResult[] {
|
|
1415
|
-
if (!Array.isArray(value)) return [];
|
|
1416
|
-
const parsed: QmdSearchResult[] = [];
|
|
1417
|
-
for (const entry of value) {
|
|
1418
|
-
if (!entry || typeof entry !== "object") continue;
|
|
1419
|
-
const candidate = entry as Partial<QmdSearchResult>;
|
|
1420
|
-
if (
|
|
1421
|
-
typeof candidate.path !== "string" ||
|
|
1422
|
-
typeof candidate.score !== "number"
|
|
1423
|
-
)
|
|
1424
|
-
continue;
|
|
1425
|
-
parsed.push({
|
|
1426
|
-
docid: typeof candidate.docid === "string" ? candidate.docid : "",
|
|
1427
|
-
path: candidate.path,
|
|
1428
|
-
snippet: typeof candidate.snippet === "string" ? candidate.snippet : "",
|
|
1429
|
-
score: candidate.score,
|
|
1430
|
-
explain: parseQmdExplain(candidate.explain),
|
|
1431
|
-
transport:
|
|
1432
|
-
candidate.transport === "daemon" ||
|
|
1433
|
-
candidate.transport === "subprocess" ||
|
|
1434
|
-
candidate.transport === "hybrid" ||
|
|
1435
|
-
candidate.transport === "scoped_prefilter"
|
|
1436
|
-
? candidate.transport
|
|
1437
|
-
: undefined,
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
return parsed.slice(0, 32);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
export function mergeArtifactRecallCandidates(
|
|
1444
|
-
candidatesByNamespace: MemoryFile[][],
|
|
1445
|
-
limit: number,
|
|
1446
|
-
): MemoryFile[] {
|
|
1447
|
-
const cappedLimit = Math.max(0, limit);
|
|
1448
|
-
if (cappedLimit === 0) return [];
|
|
1449
|
-
|
|
1450
|
-
const out: MemoryFile[] = [];
|
|
1451
|
-
const seen = new Set<string>();
|
|
1452
|
-
let offset = 0;
|
|
1453
|
-
while (out.length < cappedLimit) {
|
|
1454
|
-
let hasAnyCandidateAtOffset = false;
|
|
1455
|
-
for (const list of candidatesByNamespace) {
|
|
1456
|
-
if (offset >= list.length) continue;
|
|
1457
|
-
hasAnyCandidateAtOffset = true;
|
|
1458
|
-
const item = list[offset];
|
|
1459
|
-
const dedupeKey = `${item.frontmatter.id}:${item.frontmatter.sourceMemoryId ?? ""}:${item.content}`;
|
|
1460
|
-
if (seen.has(dedupeKey)) continue;
|
|
1461
|
-
seen.add(dedupeKey);
|
|
1462
|
-
out.push(item);
|
|
1463
|
-
if (out.length >= cappedLimit) break;
|
|
1464
|
-
}
|
|
1465
|
-
if (!hasAnyCandidateAtOffset) break;
|
|
1466
|
-
offset += 1;
|
|
1467
|
-
}
|
|
1468
|
-
return out;
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
export function resolveRecentThreadMemoryPaths(options: {
|
|
1472
|
-
threadEpisodeIds: string[];
|
|
1473
|
-
currentMemoryId: string;
|
|
1474
|
-
allMemsForGraph: MemoryFile[] | null | undefined;
|
|
1475
|
-
pathById?: Map<string, string>;
|
|
1476
|
-
storageDir: string;
|
|
1477
|
-
maxRecent: number;
|
|
1478
|
-
}): string[] {
|
|
1479
|
-
const maxRecent = Math.max(0, options.maxRecent);
|
|
1480
|
-
if (options.threadEpisodeIds.length === 0 || maxRecent === 0) return [];
|
|
1481
|
-
const pathById =
|
|
1482
|
-
options.pathById ??
|
|
1483
|
-
buildMemoryPathById(options.allMemsForGraph, options.storageDir);
|
|
1484
|
-
if (pathById.size === 0) return [];
|
|
1485
|
-
|
|
1486
|
-
// #1635 (defensive): skip pending_review ids from legacy episode sets.
|
|
1487
|
-
const pendingReviewIds = new Set<string>(
|
|
1488
|
-
(options.allMemsForGraph ?? [])
|
|
1489
|
-
.filter((m) => m.frontmatter.status === "pending_review" && m.frontmatter.id)
|
|
1490
|
-
.map((m) => m.frontmatter.id as string),
|
|
1491
|
-
);
|
|
1492
|
-
|
|
1493
|
-
return options.threadEpisodeIds
|
|
1494
|
-
.filter((id) => id !== options.currentMemoryId)
|
|
1495
|
-
.filter((id) => !pendingReviewIds.has(id))
|
|
1496
|
-
.slice(-maxRecent)
|
|
1497
|
-
.map((id) => pathById.get(id))
|
|
1498
|
-
.filter((p): p is string => typeof p === "string" && p.length > 0);
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
export function buildMemoryPathById(
|
|
1502
|
-
allMemsForGraph: MemoryFile[] | null | undefined,
|
|
1503
|
-
storageDir: string,
|
|
1504
|
-
): Map<string, string> {
|
|
1505
|
-
const pathById = new Map<string, string>();
|
|
1506
|
-
for (const mem of allMemsForGraph ?? []) {
|
|
1507
|
-
const id = mem.frontmatter.id;
|
|
1508
|
-
if (!id) continue;
|
|
1509
|
-
pathById.set(id, path.relative(storageDir, mem.path));
|
|
1510
|
-
}
|
|
1511
|
-
return pathById;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
export function appendMemoryToGraphContext(options: {
|
|
1515
|
-
allMemsForGraph: MemoryFile[] | null | undefined;
|
|
1516
|
-
storageDir: string;
|
|
1517
|
-
memoryRelPath: string;
|
|
1518
|
-
memoryId: string;
|
|
1519
|
-
category: MemoryFile["frontmatter"]["category"];
|
|
1520
|
-
content: string;
|
|
1521
|
-
entityRef: string | undefined;
|
|
1522
|
-
}): void {
|
|
1523
|
-
if (!Array.isArray(options.allMemsForGraph)) return;
|
|
1524
|
-
|
|
1525
|
-
const nowIso = new Date().toISOString();
|
|
1526
|
-
options.allMemsForGraph.push({
|
|
1527
|
-
path: path.join(options.storageDir, options.memoryRelPath),
|
|
1528
|
-
content: options.content,
|
|
1529
|
-
frontmatter: {
|
|
1530
|
-
id: options.memoryId,
|
|
1531
|
-
category: options.category,
|
|
1532
|
-
created: nowIso,
|
|
1533
|
-
updated: nowIso,
|
|
1534
|
-
source: "extraction",
|
|
1535
|
-
confidence: 0.8,
|
|
1536
|
-
confidenceTier: "implied",
|
|
1537
|
-
tags: [],
|
|
1538
|
-
entityRef: options.entityRef,
|
|
1539
|
-
status: "active",
|
|
1540
|
-
},
|
|
1541
|
-
});
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
export function resolvePersistedMemoryRelativePath(options: {
|
|
1545
|
-
memoryId: string;
|
|
1546
|
-
pathById: Map<string, string>;
|
|
1547
|
-
category: string;
|
|
1548
|
-
}): string {
|
|
1549
|
-
const persisted = options.pathById.get(options.memoryId);
|
|
1550
|
-
if (persisted) return persisted;
|
|
1551
|
-
if (options.category === "correction") {
|
|
1552
|
-
return path.join("corrections", `${options.memoryId}.md`);
|
|
1553
|
-
}
|
|
1554
|
-
// Pick the subtree that matches the StorageManager.writeMemory routing
|
|
1555
|
-
// so fallback paths (used before memoryPathById has seen the fresh
|
|
1556
|
-
// write) agree with where the file actually lives. Routing goes through
|
|
1557
|
-
// the shared categoryDirName() chokepoint (utils/category-dir.ts) so
|
|
1558
|
-
// every category — decisions/, preferences/, reasoning-traces/, ... —
|
|
1559
|
-
// resolves to the same dir the writer used; otherwise graph edges point
|
|
1560
|
-
// at the wrong subtree and graph expansion silently drops those nodes
|
|
1561
|
-
// when readMemoryByPath cannot resolve them (issue #564 PR 3 / #1546).
|
|
1562
|
-
const subtree = categoryDirName(options.category);
|
|
1563
|
-
const idParts = options.memoryId.split("-");
|
|
1564
|
-
const maybeTimestamp = Number(idParts[1]);
|
|
1565
|
-
if (Number.isFinite(maybeTimestamp) && maybeTimestamp > 0) {
|
|
1566
|
-
const day = new Date(maybeTimestamp).toISOString().slice(0, 10);
|
|
1567
|
-
return path.join(subtree, day, `${options.memoryId}.md`);
|
|
1568
|
-
}
|
|
1569
|
-
return path.join(subtree, `${options.memoryId}.md`);
|
|
1570
|
-
}
|
|
1571
549
|
|
|
1572
550
|
export class Orchestrator {
|
|
1573
551
|
|
|
@@ -1706,7 +684,8 @@ export class Orchestrator {
|
|
|
1706
684
|
statuses: Map<string, "active" | "superseded" | "archived" | "missing">;
|
|
1707
685
|
}
|
|
1708
686
|
>();
|
|
1709
|
-
|
|
687
|
+
/** Read by NamespaceReadFanoutCoordinator (seam 26), hence not `private`. */
|
|
688
|
+
static readonly ARTIFACT_STATUS_CACHE_TTL_MS = 60_000;
|
|
1710
689
|
|
|
1711
690
|
// Access tracking buffer (Phase 1A)
|
|
1712
691
|
// Maps memoryId -> {count, lastAccessed} for batched updates
|
|
@@ -2273,98 +1252,17 @@ export class Orchestrator {
|
|
|
2273
1252
|
resolvedStorageDir: string,
|
|
2274
1253
|
configured: Set<string>,
|
|
2275
1254
|
): { namespace: string; identityToken: string; storageDir: string } {
|
|
2276
|
-
|
|
1255
|
+
return this.namespaceReadFanoutCoordinator.preferNamespaceStorageDirHintOwner(
|
|
2277
1256
|
current,
|
|
2278
|
-
resolvedStorageDir,
|
|
2279
|
-
configured,
|
|
2280
|
-
);
|
|
2281
|
-
const candidateRank = this.namespaceStorageDirHintOwnershipRank(
|
|
2282
1257
|
candidate,
|
|
2283
1258
|
resolvedStorageDir,
|
|
2284
1259
|
configured,
|
|
2285
1260
|
);
|
|
2286
|
-
if (candidateRank < currentRank) return candidate;
|
|
2287
|
-
if (candidateRank > currentRank) return current;
|
|
2288
|
-
|
|
2289
|
-
const byName = candidate.namespace.localeCompare(current.namespace);
|
|
2290
|
-
if (byName < 0) return candidate;
|
|
2291
|
-
if (byName > 0) return current;
|
|
2292
|
-
return candidate.identityToken.localeCompare(current.identityToken) < 0
|
|
2293
|
-
? candidate
|
|
2294
|
-
: current;
|
|
2295
1261
|
}
|
|
2296
1262
|
|
|
2297
1263
|
private loadNamespaceStorageDirHintsFromCatalog(): void {
|
|
2298
|
-
|
|
2299
|
-
this.namespaceStorageDirHintsLoaded = true;
|
|
2300
|
-
const catalogPath = path.join(this.config.memoryDir, "state", "namespaces.jsonl");
|
|
2301
|
-
if (!existsSync(catalogPath)) return;
|
|
2302
|
-
|
|
2303
|
-
let body: string;
|
|
2304
|
-
try {
|
|
2305
|
-
body = readFileSync(catalogPath, "utf8");
|
|
2306
|
-
} catch {
|
|
2307
|
-
return;
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
const compactedByNamespace = new Map<
|
|
2311
|
-
string,
|
|
2312
|
-
{ namespace: string; identityToken: string; storageDir: string }
|
|
2313
|
-
>();
|
|
2314
|
-
for (const line of body.split(/\r?\n/)) {
|
|
2315
|
-
const trimmed = line.trim();
|
|
2316
|
-
if (!trimmed) continue;
|
|
2317
|
-
try {
|
|
2318
|
-
const parsed = JSON.parse(trimmed) as unknown;
|
|
2319
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
2320
|
-
const record = parsed as Record<string, unknown>;
|
|
2321
|
-
if (
|
|
2322
|
-
typeof record.namespace !== "string" ||
|
|
2323
|
-
typeof record.storageDir !== "string" ||
|
|
2324
|
-
typeof record.identityToken !== "string"
|
|
2325
|
-
) {
|
|
2326
|
-
continue;
|
|
2327
|
-
}
|
|
2328
|
-
const namespace = normalizeNamespaceIdentity(record.namespace);
|
|
2329
|
-
if (!namespace || record.identityToken !== namespaceIdentityToken(namespace)) continue;
|
|
2330
|
-
compactedByNamespace.set(namespace, {
|
|
2331
|
-
namespace,
|
|
2332
|
-
identityToken: record.identityToken,
|
|
2333
|
-
storageDir: record.storageDir,
|
|
2334
|
-
});
|
|
2335
|
-
} catch {
|
|
2336
|
-
// Catalog hints are best-effort. The catalog reader still owns full recovery.
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
const configured = new Set(
|
|
2341
|
-
this.configuredNamespaceList().map((namespace) => normalizeNamespaceIdentity(namespace)),
|
|
1264
|
+
return this.namespaceReadFanoutCoordinator.loadNamespaceStorageDirHintsFromCatalog(
|
|
2342
1265
|
);
|
|
2343
|
-
const preferredByStorageDir = new Map<
|
|
2344
|
-
string,
|
|
2345
|
-
{ namespace: string; identityToken: string; storageDir: string }
|
|
2346
|
-
>();
|
|
2347
|
-
for (const record of compactedByNamespace.values()) {
|
|
2348
|
-
if (!this.storageDirMatchesNamespaceHint(record.namespace, record.storageDir)) {
|
|
2349
|
-
continue;
|
|
2350
|
-
}
|
|
2351
|
-
const resolvedStorageDir = path.resolve(record.storageDir);
|
|
2352
|
-
const current = preferredByStorageDir.get(resolvedStorageDir);
|
|
2353
|
-
preferredByStorageDir.set(
|
|
2354
|
-
resolvedStorageDir,
|
|
2355
|
-
current
|
|
2356
|
-
? this.preferNamespaceStorageDirHintOwner(
|
|
2357
|
-
current,
|
|
2358
|
-
record,
|
|
2359
|
-
resolvedStorageDir,
|
|
2360
|
-
configured,
|
|
2361
|
-
)
|
|
2362
|
-
: record,
|
|
2363
|
-
);
|
|
2364
|
-
}
|
|
2365
|
-
for (const record of preferredByStorageDir.values()) {
|
|
2366
|
-
this.rememberNamespaceStorageDirHint(record.namespace, record.storageDir);
|
|
2367
|
-
}
|
|
2368
1266
|
}
|
|
2369
1267
|
|
|
2370
1268
|
private async maintenanceNamespaces(
|
|
@@ -2440,68 +1338,9 @@ export class Orchestrator {
|
|
|
2440
1338
|
searchOptions?: SearchQueryOptions;
|
|
2441
1339
|
execution?: SearchExecutionOptions;
|
|
2442
1340
|
}): Promise<QmdSearchResult[]> {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
options.namespaces.length === 0
|
|
2447
|
-
) {
|
|
2448
|
-
return [];
|
|
2449
|
-
}
|
|
2450
|
-
const namespaces = resolveNamespaceCapabilities(this.config).namespaces
|
|
2451
|
-
? Array.from(
|
|
2452
|
-
new Set(
|
|
2453
|
-
(options.namespaces?.length
|
|
2454
|
-
? options.namespaces
|
|
2455
|
-
: this.configuredNamespaceList()
|
|
2456
|
-
)
|
|
2457
|
-
.map((value) => value.trim())
|
|
2458
|
-
.filter(Boolean),
|
|
2459
|
-
),
|
|
2460
|
-
)
|
|
2461
|
-
: [this.config.defaultNamespace];
|
|
2462
|
-
|
|
2463
|
-
if (!resolveNamespaceCapabilities(this.config).namespaces) {
|
|
2464
|
-
switch (options.mode) {
|
|
2465
|
-
case "hybrid":
|
|
2466
|
-
return await this.qmd.hybridSearch(
|
|
2467
|
-
options.query,
|
|
2468
|
-
undefined,
|
|
2469
|
-
options.maxResults,
|
|
2470
|
-
options.execution,
|
|
2471
|
-
);
|
|
2472
|
-
case "bm25":
|
|
2473
|
-
return await this.qmd.bm25Search(
|
|
2474
|
-
options.query,
|
|
2475
|
-
undefined,
|
|
2476
|
-
options.maxResults,
|
|
2477
|
-
options.execution,
|
|
2478
|
-
);
|
|
2479
|
-
case "vector":
|
|
2480
|
-
return await this.qmd.vectorSearch(
|
|
2481
|
-
options.query,
|
|
2482
|
-
undefined,
|
|
2483
|
-
options.maxResults,
|
|
2484
|
-
options.execution,
|
|
2485
|
-
);
|
|
2486
|
-
default:
|
|
2487
|
-
return await this.qmd.search(
|
|
2488
|
-
options.query,
|
|
2489
|
-
undefined,
|
|
2490
|
-
options.maxResults,
|
|
2491
|
-
options.searchOptions,
|
|
2492
|
-
options.execution,
|
|
2493
|
-
);
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
return await this.namespaceSearchRouter.searchAcrossNamespaces({
|
|
2498
|
-
query: options.query,
|
|
2499
|
-
namespaces,
|
|
2500
|
-
maxResults: options.maxResults,
|
|
2501
|
-
mode: options.mode,
|
|
2502
|
-
searchOptions: options.searchOptions,
|
|
2503
|
-
execution: options.execution,
|
|
2504
|
-
});
|
|
1341
|
+
return this.namespaceReadFanoutCoordinator.searchAcrossNamespaces(
|
|
1342
|
+
options,
|
|
1343
|
+
);
|
|
2505
1344
|
}
|
|
2506
1345
|
|
|
2507
1346
|
async searchHealthForNamespace(
|
|
@@ -3048,92 +1887,10 @@ export class Orchestrator {
|
|
|
3048
1887
|
storage: StorageManager,
|
|
3049
1888
|
sourceIds: string[],
|
|
3050
1889
|
): Promise<Map<string, "active" | "superseded" | "archived" | "missing">> {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
snapshot !== undefined &&
|
|
3056
|
-
Date.now() - snapshot.loadedAtMs <=
|
|
3057
|
-
Orchestrator.ARTIFACT_STATUS_CACHE_TTL_MS &&
|
|
3058
|
-
snapshot.statusVersion === currentStatusVersion;
|
|
3059
|
-
|
|
3060
|
-
const rebuildSnapshot = async () => {
|
|
3061
|
-
const MAX_STABLE_READ_ATTEMPTS = 3;
|
|
3062
|
-
let latestStatuses = new Map<
|
|
3063
|
-
string,
|
|
3064
|
-
"active" | "superseded" | "archived" | "missing"
|
|
3065
|
-
>();
|
|
3066
|
-
let latestVersionAfter = storage.getMemoryStatusVersion();
|
|
3067
|
-
|
|
3068
|
-
for (let attempt = 0; attempt < MAX_STABLE_READ_ATTEMPTS; attempt += 1) {
|
|
3069
|
-
const versionBefore = storage.getMemoryStatusVersion();
|
|
3070
|
-
const allMemories = await storage.readAllMemories();
|
|
3071
|
-
const versionAfter = storage.getMemoryStatusVersion();
|
|
3072
|
-
latestVersionAfter = versionAfter;
|
|
3073
|
-
latestStatuses = new Map(
|
|
3074
|
-
allMemories.map((m) => [
|
|
3075
|
-
m.frontmatter.id,
|
|
3076
|
-
(m.frontmatter.status ?? "active") as
|
|
3077
|
-
| "active"
|
|
3078
|
-
| "superseded"
|
|
3079
|
-
| "archived"
|
|
3080
|
-
| "missing",
|
|
3081
|
-
]),
|
|
3082
|
-
);
|
|
3083
|
-
|
|
3084
|
-
if (versionAfter === versionBefore) {
|
|
3085
|
-
const rebuilt = {
|
|
3086
|
-
loadedAtMs: Date.now(),
|
|
3087
|
-
statusVersion: versionAfter,
|
|
3088
|
-
statuses: latestStatuses,
|
|
3089
|
-
};
|
|
3090
|
-
this.artifactSourceStatusCache.set(storage, rebuilt);
|
|
3091
|
-
return rebuilt;
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
// Sustained write churn: return latest read without caching a potentially torn snapshot.
|
|
3096
|
-
return {
|
|
3097
|
-
loadedAtMs: Date.now(),
|
|
3098
|
-
statusVersion: latestVersionAfter,
|
|
3099
|
-
statuses: latestStatuses,
|
|
3100
|
-
};
|
|
3101
|
-
};
|
|
3102
|
-
|
|
3103
|
-
if (!isFresh) {
|
|
3104
|
-
snapshot = await rebuildSnapshot();
|
|
3105
|
-
} else {
|
|
3106
|
-
// Warm cache may miss brand-new sourceMemoryId values created after snapshot build.
|
|
3107
|
-
// Refresh once on-demand when unseen IDs are requested.
|
|
3108
|
-
const hasUnknownSourceIds = sourceIds.some(
|
|
3109
|
-
(id) => !snapshot?.statuses.has(id),
|
|
3110
|
-
);
|
|
3111
|
-
if (hasUnknownSourceIds) {
|
|
3112
|
-
snapshot = await rebuildSnapshot();
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
// Persist negative lookups in the cached snapshot so stale source IDs do not
|
|
3117
|
-
// trigger repeated full snapshot rebuilds on every matching recall.
|
|
3118
|
-
for (const id of sourceIds) {
|
|
3119
|
-
if (!snapshot?.statuses.has(id)) {
|
|
3120
|
-
snapshot?.statuses.set(id, "missing");
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
const statuses = new Map<
|
|
3125
|
-
string,
|
|
3126
|
-
"active" | "superseded" | "archived" | "missing"
|
|
3127
|
-
>();
|
|
3128
|
-
for (const id of sourceIds) {
|
|
3129
|
-
const status = snapshot?.statuses.get(id);
|
|
3130
|
-
if (status) {
|
|
3131
|
-
statuses.set(id, status);
|
|
3132
|
-
} else {
|
|
3133
|
-
statuses.set(id, "missing");
|
|
3134
|
-
}
|
|
3135
|
-
}
|
|
3136
|
-
return statuses;
|
|
1890
|
+
return this.namespaceReadFanoutCoordinator.resolveArtifactSourceStatuses(
|
|
1891
|
+
storage,
|
|
1892
|
+
sourceIds,
|
|
1893
|
+
);
|
|
3137
1894
|
}
|
|
3138
1895
|
|
|
3139
1896
|
/**
|
|
@@ -3226,27 +1983,6 @@ export class Orchestrator {
|
|
|
3226
1983
|
);
|
|
3227
1984
|
}
|
|
3228
1985
|
|
|
3229
|
-
/**
|
|
3230
|
-
* Run the pattern-reinforcement maintenance job (issue #687 PR 2/4).
|
|
3231
|
-
*
|
|
3232
|
-
* Cadence-gated on `patternReinforcementCadenceMs` so every caller
|
|
3233
|
-
* (orchestrator cron path, MCP tool, CLI) shares a single floor —
|
|
3234
|
-
* none can call this on a hot loop and burn the corpus. When the
|
|
3235
|
-
* feature is disabled or the cadence has not elapsed, returns a
|
|
3236
|
-
* synthetic "skipped" result rather than throwing.
|
|
3237
|
-
*
|
|
3238
|
-
* Cadence tracking is per-namespace so a tenant-scoped MCP run in
|
|
3239
|
-
* one namespace does not silence a cron run in another (PR #730
|
|
3240
|
-
* review feedback, Codex P2). Pass `force: true` for ad-hoc
|
|
3241
|
-
* operator runs that must bypass the cadence floor — mirrors the
|
|
3242
|
-
* pattern used by other maintenance MCP tools.
|
|
3243
|
-
*
|
|
3244
|
-
* `force` deliberately does NOT bypass the master
|
|
3245
|
-
* `patternReinforcementEnabled` flag (PR #730 review feedback,
|
|
3246
|
-
* Cursor Medium). Operators who have explicitly disabled the
|
|
3247
|
-
* feature must not have their corpus mutated by an MCP tool call —
|
|
3248
|
-
* the only way to run the job is to enable the feature in config.
|
|
3249
|
-
*/
|
|
3250
1986
|
async runPatternReinforcement(options: {
|
|
3251
1987
|
force?: boolean;
|
|
3252
1988
|
namespace?: string;
|
|
@@ -3256,69 +1992,16 @@ export class Orchestrator {
|
|
|
3256
1992
|
namespace: string;
|
|
3257
1993
|
result?: PatternReinforcementResult;
|
|
3258
1994
|
}> {
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
// force=true. `force` only relaxes the cadence floor below.
|
|
3262
|
-
if (!resolveConsolidationCapabilities(this.config).patternReinforcement) {
|
|
3263
|
-
return { ran: false, skippedReason: "disabled", namespace: cadenceKey };
|
|
3264
|
-
}
|
|
3265
|
-
const cadence = this.config.patternReinforcementCadenceMs;
|
|
3266
|
-
const lastAt = this.lastPatternReinforcementAtByNs.get(cadenceKey);
|
|
3267
|
-
if (
|
|
3268
|
-
!options.force &&
|
|
3269
|
-
cadence > 0 &&
|
|
3270
|
-
lastAt !== undefined &&
|
|
3271
|
-
Date.now() - lastAt < cadence
|
|
3272
|
-
) {
|
|
3273
|
-
return { ran: false, skippedReason: "cadence", namespace: cadenceKey };
|
|
3274
|
-
}
|
|
3275
|
-
const storage = options.namespace
|
|
3276
|
-
? await this.getStorage(options.namespace)
|
|
3277
|
-
: this.storage;
|
|
3278
|
-
const result = await runPatternReinforcement(storage, {
|
|
3279
|
-
categories: this.config.patternReinforcementCategories,
|
|
3280
|
-
minCount: this.config.patternReinforcementMinCount,
|
|
3281
|
-
});
|
|
3282
|
-
this.lastPatternReinforcementAtByNs.set(cadenceKey, Date.now());
|
|
3283
|
-
log.debug(
|
|
3284
|
-
`pattern reinforcement [ns=${cadenceKey || "(default)"}]: clusters=${result.clustersFound} canonicalsUpdated=${result.canonicalsUpdated} duplicatesSuperseded=${result.duplicatesSuperseded}`,
|
|
1995
|
+
return this.workspaceOpsCoordinator.runPatternReinforcement(
|
|
1996
|
+
options,
|
|
3285
1997
|
);
|
|
3286
|
-
return { ran: true, result, namespace: cadenceKey };
|
|
3287
1998
|
}
|
|
3288
1999
|
|
|
3289
|
-
/**
|
|
3290
|
-
* Fan out pattern reinforcement across all maintained namespaces (issue #1500).
|
|
3291
|
-
* Delegates per-namespace execution to {@link runPatternReinforcement} while
|
|
3292
|
-
* the planner handles discovery, budgeting, locking, and status recording.
|
|
3293
|
-
* When namespaces are disabled, runs once against default storage.
|
|
3294
|
-
*/
|
|
3295
2000
|
async runPatternReinforcementFanout(options: {
|
|
3296
2001
|
force?: boolean;
|
|
3297
2002
|
} = {}): Promise<NamespaceMaintenanceSummary> {
|
|
3298
|
-
return this.
|
|
3299
|
-
|
|
3300
|
-
async (ctx) => {
|
|
3301
|
-
const result = await this.runPatternReinforcement({
|
|
3302
|
-
namespace: ctx.candidate.namespace,
|
|
3303
|
-
force: options.force,
|
|
3304
|
-
});
|
|
3305
|
-
// runPatternReinforcement has its own per-namespace cadence gate
|
|
3306
|
-
// (lastPatternReinforcementAtByNs). When it throttles (ran:false),
|
|
3307
|
-
// signal skip so the planner records state:"skipped" and does NOT
|
|
3308
|
-
// touch lastMaintenanceAt — otherwise a throttled namespace would
|
|
3309
|
-
// look maintained while pattern reinforcement never ran (#1500
|
|
3310
|
-
// review: cadence-skip accuracy).
|
|
3311
|
-
if (!result.ran) {
|
|
3312
|
-
return {
|
|
3313
|
-
skipped: true,
|
|
3314
|
-
skipReason: result.skippedReason ?? "throttled",
|
|
3315
|
-
};
|
|
3316
|
-
}
|
|
3317
|
-
return result.result
|
|
3318
|
-
? { itemCount: result.result.clustersFound }
|
|
3319
|
-
: { itemCount: 0 };
|
|
3320
|
-
},
|
|
3321
|
-
{ enabled: resolveConsolidationCapabilities(this.config).patternReinforcement },
|
|
2003
|
+
return this.workspaceOpsCoordinator.runPatternReinforcementFanout(
|
|
2004
|
+
options,
|
|
3322
2005
|
);
|
|
3323
2006
|
}
|
|
3324
2007
|
|
|
@@ -3432,75 +2115,8 @@ export class Orchestrator {
|
|
|
3432
2115
|
}
|
|
3433
2116
|
|
|
3434
2117
|
async maybeRunFileHygiene(): Promise<void> {
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
const now = Date.now();
|
|
3439
|
-
if (now - this.lastFileHygieneRunAtMs < hygiene.runMinIntervalMs) return;
|
|
3440
|
-
this.lastFileHygieneRunAtMs = now;
|
|
3441
|
-
|
|
3442
|
-
// Rotation first (keeps bootstrap files small).
|
|
3443
|
-
if (hygiene.rotateEnabled) {
|
|
3444
|
-
for (const rel of hygiene.rotatePaths) {
|
|
3445
|
-
const abs = path.isAbsolute(rel)
|
|
3446
|
-
? rel
|
|
3447
|
-
: path.join(this.config.workspaceDir, rel);
|
|
3448
|
-
try {
|
|
3449
|
-
const raw = await readFile(abs, "utf-8");
|
|
3450
|
-
if (raw.length > hygiene.rotateMaxBytes) {
|
|
3451
|
-
const archiveDir = path.join(
|
|
3452
|
-
this.config.workspaceDir,
|
|
3453
|
-
hygiene.archiveDir,
|
|
3454
|
-
);
|
|
3455
|
-
const base = path.basename(abs);
|
|
3456
|
-
const prefix =
|
|
3457
|
-
base
|
|
3458
|
-
.toUpperCase()
|
|
3459
|
-
.replace(/\.MD$/i, "")
|
|
3460
|
-
.replace(/[^A-Z0-9]+/g, "-") || "FILE";
|
|
3461
|
-
const { newContent } = await rotateMarkdownFileToArchive({
|
|
3462
|
-
filePath: abs,
|
|
3463
|
-
archiveDir,
|
|
3464
|
-
archivePrefix: prefix,
|
|
3465
|
-
keepTailChars: hygiene.rotateKeepTailChars,
|
|
3466
|
-
});
|
|
3467
|
-
await writeFile(abs, newContent, "utf-8");
|
|
3468
|
-
}
|
|
3469
|
-
} catch {
|
|
3470
|
-
// ignore missing/unreadable targets
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
}
|
|
3474
|
-
|
|
3475
|
-
// Lint (warn before truncation risk).
|
|
3476
|
-
if (hygiene.lintEnabled) {
|
|
3477
|
-
const warnings = await lintWorkspaceFiles({
|
|
3478
|
-
workspaceDir: this.config.workspaceDir,
|
|
3479
|
-
paths: hygiene.lintPaths,
|
|
3480
|
-
budgetBytes: hygiene.lintBudgetBytes,
|
|
3481
|
-
warnRatio: hygiene.lintWarnRatio,
|
|
3482
|
-
});
|
|
3483
|
-
for (const w of warnings) {
|
|
3484
|
-
log.warn(w.message);
|
|
3485
|
-
}
|
|
3486
|
-
|
|
3487
|
-
if (hygiene.warningsLogEnabled && warnings.length > 0) {
|
|
3488
|
-
const fp = path.join(this.config.memoryDir, hygiene.warningsLogPath);
|
|
3489
|
-
await mkdir(path.dirname(fp), { recursive: true });
|
|
3490
|
-
const stamp = new Date().toISOString();
|
|
3491
|
-
const block =
|
|
3492
|
-
`\n\n## ${stamp}\n\n` +
|
|
3493
|
-
warnings.map((w) => `- ${w.message}`).join("\n") +
|
|
3494
|
-
"\n";
|
|
3495
|
-
let existing = "";
|
|
3496
|
-
try {
|
|
3497
|
-
existing = await readFile(fp, "utf-8");
|
|
3498
|
-
} catch {
|
|
3499
|
-
existing = "# Engram File Hygiene Warnings\n";
|
|
3500
|
-
}
|
|
3501
|
-
await writeFile(fp, existing + block, "utf-8");
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
2118
|
+
return this.workspaceOpsCoordinator.maybeRunFileHygiene(
|
|
2119
|
+
);
|
|
3504
2120
|
}
|
|
3505
2121
|
|
|
3506
2122
|
async runBootstrap(options: BootstrapOptions): Promise<BootstrapResult> {
|
|
@@ -3651,282 +2267,22 @@ export class Orchestrator {
|
|
|
3651
2267
|
return this.generateDaySummary(gathered);
|
|
3652
2268
|
}
|
|
3653
2269
|
|
|
3654
|
-
/**
|
|
3655
|
-
* Read today's facts and hourly summaries from storage, returning them
|
|
3656
|
-
* as a formatted string suitable for generateDaySummary().
|
|
3657
|
-
*/
|
|
3658
2270
|
async gatherTodayFacts(
|
|
3659
2271
|
namespace?: string,
|
|
3660
2272
|
options: DaySummaryGatherOptions = {},
|
|
3661
2273
|
): Promise<string> {
|
|
3662
|
-
|
|
3663
|
-
namespace
|
|
3664
|
-
|
|
3665
|
-
: this.config.defaultNamespace;
|
|
3666
|
-
const storage = await this.storageRouter.storageFor(ns);
|
|
3667
|
-
const configuredTimeZone = normalizeIanaTimeZone(options.timeZone)
|
|
3668
|
-
?? normalizeIanaTimeZone(this.config.daySummaryTimezone);
|
|
3669
|
-
const timeZone =
|
|
3670
|
-
configuredTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
3671
|
-
const now = options.now instanceof Date && Number.isFinite(options.now.getTime())
|
|
3672
|
-
? options.now
|
|
3673
|
-
: new Date();
|
|
3674
|
-
const targetLocalDate = formatDateInTimeZone(now, timeZone);
|
|
3675
|
-
// Facts are stored under UTC date directories, while the summary target is
|
|
3676
|
-
// a local calendar day. Scan the UTC-date envelope that overlaps the local
|
|
3677
|
-
// day, then filter parseable fact timestamps to that configured local day.
|
|
3678
|
-
const datesToScan = utcDateKeysForLocalDay(now, timeZone);
|
|
3679
|
-
const MAX_CHARS = 100_000;
|
|
3680
|
-
|
|
3681
|
-
// --- Read memory files from each category dir × date directory ---
|
|
3682
|
-
// Iterate every recall category dir (RECALL_FALLBACK_DIRS — single source
|
|
3683
|
-
// of truth) so the day summary includes decisions/, moments/, ... not just
|
|
3684
|
-
// facts/ (#1546). corrections/ is flat, so corrections/<date>/ never exists
|
|
3685
|
-
// and is skipped by the ENOENT guard — preserving the prior exclusion. The
|
|
3686
|
-
// per-file created→local-day filter below is unchanged.
|
|
3687
|
-
//
|
|
3688
|
-
// Symlink/containment hardening (mirrors scanDir / the CLI walker): the
|
|
3689
|
-
// gathered contents feed the day-summary LLM input, so a symlinked category
|
|
3690
|
-
// dir (decisions/ → outside memoryDir) must not be followed and leak files.
|
|
3691
|
-
// Resolve the store root once; skip symlinked / out-of-root dirs and
|
|
3692
|
-
// entries; skip the scan gracefully if the root can't be resolved.
|
|
3693
|
-
const facts: MemoryFile[] = [];
|
|
3694
|
-
let memoryRootReal: string | null = null;
|
|
3695
|
-
try {
|
|
3696
|
-
memoryRootReal = await realpath(storage.dir);
|
|
3697
|
-
} catch {
|
|
3698
|
-
memoryRootReal = null;
|
|
3699
|
-
}
|
|
3700
|
-
for (const categoryDir of RECALL_FALLBACK_DIRS) {
|
|
3701
|
-
if (memoryRootReal === null) break;
|
|
3702
|
-
for (const date of datesToScan) {
|
|
3703
|
-
const dateDir = path.join(storage.dir, categoryDir, date);
|
|
3704
|
-
try {
|
|
3705
|
-
const dirStat = await lstat(dateDir);
|
|
3706
|
-
if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
|
|
3707
|
-
assertPathInsideRoot(memoryRootReal, await realpath(dateDir), dateDir);
|
|
3708
|
-
const entries = await readdir(dateDir, { withFileTypes: true });
|
|
3709
|
-
for (const entry of entries) {
|
|
3710
|
-
if (entry.isSymbolicLink()) continue;
|
|
3711
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
3712
|
-
const fullPath = path.join(dateDir, entry.name);
|
|
3713
|
-
try {
|
|
3714
|
-
assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
|
|
3715
|
-
const raw = await readFile(fullPath, "utf-8");
|
|
3716
|
-
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
3717
|
-
if (!fmMatch) continue;
|
|
3718
|
-
const fmBlock = fmMatch[1];
|
|
3719
|
-
const content = fmMatch[2].trim();
|
|
3720
|
-
const fm: Record<string, string> = {};
|
|
3721
|
-
for (const line of fmBlock.split("\n")) {
|
|
3722
|
-
const colonIdx = line.indexOf(":");
|
|
3723
|
-
if (colonIdx === -1) continue;
|
|
3724
|
-
fm[line.slice(0, colonIdx).trim()] = line
|
|
3725
|
-
.slice(colonIdx + 1)
|
|
3726
|
-
.trim();
|
|
3727
|
-
}
|
|
3728
|
-
const created = fm.created || "unknown";
|
|
3729
|
-
const createdAt = parseFiniteDate(created);
|
|
3730
|
-
if (
|
|
3731
|
-
createdAt &&
|
|
3732
|
-
formatDateInTimeZone(createdAt, timeZone) !== targetLocalDate
|
|
3733
|
-
) {
|
|
3734
|
-
continue;
|
|
3735
|
-
}
|
|
3736
|
-
facts.push({
|
|
3737
|
-
path: fullPath,
|
|
3738
|
-
frontmatter: {
|
|
3739
|
-
id: fm.id || path.basename(entry.name, ".md"),
|
|
3740
|
-
category: (fm.category as any) || "fact",
|
|
3741
|
-
created,
|
|
3742
|
-
updated: fm.updated || created,
|
|
3743
|
-
source: fm.source || "unknown",
|
|
3744
|
-
confidence: parseFloat(fm.confidence || "0.8"),
|
|
3745
|
-
confidenceTier: (fm.confidenceTier as any) || "implied",
|
|
3746
|
-
tags: [],
|
|
3747
|
-
},
|
|
3748
|
-
content,
|
|
3749
|
-
});
|
|
3750
|
-
} catch {
|
|
3751
|
-
// Skip unreadable files
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
} catch {
|
|
3755
|
-
// Absent dir (ENOENT), symlinked/out-of-root dir, or containment
|
|
3756
|
-
// violation — skip this category/date without aborting the summary.
|
|
3757
|
-
}
|
|
3758
|
-
}
|
|
3759
|
-
}
|
|
3760
|
-
|
|
3761
|
-
// Sort facts by created timestamp (most recent last) so truncation keeps newest
|
|
3762
|
-
facts.sort((a, b) => {
|
|
3763
|
-
if (a.frontmatter.created === b.frontmatter.created) return 0;
|
|
3764
|
-
return a.frontmatter.created < b.frontmatter.created ? -1 : 1;
|
|
3765
|
-
});
|
|
3766
|
-
|
|
3767
|
-
// --- Read hourly summaries for the scanned dates ---
|
|
3768
|
-
const hourlySummaries: string[] = [];
|
|
3769
|
-
const hourlyBaseDir = path.join(storage.dir, "summaries", "hourly");
|
|
3770
|
-
try {
|
|
3771
|
-
const sessionKeys = await readdir(hourlyBaseDir, { withFileTypes: true });
|
|
3772
|
-
for (const sk of sessionKeys) {
|
|
3773
|
-
if (!sk.isDirectory()) continue;
|
|
3774
|
-
for (const date of datesToScan) {
|
|
3775
|
-
const summaryFile = path.join(hourlyBaseDir, sk.name, `${date}.md`);
|
|
3776
|
-
try {
|
|
3777
|
-
const raw = await readFile(summaryFile, "utf-8");
|
|
3778
|
-
const filtered = filterHourlySummaryMarkdownForLocalDay(
|
|
3779
|
-
raw,
|
|
3780
|
-
date,
|
|
3781
|
-
timeZone,
|
|
3782
|
-
targetLocalDate,
|
|
3783
|
-
);
|
|
3784
|
-
if (filtered) {
|
|
3785
|
-
hourlySummaries.push(filtered);
|
|
3786
|
-
}
|
|
3787
|
-
} catch {
|
|
3788
|
-
// No summary file for this session/date
|
|
3789
|
-
}
|
|
3790
|
-
}
|
|
3791
|
-
}
|
|
3792
|
-
} catch {
|
|
3793
|
-
// No hourly summaries directory
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
|
-
// --- Format and truncate ---
|
|
3797
|
-
let formatted = formatDaySummaryMemories(facts);
|
|
3798
|
-
if (hourlySummaries.length > 0) {
|
|
3799
|
-
formatted +=
|
|
3800
|
-
"\n\n---\n## Hourly Summaries\n\n" +
|
|
3801
|
-
hourlySummaries.join("\n\n---\n\n");
|
|
3802
|
-
}
|
|
3803
|
-
|
|
3804
|
-
// Truncate intelligently if over budget: drop oldest facts first
|
|
3805
|
-
if (formatted.length > MAX_CHARS) {
|
|
3806
|
-
// Re-build with fewer facts, keeping most recent
|
|
3807
|
-
while (facts.length > 1 && formatted.length > MAX_CHARS) {
|
|
3808
|
-
facts.shift(); // drop oldest
|
|
3809
|
-
formatted = formatDaySummaryMemories(facts);
|
|
3810
|
-
if (hourlySummaries.length > 0) {
|
|
3811
|
-
formatted +=
|
|
3812
|
-
"\n\n---\n## Hourly Summaries\n\n" +
|
|
3813
|
-
hourlySummaries.join("\n\n---\n\n");
|
|
3814
|
-
}
|
|
3815
|
-
}
|
|
3816
|
-
// If still over, hard truncate
|
|
3817
|
-
if (formatted.length > MAX_CHARS) {
|
|
3818
|
-
formatted = formatted.slice(0, MAX_CHARS);
|
|
3819
|
-
}
|
|
3820
|
-
}
|
|
3821
|
-
|
|
3822
|
-
log.info(
|
|
3823
|
-
`gatherTodayFacts: collected ${facts.length} facts, ${hourlySummaries.length} hourly summaries for ${targetLocalDate} (${timeZone}, ${formatted.length} chars)`,
|
|
2274
|
+
return this.workspaceOpsCoordinator.gatherTodayFacts(
|
|
2275
|
+
namespace,
|
|
2276
|
+
options,
|
|
3824
2277
|
);
|
|
3825
|
-
|
|
3826
|
-
return formatted;
|
|
3827
2278
|
}
|
|
3828
2279
|
|
|
3829
2280
|
previewMemoryActionEvent(
|
|
3830
2281
|
event: Omit<MemoryActionEvent, "timestamp"> & { timestamp?: string },
|
|
3831
2282
|
): MemoryActionEvent {
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
? event.namespace
|
|
3835
|
-
: this.config.defaultNamespace;
|
|
3836
|
-
const eligibility = parseMemoryActionEligibilityContext(
|
|
3837
|
-
event.policyEligibility,
|
|
2283
|
+
return this.workspaceOpsCoordinator.previewMemoryActionEvent(
|
|
2284
|
+
event,
|
|
3838
2285
|
);
|
|
3839
|
-
const policy = evaluateMemoryActionPolicy({
|
|
3840
|
-
action: event.action,
|
|
3841
|
-
eligibility,
|
|
3842
|
-
options: {
|
|
3843
|
-
actionsEnabled: resolveCompressionCapabilities(this.config).contextCompressionActions,
|
|
3844
|
-
maxCompressionTokensPerHour: this.config.maxCompressionTokensPerHour,
|
|
3845
|
-
},
|
|
3846
|
-
});
|
|
3847
|
-
const dryRun = event.dryRun === true;
|
|
3848
|
-
|
|
3849
|
-
const normalizedOutcome = dryRun
|
|
3850
|
-
? event.outcome === "failed"
|
|
3851
|
-
? "failed"
|
|
3852
|
-
: "skipped"
|
|
3853
|
-
: policy.decision === "allow"
|
|
3854
|
-
? event.outcome
|
|
3855
|
-
: event.outcome === "failed"
|
|
3856
|
-
? "failed"
|
|
3857
|
-
: "skipped";
|
|
3858
|
-
const sourceSessionKey =
|
|
3859
|
-
typeof event.sourceSessionKey === "string" &&
|
|
3860
|
-
event.sourceSessionKey.length > 0
|
|
3861
|
-
? event.sourceSessionKey
|
|
3862
|
-
: typeof event.sessionKey === "string" && event.sessionKey.length > 0
|
|
3863
|
-
? event.sessionKey
|
|
3864
|
-
: undefined;
|
|
3865
|
-
const outputMemoryIds = Array.isArray(event.outputMemoryIds)
|
|
3866
|
-
? Array.from(
|
|
3867
|
-
new Set(
|
|
3868
|
-
event.outputMemoryIds.filter(
|
|
3869
|
-
(value): value is string =>
|
|
3870
|
-
typeof value === "string" && value.length > 0,
|
|
3871
|
-
),
|
|
3872
|
-
),
|
|
3873
|
-
)
|
|
3874
|
-
: [];
|
|
3875
|
-
|
|
3876
|
-
const reasonParts = [
|
|
3877
|
-
event.reason,
|
|
3878
|
-
`policy:${policy.decision}`,
|
|
3879
|
-
policy.rationale,
|
|
3880
|
-
].filter(
|
|
3881
|
-
(part): part is string => typeof part === "string" && part.length > 0,
|
|
3882
|
-
);
|
|
3883
|
-
|
|
3884
|
-
return {
|
|
3885
|
-
...event,
|
|
3886
|
-
schemaVersion: event.schemaVersion ?? 1,
|
|
3887
|
-
actionId:
|
|
3888
|
-
typeof event.actionId === "string" && event.actionId.length > 0
|
|
3889
|
-
? event.actionId
|
|
3890
|
-
: `memact-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
3891
|
-
outcome: normalizedOutcome,
|
|
3892
|
-
status:
|
|
3893
|
-
event.status ??
|
|
3894
|
-
(dryRun && policy.decision === "allow" && event.outcome !== "failed"
|
|
3895
|
-
? "validated"
|
|
3896
|
-
: normalizedOutcome === "applied"
|
|
3897
|
-
? "applied"
|
|
3898
|
-
: "rejected"),
|
|
3899
|
-
actor:
|
|
3900
|
-
typeof event.actor === "string" && event.actor.length > 0
|
|
3901
|
-
? event.actor
|
|
3902
|
-
: "engram",
|
|
3903
|
-
subsystem:
|
|
3904
|
-
typeof event.subsystem === "string" && event.subsystem.length > 0
|
|
3905
|
-
? event.subsystem
|
|
3906
|
-
: "memory_action",
|
|
3907
|
-
reason: reasonParts.join(" | "),
|
|
3908
|
-
namespace,
|
|
3909
|
-
sessionKey: sourceSessionKey ?? event.sessionKey,
|
|
3910
|
-
sourceSessionKey,
|
|
3911
|
-
inputSummary:
|
|
3912
|
-
typeof event.inputSummary === "string" && event.inputSummary.length > 0
|
|
3913
|
-
? event.inputSummary
|
|
3914
|
-
: undefined,
|
|
3915
|
-
outputMemoryIds,
|
|
3916
|
-
dryRun,
|
|
3917
|
-
policyVersion:
|
|
3918
|
-
typeof event.policyVersion === "string" &&
|
|
3919
|
-
event.policyVersion.length > 0
|
|
3920
|
-
? event.policyVersion
|
|
3921
|
-
: "memory-action-policy.v1",
|
|
3922
|
-
timestamp:
|
|
3923
|
-
typeof event.timestamp === "string" && event.timestamp.length > 0
|
|
3924
|
-
? event.timestamp
|
|
3925
|
-
: new Date().toISOString(),
|
|
3926
|
-
policyDecision: policy.decision,
|
|
3927
|
-
policyRationale: policy.rationale,
|
|
3928
|
-
policyEligibility: eligibility,
|
|
3929
|
-
};
|
|
3930
2286
|
}
|
|
3931
2287
|
|
|
3932
2288
|
async appendMemoryActionEvent(
|
|
@@ -4087,63 +2443,9 @@ export class Orchestrator {
|
|
|
4087
2443
|
return this.conversationIndexCoordinator.rebuild(sessionKey, hours, opts);
|
|
4088
2444
|
}
|
|
4089
2445
|
|
|
4090
|
-
/**
|
|
4091
|
-
* Validate local LLM model availability and context window compatibility.
|
|
4092
|
-
* Warns the user if there's a mismatch.
|
|
4093
|
-
*/
|
|
4094
2446
|
private async validateLocalLlmModel(): Promise<void> {
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
const modelInfo = await this.localLlm.getLoadedModelInfo();
|
|
4098
|
-
if (!modelInfo) {
|
|
4099
|
-
log.warn(
|
|
4100
|
-
"Local LLM validation: Could not query model info from server",
|
|
4101
|
-
);
|
|
4102
|
-
log.warn(
|
|
4103
|
-
"Local LLM validation: Could not query model info. " +
|
|
4104
|
-
"Ensure LM Studio/Ollama is running with the model loaded.",
|
|
4105
|
-
);
|
|
4106
|
-
return;
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
|
-
// Check for context window mismatch
|
|
4110
|
-
const configuredMaxContext = this.config.localLlmMaxContext;
|
|
4111
|
-
|
|
4112
|
-
if (modelInfo.contextWindow) {
|
|
4113
|
-
log.debug(
|
|
4114
|
-
`Local LLM: ${modelInfo.id} loaded with ${modelInfo.contextWindow.toLocaleString()} token context window`,
|
|
4115
|
-
);
|
|
4116
|
-
|
|
4117
|
-
if (
|
|
4118
|
-
configuredMaxContext &&
|
|
4119
|
-
configuredMaxContext > modelInfo.contextWindow
|
|
4120
|
-
) {
|
|
4121
|
-
log.warn(
|
|
4122
|
-
`Local LLM context mismatch: engram configured for ${configuredMaxContext.toLocaleString()} tokens, ` +
|
|
4123
|
-
`but ${modelInfo.id} only supports ${modelInfo.contextWindow.toLocaleString()}. ` +
|
|
4124
|
-
`Reducing to ${modelInfo.contextWindow.toLocaleString()} to avoid errors.`,
|
|
4125
|
-
);
|
|
4126
|
-
// Update the config in-memory to match actual capability
|
|
4127
|
-
// (This is a temporary fix - user should update their config)
|
|
4128
|
-
(this.config as { localLlmMaxContext?: number }).localLlmMaxContext =
|
|
4129
|
-
modelInfo.contextWindow;
|
|
4130
|
-
}
|
|
4131
|
-
} else {
|
|
4132
|
-
log.debug(
|
|
4133
|
-
`Local LLM: ${modelInfo.id} loaded (context window not reported by server)`,
|
|
4134
|
-
);
|
|
4135
|
-
|
|
4136
|
-
if (!configuredMaxContext) {
|
|
4137
|
-
log.warn(
|
|
4138
|
-
"Local LLM: Server did not report context window. " +
|
|
4139
|
-
"If you get 'context length exceeded' errors, set localLlmMaxContext in your config. " +
|
|
4140
|
-
"Common defaults: LM Studio (32K), Ollama (2K-128K depending on model).",
|
|
4141
|
-
);
|
|
4142
|
-
}
|
|
4143
|
-
}
|
|
4144
|
-
} catch (err) {
|
|
4145
|
-
log.warn(`Local LLM validation failed: ${err}`);
|
|
4146
|
-
}
|
|
2447
|
+
return this.workspaceOpsCoordinator.validateLocalLlmModel(
|
|
2448
|
+
);
|
|
4147
2449
|
}
|
|
4148
2450
|
|
|
4149
2451
|
async recall(
|
|
@@ -4410,15 +2712,11 @@ export class Orchestrator {
|
|
|
4410
2712
|
recallNamespaces: string[],
|
|
4411
2713
|
targetCount: number,
|
|
4412
2714
|
): Promise<MemoryFile[]> {
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
this.fetchActiveArtifactsForNamespace(namespace, prompt, targetCount),
|
|
4418
|
-
),
|
|
2715
|
+
return this.namespaceReadFanoutCoordinator.recallArtifactsAcrossNamespaces(
|
|
2716
|
+
prompt,
|
|
2717
|
+
recallNamespaces,
|
|
2718
|
+
targetCount,
|
|
4419
2719
|
);
|
|
4420
|
-
|
|
4421
|
-
return mergeArtifactRecallCandidates(filteredByNamespace, targetCount);
|
|
4422
2720
|
}
|
|
4423
2721
|
|
|
4424
2722
|
private scopeQueryAwarePaths(
|
|
@@ -4458,51 +2756,12 @@ export class Orchestrator {
|
|
|
4458
2756
|
allowArchived?: boolean;
|
|
4459
2757
|
},
|
|
4460
2758
|
): Promise<QmdSearchResult[]> {
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
Array.from(candidatePaths).map(async (memoryPath) => {
|
|
4468
|
-
const namespace = resolveNamespaceCapabilities(this.config).namespaces
|
|
4469
|
-
? this.namespaceFromPath(memoryPath)
|
|
4470
|
-
: this.config.defaultNamespace;
|
|
4471
|
-
const storage = await this.storageRouter.storageFor(namespace);
|
|
4472
|
-
return await storage.readMemoryByPath(memoryPath);
|
|
4473
|
-
}),
|
|
4474
|
-
)
|
|
4475
|
-
).filter((memory): memory is MemoryFile => memory !== null);
|
|
4476
|
-
|
|
4477
|
-
const results: QmdSearchResult[] = [];
|
|
4478
|
-
for (const memory of memories) {
|
|
4479
|
-
const status = memory.frontmatter.status ?? "active";
|
|
4480
|
-
if (!options?.allowArchived && status !== "active") continue;
|
|
4481
|
-
|
|
4482
|
-
const haystack = [
|
|
4483
|
-
memory.content,
|
|
4484
|
-
memory.frontmatter.category,
|
|
4485
|
-
...(memory.frontmatter.tags ?? []),
|
|
4486
|
-
]
|
|
4487
|
-
.join(" ")
|
|
4488
|
-
.toLowerCase();
|
|
4489
|
-
let hits = 0;
|
|
4490
|
-
for (const token of tokens) {
|
|
4491
|
-
if (haystack.includes(token)) hits += 1;
|
|
4492
|
-
}
|
|
4493
|
-
const score = tokens.length > 0 ? hits / tokens.length : 0.01;
|
|
4494
|
-
if (tokens.length > 0 && hits === 0) continue;
|
|
4495
|
-
|
|
4496
|
-
results.push({
|
|
4497
|
-
docid: memory.frontmatter.id,
|
|
4498
|
-
path: memory.path,
|
|
4499
|
-
score,
|
|
4500
|
-
snippet: memory.content.slice(0, 400).replace(/\n/g, " "),
|
|
4501
|
-
transport: "scoped_prefilter",
|
|
4502
|
-
});
|
|
4503
|
-
}
|
|
4504
|
-
|
|
4505
|
-
return results.sort((a, b) => b.score - a.score).slice(0, cappedLimit);
|
|
2759
|
+
return this.namespaceReadFanoutCoordinator.searchScopedMemoryCandidates(
|
|
2760
|
+
candidatePaths,
|
|
2761
|
+
query,
|
|
2762
|
+
limit,
|
|
2763
|
+
options,
|
|
2764
|
+
);
|
|
4506
2765
|
}
|
|
4507
2766
|
|
|
4508
2767
|
private async fetchQmdMemoryResultsWithArtifactTopUp(
|
|
@@ -4596,26 +2855,9 @@ export class Orchestrator {
|
|
|
4596
2855
|
private async resolveStateDirForNamespace(
|
|
4597
2856
|
namespace: string,
|
|
4598
2857
|
): Promise<string> {
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
}
|
|
4602
|
-
if (namespace !== this.config.defaultNamespace) {
|
|
4603
|
-
return path.join(this.config.memoryDir, "namespaces", namespace, "state");
|
|
4604
|
-
}
|
|
4605
|
-
const candidate = path.join(
|
|
4606
|
-
this.config.memoryDir,
|
|
4607
|
-
"namespaces",
|
|
4608
|
-
this.config.defaultNamespace,
|
|
2858
|
+
return this.namespaceReadFanoutCoordinator.resolveStateDirForNamespace(
|
|
2859
|
+
namespace,
|
|
4609
2860
|
);
|
|
4610
|
-
try {
|
|
4611
|
-
const candidateStat = await stat(candidate);
|
|
4612
|
-
if (candidateStat.isDirectory()) {
|
|
4613
|
-
return path.join(candidate, "state");
|
|
4614
|
-
}
|
|
4615
|
-
} catch {
|
|
4616
|
-
// Fall back to the legacy root when the migrated default namespace directory is absent.
|
|
4617
|
-
}
|
|
4618
|
-
return path.join(this.config.memoryDir, "state");
|
|
4619
2861
|
}
|
|
4620
2862
|
|
|
4621
2863
|
private buildGraphRecallRankedResults(
|
|
@@ -4716,7 +2958,6 @@ export class Orchestrator {
|
|
|
4716
2958
|
}
|
|
4717
2959
|
|
|
4718
2960
|
|
|
4719
|
-
|
|
4720
2961
|
/**
|
|
4721
2962
|
* Clock source for the shared post-retrieval assembly/enrichment budget. The
|
|
4722
2963
|
* deadline is set from this value and every expiry check reads it back, so a
|
|
@@ -4739,84 +2980,9 @@ export class Orchestrator {
|
|
|
4739
2980
|
|
|
4740
2981
|
private get recallInternalCoordinator(): RecallInternalCoordinator {
|
|
4741
2982
|
if (!this._recallInternalCoordinator) {
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
get _recallWorkspaceOverrides() { return self._recallWorkspaceOverrides; },
|
|
4746
|
-
appendRecallSection: (sectionBuckets, sectionId, content) => self.appendRecallSection(sectionBuckets, sectionId, content),
|
|
4747
|
-
applyColdFallbackPipeline: (options) => self.applyColdFallbackPipeline(options),
|
|
4748
|
-
applyTrustScoreToBranch: (results, namespaces, caps, label) => self.applyTrustScoreToBranch(results, namespaces, caps, label),
|
|
4749
|
-
assembleRecallSections: (sectionBuckets, budgetOverride) => self.assembleRecallSections(sectionBuckets, budgetOverride),
|
|
4750
|
-
boostSearchResults: (results, _recallNamespaces, prompt, preloadedMemoryMap, options) => self.boostSearchResults(results, _recallNamespaces, prompt, preloadedMemoryMap, options),
|
|
4751
|
-
boxBuilderFor: (storage) => self.boxBuilderFor(storage),
|
|
4752
|
-
buildCompressionGuidelineRecallSection: () => self.buildCompressionGuidelineRecallSection(),
|
|
4753
|
-
buildConfiguredQmdSearchOptions: (queryText) => self.buildConfiguredQmdSearchOptions(queryText),
|
|
4754
|
-
buildGraphRecallRankedResults: (results, sourceLabelResolver, limit) => self.buildGraphRecallRankedResults(results, sourceLabelResolver, limit),
|
|
4755
|
-
buildIdentityContinuitySection: (options) => self.buildIdentityContinuitySection(options),
|
|
4756
|
-
buildLastRecallBudgetSummary: (options) => self.buildLastRecallBudgetSummary(options),
|
|
4757
|
-
buildQueryAwarePrefilter: (prompt, recallNamespaces) => self.buildQueryAwarePrefilter(prompt, recallNamespaces),
|
|
4758
|
-
collectLastRecallSources: (sectionBuckets, recallSource) => self.collectLastRecallSources(sectionBuckets, recallSource),
|
|
4759
|
-
get compounding() { return self.compounding; },
|
|
4760
|
-
get config() { return self.config; },
|
|
4761
|
-
currentPolicyVersion: () => self.currentPolicyVersion(),
|
|
4762
|
-
diversifyAndLimitRecallResults: (sectionId, results, limit, retrievalQuery, caps) => self.diversifyAndLimitRecallResults(sectionId, results, limit, retrievalQuery, caps),
|
|
4763
|
-
effectiveCronRecallInstructionHeavyTokenCap: () => self.effectiveCronRecallInstructionHeavyTokenCap(),
|
|
4764
|
-
emitTrace: (event) => self.emitTrace(event),
|
|
4765
|
-
expandResultsViaGraph: (options) => self.expandResultsViaGraph(options),
|
|
4766
|
-
extractMemoryIdsFromResults: (results) => self.extractMemoryIdsFromResults(results),
|
|
4767
|
-
get fastLlmForRerank() { return self.fastLlmForRerank; },
|
|
4768
|
-
fetchQmdMemoryResultsWithArtifactTopUp: (prompt, qmdFetchLimit, qmdHybridFetchLimit, options) => self.fetchQmdMemoryResultsWithArtifactTopUp(prompt, qmdFetchLimit, qmdHybridFetchLimit, options),
|
|
4769
|
-
filterSearchResultsForRecall: (results, preloadedMemoryMap, options) => self.filterSearchResultsForRecall(results, preloadedMemoryMap, options),
|
|
4770
|
-
formatCausalTrajectoryResults: (results) => self.formatCausalTrajectoryResults(results),
|
|
4771
|
-
formatConversationRecallSection: (results, maxChars) => self.formatConversationRecallSection(results, maxChars),
|
|
4772
|
-
formatHarmonicRetrievalResults: (results) => self.formatHarmonicRetrievalResults(results),
|
|
4773
|
-
formatObjectiveStateResults: (results) => self.formatObjectiveStateResults(results),
|
|
4774
|
-
formatQmdResults: (title, results, sessionKey, trustByPath) => self.formatQmdResults(title, results, sessionKey, trustByPath),
|
|
4775
|
-
formatTrustZoneResults: (results) => self.formatTrustZoneResults(results),
|
|
4776
|
-
formatVerifiedEpisodeResults: (results) => self.formatVerifiedEpisodeResults(results),
|
|
4777
|
-
formatVerifiedSemanticRuleResults: (results) => self.formatVerifiedSemanticRuleResults(results),
|
|
4778
|
-
formatWorkProductResults: (results) => self.formatWorkProductResults(results),
|
|
4779
|
-
getCodingContextForSession: (sessionKey) => self.getCodingContextForSession(sessionKey),
|
|
4780
|
-
getPeerIdForSession: (sessionKey) => self.getPeerIdForSession(sessionKey),
|
|
4781
|
-
getRecallBudgetChars: (override) => self.getRecallBudgetChars(override),
|
|
4782
|
-
getRecallSectionEntry: (sectionId) => self.getRecallSectionEntry(sectionId),
|
|
4783
|
-
getRecallSectionMaxChars: (sectionId) => self.getRecallSectionMaxChars(sectionId),
|
|
4784
|
-
getRecallSectionNumber: (sectionId, key) => self.getRecallSectionNumber(sectionId, key),
|
|
4785
|
-
getStorage: (namespace) => self.getStorage(namespace),
|
|
4786
|
-
get handleHistory() { return self.handleHistory; },
|
|
4787
|
-
isRecallSectionEnabled: (sectionId, defaultEnabled) => self.isRecallSectionEnabled(sectionId, defaultEnabled),
|
|
4788
|
-
isSpecializedRecallSectionEnabled: (sectionId, topLevelEnabled) => self.isSpecializedRecallSectionEnabled(sectionId, topLevelEnabled),
|
|
4789
|
-
get lastQmdReprobeAtMs() { return self.lastQmdReprobeAtMs; },
|
|
4790
|
-
set lastQmdReprobeAtMs(value) { self.lastQmdReprobeAtMs = value; },
|
|
4791
|
-
get lastRecall() { return self.lastRecall; },
|
|
4792
|
-
get lastXraySnapshot() { return self.lastXraySnapshot; },
|
|
4793
|
-
set lastXraySnapshot(value) { self.lastXraySnapshot = value; },
|
|
4794
|
-
get lcmEngine() { return self.lcmEngine; },
|
|
4795
|
-
get namespaceCatalog() { return self.namespaceCatalog; },
|
|
4796
|
-
namespaceFromPath: (p) => self.namespaceFromPath(p),
|
|
4797
|
-
get profiler() { return self.profiler; },
|
|
4798
|
-
publishRecallResults: (options) => self.publishRecallResults(options),
|
|
4799
|
-
get qmd() { return self.qmd; },
|
|
4800
|
-
queueEvalShadowRecall: (record) => self.queueEvalShadowRecall(record),
|
|
4801
|
-
readAllMemoriesForNamespaces: (namespaces) => self.readAllMemoriesForNamespaces(namespaces),
|
|
4802
|
-
recallArtifactsAcrossNamespaces: (prompt, recallNamespaces, targetCount) => self.recallArtifactsAcrossNamespaces(prompt, recallNamespaces, targetCount),
|
|
4803
|
-
recallAssemblyClockMs: () => self.recallAssemblyClockMs(),
|
|
4804
|
-
recordLastGraphRecallSnapshot: (options) => self.recordLastGraphRecallSnapshot(options),
|
|
4805
|
-
recordLastIntentSnapshot: (options) => self.recordLastIntentSnapshot(options),
|
|
4806
|
-
recordLastIntentSnapshotForNamespace: (options) => self.recordLastIntentSnapshotForNamespace(options),
|
|
4807
|
-
recordLastQmdRecallSnapshot: (options) => self.recordLastQmdRecallSnapshot(options),
|
|
4808
|
-
get rerankCache() { return self.rerankCache; },
|
|
4809
|
-
searchConversationRecallResults: (retrievalQuery, topK) => self.searchConversationRecallResults(retrievalQuery, topK),
|
|
4810
|
-
searchEmbeddingFallback: (query, limit) => self.searchEmbeddingFallback(query, limit),
|
|
4811
|
-
get sharedContext() { return self.sharedContext; },
|
|
4812
|
-
get storage() { return self.storage; },
|
|
4813
|
-
get storageRouter() { return self.storageRouter; },
|
|
4814
|
-
get summarizer() { return self.summarizer; },
|
|
4815
|
-
get tmtBuilder() { return self.tmtBuilder; },
|
|
4816
|
-
get transcript() { return self.transcript; },
|
|
4817
|
-
truncateArtifactForRecall: (text, maxChars) => self.truncateArtifactForRecall(text, maxChars),
|
|
4818
|
-
truncateRecallSectionToBudget: (content, maxChars) => self.truncateRecallSectionToBudget(content, maxChars),
|
|
4819
|
-
});
|
|
2983
|
+
this._recallInternalCoordinator = new RecallInternalCoordinator(
|
|
2984
|
+
selfDeps<ConstructorParameters<typeof RecallInternalCoordinator>[0]>(this),
|
|
2985
|
+
);
|
|
4820
2986
|
}
|
|
4821
2987
|
return this._recallInternalCoordinator;
|
|
4822
2988
|
}
|
|
@@ -4831,41 +2997,9 @@ export class Orchestrator {
|
|
|
4831
2997
|
|
|
4832
2998
|
private get recallSearchPipelineCoordinator(): RecallSearchPipelineCoordinator {
|
|
4833
2999
|
if (!this._recallSearchPipelineCoordinator) {
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
applyMemoryWorthRerank: (results, namespaces) => self.applyMemoryWorthRerank(results, namespaces),
|
|
4838
|
-
applyTrustScoreRerank: (results, namespaces) => self.applyTrustScoreRerank(results, namespaces),
|
|
4839
|
-
boostSearchResults: (results, _recallNamespaces, prompt, preloadedMemoryMap, options) => self.boostSearchResults(results, _recallNamespaces, prompt, preloadedMemoryMap, options),
|
|
4840
|
-
buildConfiguredQmdSearchOptions: (queryText) => self.buildConfiguredQmdSearchOptions(queryText),
|
|
4841
|
-
buildQueryAwarePrefilter: (prompt, recallNamespaces) => self.buildQueryAwarePrefilter(prompt, recallNamespaces),
|
|
4842
|
-
get config() { return self.config; },
|
|
4843
|
-
diversifyAndLimitRecallResults: (sectionId, results, limit, retrievalQuery, caps) => self.diversifyAndLimitRecallResults(sectionId, results, limit, retrievalQuery, caps),
|
|
4844
|
-
effectiveRecencyWeight: () => self.effectiveRecencyWeight(),
|
|
4845
|
-
get embeddingFallback() { return self.embeddingFallback; },
|
|
4846
|
-
expandResultsViaGraph: (options) => self.expandResultsViaGraph(options),
|
|
4847
|
-
get fastLlmForRerank() { return self.fastLlmForRerank; },
|
|
4848
|
-
fetchQmdMemoryResultsWithArtifactTopUp: (prompt, qmdFetchLimit, qmdHybridFetchLimit, options) => self.fetchQmdMemoryResultsWithArtifactTopUp(prompt, qmdFetchLimit, qmdHybridFetchLimit, options),
|
|
4849
|
-
filterSearchResultsByRecallSafety: (results, memoryByPath, options) => self.filterSearchResultsByRecallSafety(results, memoryByPath, options),
|
|
4850
|
-
filterSearchResultsForRecall: (results, preloadedMemoryMap, options) => self.filterSearchResultsForRecall(results, preloadedMemoryMap, options),
|
|
4851
|
-
loadSearchResultMemoryMap: (results, preloadedMemoryMap, options) => self.loadSearchResultMemoryMap(results, preloadedMemoryMap, options),
|
|
4852
|
-
namespaceFromPath: (p) => self.namespaceFromPath(p),
|
|
4853
|
-
get negatives() { return self.negatives; },
|
|
4854
|
-
get qmd() { return self.qmd; },
|
|
4855
|
-
readArchivedMemoriesForNamespaces: (namespaces) => self.readArchivedMemoriesForNamespaces(namespaces),
|
|
4856
|
-
readQmdResultMemory: (resultPath, fallbackStorage, recallNamespaces) => self.readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces),
|
|
4857
|
-
get relevance() { return self.relevance; },
|
|
4858
|
-
get rerankCache() { return self.rerankCache; },
|
|
4859
|
-
resolveArtifactSourceStatuses: (storage, sourceIds) => self.resolveArtifactSourceStatuses(storage, sourceIds),
|
|
4860
|
-
resolveColdQmdResultForRecall: (result, fallbackStorage, recallNamespaces) => self.resolveColdQmdResultForRecall(result, fallbackStorage, recallNamespaces),
|
|
4861
|
-
scopeQueryAwarePaths: (paths, recallNamespaces) => self.scopeQueryAwarePaths(paths, recallNamespaces),
|
|
4862
|
-
searchAcrossNamespaces: (options) => self.searchAcrossNamespaces(options),
|
|
4863
|
-
searchLongTermArchiveFallback: (prompt, recallNamespaces, limit, queryAwarePrefilter, abortSignal) => self.searchLongTermArchiveFallback(prompt, recallNamespaces, limit, queryAwarePrefilter, abortSignal),
|
|
4864
|
-
searchScopedMemoryCandidates: (candidatePaths, query, limit, options) => self.searchScopedMemoryCandidates(candidatePaths, query, limit, options),
|
|
4865
|
-
get storage() { return self.storage; },
|
|
4866
|
-
get storageRouter() { return self.storageRouter; },
|
|
4867
|
-
get utilityRuntimeValues() { return self.utilityRuntimeValues; },
|
|
4868
|
-
});
|
|
3000
|
+
this._recallSearchPipelineCoordinator = new RecallSearchPipelineCoordinator(
|
|
3001
|
+
selfDeps<ConstructorParameters<typeof RecallSearchPipelineCoordinator>[0]>(this),
|
|
3002
|
+
);
|
|
4869
3003
|
}
|
|
4870
3004
|
return this._recallSearchPipelineCoordinator;
|
|
4871
3005
|
}
|
|
@@ -4879,26 +3013,9 @@ export class Orchestrator {
|
|
|
4879
3013
|
|
|
4880
3014
|
private get turnIngestionCoordinator(): TurnIngestionCoordinator {
|
|
4881
3015
|
if (!this._turnIngestionCoordinator) {
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
get buffer() { return self.buffer; },
|
|
4886
|
-
bulkImportWriteNamespace: () => self.bulkImportWriteNamespace(),
|
|
4887
|
-
get config() { return self.config; },
|
|
4888
|
-
get extractionQueueCoordinator() { return self.extractionQueueCoordinator; },
|
|
4889
|
-
getStorage: (namespace) => self.getStorage(namespace),
|
|
4890
|
-
get heartbeatObserverChains() { return self.heartbeatObserverChains; },
|
|
4891
|
-
get lcmEngine() { return self.lcmEngine; },
|
|
4892
|
-
get passiveCorrectionDedup() { return self.passiveCorrectionDedup; },
|
|
4893
|
-
passiveCorrectionService: () => self.passiveCorrectionService(),
|
|
4894
|
-
get passiveCorrectionTelemetry() { return self.passiveCorrectionTelemetry; },
|
|
4895
|
-
queueBufferedExtraction: (turnsToExtract, reason, options) => self.queueBufferedExtraction(turnsToExtract, reason, options),
|
|
4896
|
-
resolveMemoryIdOrHandle: (ref, sessionKey) => self.resolveMemoryIdOrHandle(ref, sessionKey),
|
|
4897
|
-
runExtraction: (...args) => self.runExtraction(...args),
|
|
4898
|
-
get sessionObserver() { return self.sessionObserver; },
|
|
4899
|
-
shouldQueueExtraction: (turns, options) => self.shouldQueueExtraction(turns, options),
|
|
4900
|
-
get transcript() { return self.transcript; },
|
|
4901
|
-
});
|
|
3016
|
+
this._turnIngestionCoordinator = new TurnIngestionCoordinator(
|
|
3017
|
+
selfDeps<ConstructorParameters<typeof TurnIngestionCoordinator>[0]>(this),
|
|
3018
|
+
);
|
|
4902
3019
|
}
|
|
4903
3020
|
return this._turnIngestionCoordinator;
|
|
4904
3021
|
}
|
|
@@ -4912,25 +3029,9 @@ export class Orchestrator {
|
|
|
4912
3029
|
|
|
4913
3030
|
private get recallIntrospectionCoordinator(): RecallIntrospectionCoordinator {
|
|
4914
3031
|
if (!this._recallIntrospectionCoordinator) {
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
annotateDirectAnswerTier: (prompt, sessionKey, namespaces, expectedIdentity, caps, _parentAbortSignal) => self.annotateDirectAnswerTier(prompt, sessionKey, namespaces, expectedIdentity, caps, _parentAbortSignal),
|
|
4919
|
-
get config() { return self.config; },
|
|
4920
|
-
get directAnswerObservationChain() { return self.directAnswerObservationChain; },
|
|
4921
|
-
set directAnswerObservationChain(value) { self.directAnswerObservationChain = value; },
|
|
4922
|
-
effectiveCronRecallInstructionHeavyTokenCap: () => self.effectiveCronRecallInstructionHeavyTokenCap(),
|
|
4923
|
-
get faithfulnessCounters() { return self.faithfulnessCounters; },
|
|
4924
|
-
getCodingContextForSession: (sessionKey) => self.getCodingContextForSession(sessionKey),
|
|
4925
|
-
getLastGraphRecallSnapshot: (namespace) => self.getLastGraphRecallSnapshot(namespace),
|
|
4926
|
-
getLastIntentSnapshot: (namespace) => self.getLastIntentSnapshot(namespace),
|
|
4927
|
-
getLastQmdRecallSnapshot: (namespace) => self.getLastQmdRecallSnapshot(namespace),
|
|
4928
|
-
getStorage: (namespace) => self.getStorage(namespace),
|
|
4929
|
-
get graphRecallCoordinator() { return self.graphRecallCoordinator; },
|
|
4930
|
-
get lastRecall() { return self.lastRecall; },
|
|
4931
|
-
resolveStateDirForNamespace: (namespace) => self.resolveStateDirForNamespace(namespace),
|
|
4932
|
-
get storageRouter() { return self.storageRouter; },
|
|
4933
|
-
});
|
|
3032
|
+
this._recallIntrospectionCoordinator = new RecallIntrospectionCoordinator(
|
|
3033
|
+
selfDeps<ConstructorParameters<typeof RecallIntrospectionCoordinator>[0]>(this),
|
|
3034
|
+
);
|
|
4934
3035
|
}
|
|
4935
3036
|
return this._recallIntrospectionCoordinator;
|
|
4936
3037
|
}
|
|
@@ -4945,57 +3046,9 @@ export class Orchestrator {
|
|
|
4945
3046
|
|
|
4946
3047
|
private get orchestratorInitCoordinator(): OrchestratorInitCoordinator {
|
|
4947
3048
|
if (!this._orchestratorInitCoordinator) {
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
get buffer() { return self.buffer; },
|
|
4952
|
-
get compounding() { return self.compounding; },
|
|
4953
|
-
get config() { return self.config; },
|
|
4954
|
-
configuredNamespaceList: () => self.configuredNamespaceList(),
|
|
4955
|
-
get contentHashIndex() { return self.contentHashIndex; },
|
|
4956
|
-
set contentHashIndex(value) { self.contentHashIndex = value; },
|
|
4957
|
-
get conversationIndexBackend() { return self.conversationIndexBackend; },
|
|
4958
|
-
get deferredInitAbort() { return self.deferredInitAbort; },
|
|
4959
|
-
set deferredInitAbort(value) { self.deferredInitAbort = value; },
|
|
4960
|
-
deferredInitialize: (signal) => self.deferredInitialize(signal),
|
|
4961
|
-
get deferredReady() { return self.deferredReady; },
|
|
4962
|
-
set deferredReady(value) { self.deferredReady = value; },
|
|
4963
|
-
get deferredSyncSucceeded() { return self.deferredSyncSucceeded; },
|
|
4964
|
-
set deferredSyncSucceeded(value) { self.deferredSyncSucceeded = value; },
|
|
4965
|
-
disposeSearchBackendIfNeeded: () => self.disposeSearchBackendIfNeeded(),
|
|
4966
|
-
get embeddingFallback() { return self.embeddingFallback; },
|
|
4967
|
-
getWearablesService: () => self.getWearablesService(),
|
|
4968
|
-
get handleHistory() { return self.handleHistory; },
|
|
4969
|
-
get lastRecall() { return self.lastRecall; },
|
|
4970
|
-
maintenanceNamespaces: (jobName, budgetMode) => self.maintenanceNamespaces(jobName, budgetMode),
|
|
4971
|
-
get maintenanceScheduler() { return self.maintenanceScheduler; },
|
|
4972
|
-
get namespaceCatalog() { return self.namespaceCatalog; },
|
|
4973
|
-
get namespaceSearchRouter() { return self.namespaceSearchRouter; },
|
|
4974
|
-
get negatives() { return self.negatives; },
|
|
4975
|
-
passiveCorrectionService: () => self.passiveCorrectionService(),
|
|
4976
|
-
get policyRuntime() { return self.policyRuntime; },
|
|
4977
|
-
get qmd() { return self.qmd; },
|
|
4978
|
-
set qmd(value) { self.qmd = value; },
|
|
4979
|
-
get relevance() { return self.relevance; },
|
|
4980
|
-
get resolveDeferredReady() { return self.resolveDeferredReady; },
|
|
4981
|
-
set resolveDeferredReady(value) { self.resolveDeferredReady = value; },
|
|
4982
|
-
get resolveInit() { return self.resolveInit; },
|
|
4983
|
-
set resolveInit(value) { self.resolveInit = value; },
|
|
4984
|
-
get runtimePolicyValues() { return self.runtimePolicyValues; },
|
|
4985
|
-
set runtimePolicyValues(value) { self.runtimePolicyValues = value; },
|
|
4986
|
-
get sessionObserver() { return self.sessionObserver; },
|
|
4987
|
-
get sharedContext() { return self.sharedContext; },
|
|
4988
|
-
get storage() { return self.storage; },
|
|
4989
|
-
get storageRouter() { return self.storageRouter; },
|
|
4990
|
-
get summarizer() { return self.summarizer; },
|
|
4991
|
-
get tierMigrationStatus() { return self.tierMigrationStatus; },
|
|
4992
|
-
get transcript() { return self.transcript; },
|
|
4993
|
-
get utilityRuntimeValues() { return self.utilityRuntimeValues; },
|
|
4994
|
-
set utilityRuntimeValues(value) { self.utilityRuntimeValues = value; },
|
|
4995
|
-
validateLocalLlmModel: () => self.validateLocalLlmModel(),
|
|
4996
|
-
get wearablesAutoSyncHandle() { return self.wearablesAutoSyncHandle; },
|
|
4997
|
-
set wearablesAutoSyncHandle(value) { self.wearablesAutoSyncHandle = value; },
|
|
4998
|
-
});
|
|
3049
|
+
this._orchestratorInitCoordinator = new OrchestratorInitCoordinator(
|
|
3050
|
+
selfDeps<ConstructorParameters<typeof OrchestratorInitCoordinator>[0]>(this),
|
|
3051
|
+
);
|
|
4999
3052
|
}
|
|
5000
3053
|
return this._orchestratorInitCoordinator;
|
|
5001
3054
|
}
|
|
@@ -5009,22 +3062,45 @@ export class Orchestrator {
|
|
|
5009
3062
|
|
|
5010
3063
|
private get persistenceIndexCoordinator(): PersistenceIndexCoordinator {
|
|
5011
3064
|
if (!this._persistenceIndexCoordinator) {
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
get config() { return self.config; },
|
|
5016
|
-
get contentHashIndex() { return self.contentHashIndex; },
|
|
5017
|
-
contentHashIndexForStorage: (targetStorage) => self.contentHashIndexForStorage(targetStorage),
|
|
5018
|
-
get contentHashIndexesByStorageDir() { return self.contentHashIndexesByStorageDir; },
|
|
5019
|
-
get embeddingFallback() { return self.embeddingFallback; },
|
|
5020
|
-
graphIndexFor: (storage) => self.graphIndexFor(storage),
|
|
5021
|
-
readAllMemoriesForNamespaces: (namespaces) => self.readAllMemoriesForNamespaces(namespaces),
|
|
5022
|
-
semanticDedupScopeFor: (targetStorage) => self.semanticDedupScopeFor(targetStorage),
|
|
5023
|
-
});
|
|
3065
|
+
this._persistenceIndexCoordinator = new PersistenceIndexCoordinator(
|
|
3066
|
+
selfDeps<ConstructorParameters<typeof PersistenceIndexCoordinator>[0]>(this),
|
|
3067
|
+
);
|
|
5024
3068
|
}
|
|
5025
3069
|
return this._persistenceIndexCoordinator;
|
|
5026
3070
|
}
|
|
5027
3071
|
|
|
3072
|
+
/**
|
|
3073
|
+
* Workspace-ops coordinator (issue #1526 seam 24). Owns periodic
|
|
3074
|
+
* workspace/operations surfaces. Lazy + accessor-wired (late-binding
|
|
3075
|
+
* rule, seams 18–23).
|
|
3076
|
+
*/
|
|
3077
|
+
private _workspaceOpsCoordinator: WorkspaceOpsCoordinator | undefined;
|
|
3078
|
+
|
|
3079
|
+
private get workspaceOpsCoordinator(): WorkspaceOpsCoordinator {
|
|
3080
|
+
if (!this._workspaceOpsCoordinator) {
|
|
3081
|
+
this._workspaceOpsCoordinator = new WorkspaceOpsCoordinator(
|
|
3082
|
+
selfDeps<ConstructorParameters<typeof WorkspaceOpsCoordinator>[0]>(this),
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
return this._workspaceOpsCoordinator;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
/**
|
|
3089
|
+
* Namespace read-fanout coordinator (issue #1526 seam 26). Owns
|
|
3090
|
+
* namespace-scoped read fanout (hints, cross-namespace search/reads,
|
|
3091
|
+
* artifact statuses). Lazy + accessor-wired (late-binding rule).
|
|
3092
|
+
*/
|
|
3093
|
+
private _namespaceReadFanoutCoordinator: NamespaceReadFanoutCoordinator | undefined;
|
|
3094
|
+
|
|
3095
|
+
private get namespaceReadFanoutCoordinator(): NamespaceReadFanoutCoordinator {
|
|
3096
|
+
if (!this._namespaceReadFanoutCoordinator) {
|
|
3097
|
+
this._namespaceReadFanoutCoordinator = new NamespaceReadFanoutCoordinator(
|
|
3098
|
+
selfDeps<ConstructorParameters<typeof NamespaceReadFanoutCoordinator>[0]>(this),
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
return this._namespaceReadFanoutCoordinator;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
5028
3104
|
private async recallInternal(
|
|
5029
3105
|
prompt: string,
|
|
5030
3106
|
sessionKey?: string,
|
|
@@ -5159,60 +3235,9 @@ export class Orchestrator {
|
|
|
5159
3235
|
return this.config.defaultNamespace;
|
|
5160
3236
|
}
|
|
5161
3237
|
|
|
5162
|
-
/**
|
|
5163
|
-
* Lazily-constructed wearables service (Limitless / Bee / Omi
|
|
5164
|
-
* transcript ingestion). All wearables surfaces — CLI, MCP tools,
|
|
5165
|
-
* HTTP routes — share this one instance so sync state, search, and
|
|
5166
|
-
* memory writes stay consistent. Writes are pinned to the same
|
|
5167
|
-
* deterministic namespace bulk-import uses.
|
|
5168
|
-
*/
|
|
5169
3238
|
getWearablesService(): WearablesService {
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
config: this.config.wearables,
|
|
5173
|
-
getStorage: async () =>
|
|
5174
|
-
await this.getStorageForNamespace(this.bulkImportWriteNamespace()),
|
|
5175
|
-
extract: (turns) => this.extraction.extract(turns),
|
|
5176
|
-
// Smart memoryMode runs candidates through the SAME extraction
|
|
5177
|
-
// judge (cache + defer counters included) the live extraction
|
|
5178
|
-
// pipeline uses, so wearable facts get identical LLM-as-judge
|
|
5179
|
-
// durability gating.
|
|
5180
|
-
judgeFacts: (candidates) =>
|
|
5181
|
-
judgeFactDurability(
|
|
5182
|
-
candidates,
|
|
5183
|
-
this.config,
|
|
5184
|
-
this.localLlm,
|
|
5185
|
-
new FallbackLlmClient(
|
|
5186
|
-
this.config.gatewayConfig,
|
|
5187
|
-
fallbackLlmRuntimeContextFromConfig(this.config),
|
|
5188
|
-
),
|
|
5189
|
-
this.judgeVerdictCache,
|
|
5190
|
-
this.judgeDeferCounts,
|
|
5191
|
-
),
|
|
5192
|
-
searchBackend: {
|
|
5193
|
-
search: async (query, maxResults) => {
|
|
5194
|
-
if (!this.qmd.isAvailable()) return null;
|
|
5195
|
-
try {
|
|
5196
|
-
const results = await this.qmd.search(query, undefined, maxResults);
|
|
5197
|
-
return results.map((result) => ({
|
|
5198
|
-
path: result.path,
|
|
5199
|
-
score: result.score,
|
|
5200
|
-
preview: result.snippet,
|
|
5201
|
-
}));
|
|
5202
|
-
} catch {
|
|
5203
|
-
// Backend hiccup → tell the service "unavailable" so it
|
|
5204
|
-
// runs its bounded scan fallback instead of returning a
|
|
5205
|
-
// silent empty result (CLAUDE.md rule 34).
|
|
5206
|
-
return null;
|
|
5207
|
-
}
|
|
5208
|
-
},
|
|
5209
|
-
},
|
|
5210
|
-
reindexSearch: async () => {
|
|
5211
|
-
await this.qmd.update();
|
|
5212
|
-
},
|
|
5213
|
-
});
|
|
5214
|
-
}
|
|
5215
|
-
return this.wearablesServiceInstance;
|
|
3239
|
+
return this.workspaceOpsCoordinator.getWearablesService(
|
|
3240
|
+
);
|
|
5216
3241
|
}
|
|
5217
3242
|
|
|
5218
3243
|
async ingestBulkImportBatch(
|
|
@@ -5571,93 +3596,16 @@ export class Orchestrator {
|
|
|
5571
3596
|
): Promise<number> {
|
|
5572
3597
|
return this.lifecyclePolicyCoordinator.runLifecyclePolicyPass(allMemories, storage);
|
|
5573
3598
|
}
|
|
5574
|
-
/**
|
|
5575
|
-
|
|
3599
|
+
/**
|
|
3600
|
+
* Threshold (bytes) at which IDENTITY.md reflections get auto-consolidated.
|
|
3601
|
+
* Read by WorkspaceOpsCoordinator (seam 24) via `Orchestrator.…`, hence
|
|
3602
|
+
* not `private`.
|
|
3603
|
+
*/
|
|
3604
|
+
static readonly IDENTITY_CONSOLIDATE_THRESHOLD = 8_000;
|
|
5576
3605
|
|
|
5577
3606
|
private async autoConsolidateIdentity(): Promise<void> {
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
// auto-consolidation, otherwise its identity file grows unbounded and is never
|
|
5581
|
-
// consolidated. Falls back to the configured set on any catalog read failure.
|
|
5582
|
-
const namespaces = resolveNamespaceCapabilities(this.config).namespaces
|
|
5583
|
-
? await this.maintenanceNamespaces()
|
|
5584
|
-
: [this.config.defaultNamespace];
|
|
5585
|
-
|
|
5586
|
-
for (const namespace of namespaces) {
|
|
5587
|
-
const storage = await this.storageRouter.storageFor(namespace);
|
|
5588
|
-
const identityNamespace =
|
|
5589
|
-
resolveNamespaceCapabilities(this.config).namespaces &&
|
|
5590
|
-
namespace !== this.config.defaultNamespace
|
|
5591
|
-
? namespace
|
|
5592
|
-
: undefined;
|
|
5593
|
-
const reflectionsContent =
|
|
5594
|
-
(await storage.readIdentityReflections()) ?? "";
|
|
5595
|
-
|
|
5596
|
-
const existingIdentity = await storage.readIdentity(
|
|
5597
|
-
this.config.workspaceDir,
|
|
5598
|
-
identityNamespace,
|
|
5599
|
-
);
|
|
5600
|
-
const headerEnd =
|
|
5601
|
-
existingIdentity.indexOf("## Learned Patterns") !== -1
|
|
5602
|
-
? existingIdentity.indexOf("## Learned Patterns")
|
|
5603
|
-
: existingIdentity.indexOf("## Reflection");
|
|
5604
|
-
const staticHeader =
|
|
5605
|
-
(headerEnd !== -1
|
|
5606
|
-
? existingIdentity.slice(0, headerEnd)
|
|
5607
|
-
: existingIdentity
|
|
5608
|
-
).trimEnd() || "# IDENTITY";
|
|
5609
|
-
const identityContent = `${staticHeader}\n\n${reflectionsContent.trim()}\n`;
|
|
5610
|
-
if (identityContent.length < Orchestrator.IDENTITY_CONSOLIDATE_THRESHOLD)
|
|
5611
|
-
continue;
|
|
5612
|
-
|
|
5613
|
-
log.info(
|
|
5614
|
-
`IDENTITY(${namespace}) is ${identityContent.length} chars — auto-consolidating reflections`,
|
|
5615
|
-
);
|
|
5616
|
-
const result = await this.extraction.consolidateIdentity(
|
|
5617
|
-
identityContent,
|
|
5618
|
-
"## Reflection",
|
|
5619
|
-
);
|
|
5620
|
-
|
|
5621
|
-
if (!result || result.learnedPatterns.length === 0) {
|
|
5622
|
-
log.warn(
|
|
5623
|
-
`identity consolidation produced no patterns for namespace=${namespace}`,
|
|
5624
|
-
);
|
|
5625
|
-
continue;
|
|
5626
|
-
}
|
|
5627
|
-
|
|
5628
|
-
const patternsSection = [
|
|
5629
|
-
"## Learned Patterns (consolidated from reflections, " +
|
|
5630
|
-
new Date().toISOString().slice(0, 10) +
|
|
5631
|
-
")",
|
|
5632
|
-
"",
|
|
5633
|
-
...result.learnedPatterns.map((p) => `- ${p}`),
|
|
5634
|
-
"",
|
|
5635
|
-
].join("\n");
|
|
5636
|
-
|
|
5637
|
-
const newContent = staticHeader + "\n\n" + patternsSection + "\n";
|
|
5638
|
-
|
|
5639
|
-
await storage.writeIdentity(
|
|
5640
|
-
this.config.workspaceDir,
|
|
5641
|
-
newContent,
|
|
5642
|
-
identityNamespace,
|
|
5643
|
-
);
|
|
5644
|
-
await storage.writeIdentityReflections("");
|
|
5645
|
-
// NRcCL (codex P2): record a per-namespace catalog write for THIS namespace
|
|
5646
|
-
// after the identity files are updated. This fan-out can mutate a dynamic
|
|
5647
|
-
// namespace via `writeIdentity`/`writeIdentityReflections`, but the
|
|
5648
|
-
// consolidation pass's only consolidated touch covers `this.storage` (the
|
|
5649
|
-
// default) and only fires when `memoryItemMutated` was set by OTHER work — so
|
|
5650
|
-
// a namespace whose sole mutation in the pass is identity consolidation would
|
|
5651
|
-
// otherwise keep a stale `lastWriteAt`, making `listNamespaces({ writtenSince })`
|
|
5652
|
-
// and catalog-recency consumers miss the write. Best-effort and
|
|
5653
|
-
// failure-tolerant (the storage chokepoint (#1522) swallows errors, never crashing the
|
|
5654
|
-
// consolidation; gotcha #13, rule #40). No double-count with the consolidated
|
|
5655
|
-
// touch above: that one is gated on `memoryItemMutated` (which identity
|
|
5656
|
-
// consolidation does not set), and `markWrite` is idempotent regardless.
|
|
5657
|
-
log.info(
|
|
5658
|
-
`IDENTITY(${namespace}) consolidated: ${identityContent.length} → ${newContent.length} chars, ${result.learnedPatterns.length} patterns`,
|
|
5659
|
-
);
|
|
5660
|
-
}
|
|
3607
|
+
return this.workspaceOpsCoordinator.autoConsolidateIdentity(
|
|
3608
|
+
);
|
|
5661
3609
|
}
|
|
5662
3610
|
|
|
5663
3611
|
// Issue #1526: recall result formatting moved to RecallResultFormatter. Thin
|
|
@@ -6108,52 +4056,9 @@ export class Orchestrator {
|
|
|
6108
4056
|
}
|
|
6109
4057
|
}
|
|
6110
4058
|
|
|
6111
|
-
/**
|
|
6112
|
-
* Flush access tracking buffer to disk.
|
|
6113
|
-
* Called during consolidation or when buffer is full.
|
|
6114
|
-
*/
|
|
6115
4059
|
async flushAccessTracking(): Promise<void> {
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
// Build entries from buffer, merging with existing counts
|
|
6119
|
-
const entries: AccessTrackingEntry[] = [];
|
|
6120
|
-
const namespaces = resolveNamespaceCapabilities(this.config).namespaces
|
|
6121
|
-
? Array.from(
|
|
6122
|
-
new Set<string>([
|
|
6123
|
-
this.config.defaultNamespace,
|
|
6124
|
-
this.config.sharedNamespace,
|
|
6125
|
-
...this.config.namespacePolicies.map((p) => p.name),
|
|
6126
|
-
]),
|
|
6127
|
-
)
|
|
6128
|
-
: [this.config.defaultNamespace];
|
|
6129
|
-
const memories = await this.readAllMemoriesForNamespaces(namespaces);
|
|
6130
|
-
const memoryMap = new Map(memories.map((m) => [m.frontmatter.id, m]));
|
|
6131
|
-
|
|
6132
|
-
for (const [memoryId, update] of this.accessTrackingBuffer) {
|
|
6133
|
-
const memory = memoryMap.get(memoryId);
|
|
6134
|
-
const existingCount = memory?.frontmatter.accessCount ?? 0;
|
|
6135
|
-
entries.push({
|
|
6136
|
-
memoryId,
|
|
6137
|
-
newCount: existingCount + update.count,
|
|
6138
|
-
lastAccessed: update.lastAccessed,
|
|
6139
|
-
});
|
|
6140
|
-
}
|
|
6141
|
-
|
|
6142
|
-
const byNamespace = new Map<string, AccessTrackingEntry[]>();
|
|
6143
|
-
for (const e of entries) {
|
|
6144
|
-
const m = memoryMap.get(e.memoryId);
|
|
6145
|
-
if (!m) continue;
|
|
6146
|
-
const ns = this.namespaceFromPath(m.path);
|
|
6147
|
-
const list = byNamespace.get(ns) ?? [];
|
|
6148
|
-
list.push(e);
|
|
6149
|
-
byNamespace.set(ns, list);
|
|
6150
|
-
}
|
|
6151
|
-
for (const [ns, list] of byNamespace) {
|
|
6152
|
-
const sm = await this.storageRouter.storageFor(ns);
|
|
6153
|
-
await sm.flushAccessTracking(list);
|
|
6154
|
-
}
|
|
6155
|
-
this.accessTrackingBuffer.clear();
|
|
6156
|
-
log.debug(`flushed ${entries.length} access tracking entries`);
|
|
4060
|
+
return this.workspaceOpsCoordinator.flushAccessTracking(
|
|
4061
|
+
);
|
|
6157
4062
|
}
|
|
6158
4063
|
|
|
6159
4064
|
private async loadSearchResultMemoryMap(
|
|
@@ -6402,29 +4307,15 @@ export class Orchestrator {
|
|
|
6402
4307
|
}
|
|
6403
4308
|
|
|
6404
4309
|
private namespaceFromPath(p: string): string {
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
? this.qmdCollectionNamespaceFromPrefix(parts.collection)
|
|
6409
|
-
: null;
|
|
6410
|
-
if (collectionNamespace) return collectionNamespace;
|
|
6411
|
-
const m = p.match(/[\\/]+namespaces[\\/]+([^\\/]+)(?:[\\/]|$)/);
|
|
6412
|
-
if (!m?.[1]) return this.config.defaultNamespace;
|
|
6413
|
-
return namespaceIdentityFromToken(m[1]) ?? m[1];
|
|
4310
|
+
return this.namespaceReadFanoutCoordinator.namespaceFromPath(
|
|
4311
|
+
p,
|
|
4312
|
+
);
|
|
6414
4313
|
}
|
|
6415
4314
|
|
|
6416
4315
|
private storageDirNamespace(storageDir: string): string {
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
// loaded lazily via the callback (only after early returns, matching the
|
|
6421
|
-
// original behavior — codex P2).
|
|
6422
|
-
return resolveNamespaceFromStorageDir(storageDir, {
|
|
6423
|
-
config: this.config,
|
|
6424
|
-
configuredNamespaces: this.configuredNamespaceList(),
|
|
6425
|
-
hints: this.namespaceStorageDirHints,
|
|
6426
|
-
loadHints: () => this.loadNamespaceStorageDirHintsFromCatalog(),
|
|
6427
|
-
});
|
|
4316
|
+
return this.namespaceReadFanoutCoordinator.storageDirNamespace(
|
|
4317
|
+
storageDir,
|
|
4318
|
+
);
|
|
6428
4319
|
}
|
|
6429
4320
|
|
|
6430
4321
|
// #1522: catalog touch methods removed — touches now happen at the storage chokepoint.
|
|
@@ -6450,26 +4341,16 @@ export class Orchestrator {
|
|
|
6450
4341
|
private async readAllMemoriesForNamespaces(
|
|
6451
4342
|
namespaces: string[],
|
|
6452
4343
|
): Promise<MemoryFile[]> {
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
uniq.map(async (ns) => {
|
|
6456
|
-
const sm = await this.storageRouter.storageFor(ns);
|
|
6457
|
-
return sm.readAllMemories();
|
|
6458
|
-
}),
|
|
4344
|
+
return this.namespaceReadFanoutCoordinator.readAllMemoriesForNamespaces(
|
|
4345
|
+
namespaces,
|
|
6459
4346
|
);
|
|
6460
|
-
return lists.flat();
|
|
6461
4347
|
}
|
|
6462
4348
|
|
|
6463
4349
|
private async readArchivedMemoriesForNamespaces(
|
|
6464
4350
|
namespaces: string[],
|
|
6465
4351
|
): Promise<MemoryFile[]> {
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
uniq.map(async (ns) => {
|
|
6469
|
-
const sm = await this.storageRouter.storageFor(ns);
|
|
6470
|
-
return sm.readArchivedMemories();
|
|
6471
|
-
}),
|
|
4352
|
+
return this.namespaceReadFanoutCoordinator.readArchivedMemoriesForNamespaces(
|
|
4353
|
+
namespaces,
|
|
6472
4354
|
);
|
|
6473
|
-
return lists.flat();
|
|
6474
4355
|
}
|
|
6475
4356
|
}
|