Skip to content

Latest commit

 

History

History
213 lines (172 loc) · 16.1 KB

content-lifecycle.md

File metadata and controls

213 lines (172 loc) · 16.1 KB

Content lifecycle — acquire, hold, release, re-acquire

Why this exists

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.

The four verbs, and where each one lives today

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.

Constants and where they are

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.

Invariants — each one paid for by a defect

  1. 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 task result cannot be reproduced and re-running bash re-applies its side effects. The placeholder, the truncation notice and the m* recovery line all name recall.
  2. 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.
  3. Release is a replacement on the wire, never a deletion. The full text stays in the store and stays reachable: keep narrows what replays; recall still returns everything.
  4. 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.
  5. 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.
  6. 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 keep matters MOST there.

Falsifiers earned here (read before the next change)

  • A cast in a write path disables the compiler as an oracle. recall's keep never persisted anything: the part's identity lives in the part table COLUMNS, the lookup read only the data column, and an as asserted the missing sessionID into existence. session.updatePart rejected 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 recall fixture 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.log can stay empty after a run finished and state.json can lag. Judge a slow run by state.json plus a live CPU probe.
  • JS .length counts UTF-16 units; SQLite length() counts characters. A small mismatch on a result containing non-BMP characters is not a defect.

The generalisation — temporary data acquisition

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:

  1. 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.
  2. Acquire covers more than media. Sources enter through read; a document through an attachment; a search result through universalsearch. 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: recall returns a released file part's payload by its id (commit 18fe82c426).
  3. 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 (fossil snapshots, tool filediffs) 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.

The declared lifetime — the third release trigger, and why (owner, 2026-09-19)

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).

What is NOT shipped (do not read as done)

  • 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 file part HAS a release path (an expired declared span replaces its payload with a note, message-v2.ts releaseExpiredParts), and that note now NAMES THE PART ID — recall reads the payload back, exactly as it does for a tool result (commit 18fe82c426; keep is 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.

Related

  • plans/to_be_confirmed/2026-09-19_temporary-data-acquisition.md — the image instance, its gateway-transform design, and the five dead x-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 shared ToolKeptSelection, ToolRelease and the address the last two share (selectLines).
  • packages/opencode/src/tool/recall.ts — the address's only consumer: range, pattern, chars, keep, release. Its range addresses the FILE's lines when the result is a read window.
  • packages/opencode/src/tool/result-limit.ts — the ONE home of the bound pair, shared by read's window, the fold gate and recall's per-call cap.

Moved from AGENTS.md § Content Lifecycle (2026-10-13)

(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.