Development record
One session, one artefact, one telemetry file: what `history reconstruct` renders and what it refuses to assert
Summary
This spec delivers itd-2609091718595846: a per-agent archive becomes a readable session.
history reconstruct <session-id> emits two files — one Markdown artefact
(<session>.md) and one machine-readable telemetry file
(<session>.telemetry.json). Core writes nothing and knows no path; the CLI
writes both into --out, or both to stdout.
It reads the lineage itd-2609090559376002 put on each record and adds no write path of its own, so the store of adr-29 is untouched by it. Two things in the shipped shape are deliberate reversals of what was planned, and both are recorded below: the sub-agent sections are appended after a contiguous main thread rather than nested at their spawn points, and token usage is de-duplicated per response rather than summed per line.
Scope
In: internal/core/history — Reconstruct, ReconstructOptions,
Reconstruction, Telemetry, AgentTelemetry, TokenCounts, TurnCounts,
Completeness, DroppedRecord (reconstruct.go) and the artefact renderer
(reconstruct_render.go). The reconstruct sub-verb in
internal/surface/cli/history_reconstruct.go, wired from history.go. The
plugin page commands/history.md, the brief chapter
04-surfaces/11-history.md, the
regenerated surface snapshot and command reference.
Out: every write path into the store; the scanner and redaction (reconstruction reads records that were already redacted on write); the record schema; capture; ingest and migrate; any roll-up across sessions.
Approach
The signature carries the size problem
type ReconstructMode string
const ( ModeFull ReconstructMode = "full"; ModeSpine ReconstructMode = "spine" )
type ReconstructOptions struct {
SessionID string
Mode ReconstructMode // "" → ModeFull
MaxBlockBytes int // 0 = unbounded; negative is an error
}
func Reconstruct(rootSHA string, opts ReconstructOptions) (Reconstruction, error)
The plan's Reconstruct(rootSHA, sessionID) did not survive contact with a real
session: a 55-record session renders to 7.7 MB, and the whole point of the
artefact is that it be handed to a model. Both remedies are caller choices, so
they are options rather than constants. spine mode keeps the main thread whole
and reduces each delegate to its first and last turn, with counted gap markers;
MaxBlockBytes (8 KiB at the front door) caps one rendered tool input or result,
marked where it happens and counted in the telemetry. There is deliberately no
per-agent mode: abcd history show <agent-id> already answers that.
Reconstruction returns the artefact bytes, both filenames, the artefact size
and the Telemetry structure. Artefact is json:"-", so the --json envelope
carries the telemetry and the names but never the document.
Record selection, and saying what was not used
One session's records are loaded and reduced to one thread per (session, agent): readable first, then longest body, then newest capture, then filename
descending. Every loser is reported in Completeness.DroppedRecords with a
reason and named in the document's own ## Completeness section. Nothing is
dropped silently — a session whose superseded records vanished from the artefact
without a word would be a reconstruction that quietly disagreed with the store.
A session with no main-thread record still renders, labelled, with a completeness note. Only a session with no records at all is an error.
The layout: appended sections, doubly marked — a reversal
The plan called for ## Sub-agent <agent-id> sections nested at their spawn
points by spawn_depth. The corpus refuted it. The sub-agents whose id a
spawning transcript records are the ASYNCHRONOUS ones, and for those the spawn
and the join are many turns apart — one measured case spawned at turn 18 and
joined at turn 167. Splicing the section in at the spawn point puts a delegate's
conclusions in front of 148 main-thread turns that ran before those conclusions
existed, which is precisely the false inference the artefact exists to prevent.
So the main thread stays contiguous and is never spliced, and each sub-agent gets
its own appended ## Agent <agent-id> section. Attribution is carried three
ways instead of by indentation:
- Two inline markers in the spawning thread.
[SPAWNED agent <id> (<type>) here — its transcript is in section "Agent <id>". Everything below this line up to its JOIN marker ran without its result.]and[JOINED agent <id> here — its result reached this thread at this turn.], collapsed into one marker when the spawn and the join are the same turn (a synchronous delegate). - A per-agent provenance block naming the type, spawn depth, spawner, spawn
turn and tool call, join turn, span, turns, tokens and source record — plus a
CONCURRENCY:line stating how many turns of the spawning thread ran between the spawn and the join without this agent's result. ## Agent timeline, a table at the head of the document with one row per thread carrying spawn, start, end, join, turns and tokens.
Nesting survives as DATA (spawn_depth, the parent, the spawning thread), not as
layout. Concurrency is read off the timeline; section order asserts nothing
about time, and the document says so in words.
Placement is a two-rung ladder. Rung 1 is the record's own stored spawning tool
call, matched against the host thread's tool-use blocks (placed_by: record).
Rung 2 is the host transcript's own tool result naming the agent id, resolved
back to the call that issued it (placed_by: transcript) — the spec's deferred
"resolved at reconstruction time" rung, delivered. The host is the parent agent's
thread when that parent is in the set, the main thread when no parent is named,
and NOTHING when a parent is named but absent: placing such an agent on the main
thread would assert a spawn that did not happen.
An agent neither rung places goes under ## Unattributed sub-agents, last and
labelled, never interleaved. Membership is decided by "no recoverable spawn
point", which is not the same fact as the record's own spawn_attribution; the
telemetry counts the two separately (agents_unattributed versus
agents_without_spawn_point).
Structure the transcript cannot forge
A text block is the one kind of content a session participant chose the bytes of.
Rendered raw it can emit ## Agent <id>, ### Turn 99 — assistant, or a
[JOINED …] marker byte-for-byte, fabricating an agent that never ran or a
result that never arrived. Redaction has nothing to say about this: the text is
not secret, it is structural.
The answer is containment, applied uniformly. Every block — text included — goes through one fenced writer that measures the longest backtick run in the content and opens with one more, so content carrying its own fences cannot close the block containing it. One escaping mechanism everywhere beats a per-type judgement about which content is dangerous, because the judgement is what drifts.
The second half is declarative. The artefact carries a ## How to read this document section that enumerates the structures the document itself asserts and
states the invariant: everything inside a fence is something somebody said,
everything outside one is this document. A forged heading inside a fence is
quoted content asserting nothing, however exactly it matches.
TestReconstructCannotBeForgedByTranscriptText plants forged headings, a forged
turn heading, a forged join marker and a fence of its own, and asserts none of
them appears as a line OUTSIDE a fence — while also asserting the content is
still reproduced, because this is containment and not redaction.
Self-containment and determinism
Records are named by basename everywhere; the artefact carries no store path and
no harness path of its own, and the CLI additionally home-redacts every path it
prints or marshals. What the renderer EMITS carries no absolute path. What it
REPRODUCES does: transcript text contains whatever paths the participants typed,
and a real artefact of one session carries fourteen home-rooted and 1488
temporary-directory occurrences inside quoted turns. They are kept deliberately.
Stripping a path out of somebody's recorded words would falsify the record the
artefact exists to preserve, and home paths were already redacted on the way into
the store, so what remains is content rather than leakage. The self-containment
test is scoped accordingly, to what the renderer emits. The artefact carries no
generation timestamp — that lives on the telemetry alone — so the same records
reconstruct to identical bytes. TestReconstructionIsSelfContained and
TestReconstructIsDeterministic are the two detectors.
Telemetry, and the counting bug it exists to avoid
<session>.telemetry.json, schema 1, carrying session_id, root_commit,
mode, generated_at, the span and wall_clock_seconds, turns, tokens,
tool_calls (a count per tool name), models, agent_types, one agents entry
per thread (the main thread included) with its own copy of every measure, and a
completeness block.
Tokens are counted once per distinct response id, never once per transcript
line. The harness writes one line per content block and repeats the response's
usage on every one of them, so a naive sum multiplies a response by its block
count — and the factor varies per session, so no constant could correct it. On a
real 55-record session, 3711 usage-bearing lines resolve to 1801 distinct
responses: an inflation of 2.06x (iss-2609090723027424). Turns de-duplicate the
same way, so a multi-line assistant response is one turn, and tool calls
de-duplicate on the tool-use block's own id.
Both counts are published side by side — api_responses and usage_lines_seen —
so a consumer can SEE that the de-duplication happened rather than take it on
trust. Usage carrying no message id is counted anyway (dropping it would
understate), tallied into completeness.usage_without_message_id, and given a
note saying the totals are an upper bound to that extent. That is the one
condition under which they can still be inflated, and it says so.
completeness is not decoration. The intent's scope condition says telemetry
describes what the harness recorded and is not a billing record, and a derived
measure that cannot say what it was missing invites exactly the cross-version
comparison the condition warns against. It reports whether the main thread was
present, records found against records used and why each loser was dropped, the
agent counts that failed each attribution question, unparseable lines,
un-de-duplicable usage, the field NAMES that were absent from the source lines
(distinguishing "zero happened" from "this harness does not record it"), and the
elision and omission counts.
One asymmetry worth stating: telemetry is computed BEFORE elision and spine
reduction, so the token and turn counts describe the whole transcript while
elided_blocks, elided_bytes and omitted_turns describe the artefact.
Surfaces
abcd history reconstruct <session-id>, exactly one argument. --out (default
., - for stdout), --mode (full | spine, default full),
--max-block-bytes (default 8192, 0 disables). Both files are written
atomically into an --out that must already exist — the directory is never
created — together or not at all. On stdout the artefact passes through the
terminal-safety filter first, because it is stored transcript text, and the
telemetry follows it in a fenced JSON block so a reader piping to a file still
gets both. The --json envelope carries the Reconstruction plus either the
written paths or the artefact string, never both.
The verb carries its section in commands/history.md, its row in the brief's
history chapter sub-verb table, and a regenerated surface snapshot and command
reference.
How the Acceptance Criteria are satisfied
- ac-1 (one artefact; each sub-agent attributable to its spawn point). One
Markdown document holding the main thread and every stored sub-agent, with the
spawn/join markers, the per-agent provenance block and the timeline table
carrying the attribution, and the unplaceable segregated. Tests:
TestReconstructMarksEverySubagentAtItsSpawnAndJoinPoint,TestReconstructPlacesAnAgentFromTheSpawningTranscriptAlone(the second attribution rung on its own),TestReconstructSegregatesUnattributedSubagents,TestReconstructChoosesOneRecordPerAgentAndSaysWhich,TestReconstructRendersASessionWithNoMainThread,TestReconstructRefusesWhatItCannotAnswer. On the real 55-record session, all 54 sub-agents were placed. - ac-2 (a machine-readable telemetry file with the named measures).
The
Telemetrystructure and its per-agent breakdown. Tests:TestTelemetryReportsEveryRequiredMeasureasserts each named field is present and non-trivial over a fixture session;TestTelemetryReportsItsOwnIncompletenessasserts the completeness block names a field the fixture deliberately omits;TestTelemetryCountsOneUsagePerResponseandTestTelemetryReportsUsageItCouldNotDeduplicatehold the counting rule and its stated exception. - ac-3 (readable without the store or the harness's files).
TestReconstructionIsSelfContainedasserts the artefact contains no absolute path, no store root and no reference resolvable only against the harness;TestReconstructIsDeterministicbounds it from the other side by proving the document carries nothing about the run that produced it.
Tests
internal/core/history: TestReconstructMarksEverySubagentAtItsSpawnAndJoinPoint,
TestReconstructPlacesAnAgentFromTheSpawningTranscriptAlone,
TestReconstructSegregatesUnattributedSubagents,
TestReconstructRendersASessionWithNoMainThread,
TestReconstructChoosesOneRecordPerAgentAndSaysWhich,
TestReconstructRefusesWhatItCannotAnswer,
TestReconstructionIsSelfContained, TestReconstructIsDeterministic,
TestReconstructCannotBeForgedByTranscriptText,
TestReconstructCapsABlockAndSaysSo,
TestSpineModeKeepsTheThreadAndSummarisesTheDelegates,
TestTelemetryReportsEveryRequiredMeasure,
TestTelemetryReportsItsOwnIncompleteness,
TestTelemetryCountsOneUsagePerResponse,
TestTelemetryReportsUsageItCouldNotDeduplicate.
internal/surface/cli: TestReconstructWritesBothFiles,
TestReconstructToStdoutWritesNoFiles,
TestReconstructJSONEnvelopeCarriesTheTelemetry,
TestReconstructRefusesAnAbsentOutDirectory,
TestReconstructRefusesAnUnknownSession.
Gates: surface_coverage over the new sub-verb, the regenerated snapshot and
command reference, and make record-lint over this spec and its intent.
Measured run: a real 55-record session reconstructs to a 7.7 MB artefact plus 64 KB of telemetry in about 0.6 seconds, placing all 54 sub-agents.
Uncertainties
- Whether the full artefact fits the context it is read into. 7.7 MB for a
55-record session does not, for most readers.
spinemode and the per-block cap are the two answers shipped, and neither has been evaluated against a real question put to a real model. - How much lineage survives without the harness's per-agent sidecar. The first attribution rung depends on it. The second rung recovers a spawn point without it, but only for agents the spawning transcript names, so what degrades is the precision of the timeline rather than its existence — and the rate has not been measured.
- Cross-version comparability of the telemetry. The completeness block is what makes a comparison arguable rather than reckless, but nothing has yet compared two harness versions to find out whether the fields hold still.
Out of scope
Every write path into the store. The record schema and the lineage fields, which are itd-2609090559376002's. Capture, ingest and migrate. A corpus-level telemetry roll-up across sessions, which needs a stable per-session file first — this is it, and the roll-up is not. Structured extraction of findings from a transcript. Any reconciliation of the token counts against a vendor's accounting.
Frontmatter
| id | spc-2609091722269727 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-2609091722269727-any-captured-session-can-be-handed-to-an.md
open on GitHub ↗ · commit history ↗
References1
- itd-2609091718595846Any Captured Session Can Be Handed to an Agent as One Self-Contained Artefact, With a Telemetry File Saying What It Costimplements
mentions3
- adr-29A native local redacted transcript corpus
- iss-2609090723027424Summing the token usage reported in a transcript over-counts it, because the harness repeats one response's usage on every content-block line it writes. A single assistant response is written as several lines, each carrying the same message id and the same usage object, so a naive sum multiplies that response's cost by its block count. Measured on one sub-agent transcript here the naive total was 26.7 million against 15.0 million de-duplicated by message id, a factor of 1.77; measured across ten transcripts the same comparison gave 132,336 against 25,046, a factor of 5.28. The factor varies with how many blocks a response happens to have, so it cannot be corrected after the fact by a constant. Any telemetry derived from these transcripts must state the rule that one usage counts once per message id, and must be tested against a fixture containing a multi-block response with a known total. Without that rule a telemetry file is not merely imprecise, it is confidently wrong by a factor that changes between sessions, which is worse than absent for the comparison across runs that such a file exists to support.
- itd-2609090559376002Every Sub-Agent a Session Spawns Leaves the Same Redacted Record Its Main Thread Does