@remnic/core 9.3.749 → 9.3.751
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-boundary.d.ts +8 -8
- package/dist/access-boundary.js +15 -15
- package/dist/access-cli.js +39 -39
- package/dist/access-http.d.ts +8 -8
- package/dist/access-http.js +20 -20
- package/dist/access-mcp.d.ts +8 -8
- package/dist/access-mcp.js +19 -19
- package/dist/access-operations-batch.js +16 -16
- package/dist/access-operations.d.ts +8 -8
- package/dist/access-operations.js +18 -18
- package/dist/access-schema.d.ts +2 -2
- package/dist/access-schema.js +5 -5
- package/dist/{access-service-5-EVpt3v.d.ts → access-service-DmAEOJdJ.d.ts} +11 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +14 -14
- package/dist/access-surface-catalog.d.ts +8 -8
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +8 -1
- package/dist/active-recall.js.map +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +7 -7
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +7 -7
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-D_ZDxu8Y.d.ts → catalog-BpOcwHdE.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +8 -8
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-C5WN74HH.js → chunk-2HAG7JOI.js} +8 -2
- package/dist/chunk-2HAG7JOI.js.map +1 -0
- package/dist/{chunk-GKRLPHPU.js → chunk-2KK5WNQB.js} +2 -2
- package/dist/{chunk-JRDPT6DA.js → chunk-4NWIGAIC.js} +2 -2
- package/dist/{chunk-OBJUKYDO.js → chunk-4XL3CVII.js} +2 -2
- package/dist/{chunk-WWVXXSLF.js → chunk-5PZCDUJ6.js} +4 -4
- package/dist/{chunk-URQLO7BX.js → chunk-6FNZ3NMS.js} +2 -2
- package/dist/{chunk-DRUDC2UX.js → chunk-6MMONPHM.js} +2 -2
- package/dist/{chunk-RB6A7OUN.js → chunk-6SXNQFHB.js} +2 -2
- package/dist/{chunk-ZGZNLRKF.js → chunk-7TWA7DKP.js} +5 -5
- package/dist/{chunk-KMNYXLPL.js → chunk-B35L45HZ.js} +2 -2
- package/dist/{chunk-SIV3JZPP.js → chunk-CGZWGVQF.js} +5 -5
- package/dist/{chunk-CKFLLAIS.js → chunk-DANV2HTH.js} +3 -3
- package/dist/{chunk-NVYF5OB6.js → chunk-DBRVU5PR.js} +3 -3
- package/dist/{chunk-LMFS64GY.js → chunk-E4PFDD3N.js} +4 -4
- package/dist/{chunk-DIHCFJVV.js → chunk-HYI2GP3F.js} +4 -4
- package/dist/{chunk-UIKZZ242.js → chunk-IKHKHADN.js} +2 -2
- package/dist/{chunk-PUR2CKX2.js → chunk-JS2HKKOK.js} +1 -1
- package/dist/{chunk-PUR2CKX2.js.map → chunk-JS2HKKOK.js.map} +1 -1
- package/dist/{chunk-R2OAN6IP.js → chunk-K2JM4DZZ.js} +2 -2
- package/dist/{chunk-3IJGF6K2.js → chunk-K2R3DEEH.js} +2 -2
- package/dist/{chunk-HIHCB2S6.js → chunk-QRRBXD24.js} +2 -2
- package/dist/{chunk-PNUOQYRJ.js → chunk-RWZB6MAY.js} +1953 -2172
- package/dist/chunk-RWZB6MAY.js.map +1 -0
- package/dist/{chunk-ZPZ4VVLT.js → chunk-SD34EK4Z.js} +2 -2
- package/dist/{chunk-WK3NK6SP.js → chunk-SNEXVX3U.js} +6 -6
- package/dist/{chunk-FVZ2ISIX.js → chunk-TPLE6SNY.js} +7 -7
- package/dist/{chunk-24YDB5QP.js → chunk-UETBEHBA.js} +44 -24
- package/dist/chunk-UETBEHBA.js.map +1 -0
- package/dist/{chunk-5IN26V6W.js → chunk-UU32EAIY.js} +5 -5
- package/dist/{chunk-ZGAJB74V.js → chunk-W3BFT5OJ.js} +2 -2
- package/dist/{chunk-L4N7SVNN.js → chunk-WA7FECSG.js} +11 -11
- package/dist/{chunk-Q3X7GUAQ.js → chunk-WOX2UW4K.js} +2 -2
- package/dist/{chunk-XTEWVF3C.js → chunk-WWSXVOGY.js} +2 -2
- package/dist/{chunk-PWWWLD7D.js → chunk-X64YLC24.js} +153 -27
- package/dist/chunk-X64YLC24.js.map +1 -0
- package/dist/{chunk-P4FW3ZU5.js → chunk-YGGXUNS4.js} +20 -20
- package/dist/{chunk-JHQUIH3W.js → chunk-ZBVQ4ZM4.js} +19 -19
- package/dist/{cli-DuWWjaKT.d.ts → cli-lmf5RGyQ.d.ts} +3 -3
- package/dist/cli.d.ts +9 -9
- package/dist/cli.js +33 -33
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +7 -7
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +8 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -7
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +7 -7
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +7 -7
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +76 -72
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +8 -8
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +1 -1
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +7 -7
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync.d.ts +108 -1
- package/dist/offline-sync.js +7 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +15 -12
- package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/{orchestrator-CZI6hO_R.d.ts → orchestrator-D7bb8ZGp.d.ts} +193 -201
- package/dist/orchestrator.d.ts +5 -5
- package/dist/orchestrator.js +51 -41
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +9 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/lancedb-backend.js +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.js +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/orama-backend.js +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-D3aSiu9t.d.ts → semantic-consolidation-CTfbVwCA.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +7 -7
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +6 -6
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/types.d.ts +12 -12
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-CZm6rx9-.d.ts → types-D4NYDtXI.d.ts} +7 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +7 -7
- package/package.json +2 -2
- package/src/access-service.ts +22 -0
- package/src/config.ts +5 -0
- package/src/index.ts +3 -1
- package/src/offline-sync.test.ts +354 -52
- package/src/offline-sync.ts +332 -24
- package/src/orchestration/extraction-run.ts +4 -34
- package/src/orchestration/namespace-read-fanout.ts +484 -0
- package/src/orchestration/orchestrator-helpers.ts +1130 -0
- package/src/orchestration/self-deps.ts +50 -0
- package/src/orchestration/workspace-ops.ts +789 -0
- package/src/orchestrator.ts +177 -2296
- package/src/types.ts +9 -0
- package/dist/chunk-24YDB5QP.js.map +0 -1
- package/dist/chunk-C5WN74HH.js.map +0 -1
- package/dist/chunk-PNUOQYRJ.js.map +0 -1
- package/dist/chunk-PWWWLD7D.js.map +0 -1
- /package/dist/{chunk-GKRLPHPU.js.map → chunk-2KK5WNQB.js.map} +0 -0
- /package/dist/{chunk-JRDPT6DA.js.map → chunk-4NWIGAIC.js.map} +0 -0
- /package/dist/{chunk-OBJUKYDO.js.map → chunk-4XL3CVII.js.map} +0 -0
- /package/dist/{chunk-WWVXXSLF.js.map → chunk-5PZCDUJ6.js.map} +0 -0
- /package/dist/{chunk-URQLO7BX.js.map → chunk-6FNZ3NMS.js.map} +0 -0
- /package/dist/{chunk-DRUDC2UX.js.map → chunk-6MMONPHM.js.map} +0 -0
- /package/dist/{chunk-RB6A7OUN.js.map → chunk-6SXNQFHB.js.map} +0 -0
- /package/dist/{chunk-ZGZNLRKF.js.map → chunk-7TWA7DKP.js.map} +0 -0
- /package/dist/{chunk-KMNYXLPL.js.map → chunk-B35L45HZ.js.map} +0 -0
- /package/dist/{chunk-SIV3JZPP.js.map → chunk-CGZWGVQF.js.map} +0 -0
- /package/dist/{chunk-CKFLLAIS.js.map → chunk-DANV2HTH.js.map} +0 -0
- /package/dist/{chunk-NVYF5OB6.js.map → chunk-DBRVU5PR.js.map} +0 -0
- /package/dist/{chunk-LMFS64GY.js.map → chunk-E4PFDD3N.js.map} +0 -0
- /package/dist/{chunk-DIHCFJVV.js.map → chunk-HYI2GP3F.js.map} +0 -0
- /package/dist/{chunk-UIKZZ242.js.map → chunk-IKHKHADN.js.map} +0 -0
- /package/dist/{chunk-R2OAN6IP.js.map → chunk-K2JM4DZZ.js.map} +0 -0
- /package/dist/{chunk-3IJGF6K2.js.map → chunk-K2R3DEEH.js.map} +0 -0
- /package/dist/{chunk-HIHCB2S6.js.map → chunk-QRRBXD24.js.map} +0 -0
- /package/dist/{chunk-ZPZ4VVLT.js.map → chunk-SD34EK4Z.js.map} +0 -0
- /package/dist/{chunk-WK3NK6SP.js.map → chunk-SNEXVX3U.js.map} +0 -0
- /package/dist/{chunk-FVZ2ISIX.js.map → chunk-TPLE6SNY.js.map} +0 -0
- /package/dist/{chunk-5IN26V6W.js.map → chunk-UU32EAIY.js.map} +0 -0
- /package/dist/{chunk-ZGAJB74V.js.map → chunk-W3BFT5OJ.js.map} +0 -0
- /package/dist/{chunk-L4N7SVNN.js.map → chunk-WA7FECSG.js.map} +0 -0
- /package/dist/{chunk-Q3X7GUAQ.js.map → chunk-WOX2UW4K.js.map} +0 -0
- /package/dist/{chunk-XTEWVF3C.js.map → chunk-WWSXVOGY.js.map} +0 -0
- /package/dist/{chunk-P4FW3ZU5.js.map → chunk-YGGXUNS4.js.map} +0 -0
- /package/dist/{chunk-JHQUIH3W.js.map → chunk-ZBVQ4ZM4.js.map} +0 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-ops coordinator — extracted from the orchestrator
|
|
3
|
+
* (issue #1526, seam 24).
|
|
4
|
+
*
|
|
5
|
+
* Owns the periodic workspace/operations surfaces:
|
|
6
|
+
* - day-summary fact gathering (gatherTodayFacts)
|
|
7
|
+
* - memory-action policy preview (previewMemoryActionEvent)
|
|
8
|
+
* - file hygiene runs and identity auto-consolidation
|
|
9
|
+
* - wearables service construction
|
|
10
|
+
* - local-LLM model validation
|
|
11
|
+
* - pattern-reinforcement runs and their namespace fanout
|
|
12
|
+
* - access-tracking flush
|
|
13
|
+
*
|
|
14
|
+
* Behavior-preserving move from orchestrator.ts (late-binding deps rule,
|
|
15
|
+
* seams 18–23).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { lstat, mkdir, readFile, readdir, realpath, writeFile } from "node:fs/promises";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { resolveCompressionCapabilities, resolveConsolidationCapabilities, resolveNamespaceCapabilities } from "../capabilities.js";
|
|
21
|
+
import { formatDaySummaryMemories } from "../day-summary.js";
|
|
22
|
+
import { type JudgeVerdict, judgeFactDurability } from "../extraction-judge.js";
|
|
23
|
+
import { ExtractionEngine } from "../extraction.js";
|
|
24
|
+
import { FallbackLlmClient, fallbackLlmRuntimeContextFromConfig } from "../fallback-llm.js";
|
|
25
|
+
import { lintWorkspaceFiles, rotateMarkdownFileToArchive } from "../hygiene.js";
|
|
26
|
+
import { StorageManager } from "../index.js";
|
|
27
|
+
import { LocalLlmClient } from "../local-llm.js";
|
|
28
|
+
import { log } from "../logger.js";
|
|
29
|
+
import type { NamespaceMaintenanceFanoutRunnerContext } from "../maintenance/namespace-maintenance-fanout.js";
|
|
30
|
+
import type { NamespaceMaintenanceSummary } from "../maintenance/namespace-planner.js";
|
|
31
|
+
import { type PatternReinforcementResult, runPatternReinforcement } from "../maintenance/pattern-reinforcement.js";
|
|
32
|
+
import { evaluateMemoryActionPolicy } from "../memory-action-policy.js";
|
|
33
|
+
import { NamespaceStorageRouter } from "../namespaces/storage.js";
|
|
34
|
+
import { parseMemoryActionEligibilityContext } from "../schemas.js";
|
|
35
|
+
import type { SearchBackend } from "../search/port.js";
|
|
36
|
+
import { type AccessTrackingEntry, type MemoryActionEvent, type MemoryFile, type PluginConfig, confidenceTier } from "../types.js";
|
|
37
|
+
import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
|
|
38
|
+
import { assertPathInsideRoot } from "../utils/path-containment.js";
|
|
39
|
+
import { WearablesService } from "../wearables/service.js";
|
|
40
|
+
import {
|
|
41
|
+
Orchestrator,
|
|
42
|
+
filterHourlySummaryMarkdownForLocalDay,
|
|
43
|
+
formatDateInTimeZone,
|
|
44
|
+
normalizeIanaTimeZone,
|
|
45
|
+
parseFiniteDate,
|
|
46
|
+
utcDateKeysForLocalDay,
|
|
47
|
+
type DaySummaryGatherOptions,
|
|
48
|
+
} from "../orchestrator.js";
|
|
49
|
+
|
|
50
|
+
export interface WorkspaceOpsDeps {
|
|
51
|
+
readonly accessTrackingBuffer: Map<string, { count: number; lastAccessed: string }>;
|
|
52
|
+
bulkImportWriteNamespace(): string;
|
|
53
|
+
readonly config: PluginConfig;
|
|
54
|
+
readonly extraction: ExtractionEngine;
|
|
55
|
+
getStorage(namespace?: string): Promise<StorageManager>;
|
|
56
|
+
getStorageForNamespace(namespace?: string): Promise<StorageManager>;
|
|
57
|
+
readonly judgeDeferCounts: Map<string, number>;
|
|
58
|
+
readonly judgeVerdictCache: Map<string, JudgeVerdict>;
|
|
59
|
+
lastFileHygieneRunAtMs: number;
|
|
60
|
+
readonly lastPatternReinforcementAtByNs: Map<string, number>;
|
|
61
|
+
readonly localLlm: LocalLlmClient;
|
|
62
|
+
maintenanceNamespaces(
|
|
63
|
+
jobName?: string,
|
|
64
|
+
budgetMode?: "cycle" | "unbounded",
|
|
65
|
+
): Promise<string[]>;
|
|
66
|
+
namespaceFromPath(p: string): string;
|
|
67
|
+
readonly qmd: SearchBackend;
|
|
68
|
+
readAllMemoriesForNamespaces(
|
|
69
|
+
namespaces: string[],
|
|
70
|
+
): Promise<MemoryFile[]>;
|
|
71
|
+
runNamespaceMaintenanceFanoutForJob(
|
|
72
|
+
jobName: string,
|
|
73
|
+
runner: (ctx: NamespaceMaintenanceFanoutRunnerContext) => Promise<{ itemCount?: number } | undefined>,
|
|
74
|
+
options?: { enabled?: boolean },
|
|
75
|
+
): Promise<NamespaceMaintenanceSummary>;
|
|
76
|
+
runPatternReinforcement(options?: {
|
|
77
|
+
force?: boolean;
|
|
78
|
+
namespace?: string;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
ran: boolean;
|
|
81
|
+
skippedReason?: "disabled" | "cadence";
|
|
82
|
+
namespace: string;
|
|
83
|
+
result?: PatternReinforcementResult;
|
|
84
|
+
}>;
|
|
85
|
+
readonly storage: StorageManager;
|
|
86
|
+
readonly storageRouter: NamespaceStorageRouter;
|
|
87
|
+
wearablesServiceInstance: WearablesService | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class WorkspaceOpsCoordinator {
|
|
91
|
+
constructor(
|
|
92
|
+
private readonly deps: WorkspaceOpsDeps,
|
|
93
|
+
) {}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Run the pattern-reinforcement maintenance job (issue #687 PR 2/4).
|
|
97
|
+
*
|
|
98
|
+
* Cadence-gated on `patternReinforcementCadenceMs` so every caller
|
|
99
|
+
* (orchestrator cron path, MCP tool, CLI) shares a single floor —
|
|
100
|
+
* none can call this on a hot loop and burn the corpus. When the
|
|
101
|
+
* feature is disabled or the cadence has not elapsed, returns a
|
|
102
|
+
* synthetic "skipped" result rather than throwing.
|
|
103
|
+
*
|
|
104
|
+
* Cadence tracking is per-namespace so a tenant-scoped MCP run in
|
|
105
|
+
* one namespace does not silence a cron run in another (PR #730
|
|
106
|
+
* review feedback, Codex P2). Pass `force: true` for ad-hoc
|
|
107
|
+
* operator runs that must bypass the cadence floor — mirrors the
|
|
108
|
+
* pattern used by other maintenance MCP tools.
|
|
109
|
+
*
|
|
110
|
+
* `force` deliberately does NOT bypass the master
|
|
111
|
+
* `patternReinforcementEnabled` flag (PR #730 review feedback,
|
|
112
|
+
* Cursor Medium). Operators who have explicitly disabled the
|
|
113
|
+
* feature must not have their corpus mutated by an MCP tool call —
|
|
114
|
+
* the only way to run the job is to enable the feature in config.
|
|
115
|
+
*/
|
|
116
|
+
async runPatternReinforcement(options: {
|
|
117
|
+
force?: boolean;
|
|
118
|
+
namespace?: string;
|
|
119
|
+
} = {}): Promise<{
|
|
120
|
+
ran: boolean;
|
|
121
|
+
skippedReason?: "disabled" | "cadence";
|
|
122
|
+
namespace: string;
|
|
123
|
+
result?: PatternReinforcementResult;
|
|
124
|
+
}> {
|
|
125
|
+
const cadenceKey = options.namespace ?? "";
|
|
126
|
+
// Master switch: a disabled feature is never bypassed, even with
|
|
127
|
+
// force=true. `force` only relaxes the cadence floor below.
|
|
128
|
+
if (!resolveConsolidationCapabilities(this.deps.config).patternReinforcement) {
|
|
129
|
+
return { ran: false, skippedReason: "disabled", namespace: cadenceKey };
|
|
130
|
+
}
|
|
131
|
+
const cadence = this.deps.config.patternReinforcementCadenceMs;
|
|
132
|
+
const lastAt = this.deps.lastPatternReinforcementAtByNs.get(cadenceKey);
|
|
133
|
+
if (
|
|
134
|
+
!options.force &&
|
|
135
|
+
cadence > 0 &&
|
|
136
|
+
lastAt !== undefined &&
|
|
137
|
+
Date.now() - lastAt < cadence
|
|
138
|
+
) {
|
|
139
|
+
return { ran: false, skippedReason: "cadence", namespace: cadenceKey };
|
|
140
|
+
}
|
|
141
|
+
const storage = options.namespace
|
|
142
|
+
? await this.deps.getStorage(options.namespace)
|
|
143
|
+
: this.deps.storage;
|
|
144
|
+
const result = await runPatternReinforcement(storage, {
|
|
145
|
+
categories: this.deps.config.patternReinforcementCategories,
|
|
146
|
+
minCount: this.deps.config.patternReinforcementMinCount,
|
|
147
|
+
});
|
|
148
|
+
this.deps.lastPatternReinforcementAtByNs.set(cadenceKey, Date.now());
|
|
149
|
+
log.debug(
|
|
150
|
+
`pattern reinforcement [ns=${cadenceKey || "(default)"}]: clusters=${result.clustersFound} canonicalsUpdated=${result.canonicalsUpdated} duplicatesSuperseded=${result.duplicatesSuperseded}`,
|
|
151
|
+
);
|
|
152
|
+
return { ran: true, result, namespace: cadenceKey };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fan out pattern reinforcement across all maintained namespaces (issue #1500).
|
|
157
|
+
* Delegates per-namespace execution to {@link runPatternReinforcement} while
|
|
158
|
+
* the planner handles discovery, budgeting, locking, and status recording.
|
|
159
|
+
* When namespaces are disabled, runs once against default storage.
|
|
160
|
+
*/
|
|
161
|
+
async runPatternReinforcementFanout(options: {
|
|
162
|
+
force?: boolean;
|
|
163
|
+
} = {}): Promise<NamespaceMaintenanceSummary> {
|
|
164
|
+
return this.deps.runNamespaceMaintenanceFanoutForJob(
|
|
165
|
+
"pattern-reinforcement",
|
|
166
|
+
async (ctx) => {
|
|
167
|
+
const result = await this.deps.runPatternReinforcement({
|
|
168
|
+
namespace: ctx.candidate.namespace,
|
|
169
|
+
force: options.force,
|
|
170
|
+
});
|
|
171
|
+
// runPatternReinforcement has its own per-namespace cadence gate
|
|
172
|
+
// (lastPatternReinforcementAtByNs). When it throttles (ran:false),
|
|
173
|
+
// signal skip so the planner records state:"skipped" and does NOT
|
|
174
|
+
// touch lastMaintenanceAt — otherwise a throttled namespace would
|
|
175
|
+
// look maintained while pattern reinforcement never ran (#1500
|
|
176
|
+
// review: cadence-skip accuracy).
|
|
177
|
+
if (!result.ran) {
|
|
178
|
+
return {
|
|
179
|
+
skipped: true,
|
|
180
|
+
skipReason: result.skippedReason ?? "throttled",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return result.result
|
|
184
|
+
? { itemCount: result.result.clustersFound }
|
|
185
|
+
: { itemCount: 0 };
|
|
186
|
+
},
|
|
187
|
+
{ enabled: resolveConsolidationCapabilities(this.deps.config).patternReinforcement },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async maybeRunFileHygiene(): Promise<void> {
|
|
192
|
+
const hygiene = this.deps.config.fileHygiene;
|
|
193
|
+
if (!hygiene?.enabled) return;
|
|
194
|
+
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
if (now - this.deps.lastFileHygieneRunAtMs < hygiene.runMinIntervalMs) return;
|
|
197
|
+
this.deps.lastFileHygieneRunAtMs = now;
|
|
198
|
+
|
|
199
|
+
// Rotation first (keeps bootstrap files small).
|
|
200
|
+
if (hygiene.rotateEnabled) {
|
|
201
|
+
for (const rel of hygiene.rotatePaths) {
|
|
202
|
+
const abs = path.isAbsolute(rel)
|
|
203
|
+
? rel
|
|
204
|
+
: path.join(this.deps.config.workspaceDir, rel);
|
|
205
|
+
try {
|
|
206
|
+
const raw = await readFile(abs, "utf-8");
|
|
207
|
+
if (raw.length > hygiene.rotateMaxBytes) {
|
|
208
|
+
const archiveDir = path.join(
|
|
209
|
+
this.deps.config.workspaceDir,
|
|
210
|
+
hygiene.archiveDir,
|
|
211
|
+
);
|
|
212
|
+
const base = path.basename(abs);
|
|
213
|
+
const prefix =
|
|
214
|
+
base
|
|
215
|
+
.toUpperCase()
|
|
216
|
+
.replace(/\.MD$/i, "")
|
|
217
|
+
.replace(/[^A-Z0-9]+/g, "-") || "FILE";
|
|
218
|
+
const { newContent } = await rotateMarkdownFileToArchive({
|
|
219
|
+
filePath: abs,
|
|
220
|
+
archiveDir,
|
|
221
|
+
archivePrefix: prefix,
|
|
222
|
+
keepTailChars: hygiene.rotateKeepTailChars,
|
|
223
|
+
});
|
|
224
|
+
await writeFile(abs, newContent, "utf-8");
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// ignore missing/unreadable targets
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Lint (warn before truncation risk).
|
|
233
|
+
if (hygiene.lintEnabled) {
|
|
234
|
+
const warnings = await lintWorkspaceFiles({
|
|
235
|
+
workspaceDir: this.deps.config.workspaceDir,
|
|
236
|
+
paths: hygiene.lintPaths,
|
|
237
|
+
budgetBytes: hygiene.lintBudgetBytes,
|
|
238
|
+
warnRatio: hygiene.lintWarnRatio,
|
|
239
|
+
});
|
|
240
|
+
for (const w of warnings) {
|
|
241
|
+
log.warn(w.message);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (hygiene.warningsLogEnabled && warnings.length > 0) {
|
|
245
|
+
const fp = path.join(this.deps.config.memoryDir, hygiene.warningsLogPath);
|
|
246
|
+
await mkdir(path.dirname(fp), { recursive: true });
|
|
247
|
+
const stamp = new Date().toISOString();
|
|
248
|
+
const block =
|
|
249
|
+
`\n\n## ${stamp}\n\n` +
|
|
250
|
+
warnings.map((w) => `- ${w.message}`).join("\n") +
|
|
251
|
+
"\n";
|
|
252
|
+
let existing = "";
|
|
253
|
+
try {
|
|
254
|
+
existing = await readFile(fp, "utf-8");
|
|
255
|
+
} catch {
|
|
256
|
+
existing = "# Engram File Hygiene Warnings\n";
|
|
257
|
+
}
|
|
258
|
+
await writeFile(fp, existing + block, "utf-8");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read today's facts and hourly summaries from storage, returning them
|
|
265
|
+
* as a formatted string suitable for generateDaySummary().
|
|
266
|
+
*/
|
|
267
|
+
async gatherTodayFacts(
|
|
268
|
+
namespace?: string,
|
|
269
|
+
options: DaySummaryGatherOptions = {},
|
|
270
|
+
): Promise<string> {
|
|
271
|
+
const ns =
|
|
272
|
+
namespace && namespace.length > 0
|
|
273
|
+
? namespace
|
|
274
|
+
: this.deps.config.defaultNamespace;
|
|
275
|
+
const storage = await this.deps.storageRouter.storageFor(ns);
|
|
276
|
+
const configuredTimeZone = normalizeIanaTimeZone(options.timeZone)
|
|
277
|
+
?? normalizeIanaTimeZone(this.deps.config.daySummaryTimezone);
|
|
278
|
+
const timeZone =
|
|
279
|
+
configuredTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
280
|
+
const now = options.now instanceof Date && Number.isFinite(options.now.getTime())
|
|
281
|
+
? options.now
|
|
282
|
+
: new Date();
|
|
283
|
+
const targetLocalDate = formatDateInTimeZone(now, timeZone);
|
|
284
|
+
// Facts are stored under UTC date directories, while the summary target is
|
|
285
|
+
// a local calendar day. Scan the UTC-date envelope that overlaps the local
|
|
286
|
+
// day, then filter parseable fact timestamps to that configured local day.
|
|
287
|
+
const datesToScan = utcDateKeysForLocalDay(now, timeZone);
|
|
288
|
+
const MAX_CHARS = 100_000;
|
|
289
|
+
|
|
290
|
+
// --- Read memory files from each category dir × date directory ---
|
|
291
|
+
// Iterate every recall category dir (RECALL_FALLBACK_DIRS — single source
|
|
292
|
+
// of truth) so the day summary includes decisions/, moments/, ... not just
|
|
293
|
+
// facts/ (#1546). corrections/ is flat, so corrections/<date>/ never exists
|
|
294
|
+
// and is skipped by the ENOENT guard — preserving the prior exclusion. The
|
|
295
|
+
// per-file created→local-day filter below is unchanged.
|
|
296
|
+
//
|
|
297
|
+
// Symlink/containment hardening (mirrors scanDir / the CLI walker): the
|
|
298
|
+
// gathered contents feed the day-summary LLM input, so a symlinked category
|
|
299
|
+
// dir (decisions/ → outside memoryDir) must not be followed and leak files.
|
|
300
|
+
// Resolve the store root once; skip symlinked / out-of-root dirs and
|
|
301
|
+
// entries; skip the scan gracefully if the root can't be resolved.
|
|
302
|
+
const facts: MemoryFile[] = [];
|
|
303
|
+
let memoryRootReal: string | null = null;
|
|
304
|
+
try {
|
|
305
|
+
memoryRootReal = await realpath(storage.dir);
|
|
306
|
+
} catch {
|
|
307
|
+
memoryRootReal = null;
|
|
308
|
+
}
|
|
309
|
+
for (const categoryDir of RECALL_FALLBACK_DIRS) {
|
|
310
|
+
if (memoryRootReal === null) break;
|
|
311
|
+
for (const date of datesToScan) {
|
|
312
|
+
const dateDir = path.join(storage.dir, categoryDir, date);
|
|
313
|
+
try {
|
|
314
|
+
const dirStat = await lstat(dateDir);
|
|
315
|
+
if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
|
|
316
|
+
assertPathInsideRoot(memoryRootReal, await realpath(dateDir), dateDir);
|
|
317
|
+
const entries = await readdir(dateDir, { withFileTypes: true });
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
if (entry.isSymbolicLink()) continue;
|
|
320
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
321
|
+
const fullPath = path.join(dateDir, entry.name);
|
|
322
|
+
try {
|
|
323
|
+
assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
|
|
324
|
+
const raw = await readFile(fullPath, "utf-8");
|
|
325
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
326
|
+
if (!fmMatch) continue;
|
|
327
|
+
const fmBlock = fmMatch[1];
|
|
328
|
+
const content = fmMatch[2].trim();
|
|
329
|
+
const fm: Record<string, string> = {};
|
|
330
|
+
for (const line of fmBlock.split("\n")) {
|
|
331
|
+
const colonIdx = line.indexOf(":");
|
|
332
|
+
if (colonIdx === -1) continue;
|
|
333
|
+
fm[line.slice(0, colonIdx).trim()] = line
|
|
334
|
+
.slice(colonIdx + 1)
|
|
335
|
+
.trim();
|
|
336
|
+
}
|
|
337
|
+
const created = fm.created || "unknown";
|
|
338
|
+
const createdAt = parseFiniteDate(created);
|
|
339
|
+
if (
|
|
340
|
+
createdAt &&
|
|
341
|
+
formatDateInTimeZone(createdAt, timeZone) !== targetLocalDate
|
|
342
|
+
) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
facts.push({
|
|
346
|
+
path: fullPath,
|
|
347
|
+
frontmatter: {
|
|
348
|
+
id: fm.id || path.basename(entry.name, ".md"),
|
|
349
|
+
category: (fm.category as any) || "fact",
|
|
350
|
+
created,
|
|
351
|
+
updated: fm.updated || created,
|
|
352
|
+
source: fm.source || "unknown",
|
|
353
|
+
confidence: parseFloat(fm.confidence || "0.8"),
|
|
354
|
+
confidenceTier: (fm.confidenceTier as any) || "implied",
|
|
355
|
+
tags: [],
|
|
356
|
+
},
|
|
357
|
+
content,
|
|
358
|
+
});
|
|
359
|
+
} catch {
|
|
360
|
+
// Skip unreadable files
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
// Absent dir (ENOENT), symlinked/out-of-root dir, or containment
|
|
365
|
+
// violation — skip this category/date without aborting the summary.
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Sort facts by created timestamp (most recent last) so truncation keeps newest
|
|
371
|
+
facts.sort((a, b) => {
|
|
372
|
+
if (a.frontmatter.created === b.frontmatter.created) return 0;
|
|
373
|
+
return a.frontmatter.created < b.frontmatter.created ? -1 : 1;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// --- Read hourly summaries for the scanned dates ---
|
|
377
|
+
const hourlySummaries: string[] = [];
|
|
378
|
+
const hourlyBaseDir = path.join(storage.dir, "summaries", "hourly");
|
|
379
|
+
try {
|
|
380
|
+
const sessionKeys = await readdir(hourlyBaseDir, { withFileTypes: true });
|
|
381
|
+
for (const sk of sessionKeys) {
|
|
382
|
+
if (!sk.isDirectory()) continue;
|
|
383
|
+
for (const date of datesToScan) {
|
|
384
|
+
const summaryFile = path.join(hourlyBaseDir, sk.name, `${date}.md`);
|
|
385
|
+
try {
|
|
386
|
+
const raw = await readFile(summaryFile, "utf-8");
|
|
387
|
+
const filtered = filterHourlySummaryMarkdownForLocalDay(
|
|
388
|
+
raw,
|
|
389
|
+
date,
|
|
390
|
+
timeZone,
|
|
391
|
+
targetLocalDate,
|
|
392
|
+
);
|
|
393
|
+
if (filtered) {
|
|
394
|
+
hourlySummaries.push(filtered);
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// No summary file for this session/date
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
// No hourly summaries directory
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Format and truncate ---
|
|
406
|
+
let formatted = formatDaySummaryMemories(facts);
|
|
407
|
+
if (hourlySummaries.length > 0) {
|
|
408
|
+
formatted +=
|
|
409
|
+
"\n\n---\n## Hourly Summaries\n\n" +
|
|
410
|
+
hourlySummaries.join("\n\n---\n\n");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Truncate intelligently if over budget: drop oldest facts first
|
|
414
|
+
if (formatted.length > MAX_CHARS) {
|
|
415
|
+
// Re-build with fewer facts, keeping most recent
|
|
416
|
+
while (facts.length > 1 && formatted.length > MAX_CHARS) {
|
|
417
|
+
facts.shift(); // drop oldest
|
|
418
|
+
formatted = formatDaySummaryMemories(facts);
|
|
419
|
+
if (hourlySummaries.length > 0) {
|
|
420
|
+
formatted +=
|
|
421
|
+
"\n\n---\n## Hourly Summaries\n\n" +
|
|
422
|
+
hourlySummaries.join("\n\n---\n\n");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// If still over, hard truncate
|
|
426
|
+
if (formatted.length > MAX_CHARS) {
|
|
427
|
+
formatted = formatted.slice(0, MAX_CHARS);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
log.info(
|
|
432
|
+
`gatherTodayFacts: collected ${facts.length} facts, ${hourlySummaries.length} hourly summaries for ${targetLocalDate} (${timeZone}, ${formatted.length} chars)`,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
return formatted;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
previewMemoryActionEvent(
|
|
439
|
+
event: Omit<MemoryActionEvent, "timestamp"> & { timestamp?: string },
|
|
440
|
+
): MemoryActionEvent {
|
|
441
|
+
const namespace =
|
|
442
|
+
typeof event.namespace === "string" && event.namespace.length > 0
|
|
443
|
+
? event.namespace
|
|
444
|
+
: this.deps.config.defaultNamespace;
|
|
445
|
+
const eligibility = parseMemoryActionEligibilityContext(
|
|
446
|
+
event.policyEligibility,
|
|
447
|
+
);
|
|
448
|
+
const policy = evaluateMemoryActionPolicy({
|
|
449
|
+
action: event.action,
|
|
450
|
+
eligibility,
|
|
451
|
+
options: {
|
|
452
|
+
actionsEnabled: resolveCompressionCapabilities(this.deps.config).contextCompressionActions,
|
|
453
|
+
maxCompressionTokensPerHour: this.deps.config.maxCompressionTokensPerHour,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
const dryRun = event.dryRun === true;
|
|
457
|
+
|
|
458
|
+
const normalizedOutcome = dryRun
|
|
459
|
+
? event.outcome === "failed"
|
|
460
|
+
? "failed"
|
|
461
|
+
: "skipped"
|
|
462
|
+
: policy.decision === "allow"
|
|
463
|
+
? event.outcome
|
|
464
|
+
: event.outcome === "failed"
|
|
465
|
+
? "failed"
|
|
466
|
+
: "skipped";
|
|
467
|
+
const sourceSessionKey =
|
|
468
|
+
typeof event.sourceSessionKey === "string" &&
|
|
469
|
+
event.sourceSessionKey.length > 0
|
|
470
|
+
? event.sourceSessionKey
|
|
471
|
+
: typeof event.sessionKey === "string" && event.sessionKey.length > 0
|
|
472
|
+
? event.sessionKey
|
|
473
|
+
: undefined;
|
|
474
|
+
const outputMemoryIds = Array.isArray(event.outputMemoryIds)
|
|
475
|
+
? Array.from(
|
|
476
|
+
new Set(
|
|
477
|
+
event.outputMemoryIds.filter(
|
|
478
|
+
(value): value is string =>
|
|
479
|
+
typeof value === "string" && value.length > 0,
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
)
|
|
483
|
+
: [];
|
|
484
|
+
|
|
485
|
+
const reasonParts = [
|
|
486
|
+
event.reason,
|
|
487
|
+
`policy:${policy.decision}`,
|
|
488
|
+
policy.rationale,
|
|
489
|
+
].filter(
|
|
490
|
+
(part): part is string => typeof part === "string" && part.length > 0,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
...event,
|
|
495
|
+
schemaVersion: event.schemaVersion ?? 1,
|
|
496
|
+
actionId:
|
|
497
|
+
typeof event.actionId === "string" && event.actionId.length > 0
|
|
498
|
+
? event.actionId
|
|
499
|
+
: `memact-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
500
|
+
outcome: normalizedOutcome,
|
|
501
|
+
status:
|
|
502
|
+
event.status ??
|
|
503
|
+
(dryRun && policy.decision === "allow" && event.outcome !== "failed"
|
|
504
|
+
? "validated"
|
|
505
|
+
: normalizedOutcome === "applied"
|
|
506
|
+
? "applied"
|
|
507
|
+
: "rejected"),
|
|
508
|
+
actor:
|
|
509
|
+
typeof event.actor === "string" && event.actor.length > 0
|
|
510
|
+
? event.actor
|
|
511
|
+
: "engram",
|
|
512
|
+
subsystem:
|
|
513
|
+
typeof event.subsystem === "string" && event.subsystem.length > 0
|
|
514
|
+
? event.subsystem
|
|
515
|
+
: "memory_action",
|
|
516
|
+
reason: reasonParts.join(" | "),
|
|
517
|
+
namespace,
|
|
518
|
+
sessionKey: sourceSessionKey ?? event.sessionKey,
|
|
519
|
+
sourceSessionKey,
|
|
520
|
+
inputSummary:
|
|
521
|
+
typeof event.inputSummary === "string" && event.inputSummary.length > 0
|
|
522
|
+
? event.inputSummary
|
|
523
|
+
: undefined,
|
|
524
|
+
outputMemoryIds,
|
|
525
|
+
dryRun,
|
|
526
|
+
policyVersion:
|
|
527
|
+
typeof event.policyVersion === "string" &&
|
|
528
|
+
event.policyVersion.length > 0
|
|
529
|
+
? event.policyVersion
|
|
530
|
+
: "memory-action-policy.v1",
|
|
531
|
+
timestamp:
|
|
532
|
+
typeof event.timestamp === "string" && event.timestamp.length > 0
|
|
533
|
+
? event.timestamp
|
|
534
|
+
: new Date().toISOString(),
|
|
535
|
+
policyDecision: policy.decision,
|
|
536
|
+
policyRationale: policy.rationale,
|
|
537
|
+
policyEligibility: eligibility,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Validate local LLM model availability and context window compatibility.
|
|
543
|
+
* Warns the user if there's a mismatch.
|
|
544
|
+
*/
|
|
545
|
+
async validateLocalLlmModel(): Promise<void> {
|
|
546
|
+
log.debug("Local LLM: validating model configuration");
|
|
547
|
+
try {
|
|
548
|
+
const modelInfo = await this.deps.localLlm.getLoadedModelInfo();
|
|
549
|
+
if (!modelInfo) {
|
|
550
|
+
log.warn(
|
|
551
|
+
"Local LLM validation: Could not query model info from server",
|
|
552
|
+
);
|
|
553
|
+
log.warn(
|
|
554
|
+
"Local LLM validation: Could not query model info. " +
|
|
555
|
+
"Ensure LM Studio/Ollama is running with the model loaded.",
|
|
556
|
+
);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Check for context window mismatch
|
|
561
|
+
const configuredMaxContext = this.deps.config.localLlmMaxContext;
|
|
562
|
+
|
|
563
|
+
if (modelInfo.contextWindow) {
|
|
564
|
+
log.debug(
|
|
565
|
+
`Local LLM: ${modelInfo.id} loaded with ${modelInfo.contextWindow.toLocaleString()} token context window`,
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
if (
|
|
569
|
+
configuredMaxContext &&
|
|
570
|
+
configuredMaxContext > modelInfo.contextWindow
|
|
571
|
+
) {
|
|
572
|
+
log.warn(
|
|
573
|
+
`Local LLM context mismatch: engram configured for ${configuredMaxContext.toLocaleString()} tokens, ` +
|
|
574
|
+
`but ${modelInfo.id} only supports ${modelInfo.contextWindow.toLocaleString()}. ` +
|
|
575
|
+
`Reducing to ${modelInfo.contextWindow.toLocaleString()} to avoid errors.`,
|
|
576
|
+
);
|
|
577
|
+
// Update the config in-memory to match actual capability
|
|
578
|
+
// (This is a temporary fix - user should update their config)
|
|
579
|
+
(this.deps.config as { localLlmMaxContext?: number }).localLlmMaxContext =
|
|
580
|
+
modelInfo.contextWindow;
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
log.debug(
|
|
584
|
+
`Local LLM: ${modelInfo.id} loaded (context window not reported by server)`,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
if (!configuredMaxContext) {
|
|
588
|
+
log.warn(
|
|
589
|
+
"Local LLM: Server did not report context window. " +
|
|
590
|
+
"If you get 'context length exceeded' errors, set localLlmMaxContext in your config. " +
|
|
591
|
+
"Common defaults: LM Studio (32K), Ollama (2K-128K depending on model).",
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch (err) {
|
|
596
|
+
log.warn(`Local LLM validation failed: ${err}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Lazily-constructed wearables service (Limitless / Bee / Omi
|
|
602
|
+
* transcript ingestion). All wearables surfaces — CLI, MCP tools,
|
|
603
|
+
* HTTP routes — share this one instance so sync state, search, and
|
|
604
|
+
* memory writes stay consistent. Writes are pinned to the same
|
|
605
|
+
* deterministic namespace bulk-import uses.
|
|
606
|
+
*/
|
|
607
|
+
getWearablesService(): WearablesService {
|
|
608
|
+
if (!this.deps.wearablesServiceInstance) {
|
|
609
|
+
this.deps.wearablesServiceInstance = new WearablesService({
|
|
610
|
+
config: this.deps.config.wearables,
|
|
611
|
+
getStorage: async () =>
|
|
612
|
+
await this.deps.getStorageForNamespace(this.deps.bulkImportWriteNamespace()),
|
|
613
|
+
extract: (turns) => this.deps.extraction.extract(turns),
|
|
614
|
+
// Smart memoryMode runs candidates through the SAME extraction
|
|
615
|
+
// judge (cache + defer counters included) the live extraction
|
|
616
|
+
// pipeline uses, so wearable facts get identical LLM-as-judge
|
|
617
|
+
// durability gating.
|
|
618
|
+
judgeFacts: (candidates) =>
|
|
619
|
+
judgeFactDurability(
|
|
620
|
+
candidates,
|
|
621
|
+
this.deps.config,
|
|
622
|
+
this.deps.localLlm,
|
|
623
|
+
new FallbackLlmClient(
|
|
624
|
+
this.deps.config.gatewayConfig,
|
|
625
|
+
fallbackLlmRuntimeContextFromConfig(this.deps.config),
|
|
626
|
+
),
|
|
627
|
+
this.deps.judgeVerdictCache,
|
|
628
|
+
this.deps.judgeDeferCounts,
|
|
629
|
+
),
|
|
630
|
+
searchBackend: {
|
|
631
|
+
search: async (query, maxResults) => {
|
|
632
|
+
if (!this.deps.qmd.isAvailable()) return null;
|
|
633
|
+
try {
|
|
634
|
+
const results = await this.deps.qmd.search(query, undefined, maxResults);
|
|
635
|
+
return results.map((result) => ({
|
|
636
|
+
path: result.path,
|
|
637
|
+
score: result.score,
|
|
638
|
+
preview: result.snippet,
|
|
639
|
+
}));
|
|
640
|
+
} catch {
|
|
641
|
+
// Backend hiccup → tell the service "unavailable" so it
|
|
642
|
+
// runs its bounded scan fallback instead of returning a
|
|
643
|
+
// silent empty result (CLAUDE.md rule 34).
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
reindexSearch: async () => {
|
|
649
|
+
await this.deps.qmd.update();
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return this.deps.wearablesServiceInstance;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async autoConsolidateIdentity(): Promise<void> {
|
|
657
|
+
// Fan out over the catalog-union namespace set (issue #1499 sweep): a dynamic
|
|
658
|
+
// namespace that accumulated IDENTITY.md reflections must also be eligible for
|
|
659
|
+
// auto-consolidation, otherwise its identity file grows unbounded and is never
|
|
660
|
+
// consolidated. Falls back to the configured set on any catalog read failure.
|
|
661
|
+
const namespaces = resolveNamespaceCapabilities(this.deps.config).namespaces
|
|
662
|
+
? await this.deps.maintenanceNamespaces()
|
|
663
|
+
: [this.deps.config.defaultNamespace];
|
|
664
|
+
|
|
665
|
+
for (const namespace of namespaces) {
|
|
666
|
+
const storage = await this.deps.storageRouter.storageFor(namespace);
|
|
667
|
+
const identityNamespace =
|
|
668
|
+
resolveNamespaceCapabilities(this.deps.config).namespaces &&
|
|
669
|
+
namespace !== this.deps.config.defaultNamespace
|
|
670
|
+
? namespace
|
|
671
|
+
: undefined;
|
|
672
|
+
const reflectionsContent =
|
|
673
|
+
(await storage.readIdentityReflections()) ?? "";
|
|
674
|
+
|
|
675
|
+
const existingIdentity = await storage.readIdentity(
|
|
676
|
+
this.deps.config.workspaceDir,
|
|
677
|
+
identityNamespace,
|
|
678
|
+
);
|
|
679
|
+
const headerEnd =
|
|
680
|
+
existingIdentity.indexOf("## Learned Patterns") !== -1
|
|
681
|
+
? existingIdentity.indexOf("## Learned Patterns")
|
|
682
|
+
: existingIdentity.indexOf("## Reflection");
|
|
683
|
+
const staticHeader =
|
|
684
|
+
(headerEnd !== -1
|
|
685
|
+
? existingIdentity.slice(0, headerEnd)
|
|
686
|
+
: existingIdentity
|
|
687
|
+
).trimEnd() || "# IDENTITY";
|
|
688
|
+
const identityContent = `${staticHeader}\n\n${reflectionsContent.trim()}\n`;
|
|
689
|
+
if (identityContent.length < Orchestrator.IDENTITY_CONSOLIDATE_THRESHOLD)
|
|
690
|
+
continue;
|
|
691
|
+
|
|
692
|
+
log.info(
|
|
693
|
+
`IDENTITY(${namespace}) is ${identityContent.length} chars — auto-consolidating reflections`,
|
|
694
|
+
);
|
|
695
|
+
const result = await this.deps.extraction.consolidateIdentity(
|
|
696
|
+
identityContent,
|
|
697
|
+
"## Reflection",
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
if (!result || result.learnedPatterns.length === 0) {
|
|
701
|
+
log.warn(
|
|
702
|
+
`identity consolidation produced no patterns for namespace=${namespace}`,
|
|
703
|
+
);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const patternsSection = [
|
|
708
|
+
"## Learned Patterns (consolidated from reflections, " +
|
|
709
|
+
new Date().toISOString().slice(0, 10) +
|
|
710
|
+
")",
|
|
711
|
+
"",
|
|
712
|
+
...result.learnedPatterns.map((p) => `- ${p}`),
|
|
713
|
+
"",
|
|
714
|
+
].join("\n");
|
|
715
|
+
|
|
716
|
+
const newContent = staticHeader + "\n\n" + patternsSection + "\n";
|
|
717
|
+
|
|
718
|
+
await storage.writeIdentity(
|
|
719
|
+
this.deps.config.workspaceDir,
|
|
720
|
+
newContent,
|
|
721
|
+
identityNamespace,
|
|
722
|
+
);
|
|
723
|
+
await storage.writeIdentityReflections("");
|
|
724
|
+
// NRcCL (codex P2): record a per-namespace catalog write for THIS namespace
|
|
725
|
+
// after the identity files are updated. This fan-out can mutate a dynamic
|
|
726
|
+
// namespace via `writeIdentity`/`writeIdentityReflections`, but the
|
|
727
|
+
// consolidation pass's only consolidated touch covers `this.deps.storage` (the
|
|
728
|
+
// default) and only fires when `memoryItemMutated` was set by OTHER work — so
|
|
729
|
+
// a namespace whose sole mutation in the pass is identity consolidation would
|
|
730
|
+
// otherwise keep a stale `lastWriteAt`, making `listNamespaces({ writtenSince })`
|
|
731
|
+
// and catalog-recency consumers miss the write. Best-effort and
|
|
732
|
+
// failure-tolerant (the storage chokepoint (#1522) swallows errors, never crashing the
|
|
733
|
+
// consolidation; gotcha #13, rule #40). No double-count with the consolidated
|
|
734
|
+
// touch above: that one is gated on `memoryItemMutated` (which identity
|
|
735
|
+
// consolidation does not set), and `markWrite` is idempotent regardless.
|
|
736
|
+
log.info(
|
|
737
|
+
`IDENTITY(${namespace}) consolidated: ${identityContent.length} → ${newContent.length} chars, ${result.learnedPatterns.length} patterns`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Flush access tracking buffer to disk.
|
|
744
|
+
* Called during consolidation or when buffer is full.
|
|
745
|
+
*/
|
|
746
|
+
async flushAccessTracking(): Promise<void> {
|
|
747
|
+
if (this.deps.accessTrackingBuffer.size === 0) return;
|
|
748
|
+
|
|
749
|
+
// Build entries from buffer, merging with existing counts
|
|
750
|
+
const entries: AccessTrackingEntry[] = [];
|
|
751
|
+
const namespaces = resolveNamespaceCapabilities(this.deps.config).namespaces
|
|
752
|
+
? Array.from(
|
|
753
|
+
new Set<string>([
|
|
754
|
+
this.deps.config.defaultNamespace,
|
|
755
|
+
this.deps.config.sharedNamespace,
|
|
756
|
+
...this.deps.config.namespacePolicies.map((p) => p.name),
|
|
757
|
+
]),
|
|
758
|
+
)
|
|
759
|
+
: [this.deps.config.defaultNamespace];
|
|
760
|
+
const memories = await this.deps.readAllMemoriesForNamespaces(namespaces);
|
|
761
|
+
const memoryMap = new Map(memories.map((m) => [m.frontmatter.id, m]));
|
|
762
|
+
|
|
763
|
+
for (const [memoryId, update] of this.deps.accessTrackingBuffer) {
|
|
764
|
+
const memory = memoryMap.get(memoryId);
|
|
765
|
+
const existingCount = memory?.frontmatter.accessCount ?? 0;
|
|
766
|
+
entries.push({
|
|
767
|
+
memoryId,
|
|
768
|
+
newCount: existingCount + update.count,
|
|
769
|
+
lastAccessed: update.lastAccessed,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const byNamespace = new Map<string, AccessTrackingEntry[]>();
|
|
774
|
+
for (const e of entries) {
|
|
775
|
+
const m = memoryMap.get(e.memoryId);
|
|
776
|
+
if (!m) continue;
|
|
777
|
+
const ns = this.deps.namespaceFromPath(m.path);
|
|
778
|
+
const list = byNamespace.get(ns) ?? [];
|
|
779
|
+
list.push(e);
|
|
780
|
+
byNamespace.set(ns, list);
|
|
781
|
+
}
|
|
782
|
+
for (const [ns, list] of byNamespace) {
|
|
783
|
+
const sm = await this.deps.storageRouter.storageFor(ns);
|
|
784
|
+
await sm.flushAccessTracking(list);
|
|
785
|
+
}
|
|
786
|
+
this.deps.accessTrackingBuffer.clear();
|
|
787
|
+
log.debug(`flushed ${entries.length} access tracking entries`);
|
|
788
|
+
}
|
|
789
|
+
}
|