fix: support twenty-member deterministic bootstrap

This commit is contained in:
777genius 2026-05-19 20:00:59 +03:00
parent bf3011624d
commit e22589f6c4
6 changed files with 180 additions and 53 deletions

View file

@ -95,6 +95,9 @@ export CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH=/Users/belief/dev/projects/claud
The source launcher executes `src/entrypoints/cli.tsx` through Bun. It is the right default for local
debug loops, live model/provider checks, and cross-repo runtime fixes.
Source-mode teammate startup can be slower than bundled startup, so live smoke harnesses may set
`CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS` to a larger value when they are validating source
behavior instead of watchdog latency.
Release or production-like smoke checks must validate the built wrapper:

View file

@ -1840,7 +1840,7 @@ function mergeProvisioningWarnings(
}
const DETERMINISTIC_BOOTSTRAP_LARGE_TEAM_WARNING_THRESHOLD = 8;
const DETERMINISTIC_BOOTSTRAP_MAX_PRIMARY_MEMBERS = 16;
const DETERMINISTIC_BOOTSTRAP_MAX_PRIMARY_MEMBERS = 20;
function buildLargeDeterministicBootstrapWarning(memberCount: number): string | null {
if (memberCount <= DETERMINISTIC_BOOTSTRAP_LARGE_TEAM_WARNING_THRESHOLD) {
@ -4552,7 +4552,7 @@ interface RuntimeBootstrapSpec {
const DETERMINISTIC_BOOTSTRAP_MIN_TIMEOUT_MS = 120_000;
const DETERMINISTIC_BOOTSTRAP_TIMEOUT_PER_MEMBER_MS = 60_000;
const DETERMINISTIC_BOOTSTRAP_MAX_TIMEOUT_MS = 300_000;
const DETERMINISTIC_BOOTSTRAP_MAX_TIMEOUT_MS = 600_000;
const DETERMINISTIC_BOOTSTRAP_OUTER_TIMEOUT_GRACE_MS = 30_000;
function getDeterministicBootstrapTimeoutMs(memberCount: number): number {

View file

@ -31,6 +31,10 @@ export interface NativeAppManagedBootstrapBuildResult {
const MAX_NATIVE_BOOTSTRAP_BRIEFING_CHARS = 18_000;
const MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS = 24_000;
export const MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS = 256_000;
const MIN_NATIVE_BOOTSTRAP_CONTEXT_CHARS = 6_000;
const NATIVE_BOOTSTRAP_COMPACT_ROSTER_MEMBER_COUNT = 10;
const NATIVE_BOOTSTRAP_SPEC_JSON_OVERHEAD_MIN_CHARS = 64_000;
const NATIVE_BOOTSTRAP_SPEC_JSON_OVERHEAD_PER_MEMBER_CHARS = 4_000;
const NATIVE_BOOTSTRAP_LARGE_ROSTER_MEMBER_COUNT = 7;
const NATIVE_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85;
@ -72,6 +76,7 @@ function buildContextText(params: {
providerId?: TeamProviderId;
cwd: string;
briefing: string;
maxContextChars?: number;
}): string {
const briefing = boundText(
redactNativeBootstrapContextText(params.briefing),
@ -90,7 +95,39 @@ function buildContextText(params: {
'</member_briefing_context_data>',
'</agent_teams_native_bootstrap_context>',
].join('\n'),
MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS
params.maxContextChars ?? MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS
);
}
function shouldUseCompactNativeBootstrapContext(nativeMemberCount: number): boolean {
if (nativeMemberCount >= NATIVE_BOOTSTRAP_COMPACT_ROSTER_MEMBER_COUNT) {
return true;
}
const reservedSpecOverhead = Math.max(
NATIVE_BOOTSTRAP_SPEC_JSON_OVERHEAD_MIN_CHARS,
nativeMemberCount * NATIVE_BOOTSTRAP_SPEC_JSON_OVERHEAD_PER_MEMBER_CHARS
);
return (
nativeMemberCount * MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS >
MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS - reservedSpecOverhead
);
}
function getNativeBootstrapContextCharLimit(nativeMemberCount: number): number {
if (!shouldUseCompactNativeBootstrapContext(nativeMemberCount)) {
return MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS;
}
const reservedSpecOverhead = Math.max(
NATIVE_BOOTSTRAP_SPEC_JSON_OVERHEAD_MIN_CHARS,
nativeMemberCount * NATIVE_BOOTSTRAP_SPEC_JSON_OVERHEAD_PER_MEMBER_CHARS
);
const aggregateContextBudget = Math.max(
MIN_NATIVE_BOOTSTRAP_CONTEXT_CHARS * nativeMemberCount,
MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS - reservedSpecOverhead
);
return Math.min(
MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS,
Math.floor(aggregateContextBudget / Math.max(1, nativeMemberCount))
);
}
@ -162,6 +199,63 @@ function buildLocalNativeMemberBriefing(params: {
.join('\n');
}
function formatCompactField(label: string, value: string | undefined, maxChars: number): string {
const trimmed = value?.trim();
if (!trimmed) {
return '';
}
return `${label}: ${boundText(redactNativeBootstrapContextText(trimmed), maxChars)}`;
}
async function readCompactTaskBriefing(params: {
controller: ReturnType<typeof createController>;
memberName: string;
}): Promise<string> {
try {
return String(await params.controller.tasks.taskBriefing(params.memberName));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `Task briefing unavailable during startup context generation: ${message}`;
}
}
function buildCompactNativeMemberBriefing(params: {
teamName: string;
cwd: string;
providerId?: TeamProviderId;
member: TeamCreateRequest['members'][number];
taskBriefing: string;
maxContextChars: number;
}): string {
const member = params.member;
const taskBriefingLimit = Math.max(1_200, Math.floor(params.maxContextChars * 0.55));
return [
`You are ${member.name}, a teammate in team ${params.teamName}.`,
`Provider: ${params.providerId ?? 'anthropic'}`,
`Project: ${member.cwd?.trim() || params.cwd}`,
formatCompactField('Role', member.role, 700),
formatCompactField('Workflow', member.workflow, 1_200),
formatCompactField('Model', member.model, 300),
formatCompactField('Effort', member.effort, 100),
'',
'The app loaded compact startup context for a large native team.',
'',
'Startup rules:',
'- This bootstrap turn is private. Reply locally once, then stop and wait for real work.',
'- Do not call member_briefing for launch readiness in this flow.',
'- Do not send messages to the user, lead, or teammates during bootstrap.',
'- Before real task work, use task_briefing as your working queue.',
'- Start real work only from an assigned task or a direct app-delivered instruction.',
'- Post durable task results as task comments before completing tasks.',
'- Ask the lead when blocked instead of guessing.',
'',
'Current task briefing:',
boundText(redactNativeBootstrapContextText(params.taskBriefing), taskBriefingLimit),
]
.filter((line) => line.length > 0)
.join('\n');
}
export async function buildNativeAppManagedBootstrapSpecs(params: {
teamName: string;
cwd: string;
@ -182,39 +276,52 @@ export async function buildNativeAppManagedBootstrapSpecsWithDiagnostics(params:
});
const result = new Map<string, NativeAppManagedBootstrapSpec>();
let totalContextChars = 0;
let nativeMemberCount = 0;
for (const member of params.members) {
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? 'anthropic';
if (!isNativeAppManagedBootstrapProvider(providerId)) {
continue;
}
nativeMemberCount += 1;
const nativeMembers = params.members
.map((member) => ({
member,
providerId: normalizeOptionalTeamProviderId(member.providerId) ?? 'anthropic',
}))
.filter(({ providerId }) => isNativeAppManagedBootstrapProvider(providerId));
const nativeMemberCount = nativeMembers.length;
const compactContext = shouldUseCompactNativeBootstrapContext(nativeMemberCount);
const maxContextChars = getNativeBootstrapContextCharLimit(nativeMemberCount);
for (const { member, providerId } of nativeMembers) {
let briefing: string;
try {
briefing = String(
await controller.tasks.memberBriefing(member.name, {
runtimeProvider: 'native',
includeActiveProcesses: false,
})
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Member not found in team metadata or inboxes')) {
throw error;
}
// In createTeam, the orchestrator's canonical config/inboxes may not
// exist until after the lead process runs. Fail-closed would break team
// creation, so use bounded request metadata while keeping readiness tied
// to the private bootstrap proof, never to this context load.
briefing = buildLocalNativeMemberBriefing({
if (compactContext) {
briefing = buildCompactNativeMemberBriefing({
teamName: params.teamName,
cwd: params.cwd,
providerId,
member,
unavailableReason: message,
taskBriefing: await readCompactTaskBriefing({ controller, memberName: member.name }),
maxContextChars,
});
} else {
try {
briefing = String(
await controller.tasks.memberBriefing(member.name, {
runtimeProvider: 'native',
includeActiveProcesses: false,
})
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('Member not found in team metadata or inboxes')) {
throw error;
}
// In createTeam, the orchestrator's canonical config/inboxes may not
// exist until after the lead process runs. Fail-closed would break team
// creation, so use bounded request metadata while keeping readiness tied
// to the private bootstrap proof, never to this context load.
briefing = buildLocalNativeMemberBriefing({
teamName: params.teamName,
cwd: params.cwd,
providerId,
member,
unavailableReason: message,
});
}
}
const boundedBriefing = boundText(
redactNativeBootstrapContextText(briefing),
@ -229,6 +336,7 @@ export async function buildNativeAppManagedBootstrapSpecsWithDiagnostics(params:
providerId,
cwd: member.cwd?.trim() || params.cwd,
briefing: boundedBriefing,
maxContextChars,
});
totalContextChars += contextText.length;
if (totalContextChars > MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS) {

View file

@ -55,6 +55,7 @@ liveDescribe('Anthropic launch selection live e2e', () => {
let previousAnthropicAuthToken: string | undefined;
let previousDisableAppBootstrap: string | undefined;
let previousDisableRuntimeBootstrap: string | undefined;
let previousRuntimeReadyTimeout: string | undefined;
let previousClaudeJsonConfig: string | null | undefined;
let svc: TeamProvisioningService | null;
let teamName: string | null;
@ -98,10 +99,13 @@ liveDescribe('Anthropic launch selection live e2e', () => {
previousAnthropicAuthToken = process.env.ANTHROPIC_AUTH_TOKEN;
previousDisableAppBootstrap = process.env.CLAUDE_APP_DISABLE_DETERMINISTIC_TEAM_BOOTSTRAP;
previousDisableRuntimeBootstrap = process.env.CLAUDE_DISABLE_DETERMINISTIC_TEAM_BOOTSTRAP;
previousRuntimeReadyTimeout = process.env.CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS;
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_PROCESS_RUNTIME_READY_TIMEOUT_MS =
process.env.CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS?.trim() || '90000';
process.env.HOME = subscriptionAuth ? os.userInfo().homedir : tempHome;
process.env.USERPROFILE = subscriptionAuth ? os.userInfo().homedir : tempHome;
process.env.NODE_ENV = 'production';
@ -117,6 +121,7 @@ liveDescribe('Anthropic launch selection live e2e', () => {
});
afterEach(async () => {
const preserveArtifacts = process.env.ANTHROPIC_LAUNCH_SELECTION_KEEP_TEMP === '1';
const beforeStopSnapshot = svc && teamName ? await safeRuntimeSnapshot(svc, teamName) : null;
if (svc && teamName) {
await svc.stopTeam(teamName).catch(() => undefined);
@ -125,10 +130,10 @@ liveDescribe('Anthropic launch selection live e2e', () => {
const afterStopSnapshot = svc && teamName ? await safeRuntimeSnapshot(svc, teamName) : null;
await terminateSmokeOwnedProcessBackends(afterStopSnapshot);
if (subscriptionAuth && projectPath) {
if (!preserveArtifacts && subscriptionAuth && projectPath) {
await removeClaudeProjectArtifacts(tempClaudeRoot, projectPath);
}
if (subscriptionAuth && teamName) {
if (!preserveArtifacts && subscriptionAuth && teamName) {
await removeTeamArtifacts(teamName);
}
if (subscriptionAuth && previousClaudeJsonConfig !== undefined) {
@ -145,25 +150,26 @@ liveDescribe('Anthropic launch selection live e2e', () => {
restoreEnv('ANTHROPIC_AUTH_TOKEN', previousAnthropicAuthToken);
restoreEnv('CLAUDE_APP_DISABLE_DETERMINISTIC_TEAM_BOOTSTRAP', previousDisableAppBootstrap);
restoreEnv('CLAUDE_DISABLE_DETERMINISTIC_TEAM_BOOTSTRAP', previousDisableRuntimeBootstrap);
restoreEnv('CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS', previousRuntimeReadyTimeout);
if (process.env.ANTHROPIC_LAUNCH_SELECTION_KEEP_TEMP === '1') {
if (preserveArtifacts) {
process.stderr.write(`[AnthropicLaunchSelection.live] preserved temp dir: ${tempDir}\n`);
} else {
await removeTempDirWithRetries(tempDir);
}
if (subscriptionAuth && projectPath) {
if (!preserveArtifacts && subscriptionAuth && projectPath) {
await removeClaudeProjectArtifacts(tempClaudeRoot, projectPath);
}
if (subscriptionAuth && teamName) {
if (!preserveArtifacts && subscriptionAuth && teamName) {
await removeTeamArtifacts(teamName);
}
if (subscriptionAuth && (projectPath || teamName)) {
if (!preserveArtifacts && subscriptionAuth && (projectPath || teamName)) {
await new Promise((resolve) => setTimeout(resolve, 10_000));
}
if (subscriptionAuth && projectPath) {
if (!preserveArtifacts && subscriptionAuth && projectPath) {
await removeClaudeProjectArtifacts(tempClaudeRoot, projectPath);
}
if (subscriptionAuth && teamName) {
if (!preserveArtifacts && subscriptionAuth && teamName) {
await removeTeamArtifacts(teamName);
}
discardKnownAnthropicLaunchSelectionWarnings();

View file

@ -1,7 +1,6 @@
import { mkdtemp, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
@ -135,7 +134,7 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
expect(result.diagnostics.warning).toMatch(/Large native team startup context/);
});
it('fails closed when aggregate native context budget is exceeded', async () => {
it('compacts twenty large native contexts within the aggregate budget', async () => {
const hugeRole = 'x'.repeat(40_000);
await new TeamMetaStore().writeMeta('large-native-team', {
cwd: '/tmp/workspace',
@ -145,23 +144,34 @@ describe('NativeAppManagedBootstrapContextBuilder', () => {
});
await new TeamMembersMetaStore().writeMembers(
'large-native-team',
Array.from({ length: 16 }, (_, index) => ({
Array.from({ length: 20 }, (_, index) => ({
name: `member-${index}`,
providerId: 'anthropic' as const,
role: hugeRole,
}))
);
await expect(
buildNativeAppManagedBootstrapSpecs({
teamName: 'large-native-team',
cwd: '/tmp/workspace',
members: Array.from({ length: 16 }, (_, index) => ({
name: `member-${index}`,
providerId: 'anthropic' as const,
role: hugeRole,
})),
})
).rejects.toThrow(/aggregate size budget/);
const result = await buildNativeAppManagedBootstrapSpecsWithDiagnostics({
teamName: 'large-native-team',
cwd: '/tmp/workspace',
members: Array.from({ length: 20 }, (_, index) => ({
name: `member-${index}`,
providerId: 'anthropic' as const,
role: hugeRole,
})),
});
const totalContextChars = [...result.specs.values()].reduce(
(sum, spec) => sum + spec.contextText.length,
0
);
const firstContext = result.specs.get('member-0')?.contextText ?? '';
expect(result.specs.size).toBe(20);
expect(totalContextChars).toBeLessThanOrEqual(MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS);
expect(firstContext).toContain('The app loaded compact startup context');
expect(firstContext).toContain('Startup rules:');
expect(firstContext).toContain('Current task briefing:');
expect(firstContext).toContain('[truncated native bootstrap context]');
});
});

View file

@ -18279,14 +18279,14 @@ describe('TeamProvisioningService', () => {
it('fails before spawning when deterministic launch exceeds the current primary teammate cap', async () => {
allowConsoleLogs();
const members = Array.from({ length: 17 }, (_, index) => `member-${index + 1}`);
const members = Array.from({ length: 21 }, (_, index) => `member-${index + 1}`);
await expect(
startDeterministicLaunchCloseHarness({
teamName: 'launch-too-many-primary-members',
members,
})
).rejects.toThrow(/up to 16 primary teammates/);
).rejects.toThrow(/up to 20 primary teammates/);
expect(spawnCli).not.toHaveBeenCalled();
});