Transformation and savings accounting
The savings ledger has two kinds of records:
transformationrecords measure text that the harness actually changed.savingsrecords retain the older cost and counterfactual accounting.
Do not add the two kinds together. Transformation records use exact UTF-8 bytes. Older savings records use characters and may include estimates.
Exact transformation rule
Whenever the harness changes a command, a tool result, a stored tool result, a rendered context, or an inserted model-facing message, it writes one transformation record. If the input and output are identical, it writes nothing.
Set loop.transform_log_mode to one of these values:
| Mode | What is saved |
|---|---|
counts | The transform name and location; run, task, session, turn, and tool-call ID; chain order; exact input, output, and delta bytes; character counts; change count; and SHA-256 hashes. This is the default. |
debug | Everything in counts, plus located before/after excerpts in the JSONL record and complete before/after text files. |
counts does not retain the changed text. debug does. Debug data can contain text that a later redaction removes, so protect it like a raw transcript.
The ledger is written to one of these locations:
<run_dir>/savings/<task>.jsonl<project_root>/.llm_assist/sessions/<session_id>/savings.jsonl- legacy:
<task_cwd>/.savings.jsonl
For a normal run, debug files are beside the task ledger:
<run_dir>/savings/<task>.transform_debug/<event-id>.before.txt
<run_dir>/savings/<task>.transform_debug/<event-id>.after.txt
Assistant-mode debug files use savings.transform_debug/ beside savings.jsonl.
Logging is best effort. If the ledger cannot be opened or written, Yuj warns, disables accounting for that task, and continues the run. A debug-sidecar failure is recorded as debug_write_error when the JSONL write still succeeds.
Record fields
A transformation record contains:
| Field | Meaning |
|---|---|
event_id | Unique event ID for this ledger-open cycle. |
run, task, session, turn, tool_call_id | Where the change happened. Empty IDs mean that no tool call owned the change. |
surface | execution_command, tool_output, tool_output_fragment, stored_tool_output, context_render, or injected_message. |
chain_id, chain_step | Order of successive changes to the same text. The previous output hash must equal the next input hash within a multi-step chain. |
bucket, layer, mechanism | Attribution labels. |
input_bytes, output_bytes, delta_bytes | Exact len(text.encode("utf-8")) values. |
input_chars, output_chars, delta_chars | Python string lengths, retained because several harness limits use characters. |
change_count | Number of matches, rows, messages, or rules changed by this event. |
input_sha256, output_sha256 | Hashes of the complete UTF-8 values. |
ctx | Non-content details such as rule name, path, age band, page, or delivery mode. |
Transformation records do not contain delta_tokens_est. Token counts depend on the model tokenizer and are not inferred from bytes.
Debug evidence
Debug JSON contains byte ranges and surrounding text, not an isolated control character pair. For example:
{
"mechanism": "collapse_blank_lines",
"input_bytes": 31,
"output_bytes": 30,
"delta_bytes": -1,
"change_count": 1,
"changes": [
{
"input_byte_range": [13, 16],
"output_byte_range": [13, 15],
"before": "previous line\n\n\nnext line",
"after": "previous line\n\nnext line"
}
]
}
The complete values live in the event’s input_full_path and output_full_path. Their byte counts and SHA-256 hashes must match the JSONL record.
Instrumented transformations
The following table is the implementation inventory. Dynamic rule names appear after the mechanism prefix in the ledger.
| Area | Bucket and mechanisms | Exact before and after values | Main source |
|---|---|---|---|
| Command redirects | bash_command_transform / redirect:<rule>; command_intervention / redirect_refusal:<rule> | Original command to no execution; empty result to the refusal result | harness/tools.py |
| Forbidden commands | bash_command_transform / forbidden:<rule> | Original command to the safe refusal command that actually ran | bash_quirks/_forbidden.py, harness/tools.py |
| Quiet and test flags | bash_command_transform / universal:<rule>, test_flag:<flag> | Each command before and after that one rule | bash_quirks/transforms.py, harness/tools.py |
| Shell cleanup | tool_output_filter: ANSI stripping, blank-line collapse, duplicate-line collapse, similar-line collapse, traceback folding | Tool output before and after each cleanup | harness/_tool_filters.py |
| Deterministic output normalization | tool_output_normalize: ls timestamps, runner timing, current-working-directory paths, object addresses | Tool output before and after each normalization | harness/_tool_filters.py |
| Test-output condensation | bash_output_condense / passed_line_stripping | Complete test output before and after passed-line condensation | bash_quirks/_output.py |
| Redaction | tool_output_redaction / <rule> | Output before and after each redaction rule | bash_quirks/_redactions.py |
| Output limits | truncate_output / head_tail_truncation; sink_surface / head_tail_with_pointer | Complete admitted output before and after clipping or sinking | harness/_tool_filters.py, harness/_loop/state_projection.py |
| Structured test projection | structured_projection / <runner>_digest | Raw test output to digest plus raw-output pointer | harness/_loop/state_projection.py |
| Read projection | read_projection: range selection and line numbering; tool_result_reminder: empty, past-EOF, and truncated reminders | File text, selected text, numbered text, and reminder result in order | harness/_tools/read.py |
| Search projection | search_normalize, search_filter, search_pagination, tool_quirks_glob_refusal | Raw matches or paths through sorting, ignore filtering, pagination, or refusal | harness/_tools/grep.py, harness/_tools/_common.py, tool_quirks/transforms.py |
| Definition outline | outline_vs_read / list_definitions | Complete source text to the returned definition outline | harness/_tools/list_definitions.py |
| Tool-result envelopes | tool_result_envelope: security-marker insertion and unified envelope | Output before and after envelope work | harness/tools.py |
| Repeated output | output_dedup / <tool>; dedup / <tier> | Repeated output to its back-reference or stored-context stub | harness/_loop/_dispatch_tool_call.py, context_strategies/solver_state_context.py |
| Half-life | context_projection / halflife_decay | Each older tool result before and after its age-band cap | context_strategies/halflife_context.py |
| Thought retention | context_projection / think_retention_window | Complete message-list JSON before and after expired think pairs are removed | harness/context.py |
| Other context modes | context_projection / <mode> | Complete message-list JSON before and after the mode’s projection | context_strategies/ |
| Digest or checkpoint compaction | context_compaction / <method> | Complete message-list JSON before and after compaction, including its overflow guard | harness/_loop/compaction.py |
| Pre-flight re-clip | preflight_reclip / oversized_message_head_tail | The oversized message before and after the re-clip | harness/_loop/compaction.py |
| Adaptive intervention | adaptive_intervention / adaptive_user_turn, adaptive_tool_result_note | Empty text to the inserted user message, or the tool result before and after the detector note | harness/_loop/run_step.py, harness/_loop/_dispatch_tool_call.py |
| Gate and policy rejection | guardrail_intervention: done guard, schema, inactive-tool, permission, plan-mode, mutation, contract, rumination, and pre-tool-hook blocks | Empty text to the rejection shown as the tool result | harness/_loop/_dispatch_tool_call.py |
| Guardrail warning | guardrail_intervention: error, security, contract, test-read, rumination, duplicate-call, and gate-grace warnings | Empty text to the warning delivered in the next synthetic user turn | harness/_loop/_dispatch_tool_call.py |
| Security intervention | security_intervention / argument_block, result_block | Empty text to an argument refusal, or raw result to a result refusal | harness/tools.py |
| Stale-file intervention | stale_file_intervention / stale_precheck_error, stale_edit_block, stale_edit_warning | Empty text to a refusal, or edit result before and after a warning | harness/tools.py |
| Hooks and injections | hook_intervention, injection / <name> | Empty text to the next synthetic user turn; terminal hook context remains attached to its terminal result | harness/_loop/_dispatch_tool_call.py, harness/loop.py, harness/injections.py |
| Stream rules | stream_rule_intervention / next_turn_interrupt_fragment, retry_interrupt_fragment, tool_result_reminder | Empty text to an inserted message, or tool result before and after a reminder | harness/loop.py, harness/_loop/chat_io.py |
| Diagnostics and post-edit checks | diagnostic_intervention / lsp_diagnostics, post_edit_validation / <check> | Tool result before and after diagnostics, or empty text to failed-check output | harness/_loop/_dispatch_tool_call.py, harness/post_edit.py |
| Advisor and observations | advisor_intervention / advisor_note, harness_observation / open_red_observation_packet | Empty text to the inserted message | harness/advisor.py, harness/harness_observation.py |
Whole message lists use compact UTF-8 JSON labeled message_list_json_utf8_v1 in ctx.encoding. This keeps roles, tool-call IDs, and content together in debug evidence.
Counting rules
- Sum
delta_bytesfor observed size changes. A negative delta removed bytes; a positive delta added bytes. - Group by
surfacebefore interpreting totals. Command bytes, model-visible tool-output bytes, and repeated context-render bytes are different quantities. - Sequential transforms are additive only when their chain is continuous. The summary command reports chain breaks.
- Half-life is recorded every time a request render shortens an older tool result. This measures repeated prompt reduction, not unique stored bytes deleted.
- A command rewrite records only the original and executed commands. It does not invent the output that the unmodified command might have produced.
- A redirect or forbidden-command record measures suppression/replacement and the refusal text. It does not claim counterfactual output savings.
- An intervention measures the text inserted or replaced. It does not assign later behavior to that intervention.
- A verbatim control should have no transformation records except mechanisms that its resolved configuration explicitly leaves enabled. The ledger makes those exceptions visible instead of assuming the control was verbatim.
- For an old run, exact reconstruction is possible only when retained artifacts contain both values or exact counts. A transcript cannot recover text removed before the request was written.
Summary command
# One task ledger.
python3 -m scripts.llm_solver.analysis.savings_summary \
results/<run>/savings/<task>.jsonl
# Every task under one run.
python3 -m scripts.llm_solver.analysis.savings_summary results/<run>/
# Machine-readable output.
python3 -m scripts.llm_solver.analysis.savings_summary \
--json results/<run>/
The report keeps exact transformation bytes separate from legacy character-based records. It groups transformations by surface, layer, bucket, and mechanism and reports per-run, per-task totals, so the same task in two arms remains two rows.
Legacy savings records
The older savings event remains for values that are not an observed before/after text mutation:
- configured prompt, tool-schema, project-instruction, skill-catalog, profile, and pretest costs;
- counterfactual comparisons such as
apply_patchversus an edit loop; - estimates retained for compatibility.
These records use input_chars, output_chars, delta_chars, delta_tokens_est, and measure_type. Exact and estimated legacy rows stay separate. They must not be folded into transformation-byte totals.
Adding a transformation
Record at the point that owns both values:
from ..savings import get_ledger
get_ledger().record_transform(
bucket="tool_output_filter",
layer="harness",
mechanism="new_filter",
before=raw,
after=transformed,
surface="tool_output",
change_count=matches,
ctx={"tool_name": tool_name},
)
Do not write a record for a no-op. Do not put before/after content in ctx; normal counts mode must remain content-free. Add a new inventory row when a new bucket or model-visible surface is introduced.