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
@@ -3,16 +3,21 @@
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
- // 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
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
+ // `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. 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.
16
21
  //
17
22
  // Stable `usage/read` shape:
18
23
  // result: {
@@ -39,12 +44,33 @@
39
44
  // adapter passes no credentials (the serve subprocess uses its ambient
40
45
  // session) and evidence refs are static strings.
41
46
  import { spawn } from "node:child_process";
47
+ import { randomBytes } from "node:crypto";
42
48
  import fs from "node:fs";
43
49
  import { MUSE_CLI_ENV, resolveMuseAuthFile, resolveMuseBin } from "../cli-env.js";
44
50
  import { normalizeAdapterWindow, normalizeUnavailableWindow, } from "./adapter.js";
45
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;
46
57
  /** Methods this client may ever send. Anything else throws before write. */
47
58
  export const MSP_READ_ONLY_METHODS = ["initialize", "initialized", "usage/read"];
59
+ /**
60
+ * NOT-270: execution methods for REAL Dealer work through the server-owned
61
+ * serve host (the NOT-269 proven lifecycle: only a host that observes the
62
+ * account's provider traffic can answer `usage/read`). This is a separate
63
+ * allowlist from the read-only one above — a capacity read still never sends
64
+ * these. `session/resume` stays forbidden (state-changing, carries no usage
65
+ * per NOT-269 leg 2); `turn/steer` and approval writes are never sent by the
66
+ * headless runner, so they are refused here too.
67
+ */
68
+ export const MSP_EXEC_METHODS = [
69
+ "session/start",
70
+ "turn/start",
71
+ "turn/cancel",
72
+ "session/read",
73
+ ];
48
74
  /**
49
75
  * Client identity sent in the `initialize` handshake. The stable schema
50
76
  * requires `clientInfo.name` to match `^[a-z0-9_]+$` plus a version string.
@@ -68,6 +94,27 @@ export function assertMuseReadOnlyMethod(method) {
68
94
  throw new Error(`muse-serve: refusing non-read method ${method}`);
69
95
  }
70
96
  }
97
+ export function assertMuseExecMethod(method) {
98
+ if (!MSP_EXEC_METHODS.includes(method)) {
99
+ throw new Error(`muse-serve: refusing non-execution method ${method}`);
100
+ }
101
+ }
102
+ /**
103
+ * UUIDv7 for MSP `commandId` idempotency handles (session/start, turn/start,
104
+ * turn/cancel require UUIDv7). Time-ordered; randomness from crypto.
105
+ */
106
+ export function museUuidv7(nowMs = Date.now()) {
107
+ const b = Buffer.alloc(16);
108
+ const ts = BigInt(nowMs) & ((1n << 48n) - 1n);
109
+ for (let i = 0; i < 6; i++)
110
+ b[i] = Number((ts >> BigInt(40 - 8 * i)) & 0xffn);
111
+ randomBytes(10).copy(b, 6);
112
+ b[6] = 0x70 | (b[6] & 0x0f);
113
+ b[8] = 0x80 | (b[8] & 0x3f);
114
+ const hex = b.toString("hex");
115
+ return (`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-` +
116
+ `${hex.slice(16, 20)}-${hex.slice(20)}`);
117
+ }
71
118
  function readOnlyRequest(id, method, params) {
72
119
  assertMuseReadOnlyMethod(method);
73
120
  return `${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`;
@@ -79,9 +126,17 @@ function readOnlyNotification(method, params) {
79
126
  /**
80
127
  * Stable Session Protocol argv. Fixed: capacity reads never `exec`, and the
81
128
  * host takes no `--protocol` flag — `muse serve` with that flag exits 2
82
- * against the shipped binary, so this stays exactly `["serve"]`.
129
+ * against the shipped binary.
130
+ *
131
+ * NOT-270: the owned host is also the execution host for real Dealer Muse
132
+ * turns (the NOT-269 precondition — only a host that observes provider
133
+ * traffic can answer `usage/read`). Sandbox posture is fixed at host
134
+ * startup and applies to every session on the host, so the host carries the
135
+ * same `--sandbox-network restricted` every `muse exec` developer invocation
136
+ * already passes (see runners/muse-code-args.ts — a fixed constant, no
137
+ * per-session variation). Approval mode stays per-session on the wire.
83
138
  */
84
- export const MUSE_SERVE_ARGV = ["serve"];
139
+ export const MUSE_SERVE_ARGV = ["serve", "--sandbox-network", "restricted"];
85
140
  /** JSON-RPC request ids for the one-shot read (init handshake, then usage). */
86
141
  export const MSP_INITIALIZE_ID = "muse-capacity-init";
87
142
  export const MSP_USAGE_READ_ID = "muse-capacity-1";
@@ -140,6 +195,8 @@ function windowReadingFromEntry(kind, entry, observedAt) {
140
195
  usedPercent,
141
196
  resetAt,
142
197
  observedAt,
198
+ staleAfterMs: MUSE_CAPACITY_STALE_AFTER_MS,
199
+ expiresAfterMs: MUSE_CAPACITY_EXPIRES_AFTER_MS,
143
200
  source: "supported_protocol",
144
201
  // Muse reports exactly this one rolling/weekly pair — it is always the
145
202
  // account-wide critical window, never a model-specific extra.
@@ -237,7 +294,8 @@ export function museCapacityTimeoutMs() {
237
294
  return 15_000;
238
295
  }
239
296
  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) {
297
+ /** Parse one stdio JSON-RPC line; null for blank/non-JSON chatter. Shared with the owned host. */
298
+ export function parseMuseRpcLine(line) {
241
299
  const t = line.trim();
242
300
  if (!t)
243
301
  return null;
@@ -257,8 +315,19 @@ function errorLooksAuth(error) {
257
315
  const e = error;
258
316
  return AUTH_RE.test(String(e.message ?? "")) || e.code === 401 || e.code === 403;
259
317
  }
318
+ /** Classify an MSP RPC error for the owned host: auth, missing method, or other. */
319
+ export function museClassifyRpcError(error) {
320
+ if (errorLooksAuth(error))
321
+ return "auth";
322
+ if (error !== null &&
323
+ typeof error === "object" &&
324
+ error.code === -32601) {
325
+ return "unsupported";
326
+ }
327
+ return "other";
328
+ }
260
329
  /** The only credential signals the repo knows (cf. agent-health): env key or login file. */
261
- function hasMuseCredential(env, authFilePath) {
330
+ export function hasMuseCredential(env, authFilePath) {
262
331
  const key = env.META_API_KEY;
263
332
  if (typeof key === "string" && key.length > 0)
264
333
  return true;
@@ -273,8 +342,11 @@ function hasMuseCredential(env, authFilePath) {
273
342
  * Spawn `muse serve`, run the stable handshake (`initialize` with a
274
343
  * `clientInfo` identity, then the `initialized` notification), send one
275
344
  * `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.
345
+ * connection down. Low-level primitive (unit-tested); production uses the
346
+ * owned long-lived host (`./muse-host.js`) — a fresh connection is
347
+ * unobserved by construction (NOT-269). Never throws for provider-side
348
+ * failures — those come back as `failure.kind`. Never sends anything
349
+ * outside MSP_READ_ONLY_METHODS.
278
350
  */
279
351
  export function requestMuseUsage(opts = {}) {
280
352
  const command = opts.command ?? resolveMuseBin();
@@ -309,7 +381,7 @@ export function requestMuseUsage(opts = {}) {
309
381
  finish({ payload: null, updates, failure: { kind } });
310
382
  };
311
383
  const onLine = (line) => {
312
- const msg = parseRpcLine(line);
384
+ const msg = parseMuseRpcLine(line);
313
385
  if (!msg)
314
386
  return; // Tolerate non-JSON chatter; malformed verdict comes from the payload.
315
387
  if (typeof msg.method === "string" && msg.id === undefined) {
@@ -476,7 +548,226 @@ function failureToUnavailable(nowMs, kind) {
476
548
  }
477
549
  /** Evidence refs are static — the failure kind only, never tokens or payloads. */
478
550
  export function museEvidenceRef(kind) {
479
- return `muse-serve:${kind === "read" ? "usage/read" : kind}`;
551
+ if (kind === "read")
552
+ return "muse-serve:usage/read";
553
+ if (kind === "changed")
554
+ return "muse-serve:usage/changed";
555
+ return `muse-serve:${kind}`;
556
+ }
557
+ /** Window keys that carry real Muse capacity (NOT-266: never the sentinel). */
558
+ export const MUSE_KNOWN_WINDOW_KEYS = ["rolling_all_models", "weekly_all_models"];
559
+ function observedMs(value) {
560
+ if (typeof value !== "string")
561
+ return null;
562
+ const ms = Date.parse(value);
563
+ return Number.isFinite(ms) ? ms : null;
564
+ }
565
+ /**
566
+ * The payload's own arrival stamp (`usage.observedAtMs`), mirroring
567
+ * `museUsageToReadings` — per-window diagnostics share it so a
568
+ * present-but-malformed sibling never outranks a valid row by wall clock.
569
+ */
570
+ export function musePayloadObservedAt(payload, fallback) {
571
+ if (payload && typeof payload === "object") {
572
+ const result = payload;
573
+ const usage = "usage" in result && result.usage !== null && typeof result.usage === "object"
574
+ ? result.usage
575
+ : result;
576
+ const ms = usage.observedAtMs;
577
+ if (typeof ms === "number" && Number.isFinite(ms)) {
578
+ return new Date(ms).toISOString();
579
+ }
580
+ }
581
+ return fallback;
582
+ }
583
+ /**
584
+ * NOT-270: newest-`observedAtMs`-wins ingest for Muse usage observations.
585
+ * Accepts a `usage/read` result envelope or a bare `usage/changed` params
586
+ * object (the stable schema carries `SubscriptionUsage` directly in the
587
+ * notification). Every window compares its `observedAt` against the stored
588
+ * row for the same key: an older observation never overwrites or deletes a
589
+ * newer one, so a slow finishing read cannot clobber rows a newer
590
+ * notification already wrote, and a fresh-host `missing` (no `usage`)
591
+ * never touches stored rows at all.
592
+ *
593
+ * A fully-observed pair also deletes the `muse_account_usage` failure
594
+ * sentinel; per-window `unparsable` diagnostics apply the same newest-wins
595
+ * rule and never sit beside a newer valid row for the same window.
596
+ */
597
+ export async function ingestMuseUsagePayload(payload, opts = {}) {
598
+ const { recordCapacitySnapshots, listCapacitySnapshots, deleteCapacitySnapshots } = await import("../repository/runtime-capacity.js");
599
+ const nowMs = Date.now();
600
+ const fallback = opts.fallbackObservedAt ?? new Date(nowMs).toISOString();
601
+ const evidenceRef = opts.evidenceRef ?? museEvidenceRef("read");
602
+ const stored = new Map(listCapacitySnapshots(MUSE_RUNTIME).map((w) => [w.windowKey, w]));
603
+ const outcome = { written: [], skippedStale: [] };
604
+ const consider = (input) => {
605
+ const prev = stored.get(input.windowKey);
606
+ const prevMs = prev ? observedMs(prev.observedAt) : null;
607
+ const nextMs = observedMs(input.observedAt);
608
+ if (prevMs !== null && nextMs !== null) {
609
+ if (nextMs < prevMs) {
610
+ // Strictly older observation — never clobbers a newer row.
611
+ outcome.skippedStale.push(input.windowKey);
612
+ return;
613
+ }
614
+ if (nextMs === prevMs && input.source === "unavailable") {
615
+ // Same-stamp diagnostic — never relabels the stored row (a
616
+ // same-stamp `usage/changed` + final `usage/read` pair must not
617
+ // let the diagnostic half win; recovery at a newer stamp still
618
+ // applies through the strictly-newer path).
619
+ outcome.skippedStale.push(input.windowKey);
620
+ return;
621
+ }
622
+ // Same-stamp good observation: the final `usage/read` is
623
+ // authoritative over the earlier `usage/changed` for the same
624
+ // arrival stamp — rewrite idempotently.
625
+ }
626
+ recordCapacitySnapshots(MUSE_RUNTIME, [
627
+ {
628
+ windowKey: input.windowKey,
629
+ providerBucket: input.providerBucket,
630
+ durationMinutes: input.durationMinutes ?? null,
631
+ displayLabel: input.displayLabel,
632
+ usedValue: input.usedValue ?? null,
633
+ usedUnit: input.usedUnit ?? null,
634
+ remainingPercent: input.remainingPercent ?? null,
635
+ resetAt: input.resetAt ?? null,
636
+ observedAt: input.observedAt,
637
+ freshUntil: input.freshUntil ?? null,
638
+ expiresAt: input.expiresAt ?? null,
639
+ source: input.source,
640
+ unavailableReason: input.unavailableReason ?? null,
641
+ evidenceRef,
642
+ criticalRole: input.criticalRole ?? null,
643
+ },
644
+ ]);
645
+ stored.delete(input.windowKey);
646
+ outcome.written.push(input.windowKey);
647
+ };
648
+ const payloadObservedAt = musePayloadObservedAt(payload, fallback);
649
+ let parsed = null;
650
+ try {
651
+ parsed = museUsageToReadings(payload, fallback);
652
+ }
653
+ catch {
654
+ parsed = null;
655
+ }
656
+ if (parsed && parsed.windows.length > 0) {
657
+ const normalized = parsed.windows.map((w) => normalizeAdapterWindow(MUSE_RUNTIME, w, nowMs));
658
+ for (let i = 0; i < parsed.windows.length; i++) {
659
+ const n = normalized[i];
660
+ if (n.unavailableReason !== null || n.remainingPercent === null) {
661
+ consider({
662
+ windowKey: n.windowKey,
663
+ providerBucket: n.providerBucket,
664
+ durationMinutes: n.durationMinutes,
665
+ displayLabel: n.displayLabel,
666
+ observedAt: n.observedAt,
667
+ source: "unavailable",
668
+ unavailableReason: "unparsable",
669
+ criticalRole: n.criticalRole,
670
+ });
671
+ }
672
+ else {
673
+ const raw = parsed.windows[i];
674
+ consider({
675
+ windowKey: raw.windowKey,
676
+ providerBucket: raw.providerBucket,
677
+ durationMinutes: raw.durationMinutes,
678
+ displayLabel: n.displayLabel,
679
+ usedValue: raw.usedValue,
680
+ usedUnit: raw.usedUnit,
681
+ remainingPercent: n.remainingPercent,
682
+ resetAt: n.resetAt,
683
+ observedAt: raw.observedAt ?? fallback,
684
+ freshUntil: n.freshUntil,
685
+ expiresAt: n.expiresAt,
686
+ source: "supported_protocol",
687
+ criticalRole: raw.criticalRole,
688
+ });
689
+ }
690
+ }
691
+ for (const kind of museBadWindowKinds(payload)) {
692
+ const key = kind === "rolling" ? "rolling_all_models" : "weekly_all_models";
693
+ if (outcome.written.includes(key) || outcome.skippedStale.includes(key))
694
+ continue;
695
+ consider({
696
+ windowKey: key,
697
+ providerBucket: "all_models",
698
+ durationMinutes: kind === "weekly" ? MUSE_WEEKLY_DURATION_MINUTES : null,
699
+ displayLabel: kind,
700
+ observedAt: payloadObservedAt,
701
+ source: "unavailable",
702
+ unavailableReason: "unparsable",
703
+ criticalRole: kind === "rolling" ? "five_hour" : "weekly",
704
+ });
705
+ }
706
+ if (outcome.written.some((k) => MUSE_KNOWN_WINDOW_KEYS.includes(k))) {
707
+ deleteCapacitySnapshots(MUSE_RUNTIME, [SENTINEL_WINDOW_KEY]);
708
+ }
709
+ return outcome;
710
+ }
711
+ if (parsed && museBadWindowKinds(payload).length > 0) {
712
+ for (const kind of museBadWindowKinds(payload)) {
713
+ const key = kind === "rolling" ? "rolling_all_models" : "weekly_all_models";
714
+ consider({
715
+ windowKey: key,
716
+ providerBucket: "all_models",
717
+ durationMinutes: kind === "weekly" ? MUSE_WEEKLY_DURATION_MINUTES : null,
718
+ displayLabel: kind,
719
+ observedAt: payloadObservedAt,
720
+ source: "unavailable",
721
+ unavailableReason: "unparsable",
722
+ criticalRole: kind === "rolling" ? "five_hour" : "weekly",
723
+ });
724
+ }
725
+ if (outcome.written.length > 0) {
726
+ deleteCapacitySnapshots(MUSE_RUNTIME, [SENTINEL_WINDOW_KEY]);
727
+ }
728
+ return outcome;
729
+ }
730
+ // No observation in the payload (fresh/unobserved host reads `missing`):
731
+ // never overwrite or delete stored rows.
732
+ return outcome;
733
+ }
734
+ /**
735
+ * NOT-270: record a host-level failure without touching last-good rows. A
736
+ * failure diagnostic (the `muse_account_usage` sentinel) is written only
737
+ * when no per-window row exists — it must never sit beside or relabel
738
+ * valid windows, nor beside per-window `unparsable` diagnostics (those
739
+ * already describe the 5H/1W entry; a sentinel next to them would mix two
740
+ * diagnostics for the same entry). A stored sentinel alone is refreshed in
741
+ * place.
742
+ */
743
+ export async function noteMuseCapacityFailure(reason, nowMs = Date.now()) {
744
+ const { recordCapacitySnapshots, listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
745
+ const stored = listCapacitySnapshots(MUSE_RUNTIME);
746
+ if (stored.some((w) => MUSE_KNOWN_WINDOW_KEYS.includes(w.windowKey)))
747
+ return "preserved";
748
+ const normalized = normalizeUnavailableWindow(MUSE_RUNTIME, {
749
+ windowKey: SENTINEL_WINDOW_KEY,
750
+ providerBucket: "account",
751
+ providerLabel: "account_usage",
752
+ reason,
753
+ observedAt: new Date(nowMs).toISOString(),
754
+ }, nowMs);
755
+ recordCapacitySnapshots(MUSE_RUNTIME, [
756
+ {
757
+ windowKey: normalized.windowKey,
758
+ providerBucket: normalized.providerBucket,
759
+ durationMinutes: normalized.durationMinutes,
760
+ displayLabel: normalized.displayLabel,
761
+ observedAt: normalized.observedAt,
762
+ freshUntil: normalized.freshUntil,
763
+ expiresAt: normalized.expiresAt,
764
+ source: "unavailable",
765
+ unavailableReason: normalized.unavailableReason,
766
+ evidenceRef: normalized.evidenceRef,
767
+ criticalRole: normalized.criticalRole,
768
+ },
769
+ ]);
770
+ return "sentinel";
480
771
  }
481
772
  /**
482
773
  * One bounded, non-billable read of Muse account usage. Never throws:
@@ -572,34 +863,24 @@ export function createMuseCapacityAdapter(opts = {}) {
572
863
  };
573
864
  }
574
865
  /**
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.
866
+ * NOT-270: the one-shot production poll (`refreshMuseCapacityFromServe` /
867
+ * `maybeRefreshMuseCapacityFromServe`) is retired. NOT-269 proved a fresh
868
+ * `muse serve` host is structurally unobserved: spawn → `usage/read` →
869
+ * exit can only ever read `missing`, so polling it could never refresh
870
+ * capacity. Production refreshes go through the server-owned long-lived
871
+ * host (`./muse-host.js`), which holds one connection open, ingests
872
+ * `usage/changed` as received, and runs throttled `usage/read` on that
873
+ * same observed host.
583
874
  */
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
875
  /** Default minimum gap between production Muse refreshes (5 minutes). */
598
876
  export const MUSE_REFRESH_THROTTLE_MS_DEFAULT = 5 * 60 * 1000;
599
877
  /**
600
878
  * Throttle bound for production refreshes. Override with
601
- * `AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS` (milliseconds); `off` disables
602
- * refresh entirely. Non-positive or unparsable values fall back to the default.
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.
883
+ * Shared with the owned host (`./muse-host.js`).
603
884
  */
604
885
  export function museRefreshThrottleMs() {
605
886
  const raw = process.env.AGENT_DEALER_MUSE_CAPACITY_REFRESH_MS;
@@ -612,30 +893,6 @@ export function museRefreshThrottleMs() {
612
893
  return n;
613
894
  return MUSE_REFRESH_THROTTLE_MS_DEFAULT;
614
895
  }
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
896
  /** Normalize one unavailable Muse reading for direct persistence (tests/tools). */
640
897
  export function normalizeMuseUnavailable(reason, nowMs = Date.now()) {
641
898
  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);