diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index b5e29d32..19ce341e 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -171,6 +171,7 @@ import { } from './bootstrap/BootstrapProofValidation'; import { buildNativeAppManagedBootstrapSpecs, + buildNativeAppManagedBootstrapSpecsWithDiagnostics, type NativeAppManagedBootstrapSpec, } from './bootstrap/NativeAppManagedBootstrapContextBuilder'; import { @@ -18911,16 +18912,30 @@ export class TeamProvisioningService { 'Building deterministic create bootstrap spec', `expectedMembers=${effectiveMemberSpecs.length}` ); - const nativeAppManagedBootstrapByMember = await buildNativeAppManagedBootstrapSpecs({ + const nativeBootstrapBuild = await buildNativeAppManagedBootstrapSpecsWithDiagnostics({ teamName: request.teamName, cwd: request.cwd, members: effectiveMemberSpecs, }); + if (nativeBootstrapBuild.diagnostics.warning) { + run.progress = { + ...run.progress, + warnings: mergeProvisioningWarnings( + run.progress.warnings, + nativeBootstrapBuild.diagnostics.warning + ), + }; + emitProvisioningCheckpoint( + run, + 'Native bootstrap startup context is large', + nativeBootstrapBuild.diagnostics.warning + ); + } const bootstrapSpec = buildDeterministicCreateBootstrapSpec( runId, request, effectiveMemberSpecs, - nativeAppManagedBootstrapByMember + nativeBootstrapBuild.specs ); emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file'); bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec); @@ -20189,15 +20204,30 @@ export class TeamProvisioningService { 'Building deterministic launch bootstrap spec', `expectedMembers=${effectiveMemberSpecs.length}` ); + const nativeBootstrapBuild = await buildNativeAppManagedBootstrapSpecsWithDiagnostics({ + teamName: request.teamName, + cwd: request.cwd, + members: effectiveMemberSpecs, + }); + if (nativeBootstrapBuild.diagnostics.warning) { + run.progress = { + ...run.progress, + warnings: mergeProvisioningWarnings( + run.progress.warnings, + nativeBootstrapBuild.diagnostics.warning + ), + }; + emitProvisioningCheckpoint( + run, + 'Native bootstrap startup context is large', + nativeBootstrapBuild.diagnostics.warning + ); + } const bootstrapSpec = buildDeterministicLaunchBootstrapSpec( runId, request, effectiveMemberSpecs, - await buildNativeAppManagedBootstrapSpecs({ - teamName: request.teamName, - cwd: request.cwd, - members: effectiveMemberSpecs, - }) + nativeBootstrapBuild.specs ); emitProvisioningCheckpoint(run, 'Writing deterministic bootstrap spec file'); bootstrapSpecPath = await writeDeterministicBootstrapSpecFile(bootstrapSpec); diff --git a/src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder.ts b/src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder.ts index eb761ac3..3ffa0367 100644 --- a/src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder.ts +++ b/src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder.ts @@ -16,9 +16,23 @@ export interface NativeAppManagedBootstrapSpec { generatedAt: string; } +export interface NativeAppManagedBootstrapBuildDiagnostics { + nativeMemberCount: number; + totalContextChars: number; + totalContextLimitChars: number; + warning: string | null; +} + +export interface NativeAppManagedBootstrapBuildResult { + specs: Map; + diagnostics: NativeAppManagedBootstrapBuildDiagnostics; +} + const MAX_NATIVE_BOOTSTRAP_BRIEFING_CHARS = 18_000; const MAX_NATIVE_BOOTSTRAP_CONTEXT_CHARS = 24_000; -const MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS = 96_000; +export const MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS = 256_000; +const NATIVE_BOOTSTRAP_LARGE_ROSTER_MEMBER_COUNT = 7; +const NATIVE_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85; export function isNativeAppManagedBootstrapProvider(providerId?: TeamProviderId): boolean { return providerId == null || providerId === 'anthropic' || providerId === 'codex'; @@ -80,6 +94,45 @@ function buildContextText(params: { ); } +function formatCompactChars(value: number): string { + if (!Number.isFinite(value)) { + return 'unknown'; + } + if (value >= 1000) { + return `${Math.round(value / 1000)}k chars`; + } + return `${Math.max(0, Math.round(value))} chars`; +} + +function buildNativeBootstrapWarning(params: { + nativeMemberCount: number; + totalContextChars: number; + totalContextLimitChars: number; +}): string | null { + if (params.nativeMemberCount === 0) { + return null; + } + const ratio = + params.totalContextLimitChars > 0 + ? params.totalContextChars / params.totalContextLimitChars + : 0; + const isLargeNativeRoster = + params.nativeMemberCount >= NATIVE_BOOTSTRAP_LARGE_ROSTER_MEMBER_COUNT; + const isNearLimit = ratio >= NATIVE_BOOTSTRAP_NEAR_LIMIT_RATIO; + if (!isLargeNativeRoster && !isNearLimit) { + return null; + } + + const usage = `${formatCompactChars(params.totalContextChars)} / ${formatCompactChars( + params.totalContextLimitChars + )}`; + const percent = `${Math.round(ratio * 100)}%`; + const rosterHint = `${params.nativeMemberCount} native app-managed member${ + params.nativeMemberCount === 1 ? '' : 's' + }`; + return `Large native team startup context: ${usage} (${percent}) across ${rosterHint}. Launch can continue, but if bootstrap confirmation is slow or fails, reduce native member count or use OpenCode for secondary members.`; +} + function buildLocalNativeMemberBriefing(params: { teamName: string; cwd: string; @@ -114,6 +167,14 @@ export async function buildNativeAppManagedBootstrapSpecs(params: { cwd: string; members: TeamCreateRequest['members']; }): Promise> { + return (await buildNativeAppManagedBootstrapSpecsWithDiagnostics(params)).specs; +} + +export async function buildNativeAppManagedBootstrapSpecsWithDiagnostics(params: { + teamName: string; + cwd: string; + members: TeamCreateRequest['members']; +}): Promise { const controller = createController({ teamName: params.teamName, claudeDir: getClaudeBasePath(), @@ -121,12 +182,14 @@ export async function buildNativeAppManagedBootstrapSpecs(params: { }); const result = new Map(); let totalContextChars = 0; + let nativeMemberCount = 0; for (const member of params.members) { const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? 'anthropic'; if (!isNativeAppManagedBootstrapProvider(providerId)) { continue; } + nativeMemberCount += 1; let briefing: string; try { @@ -182,5 +245,16 @@ export async function buildNativeAppManagedBootstrapSpecs(params: { }); } - return result; + const diagnostics: NativeAppManagedBootstrapBuildDiagnostics = { + nativeMemberCount, + totalContextChars, + totalContextLimitChars: MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS, + warning: buildNativeBootstrapWarning({ + nativeMemberCount, + totalContextChars, + totalContextLimitChars: MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS, + }), + }; + + return { specs: result, diagnostics }; } diff --git a/test/main/services/team/NativeAppManagedBootstrapContextBuilder.test.ts b/test/main/services/team/NativeAppManagedBootstrapContextBuilder.test.ts index 7bb2abe4..51901da6 100644 --- a/test/main/services/team/NativeAppManagedBootstrapContextBuilder.test.ts +++ b/test/main/services/team/NativeAppManagedBootstrapContextBuilder.test.ts @@ -6,7 +6,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { buildNativeAppManagedBootstrapSpecs, + buildNativeAppManagedBootstrapSpecsWithDiagnostics, hashNativeBootstrapText, + MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS, } from '../../../../src/main/services/team/bootstrap/NativeAppManagedBootstrapContextBuilder'; import { TeamMembersMetaStore } from '../../../../src/main/services/team/TeamMembersMetaStore'; import { TeamMetaStore } from '../../../../src/main/services/team/TeamMetaStore'; @@ -99,6 +101,40 @@ describe('NativeAppManagedBootstrapContextBuilder', () => { expect(alice?.contextHash).toBe(hashNativeBootstrapText(alice?.contextText ?? '')); }); + it('warns but still builds for large native rosters below the aggregate budget', async () => { + await new TeamMetaStore().writeMeta('large-warning-native-team', { + cwd: '/tmp/workspace', + providerId: 'anthropic', + model: 'claude-opus-4-6', + createdAt: Date.now(), + }); + await new TeamMembersMetaStore().writeMembers( + 'large-warning-native-team', + Array.from({ length: 7 }, (_, index) => ({ + name: `member-${index}`, + providerId: 'anthropic' as const, + role: 'Developer', + })) + ); + + const result = await buildNativeAppManagedBootstrapSpecsWithDiagnostics({ + teamName: 'large-warning-native-team', + cwd: '/tmp/workspace', + members: Array.from({ length: 7 }, (_, index) => ({ + name: `member-${index}`, + providerId: 'anthropic' as const, + role: 'Developer', + })), + }); + + expect(result.specs.size).toBe(7); + expect(result.diagnostics.nativeMemberCount).toBe(7); + expect(result.diagnostics.totalContextLimitChars).toBe( + MAX_NATIVE_BOOTSTRAP_TOTAL_CONTEXT_CHARS + ); + expect(result.diagnostics.warning).toMatch(/Large native team startup context/); + }); + it('fails closed when aggregate native context budget is exceeded', async () => { const hugeRole = 'x'.repeat(40_000); await new TeamMetaStore().writeMeta('large-native-team', { @@ -109,7 +145,7 @@ describe('NativeAppManagedBootstrapContextBuilder', () => { }); await new TeamMembersMetaStore().writeMembers( 'large-native-team', - Array.from({ length: 8 }, (_, index) => ({ + Array.from({ length: 16 }, (_, index) => ({ name: `member-${index}`, providerId: 'anthropic' as const, role: hugeRole, @@ -120,7 +156,7 @@ describe('NativeAppManagedBootstrapContextBuilder', () => { buildNativeAppManagedBootstrapSpecs({ teamName: 'large-native-team', cwd: '/tmp/workspace', - members: Array.from({ length: 8 }, (_, index) => ({ + members: Array.from({ length: 16 }, (_, index) => ({ name: `member-${index}`, providerId: 'anthropic' as const, role: hugeRole,