Skip to content

Commit cb69e50

Browse files
authored
Wire production foreign-conflict resolver for rebases (#140)
* Wire production rebase conflict resolver * Harden conflict resolver resource settlement * Enforce tracked spawn deadlines * Retain untracked process ownership * Document retained process outcomes * Cover escaped pipe ownership wait * Close conflict lifecycle gaps * Use monotonic conflict deadline * Cover forward conflict clock steps * Seed conflict storage before importing snapshot * Recheck conflict import deadline before Docker * Fail closed on escaped rebase processes * Preflight unsettled rebase recovery * Retain profile staging cleanup ownership * Keep recovered rebase processes fail closed * Recover pre-spawn rebase ownership * Keep recovery paths and conflict deadlines trusted * Preserve configured adapter timeout cap * Carry invocation budget through container launch * Clarify pre-merge orchestration boundary
1 parent e4e8f27 commit cb69e50

37 files changed

Lines changed: 2558 additions & 305 deletions

‎agents/adapters/claude.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { readCapturedFile } from '../container/profile.ts';
44
import { createClaudeCommand, createPhasePolicy, MAX_COMMAND_SCHEMA_BYTES } from '../policy.ts';
55
import { launchInvocation } from './supervisor.ts';
66
import { setUpProfile } from './setup.ts';
7-
import { assertAdapterRequest, createAdapterInvocationBudget, type AgentAdapterOptions, type AgentAdapterRequest } from './types.ts';
7+
import { assertAdapterRequest, capAdapterInvocationBudget, createAdapterInvocationBudget,
8+
type AgentAdapterOptions, type AgentAdapterRequest } from './types.ts';
89

910
/**
1011
* Read Claude's `--output-format json` envelope. A result whose `subtype` is not `success`, or whose `is_error` is
@@ -46,7 +47,9 @@ export function startClaudeInvocation(request: AgentAdapterRequest,
4647
oauthToken: string, options: AgentAdapterOptions = {}): InvocationHandle {
4748
if (!oauthToken || oauthToken.includes('\0')) throw new Error('Claude OAuth token is malformed.');
4849
const policy = createPhasePolicy(request.invocation);
49-
const remaining = createAdapterInvocationBudget(request.invocation, options.timeoutMs);
50+
const remaining = options.invocationBudget
51+
? capAdapterInvocationBudget(options.invocationBudget, options.timeoutMs)
52+
: createAdapterInvocationBudget(request.invocation, options.timeoutMs);
5053
const structured = policy.phase === 'planning';
5154
// Invalid input throws here, before anything is allocated; only Docker setup runs inside the handle.
5255
const command = createClaudeCommand(policy, request.prompt,
@@ -55,5 +58,6 @@ export function startClaudeInvocation(request: AgentAdapterRequest,
5558
return launchInvocation(request.invocation, remaining, (signal, start) => setUpProfile(request, remaining, signal,
5659
network => ({ ...request, policy, network, command, claudeToken: oauthToken }),
5760
profile => start(profile, { ...options, secrets: { CLAUDE_CODE_OAUTH_TOKEN: oauthToken },
61+
processLifecycle: request.processLifecycle,
5862
invocationBudget: remaining, decode: (_profile, raw) => parseClaudeOutput(raw, structured) })));
5963
}

‎agents/adapters/codex.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { agentContainerId } from '../container/run.ts';
44
import { createCodexCommand, createPhasePolicy } from '../policy.ts';
55
import { launchInvocation, readBoundedContainerFile } from './supervisor.ts';
66
import { setUpProfile } from './setup.ts';
7-
import { assertAdapterRequest, createAdapterInvocationBudget, type AgentAdapterOptions, type AgentAdapterRequest } from './types.ts';
7+
import { assertAdapterRequest, capAdapterInvocationBudget, createAdapterInvocationBudget,
8+
type AgentAdapterOptions, type AgentAdapterRequest } from './types.ts';
89

910
export const CODEX_OUTPUT_FILE = '/run/codeboost-output/final.txt';
1011
// Read the output from the container this invocation created, by ID; a same-named replacement must not answer.
@@ -25,7 +26,9 @@ export function startCodexInvocation(request: AgentAdapterRequest,
2526
authFile: string, options: AgentAdapterOptions = {}): InvocationHandle {
2627
if (!authFile || authFile.includes('\0')) throw new Error('Codex auth path is malformed.');
2728
const policy = createPhasePolicy(request.invocation);
28-
const remaining = createAdapterInvocationBudget(request.invocation, options.timeoutMs);
29+
const remaining = options.invocationBudget
30+
? capAdapterInvocationBudget(options.invocationBudget, options.timeoutMs)
31+
: createAdapterInvocationBudget(request.invocation, options.timeoutMs);
2932
// Invalid input, including any phase Codex is refused (today every phase, #93), throws here, before anything is
3033
// allocated; only Docker setup runs inside the handle.
3134
const command = createCodexCommand(policy, request.prompt);

‎agents/adapters/setup.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,31 @@ export async function setUpProfile(request: AgentAdapterRequest, remaining: () =
2020
profileOptions: (network: VendorNetwork) => Omit<ProfileOptions, 'timeoutMs' | 'signal'>,
2121
start: (profile: ContainerProfile) => InvocationHandle): Promise<InvocationHandle> {
2222
const network = await createVendorNetwork(request.invocation, request.imageId, request.networkAllocationId,
23-
Math.min(60_000, remaining()), signal);
23+
Math.min(60_000, remaining()), signal, request.processLifecycle, remaining);
2424
let profile: ContainerProfile;
2525
try {
2626
profile = await createContainerProfile({ ...profileOptions(network), timeoutMs: Math.min(60_000, remaining()),
27-
signal });
27+
invocationBudget: remaining, signal });
2828
} catch (error) {
2929
// Profile creation removes the network itself once it has claimed it; otherwise the network is still ours.
3030
if (error instanceof ProfileCreationCleanupError) throw error;
31-
try { await removeVendorNetwork(network, CLEANUP_TIMEOUT_MS); }
31+
try { await removeVendorNetwork(network, CLEANUP_TIMEOUT_MS, request.processLifecycle); }
3232
catch (cleanupError) {
3333
throw new AdapterSetupCleanupError(error, cleanupError,
34-
(budgetMs = CLEANUP_TIMEOUT_MS) => removeVendorNetwork(network, budgetMs), () => vendorNetworkResources(network));
34+
(budgetMs = CLEANUP_TIMEOUT_MS) => removeVendorNetwork(network, budgetMs, request.processLifecycle),
35+
() => vendorNetworkResources(network));
3536
}
3637
throw error;
3738
}
3839
// The profile owns the network and staging now. If the handoff itself throws, nothing else will release them:
3940
// the launcher treats a failure without a handle as "nothing allocated", so release (or retain) them here.
4041
try { return start(profile); }
4142
catch (error) {
42-
try { await disposeContainerProfile(profile, CLEANUP_TIMEOUT_MS); }
43+
try { await disposeContainerProfile(profile, CLEANUP_TIMEOUT_MS, request.processLifecycle); }
4344
catch (cleanupError) {
4445
throw new AdapterSetupCleanupError(error, cleanupError,
45-
(budgetMs = CLEANUP_TIMEOUT_MS) => disposeContainerProfile(profile, budgetMs), () => containerProfileResources(profile));
46+
(budgetMs = CLEANUP_TIMEOUT_MS) => disposeContainerProfile(profile, budgetMs, request.processLifecycle),
47+
() => containerProfileResources(profile));
4648
}
4749
throw error;
4850
}

0 commit comments

Comments
 (0)