@iowarp/clio-coder 0.3.3 → 0.3.6
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 +74 -0
- package/CONTRIBUTING.md +7 -7
- package/README.md +3 -3
- package/dist/{acp-P2AQILE2.js → acp-2BEHC4DL.js} +9 -8
- package/dist/{agents-72W3BI7I.js → agents-LNNFTM53.js} +29 -24
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-5TWEIYDN.js → auth-KXXFI2VS.js} +14 -10
- package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
- package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
- package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
- package/dist/chunk-3BPUFZDL.js +37 -0
- package/dist/chunk-43AOLP7E.js +375 -0
- package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
- package/dist/{chunk-6SGHMWE3.js → chunk-5JGRAMKL.js} +5 -5
- package/dist/{chunk-V6RTAOC2.js → chunk-6US73PDB.js} +572 -51
- package/dist/{chunk-5UFT4SUX.js → chunk-6XXKFVSN.js} +3 -3
- package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
- package/dist/chunk-AOCYTWAV.js +449 -0
- package/dist/chunk-CFGTUFWB.js +67 -0
- package/dist/chunk-CJUB2JJ2.js +1478 -0
- package/dist/{chunk-FNTMWMX5.js → chunk-CKXWIANG.js} +14 -12
- package/dist/{chunk-PIWWS5BL.js → chunk-CYQKWTG3.js} +63 -78
- package/dist/{chunk-LZSJBIVT.js → chunk-DJVECN66.js} +271 -762
- package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
- package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
- package/dist/{chunk-STBPMHSX.js → chunk-EKY57CSP.js} +51 -84
- package/dist/{chunk-DUYJ5IO6.js → chunk-EYPA3EGJ.js} +12 -4
- package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
- package/dist/chunk-FYYLNIL5.js +313 -0
- package/dist/{chunk-OQ33BKR3.js → chunk-G7MUEIGA.js} +3 -60
- package/dist/chunk-GEYXPTRF.js +613 -0
- package/dist/chunk-GOXNB3AO.js +261 -0
- package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
- package/dist/chunk-HWUFFB6L.js +83 -0
- package/dist/{chunk-4XUGQOHA.js → chunk-K7T3E2SR.js} +15 -8
- package/dist/chunk-K7VKOLQQ.js +15 -0
- package/dist/{chunk-UFIIWP2H.js → chunk-KHSFENX2.js} +8 -8
- package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
- package/dist/chunk-LCGCVYZ4.js +57 -0
- package/dist/chunk-LL4KHSZI.js +22 -0
- package/dist/{chunk-PAJK6MAQ.js → chunk-LYF7OHWH.js} +42 -15
- package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
- package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
- package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
- package/dist/{chunk-TZK7PACC.js → chunk-NILBFAPG.js} +14 -8
- package/dist/chunk-ODFEOB4F.js +1082 -0
- package/dist/{chunk-AGYYIBLL.js → chunk-OH3TOQTB.js} +6 -2
- package/dist/chunk-OZNBF4L3.js +23 -0
- package/dist/{chunk-DSELYM6W.js → chunk-PBTHKCPN.js} +30 -10
- package/dist/{verify-375KUB3Y.js → chunk-PCZJO5TI.js} +127 -42
- package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
- package/dist/{chunk-SRDMMSEP.js → chunk-QM3F2GKX.js} +1063 -1645
- package/dist/{chunk-X6IAEBZR.js → chunk-QNQHSOLF.js} +7 -7
- package/dist/{chunk-OC7FIQPC.js → chunk-R46L2BIR.js} +10 -7
- package/dist/{chunk-2TLUCQVG.js → chunk-RD5U66HV.js} +3 -3
- package/dist/{chunk-6N5PTWMY.js → chunk-RY3LY4J5.js} +50 -13
- package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
- package/dist/{chunk-OOJYHWRB.js → chunk-TSHXZTOQ.js} +6 -5
- package/dist/chunk-TZSKNMZG.js +434 -0
- package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
- package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
- package/dist/{chunk-COU2UHX6.js → chunk-VEZEGCGW.js} +170 -2
- package/dist/chunk-W6GROXXM.js +69 -0
- package/dist/{chunk-OAO4GE4M.js → chunk-WHGPSPT5.js} +2 -2
- package/dist/chunk-WHJYKASB.js +677 -0
- package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
- package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
- package/dist/{chunk-TZTZS7QK.js → chunk-XE2VEJHX.js} +5 -3
- package/dist/{chunk-LM5TQCJZ.js → chunk-XF5N4U5A.js} +8 -7
- package/dist/{chunk-LWLEKMDQ.js → chunk-XXQNGV4M.js} +1073 -552
- package/dist/{chunk-KZWTDYJF.js → chunk-XYDYPRZI.js} +7 -7
- package/dist/chunk-ZGVHUX3M.js +66 -0
- package/dist/{chunk-LW6DSM3M.js → chunk-ZRGEBJ4T.js} +1192 -1119
- package/dist/{chunk-2DJ2KNFG.js → chunk-ZXF4XRKW.js} +202 -40
- package/dist/chunk-ZZMN5OM4.js +122 -0
- package/dist/cli/index.js +34 -30
- package/dist/{clio-JOU4FXVA.js → clio-M2KGYUFZ.js} +7 -6
- package/dist/{code-nav-7AX6FYE6.js → code-nav-GQNL7XA6.js} +8 -6
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
- package/dist/{config-XCDVKR23.js → config-XUUYQIWO.js} +47 -35
- package/dist/{configure-4GAP54ZW.js → configure-IHJ7YOMV.js} +18 -15
- package/dist/{context-77FM5DV5.js → context-74JLXAWD.js} +18 -10
- package/dist/{context-4UOGGLQ5.js → context-75MIWW3U.js} +41 -29
- package/dist/{context-5VKGUVJJ.js → context-ZQ7SIFJV.js} +85 -9
- package/dist/{context-clear-XXJRLCJJ.js → context-clear-GYKWNUML.js} +41 -29
- package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
- package/dist/context-working-set-UX5KEP4J.js +1553 -0
- package/dist/{dispatch-runner-QPRDDBDX.js → dispatch-runner-GIJBHNFL.js} +47 -32
- package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
- package/dist/{doctor-HR46URBJ.js → doctor-SVJ5BZCW.js} +12 -12
- package/dist/{eval-XSSNATB4.js → eval-CG6LLBLD.js} +54 -238
- package/dist/{evidence-6HG2PY2B.js → evidence-ZYFIEN42.js} +57 -28
- package/dist/{evolve-K7YU3NCY.js → evolve-QGEXEMDW.js} +36 -25
- package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
- package/dist/{fleet-VY3HHKN6.js → fleet-S5R4ZOQY.js} +73 -44
- package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-BHSNPBMH.js} +3 -3
- package/dist/{init-JYGXI3FK.js → init-5DRU55YR.js} +49 -37
- package/dist/memory-7YKKR6UC.js +467 -0
- package/dist/{models-I5QWSEOM.js → models-ZPOLRU2C.js} +24 -21
- package/dist/{monitor-GE4ID3IA.js → monitor-US5F5YGZ.js} +73 -46
- package/dist/{orchestrator-EM5MC3HM.js → orchestrator-E2AL4T5N.js} +1624 -1007
- package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
- package/dist/{reset-L2FQEE3E.js → reset-KZ652EK6.js} +6 -5
- package/dist/{run-ZU3QMZPZ.js → run-SRNBKDWD.js} +76 -54
- package/dist/{share-S5BZQC5I.js → share-CGZE33UP.js} +7 -6
- package/dist/{skills-X5VXCRNQ.js → skills-S2X4DLY5.js} +4 -4
- package/dist/{skills-eval-WKIHWTHR.js → skills-eval-W2GGIC4R.js} +40 -29
- package/dist/{targets-SNCPI2NR.js → targets-54SWINWB.js} +28 -23
- package/dist/{terminal-lease-BNAHVHBS.js → terminal-lease-SAIF2OGY.js} +6 -4
- package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
- package/dist/{upgrade-JQHHPQ4K.js → upgrade-JKAR27XC.js} +20 -19
- package/dist/{usage-OR4O5SMZ.js → usage-MSAWCLX4.js} +79 -36
- package/dist/verifiers-NCBTHHN2.js +1220 -0
- package/dist/verify-X5HDROLA.js +25 -0
- package/dist/{wiki-generate-UEXP2ARI.js → wiki-generate-GUSOQ6ZP.js} +50 -37
- package/dist/worker/entry.js +90 -70
- package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
- package/docs/README.md +8 -7
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-placement.md +1 -2
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +60 -26
- package/docs/config-knobs-audit.md +1 -2
- package/docs/configuration-and-targets.md +26 -1
- package/docs/context-engine.md +67 -13
- package/docs/context-working-set.md +194 -0
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +6 -6
- package/docs/documentation-guide.md +7 -6
- package/docs/environment-variables.md +2 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +4 -32
- package/docs/evidence-and-memory.md +139 -7
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +1 -1
- package/docs/extensions-and-sharing.md +2 -2
- package/docs/fleet-dispatch.md +49 -8
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +19 -2
- package/docs/model-catalog.md +7 -9
- package/docs/observability.md +4 -4
- package/docs/proactive-memory.md +26 -16
- package/docs/prompt-envelope-and-tools.md +7 -5
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +43 -40
- package/docs/safety-model.md +49 -8
- package/docs/scientific-validation.md +21 -3
- package/docs/session-lifecycle.md +3 -3
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +79 -12
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +38 -4
- package/docs/worker-dispatch-mechanics.md +11 -1
- package/package.json +13 -13
- package/skills/meta/clio-test/SKILL.md +20 -17
- package/skills/meta/clio-test/evals.md +3 -3
- package/skills/meta/clio-test/references/harness.md +35 -6
- package/skills/meta/clio-test/references/test-map.md +20 -10
- package/skills/registry.yaml +2 -2
- package/skills/skill-marketplace.json +1 -1
- package/src/cli/agents.ts +2 -3
- package/src/cli/argv.ts +14 -1
- package/src/cli/context-working-set.ts +513 -0
- package/src/cli/context.ts +8 -0
- package/src/cli/evidence.ts +20 -2
- package/src/cli/fleet.ts +15 -0
- package/src/cli/index.ts +5 -1
- package/src/cli/memory.ts +272 -10
- package/src/cli/modes/json-stream.ts +2 -2
- package/src/cli/modes/print.ts +12 -1
- package/src/cli/run.ts +22 -2
- package/src/cli/targets.ts +12 -3
- package/src/cli/usage.ts +55 -7
- package/src/cli/verifiers.ts +325 -0
- package/src/core/bash-exec.ts +39 -14
- package/src/core/bus-events.ts +22 -4
- package/src/core/config.ts +54 -0
- package/src/core/defaults.ts +50 -3
- package/src/core/response-model-id.ts +134 -0
- package/src/core/toml.ts +62 -0
- package/src/core/verification-scripts.ts +6 -0
- package/src/core/workspace-files.ts +0 -1
- package/src/domains/agents/builtins/architect.md +1 -1
- package/src/domains/agents/builtins/verifier.md +3 -0
- package/src/domains/agents/catalog.ts +5 -4
- package/src/domains/agents/recipe.ts +54 -14
- package/src/domains/agents/result-contract.ts +7 -4
- package/src/domains/config/classify.ts +1 -0
- package/src/domains/context/bootstrap.ts +36 -27
- package/src/domains/context/project-metadata.ts +19 -63
- package/src/domains/context/prompt-context.ts +8 -0
- package/src/domains/context/working-set/contract.ts +161 -0
- package/src/domains/context/working-set/defaults.ts +28 -0
- package/src/domains/context/working-set/engine.ts +203 -0
- package/src/domains/context/working-set/fold.ts +62 -0
- package/src/domains/context/working-set/horizon.ts +38 -0
- package/src/domains/context/working-set/marker.ts +103 -0
- package/src/domains/context/working-set/path-index.ts +436 -0
- package/src/domains/context/working-set/payload.ts +152 -0
- package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
- package/src/domains/context/working-set/policies/index.ts +20 -0
- package/src/domains/context/working-set/policies/structural.ts +160 -0
- package/src/domains/context/working-set/project.ts +132 -0
- package/src/domains/context/working-set/protect.ts +109 -0
- package/src/domains/context/working-set/recall.ts +177 -0
- package/src/domains/context/working-set/replay/controls.ts +112 -0
- package/src/domains/context/working-set/replay/load-clio.ts +199 -0
- package/src/domains/context/working-set/replay/metrics.ts +185 -0
- package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
- package/src/domains/context/working-set/replay/report.ts +139 -0
- package/src/domains/context/working-set/replay/runner.ts +325 -0
- package/src/domains/context/working-set/replay/synthetic.ts +422 -0
- package/src/domains/context/working-set/replay/trace.ts +21 -0
- package/src/domains/context/working-set/visible.ts +54 -0
- package/src/domains/dispatch/budget-envelope.ts +396 -0
- package/src/domains/dispatch/contract.ts +2 -0
- package/src/domains/dispatch/extension.ts +81 -27
- package/src/domains/dispatch/orphan-recovery.ts +1 -0
- package/src/domains/dispatch/receipt-integrity.ts +4 -0
- package/src/domains/dispatch/state.ts +1 -0
- package/src/domains/dispatch/types.ts +10 -3
- package/src/domains/dispatch/validation.ts +14 -0
- package/src/domains/dispatch/worker-spawn.ts +14 -3
- package/src/domains/eval/metrics/evidence.ts +0 -116
- package/src/domains/eval/metrics/invariants.ts +1 -1
- package/src/domains/eval/runners/clio-run.ts +1 -10
- package/src/domains/eval/runners/external-command.ts +2 -29
- package/src/domains/eval/schema/suite.ts +0 -7
- package/src/domains/eval/suites/run.ts +1 -7
- package/src/domains/evidence/build.ts +112 -45
- package/src/domains/evidence/eval.ts +24 -7
- package/src/domains/evidence/index.ts +53 -0
- package/src/domains/evidence/ordering.ts +12 -0
- package/src/domains/evidence/run-trust.ts +221 -0
- package/src/domains/evidence/store.ts +46 -6
- package/src/domains/evidence/trust-status.ts +854 -0
- package/src/domains/evidence/types.ts +26 -0
- package/src/domains/memory/index.ts +22 -0
- package/src/domains/memory/operations.ts +58 -1
- package/src/domains/memory/promotion.ts +281 -0
- package/src/domains/memory/prompt-section.ts +25 -5
- package/src/domains/memory/proposal.ts +51 -7
- package/src/domains/memory/task-bank.ts +3 -2
- package/src/domains/memory/task-memory-handoff.ts +181 -24
- package/src/domains/memory/task-memory-policy.ts +3 -1
- package/src/domains/memory/types.ts +37 -0
- package/src/domains/memory/validate.ts +178 -0
- package/src/domains/middleware/memory-intervention.ts +38 -25
- package/src/domains/middleware/runtime.ts +6 -0
- package/src/domains/middleware/skills-reminder.ts +19 -4
- package/src/domains/middleware/stalled-turn.ts +208 -5
- package/src/domains/middleware/types.ts +10 -0
- package/src/domains/observability/contract.ts +6 -1
- package/src/domains/observability/cost.ts +20 -4
- package/src/domains/observability/extension.ts +2 -2
- package/src/domains/providers/index.ts +3 -0
- package/src/domains/providers/model-discovery.ts +9 -0
- package/src/domains/providers/runtime-resolution.ts +38 -1
- package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
- package/src/domains/providers/types/context-window-slots.ts +18 -0
- package/src/domains/providers/types/runtime-descriptor.ts +3 -1
- package/src/domains/safety/autonomy.ts +1 -1
- package/src/domains/safety/call-target.ts +211 -14
- package/src/domains/safety/decision-presentation.ts +268 -0
- package/src/domains/safety/default-path-policy.ts +8 -0
- package/src/domains/safety/finish-contract.ts +4 -3
- package/src/domains/safety/policy-engine.ts +48 -6
- package/src/domains/safety/redaction.ts +73 -0
- package/src/domains/session/compaction/compact.ts +23 -1
- package/src/domains/session/compaction/cut-point.ts +2 -0
- package/src/domains/session/compaction/tokens.ts +16 -1
- package/src/domains/session/context-ledger.ts +12 -1
- package/src/domains/session/decision-board.ts +4 -0
- package/src/domains/session/entries.ts +110 -1
- package/src/domains/session/history.ts +68 -19
- package/src/domains/session/manager.ts +9 -2
- package/src/domains/session/migrations/index.ts +22 -3
- package/src/domains/session/usage.ts +24 -7
- package/src/engine/acp/event-mapper.ts +7 -0
- package/src/engine/acp/server.ts +32 -2
- package/src/engine/agent.ts +18 -1
- package/src/engine/apis/lmstudio.ts +25 -4
- package/src/engine/apis/openai-completions.ts +147 -22
- package/src/engine/claude/sdk-runtime.ts +8 -2
- package/src/engine/claude/tool-safety.ts +13 -0
- package/src/engine/loop-guard.ts +27 -3
- package/src/engine/session.ts +9 -3
- package/src/engine/worker-events.ts +4 -3
- package/src/engine/worker-runtime.ts +59 -54
- package/src/entry/orchestrator.ts +34 -5
- package/src/interactive/chat-loop-messages.ts +40 -6
- package/src/interactive/chat-loop.ts +13 -0
- package/src/interactive/chat-panel.ts +17 -1
- package/src/interactive/chat-renderer.ts +49 -24
- package/src/interactive/clio-editor.ts +44 -7
- package/src/interactive/context-meter.ts +10 -0
- package/src/interactive/context-overlay.ts +120 -7
- package/src/interactive/context-recall-command.ts +110 -0
- package/src/interactive/cost-overlay.ts +39 -8
- package/src/interactive/dispatch-board.ts +212 -35
- package/src/interactive/footer/widgets.ts +13 -0
- package/src/interactive/interactive-application.ts +6 -1
- package/src/interactive/interactive-input-runtime.ts +11 -1
- package/src/interactive/interactive-presentation.ts +11 -1
- package/src/interactive/interactive-slash-runtime.ts +37 -1
- package/src/interactive/memory-overlay.ts +89 -4
- package/src/interactive/model-session-replay.ts +21 -0
- package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
- package/src/interactive/overlay-frame.ts +5 -2
- package/src/interactive/overlay-general-openers.ts +46 -1
- package/src/interactive/overlay-key-routing.ts +41 -1
- package/src/interactive/overlay-lifecycle.ts +11 -4
- package/src/interactive/overlay-permission-lifecycle.ts +23 -8
- package/src/interactive/overlay-session-lifecycle.ts +8 -4
- package/src/interactive/overlay-transitions.ts +11 -0
- package/src/interactive/overlays/ask-user.ts +74 -30
- package/src/interactive/overlays/decisions.ts +3 -1
- package/src/interactive/permission-hint.ts +35 -0
- package/src/interactive/permission-overlay.ts +95 -45
- package/src/interactive/renderers/tool-execution.ts +37 -51
- package/src/interactive/session-last-turn.ts +8 -1
- package/src/interactive/session-transcript.ts +2 -2
- package/src/interactive/session-usage-reseed.ts +36 -10
- package/src/interactive/slash-commands.ts +31 -4
- package/src/interactive/status/summary.ts +5 -0
- package/src/interactive/status/types.ts +5 -0
- package/src/interactive/terminal-lease.ts +1 -0
- package/src/interactive/turn-context.ts +333 -110
- package/src/interactive/turn-middleware.ts +7 -6
- package/src/interactive/turn-runtime.ts +37 -8
- package/src/interactive/turn-state.ts +3 -0
- package/src/interactive/worker-progress.ts +440 -0
- package/src/interactive/worker-stream.ts +51 -110
- package/src/tools/agent-tools.ts +39 -7
- package/src/tools/ask-user.ts +21 -1
- package/src/tools/bash.ts +144 -82
- package/src/tools/builtin-tool-catalog.ts +11 -5
- package/src/tools/context/index.ts +107 -5
- package/src/tools/context/surface.ts +3 -2
- package/src/tools/core-bootstrap.ts +21 -0
- package/src/tools/dispatch-arguments.ts +8 -0
- package/src/tools/dispatch-event-text.ts +19 -0
- package/src/tools/dispatch-runner.ts +9 -7
- package/src/tools/dispatch.ts +24 -1
- package/src/tools/monitor.ts +43 -20
- package/src/tools/registry.ts +72 -10
- package/src/tools/result-disposition.ts +706 -0
- package/src/tools/result-shaping.ts +321 -20
- package/src/tools/safe-exec.ts +2 -0
- package/src/tools/verify/authoring.ts +1120 -0
- package/src/tools/verify/catalog.ts +346 -0
- package/src/tools/verify/index.ts +13 -3
- package/src/tools/verify/scripts.ts +135 -37
- package/src/tools/verify/surface.ts +9 -5
- package/src/tools/worker-evidence.ts +54 -12
- package/src/worker/spec-contract.ts +43 -3
- package/dist/chunk-J7CWMCQD.js +0 -255
- package/dist/chunk-T6YILFSB.js +0 -80
- package/dist/chunk-VAKQQHWR.js +0 -434
- package/dist/chunk-VPAYEGVX.js +0 -184
- package/dist/chunk-XBXAASKX.js +0 -18
- package/dist/memory-WFZMGYHX.js +0 -236
- package/src/domains/eval/metrics/chaos-stream.ts +0 -93
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { parse as parseYaml } from "yaml";
|
|
22
|
+
import { createTomlFileReader, type TomlFileReader, tomlTableAt } from "../../core/toml.js";
|
|
22
23
|
|
|
23
24
|
/** Longest description worth carrying into a one-sentence identity line. */
|
|
24
25
|
const MAX_DESCRIPTION_CHARS = 400;
|
|
@@ -39,7 +40,7 @@ interface MetadataFragment {
|
|
|
39
40
|
description?: string | null;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
type MetadataReader = (cwd: string) => { file: string; fragment: MetadataFragment } | null;
|
|
43
|
+
type MetadataReader = (cwd: string, tomlFiles: TomlFileReader) => { file: string; fragment: MetadataFragment } | null;
|
|
43
44
|
|
|
44
45
|
function readText(cwd: string, relative: string): string | null {
|
|
45
46
|
try {
|
|
@@ -83,54 +84,6 @@ function jsonRecord(cwd: string, relative: string): Record<string, unknown> | nu
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
* One key from one TOML table, without a TOML parser.
|
|
88
|
-
*
|
|
89
|
-
* Deliberately narrow: it reads `key = "value"` (single-line, single- or
|
|
90
|
-
* double-quoted) from the named top-level table and understands nothing else.
|
|
91
|
-
* That is the entire shape `description` and `name` take in `pyproject.toml`
|
|
92
|
-
* and `Cargo.toml`. A multi-line or otherwise exotic value reads as absent,
|
|
93
|
-
* which costs a description and never produces a wrong one.
|
|
94
|
-
*/
|
|
95
|
-
function tomlTableValue(raw: string, table: string, key: string): string | null {
|
|
96
|
-
const lines = raw.split(/\r?\n/u);
|
|
97
|
-
let inTable = false;
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
const trimmed = line.trim();
|
|
100
|
-
if (trimmed.startsWith("[")) {
|
|
101
|
-
inTable = trimmed === `[${table}]`;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (!inTable) continue;
|
|
105
|
-
const match = /^([A-Za-z0-9_-]+)\s*=\s*(.*)$/u.exec(trimmed);
|
|
106
|
-
if (!match || match[1] !== key) continue;
|
|
107
|
-
const value = (match[2] ?? "").trim();
|
|
108
|
-
const quoted = /^"((?:[^"\\]|\\.)*)"$/u.exec(value) ?? /^'([^']*)'$/u.exec(value);
|
|
109
|
-
if (!quoted) return null;
|
|
110
|
-
return (quoted[1] ?? "").replace(/\\"/gu, '"');
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Whether a TOML file declares the named top-level table, e.g. `tool.tox`.
|
|
117
|
-
* A trimmed line must start with `[table]` and carry nothing after it but an
|
|
118
|
-
* optional `# comment`, the only content TOML itself allows there. A raw
|
|
119
|
-
* `raw.includes("[table]")` check would also fire on a comment mentioning the
|
|
120
|
-
* table or a string value that happens to contain the same bracketed text;
|
|
121
|
-
* anchoring to the start of the line rules both out without a TOML parser,
|
|
122
|
-
* while still accepting the valid `[table] # comment` form that a plain
|
|
123
|
-
* equality check would have missed.
|
|
124
|
-
*/
|
|
125
|
-
export function hasTomlTable(raw: string, table: string): boolean {
|
|
126
|
-
const header = `[${table}]`;
|
|
127
|
-
return raw.split(/\r?\n/u).some((line) => {
|
|
128
|
-
const trimmed = line.trim();
|
|
129
|
-
if (!trimmed.startsWith(header)) return false;
|
|
130
|
-
return /^\s*(?:#.*)?$/u.test(trimmed.slice(header.length));
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
87
|
/** One `KEY = value` line from a Doxyfile or an INI-shaped config. */
|
|
135
88
|
function keyEqualsValue(raw: string, key: string): string | null {
|
|
136
89
|
const match = new RegExp(`^[ \\t]*${key}[ \\t]*(?:\\+?=)[ \\t]*(.+)$`, "mu").exec(raw);
|
|
@@ -182,27 +135,30 @@ const packageJson: MetadataReader = (cwd) => {
|
|
|
182
135
|
return { file: "package.json", fragment: { name: name(record.name), description: description(record.description) } };
|
|
183
136
|
};
|
|
184
137
|
|
|
185
|
-
const pyprojectToml: MetadataReader = (
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
138
|
+
const pyprojectToml: MetadataReader = (_cwd, tomlFiles) => {
|
|
139
|
+
const document = tomlFiles.read("pyproject.toml");
|
|
140
|
+
if (document === null) return null;
|
|
141
|
+
const project = tomlTableAt(document, ["project"]);
|
|
142
|
+
const poetry = tomlTableAt(document, ["tool", "poetry"]);
|
|
188
143
|
// PEP 621 `[project]` first, then Poetry's older table.
|
|
189
|
-
const projectName = tomlTableValue(raw, "project", "name") ?? tomlTableValue(raw, "tool.poetry", "name");
|
|
190
|
-
const projectDescription =
|
|
191
|
-
tomlTableValue(raw, "project", "description") ?? tomlTableValue(raw, "tool.poetry", "description");
|
|
192
144
|
return {
|
|
193
145
|
file: "pyproject.toml",
|
|
194
|
-
fragment: {
|
|
146
|
+
fragment: {
|
|
147
|
+
name: name(project?.name) ?? name(poetry?.name),
|
|
148
|
+
description: description(project?.description) ?? description(poetry?.description),
|
|
149
|
+
},
|
|
195
150
|
};
|
|
196
151
|
};
|
|
197
152
|
|
|
198
|
-
const cargoToml: MetadataReader = (
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
153
|
+
const cargoToml: MetadataReader = (_cwd, tomlFiles) => {
|
|
154
|
+
const document = tomlFiles.read("Cargo.toml");
|
|
155
|
+
if (document === null) return null;
|
|
156
|
+
const packageTable = tomlTableAt(document, ["package"]);
|
|
201
157
|
return {
|
|
202
158
|
file: "Cargo.toml",
|
|
203
159
|
fragment: {
|
|
204
|
-
name: name(
|
|
205
|
-
description: description(
|
|
160
|
+
name: name(packageTable?.name),
|
|
161
|
+
description: description(packageTable?.description),
|
|
206
162
|
},
|
|
207
163
|
};
|
|
208
164
|
};
|
|
@@ -488,13 +444,13 @@ function readmeTitle(content: string): string | null {
|
|
|
488
444
|
* a CMake project that names itself but describes itself only in its README
|
|
489
445
|
* should get both, not one or neither.
|
|
490
446
|
*/
|
|
491
|
-
export function readProjectMetadata(cwd: string): ProjectMetadata {
|
|
447
|
+
export function readProjectMetadata(cwd: string, tomlFiles = createTomlFileReader(cwd)): ProjectMetadata {
|
|
492
448
|
const out: ProjectMetadata = { name: null, description: null, nameSource: null, descriptionSource: null };
|
|
493
449
|
for (const reader of METADATA_READERS) {
|
|
494
450
|
if (out.name !== null && out.description !== null) break;
|
|
495
451
|
let read: ReturnType<MetadataReader>;
|
|
496
452
|
try {
|
|
497
|
-
read = reader(cwd);
|
|
453
|
+
read = reader(cwd, tomlFiles);
|
|
498
454
|
} catch {
|
|
499
455
|
continue;
|
|
500
456
|
}
|
|
@@ -20,6 +20,10 @@ import { wikiCompleteness, wikiStaleness } from "./wiki/staleness.js";
|
|
|
20
20
|
* context-init preload reporting, and `clio-coder config inspect`, so every surface
|
|
21
21
|
* measures the same text the session prompt would preload.
|
|
22
22
|
*/
|
|
23
|
+
/** The one line that stands in for a handbook this workspace does not have. */
|
|
24
|
+
export const HANDBOOK_ABSENT_FRAGMENT =
|
|
25
|
+
"<handbook>none: this workspace has no CLIO-CODER.md, so do not read one; learn the repository from its files, and the operator can run /context init to write a handbook</handbook>";
|
|
26
|
+
|
|
23
27
|
export function renderPromptContext(cwd: string): ProjectPromptContext {
|
|
24
28
|
const projectType = detectProjectType(cwd);
|
|
25
29
|
const pieces = [renderProjectTypeFragment(projectType)];
|
|
@@ -30,6 +34,10 @@ export function renderPromptContext(cwd: string): ProjectPromptContext {
|
|
|
30
34
|
for (const issue of loadedClioMd.errors) {
|
|
31
35
|
warnings.push(`clio: malformed ${issue.path} ignored: ${issue.error}`);
|
|
32
36
|
}
|
|
37
|
+
// Said where the handbook would have been: a model that sees no project
|
|
38
|
+
// context spends its first tool call reading CLIO-CODER.md and gets
|
|
39
|
+
// ENOENT, while the operator's header already says it is missing (#191).
|
|
40
|
+
if (loadedClioMd.files.length === 0 && loadedClioMd.errors.length === 0) pieces.push(HANDBOOK_ABSENT_FRAGMENT);
|
|
33
41
|
const codewiki = readCodewiki(cwd);
|
|
34
42
|
if (codewiki) {
|
|
35
43
|
const state = readClioState(cwd);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working-set layer contract.
|
|
3
|
+
*
|
|
4
|
+
* The working set is what the model sees on the next request. The ledger is
|
|
5
|
+
* durable, append-only truth. This layer decides which tool-result bodies and
|
|
6
|
+
* thinking blocks leave the working set, records that decision as
|
|
7
|
+
* `contextEviction` entries, and applies it as an in-memory projection when
|
|
8
|
+
* the replay messages are built. Nothing here rewrites the ledger and nothing
|
|
9
|
+
* here calls a model.
|
|
10
|
+
*
|
|
11
|
+
* Shared by the live engine and the replay-lite runner by construction: a
|
|
12
|
+
* policy is a pure function of `PolicyInput`, so the same selection runs in
|
|
13
|
+
* both places. Widening these types is an owner decision; workers build
|
|
14
|
+
* against them.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { WorkingSetPolicyId, WorkingSetSettings } from "../../../core/defaults.js";
|
|
18
|
+
import type {
|
|
19
|
+
ContextEvictionEntry,
|
|
20
|
+
ContextRecallEntry,
|
|
21
|
+
EvictedItem,
|
|
22
|
+
EvictionReason,
|
|
23
|
+
EvictionTrigger,
|
|
24
|
+
RecallTrigger,
|
|
25
|
+
SessionEntry,
|
|
26
|
+
WorkingSetRef,
|
|
27
|
+
} from "../../session/entries.js";
|
|
28
|
+
|
|
29
|
+
export { EVICTION_REASONS, EVICTION_TRIGGERS, RECALL_TRIGGERS } from "../../session/entries.js";
|
|
30
|
+
export type {
|
|
31
|
+
ContextEvictionEntry,
|
|
32
|
+
ContextRecallEntry,
|
|
33
|
+
EvictedItem,
|
|
34
|
+
EvictionReason,
|
|
35
|
+
EvictionTrigger,
|
|
36
|
+
RecallTrigger,
|
|
37
|
+
WorkingSetPolicyId,
|
|
38
|
+
WorkingSetRef,
|
|
39
|
+
WorkingSetSettings,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What the fold knows about one evicted unit. Keyed in `WorkingSetView.evicted`
|
|
44
|
+
* by the entry turnId (`ref.entry`); `fold.ts` owns `refKey` / `parseRefKey`.
|
|
45
|
+
*/
|
|
46
|
+
export interface EvictedState {
|
|
47
|
+
reason: EvictionReason;
|
|
48
|
+
marker: string;
|
|
49
|
+
by?: string;
|
|
50
|
+
tokensFreed: number;
|
|
51
|
+
/** turnId of the `contextEviction` entry that evicted it (the latest one, after churn). */
|
|
52
|
+
evictedAtTurnId: string;
|
|
53
|
+
policyId: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The fold of every `contextEviction` / `contextRecall` entry on the active
|
|
58
|
+
* path. A recall does not remove its key: the recalled body lives in the
|
|
59
|
+
* recall tool result at the tail of the working set, the marker stays at the
|
|
60
|
+
* original position so the prefix cache is untouched, and repeated recalls of
|
|
61
|
+
* one ref are the churn signal.
|
|
62
|
+
*/
|
|
63
|
+
export interface WorkingSetView {
|
|
64
|
+
evicted: ReadonlyMap<string, EvictedState>;
|
|
65
|
+
/** Applied eviction events on the active path. */
|
|
66
|
+
evictionEvents: number;
|
|
67
|
+
/** Items evicted across all events, including re-evictions after recall. */
|
|
68
|
+
itemsEvicted: number;
|
|
69
|
+
/** Recall entries on the active path. `churn = recalls / itemsEvicted`. */
|
|
70
|
+
recalls: number;
|
|
71
|
+
/** Policy that produced the most recent event; null when none. */
|
|
72
|
+
lastPolicyId: string | null;
|
|
73
|
+
/** turnId of the most recent eviction event; null when none. */
|
|
74
|
+
lastEvictionTurnId: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const EMPTY_WORKING_SET_VIEW: WorkingSetView = Object.freeze({
|
|
78
|
+
evicted: new Map<string, EvictedState>(),
|
|
79
|
+
evictionEvents: 0,
|
|
80
|
+
itemsEvicted: 0,
|
|
81
|
+
recalls: 0,
|
|
82
|
+
lastPolicyId: null,
|
|
83
|
+
lastEvictionTurnId: null,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export interface PressureInput {
|
|
87
|
+
/** Estimated tokens in the current working set (projected), same estimator as the live pressure check. */
|
|
88
|
+
tokens: number;
|
|
89
|
+
contextWindow: number;
|
|
90
|
+
/** `compaction.threshold`. */
|
|
91
|
+
threshold: number;
|
|
92
|
+
/** `context.workingSet.target`: the ratio an applied event batches down to. */
|
|
93
|
+
target: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Everything a policy may look at. `entries` are the active-path entries the
|
|
98
|
+
* model can currently see: after the latest `compactionSummary` cut, in ledger
|
|
99
|
+
* order, as `selectVisibleEntries` in visible.ts produces them. They are NOT
|
|
100
|
+
* projected: a policy must consult `view.evicted` to skip units that are
|
|
101
|
+
* already out. The view is folded over the full active path, so a ref evicted
|
|
102
|
+
* before a later compaction is still known. Token counts enter selection only
|
|
103
|
+
* through `settings.minEvictableTokens` and the headroom arithmetic against
|
|
104
|
+
* `pressure.target`; no rule may rank candidates by size or recency score.
|
|
105
|
+
*/
|
|
106
|
+
export interface PolicyInput {
|
|
107
|
+
entries: ReadonlyArray<SessionEntry>;
|
|
108
|
+
view: WorkingSetView;
|
|
109
|
+
/** Session working directory for the path index; null when unknown (paths stay relative). */
|
|
110
|
+
cwd: string | null;
|
|
111
|
+
settings: WorkingSetSettings;
|
|
112
|
+
pressure: PressureInput;
|
|
113
|
+
/** chars/4 estimator shared with `context-accounting.ts`, so replay and live agree. */
|
|
114
|
+
estimateTokens: (entry: SessionEntry) => number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A unit the policy wants out, with the typed reason. Ordered: apply in this order, stop when headroom is met. */
|
|
118
|
+
export interface EvictionCandidate {
|
|
119
|
+
ref: WorkingSetRef;
|
|
120
|
+
reason: EvictionReason;
|
|
121
|
+
by?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface WorkingSetPolicy {
|
|
125
|
+
readonly id: WorkingSetPolicyId;
|
|
126
|
+
/**
|
|
127
|
+
* Select candidates. Must be pure and deterministic for a given input.
|
|
128
|
+
* Returns an empty array when nothing qualifies. Units already in
|
|
129
|
+
* `input.view.evicted` must not be returned.
|
|
130
|
+
*/
|
|
131
|
+
select(input: PolicyInput): ReadonlyArray<EvictionCandidate>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Materialized selection: markers rendered, tokens estimated, ready to become a ledger entry. */
|
|
135
|
+
export interface EvictionPlan {
|
|
136
|
+
policyId: WorkingSetPolicyId;
|
|
137
|
+
items: ReadonlyArray<EvictedItem>;
|
|
138
|
+
tokensBefore: number;
|
|
139
|
+
tokensAfter: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Fields the caller adds when appending the plan as a ledger entry. */
|
|
143
|
+
export type ContextEvictionFields = Omit<ContextEvictionEntry, "turnId" | "parentTurnId" | "timestamp">;
|
|
144
|
+
export type ContextRecallFields = Omit<ContextRecallEntry, "turnId" | "parentTurnId" | "timestamp">;
|
|
145
|
+
|
|
146
|
+
/** Typed failure for recall by ref. `recallErrorMessage` lists the refs that are evicted beside it. */
|
|
147
|
+
export type RecallError =
|
|
148
|
+
| { kind: "not_on_active_path"; ref: string }
|
|
149
|
+
| { kind: "not_evicted"; ref: string }
|
|
150
|
+
| { kind: "invalid_ref"; ref: string };
|
|
151
|
+
|
|
152
|
+
export interface RecallResult {
|
|
153
|
+
ref: WorkingSetRef;
|
|
154
|
+
/** The ledger entry whose body is readmitted. */
|
|
155
|
+
entry: SessionEntry;
|
|
156
|
+
/** Exact original body as the projection would have rendered it before eviction. */
|
|
157
|
+
body: string;
|
|
158
|
+
tokens: number;
|
|
159
|
+
/** Present when the original result was offloaded; recall returns the pointer, never the file. */
|
|
160
|
+
offloadPath?: string;
|
|
161
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Working-set settings: user-visible defaults. The structural type lives in
|
|
3
|
+
* `src/core/defaults.ts` beside the rest of the settings tree so core stays
|
|
4
|
+
* free of a backward domain dependency; this module pairs it with the value
|
|
5
|
+
* the DEFAULT_SETTINGS tree and the engine read at runtime.
|
|
6
|
+
*
|
|
7
|
+
* `structural-v1` is the default: typed path-keyed rules first, the age rule
|
|
8
|
+
* last and batched to `target`. On the reproducible 24-trace procedural grid,
|
|
9
|
+
* it meets the recorded default rule at 32k, 64k, and 128k: retention is no
|
|
10
|
+
* lower than `age-horizon`, precision is higher than random, and the target
|
|
11
|
+
* stop remains active above 32k. `age-horizon` stays available as the exact
|
|
12
|
+
* pre-layer selection recorded through the ledger.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { WorkingSetSettings } from "../../../core/defaults.js";
|
|
16
|
+
|
|
17
|
+
export type { WorkingSetPolicyId, WorkingSetSettings } from "../../../core/defaults.js";
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_WORKING_SET_SETTINGS: WorkingSetSettings = {
|
|
20
|
+
enabled: true,
|
|
21
|
+
policy: "structural-v1",
|
|
22
|
+
target: 0.6,
|
|
23
|
+
protectLastTurns: 6,
|
|
24
|
+
// The procedural floor sweep found marker break-even near 50 tokens. A zero
|
|
25
|
+
// floor saved only 0.167 summaries at 64k and none at 128k while reducing
|
|
26
|
+
// covered retention by 0.0076 and 0.0237. Keep 200 as the churn guard.
|
|
27
|
+
minEvictableTokens: 200,
|
|
28
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a policy's selection into something the session can append.
|
|
3
|
+
*
|
|
4
|
+
* `planEviction` is the whole decision: it asks the policy what should leave,
|
|
5
|
+
* materializes each candidate into an `EvictedItem` (marker rendered, tokens
|
|
6
|
+
* measured), and prices the result against the projection the model will
|
|
7
|
+
* actually receive. It writes nothing and calls no model, so the live engine
|
|
8
|
+
* and the replay-lite runner drive it identically.
|
|
9
|
+
*
|
|
10
|
+
* `buildEvictionFields` is the boring half: the plan plus the trigger facts,
|
|
11
|
+
* shaped as the ledger entry minus the three fields the session owns
|
|
12
|
+
* (`turnId`, `parentTurnId`, `timestamp`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { EvictedItem, SessionEntry } from "../../session/entries.js";
|
|
16
|
+
import type {
|
|
17
|
+
ContextEvictionFields,
|
|
18
|
+
EvictedState,
|
|
19
|
+
EvictionCandidate,
|
|
20
|
+
EvictionPlan,
|
|
21
|
+
EvictionTrigger,
|
|
22
|
+
PolicyInput,
|
|
23
|
+
WorkingSetPolicy,
|
|
24
|
+
WorkingSetView,
|
|
25
|
+
} from "./contract.js";
|
|
26
|
+
import { refKey } from "./fold.js";
|
|
27
|
+
import { renderMarker } from "./marker.js";
|
|
28
|
+
import { callPathsByToolCallId } from "./path-index.js";
|
|
29
|
+
import { hasThinking, offloadPathOf, primaryPathOf, toolResultPayload, toolResultText } from "./payload.js";
|
|
30
|
+
import { projectWorkingSet } from "./project.js";
|
|
31
|
+
|
|
32
|
+
/** Paths of the tool calls whose results may be evicted: `callPathsByToolCallId` over the policy input. */
|
|
33
|
+
export type CallPaths = ReadonlyMap<string, string>;
|
|
34
|
+
const NO_CALL_PATHS: CallPaths = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The event has no turnId until `session.appendEntry` gives it one, and the
|
|
38
|
+
* projection reads only `reason` and `marker`, so plan-time states carry this
|
|
39
|
+
* placeholder rather than a fabricated id.
|
|
40
|
+
*/
|
|
41
|
+
const PENDING_EVENT_TURN_ID = "";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The stub that replaces this unit's body, or null when there is nothing to
|
|
45
|
+
* evict. Thinking eviction renders no marker at all: the reasoning simply
|
|
46
|
+
* stops being replayed.
|
|
47
|
+
*/
|
|
48
|
+
function markerFor(entry: SessionEntry, candidate: EvictionCandidate, callPaths: CallPaths): string | null {
|
|
49
|
+
if (entry.kind !== "message") return null;
|
|
50
|
+
if (entry.role === "assistant") return hasThinking(entry.payload) ? "" : null;
|
|
51
|
+
if (entry.role !== "tool_result") return null;
|
|
52
|
+
const payload = toolResultPayload(entry.payload);
|
|
53
|
+
const toolCallId = typeof payload.obj.toolCallId === "string" ? payload.obj.toolCallId : undefined;
|
|
54
|
+
return renderMarker({
|
|
55
|
+
ref: candidate.ref,
|
|
56
|
+
reason: candidate.reason,
|
|
57
|
+
by: candidate.by,
|
|
58
|
+
toolName: payload.toolName,
|
|
59
|
+
text: toolResultText(payload.result),
|
|
60
|
+
offloadPath: offloadPathOf(payload),
|
|
61
|
+
path: primaryPathOf(payload) ?? (toolCallId === undefined ? undefined : callPaths.get(toolCallId)),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pendingState(candidate: EvictionCandidate, marker: string, policyId: string): EvictedState {
|
|
66
|
+
return {
|
|
67
|
+
reason: candidate.reason,
|
|
68
|
+
marker,
|
|
69
|
+
...(candidate.by === undefined ? {} : { by: candidate.by }),
|
|
70
|
+
tokensFreed: 0,
|
|
71
|
+
evictedAtTurnId: PENDING_EVENT_TURN_ID,
|
|
72
|
+
policyId,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A view holding exactly one item, for pricing that item on its own. */
|
|
77
|
+
function soloView(key: string, state: EvictedState): WorkingSetView {
|
|
78
|
+
return {
|
|
79
|
+
evicted: new Map([[key, state]]),
|
|
80
|
+
// Zero events: pricing one body must not also stamp usage invalidation,
|
|
81
|
+
// which would put the cost of a different mechanism in this item's total.
|
|
82
|
+
evictionEvents: 0,
|
|
83
|
+
itemsEvicted: 1,
|
|
84
|
+
recalls: 0,
|
|
85
|
+
lastPolicyId: null,
|
|
86
|
+
lastEvictionTurnId: null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The view this plan would produce. `evictionEvents` and `lastEvictionTurnId`
|
|
92
|
+
* stay where they were on purpose: both totals are then measured under the same
|
|
93
|
+
* usage-invalidation state, so `tokensBefore - tokensAfter` is exactly the
|
|
94
|
+
* bodies this event removes and nothing else.
|
|
95
|
+
*/
|
|
96
|
+
function viewWithItems(view: WorkingSetView, items: ReadonlyArray<EvictedItem>, policyId: string): WorkingSetView {
|
|
97
|
+
const evicted = new Map(view.evicted);
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
evicted.set(refKey(item.ref), {
|
|
100
|
+
reason: item.reason,
|
|
101
|
+
marker: item.marker,
|
|
102
|
+
...(item.by === undefined ? {} : { by: item.by }),
|
|
103
|
+
tokensFreed: item.tokensFreed,
|
|
104
|
+
evictedAtTurnId: PENDING_EVENT_TURN_ID,
|
|
105
|
+
policyId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return { ...view, evicted, itemsEvicted: view.itemsEvicted + items.length };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sumTokens(entries: ReadonlyArray<SessionEntry>, estimate: (entry: SessionEntry) => number): number {
|
|
112
|
+
let total = 0;
|
|
113
|
+
for (const entry of entries) total += estimate(entry);
|
|
114
|
+
return total;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What one candidate takes out of the working set: the entry as it stands now
|
|
119
|
+
* minus the entry as the projection would render it. Zero when the candidate
|
|
120
|
+
* does not apply to the entry, and never negative, because a marker longer than
|
|
121
|
+
* the body it replaces is a bad trade, not a negative saving.
|
|
122
|
+
*
|
|
123
|
+
* Exported so a policy can do headroom arithmetic (`structural-v1` rung 6 needs
|
|
124
|
+
* to know when to stop) against the same numbers `planEviction` will record.
|
|
125
|
+
* A policy that priced evictions its own way would report headroom the ledger
|
|
126
|
+
* then contradicts. Pass the same `callPaths` the plan will use, or the marker
|
|
127
|
+
* priced here is a few bytes shorter than the one recorded.
|
|
128
|
+
*/
|
|
129
|
+
export function tokensFreedByEviction(
|
|
130
|
+
estimateTokens: (entry: SessionEntry) => number,
|
|
131
|
+
entry: SessionEntry,
|
|
132
|
+
candidate: EvictionCandidate,
|
|
133
|
+
callPaths: CallPaths = NO_CALL_PATHS,
|
|
134
|
+
): number {
|
|
135
|
+
const marker = markerFor(entry, candidate, callPaths);
|
|
136
|
+
if (marker === null) return 0;
|
|
137
|
+
const key = refKey(candidate.ref);
|
|
138
|
+
const projected = projectWorkingSet([entry], soloView(key, pendingState(candidate, marker, "")))[0] ?? entry;
|
|
139
|
+
return Math.max(0, estimateTokens(entry) - estimateTokens(projected));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function planEviction(policy: WorkingSetPolicy, input: PolicyInput): EvictionPlan | null {
|
|
143
|
+
const candidates = policy.select(input);
|
|
144
|
+
if (candidates.length === 0) return null;
|
|
145
|
+
|
|
146
|
+
const byTurnId = new Map<string, SessionEntry>();
|
|
147
|
+
for (const entry of input.entries) byTurnId.set(entry.turnId, entry);
|
|
148
|
+
const callPaths = callPathsByToolCallId(input.entries);
|
|
149
|
+
|
|
150
|
+
const items: EvictedItem[] = [];
|
|
151
|
+
const claimed = new Set<string>();
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
const key = refKey(candidate.ref);
|
|
154
|
+
// A policy is contractually forbidden from returning a unit that is
|
|
155
|
+
// already out, and a duplicate inside one selection would double-count
|
|
156
|
+
// the tokens it frees. Both are cheap to refuse here.
|
|
157
|
+
if (input.view.evicted.has(key) || claimed.has(key)) continue;
|
|
158
|
+
const entry = byTurnId.get(key);
|
|
159
|
+
if (entry === undefined) continue;
|
|
160
|
+
const marker = markerFor(entry, candidate, callPaths);
|
|
161
|
+
if (marker === null) continue;
|
|
162
|
+
// A marker at least as long as the body it replaces is a cold turn bought
|
|
163
|
+
// for nothing, whatever the policy's reason. Refused here so no policy can
|
|
164
|
+
// record an eviction that freed nothing.
|
|
165
|
+
const tokensFreed = tokensFreedByEviction(input.estimateTokens, entry, candidate, callPaths);
|
|
166
|
+
if (tokensFreed <= 0) continue;
|
|
167
|
+
claimed.add(key);
|
|
168
|
+
items.push({
|
|
169
|
+
ref: candidate.ref,
|
|
170
|
+
reason: candidate.reason,
|
|
171
|
+
tokensFreed,
|
|
172
|
+
marker,
|
|
173
|
+
...(candidate.by === undefined ? {} : { by: candidate.by }),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (items.length === 0) return null;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
policyId: policy.id,
|
|
180
|
+
items,
|
|
181
|
+
tokensBefore: sumTokens(projectWorkingSet(input.entries, input.view), input.estimateTokens),
|
|
182
|
+
tokensAfter: sumTokens(
|
|
183
|
+
projectWorkingSet(input.entries, viewWithItems(input.view, items, policy.id)),
|
|
184
|
+
input.estimateTokens,
|
|
185
|
+
),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildEvictionFields(
|
|
190
|
+
plan: EvictionPlan,
|
|
191
|
+
meta: { trigger: EvictionTrigger; pressureBefore: number | null; snapshotIdBefore: string | null },
|
|
192
|
+
): ContextEvictionFields {
|
|
193
|
+
return {
|
|
194
|
+
kind: "contextEviction",
|
|
195
|
+
policyId: plan.policyId,
|
|
196
|
+
trigger: meta.trigger,
|
|
197
|
+
evicted: plan.items,
|
|
198
|
+
tokensBefore: plan.tokensBefore,
|
|
199
|
+
tokensAfter: plan.tokensAfter,
|
|
200
|
+
pressureBefore: meta.pressureBefore,
|
|
201
|
+
snapshotIdBefore: meta.snapshotIdBefore,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fold the working-set ledger records on the active path into a view.
|
|
3
|
+
*
|
|
4
|
+
* Pure over entries. The active path is selected here, through
|
|
5
|
+
* `filterEntriesToActivePath`, so every consumer (live projection, recall,
|
|
6
|
+
* overlay, replay-lite) shares the same #94 discipline: after a `/tree` switch
|
|
7
|
+
* the raw file still holds abandoned turns, and an eviction recorded on an
|
|
8
|
+
* abandoned branch must not project onto the live one. Forks inherit the view
|
|
9
|
+
* of their shared prefix for the same reason compaction summaries do.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SessionEntry } from "../../session/entries.js";
|
|
13
|
+
import { filterEntriesToActivePath } from "../../session/tree/active-path.js";
|
|
14
|
+
import type { EvictedState, WorkingSetRef, WorkingSetView } from "./contract.js";
|
|
15
|
+
|
|
16
|
+
/** Ref keys index `WorkingSetView.evicted`. Today a key is the entry turnId. */
|
|
17
|
+
export function refKey(ref: WorkingSetRef): string {
|
|
18
|
+
return ref.entry;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseRefKey(key: string): WorkingSetRef | null {
|
|
22
|
+
const trimmed = key.trim();
|
|
23
|
+
if (trimmed.length === 0 || /\s/.test(trimmed)) return null;
|
|
24
|
+
return { entry: trimmed };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function foldWorkingSet(entries: ReadonlyArray<SessionEntry>, activeLeafTurnId?: string): WorkingSetView {
|
|
28
|
+
const active = filterEntriesToActivePath(entries, activeLeafTurnId);
|
|
29
|
+
const evicted = new Map<string, EvictedState>();
|
|
30
|
+
let evictionEvents = 0;
|
|
31
|
+
let itemsEvicted = 0;
|
|
32
|
+
let recalls = 0;
|
|
33
|
+
let lastPolicyId: string | null = null;
|
|
34
|
+
let lastEvictionTurnId: string | null = null;
|
|
35
|
+
for (const entry of active) {
|
|
36
|
+
if (entry.kind === "contextEviction") {
|
|
37
|
+
evictionEvents += 1;
|
|
38
|
+
lastPolicyId = entry.policyId;
|
|
39
|
+
lastEvictionTurnId = entry.turnId;
|
|
40
|
+
for (const item of entry.evicted) {
|
|
41
|
+
itemsEvicted += 1;
|
|
42
|
+
evicted.set(refKey(item.ref), {
|
|
43
|
+
reason: item.reason,
|
|
44
|
+
marker: item.marker,
|
|
45
|
+
...(item.by === undefined ? {} : { by: item.by }),
|
|
46
|
+
tokensFreed: item.tokensFreed,
|
|
47
|
+
evictedAtTurnId: entry.turnId,
|
|
48
|
+
policyId: entry.policyId,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// A recall does not un-evict. The recalled body rides the recall tool
|
|
54
|
+
// result at the tail of the working set, which is where the model asked
|
|
55
|
+
// for it and where it costs no cold prefix; readmitting it at the
|
|
56
|
+
// original position would duplicate the bytes and invalidate the cache
|
|
57
|
+
// for everything after it. The marker stays, byte-stable, and a second
|
|
58
|
+
// recall of the same ref is the churn signal.
|
|
59
|
+
if (entry.kind === "contextRecall") recalls += 1;
|
|
60
|
+
}
|
|
61
|
+
return { evicted, evictionEvents, itemsEvicted, recalls, lastPolicyId, lastEvictionTurnId };
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protection horizon: where the recent, untouchable window begins.
|
|
3
|
+
*
|
|
4
|
+
* Both policies and every protection predicate answer "is this entry inside the
|
|
5
|
+
* last `protectLastTurns` turns" the same way, so the arithmetic lives here
|
|
6
|
+
* once. It is the same cutoff `maskStaleObservations` used before this layer
|
|
7
|
+
* existed, which is what keeps `age-horizon` selection-identical to the
|
|
8
|
+
* destructive stage it replaced.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SessionEntry } from "../../session/entries.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What starts a turn, in the sense the protection horizon counts. A local `!`
|
|
15
|
+
* bash execution and a branch summary both open a new stretch of work the same
|
|
16
|
+
* way an operator message does.
|
|
17
|
+
*/
|
|
18
|
+
export function isTurnStart(entry: SessionEntry): boolean {
|
|
19
|
+
if (entry.kind === "bashExecution" || entry.kind === "branchSummary") return true;
|
|
20
|
+
return entry.kind === "message" && entry.role === "user";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Index of the first protected entry: walk back until `protectLastTurns` turn
|
|
25
|
+
* starts have been seen. Entries before it are candidates, entries from it on
|
|
26
|
+
* are the recent window nothing touches.
|
|
27
|
+
*/
|
|
28
|
+
export function protectionCutoffIndex(entries: ReadonlyArray<SessionEntry>, protectLastTurns: number): number {
|
|
29
|
+
const horizon = Math.max(1, Math.floor(protectLastTurns));
|
|
30
|
+
let seen = 0;
|
|
31
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
32
|
+
const entry = entries[i];
|
|
33
|
+
if (!entry || !isTurnStart(entry)) continue;
|
|
34
|
+
seen += 1;
|
|
35
|
+
if (seen >= horizon) return i;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|