Skip to content

Claude Agent Teams: Refactor sessions to support multi-repository (0..N repos)#950

Open
Connoropolous wants to merge 5 commits intomainfrom
claude-multi-repo-teams
Open

Claude Agent Teams: Refactor sessions to support multi-repository (0..N repos)#950
Connoropolous wants to merge 5 commits intomainfrom
claude-multi-repo-teams

Conversation

@Connoropolous
Copy link
Copy Markdown
Contributor

Summary

  • CyrusAgentSession now carries repositoryIds: string[] directly — supporting 0, 1, or N repositories per session
  • EdgeWorker consolidates from a per-repo Map<string, AgentSessionManager> to a single global agentSessionManager with an ActivitySinkResolver callback
  • RepositoryRouter returns repositories: RepositoryConfig[] (plural); issueRepositoryCache, getCachedRepository, restoreIssueRepositoryCache removed entirely
  • PersistenceManager introduces v4.0 flat format with migration from v2.0→v4.0 and v3.0→v4.0
  • All handler signatures, event emissions, and method params updated from singular to plural

Guided Tour

Layer 1: Data Model (packages/core)

Start here — these define the new shape of everything.

  1. CyrusAgentSession.tsrepositoryIds: string[] added, issueId removed
  2. config-types.ts — Handler callbacks: repositoryrepositories, repositoryIdrepositoryIds
  3. PersistenceManager.ts — New SerializableEdgeWorkerState (flat), v2→v4 and v3→v4 migration logic

Layer 2: Architecture (packages/edge-worker/src)

These are the structural changes that make multi-repo possible.

  1. AgentSessionManager.ts — New ActivitySinkResolver type; constructor takes resolver callback instead of fixed sink; createLinearAgentSession/createChatSession accept repositoryIds
  2. RepositoryRouter.tsRepositoryRoutingResult.repositories (plural); selectRepositoriesFromResponse; cache removed
  3. types.tsEdgeWorkerEvents emit repositoryIds: string[]
  4. prompt-assembly/types.tsPromptAssemblyInput.repositories (was singular)

Layer 3: EdgeWorker (the big one)

  1. EdgeWorker.ts — Single agentSessionManager created in constructor with resolver; resolveRepositories() helper; findSessionByIssueId() replaces cache lookups; all webhook handlers, serialization, and event emissions updated

Layer 4: Service Methods (mechanical wrapping)

  1. PromptBuilder.ts — 6 methods: repositoryrepositories, extract [0]! internally
  2. RunnerSelectionService.tsbuildAllowedTools/buildDisallowedTools: same pattern
  3. ActivityPoster.ts — 6 methods: repositoryIdrepositoryIds
  4. GitService.tscreateGitWorktree: repositoryrepositories

Layer 5: Consumers

  1. ChatSessionHandler.ts — Passes () => noopSink and [] for repositoryIds
  2. WorkerService.ts (CLI) — Handler signatures and event log messages updated

Layer 6: Tests (17 files)

  1. PersistenceManager.migration.test.ts — 9 tests covering v2→v4, v3→v4, v4 direct, error cases
  2. RepositoryRouter.test.ts — Cache tests removed, assertions updated for plural
  3. AgentSessionManager.*.test.ts (5 files) — Constructor and method signature updates
  4. EdgeWorker.*.test.ts (11 files) — Singular manager mock, removed cache references, issueIdissueContext
  5. GitService.test.ts — Array wrapping

Eliminated Patterns (grep-verified zero matches in src/)

  • agentSessionManagers (old per-repo Map)
  • getCachedRepository / getIssueRepositoryCache / restoreIssueRepositoryCache
  • selectRepositoryFromResponse (singular)
  • routingResult.repository (singular)
  • session.issueId (deprecated field)

Test plan

  • pnpm build — 15/15 packages clean
  • pnpm typecheck — 15/15 packages clean
  • pnpm --filter cyrus-core test:run — 44/44 tests pass
  • pnpm --filter cyrus-edge-worker test:run — 536/536 test assertions pass (26 pre-existing EACCES uncaught exceptions from /tmp/test-cyrus-home/logs/ owned by agentops — identical on main)
  • pnpm test:packages:run — all package test assertions pass (gemini-runner has same pre-existing EACCES issue)
  • F1 test drive to validate end-to-end

🤖 Generated with Claude Code

Sessions now carry their own `repositoryIds: string[]` instead of being
nested under a single repository key. This eliminates the per-repo
AgentSessionManager map in favor of a single global instance with an
ActivitySinkResolver callback, removes the issueRepositoryCache from
RepositoryRouter, and introduces a v4.0 persistence format with
migration from v2.0 and v3.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cursor
Copy link
Copy Markdown

cursor bot commented Mar 8, 2026

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on April 3.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@Connoropolous
Copy link
Copy Markdown
Contributor Author

Guided Tour

Read in this order. Each section builds on the previous.


Stop 1: The New Data Model

packages/core/src/CyrusAgentSession.ts

The single most important change. CyrusAgentSession gains:

repositoryIds: string[]  // 0 = standalone, 1 = standard, N = multi-repo

The deprecated issueId field is removed — use issueContext?.issueId instead.

packages/core/src/config-types.ts

Handler callbacks go plural:

  • createWorkspace(issue, repository)createWorkspace(issue, repositories)
  • onSessionStart(id, issue, repositoryId)onSessionStart(id, issue, repositoryIds)
  • Same for onSessionEnd, onClaudeMessage

Stop 2: Persistence & Migration

packages/core/src/PersistenceManager.ts

Before (v2/v3): Sessions nested under repo ID keys:

agentSessions[repoId][sessionId] = session

After (v4): Flat — sessions carry their own repo association:

agentSessions[sessionId] = { ...session, repositoryIds: [repoId] }

Migration functions: migrateV2ToV4(), migrateV3ToV4(). The issueRepositoryCache field is dropped entirely.

packages/core/test/PersistenceManager.migration.test.ts

9 tests proving both migration paths and edge cases.


Stop 3: The Architecture Pivot

packages/edge-worker/src/AgentSessionManager.ts

New exported type:

export type ActivitySinkResolver = (repositoryIds: string[]) => IActivitySink;

Constructor takes this resolver instead of a fixed sink. Methods createLinearAgentSession and createChatSession now accept repositoryIds: string[].

packages/edge-worker/src/RepositoryRouter.ts

The routing result changes from:

{ type: "selected", repository: repo }      // old
{ type: "selected", repositories: [repo] }  // new

Deleted: issueRepositoryCache, getCachedRepository(), restoreIssueRepositoryCache(), getIssueRepositoryCache().
Renamed: selectRepositoryFromResponseselectRepositoriesFromResponse.

packages/edge-worker/src/types.ts

All EdgeWorkerEvents now emit repositoryIds: string[] instead of repositoryId: string.


Stop 4: EdgeWorker.ts (the big diff)

packages/edge-worker/src/EdgeWorker.ts

Key structural changes:

  1. Line ~168: agentSessionManagers: MapagentSessionManager!: AgentSessionManager (singular)
  2. Lines ~309-340: Single manager created in constructor with ActivitySinkResolver that looks up issue trackers dynamically
  3. New helpers: resolveRepositories(repositoryIds) and findSessionByIssueId(issueId) replace cache lookups
  4. Webhook handlers: Use findSessionByIssueId + resolveRepositories instead of getCachedRepository
  5. Serialization: serializeMappings/restoreMappings use flat v4.0 format
  6. All call sites: Singular repository wrapped in [repository] when calling refactored methods; repositoryId wrapped in [repositoryId] for ActivityPoster calls

Stop 5: Service Methods (mechanical changes)

These all follow the same pattern: parameter goes from singular to array, first element extracted with ! assertion, all existing logic unchanged.

  • PromptBuilder.ts — 6 methods
  • RunnerSelectionService.ts — 2 methods
  • ActivityPoster.ts — 6 methods
  • GitService.ts — 1 method

Stop 6: Consumers

  • ChatSessionHandler.ts — Passes noop sink resolver and empty [] repositoryIds
  • apps/cli/src/services/WorkerService.ts — Handler signatures and event log messages

Stop 7: Tests (17 files)

Migration testsPersistenceManager.migration.test.ts (9 tests: v2→v4, v3→v4, direct v4, errors)

Router testsRepositoryRouter.test.ts (cache tests removed, assertions for repositories[0])

ASM tests (5 files) — Constructor: mockSink() => mockSink; method calls add [] repositoryIds

EdgeWorker tests (11 files) — Mocks updated from per-repo manager map to singular; removed getIssueRepositoryCache refs; session.issueIdsession.issueContext

GitService testcreateGitWorktree(issue, repo)createGitWorktree(issue, [repo])

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd6fd8695e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Connoropolous Connoropolous changed the title Refactor sessions to support multi-repository (0..N repos) Claude Agent Teams: Refactor sessions to support multi-repository (0..N repos) Mar 8, 2026
Connoropolous and others added 4 commits March 8, 2026 15:51
…onByIssueId searches all sessions

Addresses two valid review comments on PR #950:

P1: hasActiveSession callback now filters by repositoryIds.includes(repositoryId)
instead of returning true for any repo with an active session on the issue.

P2: findSessionByIssueId now uses getSessionsByIssueId (all statuses) with
active-session preference, preserving repository mapping after session completion.

Also adds getSessionsByIssueId mock to all 12 EdgeWorker test files and updates
the missing-session-recovery test assertion to match the new code path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves conflicts in prompt-assembly-utils.ts (repositories plural + default
fields) and missing-session-recovery test (attachmentsDir + array syntax).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…all sites

Leaf services now accept singular parameters instead of arrays:

- PromptBuilder: 6 methods changed from repositories[] to repository
- RunnerSelectionService: buildAllowedTools/buildDisallowedTools take repository
- GitService: createGitWorktree takes repository
- ActivityPoster: 6 methods changed from repositoryIds[] to repositoryId
- config-types: createWorkspace handler takes repository (singular)

EdgeWorker (orchestrator layer) updated:
- Removed all [repository] and [repositoryId] array wrappings at call sites
- createLinearAgentSession accepts repository + repositoryIds separately
- initializeAgentRunner accepts singular repository
- Event handlers use clear primaryRepository naming
- Router result extraction happens at proper orchestration boundaries

The remaining repositories[0] extractions in EdgeWorker are at legitimate
boundaries: router results, ActivitySinkResolver callback, and
PromptAssemblyInput data container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every repositories[0] extraction is replaced with proper multi-repo handling:

- getPrimaryRepository() helper: single authorized place for the [0] convention,
  documented as "used for workspace creation and git operations"
- ActivitySinkResolver: iterates all repositoryIds to find valid tracker
- Event handlers (subroutine, validation): pass full repositories array
- handleIssueUnassigned/ContentUpdate: resolve and pass all repos
- handleAgentSessionCreated: access check iterates ALL repos, selection
  activity shows ALL repo names, initializeAgentRunner receives full set
- initializeAgentRunner: accepts repositories[], maps all IDs to session,
  uses getPrimaryRepository() only for workspace creation
- handleRepositorySelectionResponse: passes full set through
- buildAllowedTools/buildDisallowedTools: merge tools from ALL repos
  via flatMap + Set (union strategy)
- postRepositorySelectionActivity: accepts repositories[], formats all
  names, iterates to find valid tracker for posting
- assemblePrompt: uses getPrimaryRepository() for label-based prompt

Result: ONE [0] in the entire edge-worker source (inside getPrimaryRepository).
Previously there were 6 in EdgeWorker.ts alone plus others in leaf services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant