Owner, 2026-09-19 (verbatim):
«здесь не просто экономия токенов здесь самоподчистка контента, то что человек делает автоматом и то что в большинстве агентных систем отсутствует на прочь, это сильно снижает эффективность размазывая внимание нерелевантной информацией»
Two costs, and the second is the larger one. The budget cost is measurable and small (see
docs/compaction.md). The attention cost is what actually degrades work: a transcript full of
resolved tool output is not merely expensive, it competes for the same attention as the task. The
mechanism below exists to make the attended window contain what the current task needs, and the
released content remain reachable without occupying it.
| verb | mechanism | site | status |
|---|---|---|---|
| acquire | a tool result arrives, or read / webfetch / codegraph return content |
tool layer | shipped |
| hold | resident until the request goes out — the rendering is a PURE FUNCTION of the part | message-v2.ts |
shipped |
| release — automatic | ONE bound, in ONE home: a result over 16 384 lines OR 256 KB folds into an ID-addressed placeholder. Under BOTH bounds nothing is released — the 8 000-char gate that used to decide alone is GONE (owner, 2026-10-13) | @/tool/result-limit |
shipped |
| release — deliberate | recall(…, keep: true) REPLACES the result with the chosen slice; recall(…, release: true) (reason required) replaces the WHOLE result with the placeholder, the stored text untouched |
ToolKeptSelection / ToolRelease |
shipped |
| re-acquire | recall(id, range, pattern) returns the stored result by its address |
tool/recall.ts |
shipped |
| hold for attachments (image, document, sources) | — | — | NOT shipped — see plans/to_be_confirmed/2026-09-19_temporary-data-acquisition.md |
The first five are one mechanism. They are described here together because they only make sense together: a release with no address is a loss, and an address with no re-acquire is decoration.
RESULT_LIMIT_LINES = 16_384 tool/result-limit.ts the line bound: read's window, the fold gate, recall's per-call cap
RESULT_LIMIT_CHARS = 256 KB tool/result-limit.ts …and the byte bound. ONE pair, ONE home — a second copy is the defect
REPLAY_TOOL_OUTPUT_MAX_CHARS = RESULT_LIMIT_CHARS message-v2.ts the SAME number: what a request carries and what `read` returns (owner, 2026-10-13)
DECIDED 2026-10-13 — owner, «Поднять до лимита чтения»: the replay cap and read's window are ONE number
(RESULT_LIMIT_CHARS), resolved in one home. Before the decision the 32 KB default cut 353 stored parts /
17.15 MB of this worktree's history into «not folded, but sent truncated». The price is ACCEPTED and named: a
result up to the read limit is paid in EVERY request while it stays in the window — until the caller releases
it (recall … release), which is what that verb is for.
A placeholder is a pure function of the part — byte-identical across builds and across every request that
ever carries it. The 2026-10-13 change kept that property while moving WHERE it is produced: it is no longer a
size decision taken per request, but the rendering of a decision recorded ON the part (released) or forced by
the one bound pair. The prefix cannot be rewritten under the provider by a turn boundary that does not exist.
- The address is printed. A drop that leaves no way back is a loss, and "re-read or re-run the
tool" is not a way back: a
taskresult cannot be reproduced and re-runningbashre-applies its side effects. The placeholder, the truncation notice and them*recovery line all namerecall. - A reduction must never blank content. Two guards, and BOTH are required — the producer refuses a selection that selects nothing, AND the consumer treats an empty selection as "no selection" and falls back. With only the consumer guard, a stored empty selection still renders empty; with only the producer guard, a hand-edited or legacy part still does.
- Release is a replacement on the wire, never a deletion. The full text stays in the store and
stays reachable:
keepnarrows what replays;recallstill returns everything. - A reduction's claims must match its behaviour. The placeholder names the tool that reads the content back; the truncation notice names the part id and no longer blames compaction for truncating, which it never does.
- A narrowed selection carries a
reason. Narrowing history without a motive is a silent edit; the reason is persisted inside the selection so the narrowing is auditable. - A status without a release path is the one that grows without bound. Errors had no size gate at
all — they replayed in full on every later turn — so
keepmatters MOST there.
- A cast in a write path disables the compiler as an oracle.
recall'skeepnever persisted anything: the part's identity lives in theparttable COLUMNS, the lookup read only thedatacolumn, and anasasserted the missingsessionIDinto existence.session.updatePartrejected it at runtime. Typecheck exit 0, three green suites, and a hand-built fixture all missed it; a single call against the real database found it. Type the value so the compiler is the oracle. - A fixture's schema must match production's. The
recallfixture had(id, type, data)— missing the two columns the defect was about, so it could not observe the wrong query. - A write is verified by reading the artefact back, never by the tool's own answer.
range "-N"means "from the START" (slice[:N]), not "the last N". To reach a tail, address it explicitly — the answer's header prints the total.stdout_text.logcan stay empty after a run finished andstate.jsoncan lag. Judge a slow run bystate.jsonplus a live CPU probe.- JS
.lengthcounts UTF-16 units; SQLitelength()counts characters. A small mismatch on a result containing non-BMP characters is not a defect.
Owner, 2026-09-19 (verbatim):
«долелываем наш пайплайн с картинками, хотя вообще давай его расширим до temporary data aquisition. Например документ — окей мы сним будем возиться 3 хода, все отпустили, кстати - включая исходники, иногда лучше цепануть несколько исходников и поправить где надо, а потом отпустить и сделать отчет. Дифы покажут правки.»
A screenshot, a document, and a set of source files are the same shape: external content brought
into the window for a bounded purpose, worked on, and then let go. The image actualizer
(plans/to_be_confirmed/2026-09-19_temporary-data-acquisition.md) is the image instance of it, and its §2.3 already names the
shape — an active set.
What the generalisation adds:
- Hold is declared, not implied. A release happens by the ONE bound (16 384 lines / 256 KB), when the
caller lets a result go (
release: true), or when a declared span ends (ttlUntil). The plan's open decision 3 ("auto-detach at a fold or only by the tool") is exactly this question, and the caller's own release is now the general answer — the bound is the floor under it, not the mechanism. - Acquire covers more than media. Sources enter through
read; a document through an attachment; a search result throughuniversalsearch. All of them already land as tool parts with an address, so the release/re-acquire half is shipped for them TODAY — and for attachments too since 2026-09-30:recallreturns a releasedfilepart's payload by its id (commit18fe82c426). - Release produces a report, and the diffs are its evidence. Letting go of a working set is the
moment to state what changed: the diffs recorded while it was held (
fossilsnapshots, toolfilediffs) are the report's evidence, not a recollection of it. This is the same rule as "a write is verified by reading the artefact back", applied to a whole working set.
Release has three triggers: the ONE bound (@/tool/result-limit — 16 384 lines or 256 KB), a release the caller
asked for, and the fold. The DECLARED SPAN is the fourth, and the owner's two cases are what it exists for:
- Reconnaissance. «Мы конечно можем отправить explorer agent — но это не всегда целесообразно, иногда надо просто решить вопрос по быстрому и не засрать своё окно.» Acquire the source, read it, take the answer, release it: the window keeps the conclusion, not the reading.
- GUI debugging. «Ловим снапшоты каждую секунду, пачка форм, кликов — сделали по сути мы ничего в коде не правили, просто сделали гуй, но окно забито и надо вызывать компакт.» Screenshots accumulate as evidence that has already been read. Today the only exit is a compaction; with a declared span the agent reports to itself, stops the snapshots, and the content is clean and the head is clear.
The frame, in the owner's words. «Человек берёт справочник, читает оглавление, открывает страницу, выписывает формулу, закрывает и забывает о нём — формула на столе. А мы всё, чего касаемся, за собой тянем… attention размажется.» That is this document's own thesis, stated by the person the tool is for: the cost is ATTENTION first, budget second, and the window must hold what the CURRENT task needs.
Shape: ttl on the part that already carries the payload — not a table, because the part has a row already,
and because the field has to be walkable on its own:
null ⇒ permanent: the mechanism does not apply at all
"tmp_xxx" ⇒ scoped to one temporary enable
an integer turn ⇒ a RESOLVED absolute turn, so the comparison is a numeric range, never per-row arithmetic
It follows a precedent this repository already signs: compacted was promoted from JSON data.compacted to a
real column for indexable visible loads (schema-project.sql.ts:51) and is reversible (revert.ts:115,240).
The difference that matters: compacted hides the WHOLE message; ttl removes only the PAYLOAD, and the result
text stays readable.
Its domain is the sub-threshold spam — hundreds of small tool results that today live in the window forever, because neither the bound pair nor a fold ever touches them.
Plans: plans/to_be_confirmed/2026-09-19_temporary-data-acquisition.md (T6, with the one-pass migration that also drops
held_media, indexes the turn counter and settles the six zero-row tables).
- No hold, no lifetime. Nothing counts turns for a frame; a held set has no expiry.
- Attachments: released, and re-acquirable since 2026-09-30. A
filepart HAS a release path (an expired declared span replaces its payload with a note,message-v2.tsreleaseExpiredParts), and that note now NAMES THE PART ID —recallreads the payload back, exactly as it does for a tool result (commit18fe82c426;keepis refused for one, since it is a single payload). The wording that stood here — «Attachments have no release and no re-acquire… immutable — there is no removal path except the fold. The actualizer is unbuilt.» — was true when written and is now half false: what is still unbuilt is the ACTUALIZER (the hold side and the ledger), not the way back. - The active set has no home. No store, no ledger keyed by id; §4.1 of the actualizer plan puts the ledger in permanent memory, which is a decision, not an implementation.
plans/to_be_confirmed/2026-09-19_temporary-data-acquisition.md— the image instance, its gateway-transform design, and the five deadx-opencode-*reads that need their writer.docs/compaction.md— the three measures, one space each; the window budget this lifecycle spends.packages/opencode/src/session/message-v2.ts— the gate, the placeholder,selectLines, the sharedToolKeptSelection,ToolReleaseand the address the last two share (selectLines).packages/opencode/src/tool/recall.ts— the address's only consumer:range,pattern,chars,keep,release. Itsrangeaddresses the FILE's lines when the result is areadwindow.packages/opencode/src/tool/result-limit.ts— the ONE home of the bound pair, shared byread's window, the fold gate andrecall's per-call cap.
(links inside the moved text use repo-root-relative paths, as they were written in AGENTS.md)
Owner, 2026-09-19: «здесь не просто экономия токенов здесь самоподчистка контента, то что человек делает автоматом и то что в большинстве агентных систем отсутствует на прочь, это сильно снижает эффективность размазывая внимание нерелевантной информацией». The attended window must hold what the CURRENT task needs; released content stays reachable without occupying it. Full design: docs/content-lifecycle.md.
| verb | mechanism |
|---|---|
| acquire | a tool result arrives (read / webfetch / codegraph / …) |
| hold | resident until the request goes out — the rendering is a PURE FUNCTION of the part, so it never changes after it is sent |
| release | > 8 000 chars → an ID-addressed placeholder in EVERY request; or recall(…, keep: true) → only the chosen slice |
| re-acquire | recall(id, range, pattern) returns the stored result by its address |
Corollary — verify a reduction where it LANDS. keep was reported "shipped and verified" and had
never written a byte: a part's identity lives in the part table COLUMNS, the lookup read only data,
and an as asserted the missing sessionID into existence. bun typecheck exit 0 and three green
suites missed it; one live call against the real database found it. A cast in a write path is a
disabled oracle — type the value; and a fixture's schema must match production's, or it cannot observe
the wrong query.
Shipped 2026-09-30: attachments ARE re-acquirable — a released file part names its own id on the
wire and recall returns its payload (commit 18fe82c426); keep is refused for one, since an
attachment is a single payload and a kept slice would send the provider a truncated data URL. Still
planned, not shipped: nothing counts a lifetime for an attachment, and the actualizer and its ledger
are unbuilt. The generalisation to
temporary data acquisition is in plans/to_be_confirmed/2026-09-19_temporary-data-acquisition.md:
a document, a set of sources, a screenshot — acquire, hold for a declared span, release, and let the
recorded diffs be the report's evidence.