@pennixrv/trellis 0.7.1-beta.9 → 0.7.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +39 -26
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/teamwork/adapters/acp.d.ts +20 -0
- package/dist/commands/teamwork/adapters/acp.d.ts.map +1 -0
- package/dist/commands/teamwork/adapters/acp.js +268 -0
- package/dist/commands/teamwork/adapters/acp.js.map +1 -0
- package/dist/commands/teamwork/adapters/claude.d.ts +1 -0
- package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -1
- package/dist/commands/teamwork/adapters/claude.js +5 -0
- package/dist/commands/teamwork/adapters/claude.js.map +1 -1
- package/dist/commands/teamwork/adapters/codex.d.ts +3 -2
- package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -1
- package/dist/commands/teamwork/adapters/codex.js +14 -15
- package/dist/commands/teamwork/adapters/codex.js.map +1 -1
- package/dist/commands/teamwork/adapters/index.d.ts +23 -1
- package/dist/commands/teamwork/adapters/index.d.ts.map +1 -1
- package/dist/commands/teamwork/adapters/index.js +163 -24
- package/dist/commands/teamwork/adapters/index.js.map +1 -1
- package/dist/commands/teamwork/agent-loader.d.ts +2 -1
- package/dist/commands/teamwork/agent-loader.d.ts.map +1 -1
- package/dist/commands/teamwork/agent-loader.js +2 -1
- package/dist/commands/teamwork/agent-loader.js.map +1 -1
- package/dist/commands/teamwork/artifact.d.ts +33 -10
- package/dist/commands/teamwork/artifact.d.ts.map +1 -1
- package/dist/commands/teamwork/artifact.js +182 -50
- package/dist/commands/teamwork/artifact.js.map +1 -1
- package/dist/commands/teamwork/batch-reservation.d.ts +16 -0
- package/dist/commands/teamwork/batch-reservation.d.ts.map +1 -0
- package/dist/commands/teamwork/batch-reservation.js +43 -0
- package/dist/commands/teamwork/batch-reservation.js.map +1 -0
- package/dist/commands/teamwork/batch.d.ts +22 -0
- package/dist/commands/teamwork/batch.d.ts.map +1 -0
- package/dist/commands/teamwork/batch.js +394 -0
- package/dist/commands/teamwork/batch.js.map +1 -0
- package/dist/commands/teamwork/context-loader.d.ts +1 -1
- package/dist/commands/teamwork/context-loader.d.ts.map +1 -1
- package/dist/commands/teamwork/context-loader.js +19 -6
- package/dist/commands/teamwork/context-loader.js.map +1 -1
- package/dist/commands/teamwork/coordination.d.ts +88 -0
- package/dist/commands/teamwork/coordination.d.ts.map +1 -0
- package/dist/commands/teamwork/coordination.js +153 -0
- package/dist/commands/teamwork/coordination.js.map +1 -0
- package/dist/commands/teamwork/coordinator.d.ts +17 -3
- package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
- package/dist/commands/teamwork/coordinator.js +772 -289
- package/dist/commands/teamwork/coordinator.js.map +1 -1
- package/dist/commands/teamwork/create.d.ts +1 -0
- package/dist/commands/teamwork/create.d.ts.map +1 -1
- package/dist/commands/teamwork/create.js +2 -1
- package/dist/commands/teamwork/create.js.map +1 -1
- package/dist/commands/teamwork/dev-parse-trace.d.ts +2 -1
- package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -1
- package/dist/commands/teamwork/dev-parse-trace.js +2 -0
- package/dist/commands/teamwork/dev-parse-trace.js.map +1 -1
- package/dist/commands/teamwork/foreground-wait.d.ts +6 -0
- package/dist/commands/teamwork/foreground-wait.d.ts.map +1 -0
- package/dist/commands/teamwork/foreground-wait.js +65 -0
- package/dist/commands/teamwork/foreground-wait.js.map +1 -0
- package/dist/commands/teamwork/guard.d.ts +2 -2
- package/dist/commands/teamwork/guard.d.ts.map +1 -1
- package/dist/commands/teamwork/guard.js +23 -5
- package/dist/commands/teamwork/guard.js.map +1 -1
- package/dist/commands/teamwork/index.d.ts.map +1 -1
- package/dist/commands/teamwork/index.js +139 -10
- package/dist/commands/teamwork/index.js.map +1 -1
- package/dist/commands/teamwork/kill.d.ts +2 -0
- package/dist/commands/teamwork/kill.d.ts.map +1 -1
- package/dist/commands/teamwork/kill.js +79 -38
- package/dist/commands/teamwork/kill.js.map +1 -1
- package/dist/commands/teamwork/messages.d.ts.map +1 -1
- package/dist/commands/teamwork/messages.js +3 -0
- package/dist/commands/teamwork/messages.js.map +1 -1
- package/dist/commands/teamwork/plan.d.ts +22 -0
- package/dist/commands/teamwork/plan.d.ts.map +1 -0
- package/dist/commands/teamwork/plan.js +288 -0
- package/dist/commands/teamwork/plan.js.map +1 -0
- package/dist/commands/teamwork/process-identity.d.ts +3 -0
- package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
- package/dist/commands/teamwork/process-identity.js +24 -0
- package/dist/commands/teamwork/process-identity.js.map +1 -0
- package/dist/commands/teamwork/profiles.d.ts +43 -6
- package/dist/commands/teamwork/profiles.d.ts.map +1 -1
- package/dist/commands/teamwork/profiles.js +138 -110
- package/dist/commands/teamwork/profiles.js.map +1 -1
- package/dist/commands/teamwork/prompt.d.ts +4 -0
- package/dist/commands/teamwork/prompt.d.ts.map +1 -0
- package/dist/commands/teamwork/prompt.js +33 -0
- package/dist/commands/teamwork/prompt.js.map +1 -0
- package/dist/commands/teamwork/result.d.ts +54 -0
- package/dist/commands/teamwork/result.d.ts.map +1 -0
- package/dist/commands/teamwork/result.js +94 -0
- package/dist/commands/teamwork/result.js.map +1 -0
- package/dist/commands/teamwork/rm.d.ts.map +1 -1
- package/dist/commands/teamwork/rm.js +64 -64
- package/dist/commands/teamwork/rm.js.map +1 -1
- package/dist/commands/teamwork/run.d.ts +5 -0
- package/dist/commands/teamwork/run.d.ts.map +1 -1
- package/dist/commands/teamwork/run.js +120 -24
- package/dist/commands/teamwork/run.js.map +1 -1
- package/dist/commands/teamwork/runtime.d.ts +9 -0
- package/dist/commands/teamwork/runtime.d.ts.map +1 -0
- package/dist/commands/teamwork/runtime.js +372 -0
- package/dist/commands/teamwork/runtime.js.map +1 -0
- package/dist/commands/teamwork/send.d.ts +1 -0
- package/dist/commands/teamwork/send.d.ts.map +1 -1
- package/dist/commands/teamwork/send.js +2 -1
- package/dist/commands/teamwork/send.js.map +1 -1
- package/dist/commands/teamwork/spawn.d.ts +10 -3
- package/dist/commands/teamwork/spawn.d.ts.map +1 -1
- package/dist/commands/teamwork/spawn.js +41 -29
- package/dist/commands/teamwork/spawn.js.map +1 -1
- package/dist/commands/teamwork/store/lock.js +28 -4
- package/dist/commands/teamwork/store/lock.js.map +1 -1
- package/dist/commands/teamwork/supervisor/inbox.d.ts +1 -0
- package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/inbox.js +28 -9
- package/dist/commands/teamwork/supervisor/inbox.js.map +1 -1
- package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/stdout.js +2 -3
- package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
- package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor/warning.js +2 -3
- package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
- package/dist/commands/teamwork/supervisor.d.ts +5 -0
- package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
- package/dist/commands/teamwork/supervisor.js +21 -12
- package/dist/commands/teamwork/supervisor.js.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.d.ts +0 -3
- package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.js +9 -54
- package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
- package/dist/commands/teamwork/teammate-config.d.ts +5 -1
- package/dist/commands/teamwork/teammate-config.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-config.js +62 -19
- package/dist/commands/teamwork/teammate-config.js.map +1 -1
- package/dist/commands/teamwork/teammate-launch.d.ts +14 -4
- package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-launch.js +129 -20
- package/dist/commands/teamwork/teammate-launch.js.map +1 -1
- package/dist/commands/teamwork/wait.d.ts +2 -0
- package/dist/commands/teamwork/wait.d.ts.map +1 -1
- package/dist/commands/teamwork/wait.js +44 -3
- package/dist/commands/teamwork/wait.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +4 -3
- package/dist/commands/update.js.map +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +14 -32
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.11.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.14.json +11 -0
- 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/migrations/manifests/0.7.1-beta.17.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.18.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.19.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.6.json +1 -1
- package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
- package/dist/templates/codex/hooks/session-start.py +14 -11
- package/dist/templates/codex/hooks.json +10 -0
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +14 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +4 -0
- package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +14 -4
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +31 -2
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -31
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +94 -24
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +112 -20
- package/dist/templates/copilot/hooks/session-start.py +14 -11
- package/dist/templates/copilot/index.d.ts +1 -6
- package/dist/templates/copilot/index.d.ts.map +1 -1
- package/dist/templates/copilot/index.js +1 -14
- package/dist/templates/copilot/index.js.map +1 -1
- package/dist/templates/markdown/spec/backend/index.md.txt +4 -35
- package/dist/templates/markdown/spec/frontend/index.md.txt +4 -36
- package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
- package/dist/templates/pi/extensions/trellis/index.ts.txt +17 -1
- package/dist/templates/shared-hooks/guard-teamwork-wait.py +53 -0
- package/dist/templates/shared-hooks/index.d.ts +1 -1
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +1 -0
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-workflow-state.py +8 -3
- package/dist/templates/shared-hooks/session-start.py +16 -12
- package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +9 -0
- package/dist/templates/trellis/agents/profiles/architecture.md +9 -0
- package/dist/templates/trellis/agents/profiles/diagnosis.md +9 -0
- package/dist/templates/trellis/agents/profiles/exploration.md +9 -0
- package/dist/templates/trellis/agents/profiles/research.md +9 -0
- package/dist/templates/trellis/agents/profiles/review.md +9 -0
- package/dist/templates/trellis/agents/profiles/verification.md +9 -0
- package/dist/templates/trellis/agents/teammate-profiles.json +56 -4
- package/dist/templates/trellis/agents/teammate.md +4 -63
- package/dist/templates/trellis/config.yaml +2 -5
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +16 -4
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/task_continuity.py +54 -28
- package/dist/templates/trellis/scripts/common/task_store.py +7 -2
- package/dist/templates/trellis/scripts/common/task_workflow.py +270 -18
- package/dist/templates/trellis/scripts/task.py +27 -3
- package/dist/templates/trellis/teamwork.json +10 -0
- package/dist/templates/trellis/workflow.md +7 -0
- package/dist/utils/asset-backup.d.ts.map +1 -1
- package/dist/utils/asset-backup.js +10 -4
- package/dist/utils/asset-backup.js.map +1 -1
- package/dist/utils/template-fetcher.d.ts +4 -19
- package/dist/utils/template-fetcher.d.ts.map +1 -1
- package/dist/utils/template-fetcher.js +9 -45
- package/dist/utils/template-fetcher.js.map +1 -1
- package/package.json +3 -2
- package/dist/templates/copilot/prompts/before-dev.prompt.md +0 -39
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +0 -13
- package/dist/templates/copilot/prompts/break-loop.prompt.md +0 -129
- package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +0 -170
- package/dist/templates/copilot/prompts/check.prompt.md +0 -110
- package/dist/templates/copilot/prompts/create-command.prompt.md +0 -116
- package/dist/templates/copilot/prompts/finish-work.prompt.md +0 -22
- package/dist/templates/copilot/prompts/integrate-skill.prompt.md +0 -223
- package/dist/templates/copilot/prompts/onboard.prompt.md +0 -362
- package/dist/templates/copilot/prompts/record-session.prompt.md +0 -58
- package/dist/templates/copilot/prompts/start.prompt.md +0 -65
- package/dist/templates/copilot/prompts/update-spec.prompt.md +0 -358
|
@@ -1,12 +1,13 @@
|
|
|
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";
|
|
6
|
-
import { assertTeammateAdapterContract, } from "./teammate-adapters.js";
|
|
7
|
+
import { TeammateReportReadError, cleanupSettledTeamworkArtifacts, readTeammateReport, writeTeammateQueueFact, writeTeammateClaim, writeTeammateDisposition, validateTeamworkPlan, } from "./artifact.js";
|
|
8
|
+
import { assertTeammateAdapterContract, assertTeammateHostCapability, } from "./teammate-adapters.js";
|
|
7
9
|
import { effectiveTeammateCapacity, resolveTeammateRuntimeConfig, } from "./teammate-config.js";
|
|
8
|
-
import {
|
|
9
|
-
import { buildTeammateLaunchSpec, } from "./teammate-launch.js";
|
|
10
|
+
import { readProcessIdentity } from "./process-identity.js";
|
|
10
11
|
export function assertTeamworkPlanCurrent(plan, taskRoot) {
|
|
11
12
|
const taskFile = path.join(taskRoot, "task.json");
|
|
12
13
|
const stat = fs.lstatSync(taskFile);
|
|
@@ -37,6 +38,165 @@ export function assertTeamworkPlanCurrent(plan, taskRoot) {
|
|
|
37
38
|
throw new Error("Teamwork plan does not match the current active task phase and revisions");
|
|
38
39
|
}
|
|
39
40
|
}
|
|
41
|
+
function terminalReceiptFromProjection(plan, projection) {
|
|
42
|
+
const mismatch = (reason) => {
|
|
43
|
+
throw new Error(`batch '${plan.batchId}' ${reason}`);
|
|
44
|
+
};
|
|
45
|
+
if (projection.issues.length > 0)
|
|
46
|
+
mismatch("has coordination projection issues");
|
|
47
|
+
const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
|
|
48
|
+
if (!batch)
|
|
49
|
+
return mismatch("does not match the frozen plan");
|
|
50
|
+
if (batch.planHash !== plan.planHash)
|
|
51
|
+
return mismatch("does not match the frozen plan");
|
|
52
|
+
if (batch.state !== "ALL_ACCEPTED" && batch.state !== "SETTLED_WITH_FAILURE")
|
|
53
|
+
mismatch("is not terminal");
|
|
54
|
+
const effectiveCapacity = batch.effectiveCapacity;
|
|
55
|
+
if (typeof effectiveCapacity !== "number" ||
|
|
56
|
+
!Number.isSafeInteger(effectiveCapacity) ||
|
|
57
|
+
effectiveCapacity < 1)
|
|
58
|
+
return mismatch("has no valid effective capacity");
|
|
59
|
+
if (projection.leases.some((lease) => lease.batchId === plan.batchId))
|
|
60
|
+
mismatch("still owns capacity leases");
|
|
61
|
+
const projectedAssignments = projection.assignments.filter((assignment) => assignment.batchId === plan.batchId);
|
|
62
|
+
if (projectedAssignments.length !== plan.assignments.length ||
|
|
63
|
+
new Set(projectedAssignments.map((assignment) => assignment.assignmentId))
|
|
64
|
+
.size !== plan.assignments.length)
|
|
65
|
+
mismatch("does not contain the exact frozen assignment set");
|
|
66
|
+
const assignmentById = new Map(projectedAssignments.map((assignment) => [
|
|
67
|
+
assignment.assignmentId,
|
|
68
|
+
assignment,
|
|
69
|
+
]));
|
|
70
|
+
const planAssignmentIds = new Set(plan.assignments.map((assignment) => assignment.assignmentId));
|
|
71
|
+
const attempts = projection.attempts.filter((attempt) => attempt.batchId === plan.batchId);
|
|
72
|
+
const teammates = projection.teammates.filter((teammate) => teammate.batchId === plan.batchId);
|
|
73
|
+
if (attempts.length !== teammates.length)
|
|
74
|
+
mismatch("has unmatched provider attempts and teammates");
|
|
75
|
+
for (const attempt of attempts) {
|
|
76
|
+
if (!attempt.assignmentId ||
|
|
77
|
+
!planAssignmentIds.has(attempt.assignmentId) ||
|
|
78
|
+
!attempt.teammateId ||
|
|
79
|
+
attempt.terminal !== true ||
|
|
80
|
+
!["REPORT_SUBMITTED", "FAILED", "FINAL_FAILED"].includes(attempt.state))
|
|
81
|
+
mismatch("has an unresolved provider attempt");
|
|
82
|
+
const teammate = teammates.find((entry) => entry.attemptId === attempt.attemptId &&
|
|
83
|
+
entry.teammateId === attempt.teammateId);
|
|
84
|
+
if (teammate?.terminal !== true ||
|
|
85
|
+
(teammate?.state !== "REPORT_SUBMITTED" && teammate?.state !== "FAILED"))
|
|
86
|
+
mismatch("has an unresolved teammate terminal fact");
|
|
87
|
+
}
|
|
88
|
+
const assignments = plan.assignments.map((assignment) => {
|
|
89
|
+
const projected = assignmentById.get(assignment.assignmentId);
|
|
90
|
+
if (!projected ||
|
|
91
|
+
(projected.state !== "ACCEPTED" && projected.state !== "FINAL_FAILED"))
|
|
92
|
+
return mismatch(`assignment '${assignment.assignmentId}' is not settled`);
|
|
93
|
+
if (projected.state === "ACCEPTED") {
|
|
94
|
+
const acceptedAttempt = attempts.find((attempt) => attempt.attemptId === projected.attemptId &&
|
|
95
|
+
attempt.assignmentId === assignment.assignmentId);
|
|
96
|
+
if (acceptedAttempt?.state !== "REPORT_SUBMITTED")
|
|
97
|
+
return mismatch(`assignment '${assignment.assignmentId}' has no accepted attempt`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
assignmentId: assignment.assignmentId,
|
|
101
|
+
status: projected.state,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
const status = assignments.every((item) => item.status === "ACCEPTED")
|
|
105
|
+
? "ALL_ACCEPTED"
|
|
106
|
+
: "SETTLED_WITH_FAILURE";
|
|
107
|
+
if (batch.state !== status)
|
|
108
|
+
mismatch("terminal state disagrees with its assignment dispositions");
|
|
109
|
+
const retryCounts = Object.fromEntries(plan.assignments.flatMap((assignment) => {
|
|
110
|
+
const count = assignmentById.get(assignment.assignmentId)?.retryCount;
|
|
111
|
+
if (count === undefined)
|
|
112
|
+
return mismatch(`assignment '${assignment.assignmentId}' is missing`);
|
|
113
|
+
const started = attempts.some((attempt) => attempt.assignmentId === assignment.assignmentId);
|
|
114
|
+
return started || count > 0 ? [[assignment.assignmentId, count]] : [];
|
|
115
|
+
}));
|
|
116
|
+
const evaluationGroups = [
|
|
117
|
+
...new Set(plan.assignments
|
|
118
|
+
.map((assignment) => assignment.evaluationGroup?.id)
|
|
119
|
+
.filter((id) => id !== undefined)),
|
|
120
|
+
].map((id) => ({
|
|
121
|
+
id,
|
|
122
|
+
status: plan.assignments
|
|
123
|
+
.filter((assignment) => assignment.evaluationGroup?.id === id)
|
|
124
|
+
.every((assignment) => assignmentById.get(assignment.assignmentId)?.state === "ACCEPTED")
|
|
125
|
+
? "COMPLETE"
|
|
126
|
+
: "INCOMPLETE",
|
|
127
|
+
}));
|
|
128
|
+
return {
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
batchId: plan.batchId,
|
|
131
|
+
status,
|
|
132
|
+
assignments,
|
|
133
|
+
capacity: effectiveCapacity,
|
|
134
|
+
retryCounts,
|
|
135
|
+
evaluationGroups,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function reconciliationReceipt(plan, projection, capacity) {
|
|
139
|
+
return {
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
batchId: plan.batchId,
|
|
142
|
+
status: "RECONCILIATION_REQUIRED",
|
|
143
|
+
assignments: plan.assignments.map((item) => {
|
|
144
|
+
const current = projection.assignments.find((entry) => entry.batchId === plan.batchId &&
|
|
145
|
+
entry.assignmentId === item.assignmentId);
|
|
146
|
+
return {
|
|
147
|
+
assignmentId: item.assignmentId,
|
|
148
|
+
status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
|
|
149
|
+
? current.state
|
|
150
|
+
: "UNKNOWN",
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
capacity: projection.batches.find((entry) => entry.batchId === plan.batchId)
|
|
154
|
+
?.effectiveCapacity ?? capacity,
|
|
155
|
+
retryCounts: Object.fromEntries(projection.assignments
|
|
156
|
+
.filter((entry) => entry.batchId === plan.batchId)
|
|
157
|
+
.map((entry) => [entry.assignmentId, entry.retryCount])),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function recordRecoveryReconciliation(input) {
|
|
161
|
+
const { plan, cwd, by, existingState, projection, capacity, reason } = input;
|
|
162
|
+
if (existingState !== "RECONCILIATION_REQUIRED" &&
|
|
163
|
+
existingState !== "ALL_ACCEPTED" &&
|
|
164
|
+
existingState !== "SETTLED_WITH_FAILURE") {
|
|
165
|
+
await appendTransition({
|
|
166
|
+
sessionId: plan.sessionId,
|
|
167
|
+
cwd,
|
|
168
|
+
by,
|
|
169
|
+
batchId: plan.batchId,
|
|
170
|
+
entity: "batch",
|
|
171
|
+
id: plan.batchId,
|
|
172
|
+
state: "RECONCILIATION_REQUIRED",
|
|
173
|
+
states: new Map([[plan.batchId, existingState]]),
|
|
174
|
+
planHash: plan.planHash,
|
|
175
|
+
payload: { recoveredAfterRestart: true },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const receipt = reconciliationReceipt(plan, projection, capacity);
|
|
179
|
+
await appendAction({
|
|
180
|
+
sessionId: plan.sessionId,
|
|
181
|
+
cwd,
|
|
182
|
+
by,
|
|
183
|
+
batchId: plan.batchId,
|
|
184
|
+
entity: "batch",
|
|
185
|
+
action: "reconciliation_required",
|
|
186
|
+
reason,
|
|
187
|
+
});
|
|
188
|
+
await appendAction({
|
|
189
|
+
sessionId: plan.sessionId,
|
|
190
|
+
cwd,
|
|
191
|
+
by,
|
|
192
|
+
batchId: plan.batchId,
|
|
193
|
+
entity: "batch",
|
|
194
|
+
action: "receipt_recorded",
|
|
195
|
+
id: `${plan.batchId}:RECONCILIATION_REQUIRED`,
|
|
196
|
+
payload: { ...receipt, reason },
|
|
197
|
+
});
|
|
198
|
+
return receipt;
|
|
199
|
+
}
|
|
40
200
|
async function recoverExistingBatch(input) {
|
|
41
201
|
const projection = await readCoordinationState({
|
|
42
202
|
sessionId: input.plan.sessionId,
|
|
@@ -52,64 +212,55 @@ async function recoverExistingBatch(input) {
|
|
|
52
212
|
sessionId: input.plan.sessionId,
|
|
53
213
|
cwd: input.cwd,
|
|
54
214
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
215
|
+
if (existing.state === "ALL_ACCEPTED" ||
|
|
216
|
+
existing.state === "SETTLED_WITH_FAILURE") {
|
|
217
|
+
let receipt;
|
|
218
|
+
try {
|
|
219
|
+
receipt = terminalReceiptFromProjection(input.plan, projection);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
return recordRecoveryReconciliation({
|
|
223
|
+
...input,
|
|
224
|
+
existingState: existing.state,
|
|
225
|
+
projection,
|
|
226
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const receipts = events.filter((event) => event.kind === "coordination" &&
|
|
230
|
+
event.batchId === input.plan.batchId &&
|
|
231
|
+
event.action === "receipt_recorded" &&
|
|
232
|
+
event.payload?.status !== "RECONCILIATION_REQUIRED");
|
|
233
|
+
if (receipts.length > 1 ||
|
|
234
|
+
(receipts.length === 1 &&
|
|
235
|
+
!isDeepStrictEqual(receipts[0]?.payload, receipt))) {
|
|
236
|
+
return recordRecoveryReconciliation({
|
|
237
|
+
...input,
|
|
238
|
+
existingState: existing.state,
|
|
239
|
+
projection,
|
|
240
|
+
reason: "saved terminal receipt disagrees with durable coordination facts",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (receipts.length === 0) {
|
|
244
|
+
await appendAction({
|
|
245
|
+
sessionId: input.plan.sessionId,
|
|
246
|
+
cwd: input.cwd,
|
|
247
|
+
by: input.by,
|
|
248
|
+
batchId: input.plan.batchId,
|
|
249
|
+
entity: "batch",
|
|
250
|
+
action: "receipt_recorded",
|
|
251
|
+
id: `${input.plan.batchId}:${receipt.status}`,
|
|
252
|
+
payload: { ...receipt },
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
await assertTeamworkReceiptDurable(input.plan, receipt, input.cwd);
|
|
256
|
+
return receipt;
|
|
79
257
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
|
|
86
|
-
? current.state
|
|
87
|
-
: "UNKNOWN",
|
|
88
|
-
};
|
|
89
|
-
});
|
|
90
|
-
const retryCounts = Object.fromEntries(projection.assignments
|
|
91
|
-
.filter((entry) => entry.batchId === input.plan.batchId)
|
|
92
|
-
.map((entry) => [entry.assignmentId, entry.retryCount]));
|
|
93
|
-
const receipt = {
|
|
94
|
-
schemaVersion: 1,
|
|
95
|
-
batchId: input.plan.batchId,
|
|
96
|
-
status: "RECONCILIATION_REQUIRED",
|
|
97
|
-
assignments,
|
|
98
|
-
capacity: existing.effectiveCapacity ?? input.capacity,
|
|
99
|
-
retryCounts,
|
|
100
|
-
};
|
|
101
|
-
await recordCoordinationEvent({
|
|
102
|
-
sessionId: input.plan.sessionId,
|
|
103
|
-
cwd: input.cwd,
|
|
104
|
-
by: input.by,
|
|
105
|
-
batchId: input.plan.batchId,
|
|
106
|
-
entity: "batch",
|
|
107
|
-
action: "receipt_recorded",
|
|
108
|
-
receiptId: `${input.plan.batchId}:RECONCILIATION_REQUIRED`,
|
|
109
|
-
payload: { ...receipt, reason: "recovered batch requires reconciliation" },
|
|
110
|
-
idempotencyKey: `${input.plan.batchId}:RECONCILIATION_REQUIRED:receipt`,
|
|
258
|
+
return recordRecoveryReconciliation({
|
|
259
|
+
...input,
|
|
260
|
+
existingState: existing.state,
|
|
261
|
+
projection,
|
|
262
|
+
reason: "recovered nonterminal batch requires reconciliation",
|
|
111
263
|
});
|
|
112
|
-
return receipt;
|
|
113
264
|
}
|
|
114
265
|
function transitionState(states, key, next, entity) {
|
|
115
266
|
assertCoordinationTransition(entity, states.get(key), next);
|
|
@@ -119,6 +270,7 @@ async function appendTransition(input) {
|
|
|
119
270
|
transitionState(input.states, input.id, input.state, input.entity);
|
|
120
271
|
await recordCoordinationEvent({
|
|
121
272
|
sessionId: input.sessionId,
|
|
273
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
122
274
|
by: input.by,
|
|
123
275
|
batchId: input.batchId,
|
|
124
276
|
entity: input.entity,
|
|
@@ -141,6 +293,7 @@ async function appendTransition(input) {
|
|
|
141
293
|
async function appendAction(input) {
|
|
142
294
|
await recordCoordinationEvent({
|
|
143
295
|
sessionId: input.sessionId,
|
|
296
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
144
297
|
by: input.by,
|
|
145
298
|
batchId: input.batchId,
|
|
146
299
|
entity: input.entity,
|
|
@@ -164,47 +317,60 @@ async function appendAction(input) {
|
|
|
164
317
|
idempotencyKey: `${input.batchId}:${input.action}:${input.id ?? input.leaseId ?? input.attemptId ?? "event"}`,
|
|
165
318
|
});
|
|
166
319
|
}
|
|
167
|
-
function readPlanFile(planPath) {
|
|
320
|
+
export function readPlanFile(planPath) {
|
|
321
|
+
const stat = fs.lstatSync(planPath);
|
|
322
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024)
|
|
323
|
+
throw new Error("Teamwork plan must be a regular file under 2097152 bytes");
|
|
168
324
|
const raw = fs.readFileSync(planPath, "utf8");
|
|
169
325
|
return JSON.parse(raw);
|
|
170
326
|
}
|
|
171
327
|
export function loadTeamworkPlan(planPath, taskRoot) {
|
|
172
|
-
const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot);
|
|
328
|
+
const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot, undefined, { allowExpired: true });
|
|
173
329
|
assertTeamworkPlanCurrent(plan, taskRoot);
|
|
174
330
|
return plan;
|
|
175
331
|
}
|
|
176
332
|
export async function runTeamworkCoordinator(input) {
|
|
177
|
-
const plan = validateTeamworkPlan(input.plan, input.taskRoot
|
|
333
|
+
const plan = validateTeamworkPlan(input.plan, input.taskRoot, undefined, {
|
|
334
|
+
allowExpired: true,
|
|
335
|
+
});
|
|
178
336
|
assertTeamworkPlanCurrent(plan, input.taskRoot);
|
|
179
337
|
const cwd = input.cwd ?? input.taskRoot;
|
|
180
338
|
const frozenLaunches = new Map();
|
|
181
339
|
for (const assignment of plan.assignments) {
|
|
182
340
|
assertTeammateAdapterContract(assignment.provider);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
profile: assignment.profile,
|
|
186
|
-
provider: assignment.provider,
|
|
187
|
-
model: assignment.model,
|
|
188
|
-
});
|
|
189
|
-
frozenLaunches.set(assignment.assignmentId, {
|
|
190
|
-
profile,
|
|
191
|
-
launch: buildTeammateLaunchSpec({
|
|
192
|
-
cwd,
|
|
193
|
-
profile,
|
|
194
|
-
host: input.host,
|
|
195
|
-
contextFiles: assignment.contextFiles,
|
|
196
|
-
contextManifests: assignment.contextManifests,
|
|
197
|
-
protectedTargets: plan.protectedTargets,
|
|
198
|
-
}),
|
|
199
|
-
});
|
|
341
|
+
assertTeammateHostCapability(assignment.provider, input.host, assignment.execution.profile.options);
|
|
342
|
+
frozenLaunches.set(assignment.assignmentId, assignment.execution);
|
|
200
343
|
}
|
|
201
|
-
const config = input.config ??
|
|
344
|
+
const config = input.config ??
|
|
345
|
+
resolveTeammateRuntimeConfig(cwd, process.env, plan.coordinationMode);
|
|
202
346
|
const capacity = effectiveTeammateCapacity({
|
|
203
347
|
requested: plan.requestedCapacity,
|
|
348
|
+
assignmentCount: plan.assignments.length,
|
|
204
349
|
config,
|
|
205
350
|
verifiedHostCap: input.host.verifiedHostCap,
|
|
206
351
|
});
|
|
207
352
|
const by = input.by ?? "main";
|
|
353
|
+
const existing = await readCoordinationState({
|
|
354
|
+
sessionId: plan.sessionId,
|
|
355
|
+
cwd,
|
|
356
|
+
});
|
|
357
|
+
const settled = existing.batches.find((batch) => batch.batchId === plan.batchId);
|
|
358
|
+
if (settled &&
|
|
359
|
+
["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(settled.state)) {
|
|
360
|
+
let terminal = false;
|
|
361
|
+
try {
|
|
362
|
+
terminalReceiptFromProjection(plan, existing);
|
|
363
|
+
terminal = true;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Inconsistent terminal facts still require the protected recovery path.
|
|
367
|
+
}
|
|
368
|
+
if (terminal) {
|
|
369
|
+
const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
|
|
370
|
+
if (recovered)
|
|
371
|
+
return recovered;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
208
374
|
const batchStates = new Map();
|
|
209
375
|
const assignmentStates = new Map();
|
|
210
376
|
const attemptStates = new Map();
|
|
@@ -215,19 +381,8 @@ export async function runTeamworkCoordinator(input) {
|
|
|
215
381
|
capacity,
|
|
216
382
|
});
|
|
217
383
|
let guardHeld = true;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
await input.guard.release({
|
|
224
|
-
batchId: plan.batchId,
|
|
225
|
-
planHash: plan.planHash,
|
|
226
|
-
capacity,
|
|
227
|
-
});
|
|
228
|
-
guardHeld = false;
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
384
|
+
// Failed recovery cannot prove that an older provider process exited.
|
|
385
|
+
const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
|
|
231
386
|
if (recovered) {
|
|
232
387
|
if (recovered.status !== "RECONCILIATION_REQUIRED") {
|
|
233
388
|
await input.guard.release({
|
|
@@ -239,6 +394,14 @@ export async function runTeamworkCoordinator(input) {
|
|
|
239
394
|
}
|
|
240
395
|
return recovered;
|
|
241
396
|
}
|
|
397
|
+
if (Date.parse(plan.deadline) <= Date.now()) {
|
|
398
|
+
await input.guard.release({
|
|
399
|
+
batchId: plan.batchId,
|
|
400
|
+
planHash: plan.planHash,
|
|
401
|
+
capacity,
|
|
402
|
+
});
|
|
403
|
+
throw new Error("deadline elapsed before batch dispatch");
|
|
404
|
+
}
|
|
242
405
|
try {
|
|
243
406
|
for (const [queueIndex, assignment] of plan.assignments.entries()) {
|
|
244
407
|
writeTeammateQueueFact({
|
|
@@ -328,8 +491,8 @@ export async function runTeamworkCoordinator(input) {
|
|
|
328
491
|
const active = new Map();
|
|
329
492
|
const waits = new Map();
|
|
330
493
|
const final = new Map();
|
|
331
|
-
const acceptedAttempts = new Map();
|
|
332
494
|
const retryCounts = {};
|
|
495
|
+
let admissionClosed = false;
|
|
333
496
|
async function reconcile(reason) {
|
|
334
497
|
await appendAction({
|
|
335
498
|
sessionId: plan.sessionId,
|
|
@@ -372,9 +535,39 @@ export async function runTeamworkCoordinator(input) {
|
|
|
372
535
|
});
|
|
373
536
|
return receipt;
|
|
374
537
|
}
|
|
538
|
+
async function failPending(reason) {
|
|
539
|
+
admissionClosed = true;
|
|
540
|
+
while (pending.length > 0) {
|
|
541
|
+
const assignment = pending.shift();
|
|
542
|
+
if (!assignment)
|
|
543
|
+
continue;
|
|
544
|
+
await appendTransition({
|
|
545
|
+
sessionId: plan.sessionId,
|
|
546
|
+
by,
|
|
547
|
+
batchId: plan.batchId,
|
|
548
|
+
entity: "assignment",
|
|
549
|
+
id: assignment.assignmentId,
|
|
550
|
+
state: "FINAL_FAILED",
|
|
551
|
+
states: assignmentStates,
|
|
552
|
+
assignmentId: assignment.assignmentId,
|
|
553
|
+
retryCount: retryCounts[assignment.assignmentId] ?? 0,
|
|
554
|
+
payload: { reason, dispatched: false },
|
|
555
|
+
});
|
|
556
|
+
final.set(assignment.assignmentId, "FINAL_FAILED");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
375
559
|
async function dispatch(slot, assignment, retryCount) {
|
|
376
|
-
const
|
|
377
|
-
|
|
560
|
+
const boundedId = (kind) => {
|
|
561
|
+
const candidate = `${assignment.assignmentId}.${kind}.${retryCount}`;
|
|
562
|
+
return candidate.length <= 64
|
|
563
|
+
? candidate
|
|
564
|
+
: crypto
|
|
565
|
+
.createHash("sha256")
|
|
566
|
+
.update(`${assignment.assignmentId}:${kind}:${retryCount}`)
|
|
567
|
+
.digest("hex");
|
|
568
|
+
};
|
|
569
|
+
const attemptId = boundedId("attempt");
|
|
570
|
+
const teammateId = boundedId("teammate");
|
|
378
571
|
const frozen = frozenLaunches.get(assignment.assignmentId);
|
|
379
572
|
if (!frozen)
|
|
380
573
|
throw new Error(`missing frozen launch for '${assignment.assignmentId}'`);
|
|
@@ -388,6 +581,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
388
581
|
profile,
|
|
389
582
|
launch,
|
|
390
583
|
};
|
|
584
|
+
input.runtime.prepare?.(context);
|
|
391
585
|
await appendTransition({
|
|
392
586
|
sessionId: plan.sessionId,
|
|
393
587
|
by,
|
|
@@ -510,6 +704,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
510
704
|
teammateId,
|
|
511
705
|
slot,
|
|
512
706
|
retryCount,
|
|
707
|
+
transitionKey: `attempt:${attemptId}`,
|
|
513
708
|
...(context.providerSessionId
|
|
514
709
|
? { providerSessionId: context.providerSessionId }
|
|
515
710
|
: {}),
|
|
@@ -562,7 +757,63 @@ export async function runTeamworkCoordinator(input) {
|
|
|
562
757
|
});
|
|
563
758
|
guardHeld = false;
|
|
564
759
|
}
|
|
760
|
+
async function resumeFromWait(current) {
|
|
761
|
+
if (assignmentStates.get(current.assignment.assignmentId) !==
|
|
762
|
+
"WAITING_FOR_MAIN_COMMAND")
|
|
763
|
+
return;
|
|
764
|
+
const transitionKey = `continuation:${current.continuations}`;
|
|
765
|
+
await appendTransition({
|
|
766
|
+
sessionId: plan.sessionId,
|
|
767
|
+
by,
|
|
768
|
+
batchId: plan.batchId,
|
|
769
|
+
entity: "attempt",
|
|
770
|
+
id: current.context.attemptId,
|
|
771
|
+
state: "RUNNING",
|
|
772
|
+
states: attemptStates,
|
|
773
|
+
assignmentId: current.assignment.assignmentId,
|
|
774
|
+
attemptId: current.context.attemptId,
|
|
775
|
+
teammateId: current.context.teammateId,
|
|
776
|
+
retryCount: current.retryCount,
|
|
777
|
+
...(current.providerSessionId
|
|
778
|
+
? { providerSessionId: current.providerSessionId }
|
|
779
|
+
: {}),
|
|
780
|
+
transitionKey,
|
|
781
|
+
});
|
|
782
|
+
await appendTransition({
|
|
783
|
+
sessionId: plan.sessionId,
|
|
784
|
+
by,
|
|
785
|
+
batchId: plan.batchId,
|
|
786
|
+
entity: "teammate",
|
|
787
|
+
id: current.context.teammateId,
|
|
788
|
+
state: "RUNNING",
|
|
789
|
+
states: teammateStates,
|
|
790
|
+
attemptId: current.context.attemptId,
|
|
791
|
+
teammateId: current.context.teammateId,
|
|
792
|
+
...(current.providerSessionId
|
|
793
|
+
? { providerSessionId: current.providerSessionId }
|
|
794
|
+
: {}),
|
|
795
|
+
transitionKey,
|
|
796
|
+
});
|
|
797
|
+
await appendTransition({
|
|
798
|
+
sessionId: plan.sessionId,
|
|
799
|
+
by,
|
|
800
|
+
batchId: plan.batchId,
|
|
801
|
+
entity: "assignment",
|
|
802
|
+
id: current.assignment.assignmentId,
|
|
803
|
+
state: "EXECUTING",
|
|
804
|
+
states: assignmentStates,
|
|
805
|
+
assignmentId: current.assignment.assignmentId,
|
|
806
|
+
attemptId: current.context.attemptId,
|
|
807
|
+
teammateId: current.context.teammateId,
|
|
808
|
+
retryCount: current.retryCount,
|
|
809
|
+
...(current.providerSessionId
|
|
810
|
+
? { providerSessionId: current.providerSessionId }
|
|
811
|
+
: {}),
|
|
812
|
+
transitionKey,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
565
815
|
async function cancelAndDrain(reason, terminal) {
|
|
816
|
+
await failPending(reason);
|
|
566
817
|
if (batchStates.get(plan.batchId) === "RUNNING") {
|
|
567
818
|
await appendTransition({
|
|
568
819
|
sessionId: plan.sessionId,
|
|
@@ -575,52 +826,68 @@ export async function runTeamworkCoordinator(input) {
|
|
|
575
826
|
payload: { reason },
|
|
576
827
|
});
|
|
577
828
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
829
|
+
const cancel = input.runtime.cancel;
|
|
830
|
+
const outcomes = await Promise.all([...active.values()].map(async (current) => {
|
|
831
|
+
if (terminal?.current === current)
|
|
832
|
+
return { current, result: terminal.result };
|
|
833
|
+
if (!cancel)
|
|
834
|
+
return {
|
|
835
|
+
current,
|
|
836
|
+
error: new Error(`host cannot cancel active attempt: ${reason}`),
|
|
837
|
+
};
|
|
838
|
+
try {
|
|
839
|
+
return { current, result: await cancel(current.context) };
|
|
585
840
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (!input.runtime.cancel)
|
|
589
|
-
return reconcile(`host cannot cancel active attempt: ${reason}`);
|
|
590
|
-
result = await input.runtime.cancel(current.context);
|
|
591
|
-
}
|
|
592
|
-
catch (error) {
|
|
593
|
-
return reconcile(error instanceof Error ? error.message : String(error));
|
|
594
|
-
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
return { current, error };
|
|
595
843
|
}
|
|
844
|
+
}));
|
|
845
|
+
let cancellationFailure;
|
|
846
|
+
for (const outcome of outcomes) {
|
|
847
|
+
const { current } = outcome;
|
|
848
|
+
if ("error" in outcome) {
|
|
849
|
+
cancellationFailure ??=
|
|
850
|
+
outcome.error instanceof Error
|
|
851
|
+
? outcome.error.message
|
|
852
|
+
: String(outcome.error);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const { result } = outcome;
|
|
596
856
|
if (result.kind === "reconciliation" ||
|
|
597
857
|
result.kind === "timeout" ||
|
|
598
858
|
result.terminal !== true) {
|
|
599
|
-
|
|
859
|
+
cancellationFailure ??= `attempt cancellation is not terminal: ${reason}`;
|
|
860
|
+
continue;
|
|
600
861
|
}
|
|
601
862
|
if (result.providerSessionId &&
|
|
602
863
|
current.providerSessionId &&
|
|
603
864
|
result.providerSessionId !== current.providerSessionId) {
|
|
604
|
-
|
|
865
|
+
cancellationFailure ??=
|
|
866
|
+
"cancellation returned a different host session";
|
|
867
|
+
continue;
|
|
605
868
|
}
|
|
606
869
|
if (result.providerSessionId) {
|
|
607
870
|
current.providerSessionId = result.providerSessionId;
|
|
608
871
|
current.context.providerSessionId = result.providerSessionId;
|
|
609
872
|
}
|
|
610
873
|
try {
|
|
611
|
-
|
|
874
|
+
await resumeFromWait(current);
|
|
875
|
+
if (result.kind === "report") {
|
|
612
876
|
await complete(current, false);
|
|
877
|
+
}
|
|
613
878
|
else
|
|
614
879
|
await fail(current, result.reason ?? "cancelled by coordinator", false);
|
|
615
880
|
}
|
|
616
881
|
catch (error) {
|
|
617
|
-
|
|
882
|
+
cancellationFailure ??=
|
|
883
|
+
error instanceof Error ? error.message : String(error);
|
|
884
|
+
continue;
|
|
618
885
|
}
|
|
619
886
|
active.delete(current.slot);
|
|
620
887
|
waits.delete(current.slot);
|
|
621
888
|
}
|
|
622
|
-
if (
|
|
623
|
-
return reconcile(
|
|
889
|
+
if (cancellationFailure)
|
|
890
|
+
return reconcile(cancellationFailure);
|
|
624
891
|
return settle();
|
|
625
892
|
}
|
|
626
893
|
async function fail(activeWork, reason, allowRetry = true) {
|
|
@@ -657,7 +924,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
657
924
|
: {}),
|
|
658
925
|
terminal: true,
|
|
659
926
|
});
|
|
660
|
-
if (allowRetry && retryCount < assignment.maxRetries) {
|
|
927
|
+
if (allowRetry && !admissionClosed && retryCount < assignment.maxRetries) {
|
|
661
928
|
await release(activeWork);
|
|
662
929
|
retryCounts[assignment.assignmentId] = retryCount + 1;
|
|
663
930
|
pending.unshift(assignment);
|
|
@@ -743,7 +1010,6 @@ export async function runTeamworkCoordinator(input) {
|
|
|
743
1010
|
});
|
|
744
1011
|
await release(activeWork);
|
|
745
1012
|
final.set(assignment.assignmentId, "ACCEPTED");
|
|
746
|
-
acceptedAttempts.set(assignment.assignmentId, context.attemptId);
|
|
747
1013
|
}
|
|
748
1014
|
async function complete(activeWork, allowRetry = true) {
|
|
749
1015
|
const { assignment, context, retryCount } = activeWork;
|
|
@@ -782,9 +1048,23 @@ export async function runTeamworkCoordinator(input) {
|
|
|
782
1048
|
});
|
|
783
1049
|
}
|
|
784
1050
|
catch (error) {
|
|
1051
|
+
if (error instanceof TeammateReportReadError && error.kind !== "missing")
|
|
1052
|
+
throw error;
|
|
785
1053
|
await fail(activeWork, error instanceof Error ? error.message : String(error), allowRetry);
|
|
786
1054
|
return;
|
|
787
1055
|
}
|
|
1056
|
+
if (context.launch.reportVersion === 2 && report.schemaVersion !== 2) {
|
|
1057
|
+
await fail(activeWork, "report schema_version must match the frozen result contract (2)", allowRetry);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (report.status === "blocked") {
|
|
1061
|
+
await fail(activeWork, `teammate reported blocked: ${report.evidence[0]?.summary ?? "blocker evidence missing"}`, false);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (report.status !== "complete") {
|
|
1065
|
+
await fail(activeWork, `teammate reported ${report.status}`, allowRetry);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
788
1068
|
if (input.runtime.validateReport &&
|
|
789
1069
|
!(await input.runtime.validateReport(context, report))) {
|
|
790
1070
|
await fail(activeWork, "report rejected by acceptance contract", allowRetry);
|
|
@@ -870,7 +1150,6 @@ export async function runTeamworkCoordinator(input) {
|
|
|
870
1150
|
const cleanupFailures = cleanupSettledTeamworkArtifacts({
|
|
871
1151
|
taskRoot: input.taskRoot,
|
|
872
1152
|
assignments: plan.assignments,
|
|
873
|
-
acceptedAttempts,
|
|
874
1153
|
});
|
|
875
1154
|
if (cleanupFailures.length > 0) {
|
|
876
1155
|
return reconcile(`Teamwork batch cleanup failed: ${cleanupFailures.join("; ")}`);
|
|
@@ -918,20 +1197,44 @@ export async function runTeamworkCoordinator(input) {
|
|
|
918
1197
|
id: `${plan.batchId}:${status}`,
|
|
919
1198
|
payload: { ...receipt },
|
|
920
1199
|
});
|
|
921
|
-
await assertTeamworkReceiptDurable(plan
|
|
1200
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
922
1201
|
await releaseGuard();
|
|
923
1202
|
return receipt;
|
|
924
1203
|
}
|
|
925
1204
|
async function fill() {
|
|
1205
|
+
if (admissionClosed)
|
|
1206
|
+
return;
|
|
926
1207
|
while (active.size < capacity && pending.length > 0) {
|
|
927
|
-
|
|
1208
|
+
if (Date.parse(plan.deadline) <= Date.now()) {
|
|
1209
|
+
await failPending("batch deadline elapsed before dispatch");
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
assertTeamworkPlanCurrent(plan, input.taskRoot);
|
|
1214
|
+
}
|
|
1215
|
+
catch (error) {
|
|
1216
|
+
await failPending(`task phase changed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const assignment = pending[0];
|
|
928
1220
|
if (!assignment)
|
|
929
1221
|
return;
|
|
930
1222
|
const retryCount = retryCounts[assignment.assignmentId] ?? 0;
|
|
931
1223
|
let slot = 0;
|
|
932
1224
|
while (active.has(slot))
|
|
933
1225
|
slot += 1;
|
|
934
|
-
|
|
1226
|
+
try {
|
|
1227
|
+
await dispatch(slot, assignment, retryCount);
|
|
1228
|
+
pending.shift();
|
|
1229
|
+
}
|
|
1230
|
+
catch (error) {
|
|
1231
|
+
if (!assignmentStates.has(assignment.assignmentId) ||
|
|
1232
|
+
assignmentStates.get(assignment.assignmentId) === "QUEUED") {
|
|
1233
|
+
await failPending(`launch preflight failed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
935
1238
|
}
|
|
936
1239
|
}
|
|
937
1240
|
try {
|
|
@@ -991,162 +1294,116 @@ export async function runTeamworkCoordinator(input) {
|
|
|
991
1294
|
result: next.result,
|
|
992
1295
|
});
|
|
993
1296
|
}
|
|
1297
|
+
try {
|
|
1298
|
+
await resumeFromWait(current);
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1302
|
+
}
|
|
994
1303
|
let releaseSlot = true;
|
|
995
1304
|
if (next.result.kind === "timeout") {
|
|
996
1305
|
current.continuations += 1;
|
|
997
|
-
if (
|
|
998
|
-
return
|
|
1306
|
+
if (Date.now() >= Date.parse(plan.deadline))
|
|
1307
|
+
return cancelAndDrain("batch deadline elapsed");
|
|
1308
|
+
const transitionKey = `continuation:${current.continuations}`;
|
|
1309
|
+
await appendTransition({
|
|
1310
|
+
sessionId: plan.sessionId,
|
|
1311
|
+
by,
|
|
1312
|
+
batchId: plan.batchId,
|
|
1313
|
+
entity: "attempt",
|
|
1314
|
+
id: current.context.attemptId,
|
|
1315
|
+
state: "TIMEOUT_YIELDED",
|
|
1316
|
+
states: attemptStates,
|
|
1317
|
+
assignmentId: current.assignment.assignmentId,
|
|
1318
|
+
attemptId: current.context.attemptId,
|
|
1319
|
+
teammateId: current.context.teammateId,
|
|
1320
|
+
retryCount: current.retryCount,
|
|
1321
|
+
...(current.providerSessionId
|
|
1322
|
+
? { providerSessionId: current.providerSessionId }
|
|
1323
|
+
: {}),
|
|
1324
|
+
transitionKey,
|
|
1325
|
+
});
|
|
1326
|
+
await appendTransition({
|
|
1327
|
+
sessionId: plan.sessionId,
|
|
1328
|
+
by,
|
|
1329
|
+
batchId: plan.batchId,
|
|
1330
|
+
entity: "teammate",
|
|
1331
|
+
id: current.context.teammateId,
|
|
1332
|
+
state: "TIMEOUT_YIELDED",
|
|
1333
|
+
states: teammateStates,
|
|
1334
|
+
attemptId: current.context.attemptId,
|
|
1335
|
+
teammateId: current.context.teammateId,
|
|
1336
|
+
...(current.providerSessionId
|
|
1337
|
+
? { providerSessionId: current.providerSessionId }
|
|
1338
|
+
: {}),
|
|
1339
|
+
transitionKey,
|
|
1340
|
+
});
|
|
1341
|
+
await appendTransition({
|
|
1342
|
+
sessionId: plan.sessionId,
|
|
1343
|
+
by,
|
|
1344
|
+
batchId: plan.batchId,
|
|
1345
|
+
entity: "assignment",
|
|
1346
|
+
id: current.assignment.assignmentId,
|
|
1347
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1348
|
+
states: assignmentStates,
|
|
1349
|
+
assignmentId: current.assignment.assignmentId,
|
|
1350
|
+
attemptId: current.context.attemptId,
|
|
1351
|
+
teammateId: current.context.teammateId,
|
|
1352
|
+
retryCount: current.retryCount,
|
|
1353
|
+
...(current.providerSessionId
|
|
1354
|
+
? { providerSessionId: current.providerSessionId }
|
|
1355
|
+
: {}),
|
|
1356
|
+
transitionKey,
|
|
1357
|
+
});
|
|
1358
|
+
await appendTransition({
|
|
1359
|
+
sessionId: plan.sessionId,
|
|
1360
|
+
by,
|
|
1361
|
+
batchId: plan.batchId,
|
|
1362
|
+
entity: "attempt",
|
|
1363
|
+
id: current.context.attemptId,
|
|
1364
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1365
|
+
states: attemptStates,
|
|
1366
|
+
assignmentId: current.assignment.assignmentId,
|
|
1367
|
+
attemptId: current.context.attemptId,
|
|
1368
|
+
teammateId: current.context.teammateId,
|
|
1369
|
+
retryCount: current.retryCount,
|
|
1370
|
+
...(current.providerSessionId
|
|
1371
|
+
? { providerSessionId: current.providerSessionId }
|
|
1372
|
+
: {}),
|
|
1373
|
+
transitionKey,
|
|
1374
|
+
});
|
|
1375
|
+
await appendTransition({
|
|
1376
|
+
sessionId: plan.sessionId,
|
|
1377
|
+
by,
|
|
1378
|
+
batchId: plan.batchId,
|
|
1379
|
+
entity: "teammate",
|
|
1380
|
+
id: current.context.teammateId,
|
|
1381
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1382
|
+
states: teammateStates,
|
|
1383
|
+
attemptId: current.context.attemptId,
|
|
1384
|
+
teammateId: current.context.teammateId,
|
|
1385
|
+
...(current.providerSessionId
|
|
1386
|
+
? { providerSessionId: current.providerSessionId }
|
|
1387
|
+
: {}),
|
|
1388
|
+
transitionKey,
|
|
1389
|
+
});
|
|
1390
|
+
let continued;
|
|
1391
|
+
try {
|
|
1392
|
+
continued = input.runtime.continue(current.context);
|
|
999
1393
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
sessionId: plan.sessionId,
|
|
1003
|
-
by,
|
|
1004
|
-
batchId: plan.batchId,
|
|
1005
|
-
entity: "attempt",
|
|
1006
|
-
id: current.context.attemptId,
|
|
1007
|
-
state: "TIMEOUT_YIELDED",
|
|
1008
|
-
states: attemptStates,
|
|
1009
|
-
assignmentId: current.assignment.assignmentId,
|
|
1010
|
-
attemptId: current.context.attemptId,
|
|
1011
|
-
teammateId: current.context.teammateId,
|
|
1012
|
-
retryCount: current.retryCount,
|
|
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: "teammate",
|
|
1023
|
-
id: current.context.teammateId,
|
|
1024
|
-
state: "TIMEOUT_YIELDED",
|
|
1025
|
-
states: teammateStates,
|
|
1026
|
-
attemptId: current.context.attemptId,
|
|
1027
|
-
teammateId: current.context.teammateId,
|
|
1028
|
-
...(current.providerSessionId
|
|
1029
|
-
? { providerSessionId: current.providerSessionId }
|
|
1030
|
-
: {}),
|
|
1031
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1032
|
-
});
|
|
1033
|
-
await appendTransition({
|
|
1034
|
-
sessionId: plan.sessionId,
|
|
1035
|
-
by,
|
|
1036
|
-
batchId: plan.batchId,
|
|
1037
|
-
entity: "assignment",
|
|
1038
|
-
id: current.assignment.assignmentId,
|
|
1039
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1040
|
-
states: assignmentStates,
|
|
1041
|
-
assignmentId: current.assignment.assignmentId,
|
|
1042
|
-
attemptId: current.context.attemptId,
|
|
1043
|
-
teammateId: current.context.teammateId,
|
|
1044
|
-
retryCount: current.retryCount,
|
|
1045
|
-
...(current.providerSessionId
|
|
1046
|
-
? { providerSessionId: current.providerSessionId }
|
|
1047
|
-
: {}),
|
|
1048
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1049
|
-
});
|
|
1050
|
-
await appendTransition({
|
|
1051
|
-
sessionId: plan.sessionId,
|
|
1052
|
-
by,
|
|
1053
|
-
batchId: plan.batchId,
|
|
1054
|
-
entity: "attempt",
|
|
1055
|
-
id: current.context.attemptId,
|
|
1056
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1057
|
-
states: attemptStates,
|
|
1058
|
-
assignmentId: current.assignment.assignmentId,
|
|
1059
|
-
attemptId: current.context.attemptId,
|
|
1060
|
-
teammateId: current.context.teammateId,
|
|
1061
|
-
retryCount: current.retryCount,
|
|
1062
|
-
...(current.providerSessionId
|
|
1063
|
-
? { providerSessionId: current.providerSessionId }
|
|
1064
|
-
: {}),
|
|
1065
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1066
|
-
});
|
|
1067
|
-
await appendTransition({
|
|
1068
|
-
sessionId: plan.sessionId,
|
|
1069
|
-
by,
|
|
1070
|
-
batchId: plan.batchId,
|
|
1071
|
-
entity: "teammate",
|
|
1072
|
-
id: current.context.teammateId,
|
|
1073
|
-
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1074
|
-
states: teammateStates,
|
|
1075
|
-
attemptId: current.context.attemptId,
|
|
1076
|
-
teammateId: current.context.teammateId,
|
|
1077
|
-
...(current.providerSessionId
|
|
1078
|
-
? { providerSessionId: current.providerSessionId }
|
|
1079
|
-
: {}),
|
|
1080
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1081
|
-
});
|
|
1082
|
-
let continued;
|
|
1083
|
-
try {
|
|
1084
|
-
continued = await input.runtime.continue(current.context);
|
|
1085
|
-
}
|
|
1086
|
-
catch (error) {
|
|
1087
|
-
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1088
|
-
}
|
|
1089
|
-
if (continued.providerSessionId &&
|
|
1090
|
-
current.providerSessionId &&
|
|
1091
|
-
continued.providerSessionId !== current.providerSessionId) {
|
|
1092
|
-
return reconcile("continuation returned a different host session");
|
|
1093
|
-
}
|
|
1094
|
-
if (continued.providerSessionId) {
|
|
1095
|
-
current.providerSessionId = continued.providerSessionId;
|
|
1096
|
-
current.context.providerSessionId = continued.providerSessionId;
|
|
1097
|
-
}
|
|
1098
|
-
await appendTransition({
|
|
1099
|
-
sessionId: plan.sessionId,
|
|
1100
|
-
by,
|
|
1101
|
-
batchId: plan.batchId,
|
|
1102
|
-
entity: "attempt",
|
|
1103
|
-
id: current.context.attemptId,
|
|
1104
|
-
state: "RUNNING",
|
|
1105
|
-
states: attemptStates,
|
|
1106
|
-
assignmentId: current.assignment.assignmentId,
|
|
1107
|
-
attemptId: current.context.attemptId,
|
|
1108
|
-
teammateId: current.context.teammateId,
|
|
1109
|
-
retryCount: current.retryCount,
|
|
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: "teammate",
|
|
1120
|
-
id: current.context.teammateId,
|
|
1121
|
-
state: "RUNNING",
|
|
1122
|
-
states: teammateStates,
|
|
1123
|
-
attemptId: current.context.attemptId,
|
|
1124
|
-
teammateId: current.context.teammateId,
|
|
1125
|
-
...(current.providerSessionId
|
|
1126
|
-
? { providerSessionId: current.providerSessionId }
|
|
1127
|
-
: {}),
|
|
1128
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1129
|
-
});
|
|
1130
|
-
await appendTransition({
|
|
1131
|
-
sessionId: plan.sessionId,
|
|
1132
|
-
by,
|
|
1133
|
-
batchId: plan.batchId,
|
|
1134
|
-
entity: "assignment",
|
|
1135
|
-
id: current.assignment.assignmentId,
|
|
1136
|
-
state: "EXECUTING",
|
|
1137
|
-
states: assignmentStates,
|
|
1138
|
-
assignmentId: current.assignment.assignmentId,
|
|
1139
|
-
attemptId: current.context.attemptId,
|
|
1140
|
-
teammateId: current.context.teammateId,
|
|
1141
|
-
retryCount: current.retryCount,
|
|
1142
|
-
...(current.providerSessionId
|
|
1143
|
-
? { providerSessionId: current.providerSessionId }
|
|
1144
|
-
: {}),
|
|
1145
|
-
transitionKey: `continuation:${current.continuations}`,
|
|
1146
|
-
});
|
|
1147
|
-
waits.set(next.slot, Promise.resolve({ slot: next.slot, result: continued }));
|
|
1148
|
-
releaseSlot = false;
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1149
1396
|
}
|
|
1397
|
+
waits.set(next.slot, Promise.resolve(continued)
|
|
1398
|
+
.then((result) => ({ slot: next.slot, result }))
|
|
1399
|
+
.catch((error) => ({
|
|
1400
|
+
slot: next.slot,
|
|
1401
|
+
result: {
|
|
1402
|
+
kind: "reconciliation",
|
|
1403
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1404
|
+
},
|
|
1405
|
+
})));
|
|
1406
|
+
releaseSlot = false;
|
|
1150
1407
|
}
|
|
1151
1408
|
else if (next.result.kind === "report") {
|
|
1152
1409
|
try {
|
|
@@ -1177,22 +1434,248 @@ export async function runTeamworkCoordinator(input) {
|
|
|
1177
1434
|
}
|
|
1178
1435
|
return settle();
|
|
1179
1436
|
}
|
|
1180
|
-
export async function assertTeamworkReceiptDurable(
|
|
1437
|
+
export async function assertTeamworkReceiptDurable(plan, receipt, cwd) {
|
|
1181
1438
|
const [state, events] = await Promise.all([
|
|
1182
|
-
readCoordinationState({
|
|
1183
|
-
|
|
1439
|
+
readCoordinationState({
|
|
1440
|
+
sessionId: plan.sessionId,
|
|
1441
|
+
...(cwd ? { cwd } : {}),
|
|
1442
|
+
}),
|
|
1443
|
+
readTeamworkEvents({
|
|
1444
|
+
sessionId: plan.sessionId,
|
|
1445
|
+
...(cwd ? { cwd } : {}),
|
|
1446
|
+
}),
|
|
1184
1447
|
]);
|
|
1185
|
-
const
|
|
1186
|
-
if (
|
|
1187
|
-
(batch.
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const receiptRecorded = events.some((event) => event.kind === "coordination" &&
|
|
1191
|
-
event.batchId === batchId &&
|
|
1448
|
+
const expected = terminalReceiptFromProjection(plan, state);
|
|
1449
|
+
if (!isDeepStrictEqual(receipt, expected))
|
|
1450
|
+
throw new Error(`batch '${plan.batchId}' receipt disagrees with durable facts`);
|
|
1451
|
+
const receipts = events.filter((event) => event.kind === "coordination" &&
|
|
1452
|
+
event.batchId === plan.batchId &&
|
|
1192
1453
|
event.action === "receipt_recorded" &&
|
|
1193
|
-
event.payload?.status
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1454
|
+
event.payload?.status !== "RECONCILIATION_REQUIRED");
|
|
1455
|
+
if (receipts.length !== 1 ||
|
|
1456
|
+
!isDeepStrictEqual(receipts[0]?.payload, expected))
|
|
1457
|
+
throw new Error(`batch '${plan.batchId}' has no matching durable receipt event`);
|
|
1458
|
+
}
|
|
1459
|
+
/** Retire uncertain work only from associated terminal or process-birth evidence. */
|
|
1460
|
+
export async function reconcileTeamworkBatch(input) {
|
|
1461
|
+
const { plan, cwd } = input;
|
|
1462
|
+
const projection = await readCoordinationState({
|
|
1463
|
+
sessionId: plan.sessionId,
|
|
1464
|
+
cwd,
|
|
1465
|
+
});
|
|
1466
|
+
const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
|
|
1467
|
+
if (batch?.planHash !== plan.planHash)
|
|
1468
|
+
throw new Error("Batch does not match the frozen plan");
|
|
1469
|
+
const events = await readTeamworkEvents({ sessionId: plan.sessionId, cwd });
|
|
1470
|
+
const resolving = events.some((event) => event.kind === "coordination" &&
|
|
1471
|
+
event.batchId === plan.batchId &&
|
|
1472
|
+
event.action === "reconciliation_resolved");
|
|
1473
|
+
if (["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state)) {
|
|
1474
|
+
const receipt = terminalReceiptFromProjection(plan, projection);
|
|
1475
|
+
if (input.execute) {
|
|
1476
|
+
const recovered = await recoverExistingBatch({
|
|
1477
|
+
plan,
|
|
1478
|
+
cwd,
|
|
1479
|
+
by: "main",
|
|
1480
|
+
capacity: receipt.capacity,
|
|
1481
|
+
});
|
|
1482
|
+
if (!recovered)
|
|
1483
|
+
throw new Error("Settled batch disappeared during reconciliation");
|
|
1484
|
+
return recovered;
|
|
1485
|
+
}
|
|
1486
|
+
return receipt;
|
|
1487
|
+
}
|
|
1488
|
+
if (batch.state !== "RECONCILIATION_REQUIRED" &&
|
|
1489
|
+
!(resolving && batch.state === "SETTLING"))
|
|
1490
|
+
throw new Error("Recover the original batch into RECONCILIATION_REQUIRED first");
|
|
1491
|
+
const attempts = projection.attempts.filter((entry) => entry.batchId === plan.batchId);
|
|
1492
|
+
const blockers = projection.issues.filter((issue) => !issue.includes(": reconciliation required"));
|
|
1493
|
+
for (const attempt of attempts) {
|
|
1494
|
+
if (attempt.terminal)
|
|
1495
|
+
continue;
|
|
1496
|
+
const facts = events.filter((event) => event.kind === "coordination" &&
|
|
1497
|
+
event.batchId === plan.batchId &&
|
|
1498
|
+
event.attemptId === attempt.attemptId &&
|
|
1499
|
+
event.planHash === plan.planHash);
|
|
1500
|
+
if (facts.some((event) => event.kind === "coordination" && event.action === "process_exited"))
|
|
1501
|
+
continue;
|
|
1502
|
+
const started = facts
|
|
1503
|
+
.filter((event) => event.kind === "coordination" && event.action === "process_started")
|
|
1504
|
+
.pop();
|
|
1505
|
+
const pid = started?.kind === "coordination" ? started.payload?.pid : undefined;
|
|
1506
|
+
const birth = started?.kind === "coordination"
|
|
1507
|
+
? started.payload?.processIdentity
|
|
1508
|
+
: undefined;
|
|
1509
|
+
let exited = false;
|
|
1510
|
+
if (typeof pid === "number" &&
|
|
1511
|
+
Number.isSafeInteger(pid) &&
|
|
1512
|
+
pid > 0 &&
|
|
1513
|
+
typeof birth === "string" &&
|
|
1514
|
+
birth.startsWith(`${pid}:`)) {
|
|
1515
|
+
const current = readProcessIdentity(pid);
|
|
1516
|
+
if (current)
|
|
1517
|
+
exited = current !== birth;
|
|
1518
|
+
else {
|
|
1519
|
+
try {
|
|
1520
|
+
process.kill(pid, 0);
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
exited = error.code === "ESRCH";
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (!exited)
|
|
1528
|
+
blockers.push(`Attempt ${attempt.attemptId} lacks confirmed process exit; preserve its lease`);
|
|
1196
1529
|
}
|
|
1530
|
+
if (!input.execute)
|
|
1531
|
+
return { status: blockers.length ? "blocked" : "ready", blockers };
|
|
1532
|
+
if (blockers.length)
|
|
1533
|
+
throw new Error(blockers.join("; "));
|
|
1534
|
+
if (!input.reason?.trim())
|
|
1535
|
+
throw new Error("Reconciliation requires an explicit disposition reason");
|
|
1536
|
+
const by = "main";
|
|
1537
|
+
for (const attempt of attempts) {
|
|
1538
|
+
await recordCoordinationEvent({
|
|
1539
|
+
sessionId: plan.sessionId,
|
|
1540
|
+
cwd,
|
|
1541
|
+
by,
|
|
1542
|
+
batchId: plan.batchId,
|
|
1543
|
+
entity: "attempt",
|
|
1544
|
+
action: "terminal_observed",
|
|
1545
|
+
attemptId: attempt.attemptId,
|
|
1546
|
+
teammateId: attempt.teammateId,
|
|
1547
|
+
terminal: true,
|
|
1548
|
+
reason: input.reason,
|
|
1549
|
+
idempotencyKey: `${plan.batchId}:reconciled-terminal:${attempt.attemptId}`,
|
|
1550
|
+
});
|
|
1551
|
+
if (!["FINAL_FAILED", "REPORT_SUBMITTED"].includes(attempt.state))
|
|
1552
|
+
await appendTransition({
|
|
1553
|
+
sessionId: plan.sessionId,
|
|
1554
|
+
cwd,
|
|
1555
|
+
by,
|
|
1556
|
+
batchId: plan.batchId,
|
|
1557
|
+
entity: "attempt",
|
|
1558
|
+
id: attempt.attemptId,
|
|
1559
|
+
attemptId: attempt.attemptId,
|
|
1560
|
+
assignmentId: attempt.assignmentId,
|
|
1561
|
+
teammateId: attempt.teammateId,
|
|
1562
|
+
retryCount: attempt.retryCount,
|
|
1563
|
+
state: "FINAL_FAILED",
|
|
1564
|
+
terminal: true,
|
|
1565
|
+
states: new Map([[attempt.attemptId, attempt.state]]),
|
|
1566
|
+
transitionKey: "reconcile",
|
|
1567
|
+
});
|
|
1568
|
+
const teammate = projection.teammates.find((entry) => entry.batchId === plan.batchId &&
|
|
1569
|
+
entry.teammateId === attempt.teammateId);
|
|
1570
|
+
if (teammate && !["FAILED", "REPORT_SUBMITTED"].includes(teammate.state))
|
|
1571
|
+
await appendTransition({
|
|
1572
|
+
sessionId: plan.sessionId,
|
|
1573
|
+
cwd,
|
|
1574
|
+
by,
|
|
1575
|
+
batchId: plan.batchId,
|
|
1576
|
+
entity: "teammate",
|
|
1577
|
+
id: teammate.teammateId,
|
|
1578
|
+
teammateId: teammate.teammateId,
|
|
1579
|
+
attemptId: attempt.attemptId,
|
|
1580
|
+
state: "FAILED",
|
|
1581
|
+
terminal: true,
|
|
1582
|
+
states: new Map([[teammate.teammateId, teammate.state]]),
|
|
1583
|
+
transitionKey: "reconcile",
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
for (const assignment of projection.assignments.filter((entry) => entry.batchId === plan.batchId)) {
|
|
1587
|
+
if (!["ACCEPTED", "FINAL_FAILED"].includes(assignment.state))
|
|
1588
|
+
await appendTransition({
|
|
1589
|
+
sessionId: plan.sessionId,
|
|
1590
|
+
cwd,
|
|
1591
|
+
by,
|
|
1592
|
+
batchId: plan.batchId,
|
|
1593
|
+
entity: "assignment",
|
|
1594
|
+
id: assignment.assignmentId,
|
|
1595
|
+
assignmentId: assignment.assignmentId,
|
|
1596
|
+
attemptId: assignment.attemptId,
|
|
1597
|
+
teammateId: assignment.teammateId,
|
|
1598
|
+
retryCount: assignment.retryCount,
|
|
1599
|
+
state: "FINAL_FAILED",
|
|
1600
|
+
states: new Map([[assignment.assignmentId, assignment.state]]),
|
|
1601
|
+
transitionKey: "reconcile",
|
|
1602
|
+
payload: { reason: input.reason },
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
for (const lease of projection.leases.filter((entry) => entry.batchId === plan.batchId))
|
|
1606
|
+
await recordCoordinationEvent({
|
|
1607
|
+
sessionId: plan.sessionId,
|
|
1608
|
+
cwd,
|
|
1609
|
+
by,
|
|
1610
|
+
batchId: plan.batchId,
|
|
1611
|
+
entity: "attempt",
|
|
1612
|
+
action: "lease_released",
|
|
1613
|
+
leaseId: lease.leaseId,
|
|
1614
|
+
protected: false,
|
|
1615
|
+
reason: input.reason,
|
|
1616
|
+
idempotencyKey: `${plan.batchId}:reconciled-release:${lease.leaseId}`,
|
|
1617
|
+
});
|
|
1618
|
+
const accepted = projection.assignments
|
|
1619
|
+
.filter((entry) => entry.batchId === plan.batchId)
|
|
1620
|
+
.every((entry) => entry.state === "ACCEPTED");
|
|
1621
|
+
const states = new Map([
|
|
1622
|
+
[plan.batchId, batch.state],
|
|
1623
|
+
]);
|
|
1624
|
+
await recordCoordinationEvent({
|
|
1625
|
+
sessionId: plan.sessionId,
|
|
1626
|
+
cwd,
|
|
1627
|
+
by,
|
|
1628
|
+
batchId: plan.batchId,
|
|
1629
|
+
entity: "batch",
|
|
1630
|
+
action: "reconciliation_resolved",
|
|
1631
|
+
reason: input.reason,
|
|
1632
|
+
idempotencyKey: `${plan.batchId}:reconciled`,
|
|
1633
|
+
});
|
|
1634
|
+
if (batch.state !== "SETTLING")
|
|
1635
|
+
await appendTransition({
|
|
1636
|
+
sessionId: plan.sessionId,
|
|
1637
|
+
cwd,
|
|
1638
|
+
by,
|
|
1639
|
+
batchId: plan.batchId,
|
|
1640
|
+
entity: "batch",
|
|
1641
|
+
id: plan.batchId,
|
|
1642
|
+
state: "SETTLING",
|
|
1643
|
+
states,
|
|
1644
|
+
transitionKey: "reconcile",
|
|
1645
|
+
});
|
|
1646
|
+
await appendTransition({
|
|
1647
|
+
sessionId: plan.sessionId,
|
|
1648
|
+
cwd,
|
|
1649
|
+
by,
|
|
1650
|
+
batchId: plan.batchId,
|
|
1651
|
+
entity: "batch",
|
|
1652
|
+
id: plan.batchId,
|
|
1653
|
+
state: accepted ? "ALL_ACCEPTED" : "SETTLED_WITH_FAILURE",
|
|
1654
|
+
states,
|
|
1655
|
+
transitionKey: "reconcile",
|
|
1656
|
+
});
|
|
1657
|
+
const receipt = terminalReceiptFromProjection(plan, await readCoordinationState({ sessionId: plan.sessionId, cwd }));
|
|
1658
|
+
await appendAction({
|
|
1659
|
+
sessionId: plan.sessionId,
|
|
1660
|
+
cwd,
|
|
1661
|
+
by,
|
|
1662
|
+
batchId: plan.batchId,
|
|
1663
|
+
entity: "batch",
|
|
1664
|
+
action: "receipt_recorded",
|
|
1665
|
+
id: `${plan.batchId}:${receipt.status}`,
|
|
1666
|
+
payload: { ...receipt },
|
|
1667
|
+
});
|
|
1668
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
1669
|
+
return receipt;
|
|
1670
|
+
}
|
|
1671
|
+
/** Read settled facts without admitting work or consulting current resource policy. */
|
|
1672
|
+
export async function readSettledTeamworkReceipt(plan, cwd) {
|
|
1673
|
+
const state = await readCoordinationState({ sessionId: plan.sessionId, cwd });
|
|
1674
|
+
const batch = state.batches.find((entry) => entry.batchId === plan.batchId);
|
|
1675
|
+
if (!batch || !["ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
|
|
1676
|
+
return undefined;
|
|
1677
|
+
const receipt = terminalReceiptFromProjection(plan, state);
|
|
1678
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
1679
|
+
return receipt;
|
|
1197
1680
|
}
|
|
1198
1681
|
//# sourceMappingURL=coordinator.js.map
|