brainclaw 1.26.2 → 1.27.0
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/README.md +13 -0
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-coordination.js +65 -1
- package/dist/commands/attempt-authority.js +80 -0
- package/dist/commands/harvest.js +140 -61
- package/dist/commands/loop.js +34 -0
- package/dist/commands/loops-handlers.js +87 -14
- package/dist/commands/mcp-catalog.js +42 -18
- package/dist/commands/mcp-schemas.generated.js +44 -0
- package/dist/commands/mcp-write-claims.js +128 -1
- package/dist/commands/mcp-write-coordination.js +146 -76
- package/dist/core/agent-capability.js +1 -1
- package/dist/core/agentrun-reconciler.js +148 -22
- package/dist/core/agentruns.js +254 -29
- package/dist/core/assignment-request-schema.js +7 -0
- package/dist/core/assignment-sweeper.js +5 -3
- package/dist/core/assignments.js +131 -33
- package/dist/core/claim-request-schema.js +7 -0
- package/dist/core/claims.js +53 -2
- package/dist/core/dispatch-status.js +16 -6
- package/dist/core/dispatcher.js +51 -51
- package/dist/core/entity-operations.js +20 -0
- package/dist/core/events.js +4 -0
- package/dist/core/execution-adapters.js +160 -14
- package/dist/core/execution-contract.js +345 -0
- package/dist/core/execution.js +130 -16
- package/dist/core/harness-adapters/base.js +150 -0
- package/dist/core/harness-adapters/claude.js +39 -0
- package/dist/core/harness-adapters/codex.js +57 -0
- package/dist/core/harness-adapters/harvest.js +109 -0
- package/dist/core/harness-adapters/index.js +8 -0
- package/dist/core/harness-adapters/prompt-only.js +13 -0
- package/dist/core/harness-adapters/registry.js +48 -0
- package/dist/core/harness-adapters/result.js +33 -0
- package/dist/core/harness-adapters/types.js +2 -0
- package/dist/core/ideation-loop-close.js +25 -2
- package/dist/core/instruction-templates.js +3 -2
- package/dist/core/loop-turn-dispatch.js +207 -0
- package/dist/core/loops/artifact-contract.js +11 -0
- package/dist/core/loops/attempt-authority.js +476 -0
- package/dist/core/loops/attempt-generations.js +509 -0
- package/dist/core/loops/attempt-reservation.js +197 -35
- package/dist/core/loops/attempt-rollout.js +404 -0
- package/dist/core/loops/attempt-takeover.js +155 -0
- package/dist/core/loops/bootstrap-acquire.js +7 -3
- package/dist/core/loops/evidence.js +187 -0
- package/dist/core/loops/facade-schema.js +41 -10
- package/dist/core/loops/gate-policy.js +485 -0
- package/dist/core/loops/impl-bind.js +37 -79
- package/dist/core/loops/index.js +9 -0
- package/dist/core/loops/iteration-engine.js +31 -19
- package/dist/core/loops/kind-policies.js +90 -0
- package/dist/core/loops/lock.js +71 -13
- package/dist/core/loops/reconcile-turn.js +235 -18
- package/dist/core/loops/result-reducers.js +99 -10
- package/dist/core/loops/store.js +30 -3
- package/dist/core/loops/turn-execution.js +480 -0
- package/dist/core/loops/types.js +113 -2
- package/dist/core/loops/verbs.js +332 -99
- package/dist/core/loops/verify-command.js +31 -8
- package/dist/core/loops/workspace-digest.js +54 -0
- package/dist/core/review-loop-close.js +25 -3
- package/dist/core/review-loop-turn-dispatch.js +210 -161
- package/dist/core/runtime-signals.js +62 -25
- package/dist/core/schema.js +35 -0
- package/dist/core/spawn-check.js +3 -2
- package/dist/core/upgrades/backup.js +27 -4
- package/dist/facts.js +7 -6
- package/dist/facts.json +6 -5
- package/docs/cli.md +49 -1
- package/docs/concepts/attempt-authority.md +407 -0
- package/docs/concepts/evidence-attestations.md +135 -0
- package/docs/concepts/execution-contract.md +166 -0
- package/docs/concepts/harness-adapters.md +166 -0
- package/docs/concepts/ideation-loop.md +5 -4
- package/docs/concepts/loop-engine.md +302 -113
- package/docs/index.md +4 -1
- package/docs/integrations/codex.md +3 -3
- package/docs/integrations/mcp.md +59 -5
- package/docs/loops/debug.md +144 -0
- package/docs/loops/ideation.md +158 -0
- package/docs/loops/implementation.md +154 -0
- package/docs/loops/research.md +136 -0
- package/docs/loops/review.md +200 -0
- package/docs/mcp-schema-changelog.md +14 -5
- package/package.json +1 -1
|
@@ -33,13 +33,17 @@
|
|
|
33
33
|
* @module
|
|
34
34
|
*/
|
|
35
35
|
import { spawnSync } from 'node:child_process';
|
|
36
|
-
import
|
|
36
|
+
import fs from 'node:fs';
|
|
37
|
+
import { loadAgentRun, recordExecutionContractAnomaly, transitionAgentRun, listAgentRuns } from './agentruns.js';
|
|
37
38
|
import { loadClaim, releaseClaim } from './claims.js';
|
|
38
39
|
import { loadAssignment } from './assignments.js';
|
|
39
40
|
import { createRuntimeEvent } from './events.js';
|
|
40
41
|
import { nowISO } from './ids.js';
|
|
41
|
-
import { readHeartbeat, readLogTail, signalExists, latestActivityMs, readCompletionSignals } from './runtime-signals.js';
|
|
42
|
+
import { readContractAck, readHeartbeat, readLogTail, signalExists, latestActivityMs, readCompletionSignals } from './runtime-signals.js';
|
|
43
|
+
import { validateWorkerContractAcceptance } from './execution-contract.js';
|
|
42
44
|
import { findReservationByRunId, evidenceMatchesAttempt, launchGrant, revokeLaunchGrant } from './loops/attempt-reservation.js';
|
|
45
|
+
import { executionContractForGeneration } from './loops/attempt-authority.js';
|
|
46
|
+
import { readLaunchDecision, resolveTurnGenerationChain } from './loops/attempt-generations.js';
|
|
43
47
|
import { reconcileFailedTurn } from './loops/reconcile-turn.js';
|
|
44
48
|
// ── Constants ──────────────────────────────────────────────────────────────
|
|
45
49
|
/**
|
|
@@ -55,6 +59,15 @@ export const DEFAULT_HEALTH_CHECK_GRACE_MS = 60_000;
|
|
|
55
59
|
export const DEFAULT_STALE_AFTER_MS = 30 * 60_000;
|
|
56
60
|
export const DEFAULT_DEAD_PID_READ_SWEEP_AGE_MS = 5 * 60_000;
|
|
57
61
|
export const DEFAULT_DEAD_PID_READ_SWEEP_LIMIT = 50;
|
|
62
|
+
function normalizedWorkspace(value) {
|
|
63
|
+
try {
|
|
64
|
+
const resolved = fs.realpathSync.native(value);
|
|
65
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
58
71
|
/**
|
|
59
72
|
* pln#520 step 1 — a heartbeat older than this (with no completion signal) means
|
|
60
73
|
* the worker reached its loop then went silent: `stalled`. Default 10 min.
|
|
@@ -164,17 +177,25 @@ export function collectEvidence(run, cwd, options) {
|
|
|
164
177
|
// coordination dir (the dispatcher's ackRoot), which is `cwd` for the
|
|
165
178
|
// reconciler. Keyed by assignment_id.
|
|
166
179
|
const signalRoot = cwd ?? process.cwd();
|
|
180
|
+
const signalReservation = findReservationByRunId(run.id, cwd);
|
|
181
|
+
let v2SignalRunId;
|
|
182
|
+
try {
|
|
183
|
+
if (signalReservation && resolveTurnGenerationChain(signalReservation.store_root, signalReservation.turn_id)) {
|
|
184
|
+
v2SignalRunId = run.id;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch { /* strict evidence handling below remains fail-closed */ }
|
|
167
188
|
let completed_signal = false;
|
|
168
189
|
let failed_signal = false;
|
|
169
190
|
let heartbeat_exists = false;
|
|
170
191
|
let heartbeat_age_ms;
|
|
171
192
|
try {
|
|
172
|
-
completed_signal = signalExists(signalRoot, run.assignment_id, 'completed');
|
|
173
|
-
failed_signal = signalExists(signalRoot, run.assignment_id, 'failed');
|
|
193
|
+
completed_signal = signalExists(signalRoot, run.assignment_id, 'completed', v2SignalRunId);
|
|
194
|
+
failed_signal = signalExists(signalRoot, run.assignment_id, 'failed', v2SignalRunId);
|
|
174
195
|
// sprint 1.5: also read the worktree-local heartbeat — the only location a
|
|
175
196
|
// sandboxed worker can write (the project-root signal dir is outside its
|
|
176
197
|
// writable roots).
|
|
177
|
-
const hb = readHeartbeat(signalRoot, run.assignment_id, run.worktree_path);
|
|
198
|
+
const hb = readHeartbeat(signalRoot, run.assignment_id, run.worktree_path, v2SignalRunId);
|
|
178
199
|
heartbeat_exists = hb.exists;
|
|
179
200
|
if (hb.exists && hb.mtimeMs !== undefined)
|
|
180
201
|
heartbeat_age_ms = now - hb.mtimeMs;
|
|
@@ -185,7 +206,7 @@ export function collectEvidence(run, cwd, options) {
|
|
|
185
206
|
// its heartbeat is frozen (written once at step 0).
|
|
186
207
|
let fs_activity_age_ms;
|
|
187
208
|
try {
|
|
188
|
-
const lastFs = latestActivityMs(signalRoot, run.assignment_id, run.worktree_path);
|
|
209
|
+
const lastFs = latestActivityMs(signalRoot, run.assignment_id, run.worktree_path, v2SignalRunId);
|
|
189
210
|
if (lastFs !== undefined)
|
|
190
211
|
fs_activity_age_ms = now - lastFs;
|
|
191
212
|
}
|
|
@@ -198,20 +219,63 @@ export function collectEvidence(run, cwd, options) {
|
|
|
198
219
|
// (no owning reservation) keep presence-based acceptance.
|
|
199
220
|
let turn_owned = false;
|
|
200
221
|
let turn_keyed_completed = false;
|
|
222
|
+
let contract_acceptance_anomaly = Boolean(run.execution_contract_anomaly);
|
|
201
223
|
try {
|
|
202
224
|
const reservation = findReservationByRunId(run.id, cwd);
|
|
203
225
|
if (reservation) {
|
|
204
226
|
turn_owned = true;
|
|
205
|
-
const bodies = readCompletionSignals(signalRoot, run.assignment_id);
|
|
227
|
+
const bodies = readCompletionSignals(signalRoot, run.assignment_id, v2SignalRunId);
|
|
228
|
+
const bootstrapAck = readContractAck(signalRoot, run.assignment_id, v2SignalRunId);
|
|
229
|
+
const hasTerminalBody = bodies.completed !== undefined || bodies.failed !== undefined;
|
|
230
|
+
const generationChain = resolveTurnGenerationChain(reservation.store_root, reservation.turn_id);
|
|
231
|
+
const generation = generationChain?.latest_generation;
|
|
232
|
+
const generationApplies = generation !== undefined && generation.run_id === run.id
|
|
233
|
+
&& (generationChain?.status === 'active' || generationChain?.status === 'settled');
|
|
234
|
+
const generationLaunch = generationApplies
|
|
235
|
+
? readLaunchDecision(reservation.store_root, reservation.turn_id, generation.attempt_epoch)
|
|
236
|
+
: undefined;
|
|
237
|
+
const expectedContractRef = generationApplies
|
|
238
|
+
? executionContractForGeneration(reservation, generation).ref
|
|
239
|
+
: reservation.execution_contract_ref;
|
|
240
|
+
const bootstrapAccepted = !expectedContractRef || (bootstrapAck?.status === 'accepted'
|
|
241
|
+
&& bootstrapAck.turn_id === reservation.turn_id
|
|
242
|
+
&& bootstrapAck.run_id === (generationApplies ? generation.run_id : reservation.child_ids.run_id)
|
|
243
|
+
&& bootstrapAck.nonce === (generationApplies ? generation.launch_nonce : reservation.launch?.token)
|
|
244
|
+
&& (!generationApplies || bootstrapAck.cwd === normalizedWorkspace(generation.workspace_path))
|
|
245
|
+
&& (!generationApplies || bootstrapAck.attempt_epoch === generation.attempt_epoch)
|
|
246
|
+
&& (!generationApplies || bootstrapAck.workspace_digest === generation.workspace_digest)
|
|
247
|
+
&& validateWorkerContractAcceptance(expectedContractRef, {
|
|
248
|
+
contract_hash: bootstrapAck.contract_hash,
|
|
249
|
+
capability_snapshot_hash: bootstrapAck.capability_snapshot_hash,
|
|
250
|
+
}, generationApplies ? generationLaunch?.decision : reservation.launch?.status).kind === 'accepted');
|
|
251
|
+
if (expectedContractRef && ((bootstrapAck && !bootstrapAccepted) || (hasTerminalBody && !bootstrapAccepted))) {
|
|
252
|
+
contract_acceptance_anomaly = true;
|
|
253
|
+
}
|
|
254
|
+
const contractAccepted = (body) => {
|
|
255
|
+
if (!expectedContractRef)
|
|
256
|
+
return true;
|
|
257
|
+
if (!bootstrapAccepted)
|
|
258
|
+
return false;
|
|
259
|
+
if (!body?.contract_hash || !body.capability_snapshot_hash) {
|
|
260
|
+
contract_acceptance_anomaly = Boolean(body);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const verdict = validateWorkerContractAcceptance(expectedContractRef, { contract_hash: body.contract_hash, capability_snapshot_hash: body.capability_snapshot_hash }, generationApplies ? generationLaunch?.decision : reservation.launch?.status);
|
|
264
|
+
if (verdict.kind !== 'accepted')
|
|
265
|
+
contract_acceptance_anomaly = true;
|
|
266
|
+
return verdict.kind === 'accepted';
|
|
267
|
+
};
|
|
206
268
|
// Evidence must be turn-keyed AND carry the RIGHT status (a `.completed`
|
|
207
269
|
// file whose body says status:'failed' is not completion evidence —
|
|
208
270
|
// read-strict trusts the body, not the filename, review PR2b-c #C2).
|
|
209
271
|
const matchedCompleted = bodies.completed !== undefined
|
|
210
272
|
&& bodies.completed.status === 'completed'
|
|
211
|
-
&& evidenceMatchesAttempt(reservation, bodies.completed)
|
|
273
|
+
&& evidenceMatchesAttempt(reservation, { ...bodies.completed, assignment_id: run.assignment_id })
|
|
274
|
+
&& contractAccepted(bodies.completed);
|
|
212
275
|
const matchedFailed = bodies.failed !== undefined
|
|
213
276
|
&& bodies.failed.status === 'failed'
|
|
214
|
-
&& evidenceMatchesAttempt(reservation, bodies.failed)
|
|
277
|
+
&& evidenceMatchesAttempt(reservation, { ...bodies.failed, assignment_id: run.assignment_id })
|
|
278
|
+
&& contractAccepted(bodies.failed);
|
|
215
279
|
if (matchedCompleted && matchedFailed) {
|
|
216
280
|
// §13 R4 — a completed+failed contradiction WITHHOLDS both (never a
|
|
217
281
|
// silent accept). Conflict-event journaling is deferred to
|
|
@@ -234,7 +298,7 @@ export function collectEvidence(run, cwd, options) {
|
|
|
234
298
|
return {
|
|
235
299
|
age_ms, has_post_start_commit, claim_released, assignment_completed, process_alive,
|
|
236
300
|
completed_signal, failed_signal, heartbeat_exists, heartbeat_age_ms, fs_activity_age_ms,
|
|
237
|
-
turn_owned, turn_keyed_completed,
|
|
301
|
+
turn_owned, turn_keyed_completed, contract_acceptance_anomaly,
|
|
238
302
|
};
|
|
239
303
|
}
|
|
240
304
|
/**
|
|
@@ -322,6 +386,16 @@ export function reconcileStrandedFailureClaimAtRead(run, cwd, options = {}) {
|
|
|
322
386
|
if (!run.claim_id)
|
|
323
387
|
return false;
|
|
324
388
|
const now = options.nowMs ?? Date.now();
|
|
389
|
+
if (run.status_reason?.startsWith('reserved_never_launched:')) {
|
|
390
|
+
const reservation = findReservationByRunId(run.id, cwd);
|
|
391
|
+
const dispatchDeadline = reservation ? Date.parse(reservation.lease_deadline) : Number.NaN;
|
|
392
|
+
if (reservation?.decision === 'committed'
|
|
393
|
+
&& reservation.launch?.status === 'revoked'
|
|
394
|
+
&& Number.isFinite(dispatchDeadline)
|
|
395
|
+
&& now < dispatchDeadline) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
325
399
|
const anchor = Date.parse(run.completed_at ?? run.updated_at ?? run.created_at);
|
|
326
400
|
if (Number.isFinite(anchor) && now - anchor > STRANDED_RELEASE_RETRY_WINDOW_MS)
|
|
327
401
|
return false;
|
|
@@ -366,6 +440,8 @@ function describeEvidence(evidence) {
|
|
|
366
440
|
const reasons = [];
|
|
367
441
|
if (evidence.completed_signal)
|
|
368
442
|
reasons.push('wrapper wrote completed sentinel');
|
|
443
|
+
if (evidence.contract_acceptance_anomaly)
|
|
444
|
+
reasons.push('post-crossing contract acceptance anomaly (respawn forbidden)');
|
|
369
445
|
if (evidence.has_post_start_commit)
|
|
370
446
|
reasons.push('post-start commit on worktree branch');
|
|
371
447
|
if (evidence.claim_released)
|
|
@@ -457,7 +533,7 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
457
533
|
const evidence = {
|
|
458
534
|
age_ms: 0, has_post_start_commit: false, claim_released: false,
|
|
459
535
|
assignment_completed: false, process_alive: undefined,
|
|
460
|
-
completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false,
|
|
536
|
+
completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false, contract_acceptance_anomaly: false,
|
|
461
537
|
};
|
|
462
538
|
return {
|
|
463
539
|
run_id: runId, action: 'no_op', reason: 'run not found', evidence,
|
|
@@ -473,6 +549,23 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
473
549
|
evidence, previous_status, current_status: run.status,
|
|
474
550
|
};
|
|
475
551
|
}
|
|
552
|
+
if (evidence.contract_acceptance_anomaly) {
|
|
553
|
+
try {
|
|
554
|
+
recordExecutionContractAnomaly(runId, {
|
|
555
|
+
source: 'reconciler',
|
|
556
|
+
reason: 'bootstrap or terminal evidence did not accept the immutable execution contract',
|
|
557
|
+
}, cwd);
|
|
558
|
+
}
|
|
559
|
+
catch { /* ack/sentinel remains a durable fallback fence */ }
|
|
560
|
+
return {
|
|
561
|
+
run_id: runId,
|
|
562
|
+
action: 'health_check_unverified',
|
|
563
|
+
reason: 'post_crossing_contract_anomaly: accepted contract ref differs or is missing; convergence withheld and respawn=false',
|
|
564
|
+
evidence,
|
|
565
|
+
previous_status,
|
|
566
|
+
current_status: run.status,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
476
569
|
// pln#630 PR2c-lease (§4 + R5): a turn-owned run preallocated `created`/
|
|
477
570
|
// `launching` converges on its DISPATCH/LAUNCH LEASE, not the pid/heartbeat
|
|
478
571
|
// heuristics below (which assume a worker already spawned and can emit
|
|
@@ -586,8 +679,10 @@ export function reconcileAgentRun(runId, cwd, options = {}) {
|
|
|
586
679
|
* - within lease → NO-OP (the worker may yet cross the launch fence / start);
|
|
587
680
|
* - past lease + launch grant CROSSED → `failed` / `launch_attempted_unknown`
|
|
588
681
|
* (the worker WAS invoked; its outcome is unknowable, so it must not complete);
|
|
589
|
-
* - past lease + grant not crossed
|
|
590
|
-
*
|
|
682
|
+
* - past launch lease + grant not crossed, while dispatch lease is live →
|
|
683
|
+
* revoke and retain the non-terminal run for re-arm;
|
|
684
|
+
* - past dispatch lease + grant not crossed → `cancelled` /
|
|
685
|
+
* `reserved_never_launched` (the recovery window is exhausted).
|
|
591
686
|
*/
|
|
592
687
|
function reconcileTurnOwnedPreRunLease(run, reservation, evidence, cwd, options) {
|
|
593
688
|
const now = options.nowMs ?? Date.now();
|
|
@@ -620,13 +715,8 @@ function reconcileTurnOwnedPreRunLease(run, reservation, evidence, cwd, options)
|
|
|
620
715
|
}
|
|
621
716
|
// Past lease, no accepted completion — the launch-grant status (authoritative,
|
|
622
717
|
// decision-file reconciled) decides the terminal.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const targetStatus = crossed ? 'failed' : 'cancelled';
|
|
626
|
-
const action = crossed ? 'inferred_failed' : 'inferred_cancelled';
|
|
627
|
-
const reason = crossed
|
|
628
|
-
? `launch_attempted_unknown: launch grant crossed but run never reached running by lease ${leaseISO} — outcome unknown, never completed`
|
|
629
|
-
: `reserved_never_launched: no launch receipt by lease ${leaseISO} (grant=${grant?.status ?? 'none'})`;
|
|
718
|
+
let grant = launchGrant(reservation.turn_id, cwd);
|
|
719
|
+
let crossed = grant?.status === 'crossed';
|
|
630
720
|
// pln#630 dec#149 R1 (review Finding 2) — make the reserved_never_launched strand REACHABLE
|
|
631
721
|
// through the WIRED lazy reconciler: revoke the still-armed grant so its authoritative status
|
|
632
722
|
// becomes `revoked`. That is what lets reconcileTurn's fix-cycle strand detector see the strand
|
|
@@ -638,6 +728,42 @@ function reconcileTurnOwnedPreRunLease(run, reservation, evidence, cwd, options)
|
|
|
638
728
|
revokeLaunchGrant(reservation.turn_id, grant.epoch, 'reserved_never_launched', cwd, actor);
|
|
639
729
|
}
|
|
640
730
|
catch { /* raced to crossed/revoked — authoritative status governs */ }
|
|
731
|
+
grant = launchGrant(reservation.turn_id, cwd);
|
|
732
|
+
crossed = grant?.status === 'crossed';
|
|
733
|
+
}
|
|
734
|
+
if (evidence.contract_acceptance_anomaly) {
|
|
735
|
+
try {
|
|
736
|
+
recordExecutionContractAnomaly(run.id, {
|
|
737
|
+
source: 'reconciler',
|
|
738
|
+
reason: 'bootstrap or terminal evidence did not accept the immutable execution contract',
|
|
739
|
+
}, cwd);
|
|
740
|
+
}
|
|
741
|
+
catch { /* ack/sentinel remains a durable fallback fence */ }
|
|
742
|
+
return {
|
|
743
|
+
run_id: run.id,
|
|
744
|
+
action: 'health_check_unverified',
|
|
745
|
+
reason: 'post_crossing_contract_anomaly: accepted contract ref differs or is missing; convergence withheld and respawn=false',
|
|
746
|
+
evidence,
|
|
747
|
+
previous_status,
|
|
748
|
+
current_status: run.status,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
const targetStatus = crossed ? 'failed' : 'cancelled';
|
|
752
|
+
const action = crossed ? 'inferred_failed' : 'inferred_cancelled';
|
|
753
|
+
const reason = crossed
|
|
754
|
+
? `launch_attempted_unknown: launch grant crossed but run never reached running by lease ${leaseISO} — outcome unknown, never completed`
|
|
755
|
+
: `reserved_never_launched: no launch receipt by lease ${leaseISO} (grant=${grant?.status ?? 'none'})`;
|
|
756
|
+
const dispatchLeaseMs = Date.parse(reservation.lease_deadline);
|
|
757
|
+
const dispatchLeaseLive = Number.isFinite(dispatchLeaseMs) && now < dispatchLeaseMs;
|
|
758
|
+
if (!crossed && dispatchLeaseLive) {
|
|
759
|
+
return {
|
|
760
|
+
run_id: run.id,
|
|
761
|
+
action: 'no_op',
|
|
762
|
+
reason: `${reason}; retained non-terminal for re-arm until dispatch lease ${reservation.lease_deadline}`,
|
|
763
|
+
evidence,
|
|
764
|
+
previous_status,
|
|
765
|
+
current_status: run.status,
|
|
766
|
+
};
|
|
641
767
|
}
|
|
642
768
|
try {
|
|
643
769
|
transitionAgentRun(run.id, targetStatus, { actor, status_reason: reason }, cwd);
|
|
@@ -680,7 +806,7 @@ export function reconcileDeadPidRunningAgentRunAtRead(runId, cwd, options = {})
|
|
|
680
806
|
const evidence = {
|
|
681
807
|
age_ms: 0, has_post_start_commit: false, claim_released: false,
|
|
682
808
|
assignment_completed: false, process_alive: undefined,
|
|
683
|
-
completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false,
|
|
809
|
+
completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false, contract_acceptance_anomaly: false,
|
|
684
810
|
};
|
|
685
811
|
return {
|
|
686
812
|
run_id: runId, action: 'no_op', reason: 'run not found', evidence,
|
|
@@ -854,7 +980,7 @@ export function reconcileAllOpenRuns(cwd, filter = {}, options = {}) {
|
|
|
854
980
|
catch {
|
|
855
981
|
results.push({
|
|
856
982
|
run_id: run.id, action: 'no_op', reason: 'reconcile threw — skipped',
|
|
857
|
-
evidence: { age_ms: 0, has_post_start_commit: false, claim_released: false, assignment_completed: false, process_alive: undefined, completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false },
|
|
983
|
+
evidence: { age_ms: 0, has_post_start_commit: false, claim_released: false, assignment_completed: false, process_alive: undefined, completed_signal: false, failed_signal: false, heartbeat_exists: false, turn_owned: false, turn_keyed_completed: false, contract_acceptance_anomaly: false },
|
|
858
984
|
previous_status: run.status, current_status: run.status,
|
|
859
985
|
});
|
|
860
986
|
}
|
package/dist/core/agentruns.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { AgentRunSchema } from './schema.js';
|
|
12
|
+
import { RuntimeCapabilityObservationSchema } from './execution-contract.js';
|
|
12
13
|
import { resolveOwnerProjectId } from './config.js';
|
|
13
14
|
import { entityRecordDirs, resolveEntityDir } from './io.js';
|
|
14
15
|
import { mutate } from './mutation-pipeline.js';
|
|
@@ -18,6 +19,47 @@ import { appendAuditEntry } from './audit.js';
|
|
|
18
19
|
import { appendEvent } from './event-log.js';
|
|
19
20
|
import { createRuntimeEvent } from './events.js';
|
|
20
21
|
import { emitRegistryPostImage, registryFaultPoint } from './events/registry-post-image.js';
|
|
22
|
+
import { findReservationByRunId } from './loops/attempt-reservation.js';
|
|
23
|
+
import { resolveTurnGenerationChain } from './loops/attempt-generations.js';
|
|
24
|
+
export class AgentRunFencedError extends Error {
|
|
25
|
+
runId;
|
|
26
|
+
activeRunId;
|
|
27
|
+
attemptEpoch;
|
|
28
|
+
authorityStatus;
|
|
29
|
+
constructor(runId, activeRunId, attemptEpoch, authorityStatus) {
|
|
30
|
+
super(`AgentRun ${runId} is fenced by attempt epoch ${attemptEpoch}`
|
|
31
|
+
+ `${activeRunId ? ` (authoritative run: ${activeRunId})` : ''}`
|
|
32
|
+
+ ` [${authorityStatus}]`);
|
|
33
|
+
this.runId = runId;
|
|
34
|
+
this.activeRunId = activeRunId;
|
|
35
|
+
this.attemptEpoch = attemptEpoch;
|
|
36
|
+
this.authorityStatus = authorityStatus;
|
|
37
|
+
this.name = 'AgentRunFencedError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Refuse late writes from an AgentRun superseded by attempt-authority v2.
|
|
42
|
+
*
|
|
43
|
+
* The immutable generation chain is authoritative; assignment status and the
|
|
44
|
+
* mutable head projection are deliberately not consulted. A settled latest
|
|
45
|
+
* generation may still finish its replayable projections after publishing its
|
|
46
|
+
* close cell. Controllers may explicitly override this guard only to project a
|
|
47
|
+
* terminal state onto the run that they just fenced.
|
|
48
|
+
*/
|
|
49
|
+
function assertAgentRunMutationAllowed(id, cwd, allowFencedProjection = false) {
|
|
50
|
+
if (allowFencedProjection)
|
|
51
|
+
return;
|
|
52
|
+
const reservation = findReservationByRunId(id, cwd);
|
|
53
|
+
if (!reservation)
|
|
54
|
+
return;
|
|
55
|
+
const chain = resolveTurnGenerationChain(reservation.store_root, reservation.turn_id);
|
|
56
|
+
if (!chain)
|
|
57
|
+
return; // Legacy attempt authority remains compatible.
|
|
58
|
+
const latest = chain.latest_generation;
|
|
59
|
+
if (latest.run_id === id && (chain.status === 'active' || chain.status === 'settled'))
|
|
60
|
+
return;
|
|
61
|
+
throw new AgentRunFencedError(id, chain.status === 'active' ? latest.run_id : null, latest.attempt_epoch, chain.status);
|
|
62
|
+
}
|
|
21
63
|
function agentRunsDir(cwd, mode = 'read') {
|
|
22
64
|
return resolveEntityDir('runs', cwd, mode);
|
|
23
65
|
}
|
|
@@ -45,33 +87,37 @@ function agentRunStoreForDir(dirPath) {
|
|
|
45
87
|
// loader found it; removing it stops the next reader from reintroducing that asymmetry.
|
|
46
88
|
export function saveAgentRun(run, cwd) {
|
|
47
89
|
mutate({ cwd }, () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
registryFaultPoint('after_registry_journal');
|
|
60
|
-
store.save(parsed);
|
|
61
|
-
// Converge the other layout (mirrors saveClaim / saveAssignment): leaving a legacy copy
|
|
62
|
-
// holding the stale status is what let a deleted record be resurrected by its own zombie.
|
|
63
|
-
const writeDir = agentRunsDir(cwd, 'write');
|
|
64
|
-
for (const dirPath of entityRecordDirs('runs', cwd ?? process.cwd())) {
|
|
65
|
-
if (dirPath === writeDir)
|
|
66
|
-
continue;
|
|
67
|
-
const legacyPath = path.join(dirPath, `${parsed.id}.json`);
|
|
68
|
-
try {
|
|
69
|
-
if (fs.existsSync(legacyPath))
|
|
70
|
-
fs.unlinkSync(legacyPath);
|
|
71
|
-
}
|
|
72
|
-
catch { /* best effort — the dual-layout list keeps it visible */ }
|
|
73
|
-
}
|
|
90
|
+
saveAgentRunUnlocked(run, cwd);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Store-lock caller variant used by create-or-validate projection repair. */
|
|
94
|
+
function saveAgentRunUnlocked(run, cwd) {
|
|
95
|
+
ensureAgentRunsDir(cwd);
|
|
96
|
+
const store = new JsonStore({
|
|
97
|
+
dirPath: agentRunsDir(cwd, 'write'),
|
|
98
|
+
documentType: 'agent_run',
|
|
99
|
+
getId: (item) => item.id,
|
|
100
|
+
sort: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
74
101
|
});
|
|
102
|
+
const parsed = AgentRunSchema.parse(run);
|
|
103
|
+
// pln#568 (I2): journal the post-image BEFORE the projection write.
|
|
104
|
+
const created = !store.exists(parsed.id);
|
|
105
|
+
emitRegistryPostImage('agent_run', parsed, { created, agent: parsed.agent, agent_id: parsed.agent_id, session_id: parsed.session_id, cwd });
|
|
106
|
+
registryFaultPoint('after_registry_journal');
|
|
107
|
+
store.save(parsed);
|
|
108
|
+
// Converge the other layout (mirrors saveClaim / saveAssignment): leaving a legacy copy
|
|
109
|
+
// holding the stale status is what let a deleted record be resurrected by its own zombie.
|
|
110
|
+
const writeDir = agentRunsDir(cwd, 'write');
|
|
111
|
+
for (const dirPath of entityRecordDirs('runs', cwd ?? process.cwd())) {
|
|
112
|
+
if (dirPath === writeDir)
|
|
113
|
+
continue;
|
|
114
|
+
const legacyPath = path.join(dirPath, `${parsed.id}.json`);
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(legacyPath))
|
|
117
|
+
fs.unlinkSync(legacyPath);
|
|
118
|
+
}
|
|
119
|
+
catch { /* best effort — the dual-layout list keeps it visible */ }
|
|
120
|
+
}
|
|
75
121
|
}
|
|
76
122
|
export function loadAgentRun(id, cwd) {
|
|
77
123
|
// Record-specific across both layouts (pln#649, shared io.ts primitive). Resolving
|
|
@@ -96,6 +142,92 @@ export function loadAgentRun(id, cwd) {
|
|
|
96
142
|
}
|
|
97
143
|
return undefined;
|
|
98
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Persist the first execution-contract anomaly for a run.
|
|
147
|
+
*
|
|
148
|
+
* The field is deliberately monotone: later correct-looking evidence cannot
|
|
149
|
+
* erase an already-observed post-crossing mismatch and reopen convergence.
|
|
150
|
+
*/
|
|
151
|
+
export function recordExecutionContractAnomaly(id, anomaly, cwd) {
|
|
152
|
+
assertAgentRunMutationAllowed(id, cwd);
|
|
153
|
+
return mutate({ cwd }, () => {
|
|
154
|
+
const run = loadAgentRun(id, cwd);
|
|
155
|
+
if (!run)
|
|
156
|
+
throw new Error(`AgentRun not found: ${id}`);
|
|
157
|
+
if (run.execution_contract_anomaly)
|
|
158
|
+
return run;
|
|
159
|
+
const now = nowISO();
|
|
160
|
+
run.execution_contract_anomaly = {
|
|
161
|
+
detected_at: now,
|
|
162
|
+
source: anomaly.source,
|
|
163
|
+
reason: anomaly.reason,
|
|
164
|
+
accepted_contract_hash: anomaly.accepted_contract_hash,
|
|
165
|
+
accepted_capability_snapshot_hash: anomaly.accepted_capability_snapshot_hash,
|
|
166
|
+
};
|
|
167
|
+
run.updated_at = now;
|
|
168
|
+
run.last_event_at = now;
|
|
169
|
+
saveAgentRunUnlocked(run, cwd);
|
|
170
|
+
return run;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Persist a post-start observation without ever rewriting the frozen snapshot. */
|
|
174
|
+
export function recordRuntimeCapabilityObservation(id, observation, diagnostic, cwd) {
|
|
175
|
+
assertAgentRunMutationAllowed(id, cwd);
|
|
176
|
+
return mutate({ cwd }, () => {
|
|
177
|
+
const run = loadAgentRun(id, cwd);
|
|
178
|
+
if (!run)
|
|
179
|
+
throw new Error(`AgentRun not found: ${id}`);
|
|
180
|
+
const parsed = RuntimeCapabilityObservationSchema.parse(observation);
|
|
181
|
+
if (run.runtime_capability_observation) {
|
|
182
|
+
if (JSON.stringify(run.runtime_capability_observation) !== JSON.stringify(parsed)) {
|
|
183
|
+
throw new Error(`AgentRun ${id} already has a different runtime capability observation`);
|
|
184
|
+
}
|
|
185
|
+
return run;
|
|
186
|
+
}
|
|
187
|
+
const now = nowISO();
|
|
188
|
+
run.runtime_capability_observation = parsed;
|
|
189
|
+
if (diagnostic)
|
|
190
|
+
run.harness_exit_diagnostic = diagnostic;
|
|
191
|
+
const frozenRef = run.execution_contract_ref;
|
|
192
|
+
const frozenHarness = run.capability_snapshot?.resolved.harness;
|
|
193
|
+
const resolvedModel = frozenHarness?.resolved_model ?? run.capability_snapshot?.resolved.model;
|
|
194
|
+
const mismatches = [];
|
|
195
|
+
if (frozenRef?.hash !== parsed.contract_hash) {
|
|
196
|
+
mismatches.push(`observed contract hash '${parsed.contract_hash}' differs from frozen '${frozenRef?.hash ?? 'absent'}'`);
|
|
197
|
+
}
|
|
198
|
+
if (frozenRef?.snapshot_hash !== parsed.capability_snapshot_hash) {
|
|
199
|
+
mismatches.push(`observed capability snapshot hash '${parsed.capability_snapshot_hash}' differs from frozen '${frozenRef?.snapshot_hash ?? 'absent'}'`);
|
|
200
|
+
}
|
|
201
|
+
if (parsed.accepted_contract_hash && parsed.accepted_contract_hash !== frozenRef?.hash) {
|
|
202
|
+
mismatches.push(`accepted contract hash '${parsed.accepted_contract_hash}' differs from frozen '${frozenRef?.hash ?? 'absent'}'`);
|
|
203
|
+
}
|
|
204
|
+
if (parsed.accepted_capability_snapshot_hash && parsed.accepted_capability_snapshot_hash !== frozenRef?.snapshot_hash) {
|
|
205
|
+
mismatches.push(`accepted capability snapshot hash '${parsed.accepted_capability_snapshot_hash}' differs from frozen '${frozenRef?.snapshot_hash ?? 'absent'}'`);
|
|
206
|
+
}
|
|
207
|
+
if (frozenHarness && parsed.adapter_id && frozenHarness.adapter_id !== parsed.adapter_id) {
|
|
208
|
+
mismatches.push(`observed adapter '${parsed.adapter_id}' differs from frozen '${frozenHarness.adapter_id}'`);
|
|
209
|
+
}
|
|
210
|
+
if (frozenHarness && parsed.adapter_version && frozenHarness.adapter_version !== parsed.adapter_version) {
|
|
211
|
+
mismatches.push(`observed adapter version '${parsed.adapter_version}' differs from frozen '${frozenHarness.adapter_version}'`);
|
|
212
|
+
}
|
|
213
|
+
if (resolvedModel && parsed.observed_model && resolvedModel !== parsed.observed_model) {
|
|
214
|
+
mismatches.push(`observed model '${parsed.observed_model}' differs from frozen resolved model '${resolvedModel}'`);
|
|
215
|
+
}
|
|
216
|
+
if (mismatches.length > 0 && !run.execution_contract_anomaly) {
|
|
217
|
+
run.execution_contract_anomaly = {
|
|
218
|
+
detected_at: now,
|
|
219
|
+
source: 'reconciler',
|
|
220
|
+
reason: mismatches.join('; '),
|
|
221
|
+
accepted_contract_hash: parsed.accepted_contract_hash,
|
|
222
|
+
accepted_capability_snapshot_hash: parsed.accepted_capability_snapshot_hash,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
run.updated_at = now;
|
|
226
|
+
run.last_event_at = now;
|
|
227
|
+
saveAgentRunUnlocked(run, cwd);
|
|
228
|
+
return run;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
99
231
|
/**
|
|
100
232
|
* BOTH LAYOUTS, canonical winning on a duplicate id (mirrors listClaims / listAssignments).
|
|
101
233
|
*
|
|
@@ -171,10 +303,10 @@ const VALID_TRANSITIONS = new Map([
|
|
|
171
303
|
['timed_out', new Set()],
|
|
172
304
|
['interrupted', new Set()],
|
|
173
305
|
]);
|
|
174
|
-
|
|
306
|
+
function buildAgentRun(options, cwd) {
|
|
175
307
|
const generated = options.id ? undefined : generateAgentRunId(cwd);
|
|
176
308
|
const now = nowISO();
|
|
177
|
-
|
|
309
|
+
return AgentRunSchema.parse({
|
|
178
310
|
schema_version: 1,
|
|
179
311
|
id: options.id ?? generated.id,
|
|
180
312
|
// Same landmine as createAssignment: `generated` is undefined when the caller
|
|
@@ -202,6 +334,8 @@ export function createAgentRun(options, cwd) {
|
|
|
202
334
|
shell: options.shell,
|
|
203
335
|
pid: options.pid,
|
|
204
336
|
provider_run_id: options.provider_run_id,
|
|
337
|
+
execution_contract_ref: options.execution_contract_ref,
|
|
338
|
+
capability_snapshot: options.capability_snapshot,
|
|
205
339
|
created_at: now,
|
|
206
340
|
updated_at: now,
|
|
207
341
|
last_event_at: now,
|
|
@@ -211,7 +345,8 @@ export function createAgentRun(options, cwd) {
|
|
|
211
345
|
artifacts: [],
|
|
212
346
|
tags: options.tags ?? [],
|
|
213
347
|
});
|
|
214
|
-
|
|
348
|
+
}
|
|
349
|
+
function emitAgentRunCreatedSideEffects(run, options, cwd) {
|
|
215
350
|
emitAgentRunEvent(run, 'run_created', options.agent, cwd);
|
|
216
351
|
if (run.status !== 'created') {
|
|
217
352
|
emitAgentRunEvent(run, `run_${run.status}`, options.agent, cwd);
|
|
@@ -226,9 +361,98 @@ export function createAgentRun(options, cwd) {
|
|
|
226
361
|
scope: run.scope,
|
|
227
362
|
session_id: run.session_id,
|
|
228
363
|
}, cwd);
|
|
364
|
+
}
|
|
365
|
+
export function createAgentRun(options, cwd) {
|
|
366
|
+
const run = buildAgentRun(options, cwd);
|
|
367
|
+
saveAgentRun(run, cwd);
|
|
368
|
+
emitAgentRunCreatedSideEffects(run, options, cwd);
|
|
229
369
|
return run;
|
|
230
370
|
}
|
|
371
|
+
export class AgentRunProjectionConflictError extends Error {
|
|
372
|
+
runId;
|
|
373
|
+
constructor(runId, detail) {
|
|
374
|
+
super(`AgentRun projection conflict for ${runId}: ${detail}`);
|
|
375
|
+
this.runId = runId;
|
|
376
|
+
this.name = 'AgentRunProjectionConflictError';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const RECOVERABLE_PROJECTION_RUN_STATUSES = new Set([
|
|
380
|
+
'created', 'launching', 'waiting_input', 'running',
|
|
381
|
+
]);
|
|
382
|
+
function assertAgentRunProjectionMatches(existing, expected) {
|
|
383
|
+
const fields = [
|
|
384
|
+
'id', 'assignment_id', 'claim_id', 'agent', 'agent_id', 'transport', 'scope',
|
|
385
|
+
'worktree_path', 'attempt_index',
|
|
386
|
+
];
|
|
387
|
+
for (const field of fields) {
|
|
388
|
+
if (existing[field] !== expected[field]) {
|
|
389
|
+
throw new AgentRunProjectionConflictError(expected.id, `${String(field)} differs (existing=${String(existing[field])}, expected=${String(expected[field])})`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (existing.project_id !== undefined && existing.project_id !== expected.project_id) {
|
|
393
|
+
throw new AgentRunProjectionConflictError(expected.id, 'project_id differs');
|
|
394
|
+
}
|
|
395
|
+
if (existing.execution_contract_ref !== undefined
|
|
396
|
+
&& expected.execution_contract_ref !== undefined
|
|
397
|
+
&& JSON.stringify(existing.execution_contract_ref) !== JSON.stringify(expected.execution_contract_ref)) {
|
|
398
|
+
throw new AgentRunProjectionConflictError(expected.id, 'execution_contract_ref differs');
|
|
399
|
+
}
|
|
400
|
+
if (existing.capability_snapshot !== undefined
|
|
401
|
+
&& expected.capability_snapshot !== undefined
|
|
402
|
+
&& JSON.stringify(existing.capability_snapshot) !== JSON.stringify(expected.capability_snapshot)) {
|
|
403
|
+
throw new AgentRunProjectionConflictError(expected.id, 'capability_snapshot differs');
|
|
404
|
+
}
|
|
405
|
+
if (!RECOVERABLE_PROJECTION_RUN_STATUSES.has(existing.status)) {
|
|
406
|
+
throw new AgentRunProjectionConflictError(expected.id, `existing status is terminal (${existing.status})`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Create-or-validate a deterministic AgentRun projection for one physical
|
|
411
|
+
* generation of a logical turn. P0 callers omit `attempt_index` and therefore
|
|
412
|
+
* keep the legacy value 1; AttemptAuthority v2 supplies the immutable
|
|
413
|
+
* generation index. Recovery never changes an existing run's generation and
|
|
414
|
+
* never resets an existing live run to `created`.
|
|
415
|
+
*/
|
|
416
|
+
export function ensureAgentRunProjection(options, cwd) {
|
|
417
|
+
const normalized = {
|
|
418
|
+
...options,
|
|
419
|
+
attempt_index: options.attempt_index ?? 1,
|
|
420
|
+
};
|
|
421
|
+
const expected = buildAgentRun(normalized, cwd);
|
|
422
|
+
let created = false;
|
|
423
|
+
let repaired = false;
|
|
424
|
+
const run = mutate({ cwd }, () => {
|
|
425
|
+
const existing = loadAgentRun(options.id, cwd);
|
|
426
|
+
if (existing) {
|
|
427
|
+
const requiredTags = normalized.tags ?? [];
|
|
428
|
+
const missingContractRef = existing.execution_contract_ref === undefined && expected.execution_contract_ref !== undefined;
|
|
429
|
+
const missingCapabilitySnapshot = existing.capability_snapshot === undefined && expected.capability_snapshot !== undefined;
|
|
430
|
+
const missingTags = !requiredTags.every((tag) => existing.tags.includes(tag));
|
|
431
|
+
const enriched = !missingContractRef && !missingCapabilitySnapshot && !missingTags
|
|
432
|
+
? existing
|
|
433
|
+
: {
|
|
434
|
+
...existing,
|
|
435
|
+
...(missingContractRef ? { execution_contract_ref: expected.execution_contract_ref } : {}),
|
|
436
|
+
...(missingCapabilitySnapshot ? { capability_snapshot: expected.capability_snapshot } : {}),
|
|
437
|
+
tags: [...new Set([...existing.tags, ...requiredTags])],
|
|
438
|
+
};
|
|
439
|
+
assertAgentRunProjectionMatches(enriched, expected);
|
|
440
|
+
if (enriched !== existing) {
|
|
441
|
+
saveAgentRunUnlocked(enriched, cwd);
|
|
442
|
+
repaired = true;
|
|
443
|
+
}
|
|
444
|
+
return enriched;
|
|
445
|
+
}
|
|
446
|
+
saveAgentRunUnlocked(expected, cwd);
|
|
447
|
+
created = true;
|
|
448
|
+
return expected;
|
|
449
|
+
});
|
|
450
|
+
if (created)
|
|
451
|
+
emitAgentRunCreatedSideEffects(run, normalized, cwd);
|
|
452
|
+
return { run, created, ...(repaired ? { repaired: true } : {}) };
|
|
453
|
+
}
|
|
231
454
|
export function transitionAgentRun(id, newStatus, options = {}, cwd) {
|
|
455
|
+
assertAgentRunMutationAllowed(id, cwd, options.allow_fenced_projection);
|
|
232
456
|
const run = loadAgentRun(id, cwd);
|
|
233
457
|
if (!run)
|
|
234
458
|
throw new Error(`AgentRun not found: ${id}`);
|
|
@@ -316,6 +540,7 @@ export function transitionAgentRun(id, newStatus, options = {}, cwd) {
|
|
|
316
540
|
return { run, previous_status };
|
|
317
541
|
}
|
|
318
542
|
export function recordAgentRunProgress(id, options = {}, cwd) {
|
|
543
|
+
assertAgentRunMutationAllowed(id, cwd);
|
|
319
544
|
const run = loadAgentRun(id, cwd);
|
|
320
545
|
if (!run)
|
|
321
546
|
throw new Error(`AgentRun not found: ${id}`);
|
|
@@ -80,6 +80,13 @@ export const AssignmentUpdateRequestSchema = z.object({
|
|
|
80
80
|
error_message: z.string().describe('Error details (for failed status).').optional(),
|
|
81
81
|
blocker: z.string().describe('Blocker description (for blocked status).').optional(),
|
|
82
82
|
action_required: ActionRequiredSchema.optional(),
|
|
83
|
+
/** AttemptAuthority v2 fence. Required together once the Assignment has a v2 generation chain. */
|
|
84
|
+
turn_id: z.string().optional(),
|
|
85
|
+
run_id: z.string().optional(),
|
|
86
|
+
nonce: z.string().optional(),
|
|
87
|
+
attempt_epoch: z.number().int().nonnegative().optional(),
|
|
88
|
+
execution_contract_hash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
89
|
+
workspace_digest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
83
90
|
...CallerIdentity,
|
|
84
91
|
});
|
|
85
92
|
export const AssignmentActionRequestSchema = z.object({
|