@pennixrv/trellis 0.7.1-beta.15 → 0.7.1-beta.17
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 +490 -252
- package/dist/commands/teamwork/coordinator.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.16.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.17.json +9 -0
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +2 -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 -13
- package/dist/templates/trellis/agents/teammate.md +7 -5
- package/dist/templates/trellis/scripts/common/task_workflow.py +102 -2
- package/dist/templates/trellis/scripts/task.py +9 -0
- package/dist/templates/trellis/workflow.md +7 -0
- 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
|
-
};
|
|
256
|
+
return recordRecoveryReconciliation({
|
|
257
|
+
...input,
|
|
258
|
+
existingState: existing.state,
|
|
259
|
+
projection,
|
|
260
|
+
reason: "recovered nonterminal batch requires reconciliation",
|
|
87
261
|
});
|
|
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`,
|
|
109
|
-
});
|
|
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,
|
|
@@ -313,8 +466,9 @@ export async function runTeamworkCoordinator(input) {
|
|
|
313
466
|
const active = new Map();
|
|
314
467
|
const waits = new Map();
|
|
315
468
|
const final = new Map();
|
|
316
|
-
const
|
|
469
|
+
const retainedReports = new Map();
|
|
317
470
|
const retryCounts = {};
|
|
471
|
+
let admissionClosed = false;
|
|
318
472
|
async function reconcile(reason) {
|
|
319
473
|
await appendAction({
|
|
320
474
|
sessionId: plan.sessionId,
|
|
@@ -357,9 +511,39 @@ export async function runTeamworkCoordinator(input) {
|
|
|
357
511
|
});
|
|
358
512
|
return receipt;
|
|
359
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
|
+
}
|
|
360
535
|
async function dispatch(slot, assignment, retryCount) {
|
|
361
|
-
const
|
|
362
|
-
|
|
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");
|
|
363
547
|
const frozen = frozenLaunches.get(assignment.assignmentId);
|
|
364
548
|
if (!frozen)
|
|
365
549
|
throw new Error(`missing frozen launch for '${assignment.assignmentId}'`);
|
|
@@ -495,6 +679,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
495
679
|
teammateId,
|
|
496
680
|
slot,
|
|
497
681
|
retryCount,
|
|
682
|
+
transitionKey: `attempt:${attemptId}`,
|
|
498
683
|
...(context.providerSessionId
|
|
499
684
|
? { providerSessionId: context.providerSessionId }
|
|
500
685
|
: {}),
|
|
@@ -547,7 +732,63 @@ export async function runTeamworkCoordinator(input) {
|
|
|
547
732
|
});
|
|
548
733
|
guardHeld = false;
|
|
549
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
|
+
}
|
|
550
790
|
async function cancelAndDrain(reason, terminal) {
|
|
791
|
+
await failPending(reason);
|
|
551
792
|
if (batchStates.get(plan.batchId) === "RUNNING") {
|
|
552
793
|
await appendTransition({
|
|
553
794
|
sessionId: plan.sessionId,
|
|
@@ -560,52 +801,68 @@ export async function runTeamworkCoordinator(input) {
|
|
|
560
801
|
payload: { reason },
|
|
561
802
|
});
|
|
562
803
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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) };
|
|
570
815
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (!input.runtime.cancel)
|
|
574
|
-
return reconcile(`host cannot cancel active attempt: ${reason}`);
|
|
575
|
-
result = await input.runtime.cancel(current.context);
|
|
576
|
-
}
|
|
577
|
-
catch (error) {
|
|
578
|
-
return reconcile(error instanceof Error ? error.message : String(error));
|
|
579
|
-
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
return { current, error };
|
|
580
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;
|
|
829
|
+
}
|
|
830
|
+
const { result } = outcome;
|
|
581
831
|
if (result.kind === "reconciliation" ||
|
|
582
832
|
result.kind === "timeout" ||
|
|
583
833
|
result.terminal !== true) {
|
|
584
|
-
|
|
834
|
+
cancellationFailure ??= `attempt cancellation is not terminal: ${reason}`;
|
|
835
|
+
continue;
|
|
585
836
|
}
|
|
586
837
|
if (result.providerSessionId &&
|
|
587
838
|
current.providerSessionId &&
|
|
588
839
|
result.providerSessionId !== current.providerSessionId) {
|
|
589
|
-
|
|
840
|
+
cancellationFailure ??=
|
|
841
|
+
"cancellation returned a different host session";
|
|
842
|
+
continue;
|
|
590
843
|
}
|
|
591
844
|
if (result.providerSessionId) {
|
|
592
845
|
current.providerSessionId = result.providerSessionId;
|
|
593
846
|
current.context.providerSessionId = result.providerSessionId;
|
|
594
847
|
}
|
|
595
848
|
try {
|
|
596
|
-
|
|
849
|
+
await resumeFromWait(current);
|
|
850
|
+
if (result.kind === "report") {
|
|
597
851
|
await complete(current, false);
|
|
852
|
+
}
|
|
598
853
|
else
|
|
599
854
|
await fail(current, result.reason ?? "cancelled by coordinator", false);
|
|
600
855
|
}
|
|
601
856
|
catch (error) {
|
|
602
|
-
|
|
857
|
+
cancellationFailure ??=
|
|
858
|
+
error instanceof Error ? error.message : String(error);
|
|
859
|
+
continue;
|
|
603
860
|
}
|
|
604
861
|
active.delete(current.slot);
|
|
605
862
|
waits.delete(current.slot);
|
|
606
863
|
}
|
|
607
|
-
if (
|
|
608
|
-
return reconcile(
|
|
864
|
+
if (cancellationFailure)
|
|
865
|
+
return reconcile(cancellationFailure);
|
|
609
866
|
return settle();
|
|
610
867
|
}
|
|
611
868
|
async function fail(activeWork, reason, allowRetry = true) {
|
|
@@ -642,7 +899,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
642
899
|
: {}),
|
|
643
900
|
terminal: true,
|
|
644
901
|
});
|
|
645
|
-
if (allowRetry && retryCount < assignment.maxRetries) {
|
|
902
|
+
if (allowRetry && !admissionClosed && retryCount < assignment.maxRetries) {
|
|
646
903
|
await release(activeWork);
|
|
647
904
|
retryCounts[assignment.assignmentId] = retryCount + 1;
|
|
648
905
|
pending.unshift(assignment);
|
|
@@ -728,7 +985,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
728
985
|
});
|
|
729
986
|
await release(activeWork);
|
|
730
987
|
final.set(assignment.assignmentId, "ACCEPTED");
|
|
731
|
-
|
|
988
|
+
retainedReports.set(assignment.assignmentId, context.attemptId);
|
|
732
989
|
}
|
|
733
990
|
async function complete(activeWork, allowRetry = true) {
|
|
734
991
|
const { assignment, context, retryCount } = activeWork;
|
|
@@ -767,9 +1024,20 @@ export async function runTeamworkCoordinator(input) {
|
|
|
767
1024
|
});
|
|
768
1025
|
}
|
|
769
1026
|
catch (error) {
|
|
1027
|
+
if (error instanceof TeammateReportReadError && error.kind !== "missing")
|
|
1028
|
+
throw error;
|
|
770
1029
|
await fail(activeWork, error instanceof Error ? error.message : String(error), allowRetry);
|
|
771
1030
|
return;
|
|
772
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
|
+
}
|
|
773
1041
|
if (input.runtime.validateReport &&
|
|
774
1042
|
!(await input.runtime.validateReport(context, report))) {
|
|
775
1043
|
await fail(activeWork, "report rejected by acceptance contract", allowRetry);
|
|
@@ -855,7 +1123,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
855
1123
|
const cleanupFailures = cleanupSettledTeamworkArtifacts({
|
|
856
1124
|
taskRoot: input.taskRoot,
|
|
857
1125
|
assignments: plan.assignments,
|
|
858
|
-
|
|
1126
|
+
retainedReports,
|
|
859
1127
|
});
|
|
860
1128
|
if (cleanupFailures.length > 0) {
|
|
861
1129
|
return reconcile(`Teamwork batch cleanup failed: ${cleanupFailures.join("; ")}`);
|
|
@@ -903,12 +1171,25 @@ export async function runTeamworkCoordinator(input) {
|
|
|
903
1171
|
id: `${plan.batchId}:${status}`,
|
|
904
1172
|
payload: { ...receipt },
|
|
905
1173
|
});
|
|
906
|
-
await assertTeamworkReceiptDurable(plan
|
|
1174
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
907
1175
|
await releaseGuard();
|
|
908
1176
|
return receipt;
|
|
909
1177
|
}
|
|
910
1178
|
async function fill() {
|
|
1179
|
+
if (admissionClosed)
|
|
1180
|
+
return;
|
|
911
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
|
+
}
|
|
912
1193
|
const assignment = pending.shift();
|
|
913
1194
|
if (!assignment)
|
|
914
1195
|
return;
|
|
@@ -976,162 +1257,116 @@ export async function runTeamworkCoordinator(input) {
|
|
|
976
1257
|
result: next.result,
|
|
977
1258
|
});
|
|
978
1259
|
}
|
|
1260
|
+
try {
|
|
1261
|
+
await resumeFromWait(current);
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1265
|
+
}
|
|
979
1266
|
let releaseSlot = true;
|
|
980
1267
|
if (next.result.kind === "timeout") {
|
|
981
1268
|
current.continuations += 1;
|
|
982
|
-
if (
|
|
983
|
-
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);
|
|
984
1356
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
sessionId: plan.sessionId,
|
|
988
|
-
by,
|
|
989
|
-
batchId: plan.batchId,
|
|
990
|
-
entity: "attempt",
|
|
991
|
-
id: current.context.attemptId,
|
|
992
|
-
state: "TIMEOUT_YIELDED",
|
|
993
|
-
states: attemptStates,
|
|
994
|
-
assignmentId: current.assignment.assignmentId,
|
|
995
|
-
attemptId: current.context.attemptId,
|
|
996
|
-
teammateId: current.context.teammateId,
|
|
997
|
-
retryCount: current.retryCount,
|
|
998
|
-
...(current.providerSessionId
|
|
999
|
-
? { providerSessionId: current.providerSessionId }
|
|
1000
|
-
: {}),
|
|
1001
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1002
|
-
});
|
|
1003
|
-
await appendTransition({
|
|
1004
|
-
sessionId: plan.sessionId,
|
|
1005
|
-
by,
|
|
1006
|
-
batchId: plan.batchId,
|
|
1007
|
-
entity: "teammate",
|
|
1008
|
-
id: current.context.teammateId,
|
|
1009
|
-
state: "TIMEOUT_YIELDED",
|
|
1010
|
-
states: teammateStates,
|
|
1011
|
-
attemptId: current.context.attemptId,
|
|
1012
|
-
teammateId: current.context.teammateId,
|
|
1013
|
-
...(current.providerSessionId
|
|
1014
|
-
? { providerSessionId: current.providerSessionId }
|
|
1015
|
-
: {}),
|
|
1016
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1017
|
-
});
|
|
1018
|
-
await appendTransition({
|
|
1019
|
-
sessionId: plan.sessionId,
|
|
1020
|
-
by,
|
|
1021
|
-
batchId: plan.batchId,
|
|
1022
|
-
entity: "assignment",
|
|
1023
|
-
id: current.assignment.assignmentId,
|
|
1024
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1025
|
-
states: assignmentStates,
|
|
1026
|
-
assignmentId: current.assignment.assignmentId,
|
|
1027
|
-
attemptId: current.context.attemptId,
|
|
1028
|
-
teammateId: current.context.teammateId,
|
|
1029
|
-
retryCount: current.retryCount,
|
|
1030
|
-
...(current.providerSessionId
|
|
1031
|
-
? { providerSessionId: current.providerSessionId }
|
|
1032
|
-
: {}),
|
|
1033
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1034
|
-
});
|
|
1035
|
-
await appendTransition({
|
|
1036
|
-
sessionId: plan.sessionId,
|
|
1037
|
-
by,
|
|
1038
|
-
batchId: plan.batchId,
|
|
1039
|
-
entity: "attempt",
|
|
1040
|
-
id: current.context.attemptId,
|
|
1041
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1042
|
-
states: attemptStates,
|
|
1043
|
-
assignmentId: current.assignment.assignmentId,
|
|
1044
|
-
attemptId: current.context.attemptId,
|
|
1045
|
-
teammateId: current.context.teammateId,
|
|
1046
|
-
retryCount: current.retryCount,
|
|
1047
|
-
...(current.providerSessionId
|
|
1048
|
-
? { providerSessionId: current.providerSessionId }
|
|
1049
|
-
: {}),
|
|
1050
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1051
|
-
});
|
|
1052
|
-
await appendTransition({
|
|
1053
|
-
sessionId: plan.sessionId,
|
|
1054
|
-
by,
|
|
1055
|
-
batchId: plan.batchId,
|
|
1056
|
-
entity: "teammate",
|
|
1057
|
-
id: current.context.teammateId,
|
|
1058
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1059
|
-
states: teammateStates,
|
|
1060
|
-
attemptId: current.context.attemptId,
|
|
1061
|
-
teammateId: current.context.teammateId,
|
|
1062
|
-
...(current.providerSessionId
|
|
1063
|
-
? { providerSessionId: current.providerSessionId }
|
|
1064
|
-
: {}),
|
|
1065
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1066
|
-
});
|
|
1067
|
-
let continued;
|
|
1068
|
-
try {
|
|
1069
|
-
continued = await input.runtime.continue(current.context);
|
|
1070
|
-
}
|
|
1071
|
-
catch (error) {
|
|
1072
|
-
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1073
|
-
}
|
|
1074
|
-
if (continued.providerSessionId &&
|
|
1075
|
-
current.providerSessionId &&
|
|
1076
|
-
continued.providerSessionId !== current.providerSessionId) {
|
|
1077
|
-
return reconcile("continuation returned a different host session");
|
|
1078
|
-
}
|
|
1079
|
-
if (continued.providerSessionId) {
|
|
1080
|
-
current.providerSessionId = continued.providerSessionId;
|
|
1081
|
-
current.context.providerSessionId = continued.providerSessionId;
|
|
1082
|
-
}
|
|
1083
|
-
await appendTransition({
|
|
1084
|
-
sessionId: plan.sessionId,
|
|
1085
|
-
by,
|
|
1086
|
-
batchId: plan.batchId,
|
|
1087
|
-
entity: "attempt",
|
|
1088
|
-
id: current.context.attemptId,
|
|
1089
|
-
state: "RUNNING",
|
|
1090
|
-
states: attemptStates,
|
|
1091
|
-
assignmentId: current.assignment.assignmentId,
|
|
1092
|
-
attemptId: current.context.attemptId,
|
|
1093
|
-
teammateId: current.context.teammateId,
|
|
1094
|
-
retryCount: current.retryCount,
|
|
1095
|
-
...(current.providerSessionId
|
|
1096
|
-
? { providerSessionId: current.providerSessionId }
|
|
1097
|
-
: {}),
|
|
1098
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1099
|
-
});
|
|
1100
|
-
await appendTransition({
|
|
1101
|
-
sessionId: plan.sessionId,
|
|
1102
|
-
by,
|
|
1103
|
-
batchId: plan.batchId,
|
|
1104
|
-
entity: "teammate",
|
|
1105
|
-
id: current.context.teammateId,
|
|
1106
|
-
state: "RUNNING",
|
|
1107
|
-
states: teammateStates,
|
|
1108
|
-
attemptId: current.context.attemptId,
|
|
1109
|
-
teammateId: current.context.teammateId,
|
|
1110
|
-
...(current.providerSessionId
|
|
1111
|
-
? { providerSessionId: current.providerSessionId }
|
|
1112
|
-
: {}),
|
|
1113
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1114
|
-
});
|
|
1115
|
-
await appendTransition({
|
|
1116
|
-
sessionId: plan.sessionId,
|
|
1117
|
-
by,
|
|
1118
|
-
batchId: plan.batchId,
|
|
1119
|
-
entity: "assignment",
|
|
1120
|
-
id: current.assignment.assignmentId,
|
|
1121
|
-
state: "EXECUTING",
|
|
1122
|
-
states: assignmentStates,
|
|
1123
|
-
assignmentId: current.assignment.assignmentId,
|
|
1124
|
-
attemptId: current.context.attemptId,
|
|
1125
|
-
teammateId: current.context.teammateId,
|
|
1126
|
-
retryCount: current.retryCount,
|
|
1127
|
-
...(current.providerSessionId
|
|
1128
|
-
? { providerSessionId: current.providerSessionId }
|
|
1129
|
-
: {}),
|
|
1130
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1131
|
-
});
|
|
1132
|
-
waits.set(next.slot, Promise.resolve({ slot: next.slot, result: continued }));
|
|
1133
|
-
releaseSlot = false;
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1134
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;
|
|
1135
1370
|
}
|
|
1136
1371
|
else if (next.result.kind === "report") {
|
|
1137
1372
|
try {
|
|
@@ -1162,22 +1397,25 @@ export async function runTeamworkCoordinator(input) {
|
|
|
1162
1397
|
}
|
|
1163
1398
|
return settle();
|
|
1164
1399
|
}
|
|
1165
|
-
export async function assertTeamworkReceiptDurable(
|
|
1400
|
+
export async function assertTeamworkReceiptDurable(plan, receipt, cwd) {
|
|
1166
1401
|
const [state, events] = await Promise.all([
|
|
1167
|
-
readCoordinationState({
|
|
1168
|
-
|
|
1402
|
+
readCoordinationState({
|
|
1403
|
+
sessionId: plan.sessionId,
|
|
1404
|
+
...(cwd ? { cwd } : {}),
|
|
1405
|
+
}),
|
|
1406
|
+
readTeamworkEvents({
|
|
1407
|
+
sessionId: plan.sessionId,
|
|
1408
|
+
...(cwd ? { cwd } : {}),
|
|
1409
|
+
}),
|
|
1169
1410
|
]);
|
|
1170
|
-
const
|
|
1171
|
-
if (
|
|
1172
|
-
(batch.
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (!receiptRecorded) {
|
|
1180
|
-
throw new Error(`batch '${batchId}' has no matching durable receipt event`);
|
|
1181
|
-
}
|
|
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`);
|
|
1182
1420
|
}
|
|
1183
1421
|
//# sourceMappingURL=coordinator.js.map
|