agent-dealer 0.2.0 → 1.0.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/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// packages/server/src/coordinator/worker-loop.ts
|
|
2
|
+
//
|
|
3
|
+
// The leased effect worker + its poll loop. A dispatcher claims a queued work item with a
|
|
4
|
+
// compare-and-set (so concurrent dispatchers never run one twice), records a running
|
|
5
|
+
// worker_session, keeps a token-fenced heartbeat while the effect handler runs, then
|
|
6
|
+
// applies the structured completion transactionally. Bounded concurrency; non-overlapping
|
|
7
|
+
// polling.
|
|
8
|
+
//
|
|
9
|
+
// NOT-59 exports startCoordinatorLoop/stopCoordinatorLoop but does NOT wire them into the
|
|
10
|
+
// server — index.ts integration lands with NOT-60.
|
|
11
|
+
import { v4 as uuid } from "uuid";
|
|
12
|
+
import { getDb } from "../db/index.js";
|
|
13
|
+
import { getAgent } from "../repository/agents.js";
|
|
14
|
+
import { getIssue } from "../repository/issues.js";
|
|
15
|
+
import { getWorkflowInstance, getActiveWorkflowInstance, appendWorkflowEvent, } from "../repository/workflow-events.js";
|
|
16
|
+
import { createWorkerSession, startSession, heartbeatSession, completeSession, } from "../repository/worker-sessions.js";
|
|
17
|
+
import { attemptCapReached, bindWorkItemSession, claimWorkItem, finishWorkItem, getWorkItem, refreshHeartbeat, requeueWorkItem, } from "../repository/work-items.js";
|
|
18
|
+
import { applyCompletion, routeAppliedOutcome, routeCapEscalation } from "./commands.js";
|
|
19
|
+
import { getEffectHandler } from "./effect-registry.js";
|
|
20
|
+
import { parseProfileSnapshot } from "@agent-dealer/shared";
|
|
21
|
+
import { buildProfileSnapshot, serializeProfileSnapshot } from "./profile-snapshot.js";
|
|
22
|
+
import { recoverCoordinator } from "./recovery.js";
|
|
23
|
+
import { observeClockJump } from "./clock-jump.js";
|
|
24
|
+
import { admitNext } from "./admission.js";
|
|
25
|
+
import { workerSessionPayload } from "./session-progress.js";
|
|
26
|
+
import { runtimeAvailability } from "../repository/runtime-availability.js";
|
|
27
|
+
import { deferLeasedWorkItemForUsageCap } from "./usage-cap-defer.js";
|
|
28
|
+
import { outcomeShouldRecordError, reasonForWorkerFailedEvent } from "./failure-reason.js";
|
|
29
|
+
const num = (name, dflt) => Number(process.env[name] ?? dflt);
|
|
30
|
+
export const coordinatorConfig = {
|
|
31
|
+
get maxConcurrency() {
|
|
32
|
+
return num("MAX_COORDINATOR_CONCURRENCY", 2);
|
|
33
|
+
},
|
|
34
|
+
get leaseMs() {
|
|
35
|
+
// Default 60s — coordinator Node-worker liveness, not Cursor think time. See
|
|
36
|
+
// docs/TROUBLESHOOTING.md#coordinator-lease--heartbeat-not-113 (NOT-113 decision).
|
|
37
|
+
return num("COORDINATOR_LEASE_MS", 60_000);
|
|
38
|
+
},
|
|
39
|
+
get heartbeatMs() {
|
|
40
|
+
return num("COORDINATOR_HEARTBEAT_MS", 15_000);
|
|
41
|
+
},
|
|
42
|
+
get failBackoffMs() {
|
|
43
|
+
return num("COORDINATOR_FAIL_BACKOFF_MS", 10_000);
|
|
44
|
+
},
|
|
45
|
+
get pollIntervalMs() {
|
|
46
|
+
return num("COORDINATOR_POLL_INTERVAL_MS", 3_000);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const roleFor = {
|
|
50
|
+
developer: "developer",
|
|
51
|
+
reviewer: "reviewer",
|
|
52
|
+
};
|
|
53
|
+
// Keyed by lease token, not work-item id: recovery can requeue an expired item while its
|
|
54
|
+
// zombie handler is still unwinding, and a later tick may re-claim the same id. Keying by
|
|
55
|
+
// the per-attempt token keeps the two tracked separately, so the concurrency bound holds
|
|
56
|
+
// and drainCoordinator awaits both.
|
|
57
|
+
const active = new Map();
|
|
58
|
+
/** Visible for tests — the number of effect attempts this process is running. */
|
|
59
|
+
export function activeAttemptCount() {
|
|
60
|
+
return active.size;
|
|
61
|
+
}
|
|
62
|
+
function isFailureOutcome(outcome) {
|
|
63
|
+
return outcome.kind === "session_failed" || outcome.kind === "publish_failed" || outcome.kind === "deck_failure";
|
|
64
|
+
}
|
|
65
|
+
/** `timed_out` is the only outcome meaning the agent process itself hit its wall clock —
|
|
66
|
+
* every other kind (no_pr, dirty_worktree, checks_failed, ...) is a session that ran to
|
|
67
|
+
* completion and handed off a (possibly unwanted) verified result. */
|
|
68
|
+
function isTimedOutOutcome(outcome) {
|
|
69
|
+
return outcome.kind === "timed_out";
|
|
70
|
+
}
|
|
71
|
+
/** completeSession is bookkeeping — its failure must never re-route or revive a work item. */
|
|
72
|
+
function safeCompleteSession(sessionId, status, error) {
|
|
73
|
+
try {
|
|
74
|
+
completeSession(sessionId, {
|
|
75
|
+
status,
|
|
76
|
+
errorJson: error !== undefined ? JSON.stringify(error) : undefined,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error("[coordinator] completeSession", sessionId, err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A failed effect (handler threw, or lost its lease mid-run). One transaction, fenced on
|
|
85
|
+
* the lease token: either requeue with backoff, or — at the attempt cap — CAS to `dead`
|
|
86
|
+
* AND route it as `session_failed` together, so a crash can't strand the workflow with a
|
|
87
|
+
* dead item and no next effect.
|
|
88
|
+
*/
|
|
89
|
+
function handleEffectFailure(itemId, leaseToken, error) {
|
|
90
|
+
getDb().transaction(() => {
|
|
91
|
+
const item = getWorkItem(itemId);
|
|
92
|
+
if (!item || item.status !== "leased")
|
|
93
|
+
return; // already reclaimed / finished by a peer
|
|
94
|
+
if (!attemptCapReached(item)) {
|
|
95
|
+
requeueWorkItem(itemId, leaseToken, error, { backoffMs: coordinatorConfig.failBackoffMs });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const dead = finishWorkItem(itemId, leaseToken, { status: "dead", error });
|
|
99
|
+
if (!dead)
|
|
100
|
+
return; // a peer won the CAS
|
|
101
|
+
const issue = getIssue(dead.issueId);
|
|
102
|
+
const instance = getActiveWorkflowInstance(dead.issueId);
|
|
103
|
+
if (issue && instance && instance.id === dead.workflowInstanceId) {
|
|
104
|
+
const reason = error && typeof error === "object" && "reason" in error && typeof error.reason === "string"
|
|
105
|
+
? error.reason
|
|
106
|
+
: error && typeof error === "object" && "error" in error && typeof error.error === "string"
|
|
107
|
+
? error.error
|
|
108
|
+
: String(error);
|
|
109
|
+
routeAppliedOutcome(issue, instance, dead, { kind: "session_failed", reason });
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
}
|
|
113
|
+
async function processWorkItem(claimed) {
|
|
114
|
+
const leaseToken = claimed.leaseToken;
|
|
115
|
+
if (!leaseToken)
|
|
116
|
+
return; // not actually leased — defensive
|
|
117
|
+
const issue = getIssue(claimed.issueId);
|
|
118
|
+
const instance = getWorkflowInstance(claimed.workflowInstanceId);
|
|
119
|
+
if (!issue || !instance) {
|
|
120
|
+
handleEffectFailure(claimed.id, leaseToken, { reason: "issue or workflow instance vanished" });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let payload = {};
|
|
124
|
+
try {
|
|
125
|
+
if (claimed.payloadJson)
|
|
126
|
+
payload = JSON.parse(claimed.payloadJson);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
payload = {};
|
|
130
|
+
}
|
|
131
|
+
const inputSha = payload.inputSha ?? null;
|
|
132
|
+
// The execution-profile snapshot was frozen into the work-item payload when the item
|
|
133
|
+
// was *queued* (commands.ts), so a profile edit between enqueue and claim can never
|
|
134
|
+
// change this session (design §"Immutable execution-profile snapshot" / NOT-60
|
|
135
|
+
// acceptance criteria). Fall back to a live resolve only for a legacy item queued
|
|
136
|
+
// before the snapshot was carried on the payload.
|
|
137
|
+
const role = roleFor[claimed.kind];
|
|
138
|
+
const agentId = claimed.kind === "developer" ? issue.developerAgentId : issue.reviewerAgentId;
|
|
139
|
+
let snapshot = parseProfileSnapshot(payload.profileSnapshot);
|
|
140
|
+
if (!snapshot) {
|
|
141
|
+
const agent = agentId ? getAgent(agentId) : null;
|
|
142
|
+
snapshot = agent ? buildProfileSnapshot(agent, role) : null;
|
|
143
|
+
}
|
|
144
|
+
const runtime = snapshot?.runtime ?? null;
|
|
145
|
+
if (runtime) {
|
|
146
|
+
const avail = runtimeAvailability(runtime);
|
|
147
|
+
if (!avail.available) {
|
|
148
|
+
const cap = {
|
|
149
|
+
kind: "usage_capped",
|
|
150
|
+
until: avail.until,
|
|
151
|
+
reason: avail.reason,
|
|
152
|
+
};
|
|
153
|
+
const deferResult = deferLeasedWorkItemForUsageCap(claimed, leaseToken, cap, issue, instance);
|
|
154
|
+
if (deferResult.deferred)
|
|
155
|
+
return;
|
|
156
|
+
if (deferResult.escalated) {
|
|
157
|
+
getDb().transaction(() => {
|
|
158
|
+
const finished = finishWorkItem(claimed.id, leaseToken, { status: "done", result: cap });
|
|
159
|
+
if (finished)
|
|
160
|
+
routeCapEscalation(issue, instance, finished, cap);
|
|
161
|
+
})();
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Create + bind + start the session
|
|
167
|
+
// leaves a running session that recovery (which keys off work_items) cannot locate. The
|
|
168
|
+
// bind is fenced on the lease token: if this attempt lost its lease between claim and
|
|
169
|
+
// here, the transaction rolls back (session creation undone) and the attempt is dropped.
|
|
170
|
+
let session;
|
|
171
|
+
try {
|
|
172
|
+
session = getDb().transaction(() => {
|
|
173
|
+
const s = createWorkerSession({
|
|
174
|
+
issueId: claimed.issueId,
|
|
175
|
+
role,
|
|
176
|
+
round: claimed.round,
|
|
177
|
+
agentId,
|
|
178
|
+
runtime: snapshot?.runtime ?? null,
|
|
179
|
+
model: snapshot?.model ?? null,
|
|
180
|
+
budgetJson: snapshot?.budgetJson ?? null,
|
|
181
|
+
inputSha,
|
|
182
|
+
metadataJson: JSON.stringify({ workItemId: claimed.id }),
|
|
183
|
+
profileSnapshotJson: snapshot ? serializeProfileSnapshot(snapshot) : null,
|
|
184
|
+
});
|
|
185
|
+
if (!bindWorkItemSession(claimed.id, s.id, leaseToken)) {
|
|
186
|
+
throw new Error("lease lost before session setup");
|
|
187
|
+
}
|
|
188
|
+
startSession(s.id);
|
|
189
|
+
// NOT-109: attribute start to the role (not system) so Issue Detail shows
|
|
190
|
+
// "Developer started" / "Reviewer started" with a role badge, and carry runtime /
|
|
191
|
+
// model / session id for ops debugging.
|
|
192
|
+
appendWorkflowEvent({
|
|
193
|
+
issueId: claimed.issueId,
|
|
194
|
+
workflowInstanceId: instance.id,
|
|
195
|
+
workerSessionId: s.id,
|
|
196
|
+
type: "worker.started",
|
|
197
|
+
actorType: role,
|
|
198
|
+
stage: issue.status,
|
|
199
|
+
round: claimed.round,
|
|
200
|
+
payload: workerSessionPayload({
|
|
201
|
+
runtime: snapshot?.runtime ?? null,
|
|
202
|
+
model: snapshot?.model ?? null,
|
|
203
|
+
sessionId: s.id,
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
return s;
|
|
207
|
+
})();
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
console.error("[coordinator] session setup", claimed.id, err);
|
|
211
|
+
return; // lost the lease — recovery will reprocess the item when the lease expires
|
|
212
|
+
}
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const heartbeat = setInterval(() => {
|
|
215
|
+
if (refreshHeartbeat(claimed.id, leaseToken, { leaseMs: coordinatorConfig.leaseMs })) {
|
|
216
|
+
heartbeatSession(session.id);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
controller.abort();
|
|
220
|
+
}
|
|
221
|
+
}, coordinatorConfig.heartbeatMs);
|
|
222
|
+
let outcome;
|
|
223
|
+
try {
|
|
224
|
+
outcome = await getEffectHandler(claimed.kind)({
|
|
225
|
+
workItem: { ...claimed, workerSessionId: session.id },
|
|
226
|
+
issue,
|
|
227
|
+
instance,
|
|
228
|
+
signal: controller.signal,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
clearInterval(heartbeat);
|
|
233
|
+
try {
|
|
234
|
+
handleEffectFailure(claimed.id, leaseToken, { error: String(err) });
|
|
235
|
+
}
|
|
236
|
+
catch (routeErr) {
|
|
237
|
+
// routing threw — leave the item leased for recovery to retry
|
|
238
|
+
console.error("[coordinator] handleEffectFailure", claimed.id, routeErr);
|
|
239
|
+
}
|
|
240
|
+
safeCompleteSession(session.id, "failed", { error: String(err) });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
clearInterval(heartbeat);
|
|
244
|
+
// The completion CAS is fenced on leaseToken and is the only path to a terminal state —
|
|
245
|
+
// a duplicate or a reclaimed-then-late completion applies nothing.
|
|
246
|
+
const result = await applyCompletion(claimed.id, leaseToken, outcome);
|
|
247
|
+
if (!result.applied) {
|
|
248
|
+
safeCompleteSession(session.id, "cancelled", { reason: result.reason });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (result.triggerReflect) {
|
|
252
|
+
const { triggerIssueReflect } = await import("./reflect-trigger.js");
|
|
253
|
+
void triggerIssueReflect(claimed.issueId).catch(() => { });
|
|
254
|
+
}
|
|
255
|
+
// NOT-136: a deck-unavailable session never spawned anything — `cancelled`, not `failed`
|
|
256
|
+
// (which would read as a worker crash) and not `done` (which would claim it ran).
|
|
257
|
+
if (outcome.kind === "deck_unavailable") {
|
|
258
|
+
safeCompleteSession(session.id, "cancelled", { reason: outcome.reason });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const sessionStatus = outcome.kind === "usage_capped"
|
|
262
|
+
? "done"
|
|
263
|
+
: isTimedOutOutcome(outcome)
|
|
264
|
+
? "timed_out"
|
|
265
|
+
: isFailureOutcome(outcome)
|
|
266
|
+
? "failed"
|
|
267
|
+
: "done";
|
|
268
|
+
// NOT-113: persist a prose reason on failure-ish completions (including dirty_worktree,
|
|
269
|
+
// which stays status=done for preservation but still needs errorJson for the detail strip).
|
|
270
|
+
const errorPayload = outcomeShouldRecordError(outcome)
|
|
271
|
+
? {
|
|
272
|
+
reason: reasonForWorkerFailedEvent({
|
|
273
|
+
outcome,
|
|
274
|
+
logPath: session.logPath,
|
|
275
|
+
runtime: session.runtime ?? undefined,
|
|
276
|
+
}),
|
|
277
|
+
}
|
|
278
|
+
: undefined;
|
|
279
|
+
safeCompleteSession(session.id, sessionStatus, errorPayload);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Claims and starts as many work items as free concurrency slots allow, then returns the
|
|
283
|
+
* count started. Not awaited internally — each item runs to completion in the background
|
|
284
|
+
* and is tracked in `active`.
|
|
285
|
+
*
|
|
286
|
+
* Each tick also runs `recoverCoordinator` so expired leases are reclaimed without waiting
|
|
287
|
+
* for another process restart (NOT-116). Startup recovery alone is too early when the
|
|
288
|
+
* coordinator restarts while leases are still valid — heartbeats are gone, then leases
|
|
289
|
+
* expire later with nothing polling reclaim.
|
|
290
|
+
*/
|
|
291
|
+
export async function runCoordinatorTick(opts) {
|
|
292
|
+
// NOT-116 / NOT-102: reclaim expired leases + finish auto-merges stranded before a crash.
|
|
293
|
+
// recoverCoordinator also runs recoverStrandedAutoMerges.
|
|
294
|
+
try {
|
|
295
|
+
await recoverCoordinator({ now: opts?.now });
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
console.error("[coordinator] recoverCoordinator", err);
|
|
299
|
+
}
|
|
300
|
+
// NOT-103: level-triggered admission — free slot + eligible queued entry → startWorkflowCore.
|
|
301
|
+
// Not hooked into terminal transitions; polling recovers correctly after restart.
|
|
302
|
+
try {
|
|
303
|
+
await admitNext();
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
console.error("[coordinator] admitNext", err);
|
|
307
|
+
}
|
|
308
|
+
const leaseOwner = opts?.leaseOwner ?? `loop-${process.pid}-${uuid().slice(0, 8)}`;
|
|
309
|
+
let started = 0;
|
|
310
|
+
while (active.size < coordinatorConfig.maxConcurrency) {
|
|
311
|
+
const item = claimWorkItem(leaseOwner, { leaseMs: coordinatorConfig.leaseMs });
|
|
312
|
+
if (!item || !item.leaseToken)
|
|
313
|
+
break;
|
|
314
|
+
const key = item.leaseToken;
|
|
315
|
+
const job = processWorkItem(item)
|
|
316
|
+
.catch((err) => console.error("[coordinator] processWorkItem", item.id, err))
|
|
317
|
+
.finally(() => active.delete(key));
|
|
318
|
+
active.set(key, job);
|
|
319
|
+
started++;
|
|
320
|
+
}
|
|
321
|
+
return started;
|
|
322
|
+
}
|
|
323
|
+
/** Awaits every in-flight work item — for tests and graceful shutdown. */
|
|
324
|
+
export async function drainCoordinator() {
|
|
325
|
+
await Promise.all([...active.values()]);
|
|
326
|
+
}
|
|
327
|
+
let loopTimer = null;
|
|
328
|
+
let ticking = false;
|
|
329
|
+
export function startCoordinatorLoop() {
|
|
330
|
+
if (loopTimer)
|
|
331
|
+
return;
|
|
332
|
+
const leaseOwner = `loop-${process.pid}-${uuid().slice(0, 8)}`;
|
|
333
|
+
loopTimer = setInterval(() => {
|
|
334
|
+
// NOT-125: sampled HERE, in the timer itself, rather than inside runCoordinatorTick.
|
|
335
|
+
// Half of what this measures is "did the poll timer fire on schedule?", and only this
|
|
336
|
+
// callback runs on a schedule — runCoordinatorTick is also driven ad hoc (tests, manual
|
|
337
|
+
// recovery), where the gap between two calls means nothing and would read as a sleep.
|
|
338
|
+
// Before the `ticking` guard, too, so a tick that overruns the interval still leaves the
|
|
339
|
+
// clock baseline advancing at the true cadence.
|
|
340
|
+
const jump = observeClockJump();
|
|
341
|
+
if (jump) {
|
|
342
|
+
console.warn(`[coordinator] host clock jumped ${Math.round(jump.wallGapMs / 1000)}s between poll ticks — likely host sleep`, {
|
|
343
|
+
unelapsedMs: Math.round(jump.unelapsedMs),
|
|
344
|
+
unobservedMs: Math.round(jump.unobservedMs),
|
|
345
|
+
graceUntil: new Date(jump.graceUntil).toISOString(),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (ticking)
|
|
349
|
+
return; // non-overlapping polling
|
|
350
|
+
ticking = true;
|
|
351
|
+
runCoordinatorTick({ leaseOwner })
|
|
352
|
+
.catch((err) => console.error("[coordinator] tick", err))
|
|
353
|
+
.finally(() => {
|
|
354
|
+
ticking = false;
|
|
355
|
+
});
|
|
356
|
+
}, coordinatorConfig.pollIntervalMs);
|
|
357
|
+
}
|
|
358
|
+
export function stopCoordinatorLoop() {
|
|
359
|
+
if (loopTimer)
|
|
360
|
+
clearInterval(loopTimer);
|
|
361
|
+
loopTimer = null;
|
|
362
|
+
ticking = false;
|
|
363
|
+
}
|