Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/dev/agent-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Each rule below spans more than one file. The rule names the code that owns it,
- **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, the step starts a fresh session. The workflow engine never copies a session. A step's report carries the tool and that tool's native session id.
- **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.
- **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. On a git-kind team, the first restore 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 restore in that run, hit or miss, reads that clone, so a session archived again under the same id restores as origin last archived it, 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 the claim has just synced the reports checkout, 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 session commands that only read it (`list`, `search`, `resume`) 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.

Expand Down
Loading
Loading