feat: add Grok CLI as a bidirectional harness, plus Claude Code porting fixes#4
Open
Solomon-mithra wants to merge 8 commits into
Open
Conversation
Reads ~/.grok/sessions/<percent-encoded-cwd>/<uuid>/ (summary.json + chat_history.jsonl) into the Hub IR: text turns only; reasoning, tool_result, and tool-call-only assistant records counted into droppedTurns; synthetic user injections (system reminders, compaction meta, project instructions) surfaced via sourceMetadata.syntheticSkipped. Import-only for now: no export adapter is registered, so listInstalled() never offers grok as a destination and verify never targets it. GROK_HOME override honored in detection and the adapter, mirroring CODEX_HOME.
… harness Synthesizes a native Grok session directory (summary.json + chat_history.jsonl) under sessions/<percent-encoded-cwd>/<uuid>/, in Grok's own message shapes: user text re-wrapped in the <user_query> envelope Grok writes itself, assistant content as a plain string with model_id. The import adapter's envelope strip is now the exact inverse (one newline each side), so grok round-trips through the hub with full text parity. - summary.json carries an openmemory_source stamp; teardown() scans for it (no adapter ledger, matching the other exports) - existingDestId overwrites the same session dir in place - verify: grok added as an isolated destination (setGrokSessionsRoot temp root, reimport via the grok import adapter's explicit-path parse) - session ids minted as UUIDv7 via Bun.randomUUIDv7 to match native ids
…ance stamp Porting claude-code -> grok and then grok -> claude-code duplicated the original claude session: the grok copy has a fresh session id, so the ledger triple (grok, <copy-id>, claude-code) had no record and exported as new. The data to prevent this already existed — every export stamps provenance onto the destination. Importers now surface that stamp as sourceMetadata.openmemoryOrigin (grok: summary.json openmemory_source; claude-code: the per-line openmemorySource), and exportSessions() skips any session whose origin harness IS the destination, reporting the original id in the outcome. Exporting to a third harness is unaffected; --force overrides deliberately.
…eric becomes a dash encodeCwd only mapped "/" to "-", but Claude Code encodes every character outside [A-Za-z0-9] (verified against a real store: /Users/x/Job_resume -> -Users-x-Job-resume). Any cwd containing "_", ".", or spaces was exported into a sibling project folder Claude never reads for that cwd, so the ported session existed on disk but was invisible in the resume picker. Found by dogfooding: ported a session for a cwd under Job_resume/ and could not find it in claude --resume.
…summary record
The /resume picker titles a session by its first user prompt unless the file
carries a summary record for the chain leaf — the mechanism Claude itself uses
for compacted sessions. Ported sessions were losing their source title (Grok's
generated_title, opencode's session title) and degrading to whatever the first
prompt happened to be.
buildTranscript() now appends {type: summary, summary: <IR title>, leafUuid:
<last turn uuid>} when the session has a non-blank title and at least one turn.
The claude-code import parser already ignores summary records, so re-import
parity is unchanged.
The export side writes a summary record carrying the source title; the import side ignored it, so a session ported INTO Claude (or compacted by Claude itself) still listed under its first user message in the source picker. deriveTitle now prefers the last summary record when one exists — exact, no heuristics — and falls back to first-message derivation otherwise. Round-trip parity test extended: title now survives claude export -> claude import.
…overy Claude Code runs background side-sessions to generate the conversation summaries shown in its own UI. They land in ~/.claude/projects as ordinary 2-turn .jsonl files (a queue-operation line, the summarizer prompt as a 'user' turn, the <summary> reply) with no isMeta/isSidechain marking, so discovery listed every one of them as a bogus '2 messages' conversation — 121 of them on the store where this was found, burying the real sessions in the source picker. previewSessionFile() now flags them (the summarizer prompt's fixed preamble is the discriminator; only the FIRST user turn is checked, so real chats that merely mention the phrase are unaffected) and listSessions() skips them. parse() by explicit path is deliberately untouched.
… title with env boilerplate Three more discovery warts found on a real store: - agent-*.jsonl subagent transcripts (the newer flat layout keeps them in the project dir itself, not under subagents/) listed as bogus 0-message chats. Excluded by name in listClaudeSessionPaths, and execSync's interpolated find string became execFileSync with an argument array while touching it. - All-sidechain / aborted sessions listed with 0 messages. previewSessionFile now reports turnCount (same extraction rules as parseSessionFile, shared via extractTurns) and listSessions skips zero-turn sessions, matching the codex adapter's no-turn skip. - Sessions whose first 'user' turn is an injected <user_info> environment block (OS Version / Shell / Workspace Path) were titled with that boilerplate. Added to preview.ts BOILERPLATE_BLOCKS so the title falls through to the first real human message.
Author
|
@HowieG @taranjeet , do you think this Grok implementation would be helpful |
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.
This PR adds Grok CLI as a fourth harness, in both directions, plus five Claude Code porting fixes found through hands-on use of openmemory against my real session stores. Every change here came from the same loop: port a session, open the destination harness, notice what's wrong, fix it, verify on the real store.
1. Grok CLI as a bidirectional harness (commits 1 and 2)
Grok stores sessions at
~/.grok/sessions/<percent-encoded-cwd>/<uuid>/withsummary.json(id, cwd, timestamps, generated title) andchat_history.jsonl(one message per line).Import (
src/adapters/grok/import.ts, modeled on the codex adapter): detect viaGROK_HOME ?? ~/.grok, discover newest-first from summary timestamps, parse to text turns. Grok wraps every real user turn in a<user_query>envelope; the parser strips exactly that envelope.reasoning,tool_result, and tool-call-only assistant records count intodroppedTurns; synthetic injections (synthetic_reason) are surfaced viasourceMetadata.syntheticSkipped; malformed lines land insourceMetadata.skippedLines(AC7 pattern).cwdis authoritative fromsummary.json; the fallback percent-decodes the directory name, which is lossless (unlike claude-code's dash munging).Export (
src/adapters/grok/export.ts): synthesizes a native session pair in Grok's own shapes (user text re-wrapped in the<user_query>envelope, assistant content as a plain string withmodel_id, UUIDv7 session ids viaBun.randomUUIDv7). The summary carries every field present in all native summaries; a store test showed Grok's parser drops sessions whose summary is missing any of them.openmemory_sourcein the summary is the provenance stamp forteardown()(no adapter ledger, matching the other exports). Runtime state files (events.jsonl,plan.json, ...) are deliberately not fabricated.current_model_idis pinned to a Grok model, never the source harness's model. The envelope strip on import is the exact inverse of the wrap on export, so grok round-trips through the hub with full text and title parity (covered by a round-trip test).verifygained grok as an isolated destination (setGrokSessionsRootat the temp root, reimport through the grok import adapter's explicit-path parse).Verified live: ported a real 5,000-message Grok session into Claude Code and back, and Claude Code and Codex sessions into Grok; the ported sessions appear in each harness's own resume picker.
2. Round-trip echo suppression (commit 3)
Porting A to B and then B back to A duplicated the original: the ported copy has a fresh session id the ledger has never seen. On my store one session tripled through a claude -> grok -> claude -> grok chain.
The data to prevent this already existed, since every export stamps provenance onto its destination. Importers now surface that stamp as
sourceMetadata.openmemoryOrigin(grok readssummary.json'sopenmemory_source; claude-code reads the per-lineopenmemorySource), andexportSessions()skips any session whose origin harness IS the destination, reporting the original id in the outcome. Exports to a third harness are unaffected;--forcedeliberately overrides.3. Claude cwd encoding fix (commit 4)
encodeCwdmapped only/to-, but Claude Code encodes every character outside[A-Za-z0-9]. Any cwd containing_,., or spaces exported into a sibling project folder Claude never reads, so the session existed on disk and in the ledger but was invisible inclaude --resume. Verified against a real store (same path, both encodings):Fix:
cwd.replace(/[^A-Za-z0-9]/g, "-"), plus regression cases (underscore and dot-plus-space paths).4. Session titles preserved in both directions (commits 5 and 6)
Ported sessions degraded to "whatever the first prompt happened to be" in Claude's
/resumeand in openmemory's own picker.summaryrecord ({type, summary, leafUuid}) carrying the IR title, the same mechanism Claude uses for compacted sessions. Verified on Claude Code 2.1.205: the resume picker shows the ported session under its source title.summaryrecords as the title (last record wins), so sessions ported into Claude, and Claude's own compacted sessions, keep their real titles in the source picker. Exact, no heuristics; round-trip title parity is asserted in tests.5. Claude discovery hygiene (commits 7 and 8)
Three kinds of junk flooded the Claude source picker on a real store (65 real conversations were buried under roughly 130 garbage rows):
.jsonlfiles with noisMeta/isSidechainmarking. 121 on my store, each listed as a bogus "2 messages" chat. Now flagged by the summarizer prompt's fixed preamble (checked only against the first real user turn, so conversations that merely mention the phrase are unaffected) and skipped.agent-*.jsonldirectly in the project dir) and zero-turn/aborted sessions, listed as "0 messages" rows. Discovery now skips zero-turn sessions (matching the codex adapter's no-turn skip, via a sharedextractTurnshelper) and excludesagent-*.jsonlby name. Thefindinvocation also moved fromexecSyncstring interpolation toexecFileSyncwith an argument array.<user_info>block (OS Version / Shell / Workspace Path) were titled with that block. Added to the preview boilerplate strip list so titles fall through to the first real human message.After: discovery on the same store returns 65 sessions, 0 junk.
Tests
grok-import.test.ts(13),grok-export.test.ts(10, including export -> import round-trip parity), echo-suppression cases inorchestrator.test.ts, plus extensions to the detect, registry, preview, and claude-code import/export suites.detectreal-machine E2E, registry real-capabilities, TUI wizard walk) fail identically on unmodifiedmainhere because opencode is not installed; no new failures are introduced.Known gaps / not in scope
session.title(pre-existing, documented in CLAUDE.md).Happy to split any part of this into separate PRs if that's easier to review.