[feat] Session archive: no local paths, and record what the session ran as
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- git, node.js, typescript
Research direction
Start with SyncManager.saveSessionLocked in sync.ts and the existing tests under src/__tests__/; then trace the named publication paths and scrubSession in scrub.ts. First verify the Claude Code thinking-signature behavior described in Slice 2. Done means the acceptance criteria are covered by tests, the real-session resume check is recorded, docs are updated, and lint, typecheck, and tests pass.
Written by the indexing model from the issue text.
Description
Goal
A session pushed to the team archive (teamai session push --scrub, teamai-share-session, workflow publish --sessions, score --drain) should keep no absolute local paths of the member's machine, and should record what the session ran as, so the team can debug it, group it by team revision, and compare runs.
Ships as one PR, built and verified slice by slice.
Today
A real _index.json entry after teamai session push --scrub:
{
"sessionName": "claude-code_test_20261030",
"author": "Alice Doe",
"platform": "claude-code",
"cwd": "/Users/alice/Repositorios/app",
"repoIdentity": "github.com/acme/app",
"sessionId": "6e6403a5-6ada-4c5a-95a3-1f302fe92912"
}
The same path is in meta.json (origin.cwd) and throughout the scrubbed transcript:
tool_call {"command": "cat /Users/alice/Repositorios/app/skill-data/workflow/references/hosts.md"}
tool_result Output is being written to: /private/tmp/claude-502/-Users-alice-Repositorios-app/6e64…/tasks/bl7y.output
Nothing records the team revision the session ran with. docs/guide/scorers.md:225 states it: a pushed session's label "has no agent, because what a session ran as is recorded only on the machine that ran it". The data exists locally: the session-start hook captures it (SessionCaptures, #1178, built from #1176's AgentObservation), but push never carries it.
Where the archived cwd is read
| Reader | Uses archived cwd? |
|---|---|
Workflow restore, openSessionArchive (archive.ts:58,87) |
No: looks up by repoIdentity(target.cwd) + platform + sessionId, then overwrites cwd with target.cwd |
teamai session resume (session-cmd.ts:549-561) |
No: overwrites with --cwd or process.cwd() |
teamai session pull |
No: uses the local cwd for repoIdentity only |
score --team-session (scoring.ts:306) |
Log line only |
| Dashboard session sharing, scorer name join | No |
So the absolute path is not needed to resume or hand over a session.
Slice 1: archived cwd is relative to its repo or workspace
The cwd is stored relative to the root the session belongs to. That keeps the sub-directory for monorepos and multi-repo workspaces and drops the machine prefix.
Native session.cwd |
Archived cwd |
|---|---|
/Users/alice/app (git top level) |
. |
/Users/alice/app/packages/api (monorepo) |
packages/api |
/Users/alice/ws/svc-a (child repo of a workspace) |
. (root is svc-a, its own repo) |
/Users/alice/ws/backend (plain folder inside a non-git teamai workspace ws/) |
backend (root is the workspace's projectRoot) |
/Users/alice/tmp/scratch (no repo, no teamai config) |
"" |
md5:3f2a… (codebuddy-ide placeholder) |
"" |
| Removed workflow worktree | "" |
Rules:
- Root = git top level of
cwd, else theprojectRootof the teamai config found forcwd, else none (""). - Computed once in
SyncManager.saveSessionLocked(sync.ts:704). All four push paths go through it:archive.ts:159,archive.ts:192,session-cmd.ts:273,session-migrate-cmd.ts:482. - The index entry takes
cwdfrommeta.origin.cwd, notsession.cwd(sync.ts:749). Today a save writessession.cwdand a rebuild (sync.ts:1026) writesmeta.origin.cwd: they must agree. authorForCwdandderiveArchiveIdentitykeep reading the native cwd before saving. The archive key does not change.loadSessionreadsorigin.cwd ?? "". Restore and resume keep overwriting it.
After:
{ "sessionName": "claude-code_test_20261030", "cwd": ".", "repoIdentity": "github.com/acme/app", "...": "..." }
Slice 2: a scrubbed thinking block still resumes (verify first)
scrubBlock (scrub.ts:58-63) masks a thinking block's text and keeps its signature. The Claude Code adapter writes it back (claude-code.ts:260), so a resumed session sends a signature that no longer matches its text. This already happens today whenever a secret is masked inside thinking, and Slice 3 makes it common, since thinking often names paths. Whether the API rejects it is not known: thinking from earlier turns is dropped from context, so its signature may never be checked.
Start with the check, before any code in this slice:
- Resume a Claude Code session whose thinking in an earlier turn has altered text and its original signature. → verify: the first new turn is accepted.
- Accepted: this slice ships no code. Masked thinking stays a native, signed block, and the PR records the check.
- Rejected: changed blocks only are archived as text, as below.
| Thinking block | Archived as (plan B only) |
|---|---|
| Unchanged by the scrub | { type: "thinking", text, signature }, as today |
| Changed by the scrub | { type: "text", text: "<thinking>\n…masked…\n</thinking>" }, no signature (the degradeThinkingBlocks convention, migrate.ts:144) |
Plan B keeps every word of the reasoning for the team, but a resumed session carries those blocks as text, which the API does not drop from context. Do not leave an unsigned native thinking block: it is not known to be valid either.
Slice 3: published transcripts carry no local paths
On archive publication only, the scrub rewrites local paths in the title, text, thinking text, tool call argument string values, tool result text, and string metadata kept in the message.
| Before | After |
|---|---|
/Users/alice/app/src/sync.ts |
<repo>/src/sync.ts |
/Users/alice/app |
<repo> |
/Users/alice/.claude/settings.json |
~/.claude/settings.json |
/Users/alice/other-project/x.ts |
~/other-project/x.ts |
/private/tmp/claude-502/-Users-alice-app/6e64…/tasks/b.output |
<tmp>/<project>/6e64…/tasks/b.output |
C:\Users\alice\app\src\sync.ts |
<repo>\src\sync.ts |
file:///Users/alice/app/README.md |
file://<repo>/README.md |
src/sync.ts, /api/v1/users, https://github.com/acme/app |
unchanged |
Rules:
<repo>is the Slice 1 root,~this machine's home directory,<tmp>the per-user temp root (os.tmpdir()resolved,/private/tmp/claude-<uid>,/tmp/claude-<uid>).<project>is the tool's encoded form of the root (encodeCwdClaude). Longest prefix wins.- Walk structured values, never a regex over serialized JSON: a Windows path is escaped there.
- Publication only.
AgentSession.findalso scrubs for scoring and skill attribution, which match tool call paths against delivered file paths (session-work.ts:128,skill-use.ts:300). That read keeps native paths. The archive push paths pass an explicit option. - Tool call change detection compares values (
next === raw), not the count of<REDACTED:markers (scrub.ts:70). Otherwise a path rewrite with no secret in the same arguments is dropped. - Image
filePathhandling is unchanged. - Push without
--scrubkeeps its current as-is behaviour and warning.
Slice 4: the archive records what the session ran as
At push, read the session's local captures and store their settled observation in the meta:
SessionSyncMeta {
origin: { platform, author, cwd, repoIdentity, createdAt, sessionId, title? }
+ agent?: AgentObservation // SessionCaptures.agentOf(captures, reportedModel)
}
IndexEntry {
+ teamRevision?: string // agent.team.revision
}
Example meta.json:
"agent": {
"v": 1,
"tool": { "name": "claude", "version": "2.1.301" },
"model": { "requested": "claude-opus-5-5", "reported": "claude-opus-5-5" },
"team": { "revision": "7a6134a25e74…", "localChanges": false, "selection": { "primaryRole": "backend" } },
"project": { "commit": "8e04ff430…", "dirty": true },
"teamai": "0.31.0",
"driver": "session",
"fingerprint": "…"
}
| Case | agent |
Index teamRevision |
|---|---|---|
| One capture | the observation | team.revision |
| Captures agree | the observation | team.revision |
A capture is marked teamChanged |
without team and fingerprint (agentOf rule) |
absent |
| Captures differ, or none (session predates the hook, tool has no hook, captures swept) | absent | absent |
| Session teamai launched (workflow step) | absent: none is captured (captures.ts:61); the run snapshot already records each pass's agent |
absent |
session migrate --push |
absent | absent |
Rules:
- Never fill it from the team clone's HEAD at push time. That is the revision at push, not the one the session ran with.
- Parse with
AgentObservationSchemaon read: an invalid value is treated as absent. The schema is an allowlist with no path fields. score --team-sessionuses the archivedagentas the label'sagentinstead of none (scoring.ts:310), so pushed sessions group by tool, model and team revision on the Scorers page.- Re-push of the same session overwrites
agentwith what the captures say at that time.
What this enables: show the exact skill text a session had (git show <rev>:skills/<name>/SKILL.md), flag sessions that ran on a stale team revision or with localChanges, group sessions and scores by team revision, and compare runs with the same project.commit and different team.revision.
Acceptance criteria
- Pushing a Claude session started in
/…/app/packages/apiwrites"cwd": "packages/api"to bothmeta.jsonand_index.json. Rebuilding the index produces the same entry. - Each row of the Slice 1 table holds, for
session push,session migrate --push,workflow publish --sessionsandscore --drain. - A session archived with a relative
cwdrestores through a workflow handover and throughteamai session resumeon another directory, and its turns are intact. - A Claude Code session whose thinking was masked by the scrub (a secret or a local path) resumes after restore, and its first new turn is accepted. Under plan B only: that block is archived as
<thinking>text with nosignature, and an untouched thinking block keeps its signature. - After
teamai session push --scrub,rg -F "$HOME" <archive dir>andrg "/Users/|/home/|C:\\\\Users\\\\|claude-[0-9]+/" <archive dir>find nothing for a fixture covering every row of the Slice 3 table. - Scoring a native session still attributes a team skill read through its absolute
SKILL.mdpath. - A tool call whose arguments contain a local path and no secret is archived with the path rewritten.
- A session with one capture is archived with
agentequal toagentOfof its captures andteamRevisionin its index entry. A session with no captures has neither. score --team-sessionon a pushed session withagentgives a label with thatagent. One withoutagentstill gives a label without it.- An archive written before this change (absolute
cwd, noagent) still lists, restores, resumes and scores.
Definition of done
- Acceptance criteria covered by tests in
src/__tests__/(Vitest), through the public seams:SyncManager,scrubSession,AgentSession,openSessionArchive, thesessionandscorecommands. - Slice 2's check recorded in the PR: its result, the Claude Code version, and which plan shipped. Then push a real Claude Code session with
--scrub, resume it on another directory, and confirm the first turn after resume is accepted (no signature error). - Docs updated in
docs/guide/anddocs/guide/zh-CN/:scorers.md(thein /home/alice/appexamples at :203 and :211, and :225),sharing.md(what a published session keeps). npm run lint,npm run typecheck,npm testgreen.
Out of scope
- Rewriting archives already on
teamai-reports. They are fixed when re-pushed, and git history keeps old content regardless. - Refusing a push on local paths, as
local_path_detecteddoes for run snapshots: almost every transcript would be refused. - Removing signatures from untouched thinking blocks.
- Dashboard views for archived sessions by team revision. This issue records the data.
- Archive identity for sessions outside any repo (
_unattributed). - Captures for sessions teamai launched.
- The fallback to the CLI's directory when
session.cwdis empty (archive.ts:159,session-cmd.ts:273,session-migrate-cmd.ts:482), beyond what Slice 1's rules already cover.
Related
#1176 (agent observation), #1178 (session captures), #913 (workspace children)
- Dominant language
- TypeScript
- Stars
- 5.7k
- Forks
- 486
- Avg merge
- 19h
- Merged PRs (30d)
- 415
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Tencent/teamai-cli
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 25/100
Tencent/teamai-cli#1605 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 35/100
Tencent/teamai-cli#1590 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
Tencent/teamai-cli#1589 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 20/100
Tencent/teamai-cli#1564 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
Tencent/teamai-cli#1563 ·
Maintainers usually reply within 1 day
All issues in Tencent/teamai-cli
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Slaymish/HealthAgent#7 ·
-
area: engine topic: math type: bug
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Get-Concord-AI/concord-mcp#310 ·
Maintainers usually reply within 1 day
-
Correct install_claude: 'marketplace add' over a different source is refused, not replaced in placeOpentype:bug
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
nightgauge/nightgauge#2804 ·
Maintainers usually reply within 1 day