@caupulican/pi-adaptative 0.81.26 → 0.81.29
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/CHANGELOG.md +45 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +3 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +4 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +19 -2
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +30 -12
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +128 -36
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +6 -0
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +35 -0
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
- package/dist/core/autonomy/bounded-completion.js +59 -19
- package/dist/core/autonomy/bounded-completion.js.map +1 -1
- package/dist/core/autonomy/contracts.d.ts +2 -0
- package/dist/core/autonomy/contracts.d.ts.map +1 -1
- package/dist/core/autonomy/contracts.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts +31 -0
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -0
- package/dist/core/autonomy/lane-tool-surface.js +96 -0
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -0
- package/dist/core/autonomy/lane-tracker.d.ts +5 -0
- package/dist/core/autonomy/lane-tracker.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tracker.js +14 -3
- package/dist/core/autonomy/lane-tracker.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +28 -4
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +237 -28
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/billing-failover-controller.d.ts.map +1 -1
- package/dist/core/billing-failover-controller.js +1 -1
- package/dist/core/billing-failover-controller.js.map +1 -1
- package/dist/core/context/current-work-memory.d.ts +9 -0
- package/dist/core/context/current-work-memory.d.ts.map +1 -0
- package/dist/core/context/current-work-memory.js +61 -0
- package/dist/core/context/current-work-memory.js.map +1 -0
- package/dist/core/context/file-store-memory-provider.d.ts +9 -0
- package/dist/core/context/file-store-memory-provider.d.ts.map +1 -0
- package/dist/core/context/file-store-memory-provider.js +106 -0
- package/dist/core/context/file-store-memory-provider.js.map +1 -0
- package/dist/core/context/long-term-memory-trigger.d.ts +15 -0
- package/dist/core/context/long-term-memory-trigger.d.ts.map +1 -0
- package/dist/core/context/long-term-memory-trigger.js +39 -0
- package/dist/core/context/long-term-memory-trigger.js.map +1 -0
- package/dist/core/context/memory-prompt-block.d.ts +3 -1
- package/dist/core/context/memory-prompt-block.d.ts.map +1 -1
- package/dist/core/context/memory-prompt-block.js +20 -3
- package/dist/core/context/memory-prompt-block.js.map +1 -1
- package/dist/core/context/memory-prompt-budget.d.ts +17 -0
- package/dist/core/context/memory-prompt-budget.d.ts.map +1 -0
- package/dist/core/context/memory-prompt-budget.js +64 -0
- package/dist/core/context/memory-prompt-budget.js.map +1 -0
- package/dist/core/context/memory-provider-contract.d.ts.map +1 -1
- package/dist/core/context/memory-provider-contract.js +2 -5
- package/dist/core/context/memory-provider-contract.js.map +1 -1
- package/dist/core/context/memory-tier-composer.d.ts +27 -0
- package/dist/core/context/memory-tier-composer.d.ts.map +1 -0
- package/dist/core/context/memory-tier-composer.js +79 -0
- package/dist/core/context/memory-tier-composer.js.map +1 -0
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +2 -0
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/context-pipeline.d.ts +5 -3
- package/dist/core/context-pipeline.d.ts.map +1 -1
- package/dist/core/context-pipeline.js +15 -5
- package/dist/core/context-pipeline.js.map +1 -1
- package/dist/core/cost/cost-summary.d.ts +3 -1
- package/dist/core/cost/cost-summary.d.ts.map +1 -1
- package/dist/core/cost/cost-summary.js +3 -3
- package/dist/core/cost/cost-summary.js.map +1 -1
- package/dist/core/cost-guard.d.ts +17 -7
- package/dist/core/cost-guard.d.ts.map +1 -1
- package/dist/core/cost-guard.js +33 -10
- package/dist/core/cost-guard.js.map +1 -1
- package/dist/core/default-tool-surface.d.ts +9 -0
- package/dist/core/default-tool-surface.d.ts.map +1 -0
- package/dist/core/default-tool-surface.js +18 -0
- package/dist/core/default-tool-surface.js.map +1 -0
- package/dist/core/delegation/worker-result.d.ts.map +1 -1
- package/dist/core/delegation/worker-result.js +3 -0
- package/dist/core/delegation/worker-result.js.map +1 -1
- package/dist/core/delegation/worker-runner.d.ts +11 -5
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +77 -16
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +15 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +2 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +7 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +9 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/failure-corpus.d.ts.map +1 -1
- package/dist/core/failure-corpus.js +2 -4
- package/dist/core/failure-corpus.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +9 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.d.ts.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.js +4 -3
- package/dist/core/goals/goal-continuation-prompt.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts +12 -0
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +42 -4
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/local-runtime-controller.d.ts +16 -0
- package/dist/core/local-runtime-controller.d.ts.map +1 -1
- package/dist/core/local-runtime-controller.js +107 -28
- package/dist/core/local-runtime-controller.js.map +1 -1
- package/dist/core/memory/memory-manager.d.ts +8 -3
- package/dist/core/memory/memory-manager.d.ts.map +1 -1
- package/dist/core/memory/memory-manager.js +27 -11
- package/dist/core/memory/memory-manager.js.map +1 -1
- package/dist/core/memory/memory-provider.d.ts +5 -1
- package/dist/core/memory/memory-provider.d.ts.map +1 -1
- package/dist/core/memory/memory-provider.js.map +1 -1
- package/dist/core/memory/providers/file-store.d.ts +3 -1
- package/dist/core/memory/providers/file-store.d.ts.map +1 -1
- package/dist/core/memory/providers/file-store.js +7 -2
- package/dist/core/memory/providers/file-store.js.map +1 -1
- package/dist/core/memory/providers/transcript-recall.d.ts +1 -0
- package/dist/core/memory/providers/transcript-recall.d.ts.map +1 -1
- package/dist/core/memory/providers/transcript-recall.js +4 -7
- package/dist/core/memory/providers/transcript-recall.js.map +1 -1
- package/dist/core/memory-controller.d.ts +36 -10
- package/dist/core/memory-controller.d.ts.map +1 -1
- package/dist/core/memory-controller.js +137 -21
- package/dist/core/memory-controller.js.map +1 -1
- package/dist/core/model-registry.d.ts +25 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +91 -8
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +30 -11
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-router-controller.d.ts.map +1 -1
- package/dist/core/model-router-controller.js +33 -29
- package/dist/core/model-router-controller.js.map +1 -1
- package/dist/core/model-selection-controller.d.ts +3 -0
- package/dist/core/model-selection-controller.d.ts.map +1 -1
- package/dist/core/model-selection-controller.js +8 -1
- package/dist/core/model-selection-controller.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts +2 -0
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +10 -4
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/models/perf-profile.d.ts.map +1 -1
- package/dist/core/models/perf-profile.js +95 -33
- package/dist/core/models/perf-profile.js.map +1 -1
- package/dist/core/models/runtime-arbiter.d.ts +11 -0
- package/dist/core/models/runtime-arbiter.d.ts.map +1 -1
- package/dist/core/models/runtime-arbiter.js +35 -13
- package/dist/core/models/runtime-arbiter.js.map +1 -1
- package/dist/core/profile-filter-controller.d.ts +11 -0
- package/dist/core/profile-filter-controller.d.ts.map +1 -1
- package/dist/core/profile-filter-controller.js +56 -11
- package/dist/core/profile-filter-controller.js.map +1 -1
- package/dist/core/profile-registry.d.ts +2 -2
- package/dist/core/profile-registry.d.ts.map +1 -1
- package/dist/core/profile-registry.js +58 -7
- package/dist/core/profile-registry.js.map +1 -1
- package/dist/core/reflection-controller.d.ts +9 -7
- package/dist/core/reflection-controller.d.ts.map +1 -1
- package/dist/core/reflection-controller.js +81 -12
- package/dist/core/reflection-controller.js.map +1 -1
- package/dist/core/research/research-runner.d.ts +2 -2
- package/dist/core/research/research-runner.d.ts.map +1 -1
- package/dist/core/research/research-runner.js +8 -6
- package/dist/core/research/research-runner.js.map +1 -1
- package/dist/core/resource-loader.d.ts +4 -2
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +11 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/runtime-builder.d.ts +30 -3
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +140 -40
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -7
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +38 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/security/secret-text.d.ts +3 -0
- package/dist/core/security/secret-text.d.ts.map +1 -0
- package/dist/core/security/secret-text.js +26 -0
- package/dist/core/security/secret-text.js.map +1 -0
- package/dist/core/settings-manager.d.ts +59 -17
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +300 -51
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +1 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +7 -0
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.js +23 -1
- package/dist/core/system-prompt-builder.js.map +1 -1
- package/dist/core/tool-gate-controller.d.ts +3 -0
- package/dist/core/tool-gate-controller.d.ts.map +1 -1
- package/dist/core/tool-gate-controller.js +20 -15
- package/dist/core/tool-gate-controller.js.map +1 -1
- package/dist/core/tool-selection/expected-utility.d.ts +51 -0
- package/dist/core/tool-selection/expected-utility.d.ts.map +1 -0
- package/dist/core/tool-selection/expected-utility.js +85 -0
- package/dist/core/tool-selection/expected-utility.js.map +1 -0
- package/dist/core/tool-selection/tool-performance-store.d.ts +82 -0
- package/dist/core/tool-selection/tool-performance-store.d.ts.map +1 -0
- package/dist/core/tool-selection/tool-performance-store.js +249 -0
- package/dist/core/tool-selection/tool-performance-store.js.map +1 -0
- package/dist/core/tool-selection/tool-selection-controller.d.ts +41 -0
- package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -0
- package/dist/core/tool-selection/tool-selection-controller.js +199 -0
- package/dist/core/tool-selection/tool-selection-controller.js.map +1 -0
- package/dist/core/tools/delegate-status.d.ts +9 -0
- package/dist/core/tools/delegate-status.d.ts.map +1 -0
- package/dist/core/tools/delegate-status.js +52 -0
- package/dist/core/tools/delegate-status.js.map +1 -0
- package/dist/core/tools/delegate.d.ts +10 -0
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +36 -13
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/run-toolkit-script.d.ts +3 -0
- package/dist/core/tools/run-toolkit-script.d.ts.map +1 -1
- package/dist/core/tools/run-toolkit-script.js +17 -7
- package/dist/core/tools/run-toolkit-script.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +69 -29
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -0
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/config-backup.d.ts +7 -1
- package/dist/modes/interactive/config-backup.d.ts.map +1 -1
- package/dist/modes/interactive/config-backup.js +196 -32
- package/dist/modes/interactive/config-backup.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +22 -28
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
- package/dist/modes/interactive/local-model-commands.js +2 -0
- package/dist/modes/interactive/local-model-commands.js.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.d.ts +6 -4
- package/dist/modes/interactive/profile-menu-controller.d.ts.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.js +248 -65
- package/dist/modes/interactive/profile-menu-controller.js.map +1 -1
- package/dist/modes/interactive/report-commands.d.ts.map +1 -1
- package/dist/modes/interactive/report-commands.js +8 -2
- package/dist/modes/interactive/report-commands.js.map +1 -1
- package/dist/modes/interactive/settings-selector-flow.d.ts.map +1 -1
- package/dist/modes/interactive/settings-selector-flow.js +8 -0
- package/dist/modes/interactive/settings-selector-flow.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +3 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/docs/extensions.md +32 -0
- package/docs/integration-sweep-builder-blueprint-2026-07-09.md +365 -0
- package/docs/integration-sweep-resume-2026-07-09.md +407 -0
- package/docs/resources.md +15 -7
- package/docs/settings.md +56 -5
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/preset.ts +2 -2
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +44 -35
- package/package.json +4 -4
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { MemoryPromptBudget } from "../context/memory-prompt-budget.ts";
|
|
1
2
|
import type { ToolDefinition } from "../extensions/types.ts";
|
|
2
3
|
export type MemorySurface = "context" | "routing" | "tooling" | "parametric";
|
|
4
|
+
/** Where raw memory queries are processed. Omitted classifications fail closed as external. */
|
|
5
|
+
export type MemoryProviderEgress = "local" | "external";
|
|
3
6
|
export interface MemoryCapabilities {
|
|
4
7
|
surfaces: MemorySurface[];
|
|
5
8
|
}
|
|
@@ -10,11 +13,12 @@ export interface MemoryLifecycleContext {
|
|
|
10
13
|
}
|
|
11
14
|
export interface MemoryProvider {
|
|
12
15
|
readonly name: string;
|
|
16
|
+
readonly egress?: MemoryProviderEgress;
|
|
13
17
|
isAvailable(): boolean | Promise<boolean>;
|
|
14
18
|
getCapabilities(): MemoryCapabilities;
|
|
15
19
|
initialize(sessionId: string, ctx: MemoryLifecycleContext): Promise<void>;
|
|
16
20
|
shutdown(): Promise<void>;
|
|
17
|
-
systemPromptBlock?(): string;
|
|
21
|
+
systemPromptBlock?(budget?: MemoryPromptBudget): string;
|
|
18
22
|
prefetch?(query: string): Promise<string>;
|
|
19
23
|
syncTurn?(user: string, assistant: string): Promise<void>;
|
|
20
24
|
onPreCompress?(): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-provider.d.ts","sourceRoot":"","sources":["../../../src/core/memory/memory-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;AAE7E,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,WAAW,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,eAAe,IAAI,kBAAkB,CAAC;IACtC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B,iBAAiB,CAAC,
|
|
1
|
+
{"version":3,"file":"memory-provider.d.ts","sourceRoot":"","sources":["../../../src/core/memory/memory-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;AAE7E,+FAA+F;AAC/F,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,UAAU,CAAC;AAExD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IACvC,WAAW,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,eAAe,IAAI,kBAAkB,CAAC;IACtC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B,iBAAiB,CAAC,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC;IACxD,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,aAAa,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,kBAAkB,CAAC,IAAI,cAAc,EAAE,CAAC;IACxC,iBAAiB,CAAC,IAAI,MAAM,EAAE,CAAC;CAC/B","sourcesContent":["import type { MemoryPromptBudget } from \"../context/memory-prompt-budget.ts\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\n\nexport type MemorySurface = \"context\" | \"routing\" | \"tooling\" | \"parametric\";\n\n/** Where raw memory queries are processed. Omitted classifications fail closed as external. */\nexport type MemoryProviderEgress = \"local\" | \"external\";\n\nexport interface MemoryCapabilities {\n\tsurfaces: MemorySurface[];\n}\n\nexport interface MemoryLifecycleContext {\n\tagentDir: string;\n\tcwd: string;\n\tisChildSession: boolean;\n}\n\nexport interface MemoryProvider {\n\treadonly name: string;\n\treadonly egress?: MemoryProviderEgress;\n\tisAvailable(): boolean | Promise<boolean>;\n\tgetCapabilities(): MemoryCapabilities;\n\tinitialize(sessionId: string, ctx: MemoryLifecycleContext): Promise<void>;\n\tshutdown(): Promise<void>;\n\t// context surface:\n\tsystemPromptBlock?(budget?: MemoryPromptBudget): string;\n\tprefetch?(query: string): Promise<string>;\n\tsyncTurn?(user: string, assistant: string): Promise<void>;\n\tonPreCompress?(): Promise<string>;\n\tonSessionEnd?(): Promise<void>;\n\tgetToolDefinitions?(): ToolDefinition[];\n\tgetContextMarkers?(): string[];\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-provider.js","sourceRoot":"","sources":["../../../src/core/memory/memory-provider.ts"],"names":[],"mappings":"","sourcesContent":["import type { ToolDefinition } from \"../extensions/types.ts\";\n\nexport type MemorySurface = \"context\" | \"routing\" | \"tooling\" | \"parametric\";\n\nexport interface MemoryCapabilities {\n\tsurfaces: MemorySurface[];\n}\n\nexport interface MemoryLifecycleContext {\n\tagentDir: string;\n\tcwd: string;\n\tisChildSession: boolean;\n}\n\nexport interface MemoryProvider {\n\treadonly name: string;\n\tisAvailable(): boolean | Promise<boolean>;\n\tgetCapabilities(): MemoryCapabilities;\n\tinitialize(sessionId: string, ctx: MemoryLifecycleContext): Promise<void>;\n\tshutdown(): Promise<void>;\n\t// context surface:\n\tsystemPromptBlock?(): string;\n\tprefetch?(query: string): Promise<string>;\n\tsyncTurn?(user: string, assistant: string): Promise<void>;\n\tonPreCompress?(): Promise<string>;\n\tonSessionEnd?(): Promise<void>;\n\tgetToolDefinitions?(): ToolDefinition[];\n\tgetContextMarkers?(): string[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"memory-provider.js","sourceRoot":"","sources":["../../../src/core/memory/memory-provider.ts"],"names":[],"mappings":"","sourcesContent":["import type { MemoryPromptBudget } from \"../context/memory-prompt-budget.ts\";\nimport type { ToolDefinition } from \"../extensions/types.ts\";\n\nexport type MemorySurface = \"context\" | \"routing\" | \"tooling\" | \"parametric\";\n\n/** Where raw memory queries are processed. Omitted classifications fail closed as external. */\nexport type MemoryProviderEgress = \"local\" | \"external\";\n\nexport interface MemoryCapabilities {\n\tsurfaces: MemorySurface[];\n}\n\nexport interface MemoryLifecycleContext {\n\tagentDir: string;\n\tcwd: string;\n\tisChildSession: boolean;\n}\n\nexport interface MemoryProvider {\n\treadonly name: string;\n\treadonly egress?: MemoryProviderEgress;\n\tisAvailable(): boolean | Promise<boolean>;\n\tgetCapabilities(): MemoryCapabilities;\n\tinitialize(sessionId: string, ctx: MemoryLifecycleContext): Promise<void>;\n\tshutdown(): Promise<void>;\n\t// context surface:\n\tsystemPromptBlock?(budget?: MemoryPromptBudget): string;\n\tprefetch?(query: string): Promise<string>;\n\tsyncTurn?(user: string, assistant: string): Promise<void>;\n\tonPreCompress?(): Promise<string>;\n\tonSessionEnd?(): Promise<void>;\n\tgetToolDefinitions?(): ToolDefinition[];\n\tgetContextMarkers?(): string[];\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MemoryPromptBudget } from "../../context/memory-prompt-budget.ts";
|
|
1
2
|
import type { ToolDefinition } from "../../extensions/types.ts";
|
|
2
3
|
import type { MemoryLifecycleContext, MemoryProvider } from "../memory-provider.ts";
|
|
3
4
|
/**
|
|
@@ -8,6 +9,7 @@ import type { MemoryLifecycleContext, MemoryProvider } from "../memory-provider.
|
|
|
8
9
|
export declare function supersedeNearDuplicateLine(existing: string, content: string): string | null;
|
|
9
10
|
export declare class FileStoreProvider implements MemoryProvider {
|
|
10
11
|
readonly name = "file-store";
|
|
12
|
+
readonly egress = "local";
|
|
11
13
|
private ctx?;
|
|
12
14
|
private memoryFilePath;
|
|
13
15
|
private userFilePath;
|
|
@@ -20,7 +22,7 @@ export declare class FileStoreProvider implements MemoryProvider {
|
|
|
20
22
|
surfaces: "context"[];
|
|
21
23
|
};
|
|
22
24
|
initialize(_sessionId: string, ctx: MemoryLifecycleContext): Promise<void>;
|
|
23
|
-
systemPromptBlock(): string;
|
|
25
|
+
systemPromptBlock(budget?: MemoryPromptBudget): string;
|
|
24
26
|
prefetch(_query: string): Promise<string>;
|
|
25
27
|
shutdown(): Promise<void>;
|
|
26
28
|
getContextMarkers(): string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../../../../src/core/memory/providers/file-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEpF;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsB3F;AAiBD,qBAAa,iBAAkB,YAAW,cAAc;IACvD,SAAgB,IAAI,gBAAgB;IAEpC,OAAO,CAAC,GAAG,CAAC,CAAyB;IACrC,OAAO,CAAC,cAAc,CAAM;IAC5B,OAAO,CAAC,YAAY,CAAM;IAE1B,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,eAAe,CAAM;IAG7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAQ;IAC7C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAQ;IAEpC,WAAW,IAAI,OAAO,CAE5B;IAEM,eAAe;;MAErB;IAEY,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBtF;IAEM,iBAAiB,IAAI,MAAM,CAwCjC;IAEY,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrD;IAEY,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAErC;IAEM,iBAAiB,IAAI,MAAM,EAAE,CAEnC;IAEM,kBAAkB,IAAI,cAAc,EAAE,CAoK5C;CACD","sourcesContent":["import { existsSync, promises as fs, mkdirSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { type Static, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../../extensions/types.ts\";\nimport { hasInvisibleUnicode, scanContextFileThreats, stripInvisibleUnicode } from \"../../resource-loader.ts\";\nimport { jaccard, tokenize } from \"../../tools/skill-audit.ts\";\nimport type { MemoryLifecycleContext, MemoryProvider } from \"../memory-provider.ts\";\n\n/**\n * R5 confront-before-write (anti append-rot): if `content` is a near-duplicate of an existing\n * non-empty line (token Jaccard ≥ threshold — i.e. the same fact reworded), supersede that line in\n * place and return the rewritten file; otherwise return null (the caller appends normally).\n */\nexport function supersedeNearDuplicateLine(existing: string, content: string): string | null {\n\tconst NEAR_DUP_THRESHOLD = 0.6;\n\tconst contentTokens = tokenize(content);\n\tif (contentTokens.length === 0) return null;\n\tconst lines = existing.split(\"\\n\");\n\tlet bestIdx = -1;\n\tlet bestScore = NEAR_DUP_THRESHOLD;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\t\tif (!line) continue;\n\t\t// Never supersede structural Markdown (headings, list markers as headings) — a fact must not\n\t\t// silently overwrite section structure (Bug #15).\n\t\tif (line.startsWith(\"#\")) continue;\n\t\tconst score = jaccard(contentTokens, tokenize(line));\n\t\tif (score >= bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbestIdx = i;\n\t\t}\n\t}\n\tif (bestIdx === -1) return null;\n\tlines[bestIdx] = content;\n\treturn lines.join(\"\\n\");\n}\n\nconst memorySchema = Type.Object({\n\taction: Type.Union([Type.Literal(\"add\"), Type.Literal(\"replace\"), Type.Literal(\"remove\")], {\n\t\tdescription: \"Action to perform: add new content, replace existing content, or remove content\",\n\t}),\n\ttarget: Type.Union([Type.Literal(\"memory\"), Type.Literal(\"user\")], {\n\t\tdescription: \"Target file: 'memory' for MEMORY.md, 'user' for USER.md\",\n\t}),\n\tcontent: Type.Optional(Type.String({ description: \"Content to write (required for 'add' or 'replace')\" })),\n\toldContent: Type.Optional(\n\t\tType.String({ description: \"Exact substring to replace or remove (required for 'replace' or 'remove')\" }),\n\t),\n});\n\ntype MemoryParams = Static<typeof memorySchema>;\n\nexport class FileStoreProvider implements MemoryProvider {\n\tpublic readonly name = \"file-store\";\n\n\tprivate ctx?: MemoryLifecycleContext;\n\tprivate memoryFilePath = \"\";\n\tprivate userFilePath = \"\";\n\n\tprivate lastWrittenMemory = \"\";\n\tprivate lastWrittenUser = \"\";\n\n\t// Character budgets\n\tprivate static readonly BUDGET_MEMORY = 2200;\n\tprivate static readonly BUDGET_USER = 1375;\n\n\tpublic isAvailable(): boolean {\n\t\treturn true;\n\t}\n\n\tpublic getCapabilities() {\n\t\treturn { surfaces: [\"context\" as const] };\n\t}\n\n\tpublic async initialize(_sessionId: string, ctx: MemoryLifecycleContext): Promise<void> {\n\t\tthis.ctx = ctx;\n\t\tthis.memoryFilePath = join(ctx.agentDir, \"MEMORY.md\");\n\t\tthis.userFilePath = join(ctx.agentDir, \"USER.md\");\n\n\t\t// Ensure agentDir exists\n\t\tif (!existsSync(ctx.agentDir)) {\n\t\t\tmkdirSync(ctx.agentDir, { recursive: true });\n\t\t}\n\n\t\t// Initialize files if they do not exist\n\t\tif (!existsSync(this.memoryFilePath)) {\n\t\t\twriteFileSync(this.memoryFilePath, \"\", \"utf-8\");\n\t\t}\n\t\tif (!existsSync(this.userFilePath)) {\n\t\t\twriteFileSync(this.userFilePath, \"\", \"utf-8\");\n\t\t}\n\n\t\t// Load initial contents\n\t\tthis.lastWrittenMemory = await fs.readFile(this.memoryFilePath, \"utf-8\");\n\t\tthis.lastWrittenUser = await fs.readFile(this.userFilePath, \"utf-8\");\n\t}\n\n\tpublic systemPromptBlock(): string {\n\t\tconst sanitize = (content: string) => {\n\t\t\t// Strip hidden/bidi-control chars before injecting memory into the prompt (defence in depth: the\n\t\t\t// write path already blocks them, but a file edited out-of-band could carry them). Strip #31.\n\t\t\tconst lines = stripInvisibleUnicode(content).cleaned.split(\"\\n\");\n\t\t\tconst sanitizedLines = lines.map((line) => {\n\t\t\t\tconst threats = scanContextFileThreats(line);\n\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\treturn `[BLOCKED: potential threat detected (${threats.join(\", \")})]`;\n\t\t\t\t}\n\t\t\t\treturn line;\n\t\t\t});\n\t\t\treturn sanitizedLines.join(\"\\n\");\n\t\t};\n\n\t\t// Read-time budget guard (cost): the memory tool already caps writes at BUDGET_*, but a file edited\n\t\t// externally (or by any path that bypasses the tool) could be arbitrarily large and would then\n\t\t// bloat the system prompt on EVERY turn. Cap the injected view to the same budget so the per-turn\n\t\t// cost stays bounded; the file on disk is untouched and the model is told it was truncated.\n\t\tconst cap = (content: string, limit: number) => {\n\t\t\tif (content.length <= limit) return content;\n\t\t\treturn `${content.slice(0, limit)}\\n[…truncated to ${limit} chars for the prompt; full file is on disk]`;\n\t\t};\n\n\t\tconst mem = cap(sanitize(this.lastWrittenMemory), FileStoreProvider.BUDGET_MEMORY);\n\t\tconst usr = cap(sanitize(this.lastWrittenUser), FileStoreProvider.BUDGET_USER);\n\n\t\tconst blocks: string[] = [];\n\t\tif (mem.trim()) {\n\t\t\tblocks.push(`## MEMORY.md:\\n${mem}`);\n\t\t}\n\t\tif (usr.trim()) {\n\t\t\tblocks.push(`## USER.md:\\n${usr}`);\n\t\t}\n\n\t\tif (blocks.length === 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\treturn `=== Persistent Memory (file-store) ===\\n[System Note: Below is a snapshot of your persistent memory. You can update these using the 'memory' tool.]\\n\\n${blocks.join(\"\\n\\n\")}`;\n\t}\n\n\tpublic async prefetch(_query: string): Promise<string> {\n\t\t// static system prompt block is sufficient for file-store default; no-op prefetch\n\t\treturn \"\";\n\t}\n\n\tpublic async shutdown(): Promise<void> {\n\t\t// no-op\n\t}\n\n\tpublic getContextMarkers(): string[] {\n\t\treturn [];\n\t}\n\n\tpublic getToolDefinitions(): ToolDefinition[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tname: \"memory\",\n\t\t\t\tlabel: \"Persistent Memory Manager\",\n\t\t\t\tdescription: \"Add, replace, or remove contents in persistent memory files (MEMORY.md/USER.md).\",\n\t\t\t\tparameters: memorySchema,\n\t\t\t\texecute: async (_toolCallId, params: MemoryParams, _signal, _onUpdate, _execCtx) => {\n\t\t\t\t\tif (this.ctx?.isChildSession) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: \"Error: Writes to persistent memory are not allowed in child sessions (subagents).\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: \"Child session write-gated\" },\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\tconst { action, target, content, oldContent } = params;\n\n\t\t\t\t\t// Strict-scope injection guard on the high-privilege WRITE path (agy #31): a poisoned\n\t\t\t\t\t// memory entry persists across sessions and is injected into every future system prompt,\n\t\t\t\t\t// so block outright rather than strip. Hidden/bidi-control chars have no legitimate place\n\t\t\t\t\t// in a memory note, so reject those too.\n\t\t\t\t\tif ((action === \"add\" || action === \"replace\") && content !== undefined) {\n\t\t\t\t\t\tif (hasInvisibleUnicode(content)) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: \"Error: memory write rejected — contains hidden/bidirectional control characters.\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Invisible unicode in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst threats = scanContextFileThreats(content, \"strict\");\n\t\t\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: memory write rejected — potential injection/exfiltration detected (${threats.join(\", \")}).`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Threat in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst filePath = target === \"memory\" ? this.memoryFilePath : this.userFilePath;\n\t\t\t\t\tconst budget = target === \"memory\" ? FileStoreProvider.BUDGET_MEMORY : FileStoreProvider.BUDGET_USER;\n\n\t\t\t\t\tlet release: (() => Promise<void>) | undefined;\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// File lock\n\t\t\t\t\t\trelease = await lockfile.lock(filePath, { realpath: false, retries: 5 });\n\n\t\t\t\t\t\tconst lastWritten = target === \"memory\" ? this.lastWrittenMemory : this.lastWrittenUser;\n\t\t\t\t\t\t// Read current file content on disk for drift detection\n\t\t\t\t\t\tconst currentOnDisk = await fs.readFile(filePath, \"utf-8\");\n\t\t\t\t\t\tif (currentOnDisk !== lastWritten) {\n\t\t\t\t\t\t\t// Drift detected. Backup current file and refuse write.\n\t\t\t\t\t\t\tconst backupPath = `${filePath}.bak.${Date.now()}`;\n\t\t\t\t\t\t\tawait fs.writeFile(backupPath, currentOnDisk, \"utf-8\");\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Drift detected. The memory file has been modified out-of-band by an external process. A backup was created at ${backupPath}. Operation aborted.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Drift detected\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet newContent = currentOnDisk;\n\t\t\t\t\t\tif (action === \"add\") {\n\t\t\t\t\t\t\tif (content === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'content' is required for action 'add'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// R5: confront before write. If this fact is a near-duplicate of an existing line,\n\t\t\t\t\t\t\t// supersede it in place instead of appending a redundant copy (prevents append-rot).\n\t\t\t\t\t\t\tconst superseded = supersedeNearDuplicateLine(currentOnDisk, content);\n\t\t\t\t\t\t\tif (superseded !== null) {\n\t\t\t\t\t\t\t\tnewContent = superseded;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tnewContent =\n\t\t\t\t\t\t\t\t\tnewContent.endsWith(\"\\n\") || newContent === \"\"\n\t\t\t\t\t\t\t\t\t\t? `${newContent}${content}\\n`\n\t\t\t\t\t\t\t\t\t\t: `${newContent}\\n${content}\\n`;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (action === \"replace\") {\n\t\t\t\t\t\t\tif (content === undefined || oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameters 'content' and 'oldContent' are required for action 'replace'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to replace ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, content);\n\t\t\t\t\t\t} else if (action === \"remove\") {\n\t\t\t\t\t\t\tif (oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'oldContent' is required for action 'remove'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to remove ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, \"\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Budget check\n\t\t\t\t\t\tif (newContent.length > budget) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Memory budget exceeded. ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"} limit is ${budget} characters. Current operation would result in ${newContent.length} characters.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Memory budget exceeded\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Atomic write\n\t\t\t\t\t\tconst tmpPath = `${filePath}.tmp`;\n\t\t\t\t\t\tawait fs.writeFile(tmpPath, newContent, \"utf-8\");\n\t\t\t\t\t\tawait fs.rename(tmpPath, filePath);\n\n\t\t\t\t\t\t// Update in-memory tracker\n\t\t\t\t\t\tif (target === \"memory\") {\n\t\t\t\t\t\t\tthis.lastWrittenMemory = newContent;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.lastWrittenUser = newContent;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Successfully updated ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"}.`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: true },\n\t\t\t\t\t\t};\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Error: Failed to perform memory operation: ${String(err)}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: String(err) },\n\t\t\t\t\t\t};\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (release) {\n\t\t\t\t\t\t\tawait release();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../../../../src/core/memory/providers/file-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEpF;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsB3F;AAiBD,qBAAa,iBAAkB,YAAW,cAAc;IACvD,SAAgB,IAAI,gBAAgB;IACpC,SAAgB,MAAM,WAAW;IAEjC,OAAO,CAAC,GAAG,CAAC,CAAyB;IACrC,OAAO,CAAC,cAAc,CAAM;IAC5B,OAAO,CAAC,YAAY,CAAM;IAE1B,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,eAAe,CAAM;IAG7B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAQ;IAC7C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAQ;IAEpC,WAAW,IAAI,OAAO,CAE5B;IAEM,eAAe;;MAErB;IAEY,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBtF;IAEM,iBAAiB,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,MAAM,CA4C5D;IAEY,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGrD;IAEY,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAErC;IAEM,iBAAiB,IAAI,MAAM,EAAE,CAEnC;IAEM,kBAAkB,IAAI,cAAc,EAAE,CAoK5C;CACD","sourcesContent":["import { existsSync, promises as fs, mkdirSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { type Static, Type } from \"typebox\";\nimport type { MemoryPromptBudget } from \"../../context/memory-prompt-budget.ts\";\nimport type { ToolDefinition } from \"../../extensions/types.ts\";\nimport { hasInvisibleUnicode, scanContextFileThreats, stripInvisibleUnicode } from \"../../resource-loader.ts\";\nimport { jaccard, tokenize } from \"../../tools/skill-audit.ts\";\nimport type { MemoryLifecycleContext, MemoryProvider } from \"../memory-provider.ts\";\n\n/**\n * R5 confront-before-write (anti append-rot): if `content` is a near-duplicate of an existing\n * non-empty line (token Jaccard ≥ threshold — i.e. the same fact reworded), supersede that line in\n * place and return the rewritten file; otherwise return null (the caller appends normally).\n */\nexport function supersedeNearDuplicateLine(existing: string, content: string): string | null {\n\tconst NEAR_DUP_THRESHOLD = 0.6;\n\tconst contentTokens = tokenize(content);\n\tif (contentTokens.length === 0) return null;\n\tconst lines = existing.split(\"\\n\");\n\tlet bestIdx = -1;\n\tlet bestScore = NEAR_DUP_THRESHOLD;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\t\tif (!line) continue;\n\t\t// Never supersede structural Markdown (headings, list markers as headings) — a fact must not\n\t\t// silently overwrite section structure (Bug #15).\n\t\tif (line.startsWith(\"#\")) continue;\n\t\tconst score = jaccard(contentTokens, tokenize(line));\n\t\tif (score >= bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbestIdx = i;\n\t\t}\n\t}\n\tif (bestIdx === -1) return null;\n\tlines[bestIdx] = content;\n\treturn lines.join(\"\\n\");\n}\n\nconst memorySchema = Type.Object({\n\taction: Type.Union([Type.Literal(\"add\"), Type.Literal(\"replace\"), Type.Literal(\"remove\")], {\n\t\tdescription: \"Action to perform: add new content, replace existing content, or remove content\",\n\t}),\n\ttarget: Type.Union([Type.Literal(\"memory\"), Type.Literal(\"user\")], {\n\t\tdescription: \"Target file: 'memory' for MEMORY.md, 'user' for USER.md\",\n\t}),\n\tcontent: Type.Optional(Type.String({ description: \"Content to write (required for 'add' or 'replace')\" })),\n\toldContent: Type.Optional(\n\t\tType.String({ description: \"Exact substring to replace or remove (required for 'replace' or 'remove')\" }),\n\t),\n});\n\ntype MemoryParams = Static<typeof memorySchema>;\n\nexport class FileStoreProvider implements MemoryProvider {\n\tpublic readonly name = \"file-store\";\n\tpublic readonly egress = \"local\";\n\n\tprivate ctx?: MemoryLifecycleContext;\n\tprivate memoryFilePath = \"\";\n\tprivate userFilePath = \"\";\n\n\tprivate lastWrittenMemory = \"\";\n\tprivate lastWrittenUser = \"\";\n\n\t// Character budgets\n\tprivate static readonly BUDGET_MEMORY = 2200;\n\tprivate static readonly BUDGET_USER = 1375;\n\n\tpublic isAvailable(): boolean {\n\t\treturn true;\n\t}\n\n\tpublic getCapabilities() {\n\t\treturn { surfaces: [\"context\" as const] };\n\t}\n\n\tpublic async initialize(_sessionId: string, ctx: MemoryLifecycleContext): Promise<void> {\n\t\tthis.ctx = ctx;\n\t\tthis.memoryFilePath = join(ctx.agentDir, \"MEMORY.md\");\n\t\tthis.userFilePath = join(ctx.agentDir, \"USER.md\");\n\n\t\t// Ensure agentDir exists\n\t\tif (!existsSync(ctx.agentDir)) {\n\t\t\tmkdirSync(ctx.agentDir, { recursive: true });\n\t\t}\n\n\t\t// Initialize files if they do not exist\n\t\tif (!existsSync(this.memoryFilePath)) {\n\t\t\twriteFileSync(this.memoryFilePath, \"\", \"utf-8\");\n\t\t}\n\t\tif (!existsSync(this.userFilePath)) {\n\t\t\twriteFileSync(this.userFilePath, \"\", \"utf-8\");\n\t\t}\n\n\t\t// Load initial contents\n\t\tthis.lastWrittenMemory = await fs.readFile(this.memoryFilePath, \"utf-8\");\n\t\tthis.lastWrittenUser = await fs.readFile(this.userFilePath, \"utf-8\");\n\t}\n\n\tpublic systemPromptBlock(budget?: MemoryPromptBudget): string {\n\t\tconst sanitize = (content: string) => {\n\t\t\t// Strip hidden/bidi-control chars before injecting memory into the prompt (defence in depth: the\n\t\t\t// write path already blocks them, but a file edited out-of-band could carry them). Strip #31.\n\t\t\tconst lines = stripInvisibleUnicode(content).cleaned.split(\"\\n\");\n\t\t\tconst sanitizedLines = lines.map((line) => {\n\t\t\t\tconst threats = scanContextFileThreats(line);\n\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\treturn `[BLOCKED: potential threat detected (${threats.join(\", \")})]`;\n\t\t\t\t}\n\t\t\t\treturn line;\n\t\t\t});\n\t\t\treturn sanitizedLines.join(\"\\n\");\n\t\t};\n\n\t\t// Read-time budget guard (cost): the memory tool already caps writes at BUDGET_*, but a file edited\n\t\t// externally (or by any path that bypasses the tool) could be arbitrarily large and would then\n\t\t// bloat the system prompt on EVERY turn. Cap the injected view to the same budget so the per-turn\n\t\t// cost stays bounded; the file on disk is untouched and the model is told it was truncated.\n\t\tconst cap = (content: string, limit: number) => {\n\t\t\tif (content.length <= limit) return content;\n\t\t\treturn `${content.slice(0, limit)}\\n[…truncated to ${limit} chars for the prompt; full file is on disk]`;\n\t\t};\n\n\t\tconst mem = cap(sanitize(this.lastWrittenMemory), FileStoreProvider.BUDGET_MEMORY);\n\t\tconst usr = cap(sanitize(this.lastWrittenUser), FileStoreProvider.BUDGET_USER);\n\n\t\tconst blocks: string[] = [];\n\t\tif (mem.trim()) {\n\t\t\tblocks.push(`## MEMORY.md:\\n${mem}`);\n\t\t}\n\t\tif (usr.trim()) {\n\t\t\tblocks.push(`## USER.md:\\n${usr}`);\n\t\t}\n\n\t\tif (blocks.length === 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tconst block = `=== Persistent Memory (file-store) ===\\n[System Note: Below is a snapshot of your persistent memory. You can update these using the 'memory' tool.]\\n\\n${blocks.join(\"\\n\\n\")}`;\n\t\tif (budget?.compact && (block.split(\"\\n\").length > budget.maxLines || block.length > budget.maxChars)) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn block;\n\t}\n\n\tpublic async prefetch(_query: string): Promise<string> {\n\t\t// static system prompt block is sufficient for file-store default; no-op prefetch\n\t\treturn \"\";\n\t}\n\n\tpublic async shutdown(): Promise<void> {\n\t\t// no-op\n\t}\n\n\tpublic getContextMarkers(): string[] {\n\t\treturn [];\n\t}\n\n\tpublic getToolDefinitions(): ToolDefinition[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tname: \"memory\",\n\t\t\t\tlabel: \"Persistent Memory Manager\",\n\t\t\t\tdescription: \"Add, replace, or remove contents in persistent memory files (MEMORY.md/USER.md).\",\n\t\t\t\tparameters: memorySchema,\n\t\t\t\texecute: async (_toolCallId, params: MemoryParams, _signal, _onUpdate, _execCtx) => {\n\t\t\t\t\tif (this.ctx?.isChildSession) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: \"Error: Writes to persistent memory are not allowed in child sessions (subagents).\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: \"Child session write-gated\" },\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\tconst { action, target, content, oldContent } = params;\n\n\t\t\t\t\t// Strict-scope injection guard on the high-privilege WRITE path (agy #31): a poisoned\n\t\t\t\t\t// memory entry persists across sessions and is injected into every future system prompt,\n\t\t\t\t\t// so block outright rather than strip. Hidden/bidi-control chars have no legitimate place\n\t\t\t\t\t// in a memory note, so reject those too.\n\t\t\t\t\tif ((action === \"add\" || action === \"replace\") && content !== undefined) {\n\t\t\t\t\t\tif (hasInvisibleUnicode(content)) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: \"Error: memory write rejected — contains hidden/bidirectional control characters.\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Invisible unicode in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst threats = scanContextFileThreats(content, \"strict\");\n\t\t\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: memory write rejected — potential injection/exfiltration detected (${threats.join(\", \")}).`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Threat in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst filePath = target === \"memory\" ? this.memoryFilePath : this.userFilePath;\n\t\t\t\t\tconst budget = target === \"memory\" ? FileStoreProvider.BUDGET_MEMORY : FileStoreProvider.BUDGET_USER;\n\n\t\t\t\t\tlet release: (() => Promise<void>) | undefined;\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// File lock\n\t\t\t\t\t\trelease = await lockfile.lock(filePath, { realpath: false, retries: 5 });\n\n\t\t\t\t\t\tconst lastWritten = target === \"memory\" ? this.lastWrittenMemory : this.lastWrittenUser;\n\t\t\t\t\t\t// Read current file content on disk for drift detection\n\t\t\t\t\t\tconst currentOnDisk = await fs.readFile(filePath, \"utf-8\");\n\t\t\t\t\t\tif (currentOnDisk !== lastWritten) {\n\t\t\t\t\t\t\t// Drift detected. Backup current file and refuse write.\n\t\t\t\t\t\t\tconst backupPath = `${filePath}.bak.${Date.now()}`;\n\t\t\t\t\t\t\tawait fs.writeFile(backupPath, currentOnDisk, \"utf-8\");\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Drift detected. The memory file has been modified out-of-band by an external process. A backup was created at ${backupPath}. Operation aborted.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Drift detected\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet newContent = currentOnDisk;\n\t\t\t\t\t\tif (action === \"add\") {\n\t\t\t\t\t\t\tif (content === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'content' is required for action 'add'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// R5: confront before write. If this fact is a near-duplicate of an existing line,\n\t\t\t\t\t\t\t// supersede it in place instead of appending a redundant copy (prevents append-rot).\n\t\t\t\t\t\t\tconst superseded = supersedeNearDuplicateLine(currentOnDisk, content);\n\t\t\t\t\t\t\tif (superseded !== null) {\n\t\t\t\t\t\t\t\tnewContent = superseded;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tnewContent =\n\t\t\t\t\t\t\t\t\tnewContent.endsWith(\"\\n\") || newContent === \"\"\n\t\t\t\t\t\t\t\t\t\t? `${newContent}${content}\\n`\n\t\t\t\t\t\t\t\t\t\t: `${newContent}\\n${content}\\n`;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (action === \"replace\") {\n\t\t\t\t\t\t\tif (content === undefined || oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameters 'content' and 'oldContent' are required for action 'replace'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to replace ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, content);\n\t\t\t\t\t\t} else if (action === \"remove\") {\n\t\t\t\t\t\t\tif (oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'oldContent' is required for action 'remove'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to remove ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, \"\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Budget check\n\t\t\t\t\t\tif (newContent.length > budget) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Memory budget exceeded. ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"} limit is ${budget} characters. Current operation would result in ${newContent.length} characters.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Memory budget exceeded\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Atomic write\n\t\t\t\t\t\tconst tmpPath = `${filePath}.tmp`;\n\t\t\t\t\t\tawait fs.writeFile(tmpPath, newContent, \"utf-8\");\n\t\t\t\t\t\tawait fs.rename(tmpPath, filePath);\n\n\t\t\t\t\t\t// Update in-memory tracker\n\t\t\t\t\t\tif (target === \"memory\") {\n\t\t\t\t\t\t\tthis.lastWrittenMemory = newContent;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.lastWrittenUser = newContent;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Successfully updated ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"}.`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: true },\n\t\t\t\t\t\t};\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Error: Failed to perform memory operation: ${String(err)}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: String(err) },\n\t\t\t\t\t\t};\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (release) {\n\t\t\t\t\t\t\tawait release();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\t}\n}\n"]}
|
|
@@ -48,6 +48,7 @@ const memorySchema = Type.Object({
|
|
|
48
48
|
});
|
|
49
49
|
export class FileStoreProvider {
|
|
50
50
|
name = "file-store";
|
|
51
|
+
egress = "local";
|
|
51
52
|
ctx;
|
|
52
53
|
memoryFilePath = "";
|
|
53
54
|
userFilePath = "";
|
|
@@ -81,7 +82,7 @@ export class FileStoreProvider {
|
|
|
81
82
|
this.lastWrittenMemory = await fs.readFile(this.memoryFilePath, "utf-8");
|
|
82
83
|
this.lastWrittenUser = await fs.readFile(this.userFilePath, "utf-8");
|
|
83
84
|
}
|
|
84
|
-
systemPromptBlock() {
|
|
85
|
+
systemPromptBlock(budget) {
|
|
85
86
|
const sanitize = (content) => {
|
|
86
87
|
// Strip hidden/bidi-control chars before injecting memory into the prompt (defence in depth: the
|
|
87
88
|
// write path already blocks them, but a file edited out-of-band could carry them). Strip #31.
|
|
@@ -116,7 +117,11 @@ export class FileStoreProvider {
|
|
|
116
117
|
if (blocks.length === 0) {
|
|
117
118
|
return "";
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
const block = `=== Persistent Memory (file-store) ===\n[System Note: Below is a snapshot of your persistent memory. You can update these using the 'memory' tool.]\n\n${blocks.join("\n\n")}`;
|
|
121
|
+
if (budget?.compact && (block.split("\n").length > budget.maxLines || block.length > budget.maxChars)) {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
return block;
|
|
120
125
|
}
|
|
121
126
|
async prefetch(_query) {
|
|
122
127
|
// static system prompt block is sufficient for file-store default; no-op prefetch
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store.js","sourceRoot":"","sources":["../../../../src/core/memory/providers/file-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, promises as fs, mkdirSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { type Static, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../../extensions/types.ts\";\nimport { hasInvisibleUnicode, scanContextFileThreats, stripInvisibleUnicode } from \"../../resource-loader.ts\";\nimport { jaccard, tokenize } from \"../../tools/skill-audit.ts\";\nimport type { MemoryLifecycleContext, MemoryProvider } from \"../memory-provider.ts\";\n\n/**\n * R5 confront-before-write (anti append-rot): if `content` is a near-duplicate of an existing\n * non-empty line (token Jaccard ≥ threshold — i.e. the same fact reworded), supersede that line in\n * place and return the rewritten file; otherwise return null (the caller appends normally).\n */\nexport function supersedeNearDuplicateLine(existing: string, content: string): string | null {\n\tconst NEAR_DUP_THRESHOLD = 0.6;\n\tconst contentTokens = tokenize(content);\n\tif (contentTokens.length === 0) return null;\n\tconst lines = existing.split(\"\\n\");\n\tlet bestIdx = -1;\n\tlet bestScore = NEAR_DUP_THRESHOLD;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\t\tif (!line) continue;\n\t\t// Never supersede structural Markdown (headings, list markers as headings) — a fact must not\n\t\t// silently overwrite section structure (Bug #15).\n\t\tif (line.startsWith(\"#\")) continue;\n\t\tconst score = jaccard(contentTokens, tokenize(line));\n\t\tif (score >= bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbestIdx = i;\n\t\t}\n\t}\n\tif (bestIdx === -1) return null;\n\tlines[bestIdx] = content;\n\treturn lines.join(\"\\n\");\n}\n\nconst memorySchema = Type.Object({\n\taction: Type.Union([Type.Literal(\"add\"), Type.Literal(\"replace\"), Type.Literal(\"remove\")], {\n\t\tdescription: \"Action to perform: add new content, replace existing content, or remove content\",\n\t}),\n\ttarget: Type.Union([Type.Literal(\"memory\"), Type.Literal(\"user\")], {\n\t\tdescription: \"Target file: 'memory' for MEMORY.md, 'user' for USER.md\",\n\t}),\n\tcontent: Type.Optional(Type.String({ description: \"Content to write (required for 'add' or 'replace')\" })),\n\toldContent: Type.Optional(\n\t\tType.String({ description: \"Exact substring to replace or remove (required for 'replace' or 'remove')\" }),\n\t),\n});\n\ntype MemoryParams = Static<typeof memorySchema>;\n\nexport class FileStoreProvider implements MemoryProvider {\n\tpublic readonly name = \"file-store\";\n\n\tprivate ctx?: MemoryLifecycleContext;\n\tprivate memoryFilePath = \"\";\n\tprivate userFilePath = \"\";\n\n\tprivate lastWrittenMemory = \"\";\n\tprivate lastWrittenUser = \"\";\n\n\t// Character budgets\n\tprivate static readonly BUDGET_MEMORY = 2200;\n\tprivate static readonly BUDGET_USER = 1375;\n\n\tpublic isAvailable(): boolean {\n\t\treturn true;\n\t}\n\n\tpublic getCapabilities() {\n\t\treturn { surfaces: [\"context\" as const] };\n\t}\n\n\tpublic async initialize(_sessionId: string, ctx: MemoryLifecycleContext): Promise<void> {\n\t\tthis.ctx = ctx;\n\t\tthis.memoryFilePath = join(ctx.agentDir, \"MEMORY.md\");\n\t\tthis.userFilePath = join(ctx.agentDir, \"USER.md\");\n\n\t\t// Ensure agentDir exists\n\t\tif (!existsSync(ctx.agentDir)) {\n\t\t\tmkdirSync(ctx.agentDir, { recursive: true });\n\t\t}\n\n\t\t// Initialize files if they do not exist\n\t\tif (!existsSync(this.memoryFilePath)) {\n\t\t\twriteFileSync(this.memoryFilePath, \"\", \"utf-8\");\n\t\t}\n\t\tif (!existsSync(this.userFilePath)) {\n\t\t\twriteFileSync(this.userFilePath, \"\", \"utf-8\");\n\t\t}\n\n\t\t// Load initial contents\n\t\tthis.lastWrittenMemory = await fs.readFile(this.memoryFilePath, \"utf-8\");\n\t\tthis.lastWrittenUser = await fs.readFile(this.userFilePath, \"utf-8\");\n\t}\n\n\tpublic systemPromptBlock(): string {\n\t\tconst sanitize = (content: string) => {\n\t\t\t// Strip hidden/bidi-control chars before injecting memory into the prompt (defence in depth: the\n\t\t\t// write path already blocks them, but a file edited out-of-band could carry them). Strip #31.\n\t\t\tconst lines = stripInvisibleUnicode(content).cleaned.split(\"\\n\");\n\t\t\tconst sanitizedLines = lines.map((line) => {\n\t\t\t\tconst threats = scanContextFileThreats(line);\n\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\treturn `[BLOCKED: potential threat detected (${threats.join(\", \")})]`;\n\t\t\t\t}\n\t\t\t\treturn line;\n\t\t\t});\n\t\t\treturn sanitizedLines.join(\"\\n\");\n\t\t};\n\n\t\t// Read-time budget guard (cost): the memory tool already caps writes at BUDGET_*, but a file edited\n\t\t// externally (or by any path that bypasses the tool) could be arbitrarily large and would then\n\t\t// bloat the system prompt on EVERY turn. Cap the injected view to the same budget so the per-turn\n\t\t// cost stays bounded; the file on disk is untouched and the model is told it was truncated.\n\t\tconst cap = (content: string, limit: number) => {\n\t\t\tif (content.length <= limit) return content;\n\t\t\treturn `${content.slice(0, limit)}\\n[…truncated to ${limit} chars for the prompt; full file is on disk]`;\n\t\t};\n\n\t\tconst mem = cap(sanitize(this.lastWrittenMemory), FileStoreProvider.BUDGET_MEMORY);\n\t\tconst usr = cap(sanitize(this.lastWrittenUser), FileStoreProvider.BUDGET_USER);\n\n\t\tconst blocks: string[] = [];\n\t\tif (mem.trim()) {\n\t\t\tblocks.push(`## MEMORY.md:\\n${mem}`);\n\t\t}\n\t\tif (usr.trim()) {\n\t\t\tblocks.push(`## USER.md:\\n${usr}`);\n\t\t}\n\n\t\tif (blocks.length === 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\treturn `=== Persistent Memory (file-store) ===\\n[System Note: Below is a snapshot of your persistent memory. You can update these using the 'memory' tool.]\\n\\n${blocks.join(\"\\n\\n\")}`;\n\t}\n\n\tpublic async prefetch(_query: string): Promise<string> {\n\t\t// static system prompt block is sufficient for file-store default; no-op prefetch\n\t\treturn \"\";\n\t}\n\n\tpublic async shutdown(): Promise<void> {\n\t\t// no-op\n\t}\n\n\tpublic getContextMarkers(): string[] {\n\t\treturn [];\n\t}\n\n\tpublic getToolDefinitions(): ToolDefinition[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tname: \"memory\",\n\t\t\t\tlabel: \"Persistent Memory Manager\",\n\t\t\t\tdescription: \"Add, replace, or remove contents in persistent memory files (MEMORY.md/USER.md).\",\n\t\t\t\tparameters: memorySchema,\n\t\t\t\texecute: async (_toolCallId, params: MemoryParams, _signal, _onUpdate, _execCtx) => {\n\t\t\t\t\tif (this.ctx?.isChildSession) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: \"Error: Writes to persistent memory are not allowed in child sessions (subagents).\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: \"Child session write-gated\" },\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\tconst { action, target, content, oldContent } = params;\n\n\t\t\t\t\t// Strict-scope injection guard on the high-privilege WRITE path (agy #31): a poisoned\n\t\t\t\t\t// memory entry persists across sessions and is injected into every future system prompt,\n\t\t\t\t\t// so block outright rather than strip. Hidden/bidi-control chars have no legitimate place\n\t\t\t\t\t// in a memory note, so reject those too.\n\t\t\t\t\tif ((action === \"add\" || action === \"replace\") && content !== undefined) {\n\t\t\t\t\t\tif (hasInvisibleUnicode(content)) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: \"Error: memory write rejected — contains hidden/bidirectional control characters.\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Invisible unicode in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst threats = scanContextFileThreats(content, \"strict\");\n\t\t\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: memory write rejected — potential injection/exfiltration detected (${threats.join(\", \")}).`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Threat in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst filePath = target === \"memory\" ? this.memoryFilePath : this.userFilePath;\n\t\t\t\t\tconst budget = target === \"memory\" ? FileStoreProvider.BUDGET_MEMORY : FileStoreProvider.BUDGET_USER;\n\n\t\t\t\t\tlet release: (() => Promise<void>) | undefined;\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// File lock\n\t\t\t\t\t\trelease = await lockfile.lock(filePath, { realpath: false, retries: 5 });\n\n\t\t\t\t\t\tconst lastWritten = target === \"memory\" ? this.lastWrittenMemory : this.lastWrittenUser;\n\t\t\t\t\t\t// Read current file content on disk for drift detection\n\t\t\t\t\t\tconst currentOnDisk = await fs.readFile(filePath, \"utf-8\");\n\t\t\t\t\t\tif (currentOnDisk !== lastWritten) {\n\t\t\t\t\t\t\t// Drift detected. Backup current file and refuse write.\n\t\t\t\t\t\t\tconst backupPath = `${filePath}.bak.${Date.now()}`;\n\t\t\t\t\t\t\tawait fs.writeFile(backupPath, currentOnDisk, \"utf-8\");\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Drift detected. The memory file has been modified out-of-band by an external process. A backup was created at ${backupPath}. Operation aborted.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Drift detected\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet newContent = currentOnDisk;\n\t\t\t\t\t\tif (action === \"add\") {\n\t\t\t\t\t\t\tif (content === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'content' is required for action 'add'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// R5: confront before write. If this fact is a near-duplicate of an existing line,\n\t\t\t\t\t\t\t// supersede it in place instead of appending a redundant copy (prevents append-rot).\n\t\t\t\t\t\t\tconst superseded = supersedeNearDuplicateLine(currentOnDisk, content);\n\t\t\t\t\t\t\tif (superseded !== null) {\n\t\t\t\t\t\t\t\tnewContent = superseded;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tnewContent =\n\t\t\t\t\t\t\t\t\tnewContent.endsWith(\"\\n\") || newContent === \"\"\n\t\t\t\t\t\t\t\t\t\t? `${newContent}${content}\\n`\n\t\t\t\t\t\t\t\t\t\t: `${newContent}\\n${content}\\n`;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (action === \"replace\") {\n\t\t\t\t\t\t\tif (content === undefined || oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameters 'content' and 'oldContent' are required for action 'replace'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to replace ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, content);\n\t\t\t\t\t\t} else if (action === \"remove\") {\n\t\t\t\t\t\t\tif (oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'oldContent' is required for action 'remove'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to remove ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, \"\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Budget check\n\t\t\t\t\t\tif (newContent.length > budget) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Memory budget exceeded. ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"} limit is ${budget} characters. Current operation would result in ${newContent.length} characters.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Memory budget exceeded\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Atomic write\n\t\t\t\t\t\tconst tmpPath = `${filePath}.tmp`;\n\t\t\t\t\t\tawait fs.writeFile(tmpPath, newContent, \"utf-8\");\n\t\t\t\t\t\tawait fs.rename(tmpPath, filePath);\n\n\t\t\t\t\t\t// Update in-memory tracker\n\t\t\t\t\t\tif (target === \"memory\") {\n\t\t\t\t\t\t\tthis.lastWrittenMemory = newContent;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.lastWrittenUser = newContent;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Successfully updated ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"}.`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: true },\n\t\t\t\t\t\t};\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Error: Failed to perform memory operation: ${String(err)}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: String(err) },\n\t\t\t\t\t\t};\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (release) {\n\t\t\t\t\t\t\tawait release();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"file-store.js","sourceRoot":"","sources":["../../../../src/core/memory/providers/file-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, promises as fs, mkdirSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { type Static, Type } from \"typebox\";\nimport type { MemoryPromptBudget } from \"../../context/memory-prompt-budget.ts\";\nimport type { ToolDefinition } from \"../../extensions/types.ts\";\nimport { hasInvisibleUnicode, scanContextFileThreats, stripInvisibleUnicode } from \"../../resource-loader.ts\";\nimport { jaccard, tokenize } from \"../../tools/skill-audit.ts\";\nimport type { MemoryLifecycleContext, MemoryProvider } from \"../memory-provider.ts\";\n\n/**\n * R5 confront-before-write (anti append-rot): if `content` is a near-duplicate of an existing\n * non-empty line (token Jaccard ≥ threshold — i.e. the same fact reworded), supersede that line in\n * place and return the rewritten file; otherwise return null (the caller appends normally).\n */\nexport function supersedeNearDuplicateLine(existing: string, content: string): string | null {\n\tconst NEAR_DUP_THRESHOLD = 0.6;\n\tconst contentTokens = tokenize(content);\n\tif (contentTokens.length === 0) return null;\n\tconst lines = existing.split(\"\\n\");\n\tlet bestIdx = -1;\n\tlet bestScore = NEAR_DUP_THRESHOLD;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\t\tif (!line) continue;\n\t\t// Never supersede structural Markdown (headings, list markers as headings) — a fact must not\n\t\t// silently overwrite section structure (Bug #15).\n\t\tif (line.startsWith(\"#\")) continue;\n\t\tconst score = jaccard(contentTokens, tokenize(line));\n\t\tif (score >= bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbestIdx = i;\n\t\t}\n\t}\n\tif (bestIdx === -1) return null;\n\tlines[bestIdx] = content;\n\treturn lines.join(\"\\n\");\n}\n\nconst memorySchema = Type.Object({\n\taction: Type.Union([Type.Literal(\"add\"), Type.Literal(\"replace\"), Type.Literal(\"remove\")], {\n\t\tdescription: \"Action to perform: add new content, replace existing content, or remove content\",\n\t}),\n\ttarget: Type.Union([Type.Literal(\"memory\"), Type.Literal(\"user\")], {\n\t\tdescription: \"Target file: 'memory' for MEMORY.md, 'user' for USER.md\",\n\t}),\n\tcontent: Type.Optional(Type.String({ description: \"Content to write (required for 'add' or 'replace')\" })),\n\toldContent: Type.Optional(\n\t\tType.String({ description: \"Exact substring to replace or remove (required for 'replace' or 'remove')\" }),\n\t),\n});\n\ntype MemoryParams = Static<typeof memorySchema>;\n\nexport class FileStoreProvider implements MemoryProvider {\n\tpublic readonly name = \"file-store\";\n\tpublic readonly egress = \"local\";\n\n\tprivate ctx?: MemoryLifecycleContext;\n\tprivate memoryFilePath = \"\";\n\tprivate userFilePath = \"\";\n\n\tprivate lastWrittenMemory = \"\";\n\tprivate lastWrittenUser = \"\";\n\n\t// Character budgets\n\tprivate static readonly BUDGET_MEMORY = 2200;\n\tprivate static readonly BUDGET_USER = 1375;\n\n\tpublic isAvailable(): boolean {\n\t\treturn true;\n\t}\n\n\tpublic getCapabilities() {\n\t\treturn { surfaces: [\"context\" as const] };\n\t}\n\n\tpublic async initialize(_sessionId: string, ctx: MemoryLifecycleContext): Promise<void> {\n\t\tthis.ctx = ctx;\n\t\tthis.memoryFilePath = join(ctx.agentDir, \"MEMORY.md\");\n\t\tthis.userFilePath = join(ctx.agentDir, \"USER.md\");\n\n\t\t// Ensure agentDir exists\n\t\tif (!existsSync(ctx.agentDir)) {\n\t\t\tmkdirSync(ctx.agentDir, { recursive: true });\n\t\t}\n\n\t\t// Initialize files if they do not exist\n\t\tif (!existsSync(this.memoryFilePath)) {\n\t\t\twriteFileSync(this.memoryFilePath, \"\", \"utf-8\");\n\t\t}\n\t\tif (!existsSync(this.userFilePath)) {\n\t\t\twriteFileSync(this.userFilePath, \"\", \"utf-8\");\n\t\t}\n\n\t\t// Load initial contents\n\t\tthis.lastWrittenMemory = await fs.readFile(this.memoryFilePath, \"utf-8\");\n\t\tthis.lastWrittenUser = await fs.readFile(this.userFilePath, \"utf-8\");\n\t}\n\n\tpublic systemPromptBlock(budget?: MemoryPromptBudget): string {\n\t\tconst sanitize = (content: string) => {\n\t\t\t// Strip hidden/bidi-control chars before injecting memory into the prompt (defence in depth: the\n\t\t\t// write path already blocks them, but a file edited out-of-band could carry them). Strip #31.\n\t\t\tconst lines = stripInvisibleUnicode(content).cleaned.split(\"\\n\");\n\t\t\tconst sanitizedLines = lines.map((line) => {\n\t\t\t\tconst threats = scanContextFileThreats(line);\n\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\treturn `[BLOCKED: potential threat detected (${threats.join(\", \")})]`;\n\t\t\t\t}\n\t\t\t\treturn line;\n\t\t\t});\n\t\t\treturn sanitizedLines.join(\"\\n\");\n\t\t};\n\n\t\t// Read-time budget guard (cost): the memory tool already caps writes at BUDGET_*, but a file edited\n\t\t// externally (or by any path that bypasses the tool) could be arbitrarily large and would then\n\t\t// bloat the system prompt on EVERY turn. Cap the injected view to the same budget so the per-turn\n\t\t// cost stays bounded; the file on disk is untouched and the model is told it was truncated.\n\t\tconst cap = (content: string, limit: number) => {\n\t\t\tif (content.length <= limit) return content;\n\t\t\treturn `${content.slice(0, limit)}\\n[…truncated to ${limit} chars for the prompt; full file is on disk]`;\n\t\t};\n\n\t\tconst mem = cap(sanitize(this.lastWrittenMemory), FileStoreProvider.BUDGET_MEMORY);\n\t\tconst usr = cap(sanitize(this.lastWrittenUser), FileStoreProvider.BUDGET_USER);\n\n\t\tconst blocks: string[] = [];\n\t\tif (mem.trim()) {\n\t\t\tblocks.push(`## MEMORY.md:\\n${mem}`);\n\t\t}\n\t\tif (usr.trim()) {\n\t\t\tblocks.push(`## USER.md:\\n${usr}`);\n\t\t}\n\n\t\tif (blocks.length === 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tconst block = `=== Persistent Memory (file-store) ===\\n[System Note: Below is a snapshot of your persistent memory. You can update these using the 'memory' tool.]\\n\\n${blocks.join(\"\\n\\n\")}`;\n\t\tif (budget?.compact && (block.split(\"\\n\").length > budget.maxLines || block.length > budget.maxChars)) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn block;\n\t}\n\n\tpublic async prefetch(_query: string): Promise<string> {\n\t\t// static system prompt block is sufficient for file-store default; no-op prefetch\n\t\treturn \"\";\n\t}\n\n\tpublic async shutdown(): Promise<void> {\n\t\t// no-op\n\t}\n\n\tpublic getContextMarkers(): string[] {\n\t\treturn [];\n\t}\n\n\tpublic getToolDefinitions(): ToolDefinition[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tname: \"memory\",\n\t\t\t\tlabel: \"Persistent Memory Manager\",\n\t\t\t\tdescription: \"Add, replace, or remove contents in persistent memory files (MEMORY.md/USER.md).\",\n\t\t\t\tparameters: memorySchema,\n\t\t\t\texecute: async (_toolCallId, params: MemoryParams, _signal, _onUpdate, _execCtx) => {\n\t\t\t\t\tif (this.ctx?.isChildSession) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: \"Error: Writes to persistent memory are not allowed in child sessions (subagents).\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: \"Child session write-gated\" },\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\n\t\t\t\t\tconst { action, target, content, oldContent } = params;\n\n\t\t\t\t\t// Strict-scope injection guard on the high-privilege WRITE path (agy #31): a poisoned\n\t\t\t\t\t// memory entry persists across sessions and is injected into every future system prompt,\n\t\t\t\t\t// so block outright rather than strip. Hidden/bidi-control chars have no legitimate place\n\t\t\t\t\t// in a memory note, so reject those too.\n\t\t\t\t\tif ((action === \"add\" || action === \"replace\") && content !== undefined) {\n\t\t\t\t\t\tif (hasInvisibleUnicode(content)) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: \"Error: memory write rejected — contains hidden/bidirectional control characters.\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Invisible unicode in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst threats = scanContextFileThreats(content, \"strict\");\n\t\t\t\t\t\tif (threats.length > 0) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: memory write rejected — potential injection/exfiltration detected (${threats.join(\", \")}).`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Threat in memory write\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst filePath = target === \"memory\" ? this.memoryFilePath : this.userFilePath;\n\t\t\t\t\tconst budget = target === \"memory\" ? FileStoreProvider.BUDGET_MEMORY : FileStoreProvider.BUDGET_USER;\n\n\t\t\t\t\tlet release: (() => Promise<void>) | undefined;\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// File lock\n\t\t\t\t\t\trelease = await lockfile.lock(filePath, { realpath: false, retries: 5 });\n\n\t\t\t\t\t\tconst lastWritten = target === \"memory\" ? this.lastWrittenMemory : this.lastWrittenUser;\n\t\t\t\t\t\t// Read current file content on disk for drift detection\n\t\t\t\t\t\tconst currentOnDisk = await fs.readFile(filePath, \"utf-8\");\n\t\t\t\t\t\tif (currentOnDisk !== lastWritten) {\n\t\t\t\t\t\t\t// Drift detected. Backup current file and refuse write.\n\t\t\t\t\t\t\tconst backupPath = `${filePath}.bak.${Date.now()}`;\n\t\t\t\t\t\t\tawait fs.writeFile(backupPath, currentOnDisk, \"utf-8\");\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Drift detected. The memory file has been modified out-of-band by an external process. A backup was created at ${backupPath}. Operation aborted.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Drift detected\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet newContent = currentOnDisk;\n\t\t\t\t\t\tif (action === \"add\") {\n\t\t\t\t\t\t\tif (content === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'content' is required for action 'add'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// R5: confront before write. If this fact is a near-duplicate of an existing line,\n\t\t\t\t\t\t\t// supersede it in place instead of appending a redundant copy (prevents append-rot).\n\t\t\t\t\t\t\tconst superseded = supersedeNearDuplicateLine(currentOnDisk, content);\n\t\t\t\t\t\t\tif (superseded !== null) {\n\t\t\t\t\t\t\t\tnewContent = superseded;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tnewContent =\n\t\t\t\t\t\t\t\t\tnewContent.endsWith(\"\\n\") || newContent === \"\"\n\t\t\t\t\t\t\t\t\t\t? `${newContent}${content}\\n`\n\t\t\t\t\t\t\t\t\t\t: `${newContent}\\n${content}\\n`;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (action === \"replace\") {\n\t\t\t\t\t\t\tif (content === undefined || oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameters 'content' and 'oldContent' are required for action 'replace'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to replace ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, content);\n\t\t\t\t\t\t} else if (action === \"remove\") {\n\t\t\t\t\t\t\tif (oldContent === undefined) {\n\t\t\t\t\t\t\t\tthrow new Error(\"Parameter 'oldContent' is required for action 'remove'.\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (!currentOnDisk.includes(oldContent)) {\n\t\t\t\t\t\t\t\tthrow new Error(`The content to remove ('oldContent') was not found in the file.`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnewContent = currentOnDisk.replace(oldContent, \"\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Budget check\n\t\t\t\t\t\tif (newContent.length > budget) {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `Error: Memory budget exceeded. ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"} limit is ${budget} characters. Current operation would result in ${newContent.length} characters.`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\tdetails: { success: false, error: \"Memory budget exceeded\" },\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Atomic write\n\t\t\t\t\t\tconst tmpPath = `${filePath}.tmp`;\n\t\t\t\t\t\tawait fs.writeFile(tmpPath, newContent, \"utf-8\");\n\t\t\t\t\t\tawait fs.rename(tmpPath, filePath);\n\n\t\t\t\t\t\t// Update in-memory tracker\n\t\t\t\t\t\tif (target === \"memory\") {\n\t\t\t\t\t\t\tthis.lastWrittenMemory = newContent;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.lastWrittenUser = newContent;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Successfully updated ${target === \"memory\" ? \"MEMORY.md\" : \"USER.md\"}.`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: true },\n\t\t\t\t\t\t};\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `Error: Failed to perform memory operation: ${String(err)}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { success: false, error: String(err) },\n\t\t\t\t\t\t};\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (release) {\n\t\t\t\t\t\t\tawait release();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\t}\n}\n"]}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { MemoryCapabilities, MemoryLifecycleContext, MemoryProvider } from "../memory-provider.ts";
|
|
11
11
|
export declare class TranscriptRecallProvider implements MemoryProvider {
|
|
12
12
|
readonly name = "transcript-recall";
|
|
13
|
+
readonly egress = "local";
|
|
13
14
|
private index;
|
|
14
15
|
private currentSessionId;
|
|
15
16
|
private cwd;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript-recall.d.ts","sourceRoot":"","sources":["../../../../src/core/memory/providers/transcript-recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"transcript-recall.d.ts","sourceRoot":"","sources":["../../../../src/core/memory/providers/transcript-recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAaxG,qBAAa,wBAAyB,YAAW,cAAc;IAC9D,QAAQ,CAAC,IAAI,uBAAuB;IACpC,QAAQ,CAAC,MAAM,WAAW;IAC1B,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,gBAAgB,CAAM;IAC9B,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,QAAQ,CAAM;IAEtB,WAAW,IAAI,OAAO,CAErB;IAED,eAAe,IAAI,kBAAkB,CAEpC;IAEK,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9E;IAEK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAE9B;IAED,wFAAwF;IACxF,iBAAiB,IAAI,MAAM,EAAE,CAE5B;IAEK,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB7C;IAED,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,SAAS;CAsDjB","sourcesContent":["/**\n * TranscriptRecallProvider — cross-session similarity recall (adaptive-agent design R3).\n *\n * A read-only CONTEXT memory provider: it indexes the most-recent past session transcripts (the JSONL\n * corpus) with a dependency-free token/Jaccard index ({@link TranscriptIndex}, reusing skill_audit's\n * tokenizer) and answers `prefetch(query)` with a small `<memory_context>` recall page of the most\n * relevant past snippets. The current session and auto-learn sessions are excluded. It never writes —\n * the file-store remains the write target; this is the recall corpus.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport {\n\ttype FileEntry,\n\tgetDefaultSessionDir,\n\tisAutoLearnSessionId,\n\tloadEntriesFromFile,\n} from \"@caupulican/pi-agent-core/node\";\nimport type { MemoryCapabilities, MemoryLifecycleContext, MemoryProvider } from \"../memory-provider.ts\";\nimport { type TranscriptDoc, TranscriptIndex } from \"../transcript-index.ts\";\n\n/** Most-recent past sessions to consider. */\nconst MAX_SESSIONS = 60;\n/** Per-session text cap (keeps the index light and snippets relevant). */\nconst MAX_DOC_CHARS = 8_000;\n/** Overall corpus cap across all docs. */\nconst MAX_TOTAL_CHARS = 500_000;\n/** Skip transcript files larger than this before parsing them, so a huge log can't block/bloat the\n * first recalled turn (Bug #9). Far above a normal session; only pathological logs exceed it. */\nconst MAX_FILE_BYTES = 8_000_000;\n\nexport class TranscriptRecallProvider implements MemoryProvider {\n\treadonly name = \"transcript-recall\";\n\treadonly egress = \"local\";\n\tprivate index: TranscriptIndex | undefined;\n\tprivate currentSessionId = \"\";\n\tprivate cwd = \"\";\n\tprivate agentDir = \"\";\n\n\tisAvailable(): boolean {\n\t\treturn true;\n\t}\n\n\tgetCapabilities(): MemoryCapabilities {\n\t\treturn { surfaces: [\"context\"] };\n\t}\n\n\tasync initialize(sessionId: string, ctx: MemoryLifecycleContext): Promise<void> {\n\t\tthis.currentSessionId = sessionId;\n\t\tthis.cwd = ctx.cwd;\n\t\tthis.agentDir = ctx.agentDir;\n\t\tthis.index = undefined; // built lazily on first prefetch\n\t}\n\n\tasync shutdown(): Promise<void> {\n\t\tthis.index = undefined;\n\t}\n\n\t/** GC manages the dynamic recall page so stale pages pack while the newest are kept. */\n\tgetContextMarkers(): string[] {\n\t\treturn [\"<memory_context\"];\n\t}\n\n\tasync prefetch(query: string): Promise<string> {\n\t\tif (!query.trim()) return \"\";\n\t\tlet index: TranscriptIndex;\n\t\ttry {\n\t\t\tindex = this.ensureIndex();\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t\tif (index.size === 0) return \"\";\n\t\t// minScore is a query-CONTAINMENT threshold (fraction of the query's tokens present in the doc),\n\t\t// not Jaccard — so it is length-independent and recalls relevant long sessions. ~1/3 of query\n\t\t// terms must appear before a session is considered relevant.\n\t\tconst hits = index.query(query, { k: 3, minScore: 0.34, maxSnippetChars: 600 });\n\t\tif (hits.length === 0) return \"\";\n\t\t// MemoryManager centrally source-labels and fences every provider's complete recall page. Keep\n\t\t// this provider output raw here so the built-in path is wrapped exactly once like extensions.\n\t\tconst body = hits.map((h) => `- (${h.timestamp ?? \"earlier session\"}) ${h.snippet}`).join(\"\\n\");\n\t\treturn `<memory_context source=\"transcript-recall\">\\nRelevant context recalled from past sessions (read-only reference, untrusted, may be stale):\\n${body}\\n</memory_context>`;\n\t}\n\n\tprivate ensureIndex(): TranscriptIndex {\n\t\tif (!this.index) {\n\t\t\tthis.index = new TranscriptIndex(this.buildDocs());\n\t\t}\n\t\treturn this.index;\n\t}\n\n\tprivate buildDocs(): TranscriptDoc[] {\n\t\tconst docs: TranscriptDoc[] = [];\n\t\tlet dir: string;\n\t\ttry {\n\t\t\tdir = getDefaultSessionDir(this.cwd, this.agentDir);\n\t\t} catch {\n\t\t\treturn docs;\n\t\t}\n\n\t\tlet files: Array<{ path: string; mtime: number }>;\n\t\ttry {\n\t\t\tfiles = readdirSync(dir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => {\n\t\t\t\t\tconst path = join(dir, f);\n\t\t\t\t\tlet mtime = 0;\n\t\t\t\t\tlet size = 0;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst st = statSync(path);\n\t\t\t\t\t\tmtime = st.mtimeMs;\n\t\t\t\t\t\tsize = st.size;\n\t\t\t\t\t} catch {}\n\t\t\t\t\treturn { path, mtime, size };\n\t\t\t\t})\n\t\t\t\t.filter((f) => f.size > 0 && f.size <= MAX_FILE_BYTES) // skip oversize logs before parse (Bug #9)\n\t\t\t\t.sort((a, b) => b.mtime - a.mtime) // most-recent first\n\t\t\t\t.slice(0, MAX_SESSIONS);\n\t\t} catch {\n\t\t\treturn docs;\n\t\t}\n\n\t\tlet total = 0;\n\t\tfor (const { path } of files) {\n\t\t\tlet entries: FileEntry[];\n\t\t\ttry {\n\t\t\t\tentries = loadEntriesFromFile(path);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst header = entries.find((e): e is Extract<FileEntry, { type: \"session\" }> => e.type === \"session\");\n\t\t\tconst sessionId = header?.id;\n\t\t\tif (!sessionId || sessionId === this.currentSessionId || isAutoLearnSessionId(sessionId)) continue;\n\t\t\t// Privacy: only recall from sessions that ran in THIS working directory. A misplaced/copied\n\t\t\t// transcript with a different cwd must not leak across project boundaries (Bug #11).\n\t\t\tif (header?.cwd && resolve(header.cwd) !== resolve(this.cwd)) continue;\n\n\t\t\tconst text = extractSessionText(entries, MAX_DOC_CHARS);\n\t\t\tif (!text.trim()) continue;\n\t\t\tdocs.push({ sessionId, timestamp: header?.timestamp, text });\n\t\t\ttotal += text.length;\n\t\t\tif (total >= MAX_TOTAL_CHARS) break;\n\t\t}\n\t\treturn docs;\n\t}\n}\n\n/** Concatenate user+assistant text from a session's entries, capped to `maxChars`. */\nfunction extractSessionText(entries: FileEntry[], maxChars: number): string {\n\tconst parts: string[] = [];\n\tlet len = 0;\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst message = entry.message;\n\t\tif (message.role !== \"user\" && message.role !== \"assistant\") continue;\n\t\tconst content = message.content;\n\t\tlet text = \"\";\n\t\tif (typeof content === \"string\") {\n\t\t\ttext = content;\n\t\t} else if (Array.isArray(content)) {\n\t\t\ttext = content\n\t\t\t\t.map((b) => (b && typeof b === \"object\" && \"type\" in b && b.type === \"text\" ? (b.text ?? \"\") : \"\"))\n\t\t\t\t.join(\" \");\n\t\t}\n\t\ttext = text.trim();\n\t\tif (!text) continue;\n\t\t// Skip our own previously-injected recall pages so recalled snippets don't recirculate and\n\t\t// amplify across sessions (Bug #10).\n\t\tif (text.includes('<memory_context source=\"transcript-recall\"')) continue;\n\t\tparts.push(text);\n\t\tlen += text.length;\n\t\tif (len >= maxChars) break;\n\t}\n\treturn parts.join(\"\\n\").slice(0, maxChars);\n}\n"]}
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
import { readdirSync, statSync } from "node:fs";
|
|
11
11
|
import { join, resolve } from "node:path";
|
|
12
12
|
import { getDefaultSessionDir, isAutoLearnSessionId, loadEntriesFromFile, } from "@caupulican/pi-agent-core/node";
|
|
13
|
-
import { wrapUntrustedText } from "../../security/untrusted-boundary.js";
|
|
14
13
|
import { TranscriptIndex } from "../transcript-index.js";
|
|
15
14
|
/** Most-recent past sessions to consider. */
|
|
16
15
|
const MAX_SESSIONS = 60;
|
|
@@ -23,6 +22,7 @@ const MAX_TOTAL_CHARS = 500_000;
|
|
|
23
22
|
const MAX_FILE_BYTES = 8_000_000;
|
|
24
23
|
export class TranscriptRecallProvider {
|
|
25
24
|
name = "transcript-recall";
|
|
25
|
+
egress = "local";
|
|
26
26
|
index;
|
|
27
27
|
currentSessionId = "";
|
|
28
28
|
cwd = "";
|
|
@@ -64,12 +64,9 @@ export class TranscriptRecallProvider {
|
|
|
64
64
|
const hits = index.query(query, { k: 3, minScore: 0.34, maxSnippetChars: 600 });
|
|
65
65
|
if (hits.length === 0)
|
|
66
66
|
return "";
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
const body = hits
|
|
71
|
-
.map((h) => `- (${h.timestamp ?? "earlier session"}) ${wrapUntrustedText(h.snippet, "transcript-recall")}`)
|
|
72
|
-
.join("\n");
|
|
67
|
+
// MemoryManager centrally source-labels and fences every provider's complete recall page. Keep
|
|
68
|
+
// this provider output raw here so the built-in path is wrapped exactly once like extensions.
|
|
69
|
+
const body = hits.map((h) => `- (${h.timestamp ?? "earlier session"}) ${h.snippet}`).join("\n");
|
|
73
70
|
return `<memory_context source="transcript-recall">\nRelevant context recalled from past sessions (read-only reference, untrusted, may be stale):\n${body}\n</memory_context>`;
|
|
74
71
|
}
|
|
75
72
|
ensureIndex() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript-recall.js","sourceRoot":"","sources":["../../../../src/core/memory/providers/transcript-recall.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TranscriptRecallProvider — cross-session similarity recall (adaptive-agent design R3).\n *\n * A read-only CONTEXT memory provider: it indexes the most-recent past session transcripts (the JSONL\n * corpus) with a dependency-free token/Jaccard index ({@link TranscriptIndex}, reusing skill_audit's\n * tokenizer) and answers `prefetch(query)` with a small `<memory_context>` recall page of the most\n * relevant past snippets. The current session and auto-learn sessions are excluded. It never writes —\n * the file-store remains the write target; this is the recall corpus.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport {\n\ttype FileEntry,\n\tgetDefaultSessionDir,\n\tisAutoLearnSessionId,\n\tloadEntriesFromFile,\n} from \"@caupulican/pi-agent-core/node\";\nimport { wrapUntrustedText } from \"../../security/untrusted-boundary.ts\";\nimport type { MemoryCapabilities, MemoryLifecycleContext, MemoryProvider } from \"../memory-provider.ts\";\nimport { type TranscriptDoc, TranscriptIndex } from \"../transcript-index.ts\";\n\n/** Most-recent past sessions to consider. */\nconst MAX_SESSIONS = 60;\n/** Per-session text cap (keeps the index light and snippets relevant). */\nconst MAX_DOC_CHARS = 8_000;\n/** Overall corpus cap across all docs. */\nconst MAX_TOTAL_CHARS = 500_000;\n/** Skip transcript files larger than this before parsing them, so a huge log can't block/bloat the\n * first recalled turn (Bug #9). Far above a normal session; only pathological logs exceed it. */\nconst MAX_FILE_BYTES = 8_000_000;\n\nexport class TranscriptRecallProvider implements MemoryProvider {\n\treadonly name = \"transcript-recall\";\n\tprivate index: TranscriptIndex | undefined;\n\tprivate currentSessionId = \"\";\n\tprivate cwd = \"\";\n\tprivate agentDir = \"\";\n\n\tisAvailable(): boolean {\n\t\treturn true;\n\t}\n\n\tgetCapabilities(): MemoryCapabilities {\n\t\treturn { surfaces: [\"context\"] };\n\t}\n\n\tasync initialize(sessionId: string, ctx: MemoryLifecycleContext): Promise<void> {\n\t\tthis.currentSessionId = sessionId;\n\t\tthis.cwd = ctx.cwd;\n\t\tthis.agentDir = ctx.agentDir;\n\t\tthis.index = undefined; // built lazily on first prefetch\n\t}\n\n\tasync shutdown(): Promise<void> {\n\t\tthis.index = undefined;\n\t}\n\n\t/** GC manages the dynamic recall page so stale pages pack while the newest are kept. */\n\tgetContextMarkers(): string[] {\n\t\treturn [\"<memory_context\"];\n\t}\n\n\tasync prefetch(query: string): Promise<string> {\n\t\tif (!query.trim()) return \"\";\n\t\tlet index: TranscriptIndex;\n\t\ttry {\n\t\t\tindex = this.ensureIndex();\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t\tif (index.size === 0) return \"\";\n\t\t// minScore is a query-CONTAINMENT threshold (fraction of the query's tokens present in the doc),\n\t\t// not Jaccard — so it is length-independent and recalls relevant long sessions. ~1/3 of query\n\t\t// terms must appear before a session is considered relevant.\n\t\tconst hits = index.query(query, { k: 3, minScore: 0.34, maxSnippetChars: 600 });\n\t\tif (hits.length === 0) return \"\";\n\t\t// Recalled past text is UNTRUSTED (it may itself contain injected instructions or a forged\n\t\t// `</memory_context>` to break out). Fence each snippet with the untrusted-content boundary so a\n\t\t// payload can't escape and be replayed as a current instruction (design: recall = untrusted).\n\t\tconst body = hits\n\t\t\t.map((h) => `- (${h.timestamp ?? \"earlier session\"}) ${wrapUntrustedText(h.snippet, \"transcript-recall\")}`)\n\t\t\t.join(\"\\n\");\n\t\treturn `<memory_context source=\"transcript-recall\">\\nRelevant context recalled from past sessions (read-only reference, untrusted, may be stale):\\n${body}\\n</memory_context>`;\n\t}\n\n\tprivate ensureIndex(): TranscriptIndex {\n\t\tif (!this.index) {\n\t\t\tthis.index = new TranscriptIndex(this.buildDocs());\n\t\t}\n\t\treturn this.index;\n\t}\n\n\tprivate buildDocs(): TranscriptDoc[] {\n\t\tconst docs: TranscriptDoc[] = [];\n\t\tlet dir: string;\n\t\ttry {\n\t\t\tdir = getDefaultSessionDir(this.cwd, this.agentDir);\n\t\t} catch {\n\t\t\treturn docs;\n\t\t}\n\n\t\tlet files: Array<{ path: string; mtime: number }>;\n\t\ttry {\n\t\t\tfiles = readdirSync(dir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => {\n\t\t\t\t\tconst path = join(dir, f);\n\t\t\t\t\tlet mtime = 0;\n\t\t\t\t\tlet size = 0;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst st = statSync(path);\n\t\t\t\t\t\tmtime = st.mtimeMs;\n\t\t\t\t\t\tsize = st.size;\n\t\t\t\t\t} catch {}\n\t\t\t\t\treturn { path, mtime, size };\n\t\t\t\t})\n\t\t\t\t.filter((f) => f.size > 0 && f.size <= MAX_FILE_BYTES) // skip oversize logs before parse (Bug #9)\n\t\t\t\t.sort((a, b) => b.mtime - a.mtime) // most-recent first\n\t\t\t\t.slice(0, MAX_SESSIONS);\n\t\t} catch {\n\t\t\treturn docs;\n\t\t}\n\n\t\tlet total = 0;\n\t\tfor (const { path } of files) {\n\t\t\tlet entries: FileEntry[];\n\t\t\ttry {\n\t\t\t\tentries = loadEntriesFromFile(path);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst header = entries.find((e): e is Extract<FileEntry, { type: \"session\" }> => e.type === \"session\");\n\t\t\tconst sessionId = header?.id;\n\t\t\tif (!sessionId || sessionId === this.currentSessionId || isAutoLearnSessionId(sessionId)) continue;\n\t\t\t// Privacy: only recall from sessions that ran in THIS working directory. A misplaced/copied\n\t\t\t// transcript with a different cwd must not leak across project boundaries (Bug #11).\n\t\t\tif (header?.cwd && resolve(header.cwd) !== resolve(this.cwd)) continue;\n\n\t\t\tconst text = extractSessionText(entries, MAX_DOC_CHARS);\n\t\t\tif (!text.trim()) continue;\n\t\t\tdocs.push({ sessionId, timestamp: header?.timestamp, text });\n\t\t\ttotal += text.length;\n\t\t\tif (total >= MAX_TOTAL_CHARS) break;\n\t\t}\n\t\treturn docs;\n\t}\n}\n\n/** Concatenate user+assistant text from a session's entries, capped to `maxChars`. */\nfunction extractSessionText(entries: FileEntry[], maxChars: number): string {\n\tconst parts: string[] = [];\n\tlet len = 0;\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst message = entry.message;\n\t\tif (message.role !== \"user\" && message.role !== \"assistant\") continue;\n\t\tconst content = message.content;\n\t\tlet text = \"\";\n\t\tif (typeof content === \"string\") {\n\t\t\ttext = content;\n\t\t} else if (Array.isArray(content)) {\n\t\t\ttext = content\n\t\t\t\t.map((b) => (b && typeof b === \"object\" && \"type\" in b && b.type === \"text\" ? (b.text ?? \"\") : \"\"))\n\t\t\t\t.join(\" \");\n\t\t}\n\t\ttext = text.trim();\n\t\tif (!text) continue;\n\t\t// Skip our own previously-injected recall pages so recalled snippets don't recirculate and\n\t\t// amplify across sessions (Bug #10).\n\t\tif (text.includes('<memory_context source=\"transcript-recall\"')) continue;\n\t\tparts.push(text);\n\t\tlen += text.length;\n\t\tif (len >= maxChars) break;\n\t}\n\treturn parts.join(\"\\n\").slice(0, maxChars);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"transcript-recall.js","sourceRoot":"","sources":["../../../../src/core/memory/providers/transcript-recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAEN,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,GACnB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAsB,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE7E,6CAA6C;AAC7C,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,0EAA0E;AAC1E,MAAM,aAAa,GAAG,KAAK,CAAC;AAC5B,0CAA0C;AAC1C,MAAM,eAAe,GAAG,OAAO,CAAC;AAChC;iGACiG;AACjG,MAAM,cAAc,GAAG,SAAS,CAAC;AAEjC,MAAM,OAAO,wBAAwB;IAC3B,IAAI,GAAG,mBAAmB,CAAC;IAC3B,MAAM,GAAG,OAAO,CAAC;IAClB,KAAK,CAA8B;IACnC,gBAAgB,GAAG,EAAE,CAAC;IACtB,GAAG,GAAG,EAAE,CAAC;IACT,QAAQ,GAAG,EAAE,CAAC;IAEtB,WAAW,GAAY;QACtB,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,eAAe,GAAuB;QACrC,OAAO,EAAE,QAAQ,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;IAAA,CACjC;IAED,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,GAA2B,EAAiB;QAC/E,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;QAClC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,iCAAiC;IAAlC,CACvB;IAED,KAAK,CAAC,QAAQ,GAAkB;QAC/B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;IAAA,CACvB;IAED,wFAAwF;IACxF,iBAAiB,GAAa;QAC7B,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAAA,CAC3B;IAED,KAAK,CAAC,QAAQ,CAAC,KAAa,EAAmB;QAC9C,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAsB,CAAC;QAC3B,IAAI,CAAC;YACJ,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,CAAC;QACX,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAChC,iGAAiG;QACjG,gGAA8F;QAC9F,6DAA6D;QAC7D,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC;QAChF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACjC,+FAA+F;QAC/F,8FAA8F;QAC9F,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,SAAS,IAAI,iBAAiB,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChG,OAAO,8IAA8I,IAAI,qBAAqB,CAAC;IAAA,CAC/K;IAEO,WAAW,GAAoB;QACtC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACjB,IAAI,CAAC,KAAK,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAEO,SAAS,GAAoB;QACpC,MAAM,IAAI,GAAoB,EAAE,CAAC;QACjC,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACJ,GAAG,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,KAA6C,CAAC;QAClD,IAAI,CAAC;YACJ,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC;iBACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;iBACnC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACX,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBAC1B,IAAI,KAAK,GAAG,CAAC,CAAC;gBACd,IAAI,IAAI,GAAG,CAAC,CAAC;gBACb,IAAI,CAAC;oBACJ,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;oBAC1B,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC;oBACnB,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC;gBAChB,CAAC;gBAAC,MAAM,CAAC,CAAA,CAAC;gBACV,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;YAAA,CAC7B,CAAC;iBACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,cAAc,CAAC,CAAC,2CAA2C;iBACjG,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,oBAAoB;iBACtD,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,EAAE,CAAC;YAC9B,IAAI,OAAoB,CAAC;YACzB,IAAI,CAAC;gBACJ,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACR,SAAS;YACV,CAAC;YACD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAgD,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;YACvG,MAAM,SAAS,GAAG,MAAM,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC,gBAAgB,IAAI,oBAAoB,CAAC,SAAS,CAAC;gBAAE,SAAS;YACnG,4FAA4F;YAC5F,qFAAqF;YACrF,IAAI,MAAM,EAAE,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,SAAS;YAEvE,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;YACxD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,SAAS;YAC3B,IAAI,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7D,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC;YACrB,IAAI,KAAK,IAAI,eAAe;gBAAE,MAAM;QACrC,CAAC;QACD,OAAO,IAAI,CAAC;IAAA,CACZ;CACD;AAED,sFAAsF;AACtF,SAAS,kBAAkB,CAAC,OAAoB,EAAE,QAAgB,EAAU;IAC3E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QACvC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC9B,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW;YAAE,SAAS;QACtE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YACjC,IAAI,GAAG,OAAO,CAAC;QAChB,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACnC,IAAI,GAAG,OAAO;iBACZ,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;iBAClG,IAAI,CAAC,GAAG,CAAC,CAAC;QACb,CAAC;QACD,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,2FAA2F;QAC3F,qCAAqC;QACrC,IAAI,IAAI,CAAC,QAAQ,CAAC,4CAA4C,CAAC;YAAE,SAAS;QAC1E,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC;QACnB,IAAI,GAAG,IAAI,QAAQ;YAAE,MAAM;IAC5B,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,CAC3C","sourcesContent":["/**\n * TranscriptRecallProvider — cross-session similarity recall (adaptive-agent design R3).\n *\n * A read-only CONTEXT memory provider: it indexes the most-recent past session transcripts (the JSONL\n * corpus) with a dependency-free token/Jaccard index ({@link TranscriptIndex}, reusing skill_audit's\n * tokenizer) and answers `prefetch(query)` with a small `<memory_context>` recall page of the most\n * relevant past snippets. The current session and auto-learn sessions are excluded. It never writes —\n * the file-store remains the write target; this is the recall corpus.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport {\n\ttype FileEntry,\n\tgetDefaultSessionDir,\n\tisAutoLearnSessionId,\n\tloadEntriesFromFile,\n} from \"@caupulican/pi-agent-core/node\";\nimport type { MemoryCapabilities, MemoryLifecycleContext, MemoryProvider } from \"../memory-provider.ts\";\nimport { type TranscriptDoc, TranscriptIndex } from \"../transcript-index.ts\";\n\n/** Most-recent past sessions to consider. */\nconst MAX_SESSIONS = 60;\n/** Per-session text cap (keeps the index light and snippets relevant). */\nconst MAX_DOC_CHARS = 8_000;\n/** Overall corpus cap across all docs. */\nconst MAX_TOTAL_CHARS = 500_000;\n/** Skip transcript files larger than this before parsing them, so a huge log can't block/bloat the\n * first recalled turn (Bug #9). Far above a normal session; only pathological logs exceed it. */\nconst MAX_FILE_BYTES = 8_000_000;\n\nexport class TranscriptRecallProvider implements MemoryProvider {\n\treadonly name = \"transcript-recall\";\n\treadonly egress = \"local\";\n\tprivate index: TranscriptIndex | undefined;\n\tprivate currentSessionId = \"\";\n\tprivate cwd = \"\";\n\tprivate agentDir = \"\";\n\n\tisAvailable(): boolean {\n\t\treturn true;\n\t}\n\n\tgetCapabilities(): MemoryCapabilities {\n\t\treturn { surfaces: [\"context\"] };\n\t}\n\n\tasync initialize(sessionId: string, ctx: MemoryLifecycleContext): Promise<void> {\n\t\tthis.currentSessionId = sessionId;\n\t\tthis.cwd = ctx.cwd;\n\t\tthis.agentDir = ctx.agentDir;\n\t\tthis.index = undefined; // built lazily on first prefetch\n\t}\n\n\tasync shutdown(): Promise<void> {\n\t\tthis.index = undefined;\n\t}\n\n\t/** GC manages the dynamic recall page so stale pages pack while the newest are kept. */\n\tgetContextMarkers(): string[] {\n\t\treturn [\"<memory_context\"];\n\t}\n\n\tasync prefetch(query: string): Promise<string> {\n\t\tif (!query.trim()) return \"\";\n\t\tlet index: TranscriptIndex;\n\t\ttry {\n\t\t\tindex = this.ensureIndex();\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t\tif (index.size === 0) return \"\";\n\t\t// minScore is a query-CONTAINMENT threshold (fraction of the query's tokens present in the doc),\n\t\t// not Jaccard — so it is length-independent and recalls relevant long sessions. ~1/3 of query\n\t\t// terms must appear before a session is considered relevant.\n\t\tconst hits = index.query(query, { k: 3, minScore: 0.34, maxSnippetChars: 600 });\n\t\tif (hits.length === 0) return \"\";\n\t\t// MemoryManager centrally source-labels and fences every provider's complete recall page. Keep\n\t\t// this provider output raw here so the built-in path is wrapped exactly once like extensions.\n\t\tconst body = hits.map((h) => `- (${h.timestamp ?? \"earlier session\"}) ${h.snippet}`).join(\"\\n\");\n\t\treturn `<memory_context source=\"transcript-recall\">\\nRelevant context recalled from past sessions (read-only reference, untrusted, may be stale):\\n${body}\\n</memory_context>`;\n\t}\n\n\tprivate ensureIndex(): TranscriptIndex {\n\t\tif (!this.index) {\n\t\t\tthis.index = new TranscriptIndex(this.buildDocs());\n\t\t}\n\t\treturn this.index;\n\t}\n\n\tprivate buildDocs(): TranscriptDoc[] {\n\t\tconst docs: TranscriptDoc[] = [];\n\t\tlet dir: string;\n\t\ttry {\n\t\t\tdir = getDefaultSessionDir(this.cwd, this.agentDir);\n\t\t} catch {\n\t\t\treturn docs;\n\t\t}\n\n\t\tlet files: Array<{ path: string; mtime: number }>;\n\t\ttry {\n\t\t\tfiles = readdirSync(dir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => {\n\t\t\t\t\tconst path = join(dir, f);\n\t\t\t\t\tlet mtime = 0;\n\t\t\t\t\tlet size = 0;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst st = statSync(path);\n\t\t\t\t\t\tmtime = st.mtimeMs;\n\t\t\t\t\t\tsize = st.size;\n\t\t\t\t\t} catch {}\n\t\t\t\t\treturn { path, mtime, size };\n\t\t\t\t})\n\t\t\t\t.filter((f) => f.size > 0 && f.size <= MAX_FILE_BYTES) // skip oversize logs before parse (Bug #9)\n\t\t\t\t.sort((a, b) => b.mtime - a.mtime) // most-recent first\n\t\t\t\t.slice(0, MAX_SESSIONS);\n\t\t} catch {\n\t\t\treturn docs;\n\t\t}\n\n\t\tlet total = 0;\n\t\tfor (const { path } of files) {\n\t\t\tlet entries: FileEntry[];\n\t\t\ttry {\n\t\t\t\tentries = loadEntriesFromFile(path);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst header = entries.find((e): e is Extract<FileEntry, { type: \"session\" }> => e.type === \"session\");\n\t\t\tconst sessionId = header?.id;\n\t\t\tif (!sessionId || sessionId === this.currentSessionId || isAutoLearnSessionId(sessionId)) continue;\n\t\t\t// Privacy: only recall from sessions that ran in THIS working directory. A misplaced/copied\n\t\t\t// transcript with a different cwd must not leak across project boundaries (Bug #11).\n\t\t\tif (header?.cwd && resolve(header.cwd) !== resolve(this.cwd)) continue;\n\n\t\t\tconst text = extractSessionText(entries, MAX_DOC_CHARS);\n\t\t\tif (!text.trim()) continue;\n\t\t\tdocs.push({ sessionId, timestamp: header?.timestamp, text });\n\t\t\ttotal += text.length;\n\t\t\tif (total >= MAX_TOTAL_CHARS) break;\n\t\t}\n\t\treturn docs;\n\t}\n}\n\n/** Concatenate user+assistant text from a session's entries, capped to `maxChars`. */\nfunction extractSessionText(entries: FileEntry[], maxChars: number): string {\n\tconst parts: string[] = [];\n\tlet len = 0;\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tconst message = entry.message;\n\t\tif (message.role !== \"user\" && message.role !== \"assistant\") continue;\n\t\tconst content = message.content;\n\t\tlet text = \"\";\n\t\tif (typeof content === \"string\") {\n\t\t\ttext = content;\n\t\t} else if (Array.isArray(content)) {\n\t\t\ttext = content\n\t\t\t\t.map((b) => (b && typeof b === \"object\" && \"type\" in b && b.type === \"text\" ? (b.text ?? \"\") : \"\"))\n\t\t\t\t.join(\" \");\n\t\t}\n\t\ttext = text.trim();\n\t\tif (!text) continue;\n\t\t// Skip our own previously-injected recall pages so recalled snippets don't recirculate and\n\t\t// amplify across sessions (Bug #10).\n\t\tif (text.includes('<memory_context source=\"transcript-recall\"')) continue;\n\t\tparts.push(text);\n\t\tlen += text.length;\n\t\tif (len >= maxChars) break;\n\t}\n\treturn parts.join(\"\\n\").slice(0, maxChars);\n}\n"]}
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { AgentMessage } from "@caupulican/pi-agent-core";
|
|
21
21
|
import { type MemoryPromptInclusionReport, type MemoryRetrievalDiagnostics } from "./context/memory-diagnostics.ts";
|
|
22
|
+
import { type MemoryProvider as ContextMemoryProvider } from "./context/memory-provider-contract.ts";
|
|
22
23
|
import { type MemoryRetrievalReport } from "./context/memory-retrieval.ts";
|
|
24
|
+
import type { GoalState } from "./goals/goal-state.ts";
|
|
23
25
|
import { MemoryManager } from "./memory/memory-manager.ts";
|
|
24
26
|
import type { MemoryProvider } from "./memory/memory-provider.ts";
|
|
25
27
|
import type { SettingsManager } from "./settings-manager.ts";
|
|
26
28
|
export interface MemoryControllerDeps {
|
|
27
|
-
/** Memory-retrieval + prompt-inclusion settings (
|
|
29
|
+
/** Memory-retrieval + prompt-inclusion settings (default-on gates for retrieval and surfacing). */
|
|
28
30
|
getSettingsManager(): SettingsManager;
|
|
29
31
|
/** Current turn index, stamped into a retrieval request's `createdAtTurn`. */
|
|
30
32
|
getTurnIndex(): number;
|
|
@@ -38,9 +40,21 @@ export interface MemoryControllerDeps {
|
|
|
38
40
|
isChildSession(): boolean;
|
|
39
41
|
/** Re-derive the tool registry after (re)init so the newly-surfaced memory tools take effect. */
|
|
40
42
|
refreshToolRegistry(): void;
|
|
43
|
+
/** Active model context window, used to cap prompt-visible memory. */
|
|
44
|
+
getContextWindow(): number | undefined;
|
|
45
|
+
/** Latest active goal state, used for short-term current-work memory. */
|
|
46
|
+
getGoalState(): GoalState | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Extension-contributed memory state staged across an atomic runtime reload. */
|
|
49
|
+
export interface MemoryControllerReloadSnapshot {
|
|
50
|
+
pendingMemoryProviders: MemoryProvider[];
|
|
51
|
+
pendingContextMemoryProviders: ContextMemoryProvider[];
|
|
52
|
+
memoryOkfProvider: ContextMemoryProvider | undefined;
|
|
53
|
+
fileStoreMemoryProvider: ContextMemoryProvider | undefined;
|
|
41
54
|
}
|
|
42
55
|
export declare class MemoryController {
|
|
43
56
|
private _memoryOkfProvider;
|
|
57
|
+
private _fileStoreMemoryProvider;
|
|
44
58
|
private _latestMemoryRetrievalReport;
|
|
45
59
|
private _latestMemoryPromptInclusionReport;
|
|
46
60
|
/** Plug-and-play memory subsystem. Recreated on each (re)initialize so reload is safe. */
|
|
@@ -49,6 +63,8 @@ export declare class MemoryController {
|
|
|
49
63
|
private readonly _effectivenessTracker;
|
|
50
64
|
/** Memory providers registered by extensions via pi.registerMemoryProvider, applied on (re)init. */
|
|
51
65
|
private _pendingMemoryProviders;
|
|
66
|
+
/** Context-memory providers registered by extensions via pi.registerContextMemoryProvider. */
|
|
67
|
+
private _pendingContextMemoryProviders;
|
|
52
68
|
private readonly deps;
|
|
53
69
|
constructor(deps: MemoryControllerDeps);
|
|
54
70
|
/** The live memory manager. Callers reach prefetch / tool-definitions / markers / shutdown through it. */
|
|
@@ -67,14 +83,16 @@ export declare class MemoryController {
|
|
|
67
83
|
* creation; `createOkfMemoryProvider` itself never writes/mkdirs either way).
|
|
68
84
|
*/
|
|
69
85
|
private _getMemoryOkfProvider;
|
|
86
|
+
private _getFileStoreMemoryProvider;
|
|
87
|
+
private _memoryBudget;
|
|
88
|
+
private _shouldQueryFileStoreFallback;
|
|
70
89
|
/**
|
|
71
90
|
* Observe-only local memory retrieval (see context/memory-retrieval.ts and
|
|
72
|
-
* context/okf-memory-provider.ts): default
|
|
73
|
-
* never constructs
|
|
74
|
-
* all) and returns an empty report -- fully fail-closed. When enabled,
|
|
75
|
-
* local, read-only
|
|
76
|
-
* none, e.g. a goal-continuation turn
|
|
77
|
-
* `latestUserMessageText`'s doc comment) under `DEFAULT_LOCAL_MEMORY_EGRESS_POLICY`.
|
|
91
|
+
* context/okf-memory-provider.ts): default-on, but settings-gated. When disabled,
|
|
92
|
+
* never constructs built-in context-memory providers (no directory access under
|
|
93
|
+
* `_memoryOkfDir()` at all) and returns an empty report -- fully fail-closed. When enabled,
|
|
94
|
+
* queries local, read-only providers with the latest user message text (empty if there is
|
|
95
|
+
* none, e.g. a goal-continuation turn) under `DEFAULT_LOCAL_MEMORY_EGRESS_POLICY`.
|
|
78
96
|
* Retrieved items are only ever stored in the report; nothing here touches `messages`,
|
|
79
97
|
* the transcript, or the provider-visible prompt. Never throws into a live turn: any
|
|
80
98
|
* failure (including a provider search error) degrades to an empty report.
|
|
@@ -82,10 +100,12 @@ export declare class MemoryController {
|
|
|
82
100
|
runMemoryRetrieval(messages: AgentMessage[]): Promise<MemoryRetrievalReport>;
|
|
83
101
|
/** Read-only inspection of the latest memory-retrieval report, for tests/debugging. */
|
|
84
102
|
getMemoryRetrievalReport(): MemoryRetrievalReport;
|
|
103
|
+
private _candidateForContextItem;
|
|
104
|
+
private _memoryCandidates;
|
|
85
105
|
/**
|
|
86
|
-
* Bounded prompt-surfacing
|
|
87
|
-
*
|
|
88
|
-
*
|
|
106
|
+
* Bounded prompt-surfacing for local memory evidence (see context/memory-tier-composer.ts):
|
|
107
|
+
* default-on, but gated on TWO settings (`enabled` AND `includeInPrompt`) plus at least one
|
|
108
|
+
* current-work, standing, or retrieved memory candidate -- the first
|
|
89
109
|
* two are belt-and-suspenders on top of the fact that `runMemoryRetrieval` already
|
|
90
110
|
* leaves `contextItems` empty whenever `enabled` is false, regardless of
|
|
91
111
|
* `includeInPrompt`. Reuses the `report` this pass's `runMemoryRetrieval` call already
|
|
@@ -123,6 +143,8 @@ export declare class MemoryController {
|
|
|
123
143
|
* filter — this just avoids the index query on turns that obviously don't warrant it.
|
|
124
144
|
*/
|
|
125
145
|
shouldAttemptRecall(text: string): boolean;
|
|
146
|
+
/** Legacy recall prefetch with the same hard-off and explicit external-egress policy as context retrieval. */
|
|
147
|
+
prefetchRecall(query: string): Promise<string>;
|
|
126
148
|
/** R4: score whether the agent actually used an injected recall page, so the recall gate can adapt. */
|
|
127
149
|
recordRecallOutcome(recallText: string, queryText: string, responseText: string): void;
|
|
128
150
|
/**
|
|
@@ -133,6 +155,10 @@ export declare class MemoryController {
|
|
|
133
155
|
initialize(): Promise<void>;
|
|
134
156
|
/** Register a memory provider contributed by an extension; applied on the next memory (re)init. */
|
|
135
157
|
registerMemoryProvider(provider: MemoryProvider): void;
|
|
158
|
+
/** Register a retrieval-style context memory provider contributed by an extension. */
|
|
159
|
+
registerContextMemoryProvider(provider: ContextMemoryProvider): void;
|
|
160
|
+
createReloadSnapshot(): MemoryControllerReloadSnapshot;
|
|
161
|
+
restoreReloadSnapshot(snapshot: MemoryControllerReloadSnapshot): void;
|
|
136
162
|
/** Reload starts memory providers fresh; loaded extensions re-register before the next `initialize()`. */
|
|
137
163
|
clearPendingProviders(): void;
|
|
138
164
|
}
|