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
|
# 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
|
**Scope:** Team management, task work synchronization, agent work coordination
|
||||||
**Primary repo:** `claude_team`
|
**Primary repo:** `claude_team`
|
||||||
**Secondary write-boundary repo:** `agent_teams_orchestrator` / `agent-teams-controller`
|
**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 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
### 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:
|
`wouldNudge` is true only when all are true:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24556,7 +24556,11 @@ export class TeamProvisioningService {
|
||||||
throwIfCancelled();
|
throwIfCancelled();
|
||||||
child = spawnCli(launchSpec.command, launchSpec.args, {
|
child = spawnCli(launchSpec.command, launchSpec.args, {
|
||||||
cwd: launchSpec.cwd ?? cwd,
|
cwd: launchSpec.cwd ?? cwd,
|
||||||
env: { ...env, ...launchSpec.env },
|
env: {
|
||||||
|
...env,
|
||||||
|
...launchSpec.env,
|
||||||
|
AGENT_TEAMS_MCP_CLAUDE_DIR: fixture.claudeDir,
|
||||||
|
},
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ liveDescribe('Member work sync Codex live e2e', () => {
|
||||||
let previousCliFlavor: string | undefined;
|
let previousCliFlavor: string | undefined;
|
||||||
let previousControlUrl: string | undefined;
|
let previousControlUrl: string | undefined;
|
||||||
let previousNudgeFlag: string | undefined;
|
let previousNudgeFlag: string | undefined;
|
||||||
|
let previousCodexHome: string | undefined;
|
||||||
|
let codexHomeDir: string;
|
||||||
let svc: {
|
let svc: {
|
||||||
stopTeam(teamName: string): Promise<unknown>;
|
stopTeam(teamName: string): Promise<unknown>;
|
||||||
isTeamAlive(teamName: string): boolean;
|
isTeamAlive(teamName: string): boolean;
|
||||||
|
|
@ -84,11 +86,17 @@ liveDescribe('Member work sync Codex live e2e', () => {
|
||||||
previousCliFlavor = process.env.CLAUDE_TEAM_CLI_FLAVOR;
|
previousCliFlavor = process.env.CLAUDE_TEAM_CLI_FLAVOR;
|
||||||
previousControlUrl = process.env.CLAUDE_TEAM_CONTROL_URL;
|
previousControlUrl = process.env.CLAUDE_TEAM_CONTROL_URL;
|
||||||
previousNudgeFlag = process.env.CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED;
|
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 =
|
||||||
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() || DEFAULT_ORCHESTRATOR_CLI;
|
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_CLI_FLAVOR = 'agent_teams_orchestrator';
|
||||||
process.env.CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED = '0';
|
process.env.CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED = '0';
|
||||||
|
process.env.CODEX_HOME = codexHomeDir;
|
||||||
|
|
||||||
svc = null;
|
svc = null;
|
||||||
feature = null;
|
feature = null;
|
||||||
|
|
@ -108,11 +116,14 @@ liveDescribe('Member work sync Codex live e2e', () => {
|
||||||
restoreEnv('CLAUDE_TEAM_CLI_FLAVOR', previousCliFlavor);
|
restoreEnv('CLAUDE_TEAM_CLI_FLAVOR', previousCliFlavor);
|
||||||
restoreEnv('CLAUDE_TEAM_CONTROL_URL', previousControlUrl);
|
restoreEnv('CLAUDE_TEAM_CONTROL_URL', previousControlUrl);
|
||||||
restoreEnv('CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED', previousNudgeFlag);
|
restoreEnv('CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED', previousNudgeFlag);
|
||||||
|
restoreEnv('CODEX_HOME', previousCodexHome);
|
||||||
setClaudeBasePathOverride(null);
|
setClaudeBasePathOverride(null);
|
||||||
if (process.env.MEMBER_WORK_SYNC_CODEX_KEEP_TEMP === '1') {
|
if (process.env.MEMBER_WORK_SYNC_CODEX_KEEP_TEMP === '1') {
|
||||||
console.info(`[MemberWorkSyncCodex.live] preserved temp dir: ${tempDir}`);
|
console.info(`[MemberWorkSyncCodex.live] preserved temp dir: ${tempDir}`);
|
||||||
|
console.info(`[MemberWorkSyncCodex.live] preserved CODEX_HOME: ${codexHomeDir}`);
|
||||||
} else {
|
} else {
|
||||||
await fs.rm(tempDir, { recursive: true, force: true });
|
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);
|
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 () => {
|
it('falls back to the built workspace MCP entry when source execution is unavailable', async () => {
|
||||||
const builtEntry = mockBuiltWorkspaceEntryAvailable();
|
const builtEntry = mockBuiltWorkspaceEntryAvailable();
|
||||||
const builder = new TeamMcpConfigBuilder();
|
const builder = new TeamMcpConfigBuilder();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue