Read and fork GitHub Copilot CLI sessions - #9
Merged
Merged
Conversation
Copilot keeps one directory per session under $COPILOT_HOME/session-state: workspace.yaml for the metadata, events.jsonl for the append-only log. Resuming appends to the same log, so a session is always one file. On the timeline: user.message content (the raw text, not the transformedContent copy that carries injected datetime and system-reminder context) and assistant.message content, which is empty on turns that only request tools. Sub-agent traffic arrives under subagent.* types and stays off the timeline. Recency comes from the log's mtime, not workspace.yaml's updated_at, which lags a session that is still appending. Both handoff halves are native: `copilot --resume=<id>` forks, `copilot -i <prompt>` seeds. The id goes inline because a bare --resume opens the session picker and would swallow a separated argument. Copilot also exports COPILOT_AGENT_SESSION_ID into every shell it spawns, so a catchup run from inside a session resolves that session instead of guessing by recency. Fixes #8
The CLI ships schemas/session-events.schema.json, which settles three things the first pass guessed at. Sub-agent turns are not a separate event type. They reuse user.message and assistant.message and are marked by the envelope's agentId, "absent for events from the root/main agent" — so a sub-agent's prompts and answers were landing in the transcript, and its model could overwrite the session's. Events carrying an agentId are now skipped. A session.compaction_complete carries success, and on success the summaryContent that replaced the history. A failed compaction removed nothing and is not a seam, so it no longer produces a marker that --since-compact would cut on; a successful one now carries its summary instead of being bare. The session id is the directory name alone. It is what --resume takes and what workspace.yaml repeats, and reading it from the yaml meant a listed id was not guaranteed to resolve. Tests follow the schema rather than the earlier guess, and cover the failed compaction, the sub-agent turns, the listed-id round trip, and the Copilot argv in the CLI command-contract tests.
Once the directory name became the canonical session id, dirInfo's id field was a second copy of filepath.Base(path) — state that can only ever disagree with the path it came from, and Read was seeding it from a caller-supplied Ref. The two uses derive it instead. Also names the shipped event schema, not just the live install, as where the format reference comes from, and says in the package doc that the directory name is the id --resume takes.
wilbeibi
force-pushed
the
copilot-provider
branch
from
August 25, 2026 09:34
9f76530 to
84ecd06
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #8, where a user running Claude, Copilot, and Cursor asks for handoff from Copilot. catchup read two of those three, so a Copilot session could only ever be continued in Copilot.
Approach
Copilot keeps one directory per session under
$COPILOT_HOME/session-state(default~/.copilot):workspace.yamlfor metadata,events.jsonlfor the append-only log. Resuming appends to that same log, so a session is always one file — no cross-file stitching. The directory name is the session id: what--resumetakes, and what every listing reports, so a listed id resolves by construction.Event shapes come from
schemas/session-events.schema.json, which the CLI ships. Two judgments it settles, both of which the first commit guessed wrong:user.messageandassistant.messageand are marked only by the envelope'sagentId— "absent for events from the root/main agent". They are a parent turn's tool plumbing, so every event carrying anagentIdis skipped, the model included: a sub-agent routed elsewhere would otherwise overwrite the session's.session.compaction_completecarriessuccessand, on success,summaryContent. The marker carries that summary, so--since-compactopens on what replaced the history. A failed compaction removed nothing and is not a seam, so it produces no marker at all.Recency is the event log's mtime, not
workspace.yaml'supdated_at: the yaml is rewritten when metadata changes, so it lags a session that is still appending. A test pins that. The rest of the mapping — raw versus transformed user text, empty content on tool-only turns, per-turn model — is stated in the package doc.Both handoff halves are native, so neither direction needs the refusal text zcode and deepseek carry:
copilot --resume=<id>forks andcopilot -i <prompt>seeds. The id is inline because a bare--resumeopens the session picker and would swallow a separated argument. Both spellings are pinned in the CLI command-contract tests.Copilot exports
COPILOT_AGENT_SESSION_IDinto every shell it spawns, so a catchup run from inside a session resolves that session instead of guessing by recency — the same signal Claude Code provides.workspace.yamlis a flat map of scalars, parsed in a dozen lines rather than bought with a YAML dependency.go.modis unchanged.Reviewing
Three commits, worth reading in order: the first adds the provider, the second corrects three judgments the shipped schema disproved, the third deletes the state that correction made redundant. Everything load-bearing is in
internal/copilot/copilot.go, whose package doc states the format and every call above; the rest is the ten-line wiring each provider needs, plus two doc lists.What the later commits changed, so nobody reads the first as final:
subagent.*type the reader can skip by name" toagentIdon the ordinary types — the first pass let their text into transcripts--since-compactcut pointworkspace.yaml, where a listing could report an idResolvewould then reject — and with one source of truth left,dirInfono longer carries a copy of itVerified
Against real sessions from
@github/copilot1.0.80:Also on real logs: read,
-q,--last N,--id,--json,--dir, cross-agent listing,install-skill copilot, and both fork directions with a PATH shim so nothing launched.Compaction and sub-agents were exercised on a synthetic log built to the shipped schema —
--since-compactcuts at the successful compaction, prints its summary, and drops the sub-agent turn — because no local session compacted or spawned a sub-agent. Both behaviors are mutation-checked: deleting theagentIdskip failsTestReadSkipsEverythingButConversation(7 entries, want 5), deleting the success guard failsTestCompactionMarkers.go build ./...,go vet ./..., andgo test ./...pass.