Skip to content

feat: add Grok CLI as a bidirectional harness, plus Claude Code porting fixes#4

Open
Solomon-mithra wants to merge 8 commits into
mem0ai:mainfrom
Solomon-mithra:feat/grok-harness-and-claude-fixes
Open

feat: add Grok CLI as a bidirectional harness, plus Claude Code porting fixes#4
Solomon-mithra wants to merge 8 commits into
mem0ai:mainfrom
Solomon-mithra:feat/grok-harness-and-claude-fixes

Conversation

@Solomon-mithra

@Solomon-mithra Solomon-mithra commented Jul 11, 2026

Copy link
Copy Markdown

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>/ with summary.json (id, cwd, timestamps, generated title) and chat_history.jsonl (one message per line).

Import (src/adapters/grok/import.ts, modeled on the codex adapter): detect via GROK_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 into droppedTurns; synthetic injections (synthetic_reason) are surfaced via sourceMetadata.syntheticSkipped; malformed lines land in sourceMetadata.skippedLines (AC7 pattern). cwd is authoritative from summary.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 with model_id, UUIDv7 session ids via Bun.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_source in the summary is the provenance stamp for teardown() (no adapter ledger, matching the other exports). Runtime state files (events.jsonl, plan.json, ...) are deliberately not fabricated. current_model_id is 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).

verify gained grok as an isolated destination (setGrokSessionsRoot at 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 reads summary.json's openmemory_source; claude-code reads the per-line openmemorySource), and exportSessions() skips any session whose origin harness IS the destination, reporting the original id in the outcome. Exports to a third harness are unaffected; --force deliberately overrides.

3. Claude cwd encoding fix (commit 4)

encodeCwd mapped 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 in claude --resume. Verified against a real store (same path, both encodings):

cwd:              /Users/me/projects/my_app/web
Claude reads:     -Users-me-projects-my-app-web
openmemory wrote: -Users-me-projects-my_app-web

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 /resume and in openmemory's own picker.

  • Export now appends a summary record ({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.
  • Import now honors summary records 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):

  • Summarizer side-sessions: Claude Code runs background sessions to generate the conversation summaries for its own UI, saved as ordinary 3-line .jsonl files with no isMeta/isSidechain marking. 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.
  • Subagent transcripts in the newer flat layout (agent-*.jsonl directly 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 shared extractTurns helper) and excludes agent-*.jsonl by name. The find invocation also moved from execSync string interpolation to execFileSync with an argument array.
  • Env-context boilerplate titles: sessions whose first "user" turn is an injected <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

  • 386 pass, typecheck clean. New suites: grok-import.test.ts (13), grok-export.test.ts (10, including export -> import round-trip parity), echo-suppression cases in orchestrator.test.ts, plus extensions to the detect, registry, preview, and claude-code import/export suites.
  • Three failures on my machine (detect real-machine E2E, registry real-capabilities, TUI wizard walk) fail identically on unmodified main here because opencode is not installed; no new failures are introduced.

Known gaps / not in scope

  • Codex export still does not serialize session.title (pre-existing, documented in CLAUDE.md).
  • Native, never-compacted Claude sessions still title by first user message; Claude Code does not persist its generated titles anywhere a local tool can read (its summarizer side-sessions do not reference the conversation they summarize).

Happy to split any part of this into separate PRs if that's easier to review.

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.
@Solomon-mithra

Solomon-mithra commented Jul 14, 2026

Copy link
Copy Markdown
Author

@HowieG @taranjeet , do you think this Grok implementation would be helpful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant