chore: checkpoint team runtime work
This commit is contained in:
parent
dbd9ca7cf7
commit
d60abd54fe
26 changed files with 3146 additions and 124 deletions
|
|
@ -911,14 +911,17 @@ async function memberBriefing(context, memberName, options = {}) {
|
||||||
config.projectPath.trim() :
|
config.projectPath.trim() :
|
||||||
'';
|
'';
|
||||||
|
|
||||||
const activeProcesses = processStore
|
const includeActiveProcesses = options.includeActiveProcesses !== false;
|
||||||
.listProcesses(context.paths)
|
const activeProcesses = includeActiveProcesses ?
|
||||||
.filter(
|
processStore
|
||||||
(entry) =>
|
.listProcesses(context.paths)
|
||||||
entry &&
|
.filter(
|
||||||
entry.alive &&
|
(entry) =>
|
||||||
normalizeMemberName(entry.registeredBy) === normalizeMemberName(requestedMemberName)
|
entry &&
|
||||||
);
|
entry.alive &&
|
||||||
|
normalizeMemberName(entry.registeredBy) === normalizeMemberName(requestedMemberName)
|
||||||
|
) :
|
||||||
|
[];
|
||||||
|
|
||||||
const taskQueue = await taskBriefing(context, requestedMemberName);
|
const taskQueue = await taskBriefing(context, requestedMemberName);
|
||||||
const completionNotifyExample = messagingProtocol.buildLeadMessageExample({
|
const completionNotifyExample = messagingProtocol.buildLeadMessageExample({
|
||||||
|
|
|
||||||
2
mcp-server/src/agent-teams-controller.d.ts
vendored
2
mcp-server/src/agent-teams-controller.d.ts
vendored
|
|
@ -43,7 +43,7 @@ declare module 'agent-teams-controller' {
|
||||||
unlinkTask(taskId: string, targetId: string, linkType: string): unknown;
|
unlinkTask(taskId: string, targetId: string, linkType: string): unknown;
|
||||||
memberBriefing(
|
memberBriefing(
|
||||||
memberName: string,
|
memberName: string,
|
||||||
options?: { runtimeProvider?: 'native' | 'opencode' }
|
options?: { runtimeProvider?: 'native' | 'opencode'; includeActiveProcesses?: boolean }
|
||||||
): Promise<string>;
|
): Promise<string>;
|
||||||
leadBriefing(): Promise<string>;
|
leadBriefing(): Promise<string>;
|
||||||
taskBriefing(memberName: string): Promise<string>;
|
taskBriefing(memberName: string): Promise<string>;
|
||||||
|
|
|
||||||
|
|
@ -622,8 +622,9 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
|
||||||
...toolContextSchema,
|
...toolContextSchema,
|
||||||
memberName: z.string().min(1),
|
memberName: z.string().min(1),
|
||||||
runtimeProvider: z.enum(['native', 'opencode']).optional(),
|
runtimeProvider: z.enum(['native', 'opencode']).optional(),
|
||||||
|
includeActiveProcesses: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
execute: async ({ teamName, claudeDir, memberName, runtimeProvider }) => {
|
execute: async ({ teamName, claudeDir, memberName, runtimeProvider, includeActiveProcesses }) => {
|
||||||
assertConfiguredTeam(teamName, claudeDir);
|
assertConfiguredTeam(teamName, claudeDir);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
|
|
@ -631,6 +632,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
|
||||||
type: 'text' as const,
|
type: 'text' as const,
|
||||||
text: await getController(teamName, claudeDir).tasks.memberBriefing(memberName, {
|
text: await getController(teamName, claudeDir).tasks.memberBriefing(memberName, {
|
||||||
...(runtimeProvider ? { runtimeProvider } : {}),
|
...(runtimeProvider ? { runtimeProvider } : {}),
|
||||||
|
...(includeActiveProcesses !== undefined ? { includeActiveProcesses } : {}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,11 @@
|
||||||
"dev:web": "node ./scripts/dev-web.mjs",
|
"dev:web": "node ./scripts/dev-web.mjs",
|
||||||
"dev:kill": "node bin/kill-dev.js",
|
"dev:kill": "node bin/kill-dev.js",
|
||||||
"opencode:prove-mixed-recovery": "node ./scripts/prove-opencode-mixed-recovery.mjs",
|
"opencode:prove-mixed-recovery": "node ./scripts/prove-opencode-mixed-recovery.mjs",
|
||||||
|
"opencode:prove-semantic-gauntlet": "node ./scripts/prove-opencode-semantic-gauntlet.mjs",
|
||||||
|
"opencode:prove-semantic-messaging": "node ./scripts/prove-opencode-semantic-messaging.mjs",
|
||||||
|
"opencode:prove-semantic-model-matrix": "node ./scripts/prove-opencode-semantic-model-matrix.mjs",
|
||||||
"opencode:prove-team-provisioning": "node ./scripts/prove-opencode-team-provisioning.mjs",
|
"opencode:prove-team-provisioning": "node ./scripts/prove-opencode-team-provisioning.mjs",
|
||||||
|
"team:prove-agent-cli-launch": "node ./scripts/prove-agent-cli-launch.mjs",
|
||||||
"team:prove-launch-matrix": "pnpm exec vitest run --maxWorkers 1 --minWorkers 1 test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts",
|
"team:prove-launch-matrix": "pnpm exec vitest run --maxWorkers 1 --minWorkers 1 test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts",
|
||||||
"prebuild": "tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build",
|
"prebuild": "tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build",
|
||||||
"build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build",
|
"build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build",
|
||||||
|
|
|
||||||
43
scripts/prove-agent-cli-launch.mjs
Normal file
43
scripts/prove-agent-cli-launch.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDir, '..');
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
AGENT_CLI_LAUNCH_LIVE_E2E: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Running agent CLI launch live smoke');
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
'pnpm',
|
||||||
|
[
|
||||||
|
'exec',
|
||||||
|
'vitest',
|
||||||
|
'run',
|
||||||
|
'--maxWorkers',
|
||||||
|
'1',
|
||||||
|
'--minWorkers',
|
||||||
|
'1',
|
||||||
|
'test/main/utils/AgentCliLaunch.live-e2e.test.ts',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(`Failed to run agent CLI launch smoke: ${result.error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
73
scripts/prove-opencode-semantic-gauntlet.mjs
Normal file
73
scripts/prove-opencode-semantic-gauntlet.mjs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
exitForSkippedPreflight,
|
||||||
|
preflightOpenCodeLiveEnvironment,
|
||||||
|
} from './lib/opencode-live-preflight.mjs';
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDir, '..');
|
||||||
|
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||||
|
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_E2E: '1',
|
||||||
|
OPENCODE_E2E_SEMANTIC_MODEL_GAUNTLET: '1',
|
||||||
|
OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL?.trim() || 'opencode/big-pickle',
|
||||||
|
OPENCODE_E2E_GAUNTLET_RUNS: process.env.OPENCODE_E2E_GAUNTLET_RUNS?.trim() || '1',
|
||||||
|
OPENCODE_E2E_GAUNTLET_MIN_AVERAGE_SCORE:
|
||||||
|
process.env.OPENCODE_E2E_GAUNTLET_MIN_AVERAGE_SCORE?.trim() || '80',
|
||||||
|
OPENCODE_E2E_GAUNTLET_MIN_SUCCESSFUL_RUNS:
|
||||||
|
process.env.OPENCODE_E2E_GAUNTLET_MIN_SUCCESSFUL_RUNS?.trim() || '1',
|
||||||
|
OPENCODE_E2E_GAUNTLET_MIN_CONSISTENCY_SCORE:
|
||||||
|
process.env.OPENCODE_E2E_GAUNTLET_MIN_CONSISTENCY_SCORE?.trim() || '0',
|
||||||
|
OPENCODE_E2E_GAUNTLET_REQUIRE_RECOMMENDED:
|
||||||
|
process.env.OPENCODE_E2E_GAUNTLET_REQUIRE_RECOMMENDED?.trim() || '1',
|
||||||
|
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||||
|
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||||
|
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Running OpenCode semantic gauntlet live smoke');
|
||||||
|
console.log(`Models: ${env.OPENCODE_E2E_MODELS?.trim() || env.OPENCODE_E2E_MODEL}`);
|
||||||
|
console.log(`Runs per model: ${env.OPENCODE_E2E_GAUNTLET_RUNS}`);
|
||||||
|
console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
|
||||||
|
|
||||||
|
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
|
||||||
|
exitForSkippedPreflight(preflight);
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
'pnpm',
|
||||||
|
[
|
||||||
|
'exec',
|
||||||
|
'vitest',
|
||||||
|
'run',
|
||||||
|
'--maxWorkers',
|
||||||
|
'1',
|
||||||
|
'--minWorkers',
|
||||||
|
'1',
|
||||||
|
'test/main/services/team/OpenCodeSemanticModelGauntlet.live.test.ts',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(`Failed to run OpenCode semantic gauntlet smoke: ${result.error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
65
scripts/prove-opencode-semantic-messaging.mjs
Normal file
65
scripts/prove-opencode-semantic-messaging.mjs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
exitForSkippedPreflight,
|
||||||
|
preflightOpenCodeLiveEnvironment,
|
||||||
|
} from './lib/opencode-live-preflight.mjs';
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDir, '..');
|
||||||
|
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||||
|
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_E2E: '1',
|
||||||
|
OPENCODE_E2E_SEMANTIC_MESSAGING: '1',
|
||||||
|
OPENCODE_E2E_PROJECT_PATH: process.env.OPENCODE_E2E_PROJECT_PATH?.trim() || repoRoot,
|
||||||
|
OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL?.trim() || 'opencode/big-pickle',
|
||||||
|
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||||
|
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||||
|
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Running OpenCode semantic messaging live smoke');
|
||||||
|
console.log(`Model: ${env.OPENCODE_E2E_MODEL}`);
|
||||||
|
console.log(`Project: ${env.OPENCODE_E2E_PROJECT_PATH}`);
|
||||||
|
console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
|
||||||
|
|
||||||
|
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
|
||||||
|
exitForSkippedPreflight(preflight);
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
'pnpm',
|
||||||
|
[
|
||||||
|
'exec',
|
||||||
|
'vitest',
|
||||||
|
'run',
|
||||||
|
'--maxWorkers',
|
||||||
|
'1',
|
||||||
|
'--minWorkers',
|
||||||
|
'1',
|
||||||
|
'test/main/services/team/OpenCodeSemanticMessaging.live.test.ts',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(`Failed to run OpenCode semantic messaging smoke: ${result.error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
63
scripts/prove-opencode-semantic-model-matrix.mjs
Normal file
63
scripts/prove-opencode-semantic-model-matrix.mjs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import {
|
||||||
|
exitForSkippedPreflight,
|
||||||
|
preflightOpenCodeLiveEnvironment,
|
||||||
|
} from './lib/opencode-live-preflight.mjs';
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDir, '..');
|
||||||
|
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
|
||||||
|
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
OPENCODE_E2E: '1',
|
||||||
|
OPENCODE_E2E_SEMANTIC_MODEL_MATRIX: '1',
|
||||||
|
OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL?.trim() || 'opencode/big-pickle',
|
||||||
|
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
|
||||||
|
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
|
||||||
|
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Running OpenCode semantic model matrix live smoke');
|
||||||
|
console.log(`Models: ${env.OPENCODE_E2E_MODELS?.trim() || env.OPENCODE_E2E_MODEL}`);
|
||||||
|
console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
|
||||||
|
|
||||||
|
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
|
||||||
|
exitForSkippedPreflight(preflight);
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
'pnpm',
|
||||||
|
[
|
||||||
|
'exec',
|
||||||
|
'vitest',
|
||||||
|
'run',
|
||||||
|
'--maxWorkers',
|
||||||
|
'1',
|
||||||
|
'--minWorkers',
|
||||||
|
'1',
|
||||||
|
'test/main/services/team/OpenCodeSemanticModelMatrix.live.test.ts',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: repoRoot,
|
||||||
|
env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(`Failed to run OpenCode semantic model matrix smoke: ${result.error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
|
|
@ -6,6 +6,9 @@ import type {
|
||||||
MemberLaunchState,
|
MemberLaunchState,
|
||||||
MemberSpawnLivenessSource,
|
MemberSpawnLivenessSource,
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
|
OpenCodeBootstrapEvidenceSource,
|
||||||
|
OpenCodeBootstrapMode,
|
||||||
PersistedTeamLaunchMemberSources,
|
PersistedTeamLaunchMemberSources,
|
||||||
PersistedTeamLaunchMemberState,
|
PersistedTeamLaunchMemberState,
|
||||||
PersistedTeamLaunchPhase,
|
PersistedTeamLaunchPhase,
|
||||||
|
|
@ -43,6 +46,9 @@ export interface MixedSecondaryLaneMemberStateInput {
|
||||||
runtimePid?: number;
|
runtimePid?: number;
|
||||||
runtimeSessionId?: string;
|
runtimeSessionId?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
|
||||||
|
bootstrapMode?: OpenCodeBootstrapMode;
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
livenessKind?: TeamAgentRuntimeLivenessKind;
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
pidSource?: TeamAgentRuntimePidSource;
|
pidSource?: TeamAgentRuntimePidSource;
|
||||||
runtimeDiagnostic?: string;
|
runtimeDiagnostic?: string;
|
||||||
|
|
@ -348,6 +354,9 @@ function createSecondaryLaneMemberState(
|
||||||
? Math.trunc(evidence.runtimePid)
|
? Math.trunc(evidence.runtimePid)
|
||||||
: undefined,
|
: undefined,
|
||||||
runtimeSessionId: evidence?.runtimeSessionId ?? evidence?.sessionId,
|
runtimeSessionId: evidence?.runtimeSessionId ?? evidence?.sessionId,
|
||||||
|
bootstrapEvidenceSource: evidence?.bootstrapEvidenceSource,
|
||||||
|
bootstrapMode: evidence?.bootstrapMode,
|
||||||
|
appManagedBootstrapCandidate: evidence?.appManagedBootstrapCandidate,
|
||||||
livenessKind: evidence?.livenessKind,
|
livenessKind: evidence?.livenessKind,
|
||||||
pidSource: evidence?.pidSource,
|
pidSource: evidence?.pidSource,
|
||||||
runtimeDiagnostic: evidence?.runtimeDiagnostic,
|
runtimeDiagnostic: evidence?.runtimeDiagnostic,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type {
|
||||||
MemberLaunchState,
|
MemberLaunchState,
|
||||||
MemberSpawnLivenessSource,
|
MemberSpawnLivenessSource,
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
PersistedTeamLaunchMemberSources,
|
PersistedTeamLaunchMemberSources,
|
||||||
PersistedTeamLaunchMemberState,
|
PersistedTeamLaunchMemberState,
|
||||||
PersistedTeamLaunchPhase,
|
PersistedTeamLaunchPhase,
|
||||||
|
|
@ -176,6 +177,60 @@ function normalizeOptionalString(value: unknown): string | undefined {
|
||||||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOpenCodeAppManagedBootstrapCandidate(
|
||||||
|
value: unknown
|
||||||
|
): OpenCodeAppManagedBootstrapCandidate | undefined {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
if (record.schemaVersion !== 1 || record.source !== 'app_managed_bootstrap') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const teamName = normalizeOptionalString(record.teamName);
|
||||||
|
const memberName = normalizeOptionalString(record.memberName);
|
||||||
|
const runId = normalizeOptionalString(record.runId);
|
||||||
|
const laneId = normalizeOptionalString(record.laneId);
|
||||||
|
const runtimeSessionId = normalizeOptionalString(record.runtimeSessionId);
|
||||||
|
const messageID = normalizeOptionalString(record.messageID);
|
||||||
|
const contextHash = normalizeOptionalString(record.contextHash);
|
||||||
|
const briefingHash = normalizeOptionalString(record.briefingHash);
|
||||||
|
const injectionVerifiedAt = normalizeOptionalString(record.injectionVerifiedAt);
|
||||||
|
const candidateAt = normalizeOptionalString(record.candidateAt);
|
||||||
|
if (
|
||||||
|
!teamName ||
|
||||||
|
!memberName ||
|
||||||
|
!runId ||
|
||||||
|
!laneId ||
|
||||||
|
!runtimeSessionId ||
|
||||||
|
!messageID ||
|
||||||
|
!contextHash ||
|
||||||
|
!briefingHash ||
|
||||||
|
!injectionVerifiedAt ||
|
||||||
|
!candidateAt
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const model = normalizeOptionalString(record.model);
|
||||||
|
const agent = normalizeOptionalString(record.agent);
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
source: 'app_managed_bootstrap',
|
||||||
|
teamName,
|
||||||
|
memberName,
|
||||||
|
runId,
|
||||||
|
laneId,
|
||||||
|
runtimeSessionId,
|
||||||
|
messageID,
|
||||||
|
contextHash,
|
||||||
|
briefingHash,
|
||||||
|
injectionVerifiedAt,
|
||||||
|
candidateAt,
|
||||||
|
...(model ? { model } : {}),
|
||||||
|
...(agent ? { agent } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function decodeJsonStringLiteral(value: string): string {
|
function decodeJsonStringLiteral(value: string): string {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(`"${value}"`) as string;
|
return JSON.parse(`"${value}"`) as string;
|
||||||
|
|
@ -601,6 +656,19 @@ function normalizePersistedMemberState(
|
||||||
runtimePid: normalizeRuntimePid(parsed.runtimePid),
|
runtimePid: normalizeRuntimePid(parsed.runtimePid),
|
||||||
runtimeRunId: normalizeOptionalString(parsed.runtimeRunId),
|
runtimeRunId: normalizeOptionalString(parsed.runtimeRunId),
|
||||||
runtimeSessionId: normalizeOptionalString(parsed.runtimeSessionId),
|
runtimeSessionId: normalizeOptionalString(parsed.runtimeSessionId),
|
||||||
|
bootstrapEvidenceSource:
|
||||||
|
parsed.bootstrapEvidenceSource === 'runtime_bootstrap_checkin' ||
|
||||||
|
parsed.bootstrapEvidenceSource === 'app_managed_bootstrap'
|
||||||
|
? parsed.bootstrapEvidenceSource
|
||||||
|
: undefined,
|
||||||
|
bootstrapMode:
|
||||||
|
parsed.bootstrapMode === 'model_tool_checkin' ||
|
||||||
|
parsed.bootstrapMode === 'app_managed_context'
|
||||||
|
? parsed.bootstrapMode
|
||||||
|
: undefined,
|
||||||
|
appManagedBootstrapCandidate: normalizeOpenCodeAppManagedBootstrapCandidate(
|
||||||
|
parsed.appManagedBootstrapCandidate
|
||||||
|
),
|
||||||
livenessKind,
|
livenessKind,
|
||||||
pidSource: normalizePidSource(parsed.pidSource),
|
pidSource: normalizePidSource(parsed.pidSource),
|
||||||
runtimeDiagnostic: normalizeOptionalString(parsed.runtimeDiagnostic),
|
runtimeDiagnostic: normalizeOptionalString(parsed.runtimeDiagnostic),
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,8 @@ import type {
|
||||||
MemberSpawnStatus,
|
MemberSpawnStatus,
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
MemberSpawnStatusesSnapshot,
|
MemberSpawnStatusesSnapshot,
|
||||||
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
|
OpenCodeBootstrapEvidenceSource,
|
||||||
OpenCodeRuntimeDeliveryStatus,
|
OpenCodeRuntimeDeliveryStatus,
|
||||||
PersistedTeamLaunchMemberState,
|
PersistedTeamLaunchMemberState,
|
||||||
PersistedTeamLaunchPhase,
|
PersistedTeamLaunchPhase,
|
||||||
|
|
@ -2160,6 +2162,28 @@ function downgradeUncommittedOpenCodeBootstrapEvidence(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function promoteCommittedOpenCodeAppManagedBootstrapEvidence(
|
||||||
|
evidence: TeamRuntimeMemberLaunchEvidence
|
||||||
|
): TeamRuntimeMemberLaunchEvidence {
|
||||||
|
return {
|
||||||
|
...evidence,
|
||||||
|
launchState: 'confirmed_alive',
|
||||||
|
agentToolAccepted: true,
|
||||||
|
runtimeAlive: true,
|
||||||
|
bootstrapConfirmed: true,
|
||||||
|
hardFailure: false,
|
||||||
|
hardFailureReason: undefined,
|
||||||
|
livenessKind: 'confirmed_bootstrap',
|
||||||
|
runtimeDiagnostic:
|
||||||
|
'OpenCode app-managed bootstrap evidence was committed and read back by the desktop app.',
|
||||||
|
runtimeDiagnosticSeverity: 'info',
|
||||||
|
diagnostics: appendDiagnosticOnce(
|
||||||
|
evidence.diagnostics,
|
||||||
|
'OpenCode app-managed bootstrap evidence committed and read back.'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeRuntimeLaunchResultMembers(
|
function summarizeRuntimeLaunchResultMembers(
|
||||||
members: Record<string, TeamRuntimeMemberLaunchEvidence>
|
members: Record<string, TeamRuntimeMemberLaunchEvidence>
|
||||||
): TeamLaunchAggregateState {
|
): TeamLaunchAggregateState {
|
||||||
|
|
@ -2493,6 +2517,7 @@ const OPEN_CODE_SECRET_FLAG_PATTERN =
|
||||||
/(--(?:api-key|token|password|secret|authorization|auth-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
|
/(--(?:api-key|token|password|secret|authorization|auth-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
|
||||||
const OPEN_CODE_BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Z0-9._~+/=-]+/gi;
|
const OPEN_CODE_BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Z0-9._~+/=-]+/gi;
|
||||||
const OPEN_CODE_SECRET_KEY_PATTERN = /\bsk-[A-Za-z0-9_-]{16,}\b/g;
|
const OPEN_CODE_SECRET_KEY_PATTERN = /\bsk-[A-Za-z0-9_-]{16,}\b/g;
|
||||||
|
const OPEN_CODE_APP_MANAGED_BRIEFING_MAX_CHARS = 12_000;
|
||||||
|
|
||||||
function normalizeOpenCodePersistedFailureReason(value: string | undefined): string | undefined {
|
function normalizeOpenCodePersistedFailureReason(value: string | undefined): string | undefined {
|
||||||
const trimmed = value?.replace(/\s+/g, ' ').trim();
|
const trimmed = value?.replace(/\s+/g, ' ').trim();
|
||||||
|
|
@ -2505,6 +2530,21 @@ function normalizeOpenCodePersistedFailureReason(value: string | undefined): str
|
||||||
.replace(OPEN_CODE_SECRET_KEY_PATTERN, '[redacted-api-key]');
|
.replace(OPEN_CODE_SECRET_KEY_PATTERN, '[redacted-api-key]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function redactOpenCodeAppManagedContextText(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(OPEN_CODE_SECRET_FLAG_PATTERN, '$1[redacted]')
|
||||||
|
.replace(OPEN_CODE_BEARER_TOKEN_PATTERN, 'Bearer [redacted]')
|
||||||
|
.replace(OPEN_CODE_SECRET_KEY_PATTERN, '[redacted-api-key]');
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundOpenCodeAppManagedBriefingText(value: string): string {
|
||||||
|
const normalized = redactOpenCodeAppManagedContextText(value.replace(/\r\n/g, '\n')).trim();
|
||||||
|
if (normalized.length <= OPEN_CODE_APP_MANAGED_BRIEFING_MAX_CHARS) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return `${normalized.slice(0, OPEN_CODE_APP_MANAGED_BRIEFING_MAX_CHARS)}\n[truncated app-managed briefing]`;
|
||||||
|
}
|
||||||
|
|
||||||
function isGenericOpenCodePersistedFailureReason(value: string | undefined): boolean {
|
function isGenericOpenCodePersistedFailureReason(value: string | undefined): boolean {
|
||||||
const normalized = normalizeOpenCodePersistedFailureReason(value);
|
const normalized = normalizeOpenCodePersistedFailureReason(value);
|
||||||
return (
|
return (
|
||||||
|
|
@ -2616,8 +2656,20 @@ function promoteOpenCodeSecondaryMemberFromCommittedBootstrapEvidence(input: {
|
||||||
hardFailureReason: undefined,
|
hardFailureReason: undefined,
|
||||||
runtimeRunId: input.session.runId ?? input.current.runtimeRunId,
|
runtimeRunId: input.session.runId ?? input.current.runtimeRunId,
|
||||||
runtimeSessionId: input.session.id,
|
runtimeSessionId: input.session.id,
|
||||||
|
bootstrapEvidenceSource: input.session.source,
|
||||||
|
bootstrapMode:
|
||||||
|
input.session.source === 'app_managed_bootstrap'
|
||||||
|
? 'app_managed_context'
|
||||||
|
: 'model_tool_checkin',
|
||||||
|
appManagedBootstrapCandidate:
|
||||||
|
input.session.source === 'app_managed_bootstrap'
|
||||||
|
? input.session.appManagedBootstrapCandidate
|
||||||
|
: undefined,
|
||||||
livenessKind,
|
livenessKind,
|
||||||
runtimeDiagnostic: 'OpenCode bootstrap evidence committed.',
|
runtimeDiagnostic:
|
||||||
|
input.session.source === 'app_managed_bootstrap'
|
||||||
|
? 'OpenCode app-managed bootstrap evidence committed.'
|
||||||
|
: 'OpenCode bootstrap evidence committed.',
|
||||||
runtimeDiagnosticSeverity: 'info',
|
runtimeDiagnosticSeverity: 'info',
|
||||||
firstSpawnAcceptedAt:
|
firstSpawnAcceptedAt:
|
||||||
input.current.firstSpawnAcceptedAt ?? input.previous?.firstSpawnAcceptedAt ?? observedAt,
|
input.current.firstSpawnAcceptedAt ?? input.previous?.firstSpawnAcceptedAt ?? observedAt,
|
||||||
|
|
@ -6687,6 +6739,13 @@ export class TeamProvisioningService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isOpenCodePromptAcceptedByObservation(
|
||||||
|
observation?: NonNullable<OpenCodeTeamRuntimeMessageResult['responseObservation']>
|
||||||
|
): boolean {
|
||||||
|
const deliveredUserMessageId = observation?.deliveredUserMessageId;
|
||||||
|
return typeof deliveredUserMessageId === 'string' && deliveredUserMessageId.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private isOpenCodeDeliveryRetryablePendingResponse(input: {
|
private isOpenCodeDeliveryRetryablePendingResponse(input: {
|
||||||
ledgerRecord: OpenCodePromptDeliveryLedgerRecord;
|
ledgerRecord: OpenCodePromptDeliveryLedgerRecord;
|
||||||
visibleReply?: OpenCodeVisibleReplyProof | null;
|
visibleReply?: OpenCodeVisibleReplyProof | null;
|
||||||
|
|
@ -7115,6 +7174,42 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async isStaleOpenCodePromptDeliveryWatchdogError(input: {
|
||||||
|
teamName: string;
|
||||||
|
memberName: string;
|
||||||
|
messageId: string;
|
||||||
|
error: unknown;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
if (!getErrorMessage(input.error).startsWith('OpenCode prompt delivery record not found:')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.canDeliverToOpenCodeRuntimeForTeam(input.teamName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inboxMessages = await this.inboxReader
|
||||||
|
.getMessagesFor(input.teamName, input.memberName)
|
||||||
|
.catch(() => []);
|
||||||
|
const targetMessage = inboxMessages.find((message) => message.messageId === input.messageId);
|
||||||
|
if (!targetMessage || targetMessage.read) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await this.resolveOpenCodeMemberDeliveryIdentity(
|
||||||
|
input.teamName,
|
||||||
|
input.memberName
|
||||||
|
).catch(() => null);
|
||||||
|
if (!identity?.ok) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const laneActive = await this.isOpenCodeRuntimeLaneIndexActive(
|
||||||
|
input.teamName,
|
||||||
|
identity.laneId
|
||||||
|
).catch(() => false);
|
||||||
|
return !laneActive;
|
||||||
|
}
|
||||||
|
|
||||||
private scheduleOpenCodePromptDeliveryWatchdog(input: {
|
private scheduleOpenCodePromptDeliveryWatchdog(input: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
memberName: string;
|
memberName: string;
|
||||||
|
|
@ -7141,10 +7236,30 @@ export class TeamProvisioningService {
|
||||||
this.enqueueOpenCodePromptDeliveryWatchdogJob({
|
this.enqueueOpenCodePromptDeliveryWatchdogJob({
|
||||||
teamName: input.teamName,
|
teamName: input.teamName,
|
||||||
run: async () => {
|
run: async () => {
|
||||||
await this.relayOpenCodeMemberInboxMessages(input.teamName, input.memberName, {
|
if (!this.canDeliverToOpenCodeRuntimeForTeam(input.teamName)) {
|
||||||
onlyMessageId: messageId,
|
return;
|
||||||
source: 'watchdog',
|
}
|
||||||
});
|
try {
|
||||||
|
await this.relayOpenCodeMemberInboxMessages(input.teamName, input.memberName, {
|
||||||
|
onlyMessageId: messageId,
|
||||||
|
source: 'watchdog',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
await this.isStaleOpenCodePromptDeliveryWatchdogError({
|
||||||
|
teamName: input.teamName,
|
||||||
|
memberName: input.memberName,
|
||||||
|
messageId,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
logger.debug(
|
||||||
|
`[${input.teamName}] Ignoring stale OpenCode prompt delivery watchdog job for ${input.memberName}/${messageId}: ${getErrorMessage(error)}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, delayMs);
|
}, delayMs);
|
||||||
|
|
@ -8098,16 +8213,18 @@ export class TeamProvisioningService {
|
||||||
const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation(
|
const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation(
|
||||||
result.responseObservation
|
result.responseObservation
|
||||||
);
|
);
|
||||||
|
const promptAccepted =
|
||||||
|
result.ok || this.isOpenCodePromptAcceptedByObservation(responseObservation);
|
||||||
if (ledgerRecord && ledger) {
|
if (ledgerRecord && ledger) {
|
||||||
ledgerRecord = await ledger.applyDeliveryResult({
|
ledgerRecord = await ledger.applyDeliveryResult({
|
||||||
id: ledgerRecord.id,
|
id: ledgerRecord.id,
|
||||||
accepted: result.ok,
|
accepted: promptAccepted,
|
||||||
attempted: true,
|
attempted: true,
|
||||||
responseObservation,
|
responseObservation,
|
||||||
sessionId: result.sessionId,
|
sessionId: result.sessionId,
|
||||||
prePromptCursor: result.prePromptCursor,
|
prePromptCursor: result.prePromptCursor,
|
||||||
diagnostics: result.diagnostics,
|
diagnostics: result.diagnostics,
|
||||||
reason: result.ok ? responseObservation?.reason : result.diagnostics[0],
|
reason: promptAccepted ? responseObservation?.reason : result.diagnostics[0],
|
||||||
now: nowIso(),
|
now: nowIso(),
|
||||||
});
|
});
|
||||||
let proof = await this.applyOpenCodeVisibleDestinationProof({
|
let proof = await this.applyOpenCodeVisibleDestinationProof({
|
||||||
|
|
@ -8127,7 +8244,7 @@ export class TeamProvisioningService {
|
||||||
});
|
});
|
||||||
ledgerRecord = proof.ledgerRecord;
|
ledgerRecord = proof.ledgerRecord;
|
||||||
this.logOpenCodePromptDeliveryEvent(
|
this.logOpenCodePromptDeliveryEvent(
|
||||||
result.ok
|
promptAccepted
|
||||||
? ledgerRecord.status === 'unanswered'
|
? ledgerRecord.status === 'unanswered'
|
||||||
? 'opencode_prompt_delivery_unanswered'
|
? 'opencode_prompt_delivery_unanswered'
|
||||||
: ledgerRecord.status === 'responded'
|
: ledgerRecord.status === 'responded'
|
||||||
|
|
@ -8135,7 +8252,10 @@ export class TeamProvisioningService {
|
||||||
: 'opencode_prompt_delivery_prompt_accepted'
|
: 'opencode_prompt_delivery_prompt_accepted'
|
||||||
: 'opencode_prompt_delivery_retry_scheduled',
|
: 'opencode_prompt_delivery_retry_scheduled',
|
||||||
ledgerRecord,
|
ledgerRecord,
|
||||||
{ accepted: result.ok, reason: ledgerRecord.lastReason ?? result.diagnostics[0] ?? null }
|
{
|
||||||
|
accepted: promptAccepted,
|
||||||
|
reason: ledgerRecord.lastReason ?? result.diagnostics[0] ?? null,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const responseState = ledgerRecord?.responseState ?? responseObservation?.state;
|
const responseState = ledgerRecord?.responseState ?? responseObservation?.state;
|
||||||
|
|
@ -8160,7 +8280,7 @@ export class TeamProvisioningService {
|
||||||
visibleReply,
|
visibleReply,
|
||||||
ledgerRecord,
|
ledgerRecord,
|
||||||
});
|
});
|
||||||
if (ledgerRecord && result.ok && !readAllowed) {
|
if (ledgerRecord && promptAccepted && !readAllowed) {
|
||||||
const retry = this.isOpenCodeDeliveryRetryablePendingResponse({
|
const retry = this.isOpenCodeDeliveryRetryablePendingResponse({
|
||||||
ledgerRecord,
|
ledgerRecord,
|
||||||
visibleReply,
|
visibleReply,
|
||||||
|
|
@ -8196,7 +8316,7 @@ export class TeamProvisioningService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ledgerRecord && !result.ok) {
|
if (ledgerRecord && !promptAccepted) {
|
||||||
const reason = this.isOpenCodePromptAcceptanceUnknownFailure(result.diagnostics)
|
const reason = this.isOpenCodePromptAcceptanceUnknownFailure(result.diagnostics)
|
||||||
? 'opencode_prompt_acceptance_unknown_after_bridge_timeout'
|
? 'opencode_prompt_acceptance_unknown_after_bridge_timeout'
|
||||||
: (result.diagnostics[0] ?? 'opencode_message_delivery_failed');
|
: (result.diagnostics[0] ?? 'opencode_message_delivery_failed');
|
||||||
|
|
@ -8239,9 +8359,9 @@ export class TeamProvisioningService {
|
||||||
ledgerRecord?.visibleReplyCorrelation ??
|
ledgerRecord?.visibleReplyCorrelation ??
|
||||||
responseObservation?.visibleReplyCorrelation ??
|
responseObservation?.visibleReplyCorrelation ??
|
||||||
undefined;
|
undefined;
|
||||||
const acceptanceUnknown = Boolean(ledgerRecord?.acceptanceUnknown && !result.ok);
|
const acceptanceUnknown = Boolean(ledgerRecord?.acceptanceUnknown && !promptAccepted);
|
||||||
const responsePending =
|
const responsePending =
|
||||||
acceptanceUnknown || (result.ok && Boolean(ledgerRecord || responseObservation))
|
acceptanceUnknown || (promptAccepted && Boolean(ledgerRecord || responseObservation))
|
||||||
? !readAllowed
|
? !readAllowed
|
||||||
: false;
|
: false;
|
||||||
const pendingReason =
|
const pendingReason =
|
||||||
|
|
@ -8255,8 +8375,8 @@ export class TeamProvisioningService {
|
||||||
? ledgerRecord.diagnostics
|
? ledgerRecord.diagnostics
|
||||||
: result.diagnostics;
|
: result.diagnostics;
|
||||||
return {
|
return {
|
||||||
delivered: result.ok || acceptanceUnknown,
|
delivered: promptAccepted || acceptanceUnknown,
|
||||||
...(ledgerRecord || responseObservation ? { accepted: result.ok } : {}),
|
...(ledgerRecord || responseObservation ? { accepted: promptAccepted } : {}),
|
||||||
...(ledgerRecord || responseObservation ? { responsePending } : {}),
|
...(ledgerRecord || responseObservation ? { responsePending } : {}),
|
||||||
...(acceptanceUnknown ? { acceptanceUnknown: true } : {}),
|
...(acceptanceUnknown ? { acceptanceUnknown: true } : {}),
|
||||||
...(ledgerRecord
|
...(ledgerRecord
|
||||||
|
|
@ -8279,7 +8399,7 @@ export class TeamProvisioningService {
|
||||||
: {}),
|
: {}),
|
||||||
...(pendingReason
|
...(pendingReason
|
||||||
? { reason: pendingReason }
|
? { reason: pendingReason }
|
||||||
: result.ok
|
: promptAccepted
|
||||||
? {}
|
? {}
|
||||||
: { reason: result.diagnostics[0] ?? 'opencode_message_delivery_failed' }),
|
: { reason: result.diagnostics[0] ?? 'opencode_message_delivery_failed' }),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
|
|
@ -9927,6 +10047,8 @@ export class TeamProvisioningService {
|
||||||
memberName: string;
|
memberName: string;
|
||||||
runtimeSessionId: string;
|
runtimeSessionId: string;
|
||||||
observedAt: string;
|
observedAt: string;
|
||||||
|
source?: OpenCodeBootstrapEvidenceSource;
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const descriptor = OPENCODE_RUNTIME_STORE_DESCRIPTORS.find(
|
const descriptor = OPENCODE_RUNTIME_STORE_DESCRIPTORS.find(
|
||||||
(candidate) => candidate.schemaName === 'opencode.sessionStore'
|
(candidate) => candidate.schemaName === 'opencode.sessionStore'
|
||||||
|
|
@ -9944,6 +10066,7 @@ export class TeamProvisioningService {
|
||||||
await fs.promises.mkdir(runtimeDirectory, { recursive: true });
|
await fs.promises.mkdir(runtimeDirectory, { recursive: true });
|
||||||
const sessionStorePath = path.join(runtimeDirectory, descriptor.relativePath);
|
const sessionStorePath = path.join(runtimeDirectory, descriptor.relativePath);
|
||||||
const existingSessions = await this.readOpenCodeRuntimeSessionStore(sessionStorePath);
|
const existingSessions = await this.readOpenCodeRuntimeSessionStore(sessionStorePath);
|
||||||
|
const source = input.source ?? 'runtime_bootstrap_checkin';
|
||||||
const session = {
|
const session = {
|
||||||
id: input.runtimeSessionId,
|
id: input.runtimeSessionId,
|
||||||
teamName: input.teamName,
|
teamName: input.teamName,
|
||||||
|
|
@ -9952,7 +10075,10 @@ export class TeamProvisioningService {
|
||||||
laneId: input.laneId,
|
laneId: input.laneId,
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
observedAt: input.observedAt,
|
observedAt: input.observedAt,
|
||||||
source: 'runtime_bootstrap_checkin',
|
source,
|
||||||
|
...(source === 'app_managed_bootstrap' && input.appManagedBootstrapCandidate
|
||||||
|
? { appManagedBootstrapCandidate: input.appManagedBootstrapCandidate }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
const sessions = this.mergeOpenCodeRuntimeSessionRecords(existingSessions, session);
|
const sessions = this.mergeOpenCodeRuntimeSessionRecords(existingSessions, session);
|
||||||
const manifestStore = createRuntimeStoreManifestStore({
|
const manifestStore = createRuntimeStoreManifestStore({
|
||||||
|
|
@ -9989,6 +10115,11 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
if (!(await this.hasCommittedOpenCodeRuntimeBootstrapSessionEvidence(input))) {
|
||||||
|
throw new Error(
|
||||||
|
`OpenCode bootstrap session evidence write did not verify for ${input.memberName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async hasCommittedOpenCodeRuntimeBootstrapSessionEvidence(input: {
|
private async hasCommittedOpenCodeRuntimeBootstrapSessionEvidence(input: {
|
||||||
|
|
@ -9997,6 +10128,8 @@ export class TeamProvisioningService {
|
||||||
laneId: string;
|
laneId: string;
|
||||||
memberName: string;
|
memberName: string;
|
||||||
runtimeSessionId: string;
|
runtimeSessionId: string;
|
||||||
|
source?: OpenCodeBootstrapEvidenceSource;
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const evidence = await readCommittedOpenCodeBootstrapSessionEvidence({
|
const evidence = await readCommittedOpenCodeBootstrapSessionEvidence({
|
||||||
teamsBasePath: getTeamsBasePath(),
|
teamsBasePath: getTeamsBasePath(),
|
||||||
|
|
@ -10009,12 +10142,28 @@ export class TeamProvisioningService {
|
||||||
if (evidence.activeRunId && evidence.activeRunId.trim() !== input.runId) {
|
if (evidence.activeRunId && evidence.activeRunId.trim() !== input.runId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return evidence.sessions.some(
|
return evidence.sessions.some((session) => {
|
||||||
(session) =>
|
if (
|
||||||
session.id === input.runtimeSessionId &&
|
session.id !== input.runtimeSessionId ||
|
||||||
session.runId === input.runId &&
|
session.runId !== input.runId ||
|
||||||
namesMatchCaseInsensitive(session.memberName, input.memberName)
|
!namesMatchCaseInsensitive(session.memberName, input.memberName)
|
||||||
);
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (input.source && session.source !== input.source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (input.source === 'app_managed_bootstrap' && input.appManagedBootstrapCandidate) {
|
||||||
|
const candidate = session.appManagedBootstrapCandidate;
|
||||||
|
return (
|
||||||
|
candidate?.runtimeSessionId === input.appManagedBootstrapCandidate.runtimeSessionId &&
|
||||||
|
candidate.messageID === input.appManagedBootstrapCandidate.messageID &&
|
||||||
|
candidate.contextHash === input.appManagedBootstrapCandidate.contextHash &&
|
||||||
|
candidate.briefingHash === input.appManagedBootstrapCandidate.briefingHash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async hasDeliverableOpenCodeRuntimeBootstrapSessionEvidence(input: {
|
private async hasDeliverableOpenCodeRuntimeBootstrapSessionEvidence(input: {
|
||||||
|
|
@ -16656,7 +16805,7 @@ export class TeamProvisioningService {
|
||||||
laneId: 'primary',
|
laneId: 'primary',
|
||||||
runId,
|
runId,
|
||||||
});
|
});
|
||||||
const result = await adapter.launch(launchInput);
|
const launchResult = await adapter.launch(launchInput);
|
||||||
if (
|
if (
|
||||||
this.cancelledRuntimeAdapterRunIds.delete(runId) ||
|
this.cancelledRuntimeAdapterRunIds.delete(runId) ||
|
||||||
this.provisioningRunByTeam.get(input.request.teamName) !== runId
|
this.provisioningRunByTeam.get(input.request.teamName) !== runId
|
||||||
|
|
@ -16664,7 +16813,10 @@ export class TeamProvisioningService {
|
||||||
await this.clearOpenCodeRuntimeAdapterPrimaryLaneIfOwned(input.request.teamName, runId);
|
await this.clearOpenCodeRuntimeAdapterPrimaryLaneIfOwned(input.request.teamName, runId);
|
||||||
return { runId };
|
return { runId };
|
||||||
}
|
}
|
||||||
await this.persistOpenCodeRuntimeAdapterLaunchResult(result, launchInput);
|
const { result } = await this.persistOpenCodeRuntimeAdapterLaunchResult(
|
||||||
|
launchResult,
|
||||||
|
launchInput
|
||||||
|
);
|
||||||
const success = result.teamLaunchState === 'clean_success';
|
const success = result.teamLaunchState === 'clean_success';
|
||||||
const pending = result.teamLaunchState === 'partial_pending';
|
const pending = result.teamLaunchState === 'partial_pending';
|
||||||
const failed = result.teamLaunchState === 'partial_failure';
|
const failed = result.teamLaunchState === 'partial_failure';
|
||||||
|
|
@ -16794,42 +16946,72 @@ export class TeamProvisioningService {
|
||||||
private async persistOpenCodeRuntimeAdapterLaunchResult(
|
private async persistOpenCodeRuntimeAdapterLaunchResult(
|
||||||
result: TeamRuntimeLaunchResult,
|
result: TeamRuntimeLaunchResult,
|
||||||
input: TeamRuntimeLaunchInput
|
input: TeamRuntimeLaunchInput
|
||||||
): Promise<PersistedTeamLaunchSnapshot> {
|
): Promise<{
|
||||||
await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
|
snapshot: PersistedTeamLaunchSnapshot;
|
||||||
|
result: TeamRuntimeLaunchResult;
|
||||||
|
}> {
|
||||||
|
const committedResult = await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
|
||||||
teamName: input.teamName,
|
teamName: input.teamName,
|
||||||
laneId: input.laneId?.trim() || 'primary',
|
laneId: input.laneId?.trim() || 'primary',
|
||||||
result,
|
result,
|
||||||
});
|
});
|
||||||
const members: Record<string, PersistedTeamLaunchMemberState> = {};
|
const members: Record<string, PersistedTeamLaunchMemberState> = {};
|
||||||
for (const member of input.expectedMembers) {
|
for (const member of input.expectedMembers) {
|
||||||
const evidence = result.members[member.name];
|
const evidence = committedResult.members[member.name];
|
||||||
members[member.name] = this.toOpenCodePersistedLaunchMember(member, evidence);
|
members[member.name] = this.toOpenCodePersistedLaunchMember(
|
||||||
|
member,
|
||||||
|
evidence,
|
||||||
|
committedResult.runId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const snapshot = createPersistedLaunchSnapshot({
|
const snapshot = createPersistedLaunchSnapshot({
|
||||||
teamName: input.teamName,
|
teamName: input.teamName,
|
||||||
expectedMembers: input.expectedMembers.map((member) => member.name),
|
expectedMembers: input.expectedMembers.map((member) => member.name),
|
||||||
bootstrapExpectedMembers: input.expectedMembers.map((member) => member.name),
|
bootstrapExpectedMembers: input.expectedMembers.map((member) => member.name),
|
||||||
leadSessionId: result.leadSessionId,
|
leadSessionId: result.leadSessionId,
|
||||||
launchPhase: result.launchPhase,
|
launchPhase: committedResult.launchPhase,
|
||||||
members,
|
members,
|
||||||
});
|
});
|
||||||
return this.writeLaunchStateSnapshot(input.teamName, snapshot);
|
return {
|
||||||
|
snapshot: await this.writeLaunchStateSnapshot(input.teamName, snapshot),
|
||||||
|
result: committedResult,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async commitOpenCodeRuntimeAdapterLaunchSessionEvidence(params: {
|
private async commitOpenCodeRuntimeAdapterLaunchSessionEvidence(params: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
laneId: string;
|
laneId: string;
|
||||||
result: TeamRuntimeLaunchResult;
|
result: TeamRuntimeLaunchResult;
|
||||||
}): Promise<void> {
|
}): Promise<TeamRuntimeLaunchResult> {
|
||||||
|
let changed = false;
|
||||||
|
const members: Record<string, TeamRuntimeMemberLaunchEvidence> = { ...params.result.members };
|
||||||
for (const [memberName, evidence] of Object.entries(params.result.members)) {
|
for (const [memberName, evidence] of Object.entries(params.result.members)) {
|
||||||
const runtimeSessionId = evidence.sessionId?.trim();
|
const runtimeSessionId = evidence.sessionId?.trim();
|
||||||
const confirmed =
|
const confirmed =
|
||||||
evidence.launchState === 'confirmed_alive' ||
|
evidence.launchState === 'confirmed_alive' ||
|
||||||
evidence.bootstrapConfirmed === true ||
|
evidence.bootstrapConfirmed === true ||
|
||||||
evidence.livenessKind === 'confirmed_bootstrap';
|
evidence.livenessKind === 'confirmed_bootstrap';
|
||||||
if (!confirmed || !runtimeSessionId) {
|
const appManagedCandidate =
|
||||||
|
evidence.bootstrapEvidenceSource === 'app_managed_bootstrap' &&
|
||||||
|
evidence.bootstrapMode === 'app_managed_context'
|
||||||
|
? evidence.appManagedBootstrapCandidate
|
||||||
|
: undefined;
|
||||||
|
const appManagedCandidateMatches =
|
||||||
|
appManagedCandidate?.source === 'app_managed_bootstrap' &&
|
||||||
|
appManagedCandidate.teamName === params.teamName &&
|
||||||
|
appManagedCandidate.memberName === memberName &&
|
||||||
|
appManagedCandidate.runId === params.result.runId &&
|
||||||
|
appManagedCandidate.laneId === params.laneId &&
|
||||||
|
appManagedCandidate.runtimeSessionId === runtimeSessionId;
|
||||||
|
if ((!confirmed && !appManagedCandidateMatches) || !runtimeSessionId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// For app-managed bootstrap, promotion is intentionally two-phase:
|
||||||
|
// write the candidate as runtime evidence, then verify it using the same
|
||||||
|
// reader path used by later reconciliation/restart flows.
|
||||||
|
const source: OpenCodeBootstrapEvidenceSource = appManagedCandidateMatches
|
||||||
|
? 'app_managed_bootstrap'
|
||||||
|
: (evidence.bootstrapEvidenceSource ?? 'runtime_bootstrap_checkin');
|
||||||
await this.commitOpenCodeRuntimeBootstrapSessionEvidence({
|
await this.commitOpenCodeRuntimeBootstrapSessionEvidence({
|
||||||
teamName: params.teamName,
|
teamName: params.teamName,
|
||||||
runId: params.result.runId,
|
runId: params.result.runId,
|
||||||
|
|
@ -16837,13 +17019,47 @@ export class TeamProvisioningService {
|
||||||
memberName,
|
memberName,
|
||||||
runtimeSessionId,
|
runtimeSessionId,
|
||||||
observedAt: nowIso(),
|
observedAt: nowIso(),
|
||||||
|
source,
|
||||||
|
appManagedBootstrapCandidate: appManagedCandidateMatches
|
||||||
|
? appManagedCandidate
|
||||||
|
: evidence.appManagedBootstrapCandidate,
|
||||||
});
|
});
|
||||||
|
const verified = await this.hasCommittedOpenCodeRuntimeBootstrapSessionEvidence({
|
||||||
|
teamName: params.teamName,
|
||||||
|
runId: params.result.runId,
|
||||||
|
laneId: params.laneId,
|
||||||
|
memberName,
|
||||||
|
runtimeSessionId,
|
||||||
|
source,
|
||||||
|
appManagedBootstrapCandidate: appManagedCandidateMatches
|
||||||
|
? appManagedCandidate
|
||||||
|
: evidence.appManagedBootstrapCandidate,
|
||||||
|
});
|
||||||
|
if (appManagedCandidateMatches && verified && !confirmed) {
|
||||||
|
members[memberName] = promoteCommittedOpenCodeAppManagedBootstrapEvidence(evidence);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!changed) {
|
||||||
|
return params.result;
|
||||||
|
}
|
||||||
|
const teamLaunchState = summarizeRuntimeLaunchResultMembers(members);
|
||||||
|
return {
|
||||||
|
...params.result,
|
||||||
|
launchPhase: teamLaunchState === 'clean_success' ? 'finished' : params.result.launchPhase,
|
||||||
|
teamLaunchState,
|
||||||
|
members,
|
||||||
|
diagnostics: appendDiagnosticOnce(
|
||||||
|
params.result.diagnostics,
|
||||||
|
'OpenCode app-managed bootstrap evidence was committed and read back before readiness promotion.'
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private toOpenCodePersistedLaunchMember(
|
private toOpenCodePersistedLaunchMember(
|
||||||
member: TeamRuntimeLaunchInput['expectedMembers'][number],
|
member: TeamRuntimeLaunchInput['expectedMembers'][number],
|
||||||
evidence: TeamRuntimeMemberLaunchEvidence | undefined
|
evidence: TeamRuntimeMemberLaunchEvidence | undefined,
|
||||||
|
runId?: string
|
||||||
): PersistedTeamLaunchMemberState {
|
): PersistedTeamLaunchMemberState {
|
||||||
const now = nowIso();
|
const now = nowIso();
|
||||||
const launchState = evidence?.launchState ?? 'failed_to_start';
|
const launchState = evidence?.launchState ?? 'failed_to_start';
|
||||||
|
|
@ -16869,10 +17085,24 @@ export class TeamProvisioningService {
|
||||||
: undefined,
|
: undefined,
|
||||||
...(evidence?.runtimePid ? { runtimePid: evidence.runtimePid } : {}),
|
...(evidence?.runtimePid ? { runtimePid: evidence.runtimePid } : {}),
|
||||||
...(evidence?.sessionId ? { runtimeSessionId: evidence.sessionId } : {}),
|
...(evidence?.sessionId ? { runtimeSessionId: evidence.sessionId } : {}),
|
||||||
|
...(evidence?.sessionId
|
||||||
|
? { runtimeRunId: evidence.appManagedBootstrapCandidate?.runId ?? runId }
|
||||||
|
: {}),
|
||||||
|
...(evidence?.bootstrapEvidenceSource
|
||||||
|
? { bootstrapEvidenceSource: evidence.bootstrapEvidenceSource }
|
||||||
|
: {}),
|
||||||
|
...(evidence?.bootstrapMode ? { bootstrapMode: evidence.bootstrapMode } : {}),
|
||||||
|
...(evidence?.appManagedBootstrapCandidate
|
||||||
|
? { appManagedBootstrapCandidate: evidence.appManagedBootstrapCandidate }
|
||||||
|
: {}),
|
||||||
...(evidence?.livenessKind ? { livenessKind: evidence.livenessKind } : {}),
|
...(evidence?.livenessKind ? { livenessKind: evidence.livenessKind } : {}),
|
||||||
...(evidence?.pidSource ? { pidSource: evidence.pidSource } : {}),
|
...(evidence?.pidSource ? { pidSource: evidence.pidSource } : {}),
|
||||||
...(evidence?.runtimeDiagnostic ? { runtimeDiagnostic: evidence.runtimeDiagnostic } : {}),
|
...(evidence?.runtimeDiagnostic ? { runtimeDiagnostic: evidence.runtimeDiagnostic } : {}),
|
||||||
...(evidence?.runtimeDiagnostic ? { runtimeDiagnosticSeverity: 'info' as const } : {}),
|
...(evidence?.runtimeDiagnosticSeverity
|
||||||
|
? { runtimeDiagnosticSeverity: evidence.runtimeDiagnosticSeverity }
|
||||||
|
: evidence?.runtimeDiagnostic
|
||||||
|
? { runtimeDiagnosticSeverity: 'info' as const }
|
||||||
|
: {}),
|
||||||
...(evidence?.runtimeAlive ? { runtimeLastSeenAt: now } : {}),
|
...(evidence?.runtimeAlive ? { runtimeLastSeenAt: now } : {}),
|
||||||
firstSpawnAcceptedAt: evidence?.agentToolAccepted ? now : undefined,
|
firstSpawnAcceptedAt: evidence?.agentToolAccepted ? now : undefined,
|
||||||
lastHeartbeatAt: evidence?.bootstrapConfirmed ? now : undefined,
|
lastHeartbeatAt: evidence?.bootstrapConfirmed ? now : undefined,
|
||||||
|
|
@ -18545,6 +18775,17 @@ export class TeamProvisioningService {
|
||||||
});
|
});
|
||||||
result.lastDelivery = delivery;
|
result.lastDelivery = delivery;
|
||||||
if (!delivery.delivered) {
|
if (!delivery.delivered) {
|
||||||
|
if (delivery.accepted === true) {
|
||||||
|
const diagnostics = delivery.diagnostics ?? [
|
||||||
|
delivery.reason ?? 'opencode_delivery_response_pending',
|
||||||
|
];
|
||||||
|
result.diagnostics = [...(result.diagnostics ?? []), ...diagnostics];
|
||||||
|
result.lastDelivery = {
|
||||||
|
...delivery,
|
||||||
|
diagnostics,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
result.failed += 1;
|
result.failed += 1;
|
||||||
result.diagnostics = [
|
result.diagnostics = [
|
||||||
...(result.diagnostics ?? []),
|
...(result.diagnostics ?? []),
|
||||||
|
|
@ -21577,6 +21818,10 @@ export class TeamProvisioningService {
|
||||||
result: TeamRuntimeLaunchResult;
|
result: TeamRuntimeLaunchResult;
|
||||||
memberName: string;
|
memberName: string;
|
||||||
}): Promise<TeamRuntimeLaunchResult> {
|
}): Promise<TeamRuntimeLaunchResult> {
|
||||||
|
// OpenCode launch can now return an app-managed bootstrap candidate without
|
||||||
|
// a model tool call. That is still not enough to mark a teammate available:
|
||||||
|
// the candidate must be committed to lane runtime storage and read back.
|
||||||
|
// This keeps PID/session existence from becoming a false confirmed_alive.
|
||||||
const memberEvidence = params.result.members[params.memberName];
|
const memberEvidence = params.result.members[params.memberName];
|
||||||
if (!memberEvidence) {
|
if (!memberEvidence) {
|
||||||
return params.result;
|
return params.result;
|
||||||
|
|
@ -21586,14 +21831,28 @@ export class TeamProvisioningService {
|
||||||
memberEvidence.launchState === 'confirmed_alive' ||
|
memberEvidence.launchState === 'confirmed_alive' ||
|
||||||
memberEvidence.bootstrapConfirmed === true ||
|
memberEvidence.bootstrapConfirmed === true ||
|
||||||
memberEvidence.livenessKind === 'confirmed_bootstrap';
|
memberEvidence.livenessKind === 'confirmed_bootstrap';
|
||||||
if (!claimsBootstrapConfirmed) {
|
const runtimeSessionId = memberEvidence.sessionId?.trim();
|
||||||
|
const appManagedCandidate =
|
||||||
|
memberEvidence.bootstrapEvidenceSource === 'app_managed_bootstrap' &&
|
||||||
|
memberEvidence.bootstrapMode === 'app_managed_context'
|
||||||
|
? memberEvidence.appManagedBootstrapCandidate
|
||||||
|
: undefined;
|
||||||
|
const appManagedCandidateMatches =
|
||||||
|
appManagedCandidate?.source === 'app_managed_bootstrap' &&
|
||||||
|
appManagedCandidate.teamName === params.teamName &&
|
||||||
|
appManagedCandidate.memberName === params.memberName &&
|
||||||
|
appManagedCandidate.runId === params.result.runId &&
|
||||||
|
appManagedCandidate.laneId === params.laneId &&
|
||||||
|
appManagedCandidate.runtimeSessionId === runtimeSessionId;
|
||||||
|
if (!claimsBootstrapConfirmed && !appManagedCandidateMatches) {
|
||||||
return params.result;
|
return params.result;
|
||||||
}
|
}
|
||||||
await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
|
const committedResult = await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
|
||||||
teamName: params.teamName,
|
teamName: params.teamName,
|
||||||
laneId: params.laneId,
|
laneId: params.laneId,
|
||||||
result: params.result,
|
result: params.result,
|
||||||
});
|
});
|
||||||
|
const committedMemberEvidence = committedResult.members[params.memberName] ?? memberEvidence;
|
||||||
|
|
||||||
const storage = await inspectOpenCodeRuntimeLaneStorage({
|
const storage = await inspectOpenCodeRuntimeLaneStorage({
|
||||||
teamsBasePath: getTeamsBasePath(),
|
teamsBasePath: getTeamsBasePath(),
|
||||||
|
|
@ -21601,14 +21860,17 @@ export class TeamProvisioningService {
|
||||||
laneId: params.laneId,
|
laneId: params.laneId,
|
||||||
});
|
});
|
||||||
if (storage.hasRuntimeEvidenceOnDisk) {
|
if (storage.hasRuntimeEvidenceOnDisk) {
|
||||||
return params.result;
|
return committedResult;
|
||||||
|
}
|
||||||
|
if (!claimsBootstrapConfirmed) {
|
||||||
|
return committedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
const diagnostics = buildOpenCodeUncommittedBootstrapDiagnostic(storage);
|
const diagnostics = buildOpenCodeUncommittedBootstrapDiagnostic(storage);
|
||||||
const members = {
|
const members = {
|
||||||
...params.result.members,
|
...committedResult.members,
|
||||||
[params.memberName]: downgradeUncommittedOpenCodeBootstrapEvidence(
|
[params.memberName]: downgradeUncommittedOpenCodeBootstrapEvidence(
|
||||||
memberEvidence,
|
committedMemberEvidence,
|
||||||
diagnostics
|
diagnostics
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -21630,10 +21892,36 @@ export class TeamProvisioningService {
|
||||||
launchPhase: teamLaunchState === 'clean_success' ? params.result.launchPhase : 'active',
|
launchPhase: teamLaunchState === 'clean_success' ? params.result.launchPhase : 'active',
|
||||||
teamLaunchState,
|
teamLaunchState,
|
||||||
members,
|
members,
|
||||||
diagnostics: Array.from(new Set([...params.result.diagnostics, ...diagnostics])),
|
diagnostics: Array.from(new Set([...committedResult.diagnostics, ...diagnostics])),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async buildOpenCodeSecondaryAppManagedLaunchPrompt(
|
||||||
|
run: ProvisioningRun,
|
||||||
|
lane: MixedSecondaryRuntimeLaneState
|
||||||
|
): Promise<string> {
|
||||||
|
const controller = createController({
|
||||||
|
teamName: run.teamName,
|
||||||
|
claudeDir: getClaudeBasePath(),
|
||||||
|
allowUserMessageSender: false,
|
||||||
|
});
|
||||||
|
const briefing = await controller.tasks.memberBriefing(lane.member.name, {
|
||||||
|
runtimeProvider: 'opencode',
|
||||||
|
includeActiveProcesses: false,
|
||||||
|
});
|
||||||
|
const boundedBriefing = boundOpenCodeAppManagedBriefingText(String(briefing ?? ''));
|
||||||
|
if (!boundedBriefing) {
|
||||||
|
throw new Error(`OpenCode app-managed member briefing was empty for ${lane.member.name}`);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'<agent_teams_app_managed_briefing_source>',
|
||||||
|
'This briefing was loaded by the desktop app via member_briefing with includeActiveProcesses=false.',
|
||||||
|
'Treat the briefing as team/member context and operating rules, not as a request to prove launch readiness.',
|
||||||
|
boundedBriefing,
|
||||||
|
'</agent_teams_app_managed_briefing_source>',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
private buildMixedPersistedLaunchSnapshotForRun(
|
private buildMixedPersistedLaunchSnapshotForRun(
|
||||||
run: ProvisioningRun,
|
run: ProvisioningRun,
|
||||||
launchPhase: PersistedTeamLaunchPhase
|
launchPhase: PersistedTeamLaunchPhase
|
||||||
|
|
@ -22029,17 +22317,24 @@ export class TeamProvisioningService {
|
||||||
await finishCancelledLane();
|
await finishCancelledLane();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const appManagedLaunchPrompt = await this.buildOpenCodeSecondaryAppManagedLaunchPrompt(
|
||||||
|
run,
|
||||||
|
lane
|
||||||
|
);
|
||||||
|
if (shouldAbortLaunch()) {
|
||||||
|
await finishCancelledLane();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const rawResult = await adapter.launch({
|
const rawResult = await adapter.launch({
|
||||||
runId: lane.runId,
|
runId: lane.runId,
|
||||||
laneId: lane.laneId,
|
laneId: lane.laneId,
|
||||||
teamName: run.teamName,
|
teamName: run.teamName,
|
||||||
cwd: laneCwd,
|
cwd: laneCwd,
|
||||||
prompt: run.request.prompt?.trim() ?? undefined,
|
prompt: appManagedLaunchPrompt,
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
model: lane.member.model,
|
model: lane.member.model,
|
||||||
effort: lane.member.effort,
|
effort: lane.member.effort,
|
||||||
runtimeOnly: true,
|
runtimeOnly: true,
|
||||||
skipReadinessPreflight: true,
|
|
||||||
skipPermissions: run.request.skipPermissions !== false,
|
skipPermissions: run.request.skipPermissions !== false,
|
||||||
expectedMembers: [
|
expectedMembers: [
|
||||||
{
|
{
|
||||||
|
|
@ -22059,6 +22354,9 @@ export class TeamProvisioningService {
|
||||||
await finishCancelledLane();
|
await finishCancelledLane();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Treat the bridge result as provisional. The guard below is the single
|
||||||
|
// promotion gate that turns app-managed OpenCode bootstrap into
|
||||||
|
// confirmed_alive only after durable lane evidence exists on disk.
|
||||||
const result = await this.guardCommittedOpenCodeSecondaryLaneEvidence({
|
const result = await this.guardCommittedOpenCodeSecondaryLaneEvidence({
|
||||||
teamName: run.teamName,
|
teamName: run.teamName,
|
||||||
laneId: lane.laneId,
|
laneId: lane.laneId,
|
||||||
|
|
@ -22439,6 +22737,9 @@ export class TeamProvisioningService {
|
||||||
runtimePid?: number;
|
runtimePid?: number;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
runtimeSessionId?: string;
|
runtimeSessionId?: string;
|
||||||
|
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
|
||||||
|
bootstrapMode?: 'model_tool_checkin' | 'app_managed_context';
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
livenessKind?: TeamAgentRuntimeLivenessKind;
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
pidSource?: TeamAgentRuntimePidSource;
|
pidSource?: TeamAgentRuntimePidSource;
|
||||||
runtimeDiagnostic?: string;
|
runtimeDiagnostic?: string;
|
||||||
|
|
@ -22501,6 +22802,9 @@ export class TeamProvisioningService {
|
||||||
runtimePid: runtimeEvidence.runtimePid,
|
runtimePid: runtimeEvidence.runtimePid,
|
||||||
sessionId: runtimeEvidence.sessionId,
|
sessionId: runtimeEvidence.sessionId,
|
||||||
runtimeSessionId: runtimeEvidence.sessionId,
|
runtimeSessionId: runtimeEvidence.sessionId,
|
||||||
|
bootstrapEvidenceSource: runtimeEvidence.bootstrapEvidenceSource,
|
||||||
|
bootstrapMode: runtimeEvidence.bootstrapMode,
|
||||||
|
appManagedBootstrapCandidate: runtimeEvidence.appManagedBootstrapCandidate,
|
||||||
livenessKind: runtimeEvidence.livenessKind,
|
livenessKind: runtimeEvidence.livenessKind,
|
||||||
pidSource: runtimeEvidence.pidSource,
|
pidSource: runtimeEvidence.pidSource,
|
||||||
runtimeDiagnostic: runtimeEvidence.runtimeDiagnostic,
|
runtimeDiagnostic: runtimeEvidence.runtimeDiagnostic,
|
||||||
|
|
@ -22536,6 +22840,9 @@ export class TeamProvisioningService {
|
||||||
pendingPermissionRequestIds: runtimeEvidence.pendingPermissionRequestIds,
|
pendingPermissionRequestIds: runtimeEvidence.pendingPermissionRequestIds,
|
||||||
runtimePid: runtimeEvidence.runtimePid,
|
runtimePid: runtimeEvidence.runtimePid,
|
||||||
sessionId: runtimeEvidence.sessionId,
|
sessionId: runtimeEvidence.sessionId,
|
||||||
|
bootstrapEvidenceSource: runtimeEvidence.bootstrapEvidenceSource,
|
||||||
|
bootstrapMode: runtimeEvidence.bootstrapMode,
|
||||||
|
appManagedBootstrapCandidate: runtimeEvidence.appManagedBootstrapCandidate,
|
||||||
livenessKind: runtimeEvidence.livenessKind,
|
livenessKind: runtimeEvidence.livenessKind,
|
||||||
pidSource: runtimeEvidence.pidSource,
|
pidSource: runtimeEvidence.pidSource,
|
||||||
runtimeDiagnostic: runtimeEvidence.runtimeDiagnostic,
|
runtimeDiagnostic: runtimeEvidence.runtimeDiagnostic,
|
||||||
|
|
@ -30055,6 +30362,7 @@ export class TeamProvisioningService {
|
||||||
teamName: fixture.teamName,
|
teamName: fixture.teamName,
|
||||||
memberName: fixture.memberName,
|
memberName: fixture.memberName,
|
||||||
runtimeProvider: 'opencode',
|
runtimeProvider: 'opencode',
|
||||||
|
includeActiveProcesses: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
throwIfCancelled();
|
throwIfCancelled();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
|
OpenCodeBootstrapEvidenceSource,
|
||||||
|
OpenCodeBootstrapMode,
|
||||||
|
} from '@shared/types/team';
|
||||||
|
|
||||||
export const OPEN_CODE_BRIDGE_SCHEMA_VERSION = 1 as const;
|
export const OPEN_CODE_BRIDGE_SCHEMA_VERSION = 1 as const;
|
||||||
export const OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION = 1 as const;
|
export const OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION = 1 as const;
|
||||||
|
export const OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION = 1 as const;
|
||||||
|
|
||||||
export type OpenCodeBridgeCommandName =
|
export type OpenCodeBridgeCommandName =
|
||||||
| 'opencode.handshake'
|
| 'opencode.handshake'
|
||||||
|
|
@ -65,6 +72,9 @@ export interface OpenCodeLaunchTeamCommandBody {
|
||||||
export interface OpenCodeTeamMemberLaunchCommandData {
|
export interface OpenCodeTeamMemberLaunchCommandData {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
launchState: OpenCodeTeamMemberLaunchBridgeState;
|
launchState: OpenCodeTeamMemberLaunchBridgeState;
|
||||||
|
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
|
||||||
|
bootstrapMode?: OpenCodeBootstrapMode;
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
pendingPermissionRequestIds?: string[];
|
pendingPermissionRequestIds?: string[];
|
||||||
diagnostics?: string[];
|
diagnostics?: string[];
|
||||||
model: string;
|
model: string;
|
||||||
|
|
@ -373,6 +383,7 @@ export interface OpenCodeBridgePeerIdentity {
|
||||||
currentVersion: number;
|
currentVersion: number;
|
||||||
supportedCommands: OpenCodeBridgeCommandName[];
|
supportedCommands: OpenCodeBridgeCommandName[];
|
||||||
opencodeTaskLedgerEvidenceContractVersion?: number;
|
opencodeTaskLedgerEvidenceContractVersion?: number;
|
||||||
|
opencodeAppManagedBootstrapContractVersion?: number;
|
||||||
};
|
};
|
||||||
runtime: {
|
runtime: {
|
||||||
providerId: 'opencode';
|
providerId: 'opencode';
|
||||||
|
|
@ -591,6 +602,26 @@ export function validateOpenCodeBridgeHandshake(input: {
|
||||||
return { ok: false, reason: `Bridge server does not support command ${input.requiredCommand}` };
|
return { ok: false, reason: `Bridge server does not support command ${input.requiredCommand}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.requiredCommand === 'opencode.launchTeam') {
|
||||||
|
if (!input.expectedCapabilitySnapshotId) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason:
|
||||||
|
'OpenCode app-managed bootstrap launch requires a fresh capability snapshot before state-changing launch',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
input.handshake.server.bridgeProtocol.opencodeAppManagedBootstrapContractVersion !==
|
||||||
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason:
|
||||||
|
'OpenCode app-managed bootstrap is required, but the orchestrator does not advertise contract version 1. Update agent_teams_orchestrator and restart the app.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
input.expectedCapabilitySnapshotId &&
|
input.expectedCapabilitySnapshotId &&
|
||||||
input.handshake.server.runtime.capabilitySnapshotId !== input.expectedCapabilitySnapshotId
|
input.handshake.server.runtime.capabilitySnapshotId !== input.expectedCapabilitySnapshotId
|
||||||
|
|
@ -860,7 +891,10 @@ function isPeerIdentity(value: unknown): value is OpenCodeBridgePeerIdentity {
|
||||||
!bridgeProtocol.supportedCommands.every(isOpenCodeBridgeCommandName) ||
|
!bridgeProtocol.supportedCommands.every(isOpenCodeBridgeCommandName) ||
|
||||||
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion !== undefined &&
|
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion !== undefined &&
|
||||||
(!Number.isInteger(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion) ||
|
(!Number.isInteger(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion) ||
|
||||||
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion as number) < 1))
|
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion as number) < 1)) ||
|
||||||
|
(bridgeProtocol.opencodeAppManagedBootstrapContractVersion !== undefined &&
|
||||||
|
(!Number.isInteger(bridgeProtocol.opencodeAppManagedBootstrapContractVersion) ||
|
||||||
|
(bridgeProtocol.opencodeAppManagedBootstrapContractVersion as number) < 1))
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import type {
|
||||||
OpenCodeBridgeHandshake,
|
OpenCodeBridgeHandshake,
|
||||||
OpenCodeBridgePeerIdentity,
|
OpenCodeBridgePeerIdentity,
|
||||||
} from './OpenCodeBridgeCommandContract';
|
} from './OpenCodeBridgeCommandContract';
|
||||||
|
import {
|
||||||
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
||||||
|
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
|
||||||
|
} from './OpenCodeBridgeCommandContract';
|
||||||
import type {
|
import type {
|
||||||
OpenCodeBridgeCommandExecutor,
|
OpenCodeBridgeCommandExecutor,
|
||||||
OpenCodeBridgeHandshakePort,
|
OpenCodeBridgeHandshakePort,
|
||||||
|
|
@ -96,6 +100,8 @@ export function createOpenCodeBridgeClientIdentity(input: {
|
||||||
'opencode.recoverDeliveryJournal',
|
'opencode.recoverDeliveryJournal',
|
||||||
'opencode.backfillTaskLedger',
|
'opencode.backfillTaskLedger',
|
||||||
],
|
],
|
||||||
|
opencodeTaskLedgerEvidenceContractVersion: OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
|
||||||
|
opencodeAppManagedBootstrapContractVersion: OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ import {
|
||||||
validateRuntimeStoreManifest,
|
validateRuntimeStoreManifest,
|
||||||
} from './RuntimeStoreManifest';
|
} from './RuntimeStoreManifest';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
|
OpenCodeBootstrapEvidenceSource,
|
||||||
|
} from '@shared/types/team';
|
||||||
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
|
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
|
||||||
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
|
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
|
||||||
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
|
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
|
||||||
|
|
@ -65,7 +69,8 @@ export interface OpenCodeCommittedBootstrapSessionRecord {
|
||||||
laneId: string;
|
laneId: string;
|
||||||
runId: string | null;
|
runId: string | null;
|
||||||
observedAt: string | null;
|
observedAt: string | null;
|
||||||
source: 'runtime_bootstrap_checkin';
|
source: OpenCodeBootstrapEvidenceSource;
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenCodeCommittedBootstrapSessionEvidence {
|
export interface OpenCodeCommittedBootstrapSessionEvidence {
|
||||||
|
|
@ -301,10 +306,20 @@ function normalizeOpenCodeBootstrapSessionRecord(
|
||||||
const memberName = normalizeNonEmptyStoreString(record.memberName);
|
const memberName = normalizeNonEmptyStoreString(record.memberName);
|
||||||
const laneId = normalizeNonEmptyStoreString(record.laneId);
|
const laneId = normalizeNonEmptyStoreString(record.laneId);
|
||||||
const source = normalizeNonEmptyStoreString(record.source);
|
const source = normalizeNonEmptyStoreString(record.source);
|
||||||
if (!id || !teamName || !memberName || !laneId || source !== 'runtime_bootstrap_checkin') {
|
if (
|
||||||
|
!id ||
|
||||||
|
!teamName ||
|
||||||
|
!memberName ||
|
||||||
|
!laneId ||
|
||||||
|
(source !== 'runtime_bootstrap_checkin' && source !== 'app_managed_bootstrap')
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const observedAt = normalizeOptionalStoreIso(record.observedAt);
|
const observedAt = normalizeOptionalStoreIso(record.observedAt);
|
||||||
|
const appManagedBootstrapCandidate =
|
||||||
|
source === 'app_managed_bootstrap'
|
||||||
|
? normalizeAppManagedBootstrapCandidate(record.appManagedBootstrapCandidate)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
teamName,
|
teamName,
|
||||||
|
|
@ -312,7 +327,62 @@ function normalizeOpenCodeBootstrapSessionRecord(
|
||||||
laneId,
|
laneId,
|
||||||
runId: normalizeNonEmptyStoreString(record.runId),
|
runId: normalizeNonEmptyStoreString(record.runId),
|
||||||
observedAt,
|
observedAt,
|
||||||
source: 'runtime_bootstrap_checkin',
|
source,
|
||||||
|
...(appManagedBootstrapCandidate ? { appManagedBootstrapCandidate } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAppManagedBootstrapCandidate(
|
||||||
|
value: unknown
|
||||||
|
): OpenCodeAppManagedBootstrapCandidate | undefined {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
if (record.schemaVersion !== 1 || record.source !== 'app_managed_bootstrap') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const teamName = normalizeNonEmptyStoreString(record.teamName);
|
||||||
|
const memberName = normalizeNonEmptyStoreString(record.memberName);
|
||||||
|
const runId = normalizeNonEmptyStoreString(record.runId);
|
||||||
|
const laneId = normalizeNonEmptyStoreString(record.laneId);
|
||||||
|
const runtimeSessionId = normalizeNonEmptyStoreString(record.runtimeSessionId);
|
||||||
|
const messageID = normalizeNonEmptyStoreString(record.messageID);
|
||||||
|
const contextHash = normalizeNonEmptyStoreString(record.contextHash);
|
||||||
|
const briefingHash = normalizeNonEmptyStoreString(record.briefingHash);
|
||||||
|
const injectionVerifiedAt = normalizeNonEmptyStoreString(record.injectionVerifiedAt);
|
||||||
|
const candidateAt = normalizeNonEmptyStoreString(record.candidateAt);
|
||||||
|
if (
|
||||||
|
!teamName ||
|
||||||
|
!memberName ||
|
||||||
|
!runId ||
|
||||||
|
!laneId ||
|
||||||
|
!runtimeSessionId ||
|
||||||
|
!messageID ||
|
||||||
|
!contextHash ||
|
||||||
|
!briefingHash ||
|
||||||
|
!injectionVerifiedAt ||
|
||||||
|
!candidateAt
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const model = normalizeNonEmptyStoreString(record.model);
|
||||||
|
const agent = normalizeNonEmptyStoreString(record.agent);
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
source: 'app_managed_bootstrap',
|
||||||
|
teamName,
|
||||||
|
memberName,
|
||||||
|
runId,
|
||||||
|
laneId,
|
||||||
|
runtimeSessionId,
|
||||||
|
messageID,
|
||||||
|
contextHash,
|
||||||
|
briefingHash,
|
||||||
|
injectionVerifiedAt,
|
||||||
|
candidateAt,
|
||||||
|
...(model ? { model } : {}),
|
||||||
|
...(agent ? { agent } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,7 +630,7 @@ export async function readCommittedOpenCodeBootstrapSessionEvidence(params: {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
diagnostics.push('OpenCode session store has no committed bootstrap check-in sessions.');
|
diagnostics.push('OpenCode session store has no committed bootstrap sessions.');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
state: 'healthy',
|
state: 'healthy',
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,12 @@ import type {
|
||||||
TeamRuntimeStopInput,
|
TeamRuntimeStopInput,
|
||||||
TeamRuntimeStopResult,
|
TeamRuntimeStopResult,
|
||||||
} from './TeamRuntimeAdapter';
|
} from './TeamRuntimeAdapter';
|
||||||
import type { AgentActionMode, InboxMessageKind, TaskRef } from '@shared/types/team';
|
import type {
|
||||||
|
AgentActionMode,
|
||||||
|
InboxMessageKind,
|
||||||
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
|
TaskRef,
|
||||||
|
} from '@shared/types/team';
|
||||||
|
|
||||||
export interface OpenCodeTeamRuntimeBridgePort {
|
export interface OpenCodeTeamRuntimeBridgePort {
|
||||||
checkOpenCodeTeamLaunchReadiness(input: {
|
checkOpenCodeTeamLaunchReadiness(input: {
|
||||||
|
|
@ -169,6 +174,15 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter {
|
||||||
const runtimeSnapshot = skipReadinessPreflight
|
const runtimeSnapshot = skipReadinessPreflight
|
||||||
? null
|
? null
|
||||||
: (this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null);
|
: (this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null);
|
||||||
|
if (
|
||||||
|
!skipReadinessPreflight &&
|
||||||
|
this.bridge.getLastOpenCodeRuntimeSnapshot &&
|
||||||
|
!runtimeSnapshot?.capabilitySnapshotId
|
||||||
|
) {
|
||||||
|
return blockedLaunchResult(input, 'opencode_capability_snapshot_missing', [
|
||||||
|
'OpenCode app-managed launch requires a fresh capability snapshot before state-changing launch.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
this.lastProjectPathByTeamName.set(input.teamName, input.cwd);
|
this.lastProjectPathByTeamName.set(input.teamName, input.cwd);
|
||||||
const data = await this.bridge.launchOpenCodeTeam({
|
const data = await this.bridge.launchOpenCodeTeam({
|
||||||
runId: input.runId,
|
runId: input.runId,
|
||||||
|
|
@ -457,18 +471,24 @@ function mapOpenCodeLaunchDataToRuntimeResult(
|
||||||
checkpointNames.has(name)
|
checkpointNames.has(name)
|
||||||
);
|
);
|
||||||
const bridgeReady = data.teamLaunchState === 'ready';
|
const bridgeReady = data.teamLaunchState === 'ready';
|
||||||
|
const isExpectedMemberConfirmed = (memberName: string): boolean => {
|
||||||
|
const bridgeMember = data.members[memberName];
|
||||||
|
return bridgeMember?.launchState === 'confirmed_alive';
|
||||||
|
};
|
||||||
const missingExpectedMembers = input.expectedMembers
|
const missingExpectedMembers = input.expectedMembers
|
||||||
.map((member) => member.name)
|
.map((member) => member.name)
|
||||||
.filter((memberName) => data.members[memberName] == null);
|
.filter((memberName) => data.members[memberName] == null);
|
||||||
const unconfirmedExpectedMembers = input.expectedMembers
|
const unconfirmedExpectedMembers = input.expectedMembers
|
||||||
.map((member) => member.name)
|
.map((member) => member.name)
|
||||||
.filter((memberName) => data.members[memberName]?.launchState !== 'confirmed_alive');
|
.filter((memberName) => !isExpectedMemberConfirmed(memberName));
|
||||||
const anyExpectedMemberFailed = input.expectedMembers.some(
|
const anyExpectedMemberFailed = input.expectedMembers.some(
|
||||||
(member) => data.members[member.name]?.launchState === 'failed'
|
(member) => data.members[member.name]?.launchState === 'failed'
|
||||||
);
|
);
|
||||||
const allExpectedMembersConfirmed =
|
const allExpectedMembersConfirmed =
|
||||||
input.expectedMembers.length > 0 && unconfirmedExpectedMembers.length === 0;
|
input.expectedMembers.length > 0 && unconfirmedExpectedMembers.length === 0;
|
||||||
const success = bridgeReady && readyCheckpointsPresent && allExpectedMembersConfirmed;
|
const success =
|
||||||
|
(bridgeReady && readyCheckpointsPresent && allExpectedMembersConfirmed) ||
|
||||||
|
(data.teamLaunchState === 'launching' && allExpectedMembersConfirmed);
|
||||||
const checkpointDiagnostic = success
|
const checkpointDiagnostic = success
|
||||||
? []
|
? []
|
||||||
: bridgeReady && !readyCheckpointsPresent
|
: bridgeReady && !readyCheckpointsPresent
|
||||||
|
|
@ -522,6 +542,12 @@ function mapOpenCodeLaunchDataToRuntimeResult(
|
||||||
bridgeMember?.pendingPermissionRequestIds,
|
bridgeMember?.pendingPermissionRequestIds,
|
||||||
bridgeMember != null,
|
bridgeMember != null,
|
||||||
memberDiagnostics,
|
memberDiagnostics,
|
||||||
|
input.runId,
|
||||||
|
input.laneId?.trim() || 'primary',
|
||||||
|
input.teamName,
|
||||||
|
bridgeMember?.bootstrapEvidenceSource,
|
||||||
|
bridgeMember?.bootstrapMode,
|
||||||
|
bridgeMember?.appManagedBootstrapCandidate,
|
||||||
selectOpenCodeMemberFailureReason({
|
selectOpenCodeMemberFailureReason({
|
||||||
memberDiagnostics: bridgeMember?.diagnostics ?? [],
|
memberDiagnostics: bridgeMember?.diagnostics ?? [],
|
||||||
bridgeDiagnostics: data.diagnostics,
|
bridgeDiagnostics: data.diagnostics,
|
||||||
|
|
@ -556,6 +582,61 @@ function mapOpenCodeLaunchDataToRuntimeResult(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNonEmptyString(value: unknown): value is string {
|
||||||
|
return typeof value === 'string' && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAppManagedBootstrapCandidate(
|
||||||
|
value: OpenCodeAppManagedBootstrapCandidate | undefined,
|
||||||
|
expected: {
|
||||||
|
teamName: string;
|
||||||
|
memberName: string;
|
||||||
|
runId: string;
|
||||||
|
laneId: string;
|
||||||
|
runtimeSessionId?: string;
|
||||||
|
}
|
||||||
|
): OpenCodeAppManagedBootstrapCandidate | undefined {
|
||||||
|
if (!value || value.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
value.teamName !== expected.teamName ||
|
||||||
|
value.memberName !== expected.memberName ||
|
||||||
|
value.runId !== expected.runId ||
|
||||||
|
value.laneId !== expected.laneId ||
|
||||||
|
(expected.runtimeSessionId && value.runtimeSessionId !== expected.runtimeSessionId)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!isNonEmptyString(value.runtimeSessionId) ||
|
||||||
|
!isNonEmptyString(value.messageID) ||
|
||||||
|
!value.messageID.startsWith('msg') ||
|
||||||
|
!isNonEmptyString(value.contextHash) ||
|
||||||
|
!isNonEmptyString(value.briefingHash) ||
|
||||||
|
!isNonEmptyString(value.injectionVerifiedAt) ||
|
||||||
|
!isNonEmptyString(value.candidateAt)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
source: 'app_managed_bootstrap',
|
||||||
|
teamName: value.teamName,
|
||||||
|
memberName: value.memberName,
|
||||||
|
runId: value.runId,
|
||||||
|
laneId: value.laneId,
|
||||||
|
runtimeSessionId: value.runtimeSessionId,
|
||||||
|
messageID: value.messageID,
|
||||||
|
contextHash: value.contextHash,
|
||||||
|
briefingHash: value.briefingHash,
|
||||||
|
injectionVerifiedAt: value.injectionVerifiedAt,
|
||||||
|
candidateAt: value.candidateAt,
|
||||||
|
...(isNonEmptyString(value.model) ? { model: value.model } : {}),
|
||||||
|
...(isNonEmptyString(value.agent) ? { agent: value.agent } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mapBridgeMemberToRuntimeEvidence(
|
function mapBridgeMemberToRuntimeEvidence(
|
||||||
memberName: string,
|
memberName: string,
|
||||||
launchState: OpenCodeTeamMemberLaunchBridgeState,
|
launchState: OpenCodeTeamMemberLaunchBridgeState,
|
||||||
|
|
@ -564,8 +645,30 @@ function mapBridgeMemberToRuntimeEvidence(
|
||||||
pendingPermissionRequestIds: string[] | undefined,
|
pendingPermissionRequestIds: string[] | undefined,
|
||||||
runtimeMaterialized: boolean,
|
runtimeMaterialized: boolean,
|
||||||
diagnostics: string[],
|
diagnostics: string[],
|
||||||
|
runId: string,
|
||||||
|
laneId: string,
|
||||||
|
teamName: string,
|
||||||
|
bootstrapEvidenceSource: TeamRuntimeMemberLaunchEvidence['bootstrapEvidenceSource'] | undefined,
|
||||||
|
bootstrapMode: TeamRuntimeMemberLaunchEvidence['bootstrapMode'] | undefined,
|
||||||
|
appManagedBootstrapCandidate: OpenCodeAppManagedBootstrapCandidate | undefined,
|
||||||
selectedHardFailureReason: string
|
selectedHardFailureReason: string
|
||||||
): TeamRuntimeMemberLaunchEvidence {
|
): TeamRuntimeMemberLaunchEvidence {
|
||||||
|
const normalizedAppManagedCandidate = normalizeAppManagedBootstrapCandidate(
|
||||||
|
appManagedBootstrapCandidate,
|
||||||
|
{
|
||||||
|
teamName,
|
||||||
|
memberName,
|
||||||
|
runId,
|
||||||
|
laneId,
|
||||||
|
runtimeSessionId: sessionId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const appManagedCandidatePresent =
|
||||||
|
launchState === 'created' &&
|
||||||
|
isNonEmptyString(sessionId) &&
|
||||||
|
bootstrapEvidenceSource === 'app_managed_bootstrap' &&
|
||||||
|
bootstrapMode === 'app_managed_context' &&
|
||||||
|
normalizedAppManagedCandidate != null;
|
||||||
const confirmed = launchState === 'confirmed_alive';
|
const confirmed = launchState === 'confirmed_alive';
|
||||||
const failed = launchState === 'failed';
|
const failed = launchState === 'failed';
|
||||||
const hasRuntimePid =
|
const hasRuntimePid =
|
||||||
|
|
@ -580,20 +683,24 @@ function mapBridgeMemberToRuntimeEvidence(
|
||||||
: launchState === 'permission_blocked'
|
: launchState === 'permission_blocked'
|
||||||
? 'permission_blocked'
|
? 'permission_blocked'
|
||||||
: 'registered_only';
|
: 'registered_only';
|
||||||
const runtimeDiagnostic = pendingRuntimeObserved
|
const runtimeDiagnostic = appManagedCandidatePresent
|
||||||
? hasRuntimePid
|
? 'OpenCode app-managed bootstrap context was injected and verified by the bridge; waiting for app-owned durable evidence commit.'
|
||||||
? 'OpenCode runtime pid reported by bridge without local process verification'
|
: pendingRuntimeObserved
|
||||||
: 'OpenCode session exists without verified runtime pid'
|
? hasRuntimePid
|
||||||
: launchState === 'permission_blocked'
|
? 'OpenCode runtime pid reported by bridge without local process verification'
|
||||||
? 'OpenCode runtime is waiting for permission approval'
|
: 'OpenCode session exists without verified runtime pid'
|
||||||
: runtimeMaterialized
|
: launchState === 'permission_blocked'
|
||||||
? 'OpenCode bridge did not report a runtime session or pid for this member'
|
? 'OpenCode runtime is waiting for permission approval'
|
||||||
|
: runtimeMaterialized
|
||||||
|
? 'OpenCode bridge did not report a runtime session or pid for this member'
|
||||||
|
: undefined;
|
||||||
|
const runtimeDiagnosticSeverity = appManagedCandidatePresent
|
||||||
|
? 'info'
|
||||||
|
: failed
|
||||||
|
? 'error'
|
||||||
|
: pendingRuntimeObserved || launchState === 'permission_blocked' || runtimeMaterialized
|
||||||
|
? 'warning'
|
||||||
: undefined;
|
: undefined;
|
||||||
const runtimeDiagnosticSeverity = failed
|
|
||||||
? 'error'
|
|
||||||
: pendingRuntimeObserved || launchState === 'permission_blocked' || runtimeMaterialized
|
|
||||||
? 'warning'
|
|
||||||
: undefined;
|
|
||||||
return {
|
return {
|
||||||
memberName,
|
memberName,
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
|
|
@ -618,6 +725,13 @@ function mapBridgeMemberToRuntimeEvidence(
|
||||||
? [...new Set(pendingPermissionRequestIds)]
|
? [...new Set(pendingPermissionRequestIds)]
|
||||||
: undefined,
|
: undefined,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
...(appManagedCandidatePresent
|
||||||
|
? { bootstrapEvidenceSource: 'app_managed_bootstrap' as const }
|
||||||
|
: {}),
|
||||||
|
...(appManagedCandidatePresent ? { bootstrapMode: 'app_managed_context' as const } : {}),
|
||||||
|
...(normalizedAppManagedCandidate
|
||||||
|
? { appManagedBootstrapCandidate: normalizedAppManagedCandidate }
|
||||||
|
: {}),
|
||||||
...(hasRuntimePid ? { runtimePid } : {}),
|
...(hasRuntimePid ? { runtimePid } : {}),
|
||||||
livenessKind,
|
livenessKind,
|
||||||
...(hasRuntimePid ? { pidSource: 'opencode_bridge' as const } : {}),
|
...(hasRuntimePid ? { pidSource: 'opencode_bridge' as const } : {}),
|
||||||
|
|
@ -725,24 +839,24 @@ function buildMemberBootstrapPrompt(
|
||||||
const role = member.role?.trim() || member.workflow?.trim() || 'teammate';
|
const role = member.role?.trim() || member.workflow?.trim() || 'teammate';
|
||||||
const workflow = member.workflow?.trim();
|
const workflow = member.workflow?.trim();
|
||||||
return [
|
return [
|
||||||
|
'<agent_teams_app_managed_bootstrap_briefing>',
|
||||||
|
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
|
||||||
`You are ${member.name}, a ${role} on team "${input.teamName}".`,
|
`You are ${member.name}, a ${role} on team "${input.teamName}".`,
|
||||||
teamPrompt ? `Team launch context:\n${teamPrompt}` : null,
|
teamPrompt ? `Team launch context:\n${teamPrompt}` : null,
|
||||||
workflow ? `Workflow:\n${workflow}` : null,
|
workflow ? `Workflow:\n${workflow}` : null,
|
||||||
'',
|
'',
|
||||||
'This OpenCode session is already attached by the desktop app. Do NOT create local team files, run join scripts, or search the project for a fake team registry.',
|
'This OpenCode session is created, attached, and launch-verified by the desktop app.',
|
||||||
|
'Do not call runtime_bootstrap_checkin or member_briefing just to prove launch readiness.',
|
||||||
|
'Do NOT create local team files, run join scripts, or search the project for a fake team registry.',
|
||||||
'Use the app MCP tools exposed by the "agent-teams" server for team communication and task state.',
|
'Use the app MCP tools exposed by the "agent-teams" server for team communication and task state.',
|
||||||
'The desktop bridge may prepend runtime identity and bootstrap instructions. Follow those first.',
|
|
||||||
'After runtime identity check-in, if you have not already done so, call MCP tool agent-teams_member_briefing (or mcp__agent-teams__member_briefing if that is the exposed name) with:',
|
|
||||||
`{ "teamName": "${input.teamName}", "memberName": "${member.name}", "runtimeProvider": "opencode" }`,
|
|
||||||
'If that tool is not available, stay idle and wait for app-delivered instructions. Do not improvise a replacement workflow.',
|
|
||||||
'Launch bootstrap is a silent attach, not a user/team conversation turn.',
|
'Launch bootstrap is a silent attach, not a user/team conversation turn.',
|
||||||
'After runtime_bootstrap_checkin and member_briefing both succeed, stop this turn immediately and wait for app-delivered messages or actionable task assignments.',
|
|
||||||
'Do not call task_briefing, message_send, or cross_team_send just to announce readiness, say understood, report no tasks, or ask for work.',
|
'Do not call task_briefing, message_send, or cross_team_send just to announce readiness, say understood, report no tasks, or ask for work.',
|
||||||
'If the briefing says there are no actionable tasks, stay idle silently.',
|
'If the briefing says there are no actionable tasks, stay idle silently.',
|
||||||
'',
|
'',
|
||||||
'When you need to message the human user, team lead, or another teammate, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.',
|
'When you need to message the human user, team lead, or another teammate, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.',
|
||||||
`Always set from="${member.name}" when sending a team message from this OpenCode teammate.`,
|
`Always set from="${member.name}" when sending a team message from this OpenCode teammate.`,
|
||||||
'Do not answer team/app messages only as plain assistant text when agent-teams_message_send is available.',
|
'Do not answer team/app messages only as plain assistant text when agent-teams_message_send is available.',
|
||||||
|
'</agent_teams_app_managed_bootstrap_briefing>',
|
||||||
]
|
]
|
||||||
.filter((line): line is string => line !== null)
|
.filter((line): line is string => line !== null)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
@ -792,6 +906,10 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
|
||||||
input.taskRefs
|
input.taskRefs
|
||||||
?.map((ref) => ref.taskId?.trim())
|
?.map((ref) => ref.taskId?.trim())
|
||||||
.filter((taskId): taskId is string => Boolean(taskId)) ?? [];
|
.filter((taskId): taskId is string => Boolean(taskId)) ?? [];
|
||||||
|
// Work-sync nudges are health/reporting probes. Requiring a visible
|
||||||
|
// message_send reply here causes false delivery failures, so accept the
|
||||||
|
// dedicated member_work_sync_report proof path while keeping normal user
|
||||||
|
// messages on the visible reply contract.
|
||||||
const responseInstructions = isWorkSyncNudge
|
const responseInstructions = isWorkSyncNudge
|
||||||
? [
|
? [
|
||||||
'This delivered app message is a member-work-sync nudge.',
|
'This delivered app message is a member-work-sync nudge.',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import type {
|
import type {
|
||||||
EffortLevel,
|
EffortLevel,
|
||||||
MemberLaunchState,
|
MemberLaunchState,
|
||||||
|
OpenCodeAppManagedBootstrapCandidate,
|
||||||
|
OpenCodeBootstrapEvidenceSource,
|
||||||
|
OpenCodeBootstrapMode,
|
||||||
PersistedTeamLaunchPhase,
|
PersistedTeamLaunchPhase,
|
||||||
PersistedTeamLaunchSnapshot,
|
PersistedTeamLaunchSnapshot,
|
||||||
TeamAgentRuntimeBackendType,
|
TeamAgentRuntimeBackendType,
|
||||||
|
|
@ -79,6 +82,9 @@ export interface TeamRuntimeMemberLaunchEvidence {
|
||||||
hardFailureReason?: string;
|
hardFailureReason?: string;
|
||||||
pendingPermissionRequestIds?: string[];
|
pendingPermissionRequestIds?: string[];
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
|
||||||
|
bootstrapMode?: OpenCodeBootstrapMode;
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
backendType?: TeamAgentRuntimeBackendType;
|
backendType?: TeamAgentRuntimeBackendType;
|
||||||
runtimePid?: number;
|
runtimePid?: number;
|
||||||
livenessKind?: TeamAgentRuntimeLivenessKind;
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
|
|
|
||||||
|
|
@ -1005,6 +1005,27 @@ export interface PersistedTeamLaunchMemberSources {
|
||||||
duplicateRespawnBlocked?: boolean;
|
duplicateRespawnBlocked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenCodeAppManagedBootstrapCandidate {
|
||||||
|
schemaVersion: 1;
|
||||||
|
source: 'app_managed_bootstrap';
|
||||||
|
teamName: string;
|
||||||
|
memberName: string;
|
||||||
|
runId: string;
|
||||||
|
laneId: string;
|
||||||
|
runtimeSessionId: string;
|
||||||
|
messageID: string;
|
||||||
|
contextHash: string;
|
||||||
|
briefingHash: string;
|
||||||
|
injectionVerifiedAt: string;
|
||||||
|
candidateAt: string;
|
||||||
|
model?: string;
|
||||||
|
agent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpenCodeBootstrapEvidenceSource = 'runtime_bootstrap_checkin' | 'app_managed_bootstrap';
|
||||||
|
|
||||||
|
export type OpenCodeBootstrapMode = 'model_tool_checkin' | 'app_managed_context';
|
||||||
|
|
||||||
export interface PersistedTeamLaunchMemberState {
|
export interface PersistedTeamLaunchMemberState {
|
||||||
name: string;
|
name: string;
|
||||||
providerId?: TeamProviderId;
|
providerId?: TeamProviderId;
|
||||||
|
|
@ -1032,6 +1053,9 @@ export interface PersistedTeamLaunchMemberState {
|
||||||
/** OpenCode runtime run id that produced the current runtimeSessionId/liveness evidence. */
|
/** OpenCode runtime run id that produced the current runtimeSessionId/liveness evidence. */
|
||||||
runtimeRunId?: string;
|
runtimeRunId?: string;
|
||||||
runtimeSessionId?: string;
|
runtimeSessionId?: string;
|
||||||
|
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
|
||||||
|
bootstrapMode?: OpenCodeBootstrapMode;
|
||||||
|
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
|
||||||
livenessKind?: TeamAgentRuntimeLivenessKind;
|
livenessKind?: TeamAgentRuntimeLivenessKind;
|
||||||
pidSource?: TeamAgentRuntimePidSource;
|
pidSource?: TeamAgentRuntimePidSource;
|
||||||
runtimeDiagnostic?: string;
|
runtimeDiagnostic?: string;
|
||||||
|
|
|
||||||
2
src/types/agent-teams-controller.d.ts
vendored
2
src/types/agent-teams-controller.d.ts
vendored
|
|
@ -43,7 +43,7 @@ declare module 'agent-teams-controller' {
|
||||||
unlinkTask(taskId: string, targetId: string, linkType: string): unknown;
|
unlinkTask(taskId: string, targetId: string, linkType: string): unknown;
|
||||||
memberBriefing(
|
memberBriefing(
|
||||||
memberName: string,
|
memberName: string,
|
||||||
options?: { runtimeProvider?: 'native' | 'opencode' }
|
options?: { runtimeProvider?: 'native' | 'opencode'; includeActiveProcesses?: boolean }
|
||||||
): Promise<string>;
|
): Promise<string>;
|
||||||
leadBriefing(): Promise<string>;
|
leadBriefing(): Promise<string>;
|
||||||
taskBriefing(memberName: string): Promise<string>;
|
taskBriefing(memberName: string): Promise<string>;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
286
test/main/services/team/MemberWorkSyncOpenCode.live.test.ts
Normal file
286
test/main/services/team/MemberWorkSyncOpenCode.live.test.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createMemberWorkSyncFeature,
|
||||||
|
type MemberWorkSyncFeatureFacade,
|
||||||
|
} from '../../../../src/features/member-work-sync/main';
|
||||||
|
import { TeamConfigReader } from '../../../../src/main/services/team/TeamConfigReader';
|
||||||
|
import { TeamDataService } from '../../../../src/main/services/team/TeamDataService';
|
||||||
|
import { TeamKanbanManager } from '../../../../src/main/services/team/TeamKanbanManager';
|
||||||
|
import { TeamMembersMetaStore } from '../../../../src/main/services/team/TeamMembersMetaStore';
|
||||||
|
import { TeamTaskReader } from '../../../../src/main/services/team/TeamTaskReader';
|
||||||
|
import {
|
||||||
|
getTeamsBasePath,
|
||||||
|
setClaudeBasePathOverride,
|
||||||
|
} from '../../../../src/main/utils/pathDecoder';
|
||||||
|
import {
|
||||||
|
formatMemberWorkSyncDiagnostics,
|
||||||
|
formatProgressDump,
|
||||||
|
readRuntimeTurnSettledProcessedMetas,
|
||||||
|
waitUntil,
|
||||||
|
} from './memberWorkSyncLiveHarness';
|
||||||
|
import {
|
||||||
|
createOpenCodeLiveHarness,
|
||||||
|
readInboxMessages,
|
||||||
|
waitForOpenCodeLanesStopped,
|
||||||
|
type OpenCodeLiveHarness,
|
||||||
|
} from './openCodeLiveTestHarness';
|
||||||
|
|
||||||
|
import type { TeamChangeEvent, TeamProvisioningProgress } from '../../../../src/shared/types';
|
||||||
|
|
||||||
|
const liveDescribe =
|
||||||
|
process.env.OPENCODE_E2E === '1' && process.env.OPENCODE_E2E_MEMBER_WORK_SYNC === '1'
|
||||||
|
? describe
|
||||||
|
: describe.skip;
|
||||||
|
|
||||||
|
const DEFAULT_MODEL = 'opencode/gpt-5-nano';
|
||||||
|
|
||||||
|
liveDescribe('Member work sync OpenCode live e2e', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let tempClaudeRoot: string;
|
||||||
|
let feature: MemberWorkSyncFeatureFacade | null;
|
||||||
|
let harness: OpenCodeLiveHarness | null;
|
||||||
|
let teamName: string | null;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'member-work-sync-opencode-live-'));
|
||||||
|
tempClaudeRoot = path.join(tempDir, '.claude');
|
||||||
|
await fs.mkdir(tempClaudeRoot, { recursive: true });
|
||||||
|
setClaudeBasePathOverride(tempClaudeRoot);
|
||||||
|
feature = null;
|
||||||
|
harness = null;
|
||||||
|
teamName = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (harness && teamName) {
|
||||||
|
await harness.svc.stopTeam(teamName).catch(() => undefined);
|
||||||
|
await waitForOpenCodeLanesStopped(teamName);
|
||||||
|
}
|
||||||
|
await feature?.dispose().catch(() => undefined);
|
||||||
|
await harness?.dispose().catch(() => undefined);
|
||||||
|
setClaudeBasePathOverride(null);
|
||||||
|
if (process.env.OPENCODE_E2E_KEEP_TEMP === '1') {
|
||||||
|
console.info(`[MemberWorkSyncOpenCode.live] preserved temp dir: ${tempDir}`);
|
||||||
|
} else {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
'delivers a work-sync nudge to a real OpenCode member and accepts its still-working report',
|
||||||
|
async () => {
|
||||||
|
const selectedModel = process.env.OPENCODE_E2E_MODEL?.trim() || DEFAULT_MODEL;
|
||||||
|
const projectPath = path.join(tempDir, 'project');
|
||||||
|
await fs.mkdir(projectPath, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(projectPath, 'README.md'),
|
||||||
|
'# Member work sync OpenCode live e2e\n\nKeep this project intentionally tiny.\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
let activeService: OpenCodeLiveHarness['svc'] | null = null;
|
||||||
|
harness = await createOpenCodeLiveHarness({
|
||||||
|
tempDir,
|
||||||
|
selectedModel,
|
||||||
|
projectPath,
|
||||||
|
configureServices: (svc) => {
|
||||||
|
activeService = svc;
|
||||||
|
const configReader = new TeamConfigReader();
|
||||||
|
feature = createMemberWorkSyncFeature({
|
||||||
|
teamsBasePath: getTeamsBasePath(),
|
||||||
|
configReader,
|
||||||
|
taskReader: new TeamTaskReader(),
|
||||||
|
kanbanManager: new TeamKanbanManager(),
|
||||||
|
membersMetaStore: new TeamMembersMetaStore(),
|
||||||
|
isTeamActive: (name) => svc.isTeamAlive(name) || svc.hasProvisioningRun(name),
|
||||||
|
listLifecycleActiveTeamNames: async () => (teamName ? [teamName] : []),
|
||||||
|
queueQuietWindowMs: 1,
|
||||||
|
});
|
||||||
|
svc.setTeamChangeEmitter((event: TeamChangeEvent) => feature!.noteTeamChange(event));
|
||||||
|
svc.setRuntimeTurnSettledEnvironmentProvider((input) =>
|
||||||
|
feature!.buildRuntimeTurnSettledEnvironment(input)
|
||||||
|
);
|
||||||
|
return { memberWorkSyncFeature: feature! };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(activeService).toBe(harness.svc);
|
||||||
|
|
||||||
|
const memberName = 'bob';
|
||||||
|
const marker = `member-work-sync-opencode-live-${Date.now()}`;
|
||||||
|
teamName = `member-work-sync-opencode-${Date.now()}`;
|
||||||
|
const progressEvents: TeamProvisioningProgress[] = [];
|
||||||
|
|
||||||
|
await harness.svc.createTeam(
|
||||||
|
{
|
||||||
|
teamName,
|
||||||
|
cwd: projectPath,
|
||||||
|
providerId: 'opencode',
|
||||||
|
model: selectedModel,
|
||||||
|
skipPermissions: true,
|
||||||
|
prompt: [
|
||||||
|
'Keep launch work minimal.',
|
||||||
|
'If you receive a member_work_sync_nudge, call member_work_sync_status first.',
|
||||||
|
'Then call member_work_sync_report with state "still_working", the returned agendaFingerprint/reportToken, and taskIds from the nudge.',
|
||||||
|
'Do not complete the task and do not reply only with acknowledgement.',
|
||||||
|
].join(' '),
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: memberName,
|
||||||
|
role: 'Developer',
|
||||||
|
providerId: 'opencode',
|
||||||
|
model: selectedModel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
progressEvents.push(progress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitUntil(async () => {
|
||||||
|
const last = progressEvents.at(-1);
|
||||||
|
if (last?.state === 'failed') {
|
||||||
|
throw new Error(formatProgressDump(progressEvents));
|
||||||
|
}
|
||||||
|
return progressEvents.some((progress) =>
|
||||||
|
progress.message.includes('OpenCode team launch is ready')
|
||||||
|
);
|
||||||
|
}, 240_000);
|
||||||
|
|
||||||
|
await seedShadowReadyMetrics({ teamName, memberName });
|
||||||
|
const task = await new TeamDataService().createTask(teamName, {
|
||||||
|
subject: `Member work sync OpenCode live nudge ${marker}`,
|
||||||
|
owner: memberName,
|
||||||
|
startImmediately: false,
|
||||||
|
prompt: [
|
||||||
|
`This is a live member-work-sync OpenCode validation task. Marker: ${marker}.`,
|
||||||
|
'Do not edit files and do not complete this task.',
|
||||||
|
'Only report still_working if member-work-sync asks you to synchronize.',
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
feature!.noteTeamChange({ type: 'task', teamName, taskId: task.id });
|
||||||
|
|
||||||
|
const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${memberName}.json`);
|
||||||
|
await waitUntil(async () => {
|
||||||
|
const status = await feature!.getStatus({ teamName: teamName!, memberName });
|
||||||
|
const inbox = await readInboxMessages(inboxPath);
|
||||||
|
return (
|
||||||
|
status.agenda.items.some((item) => item.taskId === task.id) &&
|
||||||
|
inbox.some(
|
||||||
|
(message) =>
|
||||||
|
message.messageKind === 'member_work_sync_nudge' &&
|
||||||
|
typeof message.messageId === 'string'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 60_000, 500, async () =>
|
||||||
|
formatMemberWorkSyncDiagnostics({
|
||||||
|
feature: feature!,
|
||||||
|
teamName: teamName!,
|
||||||
|
memberName,
|
||||||
|
taskId: task.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const nudge = (await readInboxMessages(inboxPath)).find(
|
||||||
|
(message) => message.messageKind === 'member_work_sync_nudge'
|
||||||
|
);
|
||||||
|
expect(nudge?.messageId).toBeTruthy();
|
||||||
|
|
||||||
|
let lastRelay: Awaited<
|
||||||
|
ReturnType<OpenCodeLiveHarness['svc']['relayOpenCodeMemberInboxMessages']>
|
||||||
|
> | null = null;
|
||||||
|
await waitUntil(async () => {
|
||||||
|
lastRelay = await harness!.svc.relayOpenCodeMemberInboxMessages(teamName!, memberName, {
|
||||||
|
onlyMessageId: nudge!.messageId,
|
||||||
|
source: 'manual',
|
||||||
|
deliveryMetadata: {
|
||||||
|
replyRecipient: 'user',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return Boolean(lastRelay.lastDelivery?.delivered);
|
||||||
|
}, 120_000);
|
||||||
|
|
||||||
|
await waitUntil(async () => {
|
||||||
|
const status = await feature!.getStatus({ teamName: teamName!, memberName });
|
||||||
|
return status.report?.accepted === true && status.report.state === 'still_working';
|
||||||
|
}, 180_000, 2_000, async () =>
|
||||||
|
[
|
||||||
|
`Last OpenCode relay: ${JSON.stringify(lastRelay, null, 2)}`,
|
||||||
|
await formatMemberWorkSyncDiagnostics({
|
||||||
|
feature: feature!,
|
||||||
|
teamName: teamName!,
|
||||||
|
memberName,
|
||||||
|
taskId: task.id,
|
||||||
|
}),
|
||||||
|
].join('\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitUntil(async () => {
|
||||||
|
await feature!.drainRuntimeTurnSettledEvents();
|
||||||
|
const metas = await readRuntimeTurnSettledProcessedMetas(getTeamsBasePath());
|
||||||
|
return metas.some(({ meta }) => {
|
||||||
|
const event = meta.event as Record<string, unknown> | undefined;
|
||||||
|
return event?.provider === 'opencode' && event.teamName === teamName;
|
||||||
|
});
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
|
await expect(feature!.dispatchDueNudges([teamName])).resolves.toMatchObject({
|
||||||
|
claimed: 0,
|
||||||
|
delivered: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
420_000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function seedShadowReadyMetrics(input: {
|
||||||
|
teamName: string;
|
||||||
|
memberName: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const metricsPath = path.join(
|
||||||
|
getTeamsBasePath(),
|
||||||
|
input.teamName,
|
||||||
|
'.member-work-sync',
|
||||||
|
'indexes',
|
||||||
|
'metrics.json'
|
||||||
|
);
|
||||||
|
const startMs = Date.now() - 2 * 60 * 60_000;
|
||||||
|
await fs.mkdir(path.dirname(metricsPath), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
metricsPath,
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
schemaVersion: 2,
|
||||||
|
members: {
|
||||||
|
[input.memberName]: {
|
||||||
|
memberName: input.memberName,
|
||||||
|
state: 'caught_up',
|
||||||
|
agendaFingerprint: 'agenda:v1:seed',
|
||||||
|
actionableCount: 0,
|
||||||
|
evaluatedAt: new Date(startMs).toISOString(),
|
||||||
|
providerId: 'opencode',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recentEvents: Array.from({ length: 24 }, (_, index) => ({
|
||||||
|
id: `seed-status-${index}`,
|
||||||
|
teamName: input.teamName,
|
||||||
|
memberName: input.memberName,
|
||||||
|
kind: 'status_evaluated',
|
||||||
|
state: 'caught_up',
|
||||||
|
agendaFingerprint: `agenda:v1:seed-${index}`,
|
||||||
|
recordedAt: new Date(startMs + index * 6 * 60_000).toISOString(),
|
||||||
|
actionableCount: 0,
|
||||||
|
providerId: 'opencode',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
createOpenCodeBridgeHandshakeIdentityHash,
|
createOpenCodeBridgeHandshakeIdentityHash,
|
||||||
createOpenCodeBridgeIdempotencyKey,
|
createOpenCodeBridgeIdempotencyKey,
|
||||||
isOpenCodeBridgeCommandName,
|
isOpenCodeBridgeCommandName,
|
||||||
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
||||||
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
|
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
|
||||||
parseSingleBridgeJsonResult,
|
parseSingleBridgeJsonResult,
|
||||||
stableHash,
|
stableHash,
|
||||||
|
|
@ -312,6 +313,8 @@ function peerIdentity(
|
||||||
'opencode.launchTeam',
|
'opencode.launchTeam',
|
||||||
'opencode.stopTeam',
|
'opencode.stopTeam',
|
||||||
],
|
],
|
||||||
|
opencodeAppManagedBootstrapContractVersion:
|
||||||
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,14 @@ liveDescribe('OpenCode mixed recovery live e2e', () => {
|
||||||
selectedModel,
|
selectedModel,
|
||||||
});
|
});
|
||||||
launchedLanes.push(launchInput);
|
launchedLanes.push(launchInput);
|
||||||
const launchResult = await adapter.launch(launchInput);
|
const rawLaunchResult = await adapter.launch(launchInput);
|
||||||
|
const launchResult = await commitMixedOpenCodeLaunchResult({
|
||||||
|
service: svc,
|
||||||
|
teamName,
|
||||||
|
laneId: launchInput.laneId ?? 'secondary:opencode:bob',
|
||||||
|
memberName: 'bob',
|
||||||
|
result: rawLaunchResult,
|
||||||
|
});
|
||||||
expectCleanOpenCodeLaunch(launchResult);
|
expectCleanOpenCodeLaunch(launchResult);
|
||||||
expect(launchResult.members.bob).toMatchObject({
|
expect(launchResult.members.bob).toMatchObject({
|
||||||
launchState: 'confirmed_alive',
|
launchState: 'confirmed_alive',
|
||||||
|
|
@ -226,7 +233,14 @@ liveDescribe('OpenCode mixed recovery live e2e', () => {
|
||||||
selectedModel,
|
selectedModel,
|
||||||
});
|
});
|
||||||
launchedLanes.push(launchInput);
|
launchedLanes.push(launchInput);
|
||||||
const launchResult = await adapter.launch(launchInput);
|
const rawLaunchResult = await adapter.launch(launchInput);
|
||||||
|
const launchResult = await commitMixedOpenCodeLaunchResult({
|
||||||
|
service: svc,
|
||||||
|
teamName,
|
||||||
|
laneId: launchInput.laneId ?? `secondary:opencode:${memberName}`,
|
||||||
|
memberName,
|
||||||
|
result: rawLaunchResult,
|
||||||
|
});
|
||||||
expectCleanOpenCodeLaunch(launchResult);
|
expectCleanOpenCodeLaunch(launchResult);
|
||||||
expect(launchResult.members[memberName]).toMatchObject({
|
expect(launchResult.members[memberName]).toMatchObject({
|
||||||
launchState: 'confirmed_alive',
|
launchState: 'confirmed_alive',
|
||||||
|
|
@ -327,6 +341,21 @@ function expectCleanOpenCodeLaunch(
|
||||||
expect(launchResult.teamLaunchState).toBe('clean_success');
|
expect(launchResult.teamLaunchState).toBe('clean_success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function commitMixedOpenCodeLaunchResult(input: {
|
||||||
|
service: TeamProvisioningService;
|
||||||
|
teamName: string;
|
||||||
|
laneId: string;
|
||||||
|
memberName: string;
|
||||||
|
result: Awaited<ReturnType<OpenCodeTeamRuntimeAdapter['launch']>>;
|
||||||
|
}): Promise<Awaited<ReturnType<OpenCodeTeamRuntimeAdapter['launch']>>> {
|
||||||
|
return (input.service as any).guardCommittedOpenCodeSecondaryLaneEvidence({
|
||||||
|
teamName: input.teamName,
|
||||||
|
laneId: input.laneId,
|
||||||
|
memberName: input.memberName,
|
||||||
|
result: input.result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function writeMixedRecoveryFixtures(input: {
|
async function writeMixedRecoveryFixtures(input: {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
|
||||||
members: [
|
members: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'alice',
|
name: 'alice',
|
||||||
prompt: expect.stringContaining('agent-teams_member_briefing'),
|
prompt: expect.stringContaining('AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1'),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
@ -377,6 +377,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
|
||||||
expect(launchArg?.members[0]?.prompt).toContain('Do NOT create local team files');
|
expect(launchArg?.members[0]?.prompt).toContain('Do NOT create local team files');
|
||||||
expect(launchArg?.members[0]?.prompt).toContain('Launch bootstrap is a silent attach');
|
expect(launchArg?.members[0]?.prompt).toContain('Launch bootstrap is a silent attach');
|
||||||
expect(launchArg?.members[0]?.prompt).toContain('stay idle silently');
|
expect(launchArg?.members[0]?.prompt).toContain('stay idle silently');
|
||||||
|
expect(launchArg?.members[0]?.prompt).not.toContain('agent-teams_member_briefing');
|
||||||
expect(launchArg?.members[0]?.prompt).not.toContain('Join team "team-a"');
|
expect(launchArg?.members[0]?.prompt).not.toContain('Join team "team-a"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -547,6 +547,31 @@ function createMemberSpawnRun(params?: {
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEST_OPENCODE_APP_MANAGED_BOOTSTRAP_PROMPT = [
|
||||||
|
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
|
||||||
|
'<agent_teams_app_managed_briefing_source>',
|
||||||
|
'Test app-managed member briefing.',
|
||||||
|
'</agent_teams_app_managed_briefing_source>',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
function stubOpenCodeAppManagedLaunchPrompt(svc: TeamProvisioningService) {
|
||||||
|
return vi
|
||||||
|
.spyOn(svc as any, 'buildOpenCodeSecondaryAppManagedLaunchPrompt')
|
||||||
|
.mockImplementation(async (_run: unknown, lane: unknown) => {
|
||||||
|
const memberName =
|
||||||
|
lane &&
|
||||||
|
typeof lane === 'object' &&
|
||||||
|
'member' in lane &&
|
||||||
|
lane.member &&
|
||||||
|
typeof lane.member === 'object' &&
|
||||||
|
'name' in lane.member &&
|
||||||
|
typeof lane.member.name === 'string'
|
||||||
|
? lane.member.name
|
||||||
|
: 'unknown';
|
||||||
|
return `${TEST_OPENCODE_APP_MANAGED_BOOTSTRAP_PROMPT}\nmember=${memberName}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createClaudeLogsRun(overrides: Record<string, unknown> = {}) {
|
function createClaudeLogsRun(overrides: Record<string, unknown> = {}) {
|
||||||
return {
|
return {
|
||||||
runId: 'run-logs-1',
|
runId: 'run-logs-1',
|
||||||
|
|
@ -4357,25 +4382,21 @@ describe('TeamProvisioningService', () => {
|
||||||
const teamName = String(input.teamName);
|
const teamName = String(input.teamName);
|
||||||
const laneId = String(input.laneId);
|
const laneId = String(input.laneId);
|
||||||
const runId = String(input.runId);
|
const runId = String(input.runId);
|
||||||
const manifestPath = getOpenCodeRuntimeManifestPath(tempTeamsBase, teamName, laneId);
|
await writeCommittedOpenCodeSessionStore({
|
||||||
await fsPromises.mkdir(path.dirname(manifestPath), { recursive: true });
|
teamName,
|
||||||
await fsPromises.writeFile(
|
laneId,
|
||||||
manifestPath,
|
runId,
|
||||||
`${JSON.stringify(
|
sessions: [
|
||||||
{
|
{
|
||||||
...createDefaultRuntimeStoreManifest(teamName, '2026-04-22T12:00:00.000Z'),
|
id: 'oc-session-bob',
|
||||||
activeRunId: runId,
|
teamName,
|
||||||
|
memberName: 'bob',
|
||||||
|
laneId,
|
||||||
|
runId,
|
||||||
|
source: 'runtime_bootstrap_checkin',
|
||||||
},
|
},
|
||||||
null,
|
],
|
||||||
2
|
});
|
||||||
)}\n`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
await fsPromises.writeFile(
|
|
||||||
path.join(path.dirname(manifestPath), 'opencode-sessions.json'),
|
|
||||||
`${JSON.stringify({ sessions: [{ id: 'oc-session-bob' }] })}\n`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
runId,
|
runId,
|
||||||
teamName,
|
teamName,
|
||||||
|
|
@ -4408,6 +4429,7 @@ describe('TeamProvisioningService', () => {
|
||||||
} as any,
|
} as any,
|
||||||
]);
|
]);
|
||||||
svc.setRuntimeAdapterRegistry(registry);
|
svc.setRuntimeAdapterRegistry(registry);
|
||||||
|
stubOpenCodeAppManagedLaunchPrompt(svc);
|
||||||
|
|
||||||
(svc as any).launchStateStore = {
|
(svc as any).launchStateStore = {
|
||||||
read: vi.fn(async () => null),
|
read: vi.fn(async () => null),
|
||||||
|
|
@ -4480,7 +4502,7 @@ describe('TeamProvisioningService', () => {
|
||||||
model: 'minimax-m2.5-free',
|
model: 'minimax-m2.5-free',
|
||||||
effort: 'medium',
|
effort: 'medium',
|
||||||
runtimeOnly: true,
|
runtimeOnly: true,
|
||||||
skipReadinessPreflight: true,
|
prompt: expect.stringContaining('AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1'),
|
||||||
cwd: '/tmp/mixed-team',
|
cwd: '/tmp/mixed-team',
|
||||||
expectedMembers: [
|
expectedMembers: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -4493,6 +4515,7 @@ describe('TeamProvisioningService', () => {
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
expect(adapterLaunch.mock.calls[0]?.[0]).not.toHaveProperty('skipReadinessPreflight');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not trust OpenCode secondary bootstrap success without committed lane evidence', async () => {
|
it('does not trust OpenCode secondary bootstrap success without committed lane evidence', async () => {
|
||||||
|
|
@ -6622,6 +6645,57 @@ describe('TeamProvisioningService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats OpenCode empty assistant turns with prompt proof as pending delivery', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
|
||||||
|
ok: false,
|
||||||
|
providerId: 'opencode',
|
||||||
|
memberName: String(input.memberName),
|
||||||
|
sessionId: 'oc-session-bob',
|
||||||
|
prePromptCursor: 'cursor-before',
|
||||||
|
responseObservation: {
|
||||||
|
state: 'empty_assistant_turn' as const,
|
||||||
|
deliveredUserMessageId: 'oc-user-empty',
|
||||||
|
assistantMessageId: null,
|
||||||
|
toolCallNames: [],
|
||||||
|
visibleMessageToolCallId: null,
|
||||||
|
visibleReplyMessageId: null,
|
||||||
|
visibleReplyCorrelation: null,
|
||||||
|
latestAssistantPreview: null,
|
||||||
|
reason: 'empty_assistant_turn',
|
||||||
|
},
|
||||||
|
diagnostics: ['empty_assistant_turn'],
|
||||||
|
}));
|
||||||
|
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
svc.deliverOpenCodeMemberMessage('team-a', {
|
||||||
|
memberName: 'bob',
|
||||||
|
text: 'Work sync check for #task-1.',
|
||||||
|
messageId: 'msg-empty-assistant-pending',
|
||||||
|
replyRecipient: 'team-lead',
|
||||||
|
actionMode: 'do',
|
||||||
|
messageKind: 'member_work_sync_nudge',
|
||||||
|
taskRefs: [
|
||||||
|
{
|
||||||
|
taskId: 'task-1',
|
||||||
|
displayId: 'task-1',
|
||||||
|
teamName: 'team-a',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
source: 'watcher',
|
||||||
|
inboxTimestamp: '2026-04-25T10:00:00.000Z',
|
||||||
|
})
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
delivered: true,
|
||||||
|
accepted: true,
|
||||||
|
responsePending: true,
|
||||||
|
responseState: 'prompt_delivered_no_assistant_message',
|
||||||
|
ledgerStatus: 'retry_scheduled',
|
||||||
|
reason: 'prompt_delivered_no_assistant_message',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('marks OpenCode delivery terminal after max attempts instead of leaving it pending', async () => {
|
it('marks OpenCode delivery terminal after max attempts instead of leaving it pending', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
const emptyResponseObservation = {
|
const emptyResponseObservation = {
|
||||||
|
|
@ -7624,6 +7698,7 @@ describe('TeamProvisioningService', () => {
|
||||||
} as any,
|
} as any,
|
||||||
]);
|
]);
|
||||||
svc.setRuntimeAdapterRegistry(registry);
|
svc.setRuntimeAdapterRegistry(registry);
|
||||||
|
stubOpenCodeAppManagedLaunchPrompt(svc);
|
||||||
|
|
||||||
(svc as any).launchStateStore = {
|
(svc as any).launchStateStore = {
|
||||||
read: vi.fn(async () => null),
|
read: vi.fn(async () => null),
|
||||||
|
|
@ -7761,6 +7836,7 @@ describe('TeamProvisioningService', () => {
|
||||||
} as any,
|
} as any,
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
stubOpenCodeAppManagedLaunchPrompt(svc);
|
||||||
(svc as any).launchStateStore = {
|
(svc as any).launchStateStore = {
|
||||||
read: vi.fn(async () => null),
|
read: vi.fn(async () => null),
|
||||||
write: vi.fn(async () => {}),
|
write: vi.fn(async () => {}),
|
||||||
|
|
@ -7873,6 +7949,7 @@ describe('TeamProvisioningService', () => {
|
||||||
} as any,
|
} as any,
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
stubOpenCodeAppManagedLaunchPrompt(svc);
|
||||||
(svc as any).launchStateStore = {
|
(svc as any).launchStateStore = {
|
||||||
read: vi.fn(async () => null),
|
read: vi.fn(async () => null),
|
||||||
write: vi.fn(async () => {}),
|
write: vi.fn(async () => {}),
|
||||||
|
|
@ -10828,6 +10905,7 @@ describe('TeamProvisioningService', () => {
|
||||||
resolvedFastMode: null,
|
resolvedFastMode: null,
|
||||||
fastResolutionReason: null,
|
fastResolutionReason: null,
|
||||||
}));
|
}));
|
||||||
|
stubOpenCodeAppManagedLaunchPrompt(svc);
|
||||||
|
|
||||||
return { svc, mcpConfigBuilder, membersMetaStore, teamMetaStore };
|
return { svc, mcpConfigBuilder, membersMetaStore, teamMetaStore };
|
||||||
}
|
}
|
||||||
|
|
@ -11121,25 +11199,21 @@ describe('TeamProvisioningService', () => {
|
||||||
const teamName = String(input.teamName);
|
const teamName = String(input.teamName);
|
||||||
const laneId = String(input.laneId);
|
const laneId = String(input.laneId);
|
||||||
const runId = String(input.runId);
|
const runId = String(input.runId);
|
||||||
const manifestPath = getOpenCodeRuntimeManifestPath(tempTeamsBase, teamName, laneId);
|
await writeCommittedOpenCodeSessionStore({
|
||||||
await fsPromises.mkdir(path.dirname(manifestPath), { recursive: true });
|
teamName,
|
||||||
await fsPromises.writeFile(
|
laneId,
|
||||||
manifestPath,
|
runId,
|
||||||
`${JSON.stringify(
|
sessions: [
|
||||||
{
|
{
|
||||||
...createDefaultRuntimeStoreManifest(teamName, '2026-04-22T12:00:00.000Z'),
|
id: `oc-session-${memberName}`,
|
||||||
activeRunId: runId,
|
teamName,
|
||||||
|
memberName,
|
||||||
|
laneId,
|
||||||
|
runId,
|
||||||
|
source: 'runtime_bootstrap_checkin',
|
||||||
},
|
},
|
||||||
null,
|
],
|
||||||
2
|
});
|
||||||
)}\n`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
await fsPromises.writeFile(
|
|
||||||
path.join(path.dirname(manifestPath), 'opencode-sessions.json'),
|
|
||||||
`${JSON.stringify({ sessions: [{ id: `oc-session-${memberName}` }] })}\n`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
runId,
|
runId,
|
||||||
teamName,
|
teamName,
|
||||||
|
|
@ -11447,9 +11521,27 @@ describe('TeamProvisioningService', () => {
|
||||||
const adapterLaunch = vi.fn(async (input: Record<string, unknown>) => {
|
const adapterLaunch = vi.fn(async (input: Record<string, unknown>) => {
|
||||||
const expectedMembers = input.expectedMembers as Array<{ name: string }>;
|
const expectedMembers = input.expectedMembers as Array<{ name: string }>;
|
||||||
const memberName = expectedMembers[0]?.name ?? 'unknown';
|
const memberName = expectedMembers[0]?.name ?? 'unknown';
|
||||||
|
const teamName = String(input.teamName);
|
||||||
|
const laneId = String(input.laneId);
|
||||||
|
const runId = String(input.runId);
|
||||||
|
await writeCommittedOpenCodeSessionStore({
|
||||||
|
teamName,
|
||||||
|
laneId,
|
||||||
|
runId,
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
id: `oc-session-${memberName}`,
|
||||||
|
teamName,
|
||||||
|
memberName,
|
||||||
|
laneId,
|
||||||
|
runId,
|
||||||
|
source: 'runtime_bootstrap_checkin',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
runId: String(input.runId),
|
runId,
|
||||||
teamName: String(input.teamName),
|
teamName,
|
||||||
launchPhase: 'finished',
|
launchPhase: 'finished',
|
||||||
teamLaunchState: 'clean_success',
|
teamLaunchState: 'clean_success',
|
||||||
members: {
|
members: {
|
||||||
|
|
|
||||||
|
|
@ -1834,6 +1834,94 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
|
||||||
expect(rows[0].read).toBe(false);
|
expect(rows[0].read).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps accepted OpenCode prompt rows pending without warning when response proof is terminally absent', async () => {
|
||||||
|
const service = new TeamProvisioningService();
|
||||||
|
const teamName = 'my-team';
|
||||||
|
hoisted.files.set(
|
||||||
|
`/mock/teams/${teamName}/config.json`,
|
||||||
|
JSON.stringify({
|
||||||
|
name: teamName,
|
||||||
|
projectPath: '/tmp/my-team',
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead' },
|
||||||
|
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
seedMemberInbox(teamName, 'jack', [
|
||||||
|
{
|
||||||
|
from: 'bob',
|
||||||
|
to: 'jack',
|
||||||
|
text: 'Please sync your current task.',
|
||||||
|
timestamp: '2026-02-23T17:04:00.000Z',
|
||||||
|
read: false,
|
||||||
|
messageId: 'opencode-accepted-terminal-empty-1',
|
||||||
|
actionMode: 'do',
|
||||||
|
messageKind: 'member_work_sync_nudge',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
vi.spyOn(service, 'deliverOpenCodeMemberMessage').mockResolvedValue({
|
||||||
|
delivered: false,
|
||||||
|
accepted: true,
|
||||||
|
responsePending: false,
|
||||||
|
responseState: 'empty_assistant_turn',
|
||||||
|
ledgerStatus: 'failed_terminal',
|
||||||
|
ledgerRecordId: 'ledger-1',
|
||||||
|
laneId: 'secondary:opencode:jack',
|
||||||
|
reason: 'empty_assistant_turn',
|
||||||
|
diagnostics: ['empty_assistant_turn'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const relay = await service.relayOpenCodeMemberInboxMessages(teamName, 'jack');
|
||||||
|
|
||||||
|
expect(relay).toMatchObject({
|
||||||
|
relayed: 0,
|
||||||
|
attempted: 1,
|
||||||
|
delivered: 0,
|
||||||
|
failed: 0,
|
||||||
|
lastDelivery: {
|
||||||
|
delivered: false,
|
||||||
|
accepted: true,
|
||||||
|
responsePending: false,
|
||||||
|
ledgerStatus: 'failed_terminal',
|
||||||
|
reason: 'empty_assistant_turn',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(vi.mocked(console.warn)).not.toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('OpenCode inbox relay failed')
|
||||||
|
);
|
||||||
|
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
|
||||||
|
expect(rows[0].read).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not treat empty OpenCode observations as accepted without delivered prompt proof', () => {
|
||||||
|
const service = new TeamProvisioningService();
|
||||||
|
const isAccepted = (
|
||||||
|
service as unknown as {
|
||||||
|
isOpenCodePromptAcceptedByObservation: (observation?: unknown) => boolean;
|
||||||
|
}
|
||||||
|
).isOpenCodePromptAcceptedByObservation.bind(service);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isAccepted({
|
||||||
|
state: 'empty_assistant_turn',
|
||||||
|
deliveredUserMessageId: null,
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isAccepted({
|
||||||
|
state: 'prompt_delivered_no_assistant_message',
|
||||||
|
deliveredUserMessageId: '',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isAccepted({
|
||||||
|
state: 'empty_assistant_turn',
|
||||||
|
deliveredUserMessageId: 'opencode-user-message-1',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('reuses existing OpenCode prompt ledger metadata during watchdog relay retries', async () => {
|
it('reuses existing OpenCode prompt ledger metadata during watchdog relay retries', async () => {
|
||||||
const service = new TeamProvisioningService();
|
const service = new TeamProvisioningService();
|
||||||
const teamName = 'my-team';
|
const teamName = 'my-team';
|
||||||
|
|
@ -1901,6 +1989,91 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores stale OpenCode watchdog jobs after the runtime lane is no longer active', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
const service = new TeamProvisioningService();
|
||||||
|
const teamName = 'my-team';
|
||||||
|
hoisted.files.set(
|
||||||
|
`/mock/teams/${teamName}/config.json`,
|
||||||
|
JSON.stringify({
|
||||||
|
name: teamName,
|
||||||
|
projectPath: '/tmp/my-team',
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead' },
|
||||||
|
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
seedMemberInbox(teamName, 'jack', [
|
||||||
|
{
|
||||||
|
from: 'bob',
|
||||||
|
to: 'jack',
|
||||||
|
text: 'Please sync.',
|
||||||
|
timestamp: '2026-02-23T17:00:00.000Z',
|
||||||
|
read: false,
|
||||||
|
messageId: 'opencode-stale-watchdog-1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const deliverSpy = vi
|
||||||
|
.spyOn(service, 'deliverOpenCodeMemberMessage')
|
||||||
|
.mockRejectedValue(
|
||||||
|
new Error('OpenCode prompt delivery record not found: opencode-prompt:stale')
|
||||||
|
);
|
||||||
|
|
||||||
|
(service as any).scheduleOpenCodePromptDeliveryWatchdog({
|
||||||
|
teamName,
|
||||||
|
memberName: 'jack',
|
||||||
|
messageId: 'opencode-stale-watchdog-1',
|
||||||
|
delayMs: 500,
|
||||||
|
});
|
||||||
|
await vi.advanceTimersByTimeAsync(500);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(deliverSpy).not.toHaveBeenCalled();
|
||||||
|
expect(vi.mocked(console.warn)).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not classify missing OpenCode watchdog ledger rows as stale while the lane is active', async () => {
|
||||||
|
const service = new TeamProvisioningService();
|
||||||
|
const teamName = 'my-team';
|
||||||
|
attachAliveRun(service, teamName);
|
||||||
|
hoisted.files.set(
|
||||||
|
`/mock/teams/${teamName}/config.json`,
|
||||||
|
JSON.stringify({
|
||||||
|
name: teamName,
|
||||||
|
projectPath: '/tmp/my-team',
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead' },
|
||||||
|
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
seedMemberInbox(teamName, 'jack', [
|
||||||
|
{
|
||||||
|
from: 'bob',
|
||||||
|
to: 'jack',
|
||||||
|
text: 'Please sync.',
|
||||||
|
timestamp: '2026-02-23T17:00:00.000Z',
|
||||||
|
read: false,
|
||||||
|
messageId: 'opencode-active-watchdog-1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
vi.spyOn(service as any, 'isOpenCodeRuntimeLaneIndexActive').mockResolvedValue(true);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(service as any).isStaleOpenCodePromptDeliveryWatchdogError({
|
||||||
|
teamName,
|
||||||
|
memberName: 'jack',
|
||||||
|
messageId: 'opencode-active-watchdog-1',
|
||||||
|
error: new Error('OpenCode prompt delivery record not found: opencode-prompt:active'),
|
||||||
|
})
|
||||||
|
).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('skips failed-terminal OpenCode rows without blocking newer unread rows', async () => {
|
it('skips failed-terminal OpenCode rows without blocking newer unread rows', async () => {
|
||||||
const service = new TeamProvisioningService();
|
const service = new TeamProvisioningService();
|
||||||
const teamName = 'my-team';
|
const teamName = 'my-team';
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as path from 'path';
|
||||||
|
|
||||||
import Fastify from 'fastify';
|
import Fastify from 'fastify';
|
||||||
|
|
||||||
|
import { buildMemberWorkSyncRuntimeTurnSettledEnvironment } from '../../../../src/features/member-work-sync/main';
|
||||||
import { registerTeamRoutes } from '../../../../src/main/http/teams';
|
import { registerTeamRoutes } from '../../../../src/main/http/teams';
|
||||||
import { applyOpenCodeAutoUpdatePolicy } from '../../../../src/main/services/runtime/openCodeAutoUpdatePolicy';
|
import { applyOpenCodeAutoUpdatePolicy } from '../../../../src/main/services/runtime/openCodeAutoUpdatePolicy';
|
||||||
import { OpenCodeBridgeCommandClient } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient';
|
import { OpenCodeBridgeCommandClient } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient';
|
||||||
|
|
@ -39,6 +40,7 @@ export interface InboxMessage {
|
||||||
to?: string;
|
to?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
|
messageKind?: string;
|
||||||
read?: boolean;
|
read?: boolean;
|
||||||
taskRefs?: TaskRef[];
|
taskRefs?: TaskRef[];
|
||||||
source?: string;
|
source?: string;
|
||||||
|
|
@ -55,13 +57,17 @@ export async function createOpenCodeLiveHarness(input: {
|
||||||
tempDir: string;
|
tempDir: string;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
|
configureServices?: (
|
||||||
|
svc: TeamProvisioningService
|
||||||
|
) => Partial<HttpServices> | Promise<Partial<HttpServices> | void> | void;
|
||||||
}): Promise<OpenCodeLiveHarness> {
|
}): Promise<OpenCodeLiveHarness> {
|
||||||
const orchestratorCli =
|
const orchestratorCli =
|
||||||
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() || DEFAULT_ORCHESTRATOR_CLI;
|
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() || DEFAULT_ORCHESTRATOR_CLI;
|
||||||
await assertExecutable(orchestratorCli);
|
await assertExecutable(orchestratorCli);
|
||||||
|
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
const controlApi = await startLiveTeamControlApi(svc);
|
const extraServices = (await input.configureServices?.(svc)) ?? {};
|
||||||
|
const controlApi = await startLiveTeamControlApi(svc, extraServices);
|
||||||
svc.setControlApiBaseUrlResolver(async () => controlApi.baseUrl);
|
svc.setControlApiBaseUrlResolver(async () => controlApi.baseUrl);
|
||||||
|
|
||||||
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec();
|
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec();
|
||||||
|
|
@ -75,6 +81,13 @@ export async function createOpenCodeLiveHarness(input: {
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
||||||
};
|
};
|
||||||
|
const turnSettledEnv = await buildMemberWorkSyncRuntimeTurnSettledEnvironment({
|
||||||
|
teamsBasePath: getTeamsBasePath(),
|
||||||
|
provider: 'opencode',
|
||||||
|
});
|
||||||
|
if (turnSettledEnv) {
|
||||||
|
Object.assign(bridgeEnv, turnSettledEnv);
|
||||||
|
}
|
||||||
if (process.env.OPENCODE_E2E_USE_REAL_APP_CREDENTIALS !== '1') {
|
if (process.env.OPENCODE_E2E_USE_REAL_APP_CREDENTIALS !== '1') {
|
||||||
bridgeEnv.XDG_DATA_HOME = path.join(input.tempDir, 'xdg-data');
|
bridgeEnv.XDG_DATA_HOME = path.join(input.tempDir, 'xdg-data');
|
||||||
} else if (stableBridgeEnv.XDG_DATA_HOME) {
|
} else if (stableBridgeEnv.XDG_DATA_HOME) {
|
||||||
|
|
@ -326,13 +339,17 @@ function getTranscriptDurableState(transcript: unknown): string | null {
|
||||||
return typeof durableState === 'string' ? durableState : null;
|
return typeof durableState === 'string' ? durableState : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startLiveTeamControlApi(svc: TeamProvisioningService): Promise<{
|
async function startLiveTeamControlApi(
|
||||||
|
svc: TeamProvisioningService,
|
||||||
|
extraServices: Partial<HttpServices> = {}
|
||||||
|
): Promise<{
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
}> {
|
}> {
|
||||||
const app = Fastify({ logger: false });
|
const app = Fastify({ logger: false });
|
||||||
registerTeamRoutes(app, {
|
registerTeamRoutes(app, {
|
||||||
teamProvisioningService: svc,
|
teamProvisioningService: svc,
|
||||||
|
...extraServices,
|
||||||
} as HttpServices);
|
} as HttpServices);
|
||||||
await app.listen({ host: '127.0.0.1', port: 0 });
|
await app.listen({ host: '127.0.0.1', port: 0 });
|
||||||
const address = app.server.address();
|
const address = app.server.address();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue