Repository navigation
fix(framework): the run journal never writes on Copilot #681
Description
Activity
Narrowing this, from a captured payload rather than from source.
A real Copilot hook payload, from a probe whose hooks file declared canonical camelCase event names:
{"sessionId": "b349a8c0-…", "timestamp": 1787047151787, "cwd": "…", "toolName": "skill", "toolArgs": "{\"skill\":\"probe-echo\"}"}sessionId, nohook_event_name- exactly whatdetectHost()looks for. So the canonical path works today.The framework declares PascalCase, which Copilot accepts as an alias but rewrites while tagging the entry
_vsCodeCompat, switching it to the snake_case payload. That path is still uncaptured, so the defect stands unconfirmed - but the fix is probably one declaration rather than a change to host detection: emit the canonical camelCase names for Copilot and the existing detection holds.Capture the PascalCase-declared payload before choosing between the two.
Confirmed against a real payload, on 1.0.80
The reasoning in this ticket was read out of a
1.0.57bundle and had never met a payload. It has now. A dumping hook was registered as an additional file in~/.copilot/hooks/— nothing else in that directory was touched — and one session was run.SessionStart, as it actually arrives:{ "hook_event_name": "SessionStart", "session_id": "9410fe62-…", "timestamp": "2026-08-21T14:07:46.858Z", "cwd": "/private/tmp/cop", "source": "new" }The compat shape, exactly as reasoned:
session_idin snake_case, andhook_event_namepresent.detectHostrecognises Copilot as hassessionIdand has nohook_event_name, so it answersnullandprocessPayloadreturns before the journal is touched.Every link in the chain above holds. The fix is the one this ticket describes.
Two things the capture adds.
SessionStartalso carriesinitial_prompt— the user's own text. Nothing here reads it and nothing should, but a detector that starts logging payloads for diagnosis would be logging prompts.Stopcarries atranscript_path, which the earlier reading did not predict:{ "hook_event_name": "Stop", "session_id": "9410fe62-…", "transcript_path": "~/.copilot/session-state/9410fe62-…/events.jsonl", "stop_reason": "end_turn" }That matters twice. It is a second payload shape carrying
transcript_path, so whateverdetectHostdoes must not confuse it with Claude Code (/projects/….jsonl) or Codex (/sessions/YYYY/MM/DD/rollout-…); it matches neither today, and a test should say so rather than leaving it to luck. And it points at a file this repository has never read — see the finding filed separately.Still open: whether a project-scope
SessionStartfires when hooks are deferred past session creation. This capture used a user-scope hook andCOPILOT_ALLOW_ALL=true, so it does not answer it.Captured, and the chain read from source was right
@github/copilot@1.0.80, one real session, the framework's own installed plugin. All three hooks fired — no deferral, no trust prompt, nothing to lift:SessionStart { hook_event_name, session_id, timestamp, cwd, source, initial_prompt } PostToolUse { hook_event_name, session_id, timestamp, cwd, tool_name, tool_input, tool_result } Stop { hook_event_name, session_id, timestamp, cwd, transcript_path, stop_reason, stop_hook_active }That is the
_vsCodeCompatshape this ticket predicted, down to the spelling: snake_casesession_id,hook_event_namepresent and carrying Claude Code's own event names, nosessionIdanywhere. Against the olddetectHost()all three returnednull, so the run journal wrote nothing — confirmed by replaying the untouched captures throughjournal.js's real stdin path.One fact the ticket did not have: both shapes are real. The canonical camelCase payload from the #663 probes is also a genuine capture, so recognition was added rather than swapped. The ticket's third Done-when — "if both shapes can arrive, both are recognised" — is the case that actually applies.
What tells them apart
timestamp, together withhook_event_nameandsession_id. No other host's payload has ever carriedtimestamp, across every fixture in the repository, and the branch sits after the twotranscript_pathpatterns so a host that does carry one is claimed by its own shape first. Verified across all seventeen fixtures: every one still detects as the host it came from, Copilot's Stop event included, and that one does carry atranscript_path.record.jsnow readspayload.sessionId ?? payload.session_idfor Copilot — both spellings are Copilot's own, neither is a fallback guess.Left alone, deliberately
docs/telemetry-limits.mdis untouched. The captured session used a Bash tool only, so nothing in it bears on what the doc says about per-step breakdown. Editing it on this evidence would be the guess this milestone exists to prevent.The gap this opens
lib/step-starts.jsreads Copilot's skill invocation astoolName/toolArgs— the canonical spelling. A compatPostToolUsecarriestool_name/tool_input, so on that shape no step line opens: the session journals, and every record in it reads unattributed. Filed separately, because closing it needs a capture this one does not provide — a Copilot session that actually invokes a skill, to learn what the compat builder calls that tool and where it puts the skill name. Guessing those two field values would be the same mistake this ticket was opened to correct.Gate
node --test scripts/__tests__/aidd-telemetry-journal.test.js— 159 pass, 0 fail, 13 of them new: fixture-based recognition of both shapes, a guard proving the compat rule does not over-claim a Codex or Claude Code payload, the three untouched captures replayed end to end, and a pair showing a recognised payload writes a run file where one missing onlytimestampwrites none.Done and awaiting review in #706, which closes this on merge.
The work is on
claude/aidd-telemetry-layer-e403uf, eleven commits, targetingnext. Gate at the time of push: 365 plugin specs, 1,931 CLI unit, 577 integration, 178 e2e,tscand biome clean, no broken markdown links.Read the eleven commits rather than the pull request's file count — the branch carries its own copy of the CLI migration that
nexthas since received, so the diff counts it twice. The range and the real figures are in the first comment on #706: 142 files, +10,518 −418.Every claim about a tool in this work rests on a session that was actually run. The negative results are kept in full in
aidd_docs/tasks/2026_08/2026_08_22_telemetry-every-tool/measurements.md, including the two probes that disagreed about OpenCode and the measurement that reconciled them.- added 4 commits that reference this issue
on Aug 22, 2026 - added a commit that references this issue
on Sep 2, 2026
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Projects
- StatusShow more project fieldsDone
The breakage
On Copilot,
detectHost()returnsnullfor the framework's own hooks, so the run journal never writes a line.The chain, each link read from source:
flattenCopilotHooksShapepasses Claude's PascalCase event keys through verbatim -cli/tests/domain/formats/flat-hooks-merge.unit.test.ts:80-95assertsresult.hooks.PreToolUse._vsCodeCompat(app.js,tpe())._vsCodeCompatselects a different payload builder. The compat base is{ hook_event_name, session_id, timestamp, cwd }, snake_case; the canonical base is{ sessionId, timestamp, cwd }, camelCase.plugins/aidd-telemetry/hooks/lib/host.jsdetects Copilot as "hassessionIdAND has nohook_event_name". A compat payload has neither property: it hassession_id, and it does havehook_event_name.transcript_pathis absent), sodetectHost()returnsnullandprocessPayloadreturns before touching the journal.Bounds, stated plainly
Established by reading
@github/copilot@1.0.57's bundle. The runtime on the test machine reports1.0.80, and its binary is packed and not readable. This is not confirmed against a real payload.What settles it: install the framework's
.github/hooks/*.hooks.jsonin a repo, run a Copilot session, dump the hook's actual stdin, and read which key set arrives.Done when
detectHost()recognises whichever shape actually arrives, and a test fails if it stops doing so.Also worth knowing
Copilot defers project-scope hooks past session creation. In non-interactive prompt mode the deferred load runs only when the folder is trusted or
COPILOT_ALLOW_ALL=true/GITHUB_COPILOT_PROMPT_MODE_REPO_HOOKS=trueis set. Whether a project-scopeSessionStarthook still fires after a deferred load is not established, and the journal's first line depends on it.Relations