agent-dealer 1.2.2 → 1.2.4

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 (34) hide show
  1. package/bundle/server/dist/capacity/claude-events.js +27 -10
  2. package/bundle/server/dist/capacity/claude-local-cache.js +794 -0
  3. package/bundle/server/dist/capacity/claude-local-cache.test.js +524 -0
  4. package/bundle/server/dist/capacity/cursor-individual-credentials.js +203 -16
  5. package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +186 -2
  6. package/bundle/server/dist/capacity/cursor-individual.js +69 -2
  7. package/bundle/server/dist/capacity/cursor-individual.test.js +268 -2
  8. package/bundle/server/dist/capacity/muse-host.js +778 -0
  9. package/bundle/server/dist/capacity/muse-host.test.js +480 -0
  10. package/bundle/server/dist/capacity/muse-lifecycle.test.js +96 -0
  11. package/bundle/server/dist/capacity/muse-probe.js +250 -0
  12. package/bundle/server/dist/capacity/muse-probe.test.js +183 -0
  13. package/bundle/server/dist/capacity/muse.js +320 -63
  14. package/bundle/server/dist/capacity/muse.test.js +8 -51
  15. package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
  16. package/bundle/server/dist/index.js +15 -2
  17. package/bundle/server/dist/routes/index.js +25 -9
  18. package/bundle/server/dist/routes/runtime-capacity.test.js +20 -7
  19. package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
  20. package/bundle/server/dist/runners/muse-serve-session.js +314 -0
  21. package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
  22. package/bundle/server/package.json +2 -2
  23. package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
  24. package/bundle/server/static-ui/assets/{index-DUYzboWh.css → index-CYqRh_-S.css} +1 -1
  25. package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
  26. package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
  27. package/bundle/server/static-ui/index.html +2 -2
  28. package/bundle/shared/package.json +1 -1
  29. package/dist/doctor.d.ts +72 -0
  30. package/dist/doctor.js +235 -1
  31. package/dist/doctor.test.d.ts +1 -0
  32. package/dist/doctor.test.js +276 -0
  33. package/package.json +1 -1
  34. package/bundle/server/static-ui/assets/index-0GDYfbaM.js +0 -60
@@ -0,0 +1,250 @@
1
+ // packages/server/src/capacity/muse-probe.ts
2
+ //
3
+ // Reliable Muse 5H/1W fallback. Muse's stable protocol only exposes
4
+ // subscription capacity after the same `muse serve` process has observed
5
+ // real provider traffic. A read-only poll, session resume, or exec log cannot
6
+ // populate it (NOT-269). When the free sources have no complete current pair
7
+ // for one hour, this module runs one tiny bounded turn on a dedicated owned
8
+ // host, then performs `usage/read` on that exact host.
9
+ //
10
+ // The fallback is default-on and can be disabled with
11
+ // `AGENT_DEALER_MUSE_CAPACITY_REFRESH=off`. It is single-flight, attempted at
12
+ // most once per hour, and backs off 1h -> 2h -> 4h -> 8h after failures. The
13
+ // host disables shell and workspace writes and keeps restricted network;
14
+ // Muse 1.4's serve protocol has no per-turn max-step or disable-web-tools
15
+ // switch, so the fixed one-word prompt plus wall-clock cancellation is the
16
+ // tightest supported bound. No transcript, prompt, credential, or account
17
+ // payload is logged.
18
+ import fs from "node:fs";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import { MUSE_CODE_CONTRIBUTOR_MODEL } from "@agent-dealer/shared";
22
+ import { getDataDir } from "../db/index.js";
23
+ import { listCapacitySnapshots } from "../repository/runtime-capacity.js";
24
+ import { runMuseServeTurn } from "../runners/muse-serve-session.js";
25
+ import { configuredCapacityRuntimes } from "./service.js";
26
+ import { maybeRefreshMuseCapacityFromHost, MuseCapacityHost, } from "./muse-host.js";
27
+ import { MUSE_RUNTIME } from "./muse.js";
28
+ export const MUSE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_MUSE_CAPACITY_REFRESH";
29
+ export const MUSE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
30
+ export const MUSE_CAPACITY_REFRESH_OFF_VALUE = "off";
31
+ export const MUSE_CAPACITY_PROBE_TIMEOUT_ENV = "AGENT_DEALER_MUSE_CAPACITY_PROBE_TIMEOUT_MS";
32
+ export const MUSE_CAPACITY_PROBE_TIMEOUT_MS_DEFAULT = 60_000;
33
+ export const MUSE_CAPACITY_PROBE_STALE_AFTER_MS = 60 * 60 * 1000;
34
+ export const MUSE_CAPACITY_PROBE_ATTEMPT_COOLDOWN_MS = 60 * 60 * 1000;
35
+ export const MUSE_CAPACITY_PROBE_BACKOFF_CAP_MS = 8 * 60 * 60 * 1000;
36
+ export const MUSE_CAPACITY_PROBE_PROMPT = "Reply with exactly: OK";
37
+ export const MUSE_CAPACITY_PROBE_SERVE_ARGV = [
38
+ "serve",
39
+ "--sandbox-network",
40
+ "restricted",
41
+ "--disable-write",
42
+ "--disable-shell",
43
+ ];
44
+ export function isMusePaidFallbackEnabled() {
45
+ const setting = process.env[MUSE_CAPACITY_REFRESH_ENV];
46
+ if (setting === MUSE_CAPACITY_REFRESH_PAID_VALUE)
47
+ return true;
48
+ if (setting !== undefined && setting !== "")
49
+ return false;
50
+ // Backward compatibility: this older switch used to disable every Muse
51
+ // capacity refresh. Do not turn an existing no-refresh deployment into a
52
+ // spender on upgrade. An explicit paid-after-1h above may override it.
53
+ if (process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS === "off")
54
+ return false;
55
+ return true;
56
+ }
57
+ export function museCapacityProbeTimeoutMs() {
58
+ const raw = process.env[MUSE_CAPACITY_PROBE_TIMEOUT_ENV];
59
+ if (raw !== undefined && raw !== "") {
60
+ const n = Number(raw);
61
+ if (Number.isFinite(n) && n > 0)
62
+ return n;
63
+ }
64
+ return MUSE_CAPACITY_PROBE_TIMEOUT_MS_DEFAULT;
65
+ }
66
+ function validRoleObservedAtMs(role, nowMs) {
67
+ const row = listCapacitySnapshots(MUSE_RUNTIME).find((w) => w.criticalRole === role);
68
+ if (!row || row.remainingPercent === null || row.unavailableReason !== null || row.source === "unavailable") {
69
+ return null;
70
+ }
71
+ const observedMs = Date.parse(row.observedAt);
72
+ if (!Number.isFinite(observedMs) || observedMs > nowMs)
73
+ return null;
74
+ if (row.resetAt !== null) {
75
+ const resetMs = Date.parse(row.resetAt);
76
+ if (!Number.isFinite(resetMs) || resetMs <= nowMs)
77
+ return null;
78
+ }
79
+ return observedMs;
80
+ }
81
+ /** Both halves must be valid and under one hour old. A fresh 5H window may
82
+ * never hide a missing/stale 1W sibling (or vice versa). */
83
+ export function hasFreshMuseCapacityPair(nowMs = Date.now()) {
84
+ const fiveHour = validRoleObservedAtMs("five_hour", nowMs);
85
+ const weekly = validRoleObservedAtMs("weekly", nowMs);
86
+ return (fiveHour !== null &&
87
+ weekly !== null &&
88
+ nowMs - fiveHour < MUSE_CAPACITY_PROBE_STALE_AFTER_MS &&
89
+ nowMs - weekly < MUSE_CAPACITY_PROBE_STALE_AFTER_MS);
90
+ }
91
+ function museProbeDiagnosticLogPath() {
92
+ return path.join(getDataDir(), "capacity", "muse-probe.log");
93
+ }
94
+ function appendMuseProbeDiagnostic(nowMs, result) {
95
+ try {
96
+ const file = museProbeDiagnosticLogPath();
97
+ fs.mkdirSync(path.dirname(file), { recursive: true });
98
+ fs.appendFileSync(file, `${JSON.stringify({
99
+ ts: new Date(nowMs).toISOString(),
100
+ event: "muse_capacity_probe",
101
+ trigger: "stale_60m",
102
+ model: MUSE_CODE_CONTRIBUTOR_MODEL,
103
+ ...result,
104
+ })}\n`);
105
+ }
106
+ catch {
107
+ // Diagnostics are advisory; capacity refresh must never fail on logging.
108
+ }
109
+ }
110
+ /** One bounded real turn followed by a read on the same restricted host. */
111
+ export async function runMuseCapacityProbe(nowMs = Date.now(), opts = {}) {
112
+ const startedRealMs = Date.now();
113
+ const timeoutMs = opts.timeoutMs ?? museCapacityProbeTimeoutMs();
114
+ const ownsHost = opts.host === undefined;
115
+ const host = opts.host ??
116
+ new MuseCapacityHost({
117
+ ...opts.hostOptions,
118
+ args: opts.hostOptions?.args ?? [...MUSE_CAPACITY_PROBE_SERVE_ARGV],
119
+ timeoutMs: Math.min(opts.hostOptions?.timeoutMs ?? timeoutMs, timeoutMs),
120
+ });
121
+ let result;
122
+ try {
123
+ const turn = await runMuseServeTurn({
124
+ host,
125
+ prompt: MUSE_CAPACITY_PROBE_PROMPT,
126
+ model: MUSE_CODE_CONTRIBUTOR_MODEL,
127
+ cwd: os.tmpdir(),
128
+ timeoutMs,
129
+ rpcTimeoutMs: Math.min(15_000, timeoutMs),
130
+ });
131
+ if (!turn.admitted) {
132
+ result = {
133
+ ok: false,
134
+ admitted: false,
135
+ terminal: null,
136
+ failureKind: "unadmitted",
137
+ windowsUpdated: [],
138
+ durationMs: Date.now() - startedRealMs,
139
+ };
140
+ }
141
+ else {
142
+ await host.readUsage();
143
+ const checkNowMs = Math.max(nowMs, Date.now());
144
+ const ok = hasFreshMuseCapacityPair(checkNowMs);
145
+ result = {
146
+ ok,
147
+ admitted: true,
148
+ terminal: turn.terminal,
149
+ failureKind: ok
150
+ ? null
151
+ : turn.timedOut
152
+ ? "timeout"
153
+ : turn.failure !== null
154
+ ? "turn_failed"
155
+ : "no_windows",
156
+ windowsUpdated: ok ? ["rolling_all_models", "weekly_all_models"] : [],
157
+ durationMs: Date.now() - startedRealMs,
158
+ };
159
+ }
160
+ }
161
+ catch {
162
+ result = {
163
+ ok: false,
164
+ admitted: false,
165
+ terminal: null,
166
+ failureKind: "exception",
167
+ windowsUpdated: [],
168
+ durationMs: Date.now() - startedRealMs,
169
+ };
170
+ }
171
+ finally {
172
+ if (ownsHost)
173
+ await host.shutdown().catch(() => undefined);
174
+ }
175
+ appendMuseProbeDiagnostic(nowMs, result);
176
+ const outcome = result.ok ? "ok" : `failed: ${result.failureKind ?? "unknown"}`;
177
+ console.error(`[muse-capacity] fallback ${outcome}`);
178
+ return result;
179
+ }
180
+ let museProbeInFlight = null;
181
+ let lastMuseProbeAttemptMs = 0;
182
+ let consecutiveMuseProbeFailures = 0;
183
+ function museProbeCooldownMs() {
184
+ const shift = Math.min(consecutiveMuseProbeFailures, 3);
185
+ return Math.min(MUSE_CAPACITY_PROBE_ATTEMPT_COOLDOWN_MS * 2 ** shift, MUSE_CAPACITY_PROBE_BACKOFF_CAP_MS);
186
+ }
187
+ export function resetMuseCapacityProbeStateForTests() {
188
+ museProbeInFlight = null;
189
+ lastMuseProbeAttemptMs = 0;
190
+ consecutiveMuseProbeFailures = 0;
191
+ }
192
+ export async function maybeProbeMuseCapacity(nowMs = Date.now(), opts = {}) {
193
+ try {
194
+ if (!isMusePaidFallbackEnabled())
195
+ return { probed: false, reason: "disabled" };
196
+ if (!configuredCapacityRuntimes().includes(MUSE_RUNTIME)) {
197
+ return { probed: false, reason: "unconfigured" };
198
+ }
199
+ if (hasFreshMuseCapacityPair(nowMs))
200
+ return { probed: false, reason: "fresh" };
201
+ if (museProbeInFlight) {
202
+ const shared = await museProbeInFlight;
203
+ return {
204
+ probed: true,
205
+ reason: "shared",
206
+ ok: shared.ok,
207
+ failureKind: shared.failureKind,
208
+ windowsUpdated: shared.windowsUpdated,
209
+ };
210
+ }
211
+ if (nowMs - lastMuseProbeAttemptMs < museProbeCooldownMs()) {
212
+ return { probed: false, reason: "backoff" };
213
+ }
214
+ lastMuseProbeAttemptMs = nowMs;
215
+ const run = runMuseCapacityProbe(nowMs, opts);
216
+ museProbeInFlight = run;
217
+ try {
218
+ const result = await run;
219
+ consecutiveMuseProbeFailures = result.ok ? 0 : consecutiveMuseProbeFailures + 1;
220
+ return {
221
+ probed: true,
222
+ reason: "completed",
223
+ ok: result.ok,
224
+ failureKind: result.failureKind,
225
+ windowsUpdated: result.windowsUpdated,
226
+ };
227
+ }
228
+ finally {
229
+ if (museProbeInFlight === run)
230
+ museProbeInFlight = null;
231
+ }
232
+ }
233
+ catch {
234
+ return { probed: false, reason: "error" };
235
+ }
236
+ }
237
+ /** Free read first; only a still-missing/stale complete pair reaches the
238
+ * bounded paid fallback. Route callers run this in the background. */
239
+ export async function refreshMuseCapacityIfStale(nowMs = Date.now(), opts = {}) {
240
+ if (!configuredCapacityRuntimes().includes(MUSE_RUNTIME)) {
241
+ return { probed: false, reason: "unconfigured" };
242
+ }
243
+ try {
244
+ await maybeRefreshMuseCapacityFromHost({}, nowMs);
245
+ }
246
+ catch {
247
+ // The paid gate below still decides from durable last-good rows.
248
+ }
249
+ return maybeProbeMuseCapacity(nowMs, opts);
250
+ }
@@ -0,0 +1,183 @@
1
+ // Default-on one-hour Muse capacity fallback. Fake MSP host only: no live
2
+ // provider request and no paid model call in this suite.
3
+ import { beforeEach, test } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-probe-"));
9
+ import { MuseCapacityHost } from "./muse-host.js";
10
+ import { hasFreshMuseCapacityPair, isMusePaidFallbackEnabled, maybeProbeMuseCapacity, MUSE_CAPACITY_PROBE_SERVE_ARGV, MUSE_CAPACITY_REFRESH_ENV, resetMuseCapacityProbeStateForTests, runMuseCapacityProbe, } from "./muse-probe.js";
11
+ import { ingestMuseUsagePayload } from "./muse.js";
12
+ const { migrate } = await import("../db/index.js");
13
+ const { createAgent } = await import("../repository/agents.js");
14
+ const { clearAllCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
15
+ const FAKE = new URL("./fixtures/fake-muse-serve.mjs", import.meta.url).pathname;
16
+ function hostOpts(mode, extraEnv = {}) {
17
+ const now = Date.now();
18
+ return {
19
+ command: process.execPath,
20
+ args: [FAKE],
21
+ env: {
22
+ META_API_KEY: "test-fake-key",
23
+ FAKE_MSP_MODE: mode,
24
+ FAKE_MSP_NOW_MS: String(now),
25
+ ...extraEnv,
26
+ },
27
+ timeoutMs: 10_000,
28
+ };
29
+ }
30
+ function payload(nowMs, roles = "both") {
31
+ return {
32
+ usage: {
33
+ observedAtMs: nowMs,
34
+ tier: "synthetic",
35
+ window: {
36
+ usedPercent: 25,
37
+ resetsAtMs: nowMs + 2 * 3600_000,
38
+ windowDurationMins: 300,
39
+ },
40
+ ...(roles === "both"
41
+ ? { weekly: { usedPercent: 40, resetsAtMs: nowMs + 3 * 24 * 3600_000 } }
42
+ : {}),
43
+ },
44
+ };
45
+ }
46
+ function recordedMethods(file) {
47
+ if (!fs.existsSync(file))
48
+ return [];
49
+ return fs
50
+ .readFileSync(file, "utf8")
51
+ .split("\n")
52
+ .filter(Boolean)
53
+ .map((line) => String(JSON.parse(line).method ?? ""));
54
+ }
55
+ beforeEach(() => {
56
+ migrate();
57
+ clearAllCapacitySnapshots();
58
+ resetMuseCapacityProbeStateForTests();
59
+ delete process.env[MUSE_CAPACITY_REFRESH_ENV];
60
+ delete process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS;
61
+ const existing = createAgent({
62
+ name: `muse-probe-${Date.now()}-${Math.random()}`,
63
+ runtime: "muse_code",
64
+ deckId: "33333333-3333-4333-8333-333333333333",
65
+ });
66
+ assert.equal(existing.runtime, "muse_code");
67
+ });
68
+ test("fallback defaults on; explicit off and unrecognized values disable spending", async () => {
69
+ for (const value of [undefined, "", "paid-after-1h"]) {
70
+ if (value === undefined)
71
+ delete process.env[MUSE_CAPACITY_REFRESH_ENV];
72
+ else
73
+ process.env[MUSE_CAPACITY_REFRESH_ENV] = value;
74
+ assert.equal(isMusePaidFallbackEnabled(), true);
75
+ }
76
+ for (const value of ["off", "auto", "paid-after-1h "]) {
77
+ process.env[MUSE_CAPACITY_REFRESH_ENV] = value;
78
+ assert.equal(isMusePaidFallbackEnabled(), false);
79
+ assert.deepEqual(await maybeProbeMuseCapacity(Date.now()), {
80
+ probed: false,
81
+ reason: "disabled",
82
+ });
83
+ }
84
+ });
85
+ test("legacy refresh off remains no-spend unless paid fallback is explicitly enabled", () => {
86
+ process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS = "off";
87
+ delete process.env[MUSE_CAPACITY_REFRESH_ENV];
88
+ assert.equal(isMusePaidFallbackEnabled(), false);
89
+ process.env[MUSE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
90
+ assert.equal(isMusePaidFallbackEnabled(), true);
91
+ });
92
+ test("a complete pair under one hour suppresses the fallback", async () => {
93
+ const now = Date.now();
94
+ await ingestMuseUsagePayload(payload(now - 59 * 60_000));
95
+ assert.equal(hasFreshMuseCapacityPair(now), true);
96
+ assert.deepEqual(await maybeProbeMuseCapacity(now), { probed: false, reason: "fresh" });
97
+ });
98
+ test("one missing half triggers one minimal turn and stores exactly 5H plus 1W", async () => {
99
+ const now = Date.now();
100
+ await ingestMuseUsagePayload(payload(now - 5 * 60_000, "five_hour"));
101
+ assert.equal(hasFreshMuseCapacityPair(now), false, "a fresh half is not a complete pair");
102
+ const host = new MuseCapacityHost(hostOpts("serve-turn-full"));
103
+ try {
104
+ const outcome = await maybeProbeMuseCapacity(now, { host, timeoutMs: 10_000 });
105
+ assert.equal(outcome.probed, true);
106
+ assert.equal(outcome.reason, "completed");
107
+ assert.equal(outcome.ok, true);
108
+ assert.deepEqual(outcome.windowsUpdated?.sort(), ["rolling_all_models", "weekly_all_models"]);
109
+ const roles = listCapacitySnapshots("muse_code")
110
+ .filter((w) => w.criticalRole !== null)
111
+ .map((w) => w.criticalRole)
112
+ .sort();
113
+ assert.deepEqual(roles, ["five_hour", "weekly"]);
114
+ }
115
+ finally {
116
+ await host.shutdown();
117
+ }
118
+ });
119
+ test("concurrent stale readers single-flight one turn", async () => {
120
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-probe-record-"));
121
+ const record = path.join(dir, "methods.jsonl");
122
+ const host = new MuseCapacityHost(hostOpts("serve-turn-full", { FAKE_MSP_RECORD: record }));
123
+ try {
124
+ const now = Date.now();
125
+ const outcomes = await Promise.all(Array.from({ length: 5 }, () => maybeProbeMuseCapacity(now, { host, timeoutMs: 10_000 })));
126
+ assert.equal(outcomes.filter((o) => o.reason === "completed").length, 1);
127
+ assert.equal(outcomes.filter((o) => o.reason === "shared").length, 4);
128
+ assert.ok(outcomes.every((o) => o.probed && o.ok));
129
+ assert.equal(recordedMethods(record).filter((m) => m === "turn/start").length, 1);
130
+ }
131
+ finally {
132
+ await host.shutdown();
133
+ }
134
+ });
135
+ test("failed admission preserves rows and enters hourly backoff", async () => {
136
+ const now = Date.now();
137
+ await ingestMuseUsagePayload(payload(now - 61 * 60_000));
138
+ const before = listCapacitySnapshots("muse_code").map((w) => ({
139
+ key: w.windowKey,
140
+ remaining: w.remainingPercent,
141
+ observedAt: w.observedAt,
142
+ }));
143
+ const host = new MuseCapacityHost(hostOpts("serve-turn-rejected"));
144
+ try {
145
+ const first = await maybeProbeMuseCapacity(now, { host, timeoutMs: 10_000 });
146
+ assert.equal(first.probed, true);
147
+ assert.equal(first.ok, false);
148
+ assert.equal(first.failureKind, "unadmitted");
149
+ assert.deepEqual(await maybeProbeMuseCapacity(now + 60_000, { host, timeoutMs: 10_000 }), {
150
+ probed: false,
151
+ reason: "backoff",
152
+ });
153
+ assert.deepEqual(listCapacitySnapshots("muse_code").map((w) => ({
154
+ key: w.windowKey,
155
+ remaining: w.remainingPercent,
156
+ observedAt: w.observedAt,
157
+ })), before);
158
+ }
159
+ finally {
160
+ await host.shutdown();
161
+ }
162
+ });
163
+ test("production probe host disables shell/write and keeps restricted network", () => {
164
+ assert.deepEqual([...MUSE_CAPACITY_PROBE_SERVE_ARGV], [
165
+ "serve",
166
+ "--sandbox-network",
167
+ "restricted",
168
+ "--disable-write",
169
+ "--disable-shell",
170
+ ]);
171
+ });
172
+ test("direct probe success requires the complete pair", async () => {
173
+ const host = new MuseCapacityHost(hostOpts("serve-turn-full"));
174
+ try {
175
+ const result = await runMuseCapacityProbe(Date.now(), { host, timeoutMs: 10_000 });
176
+ assert.equal(result.ok, true);
177
+ assert.equal(result.admitted, true);
178
+ assert.equal(result.failureKind, null);
179
+ }
180
+ finally {
181
+ await host.shutdown();
182
+ }
183
+ });