@iowarp/clio-coder 0.3.2 → 0.3.4
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 +269 -458
- package/CONTRIBUTING.md +1 -1
- package/README.md +3 -3
- package/dist/{acp-BIYHVZIM.js → acp-S5R4RR5B.js} +7 -6
- package/dist/{agents-YT6SSRIT.js → agents-P6DMMVZY.js} +24 -21
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
- package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
- package/dist/{chunk-WMSVI4G2.js → chunk-2LZI5CAG.js} +133 -13
- package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
- package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
- package/dist/{chunk-WVO7V2QY.js → chunk-35MKKU5R.js} +4 -4
- package/dist/{chunk-LBNRH5WM.js → chunk-3HZ5RWN2.js} +5 -5
- package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
- package/dist/{chunk-MBS4V7ZP.js → chunk-4JUF2NNX.js} +7 -7
- package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
- package/dist/chunk-5M54SPOL.js +926 -0
- package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
- package/dist/{chunk-77VKQEHF.js → chunk-A2GZF7DC.js} +5 -5
- package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
- package/dist/chunk-AOCYTWAV.js +449 -0
- package/dist/chunk-BEY543CS.js +258 -0
- package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
- package/dist/chunk-BPGS2WCQ.js +612 -0
- package/dist/{chunk-J5HN4RYU.js → chunk-BRXQQJFP.js} +8 -8
- package/dist/chunk-CFGTUFWB.js +67 -0
- package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
- package/dist/{chunk-G2DE3C7R.js → chunk-EDRHSCIE.js} +4 -4
- package/dist/{chunk-4KLWL3UC.js → chunk-EFADSJET.js} +2 -2
- package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
- package/dist/chunk-FYYLNIL5.js +313 -0
- package/dist/{chunk-EPVUXGXG.js → chunk-HV5X7OR2.js} +14 -12
- package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
- package/dist/{chunk-IGLFWIYI.js → chunk-K6WL7QZT.js} +3 -3
- package/dist/chunk-K7VKOLQQ.js +15 -0
- package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
- package/dist/{chunk-V4RXGQ5Q.js → chunk-KRPY7NTG.js} +10 -7
- package/dist/chunk-LL4KHSZI.js +22 -0
- package/dist/{chunk-KJ5LWLOE.js → chunk-MEQ45TQ4.js} +15 -9
- package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
- package/dist/{chunk-AO4RKG4M.js → chunk-N4CZJQRK.js} +5 -5
- package/dist/{chunk-ARBGF5F7.js → chunk-NILBFAPG.js} +14 -8
- package/dist/chunk-OZNBF4L3.js +23 -0
- package/dist/{verify-G6V4D2G7.js → chunk-PCZJO5TI.js} +127 -42
- package/dist/chunk-QQK64KLB.js +1360 -0
- package/dist/{chunk-6EJV5X2W.js → chunk-QQL5RT5M.js} +979 -1619
- package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
- package/dist/{chunk-2EHAIA3X.js → chunk-RD5U66HV.js} +3 -3
- package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
- package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
- package/dist/chunk-TW3WDMVS.js +677 -0
- package/dist/chunk-TZSKNMZG.js +434 -0
- package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
- package/dist/{chunk-7EYHLWU7.js → chunk-UZHIZC5S.js} +7 -7
- package/dist/{chunk-QTYWRVRA.js → chunk-VAWWTKDP.js} +8 -8
- package/dist/{chunk-X75S7HFS.js → chunk-VEZEGCGW.js} +214 -20
- package/dist/{chunk-OHHN2SO4.js → chunk-VMNQ6OZA.js} +98 -202
- package/dist/chunk-VSNATDE6.js +122 -0
- package/dist/chunk-W6GROXXM.js +69 -0
- package/dist/chunk-WPQLXFOZ.js +375 -0
- package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
- package/dist/{chunk-3ZXDFGR5.js → chunk-X6COSD2O.js} +5 -5
- package/dist/chunk-ZGVHUX3M.js +66 -0
- package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
- package/dist/{chunk-MQSRRFWA.js → chunk-ZYKPLLNQ.js} +563 -546
- package/dist/cli/index.js +27 -23
- package/dist/{clio-4LY5K2AC.js → clio-J5JIOIDS.js} +7 -6
- package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
- package/dist/{config-GTLUW2PR.js → config-OEBMIN2U.js} +37 -27
- package/dist/{configure-R6A64DHX.js → configure-PUQOSIXQ.js} +16 -13
- package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
- package/dist/{context-RW5HC47S.js → context-MGSE4Z2T.js} +33 -23
- package/dist/{context-JFZEJ7W5.js → context-URSXPBCK.js} +17 -9
- package/dist/{context-clear-6ZHBAZZT.js → context-clear-KDAJRNUK.js} +33 -23
- package/dist/context-working-set-SBKMPPI2.js +1552 -0
- package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-MSWN72NK.js} +43 -29
- package/dist/{doctor-KI767GSN.js → doctor-7BSE27PJ.js} +10 -10
- package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
- package/dist/{evidence-UA6AWDQQ.js → evidence-SR7WXB5B.js} +51 -23
- package/dist/{evolve-QNTFGV6Z.js → evolve-K7VE2CBX.js} +30 -20
- package/dist/{fleet-Q7UOMUSG.js → fleet-7XMJNQNF.js} +48 -38
- package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
- package/dist/{init-WBB65ZHQ.js → init-JGNPAYXT.js} +41 -31
- package/dist/{memory-MD3O64RI.js → memory-4ALKDJ4Q.js} +32 -22
- package/dist/{models-BZU34YWD.js → models-ZMMLFJNN.js} +22 -19
- package/dist/{monitor-MEQA5C3I.js → monitor-2F3T5KHP.js} +55 -43
- package/dist/{orchestrator-CGFKEP27.js → orchestrator-ORHT43JB.js} +2507 -1896
- package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
- package/dist/{run-IV4Q6RLN.js → run-RF4WJGMT.js} +51 -41
- package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
- package/dist/{skills-LQEKRDTN.js → skills-PSACKC5Q.js} +2 -2
- package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WJSI55RZ.js} +34 -24
- package/dist/{targets-C4SSGQOB.js → targets-PIIRAOYS.js} +23 -20
- package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-ULWXWNVY.js} +5 -4
- package/dist/{upgrade-7TT7SQ3G.js → upgrade-346TZ6AV.js} +18 -17
- package/dist/{usage-GV4PKT3M.js → usage-6KKXR32N.js} +34 -24
- package/dist/verifiers-4UUM6TEE.js +1214 -0
- package/dist/verify-X5HDROLA.js +25 -0
- package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-7STOCIFZ.js} +42 -31
- package/dist/worker/entry.js +33 -24
- 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-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 +53 -21
- package/docs/config-knobs-audit.md +1 -2
- package/docs/configuration-and-targets.md +15 -1
- package/docs/context-engine.md +64 -12
- package/docs/context-working-set.md +194 -0
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +5 -5
- package/docs/documentation-guide.md +6 -5
- package/docs/environment-variables.md +2 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +14 -1
- package/docs/evidence-and-memory.md +74 -2
- 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 +22 -7
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +6 -6
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +7 -9
- package/docs/observability.md +4 -4
- package/docs/performance-methodology.md +2 -2
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -4
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +35 -35
- package/docs/safety-model.md +23 -4
- 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 +2 -2
- package/docs/worker-dispatch-mechanics.md +11 -1
- package/package.json +8 -11
- 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/context-working-set.ts +513 -0
- package/src/cli/context.ts +8 -0
- package/src/cli/evidence.ts +20 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/verifiers.ts +325 -0
- package/src/core/bash-exec.ts +39 -14
- package/src/core/bus-events.ts +19 -4
- package/src/core/config.ts +54 -0
- package/src/core/defaults.ts +50 -3
- package/src/core/git-commit-attribution.ts +46 -21
- package/src/core/verification-scripts.ts +6 -0
- package/src/domains/agents/builtins/verifier.md +3 -0
- package/src/domains/config/classify.ts +1 -0
- package/src/domains/config/keybindings.ts +3 -3
- 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 +21 -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/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/middleware/memory-intervention.ts +3 -0
- package/src/domains/middleware/stalled-turn.ts +165 -4
- package/src/domains/safety/autonomy.ts +1 -1
- 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/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 +2 -0
- package/src/domains/session/entries.ts +107 -1
- package/src/domains/session/manager.ts +9 -2
- package/src/domains/session/migrations/index.ts +22 -3
- package/src/engine/acp/server.ts +3 -0
- package/src/engine/agent.ts +18 -1
- package/src/engine/session.ts +9 -3
- package/src/entry/orchestrator.ts +16 -4
- package/src/interactive/chat-loop-messages.ts +18 -6
- package/src/interactive/chat-panel.ts +571 -244
- package/src/interactive/chat-renderer.ts +79 -39
- package/src/interactive/context-meter.ts +10 -0
- package/src/interactive/context-overlay.ts +81 -6
- package/src/interactive/context-recall-command.ts +110 -0
- package/src/interactive/editor-submit.ts +26 -1
- package/src/interactive/footer/widgets.ts +22 -20
- package/src/interactive/footer-panel.ts +6 -1
- package/src/interactive/interactive-application.ts +2 -0
- package/src/interactive/interactive-event-projection.ts +12 -0
- package/src/interactive/interactive-slash-runtime.ts +49 -8
- package/src/interactive/model-session-replay.ts +21 -0
- package/src/interactive/overlay-general-openers.ts +6 -0
- package/src/interactive/overlay-session-lifecycle.ts +8 -4
- package/src/interactive/overlays/ask-user.ts +146 -24
- package/src/interactive/renderers/tool-execution.ts +167 -56
- package/src/interactive/session-transcript.ts +2 -2
- package/src/interactive/slash-commands.ts +29 -2
- package/src/interactive/status/index.ts +12 -1
- package/src/interactive/status/reasoning.ts +87 -0
- package/src/interactive/status/summary.ts +13 -2
- package/src/interactive/transcript-detail.ts +120 -0
- package/src/interactive/turn-context.ts +238 -88
- package/src/interactive/turn-middleware.ts +6 -6
- package/src/tools/agent-tools.ts +11 -4
- package/src/tools/bash.ts +144 -82
- package/src/tools/builtin-tool-catalog.ts +18 -6
- package/src/tools/context/index.ts +105 -3
- package/src/tools/context/surface.ts +3 -2
- package/src/tools/core-bootstrap.ts +21 -0
- package/src/tools/dispatch-runner.ts +9 -7
- package/src/tools/monitor.ts +28 -20
- package/src/tools/presentation.ts +107 -0
- package/src/tools/registry.ts +65 -7
- package/src/tools/result-disposition.ts +550 -0
- package/src/tools/result-shaping.ts +262 -19
- package/src/tools/safe-exec.ts +2 -0
- package/src/tools/verify/authoring.ts +1119 -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 +35 -12
- package/dist/chunk-MNA4JGU4.js +0 -255
- package/dist/chunk-SRF2PJNW.js +0 -184
- package/dist/chunk-T6YILFSB.js +0 -80
- package/dist/chunk-VAKQQHWR.js +0 -434
package/src/core/defaults.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* exist. Users edit the file directly or through TUI overlays.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { DEFAULT_WORKING_SET_SETTINGS } from "../domains/context/working-set/defaults.js";
|
|
7
8
|
import type { TargetDescriptor } from "../domains/providers/types/target-descriptor.js";
|
|
8
9
|
import type { AutonomyLevel } from "../domains/safety/autonomy.js";
|
|
9
10
|
import { GUARDRAIL_DEFAULTS, type GuardrailValues } from "./guardrails.js";
|
|
@@ -98,6 +99,32 @@ export interface CompactionSettings {
|
|
|
98
99
|
systemPrompt?: string;
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Working-set layer settings (`context.workingSet`). The layer decides which
|
|
104
|
+
* tool-result bodies and thinking blocks leave the model's working set when
|
|
105
|
+
* pressure crosses `compaction.threshold`; it records evictions as ledger
|
|
106
|
+
* entries and never rewrites history. Defaults and prose live in
|
|
107
|
+
* src/domains/context/working-set/defaults.ts.
|
|
108
|
+
*
|
|
109
|
+
* - enabled: master switch. Off skips eviction and goes straight to summary
|
|
110
|
+
* compaction (the legacy destructive mask is only reachable through
|
|
111
|
+
* CLIO_CODER_LEGACY_MASK=1).
|
|
112
|
+
* - policy: candidate selection rule set.
|
|
113
|
+
* - target: used/window ratio an applied event batches down to.
|
|
114
|
+
* - protectLastTurns: recent user turns whose observations are never evicted.
|
|
115
|
+
* - minEvictableTokens: results below this estimate are never evicted; the
|
|
116
|
+
* marker would cost more than it saves.
|
|
117
|
+
*/
|
|
118
|
+
export type WorkingSetPolicyId = "age-horizon" | "structural-v1";
|
|
119
|
+
|
|
120
|
+
export interface WorkingSetSettings {
|
|
121
|
+
enabled: boolean;
|
|
122
|
+
policy: WorkingSetPolicyId;
|
|
123
|
+
target: number;
|
|
124
|
+
protectLastTurns: number;
|
|
125
|
+
minEvictableTokens: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
/**
|
|
102
129
|
* Transient provider retry controls for the interactive chat loop. These are
|
|
103
130
|
* intentionally small and mirror the session retry helper defaults. Dispatched
|
|
@@ -357,6 +384,9 @@ export const DEFAULT_SETTINGS = {
|
|
|
357
384
|
threshold: 0.8,
|
|
358
385
|
excludeLastTurns: 6,
|
|
359
386
|
} as CompactionSettings,
|
|
387
|
+
context: {
|
|
388
|
+
workingSet: DEFAULT_WORKING_SET_SETTINGS,
|
|
389
|
+
},
|
|
360
390
|
retry: {
|
|
361
391
|
enabled: true,
|
|
362
392
|
maxRetries: 3,
|
|
@@ -610,9 +640,9 @@ keybindings: {}
|
|
|
610
640
|
# auto master switch for the pre-request compaction trigger.
|
|
611
641
|
# Manual /context compact always runs the LLM summary.
|
|
612
642
|
# threshold pressure = estimated_tokens / context_window. Crossing
|
|
613
|
-
# it
|
|
614
|
-
#
|
|
615
|
-
# excludeLastTurns recent
|
|
643
|
+
# it evicts from the working set first, then runs a full
|
|
644
|
+
# LLM summary if pressure stays above the threshold.
|
|
645
|
+
# excludeLastTurns recent turns protected only by the temporary legacy mask.
|
|
616
646
|
# model optional pattern (e.g. provider/summary-model-id) for a
|
|
617
647
|
# dedicated summarization model. Absent ⇒ orchestrator target.
|
|
618
648
|
# systemPrompt optional path to a prompt-override file.
|
|
@@ -623,6 +653,23 @@ compaction:
|
|
|
623
653
|
# model: provider/summary-model-id
|
|
624
654
|
# systemPrompt: ~/.config/clio-coder/prompts/compaction.md
|
|
625
655
|
|
|
656
|
+
# Non-destructive working-set eviction before summary compaction.
|
|
657
|
+
# enabled false skips eviction and goes directly to the summary stage.
|
|
658
|
+
# policy structural-v1 evicts by what the session did since
|
|
659
|
+
# (re-reads, edits, resolved failures, consumed listings)
|
|
660
|
+
# and falls back to age only under pressure;
|
|
661
|
+
# age-horizon is the previous age-based selection.
|
|
662
|
+
# target pressure ratio an applied eviction batches down to.
|
|
663
|
+
# protectLastTurns recent user turns whose observations remain in the working set.
|
|
664
|
+
# minEvictableTokens entries below this estimate remain in the working set.
|
|
665
|
+
context:
|
|
666
|
+
workingSet:
|
|
667
|
+
enabled: true
|
|
668
|
+
policy: structural-v1
|
|
669
|
+
target: 0.6
|
|
670
|
+
protectLastTurns: 6
|
|
671
|
+
minEvictableTokens: 200
|
|
672
|
+
|
|
626
673
|
# Transient provider/stream retry controls for interactive chat.
|
|
627
674
|
# Retryable errors include overloads, rate limits, 5xx responses, network
|
|
628
675
|
# resets, and timeouts. Context overflow uses compaction recovery instead.
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
accessSync,
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
constants as fsConstants,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
3
13
|
import { join, resolve } from "node:path";
|
|
4
14
|
import { AI_AGENT_NAME } from "./agent-environment.js";
|
|
5
15
|
import { CLIO_COMMIT_TRAILERS, type CommitAttributionEvidence } from "./commit-attribution.js";
|
|
@@ -30,7 +40,7 @@ const PROBE_CACHE_MAX_ENTRIES = 32;
|
|
|
30
40
|
type RepositoryProbe =
|
|
31
41
|
| { kind: "outside" }
|
|
32
42
|
| { kind: "custom"; hooksPath: string }
|
|
33
|
-
| { kind: "default";
|
|
43
|
+
| { kind: "default"; declaredDefaultHooksPath: string | null };
|
|
34
44
|
|
|
35
45
|
const probeCache = new Map<string, { at: number; probe: RepositoryProbe }>();
|
|
36
46
|
let installedHooksDirectory: string | null = null;
|
|
@@ -171,27 +181,29 @@ case "$base_count" in
|
|
|
171
181
|
;;
|
|
172
182
|
esac
|
|
173
183
|
|
|
174
|
-
#
|
|
175
|
-
# environment
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
# Resolve the current repository's default hooks directory before interpreting
|
|
185
|
+
# the environment prepared for the originating repository. A command may have
|
|
186
|
+
# moved into a different repository in between; an explicit default path is
|
|
187
|
+
# composable only while it still names this same repository's default.
|
|
188
|
+
common_dir=$(git rev-parse --git-common-dir 2>/dev/null || true)
|
|
189
|
+
common_dir_absolute=$(cd "$common_dir" 2>/dev/null && pwd -P || true)
|
|
190
|
+
case "$common_dir_absolute" in
|
|
191
|
+
'') default_hooks_directory=; default_hook= ;;
|
|
192
|
+
*) default_hooks_directory=$common_dir_absolute/hooks; default_hook=$default_hooks_directory/$hook_name ;;
|
|
193
|
+
esac
|
|
194
|
+
|
|
195
|
+
if git config --get core.hooksPath >/dev/null 2>&1 &&
|
|
196
|
+
[ "$${DEFAULT_HOOKS_EQUIVALENT_ENV}" != "$default_hooks_directory" ]; then
|
|
197
|
+
custom_hooks=$(git config --path --get core.hooksPath 2>/dev/null || true)
|
|
179
198
|
case "$custom_hooks" in
|
|
180
199
|
'') ;;
|
|
181
200
|
/*) custom_hook=$custom_hooks/$hook_name ;;
|
|
182
201
|
*) custom_hook=$PWD/$custom_hooks/$hook_name ;;
|
|
183
202
|
esac
|
|
184
203
|
if [ -n "$custom_hooks" ] && [ -x "$custom_hook" ]; then exec "$custom_hook" "$@"; fi
|
|
185
|
-
|
|
204
|
+
exit 0
|
|
186
205
|
fi
|
|
187
206
|
|
|
188
|
-
common_dir=$(git rev-parse --git-common-dir 2>/dev/null || true)
|
|
189
|
-
case "$common_dir" in
|
|
190
|
-
'') default_hook= ;;
|
|
191
|
-
/*) default_hook=$common_dir/hooks/$hook_name ;;
|
|
192
|
-
*) default_hook=$PWD/$common_dir/hooks/$hook_name ;;
|
|
193
|
-
esac
|
|
194
|
-
|
|
195
207
|
if [ "$hook_name" != 'prepare-commit-msg' ]; then
|
|
196
208
|
if [ -n "$default_hook" ] && [ -x "$default_hook" ]; then exec "$default_hook" "$@"; fi
|
|
197
209
|
exit 0
|
|
@@ -258,11 +270,22 @@ function installManagedHook(directory: string, name: string): void {
|
|
|
258
270
|
}
|
|
259
271
|
}
|
|
260
272
|
|
|
273
|
+
function managedHooksDirectoryIsIntact(directory: string): boolean {
|
|
274
|
+
try {
|
|
275
|
+
for (const name of MANAGED_HOOK_NAMES) {
|
|
276
|
+
const hook = join(directory, name);
|
|
277
|
+
if (readFileSync(hook, "utf8") !== MANAGED_HOOK_SCRIPT) return false;
|
|
278
|
+
accessSync(hook, fsConstants.X_OK);
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
261
286
|
function managedHooksDirectory(): string {
|
|
262
287
|
const directory = join(clioStateDir(), "git-hooks", `v${MANAGED_HOOK_VERSION}`);
|
|
263
|
-
|
|
264
|
-
// directory is still there rather than re-reading all of its wrappers.
|
|
265
|
-
if (installedHooksDirectory === directory && existsSync(join(directory, "prepare-commit-msg"))) return directory;
|
|
288
|
+
if (installedHooksDirectory === directory && managedHooksDirectoryIsIntact(directory)) return directory;
|
|
266
289
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
267
290
|
for (const name of MANAGED_HOOK_NAMES) installManagedHook(directory, name);
|
|
268
291
|
installedHooksDirectory = directory;
|
|
@@ -281,13 +304,13 @@ function gitEnvironmentFingerprint(env: NodeJS.ProcessEnv): string {
|
|
|
281
304
|
function probeRepositoryUncached(cwd: string, env: NodeJS.ProcessEnv): RepositoryProbe {
|
|
282
305
|
if (gitOutput(cwd, env, ["rev-parse", "--is-inside-work-tree"]) !== "true") return { kind: "outside" };
|
|
283
306
|
const customHooksPath = gitOutput(cwd, env, ["config", "--path", "--get", "core.hooksPath"]);
|
|
284
|
-
if (customHooksPath === null) return { kind: "default",
|
|
307
|
+
if (customHooksPath === null) return { kind: "default", declaredDefaultHooksPath: null };
|
|
285
308
|
const commonDirectory = gitOutput(cwd, env, ["rev-parse", "--git-common-dir"]);
|
|
286
309
|
const defaultHooksPath = commonDirectory === null ? null : resolve(cwd, commonDirectory, "hooks");
|
|
287
310
|
if (customHooksPath.length === 0 || resolve(cwd, customHooksPath) !== defaultHooksPath) {
|
|
288
311
|
return { kind: "custom", hooksPath: customHooksPath };
|
|
289
312
|
}
|
|
290
|
-
return { kind: "default",
|
|
313
|
+
return { kind: "default", declaredDefaultHooksPath: defaultHooksPath };
|
|
291
314
|
}
|
|
292
315
|
|
|
293
316
|
function probeRepository(cwd: string, env: NodeJS.ProcessEnv, now = Date.now()): RepositoryProbe {
|
|
@@ -342,7 +365,9 @@ export function withManagedGitCommitAttributionEnvironment(
|
|
|
342
365
|
diagnostic: boundedDiagnostic(`core.hooksPath is set to '${probe.hooksPath}'; Clio commit attribution skipped`),
|
|
343
366
|
};
|
|
344
367
|
}
|
|
345
|
-
if (probe.
|
|
368
|
+
if (probe.declaredDefaultHooksPath !== null) {
|
|
369
|
+
env[DEFAULT_HOOKS_EQUIVALENT_ENV] = probe.declaredDefaultHooksPath;
|
|
370
|
+
}
|
|
346
371
|
|
|
347
372
|
let hooksDirectory: string;
|
|
348
373
|
try {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
export const VERIFICATION_SCRIPT_FAMILY_HINT = "test*/lint*/build*/typecheck*/check*/format*/ci*";
|
|
2
2
|
|
|
3
3
|
const VERIFICATION_SCRIPT_PATTERN = /^(?:test|lint|build|typecheck|check|format|ci)(?:[:.-].*)?$/;
|
|
4
|
+
const PROJECT_VERIFIER_CHECK_ID_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/;
|
|
4
5
|
|
|
5
6
|
export function isVerificationScriptName(name: string): boolean {
|
|
6
7
|
return VERIFICATION_SCRIPT_PATTERN.test(name);
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
/** Stable identifier grammar for project-declared verifier catalog checks. */
|
|
11
|
+
export function isProjectVerifierCheckId(name: string): boolean {
|
|
12
|
+
return PROJECT_VERIFIER_CHECK_ID_PATTERN.test(name);
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function declaredVerificationScripts(scripts: Record<string, unknown>): string[] {
|
|
10
16
|
return Object.keys(scripts)
|
|
11
17
|
.filter((name) => isVerificationScriptName(name))
|
|
@@ -24,6 +24,9 @@ Inspect scripts, docs, recent diffs, and touched files before choosing commands.
|
|
|
24
24
|
When a codewiki exists, prefer `code_nav` (symbol, dependents) over broad reads to scope what a diff touches.
|
|
25
25
|
Run only the checks required for the requested confidence level.
|
|
26
26
|
Prefer typed validation tools over arbitrary shell execution.
|
|
27
|
+
Call `verify()` before choosing an executable check. It lists package scripts and strict project entries from `.clio-coder/verifiers.yaml` through the same metadata shape.
|
|
28
|
+
Run a project entry only by its listed ID. Do not add args, cwd, timeout, environment, or shell composition; the catalog's exact argv, cwd, and timeout are authoritative.
|
|
29
|
+
Treat scientific validation contracts and generated handbook expectations as advisory evidence requirements. They do not become executable until the project declares a corresponding verifier-catalog entry.
|
|
27
30
|
Do not edit source files, tests, docs, configs, or generated artifacts from this role.
|
|
28
31
|
When a gate fails, report the exact command, exit status, relevant error lines, and likely owner.
|
|
29
32
|
Distinguish pre-existing failures from introduced failures when the evidence allows.
|
|
@@ -122,14 +122,14 @@ export const CLIO_APP_KEYBINDINGS = {
|
|
|
122
122
|
"clio.tool.expand": {
|
|
123
123
|
defaultKeys: "alt+o",
|
|
124
124
|
description:
|
|
125
|
-
"Fold or unfold the newest tool call or worker block between its one-line summary and full details
|
|
125
|
+
"Fold or unfold the newest tool call or worker block between its one-line summary and full details, overriding the /output level for that block",
|
|
126
126
|
},
|
|
127
127
|
"clio.tool.expandAll": {
|
|
128
128
|
// Alt+Shift+letter is commonly consumed by OS keyboard-layout switching.
|
|
129
129
|
// Keep it discoverable, but pair it with the legacy-safe Ctrl+Alt form.
|
|
130
130
|
defaultKeys: ["ctrl+alt+o", "alt+shift+o"],
|
|
131
131
|
description:
|
|
132
|
-
"Fold or unfold every tool call and worker block at once
|
|
132
|
+
"Fold or unfold every tool call and worker block at once, overriding the /output level; changing /output clears the overrides",
|
|
133
133
|
},
|
|
134
134
|
"clio.tool.liveOutput": {
|
|
135
135
|
defaultKeys: "alt+p",
|
|
@@ -145,7 +145,7 @@ export const CLIO_APP_KEYBINDINGS = {
|
|
|
145
145
|
// Alt+Shift+R as a distinct key event.
|
|
146
146
|
defaultKeys: ["ctrl+alt+r", "alt+shift+r"],
|
|
147
147
|
description:
|
|
148
|
-
"Toggle all thinking blocks between hidden markers and full bodies
|
|
148
|
+
"Toggle all thinking blocks between hidden markers and full bodies, overriding the /output level; changing /output clears the overrides",
|
|
149
149
|
},
|
|
150
150
|
"clio.editor.external": {
|
|
151
151
|
defaultKeys: "alt+g",
|
|
@@ -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
|
+
}
|