feat: session transformer & resume across Copilot, Claude Code, Codex, Gemini & gitagent#97
feat: session transformer & resume across Copilot, Claude Code, Codex, Gemini & gitagent#97Nivesh353 wants to merge 8 commits into
Conversation
Transform a conversation between tools via a neutral canonical format and
continue it in any of them (any → any) — same hub pattern as our
agent-definition adapters.
- spec/schemas/session.schema.json: canonical session format (messages + tool calls + memory)
- src/sessions/: canonical types + registry, and read/write adapters per tool
copilot → events.jsonl + session-store.db (node:sqlite)
claude → Anthropic transcript under ~/.claude/projects
gitagent → distilled MEMORY.md + committed chat/* branch (voice UI)
- src/commands/session.ts: `opengap session list | export | import`
- src/index.ts, docs.md: register + document
Resume uses each tool's own native resume; memory is carried into the target.
Includes round-trip tests, node:sqlite guard, and injection-safe git calls.
shreyas-lyzr
left a comment
There was a problem hiding this comment.
Three real issues worth fixing before merge, plus one minor observation. The session portability concept is solid and the adapter pattern is clean — mostly execution details.
Blocking: partial write in when the global store DB is missing
Blocking: silent branch-write on wrong branch in the gitagent git-native path
Non-blocking: collision in the Claude adapter
Minor: UUID validation/assignment ordering in
Inline comments below.
…tch error, encodeCwd doc, uuid validate-first
shreyas-lyzr
left a comment
There was a problem hiding this comment.
All four issues from the previous review are addressed:
- Copilot partial write: requireSqlite() and the store-DB existence check are now both at the top of writeCopilot, before writeSessionState runs. An early failure leaves no orphaned state dir.
- gitagent wrong-branch commit: the checkout fallback now throws explicitly instead of falling through silently. The finally block still restores the original branch correctly.
- encodeCwd lossy collision: the doc comment now clearly explains the slash-to-dash encoding is Claude Code's own scheme, and calls out the alice-bob/alice/bob collision scenario.
- sessionId validate-before-assign: the UUID guard runs before the const assignment, which reads cleanly now.
Good to merge.
Codex read/write adapter: parse rollout JSONL → canonical (dropping synthetic <environment_context>/<user_instructions> injections), and write a native rollout + register in ~/.codex/state_<N>.sqlite threads so `codex resume` lists it (Node 22.5+). gitagent: list all chat/* branches (matches voice UI) and read a branch's history via `git show` when it isn't checked out. Plus codex wiring in session --from/--to help + schema, docs, and codex.test.ts. 57/57 pass.
Read/write Gemini CLI sessions. Reader parses ~/.gemini/tmp/<slug>/ chats/*.jsonl (skipping $set patches + synthetic <session_context>/ editor-context turns), maps user/gemini/toolCalls/functionResponse → canonical, and resolves cwd via ~/.gemini/projects.json. Writer emits a --session-file JSONL (metadata + user/gemini turns, memory as a leading turn); metadata carries sessionId + projectHash (sha256 of cwd) so Gemini's loader accepts it. Resume: `gemini --session-file <path>`. Register gemini + wire into session --from/--to help and schema; add gemini.test.ts and docs. Verified read + resume end-to-end. 59/59 pass.
Gemini logs an assistant turn's text twice — once as a plain record, then again on the record carrying the tool call — so the reader emitted the message twice. Track the last assistant text and drop the immediate repeat (keeping its tool call). Add a dedupe test. 60/60 pass.
shreyas-lyzr
left a comment
There was a problem hiding this comment.
Reviewed the last 3 commits (codex adapter, gemini adapter, and gemini dedupe fix). Overall the implementation is solid and well-tested. A few things worth addressing before this merges.
codex.ts — hardcoded model/context-window
The write path hardcodes model to 'gpt-5.5' and modelContextWindow to 258400. If Codex validates these fields against a known-model list during resume, the imported session will fail to replay. Worth either reading the model from an existing real session (parallel to detectCopilotVersion) or confirming Codex treats unknown values permissively.
gemini.ts — tool calls dropped on write
The write path drops tool_call / tool_result items with the comment "matching Gemini's own import filter". That is the right call if Gemini's --session-file truly ignores those records, but the comment itself says "best-effort, verify". If Gemini does accept functionCall/functionResponse in its JSONL, dropping them loses context the agent would benefit from on resume. Worth surfacing this explicitly in docs.md as a known limitation (not just an inline comment) so users aren't surprised.
gemini.ts — const dir = join(cwd)
join of a single argument is a no-op — the variable just aliases cwd. Either drop it or give it a name that communicates intent.
codex.test.ts — fixture shape comment
The third test works correctly (the fixture line is a properly-enveloped response_item), but a future reader seeing the helper function might not immediately see the outer envelope. A short comment on the fixture would prevent a future edit from breaking the test.
gemini.test.ts — ordering of reasoning + last message
The first test expects ['message', 'tool_call', 'tool_result', 'reasoning', 'message']. Reasoning and the final text both come from the same record (g4 has both thoughts and content). This is correct but non-obvious — a comment marking which fixture record produces each canonical item would help when the fixture is edited later.
The Gemini writer previously dropped all tool_call/tool_result items. Gemini's --session-file import actually accepts them, so carry them: tool_call → a `gemini` record with `toolCalls`, tool_result → a `user` record with a `functionResponse`, paired by id so Gemini's history reconstruction matches call↔response. Sources that link tool calls by order rather than id (gitagent) have no ids, so their tool steps are still dropped rather than emitted unpaired (which Gemini could mis-render). Conversation text always resumes. Verified end-to-end: codex→gemini resume renders the tool steps and the agent continues in-context. Update docs limitation + add writer tests. 62/62 pass.
The Gemini writer previously dropped all tool_call/tool_result items. Gemini's --session-file import actually accepts them, so carry them: tool_call → a `gemini` record with `toolCalls`, tool_result → a `user` record with a `functionResponse`, paired by id so Gemini's history reconstruction matches call↔response. Sources that link tool calls by order rather than id (gitagent) have no ids, so their tool steps are still dropped rather than emitted unpaired (which Gemini could mis-render). Conversation text always resumes. Verified end-to-end: codex→gemini resume renders the tool steps and the agent continues in-context. Update docs limitation + add writer tests. 62/62 pass.
shreyas-lyzr
left a comment
There was a problem hiding this comment.
All three issues from the previous review are resolved.
- Codex model/context-window: both detectModel() and detectContextWindow() now scrape real local session data before falling back to gpt-5.5/258400. The fallback values are never used if a real Codex installation is present.
- Gemini join(cwd) no-op: removed. The write path uses cwd directly.
- Tool-call silent drop: documented in docs.md as a 'Known limitation' section (line 32) with the id-pairing rationale, and backed by a dedicated test ('drops tool steps when ids are absent'). Users won't be surprised.
Gemini and Codex adapters look solid. Approved.
Summary
Makes a conversation portable across tools. You can transform a session from
one coding agent into another and continue it there — any → any across
Copilot ⇄ Claude Code ⇄ gitagent.
Same pattern OpenGAP already uses for agent definitions: one canonical format
in the middle, with per-tool read/write adapters.
Copilot ─┐ ┌─ Copilot
Claude ─┼─► canonical session ─┼─ Claude (any → any)
gitagent ─┘ (neutral format) └─ gitagent
How it works
(ordered
messages+tool_call/tool_result+memory).What's included
spec/schemas/session.schema.json— canonical session schemasrc/sessions/canonical.ts— types + adapter registrysrc/sessions/{copilot,claude,gitagent}.ts— per-tool read/write adapterssrc/commands/session.ts— newopengap sessioncommand (list,export,import)src/index.ts,docs.md— register + documentPer-tool specifics
events.jsonlevents.jsonl+session-store.db(node:sqlite)copilot --resume=<id>~/.claude/projects/*.jsonlopengap run -a claude --resume=<uuid>MEMORY.mdMEMORY.md+ committedchat/*branchMEMORY.md)Memory travels with the session (carried as leading context) so the target
recalls facts even when a transcript is thin.
Usage
Testing
54/54 unit tests pass (round-trip + schema validation).
Verified end-to-end: Copilot → gitagent (recalls in CLI + voice), Copilot resume in its CLI.
Hardened: node:sqlite guard, injection-safe (arg-array) git calls, MEMORY.md import dedup.