agent-dealer 1.2.1 → 1.2.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/linear-inbox.js +9 -5
- package/bundle/server/dist/capacity/adapter.js +2 -0
- package/bundle/server/dist/capacity/claude-events.js +45 -10
- package/bundle/server/dist/capacity/claude-events.test.js +7 -0
- package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
- package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
- package/bundle/server/dist/capacity/codex-app-server.js +100 -10
- package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +425 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +369 -0
- package/bundle/server/dist/capacity/cursor-individual.js +1037 -0
- package/bundle/server/dist/capacity/cursor-individual.test.js +1081 -0
- package/bundle/server/dist/capacity/muse-host.js +748 -0
- package/bundle/server/dist/capacity/muse-host.test.js +480 -0
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
- package/bundle/server/dist/capacity/muse.js +389 -91
- package/bundle/server/dist/capacity/muse.test.js +100 -61
- package/bundle/server/dist/capacity/service.js +1 -0
- package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
- package/bundle/server/dist/db/index.js +30 -0
- package/bundle/server/dist/db/schema.sql +27 -0
- package/bundle/server/dist/index.js +17 -2
- package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
- package/bundle/server/dist/repository/repository-mappings.js +46 -0
- package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
- package/bundle/server/dist/repository/runtime-capacity.js +8 -3
- package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
- package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
- package/bundle/server/dist/routes/index.js +58 -9
- package/bundle/server/dist/routes/intake-linear.test.js +55 -1
- package/bundle/server/dist/routes/repository-mappings.js +22 -0
- package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
- package/bundle/server/dist/routes/runtime-capacity.test.js +84 -8
- package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
- package/bundle/server/dist/runners/muse-serve-session.js +313 -0
- package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
- package/bundle/server/static-ui/assets/index-CYqRh_-S.css +1 -0
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
- package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/index.d.ts +6 -6
- package/bundle/shared/dist/linear-intake.d.ts +71 -0
- package/bundle/shared/dist/linear-intake.js +90 -0
- package/bundle/shared/dist/linear-intake.test.js +80 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
- package/bundle/shared/dist/runtime-capacity.js +60 -0
- package/bundle/shared/dist/runtime-capacity.test.js +1 -0
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +57 -0
- package/dist/doctor.js +211 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +246 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
- package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
// packages/server/src/capacity/muse.test.ts
|
|
2
2
|
//
|
|
3
3
|
// NOT-247: Muse serve adapter — fake-subprocess coverage only, never a live
|
|
4
|
-
// Muse request. A fake MSP
|
|
4
|
+
// Muse request. A fake stable MSP server returns window + weekly usage (two
|
|
5
5
|
// independent snapshots with correct remaining percents and reset times), the
|
|
6
6
|
// rolling label derives from `windowDurationMins` (never hardcoded),
|
|
7
7
|
// failure modes map to explicit N/A reasons without touching runtime health,
|
|
8
|
-
// and the recorded protocol traffic proves the client
|
|
8
|
+
// and the recorded protocol traffic proves the client runs the
|
|
9
|
+
// initialize → initialized → usage/read handshake and never sends a prompt.
|
|
10
|
+
// NOT-263: the fake enforces the stable contract (no `--protocol` argv,
|
|
11
|
+
// handshake order, `usage.window` field names).
|
|
9
12
|
import { test, before } from "node:test";
|
|
10
13
|
import assert from "node:assert/strict";
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
11
15
|
import fs from "node:fs";
|
|
12
16
|
import os from "node:os";
|
|
13
17
|
import path from "node:path";
|
|
14
18
|
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-cap-"));
|
|
15
|
-
import { assertMuseReadOnlyMethod, createMuseCapacityAdapter, MSP_FORBIDDEN_MODEL_METHODS,
|
|
16
|
-
import { normalizeAdapterWindow } from "./adapter.js";
|
|
19
|
+
import { assertMuseReadOnlyMethod, createMuseCapacityAdapter, MSP_FORBIDDEN_MODEL_METHODS, MUSE_CLIENT_INFO, MUSE_SERVE_ARGV, museUsageToReadings, normalizeMuseResetsAt, normalizeMuseUnavailable, readMuseCapacity, requestMuseUsage, } from "./muse.js";
|
|
20
|
+
import { normalizeAdapterWindow, normalizeUnavailableWindow } from "./adapter.js";
|
|
17
21
|
const { migrate } = await import("../db/index.js");
|
|
18
|
-
const { clearAllCapacitySnapshots
|
|
22
|
+
const { clearAllCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
19
23
|
const { clearAllRuntimeAvailability, runtimeAvailability, } = await import("../repository/runtime-availability.js");
|
|
20
24
|
const { refreshCapacityFromAdapters } = await import("./service.js");
|
|
21
25
|
const FAKE = new URL("./fixtures/fake-muse-serve.mjs", import.meta.url).pathname;
|
|
@@ -40,6 +44,14 @@ function fakeOpts(mode, extra = {}) {
|
|
|
40
44
|
},
|
|
41
45
|
};
|
|
42
46
|
}
|
|
47
|
+
test("serve argv is the shipping contract: no --protocol flag, restricted network", () => {
|
|
48
|
+
assert.deepEqual([...MUSE_SERVE_ARGV], ["serve", "--sandbox-network", "restricted"]);
|
|
49
|
+
assert.ok(!MUSE_SERVE_ARGV.includes("--protocol"), "muse serve takes no --protocol flag");
|
|
50
|
+
});
|
|
51
|
+
test("handshake identity matches the stable clientInfo shape", () => {
|
|
52
|
+
assert.match(MUSE_CLIENT_INFO.name, /^[a-z0-9_]+$/);
|
|
53
|
+
assert.ok(MUSE_CLIENT_INFO.version.length > 0, "versioned client identity required");
|
|
54
|
+
});
|
|
43
55
|
test("usage windows keep identity, percent, duration, and reset", () => {
|
|
44
56
|
const now = Date.now();
|
|
45
57
|
const parsed = museUsageToReadings({
|
|
@@ -47,7 +59,7 @@ test("usage windows keep identity, percent, duration, and reset", () => {
|
|
|
47
59
|
usage: {
|
|
48
60
|
observedAtMs: now,
|
|
49
61
|
tier: "contributor",
|
|
50
|
-
|
|
62
|
+
window: { usedPercent: 60, resetsAtMs: now + 2 * 3600_000, windowDurationMins: 300 },
|
|
51
63
|
weekly: { usedPercent: 25, resetsAtMs: now + 3 * 24 * 3600_000 },
|
|
52
64
|
},
|
|
53
65
|
}, new Date(now).toISOString());
|
|
@@ -62,16 +74,30 @@ test("usage windows keep identity, percent, duration, and reset", () => {
|
|
|
62
74
|
const normalized = normalizeAdapterWindow("muse_code", rolling, now);
|
|
63
75
|
assert.equal(normalized.displayLabel, "5H");
|
|
64
76
|
assert.equal(normalized.remainingPercent, 40);
|
|
77
|
+
assert.equal(normalized.criticalRole, "five_hour");
|
|
65
78
|
const weekly = parsed.windows.find((w) => w.windowKey === "weekly_all_models");
|
|
66
79
|
assert.equal(normalizeAdapterWindow("muse_code", weekly, now).displayLabel, "1W");
|
|
67
80
|
assert.equal(normalizeAdapterWindow("muse_code", weekly, now).remainingPercent, 75);
|
|
81
|
+
assert.equal(normalizeAdapterWindow("muse_code", weekly, now).criticalRole, "weekly");
|
|
82
|
+
});
|
|
83
|
+
test("legacy rolling spelling maps to the same snapshot", () => {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const parsed = museUsageToReadings({
|
|
86
|
+
usage: {
|
|
87
|
+
observedAtMs: now,
|
|
88
|
+
rolling: { usedPercent: 60, resetsAtMs: now + 2 * 3600_000, windowDurationMins: 300 },
|
|
89
|
+
},
|
|
90
|
+
}, new Date(now).toISOString());
|
|
91
|
+
assert.ok(parsed);
|
|
92
|
+
assert.equal(parsed.windows.length, 1);
|
|
93
|
+
assert.equal(parsed.windows[0].windowKey, "rolling_all_models");
|
|
68
94
|
});
|
|
69
95
|
test("rolling label derives from windowDurationMins, never a hardcoded 5H", () => {
|
|
70
96
|
const now = Date.now();
|
|
71
97
|
const parsed = museUsageToReadings({
|
|
72
98
|
usage: {
|
|
73
99
|
observedAtMs: now,
|
|
74
|
-
|
|
100
|
+
window: { usedPercent: 10, resetsAtMs: now + 6 * 3600_000, windowDurationMins: 720 },
|
|
75
101
|
},
|
|
76
102
|
}, new Date(now).toISOString());
|
|
77
103
|
assert.ok(parsed);
|
|
@@ -86,7 +112,7 @@ test("payloads without an observation read null, not empty", () => {
|
|
|
86
112
|
assert.equal(museUsageToReadings({ protocol: "msp/1.3" }), null);
|
|
87
113
|
assert.equal(museUsageToReadings({ usage: null }), null);
|
|
88
114
|
assert.equal(museUsageToReadings({ usage: { observedAtMs: Date.now() } }).windows.length, 0);
|
|
89
|
-
assert.equal(museUsageToReadings({ usage: { observedAtMs: Date.now(),
|
|
115
|
+
assert.equal(museUsageToReadings({ usage: { observedAtMs: Date.now(), window: { windowDurationMins: 300 } } })
|
|
90
116
|
.windows.length, 0);
|
|
91
117
|
});
|
|
92
118
|
test("resetsAtMs accepts ms, seconds, and ISO; rejects garbage", () => {
|
|
@@ -99,7 +125,9 @@ test("resetsAtMs accepts ms, seconds, and ISO; rejects garbage", () => {
|
|
|
99
125
|
assert.equal(normalizeMuseResetsAt(null), null);
|
|
100
126
|
assert.equal(normalizeMuseResetsAt(-5), null);
|
|
101
127
|
});
|
|
102
|
-
test("only usage/read can be sent; session prompts throw before write", () => {
|
|
128
|
+
test("only the handshake and usage/read can be sent; session prompts throw before write", () => {
|
|
129
|
+
assertMuseReadOnlyMethod("initialize");
|
|
130
|
+
assertMuseReadOnlyMethod("initialized");
|
|
103
131
|
assertMuseReadOnlyMethod("usage/read");
|
|
104
132
|
for (const m of ["session/start", "session/prompt", "session/resume", "exec", "usage/write"]) {
|
|
105
133
|
assert.throws(() => assertMuseReadOnlyMethod(m), /refusing non-read method/);
|
|
@@ -144,31 +172,85 @@ test("usage/changed notifications are consumed while the connection is alive", a
|
|
|
144
172
|
assert.equal(read.failure, null);
|
|
145
173
|
assert.equal(read.updates.length, 1);
|
|
146
174
|
});
|
|
147
|
-
test("protocol traffic is
|
|
175
|
+
test("protocol traffic is the stable handshake then exactly one usage/read", async () => {
|
|
148
176
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-record-"));
|
|
149
177
|
const recordPath = path.join(dir, "methods.jsonl");
|
|
150
178
|
const { opts } = fakeOpts("full", { env: { FAKE_MSP_RECORD: recordPath } });
|
|
151
179
|
await readMuseCapacity(opts);
|
|
152
|
-
const
|
|
180
|
+
const lines = fs
|
|
153
181
|
.readFileSync(recordPath, "utf8")
|
|
154
182
|
.split("\n")
|
|
155
183
|
.filter(Boolean)
|
|
156
|
-
.map((l) =>
|
|
157
|
-
|
|
184
|
+
.map((l) => JSON.parse(l));
|
|
185
|
+
const methods = lines.map((l) => String(l.method));
|
|
186
|
+
assert.deepEqual(methods, ["initialize", "initialized", "usage/read"]);
|
|
187
|
+
const init = lines[0];
|
|
188
|
+
assert.equal(init.params?.clientInfo?.name, MUSE_CLIENT_INFO.name);
|
|
189
|
+
assert.match(String(init.params?.clientInfo?.name ?? ""), /^[a-z0-9_]+$/);
|
|
190
|
+
assert.equal(init.params?.clientInfo?.version, MUSE_CLIENT_INFO.version);
|
|
158
191
|
for (const m of methods) {
|
|
159
|
-
assert.equal(m, "usage/read");
|
|
160
192
|
assert.ok(!/session|prompt|exec|message|turn|thread/i.test(m), `billable method ${m}`);
|
|
161
193
|
}
|
|
162
194
|
});
|
|
195
|
+
test("fake rejects the removed --protocol argv like the shipped binary", async () => {
|
|
196
|
+
clearAllCapacitySnapshots();
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const result = await readMuseCapacity({
|
|
199
|
+
command: process.execPath,
|
|
200
|
+
args: [FAKE, "--protocol", "msp/1.3"],
|
|
201
|
+
env: { META_API_KEY: "test-fake-key", FAKE_MSP_MODE: "full" },
|
|
202
|
+
timeoutMs: 10_000,
|
|
203
|
+
nowMs: now,
|
|
204
|
+
});
|
|
205
|
+
assert.equal(result.windows.length, 0);
|
|
206
|
+
assert.equal(result.unavailable.length, 1);
|
|
207
|
+
assert.equal(result.unavailable[0].windowKey, "muse_account_usage");
|
|
208
|
+
assert.equal(result.unavailable[0].reason, "missing");
|
|
209
|
+
});
|
|
210
|
+
test("fake requires initialize before usage/read", async () => {
|
|
211
|
+
const child = spawn(process.execPath, [FAKE], {
|
|
212
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
213
|
+
env: { ...process.env, FAKE_MSP_MODE: "full", META_API_KEY: "test-fake-key" },
|
|
214
|
+
});
|
|
215
|
+
try {
|
|
216
|
+
const first = await new Promise((resolve, reject) => {
|
|
217
|
+
const timer = setTimeout(() => reject(new Error("timed out waiting for fake response")), 5000);
|
|
218
|
+
timer.unref?.();
|
|
219
|
+
let buf = "";
|
|
220
|
+
child.stdout.on("data", (chunk) => {
|
|
221
|
+
buf += chunk.toString();
|
|
222
|
+
const idx = buf.indexOf("\n");
|
|
223
|
+
if (idx >= 0) {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
resolve(buf.slice(0, idx));
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: "bare-read", method: "usage/read", params: {} })}\n`);
|
|
229
|
+
});
|
|
230
|
+
const msg = JSON.parse(first);
|
|
231
|
+
assert.ok(msg.error, "bare usage/read is rejected before the handshake");
|
|
232
|
+
assert.match(String(msg.error?.message ?? ""), /initialize/i);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
child.kill();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
163
238
|
test("a malformed sibling window does not sink the good one", async () => {
|
|
164
239
|
clearAllCapacitySnapshots();
|
|
165
240
|
const { opts } = fakeOpts("partial-bad-weekly");
|
|
166
241
|
const result = await readMuseCapacity(opts);
|
|
167
242
|
assert.equal(result.windows.length, 1);
|
|
168
243
|
assert.equal(result.windows[0].windowKey, "rolling_all_models");
|
|
244
|
+
assert.equal(result.windows[0].criticalRole, "five_hour");
|
|
169
245
|
assert.equal(result.unavailable.length, 1);
|
|
170
246
|
assert.equal(result.unavailable[0].windowKey, "weekly_all_models");
|
|
171
247
|
assert.equal(result.unavailable[0].reason, "unparsable");
|
|
248
|
+
// NOT-264 review finding: the unavailable half must still carry its
|
|
249
|
+
// criticalRole, or the client can't find it and synthesizes a second,
|
|
250
|
+
// duplicate N/A row for the same half instead of one.
|
|
251
|
+
assert.equal(result.unavailable[0].criticalRole, "weekly");
|
|
252
|
+
const normalizedUnavailable = normalizeUnavailableWindow("muse_code", result.unavailable[0]);
|
|
253
|
+
assert.equal(normalizedUnavailable.criticalRole, "weekly");
|
|
172
254
|
});
|
|
173
255
|
async function expectUnavailable(mode, reason, extra = {}) {
|
|
174
256
|
clearAllCapacitySnapshots();
|
|
@@ -223,53 +305,10 @@ test("no credential short-circuits to missing without spawning", async () => {
|
|
|
223
305
|
assert.equal(result.windows.length, 0);
|
|
224
306
|
assert.equal(result.unavailable[0].reason, "missing");
|
|
225
307
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const snap = await refreshMuseCapacityFromServe(fakeOpts("full").opts);
|
|
231
|
-
const keys = listCapacitySnapshots("muse_code")
|
|
232
|
-
.map((w) => w.windowKey)
|
|
233
|
-
.sort();
|
|
234
|
-
assert.deepEqual(keys, ["rolling_all_models", "weekly_all_models"]);
|
|
235
|
-
const muse = snap.runtimes.find((r) => r.runtime === "muse_code");
|
|
236
|
-
assert.ok(muse);
|
|
237
|
-
assert.equal(muse.windows.length, 2);
|
|
238
|
-
assert.equal(muse.unavailableReason, null);
|
|
239
|
-
});
|
|
240
|
-
test("a read with every window unparsable clears the stale sentinel", async () => {
|
|
241
|
-
clearAllCapacitySnapshots();
|
|
242
|
-
await refreshMuseCapacityFromServe(fakeOpts("crash").opts);
|
|
243
|
-
assert.deepEqual(listCapacitySnapshots("muse_code").map((w) => w.windowKey), ["muse_account_usage"]);
|
|
244
|
-
const result = await readMuseCapacity(fakeOpts("all-bad-windows").opts);
|
|
245
|
-
assert.equal(result.windows.length, 0);
|
|
246
|
-
assert.ok(result.unavailable.length > 0);
|
|
247
|
-
assert.ok(result.unavailable.every((u) => u.windowKey !== "muse_account_usage"));
|
|
248
|
-
await refreshMuseCapacityFromServe(fakeOpts("all-bad-windows").opts);
|
|
249
|
-
const keys = listCapacitySnapshots("muse_code").map((w) => w.windowKey);
|
|
250
|
-
assert.ok(!keys.includes("muse_account_usage"), `stale sentinel cleared, got ${keys}`);
|
|
251
|
-
assert.ok(keys.length > 0, "per-window unparsable rows persist");
|
|
252
|
-
});
|
|
253
|
-
test("production refresh is throttled and can be disabled", async () => {
|
|
254
|
-
clearAllCapacitySnapshots();
|
|
255
|
-
resetMuseCapacityRefreshState();
|
|
256
|
-
const { opts } = fakeOpts("full");
|
|
257
|
-
try {
|
|
258
|
-
assert.equal(museRefreshThrottleMs(), 5 * 60 * 1000);
|
|
259
|
-
const first = await maybeRefreshMuseCapacityFromServe(opts);
|
|
260
|
-
assert.ok(first, "first refresh runs");
|
|
261
|
-
assert.equal(await maybeRefreshMuseCapacityFromServe(opts), null, "second refresh throttled");
|
|
262
|
-
process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS = "off";
|
|
263
|
-
resetMuseCapacityRefreshState();
|
|
264
|
-
assert.equal(await maybeRefreshMuseCapacityFromServe(opts), null, "refresh disabled");
|
|
265
|
-
process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS = "garbage";
|
|
266
|
-
assert.equal(museRefreshThrottleMs(), 5 * 60 * 1000);
|
|
267
|
-
}
|
|
268
|
-
finally {
|
|
269
|
-
delete process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS;
|
|
270
|
-
resetMuseCapacityRefreshState();
|
|
271
|
-
}
|
|
272
|
-
});
|
|
308
|
+
// NOT-270: the one-shot production poll is retired (see muse-host.test.ts
|
|
309
|
+
// for the owned-host refresh, sentinel, and throttle coverage). The adapter
|
|
310
|
+
// read primitive above stays unit-tested; production ingest moved to
|
|
311
|
+
// `ingestMuseUsagePayload` newest-observedAtMs-wins.
|
|
273
312
|
test("no token, tier, or raw account payload reaches the adapter output", async () => {
|
|
274
313
|
const { opts } = fakeOpts("full");
|
|
275
314
|
const result = await readMuseCapacity(opts);
|
|
@@ -47,6 +47,7 @@ export async function ingestAdapterResult(result) {
|
|
|
47
47
|
source: w.source,
|
|
48
48
|
unavailableReason: w.unavailableReason,
|
|
49
49
|
evidenceRef: w.evidenceRef,
|
|
50
|
+
criticalRole: w.criticalRole,
|
|
50
51
|
})));
|
|
51
52
|
}
|
|
52
53
|
/** Classify a stored window at read time (freshness/expiry/reset, not just flags). */
|
|
@@ -25,8 +25,19 @@ import { resolveMuseAuthFile, resolveMuseBin } from "../cli-env.js";
|
|
|
25
25
|
import { ensureWorktreeExcluded } from "../adapters/worktree-exclude.js";
|
|
26
26
|
import { buildMuseDeveloperInvocation } from "../runners/muse-code-args.js";
|
|
27
27
|
import { buildMuseDeveloperSettings } from "../runners/muse-code-settings.js";
|
|
28
|
-
import { parseMuseRun } from "../runners/muse-code-jsonl.js";
|
|
28
|
+
import { normalizeMuseRun, parseMuseRun, } from "../runners/muse-code-jsonl.js";
|
|
29
|
+
import { runMuseServeTurn } from "../runners/muse-serve-session.js";
|
|
29
30
|
import { spawnCli } from "../runners/spawn-cli.js";
|
|
31
|
+
/**
|
|
32
|
+
* NOT-270: execution lane selection. Default is the server-owned serve host
|
|
33
|
+
* (real turns through it are the NOT-269 precondition for 5H/1W capacity).
|
|
34
|
+
* `AGENT_DEALER_MUSE_RUNNER=exec` forces the legacy `muse exec` subprocess
|
|
35
|
+
* lane — the operator escape hatch if the serve lane ever misbehaves in
|
|
36
|
+
* production (capacity then reads honest N/A until serve runs again).
|
|
37
|
+
*/
|
|
38
|
+
function museServeLaneEnabled() {
|
|
39
|
+
return process.env.AGENT_DEALER_MUSE_RUNNER !== "exec";
|
|
40
|
+
}
|
|
30
41
|
/** Worktree-relative home of every per-attempt Muse config/data dir. */
|
|
31
42
|
export const MUSE_ATTEMPT_ROOT = ".dealer-muse";
|
|
32
43
|
/**
|
|
@@ -103,9 +114,136 @@ function splitSpawnLog(raw) {
|
|
|
103
114
|
? { stdout: raw, stderr: "" }
|
|
104
115
|
: { stdout: raw.slice(0, idx), stderr: raw.slice(idx + STDERR_MARKER.length) };
|
|
105
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* NOT-270: reuse real Dealer Muse work as the observation opportunity. After
|
|
119
|
+
* a genuine Muse session ends, kick one bounded, best-effort refresh
|
|
120
|
+
* through the server-owned serve host — the proven safe point for the final
|
|
121
|
+
* `usage/read`. This deliberately bypasses the GET-route throttle
|
|
122
|
+
* (`maybeRefreshMuseCapacityFromHost` shares it): the final read at the
|
|
123
|
+
* safe point is contractual, not opportunistic — a recent Agents-page poll
|
|
124
|
+
* must never skip it. This creates no model turn of its own (the capacity
|
|
125
|
+
* client only ever sends the read-only handshake/`usage/read` allowlist)
|
|
126
|
+
* and never affects the session result. Serve-lane turns observed the host,
|
|
127
|
+
* so this hook is the final read that populates 5H/1W; after an exec-lane
|
|
128
|
+
* fallback the host stays unobserved by construction (the NOT-269
|
|
129
|
+
* structural precondition) and the read stays honest N/A with last-good
|
|
130
|
+
* rows preserved.
|
|
131
|
+
*/
|
|
132
|
+
function refreshMuseCapacityAfterSession() {
|
|
133
|
+
void (async () => {
|
|
134
|
+
try {
|
|
135
|
+
const [{ configuredCapacityRuntimes }, { refreshMuseCapacityFromHost }] = await Promise.all([import("../capacity/service.js"), import("../capacity/muse-host.js")]);
|
|
136
|
+
if (!configuredCapacityRuntimes().includes("muse_code"))
|
|
137
|
+
return;
|
|
138
|
+
await refreshMuseCapacityFromHost();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Best-effort: capacity must never break or delay a session result.
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* NOT-270: run the real developer turn through the server-owned serve host.
|
|
147
|
+
* Returns null when no turn was admitted (host unavailable, session/start
|
|
148
|
+
* or turn/start rejected) — the caller then runs the legacy `muse exec`
|
|
149
|
+
* lane. Never returns null after admission: an admitted turn is real work
|
|
150
|
+
* and its verdict is reported honestly, never retried on the other lane.
|
|
151
|
+
*
|
|
152
|
+
* The serve lane needs no per-attempt XDG config/data dirs: the host runs
|
|
153
|
+
* under its own server-owned home (`prepareMuseServeHome` in
|
|
154
|
+
* capacity/muse-host.ts — same worker settings as the exec lane's
|
|
155
|
+
* per-attempt settings.json plus a symlink to the ambient login, so runner
|
|
156
|
+
* and capacity can never diverge on identity or posture). `onSpawn` is
|
|
157
|
+
* deliberately not called: there is no per-attempt pid to persist, and
|
|
158
|
+
* recovery must never treat the shared host as an attempt process.
|
|
159
|
+
*/
|
|
160
|
+
async function runMuseServeLane(input, model, onAdmitted) {
|
|
161
|
+
const { getMuseCapacityHost } = await import("../capacity/muse-host.js");
|
|
162
|
+
// Only this call may fall back: it resolves `admitted:false` (or throws)
|
|
163
|
+
// strictly before any model work starts. Everything below runs after
|
|
164
|
+
// admission — the caller's no-double-execution rule (via onAdmitted)
|
|
165
|
+
// keeps those errors loud instead of falling back to exec.
|
|
166
|
+
const turn = await runMuseServeTurn({
|
|
167
|
+
host: getMuseCapacityHost(),
|
|
168
|
+
prompt: input.prompt,
|
|
169
|
+
model,
|
|
170
|
+
cwd: input.cwd,
|
|
171
|
+
timeoutMs: input.timeoutMs,
|
|
172
|
+
signal: input.signal,
|
|
173
|
+
onAdmitted: () => onAdmitted(),
|
|
174
|
+
});
|
|
175
|
+
if (!turn.admitted)
|
|
176
|
+
return null;
|
|
177
|
+
const cronCalls = turn.tools
|
|
178
|
+
.map((t) => t.name)
|
|
179
|
+
.filter((n) => n !== null && CRON_TOOL_RE.test(n));
|
|
180
|
+
const summary = {
|
|
181
|
+
museSessionId: turn.sessionId ?? randomUUID(),
|
|
182
|
+
confirmedModel: turn.confirmedModel,
|
|
183
|
+
usage: turn.usage,
|
|
184
|
+
failure: turn.failure,
|
|
185
|
+
cronCalls,
|
|
186
|
+
runCount: 1,
|
|
187
|
+
rawLogPath: null,
|
|
188
|
+
};
|
|
189
|
+
// Same normalized evidence both lanes write: the raw folded history, then
|
|
190
|
+
// the Claude/Cursor-shaped stream every existing log reader consumes.
|
|
191
|
+
const rawLogPath = `${input.logPath.replace(/\.ndjson$/, "")}.muse-raw.jsonl`;
|
|
192
|
+
try {
|
|
193
|
+
fs.writeFileSync(rawLogPath, `${turn.viewItems.map((i) => JSON.stringify(i)).join("\n")}\n`);
|
|
194
|
+
summary.rawLogPath = rawLogPath;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// evidence copy is best-effort; the normalized log below still carries the outcome
|
|
198
|
+
}
|
|
199
|
+
const events = normalizeMuseRun({
|
|
200
|
+
sessionId: summary.museSessionId,
|
|
201
|
+
confirmedModel: turn.confirmedModel,
|
|
202
|
+
tools: turn.tools,
|
|
203
|
+
finalText: turn.finalText,
|
|
204
|
+
usage: turn.usage,
|
|
205
|
+
failure: turn.failure,
|
|
206
|
+
}).map((e) => e.type === "result" ? { ...e, muse: { ...summary, usage: turn.usage } } : e);
|
|
207
|
+
fs.writeFileSync(input.logPath, `${events.map((e) => JSON.stringify(e)).join("\n")}\n`);
|
|
208
|
+
// The real session above is the observation opportunity: kick the
|
|
209
|
+
// best-effort capacity refresh (no synthetic turn, never throws, never
|
|
210
|
+
// blocks this result).
|
|
211
|
+
refreshMuseCapacityAfterSession();
|
|
212
|
+
// A failed session must not read as success. 124 mirrors spawn-cli's
|
|
213
|
+
// timeout exit so timeout handling downstream sees the same signal.
|
|
214
|
+
const exitCode = turn.timedOut ? 124 : turn.failure ? 1 : turn.terminal === "completed" ? 0 : 1;
|
|
215
|
+
return {
|
|
216
|
+
exitCode,
|
|
217
|
+
transcript: turn.finalText ?? "",
|
|
218
|
+
logPath: input.logPath,
|
|
219
|
+
timedOut: turn.timedOut,
|
|
220
|
+
muse: summary,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
106
223
|
export async function runMuseDeveloperSession(input) {
|
|
107
224
|
const model = input.model ?? MUSE_CODE_CONTRIBUTOR_MODEL;
|
|
108
225
|
const museSessionId = randomUUID();
|
|
226
|
+
// Serve-first: real turns through the owned host observe it (NOT-269
|
|
227
|
+
// precondition for 5H/1W). Pre-admission failure falls back to exec; an
|
|
228
|
+
// admitted turn never falls back — its errors propagate loudly instead of
|
|
229
|
+
// executing the work a second time on the exec lane.
|
|
230
|
+
// `AGENT_DEALER_MUSE_RUNNER=exec` skips the serve lane entirely
|
|
231
|
+
// (operator escape hatch).
|
|
232
|
+
if (museServeLaneEnabled()) {
|
|
233
|
+
let admitted = false;
|
|
234
|
+
try {
|
|
235
|
+
const served = await runMuseServeLane(input, model, () => {
|
|
236
|
+
admitted = true;
|
|
237
|
+
});
|
|
238
|
+
if (served)
|
|
239
|
+
return served;
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
if (admitted)
|
|
243
|
+
throw err;
|
|
244
|
+
// Pre-admission serve-lane error: fall through to the exec lane below.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
109
247
|
const invocation = buildMuseDeveloperInvocation({
|
|
110
248
|
model,
|
|
111
249
|
maxModelSteps: input.maxModelSteps ?? DEFAULT_MUSE_MAX_MODEL_STEPS,
|
|
@@ -159,6 +297,10 @@ export async function runMuseDeveloperSession(input) {
|
|
|
159
297
|
}
|
|
160
298
|
const events = run.events.map((e) => e.type === "result" ? { ...e, muse: { ...summary, usage: run.usage } } : e);
|
|
161
299
|
fs.writeFileSync(logPath, `${events.map((e) => JSON.stringify(e)).join("\n")}\n${stderr.trim() ? `${STDERR_MARKER}${stderr}` : ""}`);
|
|
300
|
+
// The real session above is the observation opportunity: kick the
|
|
301
|
+
// best-effort capacity refresh (no synthetic turn, never throws, never
|
|
302
|
+
// blocks this result).
|
|
303
|
+
refreshMuseCapacityAfterSession();
|
|
162
304
|
return {
|
|
163
305
|
// A failed session must not read as success just because Muse exited 0 (wrong model, no terminal event).
|
|
164
306
|
exitCode: run.failure && spawned.exitCode === 0 ? 1 : spawned.exitCode,
|
|
@@ -389,6 +389,11 @@ export function migrate() {
|
|
|
389
389
|
ON runtime_capacity_snapshots(runtime);
|
|
390
390
|
`);
|
|
391
391
|
}
|
|
392
|
+
// NOT-264 repair: critical_role on a table that predates it.
|
|
393
|
+
const capacityCols = db.prepare("PRAGMA table_info(runtime_capacity_snapshots)").all();
|
|
394
|
+
if (!capacityCols.some((c) => c.name === "critical_role")) {
|
|
395
|
+
db.exec("ALTER TABLE runtime_capacity_snapshots ADD COLUMN critical_role TEXT");
|
|
396
|
+
}
|
|
392
397
|
// NOT-249: normalized Cursor-team billing snapshot for databases created
|
|
393
398
|
// before schema.sql declared the table (same upgrade-path pattern as
|
|
394
399
|
// runtime_capacity_snapshots above).
|
|
@@ -433,6 +438,31 @@ export function migrate() {
|
|
|
433
438
|
db.exec("ALTER TABLE cursor_team_billing_snapshots ADD COLUMN member_limit_override_count REAL");
|
|
434
439
|
}
|
|
435
440
|
}
|
|
441
|
+
// NOT-250: normalized Cursor-individual billing snapshot for databases
|
|
442
|
+
// created before schema.sql declared the table (same upgrade-path pattern
|
|
443
|
+
// as cursor_team_billing_snapshots above).
|
|
444
|
+
const cursorIndividualBilling = db
|
|
445
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'cursor_individual_billing_snapshots'")
|
|
446
|
+
.get();
|
|
447
|
+
if (!cursorIndividualBilling) {
|
|
448
|
+
db.exec(`
|
|
449
|
+
CREATE TABLE cursor_individual_billing_snapshots (
|
|
450
|
+
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
|
|
451
|
+
cycle_label TEXT,
|
|
452
|
+
cycle_start TEXT,
|
|
453
|
+
cycle_end TEXT,
|
|
454
|
+
usage_value REAL,
|
|
455
|
+
usage_unit TEXT,
|
|
456
|
+
remaining_percent REAL,
|
|
457
|
+
source TEXT NOT NULL,
|
|
458
|
+
unavailable_reason TEXT,
|
|
459
|
+
observed_at TEXT NOT NULL,
|
|
460
|
+
fresh_until TEXT,
|
|
461
|
+
expires_at TEXT,
|
|
462
|
+
evidence_ref TEXT
|
|
463
|
+
);
|
|
464
|
+
`);
|
|
465
|
+
}
|
|
436
466
|
// NOT-103: sequential issue admission queue.
|
|
437
467
|
const queueEntries = db
|
|
438
468
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'queue_entries'")
|
|
@@ -400,6 +400,12 @@ CREATE TABLE IF NOT EXISTS runtime_capacity_snapshots (
|
|
|
400
400
|
source TEXT NOT NULL,
|
|
401
401
|
unavailable_reason TEXT,
|
|
402
402
|
evidence_ref TEXT,
|
|
403
|
+
-- NOT-264 repair: which account-wide critical window this is ('five_hour'
|
|
404
|
+
-- / 'weekly'), set once by the adapter that knows the provider's own
|
|
405
|
+
-- identity (e.g. Codex's rateLimits aggregate, or the detailed limit pair
|
|
406
|
+
-- it collapsed into) — never re-derived client-side from a window key or
|
|
407
|
+
-- label, which cannot disambiguate multiple same-duration buckets.
|
|
408
|
+
critical_role TEXT,
|
|
403
409
|
PRIMARY KEY (runtime, window_key)
|
|
404
410
|
);
|
|
405
411
|
|
|
@@ -431,6 +437,27 @@ CREATE TABLE IF NOT EXISTS cursor_team_billing_snapshots (
|
|
|
431
437
|
evidence_ref TEXT
|
|
432
438
|
);
|
|
433
439
|
|
|
440
|
+
-- NOT-250: normalized Cursor-individual billing snapshot (single row, id = 1).
|
|
441
|
+
-- Experimental dashboard billing-cycle state, not per-runtime quota windows:
|
|
442
|
+
-- independent of both runtime_capacity_snapshots and runtime_availability
|
|
443
|
+
-- (NOT-111 health). Carries normalized values plus a static evidence pointer
|
|
444
|
+
-- only — never credential material.
|
|
445
|
+
CREATE TABLE IF NOT EXISTS cursor_individual_billing_snapshots (
|
|
446
|
+
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
|
|
447
|
+
cycle_label TEXT,
|
|
448
|
+
cycle_start TEXT,
|
|
449
|
+
cycle_end TEXT,
|
|
450
|
+
usage_value REAL,
|
|
451
|
+
usage_unit TEXT,
|
|
452
|
+
remaining_percent REAL,
|
|
453
|
+
source TEXT NOT NULL,
|
|
454
|
+
unavailable_reason TEXT,
|
|
455
|
+
observed_at TEXT NOT NULL,
|
|
456
|
+
fresh_until TEXT,
|
|
457
|
+
expires_at TEXT,
|
|
458
|
+
evidence_ref TEXT
|
|
459
|
+
);
|
|
460
|
+
|
|
434
461
|
-- NOT-111: account-level runtime usage caps (keyed per runtime, not per agent profile).
|
|
435
462
|
CREATE TABLE IF NOT EXISTS runtime_availability (
|
|
436
463
|
runtime TEXT PRIMARY KEY,
|
|
@@ -10,6 +10,7 @@ import { registerQueueRoutes } from "./routes/queue.js";
|
|
|
10
10
|
import { registerHumanActionRoutes } from "./routes/human-actions.js";
|
|
11
11
|
import { registerExecutionAnalysisRoutes } from "./routes/execution-analysis.js";
|
|
12
12
|
import { registerExecutionReportRoutes } from "./routes/execution-report.js";
|
|
13
|
+
import { registerRepositoryMappingsRoutes } from "./routes/repository-mappings.js";
|
|
13
14
|
import { recoverCoordinator } from "./coordinator/recovery.js";
|
|
14
15
|
import { startCoordinatorLoop } from "./coordinator/worker-loop.js";
|
|
15
16
|
import { startLinearUsageSummary } from "./adapters/linear-graphql.js";
|
|
@@ -45,13 +46,26 @@ async function main() {
|
|
|
45
46
|
"Stop it first, or point AGENT_DEALER_HOME at a different directory.");
|
|
46
47
|
process.exit(1);
|
|
47
48
|
}
|
|
49
|
+
// NOT-270: the server-owned Muse capacity host (`muse serve` child) must
|
|
50
|
+
// be released on shutdown — it is spawned with piped stdio, is not
|
|
51
|
+
// detached, and must not be assumed to exit on stdin EOF. Every handler
|
|
52
|
+
// below shuts it down before exiting so no host process leaks.
|
|
53
|
+
const shutdownCapacityHosts = async () => {
|
|
54
|
+
try {
|
|
55
|
+
const { shutdownMuseCapacityHost } = await import("./capacity/muse-host.js");
|
|
56
|
+
await shutdownMuseCapacityHost();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Best-effort: shutdown must never block process exit.
|
|
60
|
+
}
|
|
61
|
+
};
|
|
48
62
|
process.on("SIGINT", () => {
|
|
49
63
|
removeServerPidFile();
|
|
50
|
-
process.exit(0);
|
|
64
|
+
void shutdownCapacityHosts().finally(() => process.exit(0));
|
|
51
65
|
});
|
|
52
66
|
process.on("SIGTERM", () => {
|
|
53
67
|
removeServerPidFile();
|
|
54
|
-
process.exit(0);
|
|
68
|
+
void shutdownCapacityHosts().finally(() => process.exit(0));
|
|
55
69
|
});
|
|
56
70
|
process.on("exit", removeServerPidFile);
|
|
57
71
|
migrate();
|
|
@@ -64,6 +78,7 @@ async function main() {
|
|
|
64
78
|
await registerHumanActionRoutes(app);
|
|
65
79
|
await registerExecutionAnalysisRoutes(app);
|
|
66
80
|
await registerExecutionReportRoutes(app);
|
|
81
|
+
await registerRepositoryMappingsRoutes(app);
|
|
67
82
|
const uiDist = await registerStaticUi(app);
|
|
68
83
|
// Without this, getEffectHandler() falls back to the always-session_failed placeholders
|
|
69
84
|
// (effect-registry.ts) and every started issue would burn its infra retries and
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// packages/server/src/repository/cursor-individual-billing.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-250: durable normalized Cursor-individual billing snapshot. Single-row
|
|
4
|
+
// table (`id = 1`): the experimental dashboard adapter reports one
|
|
5
|
+
// billing-cycle state, not per-window readings. Independent of
|
|
6
|
+
// `runtime_availability` (NOT-111 connection health) and written alongside
|
|
7
|
+
// (never instead of) `runtime_capacity_snapshots` — nothing here reads or
|
|
8
|
+
// writes either table.
|
|
9
|
+
//
|
|
10
|
+
// Credential hygiene: this table carries normalized billing values and a
|
|
11
|
+
// static evidence pointer only. The raw local-login token must never be
|
|
12
|
+
// written here — the adapter never passes it to this module, and tests scan
|
|
13
|
+
// stored rows for fixture secrets.
|
|
14
|
+
import { getDb } from "../db/index.js";
|
|
15
|
+
function rowToSnapshot(row) {
|
|
16
|
+
return {
|
|
17
|
+
cycleLabel: row.cycle_label,
|
|
18
|
+
cycleStart: row.cycle_start,
|
|
19
|
+
cycleEnd: row.cycle_end,
|
|
20
|
+
usageValue: row.usage_value,
|
|
21
|
+
usageUnit: row.usage_unit,
|
|
22
|
+
remainingPercent: row.remaining_percent,
|
|
23
|
+
source: row.source,
|
|
24
|
+
unavailableReason: row.unavailable_reason,
|
|
25
|
+
observedAt: row.observed_at,
|
|
26
|
+
freshUntil: row.fresh_until,
|
|
27
|
+
expiresAt: row.expires_at,
|
|
28
|
+
evidenceRef: row.evidence_ref,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Upsert the latest normalized individual billing observation (single row). */
|
|
32
|
+
export function writeCursorIndividualBillingRow(input) {
|
|
33
|
+
getDb()
|
|
34
|
+
.prepare(`
|
|
35
|
+
INSERT INTO cursor_individual_billing_snapshots (
|
|
36
|
+
id, cycle_label, cycle_start, cycle_end, usage_value, usage_unit,
|
|
37
|
+
remaining_percent, source, unavailable_reason,
|
|
38
|
+
observed_at, fresh_until, expires_at, evidence_ref
|
|
39
|
+
)
|
|
40
|
+
VALUES (
|
|
41
|
+
1, @cycle_label, @cycle_start, @cycle_end, @usage_value, @usage_unit,
|
|
42
|
+
@remaining_percent, @source, @unavailable_reason,
|
|
43
|
+
@observed_at, @fresh_until, @expires_at, @evidence_ref
|
|
44
|
+
)
|
|
45
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
46
|
+
cycle_label = excluded.cycle_label,
|
|
47
|
+
cycle_end = excluded.cycle_end,
|
|
48
|
+
cycle_start = excluded.cycle_start,
|
|
49
|
+
usage_value = excluded.usage_value,
|
|
50
|
+
usage_unit = excluded.usage_unit,
|
|
51
|
+
remaining_percent = excluded.remaining_percent,
|
|
52
|
+
source = excluded.source,
|
|
53
|
+
unavailable_reason = excluded.unavailable_reason,
|
|
54
|
+
observed_at = excluded.observed_at,
|
|
55
|
+
fresh_until = excluded.fresh_until,
|
|
56
|
+
expires_at = excluded.expires_at,
|
|
57
|
+
evidence_ref = excluded.evidence_ref
|
|
58
|
+
`)
|
|
59
|
+
.run({
|
|
60
|
+
cycle_label: input.cycleLabel,
|
|
61
|
+
cycle_start: input.cycleStart,
|
|
62
|
+
cycle_end: input.cycleEnd,
|
|
63
|
+
usage_value: input.usageValue,
|
|
64
|
+
usage_unit: input.usageUnit,
|
|
65
|
+
remaining_percent: input.remainingPercent,
|
|
66
|
+
source: input.source,
|
|
67
|
+
unavailable_reason: input.unavailableReason,
|
|
68
|
+
observed_at: input.observedAt,
|
|
69
|
+
fresh_until: input.freshUntil,
|
|
70
|
+
expires_at: input.expiresAt,
|
|
71
|
+
evidence_ref: input.evidenceRef,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** The latest normalized individual billing observation, if any. */
|
|
75
|
+
export function readCursorIndividualBillingRow() {
|
|
76
|
+
const row = getDb()
|
|
77
|
+
.prepare("SELECT * FROM cursor_individual_billing_snapshots WHERE id = 1")
|
|
78
|
+
.get();
|
|
79
|
+
return row ? rowToSnapshot(row) : null;
|
|
80
|
+
}
|
|
81
|
+
/** Test helper — clear the individual billing snapshot (leaves all other tables alone). */
|
|
82
|
+
export function clearCursorIndividualBilling() {
|
|
83
|
+
getDb().exec("DELETE FROM cursor_individual_billing_snapshots");
|
|
84
|
+
}
|