Skip to content

Latest commit

 

History

History
23 lines (16 loc) · 11.5 KB

agent-sessions.md

File metadata and controls

23 lines (16 loc) · 11.5 KB

Agent sessions

Maintainer notes on src/session-flow/, the code behind teamai session. The commands are described in the Dashboard guide page.

Each rule below spans more than one file. The rule names the code that owns it, so you can read the details there.

Rules that span files

  • Every target id is derived. An adapter's writeSession gets the id it writes under from deriveTargetSessionId in ids.ts, never from the source session, even when the source id is a UUID. The key is the target platform, the target workspace, and the source id, for every platform. Claude Code resumes by id across all project directories, so a copy in a second workspace needs its own id. A new adapter calls deriveTargetSessionId before it writes anything. OpenCode ids encode their creation time and OpenCode refuses an id it already has, so an OpenCode copy keeps OpenCode's id shape (time part, then the derived suffix) and gets new message ids from deriveOpencodeMessageIds.

  • One workspace has one spelling. deriveTargetSessionId hashes the native real path of the workspace, so /tmp/x and /private/tmp/x, or two letter cases on macOS and Windows, name the same target. It refuses a workspace that is not absolute, such as a relative path or an md5:<hash> placeholder. Don't hash a workspace through resolveRealCwd in fs.ts: that one keeps the letter case it was given and decides where files land, not ids.

  • A copy never overwrites. Every path that writes a session into a tool (AgentSession.copy in agent-session.ts for session migrate, and session resume) writes through writeCopyOnce in agent-session.ts, which derives the id and looks the target up through the adapter's existingSessionId: the derived id across all of the platform's storage (every Codex rollout, archived ones included, and Cursor's composer store as well as its transcripts), then the ids an older TeamAI wrote (legacyTargetSessionIds in ids.ts) in the target workspace only. A target that exists but does not parse still exists. When one is found, the result is reused and nothing is written. When the write fails, the rollback deletes only the derived id, and only in the target workspace: it is the one target this call created. writeJsonl writes through a temp file and a rename. The check, the write and the rollback hold a lock on that one copy, ~/.teamai/locks/session-copy-<hash>.lock keyed by the target platform and the derived id, so a second command copying the same session into the same workspace waits and then reuses the copy, and one still blocked after ten seconds fails with copy_busy.

  • Tool CLIs are found and started the Windows way. An adapter that runs a tool's CLI finds it with probeBinary and runs it through cross-spawn (execCommand in utils/exec.ts), as the Codex and OpenCode adapters do: a hand-built PATH join and node:child_process miss the npm .cmd shim on Windows. An adapter whose CLI cannot run, or cannot tell whether a session exists, throws ToolUnavailableError (adapters/base.ts), which AgentSession and session migrate --json report as tool_unavailable, never as a missing session. The sqlite3 CLI comes from findSqlite3 in sqlite.ts. Cursor and WorkBuddy list a copy only once sqlite3 registers it, so on Windows noAdapter in agent-session.ts refuses them as targets with no_adapter and reason: 'unsupported on win32'; every command that names a platform reports that failure. Windows has no CI: test it through the seams that take a platform.

  • Read the session that was listed. One id can live in several workspaces (copies an older TeamAI wrote), and Codex also accepts an id suffix. A caller that enumerates sessions passes the filePath it listed (SessionReadRef in adapters/base.ts; a conversation directory on CodeBuddy IDE), so exactly that session is read, and the session id comes from what was opened, never from the request. Reads outside the copy path go through AgentSession.find, which redacts unless the caller passes scrub: false; session push and session migrate --push pass their --scrub choice.

  • List through AgentSession.list. A caller that wants a member's sessions (session migrate, session push, a scorer, the dashboard) calls AgentSession.list({ platforms?, cwd?, updatedSince? }) in agent-session.ts, never an adapter's listConversations. Each listed session is a ref that find reads as is. A session whose first lines do not parse is still listed, under a placeholder title, so find reports it unreadable, not not_found. A tool that cannot be listed is one failure value (no_adapter, tool_unavailable, or list_failed) next to the other tools' sessions. OpenCode's CLI lists one project at a time, and inside a git repo that project is the whole repo, so its adapter keeps only the sessions of the workspace asked for. Without cwd, OpenCode is list_failed, rather than returning the workspace the process runs in as if it were all of them. Its listing has no message count or size, so those fields are absent rather than 0.

  • Stores follow the recorded tool root. Claude Code, Codex and Cursor CLI keep their sessions under the root the member recorded with teamai init (toolRoots), resolved by resolveMemberToolRoots and resolveToolRootDir, never from CLAUDE_CONFIG_DIR, CODEX_HOME or CURSOR_DATA_DIR at run time. AgentSession and the session commands that build an adapter themselves (resume, rollback, platforms) resolve the roots with recordedToolRoots in agent-session.ts for the workspace at hand and pass them through the adapter registry (adapters/index.ts) to getClaudeCodeProjectsDir, getCodexSessionsDir and getCursorProjectsDir in fs.ts. A run that reads or copies many sessions passes one toolRootsLookup(), so each workspace is resolved once. With nothing recorded they stay ~/.claude/projects, ~/.codex/sessions and ~/.cursor/projects. Cursor is the one tool with two roots, recorded as cursor-data and cursor-config rather than as cursor, because the toolPaths entry named cursor must not move: its user mcp.json, rules/ and commands/ stay in ~/.cursor whatever the variables say. Its co-author write follows the config root through cursorConfigDir in types.ts, which also carries the $XDG_CONFIG_HOME/cursor fallback that toolRoots cannot record. The variants (claude-internal, codex-internal, ...) keep their fixed paths.

  • The driver picks how a session continues. The driver that runs a workflow step decides: the same tool on the same machine resumes the session by id; a driver with its own way to move a session uses it; otherwise the local driver copies it with AgentSession.copy. Failing those, it restores a copy from the team session archive (openSessionArchive in archive.ts). Failing that too, the step starts a fresh session, with a warning that says why each way failed. workflow publish --sessions is what puts a run's sessions in the archive. sessionArchiveRoot in session-cmd-shared.ts is the one rule for where the archive lives, for the session commands and the driver alike. A restore reads the teamai-reports checkout first, as it is, since the claim that hands a run over has just synced it. On a git-kind team, a session teamai-reports does not hold is then read from the archive teamai kept on the default branch before: the first such read in a driver run fetches origin's default branch into the team clone's refs/remotes/origin/<branch> and checks that commit out in a temporary --shared clone. Every later fallback read in that run reads that clone, and the member's branch, HEAD, and files never move. close() removes the clone after the run's prompt has exited. A failed fetch reads the team clone as it is, and the restore says it may be older than origin. In self mode only teamai-reports is read, so the driver does not fetch. The workflow engine never copies a session, and a run snapshot carries only the session's { tool, id }. A step's report carries the tool and that tool's native session id. A step that continued the lease's session also echoes it as resumed, and the engine records that the reported session continues it. The next step that resumes the conversation then gets that copy, with every turn, on any machine that has it or finds it in the archive; failing that, it tries the earlier sessions the lease lists, newest first, and gets fewer turns.

  • Session index lock. Every write to a team repo archive (saveSession: dedup lookup, name choice, file writes, _index.json upsert) and every index rebuild (rebuildIndex, rebuildAllIndexes) runs inside withSessionIndexLock(repoRoot, fn). SyncManager.saveSession, deleteSession, rebuildIndex, rebuildAllIndexes and commitRebuiltIndexes are asynchronous entry points that take it themselves; the synchronous mutations are private. An existing command holding the lock can call them within the same active operation without acquiring it again. The ownership context expires when that operation releases the lock. The lock file is ~/.teamai/locks/sessions-<hash>.lock, keyed by the real path of the team repo's sessions/. It stays out of the team repo because there the archive commit could stage it and pull --all's directory renames could move it while held. It covers one machine only. Two machines that both wrote _index.json diverge there: SyncManager.gitPull (also the retry inside gitPush) merges and returns the conflicted indexes, and the command caller rebuilds them and commits the merge (commitRebuiltIndexes) under the lock, with the network pull and push outside it. When the lock stays busy the merge is left in progress. The next teamai session pull finishes it first, then runs its own pull. Every session writer to the teamai-reports checkout (session save --push, session push, session migrate --push, the merge and index rebuild of session pull, and workflow publish --sessions) goes through updateReports, under the reports lock, and the commands that only read it (session list, session search, session resume, and score --team-session) open it through readableReportsWorktree.

  • Session publication lock. session push, session migrate --push, and session pull take withSessionPublicationLock around archive mutation, Git commit, pull/merge, and push. It is keyed by the archive checkout real path under ~/.teamai/locks/session-publish-<hash>.lock; a contender waits up to ten seconds before reporting busy. Lock order is publication, then index. Network operations hold publication ownership but never the index lock. A save stages both files, keeps existing files as renameable backups, and restores/removes the pair if the atomic index write fails. Index repair happens before installing the pair so a rebuild cannot adopt a partial save. A rollback failure retains and names the backups.

Tests

Test ids through an adapter's writeSession under a temporary home, as src/__tests__/session-target-id.test.ts does. Test copies through AgentSession.copy, as the contract suite in src/__tests__/agent-session-copy.test.ts does. Test reads through AgentSession.find, as src/__tests__/agent-session-find.test.ts does, and listings through AgentSession.list, as src/__tests__/agent-session-list.test.ts does. The copy, id and find suites run against WRITABLE in src/__tests__/helpers/writable-platforms.ts, which reads the adapter registry, so a new adapter is covered without being added by hand. A case that does not apply to a platform filters it out next to the case, with the reason.