fix(team-mcp): route agent teams mcp to app state
This commit is contained in:
parent
bfd8b30ad5
commit
10188109eb
4 changed files with 34 additions and 5 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Member Work Sync Control Plane Plan
|
||||
|
||||
**Status:** Phase 1, Phase 1.5 observability, minimal read-only member details surface, and readiness-gated Phase 2 nudge outbox/dispatcher/scheduler implemented
|
||||
**Status:** Phase 1, Phase 1.5 observability, minimal read-only member details surface, and opt-in Phase 2 nudge outbox/dispatcher/scheduler implemented
|
||||
**Scope:** Team management, task work synchronization, agent work coordination
|
||||
**Primary repo:** `claude_team`
|
||||
**Secondary write-boundary repo:** `agent_teams_orchestrator` / `agent-teams-controller`
|
||||
|
|
@ -36,9 +36,10 @@ Current implementation note:
|
|||
- Phase 1.5 exposes a machine-readable `phase2Readiness` assessment from shadow metrics. It can say `collecting_shadow_data`, `blocked`, or `shadow_ready`; it still does not dispatch nudges.
|
||||
- Phase 2 storage foundation is implemented as a durable outbox: idempotency key, payload hash conflict checks, claim generation fencing, retry/terminal states.
|
||||
- Queue reconciles can plan a Phase 2 outbox item only when `phase2Readiness=shadow_ready`; read-only diagnostics never create outbox intents. This preserves the anti-spam gate and keeps UI/status reads passive.
|
||||
- Dispatcher use case runs after queued reconcile and is also exposed through the facade. It claims due outbox rows, revalidates active team/status/current fingerprint/readiness/busy/watchdog cooldown, then writes one idempotent inbox nudge through a narrow port.
|
||||
- Phase 2 nudge side effects are additionally disabled by default in production composition. Set `CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED=1` only for isolated live validation. This keeps status/report/metrics active while guaranteeing that shadow-ready metrics cannot start inbox nudges by accident.
|
||||
- Dispatcher use case can run after queued reconcile and is also exposed through the facade when nudge side effects are explicitly enabled. It claims due outbox rows, revalidates active team/status/current fingerprint/readiness/busy/watchdog cooldown, then writes one idempotent inbox nudge through a narrow port.
|
||||
- Production busy revalidation is wired through a tool-activity busy signal adapter. Active or recently finished tool calls defer Phase 2 nudges instead of interrupting work.
|
||||
- A feature-owned dispatch scheduler wakes due retryable outbox items for lifecycle-active teams only. It is bounded, unref'ed, and still relies on dispatcher revalidation before any inbox write.
|
||||
- A feature-owned dispatch scheduler wakes due retryable outbox items for lifecycle-active teams only when nudge side effects are enabled. It is bounded, unref'ed, and still relies on dispatcher revalidation before any inbox write.
|
||||
- Dispatcher applies per-member hourly rate limiting and bounded deterministic retry backoff with jitter before retrying failed nudge attempts.
|
||||
- Superseded-but-undelivered outbox items can be revived by a fresh queued reconcile for the same agenda fingerprint. Delivered nudges remain one-per-fingerprint.
|
||||
- Phase 2 dispatch stays blocked until real shadow metrics confirm that `needs_sync` churn and false positives are acceptably low.
|
||||
|
|
@ -1363,7 +1364,7 @@ Expired leases are ignored by `SyncDecisionPolicy`.
|
|||
|
||||
### 10.4 Shadow Would-Nudge Semantics
|
||||
|
||||
Phase 1 may compute `wouldNudgeCount`, but must not enqueue or send.
|
||||
Phase 1 may compute `wouldNudgeCount`, but must not enqueue or send. Production composition enforces this by default by not wiring `outboxStore`/`inboxNudge` unless `CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED=1`.
|
||||
|
||||
`wouldNudge` is true only when all are true:
|
||||
|
||||
|
|
|
|||
|
|
@ -24556,7 +24556,11 @@ export class TeamProvisioningService {
|
|||
throwIfCancelled();
|
||||
child = spawnCli(launchSpec.command, launchSpec.args, {
|
||||
cwd: launchSpec.cwd ?? cwd,
|
||||
env: { ...env, ...launchSpec.env },
|
||||
env: {
|
||||
...env,
|
||||
...launchSpec.env,
|
||||
AGENT_TEAMS_MCP_CLAUDE_DIR: fixture.claudeDir,
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ liveDescribe('Member work sync Codex live e2e', () => {
|
|||
let previousCliFlavor: string | undefined;
|
||||
let previousControlUrl: string | undefined;
|
||||
let previousNudgeFlag: string | undefined;
|
||||
let previousCodexHome: string | undefined;
|
||||
let codexHomeDir: string;
|
||||
let svc: {
|
||||
stopTeam(teamName: string): Promise<unknown>;
|
||||
isTeamAlive(teamName: string): boolean;
|
||||
|
|
@ -84,11 +86,17 @@ liveDescribe('Member work sync Codex live e2e', () => {
|
|||
previousCliFlavor = process.env.CLAUDE_TEAM_CLI_FLAVOR;
|
||||
previousControlUrl = process.env.CLAUDE_TEAM_CONTROL_URL;
|
||||
previousNudgeFlag = process.env.CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED;
|
||||
previousCodexHome = process.env.CODEX_HOME;
|
||||
|
||||
const codexHomeRoot = path.resolve('temp', 'member-work-sync-codex-live');
|
||||
await fs.mkdir(codexHomeRoot, { recursive: true });
|
||||
codexHomeDir = await fs.mkdtemp(path.join(codexHomeRoot, 'codex-home-'));
|
||||
|
||||
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH =
|
||||
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() || DEFAULT_ORCHESTRATOR_CLI;
|
||||
process.env.CLAUDE_TEAM_CLI_FLAVOR = 'agent_teams_orchestrator';
|
||||
process.env.CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED = '0';
|
||||
process.env.CODEX_HOME = codexHomeDir;
|
||||
|
||||
svc = null;
|
||||
feature = null;
|
||||
|
|
@ -108,11 +116,14 @@ liveDescribe('Member work sync Codex live e2e', () => {
|
|||
restoreEnv('CLAUDE_TEAM_CLI_FLAVOR', previousCliFlavor);
|
||||
restoreEnv('CLAUDE_TEAM_CONTROL_URL', previousControlUrl);
|
||||
restoreEnv('CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED', previousNudgeFlag);
|
||||
restoreEnv('CODEX_HOME', previousCodexHome);
|
||||
setClaudeBasePathOverride(null);
|
||||
if (process.env.MEMBER_WORK_SYNC_CODEX_KEEP_TEMP === '1') {
|
||||
console.info(`[MemberWorkSyncCodex.live] preserved temp dir: ${tempDir}`);
|
||||
console.info(`[MemberWorkSyncCodex.live] preserved CODEX_HOME: ${codexHomeDir}`);
|
||||
} else {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
await fs.rm(codexHomeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -240,6 +240,19 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
expectTsxEntry(server, sourceEntry);
|
||||
});
|
||||
|
||||
it('pins the MCP controller to the active Claude base path', async () => {
|
||||
const claudeDir = path.join(tempAppData, 'custom-claude-root');
|
||||
setClaudeBasePathOverride(claudeDir);
|
||||
mockSourceWorkspaceEntryAvailable();
|
||||
const builder = new TeamMcpConfigBuilder();
|
||||
|
||||
const configPath = await builder.writeConfigFile();
|
||||
createdPaths.push(configPath);
|
||||
|
||||
const server = readGeneratedServer(configPath);
|
||||
expect(server?.env?.AGENT_TEAMS_MCP_CLAUDE_DIR).toBe(claudeDir);
|
||||
});
|
||||
|
||||
it('falls back to the built workspace MCP entry when source execution is unavailable', async () => {
|
||||
const builtEntry = mockBuiltWorkspaceEntryAvailable();
|
||||
const builder = new TeamMcpConfigBuilder();
|
||||
|
|
|
|||
Loading…
Reference in a new issue