@pennixrv/trellis 0.7.1 → 0.7.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/ablate.d.ts.map +1 -1
- package/dist/commands/ablate.js +2 -0
- package/dist/commands/ablate.js.map +1 -1
- package/dist/commands/backup.d.ts +4 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +75 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +705 -681
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +49 -7
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/task-progress.d.ts +1 -0
- package/dist/commands/task-progress.d.ts.map +1 -1
- package/dist/commands/task-progress.js +67 -25
- package/dist/commands/task-progress.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 +42 -14
- package/dist/commands/teamwork/artifact.d.ts.map +1 -1
- package/dist/commands/teamwork/artifact.js +188 -85
- 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 +23 -0
- package/dist/commands/teamwork/batch.d.ts.map +1 -0
- package/dist/commands/teamwork/batch.js +415 -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 +33 -6
- package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
- package/dist/commands/teamwork/coordinator.js +948 -342
- 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 +25 -7
- 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 +163 -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 +55 -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 +401 -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 +42 -27
- 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 -5
- package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-adapters.js +9 -56
- 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 +15 -6
- package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
- package/dist/commands/teamwork/teammate-launch.js +129 -22
- 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/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +92 -84
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +635 -592
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +94 -89
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/shared.js +2 -2
- package/dist/configurators/shared.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/constants/paths.d.ts +3 -10
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +4 -13
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.7.1-beta.0.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.1.json +186 -0
- 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.2.json +522 -0
- package/dist/migrations/manifests/0.7.1-beta.3.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.4.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.5.json +9 -0
- package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
- package/dist/migrations/manifests/0.7.1-beta.7.json +8 -0
- package/dist/migrations/manifests/0.7.1-beta.8.json +481 -0
- package/dist/migrations/manifests/0.7.1-beta.9.json +191 -0
- package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
- package/dist/migrations/manifests/0.7.2-beta.1.json +11 -0
- package/dist/migrations/manifests/0.7.2-beta.2.json +11 -0
- package/dist/templates/claude/hooks/statusline.py +9 -4
- 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-decision-space-construction-and-merging/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +2 -0
- package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
- 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 +8 -2
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +2 -1
- package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +8 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +4 -2
- package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +2 -2
- package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
- package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +18 -3
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +34 -3
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -26
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +128 -14
- package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +152 -20
- package/dist/templates/common/commands/start.md +2 -2
- package/dist/templates/common/skills/before-dev.md +1 -4
- package/dist/templates/common/skills/break-loop.md +7 -18
- package/dist/templates/common/skills/check.md +7 -11
- package/dist/templates/common/skills/{brainstorm.md → request-intake.md} +5 -3
- package/dist/templates/common/skills/update-spec.md +5 -7
- 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/extract.js +4 -0
- package/dist/templates/extract.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 +2 -2
- package/dist/templates/opencode/lib/session-utils.js +301 -209
- package/dist/templates/pi/extensions/trellis/index.ts.txt +17 -1
- package/dist/templates/shared-hooks/guard-teamwork-wait.py +58 -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/snow/SNOW.md +1 -1
- 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 -52
- package/dist/templates/trellis/config.yaml +2 -5
- package/dist/templates/trellis/index.d.ts +4 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +22 -4
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +1 -1
- package/dist/templates/trellis/scripts/common/active_task.py +3 -47
- package/dist/templates/trellis/scripts/common/config.py +5 -0
- package/dist/templates/trellis/scripts/common/paths.py +9 -25
- package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
- package/dist/templates/trellis/scripts/common/task_context.py +11 -51
- package/dist/templates/trellis/scripts/common/task_continuity.py +191 -60
- package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
- package/dist/templates/trellis/scripts/common/task_move.py +345 -0
- package/dist/templates/trellis/scripts/common/task_store.py +51 -57
- package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
- package/dist/templates/trellis/scripts/common/task_workflow.py +395 -49
- package/dist/templates/trellis/scripts/common/tasks.py +6 -5
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
- package/dist/templates/trellis/scripts/task.py +76 -24
- package/dist/templates/trellis/teamwork.json +10 -0
- package/dist/templates/trellis/workflow.md +11 -2
- package/dist/utils/ablation-store.d.ts +1 -1
- package/dist/utils/ablation-store.d.ts.map +1 -1
- package/dist/utils/ablation-store.js +58 -24
- package/dist/utils/ablation-store.js.map +1 -1
- package/dist/utils/asset-backup.d.ts +138 -0
- package/dist/utils/asset-backup.d.ts.map +1 -0
- package/dist/utils/asset-backup.js +525 -0
- package/dist/utils/asset-backup.js.map +1 -0
- package/dist/utils/atomic-write.d.ts +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/atomic-write.js +6 -3
- package/dist/utils/atomic-write.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts +4 -1
- package/dist/utils/manifest-prune.d.ts.map +1 -1
- package/dist/utils/manifest-prune.js +17 -6
- package/dist/utils/manifest-prune.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/dist/utils/template-hash.d.ts +5 -4
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +24 -17
- package/dist/utils/template-hash.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,16 @@
|
|
|
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 {
|
|
6
|
-
import { assertTeammateAdapterContract, } from "./teammate-adapters.js";
|
|
7
|
+
import { TeammateReportReadError, 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 {
|
|
10
|
+
import { readProcessIdentity } from "./process-identity.js";
|
|
11
|
+
import { withLock } from "./store/lock.js";
|
|
12
|
+
export class TeamworkPolicyChangedError extends Error {
|
|
13
|
+
}
|
|
10
14
|
export function assertTeamworkPlanCurrent(plan, taskRoot) {
|
|
11
15
|
const taskFile = path.join(taskRoot, "task.json");
|
|
12
16
|
const stat = fs.lstatSync(taskFile);
|
|
@@ -37,6 +41,169 @@ export function assertTeamworkPlanCurrent(plan, taskRoot) {
|
|
|
37
41
|
throw new Error("Teamwork plan does not match the current active task phase and revisions");
|
|
38
42
|
}
|
|
39
43
|
}
|
|
44
|
+
function terminalReceiptFromProjection(plan, projection) {
|
|
45
|
+
const mismatch = (reason) => {
|
|
46
|
+
throw new Error(`batch '${plan.batchId}' ${reason}`);
|
|
47
|
+
};
|
|
48
|
+
if (projection.issues.length > 0)
|
|
49
|
+
mismatch("has coordination projection issues");
|
|
50
|
+
const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
|
|
51
|
+
if (!batch)
|
|
52
|
+
return mismatch("does not match the frozen plan");
|
|
53
|
+
if (batch.planHash !== plan.planHash)
|
|
54
|
+
return mismatch("does not match the frozen plan");
|
|
55
|
+
if (!["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
|
|
56
|
+
mismatch("is not terminal");
|
|
57
|
+
const effectiveCapacity = batch.effectiveCapacity;
|
|
58
|
+
if (typeof effectiveCapacity !== "number" ||
|
|
59
|
+
!Number.isSafeInteger(effectiveCapacity) ||
|
|
60
|
+
effectiveCapacity < 1)
|
|
61
|
+
return mismatch("has no valid effective capacity");
|
|
62
|
+
if (projection.leases.some((lease) => lease.batchId === plan.batchId))
|
|
63
|
+
mismatch("still owns capacity leases");
|
|
64
|
+
const projectedAssignments = projection.assignments.filter((assignment) => assignment.batchId === plan.batchId);
|
|
65
|
+
if (projectedAssignments.length !== plan.assignments.length ||
|
|
66
|
+
new Set(projectedAssignments.map((assignment) => assignment.assignmentId))
|
|
67
|
+
.size !== plan.assignments.length)
|
|
68
|
+
mismatch("does not contain the exact frozen assignment set");
|
|
69
|
+
const assignmentById = new Map(projectedAssignments.map((assignment) => [
|
|
70
|
+
assignment.assignmentId,
|
|
71
|
+
assignment,
|
|
72
|
+
]));
|
|
73
|
+
const planAssignmentIds = new Set(plan.assignments.map((assignment) => assignment.assignmentId));
|
|
74
|
+
const attempts = projection.attempts.filter((attempt) => attempt.batchId === plan.batchId);
|
|
75
|
+
const teammates = projection.teammates.filter((teammate) => teammate.batchId === plan.batchId);
|
|
76
|
+
if (attempts.length !== teammates.length)
|
|
77
|
+
mismatch("has unmatched provider attempts and teammates");
|
|
78
|
+
for (const attempt of attempts) {
|
|
79
|
+
if (!attempt.assignmentId ||
|
|
80
|
+
!planAssignmentIds.has(attempt.assignmentId) ||
|
|
81
|
+
!attempt.teammateId ||
|
|
82
|
+
attempt.terminal !== true ||
|
|
83
|
+
!["REPORT_SUBMITTED", "FAILED", "FINAL_FAILED"].includes(attempt.state))
|
|
84
|
+
mismatch("has an unresolved provider attempt");
|
|
85
|
+
const teammate = teammates.find((entry) => entry.attemptId === attempt.attemptId &&
|
|
86
|
+
entry.teammateId === attempt.teammateId);
|
|
87
|
+
if (teammate?.terminal !== true ||
|
|
88
|
+
(teammate?.state !== "REPORT_SUBMITTED" && teammate?.state !== "FAILED"))
|
|
89
|
+
mismatch("has an unresolved teammate terminal fact");
|
|
90
|
+
}
|
|
91
|
+
const assignments = plan.assignments.map((assignment) => {
|
|
92
|
+
const projected = assignmentById.get(assignment.assignmentId);
|
|
93
|
+
if (!projected ||
|
|
94
|
+
!["AWAITING_REVIEW", "ACCEPTED", "FINAL_FAILED"].includes(projected.state))
|
|
95
|
+
return mismatch(`assignment '${assignment.assignmentId}' is not settled`);
|
|
96
|
+
if (projected.state === "ACCEPTED" ||
|
|
97
|
+
projected.state === "AWAITING_REVIEW") {
|
|
98
|
+
const acceptedAttempt = attempts.find((attempt) => attempt.attemptId === projected.attemptId &&
|
|
99
|
+
attempt.assignmentId === assignment.assignmentId);
|
|
100
|
+
if (acceptedAttempt?.state !== "REPORT_SUBMITTED")
|
|
101
|
+
return mismatch(`assignment '${assignment.assignmentId}' has no accepted attempt`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
assignmentId: assignment.assignmentId,
|
|
105
|
+
status: projected.state,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
const status = assignments.some((item) => item.status === "AWAITING_REVIEW")
|
|
109
|
+
? "AWAITING_REVIEW"
|
|
110
|
+
: assignments.every((item) => item.status === "ACCEPTED")
|
|
111
|
+
? "ALL_ACCEPTED"
|
|
112
|
+
: "SETTLED_WITH_FAILURE";
|
|
113
|
+
if (batch.state !== status)
|
|
114
|
+
mismatch("terminal state disagrees with its assignment dispositions");
|
|
115
|
+
const retryCounts = Object.fromEntries(plan.assignments.flatMap((assignment) => {
|
|
116
|
+
const count = assignmentById.get(assignment.assignmentId)?.retryCount;
|
|
117
|
+
if (count === undefined)
|
|
118
|
+
return mismatch(`assignment '${assignment.assignmentId}' is missing`);
|
|
119
|
+
const started = attempts.some((attempt) => attempt.assignmentId === assignment.assignmentId);
|
|
120
|
+
return started || count > 0 ? [[assignment.assignmentId, count]] : [];
|
|
121
|
+
}));
|
|
122
|
+
const evaluationGroups = [
|
|
123
|
+
...new Set(plan.assignments
|
|
124
|
+
.map((assignment) => assignment.evaluationGroup?.id)
|
|
125
|
+
.filter((id) => id !== undefined)),
|
|
126
|
+
].map((id) => ({
|
|
127
|
+
id,
|
|
128
|
+
status: plan.assignments
|
|
129
|
+
.filter((assignment) => assignment.evaluationGroup?.id === id)
|
|
130
|
+
.every((assignment) => assignmentById.get(assignment.assignmentId)?.state === "ACCEPTED")
|
|
131
|
+
? "COMPLETE"
|
|
132
|
+
: "INCOMPLETE",
|
|
133
|
+
}));
|
|
134
|
+
return {
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
batchId: plan.batchId,
|
|
137
|
+
status,
|
|
138
|
+
assignments,
|
|
139
|
+
capacity: effectiveCapacity,
|
|
140
|
+
retryCounts,
|
|
141
|
+
evaluationGroups,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function reconciliationReceipt(plan, projection, capacity) {
|
|
145
|
+
return {
|
|
146
|
+
schemaVersion: 1,
|
|
147
|
+
batchId: plan.batchId,
|
|
148
|
+
status: "RECONCILIATION_REQUIRED",
|
|
149
|
+
assignments: plan.assignments.map((item) => {
|
|
150
|
+
const current = projection.assignments.find((entry) => entry.batchId === plan.batchId &&
|
|
151
|
+
entry.assignmentId === item.assignmentId);
|
|
152
|
+
return {
|
|
153
|
+
assignmentId: item.assignmentId,
|
|
154
|
+
status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
|
|
155
|
+
? current.state
|
|
156
|
+
: "UNKNOWN",
|
|
157
|
+
};
|
|
158
|
+
}),
|
|
159
|
+
capacity: projection.batches.find((entry) => entry.batchId === plan.batchId)
|
|
160
|
+
?.effectiveCapacity ?? capacity,
|
|
161
|
+
retryCounts: Object.fromEntries(projection.assignments
|
|
162
|
+
.filter((entry) => entry.batchId === plan.batchId)
|
|
163
|
+
.map((entry) => [entry.assignmentId, entry.retryCount])),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function recordRecoveryReconciliation(input) {
|
|
167
|
+
const { plan, cwd, by, existingState, projection, capacity, reason } = input;
|
|
168
|
+
if (existingState !== "RECONCILIATION_REQUIRED" &&
|
|
169
|
+
existingState !== "AWAITING_REVIEW" &&
|
|
170
|
+
existingState !== "ALL_ACCEPTED" &&
|
|
171
|
+
existingState !== "SETTLED_WITH_FAILURE") {
|
|
172
|
+
await appendTransition({
|
|
173
|
+
sessionId: plan.sessionId,
|
|
174
|
+
cwd,
|
|
175
|
+
by,
|
|
176
|
+
batchId: plan.batchId,
|
|
177
|
+
entity: "batch",
|
|
178
|
+
id: plan.batchId,
|
|
179
|
+
state: "RECONCILIATION_REQUIRED",
|
|
180
|
+
states: new Map([[plan.batchId, existingState]]),
|
|
181
|
+
planHash: plan.planHash,
|
|
182
|
+
payload: { recoveredAfterRestart: true },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const receipt = reconciliationReceipt(plan, projection, capacity);
|
|
186
|
+
await appendAction({
|
|
187
|
+
sessionId: plan.sessionId,
|
|
188
|
+
cwd,
|
|
189
|
+
by,
|
|
190
|
+
batchId: plan.batchId,
|
|
191
|
+
entity: "batch",
|
|
192
|
+
action: "reconciliation_required",
|
|
193
|
+
reason,
|
|
194
|
+
});
|
|
195
|
+
await appendAction({
|
|
196
|
+
sessionId: plan.sessionId,
|
|
197
|
+
cwd,
|
|
198
|
+
by,
|
|
199
|
+
batchId: plan.batchId,
|
|
200
|
+
entity: "batch",
|
|
201
|
+
action: "receipt_recorded",
|
|
202
|
+
id: `${plan.batchId}:RECONCILIATION_REQUIRED`,
|
|
203
|
+
payload: { ...receipt, reason },
|
|
204
|
+
});
|
|
205
|
+
return receipt;
|
|
206
|
+
}
|
|
40
207
|
async function recoverExistingBatch(input) {
|
|
41
208
|
const projection = await readCoordinationState({
|
|
42
209
|
sessionId: input.plan.sessionId,
|
|
@@ -52,64 +219,56 @@ async function recoverExistingBatch(input) {
|
|
|
52
219
|
sessionId: input.plan.sessionId,
|
|
53
220
|
cwd: input.cwd,
|
|
54
221
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
batchId
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
222
|
+
if (existing.state === "AWAITING_REVIEW" ||
|
|
223
|
+
existing.state === "ALL_ACCEPTED" ||
|
|
224
|
+
existing.state === "SETTLED_WITH_FAILURE") {
|
|
225
|
+
let receipt;
|
|
226
|
+
try {
|
|
227
|
+
receipt = terminalReceiptFromProjection(input.plan, projection);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
return recordRecoveryReconciliation({
|
|
231
|
+
...input,
|
|
232
|
+
existingState: existing.state,
|
|
233
|
+
projection,
|
|
234
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const receipts = events.filter((event) => event.kind === "coordination" &&
|
|
238
|
+
event.batchId === input.plan.batchId &&
|
|
239
|
+
event.action === "receipt_recorded" &&
|
|
240
|
+
event.payload?.status === receipt.status &&
|
|
241
|
+
event.payload?.status !== "RECONCILIATION_REQUIRED");
|
|
242
|
+
if (receipts.length > 0 &&
|
|
243
|
+
!isDeepStrictEqual(receipts.at(-1)?.payload, receipt)) {
|
|
244
|
+
return recordRecoveryReconciliation({
|
|
245
|
+
...input,
|
|
246
|
+
existingState: existing.state,
|
|
247
|
+
projection,
|
|
248
|
+
reason: "saved terminal receipt disagrees with durable coordination facts",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (receipts.length === 0) {
|
|
252
|
+
await appendAction({
|
|
253
|
+
sessionId: input.plan.sessionId,
|
|
254
|
+
cwd: input.cwd,
|
|
255
|
+
by: input.by,
|
|
256
|
+
batchId: input.plan.batchId,
|
|
257
|
+
entity: "batch",
|
|
258
|
+
action: "receipt_recorded",
|
|
259
|
+
id: `${input.plan.batchId}:${receipt.status}`,
|
|
260
|
+
payload: { ...receipt },
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
await assertTeamworkReceiptDurable(input.plan, receipt, input.cwd);
|
|
264
|
+
return receipt;
|
|
79
265
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
status: current?.state === "ACCEPTED" || current?.state === "FINAL_FAILED"
|
|
86
|
-
? current.state
|
|
87
|
-
: "UNKNOWN",
|
|
88
|
-
};
|
|
266
|
+
return recordRecoveryReconciliation({
|
|
267
|
+
...input,
|
|
268
|
+
existingState: existing.state,
|
|
269
|
+
projection,
|
|
270
|
+
reason: "recovered nonterminal batch requires reconciliation",
|
|
89
271
|
});
|
|
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`,
|
|
111
|
-
});
|
|
112
|
-
return receipt;
|
|
113
272
|
}
|
|
114
273
|
function transitionState(states, key, next, entity) {
|
|
115
274
|
assertCoordinationTransition(entity, states.get(key), next);
|
|
@@ -119,6 +278,7 @@ async function appendTransition(input) {
|
|
|
119
278
|
transitionState(input.states, input.id, input.state, input.entity);
|
|
120
279
|
await recordCoordinationEvent({
|
|
121
280
|
sessionId: input.sessionId,
|
|
281
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
122
282
|
by: input.by,
|
|
123
283
|
batchId: input.batchId,
|
|
124
284
|
entity: input.entity,
|
|
@@ -141,6 +301,7 @@ async function appendTransition(input) {
|
|
|
141
301
|
async function appendAction(input) {
|
|
142
302
|
await recordCoordinationEvent({
|
|
143
303
|
sessionId: input.sessionId,
|
|
304
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
144
305
|
by: input.by,
|
|
145
306
|
batchId: input.batchId,
|
|
146
307
|
entity: input.entity,
|
|
@@ -164,47 +325,60 @@ async function appendAction(input) {
|
|
|
164
325
|
idempotencyKey: `${input.batchId}:${input.action}:${input.id ?? input.leaseId ?? input.attemptId ?? "event"}`,
|
|
165
326
|
});
|
|
166
327
|
}
|
|
167
|
-
function readPlanFile(planPath) {
|
|
328
|
+
export function readPlanFile(planPath) {
|
|
329
|
+
const stat = fs.lstatSync(planPath);
|
|
330
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024)
|
|
331
|
+
throw new Error("Teamwork plan must be a regular file under 2097152 bytes");
|
|
168
332
|
const raw = fs.readFileSync(planPath, "utf8");
|
|
169
333
|
return JSON.parse(raw);
|
|
170
334
|
}
|
|
171
335
|
export function loadTeamworkPlan(planPath, taskRoot) {
|
|
172
|
-
const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot);
|
|
336
|
+
const plan = validateTeamworkPlan(readPlanFile(planPath), taskRoot, undefined, { allowExpired: true });
|
|
173
337
|
assertTeamworkPlanCurrent(plan, taskRoot);
|
|
174
338
|
return plan;
|
|
175
339
|
}
|
|
176
340
|
export async function runTeamworkCoordinator(input) {
|
|
177
|
-
const plan = validateTeamworkPlan(input.plan, input.taskRoot
|
|
341
|
+
const plan = validateTeamworkPlan(input.plan, input.taskRoot, undefined, {
|
|
342
|
+
allowExpired: true,
|
|
343
|
+
});
|
|
178
344
|
assertTeamworkPlanCurrent(plan, input.taskRoot);
|
|
179
345
|
const cwd = input.cwd ?? input.taskRoot;
|
|
180
346
|
const frozenLaunches = new Map();
|
|
181
347
|
for (const assignment of plan.assignments) {
|
|
182
348
|
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
|
-
});
|
|
349
|
+
assertTeammateHostCapability(assignment.provider, input.host, assignment.execution.profile.options);
|
|
350
|
+
frozenLaunches.set(assignment.assignmentId, assignment.execution);
|
|
200
351
|
}
|
|
201
|
-
const config = input.config ??
|
|
352
|
+
const config = input.config ??
|
|
353
|
+
resolveTeammateRuntimeConfig(cwd, process.env, plan.coordinationMode);
|
|
202
354
|
const capacity = effectiveTeammateCapacity({
|
|
203
355
|
requested: plan.requestedCapacity,
|
|
356
|
+
assignmentCount: plan.assignments.length,
|
|
204
357
|
config,
|
|
205
358
|
verifiedHostCap: input.host.verifiedHostCap,
|
|
206
359
|
});
|
|
207
360
|
const by = input.by ?? "main";
|
|
361
|
+
const existing = await readCoordinationState({
|
|
362
|
+
sessionId: plan.sessionId,
|
|
363
|
+
cwd,
|
|
364
|
+
});
|
|
365
|
+
const settled = existing.batches.find((batch) => batch.batchId === plan.batchId);
|
|
366
|
+
if (settled &&
|
|
367
|
+
["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(settled.state)) {
|
|
368
|
+
let terminal = false;
|
|
369
|
+
try {
|
|
370
|
+
terminalReceiptFromProjection(plan, existing);
|
|
371
|
+
terminal = true;
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// Inconsistent terminal facts still require the protected recovery path.
|
|
375
|
+
}
|
|
376
|
+
if (terminal) {
|
|
377
|
+
const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
|
|
378
|
+
if (recovered)
|
|
379
|
+
return recovered;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
208
382
|
const batchStates = new Map();
|
|
209
383
|
const assignmentStates = new Map();
|
|
210
384
|
const attemptStates = new Map();
|
|
@@ -215,19 +389,8 @@ export async function runTeamworkCoordinator(input) {
|
|
|
215
389
|
capacity,
|
|
216
390
|
});
|
|
217
391
|
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
|
-
}
|
|
392
|
+
// Failed recovery cannot prove that an older provider process exited.
|
|
393
|
+
const recovered = await recoverExistingBatch({ plan, cwd, by, capacity });
|
|
231
394
|
if (recovered) {
|
|
232
395
|
if (recovered.status !== "RECONCILIATION_REQUIRED") {
|
|
233
396
|
await input.guard.release({
|
|
@@ -239,6 +402,14 @@ export async function runTeamworkCoordinator(input) {
|
|
|
239
402
|
}
|
|
240
403
|
return recovered;
|
|
241
404
|
}
|
|
405
|
+
if (Date.parse(plan.deadline) <= Date.now()) {
|
|
406
|
+
await input.guard.release({
|
|
407
|
+
batchId: plan.batchId,
|
|
408
|
+
planHash: plan.planHash,
|
|
409
|
+
capacity,
|
|
410
|
+
});
|
|
411
|
+
throw new Error("deadline elapsed before batch dispatch");
|
|
412
|
+
}
|
|
242
413
|
try {
|
|
243
414
|
for (const [queueIndex, assignment] of plan.assignments.entries()) {
|
|
244
415
|
writeTeammateQueueFact({
|
|
@@ -328,8 +499,8 @@ export async function runTeamworkCoordinator(input) {
|
|
|
328
499
|
const active = new Map();
|
|
329
500
|
const waits = new Map();
|
|
330
501
|
const final = new Map();
|
|
331
|
-
const acceptedAttempts = new Map();
|
|
332
502
|
const retryCounts = {};
|
|
503
|
+
let admissionClosed = false;
|
|
333
504
|
async function reconcile(reason) {
|
|
334
505
|
await appendAction({
|
|
335
506
|
sessionId: plan.sessionId,
|
|
@@ -372,9 +543,39 @@ export async function runTeamworkCoordinator(input) {
|
|
|
372
543
|
});
|
|
373
544
|
return receipt;
|
|
374
545
|
}
|
|
546
|
+
async function failPending(reason) {
|
|
547
|
+
admissionClosed = true;
|
|
548
|
+
while (pending.length > 0) {
|
|
549
|
+
const assignment = pending.shift();
|
|
550
|
+
if (!assignment)
|
|
551
|
+
continue;
|
|
552
|
+
await appendTransition({
|
|
553
|
+
sessionId: plan.sessionId,
|
|
554
|
+
by,
|
|
555
|
+
batchId: plan.batchId,
|
|
556
|
+
entity: "assignment",
|
|
557
|
+
id: assignment.assignmentId,
|
|
558
|
+
state: "FINAL_FAILED",
|
|
559
|
+
states: assignmentStates,
|
|
560
|
+
assignmentId: assignment.assignmentId,
|
|
561
|
+
retryCount: retryCounts[assignment.assignmentId] ?? 0,
|
|
562
|
+
payload: { reason, dispatched: false },
|
|
563
|
+
});
|
|
564
|
+
final.set(assignment.assignmentId, "FINAL_FAILED");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
375
567
|
async function dispatch(slot, assignment, retryCount) {
|
|
376
|
-
const
|
|
377
|
-
|
|
568
|
+
const boundedId = (kind) => {
|
|
569
|
+
const candidate = `${assignment.assignmentId}.${kind}.${retryCount}`;
|
|
570
|
+
return candidate.length <= 64
|
|
571
|
+
? candidate
|
|
572
|
+
: crypto
|
|
573
|
+
.createHash("sha256")
|
|
574
|
+
.update(`${assignment.assignmentId}:${kind}:${retryCount}`)
|
|
575
|
+
.digest("hex");
|
|
576
|
+
};
|
|
577
|
+
const attemptId = boundedId("attempt");
|
|
578
|
+
const teammateId = boundedId("teammate");
|
|
378
579
|
const frozen = frozenLaunches.get(assignment.assignmentId);
|
|
379
580
|
if (!frozen)
|
|
380
581
|
throw new Error(`missing frozen launch for '${assignment.assignmentId}'`);
|
|
@@ -388,6 +589,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
388
589
|
profile,
|
|
389
590
|
launch,
|
|
390
591
|
};
|
|
592
|
+
input.runtime.prepare?.(context);
|
|
391
593
|
await appendTransition({
|
|
392
594
|
sessionId: plan.sessionId,
|
|
393
595
|
by,
|
|
@@ -510,6 +712,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
510
712
|
teammateId,
|
|
511
713
|
slot,
|
|
512
714
|
retryCount,
|
|
715
|
+
transitionKey: `attempt:${attemptId}`,
|
|
513
716
|
...(context.providerSessionId
|
|
514
717
|
? { providerSessionId: context.providerSessionId }
|
|
515
718
|
: {}),
|
|
@@ -562,7 +765,63 @@ export async function runTeamworkCoordinator(input) {
|
|
|
562
765
|
});
|
|
563
766
|
guardHeld = false;
|
|
564
767
|
}
|
|
768
|
+
async function resumeFromWait(current) {
|
|
769
|
+
if (assignmentStates.get(current.assignment.assignmentId) !==
|
|
770
|
+
"WAITING_FOR_MAIN_COMMAND")
|
|
771
|
+
return;
|
|
772
|
+
const transitionKey = `continuation:${current.continuations}`;
|
|
773
|
+
await appendTransition({
|
|
774
|
+
sessionId: plan.sessionId,
|
|
775
|
+
by,
|
|
776
|
+
batchId: plan.batchId,
|
|
777
|
+
entity: "attempt",
|
|
778
|
+
id: current.context.attemptId,
|
|
779
|
+
state: "RUNNING",
|
|
780
|
+
states: attemptStates,
|
|
781
|
+
assignmentId: current.assignment.assignmentId,
|
|
782
|
+
attemptId: current.context.attemptId,
|
|
783
|
+
teammateId: current.context.teammateId,
|
|
784
|
+
retryCount: current.retryCount,
|
|
785
|
+
...(current.providerSessionId
|
|
786
|
+
? { providerSessionId: current.providerSessionId }
|
|
787
|
+
: {}),
|
|
788
|
+
transitionKey,
|
|
789
|
+
});
|
|
790
|
+
await appendTransition({
|
|
791
|
+
sessionId: plan.sessionId,
|
|
792
|
+
by,
|
|
793
|
+
batchId: plan.batchId,
|
|
794
|
+
entity: "teammate",
|
|
795
|
+
id: current.context.teammateId,
|
|
796
|
+
state: "RUNNING",
|
|
797
|
+
states: teammateStates,
|
|
798
|
+
attemptId: current.context.attemptId,
|
|
799
|
+
teammateId: current.context.teammateId,
|
|
800
|
+
...(current.providerSessionId
|
|
801
|
+
? { providerSessionId: current.providerSessionId }
|
|
802
|
+
: {}),
|
|
803
|
+
transitionKey,
|
|
804
|
+
});
|
|
805
|
+
await appendTransition({
|
|
806
|
+
sessionId: plan.sessionId,
|
|
807
|
+
by,
|
|
808
|
+
batchId: plan.batchId,
|
|
809
|
+
entity: "assignment",
|
|
810
|
+
id: current.assignment.assignmentId,
|
|
811
|
+
state: "EXECUTING",
|
|
812
|
+
states: assignmentStates,
|
|
813
|
+
assignmentId: current.assignment.assignmentId,
|
|
814
|
+
attemptId: current.context.attemptId,
|
|
815
|
+
teammateId: current.context.teammateId,
|
|
816
|
+
retryCount: current.retryCount,
|
|
817
|
+
...(current.providerSessionId
|
|
818
|
+
? { providerSessionId: current.providerSessionId }
|
|
819
|
+
: {}),
|
|
820
|
+
transitionKey,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
565
823
|
async function cancelAndDrain(reason, terminal) {
|
|
824
|
+
await failPending(reason);
|
|
566
825
|
if (batchStates.get(plan.batchId) === "RUNNING") {
|
|
567
826
|
await appendTransition({
|
|
568
827
|
sessionId: plan.sessionId,
|
|
@@ -575,52 +834,68 @@ export async function runTeamworkCoordinator(input) {
|
|
|
575
834
|
payload: { reason },
|
|
576
835
|
});
|
|
577
836
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
837
|
+
const cancel = input.runtime.cancel;
|
|
838
|
+
const outcomes = await Promise.all([...active.values()].map(async (current) => {
|
|
839
|
+
if (terminal?.current === current)
|
|
840
|
+
return { current, result: terminal.result };
|
|
841
|
+
if (!cancel)
|
|
842
|
+
return {
|
|
843
|
+
current,
|
|
844
|
+
error: new Error(`host cannot cancel active attempt: ${reason}`),
|
|
845
|
+
};
|
|
846
|
+
try {
|
|
847
|
+
return { current, result: await cancel(current.context) };
|
|
585
848
|
}
|
|
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
|
-
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
return { current, error };
|
|
595
851
|
}
|
|
852
|
+
}));
|
|
853
|
+
let cancellationFailure;
|
|
854
|
+
for (const outcome of outcomes) {
|
|
855
|
+
const { current } = outcome;
|
|
856
|
+
if ("error" in outcome) {
|
|
857
|
+
cancellationFailure ??=
|
|
858
|
+
outcome.error instanceof Error
|
|
859
|
+
? outcome.error.message
|
|
860
|
+
: String(outcome.error);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const { result } = outcome;
|
|
596
864
|
if (result.kind === "reconciliation" ||
|
|
597
865
|
result.kind === "timeout" ||
|
|
598
866
|
result.terminal !== true) {
|
|
599
|
-
|
|
867
|
+
cancellationFailure ??= `attempt cancellation is not terminal: ${reason}`;
|
|
868
|
+
continue;
|
|
600
869
|
}
|
|
601
870
|
if (result.providerSessionId &&
|
|
602
871
|
current.providerSessionId &&
|
|
603
872
|
result.providerSessionId !== current.providerSessionId) {
|
|
604
|
-
|
|
873
|
+
cancellationFailure ??=
|
|
874
|
+
"cancellation returned a different host session";
|
|
875
|
+
continue;
|
|
605
876
|
}
|
|
606
877
|
if (result.providerSessionId) {
|
|
607
878
|
current.providerSessionId = result.providerSessionId;
|
|
608
879
|
current.context.providerSessionId = result.providerSessionId;
|
|
609
880
|
}
|
|
610
881
|
try {
|
|
611
|
-
|
|
882
|
+
await resumeFromWait(current);
|
|
883
|
+
if (result.kind === "report") {
|
|
612
884
|
await complete(current, false);
|
|
885
|
+
}
|
|
613
886
|
else
|
|
614
887
|
await fail(current, result.reason ?? "cancelled by coordinator", false);
|
|
615
888
|
}
|
|
616
889
|
catch (error) {
|
|
617
|
-
|
|
890
|
+
cancellationFailure ??=
|
|
891
|
+
error instanceof Error ? error.message : String(error);
|
|
892
|
+
continue;
|
|
618
893
|
}
|
|
619
894
|
active.delete(current.slot);
|
|
620
895
|
waits.delete(current.slot);
|
|
621
896
|
}
|
|
622
|
-
if (
|
|
623
|
-
return reconcile(
|
|
897
|
+
if (cancellationFailure)
|
|
898
|
+
return reconcile(cancellationFailure);
|
|
624
899
|
return settle();
|
|
625
900
|
}
|
|
626
901
|
async function fail(activeWork, reason, allowRetry = true) {
|
|
@@ -657,7 +932,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
657
932
|
: {}),
|
|
658
933
|
terminal: true,
|
|
659
934
|
});
|
|
660
|
-
if (allowRetry && retryCount < assignment.maxRetries) {
|
|
935
|
+
if (allowRetry && !admissionClosed && retryCount < assignment.maxRetries) {
|
|
661
936
|
await release(activeWork);
|
|
662
937
|
retryCounts[assignment.assignmentId] = retryCount + 1;
|
|
663
938
|
pending.unshift(assignment);
|
|
@@ -706,7 +981,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
706
981
|
await release(activeWork);
|
|
707
982
|
final.set(assignment.assignmentId, "FINAL_FAILED");
|
|
708
983
|
}
|
|
709
|
-
async function
|
|
984
|
+
async function submit(activeWork) {
|
|
710
985
|
const { assignment, context, retryCount } = activeWork;
|
|
711
986
|
try {
|
|
712
987
|
assertTeamworkPlanCurrent(plan, input.taskRoot);
|
|
@@ -715,26 +990,13 @@ export async function runTeamworkCoordinator(input) {
|
|
|
715
990
|
await fail(activeWork, `task phase changed before report acceptance: ${error instanceof Error ? error.message : String(error)}`, false);
|
|
716
991
|
return;
|
|
717
992
|
}
|
|
718
|
-
writeTeammateDisposition({
|
|
719
|
-
taskRoot: input.taskRoot,
|
|
720
|
-
briefPath: assignment.briefPath,
|
|
721
|
-
identity: {
|
|
722
|
-
taskId: plan.taskId,
|
|
723
|
-
batchId: plan.batchId,
|
|
724
|
-
assignmentId: assignment.assignmentId,
|
|
725
|
-
attemptId: context.attemptId,
|
|
726
|
-
teammateId: context.teammateId,
|
|
727
|
-
},
|
|
728
|
-
sourceDigest: assignment.sourceDigest,
|
|
729
|
-
status: "accepted",
|
|
730
|
-
});
|
|
731
993
|
await appendTransition({
|
|
732
994
|
sessionId: plan.sessionId,
|
|
733
995
|
by,
|
|
734
996
|
batchId: plan.batchId,
|
|
735
997
|
entity: "assignment",
|
|
736
998
|
id: assignment.assignmentId,
|
|
737
|
-
state: "
|
|
999
|
+
state: "AWAITING_REVIEW",
|
|
738
1000
|
states: assignmentStates,
|
|
739
1001
|
assignmentId: assignment.assignmentId,
|
|
740
1002
|
attemptId: context.attemptId,
|
|
@@ -742,8 +1004,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
742
1004
|
retryCount,
|
|
743
1005
|
});
|
|
744
1006
|
await release(activeWork);
|
|
745
|
-
final.set(assignment.assignmentId, "
|
|
746
|
-
acceptedAttempts.set(assignment.assignmentId, context.attemptId);
|
|
1007
|
+
final.set(assignment.assignmentId, "AWAITING_REVIEW");
|
|
747
1008
|
}
|
|
748
1009
|
async function complete(activeWork, allowRetry = true) {
|
|
749
1010
|
const { assignment, context, retryCount } = activeWork;
|
|
@@ -782,12 +1043,13 @@ export async function runTeamworkCoordinator(input) {
|
|
|
782
1043
|
});
|
|
783
1044
|
}
|
|
784
1045
|
catch (error) {
|
|
785
|
-
await fail(activeWork, error instanceof Error ? error.message : String(error),
|
|
1046
|
+
await fail(activeWork, error instanceof Error ? error.message : String(error), error instanceof TeammateReportReadError && error.kind === "missing"
|
|
1047
|
+
? allowRetry
|
|
1048
|
+
: false);
|
|
786
1049
|
return;
|
|
787
1050
|
}
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
await fail(activeWork, "report rejected by acceptance contract", allowRetry);
|
|
1051
|
+
if (context.launch.reportVersion === 2 && report.schemaVersion !== 2) {
|
|
1052
|
+
await fail(activeWork, "report schema_version must match the frozen result contract (2)", allowRetry);
|
|
791
1053
|
return;
|
|
792
1054
|
}
|
|
793
1055
|
try {
|
|
@@ -855,7 +1117,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
855
1117
|
teammateId: context.teammateId,
|
|
856
1118
|
retryCount,
|
|
857
1119
|
});
|
|
858
|
-
await
|
|
1120
|
+
await submit(activeWork);
|
|
859
1121
|
}
|
|
860
1122
|
async function settle() {
|
|
861
1123
|
await appendTransition({
|
|
@@ -867,16 +1129,8 @@ export async function runTeamworkCoordinator(input) {
|
|
|
867
1129
|
state: "SETTLING",
|
|
868
1130
|
states: batchStates,
|
|
869
1131
|
});
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
assignments: plan.assignments,
|
|
873
|
-
acceptedAttempts,
|
|
874
|
-
});
|
|
875
|
-
if (cleanupFailures.length > 0) {
|
|
876
|
-
return reconcile(`Teamwork batch cleanup failed: ${cleanupFailures.join("; ")}`);
|
|
877
|
-
}
|
|
878
|
-
const accepted = plan.assignments.every((assignment) => final.get(assignment.assignmentId) === "ACCEPTED");
|
|
879
|
-
const status = accepted ? "ALL_ACCEPTED" : "SETTLED_WITH_FAILURE";
|
|
1132
|
+
const pendingReview = plan.assignments.some((assignment) => final.get(assignment.assignmentId) === "AWAITING_REVIEW");
|
|
1133
|
+
const status = pendingReview ? "AWAITING_REVIEW" : "SETTLED_WITH_FAILURE";
|
|
880
1134
|
await appendTransition({
|
|
881
1135
|
sessionId: plan.sessionId,
|
|
882
1136
|
by,
|
|
@@ -886,29 +1140,7 @@ export async function runTeamworkCoordinator(input) {
|
|
|
886
1140
|
state: status,
|
|
887
1141
|
states: batchStates,
|
|
888
1142
|
});
|
|
889
|
-
const receipt = {
|
|
890
|
-
schemaVersion: 1,
|
|
891
|
-
batchId: plan.batchId,
|
|
892
|
-
status,
|
|
893
|
-
assignments: plan.assignments.map((assignment) => ({
|
|
894
|
-
assignmentId: assignment.assignmentId,
|
|
895
|
-
status: final.get(assignment.assignmentId) ?? "FINAL_FAILED",
|
|
896
|
-
})),
|
|
897
|
-
capacity,
|
|
898
|
-
retryCounts,
|
|
899
|
-
evaluationGroups: [
|
|
900
|
-
...new Set(plan.assignments
|
|
901
|
-
.map((assignment) => assignment.evaluationGroup?.id)
|
|
902
|
-
.filter((id) => id !== undefined)),
|
|
903
|
-
].map((id) => ({
|
|
904
|
-
id,
|
|
905
|
-
status: plan.assignments
|
|
906
|
-
.filter((assignment) => assignment.evaluationGroup?.id === id)
|
|
907
|
-
.every((assignment) => final.get(assignment.assignmentId) === "ACCEPTED")
|
|
908
|
-
? "COMPLETE"
|
|
909
|
-
: "INCOMPLETE",
|
|
910
|
-
})),
|
|
911
|
-
};
|
|
1143
|
+
const receipt = terminalReceiptFromProjection(plan, await readCoordinationState({ sessionId: plan.sessionId, cwd }));
|
|
912
1144
|
await appendAction({
|
|
913
1145
|
sessionId: plan.sessionId,
|
|
914
1146
|
by,
|
|
@@ -918,20 +1150,62 @@ export async function runTeamworkCoordinator(input) {
|
|
|
918
1150
|
id: `${plan.batchId}:${status}`,
|
|
919
1151
|
payload: { ...receipt },
|
|
920
1152
|
});
|
|
921
|
-
await assertTeamworkReceiptDurable(plan
|
|
1153
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
922
1154
|
await releaseGuard();
|
|
923
1155
|
return receipt;
|
|
924
1156
|
}
|
|
925
1157
|
async function fill() {
|
|
1158
|
+
if (admissionClosed)
|
|
1159
|
+
return;
|
|
926
1160
|
while (active.size < capacity && pending.length > 0) {
|
|
927
|
-
|
|
1161
|
+
if (Date.parse(plan.deadline) <= Date.now()) {
|
|
1162
|
+
await failPending("batch deadline elapsed before dispatch");
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1166
|
+
assertTeamworkPlanCurrent(plan, input.taskRoot);
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
await failPending(`task phase changed before dispatch: ${error instanceof Error ? error.message : String(error)}`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const assignment = pending[0];
|
|
928
1173
|
if (!assignment)
|
|
929
1174
|
return;
|
|
930
1175
|
const retryCount = retryCounts[assignment.assignmentId] ?? 0;
|
|
931
1176
|
let slot = 0;
|
|
932
1177
|
while (active.has(slot))
|
|
933
1178
|
slot += 1;
|
|
934
|
-
|
|
1179
|
+
try {
|
|
1180
|
+
await dispatch(slot, assignment, retryCount);
|
|
1181
|
+
pending.shift();
|
|
1182
|
+
}
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
if (!assignmentStates.has(assignment.assignmentId) ||
|
|
1185
|
+
assignmentStates.get(assignment.assignmentId) === "QUEUED") {
|
|
1186
|
+
const reason = `launch preflight failed before dispatch: ${error instanceof Error ? error.message : String(error)}`;
|
|
1187
|
+
if (error instanceof TeamworkPolicyChangedError) {
|
|
1188
|
+
await failPending(reason);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
pending.shift();
|
|
1192
|
+
await appendTransition({
|
|
1193
|
+
sessionId: plan.sessionId,
|
|
1194
|
+
by,
|
|
1195
|
+
batchId: plan.batchId,
|
|
1196
|
+
entity: "assignment",
|
|
1197
|
+
id: assignment.assignmentId,
|
|
1198
|
+
assignmentId: assignment.assignmentId,
|
|
1199
|
+
state: "FINAL_FAILED",
|
|
1200
|
+
states: assignmentStates,
|
|
1201
|
+
retryCount,
|
|
1202
|
+
payload: { reason, dispatched: false },
|
|
1203
|
+
});
|
|
1204
|
+
final.set(assignment.assignmentId, "FINAL_FAILED");
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
935
1209
|
}
|
|
936
1210
|
}
|
|
937
1211
|
try {
|
|
@@ -991,162 +1265,116 @@ export async function runTeamworkCoordinator(input) {
|
|
|
991
1265
|
result: next.result,
|
|
992
1266
|
});
|
|
993
1267
|
}
|
|
1268
|
+
try {
|
|
1269
|
+
await resumeFromWait(current);
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1273
|
+
}
|
|
994
1274
|
let releaseSlot = true;
|
|
995
1275
|
if (next.result.kind === "timeout") {
|
|
996
1276
|
current.continuations += 1;
|
|
997
|
-
if (
|
|
998
|
-
return
|
|
1277
|
+
if (Date.now() >= Date.parse(plan.deadline))
|
|
1278
|
+
return cancelAndDrain("batch deadline elapsed");
|
|
1279
|
+
const transitionKey = `continuation:${current.continuations}`;
|
|
1280
|
+
await appendTransition({
|
|
1281
|
+
sessionId: plan.sessionId,
|
|
1282
|
+
by,
|
|
1283
|
+
batchId: plan.batchId,
|
|
1284
|
+
entity: "attempt",
|
|
1285
|
+
id: current.context.attemptId,
|
|
1286
|
+
state: "TIMEOUT_YIELDED",
|
|
1287
|
+
states: attemptStates,
|
|
1288
|
+
assignmentId: current.assignment.assignmentId,
|
|
1289
|
+
attemptId: current.context.attemptId,
|
|
1290
|
+
teammateId: current.context.teammateId,
|
|
1291
|
+
retryCount: current.retryCount,
|
|
1292
|
+
...(current.providerSessionId
|
|
1293
|
+
? { providerSessionId: current.providerSessionId }
|
|
1294
|
+
: {}),
|
|
1295
|
+
transitionKey,
|
|
1296
|
+
});
|
|
1297
|
+
await appendTransition({
|
|
1298
|
+
sessionId: plan.sessionId,
|
|
1299
|
+
by,
|
|
1300
|
+
batchId: plan.batchId,
|
|
1301
|
+
entity: "teammate",
|
|
1302
|
+
id: current.context.teammateId,
|
|
1303
|
+
state: "TIMEOUT_YIELDED",
|
|
1304
|
+
states: teammateStates,
|
|
1305
|
+
attemptId: current.context.attemptId,
|
|
1306
|
+
teammateId: current.context.teammateId,
|
|
1307
|
+
...(current.providerSessionId
|
|
1308
|
+
? { providerSessionId: current.providerSessionId }
|
|
1309
|
+
: {}),
|
|
1310
|
+
transitionKey,
|
|
1311
|
+
});
|
|
1312
|
+
await appendTransition({
|
|
1313
|
+
sessionId: plan.sessionId,
|
|
1314
|
+
by,
|
|
1315
|
+
batchId: plan.batchId,
|
|
1316
|
+
entity: "assignment",
|
|
1317
|
+
id: current.assignment.assignmentId,
|
|
1318
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1319
|
+
states: assignmentStates,
|
|
1320
|
+
assignmentId: current.assignment.assignmentId,
|
|
1321
|
+
attemptId: current.context.attemptId,
|
|
1322
|
+
teammateId: current.context.teammateId,
|
|
1323
|
+
retryCount: current.retryCount,
|
|
1324
|
+
...(current.providerSessionId
|
|
1325
|
+
? { providerSessionId: current.providerSessionId }
|
|
1326
|
+
: {}),
|
|
1327
|
+
transitionKey,
|
|
1328
|
+
});
|
|
1329
|
+
await appendTransition({
|
|
1330
|
+
sessionId: plan.sessionId,
|
|
1331
|
+
by,
|
|
1332
|
+
batchId: plan.batchId,
|
|
1333
|
+
entity: "attempt",
|
|
1334
|
+
id: current.context.attemptId,
|
|
1335
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1336
|
+
states: attemptStates,
|
|
1337
|
+
assignmentId: current.assignment.assignmentId,
|
|
1338
|
+
attemptId: current.context.attemptId,
|
|
1339
|
+
teammateId: current.context.teammateId,
|
|
1340
|
+
retryCount: current.retryCount,
|
|
1341
|
+
...(current.providerSessionId
|
|
1342
|
+
? { providerSessionId: current.providerSessionId }
|
|
1343
|
+
: {}),
|
|
1344
|
+
transitionKey,
|
|
1345
|
+
});
|
|
1346
|
+
await appendTransition({
|
|
1347
|
+
sessionId: plan.sessionId,
|
|
1348
|
+
by,
|
|
1349
|
+
batchId: plan.batchId,
|
|
1350
|
+
entity: "teammate",
|
|
1351
|
+
id: current.context.teammateId,
|
|
1352
|
+
state: "WAITING_FOR_MAIN_COMMAND",
|
|
1353
|
+
states: teammateStates,
|
|
1354
|
+
attemptId: current.context.attemptId,
|
|
1355
|
+
teammateId: current.context.teammateId,
|
|
1356
|
+
...(current.providerSessionId
|
|
1357
|
+
? { providerSessionId: current.providerSessionId }
|
|
1358
|
+
: {}),
|
|
1359
|
+
transitionKey,
|
|
1360
|
+
});
|
|
1361
|
+
let continued;
|
|
1362
|
+
try {
|
|
1363
|
+
continued = input.runtime.continue(current.context);
|
|
999
1364
|
}
|
|
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;
|
|
1365
|
+
catch (error) {
|
|
1366
|
+
return reconcile(error instanceof Error ? error.message : String(error));
|
|
1149
1367
|
}
|
|
1368
|
+
waits.set(next.slot, Promise.resolve(continued)
|
|
1369
|
+
.then((result) => ({ slot: next.slot, result }))
|
|
1370
|
+
.catch((error) => ({
|
|
1371
|
+
slot: next.slot,
|
|
1372
|
+
result: {
|
|
1373
|
+
kind: "reconciliation",
|
|
1374
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1375
|
+
},
|
|
1376
|
+
})));
|
|
1377
|
+
releaseSlot = false;
|
|
1150
1378
|
}
|
|
1151
1379
|
else if (next.result.kind === "report") {
|
|
1152
1380
|
try {
|
|
@@ -1177,22 +1405,400 @@ export async function runTeamworkCoordinator(input) {
|
|
|
1177
1405
|
}
|
|
1178
1406
|
return settle();
|
|
1179
1407
|
}
|
|
1180
|
-
export async function assertTeamworkReceiptDurable(
|
|
1408
|
+
export async function assertTeamworkReceiptDurable(plan, receipt, cwd) {
|
|
1181
1409
|
const [state, events] = await Promise.all([
|
|
1182
|
-
readCoordinationState({
|
|
1183
|
-
|
|
1410
|
+
readCoordinationState({
|
|
1411
|
+
sessionId: plan.sessionId,
|
|
1412
|
+
...(cwd ? { cwd } : {}),
|
|
1413
|
+
}),
|
|
1414
|
+
readTeamworkEvents({
|
|
1415
|
+
sessionId: plan.sessionId,
|
|
1416
|
+
...(cwd ? { cwd } : {}),
|
|
1417
|
+
}),
|
|
1184
1418
|
]);
|
|
1185
|
-
const
|
|
1186
|
-
if (
|
|
1187
|
-
(batch.
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const receiptRecorded = events.some((event) => event.kind === "coordination" &&
|
|
1191
|
-
event.batchId === batchId &&
|
|
1419
|
+
const expected = terminalReceiptFromProjection(plan, state);
|
|
1420
|
+
if (!isDeepStrictEqual(receipt, expected))
|
|
1421
|
+
throw new Error(`batch '${plan.batchId}' receipt disagrees with durable facts`);
|
|
1422
|
+
const receipts = events.filter((event) => event.kind === "coordination" &&
|
|
1423
|
+
event.batchId === plan.batchId &&
|
|
1192
1424
|
event.action === "receipt_recorded" &&
|
|
1193
|
-
event.payload?.status ===
|
|
1194
|
-
|
|
1195
|
-
|
|
1425
|
+
event.payload?.status === receipt.status &&
|
|
1426
|
+
event.payload?.status !== "RECONCILIATION_REQUIRED");
|
|
1427
|
+
if (receipts.length === 0 ||
|
|
1428
|
+
!isDeepStrictEqual(receipts.at(-1)?.payload, expected))
|
|
1429
|
+
throw new Error(`batch '${plan.batchId}' has no matching durable receipt event`);
|
|
1430
|
+
}
|
|
1431
|
+
/** Retire uncertain work only from associated terminal or process-birth evidence. */
|
|
1432
|
+
export async function reconcileTeamworkBatch(input) {
|
|
1433
|
+
const { plan, cwd } = input;
|
|
1434
|
+
const projection = await readCoordinationState({
|
|
1435
|
+
sessionId: plan.sessionId,
|
|
1436
|
+
cwd,
|
|
1437
|
+
});
|
|
1438
|
+
const batch = projection.batches.find((entry) => entry.batchId === plan.batchId);
|
|
1439
|
+
if (batch?.planHash !== plan.planHash)
|
|
1440
|
+
throw new Error("Batch does not match the frozen plan");
|
|
1441
|
+
const events = await readTeamworkEvents({ sessionId: plan.sessionId, cwd });
|
|
1442
|
+
const resolving = events.some((event) => event.kind === "coordination" &&
|
|
1443
|
+
event.batchId === plan.batchId &&
|
|
1444
|
+
event.action === "reconciliation_resolved");
|
|
1445
|
+
if (["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state)) {
|
|
1446
|
+
const receipt = terminalReceiptFromProjection(plan, projection);
|
|
1447
|
+
if (input.execute) {
|
|
1448
|
+
const recovered = await recoverExistingBatch({
|
|
1449
|
+
plan,
|
|
1450
|
+
cwd,
|
|
1451
|
+
by: "main",
|
|
1452
|
+
capacity: receipt.capacity,
|
|
1453
|
+
});
|
|
1454
|
+
if (!recovered)
|
|
1455
|
+
throw new Error("Settled batch disappeared during reconciliation");
|
|
1456
|
+
return recovered;
|
|
1457
|
+
}
|
|
1458
|
+
return receipt;
|
|
1459
|
+
}
|
|
1460
|
+
if (batch.state !== "RECONCILIATION_REQUIRED" &&
|
|
1461
|
+
!(resolving && batch.state === "SETTLING"))
|
|
1462
|
+
throw new Error("Recover the original batch into RECONCILIATION_REQUIRED first");
|
|
1463
|
+
const attempts = projection.attempts.filter((entry) => entry.batchId === plan.batchId);
|
|
1464
|
+
const blockers = projection.issues.filter((issue) => !issue.includes(": reconciliation required"));
|
|
1465
|
+
for (const attempt of attempts) {
|
|
1466
|
+
if (attempt.terminal)
|
|
1467
|
+
continue;
|
|
1468
|
+
const facts = events.filter((event) => event.kind === "coordination" &&
|
|
1469
|
+
event.batchId === plan.batchId &&
|
|
1470
|
+
event.attemptId === attempt.attemptId &&
|
|
1471
|
+
event.planHash === plan.planHash);
|
|
1472
|
+
if (facts.some((event) => event.kind === "coordination" && event.action === "process_exited"))
|
|
1473
|
+
continue;
|
|
1474
|
+
const started = facts
|
|
1475
|
+
.filter((event) => event.kind === "coordination" && event.action === "process_started")
|
|
1476
|
+
.pop();
|
|
1477
|
+
const pid = started?.kind === "coordination" ? started.payload?.pid : undefined;
|
|
1478
|
+
const birth = started?.kind === "coordination"
|
|
1479
|
+
? started.payload?.processIdentity
|
|
1480
|
+
: undefined;
|
|
1481
|
+
let exited = false;
|
|
1482
|
+
let identityUnknown = true;
|
|
1483
|
+
if (typeof pid === "number" &&
|
|
1484
|
+
Number.isSafeInteger(pid) &&
|
|
1485
|
+
pid > 0 &&
|
|
1486
|
+
typeof birth === "string" &&
|
|
1487
|
+
birth.startsWith(`${pid}:`)) {
|
|
1488
|
+
const current = readProcessIdentity(pid);
|
|
1489
|
+
if (current) {
|
|
1490
|
+
exited = current !== birth;
|
|
1491
|
+
identityUnknown = false;
|
|
1492
|
+
}
|
|
1493
|
+
else {
|
|
1494
|
+
try {
|
|
1495
|
+
process.kill(pid, 0);
|
|
1496
|
+
}
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
const code = error.code;
|
|
1499
|
+
exited = code === "ESRCH";
|
|
1500
|
+
identityUnknown = code !== "ESRCH";
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
if (!exited &&
|
|
1505
|
+
identityUnknown &&
|
|
1506
|
+
input.execute &&
|
|
1507
|
+
input.confirmUnknownProcessExit &&
|
|
1508
|
+
input.reason?.trim())
|
|
1509
|
+
exited = true;
|
|
1510
|
+
if (!exited)
|
|
1511
|
+
blockers.push(`Attempt ${attempt.attemptId} lacks confirmed process exit; preserve its lease`);
|
|
1196
1512
|
}
|
|
1513
|
+
if (!input.execute)
|
|
1514
|
+
return { status: blockers.length ? "blocked" : "ready", blockers };
|
|
1515
|
+
if (blockers.length)
|
|
1516
|
+
throw new Error(blockers.join("; "));
|
|
1517
|
+
if (!input.reason?.trim())
|
|
1518
|
+
throw new Error("Reconciliation requires an explicit disposition reason");
|
|
1519
|
+
const by = "main";
|
|
1520
|
+
for (const attempt of attempts) {
|
|
1521
|
+
await recordCoordinationEvent({
|
|
1522
|
+
sessionId: plan.sessionId,
|
|
1523
|
+
cwd,
|
|
1524
|
+
by,
|
|
1525
|
+
batchId: plan.batchId,
|
|
1526
|
+
entity: "attempt",
|
|
1527
|
+
action: "terminal_observed",
|
|
1528
|
+
attemptId: attempt.attemptId,
|
|
1529
|
+
teammateId: attempt.teammateId,
|
|
1530
|
+
terminal: true,
|
|
1531
|
+
reason: input.reason,
|
|
1532
|
+
idempotencyKey: `${plan.batchId}:reconciled-terminal:${attempt.attemptId}`,
|
|
1533
|
+
});
|
|
1534
|
+
if (!["FINAL_FAILED", "REPORT_SUBMITTED"].includes(attempt.state))
|
|
1535
|
+
await appendTransition({
|
|
1536
|
+
sessionId: plan.sessionId,
|
|
1537
|
+
cwd,
|
|
1538
|
+
by,
|
|
1539
|
+
batchId: plan.batchId,
|
|
1540
|
+
entity: "attempt",
|
|
1541
|
+
id: attempt.attemptId,
|
|
1542
|
+
attemptId: attempt.attemptId,
|
|
1543
|
+
assignmentId: attempt.assignmentId,
|
|
1544
|
+
teammateId: attempt.teammateId,
|
|
1545
|
+
retryCount: attempt.retryCount,
|
|
1546
|
+
state: "FINAL_FAILED",
|
|
1547
|
+
terminal: true,
|
|
1548
|
+
states: new Map([[attempt.attemptId, attempt.state]]),
|
|
1549
|
+
transitionKey: "reconcile",
|
|
1550
|
+
});
|
|
1551
|
+
const teammate = projection.teammates.find((entry) => entry.batchId === plan.batchId &&
|
|
1552
|
+
entry.teammateId === attempt.teammateId);
|
|
1553
|
+
if (teammate && !["FAILED", "REPORT_SUBMITTED"].includes(teammate.state))
|
|
1554
|
+
await appendTransition({
|
|
1555
|
+
sessionId: plan.sessionId,
|
|
1556
|
+
cwd,
|
|
1557
|
+
by,
|
|
1558
|
+
batchId: plan.batchId,
|
|
1559
|
+
entity: "teammate",
|
|
1560
|
+
id: teammate.teammateId,
|
|
1561
|
+
teammateId: teammate.teammateId,
|
|
1562
|
+
attemptId: attempt.attemptId,
|
|
1563
|
+
state: "FAILED",
|
|
1564
|
+
terminal: true,
|
|
1565
|
+
states: new Map([[teammate.teammateId, teammate.state]]),
|
|
1566
|
+
transitionKey: "reconcile",
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
for (const assignment of projection.assignments.filter((entry) => entry.batchId === plan.batchId)) {
|
|
1570
|
+
if (!["ACCEPTED", "FINAL_FAILED"].includes(assignment.state))
|
|
1571
|
+
await appendTransition({
|
|
1572
|
+
sessionId: plan.sessionId,
|
|
1573
|
+
cwd,
|
|
1574
|
+
by,
|
|
1575
|
+
batchId: plan.batchId,
|
|
1576
|
+
entity: "assignment",
|
|
1577
|
+
id: assignment.assignmentId,
|
|
1578
|
+
assignmentId: assignment.assignmentId,
|
|
1579
|
+
attemptId: assignment.attemptId,
|
|
1580
|
+
teammateId: assignment.teammateId,
|
|
1581
|
+
retryCount: assignment.retryCount,
|
|
1582
|
+
state: "FINAL_FAILED",
|
|
1583
|
+
states: new Map([[assignment.assignmentId, assignment.state]]),
|
|
1584
|
+
transitionKey: "reconcile",
|
|
1585
|
+
payload: { reason: input.reason },
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
for (const lease of projection.leases.filter((entry) => entry.batchId === plan.batchId))
|
|
1589
|
+
await recordCoordinationEvent({
|
|
1590
|
+
sessionId: plan.sessionId,
|
|
1591
|
+
cwd,
|
|
1592
|
+
by,
|
|
1593
|
+
batchId: plan.batchId,
|
|
1594
|
+
entity: "attempt",
|
|
1595
|
+
action: "lease_released",
|
|
1596
|
+
leaseId: lease.leaseId,
|
|
1597
|
+
protected: false,
|
|
1598
|
+
reason: input.reason,
|
|
1599
|
+
idempotencyKey: `${plan.batchId}:reconciled-release:${lease.leaseId}`,
|
|
1600
|
+
});
|
|
1601
|
+
const accepted = projection.assignments
|
|
1602
|
+
.filter((entry) => entry.batchId === plan.batchId)
|
|
1603
|
+
.every((entry) => entry.state === "ACCEPTED");
|
|
1604
|
+
const states = new Map([
|
|
1605
|
+
[plan.batchId, batch.state],
|
|
1606
|
+
]);
|
|
1607
|
+
await recordCoordinationEvent({
|
|
1608
|
+
sessionId: plan.sessionId,
|
|
1609
|
+
cwd,
|
|
1610
|
+
by,
|
|
1611
|
+
batchId: plan.batchId,
|
|
1612
|
+
entity: "batch",
|
|
1613
|
+
action: "reconciliation_resolved",
|
|
1614
|
+
reason: input.reason,
|
|
1615
|
+
...(input.confirmUnknownProcessExit
|
|
1616
|
+
? { payload: { confirmedUnknownProcessExit: true } }
|
|
1617
|
+
: {}),
|
|
1618
|
+
idempotencyKey: `${plan.batchId}:reconciled:${crypto
|
|
1619
|
+
.createHash("sha256")
|
|
1620
|
+
.update(input.reason ?? "")
|
|
1621
|
+
.digest("hex")}`,
|
|
1622
|
+
});
|
|
1623
|
+
if (batch.state !== "SETTLING")
|
|
1624
|
+
await appendTransition({
|
|
1625
|
+
sessionId: plan.sessionId,
|
|
1626
|
+
cwd,
|
|
1627
|
+
by,
|
|
1628
|
+
batchId: plan.batchId,
|
|
1629
|
+
entity: "batch",
|
|
1630
|
+
id: plan.batchId,
|
|
1631
|
+
state: "SETTLING",
|
|
1632
|
+
states,
|
|
1633
|
+
transitionKey: "reconcile",
|
|
1634
|
+
});
|
|
1635
|
+
await appendTransition({
|
|
1636
|
+
sessionId: plan.sessionId,
|
|
1637
|
+
cwd,
|
|
1638
|
+
by,
|
|
1639
|
+
batchId: plan.batchId,
|
|
1640
|
+
entity: "batch",
|
|
1641
|
+
id: plan.batchId,
|
|
1642
|
+
state: accepted ? "ALL_ACCEPTED" : "SETTLED_WITH_FAILURE",
|
|
1643
|
+
states,
|
|
1644
|
+
transitionKey: "reconcile",
|
|
1645
|
+
});
|
|
1646
|
+
const receipt = terminalReceiptFromProjection(plan, await readCoordinationState({ sessionId: plan.sessionId, cwd }));
|
|
1647
|
+
await appendAction({
|
|
1648
|
+
sessionId: plan.sessionId,
|
|
1649
|
+
cwd,
|
|
1650
|
+
by,
|
|
1651
|
+
batchId: plan.batchId,
|
|
1652
|
+
entity: "batch",
|
|
1653
|
+
action: "receipt_recorded",
|
|
1654
|
+
id: `${plan.batchId}:${receipt.status}`,
|
|
1655
|
+
payload: { ...receipt },
|
|
1656
|
+
});
|
|
1657
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
1658
|
+
return receipt;
|
|
1659
|
+
}
|
|
1660
|
+
/** Read settled facts without admitting work or consulting current resource policy. */
|
|
1661
|
+
export async function readSettledTeamworkReceipt(plan, cwd) {
|
|
1662
|
+
const state = await readCoordinationState({ sessionId: plan.sessionId, cwd });
|
|
1663
|
+
const batch = state.batches.find((entry) => entry.batchId === plan.batchId);
|
|
1664
|
+
if (!batch ||
|
|
1665
|
+
!["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
|
|
1666
|
+
return undefined;
|
|
1667
|
+
const receipt = terminalReceiptFromProjection(plan, state);
|
|
1668
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
1669
|
+
return receipt;
|
|
1670
|
+
}
|
|
1671
|
+
/** Main-agent review starts only after the CLI has drained the entire batch. */
|
|
1672
|
+
export async function reviewTeamworkReport(input) {
|
|
1673
|
+
const { plan, taskRoot, cwd } = input;
|
|
1674
|
+
if (!input.reason.trim())
|
|
1675
|
+
throw new Error("Review requires an explicit reason");
|
|
1676
|
+
const assignment = plan.assignments.find((item) => item.assignmentId === input.assignmentId);
|
|
1677
|
+
if (!assignment)
|
|
1678
|
+
throw new Error("Review assignment is not in the frozen plan");
|
|
1679
|
+
return withLock(path.join(taskRoot, "teammates", `${plan.batchId}.review.lock`), async () => {
|
|
1680
|
+
assertTeamworkPlanCurrent(plan, taskRoot);
|
|
1681
|
+
const projection = await readCoordinationState({
|
|
1682
|
+
sessionId: plan.sessionId,
|
|
1683
|
+
cwd,
|
|
1684
|
+
});
|
|
1685
|
+
const batch = projection.batches.find((item) => item.batchId === plan.batchId);
|
|
1686
|
+
if (!batch ||
|
|
1687
|
+
!["AWAITING_REVIEW", "ALL_ACCEPTED", "SETTLED_WITH_FAILURE"].includes(batch.state))
|
|
1688
|
+
throw new Error("Review requires the entire batch to be collected first");
|
|
1689
|
+
terminalReceiptFromProjection(plan, projection);
|
|
1690
|
+
const work = projection.assignments.find((item) => item.batchId === plan.batchId &&
|
|
1691
|
+
item.assignmentId === assignment.assignmentId);
|
|
1692
|
+
if (!work?.attemptId || !work.teammateId)
|
|
1693
|
+
throw new Error("Review has no matching report attempt");
|
|
1694
|
+
readTeammateReport({
|
|
1695
|
+
taskRoot,
|
|
1696
|
+
briefPath: assignment.briefPath,
|
|
1697
|
+
attemptId: work.attemptId,
|
|
1698
|
+
identity: {
|
|
1699
|
+
taskId: plan.taskId,
|
|
1700
|
+
batchId: plan.batchId,
|
|
1701
|
+
assignmentId: assignment.assignmentId,
|
|
1702
|
+
teammateId: work.teammateId,
|
|
1703
|
+
planHash: plan.planHash,
|
|
1704
|
+
phaseId: plan.phaseId,
|
|
1705
|
+
taskRouteRevision: plan.taskRouteRevision,
|
|
1706
|
+
inputRevision: plan.inputRevision,
|
|
1707
|
+
},
|
|
1708
|
+
sourceDigest: assignment.sourceDigest,
|
|
1709
|
+
});
|
|
1710
|
+
const reportFile = path.join(taskRoot, path.dirname(assignment.briefPath), "attempts", work.attemptId, "report.json");
|
|
1711
|
+
const reportSha256 = crypto
|
|
1712
|
+
.createHash("sha256")
|
|
1713
|
+
.update(fs.readFileSync(reportFile))
|
|
1714
|
+
.digest("hex");
|
|
1715
|
+
if (reportSha256 !== input.reportSha256)
|
|
1716
|
+
throw new Error("Review report digest does not match the inspected report");
|
|
1717
|
+
const state = input.decision === "accepted" ? "ACCEPTED" : "FINAL_FAILED";
|
|
1718
|
+
if (work.state !== "AWAITING_REVIEW" && work.state !== state)
|
|
1719
|
+
throw new Error("Report already has a different final disposition");
|
|
1720
|
+
const by = input.by ?? "main";
|
|
1721
|
+
writeTeammateDisposition({
|
|
1722
|
+
taskRoot,
|
|
1723
|
+
briefPath: assignment.briefPath,
|
|
1724
|
+
identity: {
|
|
1725
|
+
taskId: plan.taskId,
|
|
1726
|
+
batchId: plan.batchId,
|
|
1727
|
+
assignmentId: assignment.assignmentId,
|
|
1728
|
+
attemptId: work.attemptId,
|
|
1729
|
+
teammateId: work.teammateId,
|
|
1730
|
+
},
|
|
1731
|
+
sourceDigest: assignment.sourceDigest,
|
|
1732
|
+
status: input.decision,
|
|
1733
|
+
reason: input.reason,
|
|
1734
|
+
review: {
|
|
1735
|
+
plan_hash: plan.planHash,
|
|
1736
|
+
phase_id: plan.phaseId,
|
|
1737
|
+
task_route_revision: plan.taskRouteRevision,
|
|
1738
|
+
input_revision: plan.inputRevision,
|
|
1739
|
+
report_sha256: reportSha256,
|
|
1740
|
+
acceptance: assignment.acceptance,
|
|
1741
|
+
by,
|
|
1742
|
+
},
|
|
1743
|
+
});
|
|
1744
|
+
if (work.state === "AWAITING_REVIEW")
|
|
1745
|
+
await appendTransition({
|
|
1746
|
+
sessionId: plan.sessionId,
|
|
1747
|
+
cwd,
|
|
1748
|
+
by,
|
|
1749
|
+
batchId: plan.batchId,
|
|
1750
|
+
entity: "assignment",
|
|
1751
|
+
id: assignment.assignmentId,
|
|
1752
|
+
assignmentId: assignment.assignmentId,
|
|
1753
|
+
attemptId: work.attemptId,
|
|
1754
|
+
teammateId: work.teammateId,
|
|
1755
|
+
retryCount: work.retryCount,
|
|
1756
|
+
state,
|
|
1757
|
+
states: new Map([[assignment.assignmentId, work.state]]),
|
|
1758
|
+
payload: {
|
|
1759
|
+
reason: input.reason,
|
|
1760
|
+
reportSha256,
|
|
1761
|
+
planHash: plan.planHash,
|
|
1762
|
+
acceptance: assignment.acceptance,
|
|
1763
|
+
},
|
|
1764
|
+
});
|
|
1765
|
+
let updated = await readCoordinationState({
|
|
1766
|
+
sessionId: plan.sessionId,
|
|
1767
|
+
cwd,
|
|
1768
|
+
});
|
|
1769
|
+
const remaining = updated.assignments.filter((item) => item.batchId === plan.batchId);
|
|
1770
|
+
if (batch.state === "AWAITING_REVIEW" &&
|
|
1771
|
+
!remaining.some((item) => item.state === "AWAITING_REVIEW")) {
|
|
1772
|
+
await appendTransition({
|
|
1773
|
+
sessionId: plan.sessionId,
|
|
1774
|
+
cwd,
|
|
1775
|
+
by,
|
|
1776
|
+
batchId: plan.batchId,
|
|
1777
|
+
entity: "batch",
|
|
1778
|
+
id: plan.batchId,
|
|
1779
|
+
state: remaining.every((item) => item.state === "ACCEPTED")
|
|
1780
|
+
? "ALL_ACCEPTED"
|
|
1781
|
+
: "SETTLED_WITH_FAILURE",
|
|
1782
|
+
states: new Map([[plan.batchId, batch.state]]),
|
|
1783
|
+
});
|
|
1784
|
+
updated = await readCoordinationState({
|
|
1785
|
+
sessionId: plan.sessionId,
|
|
1786
|
+
cwd,
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
const receipt = terminalReceiptFromProjection(plan, updated);
|
|
1790
|
+
await appendAction({
|
|
1791
|
+
sessionId: plan.sessionId,
|
|
1792
|
+
cwd,
|
|
1793
|
+
by,
|
|
1794
|
+
batchId: plan.batchId,
|
|
1795
|
+
entity: "batch",
|
|
1796
|
+
action: "receipt_recorded",
|
|
1797
|
+
id: `${plan.batchId}:review:${crypto.createHash("sha256").update(JSON.stringify(receipt)).digest("hex")}`,
|
|
1798
|
+
payload: { ...receipt },
|
|
1799
|
+
});
|
|
1800
|
+
await assertTeamworkReceiptDurable(plan, receipt, cwd);
|
|
1801
|
+
return receipt;
|
|
1802
|
+
});
|
|
1197
1803
|
}
|
|
1198
1804
|
//# sourceMappingURL=coordinator.js.map
|