agent-dealer 1.2.14 → 1.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/bundle/server/dist/adapters/agent-health.js +21 -0
  2. package/bundle/server/dist/capacity/claude-events.js +25 -5
  3. package/bundle/server/dist/capacity/claude-local-cache.js +159 -13
  4. package/bundle/server/dist/capacity/claude-local-cache.test.js +191 -3
  5. package/bundle/server/dist/coordinator/auto-merge.js +8 -1
  6. package/bundle/server/dist/coordinator/commands.js +153 -13
  7. package/bundle/server/dist/coordinator/delete-issue.js +473 -0
  8. package/bundle/server/dist/coordinator/human-resolution.js +4 -0
  9. package/bundle/server/dist/coordinator/human-resolution.test.js +292 -2
  10. package/bundle/server/dist/coordinator/projection.js +26 -3
  11. package/bundle/server/dist/coordinator/projection.test.js +8 -1
  12. package/bundle/server/dist/coordinator/routing.js +96 -0
  13. package/bundle/server/dist/coordinator/routing.test.js +163 -0
  14. package/bundle/server/dist/coordinator/runtime-auth-park.js +97 -0
  15. package/bundle/server/dist/coordinator/source-attachments.js +24 -1
  16. package/bundle/server/dist/coordinator/source-attachments.test.js +112 -0
  17. package/bundle/server/dist/coordinator/worker-loop.js +15 -0
  18. package/bundle/server/dist/db/index.js +4 -0
  19. package/bundle/server/dist/db/schema.sql +4 -0
  20. package/bundle/server/dist/docs-host-awake.test.js +16 -0
  21. package/bundle/server/dist/index.js +14 -1
  22. package/bundle/server/dist/power/host-awake-lifecycle.js +30 -0
  23. package/bundle/server/dist/power/host-awake-lifecycle.test.js +181 -0
  24. package/bundle/server/dist/power/host-awake.js +162 -0
  25. package/bundle/server/dist/power/host-awake.test.js +192 -0
  26. package/bundle/server/dist/power/sleep-timer.js +86 -0
  27. package/bundle/server/dist/power/sleep-timer.test.js +79 -0
  28. package/bundle/server/dist/power/status.js +31 -0
  29. package/bundle/server/dist/power/status.test.js +60 -0
  30. package/bundle/server/dist/repository/runtime-capacity.js +19 -3
  31. package/bundle/server/dist/routes/index.js +3 -0
  32. package/bundle/server/dist/routes/issues-delete.test.js +666 -0
  33. package/bundle/server/dist/routes/issues-source-attachments.test.js +36 -0
  34. package/bundle/server/dist/routes/issues.js +20 -0
  35. package/bundle/server/dist/routes/runtime-capacity.test.js +35 -0
  36. package/bundle/server/package.json +4 -3
  37. package/bundle/server/static-ui/assets/index-DL8mEXzN.css +1 -0
  38. package/bundle/server/static-ui/assets/index-FCHzrnLr.js +64 -0
  39. package/bundle/server/static-ui/index.html +2 -2
  40. package/bundle/shared/dist/runtime-capacity.d.ts +147 -0
  41. package/bundle/shared/dist/runtime-capacity.js +15 -0
  42. package/bundle/shared/package.json +1 -1
  43. package/dist/bin.js +3 -0
  44. package/dist/node-hardening.d.ts +9 -0
  45. package/dist/node-hardening.js +38 -0
  46. package/dist/ports.js +54 -14
  47. package/dist/stop-einval.test.d.ts +1 -0
  48. package/dist/stop-einval.test.js +215 -0
  49. package/dist/stop.d.ts +6 -1
  50. package/dist/stop.js +18 -7
  51. package/dist/version-output.test.js +23 -16
  52. package/package.json +1 -1
  53. package/bundle/server/static-ui/assets/index-COYJxD1y.css +0 -1
  54. package/bundle/server/static-ui/assets/index-DDbJ9ZU7.js +0 -64
@@ -7,7 +7,7 @@
7
7
  // duplicate delivery is a no-op. The effect *work* itself runs elsewhere, through a leased
8
8
  // worker whose structured result comes back into applyCompletion.
9
9
  import fs from "node:fs";
10
- import { canTransitionIssue, ExecutionContractError, tryCompileContract } from "@agent-dealer/shared";
10
+ import { canTransitionIssue, ExecutionContractError, isCursorKeychainStuckOutput, runtimeAuthClassificationForLog, tryCompileContract, } from "@agent-dealer/shared";
11
11
  import { getDb } from "../db/index.js";
12
12
  import { getIssue, incrementIssueRound, incrementIssueInfraAttempts, incrementIssueCiAttempts, resetIssueInfraAttempts, resetIssueCiAttempts, grantReviewRetry, transitionIssue, updateIssue, } from "../repository/issues.js";
13
13
  import { appendWorkflowEvent, completeWorkflowInstance, getActiveWorkflowInstance, getWorkflowInstance, listWorkflowEventsForIssue, startWorkflowInstance, WorkflowAlreadyActiveError, } from "../repository/workflow-events.js";
@@ -17,7 +17,7 @@ import { ensureIssueRepoCheckout } from "../adapters/managed-repo.js";
17
17
  import { reconcileFinding, resolveFindingsAbsentFromRound } from "../repository/findings.js";
18
18
  import { normalizeReviewerResult } from "./reviewer-result.js";
19
19
  import { getAgent } from "../repository/agents.js";
20
- import { githubIssuesSync, invalidateMuseHealthCache } from "../adapters/agent-health.js";
20
+ import { githubIssuesSync, invalidateMuseHealthCache, runtimeIssuesUncached } from "../adapters/agent-health.js";
21
21
  import { recordMuseCapabilityOverride } from "../adapters/muse-capability.js";
22
22
  import { createIssueArtifact, latestIssueArtifact } from "../repository/artifacts.js";
23
23
  import { listSourceAttachments } from "../repository/source-attachments.js";
@@ -26,11 +26,12 @@ import { completeSession, getActiveWorkerSessionForIssue, getWorkerSession, list
26
26
  import { killRunProcess } from "../runners/spawn-cli.js";
27
27
  import { buildProfileSnapshot, serializeProfileSnapshot } from "./profile-snapshot.js";
28
28
  import { workerSessionPayload } from "./session-progress.js";
29
- import { reasonForWorkerFailedEvent } from "./failure-reason.js";
30
- import { recordCausesForWorkerFailedEvent } from "./failure-cause.js";
29
+ import { reasonForWorkerFailedEvent, readSpawnLogFailureText } from "./failure-reason.js";
30
+ import { classifyAttemptFailure, recordCausesForWorkerFailedEvent } from "./failure-cause.js";
31
31
  import { routeDeveloperOutcome, routeReviewerOutcome, } from "./routing.js";
32
32
  import { projectDeveloperRoute, projectReviewerRoute } from "./projection.js";
33
33
  import { MERGE_CONFLICT_FILES_EVIDENCE_KEY, MERGE_FAILURE_EVIDENCE_KEY, MERGE_FAILURE_RESPONSE_OPTIONS, PUSH_DIVERGENCE_EVIDENCE_KEY, PUSH_DIVERGENCE_RESPONSE_OPTIONS, WORKTREE_BLOCKER_EVIDENCE_KEY, normalizeResolutionNote, parseHumanResolution, resolveHumanActionOutcome, } from "./human-resolution.js";
34
+ import { AUTH_TRANSIENT_RETRY_PAYLOAD_KEY, RUNTIME_AUTH_PARK_EVIDENCE_KEY, authProbeConfirmsFailure, defaultRemediationForRuntime, gateRuntimeAuthParkResume, parseRuntimeAuthParkEvidence, remediationFromProbe, } from "./runtime-auth-park.js";
34
35
  import { AUTO_MERGE_INTENT, finalizeAutoMerge } from "./auto-merge.js";
35
36
  import { triggerLinearPostMerge } from "./linear-merge-verify.js";
36
37
  import { conflictRepairSpent, queueConflictRepairRound } from "./merge-conflict-sync.js";
@@ -482,6 +483,25 @@ export async function applyCompletion(workItemId, leaseToken, outcome) {
482
483
  if (outcome.kind === "base_conflict") {
483
484
  return applyBaseConflictCompletion(workItemId, leaseToken, outcome);
484
485
  }
486
+ // NOT-368: classify + live-probe outside the write txn (network/CLI must never hold
487
+ // the SQLite lock). Developer and reviewer session deaths both participate — medium /
488
+ // unattributed keep today's infra-retry path inside the role's route function.
489
+ let authFailure;
490
+ const authKind = getWorkItem(workItemId)?.kind;
491
+ if ((outcome.kind === "session_failed" || outcome.kind === "timed_out") &&
492
+ (authKind === "developer" || authKind === "reviewer")) {
493
+ try {
494
+ authFailure = await prepareAuthFailureRouting(workItemId, {
495
+ kind: outcome.kind,
496
+ reason: "reason" in outcome ? outcome.reason : undefined,
497
+ logPath: "logPath" in outcome ? outcome.logPath : undefined,
498
+ });
499
+ }
500
+ catch (err) {
501
+ console.error("[coordinator] prepareAuthFailureRouting", workItemId, err);
502
+ authFailure = undefined;
503
+ }
504
+ }
485
505
  const routed = getDb().transaction(() => {
486
506
  const before = getWorkItem(workItemId);
487
507
  if (!before)
@@ -499,7 +519,7 @@ export async function applyCompletion(workItemId, leaseToken, outcome) {
499
519
  const item = finishWorkItem(workItemId, leaseToken, { status: "done", result: outcome });
500
520
  if (!item)
501
521
  return { applied: false, reason: "lease_lost" };
502
- return routeAppliedOutcome(issue, instance, item, outcome);
522
+ return routeAppliedOutcome(issue, instance, item, outcome, authFailure);
503
523
  })();
504
524
  if (routed.applied && routed.pendingAutoMerge) {
505
525
  // Await async `gh` merge outside the routing txn — never block the event loop with spawnSync.
@@ -507,6 +527,74 @@ export async function applyCompletion(workItemId, leaseToken, outcome) {
507
527
  }
508
528
  return routed;
509
529
  }
530
+ /**
531
+ * NOT-368: when a developer or reviewer session_failed / timed_out with a high-confidence
532
+ * attributed auth cause, run the runtime's live auth probe and gather consecutive-park /
533
+ * transient-retry state for the role's route function.
534
+ */
535
+ async function prepareAuthFailureRouting(workItemId, outcome) {
536
+ const item = getWorkItem(workItemId);
537
+ if (!item || (item.kind !== "developer" && item.kind !== "reviewer"))
538
+ return undefined;
539
+ const session = item.workerSessionId ? getWorkerSession(item.workerSessionId) : null;
540
+ const logPath = session?.logPath ?? outcome.logPath ?? null;
541
+ const runtimeHint = (session?.runtime ?? null);
542
+ const causes = classifyAttemptFailure({
543
+ outcomeKind: outcome.kind,
544
+ outcomeReason: outcome.reason ?? null,
545
+ logPath,
546
+ runtime: runtimeHint,
547
+ });
548
+ const primary = causes.find((c) => c.primary) ?? causes[0];
549
+ if (!primary || primary.code !== "authentication_configuration")
550
+ return undefined;
551
+ // Medium / unattributed ("Not logged in" with no vendor) keep the generic infra retry.
552
+ if (primary.confidence !== "high")
553
+ return undefined;
554
+ const haystack = `${readSpawnLogFailureText(logPath)}\n${outcome.reason ?? ""}`;
555
+ const classified = isCursorKeychainStuckOutput(haystack)
556
+ ? { runtime: "cursor_local", issue: { code: "cursor_keychain", message: defaultRemediationForRuntime("cursor_local", true) } }
557
+ : runtimeAuthClassificationForLog(haystack, runtimeHint);
558
+ const runtime = classified?.runtime ?? runtimeHint;
559
+ if (!runtime)
560
+ return undefined;
561
+ const remediation = classified?.issue.message?.trim() ||
562
+ defaultRemediationForRuntime(runtime, classified?.issue.code === "cursor_keychain");
563
+ const probeIssues = await runtimeIssuesUncached(runtime);
564
+ const probeStillFailing = authProbeConfirmsFailure(probeIssues);
565
+ const probeRemediation = remediationFromProbe(probeIssues, remediation);
566
+ return {
567
+ confidence: "high",
568
+ runtime,
569
+ remediation: probeStillFailing ? probeRemediation : remediation,
570
+ rawCause: primary.rawReason,
571
+ probeStillFailing,
572
+ consecutiveAuthParks: countConsecutiveAuthParks(item.issueId),
573
+ authTransientRetrySpent: issueSpentAuthTransientRetry(item.issueId),
574
+ };
575
+ }
576
+ /** Count trailing auth-park human actions on the issue (newest streak). */
577
+ function countConsecutiveAuthParks(issueId) {
578
+ const actions = listHumanActionsForIssue(issueId);
579
+ let n = 0;
580
+ for (let i = actions.length - 1; i >= 0; i--) {
581
+ if (parseRuntimeAuthParkEvidence(actions[i].evidenceJson) == null)
582
+ break;
583
+ n++;
584
+ }
585
+ return n;
586
+ }
587
+ /** True when any prior developer/reviewer work item on this issue spent the transient auth retry. */
588
+ function issueSpentAuthTransientRetry(issueId) {
589
+ for (const w of listWorkItemsForIssue(issueId)) {
590
+ if (w.kind !== "developer" && w.kind !== "reviewer")
591
+ continue;
592
+ const payload = parseWorkItemPayload(w.payloadJson);
593
+ if (payload[AUTH_TRANSIENT_RETRY_PAYLOAD_KEY] === true)
594
+ return true;
595
+ }
596
+ return false;
597
+ }
510
598
  function parseWorkItemPayload(json) {
511
599
  if (!json)
512
600
  return {};
@@ -906,10 +994,10 @@ export function routeCapEscalation(issue, instance, item, cap) {
906
994
  instanceCompleted: false,
907
995
  };
908
996
  }
909
- export function routeAppliedOutcome(issue, instance, item, outcome) {
997
+ export function routeAppliedOutcome(issue, instance, item, outcome, authFailure) {
910
998
  return item.kind === "developer"
911
- ? applyDeveloper(issue, instance, item, outcome)
912
- : applyReviewer(issue, instance, item, outcome);
999
+ ? applyDeveloper(issue, instance, item, outcome, authFailure)
1000
+ : applyReviewer(issue, instance, item, outcome, authFailure);
913
1001
  }
914
1002
  function eventEmitter(issue, instance, workerSessionId, stage, round) {
915
1003
  let causation = null;
@@ -955,7 +1043,7 @@ function applyProjectionTransition(issue, projection, patch) {
955
1043
  ...patch,
956
1044
  });
957
1045
  }
958
- function applyDeveloper(issue, instance, item, outcome) {
1046
+ function applyDeveloper(issue, instance, item, outcome, authFailure) {
959
1047
  const route = routeDeveloperOutcome(outcome, {
960
1048
  currentRound: issue.currentRound,
961
1049
  maxReviewRounds: issue.maxReviewRounds,
@@ -963,6 +1051,7 @@ function applyDeveloper(issue, instance, item, outcome) {
963
1051
  maxInfraAttempts: issue.maxInfraAttempts,
964
1052
  ciAttempts: issue.ciAttempts,
965
1053
  maxCiAttempts: issue.maxCiAttempts,
1054
+ ...(authFailure ? { authFailure } : {}),
966
1055
  });
967
1056
  const { projection, effect, advance } = projectDeveloperRoute(route, issue.status, issue.currentRound);
968
1057
  const ev = eventEmitter(issue, instance, item.workerSessionId, projection.issueStatus, issue.currentRound);
@@ -1051,13 +1140,14 @@ function applyDeveloper(issue, instance, item, outcome) {
1051
1140
  const issueNow = getIssue(issue.id);
1052
1141
  return applyEffect(issue, instance, effect, route, issueNow, ev, item.id);
1053
1142
  }
1054
- function applyReviewer(issue, instance, item, outcome) {
1143
+ function applyReviewer(issue, instance, item, outcome, authFailure) {
1055
1144
  let route = routeReviewerOutcome(outcome, {
1056
1145
  currentRound: issue.currentRound,
1057
1146
  maxReviewRounds: issue.maxReviewRounds,
1058
1147
  infraAttempts: issue.infraAttempts,
1059
1148
  maxInfraAttempts: issue.maxInfraAttempts,
1060
1149
  autoMerge: issue.autoMerge,
1150
+ ...(authFailure ? { authFailure } : {}),
1061
1151
  }, issue.headSha);
1062
1152
  // NOT-184: a repair loop that keeps finding new blocking issues on the same file is not
1063
1153
  // converging — hand it to a human instead of queuing another developer round.
@@ -1240,6 +1330,8 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
1240
1330
  ...(effect.retryReason ? { retryReason: effect.retryReason } : {}),
1241
1331
  ...(effect.publishOnly ? { publishOnly: true } : {}),
1242
1332
  ...(effect.branch ? { branch: effect.branch } : {}),
1333
+ // NOT-368: persist that this infra retry spent the one transient auth retry.
1334
+ ...(effect.authTransientRetry ? { [AUTH_TRANSIENT_RETRY_PAYLOAD_KEY]: true } : {}),
1243
1335
  profileSnapshot: queuedProfileSnapshot(issue, kind),
1244
1336
  },
1245
1337
  idempotencyKey,
@@ -1261,6 +1353,10 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
1261
1353
  const pushDivergence = actionType === "policy_escalation" && !resumeAsReviewer
1262
1354
  ? (effect.pushDivergence ?? null)
1263
1355
  : null;
1356
+ // NOT-368: confirmed runtime login park — resolve re-probes before re-queueing.
1357
+ // Reviewer-origin parks keep resumeAsReviewer (re-queue reviewer) AND auth-park
1358
+ // evidence (so resolve still re-probes); the two are not mutually exclusive.
1359
+ const runtimeAuthPark = actionType === "policy_escalation" ? (effect.runtimeAuthPark ?? null) : null;
1264
1360
  // NOT-280: an unchanged worktree blocker (same fingerprint) lands on the action it
1265
1361
  // already raised — still open, or reopened when a Resume changed nothing — instead of
1266
1362
  // opening an identical one on every Resume.
@@ -1284,9 +1380,11 @@ function applyEffect(issue, instance, effect, route, issueNow, ev, causativeItem
1284
1380
  ? { review: reviewerOutcome.result, ...(nonConvergence ? { nonConvergence } : {}) }
1285
1381
  : pushDivergence
1286
1382
  ? { [PUSH_DIVERGENCE_EVIDENCE_KEY]: pushDivergence }
1287
- : blockerFingerprint
1288
- ? { [WORKTREE_BLOCKER_EVIDENCE_KEY]: { fingerprint: blockerFingerprint } }
1289
- : undefined,
1383
+ : runtimeAuthPark
1384
+ ? { [RUNTIME_AUTH_PARK_EVIDENCE_KEY]: runtimeAuthPark }
1385
+ : blockerFingerprint
1386
+ ? { [WORKTREE_BLOCKER_EVIDENCE_KEY]: { fingerprint: blockerFingerprint } }
1387
+ : undefined,
1290
1388
  // issueNow.headSha, not issue.headSha: a stale outcome that itself exhausted the
1291
1389
  // infra budget already patched the newly observed head onto the issue above — the
1292
1390
  // pre-transition issue param would still carry the stale SHA a "resume" must not reuse.
@@ -2051,6 +2149,48 @@ export async function resolveHumanActionAndAdvanceAsync(actionId, resolvedBy, ch
2051
2149
  resumeLiveHeadSha = null;
2052
2150
  }
2053
2151
  }
2152
+ // NOT-368: re-probe before resolving a runtime-auth park. A still-failing probe keeps
2153
+ // the action open (no spawn); only a passing probe proceeds to resume on the salvaged
2154
+ // branch without charging a round or infra attempt.
2155
+ if (choice === "resume") {
2156
+ const authParkAction = getHumanAction(actionId);
2157
+ const authParkEvidence = authParkAction
2158
+ ? parseRuntimeAuthParkEvidence(authParkAction.evidenceJson)
2159
+ : null;
2160
+ if (authParkAction?.status === "open" && authParkEvidence) {
2161
+ let probeIssues = [];
2162
+ try {
2163
+ probeIssues = await runtimeIssuesUncached(authParkEvidence.runtime);
2164
+ }
2165
+ catch (err) {
2166
+ console.error("[coordinator] auth-park resolve probe", actionId, err);
2167
+ probeIssues = [
2168
+ {
2169
+ code: "runtime_auth",
2170
+ message: authParkEvidence.remediation,
2171
+ },
2172
+ ];
2173
+ }
2174
+ const gate = gateRuntimeAuthParkResume({
2175
+ evidence: authParkEvidence,
2176
+ probeStillFailing: authProbeConfirmsFailure(probeIssues),
2177
+ probeRemediation: remediationFromProbe(probeIssues, authParkEvidence.remediation),
2178
+ });
2179
+ if (!gate.proceed) {
2180
+ // Refresh the open action's reason so the operator sees the remediation again.
2181
+ try {
2182
+ updateOpenHumanAction(actionId, {
2183
+ reason: gate.message,
2184
+ question: questionFor("policy_escalation", gate.message, false),
2185
+ });
2186
+ }
2187
+ catch {
2188
+ // Best-effort — the 409 below is what keeps the action open.
2189
+ }
2190
+ return { ok: false, code: 409, error: gate.message };
2191
+ }
2192
+ }
2193
+ }
2054
2194
  // NOT-196: the `gh` PR-state read runs here, outside any DB transaction — the sync
2055
2195
  // core below only consumes the pre-read state. Only close choices on issues with a
2056
2196
  // PR number pay for the call; anything unreadable resolves to "unknown" (or