@fitlab-ai/agent-infra 0.11.12 → 0.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +26 -19
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/sandbox/commands/create.js +6 -2
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/control/client.js +4 -64
- package/dist/lib/sandbox/control/executor.js +4 -50
- package/dist/lib/sandbox/control/lifecycle.js +66 -4
- package/dist/lib/sandbox/control/protocol.js +5 -16
- package/dist/lib/sandbox/control/server.js +203 -234
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +28 -9
- package/dist/lib/sandbox/removal.js +19 -0
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +48 -2
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +2 -0
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +14 -0
- package/dist/lib/task/lifecycle.js +203 -31
- package/dist/lib/task/orchestration.js +26 -72
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +4 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +20 -23
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +30 -18
- package/lib/internal/task-lifecycle.ts +1 -1
- package/lib/platform/github-client.ts +34 -3
- package/lib/sandbox/commands/create.ts +6 -1
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/control/client.ts +5 -78
- package/lib/sandbox/control/executor.ts +2 -53
- package/lib/sandbox/control/lifecycle.ts +67 -4
- package/lib/sandbox/control/protocol.ts +6 -20
- package/lib/sandbox/control/server.ts +9 -44
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +35 -16
- package/lib/sandbox/removal.ts +21 -2
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +41 -2
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +1 -0
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +14 -0
- package/lib/task/lifecycle.ts +180 -27
- package/lib/task/orchestration.ts +30 -68
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +3 -9
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
3
4
|
const REGISTRY_NAME = '.short-ids.json';
|
|
4
5
|
const LOCK_NAME = '.short-ids.json.lock';
|
|
5
6
|
function normalizeShortIdInput(input, opts) {
|
|
@@ -90,20 +91,33 @@ function resolveShortIdReadOnly(input, repoRoot, opts) {
|
|
|
90
91
|
const taskId = ids[key];
|
|
91
92
|
if (!taskId) {
|
|
92
93
|
const message = Object.keys(ids).length === 0
|
|
93
|
-
? `short id '${key}' not found;
|
|
94
|
-
: `short id '${key}' not found in
|
|
94
|
+
? `short id '${key}' not found; task short-id registry is empty.`
|
|
95
|
+
: `short id '${key}' not found in task short-id registry.`;
|
|
95
96
|
return shortIdFailure('SHORT_ID_NOT_FOUND', message);
|
|
96
97
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
const activeTask = path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md');
|
|
99
|
+
const completedTask = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId, 'task.md');
|
|
100
|
+
let isValidCompletedTask = false;
|
|
101
|
+
try {
|
|
102
|
+
const task = parseTypedTaskFrontmatter(fs.readFileSync(completedTask, 'utf8'));
|
|
103
|
+
isValidCompletedTask = task.id === taskId && task.status === 'completed';
|
|
104
|
+
}
|
|
105
|
+
catch { /* a mapping is valid only while its task content is present */ }
|
|
106
|
+
if (!fs.existsSync(activeTask) && !isValidCompletedTask) {
|
|
107
|
+
return shortIdFailure('SHORT_ID_STALE', `short id '${key}' points to no active or completed task content.`, taskId);
|
|
104
108
|
}
|
|
105
109
|
return { ok: true, taskId };
|
|
106
110
|
}
|
|
111
|
+
function hasRetainedCompletedTask(activeDir, taskId) {
|
|
112
|
+
try {
|
|
113
|
+
const taskPath = path.join(path.dirname(activeDir), 'completed', taskId, 'task.md');
|
|
114
|
+
const task = parseTypedTaskFrontmatter(fs.readFileSync(taskPath, 'utf8'));
|
|
115
|
+
return task.id === taskId && task.status === 'completed';
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
107
121
|
function configuredShortIdLength(repoRoot) {
|
|
108
122
|
try {
|
|
109
123
|
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
@@ -206,7 +220,8 @@ function mutateShortIdRegistryAt(activeDir, width, taskId, requested, onlyReleas
|
|
|
206
220
|
const original = JSON.stringify(registry.ids);
|
|
207
221
|
if (pruneOrphans) {
|
|
208
222
|
for (const [key, candidate] of Object.entries(registry.ids)) {
|
|
209
|
-
if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md'))
|
|
223
|
+
if (candidate !== taskId && !fs.existsSync(path.join(activeDir, candidate, 'task.md'))
|
|
224
|
+
&& !hasRetainedCompletedTask(activeDir, candidate))
|
|
210
225
|
delete registry.ids[key];
|
|
211
226
|
}
|
|
212
227
|
}
|
|
@@ -277,34 +292,29 @@ function executeShortIdCommand(request) {
|
|
|
277
292
|
if (normalized.kind !== 'shortId') {
|
|
278
293
|
throw Object.assign(new Error(`invalid short id format '${request.argument}'`), { code: 'SHORT_ID_FORMAT_INVALID' });
|
|
279
294
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (changed)
|
|
290
|
-
writeRegistry(registryPath, registry);
|
|
291
|
-
const taskId = registry.ids[normalized.value];
|
|
292
|
-
if (!taskId)
|
|
293
|
-
throw Object.assign(new Error(`short id '${normalized.value}' not found in active task registry`), { code: 'SHORT_ID_NOT_FOUND' });
|
|
294
|
-
return { status: changed ? 'applied' : 'no-op', changed, output: taskId, error: null };
|
|
295
|
-
});
|
|
295
|
+
const registry = readRegistryStrict(registryPath, request.shortIdLength);
|
|
296
|
+
const taskId = registry.ids[normalized.value];
|
|
297
|
+
if (!taskId)
|
|
298
|
+
throw Object.assign(new Error(`short id '${normalized.value}' not found in task short-id registry`), { code: 'SHORT_ID_NOT_FOUND' });
|
|
299
|
+
if (!fs.existsSync(path.join(request.activeDir, taskId, 'task.md'))
|
|
300
|
+
&& !hasRetainedCompletedTask(request.activeDir, taskId)) {
|
|
301
|
+
throw Object.assign(new Error(`short id '${normalized.value}' points to a task without active or completed task content`), { code: 'SHORT_ID_STALE' });
|
|
302
|
+
}
|
|
303
|
+
return { status: 'no-op', changed: false, output: taskId, error: null };
|
|
296
304
|
}
|
|
297
305
|
const registry = readRegistryStrict(registryPath, request.shortIdLength);
|
|
298
306
|
if (request.verify) {
|
|
299
307
|
const active = fs.existsSync(request.activeDir)
|
|
300
308
|
? fs.readdirSync(request.activeDir).filter((entry) => /^TASK-\d{8}-\d{6}$/.test(entry) && fs.existsSync(path.join(request.activeDir, entry, 'task.md')))
|
|
301
309
|
: [];
|
|
310
|
+
const retained = Object.values(registry.ids).filter((taskId) => hasRetainedCompletedTask(request.activeDir, taskId));
|
|
311
|
+
const validTasks = new Set([...active, ...retained]);
|
|
302
312
|
const registered = new Map();
|
|
303
313
|
for (const [key, taskId] of Object.entries(registry.ids))
|
|
304
314
|
registered.set(taskId, [...(registered.get(taskId) ?? []), key]);
|
|
305
315
|
const diff = {
|
|
306
316
|
missing_in_registry: active.filter((taskId) => !registered.has(taskId)).map((taskId) => ({ taskId })),
|
|
307
|
-
orphans_in_registry: Object.entries(registry.ids).filter(([, taskId]) => !
|
|
317
|
+
orphans_in_registry: Object.entries(registry.ids).filter(([, taskId]) => !validTasks.has(taskId)).map(([key, taskId]) => ({ key, taskId })),
|
|
308
318
|
duplicate_registry_keys: [...registered.entries()].filter(([, keys]) => keys.length > 1).map(([taskId, keys]) => ({ taskId, keys }))
|
|
309
319
|
};
|
|
310
320
|
const clean = Object.values(diff).every((items) => items.length === 0);
|
|
@@ -147,14 +147,9 @@ function checkOrchestrationState({ taskDir }) {
|
|
|
147
147
|
return failResult('orchestration-state', 'Paused run requires a stable pause code and message');
|
|
148
148
|
}
|
|
149
149
|
if (run.status === 'completed') {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return failResult('orchestration-state', evidenceError);
|
|
154
|
-
}
|
|
155
|
-
else if (run.pendingDelegation !== null || !run.commitAuthorization?.consumedAt) {
|
|
156
|
-
return failResult('orchestration-state', 'Completed run must consume commit authorization and clear pending delegation');
|
|
157
|
-
}
|
|
150
|
+
const evidenceError = validateCleanCompletionEvidence(run);
|
|
151
|
+
if (evidenceError)
|
|
152
|
+
return failResult('orchestration-state', evidenceError);
|
|
158
153
|
}
|
|
159
154
|
else if (run.completionEvidence != null) {
|
|
160
155
|
return failResult('orchestration-state', 'Clean completion evidence requires a completed run');
|
|
@@ -186,10 +181,8 @@ function validateCleanCompletionEvidence(run) {
|
|
|
186
181
|
}
|
|
187
182
|
if (run.status !== 'completed'
|
|
188
183
|
|| run.pendingDelegation !== null
|
|
189
|
-
|| run.commitAuthorization?.issuedAt !== null
|
|
190
|
-
|| run.commitAuthorization?.consumedAt !== null
|
|
191
184
|
|| run.receipts.some((receipt) => receipt.stage === 'commit')) {
|
|
192
|
-
return 'Clean completion evidence conflicts with
|
|
185
|
+
return 'Clean completion evidence conflicts with pending delegation or commit receipt state';
|
|
193
186
|
}
|
|
194
187
|
return null;
|
|
195
188
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"protocolVersion": 3,
|
|
3
|
-
"packageVersion": "0.11.
|
|
4
|
-
"sourceInputHash": "
|
|
5
|
-
"internalExecutableBuildHash": "
|
|
3
|
+
"packageVersion": "0.11.13",
|
|
4
|
+
"sourceInputHash": "70e89f362895c1f8f976011715a86b47e06c105ca4285eacbb3fa51d0459a282",
|
|
5
|
+
"internalExecutableBuildHash": "f183e6d39ba985dbb2962eb951b4019af9594361ca3c46b88bf65fff325d7ad4"
|
|
6
6
|
}
|
|
@@ -307,18 +307,23 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
307
307
|
return matches[0]!;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
function
|
|
310
|
+
function consumeWithLockMode(
|
|
311
311
|
childThreadId: string,
|
|
312
312
|
consumer: string,
|
|
313
|
-
expectedHookDefinitionHash
|
|
314
|
-
expectedTaskBinding
|
|
313
|
+
expectedHookDefinitionHash: string | undefined,
|
|
314
|
+
expectedTaskBinding: CodexLifecycleTaskBinding | undefined,
|
|
315
|
+
taskLockHeld: boolean
|
|
315
316
|
): StoredCodexLifecycle {
|
|
316
317
|
if (!consumer.trim()) throw new Error('Codex lifecycle consumer is required');
|
|
317
318
|
const current = read(childThreadId);
|
|
318
319
|
const binding = expectedTaskBinding ?? current.taskBinding;
|
|
319
320
|
if (!binding) throw new Error('Codex lifecycle task receipt binding is missing');
|
|
320
321
|
const spawn = current.state.spawn;
|
|
321
|
-
if (!spawn)
|
|
322
|
+
if (!spawn) {
|
|
323
|
+
throw new Error(taskLockHeld
|
|
324
|
+
? 'Codex lifecycle task receipt binding is missing'
|
|
325
|
+
: 'Codex lifecycle spawn evidence is missing');
|
|
326
|
+
}
|
|
322
327
|
const key = recordKey(spawn.sessionId, spawn.turnId, spawn.toolUseId);
|
|
323
328
|
return update(binding, key, (latest) => {
|
|
324
329
|
if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
|
|
@@ -333,7 +338,16 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
333
338
|
}
|
|
334
339
|
if (latest.consumer) return latest;
|
|
335
340
|
return Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
|
|
336
|
-
});
|
|
341
|
+
}, taskLockHeld);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function consume(
|
|
345
|
+
childThreadId: string,
|
|
346
|
+
consumer: string,
|
|
347
|
+
expectedHookDefinitionHash?: string,
|
|
348
|
+
expectedTaskBinding?: CodexLifecycleTaskBinding
|
|
349
|
+
): StoredCodexLifecycle {
|
|
350
|
+
return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, false);
|
|
337
351
|
}
|
|
338
352
|
|
|
339
353
|
function consumeWithinTaskLock(
|
|
@@ -342,24 +356,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
342
356
|
expectedHookDefinitionHash?: string,
|
|
343
357
|
expectedTaskBinding?: CodexLifecycleTaskBinding
|
|
344
358
|
): StoredCodexLifecycle {
|
|
345
|
-
|
|
346
|
-
const current = read(childThreadId);
|
|
347
|
-
const binding = expectedTaskBinding ?? current.taskBinding;
|
|
348
|
-
if (!binding || !current.state.spawn) throw new Error('Codex lifecycle task receipt binding is missing');
|
|
349
|
-
const key = recordKey(current.state.spawn.sessionId, current.state.spawn.turnId, current.state.spawn.toolUseId);
|
|
350
|
-
return update(binding, key, (latest) => {
|
|
351
|
-
if (!latest || expectedTaskBinding && JSON.stringify(latest.taskBinding) !== JSON.stringify(expectedTaskBinding)) {
|
|
352
|
-
throw new Error('Codex lifecycle task binding does not match the expected receipt');
|
|
353
|
-
}
|
|
354
|
-
if (latest.consumer && latest.consumer !== consumer) {
|
|
355
|
-
throw new Error(`Codex lifecycle evidence was already consumed by '${latest.consumer}'`);
|
|
356
|
-
}
|
|
357
|
-
if (latest.state.status !== 'stop-ready') throw new Error('Codex lifecycle evidence is not stop-ready');
|
|
358
|
-
if (expectedHookDefinitionHash && latest.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash) {
|
|
359
|
-
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
360
|
-
}
|
|
361
|
-
return latest.consumer ? latest : Object.freeze({ ...latest, revision: latest.revision + 1, consumer, consumedAt: now(), updatedAt: now() });
|
|
362
|
-
}, true);
|
|
359
|
+
return consumeWithLockMode(childThreadId, consumer, expectedHookDefinitionHash, expectedTaskBinding, true);
|
|
363
360
|
}
|
|
364
361
|
|
|
365
362
|
function expireBefore(cutoff: string): number {
|
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
recoverSandboxControl,
|
|
3
3
|
requestSandboxControl,
|
|
4
|
-
requestSandboxTaskFinalization,
|
|
5
4
|
SandboxControlClientError
|
|
6
5
|
} from '../sandbox/control/client.ts';
|
|
7
|
-
import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
|
|
8
6
|
import { serveSandboxControl } from '../sandbox/control/server.ts';
|
|
9
7
|
import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
|
|
10
8
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
11
9
|
import { parseTaskCreateResult, taskCreateExitCode } from '../task/create-service.ts';
|
|
12
|
-
import { serializeTaskFinalizationEnvelope, type TaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
|
|
13
|
-
|
|
14
|
-
type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
|
|
15
|
-
|
|
16
|
-
function writeFinalizationEnvelope(
|
|
17
|
-
status: FinalizationStatus,
|
|
18
|
-
accepted: boolean,
|
|
19
|
-
error: { code: string; message: string; retryable: boolean },
|
|
20
|
-
changed: boolean | null = false,
|
|
21
|
-
result: unknown = null,
|
|
22
|
-
requestId: string | null = null
|
|
23
|
-
): void {
|
|
24
|
-
process.stdout.write(serializeTaskFinalizationEnvelope({ status, changed, accepted, requestId, result, error }));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
|
|
28
|
-
if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') return 'unknown';
|
|
29
|
-
return error.retryable ? 'blocked' : 'failed';
|
|
30
|
-
}
|
|
31
10
|
|
|
32
11
|
function writeClientError(error: SandboxControlClientError): void {
|
|
33
12
|
process.stderr.write(`${error.detail.message}\n`);
|
|
@@ -45,78 +24,6 @@ function recoveredExitCode(response: Awaited<ReturnType<typeof recoverSandboxCon
|
|
|
45
24
|
return response.exitCode ?? 1;
|
|
46
25
|
}
|
|
47
26
|
|
|
48
|
-
function finalizationResponseOutput(response: Awaited<ReturnType<typeof requestSandboxTaskFinalization>>): string {
|
|
49
|
-
try {
|
|
50
|
-
const value = JSON.parse(response.stdout) as Partial<TaskFinalizationEnvelope>;
|
|
51
|
-
if (value.version === 2
|
|
52
|
-
&& value.status
|
|
53
|
-
&& ['completed', 'failed', 'blocked', 'unknown'].includes(value.status)
|
|
54
|
-
&& (typeof value.changed === 'boolean' || value.changed === null)) {
|
|
55
|
-
return serializeTaskFinalizationEnvelope({
|
|
56
|
-
status: value.status,
|
|
57
|
-
changed: value.changed,
|
|
58
|
-
accepted: true,
|
|
59
|
-
requestId: response.id,
|
|
60
|
-
result: value.result ?? null,
|
|
61
|
-
error: value.error ?? null
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// Preserve non-envelope output for diagnostics from the executor.
|
|
66
|
-
}
|
|
67
|
-
return response.stdout;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function sandboxFinalizationClient(args: string[]): void {
|
|
71
|
-
if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
|
|
72
|
-
const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
|
|
73
|
-
writeFinalizationEnvelope('failed', false, error);
|
|
74
|
-
process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
|
|
75
|
-
process.exitCode = 1;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const agent = normalizeAgentToken(args[3]);
|
|
79
|
-
if (!agent) {
|
|
80
|
-
const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
|
|
81
|
-
writeFinalizationEnvelope('failed', false, error);
|
|
82
|
-
process.exitCode = 1;
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
let response;
|
|
86
|
-
try {
|
|
87
|
-
response = requestSandboxTaskFinalization({ agent });
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (!(error instanceof SandboxControlClientError)) throw error;
|
|
90
|
-
const status = finalizationErrorStatus(error.detail);
|
|
91
|
-
writeFinalizationEnvelope(status, error.accepted, error.detail, status === 'unknown' ? null : false, null, error.requestId);
|
|
92
|
-
writeClientError(error);
|
|
93
|
-
process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (response.phase === 'rejected') {
|
|
97
|
-
const error = response.error ?? {
|
|
98
|
-
code: 'SANDBOX_CONTROL_REJECTED',
|
|
99
|
-
message: 'sandbox control rejected the finalization request',
|
|
100
|
-
retryable: false
|
|
101
|
-
};
|
|
102
|
-
const status = finalizationErrorStatus(error);
|
|
103
|
-
writeFinalizationEnvelope(
|
|
104
|
-
status,
|
|
105
|
-
error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN',
|
|
106
|
-
error,
|
|
107
|
-
status === 'unknown' ? null : false,
|
|
108
|
-
null,
|
|
109
|
-
response.id
|
|
110
|
-
);
|
|
111
|
-
process.stderr.write(`${error.message}\n`);
|
|
112
|
-
process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
process.stdout.write(finalizationResponseOutput(response));
|
|
116
|
-
process.stderr.write(response.stderr);
|
|
117
|
-
process.exitCode = response.exitCode ?? 1;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
27
|
async function sandboxControl(args: string[]): Promise<void> {
|
|
121
28
|
if (!ensureInternalHandlerRoute('sandbox-control', args)) return;
|
|
122
29
|
const [operation, ...rest] = args;
|
|
@@ -165,10 +72,6 @@ async function sandboxControl(args: string[]): Promise<void> {
|
|
|
165
72
|
}
|
|
166
73
|
if (internalHandlerRoute('sandbox-control', 'client', operation ?? '')) {
|
|
167
74
|
const [family = '', ...commandArgs] = rest;
|
|
168
|
-
if (family === 'task-finalization') {
|
|
169
|
-
sandboxFinalizationClient(commandArgs);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
75
|
let response;
|
|
173
76
|
try {
|
|
174
77
|
response = requestSandboxControl({ family, args: commandArgs });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import {
|
|
2
3
|
createDirectHostExecutionContext,
|
|
3
4
|
dispatchTaskControlOperation,
|
|
@@ -6,8 +7,10 @@ import {
|
|
|
6
7
|
import { applyTaskFinalization } from '../task/finalization.ts';
|
|
7
8
|
import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.ts';
|
|
8
9
|
import { resolveSandboxControlTransport } from './task-operation-registry.ts';
|
|
9
|
-
import {
|
|
10
|
+
import { quiesceSandboxControlForTaskCompletion } from '../sandbox/control/lifecycle.ts';
|
|
11
|
+
import { resolveTaskRuntimeRoot } from '../task/runtime-paths.ts';
|
|
10
12
|
import { serializeTaskFinalizationEnvelope } from '../task/finalization-envelope.ts';
|
|
13
|
+
import { verifyTaskEvent } from '../task/verification.ts';
|
|
11
14
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
12
15
|
|
|
13
16
|
const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
|
|
@@ -79,25 +82,34 @@ async function taskFinalization(args: string[] = []): Promise<void> {
|
|
|
79
82
|
};
|
|
80
83
|
const transport = resolveSandboxControlTransport(process.env, { localWorkflow: true });
|
|
81
84
|
if (transport.kind === 'sandbox-local') {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const detail = clientError
|
|
91
|
-
? clientError.detail
|
|
92
|
-
: { code: 'SANDBOX_CONTROL_CLIENT_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false };
|
|
93
|
-
const status = detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
|
|
94
|
-
? 'unknown'
|
|
95
|
-
: detail.retryable ? 'blocked' : 'failed';
|
|
96
|
-
const changed = status === 'unknown' ? null : false;
|
|
97
|
-
process.stdout.write(envelope(status, changed, clientError?.accepted ?? false, null, detail, clientError?.requestId ?? null));
|
|
98
|
-
process.exitCode = exitCode(status);
|
|
85
|
+
const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
86
|
+
const taskId = process.env.AGENT_INFRA_TASK_ID;
|
|
87
|
+
const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
88
|
+
const controlRootId = process.env.AGENT_INFRA_CONTROL_ROOT_ID;
|
|
89
|
+
if (!statusDir || !taskId || !generation || !controlRootId || taskId !== resolved.taskId) {
|
|
90
|
+
const detail = { code: 'TASK_CONTROL_TRANSPORT_INVALID', message: 'sandbox task control identity is incomplete', retryable: false };
|
|
91
|
+
process.stdout.write(envelope('failed', false, true, null, detail));
|
|
92
|
+
process.exitCode = 1;
|
|
99
93
|
return;
|
|
100
94
|
}
|
|
95
|
+
const result = await applyTaskFinalization(boundOperation.request, {
|
|
96
|
+
repoRoot,
|
|
97
|
+
beforeLifecycle: async (currentTaskId) => {
|
|
98
|
+
if (currentTaskId !== taskId) throw new Error('SANDBOX_CONTROL_IDENTITY_MISMATCH');
|
|
99
|
+
await quiesceSandboxControlForTaskCompletion(
|
|
100
|
+
path.join(resolveTaskRuntimeRoot(taskId, { repoRoot }), 'sandbox-control'),
|
|
101
|
+
{ taskId, generation, controlRootId, statusDir }
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
preflight: (request, options) => verifyTaskEvent(
|
|
105
|
+
{ ...request, event: 'complete-task.hard-preflight' }, options
|
|
106
|
+
)
|
|
107
|
+
});
|
|
108
|
+
const status = result.status === 'prepared' ? 'failed' : result.status;
|
|
109
|
+
process.stdout.write(envelope(status, result.changed, true, result, result.error
|
|
110
|
+
?? (result.status === 'prepared' ? { code: 'TASK_FINALIZATION_PREPARATION_INCOMPLETE', message: 'finalization did not commit', retryable: true } : null)));
|
|
111
|
+
process.exitCode = exitCode(status);
|
|
112
|
+
return;
|
|
101
113
|
}
|
|
102
114
|
const result = await dispatchTaskControlOperation(
|
|
103
115
|
createDirectHostExecutionContext({ repoRoot }),
|
|
@@ -10,7 +10,7 @@ import type { TaskLifecycleResult } from '../task/lifecycle.ts';
|
|
|
10
10
|
import type { AgentClientLifecycleRecoveryResult as LifecycleRecoveryResult } from '../agent-clients/adapter.ts';
|
|
11
11
|
import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.ts';
|
|
12
12
|
|
|
13
|
-
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\n`;
|
|
13
|
+
const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.filter((intent) => intent !== 'complete').join(', ')}\nrecover-started: --auto\n`;
|
|
14
14
|
|
|
15
15
|
function usageFailure(message: string): void {
|
|
16
16
|
process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
|
|
@@ -51,6 +51,33 @@ function boundedFailureDetail(result: RunResult): string {
|
|
|
51
51
|
: `${diagnostic.slice(0, ERROR_DETAIL_LIMIT)}… [truncated]`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function requestDiagnostics(args: string[], request: RequestOptions, attempt: number, errorMessage: string): string {
|
|
55
|
+
if (process.env.AGENT_INFRA_GH_DIAGNOSTICS !== '1' && !/rate.?limit/iu.test(errorMessage)) return '';
|
|
56
|
+
|
|
57
|
+
const apiIndex = args.indexOf('api');
|
|
58
|
+
const endpoint = apiIndex < 0
|
|
59
|
+
? args.slice(0, 2).filter((arg) => !arg.startsWith('-')).join(' ')
|
|
60
|
+
: args.slice(apiIndex + 1).find((arg) => !arg.startsWith('-') && !arg.includes('=')) || 'unknown';
|
|
61
|
+
const safeEndpoint = endpoint.startsWith('http')
|
|
62
|
+
? (() => {
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(endpoint);
|
|
65
|
+
return `${url.host}${url.pathname}`;
|
|
66
|
+
} catch {
|
|
67
|
+
return 'unknown';
|
|
68
|
+
}
|
|
69
|
+
})()
|
|
70
|
+
: endpoint.split('?')[0] || 'unknown';
|
|
71
|
+
const methodIndex = args.findIndex((arg) => arg === '-X' || arg === '--method');
|
|
72
|
+
const methodFromArgs = methodIndex >= 0 ? args[methodIndex + 1] : undefined;
|
|
73
|
+
const method = methodFromArgs || request.method || (safeEndpoint === 'graphql' ? 'POST' : 'GET');
|
|
74
|
+
const tokenEnvironment = ['GH_TOKEN', 'GITHUB_TOKEN', 'GH_ENTERPRISE_TOKEN', 'GITHUB_ENTERPRISE_TOKEN']
|
|
75
|
+
.filter((name) => Boolean(process.env[name]));
|
|
76
|
+
const configDirOverride = Boolean(process.env.GH_CONFIG_DIR);
|
|
77
|
+
|
|
78
|
+
return ` [gh-diagnostic endpoint=${safeEndpoint} method=${method} attempt=${attempt} tokenEnv=${tokenEnvironment.join(',') || 'none'} ghConfigDirOverride=${configDirOverride}]`;
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
function defaultRunner(args: string[], options: RunOptions): RunResult {
|
|
55
82
|
const command = process.env.AGENT_INFRA_GH_BIN || 'gh';
|
|
56
83
|
let prefix: string[] = [];
|
|
@@ -168,13 +195,17 @@ function createGitHubClient(options: ClientOptions = {}): GitHubClient {
|
|
|
168
195
|
const result = runner(args, request);
|
|
169
196
|
if (result.status === 0) return { ok: true, value: result.stdout };
|
|
170
197
|
const error = classifyGitHubFailure(result);
|
|
171
|
-
|
|
198
|
+
const requestDetail = requestDiagnostics(args, request, attempt + 1, error.message);
|
|
199
|
+
const contextualError = requestDetail
|
|
200
|
+
? { ...error, message: `${error.message}${requestDetail}` }
|
|
201
|
+
: error;
|
|
202
|
+
if (!retryableMethod || !error.requestRetryable) return { ok: false, error: clientError(contextualError) };
|
|
172
203
|
if (attempt >= delays.length) {
|
|
173
204
|
return {
|
|
174
205
|
ok: false,
|
|
175
206
|
error: error.code === 'NETWORK_TRANSIENT'
|
|
176
|
-
? { ...clientError(
|
|
177
|
-
: clientError(
|
|
207
|
+
? { ...clientError(contextualError), code: 'NETWORK_RETRY_EXHAUSTED', message: `${contextualError.message} (retry exhausted)` }
|
|
208
|
+
: clientError(contextualError)
|
|
178
209
|
};
|
|
179
210
|
}
|
|
180
211
|
sleep(delays[attempt]!);
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
materializeSandboxControl,
|
|
92
92
|
materializeSandboxWorkspaceView,
|
|
93
93
|
prepareSandboxWorkspaceMountTargets,
|
|
94
|
+
prepareSandboxCompletedTaskMountSource,
|
|
94
95
|
sandboxControlPaths,
|
|
95
96
|
} from '../workspace-view.ts';
|
|
96
97
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
@@ -1451,7 +1452,10 @@ export async function create(
|
|
|
1451
1452
|
const taskSource = target.workspace.mode === 'task-bound'
|
|
1452
1453
|
? assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId)
|
|
1453
1454
|
: null;
|
|
1454
|
-
|
|
1455
|
+
const completedTaskSource = target.workspace.mode === 'task-bound'
|
|
1456
|
+
? prepareSandboxCompletedTaskMountSource(effectiveConfig.repoRoot, target.workspace.taskId)
|
|
1457
|
+
: null;
|
|
1458
|
+
prepareSandboxWorkspaceMountTargets(worktree, target.workspace.mode === 'task-bound' ? target.workspace.taskId : undefined);
|
|
1455
1459
|
if (previousCutoverSnapshot) {
|
|
1456
1460
|
assertSandboxControlCutoverSnapshot(previousCutoverSnapshot);
|
|
1457
1461
|
if (previousManifest) {
|
|
@@ -1494,6 +1498,7 @@ export async function create(
|
|
|
1494
1498
|
...(target.workspace.mode === 'task-bound'
|
|
1495
1499
|
? {
|
|
1496
1500
|
taskSources: [taskSource!],
|
|
1501
|
+
completedTaskSource: completedTaskSource!,
|
|
1497
1502
|
taskId: target.workspace.taskId
|
|
1498
1503
|
}
|
|
1499
1504
|
: {})
|
|
@@ -18,11 +18,10 @@ export { containerListFormat, parseLabels } from './list-running.ts';
|
|
|
18
18
|
const USAGE = `Usage: ai sandbox ls
|
|
19
19
|
|
|
20
20
|
Lists all containers for the current project. The '#' column is a
|
|
21
|
-
display-only row number; the 'SHORT' column shows the
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
A SHORT value of '-' means this container has no active task short id and may
|
|
21
|
+
display-only row number; the 'SHORT' column shows the task short id bound to
|
|
22
|
+
each task-bound container, or '-' if no short id is registered. Pass the
|
|
23
|
+
SHORT value to "ai sandbox exec" (e.g. 'ai sandbox exec 11').
|
|
24
|
+
A SHORT value of '-' means this container has no task short id and may
|
|
26
25
|
be eligible for "ai sandbox rm --unbound" cleanup. Use a full TASK-id to
|
|
27
26
|
remove a specific task-bound sandbox.
|
|
28
27
|
|
|
@@ -13,7 +13,7 @@ import { fetchSandboxRows } from './list-running.ts';
|
|
|
13
13
|
import { rmOne, rmPurge, rmUnbound } from '../removal.ts';
|
|
14
14
|
|
|
15
15
|
const USAGE = `Usage:
|
|
16
|
-
ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
|
|
16
|
+
ai sandbox rm <branch | TASK-id | short id> Remove one sandbox; use a retained short id or full TASK-id for a task-bound sandbox and a branch for branch-only sandboxes
|
|
17
17
|
ai sandbox rm --unbound [--dry-run] [--yes] Remove sandboxes not bound to an active task short id
|
|
18
18
|
ai sandbox rm --purge Tear down ALL sandboxes for the project (containers, worktrees, image, VM)`;
|
|
19
19
|
|