Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
beb921c
feat: retain turn evidence and build remaining observability deck views
Trecek Oct 7, 2026
31c01d6
fix: align deck source coverage and rendering with indexed evidence
Trecek Oct 8, 2026
60888af
test: invoke void shell boot through MiniRacer eval
Trecek Oct 8, 2026
d85907c
fix(review): preserve tokenizer exception diagnostics
Trecek Oct 8, 2026
951da9d
refactor(review): share latest observation timestamp selection
Trecek Oct 8, 2026
223a065
refactor(review): share primitive availability state checks
Trecek Oct 8, 2026
a83cce0
refactor(review): share deck evidence link parameters
Trecek Oct 8, 2026
a4afa78
refactor(review): share unavailable count labels
Trecek Oct 8, 2026
1752c3c
refactor(review): share measured token value decoding
Trecek Oct 8, 2026
5dd9f14
refactor(review): share coverage state count formatting
Trecek Oct 8, 2026
7d07bb0
refactor(review): centralize shared report test builders
Trecek Oct 8, 2026
aa21de9
refactor(review): declare tokenizer test imports once
Trecek Oct 8, 2026
9f73b07
test(review): remove duplicate missing ledger case
Trecek Oct 8, 2026
db57594
test(review): seed deck session fields without JSON round trip
Trecek Oct 8, 2026
2b99d16
test(review): isolate renderer cases and context input toggle
Trecek Oct 8, 2026
9e051dc
fix(review): reuse the session view selection set
Trecek Oct 8, 2026
8c16af5
fix(review): share coverage state cell rendering
Trecek Oct 8, 2026
339cd06
fix(review): reuse the deck day duration constant
Trecek Oct 8, 2026
da0cffb
fix(review): omit shadowed unlinked span encoding reason
Trecek Oct 8, 2026
f448651
fix(review): name context span processing arguments
Trecek Oct 8, 2026
b768f76
fix(review): reuse the shared context measure decoder
Trecek Oct 8, 2026
dcbacbd
fix(review): share level grouping while preserving window policies
Trecek Oct 8, 2026
c014585
fix(review): assert per-session missing ledger gap evidence
Trecek Oct 8, 2026
2f010b4
fix(review): verify context provenance after JSON serialization
Trecek Oct 8, 2026
c20f606
fix(review): assert prepared renderer view markers
Trecek Oct 8, 2026
8096d99
fix(review): remove redundant renderer links assertion
Trecek Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 63 additions & 14 deletions docs/developer/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ its rotated `otlp.jsonl.1` generation.
It stores report facts separately from the retained `sessions.jsonl`
projection.

Every v2 row carries `schema_version`, `kind`, `key`, `session_id`, and
Every v3 row carries `schema_version`, `kind`, `key`, `session_id`, and
`time_ms`. `time_ms` is epoch milliseconds: session rows use the source
session timestamp; OTLP rows use `timeUnixNano`, falling back to
`observedTimeUnixNano`. Rows are append-only upserts keyed by `(kind, key)`;
Expand All @@ -315,6 +315,16 @@ when a key is written again, readers keep its last row.
| `request` | `{session_id}:{request_id}` | `harness`, `request_id`, `agent_name`, `query_source`, `model`, `event_sequence`, the four token fields, `cost_usd`, `duration_ms` |
| `tool` | `{session_id}:{tool_use_id}`, or `{source_id}#{ordinal}` when there is no tool-use ID | `harness`, `agent_name`, `tool_name`, `tool_use_id`, `error_type`, `success`, `duration_ms`, `tool_input_size_bytes`, `tool_result_size_bytes`, `event_sequence` |
| `subagent` | `{source_id}#{ordinal}` | `harness`, `agent_type`, `model`, `final_model`, `model_swapped`, `event_sequence` |
| `turn` | `{source_id}:turn:{ordinal}` | Exact owning `session_key`, source identity and ordinal, request/message IDs, timestamp, harness/provider/resolved model, four structured token measures, `context_window_tokens`, `context_fraction` |

Completed sessions admit turns through their versioned `token_usage.json` descriptor.
The descriptor declares the ledger schema, basename and row count; ledger rows do not
repeat that schema marker. Only a complete, validated `turn_usage.jsonl` is admitted.
Session rows retain `turn_usage_state` and `turn_usage_reason`, including when no turn
rows exist. A null ledger reference with count zero means no published ledger, not
measured-zero counters. Missing, malformed or unsupported descriptors and ledgers
retain explicit coverage reasons. Turns keep their emitting attempt's exact key;
untimed turns and reused session IDs never require temporal attribution.

Native child `subagent` rows use
`{parent_session_key}:native:{backend}:{native_parent_session_id}:{child_id}`.
Expand All @@ -326,6 +336,17 @@ invocation observations. A managed attempt or launch alias alone cannot establis
native actor. Resumed parents sharing a native session ID require unique invocation
ownership; ambiguous children remain unattributed.

`parent_context_spans` retains counts of exact linked parent-visible invocation prompts
and returned text, with source-record/turn identity, timestamps, the resolved parent
invocation model and tokenizer provenance. These counts exclude hidden instructions,
message framing and unrelated parent work; child billing and final transcript text do
not establish what the parent received. Verified OpenAI parent models recognized by
`tiktoken` use that model's encoder for both sides. Missing links, incomplete text,
unresolved models, unsupported providers and encoder initialization failures retain
explicit unavailable reasons. Explicit empty text is a measured zero. Raw prompt and
result text are not copied into the derived index. After resolving tokenizer asset
initialization failures, use `sessions index --rebuild` to refresh unavailable counts.

Native child tokens and tools come from that child's verified dedicated transcript,
with native message/request snapshots deduplicated. Tokens require the child's own
effective provider; missing provider/model identities never inherit parent values.
Expand Down Expand Up @@ -357,7 +378,7 @@ memory; it is not persisted.

Events join to session attempts by `session_id` and `time_ms`. With a timestamp,
an event is attributed to the attempt running at that time: if it predates all
attempts, the earliest attempt is used; otherwise the latest attempt that began
attempts, the event remains unattributed; otherwise the latest attempt that began
by that time is used. Equal starts are resolved by the lexicographically greater
session key, which gives a deterministic attempt order. An event without a time
is attributed only when its session ID has exactly one attempt; otherwise
Expand All @@ -366,14 +387,15 @@ is attributed only when its session ID has exactly one attempt; otherwise
The writer holds an exclusive lease for the report index. Each commit appends
and fsyncs `rows.jsonl` before atomically replacing the versioned `state.json`
with the committed walk watermark, byte offset, `row_schema_version`, and
`child_evidence_fingerprint`. On the next open, bytes past that offset are truncated.
`report_evidence_fingerprint`. On the next open, bytes past that offset are truncated.
The row-schema marker is an application-controlled compatibility boundary, distinct
from the state envelope version. Missing/incompatible row-schema evidence resets the
generation before append admission. Recoverable byte/watermark corruption within a
known current schema retains the existing complete-line recovery behavior.

The child-evidence fingerprint covers canonical native child facts and dedicated
transcript dependencies, including changes, additions and removals. Drift selects a
The report-evidence fingerprint covers canonical native child facts, child and parent
transcript dependencies, tokenizer version, and retained turn descriptors and ledgers,
including changes, additions and removals. Drift selects a
full replay under the existing exclusive index lease before unchanged live or consumed
archive parents can be skipped. Markers commit with the successful row/watermark state;
this reset does not preserve rows whose original sources are no longer retained.
Expand All @@ -389,28 +411,31 @@ Rebuilding reads only the sources still retained on disk. Rows from OTLP data
that has rotated out survive through incremental updates, but a rebuild cannot
re-derive those older facts. A rebuild equals an incremental index when source
changes are limited to those tracked by the walk: appended OTLP, OTLP rotation,
and changed, added, or evicted session rows, plus native child evidence changes.
Parent transcript changes still require `--rebuild` when their session row is unchanged.
and changed, added, or evicted session rows, plus report-evidence dependency changes.
Changed parent transcripts, turn descriptors and ledgers refresh the index even when
their session projection is unchanged.

Codex OTLP token events are not projected: they do not carry a stable request
identity for deduplication. Codex token measures enter the report index through
the session rows.
the session rows and admitted turn ledgers.

## Observability deck

`autoskillit sessions deck <output>` incrementally refreshes the report index and writes
one self-contained HTML deck. It can be opened from `file://` without a server or network
connection. It receives all four index fact collections as mapping iterables; the
connection. It receives sessions, requests, tools, subagents and turns as mapping iterables; the
report layer imports core types and helpers rather than the execution index type.
The cohort view retains its session table. Spend, efficiency, skill and role views
select metric blocks prepared by the shared aggregation library, without browser
aggregation of measures.
aggregation of measures. Context, errors, trend, gaps and parity also select prepared
populations with their own availability coverage, history and cohort filters.
ADR-0017 records why charts use first-party SVG.

### Payload contract

The payload keys are `generated_at_ms`, `index_schema_version`, `landing`, and `history`;
column tables `tables.sessions`, `tables.skills`, and `tables.roles`; `facets[{id,label,column,kind}]`;
column tables for sessions, skills, roles, turns, errors, trends, gaps and parity;
`facets[{id,label,column,kind}]`;
`views[{id,question,decision,group,status,issue}]`;
`chips{view:{facet:[{key,label,state,count,reason,issue,match|days}]}}`; and
`availability[{state,label,description}]`. `match` is used for value facets and `days`
Expand All @@ -433,6 +458,27 @@ Role level selections mean the spawning skill level, while the actor remains L0.
Identity details, definitions and known cross-references remain available in empty
usage cohorts. A stale explicit window or facet selection yields no measurements.

Context shows each turn's cache-read occupancy proxy as a percentage of its resolved
model window. Missing windows or counters leave gaps in the series. Cache read, cache
write and output are visible by default; inclusive input is an optional separate
series. Claude's ledger input already includes cache read and cache creation, so adding
it to that stack would double count. Parent prompting and returned text are a separate
comparison of recorded spans tokenized at the parent invocation model, with matched
sample counts and explicit missing-source coverage.

Errors distinguish session failures from failed indexed tool events and identify
symptom, skill and recipe step. Session failure rates use known outcomes; missing tool
events do not establish zero errors or a complete tool history. Trends use UTC daily
buckets and compare the earlier and later halves of the selected interval, retaining
model, skill, step and source-pair identity. Sample counts, missing buckets and untimed
exclusions accompany the descriptive changes; absent observations establish no trend.

Gaps describe unanswered questions and their missing prerequisites. Parity compares
observed harness/provider fields against the deck's target fields. Both derive coverage
from selected eligible records, including absent ledgers, unknown windows, unattributed
tools and missing or unsupported parent spans. Mixed coverage and no observations are
derived labels, not additional primitive token states; measured zero remains distinct.

Canonical role routes provide offline definition links. The artifact preserves Claude
tools/model separately from Codex reader tools/model and renders original definition
body text literally. Manual ratio/tool-mix review controls require observed eligible
Expand All @@ -453,7 +499,9 @@ the retained history cite [#4621](https://github.com/TalonT-Org/AutoSkillit/issu

Routes use `#/<view>[/<entity>][?k=v1,v2]`. Cohort selections survive navigation between
views and entities; the `sort` choice applies only to its view. Every state change is a
hash navigation, so browser back and forward restore prior selections. Planned views
hash navigation, so browser back and forward restore prior selections. Context, gaps
and parity preserve an optional exact owning-session key in the `session` parameter.
Planned views
show a notice while keeping the cohort bar available.

### Offline rendering and client code
Expand Down Expand Up @@ -491,8 +539,9 @@ assistive technology.
4. Confirm L0 is struck with its reason. On a legacy index, L1–L3 should show ✕ with
#4622; windows longer than retained history should show ✕ with #4621.
5. Follow a harness link, then use back and forward; confirm the cohort remains selected.
6. Open a planned route such as `#/spend`; confirm its notice appears and the cohort bar
remains visible.
6. Visit context, errors, trend, gaps and parity; change windows and cohort filters.
Confirm unavailable counters remain missing, observed zeros are labeled, input is
initially hidden, and session links preserve the selection across context/gaps/parity.
7. Emulate `prefers-color-scheme: dark`, then `light`; confirm both palettes render.
8. In print preview, confirm the light palette, vector bars, printed cohort sentence, and
hidden navigation rail.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies = [
"cyclopts>=4.0",
"regex>=2026.1",
"structlog>=25.0,<26.0",
"tiktoken>=0.14.0",
"zstandard>=0.23",
"uvloop>=0.21.0; sys_platform != 'win32'",
]
Expand Down
79 changes: 70 additions & 9 deletions src/autoskillit/assets/deck/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ globalThis.DeckCore = (() => {
"use strict";
const CHIP_STATES = Object.freeze({LIVE: "live", STRUCK: "struck", ABSENT: "absent"});
const DAY_MS = 86400000;
const SESSION_VIEWS = ["context", "gaps", "parity"];
const enc = encodeURIComponent;
const formatCount = n => new Intl.NumberFormat("en-US").format(n);
const isPrimitiveState = state => ["measured", "measured_zero", "unknown",
"unavailable", "not_applicable"].includes(state);
const inspectParams = row => Object.fromEntries([
["harness", row?.harness], ["provider", row?.provider], ["skill", row?.skill],
["recipe", row?.recipe], ["step", row?.step], ["model", row?.model],
["session", row?.session_key]
].filter(([, value]) => value != null).map(([key, value]) => [key, [value]]));
const countLabel = value => value == null ? "count unavailable" : formatCount(value);
const measureValue = measure => measure?.state === "measured" ? measure.value :
measure?.state === "measured_zero" ? 0 : null;
const formatStateCounts = counts => Object.entries(counts ?? {}).map(([state, count]) =>
state + " " + formatCount(count)).join(" · ");
const formatDate = ms => new Date(ms).toISOString().slice(0, 10);

function formatRatio(ratio, percent = false) {
Expand Down Expand Up @@ -84,6 +97,15 @@ globalThis.DeckCore = (() => {
return ctx.availabilityCell(measure ?? {state: "unavailable"});
}

function coverageStateCell(ctx, measure, className) {
const state = measure?.state;
if (isPrimitiveState(state)) return ctx.availabilityCell(measure);
return ctx.el("span", {class: className ??
"coverage-state coverage-state--" + (state ?? "unknown")},
state === "no_observations" ? "no observations" :
state === "mixed" ? "mixed coverage" : state ?? "unknown");
}

function ratioCell(ctx, ratio, percent = false) {
const value = formatRatio(ratio, percent);
return ctx.el("div", {class: "view-measure"}, [
Expand Down Expand Up @@ -175,9 +197,17 @@ globalThis.DeckCore = (() => {
}

function hrefFor(route, target, cohortKeys) {
const params = Object.fromEntries(cohortKeys.filter(k =>
const identityViews = ["context", "errors", "trend", "gaps", "parity", "skill", "cohort"];
const preserve = [...cohortKeys];
if (identityViews.includes(target.view)) preserve.push("skill", "recipe", "step", "model");
if (SESSION_VIEWS.includes(target.view)) preserve.push("session");
const params = Object.fromEntries(preserve.filter(k =>
Object.hasOwn(route.params, k)).map(k => [k, [...route.params[k]]]));
for (const [key, value] of Object.entries(target.params || {})) {
if (key === "session" && !SESSION_VIEWS.includes(target.view)) {
delete params[key];
continue;
}
if (value === null) delete params[key];
else Object.defineProperty(params, key, {value, enumerable: true,
writable: true, configurable: true});
Expand Down Expand Up @@ -227,17 +257,20 @@ globalThis.DeckCore = (() => {
const providers = new Set((chips.provider ?? [])
.filter(chip => provider.keys.includes(chip.key)).map(chip => chip.match));

function selectedRows(blocks) {
if (!window.chip) return [];
function selectedBlock(blocks) {
if (!window.chip) return null;
const inWindow = blocks.filter(block => block.window === window.chip.key);
const domain = [...new Set(inWindow.flatMap(block => block.levels))];
const selectedLevels = new Set(level.keys
.map(key => (chips.level ?? []).find(chip => chip.key === key)?.match)
);
const requested = domain.filter(value => selectedLevels.has(value));
if (!requested.length) return [];
const block = inWindow.find(item => sameValues(item.levels, requested));
return block?.rows ?? [];
if (!requested.length) return null;
return inWindow.find(item => sameValues(item.levels, requested)) ?? null;
}

function selectedRows(blocks) {
return selectedBlock(blocks)?.rows ?? [];
}

let skillRows = viewId === "role" ? [] : selectedRows(prepared.skills ?? []).filter(row =>
Expand All @@ -252,19 +285,47 @@ globalThis.DeckCore = (() => {
if (viewId === "role" && route.entity != null) {
roleRows = roleRows.filter(row => row.role === route.entity);
}
const viewData = prepared[viewId];
const hasPreparedPopulation = viewData !== null && typeof viewData === "object";
const block = Array.isArray(viewData?.blocks) ? selectedBlock(viewData.blocks) : null;
let rows = hasPreparedPopulation ? [] : viewId === "role" ? roleRows : skillRows;
let metrics = rows;
if (Array.isArray(viewData?.blocks)) {
const sessionKey = SESSION_VIEWS.includes(viewId) ?
params.session?.[0] ?? null : null;
const sessionBlock = sessionKey == null ? null :
(block?.sessions && Object.hasOwn(block.sessions, sessionKey) ?
block.sessions[sessionKey] : null);
const sourceRows = sessionKey == null ? block?.rows : sessionBlock?.rows;
const sourceMetrics = sessionKey == null ? block?.metrics : sessionBlock?.metrics;
const identityKeys = ["skill", "recipe", "step", "model"];
const matchesSelection = row =>
(row.harness == null || harnesses.has(row.harness)) &&
(row.provider == null || providers.has(row.provider)) &&
identityKeys.every(key => !Array.isArray(params[key]) || params[key].length === 0 ||
row[key] == null || params[key].includes(row[key]));
rows = (sourceRows ?? []).filter(matchesSelection);
const filterMetricRows = value => Array.isArray(value) ? value.filter(matchesSelection) :
value && typeof value === "object" ? Object.fromEntries(Object.entries(value).map(
([key, rows]) => [key, Array.isArray(rows) ? rows.filter(matchesSelection) : rows])) : value;
metrics = filterMetricRows(sourceMetrics) ?? [];
}
const relationships = (prepared.relationships ?? []).filter(edge => {
if (viewId === "skill" && route.entity != null) return edge.skill === route.entity;
if (viewId === "role" && route.entity != null) return edge.role === route.entity;
return true;
});

return {
rows,
metrics,
skillRows,
roleRows,
relationships,
definitions: prepared.definitions ?? {},
viewHistory: prepared.view_histories?.[viewId] ?? null,
selection: {window, level, harness, provider}
selection: {window, level, harness, provider,
session: SESSION_VIEWS.includes(viewId) ? params.session?.[0] ?? null : null}
};
}

Expand Down Expand Up @@ -390,7 +451,7 @@ globalThis.DeckCore = (() => {
return Object.freeze({decodeTable, encodeRoute, decodeRoute, hrefFor, effectiveSelection,
toggleSelection, windowSelection, selectPrepared, filterRows, populationSentence,
summarizePairs, sortRows, parseSort, chipPresentation, availabilityPresentation,
barLayout, formatCount, formatRatio, ratioSample, reviewEligibility, reviewSignal,
measureCell, ratioCell, definitionCard, toolMix, roleHarnessRows, formatDate,
isPrimitiveState, inspectParams, countLabel, measureValue, formatStateCounts, barLayout, formatCount, formatRatio, ratioSample, reviewEligibility, reviewSignal,
measureCell, coverageStateCell, ratioCell, definitionCard, toolMix, roleHarnessRows, formatDate,
CHIP_STATES, DAY_MS});
})();
Loading
Loading