@caupulican/pi-adaptative 0.80.86 → 0.80.89
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 +178 -0
- package/dist/core/agent-session.d.ts +412 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2053 -41
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/approval-gate.d.ts +4 -0
- package/dist/core/autonomy/approval-gate.d.ts.map +1 -0
- package/dist/core/autonomy/approval-gate.js +27 -0
- package/dist/core/autonomy/approval-gate.js.map +1 -0
- package/dist/core/autonomy/bounded-completion.d.ts +27 -0
- package/dist/core/autonomy/bounded-completion.d.ts.map +1 -0
- package/dist/core/autonomy/bounded-completion.js +44 -0
- package/dist/core/autonomy/bounded-completion.js.map +1 -0
- package/dist/core/autonomy/contracts.d.ts +129 -0
- package/dist/core/autonomy/contracts.d.ts.map +1 -0
- package/dist/core/autonomy/contracts.js +2 -0
- package/dist/core/autonomy/contracts.js.map +1 -0
- package/dist/core/autonomy/gates.d.ts +15 -0
- package/dist/core/autonomy/gates.d.ts.map +1 -0
- package/dist/core/autonomy/gates.js +205 -0
- package/dist/core/autonomy/gates.js.map +1 -0
- package/dist/core/autonomy/lane-tracker.d.ts +48 -0
- package/dist/core/autonomy/lane-tracker.d.ts.map +1 -0
- package/dist/core/autonomy/lane-tracker.js +125 -0
- package/dist/core/autonomy/lane-tracker.js.map +1 -0
- package/dist/core/autonomy/path-scope.d.ts +9 -0
- package/dist/core/autonomy/path-scope.d.ts.map +1 -0
- package/dist/core/autonomy/path-scope.js +122 -0
- package/dist/core/autonomy/path-scope.js.map +1 -0
- package/dist/core/autonomy/risk-assessment.d.ts +3 -0
- package/dist/core/autonomy/risk-assessment.d.ts.map +1 -0
- package/dist/core/autonomy/risk-assessment.js +122 -0
- package/dist/core/autonomy/risk-assessment.js.map +1 -0
- package/dist/core/autonomy/session-lane-record.d.ts +10 -0
- package/dist/core/autonomy/session-lane-record.d.ts.map +1 -0
- package/dist/core/autonomy/session-lane-record.js +36 -0
- package/dist/core/autonomy/session-lane-record.js.map +1 -0
- package/dist/core/autonomy/status.d.ts +40 -0
- package/dist/core/autonomy/status.d.ts.map +1 -0
- package/dist/core/autonomy/status.js +107 -0
- package/dist/core/autonomy/status.js.map +1 -0
- package/dist/core/autonomy/subagent-prompt.d.ts +21 -0
- package/dist/core/autonomy/subagent-prompt.d.ts.map +1 -0
- package/dist/core/autonomy/subagent-prompt.js +28 -0
- package/dist/core/autonomy/subagent-prompt.js.map +1 -0
- package/dist/core/autonomy/telemetry-events.d.ts +18 -0
- package/dist/core/autonomy/telemetry-events.d.ts.map +1 -0
- package/dist/core/autonomy/telemetry-events.js +60 -0
- package/dist/core/autonomy/telemetry-events.js.map +1 -0
- package/dist/core/context/artifact-retrieval.d.ts +49 -0
- package/dist/core/context/artifact-retrieval.d.ts.map +1 -0
- package/dist/core/context/artifact-retrieval.js +49 -0
- package/dist/core/context/artifact-retrieval.js.map +1 -0
- package/dist/core/context/brain-curator.d.ts +88 -0
- package/dist/core/context/brain-curator.d.ts.map +1 -0
- package/dist/core/context/brain-curator.js +192 -0
- package/dist/core/context/brain-curator.js.map +1 -0
- package/dist/core/context/context-artifacts.d.ts +94 -0
- package/dist/core/context/context-artifacts.d.ts.map +1 -0
- package/dist/core/context/context-artifacts.js +307 -0
- package/dist/core/context/context-artifacts.js.map +1 -0
- package/dist/core/context/context-audit.d.ts +66 -0
- package/dist/core/context/context-audit.d.ts.map +1 -0
- package/dist/core/context/context-audit.js +173 -0
- package/dist/core/context/context-audit.js.map +1 -0
- package/dist/core/context/context-composition.d.ts +122 -0
- package/dist/core/context/context-composition.d.ts.map +1 -0
- package/dist/core/context/context-composition.js +163 -0
- package/dist/core/context/context-composition.js.map +1 -0
- package/dist/core/context/context-item.d.ts +117 -0
- package/dist/core/context/context-item.d.ts.map +1 -0
- package/dist/core/context/context-item.js +36 -0
- package/dist/core/context/context-item.js.map +1 -0
- package/dist/core/context/context-prompt-enforcement.d.ts +86 -0
- package/dist/core/context/context-prompt-enforcement.d.ts.map +1 -0
- package/dist/core/context/context-prompt-enforcement.js +168 -0
- package/dist/core/context/context-prompt-enforcement.js.map +1 -0
- package/dist/core/context/context-prompt-policy.d.ts +90 -0
- package/dist/core/context/context-prompt-policy.d.ts.map +1 -0
- package/dist/core/context/context-prompt-policy.js +73 -0
- package/dist/core/context/context-prompt-policy.js.map +1 -0
- package/dist/core/context/context-retention.d.ts +36 -0
- package/dist/core/context/context-retention.d.ts.map +1 -0
- package/dist/core/context/context-retention.js +108 -0
- package/dist/core/context/context-retention.js.map +1 -0
- package/dist/core/context/context-store.d.ts +37 -0
- package/dist/core/context/context-store.d.ts.map +1 -0
- package/dist/core/context/context-store.js +45 -0
- package/dist/core/context/context-store.js.map +1 -0
- package/dist/core/context/memory-diagnostics.d.ts +50 -0
- package/dist/core/context/memory-diagnostics.d.ts.map +1 -0
- package/dist/core/context/memory-diagnostics.js +43 -0
- package/dist/core/context/memory-diagnostics.js.map +1 -0
- package/dist/core/context/memory-index-store.d.ts +28 -0
- package/dist/core/context/memory-index-store.d.ts.map +1 -0
- package/dist/core/context/memory-index-store.js +38 -0
- package/dist/core/context/memory-index-store.js.map +1 -0
- package/dist/core/context/memory-prompt-block.d.ts +34 -0
- package/dist/core/context/memory-prompt-block.d.ts.map +1 -0
- package/dist/core/context/memory-prompt-block.js +58 -0
- package/dist/core/context/memory-prompt-block.js.map +1 -0
- package/dist/core/context/memory-provider-contract.d.ts +114 -0
- package/dist/core/context/memory-provider-contract.d.ts.map +1 -0
- package/dist/core/context/memory-provider-contract.js +121 -0
- package/dist/core/context/memory-provider-contract.js.map +1 -0
- package/dist/core/context/memory-retrieval.d.ts +27 -0
- package/dist/core/context/memory-retrieval.d.ts.map +1 -0
- package/dist/core/context/memory-retrieval.js +91 -0
- package/dist/core/context/memory-retrieval.js.map +1 -0
- package/dist/core/context/okf-memory-provider.d.ts +26 -0
- package/dist/core/context/okf-memory-provider.d.ts.map +1 -0
- package/dist/core/context/okf-memory-provider.js +154 -0
- package/dist/core/context/okf-memory-provider.js.map +1 -0
- package/dist/core/context/okf-memory.d.ts +42 -0
- package/dist/core/context/okf-memory.d.ts.map +1 -0
- package/dist/core/context/okf-memory.js +175 -0
- package/dist/core/context/okf-memory.js.map +1 -0
- package/dist/core/context/policy-engine.d.ts +66 -0
- package/dist/core/context/policy-engine.d.ts.map +1 -0
- package/dist/core/context/policy-engine.js +171 -0
- package/dist/core/context/policy-engine.js.map +1 -0
- package/dist/core/context/policy-types.d.ts +102 -0
- package/dist/core/context/policy-types.d.ts.map +1 -0
- package/dist/core/context/policy-types.js +7 -0
- package/dist/core/context/policy-types.js.map +1 -0
- package/dist/core/context/sqlite-runtime-index.d.ts +19 -0
- package/dist/core/context/sqlite-runtime-index.d.ts.map +1 -0
- package/dist/core/context/sqlite-runtime-index.js +344 -0
- package/dist/core/context/sqlite-runtime-index.js.map +1 -0
- package/dist/core/context/storage-authority.d.ts +20 -0
- package/dist/core/context/storage-authority.d.ts.map +1 -0
- package/dist/core/context/storage-authority.js +51 -0
- package/dist/core/context/storage-authority.js.map +1 -0
- package/dist/core/context/tool-output-packer.d.ts +75 -0
- package/dist/core/context/tool-output-packer.d.ts.map +1 -0
- package/dist/core/context/tool-output-packer.js +77 -0
- package/dist/core/context/tool-output-packer.js.map +1 -0
- package/dist/core/context-gc.d.ts +13 -0
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +6 -0
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/cost/session-usage.d.ts +20 -0
- package/dist/core/cost/session-usage.d.ts.map +1 -0
- package/dist/core/cost/session-usage.js +164 -0
- package/dist/core/cost/session-usage.js.map +1 -0
- package/dist/core/delegation/session-worker-result.d.ts +10 -0
- package/dist/core/delegation/session-worker-result.d.ts.map +1 -0
- package/dist/core/delegation/session-worker-result.js +36 -0
- package/dist/core/delegation/session-worker-result.js.map +1 -0
- package/dist/core/delegation/worker-result.d.ts +9 -0
- package/dist/core/delegation/worker-result.d.ts.map +1 -0
- package/dist/core/delegation/worker-result.js +152 -0
- package/dist/core/delegation/worker-result.js.map +1 -0
- package/dist/core/delegation/worker-runner.d.ts +58 -0
- package/dist/core/delegation/worker-runner.d.ts.map +1 -0
- package/dist/core/delegation/worker-runner.js +188 -0
- package/dist/core/delegation/worker-runner.js.map +1 -0
- package/dist/core/extensions/builtin.d.ts +5 -1
- package/dist/core/extensions/builtin.d.ts.map +1 -1
- package/dist/core/extensions/builtin.js +23 -1
- package/dist/core/extensions/builtin.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +5 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +13 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/goals/goal-continuation-controller.d.ts +22 -0
- package/dist/core/goals/goal-continuation-controller.d.ts.map +1 -0
- package/dist/core/goals/goal-continuation-controller.js +88 -0
- package/dist/core/goals/goal-continuation-controller.js.map +1 -0
- package/dist/core/goals/goal-continuation-defaults.d.ts +10 -0
- package/dist/core/goals/goal-continuation-defaults.d.ts.map +1 -0
- package/dist/core/goals/goal-continuation-defaults.js +10 -0
- package/dist/core/goals/goal-continuation-defaults.js.map +1 -0
- package/dist/core/goals/goal-continuation-prompt.d.ts +18 -0
- package/dist/core/goals/goal-continuation-prompt.d.ts.map +1 -0
- package/dist/core/goals/goal-continuation-prompt.js +141 -0
- package/dist/core/goals/goal-continuation-prompt.js.map +1 -0
- package/dist/core/goals/goal-runtime-snapshot.d.ts +19 -0
- package/dist/core/goals/goal-runtime-snapshot.d.ts.map +1 -0
- package/dist/core/goals/goal-runtime-snapshot.js +23 -0
- package/dist/core/goals/goal-runtime-snapshot.js.map +1 -0
- package/dist/core/goals/goal-state.d.ts +87 -0
- package/dist/core/goals/goal-state.d.ts.map +1 -0
- package/dist/core/goals/goal-state.js +259 -0
- package/dist/core/goals/goal-state.js.map +1 -0
- package/dist/core/goals/goal-tool-core.d.ts +66 -0
- package/dist/core/goals/goal-tool-core.d.ts.map +1 -0
- package/dist/core/goals/goal-tool-core.js +146 -0
- package/dist/core/goals/goal-tool-core.js.map +1 -0
- package/dist/core/goals/session-goal-state.d.ts +10 -0
- package/dist/core/goals/session-goal-state.d.ts.map +1 -0
- package/dist/core/goals/session-goal-state.js +35 -0
- package/dist/core/goals/session-goal-state.js.map +1 -0
- package/dist/core/learning/learning-audit.d.ts +45 -0
- package/dist/core/learning/learning-audit.d.ts.map +1 -0
- package/dist/core/learning/learning-audit.js +139 -0
- package/dist/core/learning/learning-audit.js.map +1 -0
- package/dist/core/learning/learning-gate.d.ts +29 -0
- package/dist/core/learning/learning-gate.d.ts.map +1 -0
- package/dist/core/learning/learning-gate.js +150 -0
- package/dist/core/learning/learning-gate.js.map +1 -0
- package/dist/core/learning/session-learning-decision.d.ts +10 -0
- package/dist/core/learning/session-learning-decision.d.ts.map +1 -0
- package/dist/core/learning/session-learning-decision.js +36 -0
- package/dist/core/learning/session-learning-decision.js.map +1 -0
- package/dist/core/model-capability.d.ts +41 -0
- package/dist/core/model-capability.d.ts.map +1 -0
- package/dist/core/model-capability.js +101 -0
- package/dist/core/model-capability.js.map +1 -0
- package/dist/core/model-router/config-diagnostics.d.ts.map +1 -1
- package/dist/core/model-router/config-diagnostics.js +1 -0
- package/dist/core/model-router/config-diagnostics.js.map +1 -1
- package/dist/core/model-router/intent-classifier.d.ts +2 -0
- package/dist/core/model-router/intent-classifier.d.ts.map +1 -1
- package/dist/core/model-router/intent-classifier.js +154 -9
- package/dist/core/model-router/intent-classifier.js.map +1 -1
- package/dist/core/model-router/route-judge.d.ts +54 -0
- package/dist/core/model-router/route-judge.d.ts.map +1 -0
- package/dist/core/model-router/route-judge.js +128 -0
- package/dist/core/model-router/route-judge.js.map +1 -0
- package/dist/core/model-router/status.d.ts +4 -1
- package/dist/core/model-router/status.d.ts.map +1 -1
- package/dist/core/model-router/status.js +30 -6
- package/dist/core/model-router/status.js.map +1 -1
- package/dist/core/model-router/tool-escalation.d.ts +4 -6
- package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
- package/dist/core/model-router/tool-escalation.js +1 -1
- package/dist/core/model-router/tool-escalation.js.map +1 -1
- package/dist/core/models/fitness-store.d.ts +40 -0
- package/dist/core/models/fitness-store.d.ts.map +1 -0
- package/dist/core/models/fitness-store.js +61 -0
- package/dist/core/models/fitness-store.js.map +1 -0
- package/dist/core/profile-registry.d.ts.map +1 -1
- package/dist/core/profile-registry.js +1 -1
- package/dist/core/profile-registry.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +2 -0
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +12 -4
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/research/automata-provider.d.ts +5 -0
- package/dist/core/research/automata-provider.d.ts.map +1 -0
- package/dist/core/research/automata-provider.js +15 -0
- package/dist/core/research/automata-provider.js.map +1 -0
- package/dist/core/research/evidence-bundle.d.ts +10 -0
- package/dist/core/research/evidence-bundle.d.ts.map +1 -0
- package/dist/core/research/evidence-bundle.js +116 -0
- package/dist/core/research/evidence-bundle.js.map +1 -0
- package/dist/core/research/model-fitness.d.ts +82 -0
- package/dist/core/research/model-fitness.d.ts.map +1 -0
- package/dist/core/research/model-fitness.js +308 -0
- package/dist/core/research/model-fitness.js.map +1 -0
- package/dist/core/research/research-gate.d.ts +11 -0
- package/dist/core/research/research-gate.d.ts.map +1 -0
- package/dist/core/research/research-gate.js +82 -0
- package/dist/core/research/research-gate.js.map +1 -0
- package/dist/core/research/research-runner.d.ts +59 -0
- package/dist/core/research/research-runner.d.ts.map +1 -0
- package/dist/core/research/research-runner.js +155 -0
- package/dist/core/research/research-runner.js.map +1 -0
- package/dist/core/research/session-evidence-bundle.d.ts +11 -0
- package/dist/core/research/session-evidence-bundle.d.ts.map +1 -0
- package/dist/core/research/session-evidence-bundle.js +55 -0
- package/dist/core/research/session-evidence-bundle.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +4 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +160 -4
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +304 -9
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +4 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +18 -6
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +10 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/toolkit/script-registry.d.ts +34 -0
- package/dist/core/toolkit/script-registry.d.ts.map +1 -0
- package/dist/core/toolkit/script-registry.js +71 -0
- package/dist/core/toolkit/script-registry.js.map +1 -0
- package/dist/core/toolkit/script-runner.d.ts +28 -0
- package/dist/core/toolkit/script-runner.d.ts.map +1 -0
- package/dist/core/toolkit/script-runner.js +48 -0
- package/dist/core/toolkit/script-runner.js.map +1 -0
- package/dist/core/tools/artifact-retrieve.d.ts +23 -0
- package/dist/core/tools/artifact-retrieve.d.ts.map +1 -0
- package/dist/core/tools/artifact-retrieve.js +110 -0
- package/dist/core/tools/artifact-retrieve.js.map +1 -0
- package/dist/core/tools/delegate.d.ts +32 -0
- package/dist/core/tools/delegate.d.ts.map +1 -0
- package/dist/core/tools/delegate.js +60 -0
- package/dist/core/tools/delegate.js.map +1 -0
- package/dist/core/tools/fff-search-backend.d.ts +103 -0
- package/dist/core/tools/fff-search-backend.d.ts.map +1 -0
- package/dist/core/tools/fff-search-backend.js +151 -0
- package/dist/core/tools/fff-search-backend.js.map +1 -0
- package/dist/core/tools/find.d.ts +21 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +183 -10
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/goal.d.ts +35 -0
- package/dist/core/tools/goal.d.ts.map +1 -0
- package/dist/core/tools/goal.js +122 -0
- package/dist/core/tools/goal.js.map +1 -0
- package/dist/core/tools/grep.d.ts +21 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +272 -27
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +4 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +9 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/model-fitness.d.ts +30 -0
- package/dist/core/tools/model-fitness.d.ts.map +1 -0
- package/dist/core/tools/model-fitness.js +38 -0
- package/dist/core/tools/model-fitness.js.map +1 -0
- package/dist/core/tools/run-toolkit-script.d.ts +24 -0
- package/dist/core/tools/run-toolkit-script.d.ts.map +1 -0
- package/dist/core/tools/run-toolkit-script.js +103 -0
- package/dist/core/tools/run-toolkit-script.js.map +1 -0
- package/dist/core/tools/search-router.d.ts +75 -0
- package/dist/core/tools/search-router.d.ts.map +1 -0
- package/dist/core/tools/search-router.js +85 -0
- package/dist/core/tools/search-router.js.map +1 -0
- package/dist/modes/interactive/components/fitness-role-selector.d.ts +13 -0
- package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/fitness-role-selector.js +65 -0
- package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +18 -16
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +16 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +555 -11
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +308 -39
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +2 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +154 -2
- package/dist/utils/tools-manager.js.map +1 -1
- 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/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 +368 -12
- package/package.json +5 -4
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { runBoundedCompletion } from "../autonomy/bounded-completion.js";
|
|
2
|
+
/**
|
|
3
|
+
* Brain-assisted context curation (see docs/model-router-rework/brain-context-curation-design.md):
|
|
4
|
+
* a SIDECAR curator that consumes reports the context pipeline already produces and feeds back
|
|
5
|
+
* small, typed advisories. It is never a pipeline stage: every consumer must behave byte-for-byte
|
|
6
|
+
* identically when a result is absent (missing digest -> today's stub; missing relevance ->
|
|
7
|
+
* today's enforcement decision). The curator itself is provider-free — the completion executor is
|
|
8
|
+
* injected per drain, so it works against any registered local model and faux providers in tests.
|
|
9
|
+
*
|
|
10
|
+
* Memory bounds are explicit: the queue and result map are both capped, and drops are counted in
|
|
11
|
+
* telemetry rather than silent. Results are keyed for idempotency (digests by the GC record's
|
|
12
|
+
* content hash, relevance by the audit item id), so re-enqueueing the same work is free.
|
|
13
|
+
*/
|
|
14
|
+
export const CURATION_DIGEST_SYSTEM_PROMPT = [
|
|
15
|
+
"You digest tool-output chunks for a coding agent's context curator. You never solve the task.",
|
|
16
|
+
"Given a chunk, respond with STRICT JSON only - no prose:",
|
|
17
|
+
'{"digest":"<one or two sentences, max 200 characters, keeping exact identifiers>"}',
|
|
18
|
+
"Keep exact file paths, symbol names, error codes, and version strings verbatim.",
|
|
19
|
+
].join("\n");
|
|
20
|
+
export const CURATION_RELEVANCE_SYSTEM_PROMPT = [
|
|
21
|
+
"You judge whether a stale tool output is still relevant to the user's current goal.",
|
|
22
|
+
"You never solve the task. Respond with STRICT JSON only - no prose:",
|
|
23
|
+
'{"relevant":true|false,"confidence":<0..1>}',
|
|
24
|
+
"relevant=false means the chunk is about something the current goal no longer needs.",
|
|
25
|
+
"When uncertain, answer relevant=true with low confidence - keeping content is the safe default.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
const MAX_QUEUE = 32;
|
|
28
|
+
const MAX_RESULTS = 200;
|
|
29
|
+
const MAX_JOB_CONTENT_CHARS = 8_000;
|
|
30
|
+
const DIGEST_MAX_WALL_CLOCK_MS = 20_000;
|
|
31
|
+
const RELEVANCE_MAX_WALL_CLOCK_MS = 8_000;
|
|
32
|
+
export const CURATION_RELEVANCE_MIN_CONFIDENCE = 0.8;
|
|
33
|
+
function extractJsonObject(text) {
|
|
34
|
+
const trimmed = text.trim();
|
|
35
|
+
const candidates = [trimmed];
|
|
36
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(trimmed);
|
|
37
|
+
if (fenced?.[1])
|
|
38
|
+
candidates.push(fenced[1].trim());
|
|
39
|
+
const start = trimmed.indexOf("{");
|
|
40
|
+
const end = trimmed.lastIndexOf("}");
|
|
41
|
+
if (start >= 0 && end > start)
|
|
42
|
+
candidates.push(trimmed.slice(start, end + 1));
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(candidate);
|
|
46
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// try next candidate
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export function parseCurationDigest(text) {
|
|
56
|
+
const parsed = extractJsonObject(text);
|
|
57
|
+
if (!parsed)
|
|
58
|
+
return undefined;
|
|
59
|
+
const digest = parsed.digest;
|
|
60
|
+
if (typeof digest !== "string")
|
|
61
|
+
return undefined;
|
|
62
|
+
const trimmed = digest.trim().replace(/\s+/g, " ");
|
|
63
|
+
if (trimmed.length === 0 || trimmed.length > 240)
|
|
64
|
+
return undefined;
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
67
|
+
export function parseCurationRelevance(text) {
|
|
68
|
+
const parsed = extractJsonObject(text);
|
|
69
|
+
if (!parsed)
|
|
70
|
+
return undefined;
|
|
71
|
+
const record = parsed;
|
|
72
|
+
if (typeof record.relevant !== "boolean")
|
|
73
|
+
return undefined;
|
|
74
|
+
const confidence = typeof record.confidence === "number" && Number.isFinite(record.confidence)
|
|
75
|
+
? Math.max(0, Math.min(1, record.confidence))
|
|
76
|
+
: 0;
|
|
77
|
+
return { relevant: record.relevant, confidence };
|
|
78
|
+
}
|
|
79
|
+
export class BrainCurator {
|
|
80
|
+
_queue = new Map();
|
|
81
|
+
_results = new Map();
|
|
82
|
+
_jobsRun = 0;
|
|
83
|
+
_parseFailures = 0;
|
|
84
|
+
_droppedJobs = 0;
|
|
85
|
+
_localChars = 0;
|
|
86
|
+
_draining = false;
|
|
87
|
+
enqueue(job) {
|
|
88
|
+
if (this._results.has(job.key) || this._queue.has(job.key))
|
|
89
|
+
return;
|
|
90
|
+
if (this._queue.size >= MAX_QUEUE) {
|
|
91
|
+
// Drop the OLDEST queued job (newer work reflects the current goal better) and count it.
|
|
92
|
+
const oldest = this._queue.keys().next().value;
|
|
93
|
+
if (oldest !== undefined)
|
|
94
|
+
this._queue.delete(oldest);
|
|
95
|
+
this._droppedJobs++;
|
|
96
|
+
}
|
|
97
|
+
this._queue.set(job.key, { ...job, content: job.content.slice(0, MAX_JOB_CONTENT_CHARS) });
|
|
98
|
+
}
|
|
99
|
+
getDigest(key) {
|
|
100
|
+
const result = this._results.get(key);
|
|
101
|
+
return result?.ok && result.kind === "stub_digest" ? result.digest : undefined;
|
|
102
|
+
}
|
|
103
|
+
getRelevance(key) {
|
|
104
|
+
const result = this._results.get(key);
|
|
105
|
+
if (!result?.ok || result.kind !== "relevance" || result.relevant === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
return { relevant: result.relevant, confidence: result.confidence ?? 0 };
|
|
108
|
+
}
|
|
109
|
+
hasWork() {
|
|
110
|
+
return this._queue.size > 0;
|
|
111
|
+
}
|
|
112
|
+
get isDraining() {
|
|
113
|
+
return this._draining;
|
|
114
|
+
}
|
|
115
|
+
telemetry() {
|
|
116
|
+
return {
|
|
117
|
+
jobsRun: this._jobsRun,
|
|
118
|
+
parseFailures: this._parseFailures,
|
|
119
|
+
droppedJobs: this._droppedJobs,
|
|
120
|
+
localChars: this._localChars,
|
|
121
|
+
queued: this._queue.size,
|
|
122
|
+
resultsHeld: this._results.size,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Run up to `maxJobs` queued jobs through the injected local-model completer. Single-flight:
|
|
127
|
+
* a concurrent drain call returns [] immediately rather than double-running jobs. Every call
|
|
128
|
+
* is wall-clock bounded; a failed/unparseable job is recorded as a not-ok result (so it is
|
|
129
|
+
* not retried forever) and counted in telemetry.
|
|
130
|
+
*/
|
|
131
|
+
async drain(args) {
|
|
132
|
+
if (this._draining)
|
|
133
|
+
return [];
|
|
134
|
+
this._draining = true;
|
|
135
|
+
const now = args.now ?? Date.now;
|
|
136
|
+
const completed = [];
|
|
137
|
+
try {
|
|
138
|
+
const jobs = [...this._queue.values()].slice(0, Math.max(0, args.maxJobs));
|
|
139
|
+
for (const job of jobs) {
|
|
140
|
+
if (args.signal?.aborted)
|
|
141
|
+
break;
|
|
142
|
+
this._queue.delete(job.key);
|
|
143
|
+
const started = now();
|
|
144
|
+
const bounded = await runBoundedCompletion({
|
|
145
|
+
maxWallClockMs: job.kind === "stub_digest" ? DIGEST_MAX_WALL_CLOCK_MS : RELEVANCE_MAX_WALL_CLOCK_MS,
|
|
146
|
+
signal: args.signal,
|
|
147
|
+
execute: (signal) => args.complete({
|
|
148
|
+
systemPrompt: job.kind === "stub_digest" ? CURATION_DIGEST_SYSTEM_PROMPT : CURATION_RELEVANCE_SYSTEM_PROMPT,
|
|
149
|
+
userPrompt: job.kind === "stub_digest"
|
|
150
|
+
? job.content
|
|
151
|
+
: `Current goal: ${job.goal ?? "(unknown)"}\n\nStale chunk:\n${job.content}`,
|
|
152
|
+
signal,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
const ms = now() - started;
|
|
156
|
+
this._jobsRun++;
|
|
157
|
+
this._localChars += job.content.length;
|
|
158
|
+
let result = { key: job.key, kind: job.kind, ok: false, ms };
|
|
159
|
+
if (bounded.completion && !bounded.failure) {
|
|
160
|
+
if (job.kind === "stub_digest") {
|
|
161
|
+
const digest = parseCurationDigest(bounded.completion.text);
|
|
162
|
+
result = digest !== undefined ? { ...result, ok: true, digest } : result;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const relevance = parseCurationRelevance(bounded.completion.text);
|
|
166
|
+
result =
|
|
167
|
+
relevance !== undefined
|
|
168
|
+
? { ...result, ok: true, relevant: relevance.relevant, confidence: relevance.confidence }
|
|
169
|
+
: result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!result.ok)
|
|
173
|
+
this._parseFailures++;
|
|
174
|
+
this._storeResult(result);
|
|
175
|
+
completed.push(result);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
this._draining = false;
|
|
180
|
+
}
|
|
181
|
+
return completed;
|
|
182
|
+
}
|
|
183
|
+
_storeResult(result) {
|
|
184
|
+
if (this._results.size >= MAX_RESULTS) {
|
|
185
|
+
const oldest = this._results.keys().next().value;
|
|
186
|
+
if (oldest !== undefined)
|
|
187
|
+
this._results.delete(oldest);
|
|
188
|
+
}
|
|
189
|
+
this._results.set(result.key, result);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=brain-curator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brain-curator.js","sourceRoot":"","sources":["../../../src/core/context/brain-curator.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\n\n/**\n * Brain-assisted context curation (see docs/model-router-rework/brain-context-curation-design.md):\n * a SIDECAR curator that consumes reports the context pipeline already produces and feeds back\n * small, typed advisories. It is never a pipeline stage: every consumer must behave byte-for-byte\n * identically when a result is absent (missing digest -> today's stub; missing relevance ->\n * today's enforcement decision). The curator itself is provider-free — the completion executor is\n * injected per drain, so it works against any registered local model and faux providers in tests.\n *\n * Memory bounds are explicit: the queue and result map are both capped, and drops are counted in\n * telemetry rather than silent. Results are keyed for idempotency (digests by the GC record's\n * content hash, relevance by the audit item id), so re-enqueueing the same work is free.\n */\n\nexport const CURATION_DIGEST_SYSTEM_PROMPT = [\n\t\"You digest tool-output chunks for a coding agent's context curator. You never solve the task.\",\n\t\"Given a chunk, respond with STRICT JSON only - no prose:\",\n\t'{\"digest\":\"<one or two sentences, max 200 characters, keeping exact identifiers>\"}',\n\t\"Keep exact file paths, symbol names, error codes, and version strings verbatim.\",\n].join(\"\\n\");\n\nexport const CURATION_RELEVANCE_SYSTEM_PROMPT = [\n\t\"You judge whether a stale tool output is still relevant to the user's current goal.\",\n\t\"You never solve the task. Respond with STRICT JSON only - no prose:\",\n\t'{\"relevant\":true|false,\"confidence\":<0..1>}',\n\t\"relevant=false means the chunk is about something the current goal no longer needs.\",\n\t\"When uncertain, answer relevant=true with low confidence - keeping content is the safe default.\",\n].join(\"\\n\");\n\nexport interface CurationJob {\n\tkind: \"stub_digest\" | \"relevance\";\n\t/** Idempotency key: digest jobs use the GC record's content hash, relevance jobs the item id. */\n\tkey: string;\n\t/** Bounded chunk the local model must actually be able to process (sliced on enqueue). */\n\tcontent: string;\n\t/** Relevance jobs only: the goal/intent line the chunk is judged against. */\n\tgoal?: string;\n}\n\nexport interface CurationResult {\n\tkey: string;\n\tkind: CurationJob[\"kind\"];\n\tok: boolean;\n\tdigest?: string;\n\trelevant?: boolean;\n\tconfidence?: number;\n\tms: number;\n}\n\nexport interface CurationTelemetrySnapshot {\n\tjobsRun: number;\n\tparseFailures: number;\n\tdroppedJobs: number;\n\t/** Chars processed locally (an honest proxy for frontier tokens NOT spent on this work). */\n\tlocalChars: number;\n\tqueued: number;\n\tresultsHeld: number;\n}\n\nexport type CurationComplete = (input: {\n\tsystemPrompt: string;\n\tuserPrompt: string;\n\tsignal?: AbortSignal;\n}) => Promise<{ text: string; costUsd: number; stopReason: string }>;\n\nconst MAX_QUEUE = 32;\nconst MAX_RESULTS = 200;\nconst MAX_JOB_CONTENT_CHARS = 8_000;\nconst DIGEST_MAX_WALL_CLOCK_MS = 20_000;\nconst RELEVANCE_MAX_WALL_CLOCK_MS = 8_000;\nexport const CURATION_RELEVANCE_MIN_CONFIDENCE = 0.8;\n\nfunction extractJsonObject(text: string): unknown | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\tfor (const candidate of candidates) {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(candidate);\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) return parsed;\n\t\t} catch {\n\t\t\t// try next candidate\n\t\t}\n\t}\n\treturn undefined;\n}\n\nexport function parseCurationDigest(text: string): string | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst digest = (parsed as { digest?: unknown }).digest;\n\tif (typeof digest !== \"string\") return undefined;\n\tconst trimmed = digest.trim().replace(/\\s+/g, \" \");\n\tif (trimmed.length === 0 || trimmed.length > 240) return undefined;\n\treturn trimmed;\n}\n\nexport function parseCurationRelevance(text: string): { relevant: boolean; confidence: number } | undefined {\n\tconst parsed = extractJsonObject(text);\n\tif (!parsed) return undefined;\n\tconst record = parsed as { relevant?: unknown; confidence?: unknown };\n\tif (typeof record.relevant !== \"boolean\") return undefined;\n\tconst confidence =\n\t\ttypeof record.confidence === \"number\" && Number.isFinite(record.confidence)\n\t\t\t? Math.max(0, Math.min(1, record.confidence))\n\t\t\t: 0;\n\treturn { relevant: record.relevant, confidence };\n}\n\nexport class BrainCurator {\n\tprivate readonly _queue = new Map<string, CurationJob>();\n\tprivate readonly _results = new Map<string, CurationResult>();\n\tprivate _jobsRun = 0;\n\tprivate _parseFailures = 0;\n\tprivate _droppedJobs = 0;\n\tprivate _localChars = 0;\n\tprivate _draining = false;\n\n\tenqueue(job: CurationJob): void {\n\t\tif (this._results.has(job.key) || this._queue.has(job.key)) return;\n\t\tif (this._queue.size >= MAX_QUEUE) {\n\t\t\t// Drop the OLDEST queued job (newer work reflects the current goal better) and count it.\n\t\t\tconst oldest = this._queue.keys().next().value;\n\t\t\tif (oldest !== undefined) this._queue.delete(oldest);\n\t\t\tthis._droppedJobs++;\n\t\t}\n\t\tthis._queue.set(job.key, { ...job, content: job.content.slice(0, MAX_JOB_CONTENT_CHARS) });\n\t}\n\n\tgetDigest(key: string): string | undefined {\n\t\tconst result = this._results.get(key);\n\t\treturn result?.ok && result.kind === \"stub_digest\" ? result.digest : undefined;\n\t}\n\n\tgetRelevance(key: string): { relevant: boolean; confidence: number } | undefined {\n\t\tconst result = this._results.get(key);\n\t\tif (!result?.ok || result.kind !== \"relevance\" || result.relevant === undefined) return undefined;\n\t\treturn { relevant: result.relevant, confidence: result.confidence ?? 0 };\n\t}\n\n\thasWork(): boolean {\n\t\treturn this._queue.size > 0;\n\t}\n\n\tget isDraining(): boolean {\n\t\treturn this._draining;\n\t}\n\n\ttelemetry(): CurationTelemetrySnapshot {\n\t\treturn {\n\t\t\tjobsRun: this._jobsRun,\n\t\t\tparseFailures: this._parseFailures,\n\t\t\tdroppedJobs: this._droppedJobs,\n\t\t\tlocalChars: this._localChars,\n\t\t\tqueued: this._queue.size,\n\t\t\tresultsHeld: this._results.size,\n\t\t};\n\t}\n\n\t/**\n\t * Run up to `maxJobs` queued jobs through the injected local-model completer. Single-flight:\n\t * a concurrent drain call returns [] immediately rather than double-running jobs. Every call\n\t * is wall-clock bounded; a failed/unparseable job is recorded as a not-ok result (so it is\n\t * not retried forever) and counted in telemetry.\n\t */\n\tasync drain(args: {\n\t\tcomplete: CurationComplete;\n\t\tmaxJobs: number;\n\t\tsignal?: AbortSignal;\n\t\tnow?: () => number;\n\t}): Promise<CurationResult[]> {\n\t\tif (this._draining) return [];\n\t\tthis._draining = true;\n\t\tconst now = args.now ?? Date.now;\n\t\tconst completed: CurationResult[] = [];\n\t\ttry {\n\t\t\tconst jobs = [...this._queue.values()].slice(0, Math.max(0, args.maxJobs));\n\t\t\tfor (const job of jobs) {\n\t\t\t\tif (args.signal?.aborted) break;\n\t\t\t\tthis._queue.delete(job.key);\n\t\t\t\tconst started = now();\n\t\t\t\tconst bounded = await runBoundedCompletion({\n\t\t\t\t\tmaxWallClockMs: job.kind === \"stub_digest\" ? DIGEST_MAX_WALL_CLOCK_MS : RELEVANCE_MAX_WALL_CLOCK_MS,\n\t\t\t\t\tsignal: args.signal,\n\t\t\t\t\texecute: (signal) =>\n\t\t\t\t\t\targs.complete({\n\t\t\t\t\t\t\tsystemPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\" ? CURATION_DIGEST_SYSTEM_PROMPT : CURATION_RELEVANCE_SYSTEM_PROMPT,\n\t\t\t\t\t\t\tuserPrompt:\n\t\t\t\t\t\t\t\tjob.kind === \"stub_digest\"\n\t\t\t\t\t\t\t\t\t? job.content\n\t\t\t\t\t\t\t\t\t: `Current goal: ${job.goal ?? \"(unknown)\"}\\n\\nStale chunk:\\n${job.content}`,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t}),\n\t\t\t\t});\n\t\t\t\tconst ms = now() - started;\n\t\t\t\tthis._jobsRun++;\n\t\t\t\tthis._localChars += job.content.length;\n\t\t\t\tlet result: CurationResult = { key: job.key, kind: job.kind, ok: false, ms };\n\t\t\t\tif (bounded.completion && !bounded.failure) {\n\t\t\t\t\tif (job.kind === \"stub_digest\") {\n\t\t\t\t\t\tconst digest = parseCurationDigest(bounded.completion.text);\n\t\t\t\t\t\tresult = digest !== undefined ? { ...result, ok: true, digest } : result;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst relevance = parseCurationRelevance(bounded.completion.text);\n\t\t\t\t\t\tresult =\n\t\t\t\t\t\t\trelevance !== undefined\n\t\t\t\t\t\t\t\t? { ...result, ok: true, relevant: relevance.relevant, confidence: relevance.confidence }\n\t\t\t\t\t\t\t\t: result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!result.ok) this._parseFailures++;\n\t\t\t\tthis._storeResult(result);\n\t\t\t\tcompleted.push(result);\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._draining = false;\n\t\t}\n\t\treturn completed;\n\t}\n\n\tprivate _storeResult(result: CurationResult): void {\n\t\tif (this._results.size >= MAX_RESULTS) {\n\t\t\tconst oldest = this._results.keys().next().value;\n\t\t\tif (oldest !== undefined) this._results.delete(oldest);\n\t\t}\n\t\tthis._results.set(result.key, result);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact store abstraction (Phase 2): stable refs for raw large payloads, kept out of
|
|
3
|
+
* band from prompt context. This module defines the `ArtifactStore` interface plus two
|
|
4
|
+
* implementations: an in-memory one for tests, and `createFileArtifactStore` (session-
|
|
5
|
+
* scoped, filesystem-backed). A SQLite-backed implementation waits until the Phase M0
|
|
6
|
+
* storage-authority/location/concurrency decisions are accepted (see
|
|
7
|
+
* docs/context-management-rework/memory-architecture.md).
|
|
8
|
+
*
|
|
9
|
+
* `createFileArtifactStore` is wired into live grep/find tool construction in
|
|
10
|
+
* agent-session.ts (session-scoped under `<agentDir>/context-artifacts/<sessionId>/`).
|
|
11
|
+
* References are registered at pack time and released when context-gc evicts the
|
|
12
|
+
* corresponding grep/find tool result (opportunistic, conservative cleanup), with a
|
|
13
|
+
* best-effort dispose-time sweep for zero-reference artifacts. Payloads are retrievable
|
|
14
|
+
* out of band via the artifact_retrieve tool (context/artifact-retrieval.ts).
|
|
15
|
+
*/
|
|
16
|
+
import { type ContextArtifactRef } from "./context-item.ts";
|
|
17
|
+
export interface ArtifactWriteRequest {
|
|
18
|
+
kind: ContextArtifactRef["kind"];
|
|
19
|
+
content: string;
|
|
20
|
+
toolName?: string;
|
|
21
|
+
command?: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
sessionEntryId?: string;
|
|
24
|
+
createdAtTurn: number;
|
|
25
|
+
reproducible: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface ArtifactRecord {
|
|
28
|
+
ref: ContextArtifactRef;
|
|
29
|
+
content: string;
|
|
30
|
+
}
|
|
31
|
+
export type MissingArtifactReason = "not_found" | "cleaned_up";
|
|
32
|
+
export interface MissingArtifactMarker {
|
|
33
|
+
id: string;
|
|
34
|
+
missing: true;
|
|
35
|
+
reason: MissingArtifactReason;
|
|
36
|
+
}
|
|
37
|
+
export declare function isMissingArtifactMarker(value: ArtifactRecord | MissingArtifactMarker): value is MissingArtifactMarker;
|
|
38
|
+
/**
|
|
39
|
+
* Artifact id for a capture event, not merely a payload: it hashes every ref-defining
|
|
40
|
+
* field (kind, tool/command/path, content, sessionEntryId, createdAtTurn, reproducible).
|
|
41
|
+
* A repeat write with identical content but a different turn or session entry is a
|
|
42
|
+
* distinct capture and must get a distinct id -- otherwise the later capture's metadata
|
|
43
|
+
* would be silently discarded in favor of the first write. Only a truly identical
|
|
44
|
+
* request (same capture, re-submitted) is idempotent under this id.
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateArtifactId(request: Pick<ArtifactWriteRequest, "kind" | "content" | "toolName" | "command" | "path" | "sessionEntryId" | "createdAtTurn" | "reproducible">): string;
|
|
47
|
+
export interface ArtifactStore {
|
|
48
|
+
write(request: ArtifactWriteRequest): ArtifactRecord;
|
|
49
|
+
read(id: string): ArtifactRecord | MissingArtifactMarker;
|
|
50
|
+
/**
|
|
51
|
+
* Metadata-only lookup: the ref if `id` resolves to a live artifact, `undefined`
|
|
52
|
+
* otherwise. Never loads the payload -- for the file store this must not touch the
|
|
53
|
+
* payload file at all beyond an existence check, so a caller that only needs to know
|
|
54
|
+
* "does this still exist, and what are its ref fields" (e.g. a per-turn audit pass)
|
|
55
|
+
* never pays the cost of reading potentially large content off disk.
|
|
56
|
+
*/
|
|
57
|
+
readRef(id: string): ContextArtifactRef | undefined;
|
|
58
|
+
has(id: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Register that `holderId` (a context item id, session entry id, etc.) depends on this
|
|
61
|
+
* artifact. Returns false if `id` does not exist (never written, or already cleaned
|
|
62
|
+
* up) so a caller cannot believe it protected an artifact that was never registered.
|
|
63
|
+
* Callers must fail closed (treat the artifact as unprotected) on a false return.
|
|
64
|
+
*/
|
|
65
|
+
addReference(id: string, holderId: string): boolean;
|
|
66
|
+
/** Release a previously registered dependency. Returns true only if a reference was actually removed. */
|
|
67
|
+
removeReference(id: string, holderId: string): boolean;
|
|
68
|
+
referenceCount(id: string): number;
|
|
69
|
+
/** Delete only artifacts with zero active references. Returns the ids actually deleted. */
|
|
70
|
+
cleanup(): string[];
|
|
71
|
+
}
|
|
72
|
+
export declare function createInMemoryArtifactStore(): ArtifactStore;
|
|
73
|
+
export interface FileArtifactStoreOptions {
|
|
74
|
+
/** Directory the store persists artifact payloads and metadata under. Created if missing. */
|
|
75
|
+
baseDir: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Filesystem-backed `ArtifactStore`: payload and metadata (including reference holder ids)
|
|
79
|
+
* are written to `baseDir` so content, ref fields, and cleanup-protecting references all
|
|
80
|
+
* survive recreating the store (e.g. across a process restart against the same
|
|
81
|
+
* directory) -- unlike the in-memory store, which loses everything when the instance is
|
|
82
|
+
* dropped. No SQLite or other index is used; each artifact's metadata is a small sidecar
|
|
83
|
+
* JSON file next to its payload file, per the "keep SQLite out of scope unless a minimal
|
|
84
|
+
* metadata shape is unavoidable" constraint for this slice.
|
|
85
|
+
*
|
|
86
|
+
* The one thing that does NOT survive recreation: the missing-artifact reason
|
|
87
|
+
* distinction. A fresh instance has no in-memory record of which ids it personally
|
|
88
|
+
* cleaned up, so a previously-cleaned-up id reads back as "not_found" rather than
|
|
89
|
+
* "cleaned_up" after a restart. This still always returns an explicit missing marker,
|
|
90
|
+
* never fabricated or empty content -- it only affects which of the two reason codes is
|
|
91
|
+
* reported.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createFileArtifactStore(options: FileArtifactStoreOptions): ArtifactStore;
|
|
94
|
+
//# sourceMappingURL=context-artifacts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-artifacts.d.ts","sourceRoot":"","sources":["../../../src/core/context/context-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,KAAK,kBAAkB,EAAyC,MAAM,mBAAmB,CAAC;AAEnG,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,kBAAkB,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,YAAY,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,qBAAqB,CAAC;CAC9B;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,GAAG,qBAAqB,GAAG,KAAK,IAAI,qBAAqB,CAErH;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,IAAI,CACZ,oBAAoB,EACpB,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,gBAAgB,GAAG,eAAe,GAAG,cAAc,CAC1G,GACC,MAAM,CAgBR;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,OAAO,EAAE,oBAAoB,GAAG,cAAc,CAAC;IACrD,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,qBAAqB,CAAC;IACzD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAC;IACpD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACpD,yGAAyG;IACzG,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACvD,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,2FAA2F;IAC3F,OAAO,IAAI,MAAM,EAAE,CAAC;CACpB;AAQD,wBAAgB,2BAA2B,IAAI,aAAa,CA2E3D;AAED,MAAM,WAAW,wBAAwB;IACxC,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;CAChB;AA+ED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,aAAa,CA0GxF","sourcesContent":["/**\n * Artifact store abstraction (Phase 2): stable refs for raw large payloads, kept out of\n * band from prompt context. This module defines the `ArtifactStore` interface plus two\n * implementations: an in-memory one for tests, and `createFileArtifactStore` (session-\n * scoped, filesystem-backed). A SQLite-backed implementation waits until the Phase M0\n * storage-authority/location/concurrency decisions are accepted (see\n * docs/context-management-rework/memory-architecture.md).\n *\n * `createFileArtifactStore` is wired into live grep/find tool construction in\n * agent-session.ts (session-scoped under `<agentDir>/context-artifacts/<sessionId>/`).\n * References are registered at pack time and released when context-gc evicts the\n * corresponding grep/find tool result (opportunistic, conservative cleanup), with a\n * best-effort dispose-time sweep for zero-reference artifacts. Payloads are retrievable\n * out of band via the artifact_retrieve tool (context/artifact-retrieval.ts).\n */\n\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { type ContextArtifactRef, estimateByteLength, estimateLineCount } from \"./context-item.ts\";\n\nexport interface ArtifactWriteRequest {\n\tkind: ContextArtifactRef[\"kind\"];\n\tcontent: string;\n\ttoolName?: string;\n\tcommand?: string;\n\tpath?: string;\n\tsessionEntryId?: string;\n\tcreatedAtTurn: number;\n\treproducible: boolean;\n}\n\nexport interface ArtifactRecord {\n\tref: ContextArtifactRef;\n\tcontent: string;\n}\n\nexport type MissingArtifactReason = \"not_found\" | \"cleaned_up\";\n\nexport interface MissingArtifactMarker {\n\tid: string;\n\tmissing: true;\n\treason: MissingArtifactReason;\n}\n\nexport function isMissingArtifactMarker(value: ArtifactRecord | MissingArtifactMarker): value is MissingArtifactMarker {\n\treturn (value as MissingArtifactMarker).missing === true;\n}\n\n/**\n * Artifact id for a capture event, not merely a payload: it hashes every ref-defining\n * field (kind, tool/command/path, content, sessionEntryId, createdAtTurn, reproducible).\n * A repeat write with identical content but a different turn or session entry is a\n * distinct capture and must get a distinct id -- otherwise the later capture's metadata\n * would be silently discarded in favor of the first write. Only a truly identical\n * request (same capture, re-submitted) is idempotent under this id.\n */\nexport function generateArtifactId(\n\trequest: Pick<\n\t\tArtifactWriteRequest,\n\t\t\"kind\" | \"content\" | \"toolName\" | \"command\" | \"path\" | \"sessionEntryId\" | \"createdAtTurn\" | \"reproducible\"\n\t>,\n): string {\n\treturn createHash(\"sha256\")\n\t\t.update(\n\t\t\t[\n\t\t\t\trequest.kind,\n\t\t\t\trequest.toolName ?? \"\",\n\t\t\t\trequest.command ?? \"\",\n\t\t\t\trequest.path ?? \"\",\n\t\t\t\trequest.sessionEntryId ?? \"\",\n\t\t\t\tString(request.createdAtTurn),\n\t\t\t\tString(request.reproducible),\n\t\t\t\trequest.content,\n\t\t\t].join(\"\\0\"),\n\t\t)\n\t\t.digest(\"hex\")\n\t\t.slice(0, 24);\n}\n\nexport interface ArtifactStore {\n\twrite(request: ArtifactWriteRequest): ArtifactRecord;\n\tread(id: string): ArtifactRecord | MissingArtifactMarker;\n\t/**\n\t * Metadata-only lookup: the ref if `id` resolves to a live artifact, `undefined`\n\t * otherwise. Never loads the payload -- for the file store this must not touch the\n\t * payload file at all beyond an existence check, so a caller that only needs to know\n\t * \"does this still exist, and what are its ref fields\" (e.g. a per-turn audit pass)\n\t * never pays the cost of reading potentially large content off disk.\n\t */\n\treadRef(id: string): ContextArtifactRef | undefined;\n\thas(id: string): boolean;\n\t/**\n\t * Register that `holderId` (a context item id, session entry id, etc.) depends on this\n\t * artifact. Returns false if `id` does not exist (never written, or already cleaned\n\t * up) so a caller cannot believe it protected an artifact that was never registered.\n\t * Callers must fail closed (treat the artifact as unprotected) on a false return.\n\t */\n\taddReference(id: string, holderId: string): boolean;\n\t/** Release a previously registered dependency. Returns true only if a reference was actually removed. */\n\tremoveReference(id: string, holderId: string): boolean;\n\treferenceCount(id: string): number;\n\t/** Delete only artifacts with zero active references. Returns the ids actually deleted. */\n\tcleanup(): string[];\n}\n\ninterface StoredArtifact {\n\tref: ContextArtifactRef;\n\tcontent: string;\n\treferences: Set<string>;\n}\n\nexport function createInMemoryArtifactStore(): ArtifactStore {\n\tconst artifacts = new Map<string, StoredArtifact>();\n\tconst cleanedUp = new Set<string>();\n\n\treturn {\n\t\twrite(request: ArtifactWriteRequest): ArtifactRecord {\n\t\t\tconst id = generateArtifactId(request);\n\t\t\tconst existing = artifacts.get(id);\n\t\t\tif (existing) {\n\t\t\t\tcleanedUp.delete(id);\n\t\t\t\treturn { ref: existing.ref, content: existing.content };\n\t\t\t}\n\n\t\t\tconst ref: ContextArtifactRef = {\n\t\t\t\tid,\n\t\t\t\tkind: request.kind,\n\t\t\t\tsessionEntryId: request.sessionEntryId,\n\t\t\t\ttoolName: request.toolName,\n\t\t\t\tcommand: request.command,\n\t\t\t\tpath: request.path,\n\t\t\t\tbyteLength: estimateByteLength(request.content),\n\t\t\t\tlineCount: estimateLineCount(request.content),\n\t\t\t\tcreatedAtTurn: request.createdAtTurn,\n\t\t\t\treproducible: request.reproducible,\n\t\t\t};\n\t\t\tartifacts.set(id, { ref, content: request.content, references: new Set() });\n\t\t\tcleanedUp.delete(id);\n\t\t\treturn { ref, content: request.content };\n\t\t},\n\n\t\tread(id: string): ArtifactRecord | MissingArtifactMarker {\n\t\t\tconst stored = artifacts.get(id);\n\t\t\tif (!stored) {\n\t\t\t\treturn { id, missing: true, reason: cleanedUp.has(id) ? \"cleaned_up\" : \"not_found\" };\n\t\t\t}\n\t\t\treturn { ref: stored.ref, content: stored.content };\n\t\t},\n\n\t\treadRef(id: string): ContextArtifactRef | undefined {\n\t\t\treturn artifacts.get(id)?.ref;\n\t\t},\n\n\t\thas(id: string): boolean {\n\t\t\treturn artifacts.has(id);\n\t\t},\n\n\t\taddReference(id: string, holderId: string): boolean {\n\t\t\tconst stored = artifacts.get(id);\n\t\t\tif (!stored) return false;\n\t\t\tstored.references.add(holderId);\n\t\t\treturn true;\n\t\t},\n\n\t\tremoveReference(id: string, holderId: string): boolean {\n\t\t\tconst stored = artifacts.get(id);\n\t\t\tif (!stored) return false;\n\t\t\treturn stored.references.delete(holderId);\n\t\t},\n\n\t\treferenceCount(id: string): number {\n\t\t\treturn artifacts.get(id)?.references.size ?? 0;\n\t\t},\n\n\t\tcleanup(): string[] {\n\t\t\tconst deleted: string[] = [];\n\t\t\tfor (const [id, stored] of artifacts) {\n\t\t\t\tif (stored.references.size === 0) {\n\t\t\t\t\tartifacts.delete(id);\n\t\t\t\t\tcleanedUp.add(id);\n\t\t\t\t\tdeleted.push(id);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn deleted;\n\t\t},\n\t};\n}\n\nexport interface FileArtifactStoreOptions {\n\t/** Directory the store persists artifact payloads and metadata under. Created if missing. */\n\tbaseDir: string;\n}\n\ninterface PersistedArtifactMeta {\n\tref: ContextArtifactRef;\n\treferences: string[];\n}\n\nconst META_SUFFIX = \".meta.json\";\nconst PAYLOAD_SUFFIX = \".payload\";\n\n/**\n * Artifact ids are generated by `generateArtifactId` as a lowercase hex digest. Reject\n * anything else so a caller-supplied id (including one echoed back from model output)\n * can never be used as a path-traversal vector into `baseDir`.\n */\nfunction isSafeArtifactId(id: string): boolean {\n\treturn /^[0-9a-f]{1,64}$/.test(id);\n}\n\nfunction payloadPath(baseDir: string, id: string): string {\n\treturn join(baseDir, `${id}${PAYLOAD_SUFFIX}`);\n}\n\nfunction metaPath(baseDir: string, id: string): string {\n\treturn join(baseDir, `${id}${META_SUFFIX}`);\n}\n\nconst VALID_ARTIFACT_KINDS: ReadonlySet<ContextArtifactRef[\"kind\"]> = new Set([\n\t\"tool_output\",\n\t\"file_snapshot\",\n\t\"test_output\",\n\t\"diff\",\n\t\"transcript_slice\",\n]);\n\nfunction isValidArtifactRefShape(value: unknown): value is ContextArtifactRef {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst ref = value as Record<string, unknown>;\n\treturn (\n\t\ttypeof ref.id === \"string\" &&\n\t\ttypeof ref.kind === \"string\" &&\n\t\tVALID_ARTIFACT_KINDS.has(ref.kind as ContextArtifactRef[\"kind\"]) &&\n\t\ttypeof ref.byteLength === \"number\" &&\n\t\ttypeof ref.createdAtTurn === \"number\" &&\n\t\ttypeof ref.reproducible === \"boolean\"\n\t);\n}\n\n/**\n * A parsed JSON value can be syntactically valid but semantically garbage (truncated\n * write, hand-edited file, future/incompatible format). Validate shape, not just parse\n * success, so a malformed sidecar can never produce an invalid ref or crash `cleanup()` --\n * it is treated as unusable/missing, the same as a sidecar that doesn't exist.\n */\nfunction isValidPersistedArtifactMeta(value: unknown): value is PersistedArtifactMeta {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst meta = value as Record<string, unknown>;\n\treturn (\n\t\tisValidArtifactRefShape(meta.ref) &&\n\t\tArray.isArray(meta.references) &&\n\t\tmeta.references.every((entry) => typeof entry === \"string\")\n\t);\n}\n\nfunction readMeta(baseDir: string, id: string): PersistedArtifactMeta | undefined {\n\tconst path = metaPath(baseDir, id);\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(path, \"utf8\"));\n\t\treturn isValidPersistedArtifactMeta(parsed) ? parsed : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction writeMeta(baseDir: string, id: string, meta: PersistedArtifactMeta): void {\n\twriteFileSync(metaPath(baseDir, id), JSON.stringify(meta), \"utf8\");\n}\n\n/**\n * Filesystem-backed `ArtifactStore`: payload and metadata (including reference holder ids)\n * are written to `baseDir` so content, ref fields, and cleanup-protecting references all\n * survive recreating the store (e.g. across a process restart against the same\n * directory) -- unlike the in-memory store, which loses everything when the instance is\n * dropped. No SQLite or other index is used; each artifact's metadata is a small sidecar\n * JSON file next to its payload file, per the \"keep SQLite out of scope unless a minimal\n * metadata shape is unavoidable\" constraint for this slice.\n *\n * The one thing that does NOT survive recreation: the missing-artifact reason\n * distinction. A fresh instance has no in-memory record of which ids it personally\n * cleaned up, so a previously-cleaned-up id reads back as \"not_found\" rather than\n * \"cleaned_up\" after a restart. This still always returns an explicit missing marker,\n * never fabricated or empty content -- it only affects which of the two reason codes is\n * reported.\n */\nexport function createFileArtifactStore(options: FileArtifactStoreOptions): ArtifactStore {\n\tconst baseDir = options.baseDir;\n\tmkdirSync(baseDir, { recursive: true });\n\tconst cleanedUpThisInstance = new Set<string>();\n\n\treturn {\n\t\twrite(request: ArtifactWriteRequest): ArtifactRecord {\n\t\t\tconst id = generateArtifactId(request);\n\t\t\tconst existingMeta = readMeta(baseDir, id);\n\t\t\tconst existingPayloadPath = payloadPath(baseDir, id);\n\t\t\tif (existingMeta && existsSync(existingPayloadPath)) {\n\t\t\t\tcleanedUpThisInstance.delete(id);\n\t\t\t\treturn { ref: existingMeta.ref, content: readFileSync(existingPayloadPath, \"utf8\") };\n\t\t\t}\n\n\t\t\tconst ref: ContextArtifactRef = {\n\t\t\t\tid,\n\t\t\t\tkind: request.kind,\n\t\t\t\tsessionEntryId: request.sessionEntryId,\n\t\t\t\ttoolName: request.toolName,\n\t\t\t\tcommand: request.command,\n\t\t\t\tpath: request.path,\n\t\t\t\tbyteLength: estimateByteLength(request.content),\n\t\t\t\tlineCount: estimateLineCount(request.content),\n\t\t\t\tcreatedAtTurn: request.createdAtTurn,\n\t\t\t\treproducible: request.reproducible,\n\t\t\t};\n\t\t\twriteFileSync(payloadPath(baseDir, id), request.content, \"utf8\");\n\t\t\twriteMeta(baseDir, id, { ref, references: [] });\n\t\t\tcleanedUpThisInstance.delete(id);\n\t\t\treturn { ref, content: request.content };\n\t\t},\n\n\t\tread(id: string): ArtifactRecord | MissingArtifactMarker {\n\t\t\tif (!isSafeArtifactId(id)) return { id, missing: true, reason: \"not_found\" };\n\t\t\tconst meta = readMeta(baseDir, id);\n\t\t\tconst pPath = payloadPath(baseDir, id);\n\t\t\tif (!meta || !existsSync(pPath)) {\n\t\t\t\treturn { id, missing: true, reason: cleanedUpThisInstance.has(id) ? \"cleaned_up\" : \"not_found\" };\n\t\t\t}\n\t\t\treturn { ref: meta.ref, content: readFileSync(pPath, \"utf8\") };\n\t\t},\n\n\t\treadRef(id: string): ContextArtifactRef | undefined {\n\t\t\tif (!isSafeArtifactId(id)) return undefined;\n\t\t\tconst meta = readMeta(baseDir, id);\n\t\t\tif (!meta || !existsSync(payloadPath(baseDir, id))) return undefined;\n\t\t\treturn meta.ref;\n\t\t},\n\n\t\thas(id: string): boolean {\n\t\t\tif (!isSafeArtifactId(id)) return false;\n\t\t\treturn readMeta(baseDir, id) !== undefined && existsSync(payloadPath(baseDir, id));\n\t\t},\n\n\t\taddReference(id: string, holderId: string): boolean {\n\t\t\tif (!isSafeArtifactId(id)) return false;\n\t\t\tconst meta = readMeta(baseDir, id);\n\t\t\tif (!meta || !existsSync(payloadPath(baseDir, id))) return false;\n\t\t\tif (!meta.references.includes(holderId)) {\n\t\t\t\tmeta.references.push(holderId);\n\t\t\t\twriteMeta(baseDir, id, meta);\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tremoveReference(id: string, holderId: string): boolean {\n\t\t\tif (!isSafeArtifactId(id)) return false;\n\t\t\tconst meta = readMeta(baseDir, id);\n\t\t\tif (!meta) return false;\n\t\t\tconst index = meta.references.indexOf(holderId);\n\t\t\tif (index === -1) return false;\n\t\t\tmeta.references.splice(index, 1);\n\t\t\twriteMeta(baseDir, id, meta);\n\t\t\treturn true;\n\t\t},\n\n\t\treferenceCount(id: string): number {\n\t\t\tif (!isSafeArtifactId(id)) return 0;\n\t\t\treturn readMeta(baseDir, id)?.references.length ?? 0;\n\t\t},\n\n\t\tcleanup(): string[] {\n\t\t\tconst deleted: string[] = [];\n\t\t\tfor (const entry of readdirSync(baseDir)) {\n\t\t\t\tif (!entry.endsWith(META_SUFFIX)) continue;\n\t\t\t\tconst id = entry.slice(0, -META_SUFFIX.length);\n\t\t\t\tif (!isSafeArtifactId(id)) continue;\n\t\t\t\tconst meta = readMeta(baseDir, id);\n\t\t\t\tif (!meta || meta.references.length > 0) continue;\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(metaPath(baseDir, id));\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(payloadPath(baseDir, id));\n\t\t\t\t} catch {\n\t\t\t\t\t// Payload already gone; metadata removal above is what matters for reachability.\n\t\t\t\t}\n\t\t\t\tcleanedUpThisInstance.add(id);\n\t\t\t\tdeleted.push(id);\n\t\t\t}\n\t\t\treturn deleted;\n\t\t},\n\t};\n}\n"]}
|