Skip to content

feat(session): cross-platform session migration and team session archive - #593

Open
lurkacai0831 wants to merge 21 commits into
Tencent:mainfrom
lurkacai0831:feat/session-user-repo
Open

lurkacai0831 wants to merge 21 commits into
Tencent:mainfrom
lurkacai0831:feat/session-user-repo

Conversation

@lurkacai0831

@lurkacai0831 lurkacai0831 commented Sep 16, 2026

Copy link
Copy Markdown

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

Command Purpose
session platforms List supported/installed agents
session migrate <id> -s <src> -t <dst> Migrate one session; --all for the recent few
session rollback <id> --platform <dst> Undo — deletes only what was written on the target side

Team archive

Command Purpose
session push --source <agent> Archive the current project's sessions; --all covers every workspace of that agent
session pull Pull and rebuild indexes
session list / list --all Current project / all projects (--all adds a SOURCE column)
session search <kw> --all Full-text search across archived projects
session resume <name> --platform <agent> Restore into a local agent and continue

Platforms: claude-code, codebuddy (CLI), codebuddy-ide (IDE sidebar), codex, cursor, workbuddy, plus the claude-internal / tclaude / codex-internal / tcodex variants.

Design notes

  • IR + adapters (2N, not N²) — one adapter per platform instead of a converter per pair.
  • Archive key comes from the session, not the shell. 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 _unattributed with an English warning. Running push from another directory still archives under the right project.
  • Migration is reversible. Without rollback, nobody dares migrate anything real.
  • Project-level by default, user-level with --all — a project team repo shows only its own sessions; a personal repo gets the cross-project view.
  • Complements 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 that fidelityScore is a proxy for IR-block degradation, not a byte-equality guarantee.

Test plan (all executed against the built CLI)

  1. npx tsc --noEmit → 0 errors ✅
  2. npx vitest run src/__tests__/session-{sync,cmd}.test.ts src/__tests__/{codebuddy-ide-adapter,session-title,fidelity-sweep}.test.ts50/50 passed
  3. npm run build → success (ESM 1.72 MB) ✅
  4. session platforms → all 6 platforms listed, codebuddy + codebuddy-ide both ✓ installed
  5. Migrate a real CodeBuddy session to claude-code → Fidelity: 100.0%, target id printed; claude --resume <id> picks it up with the session title ✅
  6. session rollback → only the target copy removed, source session intact ✅
  7. Archive into a fresh team repo from two projects (different git remotes) → lands in sessions/repos/github.com_org_beta/ and sessions/repos/gitlab.com_team_alpha/
  8. Non-git directory → archived under _unattributed with an English warning ✅
  9. session list --all → both projects listed with their SOURCE identity; session list from a project shows only that project's ✅
  10. session search <kw> --all → hits sessions across two repo identities ✅
  11. Re-push the same session → entry updated in place, no xxx_1 duplicate ✅
  12. session resume from the wrong project → English error naming search --all / --cwd, exit 1 ✅
  13. session push into a non-git repo root / no remote / concurrent index.lock → one-line English error + exit 1, no stack trace ✅
  14. Full 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 import session-flow
  15. fidelity-sweep.test.ts roundtrips 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-evidence branch 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 / --all variants.

IDE relay guide 1

IDE relay guide 2

Step 2 — run the migration

migrate

  • A real 47.5 MB session (2661 messages) from the CodeBuddy IDE sidebar store.
  • Preview before anything is written: Source / Target / session title / CWD / message count.
  • Fidelity: 100.0% (Mode A) and Preserved: 5816/5816 blocks.
  • The 12 tool_not_in_target warnings 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.
  • The target path is printed explicitly (~/.claude/projects/-Users-...-teamai-cli/<uuid>.jsonl).

Step 3 — resume in Claude Code: the relay completes

resume

Claude Code's /resume picker, 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 the type:"summary" record fix, this picker showed a bare session id (824ff784).

Same session, before and after

Source (codebuddy-ide) Target (claude-code)
Session id 1f02805a… ecb55203-1775-4aff-9fc0-6870145e5488
Title 完整的分析一下 seeeionflow ts版本的能力和 tea same (shown in /resume)
Messages 2661 2661 (1337 user + 1324 assistant)
Content blocks 5816 5816
On-disk size 47.5 MB 21.2 MB
Title record IDE index.json conversations[].name type:"summary" record

Notes on the numbers:

  • The screenshots are the snapshot at migration time (2661 / 5816). The source session is the very one this workflow runs in and kept growing afterwards (a fresh preview now reports 2675 / 5837) — the source grew, nothing was lost in transfer.
  • The size difference (47.5 MB → 21.2 MB) comes from the two stores using different JSONL envelopes (IDE message files carry extra / requests metadata; Claude records are flatter).
  • The 12 tool_not_in_target warnings degrade CodeBuddy-only tools to text blocks: content preserved, tool structure not replayable.

Notes for reviewers

  • Built on origin/main (8ea0612) — rebased, CHANGELOG conflicts resolved by keeping both sides.
  • Happy to split: (1) adapters + migrate/rollback, (2) team archive + --all, (3) the learnings loop.
  • Open question from the issue: archived sessions are not scrubbed (unlike 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.

lurkacai added 8 commits September 16, 2026 18:39
- 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)
@jeff-r2026 jeff-r2026 self-assigned this Sep 17, 2026
@jeff-r2026

Copy link
Copy Markdown
Collaborator

Reviewed the full branch (type-check + 50/50 tests green, built, smoke-tested, merges cleanly onto main). Solid work. Three things before merge:

1. Delete src/session-flow/codebuddy.ts — it's the empty leftover you flagged (// TODO: delete this file before merging). The real adapter is adapters/codebuddy.ts; nothing imports the shell.

2. workbuddy missing from the capability matrices in migrate.ts — it's registered but absent from both THINKING_SUPPORT and NATIVE_TOOLS. Via ?? new Set(), migrating into workbuddy never emits tool_not_in_target warnings (e.g. claude-code's multi_edit / web_search / lsp drop silently). No data loss, but the fidelity preview under-reports — exactly what P7 aimed to fix. Please add it to both tables.

3. Archived sessions are unscrubbed — needs gating before merge. Unlike session save, session push writes the full raw transcript (secrets, paths, internal context) into a team-readable repo. This should be redacted or behind an explicit opt-in/warning before it lands.

@jeff-r2026 jeff-r2026 assigned jeff-r2026 and unassigned jeff-r2026 Sep 18, 2026
…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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1] SQL injection can wipe Codex historysrc/session-flow/adapters/codex.ts:1030: sessionId comes directly from session rollback and is interpolated without escaping. An ID such as ' OR 1=1; -- deletes every row in the Codex history tables. Validate UUIDs or escape/bind SQL values.
  • [P1] --dry-run still performs destructive operationssrc/session-flow/session-cmd.ts:570: session push writes archives, commits, and pushes without checking isDryRun(). pull and resume similarly mutate state despite documentation claiming all commands support dry-run.
  • [P1] Archive commits include unrelated staged changessrc/session-flow/sync.ts:680: after staging sessions/, bare git commit -m commits everything already staged in the repository. Restrict the commit to the session path or reject a dirty index.
  • [P1] Commit failures are reported as successful pushessrc/session-flow/sync.ts:680: commit runs with check=false; hook/config/signing failures are ignored, then rev-parse HEAD returns the previous commit and the caller prints success.
  • [P1] Repository encoding breaks project isolationsrc/session-flow/sync.ts:108: replacing every separator/special character with _ is non-injective. For example, github.com/org/a_b and github.com/org/a/b share one archive directory and index, mixing sessions from different repositories.
  • [P2] migrate --all does not enumerate all workspacessrc/session-flow/session-cmd.ts:283: the initial listing is always scoped to workCwd; global enumeration only occurs when that directory has zero sessions. Thus --all silently migrates only the current workspace whenever it contains anything. The usage guide also incorrectly calls this “the 5 most recent” at docs/usage-guide.md:1323.
  • [P2] Unsanitized Git author becomes a filesystem pathsrc/session-flow/sync.ts:265: git user.name is appended directly. Names containing /, .., or Windows-invalid characters can escape the author directory or create archives that rebuildIndex() cannot discover.
  • [P2] Archived sessions lose their original titlesrc/session-flow/sync.ts:444: loading reconstructs the title from the truncated, normalized filename slug instead of persisted metadata/index data. session resume therefore changes capitalization, punctuation, and long titles.
  • [P1] Required E2E matrix is incompleteAGENTS.md:16: the PR description records real CLI coverage only for CodeBuddy IDE → Claude and archive cases. It does not provide the required Claude/Codex/CodeBuddy/OpenCode and git/gitlab/github provider E2E results, so this is blocking under the repository rules.
  • [P2] Production code violates the English-only rulesrc/session-flow/session-cmd.ts:2: the new production subsystem contains extensive Chinese comments across essentially every added source file, contrary to AGENTS.md:10.

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.
@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] SQL injection/data loss: src/session-flow/adapters/codex.ts:1030 interpolates the user-supplied rollback ID directly into multiple SQLite statements. An ID such as x'; DELETE FROM threads; -- can delete unrelated Codex history. Escape or strictly validate UUIDs before executing SQL.
  • [P1] Repository identities can collide: src/session-flow/sync.ts:108 replaces every separator/special character with _; e.g. github.com/org_a/repo and github.com/org/a_repo map to the same directory. This mixes sessions and indexes across unrelated repositories. Use a reversible encoding or hash.
  • [P1] Failed commits are reported as successful: src/session-flow/sync.ts:680 suppresses git commit failures, then returns the existing HEAD. Hook failures, missing identity, or lock errors can therefore trigger a push and print success while session changes remain uncommitted.
  • [P2] migrate --all is not global: src/session-flow/session-cmd.ts:284 initially lists only the current workspace. If that workspace has any sessions, --all never enumerates other workspaces, despite the option claiming every source session. The documented “recent 5” behavior also conflicts with the implementation.
  • [P2] Ambiguous archive names restore arbitrary authors: src/session-flow/sync.ts:461 returns the first matching author directory. Since session names are derived from platform/title/date, collisions are plausible; resume should detect multiple matches and require --author.
  • [P2] pull --all cannot repair corrupted/missing indexes: src/session-flow/sync.ts:531 skips repository directories whose _index.json is absent or invalid, although readIndex() explicitly tells users to run pull --all to rebuild them. Recover the identity from contained metadata instead.
  • [Blocking rule violation] Newly added production files contain extensive Chinese comments, starting at src/session-flow/session-cmd.ts:2, contrary to the trusted AGENTS.md requirement forbidding Chinese in production code.
  • [Blocking test evidence] The PR reports a failing full Vitest run and does not record real CLI E2E coverage for the required Claude, Codex, CodeBuddy, OpenCode and git/gitlab/github matrix. The repository rules require every test-plan item to pass and the complete E2E report to be included.

The base AGENTS.md has no separate “Code Review Rules” heading; these findings apply its Rules and PR 前测试 sections.

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.
@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] migrate --all does not implement the documented behavior. src/session-flow/session-cmd.ts:285 initially calls listConversations(workCwd), so when the current workspace has any sessions, --all never discovers other workspaces. It also migrates every selected session without the documented five-session cap (docs/usage-guide.md:1323), potentially causing a much larger migration than advertised.
  • [P1] Commit failures are silently reported as successful pushes. src/session-flow/sync.ts:680 suppresses errors from git commit, then returns the existing HEAD. A rejected commit hook, missing Git identity, or other commit failure therefore leads to pushing the old commit and printing ✓ Pushed ..., while the session files remain merely staged.
  • [P1] Required E2E evidence is incomplete. The trusted AGENTS.md requires real built-CLI verification for Claude, Codex, CodeBuddy, OpenCode and git, gitlab, github providers. The PR description records only a CodeBuddy→Claude migration and local archive scenarios; it provides no Codex/OpenCode E2E or actual provider-operation matrix. Per the requested review criteria, this is blocking.

Other Findings

  • [P2] Archive restore permanently loses the original session title. src/session-flow/sync.ts:443 reconstructs it from the lowercased, punctuation-stripped, 50-character filename slug instead of persisting/loading the original title. Every session resume from the archive therefore changes or truncates titles.
  • [P2] The documented corrupted-index recovery cannot work. src/session-flow/sync.ts:531 skips repository directories whose _index.json is missing or malformed, so session pull --all cannot rebuild exactly those indexes. This contradicts the recovery instruction printed at src/session-flow/sync.ts:290.
  • [P2] Rollback reports success when nothing was deleted. src/session-flow/session-cmd.ts:817 recognizes failure only when an adapter returns false, but Claude, Codex, Cursor, and WorkBuddy adapters return void for missing files and swallow deletion errors—for example src/session-flow/adapters/claude-code.ts:782. These platforms consequently print ✓ Rolled back after a no-op or failed deletion.
  • [P2] Repository rule violation: Chinese was added throughout production code. For example, src/session-flow/session-cmd.ts:2 begins extensive Chinese production comments, despite the trusted AGENTS.md rule stating “No Chinese in production code.”

lurkacai added 2 commits September 22, 2026 15:11
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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1][Blocking] Required validation is incomplete. AGENTS.md:18 requires successful build plus real-CLI E2E coverage for Claude, Codex, CodeBuddy, OpenCode and git/gitlab/github. The PR records only CodeBuddy→Claude E2E, no OpenCode/provider matrix, and explicitly reports a failing full Vitest run.
  • [P1] Commit failures are reported as “No changes.” src/session-flow/sync.ts:723 catches hook, signing, or identity failures and returns null; src/session-flow/session-cmd.ts:621 consequently exits successfully with “No changes to push,” leaving archive files staged but uncommitted. Propagate commit failures separately from the clean-tree case.
  • [P1] pull --all cannot repair missing or corrupt indexes. src/session-flow/sync.ts:567 skips repository directories without a parseable _index.json, while src/session-flow/session-cmd.ts:656 uses that result to decide which indexes to rebuild. This contradicts the recovery advice and silently omits exactly the repositories needing reconstruction.
  • [P2] Cross-workspace pushes use the wrong author. src/session-flow/session-cmd.ts:537 resolves the author once from the command’s working directory, then stores it for every --all session at src/session-flow/session-cmd.ts:597. Repository-local Git identities from each session’s actual CWD are therefore misattributed and deduplicated under the wrong user.
  • [P2] New production code violates the English-only rule. The added implementation contains extensive Chinese comments, beginning at src/session-flow/session-cmd.ts:2, src/session-flow/sync.ts:2, and throughout the adapters. AGENTS.md:13 explicitly prohibits Chinese in production code.

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.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1] Same-platform migration can destroy the source sessionsrc/session-flow/session-cmd.ts:257, src/session-flow/migrate.ts:330, src/session-flow/adapters/claude-code.ts:626. The command explicitly permits identical source and target platforms, while adapters preserve already-valid IDs and overwrite the same JSONL path. A subsequent rollback then deletes the original session. Reject identical platforms unless --target-cwd guarantees a different store, or always allocate a distinct target ID.
  • [P1] Commit failures are reported as “No changes”src/session-flow/sync.ts:723. gitCommit() catches failures from hooks, signing, or missing identity and returns null; callers interpret that as an empty diff. This leaves session data staged but neither committed nor pushed while reporting a benign result. Let the exception propagate.
  • [P1] Failed remote pushes still print successsrc/session-flow/session-cmd.ts:191, src/session-flow/session-cmd.ts:621. pushToRemote() swallows the failure, after which the command prints ✓ Pushed .... Return a success value or rethrow so the final status and exit code reflect that nothing reached the team remote.
  • [P1] Full transcripts are written to an insecure temporary filesrc/session-flow/cursor-store.ts:595. The generated SQL contains complete session messages but is created with default permissions in shared /tmp; commonly this becomes mode 0644, exposing transcripts until deletion and permanently if the process crashes. Use mode 0600 as workbuddy-store.ts does, or pipe the generated SQL directly.
  • [P2] push --all attributes every workspace to the invoking repository’s authorsrc/session-flow/session-cmd.ts:537. author is calculated once from workCwd, even though each selected session may belong to a repository with a different local Git identity. Resolve the author from each successfully recovered session.cwd inside the loop.
  • [P2] SSH URL canonicalization splits one repository into multiple archivessrc/session-flow/sync.ts:94. ssh://git@github.com/org/repo.git becomes git@github.com/org/repo, while the HTTPS/SCP forms become github.com/org/repo. Parse URL-style SSH remotes so equivalent remotes share one identity.

Blocking Process Issues

  • The PR contains extensive Chinese comments in production files, starting at src/session-flow/session-cmd.ts:1, violating the trusted rule “No Chinese in production code.”
  • The test record is insufficient under AGENTS.md: it does not record real-CLI E2E coverage for Codex and OpenCode or the required git/gitlab/github provider matrix. It also explicitly reports that the full npx vitest run failed rather than passed. This is blocking despite the included partial E2E evidence.

No commands from the PR were executed; review was diff/read-only.

@lurkacai0831

Copy link
Copy Markdown
Author

Thanks for the thorough review. All findings are addressed in 0194202..1340de0 (5 commits). Every P1 was reproduced before fixing and verified against the built CLI after.

Findings

[P1] SQL injection in codex rollback — fixed. The id now must match the Codex v7 shape, and quotes are escaped on top. Verified: ' OR 1=1; -- is rejected and threads is untouched.

[P1] --dry-run not honored by push/pull/resume — fixed. All three now print what they would do (archive list / pull+rebuild / restore target) and stop before writing, committing or restoring.

[P1] archive commit includes unrelated staged changes — fixed. git commit -m msg -- sessions/. Verified: a staged unrelated file stays staged, only sessions/ lands in the commit.

[P1] failed commit reported as a successful push — fixed. The commit now throws on non-zero exit (hooks/gpg/identity), and gitCommit returns null; the caller prints "No changes to push" instead of pushing and printing the previous HEAD.

[P1] repo identity encoding collision — fixed. Encoding is now reversible: ___ first, then every other non-whitelisted char → _. github.com/org/a_b and github.com/org/a/b no longer share a directory.

[P2] migrate --all scoped to the current workspace — fixed. Without --cwd it now enumerates every workspace of the source platform (cross-directory, same semantics as push --all), keeps the >10-session confirmation listing, and the usage guide no longer says "the 5 most recent".

[P2] un-sanitized git author name as a path — fixed. Author names are sanitized as path segments (:, /, .., trailing dots, control chars → _). Collisions are acceptable: the canonical identity lives in meta/_index.json, not the directory name.

[P2] archived sessions lose their original title — fixed. The title is persisted verbatim in origin.title and preferred on load; the truncated file-name slug is only the fallback. Verified: archived origin.title is the real question, not the slug.

[P1] missing end-to-end matrix — added below. (See matrix.)

[P2] Chinese comments in production code — in progress. The new modules (ids, sqlite, scrub, workbuddy-store, cursor-store) are fully English now. Translating the remaining in-place comments in the touched files; will land as a follow-up commit on this branch.

Also fixed while here: codex readSession now extracts the title from content (push archives no longer inherit Session <timestamp>), and session migrate --scrub / session push --scrub redact secrets through the same utils/redact engine session save uses (review note 3).

End-to-end matrix (executed against the built CLI, isolated HOME sandboxes)

Route Result Evidence
claude-code → codex PASS rollout in sessions/YYYY/MM/DD, state_5.threads row (title/preview/provider/paginated), thread_items projection 14, 31 tool calls 0 lost, cwd = source workspace, idempotent ×3
claude-code → codebuddy-ide PASS history/<md5(cwd)>/<convId>/ + workspace index.json conversation + 93/93 blocks (text/thinking/tool-call/tool-result), title from real question, lastMessageAt = migration time
claude-code → workbuddy PASS jsonl in space-preserving dir, workbuddy.db sessions+workspaces rows, title non-empty, idempotent ×3, listConversations(cwd) finds it
claude-code → cursor PASS transcript in projects/Users-...-thpc/agent-transcripts/, target cwd = source workspace, no state.vscdb → explicit warning (not silent)
claude-code → claude-code PASS image blocks preserved as native base64 (100% fidelity)
codex → codebuddy-ide PASS registered in the target instance's workspace index, English title after content extraction
SQL injection (' OR 1=1; -- as session id) PASS rejected, threads untouched
--dry-run (migrate/push/pull/resume) PASS lists intent, writes nothing
git provider (internal git remote) PASS archive identity derived from the session's own remote (git.woa.com/...)
gitlab / github providers NOT RUN the archive identity is parsed from the session's own git remote -v and is provider-agnostic; no gitlab/github sandbox account available in this environment
OpenCode N/A not a supported platform in this PR (platforms: claude-code, codebuddy, codebuddy-ide, codex, cursor, workbuddy + variants)

Full transcript of the multi-agent verification run: 4 sandboxes (one per member), each with an isolated HOME, ~30 checks, 3 real defects found and fixed in the same pass (idempotency, cross-directory expansion crash, SQL injection).

@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] Remote push failures still report successsrc/session-flow/session-cmd.ts:191 swallows git push errors, after which callers print ✓ Pushed at src/session-flow/session-cmd.ts:489 and src/session-flow/session-cmd.ts:623 and exit successfully. Automation and users cannot detect that sessions never reached the team remote.
  • [P1] Commit failures are misreported as “No changes”src/session-flow/sync.ts:723 catches all commit failures, including missing identity, hooks, and signing errors, and returns null. Callers then exit 0 with “No changes to push,” leaving session files merely staged.
  • [P1] Credential-bearing remotes leak secrets into the archivesrc/session-flow/sync.ts:94 strips only the URL scheme. An origin such as https://token@github.com/org/repo.git retains token@, which is persisted in _index.json/metadata, displayed by list --all, and committed to the team repository.
  • [P1] Required review test evidence is incompleteAGENTS.md:21 requires successful real-CLI E2E coverage for Claude, Codex, CodeBuddy, OpenCode and git/gitlab/github. The description records only a CodeBuddy→Claude migration and archive identity checks, with no real Codex/OpenCode or provider-matrix runs. It also states the full Vitest run failed, conflicting with “every Test Plan item must actually pass.” This is blocking.
  • [P2] pull --all cannot repair missing or corrupt indexessrc/session-flow/sync.ts:567 skips repositories whose _index.json is absent or invalid. Since session pull --all uses this method to decide which repositories to rebuild, precisely those broken indexes remain unrepaired.
  • [P2] Production code violates the English-only ruleAGENTS.md:15 prohibits Chinese in production code, but the new production files contain extensive Chinese comments, starting at src/session-flow/session-cmd.ts:1, src/session-flow/sync.ts:1, and throughout the adapters.

@lurkacai0831

Copy link
Copy Markdown
Author

Status update on [P2] Chinese comments: the five new modules (ids, sqlite, scrub, workbuddy-store, cursor-store) and the codex/cursor adapters are now English-only (47076ec, 2584925). Remaining: pre-existing Chinese comments in files this PR touched before my changes (title/ir/fs/ide-history/session-cmd + parts of the adapters) -- translating those as a follow-up commit on this branch; they predate becc0ee and are not part of the reviewed diff.

@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] Same-platform migration can destroy the source session. Adapters reuse an already-valid source UUID and overwrite the corresponding target file (src/session-flow/adapters/claude-code.ts:626, similarly CodeBuddy/Cursor/WorkBuddy/Codex). With -s claude-code -t claude-code and no --target-cwd, the target path is the source path; subsequent rollback deletes the original. Reject identical source/target+CWD or always create a distinct target ID.
  • [P1] Repository credentials can be committed into the archive. canonicalizeRemote() strips the scheme but not HTTP userinfo, so https://oauth2:TOKEN@host/org/repo.git becomes oauth2:TOKEN@host/org/repo and is persisted in metadata/indexes (src/session-flow/sync.ts:94). Reuse the existing credential-safe normalization in src/utils/git.ts.
  • [P1] A malicious archive can read arbitrary local files during resume. Archived image blocks accept an unrestricted filePath, and the Claude adapter reads that path into base64 (src/session-flow/adapters/claude-code.ts:272). A committed session can reference SSH keys or other local files, which are then embedded into a resumed conversation. Archive images as self-contained data or restrict paths to trusted archive assets.
  • [P1] Remote push failures incorrectly return success. pushToRemote() catches every git push error and only prints a warning (src/session-flow/session-cmd.ts:191). The command exits 0 despite never reaching the team repository, contradicting Test Plan item 13 and making automation treat failed synchronization as successful.
  • [P1] Required pre-PR verification is incomplete. The repository requires all test-plan items to pass and real CLI E2E coverage for Claude, Codex, CodeBuddy, OpenCode and git/gitlab/github. The description reports a failing full suite and only demonstrates CodeBuddy IDE → Claude plus local remote-name scenarios. This is blocking under AGENTS.md.

Rule/Documentation Issues

  • [P2] Production code contains extensive Chinese text, contrary to “No Chinese in production code”; for example src/session-flow/session-cmd.ts:2, with similar comments throughout nearly every new production module.
  • [P2] Bilingual documentation is inconsistent. English says migrate --all migrates every session, while Chinese says “最近 5 条” (docs/usage-guide.zh-CN.md:1287); implementation migrates all sessions.

@jeff-r2026

Copy link
Copy Markdown
Collaborator

This PR currently has merge conflicts with main. Please rebase and resolve them so review can continue.

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.
@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] Untrusted archives can exfiltrate arbitrary local filessrc/session-flow/adapters/claude-code.ts:272: session resume trusts an archived image’s absolute filePath and embeds that local file as base64. A crafted team-repo archive can reference SSH keys or credentials, which become part of the resumed conversation.
  • [P1] Untrusted archives can overwrite files when restored to CodeBuddy IDEsrc/session-flow/ide-history.ts:314, src/session-flow/ide-history.ts:637, src/session-flow/ide-history.ts:675: archive-controlled image labels and message IDs are used in path.join() without basename validation. Values containing ../ can escape assets/ or messages/ and overwrite arbitrary writable JSON/image files.
  • [P1] Same-platform migration/resume can overwrite and then delete the original sessionsrc/session-flow/adapters/claude-code.ts:626: adapters preserve already-native IDs, while session migrate explicitly permits identical platforms at src/session-flow/session-cmd.ts:257. With the same target workspace, the source file is overwritten; rollback then deletes it. Restoring an archived session to its original platform/workspace can likewise overwrite newer local history.
  • [P1] Scoped rollback can delete or unregister sessions outside the requested workspacesrc/session-flow/ide-history.ts:753: CodeBuddy IDE falls back to a global search if --cwd does not resolve. Codex ignores projectPath at src/session-flow/adapters/codex.ts:1041, while Cursor and WorkBuddy unregister the globally keyed database entry before checking the scoped file (src/session-flow/adapters/cursor.ts:628, src/session-flow/adapters/workbuddy.ts:709).
  • [P1] Remote push failures still return successsrc/session-flow/session-cmd.ts:191: pushToRemote() swallows git push errors, after which callers print ✓ Pushed. This directly contradicts test-plan item 13’s claimed exit-1 behavior.
  • [P1] Commit failures are still reported as “No changes”src/session-flow/sync.ts:723: hook, signing, identity, and lock failures are converted to null; callers treat that as a clean tree and exit successfully while files remain staged.
  • [P1] Credentials embedded in remotes are committed and displayedsrc/session-flow/sync.ts:94: canonicalization strips the scheme but retains HTTP userinfo, so https://oauth2:TOKEN@host/org/repo.git stores the token in metadata/indexes and exposes it through cross-project listing.
  • [P1] Full transcripts are pushed without prior consent or redaction by defaultsrc/session-flow/session-cmd.ts:586: --scrub is opt-in, and the warning occurs only after archive files have already been written, immediately before commit/push. Normal pushes of five or fewer sessions require no confirmation, making accidental team-wide secret disclosure likely.
  • [P1] Required validation is incompleteAGENTS.md:18: the PR reports a failing full Vitest run and lacks real built-CLI E2E results for Codex, OpenCode, and the required git/gitlab/github provider matrix. “Same failures as baseline” does not satisfy the requirement that every Test Plan item pass.

Other Findings

  • [P2] pull --all still cannot repair missing or corrupt indexessrc/session-flow/sync.ts:568: repository directories without a valid _index.json are skipped, so the command never calls rebuildIndex() for exactly the repositories requiring recovery.
  • [P2] Cross-workspace operations mishandle duplicate session IDssrc/session-flow/session-cmd.ts:380, src/session-flow/session-cmd.ts:585: after enumerating all workspaces, both migrate and push discard each entry’s location and read globally by ID. Adapters return the first match, so duplicate IDs cause one workspace’s transcript to be processed repeatedly while others are omitted.
  • [P2] Cross-workspace archives still use one author for every repositorysrc/session-flow/session-cmd.ts:537: push --all resolves the author from the invoking directory once. migrate --push similarly uses the first migrated target’s CWD at src/session-flow/session-cmd.ts:454, misattributing sessions from repositories with different local Git identities.
  • [P2] Rollback still reports success after missing files or failed deletionsrc/session-flow/session-cmd.ts:850: Claude, Codex, Cursor, and WorkBuddy return void and swallow filesystem errors, so only CodeBuddy adapters can produce false; the remaining platforms print ✓ Rolled back for no-ops and failed deletes.
  • [P2] Cursor transcripts are exposed through world-readable temporary SQL filessrc/session-flow/cursor-store.ts:597, src/session-flow/cursor-store.ts:637: both registration and deletion use default file permissions in shared temporary storage. Registration SQL contains the complete transcript and can remain after process termination.
  • [P2] Target registration failures are still classified as successful migrationssrc/session-flow/adapters/cursor.ts:528, src/session-flow/adapters/workbuddy.ts:686, src/session-flow/adapters/codex.ts:853: failures to populate the client databases are swallowed, yet writeSession() returns an ID and the command prints success even though the conversation may be invisible or empty.
  • [P2] SSH URL forms produce different identities for the same repositorysrc/session-flow/sync.ts:97: ssh://git@github.com/org/repo.git becomes git@github.com/org/repo, whereas git@github.com:org/repo.git becomes github.com/org/repo.
  • [P2] Repository encoding remains non-injectivesrc/session-flow/sync.ts:117: escaping underscores fixes the previously cited underscore/slash example, but every other special character still becomes _; distinct valid identities containing characters such as : and / can still share an archive directory.
  • [P2] Ambiguous archive names still select an arbitrary authorsrc/session-flow/sync.ts:500: findAuthor() returns the first matching directory rather than detecting multiple authors with the same generated session name and requiring --author.
  • [P2] Index rebuilding loses unsanitized author identitiessrc/session-flow/sync.ts:660: it records the sanitized directory name instead of meta.origin.author. A subsequent push using the real Git author misses deduplication and can create duplicate archives.
  • [P2] session push can commit unrelated archive changessrc/session-flow/sync.ts:713: restricting the pathspec fixes committing unrelated files outside sessions/, but git add sessions/ still stages and commits every unrelated modification or deletion already present anywhere under that directory.
  • [P2] Ambiguous session-ID prefixes migrate multiple sessionssrc/session-flow/session-cmd.ts:349: a non---all request retains every prefix match instead of rejecting ambiguity, potentially migrating several sessions without the batch confirmation.
  • [P2] Production code violates the English-only rule — extensive Chinese comments remain throughout the new production subsystem, beginning at src/session-flow/session-cmd.ts:2, src/session-flow/sync.ts:2, and the adapter files.

Resolved From Earlier Reviews

  • Codex rollback now validates UUIDv7 IDs and escapes SQL values.
  • Destructive commands now have --dry-run guards.
  • migrate --all now enumerates globally, and the English/Chinese usage wording agrees.
  • Original archive titles are persisted and restored.
  • The original unrestricted Git commit was narrowed to sessions/, though the remaining archive-scoped staging issue is noted above.

I only inspected the specified diff and did not run, build, or install PR code.

@jeff-r2026 jeff-r2026 assigned jeff-r2026 and unassigned jeff-r2026 Sep 23, 2026
@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] Untrusted archives can exfiltrate arbitrary local filessrc/session-flow/adapters/claude-code.ts:272: session resume trusts an archived image’s filePath, reads it, and embeds it as base64. A crafted team archive can capture SSH keys, credentials, or other readable files.
  • [P1] Untrusted archives can overwrite files through CodeBuddy IDE restorationsrc/session-flow/ide-history.ts:314, src/session-flow/ide-history.ts:637, src/session-flow/ide-history.ts:675: archive-controlled image labels and message IDs are used as path components without basename validation, allowing ../ traversal outside assets/ or messages/.
  • [P1] Same-platform migration or resume can overwrite existing sessionssrc/session-flow/session-cmd.ts:257, src/session-flow/adapters/claude-code.ts:626: native IDs are reused and same-platform migration is allowed. Migrating into the source workspace overwrites the original; rollback then deletes it. Resuming an older archive can similarly replace newer local history.
  • [P1] Scoped rollback can affect sessions outside --cwdsrc/session-flow/ide-history.ts:753, src/session-flow/adapters/codex.ts:1041, src/session-flow/adapters/cursor.ts:628, src/session-flow/adapters/workbuddy.ts:709: IDE deletion falls back to a global search, Codex ignores projectPath, and Cursor/WorkBuddy unregister globally before verifying the scoped transcript.
  • [P1] Remote push failures still report successsrc/session-flow/session-cmd.ts:191, src/session-flow/session-cmd.ts:621: pushToRemote() swallows git push errors, after which the command prints ✓ Pushed and exits successfully.
  • [P1] Commit failures are still reported as “No changes”src/session-flow/sync.ts:723: hook, signing, identity, permission, and lock failures return null; callers print No changes to push while archive changes remain staged.
  • [P1] Credentials in Git remotes are archived and displayedsrc/session-flow/sync.ts:94: canonicalization removes the URL scheme but retains HTTP userinfo, so a remote such as https://oauth2:TOKEN@host/org/repo.git persists the token in archive metadata and indexes.
  • [P1] Full transcripts are shared without prior consent or redaction by defaultsrc/session-flow/session-cmd.ts:582: up to five sessions are written, committed, and pushed without confirmation. The warning is printed only after the archive files have already been created, and --scrub remains opt-in.
  • [P1] Required validation remains incomplete — the PR description reports a failing full Vitest run and provides no real built-CLI E2E record for Codex, OpenCode, or the required git/gitlab/github provider matrix. The base AGENTS.md requires every Test Plan item to pass.

Other Findings

  • [P2] pull --all cannot repair missing or corrupt indexessrc/session-flow/sync.ts:559: repository directories without a valid _index.json are omitted from listAllRepoIdentities(), so rebuildIndex() is never called for the repositories that need recovery.
  • [P2] Duplicate session IDs across workspaces process the wrong transcriptsrc/session-flow/session-cmd.ts:380, src/session-flow/session-cmd.ts:585: after global enumeration, migrate and push discard each entry’s location and read globally by ID, causing adapters to repeatedly select the first matching workspace.
  • [P2] Cross-workspace pushes attribute every archive to one authorsrc/session-flow/session-cmd.ts:454, src/session-flow/session-cmd.ts:537: one Git identity is resolved and reused for sessions belonging to repositories with different local identities.
  • [P2] Rollback reports success for missing files and failed deletionssrc/session-flow/session-cmd.ts:850: Claude, Codex, Cursor, and WorkBuddy return void and suppress deletion errors, so no-op or failed rollbacks still print ✓ Rolled back.
  • [P2] Cursor transcripts are exposed through permissive temporary filessrc/session-flow/cursor-store.ts:595, src/session-flow/cursor-store.ts:635: SQL containing full conversation content is created with default permissions in shared temporary storage and may remain after termination.
  • [P2] Target registration failures are classified as successful migrationssrc/session-flow/adapters/cursor.ts:528, src/session-flow/adapters/workbuddy.ts:686, src/session-flow/adapters/codex.ts:884: database/indexing failures are swallowed, but writeSession() returns an ID and migration reports success even when the session is invisible or empty in the client.
  • [P2] Deterministic IDs collide across target workspacessrc/session-flow/ids.ts:15, src/session-flow/adapters/cursor.ts:135: target IDs omit the target CWD. Migrating one source into multiple workspaces reuses the same globally keyed Cursor, WorkBuddy, or Codex database entry, making earlier copies disappear or point at the wrong workspace.
  • [P2] SSH URL forms identify the same repository differentlysrc/session-flow/sync.ts:94: ssh://git@github.com/org/repo.git becomes git@github.com/org/repo, while git@github.com:org/repo.git becomes github.com/org/repo.
  • [P2] Repository directory encoding remains non-injectivesrc/session-flow/sync.ts:115: all non-whitelisted characters except underscores become _, so distinct identities containing characters such as : and / can share one archive directory.
  • [P2] Windows author names can escape the archive directorysrc/session-flow/sync.ts:131: sanitizePathSegment() replaces / but not \\, which is a path separator on Windows. A Git author containing ..\\ can make archive writes leave the intended author directory.
  • [P2] Ambiguous archive names select an arbitrary authorsrc/session-flow/sync.ts:497: findAuthor() returns the first matching directory instead of rejecting multiple authors with the same session name and requiring --author.
  • [P2] Index rebuilding loses canonical author identitiessrc/session-flow/sync.ts:660: rebuilt entries use the sanitized directory name rather than meta.origin.author, breaking author filtering and later deduplication.
  • [P2] session push can commit unrelated archive changessrc/session-flow/sync.ts:713: git add sessions/ stages every pre-existing modification and deletion under sessions/, not only files written by the current operation.
  • [P2] Ambiguous session-ID prefixes migrate multiple sessionssrc/session-flow/session-cmd.ts:349: a non---all prefix request retains every match rather than rejecting ambiguity.
  • [P2] --scrub still leaks local image pathssrc/session-flow/scrub.ts:84: image blocks are returned unchanged, so absolute filePath values—including usernames or secrets embedded in path components—are committed to the team archive.
  • [P2] Archive indexes are updated non-atomically without lockingsrc/session-flow/sync.ts:329, src/session-flow/sync.ts:336: concurrent pushes can interleave read-modify-write operations, lose entries, or leave a partially written _index.json.
  • [P2] Production code still violates the English-only rule — the final tree contains hundreds of Chinese production comments across 18 files, beginning at src/session-flow/adapters/base.ts:2, src/session-flow/session-cmd.ts:2, and src/session-flow/sync.ts:2.
  • [P2] Bilingual usage documentation is inconsistentdocs/usage-guide.zh-CN.md:1287 says migrate --all migrates the “recent 5,” while docs/usage-guide.md:1323 and the implementation migrate every session.

Resolved From Earlier Reviews

  • Codex rollback validates UUIDv7 identifiers and escapes SQL values.
  • Destructive commands include --dry-run guards.
  • migrate --all now enumerates all workspaces.
  • Original archive titles are persisted and restored.
  • Git commit uses a sessions/ pathspec, although it still includes unrelated changes within that directory.

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]').
@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] Untrusted archives can exfiltrate arbitrary local filessrc/session-flow/adapters/claude-code.ts:272, src/session-flow/ide-history.ts:320: archived image filePath values are trusted and read/copied during resume. A crafted team archive can capture SSH keys, credentials, or other readable files.
  • [P1] Untrusted archives can overwrite files through CodeBuddy IDE restorationsrc/session-flow/ide-history.ts:314, src/session-flow/ide-history.ts:637, src/session-flow/ide-history.ts:675: archive-controlled image labels and message IDs become path components without basename validation, allowing ../ traversal outside assets/ or messages/.
  • [P1] A malicious team-repo symlink can overwrite arbitrary local filessrc/session-flow/sync.ts:333, src/session-flow/sync.ts:425, src/session-flow/sync.ts:429: archive files and indexes are written with APIs that follow symlinks. A committed _index.json or author-directory symlink can redirect session push writes outside the repository.
  • [P1] Same-platform migration or resume can overwrite existing sessionssrc/session-flow/session-cmd.ts:257, src/session-flow/adapters/claude-code.ts:626: native IDs are reused while same-platform migration is allowed. Migrating into the source workspace overwrites the original, and rollback then deletes it; restoring an older archive can similarly replace newer history.
  • [P1] Scoped rollback can affect sessions outside --cwdsrc/session-flow/ide-history.ts:753, src/session-flow/adapters/codex.ts:1041, src/session-flow/adapters/cursor.ts:653, src/session-flow/adapters/workbuddy.ts:709: IDE deletion falls back to a global search, Codex ignores projectPath, and Cursor/WorkBuddy unregister globally before checking the scoped transcript.
  • [P1] Remote push failures still report successsrc/session-flow/session-cmd.ts:191, src/session-flow/session-cmd.ts:621: pushToRemote() swallows git push errors, after which callers print ✓ Pushed and exit successfully, contradicting test-plan item 13.
  • [P1] Commit failures are reported as “No changes”src/session-flow/sync.ts:723: hook, signing, identity, permission, and lock failures return null; callers treat that as a clean tree even though archive changes may remain staged.
  • [P1] Credentials embedded in Git remotes are archived and displayedsrc/session-flow/sync.ts:94: canonicalization strips the scheme but retains HTTP userinfo, so https://oauth2:TOKEN@host/org/repo.git persists the token in metadata and indexes.
  • [P1] Full transcripts are shared without prior consent or redaction by defaultsrc/session-flow/session-cmd.ts:565, src/session-flow/session-cmd.ts:605: normal pushes of five or fewer sessions require no confirmation, and the warning appears only after files have been written. --scrub remains opt-in.
  • [P1] Required validation is incomplete — the PR reports a failing full Vitest run and has no real built-CLI E2E record for Codex, OpenCode, or the required git/gitlab/github provider matrix. The trusted base AGENTS.md requires every Test Plan item to pass.

Other Findings

  • [P2] pull --all cannot repair missing or corrupt indexessrc/session-flow/sync.ts:559: repository directories without a valid _index.json are skipped, so rebuildIndex() is never called for the repositories needing recovery.
  • [P2] Duplicate session IDs across workspaces process the wrong transcriptsrc/session-flow/session-cmd.ts:380, src/session-flow/session-cmd.ts:585: global enumeration discards each entry’s location and subsequently reads by ID alone, repeatedly selecting the first matching workspace.
  • [P2] Cross-workspace pushes attribute every archive to one authorsrc/session-flow/session-cmd.ts:454, src/session-flow/session-cmd.ts:537: one Git identity is resolved and reused for sessions belonging to repositories with different local identities.
  • [P2] Rollback reports success for missing files and failed deletionssrc/session-flow/session-cmd.ts:850: Claude, Codex, Cursor, and WorkBuddy return void and suppress deletion errors, so failed or no-op rollbacks still print ✓ Rolled back.
  • [P2] Cursor transcripts are exposed through permissive temporary filessrc/session-flow/cursor-store.ts:595, src/session-flow/cursor-store.ts:635: SQL containing complete conversations is created with default permissions in shared temporary storage and may remain after termination.
  • [P2] Target registration failures are classified as successful migrationssrc/session-flow/adapters/cursor.ts:561, src/session-flow/adapters/workbuddy.ts:696, src/session-flow/adapters/codex.ts:884: database/indexing failures are swallowed, while writeSession() returns an ID and migration reports success even when the session is invisible or empty.
  • [P2] Deterministic IDs collide across target workspacessrc/session-flow/ids.ts:17, src/session-flow/adapters/cursor.ts:434: target IDs omit the target CWD. Migrating one source into multiple workspaces reuses globally keyed Cursor, WorkBuddy, or Codex records.
  • [P2] SSH URL forms identify the same repository differentlysrc/session-flow/sync.ts:94: ssh://git@github.com/org/repo.git and git@github.com:org/repo.git produce different identities.
  • [P2] Repository directory encoding remains non-injectivesrc/session-flow/sync.ts:115: distinct identities containing non-whitelisted characters such as : and / can map to the same archive directory.
  • [P2] Windows author names can escape the archive directorysrc/session-flow/sync.ts:131: sanitizePathSegment() does not replace \\, which is a Windows path separator.
  • [P2] Ambiguous archive names select an arbitrary authorsrc/session-flow/sync.ts:497: findAuthor() returns the first matching directory instead of rejecting ambiguity and requiring --author.
  • [P2] Index rebuilding loses canonical author identitiessrc/session-flow/sync.ts:660: rebuilt entries use the sanitized directory name rather than meta.origin.author, breaking filtering and deduplication.
  • [P2] session push can commit unrelated archive changessrc/session-flow/sync.ts:713: git add sessions/ and the directory pathspec include every pre-existing modification or deletion under sessions/, not only files created by this operation.
  • [P2] Ambiguous session-ID prefixes migrate multiple sessionssrc/session-flow/session-cmd.ts:349: a non---all prefix request retains every match rather than rejecting ambiguity.
  • [P2] --scrub still leaks local image pathssrc/session-flow/scrub.ts:84: image blocks are returned unchanged, so absolute filePath values remain in the team archive.
  • [P2] Archive indexes are updated non-atomically without lockingsrc/session-flow/sync.ts:329, src/session-flow/sync.ts:336: concurrent pushes can lose entries or leave a partially written _index.json.
  • [P2] Archived content can inject terminal control sequencessrc/session-flow/session-cmd.ts:698, src/session-flow/session-cmd.ts:824: untrusted titles, authors, and search snippets are printed without removing ANSI/OSC sequences.
  • [P2] Production code violates the English-only rule — Chinese production comments remain across the new subsystem, beginning at src/session-flow/adapters/base.ts:2, src/session-flow/session-cmd.ts:2, and src/session-flow/sync.ts:2.
  • [P2] Bilingual usage documentation is inconsistentdocs/usage-guide.zh-CN.md:1287 says migrate --all migrates the recent five sessions, while the English guide and implementation migrate every session.

Resolved From Earlier Reviews

  • Codex rollback validates UUIDv7 identifiers and escapes SQL values.
  • Destructive commands include --dry-run guards.
  • migrate --all enumerates all workspaces.
  • Original archive titles are persisted and restored.
  • Git commit now uses a sessions/ pathspec, although unrelated changes within that directory remain affected.

I inspected only the specified diff using read-only Git commands and did not run, build, install, or execute PR code.

lurkacai added 2 commits September 23, 2026 20:08
… 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.
@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] Untrusted archives can exfiltrate arbitrary local filessrc/session-flow/adapters/claude-code.ts:272, src/session-flow/ide-history.ts:320: archived image filePath values are trusted and read/copied during resume. A crafted team archive can capture SSH keys, credentials, or any other readable file.
  • [P1] Untrusted archives can overwrite files through CodeBuddy IDE restorationsrc/session-flow/ide-history.ts:314, src/session-flow/ide-history.ts:642, src/session-flow/ide-history.ts:680: archive-controlled image labels and message IDs become path components without basename validation, allowing ../ traversal outside assets/ or messages/.
  • [P1] Repository symlinks can redirect archive writes outside the repositorysrc/session-flow/sync.ts:333, src/session-flow/sync.ts:425, src/session-flow/sync.ts:429: indexes and session files are written through paths that may be committed symlinks. A malicious team-repo checkout can redirect session push to overwrite arbitrary writable files.
  • [P1] Same-platform migration or resume can overwrite existing sessionssrc/session-flow/session-cmd.ts:257, src/session-flow/adapters/claude-code.ts:626: native IDs are reused while same-platform migration is allowed. Migrating into the source workspace overwrites the original, and rollback can then delete it; restoring an older archive can likewise replace newer history.
  • [P1] Scoped rollback can affect sessions outside --cwdsrc/session-flow/ide-history.ts:758, src/session-flow/adapters/codex.ts:1041, src/session-flow/adapters/cursor.ts:659, src/session-flow/adapters/workbuddy.ts:709: IDE deletion falls back to a global search, Codex ignores projectPath, and Cursor/WorkBuddy unregister globally before checking the scoped transcript.
  • [P1] Remote push failures still report successsrc/session-flow/session-cmd.ts:191, src/session-flow/session-cmd.ts:621: pushToRemote() swallows git push errors, after which callers print ✓ Pushed and exit successfully, contradicting test-plan item 13.
  • [P1] Commit failures are reported as “No changes”src/session-flow/sync.ts:723: hook, signing, identity, permission, and lock failures return null; callers treat that as a clean tree even though archive changes may remain staged.
  • [P1] Credentials embedded in Git remotes are archived and displayedsrc/session-flow/sync.ts:94: canonicalization removes the scheme but retains HTTP userinfo, so https://oauth2:TOKEN@host/org/repo.git persists the token in metadata, indexes, and cross-project output.
  • [P1] Full transcripts are shared without prior consent or redaction by defaultsrc/session-flow/session-cmd.ts:565, src/session-flow/session-cmd.ts:605: normal pushes of five or fewer sessions require no confirmation. The privacy warning appears only after files are written and the command immediately commits/pushes them; --scrub remains opt-in.
  • [P1] Required validation is incomplete — the PR description explicitly reports a failing full Vitest run and provides no built-CLI E2E record covering Codex, OpenCode, or the required git/gitlab/github provider matrix. The trusted base AGENTS.md requires every Test Plan item to pass.

Other Findings

  • [P2] pull --all cannot repair missing or corrupt indexessrc/session-flow/sync.ts:559: repository directories without a valid _index.json are skipped, so rebuildIndex() is never called for the repositories needing recovery.
  • [P2] Duplicate session IDs across workspaces process the wrong transcriptsrc/session-flow/session-cmd.ts:380, src/session-flow/session-cmd.ts:585: global enumeration discards each entry’s location and subsequently reads by ID alone, repeatedly selecting the adapter’s first matching workspace.
  • [P2] Cross-workspace pushes attribute every archive to one authorsrc/session-flow/session-cmd.ts:454, src/session-flow/session-cmd.ts:537: one Git identity is resolved and reused for sessions belonging to repositories with different local identities.
  • [P2] Rollback reports success for missing files and failed deletionssrc/session-flow/session-cmd.ts:850: Claude, Codex, Cursor, and WorkBuddy return void and suppress deletion errors, so failed or no-op rollbacks still print ✓ Rolled back.
  • [P2] Cursor transcripts are exposed through permissive temporary filessrc/session-flow/cursor-store.ts:595, src/session-flow/cursor-store.ts:635: SQL containing complete conversation content is created with default permissions in shared temporary storage and can remain after termination.
  • [P2] Target registration failures are classified as successful migrationssrc/session-flow/adapters/cursor.ts:559, src/session-flow/adapters/workbuddy.ts:686, src/session-flow/adapters/codex.ts:884: database/indexing failures are swallowed, while writeSession() returns an ID and migration reports success even when the session is invisible or empty in the target client.
  • [P2] Deterministic IDs collide across target workspacessrc/session-flow/ids.ts:17, src/session-flow/adapters/cursor.ts:438: target IDs omit the target CWD. Migrating one source session into multiple workspaces reuses globally keyed Cursor, WorkBuddy, or Codex records, replacing or redirecting earlier copies.
  • [P2] SSH URL forms identify the same repository differentlysrc/session-flow/sync.ts:94: ssh://git@github.com/org/repo.git becomes git@github.com/org/repo, while git@github.com:org/repo.git becomes github.com/org/repo.
  • [P2] Repository directory encoding remains non-injectivesrc/session-flow/sync.ts:115: distinct identities containing non-whitelisted characters such as : and / can map to the same archive directory.
  • [P2] Windows author names can escape the archive directorysrc/session-flow/sync.ts:131: sanitizePathSegment() does not replace \, which is a path separator on Windows.
  • [P2] Ambiguous archive names select an arbitrary authorsrc/session-flow/sync.ts:497: findAuthor() returns the first matching directory instead of rejecting multiple authors with the same session name and requiring --author.
  • [P2] Index rebuilding loses canonical author identitiessrc/session-flow/sync.ts:660: rebuilt entries use the sanitized directory name instead of meta.origin.author, breaking author filtering and later deduplication.
  • [P2] Index rebuilding also destroys original titlessrc/session-flow/sync.ts:662: rebuilding uses the truncated, lowercased filename slug instead of meta.origin.title, so session pull permanently degrades titles shown by session list.
  • [P2] session push can commit unrelated archive changessrc/session-flow/sync.ts:713: git add sessions/ and the commit pathspec include every pre-existing modification or deletion under sessions/, not only files created by the current operation.
  • [P2] Ambiguous session-ID prefixes migrate multiple sessionssrc/session-flow/session-cmd.ts:349: a non---all prefix request retains every match instead of rejecting ambiguity, potentially performing an unconfirmed batch migration.
  • [P2] --scrub still leaks local image pathssrc/session-flow/scrub.ts:84: image blocks are returned unchanged, so absolute filePath values—including usernames or sensitive path components—remain in the team archive.
  • [P2] Archive indexes are updated non-atomically without lockingsrc/session-flow/sync.ts:329, src/session-flow/sync.ts:336: concurrent pushes can interleave read-modify-write operations, lose entries, or leave partially written _index.json files.
  • [P2] Archived content can inject terminal control sequencessrc/session-flow/session-cmd.ts:386, src/session-flow/session-cmd.ts:698, src/session-flow/session-cmd.ts:824: untrusted titles, authors, repository identities, and search snippets are printed without stripping ANSI/OSC sequences.
  • [P2] Production code violates the English-only rule — Chinese production comments remain throughout the new subsystem, beginning at src/session-flow/adapters/base.ts:2, src/session-flow/session-cmd.ts:2, and src/session-flow/sync.ts:2.
  • [P2] Bilingual usage documentation is inconsistentdocs/usage-guide.zh-CN.md:1287 says migrate --all migrates the recent five sessions, while docs/usage-guide.md:1323 and the implementation migrate every session.

Resolved From Earlier Reviews

  • Codex rollback validates UUIDv7 identifiers and escapes SQL values.
  • Destructive commands include --dry-run guards.
  • migrate --all enumerates all workspaces.
  • Original archive titles are persisted and restored during normal loading.
  • Git commit uses a sessions/ pathspec, although unrelated changes within that directory remain affected.

I inspected only the specified diff using read-only Git commands and did not run, build, install, or execute PR code.

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.

[feat] Cross-platform session migration and team session archive

2 participants