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.
- package/bundle/server/dist/capacity/claude-events.js +27 -10
- package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
- package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +203 -16
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +186 -2
- package/bundle/server/dist/capacity/cursor-individual.js +69 -2
- package/bundle/server/dist/capacity/cursor-individual.test.js +268 -2
- package/bundle/server/dist/capacity/muse-host.js +748 -0
- package/bundle/server/dist/capacity/muse-host.test.js +480 -0
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
- package/bundle/server/dist/capacity/muse.js +307 -60
- package/bundle/server/dist/capacity/muse.test.js +8 -51
- package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
- package/bundle/server/dist/index.js +15 -2
- package/bundle/server/dist/routes/index.js +26 -9
- package/bundle/server/dist/routes/runtime-capacity.test.js +12 -6
- package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
- package/bundle/server/dist/runners/muse-serve-session.js +313 -0
- package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
- package/bundle/server/static-ui/assets/{index-DUYzboWh.css → index-CYqRh_-S.css} +1 -1
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
- package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +57 -0
- package/dist/doctor.js +206 -1
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +246 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-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: `
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
|
14
|
-
// prompt, or run model work. It never touches the Keychain
|
|
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
|
|
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
|
-
|
|
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.
|
|
277
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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);
|