@fitlab-ai/agent-infra 0.9.5 → 0.9.6
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/bin/internal-cli.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +126 -16
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +48 -4
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/internal/codex-lifecycle.js +100 -2
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +5 -4
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +15 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +62 -26
- package/dist/lib/sandbox/control/server.js +222 -102
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +73 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +133 -4
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +142 -15
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +51 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/git/workflow.ts +0 -1
- package/lib/internal/codex-lifecycle.ts +107 -3
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +4 -4
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +17 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +96 -69
- package/lib/sandbox/control/server.ts +239 -110
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +81 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +199 -4
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +48 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -159,6 +159,22 @@ function resolveProjectPolicy(repoRoot, client) {
|
|
|
159
159
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
160
160
|
return normalizeAgentClients(raw).state[client].orchestration;
|
|
161
161
|
}
|
|
162
|
+
function canRecoverCodexUnsupportedPause(run, taskDir) {
|
|
163
|
+
return run.schemaVersion === 2
|
|
164
|
+
&& run.status === 'paused'
|
|
165
|
+
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
166
|
+
&& run.modelPolicySource?.client === 'codex'
|
|
167
|
+
&& run.stepCount === 0
|
|
168
|
+
&& run.nextStage === null
|
|
169
|
+
&& run.baseline === ''
|
|
170
|
+
&& run.pendingDelegation === null
|
|
171
|
+
&& run.receipts.length === 0
|
|
172
|
+
&& run.commitAuthorization?.issuedAt === null
|
|
173
|
+
&& run.commitAuthorization?.consumedAt === null
|
|
174
|
+
&& run.completionEvidence == null
|
|
175
|
+
&& !fs.existsSync(commitIntentPath(taskDir))
|
|
176
|
+
&& (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
|
|
177
|
+
}
|
|
162
178
|
function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
163
179
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
164
180
|
if (!resolved.ok)
|
|
@@ -204,6 +220,39 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
204
220
|
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
205
221
|
}
|
|
206
222
|
}
|
|
223
|
+
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
224
|
+
if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
|
|
225
|
+
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
226
|
+
}
|
|
227
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
228
|
+
const previousPause = existing.pause;
|
|
229
|
+
const recovery = {
|
|
230
|
+
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
231
|
+
recoveredAt: now,
|
|
232
|
+
previousSchemaVersion: 2,
|
|
233
|
+
previousStatus: 'paused',
|
|
234
|
+
previousPause,
|
|
235
|
+
client: 'codex',
|
|
236
|
+
guards: {
|
|
237
|
+
stepCount: 0,
|
|
238
|
+
nextStage: null,
|
|
239
|
+
baselineEmpty: true,
|
|
240
|
+
receiptCount: 0,
|
|
241
|
+
pendingDelegation: false,
|
|
242
|
+
commitAuthorizationUnused: true,
|
|
243
|
+
completionEvidenceAbsent: true,
|
|
244
|
+
commitIntentAbsent: true
|
|
245
|
+
},
|
|
246
|
+
resultingStatus: 'running'
|
|
247
|
+
};
|
|
248
|
+
const resumed = withUpdatedRun(existing, {
|
|
249
|
+
status: 'running',
|
|
250
|
+
pause: null,
|
|
251
|
+
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
252
|
+
}, () => now);
|
|
253
|
+
saveRun(resolved.taskDir, resumed);
|
|
254
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
255
|
+
}
|
|
207
256
|
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
208
257
|
const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
|
|
209
258
|
saveRun(resolved.taskDir, resumed);
|
|
@@ -1059,7 +1108,7 @@ function matchingDelegations(predicate, options) {
|
|
|
1059
1108
|
.filter((entry) => entry.isDirectory())
|
|
1060
1109
|
.flatMap((entry) => {
|
|
1061
1110
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
1062
|
-
if (!run
|
|
1111
|
+
if (run?.status !== 'running' || !run.pendingDelegation)
|
|
1063
1112
|
return [];
|
|
1064
1113
|
const receipt = run.pendingDelegation;
|
|
1065
1114
|
return predicate(receipt)
|
|
@@ -1080,13 +1129,36 @@ function activateMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
1080
1129
|
if (!role)
|
|
1081
1130
|
return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
1082
1131
|
const matched = uniqueMatchingDelegation(client, (receipt) => receipt.status === 'prepared', options);
|
|
1083
|
-
if ('status' in matched)
|
|
1084
|
-
|
|
1132
|
+
if ('status' in matched) {
|
|
1133
|
+
if (matched.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING')
|
|
1134
|
+
return matched;
|
|
1135
|
+
const replay = uniqueMatchingDelegation(client, (receipt) => receipt.childId === event.childId && ['activated', 'stage-completed'].includes(receipt.status), options);
|
|
1136
|
+
if ('status' in replay)
|
|
1137
|
+
return matched;
|
|
1138
|
+
const receipt = replay.run.pendingDelegation;
|
|
1139
|
+
const sameEvidence = receipt.parentId === event.parentId
|
|
1140
|
+
&& receipt.spawnMode === event.spawnMode
|
|
1141
|
+
&& receipt.actualModel === event.actualModel
|
|
1142
|
+
&& receipt.actualReasoningEffort === event.actualReasoningEffort
|
|
1143
|
+
&& receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
|
|
1144
|
+
&& receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
|
|
1145
|
+
&& receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
|
|
1146
|
+
if (!sameEvidence) {
|
|
1147
|
+
return pauseOrchestration(replay.taskId, 'DELEGATION_REPLAY_CONFLICT', 'replayed native start evidence conflicts with the active receipt', true, options);
|
|
1148
|
+
}
|
|
1149
|
+
return { status: replay.run.status, changed: false, taskId: replay.taskId, run: replay.run, next: null, error: null };
|
|
1150
|
+
}
|
|
1085
1151
|
if (matched.run.receipts.some((receipt) => receipt.childId === event.childId)) {
|
|
1086
1152
|
return pauseOrchestration(matched.taskId, 'DELEGATION_IDENTITY_REUSED', 'native child identity was already used by this run', true, options);
|
|
1087
1153
|
}
|
|
1088
1154
|
return activateOrchestrationDelegation(matched.taskId, event, options);
|
|
1089
1155
|
}
|
|
1156
|
+
function pauseMatchingOrchestrationDelegation(client, code, message, options = {}) {
|
|
1157
|
+
const matched = uniqueMatchingDelegation(client, () => true, options);
|
|
1158
|
+
if ('status' in matched)
|
|
1159
|
+
return matched;
|
|
1160
|
+
return pauseOrchestration(matched.taskId, code, message, true, options);
|
|
1161
|
+
}
|
|
1090
1162
|
function sealMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
1091
1163
|
const role = managedDelegationRole(event.nativeAgent);
|
|
1092
1164
|
if (!role)
|
|
@@ -1119,6 +1191,63 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
1119
1191
|
return pauseOrchestration(matched.taskId, 'ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), true, options);
|
|
1120
1192
|
}
|
|
1121
1193
|
}
|
|
1194
|
+
function sealMatchingOrchestrationDelegationWithHostEvidence(client, event, consumeEvidence, options = {}) {
|
|
1195
|
+
const role = managedDelegationRole(event.nativeAgent);
|
|
1196
|
+
if (!role)
|
|
1197
|
+
return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
1198
|
+
const matched = uniqueMatchingDelegation(client, (receipt) => ((receipt.status === 'stage-completed' || receipt.status === 'sealed')
|
|
1199
|
+
&& receipt.childId === event.childId), options);
|
|
1200
|
+
if ('status' in matched)
|
|
1201
|
+
return matched;
|
|
1202
|
+
const receipt = matched.run.pendingDelegation;
|
|
1203
|
+
if (receipt.status === 'sealed') {
|
|
1204
|
+
return reconcileMatchingOrchestrationDelegation(client, event.childId, options);
|
|
1205
|
+
}
|
|
1206
|
+
if (receipt.role !== role) {
|
|
1207
|
+
return pauseOrchestration(matched.taskId, 'DELEGATION_ROLE_MISMATCH', `managed role ${role} does not match ${receipt.role}`, true, options);
|
|
1208
|
+
}
|
|
1209
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1210
|
+
try {
|
|
1211
|
+
const gitRoot = gitRootFor(repoRoot, options);
|
|
1212
|
+
const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
|
|
1213
|
+
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1214
|
+
gitRoot,
|
|
1215
|
+
stateRoot: repoRoot,
|
|
1216
|
+
taskId: snapshotTaskId
|
|
1217
|
+
});
|
|
1218
|
+
const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
|
|
1219
|
+
const baseEvent = { childId: event.childId, exitCode: 0, afterFingerprint, changedPaths };
|
|
1220
|
+
const validated = sealDelegation(receipt, baseEvent, { now: options.now });
|
|
1221
|
+
if (!validated.ok) {
|
|
1222
|
+
return pauseOrchestration(matched.taskId, validated.code, validated.message, true, options);
|
|
1223
|
+
}
|
|
1224
|
+
const hostEvidence = consumeEvidence(receipt);
|
|
1225
|
+
const sealed = sealDelegation(receipt, { ...baseEvent, hostEvidence }, { now: options.now });
|
|
1226
|
+
if (!sealed.ok)
|
|
1227
|
+
return pauseOrchestration(matched.taskId, sealed.code, sealed.message, true, options);
|
|
1228
|
+
const resolved = resolveTaskRef(matched.taskId, { repoRoot });
|
|
1229
|
+
if (!resolved.ok)
|
|
1230
|
+
return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1231
|
+
const updated = withUpdatedRun(matched.run, { pendingDelegation: sealed.receipt }, options.now);
|
|
1232
|
+
saveRun(resolved.taskDir, updated);
|
|
1233
|
+
return { status: 'running', changed: true, taskId: matched.taskId, run: updated, next: null, error: null };
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
return pauseOrchestration(matched.taskId, 'ORCHESTRATION_CODEX_EVIDENCE_FAILED', error instanceof Error ? error.message : String(error), true, options);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function reconcileMatchingOrchestrationDelegation(client, childId, options = {}) {
|
|
1240
|
+
const matched = uniqueMatchingDelegation(client, (receipt) => receipt.childId === childId, options);
|
|
1241
|
+
if ('status' in matched)
|
|
1242
|
+
return matched;
|
|
1243
|
+
const receipt = matched.run.pendingDelegation;
|
|
1244
|
+
if (receipt.status !== 'sealed'
|
|
1245
|
+
|| receipt.hostEvidence?.consumer !== receipt.id
|
|
1246
|
+
|| receipt.hostEvidence.consumedAt === null) {
|
|
1247
|
+
return pauseOrchestration(matched.taskId, 'ORCHESTRATION_CODEX_RECONCILIATION_FAILED', 'Codex native spawn did not produce one sealed receipt with consumed host evidence', true, options);
|
|
1248
|
+
}
|
|
1249
|
+
return { status: matched.run.status, changed: false, taskId: matched.taskId, run: matched.run, next: null, error: null };
|
|
1250
|
+
}
|
|
1122
1251
|
function activateOrchestrationDelegation(taskRef, event, options = {}) {
|
|
1123
1252
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1124
1253
|
if (!resolved.ok)
|
|
@@ -1251,5 +1380,5 @@ function pauseOrchestration(taskRef, code, message, recoverable, options = {}) {
|
|
|
1251
1380
|
saveRun(resolved.taskDir, updated);
|
|
1252
1381
|
return { status: 'paused', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|
|
1253
1382
|
}
|
|
1254
|
-
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt };
|
|
1383
|
+
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseMatchingOrchestrationDelegation, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, reconcileMatchingOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt };
|
|
1255
1384
|
//# sourceMappingURL=orchestration.js.map
|
|
@@ -239,12 +239,29 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
239
239
|
return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
|
|
240
240
|
}
|
|
241
241
|
for (const recovery of run.recoveryHistory) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
|
|
243
|
+
&& recovery.previousSchemaVersion === 1
|
|
244
|
+
&& recovery.receiptCount === 0
|
|
245
|
+
&& recovery.pendingDelegation === false
|
|
246
|
+
&& exactText(recovery.recoveredAt)
|
|
247
|
+
&& recovery.policySource;
|
|
248
|
+
const guards = recovery.guards;
|
|
249
|
+
const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
250
|
+
&& recovery.previousSchemaVersion === 2
|
|
251
|
+
&& recovery.previousStatus === 'paused'
|
|
252
|
+
&& recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
253
|
+
&& recovery.client === 'codex'
|
|
254
|
+
&& recovery.resultingStatus === 'running'
|
|
255
|
+
&& exactText(recovery.recoveredAt)
|
|
256
|
+
&& guards?.stepCount === 0
|
|
257
|
+
&& guards?.nextStage === null
|
|
258
|
+
&& guards?.baselineEmpty === true
|
|
259
|
+
&& guards?.receiptCount === 0
|
|
260
|
+
&& guards?.pendingDelegation === false
|
|
261
|
+
&& guards?.commitAuthorizationUnused === true
|
|
262
|
+
&& guards?.completionEvidenceAbsent === true
|
|
263
|
+
&& guards?.commitIntentAbsent === true;
|
|
264
|
+
if (!validModelRecovery && !validCapabilityRecovery) {
|
|
248
265
|
return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
|
|
249
266
|
}
|
|
250
267
|
}
|
|
@@ -298,6 +315,21 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
298
315
|
&& receipt.reasoningEffortFallbackReason !== null) {
|
|
299
316
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
|
|
300
317
|
}
|
|
318
|
+
if (receipt.client === 'codex') {
|
|
319
|
+
const host = receipt.hostEvidence;
|
|
320
|
+
if (host?.kind !== 'codex-lifecycle-v1'
|
|
321
|
+
|| !exactText(host.hookDefinitionHash)
|
|
322
|
+
|| !Number.isSafeInteger(host.startRevision)
|
|
323
|
+
|| host.startRevision < 1) {
|
|
324
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
|
|
325
|
+
}
|
|
326
|
+
if (['sealed', 'consumed'].includes(receipt.status) && (!Number.isSafeInteger(host.stopRevision)
|
|
327
|
+
|| host.stopRevision <= host.startRevision
|
|
328
|
+
|| host.consumer !== receipt.id
|
|
329
|
+
|| !exactText(host.consumedAt))) {
|
|
330
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex consumed stop evidence`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
301
333
|
}
|
|
302
334
|
}
|
|
303
335
|
if (run.receipts.some((receipt) => receipt.status !== 'consumed')) {
|
|
@@ -16,6 +16,7 @@ const VERIFICATION_CATALOG = {
|
|
|
16
16
|
'code.completed': gate('code-task', 'active', 'code'),
|
|
17
17
|
'review-code.completed': gate('review-code', 'active', 'review-code'),
|
|
18
18
|
'manual-validation.completed': gate('complete-manual-validation', 'active', 'manual-validation'),
|
|
19
|
+
'validation-run.completed': gate('run-manual-validation', 'active', 'validation-run'),
|
|
19
20
|
'block-task.completed': gate('block-task', 'blocked'),
|
|
20
21
|
'cancel-task.completed': gate('cancel-task', 'completed'),
|
|
21
22
|
'commit.completed': gate('commit', 'active'),
|
|
@@ -27,6 +27,8 @@ type AppServerTransportOptions = Readonly<{
|
|
|
27
27
|
cwd?: string;
|
|
28
28
|
env?: NodeJS.ProcessEnv;
|
|
29
29
|
timeoutMs?: number;
|
|
30
|
+
rolloutReadAttempts?: number;
|
|
31
|
+
rolloutRetryMs?: number;
|
|
30
32
|
}>;
|
|
31
33
|
|
|
32
34
|
type CodexRuntimeIdentity = Readonly<{
|
|
@@ -37,10 +39,12 @@ type CodexRuntimeIdentity = Readonly<{
|
|
|
37
39
|
|
|
38
40
|
const LIFECYCLE_HOOKS = Object.freeze([
|
|
39
41
|
Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
|
|
40
|
-
Object.freeze({ event: 'PostToolUse', matcher: '
|
|
41
|
-
Object.freeze({ event: 'SubagentStart', matcher: '
|
|
42
|
-
Object.freeze({ event: 'SubagentStop', matcher: '
|
|
42
|
+
Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
|
|
43
|
+
Object.freeze({ event: 'SubagentStart', matcher: '', phase: 'subagent-start' }),
|
|
44
|
+
Object.freeze({ event: 'SubagentStop', matcher: '', phase: 'subagent-stop' })
|
|
43
45
|
]);
|
|
46
|
+
const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
|
|
47
|
+
const DEFAULT_ROLLOUT_RETRY_MS = 100;
|
|
44
48
|
|
|
45
49
|
function object(value: unknown): JsonObject | null {
|
|
46
50
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -52,6 +56,65 @@ function nonEmpty(value: unknown): string | null {
|
|
|
52
56
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
function resolveCodexSpawnedChild(
|
|
60
|
+
transcriptPath: string,
|
|
61
|
+
expected: Readonly<{
|
|
62
|
+
sessionId: string;
|
|
63
|
+
toolUseId: string;
|
|
64
|
+
nativeAgent: string;
|
|
65
|
+
taskName: string;
|
|
66
|
+
requestedModel?: string;
|
|
67
|
+
requestedReasoningEffort?: string;
|
|
68
|
+
}>
|
|
69
|
+
): string {
|
|
70
|
+
const name = path.basename(transcriptPath);
|
|
71
|
+
if (!transcriptPath || (!name.endsWith(`-${expected.sessionId}.jsonl`) && name !== `${expected.sessionId}.jsonl`)) {
|
|
72
|
+
throw new Error('Codex parent rollout path is invalid');
|
|
73
|
+
}
|
|
74
|
+
const stat = fs.lstatSync(transcriptPath);
|
|
75
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Codex parent rollout is not a regular file');
|
|
76
|
+
const maxBytes = 8 * 1024 * 1024;
|
|
77
|
+
const offset = Math.max(0, stat.size - maxBytes);
|
|
78
|
+
const buffer = Buffer.alloc(stat.size - offset);
|
|
79
|
+
const descriptor = fs.openSync(transcriptPath, 'r');
|
|
80
|
+
try {
|
|
81
|
+
fs.readSync(descriptor, buffer, 0, buffer.length, offset);
|
|
82
|
+
} finally {
|
|
83
|
+
fs.closeSync(descriptor);
|
|
84
|
+
}
|
|
85
|
+
let text = buffer.toString('utf8');
|
|
86
|
+
if (offset) text = text.slice(text.indexOf('\n') + 1);
|
|
87
|
+
const records = text.split(/\r?\n/).filter(Boolean).map((line) => object(JSON.parse(line))).filter(Boolean);
|
|
88
|
+
const calls = records.filter((record) => {
|
|
89
|
+
const payload = object(record?.payload);
|
|
90
|
+
return record?.type === 'response_item'
|
|
91
|
+
&& payload?.type === 'function_call'
|
|
92
|
+
&& payload.call_id === expected.toolUseId
|
|
93
|
+
&& payload.namespace === 'collaboration'
|
|
94
|
+
&& payload.name === 'spawn_agent';
|
|
95
|
+
});
|
|
96
|
+
if (calls.length !== 1) throw new Error('Codex parent rollout spawn call was not found uniquely');
|
|
97
|
+
const call = object(calls[0]?.payload);
|
|
98
|
+
const args = object(JSON.parse(nonEmpty(call?.arguments) ?? '{}'));
|
|
99
|
+
if (
|
|
100
|
+
args?.agent_type !== expected.nativeAgent
|
|
101
|
+
|| args.task_name !== expected.taskName
|
|
102
|
+
|| (expected.requestedModel && args.model !== expected.requestedModel)
|
|
103
|
+
|| (expected.requestedReasoningEffort && args.reasoning_effort !== expected.requestedReasoningEffort)
|
|
104
|
+
) throw new Error('Codex parent rollout spawn identity does not match the hook event');
|
|
105
|
+
const activities = records.filter((record) => {
|
|
106
|
+
const payload = object(record?.payload);
|
|
107
|
+
return record?.type === 'event_msg'
|
|
108
|
+
&& payload?.type === 'sub_agent_activity'
|
|
109
|
+
&& payload.event_id === expected.toolUseId
|
|
110
|
+
&& payload.kind === 'started'
|
|
111
|
+
&& payload.agent_path === `/root/${expected.taskName}`
|
|
112
|
+
&& nonEmpty(payload.agent_thread_id);
|
|
113
|
+
});
|
|
114
|
+
if (activities.length !== 1) throw new Error('Codex parent rollout child activity was not found uniquely');
|
|
115
|
+
return nonEmpty(object(activities[0]?.payload)?.agent_thread_id)!;
|
|
116
|
+
}
|
|
117
|
+
|
|
55
118
|
function validateCodexLifecycleHookConfig(value: unknown): void {
|
|
56
119
|
const hooks = object(object(value)?.hooks);
|
|
57
120
|
const valid = hooks && LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => {
|
|
@@ -63,7 +126,7 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
|
|
|
63
126
|
return entry.hooks.some((hookValue) => {
|
|
64
127
|
const hook = object(hookValue);
|
|
65
128
|
return hook?.type === 'command'
|
|
66
|
-
&& hook.timeout ===
|
|
129
|
+
&& hook.timeout === 15
|
|
67
130
|
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
|
|
68
131
|
});
|
|
69
132
|
});
|
|
@@ -120,26 +183,38 @@ function hasCodexRuntimeLiveness(
|
|
|
120
183
|
});
|
|
121
184
|
}
|
|
122
185
|
|
|
123
|
-
function parseCodexThreadResolution(readValue: unknown,
|
|
186
|
+
function parseCodexThreadResolution(readValue: unknown, rolloutRecords: readonly unknown[]): ThreadResolution {
|
|
124
187
|
const readRoot = object(readValue);
|
|
125
|
-
const resumeRoot = object(resumeValue);
|
|
126
188
|
const thread = object(readRoot?.thread);
|
|
127
|
-
const resumedThread = object(resumeRoot?.thread);
|
|
128
189
|
const source = object(thread?.source);
|
|
129
190
|
const subAgent = object(source?.subAgent);
|
|
130
191
|
const threadSpawn = object(subAgent?.thread_spawn);
|
|
192
|
+
const sessionMeta = rolloutRecords
|
|
193
|
+
.map(object)
|
|
194
|
+
.find((record) => record?.type === 'session_meta');
|
|
195
|
+
const sessionPayload = object(sessionMeta?.payload);
|
|
196
|
+
const turnContext = rolloutRecords
|
|
197
|
+
.map(object)
|
|
198
|
+
.filter((record) => record?.type === 'turn_context')
|
|
199
|
+
.at(-1);
|
|
200
|
+
const turnPayload = object(turnContext?.payload);
|
|
131
201
|
const childThreadId = nonEmpty(thread?.id);
|
|
132
202
|
const parentThreadId = nonEmpty(thread?.parentThreadId);
|
|
133
203
|
const sourceParentThreadId = nonEmpty(threadSpawn?.parent_thread_id);
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
204
|
+
const rolloutChildThreadId = nonEmpty(sessionPayload?.id);
|
|
205
|
+
const rolloutParentThreadId = nonEmpty(sessionPayload?.parent_thread_id);
|
|
206
|
+
const agentRole = nonEmpty(sessionPayload?.agent_role);
|
|
207
|
+
const model = nonEmpty(turnPayload?.model);
|
|
208
|
+
const reasoningEffort = nonEmpty(turnPayload?.effort);
|
|
137
209
|
const forkedFromId = thread?.forkedFromId;
|
|
138
210
|
if (
|
|
139
211
|
!childThreadId
|
|
140
|
-
||
|
|
212
|
+
|| rolloutChildThreadId !== childThreadId
|
|
141
213
|
|| !parentThreadId
|
|
142
214
|
|| !sourceParentThreadId
|
|
215
|
+
|| rolloutParentThreadId !== parentThreadId
|
|
216
|
+
|| rolloutParentThreadId !== sourceParentThreadId
|
|
217
|
+
|| !/^agent-infra-lifecycle-(executor|reviewer)$/.test(agentRole ?? '')
|
|
143
218
|
|| !model
|
|
144
219
|
|| !reasoningEffort
|
|
145
220
|
|| (forkedFromId !== null && typeof forkedFromId !== 'string')
|
|
@@ -152,7 +227,8 @@ function parseCodexThreadResolution(readValue: unknown, resumeValue: unknown): T
|
|
|
152
227
|
childThreadId,
|
|
153
228
|
parentThreadId,
|
|
154
229
|
forkedFromId,
|
|
155
|
-
sourceParentThreadId
|
|
230
|
+
sourceParentThreadId,
|
|
231
|
+
nativeAgent: agentRole!
|
|
156
232
|
}),
|
|
157
233
|
settings: Object.freeze({
|
|
158
234
|
type: 'app-settings',
|
|
@@ -163,6 +239,49 @@ function parseCodexThreadResolution(readValue: unknown, resumeValue: unknown): T
|
|
|
163
239
|
});
|
|
164
240
|
}
|
|
165
241
|
|
|
242
|
+
function readCodexRolloutRecords(readValue: unknown, childThreadId: string): readonly unknown[] {
|
|
243
|
+
const thread = object(object(readValue)?.thread);
|
|
244
|
+
const rolloutPath = nonEmpty(thread?.path);
|
|
245
|
+
const rolloutName = rolloutPath ? path.basename(rolloutPath) : '';
|
|
246
|
+
if (!rolloutPath || (rolloutName !== `${childThreadId}.jsonl` && !rolloutName.endsWith(`-${childThreadId}.jsonl`))) {
|
|
247
|
+
throw new Error('Codex App Server rollout path is invalid');
|
|
248
|
+
}
|
|
249
|
+
const stat = fs.statSync(rolloutPath);
|
|
250
|
+
if (!stat.isFile()) throw new Error('Codex App Server rollout path is not a regular file');
|
|
251
|
+
const maxBytes = 1024 * 1024;
|
|
252
|
+
const bytes = Math.min(stat.size, maxBytes);
|
|
253
|
+
const descriptor = fs.openSync(rolloutPath, 'r');
|
|
254
|
+
try {
|
|
255
|
+
const buffer = Buffer.alloc(bytes);
|
|
256
|
+
fs.readSync(descriptor, buffer, 0, bytes, 0);
|
|
257
|
+
const text = buffer.toString('utf8');
|
|
258
|
+
const completeText = stat.size <= maxBytes ? text : text.slice(0, text.lastIndexOf('\n'));
|
|
259
|
+
return Object.freeze(completeText.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)));
|
|
260
|
+
} finally {
|
|
261
|
+
fs.closeSync(descriptor);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function resolveCodexRolloutRecords(
|
|
266
|
+
readValue: unknown,
|
|
267
|
+
childThreadId: string,
|
|
268
|
+
attempts: number,
|
|
269
|
+
retryMs: number
|
|
270
|
+
): Promise<readonly unknown[]> {
|
|
271
|
+
let lastError: unknown;
|
|
272
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
273
|
+
try {
|
|
274
|
+
const records = readCodexRolloutRecords(readValue, childThreadId);
|
|
275
|
+
parseCodexThreadResolution(readValue, records);
|
|
276
|
+
return records;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
lastError = error;
|
|
279
|
+
if (attempt + 1 < attempts) await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
throw lastError;
|
|
283
|
+
}
|
|
284
|
+
|
|
166
285
|
function parseCodexTurnCompleted(value: unknown): Extract<CodexLifecycleEvent, { type: 'app-terminal' }> {
|
|
167
286
|
const root = object(value);
|
|
168
287
|
const turn = object(root?.turn);
|
|
@@ -333,12 +452,17 @@ async function resolveCodexThread(
|
|
|
333
452
|
const readResult = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
|
|
334
453
|
const readThread = object(readResult?.thread);
|
|
335
454
|
if (nonEmpty(readThread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
|
|
336
|
-
const resumeResult = await transport.request('thread/resume', { threadId: childThreadId });
|
|
337
455
|
let resolution: ThreadResolution;
|
|
338
456
|
try {
|
|
339
|
-
|
|
457
|
+
const rolloutRecords = await resolveCodexRolloutRecords(
|
|
458
|
+
readResult,
|
|
459
|
+
childThreadId,
|
|
460
|
+
options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS,
|
|
461
|
+
options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS
|
|
462
|
+
);
|
|
463
|
+
resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
340
464
|
} catch {
|
|
341
|
-
throw new Error('Codex App Server
|
|
465
|
+
throw new Error('Codex App Server rollout metadata is unavailable');
|
|
342
466
|
}
|
|
343
467
|
const reroutes = transport.notifications
|
|
344
468
|
.filter((entry) => entry.method === 'model/rerouted')
|
|
@@ -368,6 +492,8 @@ async function resolveCodexTerminal(
|
|
|
368
492
|
if (nonEmpty(thread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
|
|
369
493
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
370
494
|
const turn = turns.at(-1);
|
|
495
|
+
const turnValue = object(turn);
|
|
496
|
+
if (!turnValue || turnValue.status === 'inProgress') throw new Error('CODEX_TURN_NOT_TERMINAL');
|
|
371
497
|
return parseCodexTurnCompleted({ threadId: childThreadId, turn });
|
|
372
498
|
} finally {
|
|
373
499
|
transport.close();
|
|
@@ -434,6 +560,7 @@ export {
|
|
|
434
560
|
parseCodexHooksList,
|
|
435
561
|
parseCodexThreadResolution,
|
|
436
562
|
parseCodexTurnCompleted,
|
|
563
|
+
resolveCodexSpawnedChild,
|
|
437
564
|
preflightCodexLifecycleEvidence,
|
|
438
565
|
resolveCodexTerminal,
|
|
439
566
|
resolveCodexThread,
|
|
@@ -21,7 +21,9 @@ type CodexLifecycleEvent =
|
|
|
21
21
|
sessionId: string;
|
|
22
22
|
turnId: string;
|
|
23
23
|
childThreadId: string;
|
|
24
|
+
parentThreadId: string;
|
|
24
25
|
nativeAgent: string;
|
|
26
|
+
source?: 'hook' | 'parent-rollout';
|
|
25
27
|
}>
|
|
26
28
|
| Readonly<{
|
|
27
29
|
type: 'app-thread';
|
|
@@ -29,6 +31,7 @@ type CodexLifecycleEvent =
|
|
|
29
31
|
parentThreadId: string;
|
|
30
32
|
forkedFromId: string | null;
|
|
31
33
|
sourceParentThreadId: string;
|
|
34
|
+
nativeAgent: string;
|
|
32
35
|
}>
|
|
33
36
|
| Readonly<{
|
|
34
37
|
type: 'app-settings';
|
|
@@ -56,6 +59,7 @@ type CodexLifecycleEvent =
|
|
|
56
59
|
turnId: string;
|
|
57
60
|
childThreadId: string;
|
|
58
61
|
nativeAgent: string;
|
|
62
|
+
source?: 'hook' | 'parent-rollout';
|
|
59
63
|
}>;
|
|
60
64
|
|
|
61
65
|
type CodexStartEvidence = Readonly<{
|
|
@@ -79,7 +83,7 @@ type CodexStopEvidence = Readonly<{
|
|
|
79
83
|
childThreadId: string;
|
|
80
84
|
turnId: string;
|
|
81
85
|
terminalStatus: 'completed';
|
|
82
|
-
hookStopObserved:
|
|
86
|
+
hookStopObserved: boolean;
|
|
83
87
|
}>;
|
|
84
88
|
|
|
85
89
|
type CodexLifecycleStatus =
|
|
@@ -153,20 +157,32 @@ function requiredStrings(event: CodexLifecycleEvent): boolean {
|
|
|
153
157
|
function derive(state: CodexLifecycleState): CodexLifecycleState {
|
|
154
158
|
const { spawn, child, thread, settings, reroute, terminal, stop } = state;
|
|
155
159
|
if (spawn && child && (
|
|
156
|
-
spawn.sessionId !== child.
|
|
160
|
+
spawn.sessionId !== child.parentThreadId
|
|
157
161
|
|| spawn.nativeAgent !== child.nativeAgent
|
|
158
162
|
)) {
|
|
159
163
|
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook spawn and child identity do not match');
|
|
160
164
|
}
|
|
165
|
+
if (child && child.sessionId !== child.parentThreadId) {
|
|
166
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'child event session does not match the parent thread');
|
|
167
|
+
}
|
|
161
168
|
if (child && thread && child.childThreadId !== thread.childThreadId) {
|
|
162
169
|
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server child identity do not match');
|
|
163
170
|
}
|
|
171
|
+
if (child && thread && child.nativeAgent !== thread.nativeAgent) {
|
|
172
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server agent roles do not match');
|
|
173
|
+
}
|
|
174
|
+
if (child && thread && child.parentThreadId !== thread.parentThreadId) {
|
|
175
|
+
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'hook and App Server parent identity do not match');
|
|
176
|
+
}
|
|
164
177
|
if (spawn && thread && (
|
|
165
178
|
thread.parentThreadId !== spawn.sessionId
|
|
166
179
|
|| thread.sourceParentThreadId !== spawn.sessionId
|
|
167
180
|
)) {
|
|
168
181
|
return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'App Server parent identity does not match the spawning session');
|
|
169
182
|
}
|
|
183
|
+
if (spawn && thread && spawn.nativeAgent !== thread.nativeAgent) {
|
|
184
|
+
return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'spawn and App Server agent roles do not match');
|
|
185
|
+
}
|
|
170
186
|
if (thread?.forkedFromId !== null && thread?.forkedFromId !== undefined) {
|
|
171
187
|
return invalid(state, 'CODEX_EVIDENCE_FORK_FORBIDDEN', 'forked child threads are not fresh lifecycle evidence');
|
|
172
188
|
}
|
|
@@ -233,7 +249,7 @@ function derive(state: CodexLifecycleState): CodexLifecycleState {
|
|
|
233
249
|
childThreadId: startEvidence.childThreadId,
|
|
234
250
|
turnId: terminal.turnId,
|
|
235
251
|
terminalStatus: 'completed',
|
|
236
|
-
hookStopObserved:
|
|
252
|
+
hookStopObserved: stop.source !== 'parent-rollout'
|
|
237
253
|
});
|
|
238
254
|
}
|
|
239
255
|
const status: CodexLifecycleStatus = stopEvidence
|
|
@@ -131,7 +131,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
131
131
|
if (event.type === 'hook-child') {
|
|
132
132
|
const matches = recordFiles(root).filter((file) => {
|
|
133
133
|
const state = readRecord(file).state;
|
|
134
|
-
return state.spawn?.sessionId === event.
|
|
134
|
+
return state.spawn?.sessionId === event.parentThreadId
|
|
135
135
|
&& state.spawn.nativeAgent === event.nativeAgent
|
|
136
136
|
&& (!state.child || state.child.childThreadId === event.childThreadId);
|
|
137
137
|
});
|
|
@@ -179,6 +179,45 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
function applyToSpawn(
|
|
183
|
+
identity: Readonly<{ sessionId: string; turnId: string; toolUseId: string }>,
|
|
184
|
+
event: Extract<CodexLifecycleEvent, { type: 'hook-child' }>
|
|
185
|
+
): CodexLifecycleStoreResult {
|
|
186
|
+
return withWriteLock(() => {
|
|
187
|
+
const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
|
|
188
|
+
if (!fs.existsSync(file)) throw new Error('Codex lifecycle spawn identity was not found');
|
|
189
|
+
const current = readRecord(file);
|
|
190
|
+
if (current.consumer) throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
191
|
+
if (
|
|
192
|
+
current.state.spawn?.sessionId !== identity.sessionId
|
|
193
|
+
|| current.state.spawn.turnId !== identity.turnId
|
|
194
|
+
|| current.state.spawn.toolUseId !== identity.toolUseId
|
|
195
|
+
) throw new Error('Codex lifecycle spawn identity does not match the stored event');
|
|
196
|
+
const nextState = reduceCodexLifecycleEvent(current.state, event);
|
|
197
|
+
const next = Object.freeze({
|
|
198
|
+
...current,
|
|
199
|
+
revision: current.revision + 1,
|
|
200
|
+
state: nextState,
|
|
201
|
+
updatedAt: now()
|
|
202
|
+
});
|
|
203
|
+
writeRecord(file, next, current.revision);
|
|
204
|
+
return Object.freeze({ path: file, revision: next.revision, state: next.state });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function findByParent(parentThreadId: string): readonly StoredCodexLifecycle[] {
|
|
209
|
+
const matches = recordFiles(root)
|
|
210
|
+
.map(readRecord)
|
|
211
|
+
.filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
|
|
212
|
+
&& ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
|
|
213
|
+
const unconsumed = matches.filter((record) => !record.consumer);
|
|
214
|
+
if (unconsumed.length > 1) throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
|
|
215
|
+
if (unconsumed.length === 1) return Object.freeze(unconsumed);
|
|
216
|
+
return Object.freeze(matches
|
|
217
|
+
.filter((record) => record.consumer && record.state.status === 'stop-ready')
|
|
218
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
|
|
219
|
+
}
|
|
220
|
+
|
|
182
221
|
function read(childThreadId: string): StoredCodexLifecycle {
|
|
183
222
|
const matches = findByChild(childThreadId);
|
|
184
223
|
if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
@@ -196,7 +235,16 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
196
235
|
if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
|
|
197
236
|
const file = matches[0]!;
|
|
198
237
|
const current = readRecord(file);
|
|
199
|
-
if (current.consumer)
|
|
238
|
+
if (current.consumer) {
|
|
239
|
+
if (current.consumer !== consumer) {
|
|
240
|
+
throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
expectedHookDefinitionHash
|
|
244
|
+
&& current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash
|
|
245
|
+
) throw new Error('Codex lifecycle hook definition hash is stale');
|
|
246
|
+
return current;
|
|
247
|
+
}
|
|
200
248
|
if (current.state.status !== 'stop-ready') throw new Error('Codex lifecycle evidence is not stop-ready');
|
|
201
249
|
if (
|
|
202
250
|
expectedHookDefinitionHash
|
|
@@ -239,7 +287,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
|
|
|
239
287
|
});
|
|
240
288
|
}
|
|
241
289
|
|
|
242
|
-
return Object.freeze({ root, apply, consume, expireBefore, read });
|
|
290
|
+
return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
|
|
243
291
|
}
|
|
244
292
|
|
|
245
293
|
export { createCodexLifecycleStore };
|
|
@@ -15,7 +15,7 @@ const codexAdapter = defineAgentClientAdapter({
|
|
|
15
15
|
commands: { level: 'integrated' },
|
|
16
16
|
hooks: { level: 'integrated' },
|
|
17
17
|
subagents: { level: 'experimental' },
|
|
18
|
-
orchestration: { level: '
|
|
18
|
+
orchestration: { level: 'experimental' },
|
|
19
19
|
sandbox: { level: 'integrated' },
|
|
20
20
|
verification: { level: 'compatible' }
|
|
21
21
|
},
|