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.
- package/bundle/server/dist/adapters/agent-health.js +21 -0
- package/bundle/server/dist/capacity/claude-events.js +25 -5
- package/bundle/server/dist/capacity/claude-local-cache.js +159 -13
- package/bundle/server/dist/capacity/claude-local-cache.test.js +191 -3
- package/bundle/server/dist/coordinator/auto-merge.js +8 -1
- package/bundle/server/dist/coordinator/commands.js +153 -13
- package/bundle/server/dist/coordinator/delete-issue.js +473 -0
- package/bundle/server/dist/coordinator/human-resolution.js +4 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +292 -2
- package/bundle/server/dist/coordinator/projection.js +26 -3
- package/bundle/server/dist/coordinator/projection.test.js +8 -1
- package/bundle/server/dist/coordinator/routing.js +96 -0
- package/bundle/server/dist/coordinator/routing.test.js +163 -0
- package/bundle/server/dist/coordinator/runtime-auth-park.js +97 -0
- package/bundle/server/dist/coordinator/source-attachments.js +24 -1
- package/bundle/server/dist/coordinator/source-attachments.test.js +112 -0
- package/bundle/server/dist/coordinator/worker-loop.js +15 -0
- package/bundle/server/dist/db/index.js +4 -0
- package/bundle/server/dist/db/schema.sql +4 -0
- package/bundle/server/dist/docs-host-awake.test.js +16 -0
- package/bundle/server/dist/index.js +14 -1
- package/bundle/server/dist/power/host-awake-lifecycle.js +30 -0
- package/bundle/server/dist/power/host-awake-lifecycle.test.js +181 -0
- package/bundle/server/dist/power/host-awake.js +162 -0
- package/bundle/server/dist/power/host-awake.test.js +192 -0
- package/bundle/server/dist/power/sleep-timer.js +86 -0
- package/bundle/server/dist/power/sleep-timer.test.js +79 -0
- package/bundle/server/dist/power/status.js +31 -0
- package/bundle/server/dist/power/status.test.js +60 -0
- package/bundle/server/dist/repository/runtime-capacity.js +19 -3
- package/bundle/server/dist/routes/index.js +3 -0
- package/bundle/server/dist/routes/issues-delete.test.js +666 -0
- package/bundle/server/dist/routes/issues-source-attachments.test.js +36 -0
- package/bundle/server/dist/routes/issues.js +20 -0
- package/bundle/server/dist/routes/runtime-capacity.test.js +35 -0
- package/bundle/server/package.json +4 -3
- package/bundle/server/static-ui/assets/index-DL8mEXzN.css +1 -0
- package/bundle/server/static-ui/assets/index-FCHzrnLr.js +64 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/runtime-capacity.d.ts +147 -0
- package/bundle/shared/dist/runtime-capacity.js +15 -0
- package/bundle/shared/package.json +1 -1
- package/dist/bin.js +3 -0
- package/dist/node-hardening.d.ts +9 -0
- package/dist/node-hardening.js +38 -0
- package/dist/ports.js +54 -14
- package/dist/stop-einval.test.d.ts +1 -0
- package/dist/stop-einval.test.js +215 -0
- package/dist/stop.d.ts +6 -1
- package/dist/stop.js +18 -7
- package/dist/version-output.test.js +23 -16
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-COYJxD1y.css +0 -1
- 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
|
-
:
|
|
1288
|
-
? { [
|
|
1289
|
-
:
|
|
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
|