brainclaw 1.28.1 → 1.28.2
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/brainclaw-vscode.vsix +0 -0
- package/dist/commands/code-map.js +2 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/harvest.js +32 -43
- package/dist/commands/loops-handlers.js +66 -3
- package/dist/commands/mcp-write-coordination.js +413 -137
- package/dist/commands/mcp.js +32 -4
- package/dist/core/agentrun-reconciler.js +138 -4
- package/dist/core/claims.js +4 -1
- package/dist/core/code-map/backend.js +8 -0
- package/dist/core/execution-adapters.js +15 -7
- package/dist/core/hygiene-policy.js +2 -1
- package/dist/core/loop-turn-dispatch.js +18 -1
- package/dist/core/loops/attempt-authority.js +22 -4
- package/dist/core/loops/attempt-generations.js +17 -4
- package/dist/core/loops/attempt-reservation.js +14 -1
- package/dist/core/loops/attempt-takeover.js +173 -76
- package/dist/core/loops/reconcile-turn.js +224 -26
- package/dist/core/loops/result-reducers.js +8 -8
- package/dist/core/loops/turn-execution.js +38 -19
- package/dist/core/loops/types.js +3 -0
- package/dist/core/loops/verbs.js +1 -1
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/docs/code-map.md +10 -0
- package/docs/concepts/loop-engine.md +5 -0
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getReservation, evidenceMatchesAttempt, currentNonce, launchGrant, resolveTurnId } from './attempt-reservation.js';
|
|
3
|
+
import { getReservation, evidenceMatchesAttempt, currentNonce, findReservationByAssignmentId, findReservationByRunId, launchGrant, resolveTurnId } from './attempt-reservation.js';
|
|
4
4
|
import { getLoop } from './store.js';
|
|
5
5
|
import { completeTurnWithEvidence, addArtifactWithEvidence, complete_turn, advance } from './verbs.js';
|
|
6
6
|
import { reducerForKind } from './result-reducers.js';
|
|
7
7
|
import { loadAgentRun, recordExecutionContractAnomaly, transitionAgentRun } from '../agentruns.js';
|
|
8
|
-
import { loadAssignment, transitionAssignment } from '../assignments.js';
|
|
8
|
+
import { convergeAssignmentToTerminal, loadAssignment, transitionAssignment } from '../assignments.js';
|
|
9
9
|
import { loadClaim, releaseClaim, releaseClaimIfActive } from '../claims.js';
|
|
10
10
|
import { createRuntimeEvent } from '../events.js';
|
|
11
11
|
import { readCompletionSignals, readContractAck } from '../runtime-signals.js';
|
|
@@ -17,6 +17,143 @@ import { executionContractForGeneration, settleActiveAttemptGenerationV2 } from
|
|
|
17
17
|
import { fenceForGeneration, resolveTurnGenerationChain } from './attempt-generations.js';
|
|
18
18
|
import { readLocalAuthorityHome } from './attempt-rollout.js';
|
|
19
19
|
import { LaneResultSchema } from '../schema.js';
|
|
20
|
+
/** Prefer the current run-scoped runtime channel, with assignment-scoped files
|
|
21
|
+
* retained only as a compatibility fallback for pre-run-keyed dispatches. */
|
|
22
|
+
function readAttemptCompletionSignals(root, assignmentId, runId) {
|
|
23
|
+
const scoped = readCompletionSignals(root, assignmentId, runId);
|
|
24
|
+
const legacy = readCompletionSignals(root, assignmentId);
|
|
25
|
+
return {
|
|
26
|
+
completed: scoped.completed ?? legacy.completed,
|
|
27
|
+
failed: scoped.failed ?? legacy.failed,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function readAttemptContractAck(root, assignmentId, runId) {
|
|
31
|
+
return readContractAck(root, assignmentId, runId) ?? readContractAck(root, assignmentId);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the authoritative generation coordinates for a worker result.
|
|
35
|
+
*
|
|
36
|
+
* A file-fallback LANE-RESULT may omit the mechanical fence fields because the
|
|
37
|
+
* wrapper already wrote them to its completion signal. The worker-controlled
|
|
38
|
+
* fields always win when present, so stale/mismatched evidence is still rejected
|
|
39
|
+
* by reconcileTurn. Missing fields are filled only from the reservation's active
|
|
40
|
+
* generation and its run-keyed completion signal.
|
|
41
|
+
*/
|
|
42
|
+
export function turnOwnedLaneEvidence(lane, cwd, reservationOverride) {
|
|
43
|
+
const reservation = reservationOverride ?? findReservationByAssignmentId(lane.assignment_id, cwd);
|
|
44
|
+
if (!reservation)
|
|
45
|
+
return undefined;
|
|
46
|
+
const chain = resolveTurnGenerationChain(cwd, reservation.turn_id);
|
|
47
|
+
const generation = chain?.latest_generation;
|
|
48
|
+
const runId = generation?.run_id ?? reservation.child_ids.run_id;
|
|
49
|
+
const completion = readAttemptCompletionSignals(cwd, generation?.assignment_id ?? reservation.child_ids.assignment_id, runId).completed;
|
|
50
|
+
const bootstrapAck = readAttemptContractAck(cwd, generation?.assignment_id ?? reservation.child_ids.assignment_id, runId);
|
|
51
|
+
// The pre-exec bootstrap ACK is the first durable, coordinator-authored proof
|
|
52
|
+
// that this exact launch generation crossed into the worker. Real file-fallback
|
|
53
|
+
// workers commonly write LANE-RESULT before the wrapper can emit its terminal
|
|
54
|
+
// sentinel, and they do not echo the mechanical turn/run/nonce tuple. In that
|
|
55
|
+
// window, enrich only from an ACCEPTED run-keyed ACK; reconcileTurn still checks
|
|
56
|
+
// every coordinate against the active generation, so a rejected/foreign/stale
|
|
57
|
+
// ACK cannot authorize convergence and an explicit lane value always wins.
|
|
58
|
+
const acceptedAck = bootstrapAck?.status === 'accepted' ? bootstrapAck : undefined;
|
|
59
|
+
const nonce = lane.nonce ?? completion?.nonce ?? acceptedAck?.nonce;
|
|
60
|
+
if (!nonce && !reservation.execution_contract_ref)
|
|
61
|
+
return undefined;
|
|
62
|
+
return {
|
|
63
|
+
reservation,
|
|
64
|
+
nonce,
|
|
65
|
+
run_id: runId,
|
|
66
|
+
attempt_epoch: lane.attempt_epoch ?? completion?.attempt_epoch ?? acceptedAck?.attempt_epoch ?? generation?.attempt_epoch,
|
|
67
|
+
workspace_digest: lane.workspace_digest ?? completion?.workspace_digest ?? acceptedAck?.workspace_digest ?? generation?.workspace_digest,
|
|
68
|
+
contract_hash: lane.execution_contract_hash
|
|
69
|
+
?? completion?.contract_hash
|
|
70
|
+
?? acceptedAck?.contract_hash
|
|
71
|
+
?? generation?.contract_hash
|
|
72
|
+
?? reservation.execution_contract_ref?.hash,
|
|
73
|
+
capability_snapshot_hash: lane.capability_snapshot_hash
|
|
74
|
+
?? completion?.capability_snapshot_hash
|
|
75
|
+
?? acceptedAck?.capability_snapshot_hash
|
|
76
|
+
?? reservation.execution_contract_ref?.snapshot_hash,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Reconcile one parsed worker result through the single AttemptAuthority path. */
|
|
80
|
+
export function reconcileTurnOwnedLane(lane, cwd, evidence, actor) {
|
|
81
|
+
const ev = evidence ?? turnOwnedLaneEvidence(lane, cwd);
|
|
82
|
+
if (!ev)
|
|
83
|
+
return undefined;
|
|
84
|
+
const { reservation } = ev;
|
|
85
|
+
const enrichedLane = {
|
|
86
|
+
...lane,
|
|
87
|
+
turn_id: lane.turn_id ?? reservation.turn_id,
|
|
88
|
+
run_id: lane.run_id ?? ev.run_id,
|
|
89
|
+
nonce: lane.nonce ?? ev.nonce,
|
|
90
|
+
attempt_epoch: lane.attempt_epoch ?? ev.attempt_epoch,
|
|
91
|
+
workspace_digest: lane.workspace_digest ?? ev.workspace_digest,
|
|
92
|
+
execution_contract_hash: lane.execution_contract_hash ?? ev.contract_hash,
|
|
93
|
+
capability_snapshot_hash: lane.capability_snapshot_hash ?? ev.capability_snapshot_hash,
|
|
94
|
+
};
|
|
95
|
+
const loop = getLoop(reservation.loop_id, cwd);
|
|
96
|
+
const critiques = loop?.kind === 'ideation'
|
|
97
|
+
&& reservation.phase === 'critique'
|
|
98
|
+
&& lane.artifact_type === 'critique'
|
|
99
|
+
&& (lane.body ?? '').trim().length > 0
|
|
100
|
+
? [{ body: lane.body.trim() }]
|
|
101
|
+
: undefined;
|
|
102
|
+
const result = reconcileTurn({ turn_id: reservation.turn_id, lane: enrichedLane, cwd, critiques, actor });
|
|
103
|
+
return { reservation, result };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Lazy read-path bridge: consume the exact LANE-RESULT owned by one AgentRun.
|
|
107
|
+
* Invalid JSON, foreign assignment ids, stale generation workspaces, or missing
|
|
108
|
+
* turn fences are reported without mutating slot/assignment/run/claim state.
|
|
109
|
+
*/
|
|
110
|
+
export function reconcileLaneResultForRun(run, cwd, actor = 'reconciler') {
|
|
111
|
+
if (!run.worktree_path)
|
|
112
|
+
return { found: false, valid: false, reason: 'run has no worktree_path' };
|
|
113
|
+
const resultPath = path.join(run.worktree_path, 'LANE-RESULT.json');
|
|
114
|
+
if (!fs.existsSync(resultPath))
|
|
115
|
+
return { found: false, valid: false, reason: 'LANE-RESULT.json not found' };
|
|
116
|
+
let lane;
|
|
117
|
+
try {
|
|
118
|
+
lane = LaneResultSchema.parse(JSON.parse(fs.readFileSync(resultPath, 'utf8')));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return { found: true, valid: false, reason: `invalid LANE-RESULT.json: ${err instanceof Error ? err.message : String(err)}` };
|
|
122
|
+
}
|
|
123
|
+
if (lane.assignment_id !== run.assignment_id) {
|
|
124
|
+
return { found: true, valid: false, lane, reason: `foreign assignment_id ${lane.assignment_id}; expected ${run.assignment_id}` };
|
|
125
|
+
}
|
|
126
|
+
const reservation = findReservationByRunId(run.id, cwd);
|
|
127
|
+
if (!reservation)
|
|
128
|
+
return { found: true, valid: false, lane, reason: `run ${run.id} has no owning turn reservation` };
|
|
129
|
+
const chain = resolveTurnGenerationChain(cwd, reservation.turn_id);
|
|
130
|
+
const generation = chain?.latest_generation;
|
|
131
|
+
if (generation && generation.run_id !== run.id) {
|
|
132
|
+
return { found: true, valid: false, lane, reservation, reason: `run ${run.id} is not the active generation ${generation.run_id}` };
|
|
133
|
+
}
|
|
134
|
+
if (generation) {
|
|
135
|
+
const actualWorkspace = normalizedWorkspace(run.worktree_path);
|
|
136
|
+
const expectedWorkspace = normalizedWorkspace(generation.workspace_path);
|
|
137
|
+
if (!actualWorkspace || !expectedWorkspace || actualWorkspace !== expectedWorkspace) {
|
|
138
|
+
return { found: true, valid: false, lane, reservation, reason: 'run worktree does not match the active attempt generation workspace' };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const evidence = turnOwnedLaneEvidence(lane, cwd, reservation);
|
|
142
|
+
if (!evidence) {
|
|
143
|
+
return { found: true, valid: false, lane, reservation, reason: 'LANE-RESULT lacks a run-keyed launch fence and no completion signal supplies one' };
|
|
144
|
+
}
|
|
145
|
+
const reconciled = reconcileTurnOwnedLane(lane, cwd, evidence, actor);
|
|
146
|
+
if (!reconciled)
|
|
147
|
+
return { found: true, valid: false, lane, reservation, reason: 'turn-owned reconciliation unavailable' };
|
|
148
|
+
return {
|
|
149
|
+
found: true,
|
|
150
|
+
valid: reconciled.result.reconciled,
|
|
151
|
+
lane,
|
|
152
|
+
reservation,
|
|
153
|
+
result: reconciled.result,
|
|
154
|
+
reason: reconciled.result.reason,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
20
157
|
// The terminal loop statuses (LOOP_STATUSES = open|paused|completed|blocked|cancelled).
|
|
21
158
|
// 'blocked' is LOAD-BEARING (pln#630 PR3b): the iteration cap closes a fix cycle to
|
|
22
159
|
// `blocked`, and a blocked loop must be treated as terminal both by the idempotent
|
|
@@ -49,12 +186,22 @@ function settleRunCompleted(runId, actor, cwd) {
|
|
|
49
186
|
function settleAssignment(assignmentId, actor, cwd) {
|
|
50
187
|
try {
|
|
51
188
|
const asg = loadAssignment(assignmentId, cwd);
|
|
52
|
-
if (asg
|
|
189
|
+
if (!asg || asg.status === 'completed' || asg.status === 'cancelled')
|
|
190
|
+
return;
|
|
191
|
+
if (asg.status === 'expired') {
|
|
53
192
|
try {
|
|
54
193
|
transitionAssignment(assignmentId, 'completed', { actor }, cwd);
|
|
55
194
|
}
|
|
56
|
-
catch { /*
|
|
195
|
+
catch { /* concurrent terminal transition */ }
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (asg.status === 'created') {
|
|
199
|
+
try {
|
|
200
|
+
transitionAssignment(assignmentId, 'offered', { actor }, cwd);
|
|
201
|
+
}
|
|
202
|
+
catch { /* concurrent transition */ }
|
|
57
203
|
}
|
|
204
|
+
convergeAssignmentToTerminal(assignmentId, 'completed', 'reconcileTurn: turn-keyed worker result accepted', cwd);
|
|
58
205
|
}
|
|
59
206
|
catch { /* best-effort */ }
|
|
60
207
|
}
|
|
@@ -100,6 +247,7 @@ export function reconcileTurn(input) {
|
|
|
100
247
|
? resolvedGeneration.latest_generation
|
|
101
248
|
: undefined;
|
|
102
249
|
const activeRunId = activeGeneration?.run_id ?? reservation.child_ids.run_id;
|
|
250
|
+
const activeAssignmentId = activeGeneration?.assignment_id ?? reservation.child_ids.assignment_id;
|
|
103
251
|
const activeContractRef = activeGeneration
|
|
104
252
|
? executionContractForGeneration(reservation, activeGeneration).ref
|
|
105
253
|
: reservation.execution_contract_ref;
|
|
@@ -133,8 +281,8 @@ export function reconcileTurn(input) {
|
|
|
133
281
|
};
|
|
134
282
|
}
|
|
135
283
|
if (activeContractRef) {
|
|
136
|
-
const completion =
|
|
137
|
-
const bootstrapAck =
|
|
284
|
+
const completion = readAttemptCompletionSignals(cwd ?? process.cwd(), activeAssignmentId, activeRunId).completed;
|
|
285
|
+
const bootstrapAck = readAttemptContractAck(cwd ?? process.cwd(), activeAssignmentId, activeRunId);
|
|
138
286
|
const accepted = {
|
|
139
287
|
contract_hash: lane.execution_contract_hash ?? completion?.contract_hash ?? '',
|
|
140
288
|
capability_snapshot_hash: lane.capability_snapshot_hash ?? completion?.capability_snapshot_hash ?? '',
|
|
@@ -177,7 +325,7 @@ export function reconcileTurn(input) {
|
|
|
177
325
|
event_type: 'run_blocked',
|
|
178
326
|
text: `reconcileTurn: post-crossing execution-contract acceptance anomaly for ${turn_id}; convergence WITHHELD and respawn=false`,
|
|
179
327
|
tags: ['loops', 'reconcile', 'contract-anomaly', 'turn-attempt'],
|
|
180
|
-
assignment_id:
|
|
328
|
+
assignment_id: activeAssignmentId,
|
|
181
329
|
run_id: activeRunId,
|
|
182
330
|
status_reason: 'execution_contract_acceptance_mismatch',
|
|
183
331
|
}, cwd);
|
|
@@ -198,13 +346,13 @@ export function reconcileTurn(input) {
|
|
|
198
346
|
// LANE-RESULT then exited non-zero (turn-keyed failed sentinel) is a conflict,
|
|
199
347
|
// not a clean close. ──
|
|
200
348
|
try {
|
|
201
|
-
const bodies =
|
|
349
|
+
const bodies = readAttemptCompletionSignals(cwd ?? process.cwd(), activeAssignmentId, activeRunId);
|
|
202
350
|
const matchedCompleted = bodies.completed?.status === 'completed' && evidenceMatchesAttempt(reservation, {
|
|
203
|
-
assignment_id:
|
|
351
|
+
assignment_id: activeAssignmentId,
|
|
204
352
|
...bodies.completed,
|
|
205
353
|
});
|
|
206
354
|
const matchedFailed = bodies.failed?.status === 'failed' && evidenceMatchesAttempt(reservation, {
|
|
207
|
-
assignment_id:
|
|
355
|
+
assignment_id: activeAssignmentId,
|
|
208
356
|
...bodies.failed,
|
|
209
357
|
});
|
|
210
358
|
if (matchedFailed && (matchedCompleted || lane.status === 'completed')) {
|
|
@@ -214,7 +362,7 @@ export function reconcileTurn(input) {
|
|
|
214
362
|
event_type: 'run_blocked',
|
|
215
363
|
text: `reconcileTurn: turn ${turn_id} has a completed(lane/sentinel)+failed(sentinel) contradiction — auto-stop WITHHELD (§13 R4), escalating to human`,
|
|
216
364
|
tags: ['loops', 'reconcile', 'conflict', 'turn-attempt'],
|
|
217
|
-
assignment_id:
|
|
365
|
+
assignment_id: activeAssignmentId,
|
|
218
366
|
run_id: activeRunId,
|
|
219
367
|
status_reason: 'turn_evidence_contradiction',
|
|
220
368
|
}, cwd);
|
|
@@ -307,6 +455,13 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
307
455
|
}
|
|
308
456
|
const acceptedGeneration = generationState?.latest_generation;
|
|
309
457
|
const acceptedRunId = acceptedGeneration?.run_id ?? reservation.child_ids.run_id;
|
|
458
|
+
const acceptedAssignmentId = acceptedGeneration?.assignment_id ?? reservation.child_ids.assignment_id;
|
|
459
|
+
const acceptedExecutor = acceptedGeneration?.executor ?? {
|
|
460
|
+
agent: reservation.agent,
|
|
461
|
+
agent_id: reservation.agent_id,
|
|
462
|
+
claim_id: reservation.claim_id,
|
|
463
|
+
capability_snapshot: reservation.capability_snapshot,
|
|
464
|
+
};
|
|
310
465
|
const acceptedNonce = acceptedGeneration?.launch_nonce ?? reservation.launch?.token;
|
|
311
466
|
const acceptedEpoch = acceptedGeneration?.attempt_epoch ?? reservation.epoch;
|
|
312
467
|
const acceptedContractHash = acceptedGeneration?.contract_hash
|
|
@@ -330,7 +485,7 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
330
485
|
// transition but BEFORE its own deferred release would otherwise leak the retained claim
|
|
331
486
|
// until the staleness sweep — releaseCoordinatorClaim is idempotent (no-op if not active).
|
|
332
487
|
if (LOOP_TERMINAL.has(loop.status)) {
|
|
333
|
-
releaseCoordinatorClaim(loadAssignment(
|
|
488
|
+
releaseCoordinatorClaim(loadAssignment(acceptedAssignmentId, cwd)?.claim_id ?? acceptedExecutor.claim_id, cwd);
|
|
334
489
|
return { reconciled: true, reason: `loop already ${loop.status} (idempotent no-op)`, artifacts_added: 0, loop_status: loop.status };
|
|
335
490
|
}
|
|
336
491
|
// ── Idempotency (review Findings 1+2): a TERMINAL slot durably means this turn's
|
|
@@ -366,13 +521,13 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
366
521
|
evidence_context: {
|
|
367
522
|
channel: 'reconcile_turn',
|
|
368
523
|
producer_kind: 'slot',
|
|
369
|
-
producer_id:
|
|
370
|
-
agent_id:
|
|
524
|
+
producer_id: acceptedExecutor.agent,
|
|
525
|
+
agent_id: acceptedExecutor.agent_id,
|
|
371
526
|
slot_id: slot.slot_id,
|
|
372
527
|
slot_role: slot.role,
|
|
373
528
|
turn_id,
|
|
374
|
-
assignment_id:
|
|
375
|
-
claim_id:
|
|
529
|
+
assignment_id: acceptedAssignmentId,
|
|
530
|
+
claim_id: acceptedExecutor.claim_id,
|
|
376
531
|
run_id: acceptedRunId,
|
|
377
532
|
nonce: acceptedNonce,
|
|
378
533
|
attempt_epoch: acceptedEpoch,
|
|
@@ -398,13 +553,13 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
398
553
|
evidence_context: {
|
|
399
554
|
channel: 'reconcile_turn',
|
|
400
555
|
producer_kind: 'slot',
|
|
401
|
-
producer_id:
|
|
402
|
-
agent_id:
|
|
556
|
+
producer_id: acceptedExecutor.agent,
|
|
557
|
+
agent_id: acceptedExecutor.agent_id,
|
|
403
558
|
slot_id: slot.slot_id,
|
|
404
559
|
slot_role: slot.role,
|
|
405
560
|
turn_id,
|
|
406
|
-
assignment_id:
|
|
407
|
-
claim_id:
|
|
561
|
+
assignment_id: acceptedAssignmentId,
|
|
562
|
+
claim_id: acceptedExecutor.claim_id,
|
|
408
563
|
run_id: acceptedRunId,
|
|
409
564
|
nonce: acceptedNonce,
|
|
410
565
|
attempt_epoch: acceptedEpoch,
|
|
@@ -435,7 +590,7 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
435
590
|
event_type: 'loop_artifact_harvested',
|
|
436
591
|
text: `reconcileTurn: harvested turn ${turn_id} on slot ${slot.slot_id} → loop ${loop.id} (${slot_outcome}, ${artifacts_added} artifact(s), phase ${reservation.phase})`,
|
|
437
592
|
tags: ['loops', 'reconcile', 'harvest', 'turn-attempt'],
|
|
438
|
-
assignment_id:
|
|
593
|
+
assignment_id: acceptedAssignmentId,
|
|
439
594
|
run_id: acceptedRunId,
|
|
440
595
|
attempt_epoch: acceptedGeneration?.attempt_epoch,
|
|
441
596
|
workspace_digest: acceptedGeneration?.workspace_digest,
|
|
@@ -452,7 +607,7 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
452
607
|
// (a crash that recorded the turn but not the settle still converges on replay; the
|
|
453
608
|
// fix-cycle re-dispatch mints a fresh run/assignment, so completing the old is correct). ──
|
|
454
609
|
settleRunCompleted(acceptedRunId, actor, cwd);
|
|
455
|
-
settleAssignment(
|
|
610
|
+
settleAssignment(acceptedAssignmentId, actor, cwd);
|
|
456
611
|
// ── Advance / stop decision. On a `done` outcome we either drive a deterministic stop
|
|
457
612
|
// (reviewer_green / gate → close), continue a symmetric fix cycle (bump the round + retain
|
|
458
613
|
// the claim + emit next_turn), or leave the loop open (asymmetric / no successor). ──
|
|
@@ -530,7 +685,7 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
530
685
|
event_type: 'run_blocked',
|
|
531
686
|
text: `reconcileTurn: fix-cycle round ${cur.iteration_count} of loop ${loop.id} was bumped but never dispatched (turn ${turn_id} strand) — re-emitting next_turn to self-heal`,
|
|
532
687
|
tags: ['loops', 'reconcile', 'turn-owned', 'strand-recovery'],
|
|
533
|
-
assignment_id:
|
|
688
|
+
assignment_id: acceptedAssignmentId,
|
|
534
689
|
run_id: acceptedRunId,
|
|
535
690
|
status_reason: 'fix_cycle_strand_reemit',
|
|
536
691
|
}, cwd);
|
|
@@ -560,7 +715,7 @@ function convergeLockedTurn(reservation, input, actor, cwd) {
|
|
|
560
715
|
// Release the coordinator claim now — UNLESS a fix-cycle round retained it. Target the
|
|
561
716
|
// authoritative claim the assignment is bound to, not reservation.claim_id (dec#149 #3).
|
|
562
717
|
if (!retainClaim) {
|
|
563
|
-
const authoritativeClaimId = loadAssignment(
|
|
718
|
+
const authoritativeClaimId = loadAssignment(acceptedAssignmentId, cwd)?.claim_id ?? acceptedExecutor.claim_id;
|
|
564
719
|
releaseCoordinatorClaim(authoritativeClaimId, cwd);
|
|
565
720
|
}
|
|
566
721
|
const loop_status = getLoop(loop.id, cwd)?.status ?? loop.status;
|
|
@@ -661,7 +816,7 @@ function convergeFailedLockedTurn(reservation, run, transportReason, actor, cwd)
|
|
|
661
816
|
event_type: 'run_failed',
|
|
662
817
|
text: `Released claim ${claimId} as the BUSINESS convergence of failed turn ${reservation.turn_id}: ${why}`,
|
|
663
818
|
tags: ['loops', 'reconcile', 'claim-release', 'effects-boundary'],
|
|
664
|
-
assignment_id:
|
|
819
|
+
assignment_id: run.assignment_id,
|
|
665
820
|
run_id: run.id,
|
|
666
821
|
claim_id: claimId,
|
|
667
822
|
status_reason: 'turn_failure_business_release',
|
|
@@ -676,7 +831,7 @@ function convergeFailedLockedTurn(reservation, run, transportReason, actor, cwd)
|
|
|
676
831
|
// A genuinely deleted loop leaves the claim to the staleness sweep.
|
|
677
832
|
return { converged: false, claim_released: false, reason: `loop ${reservation.loop_id} not found — claim retained for the staleness sweep` };
|
|
678
833
|
}
|
|
679
|
-
const authoritativeClaimId = loadAssignment(
|
|
834
|
+
const authoritativeClaimId = loadAssignment(run.assignment_id, cwd)?.claim_id ?? run.claim_id ?? reservation.claim_id;
|
|
680
835
|
// Terminal loop: the business story is already over — releasing is pure
|
|
681
836
|
// idempotent cleanup, mirroring reconcileTurn's terminal early-return.
|
|
682
837
|
if (LOOP_TERMINAL.has(loop.status)) {
|
|
@@ -711,6 +866,49 @@ function convergeFailedLockedTurn(reservation, run, transportReason, actor, cwd)
|
|
|
711
866
|
return { converged: false, claim_released: false, reason: `complete_turn failed: ${err instanceof Error ? err.message : String(err)} — claim retained, next pass retries` };
|
|
712
867
|
}
|
|
713
868
|
}
|
|
869
|
+
// Assignment is a business projection of the same failed turn. Leaving it
|
|
870
|
+
// offered/started while the run, slot, and claim are terminal recreates the
|
|
871
|
+
// orphaned-state ambiguity this convergence path exists to remove.
|
|
872
|
+
let assignment = loadAssignment(run.assignment_id, cwd);
|
|
873
|
+
if (assignment && !['completed', 'failed', 'blocked', 'timed_out', 'cancelled', 'expired', 'rerouted'].includes(assignment.status)) {
|
|
874
|
+
const assignmentId = assignment.id;
|
|
875
|
+
try {
|
|
876
|
+
// A running AgentRun is stronger execution evidence than a lagging
|
|
877
|
+
// Assignment projection. Legacy/file workers can leave that projection
|
|
878
|
+
// at created or accepted; walk the legal FSM edges before recording the
|
|
879
|
+
// terminal failure instead of declining forever on created -> failed or
|
|
880
|
+
// accepted -> failed. Each edge is durable, so a crash resumes from the
|
|
881
|
+
// last projection on the next lazy pass.
|
|
882
|
+
if (assignment.status === 'created' || assignment.status === 'retrying') {
|
|
883
|
+
transitionAssignment(assignmentId, 'offered', {
|
|
884
|
+
actor,
|
|
885
|
+
syncAgentRun: false,
|
|
886
|
+
status_reason: `turn ${reservation.turn_id} failure projection catch-up`,
|
|
887
|
+
}, cwd);
|
|
888
|
+
assignment = loadAssignment(assignmentId, cwd);
|
|
889
|
+
}
|
|
890
|
+
if (assignment?.status === 'accepted') {
|
|
891
|
+
transitionAssignment(assignmentId, 'started', {
|
|
892
|
+
actor,
|
|
893
|
+
syncAgentRun: false,
|
|
894
|
+
status_reason: `turn ${reservation.turn_id} failure projection catch-up`,
|
|
895
|
+
}, cwd);
|
|
896
|
+
assignment = loadAssignment(assignmentId, cwd);
|
|
897
|
+
}
|
|
898
|
+
transitionAssignment(assignmentId, 'failed', {
|
|
899
|
+
actor,
|
|
900
|
+
syncAgentRun: false,
|
|
901
|
+
status_reason: `turn ${reservation.turn_id} failed: ${transportReason}`,
|
|
902
|
+
}, cwd);
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
return {
|
|
906
|
+
converged: false,
|
|
907
|
+
claim_released: false,
|
|
908
|
+
reason: `assignment failure projection failed: ${err instanceof Error ? err.message : String(err)} — claim retained, next pass retries`,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
}
|
|
714
912
|
const released = releaseAudited(authoritativeClaimId, transportReason);
|
|
715
913
|
return {
|
|
716
914
|
converged: true,
|
|
@@ -6,7 +6,7 @@ import { LOOP_ARTIFACT_BODY_MAX_BYTES } from './types.js';
|
|
|
6
6
|
* would make complete_turn throw and crash reconcileTurn. Byte-aware, drops any
|
|
7
7
|
* partial trailing multibyte char.
|
|
8
8
|
*/
|
|
9
|
-
function
|
|
9
|
+
export function capLoopArtifactBody(s) {
|
|
10
10
|
if (Buffer.byteLength(s, 'utf8') <= LOOP_ARTIFACT_BODY_MAX_BYTES)
|
|
11
11
|
return s;
|
|
12
12
|
const marker = '…[truncated]';
|
|
@@ -35,7 +35,7 @@ export const reviewReducer = (input, attempt) => {
|
|
|
35
35
|
return { artifacts: [], slot_outcome: 'failed', failure_reason: 'review author_response produced no body' };
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
38
|
-
artifacts: [{ phase, type: 'author_response', body:
|
|
38
|
+
artifacts: [{ phase, type: 'author_response', body: capLoopArtifactBody(response), produced_by: attempt.agent }],
|
|
39
39
|
slot_outcome: 'done',
|
|
40
40
|
};
|
|
41
41
|
}
|
|
@@ -46,7 +46,7 @@ export const reviewReducer = (input, attempt) => {
|
|
|
46
46
|
return { artifacts: [], slot_outcome: 'failed', failure_reason: 'review lane completed without a review_verdict — cannot converge the loop' };
|
|
47
47
|
}
|
|
48
48
|
const summary = (lane.review_summary ?? '').trim();
|
|
49
|
-
const body =
|
|
49
|
+
const body = capLoopArtifactBody(lane.review_verdict === 'approve'
|
|
50
50
|
? `accepted${summary ? `: ${summary}` : ''}`
|
|
51
51
|
: `changes-requested${summary ? `: ${summary}` : ''}`);
|
|
52
52
|
return {
|
|
@@ -96,7 +96,7 @@ export const ideationReducer = (input, attempt) => {
|
|
|
96
96
|
artifacts: [{
|
|
97
97
|
phase,
|
|
98
98
|
type: artifactType,
|
|
99
|
-
body:
|
|
99
|
+
body: capLoopArtifactBody(body),
|
|
100
100
|
produced_by: attempt.agent,
|
|
101
101
|
addresses_critique: uniqueAddresses,
|
|
102
102
|
implementation_verify: lane.implementation_verify,
|
|
@@ -104,7 +104,7 @@ export const ideationReducer = (input, attempt) => {
|
|
|
104
104
|
slot_outcome: 'done',
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
return { artifacts: [{ phase, type: artifactType, body:
|
|
107
|
+
return { artifacts: [{ phase, type: artifactType, body: capLoopArtifactBody(body), produced_by: attempt.agent }], slot_outcome: 'done' };
|
|
108
108
|
}
|
|
109
109
|
if (lane.artifact_type !== 'critique') {
|
|
110
110
|
return { artifacts: [], slot_outcome: 'failed', failure_reason: "ideation critique requires artifact_type 'critique'" };
|
|
@@ -114,7 +114,7 @@ export const ideationReducer = (input, attempt) => {
|
|
|
114
114
|
}
|
|
115
115
|
return {
|
|
116
116
|
artifacts: critiques.map((c) => ({
|
|
117
|
-
phase, type: 'critique', body:
|
|
117
|
+
phase, type: 'critique', body: capLoopArtifactBody(c.body), produced_by: attempt.agent,
|
|
118
118
|
...(c.addresses_critique ? { addresses_critique: c.addresses_critique } : {}),
|
|
119
119
|
})),
|
|
120
120
|
slot_outcome: 'done',
|
|
@@ -131,7 +131,7 @@ export const defaultReducer = (input, attempt) => {
|
|
|
131
131
|
return { artifacts: [], slot_outcome: 'failed', failure_reason: `lane status is ${lane.status}, not completed` };
|
|
132
132
|
}
|
|
133
133
|
return {
|
|
134
|
-
artifacts: [{ phase, type: 'lane_result', body:
|
|
134
|
+
artifacts: [{ phase, type: 'lane_result', body: capLoopArtifactBody(lane.summary), produced_by: attempt.agent }],
|
|
135
135
|
slot_outcome: 'done',
|
|
136
136
|
};
|
|
137
137
|
};
|
|
@@ -159,7 +159,7 @@ function typedPhaseReducer(kind, artifactByPhase) {
|
|
|
159
159
|
return { artifacts: [], slot_outcome: 'failed', failure_reason: `${kind} phase '${phase}' produced no artifact body` };
|
|
160
160
|
}
|
|
161
161
|
return {
|
|
162
|
-
artifacts: [{ phase, type: expectedType, body:
|
|
162
|
+
artifacts: [{ phase, type: expectedType, body: capLoopArtifactBody(body), produced_by: attempt.agent }],
|
|
163
163
|
slot_outcome: 'done',
|
|
164
164
|
};
|
|
165
165
|
};
|
|
@@ -13,22 +13,23 @@ import { getLoop } from './store.js';
|
|
|
13
13
|
import { phasePolicy } from './kind-policies.js';
|
|
14
14
|
/** Idempotently materialize every child projection required before launch. */
|
|
15
15
|
export function ensureTurnExecutionProjections(reservation, input, cwd) {
|
|
16
|
-
|
|
16
|
+
const claimId = input.claim_id ?? reservation.claim_id;
|
|
17
|
+
if (claimId === '')
|
|
17
18
|
throw new Error('attempt reservation has no claim');
|
|
18
19
|
ensureAssignmentProjection({
|
|
19
20
|
id: input.assignment_id,
|
|
20
21
|
short_label: input.assignment_id,
|
|
21
|
-
claim_id:
|
|
22
|
+
claim_id: claimId,
|
|
22
23
|
agent: input.agent,
|
|
23
24
|
agent_id: input.agent_id,
|
|
24
25
|
dispatcher_agent: input.dispatcher_agent,
|
|
25
26
|
dispatcher_session_id: input.dispatcher_session_id,
|
|
26
27
|
scope: input.scope,
|
|
27
28
|
description: input.description,
|
|
28
|
-
// Assignment is
|
|
29
|
-
//
|
|
30
|
-
execution_contract_ref: reservation.execution_contract_ref,
|
|
31
|
-
capability_snapshot: reservation.capability_snapshot,
|
|
29
|
+
// Assignment is generation-scoped. Generation zero preserves the historic
|
|
30
|
+
// deterministic id; every takeover projects a fresh assignment and contract.
|
|
31
|
+
execution_contract_ref: input.execution_contract_ref ?? reservation.execution_contract_ref,
|
|
32
|
+
capability_snapshot: input.capability_snapshot ?? reservation.capability_snapshot,
|
|
32
33
|
tags: input.assignment_tags ?? ['coordinate', 'loop', 'turn-owned'],
|
|
33
34
|
}, cwd);
|
|
34
35
|
input.on_projection?.('assignment');
|
|
@@ -36,7 +37,7 @@ export function ensureTurnExecutionProjections(reservation, input, cwd) {
|
|
|
36
37
|
id: input.run_id,
|
|
37
38
|
short_label: input.run_id,
|
|
38
39
|
assignment_id: input.assignment_id,
|
|
39
|
-
claim_id:
|
|
40
|
+
claim_id: claimId,
|
|
40
41
|
attempt_index: input.attempt_index ?? 1,
|
|
41
42
|
agent: input.agent,
|
|
42
43
|
agent_id: input.agent_id,
|
|
@@ -50,7 +51,7 @@ export function ensureTurnExecutionProjections(reservation, input, cwd) {
|
|
|
50
51
|
tags: input.run_tags ?? ['turn-owned', 'loop'],
|
|
51
52
|
}, cwd);
|
|
52
53
|
input.on_projection?.('run');
|
|
53
|
-
ensureClaimAssignmentBinding(
|
|
54
|
+
ensureClaimAssignmentBinding(claimId, input.assignment_id, cwd, {
|
|
54
55
|
worktreePath: input.worktree_path,
|
|
55
56
|
});
|
|
56
57
|
input.on_projection?.('claim_binding');
|
|
@@ -61,7 +62,7 @@ export function ensureTurnExecutionProjections(reservation, input, cwd) {
|
|
|
61
62
|
input: input.task,
|
|
62
63
|
turn_id: input.turn_id,
|
|
63
64
|
assignment_id: input.assignment_id,
|
|
64
|
-
claim_id:
|
|
65
|
+
claim_id: claimId,
|
|
65
66
|
}, cwd);
|
|
66
67
|
input.on_projection?.('slot_binding');
|
|
67
68
|
input.on_projection?.('before_crossing');
|
|
@@ -71,7 +72,11 @@ function preconditionDenied(reason) {
|
|
|
71
72
|
}
|
|
72
73
|
function authorityDenied(input, turnId, reason) {
|
|
73
74
|
const reservation = getReservation(turnId, input.cwd);
|
|
74
|
-
const
|
|
75
|
+
const chain = resolveTurnGenerationChain(input.cwd, turnId);
|
|
76
|
+
const authorityClaimId = chain?.status === 'active'
|
|
77
|
+
? chain.latest_generation.executor?.claim_id ?? reservation?.claim_id
|
|
78
|
+
: reservation?.claim_id;
|
|
79
|
+
const ownsAuthority = authorityClaimId === input.claim_id;
|
|
75
80
|
const crossed = reservation?.launch?.status === 'crossed';
|
|
76
81
|
return {
|
|
77
82
|
kind: 'denied',
|
|
@@ -129,13 +134,19 @@ export function prepareTurnExecution(input) {
|
|
|
129
134
|
}, input.cwd);
|
|
130
135
|
const childIds = deriveChildIds(turnId);
|
|
131
136
|
const existingReservation = getReservation(turnId, input.cwd);
|
|
137
|
+
const v2 = resolveTurnGenerationChain(input.cwd, turnId);
|
|
138
|
+
const activeExecutor = v2?.status === 'active' && v2.latest_generation.attempt_epoch > 0
|
|
139
|
+
? v2.latest_generation.executor
|
|
140
|
+
: undefined;
|
|
132
141
|
if (slot.agent !== undefined && slot.agent !== input.agent) {
|
|
133
142
|
return preconditionDenied(`slot ${input.slot_id} belongs to agent '${slot.agent}', not '${input.agent}'`);
|
|
134
143
|
}
|
|
135
144
|
if (slot.agent_id !== undefined && slot.agent_id !== input.agent_id) {
|
|
136
145
|
return preconditionDenied(`slot ${input.slot_id} belongs to agent_id '${slot.agent_id}', not '${input.agent_id ?? 'none'}'`);
|
|
137
146
|
}
|
|
138
|
-
if (slot.claim_id !== undefined
|
|
147
|
+
if (slot.claim_id !== undefined
|
|
148
|
+
&& slot.claim_id !== input.claim_id
|
|
149
|
+
&& ['assigned', 'working', 'waiting_input'].includes(slot.status)) {
|
|
139
150
|
return preconditionDenied(`slot ${input.slot_id} is bound to claim ${slot.claim_id}, not ${input.claim_id}`);
|
|
140
151
|
}
|
|
141
152
|
if (slot.current_turn_id !== undefined
|
|
@@ -202,13 +213,15 @@ export function prepareTurnExecution(input) {
|
|
|
202
213
|
catch (error) {
|
|
203
214
|
return preconditionDenied(error instanceof Error ? error.message : String(error));
|
|
204
215
|
}
|
|
205
|
-
const frozenHarnessBinding =
|
|
216
|
+
const frozenHarnessBinding = activeExecutor?.capability_snapshot.resolved.harness
|
|
217
|
+
?? existingReservation?.capability_snapshot?.resolved.harness;
|
|
206
218
|
if (frozenHarnessBinding && JSON.stringify(frozenHarnessBinding) !== JSON.stringify(requestedHarnessBinding)) {
|
|
207
219
|
return preconditionDenied(`harness binding differs from immutable capability snapshot: frozen `
|
|
208
220
|
+ `${frozenHarnessBinding.adapter_id}@${frozenHarnessBinding.adapter_version}, requested `
|
|
209
221
|
+ `${requestedHarnessBinding.adapter_id}@${requestedHarnessBinding.adapter_version}`);
|
|
210
222
|
}
|
|
211
|
-
const capabilitySnapshot =
|
|
223
|
+
const capabilitySnapshot = activeExecutor?.capability_snapshot
|
|
224
|
+
?? existingReservation?.capability_snapshot
|
|
212
225
|
?? resolveCapabilitySnapshot(input.agent, capabilityRequirement, input.agent_id, requestedHarnessBinding);
|
|
213
226
|
if (!capabilitySnapshot.accepted) {
|
|
214
227
|
const reasons = capabilitySnapshot.reasons.map((reason) => reason.code).join(', ');
|
|
@@ -256,7 +269,6 @@ export function prepareTurnExecution(input) {
|
|
|
256
269
|
const contractRef = contract
|
|
257
270
|
? (existingReservation?.execution_contract_ref ?? executionContractRef(contract, capabilitySnapshot))
|
|
258
271
|
: undefined;
|
|
259
|
-
const v2 = resolveTurnGenerationChain(input.cwd, turnId);
|
|
260
272
|
const activeRollout = resolveActiveAttemptRollout(input.cwd);
|
|
261
273
|
const localHome = readLocalAuthorityHome(input.cwd);
|
|
262
274
|
if (activeRollout && (!localHome || !contractRef)) {
|
|
@@ -339,10 +351,16 @@ export function prepareTurnExecution(input) {
|
|
|
339
351
|
try {
|
|
340
352
|
const generation = v2.latest_generation;
|
|
341
353
|
const generationContract = executionContractForGeneration(reservation, generation);
|
|
354
|
+
const executor = generation.executor ?? {
|
|
355
|
+
agent: reservation.agent,
|
|
356
|
+
agent_id: reservation.agent_id,
|
|
357
|
+
claim_id: reservation.claim_id,
|
|
358
|
+
capability_snapshot: reservation.capability_snapshot,
|
|
359
|
+
};
|
|
342
360
|
let accepted;
|
|
343
361
|
try {
|
|
344
362
|
accepted = input.accepted_execution_contract
|
|
345
|
-
?? attestHarnessContractAcceptance(generationContract.ref,
|
|
363
|
+
?? attestHarnessContractAcceptance(generationContract.ref, executor.capability_snapshot, requestedHarnessBinding);
|
|
346
364
|
}
|
|
347
365
|
catch (error) {
|
|
348
366
|
return preconditionDenied(`worker contract acceptance unavailable before crossing: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -357,8 +375,9 @@ export function prepareTurnExecution(input) {
|
|
|
357
375
|
turn_id: turnId,
|
|
358
376
|
assignment_id: generation.assignment_id,
|
|
359
377
|
run_id: generation.run_id,
|
|
360
|
-
|
|
361
|
-
|
|
378
|
+
claim_id: executor.claim_id,
|
|
379
|
+
agent: executor.agent,
|
|
380
|
+
agent_id: executor.agent_id,
|
|
362
381
|
dispatcher_agent: input.dispatcher_agent,
|
|
363
382
|
dispatcher_agent_id: input.dispatcher_agent_id,
|
|
364
383
|
dispatcher_session_id: input.dispatcher_session_id,
|
|
@@ -370,7 +389,7 @@ export function prepareTurnExecution(input) {
|
|
|
370
389
|
run_tags: [...(input.run_tags ?? ['turn-owned', 'loop']), `attempt-generation:${generation.attempt_epoch}`],
|
|
371
390
|
attempt_index: generation.attempt_epoch + 1,
|
|
372
391
|
execution_contract_ref: generationContract.ref,
|
|
373
|
-
capability_snapshot:
|
|
392
|
+
capability_snapshot: executor.capability_snapshot,
|
|
374
393
|
on_projection: input.on_projection,
|
|
375
394
|
}, input.cwd);
|
|
376
395
|
const crossing = crossActiveAttemptGenerationV2(turnId, generation.attempt_epoch, localHome, input.dispatcher_agent_id ?? input.dispatcher_agent, input.dispatcher_agent_id ?? input.dispatcher_agent, input.cwd);
|
|
@@ -387,7 +406,7 @@ export function prepareTurnExecution(input) {
|
|
|
387
406
|
workspace_path: generation.workspace_path,
|
|
388
407
|
contract_status: 'contracted',
|
|
389
408
|
execution_contract_ref: generationContract.ref,
|
|
390
|
-
capability_snapshot:
|
|
409
|
+
capability_snapshot: executor.capability_snapshot,
|
|
391
410
|
};
|
|
392
411
|
}
|
|
393
412
|
catch (error) {
|
package/dist/core/loops/types.js
CHANGED
|
@@ -734,6 +734,9 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
|
|
|
734
734
|
slot_id: z.string().min(1),
|
|
735
735
|
turn_id: z.string().min(1),
|
|
736
736
|
assignment_id: z.string().min(1),
|
|
737
|
+
claim_id: z.string().min(1).optional(),
|
|
738
|
+
agent: z.string().min(1).optional(),
|
|
739
|
+
agent_id: z.string().min(1).optional(),
|
|
737
740
|
from_epoch: z.number().int().nonnegative(),
|
|
738
741
|
to_epoch: z.number().int().positive(),
|
|
739
742
|
from_run_id: z.string().min(1),
|
package/dist/core/loops/verbs.js
CHANGED
|
@@ -586,7 +586,7 @@ function authorizeCompleteTurnAttempt(input, slot, cwd) {
|
|
|
586
586
|
slot_role: slot.role,
|
|
587
587
|
turn_id: generation.turn_id,
|
|
588
588
|
assignment_id: generation.assignment_id,
|
|
589
|
-
claim_id:
|
|
589
|
+
claim_id: generation.executor?.claim_id ?? slot.claim_id ?? reservation.claim_id,
|
|
590
590
|
run_id: generation.run_id,
|
|
591
591
|
nonce: generation.launch_nonce,
|
|
592
592
|
attempt_epoch: generation.attempt_epoch,
|