agent-dealer 1.2.2 → 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.
Files changed (32) hide show
  1. package/bundle/server/dist/capacity/claude-events.js +27 -10
  2. package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
  3. package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -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 +748 -0
  9. package/bundle/server/dist/capacity/muse-host.test.js +480 -0
  10. package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
  11. package/bundle/server/dist/capacity/muse.js +307 -60
  12. package/bundle/server/dist/capacity/muse.test.js +8 -51
  13. package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
  14. package/bundle/server/dist/index.js +15 -2
  15. package/bundle/server/dist/routes/index.js +26 -9
  16. package/bundle/server/dist/routes/runtime-capacity.test.js +12 -6
  17. package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
  18. package/bundle/server/dist/runners/muse-serve-session.js +313 -0
  19. package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
  20. package/bundle/server/package.json +2 -2
  21. package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
  22. package/bundle/server/static-ui/assets/{index-DUYzboWh.css → index-CYqRh_-S.css} +1 -1
  23. package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
  24. package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
  25. package/bundle/server/static-ui/index.html +2 -2
  26. package/bundle/shared/package.json +1 -1
  27. package/dist/doctor.d.ts +57 -0
  28. package/dist/doctor.js +206 -1
  29. package/dist/doctor.test.d.ts +1 -0
  30. package/dist/doctor.test.js +246 -0
  31. package/package.json +1 -1
  32. package/bundle/server/static-ui/assets/index-0GDYfbaM.js +0 -60
@@ -3,16 +3,20 @@
3
3
  // NOT-247: Muse Code capacity adapter — read stable MSP usage windows.
4
4
  // NOT-263: handshake repaired against the stable schema embedded in the
5
5
  // shipped binary (`muse schema generate-json-schema`).
6
+ // NOT-270: production reads go through the server-owned long-lived host
7
+ // (`./muse-host.js`). NOT-269 proved a fresh `muse serve` host is
8
+ // structurally unobserved, so the old spawn → `usage/read` → exit poll is
9
+ // retired: only a host that observed the account's provider traffic can
10
+ // answer `usage/read` / emit `usage/changed`.
6
11
  //
7
12
  // Reads observed capacity through the stable Muse Session Protocol without
8
- // sending a prompt and without consuming model tokens: `muse serve` over a
9
- // one-shot managed stdio connection, then `initialize` → `initialized` → one
10
- // `usage/read`, then shutdown. `usage/changed` notifications received while
11
- // the connection is alive are recorded. The client enforces a read-only
13
+ // sending a prompt and without consuming model tokens: `initialize` →
14
+ // `initialized` → `usage/read` on the owned host, with `usage/changed`
15
+ // notifications ingested as received. The client enforces a read-only
12
16
  // allowlist (`initialize`, `initialized`, `usage/read`) — any other method
13
- // throws before it is written, so polling can never start a session, send a
14
- // prompt, or run model work. It never touches the Keychain or undocumented
15
- // endpoints.
17
+ // throws before it is written, so a capacity read can never start a
18
+ // session, send a prompt, or run model work. It never touches the Keychain
19
+ // or undocumented endpoints.
16
20
  //
17
21
  // Stable `usage/read` shape:
18
22
  // result: {
@@ -39,12 +43,28 @@
39
43
  // adapter passes no credentials (the serve subprocess uses its ambient
40
44
  // session) and evidence refs are static strings.
41
45
  import { spawn } from "node:child_process";
46
+ import { randomBytes } from "node:crypto";
42
47
  import fs from "node:fs";
43
48
  import { MUSE_CLI_ENV, resolveMuseAuthFile, resolveMuseBin } from "../cli-env.js";
44
49
  import { normalizeAdapterWindow, normalizeUnavailableWindow, } from "./adapter.js";
45
50
  export const MUSE_RUNTIME = "muse_code";
46
51
  /** Methods this client may ever send. Anything else throws before write. */
47
52
  export const MSP_READ_ONLY_METHODS = ["initialize", "initialized", "usage/read"];
53
+ /**
54
+ * NOT-270: execution methods for REAL Dealer work through the server-owned
55
+ * serve host (the NOT-269 proven lifecycle: only a host that observes the
56
+ * account's provider traffic can answer `usage/read`). This is a separate
57
+ * allowlist from the read-only one above — a capacity read still never sends
58
+ * these. `session/resume` stays forbidden (state-changing, carries no usage
59
+ * per NOT-269 leg 2); `turn/steer` and approval writes are never sent by the
60
+ * headless runner, so they are refused here too.
61
+ */
62
+ export const MSP_EXEC_METHODS = [
63
+ "session/start",
64
+ "turn/start",
65
+ "turn/cancel",
66
+ "session/read",
67
+ ];
48
68
  /**
49
69
  * Client identity sent in the `initialize` handshake. The stable schema
50
70
  * requires `clientInfo.name` to match `^[a-z0-9_]+$` plus a version string.
@@ -68,6 +88,27 @@ export function assertMuseReadOnlyMethod(method) {
68
88
  throw new Error(`muse-serve: refusing non-read method ${method}`);
69
89
  }
70
90
  }
91
+ export function assertMuseExecMethod(method) {
92
+ if (!MSP_EXEC_METHODS.includes(method)) {
93
+ throw new Error(`muse-serve: refusing non-execution method ${method}`);
94
+ }
95
+ }
96
+ /**
97
+ * UUIDv7 for MSP `commandId` idempotency handles (session/start, turn/start,
98
+ * turn/cancel require UUIDv7). Time-ordered; randomness from crypto.
99
+ */
100
+ export function museUuidv7(nowMs = Date.now()) {
101
+ const b = Buffer.alloc(16);
102
+ const ts = BigInt(nowMs) & ((1n << 48n) - 1n);
103
+ for (let i = 0; i < 6; i++)
104
+ b[i] = Number((ts >> BigInt(40 - 8 * i)) & 0xffn);
105
+ randomBytes(10).copy(b, 6);
106
+ b[6] = 0x70 | (b[6] & 0x0f);
107
+ b[8] = 0x80 | (b[8] & 0x3f);
108
+ const hex = b.toString("hex");
109
+ return (`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-` +
110
+ `${hex.slice(16, 20)}-${hex.slice(20)}`);
111
+ }
71
112
  function readOnlyRequest(id, method, params) {
72
113
  assertMuseReadOnlyMethod(method);
73
114
  return `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`;
@@ -79,9 +120,17 @@ function readOnlyNotification(method, params) {
79
120
  /**
80
121
  * Stable Session Protocol argv. Fixed: capacity reads never `exec`, and the
81
122
  * host takes no `--protocol` flag — `muse serve` with that flag exits 2
82
- * against the shipped binary, so this stays exactly `["serve"]`.
123
+ * against the shipped binary.
124
+ *
125
+ * NOT-270: the owned host is also the execution host for real Dealer Muse
126
+ * turns (the NOT-269 precondition — only a host that observes provider
127
+ * traffic can answer `usage/read`). Sandbox posture is fixed at host
128
+ * startup and applies to every session on the host, so the host carries the
129
+ * same `--sandbox-network restricted` every `muse exec` developer invocation
130
+ * already passes (see runners/muse-code-args.ts — a fixed constant, no
131
+ * per-session variation). Approval mode stays per-session on the wire.
83
132
  */
84
- export const MUSE_SERVE_ARGV = ["serve"];
133
+ export const MUSE_SERVE_ARGV = ["serve", "--sandbox-network", "restricted"];
85
134
  /** JSON-RPC request ids for the one-shot read (init handshake, then usage). */
86
135
  export const MSP_INITIALIZE_ID = "muse-capacity-init";
87
136
  export const MSP_USAGE_READ_ID = "muse-capacity-1";
@@ -237,7 +286,8 @@ export function museCapacityTimeoutMs() {
237
286
  return 15_000;
238
287
  }
239
288
  const AUTH_RE = /login is no longer valid|authentication failed|missing \w+ credentials|api key .*rejected|unauthenticated|unauthorized|unauthori[sz]ed|not (signed|logged) in|sign in|\b401\b|\b403\b|forbidden/i;
240
- function parseRpcLine(line) {
289
+ /** Parse one stdio JSON-RPC line; null for blank/non-JSON chatter. Shared with the owned host. */
290
+ export function parseMuseRpcLine(line) {
241
291
  const t = line.trim();
242
292
  if (!t)
243
293
  return null;
@@ -257,8 +307,19 @@ function errorLooksAuth(error) {
257
307
  const e = error;
258
308
  return AUTH_RE.test(String(e.message ?? "")) || e.code === 401 || e.code === 403;
259
309
  }
310
+ /** Classify an MSP RPC error for the owned host: auth, missing method, or other. */
311
+ export function museClassifyRpcError(error) {
312
+ if (errorLooksAuth(error))
313
+ return "auth";
314
+ if (error !== null &&
315
+ typeof error === "object" &&
316
+ error.code === -32601) {
317
+ return "unsupported";
318
+ }
319
+ return "other";
320
+ }
260
321
  /** The only credential signals the repo knows (cf. agent-health): env key or login file. */
261
- function hasMuseCredential(env, authFilePath) {
322
+ export function hasMuseCredential(env, authFilePath) {
262
323
  const key = env.META_API_KEY;
263
324
  if (typeof key === "string" && key.length > 0)
264
325
  return true;
@@ -273,8 +334,11 @@ function hasMuseCredential(env, authFilePath) {
273
334
  * Spawn `muse serve`, run the stable handshake (`initialize` with a
274
335
  * `clientInfo` identity, then the `initialized` notification), send one
275
336
  * `usage/read`, record any `usage/changed` notifications, then shut the
276
- * connection down. Never throws for provider-side failures — those come back
277
- * as `failure.kind`. Never sends anything outside MSP_READ_ONLY_METHODS.
337
+ * connection down. Low-level primitive (unit-tested); production uses the
338
+ * owned long-lived host (`./muse-host.js`) — a fresh connection is
339
+ * unobserved by construction (NOT-269). Never throws for provider-side
340
+ * failures — those come back as `failure.kind`. Never sends anything
341
+ * outside MSP_READ_ONLY_METHODS.
278
342
  */
279
343
  export function requestMuseUsage(opts = {}) {
280
344
  const command = opts.command ?? resolveMuseBin();
@@ -309,7 +373,7 @@ export function requestMuseUsage(opts = {}) {
309
373
  finish({ payload: null, updates, failure: { kind } });
310
374
  };
311
375
  const onLine = (line) => {
312
- const msg = parseRpcLine(line);
376
+ const msg = parseMuseRpcLine(line);
313
377
  if (!msg)
314
378
  return; // Tolerate non-JSON chatter; malformed verdict comes from the payload.
315
379
  if (typeof msg.method === "string" && msg.id === undefined) {
@@ -476,7 +540,226 @@ function failureToUnavailable(nowMs, kind) {
476
540
  }
477
541
  /** Evidence refs are static — the failure kind only, never tokens or payloads. */
478
542
  export function museEvidenceRef(kind) {
479
- return `muse-serve:${kind === "read" ? "usage/read" : kind}`;
543
+ if (kind === "read")
544
+ return "muse-serve:usage/read";
545
+ if (kind === "changed")
546
+ return "muse-serve:usage/changed";
547
+ return `muse-serve:${kind}`;
548
+ }
549
+ /** Window keys that carry real Muse capacity (NOT-266: never the sentinel). */
550
+ export const MUSE_KNOWN_WINDOW_KEYS = ["rolling_all_models", "weekly_all_models"];
551
+ function observedMs(value) {
552
+ if (typeof value !== "string")
553
+ return null;
554
+ const ms = Date.parse(value);
555
+ return Number.isFinite(ms) ? ms : null;
556
+ }
557
+ /**
558
+ * The payload's own arrival stamp (`usage.observedAtMs`), mirroring
559
+ * `museUsageToReadings` — per-window diagnostics share it so a
560
+ * present-but-malformed sibling never outranks a valid row by wall clock.
561
+ */
562
+ export function musePayloadObservedAt(payload, fallback) {
563
+ if (payload && typeof payload === "object") {
564
+ const result = payload;
565
+ const usage = "usage" in result && result.usage !== null && typeof result.usage === "object"
566
+ ? result.usage
567
+ : result;
568
+ const ms = usage.observedAtMs;
569
+ if (typeof ms === "number" && Number.isFinite(ms)) {
570
+ return new Date(ms).toISOString();
571
+ }
572
+ }
573
+ return fallback;
574
+ }
575
+ /**
576
+ * NOT-270: newest-`observedAtMs`-wins ingest for Muse usage observations.
577
+ * Accepts a `usage/read` result envelope or a bare `usage/changed` params
578
+ * object (the stable schema carries `SubscriptionUsage` directly in the
579
+ * notification). Every window compares its `observedAt` against the stored
580
+ * row for the same key: an older observation never overwrites or deletes a
581
+ * newer one, so a slow finishing read cannot clobber rows a newer
582
+ * notification already wrote, and a fresh-host `missing` (no `usage`)
583
+ * never touches stored rows at all.
584
+ *
585
+ * A fully-observed pair also deletes the `muse_account_usage` failure
586
+ * sentinel; per-window `unparsable` diagnostics apply the same newest-wins
587
+ * rule and never sit beside a newer valid row for the same window.
588
+ */
589
+ export async function ingestMuseUsagePayload(payload, opts = {}) {
590
+ const { recordCapacitySnapshots, listCapacitySnapshots, deleteCapacitySnapshots } = await import("../repository/runtime-capacity.js");
591
+ const nowMs = Date.now();
592
+ const fallback = opts.fallbackObservedAt ?? new Date(nowMs).toISOString();
593
+ const evidenceRef = opts.evidenceRef ?? museEvidenceRef("read");
594
+ const stored = new Map(listCapacitySnapshots(MUSE_RUNTIME).map((w) => [w.windowKey, w]));
595
+ const outcome = { written: [], skippedStale: [] };
596
+ const consider = (input) => {
597
+ const prev = stored.get(input.windowKey);
598
+ const prevMs = prev ? observedMs(prev.observedAt) : null;
599
+ const nextMs = observedMs(input.observedAt);
600
+ if (prevMs !== null && nextMs !== null) {
601
+ if (nextMs < prevMs) {
602
+ // Strictly older observation — never clobbers a newer row.
603
+ outcome.skippedStale.push(input.windowKey);
604
+ return;
605
+ }
606
+ if (nextMs === prevMs && input.source === "unavailable") {
607
+ // Same-stamp diagnostic — never relabels the stored row (a
608
+ // same-stamp `usage/changed` + final `usage/read` pair must not
609
+ // let the diagnostic half win; recovery at a newer stamp still
610
+ // applies through the strictly-newer path).
611
+ outcome.skippedStale.push(input.windowKey);
612
+ return;
613
+ }
614
+ // Same-stamp good observation: the final `usage/read` is
615
+ // authoritative over the earlier `usage/changed` for the same
616
+ // arrival stamp — rewrite idempotently.
617
+ }
618
+ recordCapacitySnapshots(MUSE_RUNTIME, [
619
+ {
620
+ windowKey: input.windowKey,
621
+ providerBucket: input.providerBucket,
622
+ durationMinutes: input.durationMinutes ?? null,
623
+ displayLabel: input.displayLabel,
624
+ usedValue: input.usedValue ?? null,
625
+ usedUnit: input.usedUnit ?? null,
626
+ remainingPercent: input.remainingPercent ?? null,
627
+ resetAt: input.resetAt ?? null,
628
+ observedAt: input.observedAt,
629
+ freshUntil: input.freshUntil ?? null,
630
+ expiresAt: input.expiresAt ?? null,
631
+ source: input.source,
632
+ unavailableReason: input.unavailableReason ?? null,
633
+ evidenceRef,
634
+ criticalRole: input.criticalRole ?? null,
635
+ },
636
+ ]);
637
+ stored.delete(input.windowKey);
638
+ outcome.written.push(input.windowKey);
639
+ };
640
+ const payloadObservedAt = musePayloadObservedAt(payload, fallback);
641
+ let parsed = null;
642
+ try {
643
+ parsed = museUsageToReadings(payload, fallback);
644
+ }
645
+ catch {
646
+ parsed = null;
647
+ }
648
+ if (parsed && parsed.windows.length > 0) {
649
+ const normalized = parsed.windows.map((w) => normalizeAdapterWindow(MUSE_RUNTIME, w, nowMs));
650
+ for (let i = 0; i < parsed.windows.length; i++) {
651
+ const n = normalized[i];
652
+ if (n.unavailableReason !== null || n.remainingPercent === null) {
653
+ consider({
654
+ windowKey: n.windowKey,
655
+ providerBucket: n.providerBucket,
656
+ durationMinutes: n.durationMinutes,
657
+ displayLabel: n.displayLabel,
658
+ observedAt: n.observedAt,
659
+ source: "unavailable",
660
+ unavailableReason: "unparsable",
661
+ criticalRole: n.criticalRole,
662
+ });
663
+ }
664
+ else {
665
+ const raw = parsed.windows[i];
666
+ consider({
667
+ windowKey: raw.windowKey,
668
+ providerBucket: raw.providerBucket,
669
+ durationMinutes: raw.durationMinutes,
670
+ displayLabel: n.displayLabel,
671
+ usedValue: raw.usedValue,
672
+ usedUnit: raw.usedUnit,
673
+ remainingPercent: n.remainingPercent,
674
+ resetAt: n.resetAt,
675
+ observedAt: raw.observedAt ?? fallback,
676
+ freshUntil: n.freshUntil,
677
+ expiresAt: n.expiresAt,
678
+ source: "supported_protocol",
679
+ criticalRole: raw.criticalRole,
680
+ });
681
+ }
682
+ }
683
+ for (const kind of museBadWindowKinds(payload)) {
684
+ const key = kind === "rolling" ? "rolling_all_models" : "weekly_all_models";
685
+ if (outcome.written.includes(key) || outcome.skippedStale.includes(key))
686
+ continue;
687
+ consider({
688
+ windowKey: key,
689
+ providerBucket: "all_models",
690
+ durationMinutes: kind === "weekly" ? MUSE_WEEKLY_DURATION_MINUTES : null,
691
+ displayLabel: kind,
692
+ observedAt: payloadObservedAt,
693
+ source: "unavailable",
694
+ unavailableReason: "unparsable",
695
+ criticalRole: kind === "rolling" ? "five_hour" : "weekly",
696
+ });
697
+ }
698
+ if (outcome.written.some((k) => MUSE_KNOWN_WINDOW_KEYS.includes(k))) {
699
+ deleteCapacitySnapshots(MUSE_RUNTIME, [SENTINEL_WINDOW_KEY]);
700
+ }
701
+ return outcome;
702
+ }
703
+ if (parsed && museBadWindowKinds(payload).length > 0) {
704
+ for (const kind of museBadWindowKinds(payload)) {
705
+ const key = kind === "rolling" ? "rolling_all_models" : "weekly_all_models";
706
+ consider({
707
+ windowKey: key,
708
+ providerBucket: "all_models",
709
+ durationMinutes: kind === "weekly" ? MUSE_WEEKLY_DURATION_MINUTES : null,
710
+ displayLabel: kind,
711
+ observedAt: payloadObservedAt,
712
+ source: "unavailable",
713
+ unavailableReason: "unparsable",
714
+ criticalRole: kind === "rolling" ? "five_hour" : "weekly",
715
+ });
716
+ }
717
+ if (outcome.written.length > 0) {
718
+ deleteCapacitySnapshots(MUSE_RUNTIME, [SENTINEL_WINDOW_KEY]);
719
+ }
720
+ return outcome;
721
+ }
722
+ // No observation in the payload (fresh/unobserved host reads `missing`):
723
+ // never overwrite or delete stored rows.
724
+ return outcome;
725
+ }
726
+ /**
727
+ * NOT-270: record a host-level failure without touching last-good rows. A
728
+ * failure diagnostic (the `muse_account_usage` sentinel) is written only
729
+ * when no per-window row exists — it must never sit beside or relabel
730
+ * valid windows, nor beside per-window `unparsable` diagnostics (those
731
+ * already describe the 5H/1W entry; a sentinel next to them would mix two
732
+ * diagnostics for the same entry). A stored sentinel alone is refreshed in
733
+ * place.
734
+ */
735
+ export async function noteMuseCapacityFailure(reason, nowMs = Date.now()) {
736
+ const { recordCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
737
+ const stored = listCapacitySnapshots(MUSE_RUNTIME);
738
+ if (stored.some((w) => MUSE_KNOWN_WINDOW_KEYS.includes(w.windowKey)))
739
+ return "preserved";
740
+ const normalized = normalizeUnavailableWindow(MUSE_RUNTIME, {
741
+ windowKey: SENTINEL_WINDOW_KEY,
742
+ providerBucket: "account",
743
+ providerLabel: "account_usage",
744
+ reason,
745
+ observedAt: new Date(nowMs).toISOString(),
746
+ }, nowMs);
747
+ recordCapacitySnapshots(MUSE_RUNTIME, [
748
+ {
749
+ windowKey: normalized.windowKey,
750
+ providerBucket: normalized.providerBucket,
751
+ durationMinutes: normalized.durationMinutes,
752
+ displayLabel: normalized.displayLabel,
753
+ observedAt: normalized.observedAt,
754
+ freshUntil: normalized.freshUntil,
755
+ expiresAt: normalized.expiresAt,
756
+ source: "unavailable",
757
+ unavailableReason: normalized.unavailableReason,
758
+ evidenceRef: normalized.evidenceRef,
759
+ criticalRole: normalized.criticalRole,
760
+ },
761
+ ]);
762
+ return "sentinel";
480
763
  }
481
764
  /**
482
765
  * One bounded, non-billable read of Muse account usage. Never throws:
@@ -572,34 +855,22 @@ export function createMuseCapacityAdapter(opts = {}) {
572
855
  };
573
856
  }
574
857
  /**
575
- * Bounded refresh: run the live adapter and ingest into normalized snapshots.
576
- * Reuses the shared ingest path; failures persist as N/A windows, never as
577
- * health rows. Imported lazily to keep the adapter module free of DB binds.
578
- *
579
- * A successful read (real windows or per-window unparsable rows) deletes
580
- * the failure sentinel (`muse_account_usage`): snapshot writes are
581
- * per-window upserts that never delete siblings, so without this a stale
582
- * N/A sentinel would linger next to the recovered rows indefinitely.
858
+ * NOT-270: the one-shot production poll (`refreshMuseCapacityFromServe` /
859
+ * `maybeRefreshMuseCapacityFromServe`) is retired. NOT-269 proved a fresh
860
+ * `muse serve` host is structurally unobserved: spawn → `usage/read` →
861
+ * exit can only ever read `missing`, so polling it could never refresh
862
+ * capacity. Production refreshes go through the server-owned long-lived
863
+ * host (`./muse-host.js`), which holds one connection open, ingests
864
+ * `usage/changed` as received, and runs throttled `usage/read` on that
865
+ * same observed host.
583
866
  */
584
- export async function refreshMuseCapacityFromServe(opts = {}) {
585
- const { ingestAdapterResult, getRuntimeCapacitySnapshot } = await import("./service.js");
586
- const { deleteCapacitySnapshots } = await import("../repository/runtime-capacity.js");
587
- const nowMs = opts.nowMs ?? Date.now();
588
- const result = await readMuseCapacity({ ...opts, nowMs });
589
- await ingestAdapterResult(result);
590
- const hasRealRows = result.windows.length > 0 ||
591
- result.unavailable.some((u) => u.windowKey !== SENTINEL_WINDOW_KEY);
592
- if (hasRealRows) {
593
- deleteCapacitySnapshots(MUSE_RUNTIME, [SENTINEL_WINDOW_KEY]);
594
- }
595
- return getRuntimeCapacitySnapshot(nowMs);
596
- }
597
867
  /** Default minimum gap between production Muse refreshes (5 minutes). */
598
868
  export const MUSE_REFRESH_THROTTLE_MS_DEFAULT = 5 * 60 * 1000;
599
869
  /**
600
870
  * Throttle bound for production refreshes. Override with
601
871
  * `AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS` (milliseconds); `off` disables
602
872
  * refresh entirely. Non-positive or unparsable values fall back to the default.
873
+ * Shared with the owned host (`./muse-host.js`).
603
874
  */
604
875
  export function museRefreshThrottleMs() {
605
876
  const raw = process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS;
@@ -612,30 +883,6 @@ export function museRefreshThrottleMs() {
612
883
  return n;
613
884
  return MUSE_REFRESH_THROTTLE_MS_DEFAULT;
614
885
  }
615
- let lastMuseRefreshMs = 0;
616
- /** Test helper — reset the refresh throttle so the next refresh runs. */
617
- export function resetMuseCapacityRefreshState() {
618
- lastMuseRefreshMs = 0;
619
- }
620
- /**
621
- * Production trigger for the Muse adapter: throttled, bounded, best-effort.
622
- * Returns the refreshed snapshot, or null when throttled/disabled. Never
623
- * throws — a failed refresh persists as N/A through the shared ingest path,
624
- * and any unexpected error resolves to null so callers (routes) can still
625
- * serve the last-known snapshot.
626
- */
627
- export async function maybeRefreshMuseCapacityFromServe(opts = {}, nowMs = Date.now()) {
628
- const throttle = museRefreshThrottleMs();
629
- if (!Number.isFinite(throttle) || nowMs - lastMuseRefreshMs < throttle)
630
- return null;
631
- lastMuseRefreshMs = nowMs;
632
- try {
633
- return await refreshMuseCapacityFromServe({ ...opts, nowMs });
634
- }
635
- catch {
636
- return null;
637
- }
638
- }
639
886
  /** Normalize one unavailable Muse reading for direct persistence (tests/tools). */
640
887
  export function normalizeMuseUnavailable(reason, nowMs = Date.now()) {
641
888
  return normalizeUnavailableWindow(MUSE_RUNTIME, {
@@ -16,10 +16,10 @@ import fs from "node:fs";
16
16
  import os from "node:os";
17
17
  import path from "node:path";
18
18
  process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-muse-cap-"));
19
- import { assertMuseReadOnlyMethod, createMuseCapacityAdapter, MSP_FORBIDDEN_MODEL_METHODS, MUSE_CLIENT_INFO, MUSE_SERVE_ARGV, museRefreshThrottleMs, museUsageToReadings, maybeRefreshMuseCapacityFromServe, normalizeMuseResetsAt, normalizeMuseUnavailable, readMuseCapacity, refreshMuseCapacityFromServe, requestMuseUsage, resetMuseCapacityRefreshState, } from "./muse.js";
19
+ import { assertMuseReadOnlyMethod, createMuseCapacityAdapter, MSP_FORBIDDEN_MODEL_METHODS, MUSE_CLIENT_INFO, MUSE_SERVE_ARGV, museUsageToReadings, normalizeMuseResetsAt, normalizeMuseUnavailable, readMuseCapacity, requestMuseUsage, } from "./muse.js";
20
20
  import { normalizeAdapterWindow, normalizeUnavailableWindow } from "./adapter.js";
21
21
  const { migrate } = await import("../db/index.js");
22
- const { clearAllCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
22
+ const { clearAllCapacitySnapshots } = await import("../repository/runtime-capacity.js");
23
23
  const { clearAllRuntimeAvailability, runtimeAvailability, } = await import("../repository/runtime-availability.js");
24
24
  const { refreshCapacityFromAdapters } = await import("./service.js");
25
25
  const FAKE = new URL("./fixtures/fake-muse-serve.mjs", import.meta.url).pathname;
@@ -44,8 +44,8 @@ function fakeOpts(mode, extra = {}) {
44
44
  },
45
45
  };
46
46
  }
47
- test("serve argv is the shipping contract: no --protocol flag", () => {
48
- assert.deepEqual([...MUSE_SERVE_ARGV], ["serve"]);
47
+ test("serve argv is the shipping contract: no --protocol flag, restricted network", () => {
48
+ assert.deepEqual([...MUSE_SERVE_ARGV], ["serve", "--sandbox-network", "restricted"]);
49
49
  assert.ok(!MUSE_SERVE_ARGV.includes("--protocol"), "muse serve takes no --protocol flag");
50
50
  });
51
51
  test("handshake identity matches the stable clientInfo shape", () => {
@@ -305,53 +305,10 @@ test("no credential short-circuits to missing without spawning", async () => {
305
305
  assert.equal(result.windows.length, 0);
306
306
  assert.equal(result.unavailable[0].reason, "missing");
307
307
  });
308
- test("a successful refresh clears the failure sentinel", async () => {
309
- clearAllCapacitySnapshots();
310
- await refreshMuseCapacityFromServe(fakeOpts("crash").opts);
311
- assert.deepEqual(listCapacitySnapshots("muse_code").map((w) => w.windowKey), ["muse_account_usage"]);
312
- const snap = await refreshMuseCapacityFromServe(fakeOpts("full").opts);
313
- const keys = listCapacitySnapshots("muse_code")
314
- .map((w) => w.windowKey)
315
- .sort();
316
- assert.deepEqual(keys, ["rolling_all_models", "weekly_all_models"]);
317
- const muse = snap.runtimes.find((r) => r.runtime === "muse_code");
318
- assert.ok(muse);
319
- assert.equal(muse.windows.length, 2);
320
- assert.equal(muse.unavailableReason, null);
321
- });
322
- test("a read with every window unparsable clears the stale sentinel", async () => {
323
- clearAllCapacitySnapshots();
324
- await refreshMuseCapacityFromServe(fakeOpts("crash").opts);
325
- assert.deepEqual(listCapacitySnapshots("muse_code").map((w) => w.windowKey), ["muse_account_usage"]);
326
- const result = await readMuseCapacity(fakeOpts("all-bad-windows").opts);
327
- assert.equal(result.windows.length, 0);
328
- assert.ok(result.unavailable.length > 0);
329
- assert.ok(result.unavailable.every((u) => u.windowKey !== "muse_account_usage"));
330
- await refreshMuseCapacityFromServe(fakeOpts("all-bad-windows").opts);
331
- const keys = listCapacitySnapshots("muse_code").map((w) => w.windowKey);
332
- assert.ok(!keys.includes("muse_account_usage"), `stale sentinel cleared, got ${keys}`);
333
- assert.ok(keys.length > 0, "per-window unparsable rows persist");
334
- });
335
- test("production refresh is throttled and can be disabled", async () => {
336
- clearAllCapacitySnapshots();
337
- resetMuseCapacityRefreshState();
338
- const { opts } = fakeOpts("full");
339
- try {
340
- assert.equal(museRefreshThrottleMs(), 5 * 60 * 1000);
341
- const first = await maybeRefreshMuseCapacityFromServe(opts);
342
- assert.ok(first, "first refresh runs");
343
- assert.equal(await maybeRefreshMuseCapacityFromServe(opts), null, "second refresh throttled");
344
- process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS = "off";
345
- resetMuseCapacityRefreshState();
346
- assert.equal(await maybeRefreshMuseCapacityFromServe(opts), null, "refresh disabled");
347
- process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS = "garbage";
348
- assert.equal(museRefreshThrottleMs(), 5 * 60 * 1000);
349
- }
350
- finally {
351
- delete process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS;
352
- resetMuseCapacityRefreshState();
353
- }
354
- });
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.
355
312
  test("no token, tier, or raw account payload reaches the adapter output", async () => {
356
313
  const { opts } = fakeOpts("full");
357
314
  const result = await readMuseCapacity(opts);