agent-dealer 1.0.2 → 1.0.3
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 +148 -40
- package/bundle/server/dist/adapters/agent-health.test.js +319 -210
- package/bundle/server/dist/adapters/linear-graphql.js +200 -6
- package/bundle/server/dist/adapters/linear-graphql.test.js +91 -1
- package/bundle/server/dist/coordinator/admission.test.js +36 -0
- package/bundle/server/dist/coordinator/branch-tip-status.js +120 -0
- package/bundle/server/dist/coordinator/branch-tip-status.test.js +189 -0
- package/bundle/server/dist/coordinator/commands.js +6 -2
- package/bundle/server/dist/coordinator/developer-effect.js +76 -12
- package/bundle/server/dist/coordinator/developer-effect.test.js +20 -0
- package/bundle/server/dist/coordinator/projection.test.js +26 -0
- package/bundle/server/dist/coordinator/prompts.js +4 -1
- package/bundle/server/dist/coordinator/prompts.test.js +12 -0
- package/bundle/server/dist/coordinator/routing.js +43 -2
- package/bundle/server/dist/coordinator/routing.test.js +51 -0
- package/bundle/server/dist/coordinator/workflows/dev-reviewer-v1.js +8 -0
- package/bundle/server/dist/coordinator/workflows/registry.js +14 -0
- package/bundle/server/dist/coordinator/workflows/registry.test.js +28 -0
- package/bundle/server/dist/coordinator/workflows/types.js +1 -0
- package/bundle/server/dist/index.js +2 -0
- package/bundle/server/dist/routes/index.js +3 -0
- package/bundle/server/dist/routes/issues.js +10 -1
- package/bundle/server/dist/routes/issues.test.js +42 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/{index-hXICi1rX.css → index-CYZRXBZj.css} +1 -1
- package/bundle/server/static-ui/assets/index-DXTVo_vz.js +60 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-0kT1vk6L.js +0 -60
|
@@ -21,23 +21,63 @@ function capHealthIssues(runtime) {
|
|
|
21
21
|
];
|
|
22
22
|
}
|
|
23
23
|
const RUNTIME_CACHE_MS = 60_000;
|
|
24
|
+
/** Soft probe failures must not stick for the full health TTL — sleep/wake flakes recover on the next tick. */
|
|
25
|
+
const SOFT_PROBE_CACHE_MS = 15_000;
|
|
26
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 8000;
|
|
27
|
+
/** After the first soft fail, retry with these delays before publishing unconfirmed auth. */
|
|
28
|
+
const DEFAULT_SOFT_RETRY_BACKOFFS_MS = [250, 750];
|
|
29
|
+
/** Require this many consecutive soft-fail rounds (each round already retried) before flipping healthy→unhealthy. */
|
|
30
|
+
const SOFT_FAIL_STREAK_TO_UNHEALTHY = 2;
|
|
31
|
+
/** Hold a recent healthy result across a single soft-fail streak after host sleep. */
|
|
32
|
+
const HEALTHY_GRACE_MS = 5 * 60_000;
|
|
24
33
|
const runtimeIssueCache = new Map();
|
|
25
34
|
let githubIssueCache = null;
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
/** Cursor soft-fail streak across health ticks (NOT-157). Reset on hard auth or success. */
|
|
36
|
+
let cursorSoftFailStreak = 0;
|
|
37
|
+
let cursorLastHealthyAt = null;
|
|
38
|
+
let probeTimeoutMsForTests = null;
|
|
39
|
+
let softRetryBackoffsMsForTests = null;
|
|
40
|
+
/**
|
|
41
|
+
* Shorten probe timeout / retry backoff in unit tests so sleep-stub scenarios stay fast.
|
|
42
|
+
* Pass `null` to restore production defaults.
|
|
43
|
+
*/
|
|
44
|
+
export function setCursorProbeTimingForTests(opts) {
|
|
45
|
+
if (opts == null) {
|
|
46
|
+
probeTimeoutMsForTests = null;
|
|
47
|
+
softRetryBackoffsMsForTests = null;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
probeTimeoutMsForTests = opts.timeoutMs === undefined ? probeTimeoutMsForTests : opts.timeoutMs;
|
|
51
|
+
softRetryBackoffsMsForTests =
|
|
52
|
+
opts.retryBackoffsMs === undefined ? softRetryBackoffsMsForTests : opts.retryBackoffsMs;
|
|
30
53
|
}
|
|
31
|
-
function
|
|
54
|
+
function probeTimeoutMs() {
|
|
55
|
+
return probeTimeoutMsForTests ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
56
|
+
}
|
|
57
|
+
function softRetryBackoffsMs() {
|
|
58
|
+
return softRetryBackoffsMsForTests ?? DEFAULT_SOFT_RETRY_BACKOFFS_MS;
|
|
59
|
+
}
|
|
60
|
+
function sleep(ms) {
|
|
61
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
}
|
|
63
|
+
function defaultRunCommand(cmd, args, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
|
32
64
|
return new Promise((resolve) => {
|
|
33
65
|
const child = spawn(cmd, args, {
|
|
34
66
|
stdio: ["ignore", "pipe", "pipe"],
|
|
35
67
|
env: process.env,
|
|
36
68
|
});
|
|
37
69
|
let output = "";
|
|
70
|
+
let settled = false;
|
|
71
|
+
const finish = (result) => {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
settled = true;
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
resolve(result);
|
|
77
|
+
};
|
|
38
78
|
const timer = setTimeout(() => {
|
|
39
79
|
child.kill("SIGTERM");
|
|
40
|
-
|
|
80
|
+
finish({ ok: false, output: output || "timeout", timedOut: true });
|
|
41
81
|
}, timeoutMs);
|
|
42
82
|
child.stdout?.on("data", (d) => {
|
|
43
83
|
output += d.toString();
|
|
@@ -46,15 +86,102 @@ function runCommand(cmd, args, timeoutMs = 8000) {
|
|
|
46
86
|
output += d.toString();
|
|
47
87
|
});
|
|
48
88
|
child.on("error", (err) => {
|
|
49
|
-
|
|
50
|
-
resolve({ ok: false, output: err.message });
|
|
89
|
+
finish({ ok: false, output: err.message, timedOut: false });
|
|
51
90
|
});
|
|
52
91
|
child.on("close", (code) => {
|
|
53
|
-
|
|
54
|
-
resolve({ ok: code === 0, output });
|
|
92
|
+
finish({ ok: code === 0, output, timedOut: false });
|
|
55
93
|
});
|
|
56
94
|
});
|
|
57
95
|
}
|
|
96
|
+
let runCommandImpl = defaultRunCommand;
|
|
97
|
+
/**
|
|
98
|
+
* Replace the process spawner in unit tests (sequence injection for soft-fail / timeout).
|
|
99
|
+
* Pass `null` to restore the real spawner.
|
|
100
|
+
*/
|
|
101
|
+
export function setRunCommandForTests(fn) {
|
|
102
|
+
runCommandImpl = fn ?? defaultRunCommand;
|
|
103
|
+
}
|
|
104
|
+
function runCommand(cmd, args, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
|
105
|
+
return runCommandImpl(cmd, args, timeoutMs);
|
|
106
|
+
}
|
|
107
|
+
/** Exported for tests — clears the shared github + runtime health caches and soft-fail streak. */
|
|
108
|
+
export function clearAgentHealthCaches() {
|
|
109
|
+
runtimeIssueCache.clear();
|
|
110
|
+
githubIssueCache = null;
|
|
111
|
+
cursorSoftFailStreak = 0;
|
|
112
|
+
cursorLastHealthyAt = null;
|
|
113
|
+
}
|
|
114
|
+
function isSoftCursorProbeIssue(issue) {
|
|
115
|
+
return (issue.code === "runtime_auth" &&
|
|
116
|
+
(/probe timed out/i.test(issue.message) || /probe failed/i.test(issue.message)));
|
|
117
|
+
}
|
|
118
|
+
function softCursorProbeIssue(result) {
|
|
119
|
+
if (result.timedOut || result.output.trim() === "timeout") {
|
|
120
|
+
return {
|
|
121
|
+
code: "runtime_auth",
|
|
122
|
+
message: "Could not confirm Cursor auth — `cursor-agent status` probe timed out",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const detail = result.output.trim().split("\n").slice(-1)[0] ?? "no output";
|
|
126
|
+
return {
|
|
127
|
+
code: "runtime_auth",
|
|
128
|
+
message: `Could not confirm Cursor auth — \`cursor-agent status\` probe failed (${detail})`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* NOT-157: classified logged-out / keychain is a hard fail (immediate). Probe timeout /
|
|
133
|
+
* unclassified non-zero exit is soft — retry with backoff, and do not flip a recent healthy
|
|
134
|
+
* result on a single soft-fail streak after host sleep.
|
|
135
|
+
*/
|
|
136
|
+
async function cursorRuntimeIssues() {
|
|
137
|
+
const backoffs = softRetryBackoffsMs();
|
|
138
|
+
const attempts = 1 + backoffs.length;
|
|
139
|
+
let last = { ok: false, output: "no probe", timedOut: false };
|
|
140
|
+
for (let i = 0; i < attempts; i++) {
|
|
141
|
+
if (i > 0)
|
|
142
|
+
await sleep(backoffs[i - 1]);
|
|
143
|
+
last = await runCommand(resolveCursorBin(), cursorInvokeArgs(["status"]), probeTimeoutMs());
|
|
144
|
+
// A failed *spawn* resolves with the error message as its output (`spawn cursor-agent
|
|
145
|
+
// ENOENT`), not with empty output — so an absent binary must be recognised here or it
|
|
146
|
+
// falls through to the unconfirmed-auth branch below and names the wrong remedy.
|
|
147
|
+
if (!last.ok && (/\bENOENT\b/.test(last.output) || (!last.output.trim() && !cursorBinExists()))) {
|
|
148
|
+
cursorSoftFailStreak = 0;
|
|
149
|
+
cursorLastHealthyAt = null;
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
code: "cli_missing",
|
|
153
|
+
message: "cursor-agent not found — run: curl https://cursor.com/install -fsS | bash",
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
const authIssue = cursorAuthIssueFromOutput(last.output);
|
|
158
|
+
if (authIssue) {
|
|
159
|
+
cursorSoftFailStreak = 0;
|
|
160
|
+
cursorLastHealthyAt = null;
|
|
161
|
+
console.warn(`[agent-health] cursor-agent status: classified auth failure (${authIssue.code})`);
|
|
162
|
+
return [authIssue];
|
|
163
|
+
}
|
|
164
|
+
if (last.ok) {
|
|
165
|
+
cursorSoftFailStreak = 0;
|
|
166
|
+
cursorLastHealthyAt = Date.now();
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
// Soft fail this attempt — try again before publishing.
|
|
170
|
+
const kind = last.timedOut || last.output.trim() === "timeout" ? "timed out" : "failed";
|
|
171
|
+
console.warn(`[agent-health] cursor-agent status probe ${kind} (attempt ${i + 1}/${attempts})`);
|
|
172
|
+
}
|
|
173
|
+
// Retries exhausted with only soft failures.
|
|
174
|
+
cursorSoftFailStreak += 1;
|
|
175
|
+
const softIssue = softCursorProbeIssue(last);
|
|
176
|
+
const kind = last.timedOut || last.output.trim() === "timeout" ? "timed out" : "failed";
|
|
177
|
+
console.warn(`[agent-health] cursor-agent status probe ${kind} after retries (streak=${cursorSoftFailStreak})`);
|
|
178
|
+
const withinGrace = cursorLastHealthyAt != null && Date.now() - cursorLastHealthyAt < HEALTHY_GRACE_MS;
|
|
179
|
+
if (withinGrace && cursorSoftFailStreak < SOFT_FAIL_STREAK_TO_UNHEALTHY) {
|
|
180
|
+
// Hold the recent healthy result — one post-sleep timeout must not park the queue.
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return [softIssue];
|
|
184
|
+
}
|
|
58
185
|
/** Exported for direct testing — bypasses the 60s cache in runtimeIssues(). */
|
|
59
186
|
export async function runtimeIssuesUncached(runtime) {
|
|
60
187
|
const issues = [];
|
|
@@ -107,34 +234,10 @@ export async function runtimeIssuesUncached(runtime) {
|
|
|
107
234
|
}
|
|
108
235
|
return issues;
|
|
109
236
|
}
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
if (!status.ok && (/\bENOENT\b/.test(status.output) || (!status.output.trim() && !cursorBinExists()))) {
|
|
115
|
-
issues.push({ code: "cli_missing", message: "cursor-agent not found — run: curl https://cursor.com/install -fsS | bash" });
|
|
116
|
-
return issues;
|
|
117
|
-
}
|
|
118
|
-
const authIssue = cursorAuthIssueFromOutput(status.output);
|
|
119
|
-
if (authIssue) {
|
|
120
|
-
issues.push(authIssue);
|
|
121
|
-
return issues;
|
|
122
|
-
}
|
|
123
|
-
// NOT-133: an unclassified *failure* of the probe itself (non-zero exit, timeout) used to
|
|
124
|
-
// be read as "healthy" and admitted the agent. Silence is not evidence of auth, so this
|
|
125
|
-
// fails closed: the agent stays unhealthy — and its issues stay queued — until the probe
|
|
126
|
-
// succeeds. That is a deliberate trade against the incident, where admitting on a guess
|
|
127
|
-
// cost 12 dead sessions and parked three issues on a human. Reported as runtime_auth so
|
|
128
|
-
// the existing agents-page CLI status renders it, with a message that says plainly the
|
|
129
|
-
// state is unconfirmed rather than asserting the agent is logged out.
|
|
130
|
-
if (!status.ok) {
|
|
131
|
-
const detail = status.output.trim().split("\n").slice(-1)[0] ?? "no output";
|
|
132
|
-
issues.push({
|
|
133
|
-
code: "runtime_auth",
|
|
134
|
-
message: `Could not confirm Cursor auth — \`cursor-agent status\` failed (${detail})`,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
return issues;
|
|
237
|
+
// NOT-157: Cursor auth probe distinguishes hard (classified logged-out / keychain) from
|
|
238
|
+
// soft (timeout / unclassified probe flake). Soft path retries with backoff and holds a
|
|
239
|
+
// recent healthy result across a single post-sleep streak.
|
|
240
|
+
return cursorRuntimeIssues();
|
|
138
241
|
}
|
|
139
242
|
/**
|
|
140
243
|
* Bedrock/Vertex installs authenticate through AWS/GCP credentials instead of a Claude
|
|
@@ -229,12 +332,17 @@ async function githubIssues() {
|
|
|
229
332
|
async function runtimeIssues(runtime) {
|
|
230
333
|
const capIssues = capHealthIssues(runtime);
|
|
231
334
|
const cached = runtimeIssueCache.get(runtime);
|
|
232
|
-
|
|
335
|
+
const ttl = cached?.softProbeFailure ? SOFT_PROBE_CACHE_MS : RUNTIME_CACHE_MS;
|
|
336
|
+
if (cached && Date.now() - cached.at < ttl) {
|
|
233
337
|
return [...capIssues, ...cached.issues];
|
|
234
338
|
}
|
|
235
339
|
const issues = await runtimeIssuesUncached(runtime);
|
|
236
340
|
const nonCap = issues.filter((i) => i.code !== "usage_capped");
|
|
237
|
-
|
|
341
|
+
// Soft fail (published or grace-held) uses a short TTL so a wake retry can clear quickly;
|
|
342
|
+
// a sticky 60s cache of "Could not confirm" is what parked the queue after sleep (NOT-157).
|
|
343
|
+
const softProbeFailure = nonCap.some(isSoftCursorProbeIssue) ||
|
|
344
|
+
(runtime === "cursor_local" && cursorSoftFailStreak > 0);
|
|
345
|
+
runtimeIssueCache.set(runtime, { at: Date.now(), issues: nonCap, softProbeFailure });
|
|
238
346
|
return [...capIssues, ...nonCap];
|
|
239
347
|
}
|
|
240
348
|
function agentSpecificIssues(agent, agentDeckOnline, mcpRegistered, deckAccessResult) {
|