agent-dealer 1.2.3 → 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.
- package/bundle/server/dist/capacity/claude-local-cache.js +16 -10
- package/bundle/server/dist/capacity/claude-local-cache.test.js +8 -4
- package/bundle/server/dist/capacity/muse-host.js +51 -21
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +2 -3
- package/bundle/server/dist/capacity/muse-probe.js +250 -0
- package/bundle/server/dist/capacity/muse-probe.test.js +183 -0
- package/bundle/server/dist/capacity/muse.js +16 -6
- package/bundle/server/dist/routes/index.js +11 -12
- package/bundle/server/dist/routes/runtime-capacity.test.js +10 -3
- package/bundle/server/dist/runners/muse-serve-session.js +7 -6
- package/bundle/server/package.json +2 -2
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +18 -3
- package/dist/doctor.js +35 -6
- package/dist/doctor.test.js +38 -8
- package/package.json +1 -1
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// `~/.claude.json` (this module — free, ingested on every capacity
|
|
17
17
|
// read; only that subtree is ever parsed, the rest of the config —
|
|
18
18
|
// accountUuid, email, credentials, projects — is never retained).
|
|
19
|
-
// 3. One minimal bounded paid probe
|
|
20
|
-
//
|
|
21
|
-
// `AGENT_DEALER_CLAUDE_CAPACITY_REFRESH=
|
|
19
|
+
// 3. One minimal bounded paid probe when every valid 5H/1W observation is
|
|
20
|
+
// older than 60 minutes. This is the default; operators can explicitly
|
|
21
|
+
// disable it with `AGENT_DEALER_CLAUDE_CAPACITY_REFRESH=off`.
|
|
22
22
|
//
|
|
23
23
|
// Both file sources write the same `claude_unified_five_hour` /
|
|
24
24
|
// `claude_unified_seven_day` window keys through the shared
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
//
|
|
69
69
|
// If a live proof ever shows the minimal probe does not reliably emit 5H/1W
|
|
70
70
|
// (neither in its stream nor via the cache side effect), the probe is a
|
|
71
|
-
// recurring paid no-op: disable the
|
|
71
|
+
// recurring paid no-op: disable the fallback and revise this ticket instead of
|
|
72
72
|
// shipping it. The `no_windows` diagnostic below exists to make that visible.
|
|
73
73
|
import { spawn } from "node:child_process";
|
|
74
74
|
import fs from "node:fs";
|
|
@@ -82,9 +82,10 @@ import { configuredCapacityRuntimes } from "./service.js";
|
|
|
82
82
|
import { CLAUDE_RUNTIME, extractClaudeCapacityFromEvents, normalizeClaudeResetsAt, recordClaudeCapacityFromEvents, recordClaudeWindowReadings, } from "./claude-events.js";
|
|
83
83
|
/** Override for the Claude cache file (tests, smoke). */
|
|
84
84
|
export const CLAUDE_CACHE_FILE_ENV = "AGENT_DEALER_CLAUDE_CACHE_FILE";
|
|
85
|
-
/** Paid-fallback
|
|
85
|
+
/** Paid-fallback setting. Unset defaults to PAID_AFTER_1H; `off` disables it. */
|
|
86
86
|
export const CLAUDE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_CLAUDE_CAPACITY_REFRESH";
|
|
87
87
|
export const CLAUDE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
|
|
88
|
+
export const CLAUDE_CAPACITY_REFRESH_OFF_VALUE = "off";
|
|
88
89
|
/** Upper bound for one probe spawn (ms). */
|
|
89
90
|
export const CLAUDE_CAPACITY_PROBE_TIMEOUT_ENV = "AGENT_DEALER_CLAUDE_PROBE_TIMEOUT_MS";
|
|
90
91
|
export const CLAUDE_PROBE_TIMEOUT_MS_DEFAULT = 90_000;
|
|
@@ -408,11 +409,16 @@ export function ingestClaudeLocalCache(nowMs = Date.now()) {
|
|
|
408
409
|
return recordClaudeWindowReadings(result.windows, CLAUDE_RUNTIME, nowMs);
|
|
409
410
|
}
|
|
410
411
|
// ---------------------------------------------------------------------------
|
|
411
|
-
// Paid fallback probe (explicit
|
|
412
|
+
// Paid fallback probe (enabled by default, explicit off switch)
|
|
412
413
|
// ---------------------------------------------------------------------------
|
|
413
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* Paid probing defaults on when the setting is absent/empty. The documented
|
|
416
|
+
* `paid-after-1h` value is accepted explicitly; `off` and unrecognized values
|
|
417
|
+
* disable spending so a typo never silently changes the configured policy.
|
|
418
|
+
*/
|
|
414
419
|
export function isClaudePaidFallbackEnabled() {
|
|
415
|
-
|
|
420
|
+
const setting = process.env[CLAUDE_CAPACITY_REFRESH_ENV];
|
|
421
|
+
return setting === undefined || setting === "" || setting === CLAUDE_CAPACITY_REFRESH_PAID_VALUE;
|
|
416
422
|
}
|
|
417
423
|
/** Bounded direct spawn of `claude` — never the coordinator, never a run. */
|
|
418
424
|
export function defaultProbeRunner(bin, argv, opts) {
|
|
@@ -712,8 +718,8 @@ function probeCooldownMs() {
|
|
|
712
718
|
* On-demand paid fallback: when `claude_code` is configured and no valid
|
|
713
719
|
* 5H/1W observation is newer than 60 minutes, run one minimal bounded probe
|
|
714
720
|
* (single-flight across concurrent readers; at most one attempt per account
|
|
715
|
-
* per 60 minutes, backing off exponentially on failure).
|
|
716
|
-
* strict no-op — no spawn, no spend. Never throws, never touches Dealer
|
|
721
|
+
* per 60 minutes, backing off exponentially on failure). Explicitly disabled
|
|
722
|
+
* is a strict no-op — no spawn, no spend. Never throws, never touches Dealer
|
|
717
723
|
* workflow/session state or `runtime_availability`.
|
|
718
724
|
*/
|
|
719
725
|
export async function maybeProbeClaudeCapacity(nowMs = Date.now(), opts = {}) {
|
|
@@ -263,12 +263,16 @@ test("a 2-hour-old cache ingests with its true age and reads expired, never live
|
|
|
263
263
|
assert.equal(w.unavailableReason, "expired");
|
|
264
264
|
}
|
|
265
265
|
});
|
|
266
|
-
test("
|
|
267
|
-
for (const value of [undefined, "", "
|
|
266
|
+
test("paid fallback defaults on; explicit off and unrecognized values never spawn", async () => {
|
|
267
|
+
for (const value of [undefined, "", "paid-after-1h"]) {
|
|
268
268
|
if (value === undefined)
|
|
269
269
|
delete process.env[CLAUDE_CAPACITY_REFRESH_ENV];
|
|
270
270
|
else
|
|
271
271
|
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = value;
|
|
272
|
+
assert.equal(isClaudePaidFallbackEnabled(), true);
|
|
273
|
+
}
|
|
274
|
+
for (const value of ["off", "auto", "paid-after-1h "]) {
|
|
275
|
+
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = value;
|
|
272
276
|
assert.equal(isClaudePaidFallbackEnabled(), false);
|
|
273
277
|
const outcome = await maybeProbeClaudeCapacity(NOW_MS, { runner: throwingRunner });
|
|
274
278
|
assert.deepEqual(outcome, { probed: false, reason: "disabled" });
|
|
@@ -276,8 +280,8 @@ test("disabled fallback never spawns: every non-opt-in value is a strict no-op",
|
|
|
276
280
|
process.env[CLAUDE_CAPACITY_REFRESH_ENV] = "paid-after-1h";
|
|
277
281
|
assert.equal(isClaudePaidFallbackEnabled(), true);
|
|
278
282
|
});
|
|
279
|
-
test("
|
|
280
|
-
process.env[CLAUDE_CAPACITY_REFRESH_ENV]
|
|
283
|
+
test("default-on fallback suppresses a fresh sample; stale data triggers exactly one", async () => {
|
|
284
|
+
delete process.env[CLAUDE_CAPACITY_REFRESH_ENV];
|
|
281
285
|
fs.writeFileSync(cacheFile, fullCacheFixture(NOW_MS - 10 * 60_000));
|
|
282
286
|
assert.equal(ingestClaudeLocalCache(NOW_MS), 2);
|
|
283
287
|
assert.ok((newestValidClaudeObservationMs(NOW_MS) ?? 0) > NOW_MS - 60 * 60_000);
|
|
@@ -44,9 +44,11 @@
|
|
|
44
44
|
// clears the sentinel.
|
|
45
45
|
// - Capacity stays independent from `runtime_availability` hard-cap
|
|
46
46
|
// admission: this module never reads or writes health rows.
|
|
47
|
-
// - The
|
|
48
|
-
// `initialized`, `usage/read`)
|
|
49
|
-
//
|
|
47
|
+
// - The free read path enforces the read-only allowlist (`initialize`,
|
|
48
|
+
// `initialized`, `usage/read`) and never starts billable work. The separate
|
|
49
|
+
// default-on fallback in `muse-probe.ts` may create a dedicated restricted
|
|
50
|
+
// host for one fixed, bounded turn after the complete pair has been absent
|
|
51
|
+
// for an hour.
|
|
50
52
|
//
|
|
51
53
|
// Execution precondition (NOT-269): only a host that observes the account's
|
|
52
54
|
// provider traffic can answer `usage/read`. Real Dealer Muse turns run
|
|
@@ -54,12 +56,12 @@
|
|
|
54
56
|
// below, driven by runners/muse-serve-session.ts) — that traffic is the
|
|
55
57
|
// observation, so the session-boundary refresh hook
|
|
56
58
|
// (`refreshMuseCapacityAfterSession` in coordinator/muse-spawn.ts) is the
|
|
57
|
-
// final `usage/read` that populates 5H/1W.
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
59
|
+
// final `usage/read` that populates 5H/1W. If no genuine turn has populated
|
|
60
|
+
// a complete current pair for an hour, `muse-probe.ts` may use its own host
|
|
61
|
+
// for the bounded paid fallback. When the normal serve execution lane cannot
|
|
62
|
+
// admit a developer turn, that session still falls back to the legacy
|
|
63
|
+
// `muse exec` subprocess before any model work starts; last-good capacity
|
|
64
|
+
// rows remain preserved.
|
|
63
65
|
//
|
|
64
66
|
// PRODUCT DECISION (2026-09-26, resolved via human_action on this ticket):
|
|
65
67
|
// the runner migration is in scope for NOT-270, in this same PR, not a
|
|
@@ -128,6 +130,8 @@ export function prepareMuseServeHome(ambientAuthFile = resolveMuseAuthFile()) {
|
|
|
128
130
|
}
|
|
129
131
|
return { configHome, dataHome, authLinked };
|
|
130
132
|
}
|
|
133
|
+
const KEYCHAIN_UNREADABLE_RE = /keychain item .* unreadable|OSStatus\s+-?\d+/i;
|
|
134
|
+
const AUTH_FAILURE_RE = /credential|auth|login|sign[ -]?in|\b401\b|\b403\b/i;
|
|
131
135
|
function readOnlyRequest(id, method, params) {
|
|
132
136
|
assertMuseReadOnlyMethod(method);
|
|
133
137
|
return `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`;
|
|
@@ -458,6 +462,7 @@ export class MuseCapacityHost {
|
|
|
458
462
|
const timeoutMs = this.opts.timeoutMs ?? museCapacityTimeoutMs();
|
|
459
463
|
const mergedEnv = this.mergedEnv();
|
|
460
464
|
if (!hasMuseCredential(mergedEnv, this.opts.authFilePath ?? resolveMuseAuthFile())) {
|
|
465
|
+
this.startFailure = "credential_missing";
|
|
461
466
|
return false;
|
|
462
467
|
}
|
|
463
468
|
const command = this.opts.command ?? resolveMuseBin();
|
|
@@ -470,7 +475,8 @@ export class MuseCapacityHost {
|
|
|
470
475
|
});
|
|
471
476
|
}
|
|
472
477
|
catch (err) {
|
|
473
|
-
this.startFailure =
|
|
478
|
+
this.startFailure =
|
|
479
|
+
err?.code === "ENOENT" ? "binary_missing" : "spawn_failed";
|
|
474
480
|
return false;
|
|
475
481
|
}
|
|
476
482
|
this.child = child;
|
|
@@ -505,14 +511,17 @@ export class MuseCapacityHost {
|
|
|
505
511
|
return;
|
|
506
512
|
}
|
|
507
513
|
});
|
|
508
|
-
child.on("error", () => {
|
|
514
|
+
child.on("error", (err) => {
|
|
509
515
|
if (!this.isCurrentChild(child))
|
|
510
516
|
return;
|
|
517
|
+
this.startFailure =
|
|
518
|
+
err?.code === "ENOENT" ? "binary_missing" : "spawn_failed";
|
|
511
519
|
this.killChild();
|
|
512
520
|
});
|
|
513
521
|
child.stdin?.on("error", () => {
|
|
514
522
|
if (!this.isCurrentChild(child))
|
|
515
523
|
return;
|
|
524
|
+
this.startFailure = "transport_error";
|
|
516
525
|
this.killChild();
|
|
517
526
|
});
|
|
518
527
|
child.on("close", () => {
|
|
@@ -521,6 +530,15 @@ export class MuseCapacityHost {
|
|
|
521
530
|
// its replacement must not kill the new host.
|
|
522
531
|
if (!this.isCurrentChild(child))
|
|
523
532
|
return;
|
|
533
|
+
if (KEYCHAIN_UNREADABLE_RE.test(this.stderrTail)) {
|
|
534
|
+
this.startFailure = "keychain_unreadable";
|
|
535
|
+
}
|
|
536
|
+
else if (AUTH_FAILURE_RE.test(this.stderrTail)) {
|
|
537
|
+
this.startFailure = "auth_rejected";
|
|
538
|
+
}
|
|
539
|
+
else if (this.startFailure === null) {
|
|
540
|
+
this.startFailure = "host_exited";
|
|
541
|
+
}
|
|
524
542
|
this.killChild();
|
|
525
543
|
});
|
|
526
544
|
const initId = `muse-capacity-init-${this.connectionEpoch}`;
|
|
@@ -529,7 +547,14 @@ export class MuseCapacityHost {
|
|
|
529
547
|
if (init?.error) {
|
|
530
548
|
const kind = museClassifyRpcError(init.error);
|
|
531
549
|
this.startFailure =
|
|
532
|
-
kind === "auth"
|
|
550
|
+
kind === "auth"
|
|
551
|
+
? "auth_rejected"
|
|
552
|
+
: kind === "unsupported"
|
|
553
|
+
? "unsupported"
|
|
554
|
+
: "handshake_rejected";
|
|
555
|
+
}
|
|
556
|
+
else if (this.startFailure === null) {
|
|
557
|
+
this.startFailure = this.dead ? "host_exited" : "handshake_timeout";
|
|
533
558
|
}
|
|
534
559
|
this.killChild();
|
|
535
560
|
return false;
|
|
@@ -579,15 +604,20 @@ export class MuseCapacityHost {
|
|
|
579
604
|
this.startFailure = null;
|
|
580
605
|
const started = await this.ensureStarted();
|
|
581
606
|
if (!started) {
|
|
582
|
-
|
|
607
|
+
// `ensureStarted` records the failure asynchronously. TypeScript does
|
|
608
|
+
// not model that mutation across the awaited call, so widen the field
|
|
609
|
+
// back to its declared type before selecting the fallback category.
|
|
610
|
+
const diagnostic = this.startFailure ?? "spawn_failed";
|
|
611
|
+
if (diagnostic === "unsupported" || diagnostic === "binary_missing") {
|
|
612
|
+
console.error(`[muse-capacity] host start failed: ${diagnostic}`);
|
|
583
613
|
await noteMuseCapacityFailure("unsupported");
|
|
584
|
-
return { status: "failure", reason: "unsupported" };
|
|
614
|
+
return { status: "failure", reason: "unsupported", diagnostic };
|
|
585
615
|
}
|
|
586
616
|
// No credential, spawn failure, timeout, or bad exit before the
|
|
587
617
|
// handshake: honest `missing`, last-good rows preserved.
|
|
588
|
-
console.error(
|
|
618
|
+
console.error(`[muse-capacity] host start failed: ${diagnostic}`);
|
|
589
619
|
await noteMuseCapacityFailure("missing");
|
|
590
|
-
return { status: "missing" };
|
|
620
|
+
return { status: "missing", diagnostic };
|
|
591
621
|
}
|
|
592
622
|
this.readSeq += 1;
|
|
593
623
|
const id = `muse-capacity-${this.connectionEpoch}-${this.readSeq}`;
|
|
@@ -601,23 +631,23 @@ export class MuseCapacityHost {
|
|
|
601
631
|
if (!this.hasInflightExecution())
|
|
602
632
|
this.killChild();
|
|
603
633
|
await noteMuseCapacityFailure("missing");
|
|
604
|
-
return { status: "missing" };
|
|
634
|
+
return { status: "missing", diagnostic: "read_timeout" };
|
|
605
635
|
}
|
|
606
636
|
if (res.error) {
|
|
607
637
|
const kind = museClassifyRpcError(res.error);
|
|
608
638
|
if (kind === "auth") {
|
|
609
639
|
console.error("[muse-capacity] host read failed: unauthenticated");
|
|
610
640
|
await noteMuseCapacityFailure("missing");
|
|
611
|
-
return { status: "missing" };
|
|
641
|
+
return { status: "missing", diagnostic: "auth_rejected" };
|
|
612
642
|
}
|
|
613
643
|
if (kind === "unsupported") {
|
|
614
644
|
console.error("[muse-capacity] host read failed: unsupported");
|
|
615
645
|
await noteMuseCapacityFailure("unsupported");
|
|
616
|
-
return { status: "failure", reason: "unsupported" };
|
|
646
|
+
return { status: "failure", reason: "unsupported", diagnostic: "unsupported" };
|
|
617
647
|
}
|
|
618
648
|
console.error("[muse-capacity] host read failed: malformed");
|
|
619
649
|
await noteMuseCapacityFailure("unparsable");
|
|
620
|
-
return { status: "failure", reason: "unparsable" };
|
|
650
|
+
return { status: "failure", reason: "unparsable", diagnostic: "read_rejected" };
|
|
621
651
|
}
|
|
622
652
|
let payload = res.result;
|
|
623
653
|
if (payload &&
|
|
@@ -633,7 +663,7 @@ export class MuseCapacityHost {
|
|
|
633
663
|
// Fresh/unobserved host: `usage` omitted — honest `missing`, never a
|
|
634
664
|
// write, so a newer known pair is never overwritten or deleted.
|
|
635
665
|
await noteMuseCapacityFailure("missing");
|
|
636
|
-
return { status: "missing" };
|
|
666
|
+
return { status: "missing", diagnostic: "unobserved" };
|
|
637
667
|
}
|
|
638
668
|
return { status: "observed", written: outcome.written, skippedStale: outcome.skippedStale };
|
|
639
669
|
}
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
// - the first observation arrives as a `usage/changed` notification;
|
|
15
15
|
// - a fresh/unobserved host reads `missing` (restart is fresh — the client
|
|
16
16
|
// holds no cross-process usage state);
|
|
17
|
-
// - resume/turn methods are refused before write
|
|
18
|
-
// manufacture a billable call.
|
|
17
|
+
// - resume/turn methods are refused before write on the free adapter path.
|
|
19
18
|
import { test } from "node:test";
|
|
20
19
|
import assert from "node:assert/strict";
|
|
21
20
|
import { assertMuseReadOnlyMethod, museUsageToReadings, readMuseCapacity, requestMuseUsage, } from "./muse.js";
|
|
@@ -90,7 +89,7 @@ test("lifecycle: a fresh host with no observation reads missing — twice", asyn
|
|
|
90
89
|
assert.equal(result.unavailable[0].reason, "missing");
|
|
91
90
|
}
|
|
92
91
|
});
|
|
93
|
-
test("lifecycle:
|
|
92
|
+
test("lifecycle: the free read adapter cannot manufacture a session, turn, or prompt", () => {
|
|
94
93
|
for (const m of ["session/start", "session/resume", "session/prompt", "turn/start", "exec"]) {
|
|
95
94
|
assert.throws(() => assertMuseReadOnlyMethod(m), /refusing non-read method/);
|
|
96
95
|
}
|
|
@@ -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
|
+
});
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
// retired: only a host that observed the account's provider traffic can
|
|
10
10
|
// answer `usage/read` / emit `usage/changed`.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
// sending a prompt
|
|
12
|
+
// The free read phase reads observed capacity through the stable Muse Session
|
|
13
|
+
// Protocol without sending a prompt or consuming model tokens: `initialize` →
|
|
14
14
|
// `initialized` → `usage/read` on the owned host, with `usage/changed`
|
|
15
15
|
// notifications ingested as received. The client enforces a read-only
|
|
16
16
|
// allowlist (`initialize`, `initialized`, `usage/read`) — any other method
|
|
17
17
|
// throws before it is written, so a capacity read can never start a
|
|
18
|
-
// session, send a prompt, or run model work.
|
|
19
|
-
//
|
|
18
|
+
// session, send a prompt, or run model work. The separate default-on fallback
|
|
19
|
+
// in `muse-probe.ts` runs only after this phase still lacks a complete current
|
|
20
|
+
// pair for an hour. Neither path touches the Keychain or undocumented endpoints.
|
|
20
21
|
//
|
|
21
22
|
// Stable `usage/read` shape:
|
|
22
23
|
// result: {
|
|
@@ -48,6 +49,11 @@ import fs from "node:fs";
|
|
|
48
49
|
import { MUSE_CLI_ENV, resolveMuseAuthFile, resolveMuseBin } from "../cli-env.js";
|
|
49
50
|
import { normalizeAdapterWindow, normalizeUnavailableWindow, } from "./adapter.js";
|
|
50
51
|
export const MUSE_RUNTIME = "muse_code";
|
|
52
|
+
/** Muse observations remain the last-known current value until the one-hour
|
|
53
|
+
* fallback boundary. This avoids the shared adapter's generic 15-minute N/A
|
|
54
|
+
* gap before a bounded refresh is eligible to run. */
|
|
55
|
+
export const MUSE_CAPACITY_STALE_AFTER_MS = 60 * 60 * 1000;
|
|
56
|
+
export const MUSE_CAPACITY_EXPIRES_AFTER_MS = 60 * 60 * 1000;
|
|
51
57
|
/** Methods this client may ever send. Anything else throws before write. */
|
|
52
58
|
export const MSP_READ_ONLY_METHODS = ["initialize", "initialized", "usage/read"];
|
|
53
59
|
/**
|
|
@@ -189,6 +195,8 @@ function windowReadingFromEntry(kind, entry, observedAt) {
|
|
|
189
195
|
usedPercent,
|
|
190
196
|
resetAt,
|
|
191
197
|
observedAt,
|
|
198
|
+
staleAfterMs: MUSE_CAPACITY_STALE_AFTER_MS,
|
|
199
|
+
expiresAfterMs: MUSE_CAPACITY_EXPIRES_AFTER_MS,
|
|
192
200
|
source: "supported_protocol",
|
|
193
201
|
// Muse reports exactly this one rolling/weekly pair — it is always the
|
|
194
202
|
// account-wide critical window, never a model-specific extra.
|
|
@@ -868,8 +876,10 @@ export function createMuseCapacityAdapter(opts = {}) {
|
|
|
868
876
|
export const MUSE_REFRESH_THROTTLE_MS_DEFAULT = 5 * 60 * 1000;
|
|
869
877
|
/**
|
|
870
878
|
* Throttle bound for production refreshes. Override with
|
|
871
|
-
* `AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS` (milliseconds); `off` disables
|
|
872
|
-
*
|
|
879
|
+
* `AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS` (milliseconds); `off` disables the
|
|
880
|
+
* free host read and, for backward-compatible no-spend behavior, also disables
|
|
881
|
+
* the paid fallback unless `AGENT_DEALER_MUSE_CAPACITY_REFRESH=paid-after-1h`
|
|
882
|
+
* is explicit. Non-positive or unparsable values fall back to the default.
|
|
873
883
|
* Shared with the owned host (`./muse-host.js`).
|
|
874
884
|
*/
|
|
875
885
|
export function museRefreshThrottleMs() {
|
|
@@ -8,7 +8,7 @@ import { getLinearUsageSnapshot } from "../adapters/linear-graphql.js";
|
|
|
8
8
|
import { listRuntimeModels } from "../runners/models.js";
|
|
9
9
|
import { configuredCapacityRuntimes, getRuntimeCapacitySnapshot } from "../capacity/service.js";
|
|
10
10
|
import { refreshClaudeCapacityIfStale } from "../capacity/claude-local-cache.js";
|
|
11
|
-
import {
|
|
11
|
+
import { refreshMuseCapacityIfStale } from "../capacity/muse-probe.js";
|
|
12
12
|
import { refreshCodexCapacityIfStale } from "../capacity/codex-app-server.js";
|
|
13
13
|
import { getCursorTeamBillingSnapshot, refreshCursorTeamBillingIfStale, } from "../capacity/cursor-team.js";
|
|
14
14
|
import { getCursorIndividualBillingSnapshot, refreshCursorIndividualBillingIfStale, refreshCursorIndividualCapacityIfStale, } from "../capacity/cursor-individual.js";
|
|
@@ -63,13 +63,12 @@ export async function registerRoutes(app) {
|
|
|
63
63
|
// NOT-245: provider-neutral capacity read model — one entry per configured
|
|
64
64
|
// runtime account with its windows, freshness, and explicit unavailable
|
|
65
65
|
// reasons. Normalized snapshots only; evidence stays server-side.
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// preserve last-good rows as N/A and never fail the read.
|
|
66
|
+
// Muse capacity: first take the free throttled read from the server-owned
|
|
67
|
+
// execution host. If there is still no complete observation under an hour
|
|
68
|
+
// old, the default-on bounded fallback runs one minimal turn on a dedicated
|
|
69
|
+
// restricted host and reads 5H/1W from that same host. This stays in the
|
|
70
|
+
// background: GET serves last-good immediately and never waits on model
|
|
71
|
+
// work. `AGENT_DEALER_MUSE_CAPACITY_REFRESH=off` disables paid fallback.
|
|
73
72
|
// NOT-246: on-demand Codex refresh — when the stored Codex snapshot is
|
|
74
73
|
// stale the read triggers one bounded, non-billable App Server poll
|
|
75
74
|
// (single-flight, never health rows, never throws); fresh snapshots and
|
|
@@ -79,9 +78,9 @@ export async function registerRoutes(app) {
|
|
|
79
78
|
// Claude Code's own free cache (plain file read, never a spawn) and then
|
|
80
79
|
// considers the paid probe without blocking the read: a bounded Haiku
|
|
81
80
|
// probe fires at most once per hour and only under the explicit
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// no
|
|
81
|
+
// valid 5H/1W observation is older than 60 minutes. This is the default;
|
|
82
|
+
// `AGENT_DEALER_CLAUDE_CAPACITY_REFRESH=off` is a strict no-op (no spawn,
|
|
83
|
+
// no spend). Failures never break the read below.
|
|
85
84
|
app.get("/api/runtime-capacity", async () => {
|
|
86
85
|
try {
|
|
87
86
|
void refreshClaudeCapacityIfStale().catch(() => {
|
|
@@ -93,7 +92,7 @@ export async function registerRoutes(app) {
|
|
|
93
92
|
}
|
|
94
93
|
try {
|
|
95
94
|
if (configuredCapacityRuntimes().includes("muse_code")) {
|
|
96
|
-
void
|
|
95
|
+
void refreshMuseCapacityIfStale().catch(() => {
|
|
97
96
|
// Best-effort: failures preserve last-good rows via the ingest path.
|
|
98
97
|
});
|
|
99
98
|
}
|
|
@@ -15,6 +15,12 @@ process.env.AGENT_DEALER_SKIP_AGENT_HEALTH = "1";
|
|
|
15
15
|
// Never spawn a real provider from route tests: the on-demand Codex refresh is
|
|
16
16
|
// covered against the fake App Server in codex-app-server.test.ts.
|
|
17
17
|
process.env.AGENT_DEALER_CODEX_CAPACITY_REFRESH = "off";
|
|
18
|
+
// Claude's paid-after-1h fallback is default-on in production. Route tests
|
|
19
|
+
// exercise stored/read behavior only and must never launch a real paid probe.
|
|
20
|
+
process.env.AGENT_DEALER_CLAUDE_CAPACITY_REFRESH = "off";
|
|
21
|
+
// Muse's one-hour fallback is also default-on; route tests cover the free
|
|
22
|
+
// host trigger only and must never run a real model turn.
|
|
23
|
+
process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH = "off";
|
|
18
24
|
// NOT-268: the route also ingests the real ~/.claude.json cache on every read. Point it at a
|
|
19
25
|
// path that never exists so a developer's own Claude usage never leaks extra windows into the
|
|
20
26
|
// fixture-controlled assertions below (this passed in CI, which has no such file, but failed
|
|
@@ -62,9 +68,10 @@ test("GET /api/runtime-capacity returns normalized entries without evidence", as
|
|
|
62
68
|
assert.ok(!raw.includes("evidence"), "no evidence pointers leak to the browser");
|
|
63
69
|
await app.close();
|
|
64
70
|
});
|
|
65
|
-
test("GET triggers the Muse refresh when muse_code is configured", async () => {
|
|
66
|
-
//
|
|
67
|
-
//
|
|
71
|
+
test("GET triggers the free Muse refresh when muse_code is configured", async () => {
|
|
72
|
+
// The route is the production trigger for Muse capacity. This suite sets
|
|
73
|
+
// the paid fallback off, so it exercises only the free owned-host read.
|
|
74
|
+
// No credential here (env key removed, empty login dir),
|
|
68
75
|
// so the refresh short-circuits to a `missing` sentinel without spawning
|
|
69
76
|
// anything live.
|
|
70
77
|
clearAllCapacitySnapshots();
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// packages/server/src/runners/muse-serve-session.ts
|
|
2
2
|
//
|
|
3
|
-
// NOT-270: run a
|
|
4
|
-
//
|
|
3
|
+
// NOT-270: run a Muse turn through a caller-owned `muse serve` host (the
|
|
4
|
+
// NOT-269 proven lifecycle). Only a host that
|
|
5
5
|
// observes the account's provider traffic can answer `usage/read`, so the
|
|
6
|
-
// observation opportunity
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// observation opportunity is a `session/start` + `turn/start` on that host.
|
|
7
|
+
// Production callers use this for genuine Dealer developer work and, when
|
|
8
|
+
// capacity has been unavailable for an hour, the dedicated bounded capacity
|
|
9
|
+
// probe. The capacity probe owns a separate restricted host and shuts it down
|
|
10
|
+
// after its final `usage/read`.
|
|
10
11
|
//
|
|
11
12
|
// Wire contract (stable MSP, Muse 1.4.x):
|
|
12
13
|
// session/start {commandId UUIDv7, workspaceRoot, modelId, approvalMode}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-dealer/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-dealer/shared": "1.2.
|
|
25
|
+
"@agent-dealer/shared": "1.2.4",
|
|
26
26
|
"@fastify/cors": "^11.0.1",
|
|
27
27
|
"@fastify/static": "^8.2.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/dist/doctor.d.ts
CHANGED
|
@@ -23,9 +23,10 @@ export declare function describeCursorIndividualLogin(status: {
|
|
|
23
23
|
export declare function checkCursorIndividualLogin(): Promise<CursorIndividualLoginReport>;
|
|
24
24
|
/** Env override for the Claude cache file (tests/smoke). */
|
|
25
25
|
export declare const CLAUDE_CAPACITY_CACHE_FILE_ENV = "AGENT_DEALER_CLAUDE_CACHE_FILE";
|
|
26
|
-
/** Paid-fallback
|
|
26
|
+
/** Paid-fallback setting: enabled by default; `off` disables it. */
|
|
27
27
|
export declare const CLAUDE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_CLAUDE_CAPACITY_REFRESH";
|
|
28
28
|
export declare const CLAUDE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
|
|
29
|
+
export declare const CLAUDE_CAPACITY_REFRESH_OFF_VALUE = "off";
|
|
29
30
|
/** Which local 5H/1W cache state the capacity ladder would read. */
|
|
30
31
|
export type ClaudeCapacitySourceKind = "fresh" | "stale" | "missing";
|
|
31
32
|
export interface ClaudeCapacitySourceReport {
|
|
@@ -42,11 +43,25 @@ export declare function describeClaudeCapacitySource(status: {
|
|
|
42
43
|
ageMs: number | null;
|
|
43
44
|
} | null | undefined): ClaudeCapacitySourceReport;
|
|
44
45
|
/**
|
|
45
|
-
* Map the paid-fallback setting to
|
|
46
|
-
*
|
|
46
|
+
* Map the paid-fallback setting to its warning line, or null when explicitly
|
|
47
|
+
* disabled (or unrecognized). Unset/empty is the paid-after-1h default.
|
|
47
48
|
* Pure (no I/O): unit tests pin both states here.
|
|
48
49
|
*/
|
|
49
50
|
export declare function describeClaudeProbeOptIn(setting: string | undefined): string | null;
|
|
51
|
+
/** Paid-fallback setting: enabled by default; `off` disables it. */
|
|
52
|
+
export declare const MUSE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_MUSE_CAPACITY_REFRESH";
|
|
53
|
+
export declare const MUSE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
|
|
54
|
+
export declare const MUSE_CAPACITY_REFRESH_OFF_VALUE = "off";
|
|
55
|
+
/** Legacy free-read throttle; `off` also preserves no-spend behavior when the
|
|
56
|
+
* new setting is absent. */
|
|
57
|
+
export declare const MUSE_CAPACITY_REFRESH_MS_ENV = "AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS";
|
|
58
|
+
/**
|
|
59
|
+
* Match the server's Muse paid-fallback precedence exactly. The explicit new
|
|
60
|
+
* paid value overrides a legacy free-refresh `off`; otherwise that legacy
|
|
61
|
+
* switch remains no-spend for backward compatibility. Unknown new values
|
|
62
|
+
* fail closed. Pure (no I/O) so doctor never risks a provider request.
|
|
63
|
+
*/
|
|
64
|
+
export declare function describeMuseProbeOptIn(setting: string | undefined, legacyRefreshMs: string | undefined): string | null;
|
|
50
65
|
/**
|
|
51
66
|
* Read the `cachedUsageUtilization.fetchedAtMs` timestamp out of Claude
|
|
52
67
|
* Code's config file (override first, then `~/.claude.json`) — the same key
|
package/dist/doctor.js
CHANGED
|
@@ -117,6 +117,9 @@ export async function runDoctor() {
|
|
|
117
117
|
const probe = describeClaudeProbeOptIn(process.env.AGENT_DEALER_CLAUDE_CAPACITY_REFRESH);
|
|
118
118
|
if (probe)
|
|
119
119
|
console.warn(probe);
|
|
120
|
+
const museProbe = describeMuseProbeOptIn(process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH, process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS);
|
|
121
|
+
if (museProbe)
|
|
122
|
+
console.warn(museProbe);
|
|
120
123
|
}
|
|
121
124
|
catch {
|
|
122
125
|
// Informational only: a broken probe must never fail doctor.
|
|
@@ -261,9 +264,10 @@ export async function checkCursorIndividualLogin() {
|
|
|
261
264
|
// ---------------------------------------------------------------------------
|
|
262
265
|
/** Env override for the Claude cache file (tests/smoke). */
|
|
263
266
|
export const CLAUDE_CAPACITY_CACHE_FILE_ENV = "AGENT_DEALER_CLAUDE_CACHE_FILE";
|
|
264
|
-
/** Paid-fallback
|
|
267
|
+
/** Paid-fallback setting: enabled by default; `off` disables it. */
|
|
265
268
|
export const CLAUDE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_CLAUDE_CAPACITY_REFRESH";
|
|
266
269
|
export const CLAUDE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
|
|
270
|
+
export const CLAUDE_CAPACITY_REFRESH_OFF_VALUE = "off";
|
|
267
271
|
/**
|
|
268
272
|
* Map a cache-file presence/age probe to the doctor line. Pure (no I/O):
|
|
269
273
|
* unit tests pin all three states here.
|
|
@@ -298,17 +302,42 @@ function formatCacheAge(ageMs) {
|
|
|
298
302
|
return `${Math.floor(hours / 24)}d`;
|
|
299
303
|
}
|
|
300
304
|
/**
|
|
301
|
-
* Map the paid-fallback setting to
|
|
302
|
-
*
|
|
305
|
+
* Map the paid-fallback setting to its warning line, or null when explicitly
|
|
306
|
+
* disabled (or unrecognized). Unset/empty is the paid-after-1h default.
|
|
303
307
|
* Pure (no I/O): unit tests pin both states here.
|
|
304
308
|
*/
|
|
305
309
|
export function describeClaudeProbeOptIn(setting) {
|
|
306
|
-
if (setting === CLAUDE_CAPACITY_REFRESH_PAID_VALUE) {
|
|
307
|
-
return ("⚠ Claude capacity paid fallback:
|
|
308
|
-
`
|
|
310
|
+
if (setting === undefined || setting === "" || setting === CLAUDE_CAPACITY_REFRESH_PAID_VALUE) {
|
|
311
|
+
return ("⚠ Claude capacity paid fallback: enabled (one ≤$0.01 Haiku probe after 1h stale — " +
|
|
312
|
+
`set ${CLAUDE_CAPACITY_REFRESH_ENV}=${CLAUDE_CAPACITY_REFRESH_OFF_VALUE} to disable)`);
|
|
309
313
|
}
|
|
310
314
|
return null;
|
|
311
315
|
}
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Muse capacity paid-fallback reporting for doctor.
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
/** Paid-fallback setting: enabled by default; `off` disables it. */
|
|
320
|
+
export const MUSE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_MUSE_CAPACITY_REFRESH";
|
|
321
|
+
export const MUSE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
|
|
322
|
+
export const MUSE_CAPACITY_REFRESH_OFF_VALUE = "off";
|
|
323
|
+
/** Legacy free-read throttle; `off` also preserves no-spend behavior when the
|
|
324
|
+
* new setting is absent. */
|
|
325
|
+
export const MUSE_CAPACITY_REFRESH_MS_ENV = "AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS";
|
|
326
|
+
/**
|
|
327
|
+
* Match the server's Muse paid-fallback precedence exactly. The explicit new
|
|
328
|
+
* paid value overrides a legacy free-refresh `off`; otherwise that legacy
|
|
329
|
+
* switch remains no-spend for backward compatibility. Unknown new values
|
|
330
|
+
* fail closed. Pure (no I/O) so doctor never risks a provider request.
|
|
331
|
+
*/
|
|
332
|
+
export function describeMuseProbeOptIn(setting, legacyRefreshMs) {
|
|
333
|
+
const enabled = setting === MUSE_CAPACITY_REFRESH_PAID_VALUE ||
|
|
334
|
+
((setting === undefined || setting === "") && legacyRefreshMs !== "off");
|
|
335
|
+
if (!enabled)
|
|
336
|
+
return null;
|
|
337
|
+
return ("⚠ Muse capacity paid fallback: enabled when Muse is configured " +
|
|
338
|
+
"(one restricted contributor-model turn after 1h stale — " +
|
|
339
|
+
`set ${MUSE_CAPACITY_REFRESH_ENV}=${MUSE_CAPACITY_REFRESH_OFF_VALUE} to disable)`);
|
|
340
|
+
}
|
|
312
341
|
/**
|
|
313
342
|
* Read the `cachedUsageUtilization.fetchedAtMs` timestamp out of Claude
|
|
314
343
|
* Code's config file (override first, then `~/.claude.json`) — the same key
|
package/dist/doctor.test.js
CHANGED
|
@@ -9,6 +9,7 @@ import assert from "node:assert/strict";
|
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
12
13
|
import Database from "better-sqlite3";
|
|
13
14
|
// Local mirror of the server module's env/key contract
|
|
14
15
|
// (packages/server/src/capacity/cursor-individual-credentials.ts). A static
|
|
@@ -45,7 +46,7 @@ test("fixture env/key names match the server module's live contract", async () =
|
|
|
45
46
|
assert.equal(mod.CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV, CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV);
|
|
46
47
|
assert.equal(mod.CURSOR_INDIVIDUAL_HOME_ENV, CURSOR_INDIVIDUAL_HOME_ENV);
|
|
47
48
|
});
|
|
48
|
-
import { CLAUDE_CAPACITY_CACHE_FILE_ENV, CLAUDE_CAPACITY_REFRESH_ENV, CLAUDE_CAPACITY_REFRESH_PAID_VALUE, CURSOR_INDIVIDUAL_LOGIN_LINES, checkClaudeCapacitySource, checkCursorIndividualLogin, describeClaudeCapacitySource, describeClaudeProbeOptIn, describeCursorIndividualLogin, } from "./doctor.js";
|
|
49
|
+
import { CLAUDE_CAPACITY_CACHE_FILE_ENV, CLAUDE_CAPACITY_REFRESH_ENV, CLAUDE_CAPACITY_REFRESH_PAID_VALUE, CURSOR_INDIVIDUAL_LOGIN_LINES, MUSE_CAPACITY_REFRESH_ENV, MUSE_CAPACITY_REFRESH_MS_ENV, MUSE_CAPACITY_REFRESH_PAID_VALUE, checkClaudeCapacitySource, checkCursorIndividualLogin, describeClaudeCapacitySource, describeClaudeProbeOptIn, describeCursorIndividualLogin, describeMuseProbeOptIn, } from "./doctor.js";
|
|
49
50
|
const SECRET = "doctor-fixture-secret-must-never-print";
|
|
50
51
|
const USER_ID = "user_doctor_fixture";
|
|
51
52
|
function fixtureJwt(sub) {
|
|
@@ -188,15 +189,44 @@ test("describe maps each cache state to its static line", () => {
|
|
|
188
189
|
assert.match(report.line, /no local 5H\/1W cache/);
|
|
189
190
|
}
|
|
190
191
|
});
|
|
191
|
-
test("probe
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
test("probe warning appears for the default and explicit paid value", () => {
|
|
193
|
+
for (const setting of [undefined, "", CLAUDE_CAPACITY_REFRESH_PAID_VALUE]) {
|
|
194
|
+
const armed = describeClaudeProbeOptIn(setting);
|
|
195
|
+
assert.ok(armed);
|
|
196
|
+
assert.match(armed, /enabled/);
|
|
197
|
+
assert.match(armed, /≤\$0\.01/);
|
|
198
|
+
assert.match(armed, /=off/);
|
|
199
|
+
}
|
|
194
200
|
assert.equal(describeClaudeProbeOptIn("off"), null);
|
|
195
201
|
assert.equal(describeClaudeProbeOptIn("auto"), null);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
});
|
|
203
|
+
test("Muse paid-fallback warning matches server defaults and legacy no-spend precedence", () => {
|
|
204
|
+
for (const setting of [undefined, "", MUSE_CAPACITY_REFRESH_PAID_VALUE]) {
|
|
205
|
+
const armed = describeMuseProbeOptIn(setting, undefined);
|
|
206
|
+
assert.ok(armed);
|
|
207
|
+
assert.match(armed, /Muse capacity paid fallback: enabled/);
|
|
208
|
+
assert.match(armed, /restricted contributor-model turn/);
|
|
209
|
+
assert.match(armed, new RegExp(`${MUSE_CAPACITY_REFRESH_ENV}=off`));
|
|
210
|
+
}
|
|
211
|
+
assert.equal(describeMuseProbeOptIn("off", undefined), null);
|
|
212
|
+
assert.equal(describeMuseProbeOptIn("auto", undefined), null);
|
|
213
|
+
assert.equal(describeMuseProbeOptIn(undefined, "off"), null);
|
|
214
|
+
assert.equal(describeMuseProbeOptIn("", "off"), null);
|
|
215
|
+
assert.ok(describeMuseProbeOptIn(MUSE_CAPACITY_REFRESH_PAID_VALUE, "off"), "the explicit new paid value overrides the legacy free-refresh switch");
|
|
216
|
+
});
|
|
217
|
+
test("Muse doctor env names and paid value match the server contract", () => {
|
|
218
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
219
|
+
const candidates = [
|
|
220
|
+
path.resolve(here, "..", "..", "server", "src", "capacity", "muse-probe.ts"),
|
|
221
|
+
path.resolve(here, "..", "..", "server", "dist", "capacity", "muse-probe.js"),
|
|
222
|
+
];
|
|
223
|
+
const src = candidates.find((file) => fs.existsSync(file));
|
|
224
|
+
assert.ok(src, "the server Muse probe module must exist for the contract pin");
|
|
225
|
+
const text = fs.readFileSync(src, "utf8");
|
|
226
|
+
assert.ok(text.includes(`"${MUSE_CAPACITY_REFRESH_ENV}"`));
|
|
227
|
+
assert.ok(text.includes(`"${MUSE_CAPACITY_REFRESH_PAID_VALUE}"`));
|
|
228
|
+
assert.ok(text.includes("AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS"));
|
|
229
|
+
assert.equal(MUSE_CAPACITY_REFRESH_MS_ENV, "AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS");
|
|
200
230
|
});
|
|
201
231
|
test("check reads fetchedAtMs from the config key, never mtime or the real home", async () => {
|
|
202
232
|
const now = Date.now();
|