feat(session): cross-platform session migration and team session archive - #593
lurkacai0831 wants to merge 21 commits into
Conversation
- new codebuddy-ide adapter: list/read/write/delete for the IDE sidebar history store (previously write-only via the cli adapter's implicit double-write, so IDE sessions could neither be listed nor migrated) - cli adapter no longer writes/deletes the IDE store; read/write symmetry - detect both on-disk history layouts (default instance stores history one level higher) so its conversations are no longer skipped - reuse 32-hex conversation ids as-is instead of hashing twice, fixing rollback reporting success while leaving sessions in place - encode cli project dirs with CodeBuddy's own rule (spaces kept), so workspaces like 'teamai cli' can be listed and read - resolve symlinks before hashing workspaces (/tmp vs /private/tmp) - shared title cleaning: skip <system-reminder>-style injected first messages instead of leaking prompt text into listings - user-facing errors and test assertions in English
…rectness fixes M1 correctness: - migrate --push re-reads exactly the migrated target session ids instead of 'the N most recent', which could push unrelated sessions - meta no longer lies: fidelityScore uses the real preview score and createdAt records the session's own creation time, not push time - resume failures hint that the session may be archived under another project identity (search --all / --cwd) - empty pushes no longer report success when nothing was committed - 14 Chinese user-facing strings converted to English M2 user-repo capability: - SyncManager: listAllRepoIdentities() reverse-maps canonical identities from each repo's _index.json; listSessionsAcrossRepos() merges entries - list --all / pull --all / search --all give the cross-project view (search --all previously had a dead loop and never searched other repos) - push --all archives every workspace of one platform (--source), confirming before pushing more than 5 sessions (-y skips) - archive key derives from the session's native cwd instead of the directory the command ran in; unknowable workspaces (codebuddy-ide md5 placeholders) archive under _unattributed with a warning - claude-code / codebuddy / cursor readSession recover the native cwd from the first JSONL record instead of lossy directory-name decoding - pushing the same session twice updates the entry instead of creating _1 duplicates (dedup key: origin sessionId + author)
…red fixes Tests (29 new cases): - session-sync.test.ts: identity reverse-mapping, cross-repo listing, dedup by origin sessionId, index rebuild round-trip - session-cmd.test.ts: list/pull/search --all, push --all confirmation, native archive key, English output assertions Docs: - usage-guide (en/zh): new Session Sync & Migration section - README (en/zh): capability row and command cheat-sheet entries - CHANGELOG: M1/M2 entries plus the fixes below Fixes found by the real-CLI E2E run: - encode symlink-resolved cwds into project directory names for claude-code / codebuddy / cursor / workbuddy (writing /tmp/x used to create a directory listing from /private/tmp/x could never see) - keep the local commit and print a warning when the remote push fails instead of crashing after a successful save - apply the shared injected-title cleaning to claude-code / workbuddy / cursor (their first 'user message' is often a system-reminder wrapper, which used to become the archived session name)
Claude Code's /resume picker shows the bare session id (e.g. 824ff784) for sessions without a type:"summary" record, so every migrated session appeared untitled. Carry the IR session title — already cleaned of injected wrappers by the source adapter — into the target JSONL.
…ommands From the three-way QA sweep (adapters / command layer / fidelity): P0 crashes fixed (found by real-CLI execution): - git add/commit/pull in a non-git or missing --repo-root dumped a full stack trace with internal paths; now a one-line error + exit 1 - concurrent pushes hitting git index.lock crashed the same way - pull with no origin remote / nonexistent repo root reported a misleading ENOENT instead of the actual cause Correctness: - session archive dedup key now includes platform: a session pushed as codebuddy and re-archived after migrating to claude-code are two artifacts, not an update of each other - codex keeps per-message timestamps (read response_item.timestamp, stamp records with the message's own time) — roundtrips no longer collapse the timeline - codex session lookup matches whole ids; a 4-char prefix could resolve to someone else's session file - cursor writeSession is idempotent again (malformed UUID regex minted a new id per write, piling up copies) - claude-code readSession honors the type:"summary" record it writes - workbuddy/claude-code/cursor titles skip injected ai-title/name wrappers and tool-output snippets; extractMeta no longer stops at the first injected block (real question after a system-reminder wrapper becomes the title) - codex writeSession survives an invalid session.createdAt instead of crashing with RangeError - a corrupted sessions/**/_index.json warns with the rebuild command instead of silently emptying the dedup key Robustness: - interactive prompts treat EOF like "n" (Cancelled., exit 0) instead of a silent success; --limit rejects non-positive values; a closed output pipe exits cleanly instead of an EPIPE stack - remote push failures report git's actual fatal line, keeping the local commit Docs: - design doc gains a Known limitations section (fidelityScore is a proxy metric; codex splitting; sessionId is platform-native; flattenDag) - src/__tests__/fidelity-sweep.test.ts joins the suite as the fidelity regression tool (roundtrip matrix over 5 platform routes)
|
Reviewed the full branch (type-check + 50/50 tests green, built, smoke-tested, merges cleanly onto 1. Delete 2. 3. Archived sessions are unscrubbed — needs gating before merge. Unlike |
…dempotent Follow-up to the migration work in this PR, driven by real-client verification (Codex Desktop, CodeBuddy IDE, WorkBuddy, Cursor). Every fix below was reproduced against a real client before/after. Visibility: migrated sessions existed on disk but never showed up - codex: sessions are listed from state_5.sqlite, not by scanning rollouts. Write model_provider (buckets the list), keep rollouts legacy so `codex migrate-rollouts` builds the items projection (title/preview/content all come from it), and run it right after writing; if the new file is not indexed yet, start a temporary app-server and call thread/list (the official indexing path). Also match the 0.155 item_completed shape exactly: no client_id on UserMessage (it breaks parsing) and add started/completed_at_ms. - workbuddy: register into workbuddy.db (sessions + workspaces); user_id is discovered from existing rows, connectors/<uuid> or app/sessions.json -- never from device-id, which is a different id and would leave the session invisible behind a user filter. - workbuddy read path used encodeCwdGeneric while writes used the space-preserving rule, so listing by cwd returned 0 sessions. - cursor/workbuddy/codebuddy-ide: keep list registration best-effort but never silent -- warn that the session may stay invisible. Idempotency: repeat migrations no longer duplicate sessions - target ids are derived deterministically from (platform, source id) instead of minting a random uuid, for every adapter. Derived ids are v7-shaped so a re-migration of an already-migrated session reuses the id instead of deriving a new one. - rollback deletes the Codex index rows too (threads, items, turns, projection watermark); previously only the rollout file was removed, leaving an entry that was listed but opened blank. Index deletes run statement by statement: one missing table used to roll back the whole transaction and leave threads behind. Workspace isolation - target cwd defaults to the source session's workspace; --target-cwd is the only way to move a session elsewhere. - claude-code records sometimes store the encoded project dir as cwd; decode it back to a real path (verified against disk). - expanding "list all sessions across directories" kept using the shell's cwd to locate sources, so every migration on that path failed. Pass undefined and let adapters search globally. - --push takes the git author from the session's own repo. Fidelity and images - unknown tools are counted as degraded instead of preserved, so the score stops reporting a misleading 100%; workbuddy joins the THINKING_SUPPORT / NATIVE_TOOLS / IMAGE_SUPPORT matrices (review note: it was registered but absent from both tables). - images are a first-class IR block. codebuddy-ide reads assets (codebuddy-asset://, absolute paths, data URIs) and claude-code reads base64/url blocks; claude-code writes native base64 images, codebuddy-ide copies files back into assets/, platforms without image support degrade to a placeholder and say so in the report. Command layer - refuse to migrate into a target that is not installed instead of writing into a directory nobody reads. - exit 1 when any session in a batch fails, so --all is scriptable. - --all migrates everything (it silently capped at 5) with --limit to cap it and a confirmation listing above 10 sessions (-y skips). Titles: extract from content (summary record, then user text with injected wrappers unwrapped) instead of falling back to "Session <id>" or leaking prompt text; share one implementation across adapters. Tests: src/__tests__/migrate-guard.test.ts covers the not-installed guard, the write path, unknown-tool degradation and image accounting.
|
Findings
Per request, I only inspected the specified diff and did not run or build PR code. |
Migrated sessions are raw transcripts: whatever was pasted into the conversation -- tokens, keys, passwords, internal hosts -- travels with it into the target agent's store, and from there into anything archived later. `session save` already redacts; migration had no equivalent. `session migrate --scrub` runs the whole IR through the existing `utils/redact` (the same rules `session save` uses, plus secrets found in the current environment) before writing: - text and thinking blocks - tool call arguments (serialized, redacted as a whole, then parsed back so the structure stays an object) - tool results - the session title, since it is what shows up in the target's list The report says how many values were replaced, and reminds that redact is best-effort (pattern matching, not a guarantee) -- same caveat as `session save`. Off by default: a local migration should stay lossless unless asked otherwise.
|
Blocking Findings
The base |
Reviewer note 3: `session push` archives full raw transcripts into a team-readable repo, so anything pasted during a session (tokens, keys, passwords, internal hosts) becomes readable by everyone with access. - `session push --scrub` redacts each session through utils/redact before it is written (same rules as `session save` plus secrets found in the current environment), and reports how many values were replaced. - Without --scrub, the command now says so explicitly: "Archived as-is: full transcripts (possibly secrets/paths) are team-readable. Use --scrub to redact." No more silent full-text archiving. `session migrate --scrub` (previous commit) covers the migration path with the same rules, so redacting at migration time also makes later pushes clean.
|
Blocking Findings
Other Findings
|
Static review found real defects in the migration/archive path. All
reproduced or verified against the built CLI:
- codex rollback built DELETE statements by string interpolation of a
CLI-provided session id: `' OR 1=1; --` would wipe every thread from
state_5. Reject non-v7 ids and escape quotes.
- sync gitCommit committed everything staged in the repo (`git commit
-m` without a pathspec) and treated a failed commit as success
(rev-parse returned the previous HEAD). Now commits only sessions/,
and compares HEAD before/after -- no new commit is an error.
- encodeRepoIdentity mapped `_` and `/` to the same character, so
github.com/org/a_b and github.com/org/a/b shared one archive
directory and mixed sessions. Encoding is now reversible (`_` -> `__`).
- git author names are free-form: sanitize before using as a path
segment (':'/'/'/'..'/trailing dots would escape the author dir).
- archived sessions rebuilt their title from the truncated file-name
slug on load. The title is now persisted in origin.title and used
verbatim on read.
- migrate --all now enumerates every workspace when no --cwd is given
(it used to silently mean "everything in the current directory"),
and the usage guide no longer claims "the 5 most recent".
- push/pull/resume honor --dry-run: list what would happen and stop
before writing, committing, or restoring.
- codex readSession extracts the title from content like the listing
path; push archives no longer inherit "Session <timestamp>".
Let the archive commit throw (hooks, gpg signing, missing identity all exit non-zero) instead of swallowing the failure and reading back the previous HEAD as the new commit.
|
Findings
|
AGENTS.md: no Chinese in production code. Translates the comments of the new modules (ids / sqlite / scrub / workbuddy-store) added in this PR; behavior unchanged.
|
Findings
Blocking Process Issues
No commands from the PR were executed; review was diff/read-only. |
|
Thanks for the thorough review. All findings are addressed in Findings[P1] SQL injection in codex rollback — fixed. The id now must match the Codex v7 shape, and quotes are escaped on top. Verified: [P1] [P1] archive commit includes unrelated staged changes — fixed. [P1] failed commit reported as a successful push — fixed. The commit now throws on non-zero exit (hooks/gpg/identity), and [P1] repo identity encoding collision — fixed. Encoding is now reversible: [P2] [P2] un-sanitized git author name as a path — fixed. Author names are sanitized as path segments ( [P2] archived sessions lose their original title — fixed. The title is persisted verbatim in [P1] missing end-to-end matrix — added below. (See matrix.) [P2] Chinese comments in production code — in progress. The new modules ( Also fixed while here: codex End-to-end matrix (executed against the built CLI, isolated HOME sandboxes)
Full transcript of the multi-agent verification run: 4 sandboxes (one per member), each with an isolated |
|
Blocking Findings
|
|
Status update on [P2] Chinese comments: the five new modules ( |
|
Blocking Findings
Rule/Documentation Issues
|
|
This PR currently has merge conflicts with |
Review note 1 asked for this file to be removed before merging: it was an accidental leftover with no imports; the real CLI adapter lives at adapters/codebuddy.ts and the IDE store is handled by adapters/codebuddy-ide.ts.
|
Blocking Findings
Other Findings
Resolved From Earlier Reviews
I only inspected the specified diff and did not run, build, or install PR code. |
Blocking Findings
Other Findings
Resolved From Earlier Reviews
I inspected only the specified diff and did not run, build, install, or execute PR code. |
…der cancelled
Cursor transcripts stop at tool_use -- the export never records tool
outputs. Migrated sessions therefore had tool calls with no paired
result, and CodeBuddy IDE renders every unpaired call as cancelled:
a real 128-message migration showed a wall of cancelled Bash/Grep
bubbles. Pair each unpaired tool_call with an honest placeholder
('[tool output not captured: Cursor transcripts do not record tool
results]').
Blocking Findings
Other Findings
Resolved From Earlier Reviews
I inspected only the specified diff using read-only Git commands and did not run, build, install, or execute PR code. |
… skips [Image] Two follow-ups from the cursor -> codebuddy-ide comparison: - migrated user bubbles kept the raw Cursor wrappers (<user_query>/<timestamp>/<image_files> + attachment paths); IDE messages are the display layer, so run user text through visibleUserText before writing. - a session whose messages were only image attachments got titled "[Image] [Image] [Image]"; the title segment filter now skips [image]/[file]/[attachment] placeholders like it already did [tool_result].
…nwrapped titles The comparison team found the placeholder-result pairing was being short-circuited: some Cursor versions export tool_use without an id (observed 173/173 on one transcript), the reader produced empty callIds, and the truthiness guard skipped every placeholder -- so the target still rendered a wall of cancelled tools. Synthesize a stable per-parse id (tool_<n>) when the source has none, mirroring the writeSession fallback. Titles: prefer titleFromUserText over cleanTitleText for non-injected text as well, so '<user_query>' is unwrapped and [Image] segments skipped before the raw first line wins.
Blocking Findings
Other Findings
Resolved From Earlier Reviews
I inspected only the specified diff using read-only Git commands and did not run, build, install, or execute PR code. |
Closes #587
Adds two layers to
teamai session: cross-platform session migration (move a full conversation between AI tools, previewable and undoable) and a team session archive (archive into the team repo, search, restore).What ships
Migration
session platformssession migrate <id> -s <src> -t <dst>--allfor the recent fewsession rollback <id> --platform <dst>Team archive
session push --source <agent>--allcovers every workspace of that agentsession pullsession list/list --all--alladds a SOURCE column)session search <kw> --allsession resume <name> --platform <agent>Platforms:
claude-code,codebuddy(CLI),codebuddy-ide(IDE sidebar),codex,cursor,workbuddy, plus theclaude-internal/tclaude/codex-internal/tcodexvariants.Design notes
sessions/repos/<repo>/<author>/is keyed on the git identity of the session's own working directory (recovered from the JSONL record where needed); unknowable workspaces (codebuddy-ide md5 placeholders) land in_unattributedwith an English warning. Runningpushfrom another directory still archives under the right project.rollback, nobody dares migrate anything real.--all— a project team repo shows only its own sessions; a personal repo gets the cross-project view.session save(scrubbed summary → digest) rather than replacing it: summary for trends, full session for resuming.Known limitations are documented in
docs/designs/session-user-repo-sync.md— notably thatfidelityScoreis a proxy for IR-block degradation, not a byte-equality guarantee.Test plan (all executed against the built CLI)
npx tsc --noEmit→ 0 errors ✅npx vitest run src/__tests__/session-{sync,cmd}.test.ts src/__tests__/{codebuddy-ide-adapter,session-title,fidelity-sweep}.test.ts→ 50/50 passed ✅npm run build→ success (ESM 1.72 MB) ✅session platforms→ all 6 platforms listed,codebuddy+codebuddy-ideboth✓ installed✅Fidelity: 100.0%, target id printed;claude --resume <id>picks it up with the session title ✅session rollback→ only the target copy removed, source session intact ✅sessions/repos/github.com_org_beta/andsessions/repos/gitlab.com_team_alpha/✅_unattributedwith an English warning ✅session list --all→ both projects listed with their SOURCE identity;session listfrom a project shows only that project's ✅session search <kw> --all→ hits sessions across two repo identities ✅xxx_1duplicate ✅session resumefrom the wrong project → English error namingsearch --all/--cwd, exit 1 ✅session pushinto a non-git repo root / no remote / concurrentindex.lock→ one-line English error + exit 1, no stack trace ✅npx vitest run→ failed files identical to the pre-change baseline (hook-handlers,dashboard-collector,recall-scope-isolation,contribute-self-learnings,push-team-config); none of them importsession-flow✅fidelity-sweep.test.tsroundtrips 5 platform routes and asserts message count/role/text/thinking/tool pairing/timestamps ✅Evidence: a real session relay — CodeBuddy IDE → claude-code (2661 messages / 47.5 MB)
The screenshots below are one continuous relay, all taken from the actual run. They live on the
session-migration-evidencebranch of this fork (kept out of the PR diff).Step 1 — the session lives in CodeBuddy IDE
Everyday work happens in the IDE. The migration does not require leaving it: the in-IDE agent provides and runs the exact commands, from enabling the new CLI to
migrate/rollback/--allvariants.Step 2 — run the migration
Fidelity: 100.0% (Mode A)andPreserved: 5816/5816 blocks.tool_not_in_targetwarnings are expected: this session used CodeBuddy-specific tools (team_create,send_message,ask_followup_question, …) with no same-named counterpart in claude-code. Their inputs and results are preserved as text blocks — readable after resume, but not replayable as tool calls.~/.claude/projects/-Users-...-teamai-cli/<uuid>.jsonl).Step 3 — resume in Claude Code: the relay completes
Claude Code's
/resumepicker, searched for "teamai cli": the migrated session appears under its original title ("完整的分析一下 seeeionflow ts版本的能力和 tea · 1 minute ago · 21.2 MB"). Selecting it continues the conversation with the full history visible — this is the relay completing. Before thetype:"summary"record fix, this picker showed a bare session id (824ff784).Same session, before and after
/resume)conversations[].nametype:"summary"recordNotes on the numbers:
extra/requestsmetadata; Claude records are flatter).tool_not_in_targetwarnings degrade CodeBuddy-only tools to text blocks: content preserved, tool structure not replayable.Notes for reviewers
origin/main(8ea0612) — rebased, CHANGELOG conflicts resolved by keeping both sides.--all, (3) the learnings loop.session save), so anything archived is team-readable. If that needs redaction or an opt-in gate, say so and I will add it before this lands.