@pennixrv/trellis 0.7.1-beta.14 → 0.7.1-beta.16
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/commands/teamwork/artifact.d.ts +5 -1
- package/dist/commands/teamwork/artifact.d.ts.map +1 -1
- package/dist/commands/teamwork/artifact.js +50 -9
- package/dist/commands/teamwork/artifact.js.map +1 -1
- package/dist/commands/teamwork/coordinator.d.ts +1 -1
- package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
- package/dist/commands/teamwork/coordinator.js +492 -265
- package/dist/commands/teamwork/coordinator.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.15.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.16.json +9 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +19 -11
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +37 -11
- package/dist/templates/trellis/agents/teammate.md +7 -5
- package/package.json +2 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
5
|
import { isTaskPhaseId, parseTaskWorkflowState, } from "@pennixrv/trellis-core/task";
|
|
4
6
|
import { assertCoordinationTransition, readTeamworkEvents, readCoordinationState, recordCoordinationEvent, } from "@pennixrv/trellis-core/teamwork";
|
|
5
|
-
import { cleanupSettledTeamworkArtifacts, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
|
|
7
|
+
import { TeammateReportReadError, cleanupSettledTeamworkArtifacts, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
|
|
6
8
|
import { assertTeammateAdapterContract, assertTeammateHostCapability, } from "./teammate-adapters.js";
|
|
7
9
|
import { effectiveTeammateCapacity, resolveTeammateRuntimeConfig, } from "./teammate-config.js";
|
|
8
10
|
export function assertTeamworkPlanCurrent(plan, taskRoot) {
|
|
@@ -35,6 +37,165 @@ export function assertTeamworkPlanCurrent(plan, taskRoot) {
|
|
|
35
37
|
throw new Error("Teamwork plan does not match the current active task phase and revisions");
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
function terminalReceiptFromProjection(plan, projection) {
|
|
41
|
+
const mismatch = (reason) => {
|
|
42
|
+
throw new Error(`batch '${plan.batchId}' ${reason}`);
|
|
43
|
+
};
|
|
44
|
+
if (projection.issues.length > 0)
|
|
45
|
+
mismatch("has coordination projection issues");
|
|
46
|
+
const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
|
|
47
|
+
if (!batch)
|
|
48
|
+
return mismatch("does not match the frozen plan");
|
|
49
|
+
if (batch.planHash !== plan.planHash)
|
|
50
|
+
return mismatch("does not match the frozen plan");
|
|
51
|
+
if (batch.state !== "ALL_ACCEPTED" && batch.state !== "SETTLED_WITH_FAILURE")
|
|
52
|
+
mismatch("is not terminal");
|
|
53
|
+
const effectiveCapacity = batch.effectiveCapacity;
|
|
54
|
+
if (typeof effectiveCapacity !== "number" ||
|
|
55
|
+
!Number.isSafeInteger(effectiveCapacity) ||
|
|
56
|
+
effectiveCapacity < 1)
|
|
57
|
+
return mismatch("has no valid effective capacity");
|
|
58
|
+
if (projection.leases.some((lease) => lease.batchId === plan.batchId))
|
|
59
|
+
mismatch("still owns capacity leases");
|
|
60
|
+
const projectedAssignments = projection.assignments.filter((assignment) => assignment.batchId === plan.batchId);
|
|
61
|
+
if (projectedAssignments.length !== plan.assignments.length ||
|
|
62
|
+
new Set(projectedAssignments.map((assignment) => assignment.assignmentId))
|
|
63
|
+
.size !== plan.assignments.length)
|
|
64
|
+
mismatch("does not contain the exact frozen assignment set");
|
|
65
|
+
const assignmentById = new Map(projectedAssignments.map((assignment) => [
|
|
66
|
+
assignment.assignmentId,
|
|
67
|
+
assignment,
|
|
68
|
+
]));
|
|
69
|
+
const planAssignmentIds = new Set(plan.assignments.map((assignment) => assignment.assignmentId));
|
|
70
|
+
const attempts = projection.attempts.filter((attempt) => attempt.batchId === plan.batchId);
|
|
71
|
+
const teammates = projection.teammates.filter((teammate) => teammate.batchId === plan.batchId);
|
|
72
|
+
if (attempts.length !== teammates.length)
|
|
73
|
+
mismatch("has unmatched provider attempts and teammates");
|
|
74
|
+
for (const attempt of attempts) {
|
|
75
|
+
if (!attempt.assignmentId ||
|
|
76
|
+
!planAssignmentIds.has(attempt.assignmentId) ||
|
|
77
|
+
!attempt.teammateId ||
|
|
78
|
+
attempt.terminal !== true ||
|
|
79
|
+
!["REPORT_SUBMITTED", "FAILED", "FINAL_FAILED"].includes(attempt.state))
|
|
80
|
+
mismatch("has an unresolved provider attempt");
|
|
81
|
+
const teammate = teammates.find((entry) => entry.attemptId === attempt.attemptId &&
|
|
82
|
+
entry.teammateId === attempt.teammateId);
|
|
83
|
+
if (teammate?.terminal !== true ||
|
|
84
|
+
(teammate?.state !== "REPORT_SUBMITTED" && teammate?.state !== "FAILED"))
|
|
85
|
+
mismatch("has an unresolved teammate terminal fact");
|
|
86
|
+
}
|
|
87
|
+
const assignments = plan.assignments.map((assignment) => {
|
|
88
|
+
const projected = assignmentById.get(assignment.assignmentId);
|
|
89
|
+
if (!projected ||
|
|
90
|
+
(projected.state !== "ACCEPTED" && projected.state !== "FINAL_FAILED"))
|
|
91
|
+
return mismatch(`assignment '${assignment.assignmentId}' is not settled`);
|
|
92
|
+
if (projected.state === "ACCEPTED") {
|
|
93
|
+
const acceptedAttempt = attempts.find((attempt) => attempt.attemptId === projected.attemptId &&
|
|
94
|
+
attempt.assignmentId === assignment.assignmentId);
|
|
95
|
+
if (acceptedAttempt?.state !== "REPORT_SUBMITTED")
|
|
96
|
+
return mismatch(`assignment '${assignment.assignmentId}' has no accepted attempt`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
assignmentId: assignment.assignmentId,
|
|
100
|
+
status: projected.state,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const status = assignments.every((item) => item.status === "ACCEPTED")
|
|
104
|
+
? "ALL_ACCEPTED"
|
|
105
|
+
: "SETTLED_WITH_FAILURE";
|
|
106
|
+
if (batch.state !== status)
|
|
107
|
+
mismatch("terminal state disagrees with its assignment dispositions");
|
|
108
|
+
const retryCounts = Object.fromEntries(plan.assignments.flatMap((assignment) => {
|
|
109
|
+
const count = assignmentById.get(assignment.assignmentId)?.retryCount;
|
|
110
|
+
if (count === undefined)
|
|
111
|
+
return mismatch(`assignment '${assignment.assignmentId}' is missing`);
|
|
112
|
+
const started = attempts.some((attempt) => attempt.assignmentId === assignment.assignmentId);
|
|
113
|
+
return started || count > 0 ? [[assignment.assignmentId, count]] : [];
|
|
114
|
+
}));
|
|
115
|
+
const evaluationGroups = [
|
|
116
|
+
...new Set(plan.assignments
|
|
117
|
+
.map((assignment) => assignment.evaluationGroup?.id)
|
|
118
|
+
.filter((id) => id !== undefined)),
|
|
119
|
+
].map((id) => ({
|
|
120
|
+
id,
|
|
121
|
+
status: plan.assignments
|
|
122
|
+
.filter((assignment) => assignment.evaluationGroup?.id === id)
|
|
123
|
+
.every((assignment) => assignmentById.get(assignment.assignmentId)?.state === "ACCEPTED")
|
|
124
|
+
? "COMPLETE"
|
|
125
|
+
: "INCOMPLETE",
|
|
126
|
+
}));
|
|
127
|
+
return {
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
batchId: plan.batchId,
|
|
130
|
+
status,
|
|
131
|
+
assignments,
|
|
132
|
+
capacity: effectiveCapacity,
|
|
133
|
+
retryCounts,
|
|
134
|
+
evaluationGroups,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function reconciliationReceipt(plan, projection, capacity) {
|
|
138
|
+
return {
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
batchId: plan.batchId,
|
|
141
|
+
status: "RECONCILIATION_REQUIRED",
|
|
142
|
+
assignments: plan.assignments.map((item) => {
|
|
143
|
+
const current = projection.assignments.find((entry) => entry.batchId === plan.batchId &&
|
|
144
|
+
entry.assignmentId === item.assignmentId);
|
|
145
|
+
return {
|
|
146
|
+
assignmentId: item.assignmentId,
|
|
147
|
+
status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
|
|
148
|
+
? current.state
|
|
149
|
+
: "UNKNOWN",
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
capacity: projection.batches.find((entry) => entry.batchId === plan.batchId)
|
|
153
|
+
?.effectiveCapacity ?? capacity,
|
|
154
|
+
retryCounts: Object.fromEntries(projection.assignments
|
|
155
|
+
.filter((entry) => entry.batchId === plan.batchId)
|
|
156
|
+
.map((entry) => [entry.assignmentId, entry.retryCount])),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function recordRecoveryReconciliation(input) {
|
|
160
|
+
const { plan, cwd, by, existingState, projection, capacity, reason } = input;
|
|
161
|
+
if (existingState !== "RECONCILIATION_REQUIRED" &&
|
|
162
|
+
existingState !== "ALL_ACCEPTED" &&
|
|
163
|
+
existingState !== "SETTLED_WITH_FAILURE") {
|
|
164
|
+
await appendTransition({
|
|
165
|
+
sessionId: plan.sessionId,
|
|
166
|
+
cwd,
|
|
167
|
+
by,
|
|
168
|
+
batchId: plan.batchId,
|
|
169
|
+
entity: "batch",
|
|
170
|
+
id: plan.batchId,
|
|
171
|
+
state: "RECONCILIATION_REQUIRED",
|
|
172
|
+
states: new Map([[plan.batchId, existingState]]),
|
|
173
|
+
planHash: plan.planHash,
|
|
174
|
+
payload: { recoveredAfterRestart: true },
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const receipt = reconciliationReceipt(plan, projection, capacity);
|
|
178
|
+
await appendAction({
|
|
179
|
+
sessionId: plan.sessionId,
|
|
180
|
+
cwd,
|
|
181
|
+
by,
|
|
182
|
+
batchId: plan.batchId,
|
|
183
|
+
entity: "batch",
|
|
184
|
+
action: "reconciliation_required",
|
|
185
|
+
reason,
|
|
186
|
+
});
|
|
187
|
+
await appendAction({
|
|
188
|
+
sessionId: plan.sessionId,
|
|
189
|
+
cwd,
|
|
190
|
+
by,
|
|
191
|
+
batchId: plan.batchId,
|
|
192
|
+
entity: "batch",
|
|
193
|
+
action: "receipt_recorded",
|
|
194
|
+
id: `${plan.batchId}:RECONCILIATION_REQUIRED`,
|
|
195
|
+
payload: { ...receipt, reason },
|
|
196
|
+
});
|
|
197
|
+
return receipt;
|
|
198
|
+
}
|
|
38
199
|
async function recoverExistingBatch(input) {
|
|
39
200
|
const projection = await readCoordinationState({
|
|
40
201
|
sessionId: input.plan.sessionId,
|
|
@@ -50,64 +211,54 @@ async function recoverExistingBatch(input) {
|
|
|
50
211
|
sessionId: input.plan.sessionId,
|
|
51
212
|
cwd: input.cwd,
|
|
52
213
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
214
|
+
if (existing.state === "ALL_ACCEPTED" ||
|
|
215
|
+
existing.state === "SETTLED_WITH_FAILURE") {
|
|
216
|
+
let receipt;
|
|
217
|
+
try {
|
|
218
|
+
receipt = terminalReceiptFromProjection(input.plan, projection);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return recordRecoveryReconciliation({
|
|
222
|
+
...input,
|
|
223
|
+
existingState: existing.state,
|
|
224
|
+
projection,
|
|
225
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const receipts = events.filter((event) => event.kind === "coordination" &&
|
|
229
|
+
event.batchId === input.plan.batchId &&
|
|
230
|
+
event.action === "receipt_recorded");
|
|
231
|
+
if (receipts.length > 1 ||
|
|
232
|
+
(receipts.length === 1 &&
|
|
233
|
+
!isDeepStrictEqual(receipts[0]?.payload, receipt))) {
|
|
234
|
+
return recordRecoveryReconciliation({
|
|
235
|
+
...input,
|
|
236
|
+
existingState: existing.state,
|
|
237
|
+
projection,
|
|
238
|
+
reason: "saved terminal receipt disagrees with durable coordination facts",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (receipts.length === 0) {
|
|
242
|
+
await appendAction({
|
|
243
|
+
sessionId: input.plan.sessionId,
|
|
244
|
+
cwd: input.cwd,
|
|
245
|
+
by: input.by,
|
|
246
|
+
batchId: input.plan.batchId,
|
|
247
|
+
entity: "batch",
|
|
248
|
+
action: "receipt_recorded",
|
|
249
|
+
id: `${input.plan.batchId}:${receipt.status}`,
|
|
250
|
+
payload: { ...receipt },
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
await assertTeamworkReceiptDurable(input.plan, receipt, input.cwd);
|
|
254
|
+
return receipt;
|
|
77
255
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
|
|
84
|
-
? current.state
|
|
85
|
-
: "UNKNOWN",
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
const retryCounts = Object.fromEntries(projection.assignments
|
|
89
|
-
.filter((entry) => entry.batchId === input.plan.batchId)
|
|
90
|
-
.map((entry) => [entry.assignmentId, entry.retryCount]));
|
|
91
|
-
const receipt = {
|
|
92
|
-
schemaVersion: 1,
|
|
93
|
-
batchId: input.plan.batchId,
|
|
94
|
-
status: "RECONCILIATION_REQUIRED",
|
|
95
|
-
assignments,
|
|
96
|
-
capacity: existing.effectiveCapacity ?? input.capacity,
|
|
97
|
-
retryCounts,
|
|
98
|
-
};
|
|
99
|
-
await recordCoordinationEvent({
|
|
100
|
-
sessionId: input.plan.sessionId,
|
|
101
|
-
cwd: input.cwd,
|
|
102
|
-
by: input.by,
|
|
103
|
-
batchId: input.plan.batchId,
|
|
104
|
-
entity: "batch",
|
|
105
|
-
action: "receipt_recorded",
|
|
106
|
-
receiptId: `${input.plan.batchId}:RECONCILIATION_REQUIRED`,
|
|
107
|
-
payload: { ...receipt, reason: "recovered batch requires reconciliation" },
|
|
108
|
-
idempotencyKey: `${input.plan.batchId}:RECONCILIATION_REQUIRED:receipt`,
|
|
256
|
+
return recordRecoveryReconciliation({
|
|
257
|
+
...input,
|
|
258
|
+
existingState: existing.state,
|
|
259
|
+
projection,
|
|
260
|
+
reason: "recovered nonterminal batch requires reconciliation",
|
|
109
261
|
});
|
|
110
|
-
return receipt;
|
|
111
262
|
}
|
|
112
263
|
function transitionState(states, key, next, entity) {
|
|
113
264
|
assertCoordinationTransition(entity, states.get(key), next);
|
|
@@ -117,6 +268,7 @@ async function appendTransition(input) {
|
|
|
117
268
|
transitionState(input.states, input.id, input.state, input.entity);
|
|
118
269
|
await recordCoordinationEvent({
|
|
119
270
|
sessionId: input.sessionId,
|
|
271
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
120
272
|
by: input.by,
|
|
121
273
|
batchId: input.batchId,
|
|
122
274
|
entity: input.entity,
|
|
@@ -139,6 +291,7 @@ async function appendTransition(input) {
|
|
|
139
291
|
async function appendAction(input) {
|
|
140
292
|
await recordCoordinationEvent({
|
|
141
293
|
sessionId: input.sessionId,
|
|
294
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
142
295
|
by: input.by,
|
|
143
296
|
batchId: input.batchId,
|
|
144
297
|
entity: input.entity,
|
|
@@ -203,19 +356,8 @@ export async function runTeamworkCoordinator(input) {
|
|
|
203
356
|
capacity,
|
|
204
357
|
});
|
|
205
358
|
let guardHeld = true;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
await input.guard.release({
|
|
212
|
-
batchId: plan.batchId,
|
|
213
|
-
planHash: plan.planHash,
|
|
214
|
-
capacity,
|
|
215
|
-
});
|
|
216
|
-
guardHeld = false;
|
|
217
|
-
throw error;
|
|
218
|
-
}
|
|
359
|
+
// Failed recovery cannot prove that an older provider process exited.
|
|
360
|
+
const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
|
|
219
361
|
if (recovered) {
|
|
220
362
|
if (recovered.status !== "RECONCILIATION_REQUIRED") {
|
|
221
363
|
await input.guard.release({
|
|
@@ -324,8 +466,9 @@ export async function runTeamworkCoordinator(input) {
|
|
|
324
466
|
const active = new Map();
|
|
325
467
|
const waits = new Map();
|
|
326
468
|
const final = new Map();
|
|
327
|
-
const
|
|
469
|
+
const retainedReports = new Map();
|
|
328
470
|
const retryCounts = {};
|
|
471
|
+
let admissionClosed = false;
|
|
329
472
|
async function reconcile(reason) {
|
|
330
473
|
await appendAction({
|
|
331
474
|
sessionId: plan.sessionId,
|
|
@@ -368,9 +511,39 @@ export async function runTeamworkCoordinator(input) {
|
|
|
368
511
|
});
|
|
369
512
|
return receipt;
|
|
370
513
|
}
|
|
514
|
+
async function failPending(reason) {
|
|
515
|
+
admissionClosed = true;
|
|
516
|
+
while (pending.length > 0) {
|
|
517
|
+
const assignment = pending.shift();
|
|
518
|
+
if (!assignment)
|
|
519
|
+
continue;
|
|
520
|
+
await appendTransition({
|
|
521
|
+
sessionId: plan.sessionId,
|
|
522
|
+
by,
|
|
523
|
+
batchId: plan.batchId,
|
|
524
|
+
entity: "assignment",
|
|
525
|
+
id: assignment.assignmentId,
|
|
526
|
+
state: "FINAL_FAILED",
|
|
527
|
+
states: assignmentStates,
|
|
528
|
+
assignmentId: assignment.assignmentId,
|
|
529
|
+
retryCount: retryCounts[assignment.assignmentId] ?? 0,
|
|
530
|
+
payload: { reason, dispatched: false },
|
|
531
|
+
});
|
|
532
|
+
final.set(assignment.assignmentId, "FINAL_FAILED");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
371
535
|
async function dispatch(slot, assignment, retryCount) {
|
|
372
|
-
const
|
|
373
|
-
|
|
536
|
+
const boundedId = (kind) => {
|
|
537
|
+
const candidate = `${assignment.assignmentId}.${kind}.${retryCount}`;
|
|
538
|
+
return candidate.length <= 64
|
|
539
|
+
? candidate
|
|
540
|
+
: crypto
|
|
541
|
+
.createHash("sha256")
|
|
542
|
+
.update(`${assignment.assignmentId}:${kind}:${retryCount}`)
|
|
543
|
+
.digest("hex");
|
|
544
|
+
};
|
|
545
|
+
const attemptId = boundedId("attempt");
|
|
546
|
+
const teammateId = boundedId("teammate");
|
|
374
547
|
const frozen = frozenLaunches.get(assignment.assignmentId);
|
|
375
548
|
if (!frozen)
|
|
376
549
|
throw new Error(`missing frozen launch for '${assignment.assignmentId}'`);
|
|
@@ -506,6 +679,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
506
679
|
teammateId,
|
|
507
680
|
slot,
|
|
508
681
|
retryCount,
|
|
682
|
+
transitionKey: `attempt:${attemptId}`,
|
|
509
683
|
...(context.providerSessionId
|
|
510
684
|
? { providerSessionId: context.providerSessionId }
|
|
511
685
|
: {}),
|
|
@@ -558,7 +732,63 @@ export async function runTeamworkCoordinator(input) {
|
|
|
558
732
|
});
|
|
559
733
|
guardHeld = false;
|
|
560
734
|
}
|
|
735
|
+
async function resumeFromWait(current) {
|
|
736
|
+
if (assignmentStates.get(current.assignment.assignmentId) !==
|
|
737
|
+
"WAITING_FOR_MAIN_COMMAND")
|
|
738
|
+
return;
|
|
739
|
+
const transitionKey = `continuation:${current.continuations}`;
|
|
740
|
+
await appendTransition({
|
|
741
|
+
sessionId: plan.sessionId,
|
|
742
|
+
by,
|
|
743
|
+
batchId: plan.batchId,
|
|
744
|
+
entity: "attempt",
|
|
745
|
+
id: current.context.attemptId,
|
|
746
|
+
state: "RUNNING",
|
|
747
|
+
states: attemptStates,
|
|
748
|
+
assignmentId: current.assignment.assignmentId,
|
|
749
|
+
attemptId: current.context.attemptId,
|
|
750
|
+
teammateId: current.context.teammateId,
|
|
751
|
+
retryCount: current.retryCount,
|
|
752
|
+
...(current.providerSessionId
|
|
753
|
+
? { providerSessionId: current.providerSessionId }
|
|
754
|
+
: {}),
|
|
755
|
+
transitionKey,
|
|
756
|
+
});
|
|
757
|
+
await appendTransition({
|
|
758
|
+
sessionId: plan.sessionId,
|
|
759
|
+
by,
|
|
760
|
+
batchId: plan.batchId,
|
|
761
|
+
entity: "teammate",
|
|
762
|
+
id: current.context.teammateId,
|
|
763
|
+
state: "RUNNING",
|
|
764
|
+
states: teammateStates,
|
|
765
|
+
attemptId: current.context.attemptId,
|
|
766
|
+
teammateId: current.context.teammateId,
|
|
767
|
+
...(current.providerSessionId
|
|
768
|
+
? { providerSessionId: current.providerSessionId }
|
|
769
|
+
: {}),
|
|
770
|
+
transitionKey,
|
|
771
|
+
});
|
|
772
|
+
await appendTransition({
|
|
773
|
+
sessionId: plan.sessionId,
|
|
774
|
+
by,
|
|
775
|
+
batchId: plan.batchId,
|
|
776
|
+
entity: "assignment",
|
|
777
|
+
id: current.assignment.assignmentId,
|
|
778
|
+
state: "EXECUTING",
|
|
779
|
+
states: assignmentStates,
|
|
780
|
+
assignmentId: current.assignment.assignmentId,
|
|
781
|
+
attemptId: current.context.attemptId,
|
|
782
|
+
teammateId: current.context.teammateId,
|
|
783
|
+
retryCount: current.retryCount,
|
|
784
|
+
...(current.providerSessionId
|
|
785
|
+
? { providerSessionId: current.providerSessionId }
|
|
786
|
+
: {}),
|
|
787
|
+
transitionKey,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
561
790
|
async function cancelAndDrain(reason, terminal) {
|
|
791
|
+
await failPending(reason);
|
|
562
792
|
if (batchStates.get(plan.batchId) === "RUNNING") {
|
|
563
793
|
await appendTransition({
|
|
564
794
|
sessionId: plan.sessionId,
|
|
@@ -571,52 +801,68 @@ export async function runTeamworkCoordinator(input) {
|
|
|
571
801
|
payload: { reason },
|
|
572
802
|
});
|
|
573
803
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
804
|
+
const cancel = input.runtime.cancel;
|
|
805
|
+
const outcomes = await Promise.all([...active.values()].map(async (current) => {
|
|
806
|
+
if (terminal?.current === current)
|
|
807
|
+
return { current, result: terminal.result };
|
|
808
|
+
if (!cancel)
|
|
809
|
+
return {
|
|
810
|
+
current,
|
|
811
|
+
error: new Error(`host cannot cancel active attempt: ${reason}`),
|
|
812
|
+
};
|
|
813
|
+
try {
|
|
814
|
+
return { current, result: await cancel(current.context) };
|
|
581
815
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
816
|
+
catch (error) {
|
|
817
|
+
return { current, error };
|
|
818
|
+
}
|
|
819
|
+
}));
|
|
820
|
+
let cancellationFailure;
|
|
821
|
+
for (const outcome of outcomes) {
|
|
822
|
+
const { current } = outcome;
|
|
823
|
+
if ("error" in outcome) {
|
|
824
|
+
cancellationFailure ??=
|
|
825
|
+
outcome.error instanceof Error
|
|
826
|
+
? outcome.error.message
|
|
827
|
+
: String(outcome.error);
|
|
828
|
+
continue;
|
|
591
829
|
}
|
|
830
|
+
const { result } = outcome;
|
|
592
831
|
if (result.kind === "reconciliation" ||
|
|
593
832
|
result.kind === "timeout" ||
|
|
594
833
|
result.terminal !== true) {
|
|
595
|
-
|
|
834
|
+
cancellationFailure ??= `attempt cancellation is not terminal: ${reason}`;
|
|
835
|
+
continue;
|
|
596
836
|
}
|
|
597
837
|
if (result.providerSessionId &&
|
|
598
838
|
current.providerSessionId &&
|
|
599
839
|
result.providerSessionId !== current.providerSessionId) {
|
|
600
|
-
|
|
840
|
+
cancellationFailure ??=
|
|
841
|
+
"cancellation returned a different host session";
|
|
842
|
+
continue;
|
|
601
843
|
}
|
|
602
844
|
if (result.providerSessionId) {
|
|
603
845
|
current.providerSessionId = result.providerSessionId;
|
|
604
846
|
current.context.providerSessionId = result.providerSessionId;
|
|
605
847
|
}
|
|
606
848
|
try {
|
|
607
|
-
|
|
849
|
+
await resumeFromWait(current);
|
|
850
|
+
if (result.kind === "report") {
|
|
608
851
|
await complete(current, false);
|
|
852
|
+
}
|
|
609
853
|
else
|
|
610
854
|
await fail(current, result.reason ?? "cancelled by coordinator", false);
|
|
611
855
|
}
|
|
612
856
|
catch (error) {
|
|
613
|
-
|
|
857
|
+
cancellationFailure ??=
|
|
858
|
+
error instanceof Error ? error.message : String(error);
|
|
859
|
+
continue;
|
|
614
860
|
}
|
|
615
861
|
active.delete(current.slot);
|
|
616
862
|
waits.delete(current.slot);
|
|
617
863
|
}
|
|
618
|
-
if (
|
|
619
|
-
return reconcile(
|
|
864
|
+
if (cancellationFailure)
|
|
865
|
+
return reconcile(cancellationFailure);
|
|
620
866
|
return settle();
|
|
621
867
|
}
|
|
622
868
|
async function fail(activeWork, reason, allowRetry = true) {
|
|
@@ -653,7 +899,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
653
899
|
: {}),
|
|
654
900
|
terminal: true,
|
|
655
901
|
});
|
|
656
|
-
if (allowRetry && retryCount < assignment.maxRetries) {
|
|
902
|
+
if (allowRetry && !admissionClosed && retryCount < assignment.maxRetries) {
|
|
657
903
|
await release(activeWork);
|
|
658
904
|
retryCounts[assignment.assignmentId] = retryCount + 1;
|
|
659
905
|
pending.unshift(assignment);
|
|
@@ -739,7 +985,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
739
985
|
});
|
|
740
986
|
await release(activeWork);
|
|
741
987
|
final.set(assignment.assignmentId, "ACCEPTED");
|
|
742
|
-
|
|
988
|
+
retainedReports.set(assignment.assignmentId, context.attemptId);
|
|
743
989
|
}
|
|
744
990
|
async function complete(activeWork, allowRetry = true) {
|
|
745
991
|
const { assignment, context, retryCount } = activeWork;
|
|
@@ -778,9 +1024,20 @@ export async function runTeamworkCoordinator(input) {
|
|
|
778
1024
|
});
|
|
779
1025
|
}
|
|
780
1026
|
catch (error) {
|
|
1027
|
+
if (error instanceof TeammateReportReadError && error.kind !== "missing")
|
|
1028
|
+
throw error;
|
|
781
1029
|
await fail(activeWork, error instanceof Error ? error.message : String(error), allowRetry);
|
|
782
1030
|
return;
|
|
783
1031
|
}
|
|
1032
|
+
if (report.status === "blocked") {
|
|
1033
|
+
retainedReports.set(assignment.assignmentId, context.attemptId);
|
|
1034
|
+
await fail(activeWork, `teammate reported blocked: ${report.evidence[0]?.summary ?? "blocker evidence missing"}`, false);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (report.status !== "complete") {
|
|
1038
|
+
await fail(activeWork, `teammate reported ${report.status}`, allowRetry);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
784
1041
|
if (input.runtime.validateReport &&
|
|
785
1042
|
!(await input.runtime.validateReport(context, report))) {
|
|
786
1043
|
await fail(activeWork, "report rejected by acceptance contract", allowRetry);
|
|
@@ -866,7 +1123,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
866
1123
|
const cleanupFailures = cleanupSettledTeamworkArtifacts({
|
|
867
1124
|
taskRoot: input.taskRoot,
|
|
868
1125
|
assignments: plan.assignments,
|
|
869
|
-
|
|
1126
|
+
retainedReports,
|
|
870
1127
|
});
|
|
871
1128
|
if (cleanupFailures.length > 0) {
|
|
872
1129
|
return reconcile(`Teamwork batch cleanup failed: ${cleanupFailures.join("; ")}`);
|
|
@@ -914,12 +1171,25 @@ export async function runTeamworkCoordinator(input) {
|
|
|
914
1171
|
id: `${plan.batchId}:${status}`,
|
|
915
1172
|
payload: { ...receipt },
|
|
916
1173
|
});
|
|
917
|
-
await assertTeamworkReceiptDurable(plan
|
|
1174
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
918
1175
|
await releaseGuard();
|
|
919
1176
|
return receipt;
|
|
920
1177
|
}
|
|
921
1178
|
async function fill() {
|
|
1179
|
+
if (admissionClosed)
|
|
1180
|
+
return;
|
|
922
1181
|
while (active.size < capacity && pending.length > 0) {
|
|
1182
|
+
if (Date.parse(plan.deadline) <= Date.now()) {
|
|
1183
|
+
await failPending("batch deadline elapsed before dispatch");
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
assertTeamworkPlanCurrent(plan, input.taskRoot);
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
await failPending(`task phase changed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
923
1193
|
const assignment = pending.shift();
|
|
924
1194
|
if (!assignment)
|
|
925
1195
|
return;
|
|
@@ -987,162 +1257,116 @@ export async function runTeamworkCoordinator(input) {
|
|
|
987
1257
|
result: next.result,
|
|
988
1258
|
});
|
|
989
1259
|
}
|
|
1260
|
+
try {
|
|
1261
|
+
await resumeFromWait(current);
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1265
|
+
}
|
|
990
1266
|
let releaseSlot = true;
|
|
991
1267
|
if (next.result.kind === "timeout") {
|
|
992
1268
|
current.continuations += 1;
|
|
993
|
-
if (
|
|
994
|
-
return
|
|
1269
|
+
if (Date.now() >= Date.parse(plan.deadline))
|
|
1270
|
+
return cancelAndDrain("batch deadline elapsed");
|
|
1271
|
+
const transitionKey = `continuation:${current.continuations}`;
|
|
1272
|
+
await appendTransition({
|
|
1273
|
+
sessionId: plan.sessionId,
|
|
1274
|
+
by,
|
|
1275
|
+
batchId: plan.batchId,
|
|
1276
|
+
entity: "attempt",
|
|
1277
|
+
id: current.context.attemptId,
|
|
1278
|
+
state: "TIMEOUT_YIELDED",
|
|
1279
|
+
states: attemptStates,
|
|
1280
|
+
assignmentId: current.assignment.assignmentId,
|
|
1281
|
+
attemptId: current.context.attemptId,
|
|
1282
|
+
teammateId: current.context.teammateId,
|
|
1283
|
+
retryCount: current.retryCount,
|
|
1284
|
+
...(current.providerSessionId
|
|
1285
|
+
? { providerSessionId: current.providerSessionId }
|
|
1286
|
+
: {}),
|
|
1287
|
+
transitionKey,
|
|
1288
|
+
});
|
|
1289
|
+
await appendTransition({
|
|
1290
|
+
sessionId: plan.sessionId,
|
|
1291
|
+
by,
|
|
1292
|
+
batchId: plan.batchId,
|
|
1293
|
+
entity: "teammate",
|
|
1294
|
+
id: current.context.teammateId,
|
|
1295
|
+
state: "TIMEOUT_YIELDED",
|
|
1296
|
+
states: teammateStates,
|
|
1297
|
+
attemptId: current.context.attemptId,
|
|
1298
|
+
teammateId: current.context.teammateId,
|
|
1299
|
+
...(current.providerSessionId
|
|
1300
|
+
? { providerSessionId: current.providerSessionId }
|
|
1301
|
+
: {}),
|
|
1302
|
+
transitionKey,
|
|
1303
|
+
});
|
|
1304
|
+
await appendTransition({
|
|
1305
|
+
sessionId: plan.sessionId,
|
|
1306
|
+
by,
|
|
1307
|
+
batchId: plan.batchId,
|
|
1308
|
+
entity: "assignment",
|
|
1309
|
+
id: current.assignment.assignmentId,
|
|
1310
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1311
|
+
states: assignmentStates,
|
|
1312
|
+
assignmentId: current.assignment.assignmentId,
|
|
1313
|
+
attemptId: current.context.attemptId,
|
|
1314
|
+
teammateId: current.context.teammateId,
|
|
1315
|
+
retryCount: current.retryCount,
|
|
1316
|
+
...(current.providerSessionId
|
|
1317
|
+
? { providerSessionId: current.providerSessionId }
|
|
1318
|
+
: {}),
|
|
1319
|
+
transitionKey,
|
|
1320
|
+
});
|
|
1321
|
+
await appendTransition({
|
|
1322
|
+
sessionId: plan.sessionId,
|
|
1323
|
+
by,
|
|
1324
|
+
batchId: plan.batchId,
|
|
1325
|
+
entity: "attempt",
|
|
1326
|
+
id: current.context.attemptId,
|
|
1327
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1328
|
+
states: attemptStates,
|
|
1329
|
+
assignmentId: current.assignment.assignmentId,
|
|
1330
|
+
attemptId: current.context.attemptId,
|
|
1331
|
+
teammateId: current.context.teammateId,
|
|
1332
|
+
retryCount: current.retryCount,
|
|
1333
|
+
...(current.providerSessionId
|
|
1334
|
+
? { providerSessionId: current.providerSessionId }
|
|
1335
|
+
: {}),
|
|
1336
|
+
transitionKey,
|
|
1337
|
+
});
|
|
1338
|
+
await appendTransition({
|
|
1339
|
+
sessionId: plan.sessionId,
|
|
1340
|
+
by,
|
|
1341
|
+
batchId: plan.batchId,
|
|
1342
|
+
entity: "teammate",
|
|
1343
|
+
id: current.context.teammateId,
|
|
1344
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1345
|
+
states: teammateStates,
|
|
1346
|
+
attemptId: current.context.attemptId,
|
|
1347
|
+
teammateId: current.context.teammateId,
|
|
1348
|
+
...(current.providerSessionId
|
|
1349
|
+
? { providerSessionId: current.providerSessionId }
|
|
1350
|
+
: {}),
|
|
1351
|
+
transitionKey,
|
|
1352
|
+
});
|
|
1353
|
+
let continued;
|
|
1354
|
+
try {
|
|
1355
|
+
continued = input.runtime.continue(current.context);
|
|
995
1356
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
sessionId: plan.sessionId,
|
|
999
|
-
by,
|
|
1000
|
-
batchId: plan.batchId,
|
|
1001
|
-
entity: "attempt",
|
|
1002
|
-
id: current.context.attemptId,
|
|
1003
|
-
state: "TIMEOUT_YIELDED",
|
|
1004
|
-
states: attemptStates,
|
|
1005
|
-
assignmentId: current.assignment.assignmentId,
|
|
1006
|
-
attemptId: current.context.attemptId,
|
|
1007
|
-
teammateId: current.context.teammateId,
|
|
1008
|
-
retryCount: current.retryCount,
|
|
1009
|
-
...(current.providerSessionId
|
|
1010
|
-
? { providerSessionId: current.providerSessionId }
|
|
1011
|
-
: {}),
|
|
1012
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1013
|
-
});
|
|
1014
|
-
await appendTransition({
|
|
1015
|
-
sessionId: plan.sessionId,
|
|
1016
|
-
by,
|
|
1017
|
-
batchId: plan.batchId,
|
|
1018
|
-
entity: "teammate",
|
|
1019
|
-
id: current.context.teammateId,
|
|
1020
|
-
state: "TIMEOUT_YIELDED",
|
|
1021
|
-
states: teammateStates,
|
|
1022
|
-
attemptId: current.context.attemptId,
|
|
1023
|
-
teammateId: current.context.teammateId,
|
|
1024
|
-
...(current.providerSessionId
|
|
1025
|
-
? { providerSessionId: current.providerSessionId }
|
|
1026
|
-
: {}),
|
|
1027
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1028
|
-
});
|
|
1029
|
-
await appendTransition({
|
|
1030
|
-
sessionId: plan.sessionId,
|
|
1031
|
-
by,
|
|
1032
|
-
batchId: plan.batchId,
|
|
1033
|
-
entity: "assignment",
|
|
1034
|
-
id: current.assignment.assignmentId,
|
|
1035
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1036
|
-
states: assignmentStates,
|
|
1037
|
-
assignmentId: current.assignment.assignmentId,
|
|
1038
|
-
attemptId: current.context.attemptId,
|
|
1039
|
-
teammateId: current.context.teammateId,
|
|
1040
|
-
retryCount: current.retryCount,
|
|
1041
|
-
...(current.providerSessionId
|
|
1042
|
-
? { providerSessionId: current.providerSessionId }
|
|
1043
|
-
: {}),
|
|
1044
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1045
|
-
});
|
|
1046
|
-
await appendTransition({
|
|
1047
|
-
sessionId: plan.sessionId,
|
|
1048
|
-
by,
|
|
1049
|
-
batchId: plan.batchId,
|
|
1050
|
-
entity: "attempt",
|
|
1051
|
-
id: current.context.attemptId,
|
|
1052
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1053
|
-
states: attemptStates,
|
|
1054
|
-
assignmentId: current.assignment.assignmentId,
|
|
1055
|
-
attemptId: current.context.attemptId,
|
|
1056
|
-
teammateId: current.context.teammateId,
|
|
1057
|
-
retryCount: current.retryCount,
|
|
1058
|
-
...(current.providerSessionId
|
|
1059
|
-
? { providerSessionId: current.providerSessionId }
|
|
1060
|
-
: {}),
|
|
1061
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1062
|
-
});
|
|
1063
|
-
await appendTransition({
|
|
1064
|
-
sessionId: plan.sessionId,
|
|
1065
|
-
by,
|
|
1066
|
-
batchId: plan.batchId,
|
|
1067
|
-
entity: "teammate",
|
|
1068
|
-
id: current.context.teammateId,
|
|
1069
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1070
|
-
states: teammateStates,
|
|
1071
|
-
attemptId: current.context.attemptId,
|
|
1072
|
-
teammateId: current.context.teammateId,
|
|
1073
|
-
...(current.providerSessionId
|
|
1074
|
-
? { providerSessionId: current.providerSessionId }
|
|
1075
|
-
: {}),
|
|
1076
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1077
|
-
});
|
|
1078
|
-
let continued;
|
|
1079
|
-
try {
|
|
1080
|
-
continued = await input.runtime.continue(current.context);
|
|
1081
|
-
}
|
|
1082
|
-
catch (error) {
|
|
1083
|
-
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1084
|
-
}
|
|
1085
|
-
if (continued.providerSessionId &&
|
|
1086
|
-
current.providerSessionId &&
|
|
1087
|
-
continued.providerSessionId !== current.providerSessionId) {
|
|
1088
|
-
return reconcile("continuation returned a different host session");
|
|
1089
|
-
}
|
|
1090
|
-
if (continued.providerSessionId) {
|
|
1091
|
-
current.providerSessionId = continued.providerSessionId;
|
|
1092
|
-
current.context.providerSessionId = continued.providerSessionId;
|
|
1093
|
-
}
|
|
1094
|
-
await appendTransition({
|
|
1095
|
-
sessionId: plan.sessionId,
|
|
1096
|
-
by,
|
|
1097
|
-
batchId: plan.batchId,
|
|
1098
|
-
entity: "attempt",
|
|
1099
|
-
id: current.context.attemptId,
|
|
1100
|
-
state: "RUNNING",
|
|
1101
|
-
states: attemptStates,
|
|
1102
|
-
assignmentId: current.assignment.assignmentId,
|
|
1103
|
-
attemptId: current.context.attemptId,
|
|
1104
|
-
teammateId: current.context.teammateId,
|
|
1105
|
-
retryCount: current.retryCount,
|
|
1106
|
-
...(current.providerSessionId
|
|
1107
|
-
? { providerSessionId: current.providerSessionId }
|
|
1108
|
-
: {}),
|
|
1109
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1110
|
-
});
|
|
1111
|
-
await appendTransition({
|
|
1112
|
-
sessionId: plan.sessionId,
|
|
1113
|
-
by,
|
|
1114
|
-
batchId: plan.batchId,
|
|
1115
|
-
entity: "teammate",
|
|
1116
|
-
id: current.context.teammateId,
|
|
1117
|
-
state: "RUNNING",
|
|
1118
|
-
states: teammateStates,
|
|
1119
|
-
attemptId: current.context.attemptId,
|
|
1120
|
-
teammateId: current.context.teammateId,
|
|
1121
|
-
...(current.providerSessionId
|
|
1122
|
-
? { providerSessionId: current.providerSessionId }
|
|
1123
|
-
: {}),
|
|
1124
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1125
|
-
});
|
|
1126
|
-
await appendTransition({
|
|
1127
|
-
sessionId: plan.sessionId,
|
|
1128
|
-
by,
|
|
1129
|
-
batchId: plan.batchId,
|
|
1130
|
-
entity: "assignment",
|
|
1131
|
-
id: current.assignment.assignmentId,
|
|
1132
|
-
state: "EXECUTING",
|
|
1133
|
-
states: assignmentStates,
|
|
1134
|
-
assignmentId: current.assignment.assignmentId,
|
|
1135
|
-
attemptId: current.context.attemptId,
|
|
1136
|
-
teammateId: current.context.teammateId,
|
|
1137
|
-
retryCount: current.retryCount,
|
|
1138
|
-
...(current.providerSessionId
|
|
1139
|
-
? { providerSessionId: current.providerSessionId }
|
|
1140
|
-
: {}),
|
|
1141
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1142
|
-
});
|
|
1143
|
-
waits.set(next.slot, Promise.resolve({ slot: next.slot, result: continued }));
|
|
1144
|
-
releaseSlot = false;
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1145
1359
|
}
|
|
1360
|
+
waits.set(next.slot, Promise.resolve(continued)
|
|
1361
|
+
.then((result) => ({ slot: next.slot, result }))
|
|
1362
|
+
.catch((error) => ({
|
|
1363
|
+
slot: next.slot,
|
|
1364
|
+
result: {
|
|
1365
|
+
kind: "reconciliation",
|
|
1366
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1367
|
+
},
|
|
1368
|
+
})));
|
|
1369
|
+
releaseSlot = false;
|
|
1146
1370
|
}
|
|
1147
1371
|
else if (next.result.kind === "report") {
|
|
1148
1372
|
try {
|
|
@@ -1173,22 +1397,25 @@ export async function runTeamworkCoordinator(input) {
|
|
|
1173
1397
|
}
|
|
1174
1398
|
return settle();
|
|
1175
1399
|
}
|
|
1176
|
-
export async function assertTeamworkReceiptDurable(
|
|
1400
|
+
export async function assertTeamworkReceiptDurable(plan, receipt, cwd) {
|
|
1177
1401
|
const [state, events] = await Promise.all([
|
|
1178
|
-
readCoordinationState({
|
|
1179
|
-
|
|
1402
|
+
readCoordinationState({
|
|
1403
|
+
sessionId: plan.sessionId,
|
|
1404
|
+
...(cwd ? { cwd } : {}),
|
|
1405
|
+
}),
|
|
1406
|
+
readTeamworkEvents({
|
|
1407
|
+
sessionId: plan.sessionId,
|
|
1408
|
+
...(cwd ? { cwd } : {}),
|
|
1409
|
+
}),
|
|
1180
1410
|
]);
|
|
1181
|
-
const
|
|
1182
|
-
if (
|
|
1183
|
-
(batch.
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
if (!receiptRecorded) {
|
|
1191
|
-
throw new Error(`batch '${batchId}' has no matching durable receipt event`);
|
|
1192
|
-
}
|
|
1411
|
+
const expected = terminalReceiptFromProjection(plan, state);
|
|
1412
|
+
if (!isDeepStrictEqual(receipt, expected))
|
|
1413
|
+
throw new Error(`batch '${plan.batchId}' receipt disagrees with durable facts`);
|
|
1414
|
+
const receipts = events.filter((event) => event.kind === "coordination" &&
|
|
1415
|
+
event.batchId === plan.batchId &&
|
|
1416
|
+
event.action === "receipt_recorded");
|
|
1417
|
+
if (receipts.length !== 1 ||
|
|
1418
|
+
!isDeepStrictEqual(receipts[0]?.payload, expected))
|
|
1419
|
+
throw new Error(`batch '${plan.batchId}' has no matching durable receipt event`);
|
|
1193
1420
|
}
|
|
1194
1421
|
//# sourceMappingURL=coordinator.js.map
|