Fleet: show subscription move reports and their evidence (#4)

The Fleet page shows subscription move reports: for each daemon, the managing agent's dated reports from GET /api/provider/moves, with from and to, the reason and policy version, each seat's account evidence and tool turn with their times, what's missing, peers and follow-up links. A daemon without the route reads as unavailable, not as no reports; the recent window and unreadable rows are named. Configured and observed seat accounts stay separate, so a move is never shown as current use. The reader keeps sign-in, last exhaustion and reset expectations. The starter agent's manage-subscriptions practice applies only when enabled and names the exact Codex home and host. Slice 18's Fleet side; not part of the curated default install.
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-10-23 07:16:56 -07:00
1 parent fdaefb174e
commit ef7dceab42
11 files changed
+178 -26

No files matched your search

+26 -3
View File
@@ -48,7 +48,7 @@
<p class="legend">— means not reported here yet. Old readings show their time. An account shared by machines is counted once.</p>
<p class="meta" id="meta">Reading…</p>
<div id="content"></div>
<footer>For history and deeper readings, add the Observatory and open it at <a href="/surfaces/observatory.html">/surfaces/observatory.html</a>. Read only: nothing here changes any machine.</footer>
<footer>For seat history and deeper readings, add the Observatory and open it at <a href="/surfaces/observatory.html">/surfaces/observatory.html</a>. Read only: nothing here changes any machine.</footer>
</main>
<script type="module">
@@ -208,7 +208,7 @@
const a = v.accounts;
if (!a.connected && !a.noReading.length) return `<section id="accounts"><h2>Accounts</h2><p class="context">Usage is not connected.</p></section>`;
const rows = a.accounts.map((x) => `<div class="fold"><div class="row"><span class="name">${esc(x.provider)} ${esc(x.account ?? DASH)}</span><span class="${x.state}">${x.state === 'out' ? 'out' : x.state === 'low' ? 'low' : 'has room'}</span>${x.reset ? `<span class="sub">${resetText(x.reset, v.now)}</span>` : ''}</div>
<div class="sub">${x.observations.map((o) => `${esc(o.host)}: ${o.windows.map((w) => `${esc(w.window ?? DASH)} ${w.usedPercent === null || w.usedPercent === undefined ? DASH : `${esc(w.usedPercent)}% used`}`).join(', ') || DASH} (${esc(ago(o.observedAt ?? o.readAt, v.now) ?? DASH)})`).join(' · ')}</div></div>`).join('');
<div class="sub">${x.observations.map((o) => `${esc(o.host)}: ${o.windows.map((w) => `${esc(w.window ?? DASH)} ${w.usedPercent === null || w.usedPercent === undefined ? DASH : `${esc(w.usedPercent)}% used`}${w.expectation ? `; expected full since ${esc(w.expectation.expectedFullSince)} (not measured; ${esc(w.expectation.note)})` : ''}`).join(', ') || DASH} (${esc(ago(o.observedAt ?? o.readAt, v.now) ?? DASH)})${o.signIn ? `; Sign-in: ${esc(o.signIn.state)} (${esc(o.signIn.evidence)}; ${esc(o.signIn.at ?? 'time not reported')})` : ''}${o.lastObservedExhausted ? `; Last observed exhausted: ${esc(o.lastObservedExhausted.at)} (${esc(o.lastObservedExhausted.window ?? DASH)}; ${esc(o.lastObservedExhausted.evidence)})` : ''}`).join(' · ')}</div></div>`).join('');
const fold = a.noReading.length ? `<p class="context">No reading yet: ${a.noReading.map(esc).join(', ')}.</p>` : '';
return `<section id="accounts"><h2>Accounts</h2>${a.connected ? '' : '<p class="context">Usage is not connected; these accounts are configured.</p>'}<div class="list">${rows}</div>${fold}${renderSeatAccounts(v)}</section>`;
}
@@ -246,6 +246,29 @@
return `<details><summary class="sub">Seats and their accounts</summary><table><thead><tr><th>Seat</th>${cols.map(([, t]) => `<th>${t}</th>`).join('')}</tr></thead><tbody>${rows.map((r) => `<tr><td>${esc(r.seat)}<div class="sub">${esc(r.machine)}</div></td>${cols.map(([k]) => `<td>${r[k] ? esc(r[k]) : dash()}</td>`).join('')}</tr>`).join('')}</tbody></table></details>`;
}
export function renderMoves(fleet) {
const hosts = fleet.subscriptions?.moves?.hosts ?? [];
const id = (host, qid) => `move-${encodeURIComponent(`${host}/${qid}`)}`;
const blocks = hosts.map((h) => {
if (!h.data) return `<p class="context">${esc(h.host)}: Move reports unavailable (${esc(h.status)}${h.detail ? `; ${esc(h.detail)}` : ''}).</p>`;
const { moves, unreadable, truncated } = h.data;
const link = (qid) => moves.some((m) => m.qitemId === qid) ? `<a href="#${id(h.host, qid)}">${esc(qid)}</a>` : esc(qid);
const evidence = (label, e) => `${label}: ${e ? `${esc(e.state)}${e.detail ? `; ${esc(e.detail)}` : ''}; ${esc(e.at ?? 'time not reported')}` : 'not reported'}`;
const rows = moves.map((m) => {
const r = m.report;
const policy = r.policy?.none === true ? 'none' : r.policy ? `${r.policy.ref}, version ${r.policy.version}` : 'not stated';
return `<details class="fold" id="${id(h.host, m.qitemId)}"><summary>${esc(r.from)} → ${esc(r.to)} · ${esc(m.reportedAt ?? 'time not reported')} · ${esc(m.qitemId)}</summary>
<p>${esc(r.reason)}</p><p class="sub">${esc(r.host)} · ${esc(r.home)} · Policy: ${esc(policy)} · reported by ${esc(m.reportedBy)} to ${esc(m.reportedTo)} · ${esc(m.state)}</p>
<ul>${r.readings.map((x) => `<li>${esc(x.subject)}: ${x.basis === 'expected' ? `expected full since ${esc(x.expectedFullSince)} (not measured)` : esc(x.basis)}${x.observedAt ? `; observed ${esc(x.observedAt)}` : ''}; ${esc(x.summary)}</li>`).join('')}</ul>
<ul>${r.seats.map((x) => `<li>${esc(x.seat)}: ${esc(x.result)}; ${evidence('Account', x.accountEvidence)}; ${evidence('Tool turn', x.toolTurn)}${x.missing ? `; missing: ${esc(x.missing)}` : ''}</li>`).join('') || '<li>No seats restarted.</li>'}</ul>
${(r.peers ?? []).map((x) => `<p class="sub">Peer ${esc(x.seat)}: ${esc(x.note)}</p>`).join('')}
${r.follows ? `<p class="sub">Follows: ${link(r.follows)}</p>` : ''}${m.followedBy?.length ? `<p class="sub">Followed by: ${m.followedBy.map(link).join(', ')}</p>` : ''}</details>`;
}).join('');
return `<h3>${esc(h.host)}</h3><p class="context">Recent reports, up to 50 per daemon.${truncated ? ' The window is full; older reports may exist.' : ''} Follow-up links cover this window only.</p>${rows || (unreadable.length ? '' : '<p class="context">No recent move reports in this window.</p>')}${unreadable.map((x) => `<p class="context">Report ${esc(x.qitemId)} could not be read: ${esc(x.reason)}.</p>`).join('')}`;
});
return `<section id="moves"><h2>Subscription moves</h2><p class="context">Managing agents' dated reports of past moves. Configured assignments and observed usage windows stay in Accounts.</p>${blocks.join('') || '<p class="context">Move reports unavailable: not read.</p>'}</section>`;
}
export function renderMachines(v) {
const m = v.machines;
const cards = m.reporting.map((x) => `<div class="machine"><div class="row"><span class="name">${esc(x.name)}</span><span class="ok">live</span>${x.version ? `<span class="sub">OpenRig ${esc(x.version)}</span>` : ''}</div>
@@ -279,7 +302,7 @@
export function renderPage(fleet, now) {
const v = { ...view(fleet, now), fleet };
return { meta: renderMeta(v), body: renderCards(v) + renderAccounts(v) + renderMachines(v) + renderRigs(v) };
return { meta: renderMeta(v), body: renderCards(v) + renderAccounts(v) + renderMoves(fleet) + renderMachines(v) + renderRigs(v) };
}
if (typeof document !== 'undefined') {
+3 -3
View File
@@ -307,11 +307,11 @@
for (const b of r.data) {
const o = observedBySeat.get(`${h.key}/${b.seat}`);
const observed = s?.status !== 'ok' ? unknown(s?.status === 'not-set-up' ? 'not set up' : s?.status === 'absent' ? 'not available on this version' : 'not read') : o?.observed?.withheld ? `not attributed${o.observed.note ? `<span class="sub">${esc(o.observed.note)}</span>` : ''}` : o?.observed?.windowsSampled === false ? unknown('no usage windows sampled') : o?.observed ? `${o.observed.differsFrom?.length ? `<span class="stale">windows differ from ${esc(o.observed.differsFrom.join(', '))}</span> · ` : ''}sampled ${ageText(o.observed.lastSampledAt, now)}${o.observed.note ? `<span class="sub">${esc(o.observed.note)}</span>` : ''}` : unknown('no observation');
seatRows.push(`<tr><td><span class="name">${esc(b.seat ?? '')}</span><span class="sub">on ${hostName(h)}</span></td><td>configured: ${b.account ? `${esc(b.account.label ?? b.account.id)}${b.account.authState ? `<span class="sub">${esc(b.account.authState)}</span>` : ''}` : unknown('no account linked')}</td><td>observed: ${observed}</td><td>${esc(s?.moves?.status === 'no-record' ? 'no record' : '')}</td></tr>`);
seatRows.push(`<tr><td><span class="name">${esc(b.seat ?? '')}</span><span class="sub">on ${hostName(h)}</span></td><td>configured: ${b.account ? `${esc(b.account.label ?? b.account.id)}${b.account.authState ? `<span class="sub">${esc(b.account.authState)}</span>` : ''}` : unknown('no account linked')}</td><td>observed: ${observed}</td></tr>`);
}
}
if (seatRows.length) parts.push(`<div class="table-wrap"><table><thead><tr><th>Seat</th><th>Configured link</th><th>Observed</th><th>Last move</th></tr></thead><tbody>${seatRows.join('')}</tbody></table></div>`);
return `<section id="subscriptions"><h2>Subscriptions</h2><p class="scope">Configured links and observed use are shown side by side and never merged. Moves between accounts are not recorded yet. Nothing here is in money.</p>${parts.join('')}</section>`;
if (seatRows.length) parts.push(`<div class="table-wrap"><table><thead><tr><th>Seat</th><th>Configured link</th><th>Observed</th></tr></thead><tbody>${seatRows.join('')}</tbody></table></div>`);
return `<section id="subscriptions"><h2>Subscriptions</h2><p class="scope">Configured links and observed use are shown side by side and never merged. Move history is shown in the Fleet page when installed. Nothing here is in money.</p>${parts.join('')}</section>`;
}
// ---- one seat --------------------------------------------------------------------------------
+29 -7
View File
@@ -307,6 +307,27 @@ export function restartSignal(before, now, previousPolledAt, polledAt) {
const READERS = { seats: readSeats, queues: readQueues, health: readHealth, messaging: readMessaging, ceremony: readCeremony, bindings: readBindings };
// A separate bounded report window on each reachable daemon, not an account assignment or a usage sample.
async function readMoves(ctx, hosts) {
const route = "/api/provider/moves?limit=50";
const polledAt = new Date(ctx.now()).toISOString();
const rows = await mapLimit(hosts, ctx.concurrency, async (host) => {
const r = host.status === "ok" ? await getJson(ctx.fetchImpl, host.url, route, ctx.timeoutMs)
: { route, status: host.status, detail: host.detail };
let result = reading(r, polledAt);
if (r.httpStatus === 404) result = { ...result, status: "unavailable", detail: "this daemon has no move-report route" };
if (r.status === "ok") {
const b = r.body;
const valid = Array.isArray(b?.moves) && Array.isArray(b?.unreadable) && typeof b.truncated === "boolean"
&& b.moves.every((m) => typeof m?.qitemId === "string" && m.report?.version === 1 && Array.isArray(m.report.seats) && Array.isArray(m.report.readings));
result = valid ? { ...result, data: { moves: b.moves, unreadable: b.unreadable, truncated: b.truncated } }
: { ...result, status: "unavailable", detail: "the move-report response shape is not supported" };
}
return { host: host.key, ...result };
});
return { hosts: rows };
}
// Subscription usage across machines, as the configured daemon already combines it. That route keeps
// each machine's reading separate and never sums them; this reader keeps it that way and only maps the
// route's host labels onto the hosts it knows.
@@ -315,24 +336,24 @@ async function readSubscriptions(ctx, daemonUrl, hosts) {
const polledAt = new Date(now()).toISOString();
const route = "/api/provider/usage/hosts";
const r = await getJson(fetchImpl, daemonUrl, route, timeoutMs);
const moves = { status: "no-record", note: "subscription moves are not recorded yet" };
if (r.status === "http-error" && r.httpStatus === 409) {
return { route, status: "not-set-up", httpStatus: 409, detail: r.detail, polledAt, observedAt: null, data: null, moves, coverage: { note: "usage windows are off on the configured daemon (provider.usage_windows)" } };
return { route, status: "not-set-up", httpStatus: 409, detail: r.detail, polledAt, observedAt: null, data: null, coverage: { note: "usage windows are off on the configured daemon (provider.usage_windows)" } };
}
if (r.status === "http-error" && r.httpStatus === 404) {
return { route, status: "absent", httpStatus: 404, detail: "this daemon's OpenRig version has no usage-across-machines route", polledAt, observedAt: null, data: null, moves, coverage: null };
return { route, status: "absent", httpStatus: 404, detail: "this daemon's OpenRig version has no usage-across-machines route", polledAt, observedAt: null, data: null, coverage: null };
}
if (r.status !== "ok") return { ...reading(r, polledAt), moves };
if (r.status !== "ok") return reading(r, polledAt);
const b = r.body ?? {};
const hostFor = (label) => (label === "local" ? hosts[0] : hosts.find((h) => h.labels.includes(label))) ?? null;
const keyFor = (label) => hostFor(label)?.key ?? `unmatched:${label}`;
const window = (w) => ({ window: w.window ?? null, usedPercent: w.usedPercent ?? null, resetsAt: w.resetsAt ?? null, observedAt: w.observedAt ?? null, freshness: w.freshness ?? null, level: w.level ?? null });
const window = (w) => ({ window: w.window ?? null, usedPercent: w.usedPercent ?? null, resetsAt: w.resetsAt ?? null, observedAt: w.observedAt ?? null, freshness: w.freshness ?? null, level: w.level ?? null, expectation: w.expectation ?? null });
const allowances = (Array.isArray(b.items) ? b.items : []).map((g) => ({
provider: g.provider ?? null, key: g.key ?? null, linkage: g.linkage ?? "unknown", notes: Array.isArray(g.notes) ? g.notes : [],
// One entry per machine that read this allowance, as that machine read it. Never added up.
observations: (Array.isArray(g.observations) ? g.observations : []).map((o) => ({
host: keyFor(o.hostId), hostLabel: o.hostId, readAt: o.readAt ?? null,
plan: o.row?.identity?.planType ?? o.row?.identity?.subscriptionType ?? null,
lastObservedExhausted: o.row?.lastObservedExhausted ?? null, signIn: o.row?.signIn ?? null,
observedAt: o.row?.observedAt ?? null, availability: o.row?.currentAvailability ?? "unknown",
windows: Array.isArray(o.row?.windows) ? o.row.windows.map(window) : [],
})),
@@ -348,7 +369,6 @@ async function readSubscriptions(ctx, daemonUrl, hosts) {
return {
route, status: "ok", httpStatus: r.httpStatus, detail: null, polledAt, observedAt: b.asOf ?? null,
data: { allowances, seatAccounts },
moves,
coverage: {
hosts: (Array.isArray(b.hosts) ? b.hosts : []).map((h) => ({ host: keyFor(h.hostId), hostLabel: h.hostId, status: h.status ?? null, usage: h.usage ?? null, readAt: h.readAt ?? null, reason: h.usageReason ?? h.error ?? null })),
skipped: Array.isArray(b.skipped) ? b.skipped : [],
@@ -448,9 +468,11 @@ export async function readFleet({
});
const subscriptions = reads.includes("subscriptions")
? (hosts[0]?.status === "ok" ? await readSubscriptions(ctx, daemonUrl, hosts) : { route: "/api/provider/usage/hosts", status: hosts[0]?.status ?? "unreachable", httpStatus: null, detail: hosts[0]?.detail ?? null, polledAt, observedAt: null, data: null, moves: { status: "no-record", note: "subscription moves are not recorded yet" }, coverage: null })
? (hosts[0]?.status === "ok" ? await readSubscriptions(ctx, daemonUrl, hosts) : { route: "/api/provider/usage/hosts", status: hosts[0]?.status ?? "unreachable", httpStatus: null, detail: hosts[0]?.detail ?? null, polledAt, observedAt: null, data: null, coverage: null })
: undefined;
if (subscriptions) subscriptions.moves = await readMoves(ctx, hosts);
const notRead = hosts.filter((h) => h.status !== "ok").map((h) => ({ key: h.key, labels: h.labels, status: h.status }));
return {
...(subscriptions ? { subscriptions } : {}),
+3 -1
View File
@@ -80,7 +80,9 @@ plane.
## Subscription rotation
You may run the subscription-rotation job only when its owner has separately enabled it for you.
Unless it has been enabled, you never move seats or accounts.
Unless it has been enabled, you never move seats or accounts. When enabled, read
`rig context get fleet-ops/manage-subscriptions` and follow its Codex-home move and reporting practice.
The person’s standing permission counts; do not ask again for each move it already covers.
## What is not yours
+6 -3
View File
@@ -5,8 +5,9 @@ A starter operations agent for an OpenRig machine, with the fleet know-how it ca
- recovering a stranded seat (a usage limit, a lost login, a restart)
- reading subscription usage honestly
- keeping agents' context healthy
- managing subscriptions when the person enables that job
The know-how is three context packs in `context/fleet-ops/`. At startup the agent adds any that are
The know-how is four context packs in `context/fleet-ops/`. At startup the agent adds any that are
missing to the machine's context library, so **every** agent on the machine can find them with
`rig context list` and read them with `rig context get fleet-ops/<name>`. You can also add them
yourself, without the agent:
@@ -15,6 +16,7 @@ yourself, without the agent:
rig context add rigs/fleet-studio/context/fleet-ops/recover-a-stranded-seat
rig context add rigs/fleet-studio/context/fleet-ops/read-usage-honestly
rig context add rigs/fleet-studio/context/fleet-ops/keep-context-healthy
rig context add rigs/fleet-studio/context/fleet-ops/manage-subscriptions
```
## Start it
@@ -27,7 +29,7 @@ rig up rigs/fleet-studio/rig.yaml
The rig also runs the Fleet page in its own container (Compose project `openrig-fleet-studio`, port
9450): open http://127.0.0.1:9450/surfaces/fleet.html. It answers which accounts have room, which
machines report here and what the rigs are doing, from what OpenRig's own daemons report. It is read
machines report here, what the rigs are doing, and the managing agent’s dated subscription move reports, from what OpenRig's own daemons report. It is read
only, needs no other app, and never asks for a sign-in or reads a token. The Observatory (`rigs/observatory/`) is an optional,
separate launch rig; Fleet Studio does not need it. Both rigs use the same starter operations agent
(`rigs/agents/starter-ops/`), each with its own startup context.
@@ -35,4 +37,5 @@ separate launch rig; Fleet Studio does not need it. Both rigs use the same start
## What it touches
Its own rig and its own files. It reads the rest of the environment with OpenRig's commands and
changes nothing outside its rig; see the agent's role.
changes nothing outside its rig unless the person separately enables the subscription-management job;
see the agent's role. Installing the bundle alone does not enable it.
@@ -0,0 +1,8 @@
name: fleet-ops/manage-subscriptions
version: "1"
taxonomy: skills
purpose: "Manage subscriptions when the person enables the job: balance Codex homes across accounts, preserve conversations, verify uptake, and report every move with its evidence and uncertainty."
files:
- path: practice.md
role: practice
summary: "Use the shipped subscription move procedure, standing account policy and separate account/turn evidence."
@@ -0,0 +1,39 @@
# Manage subscriptions
**When this applies:** the person has enabled subscription management for you. Installing this bundle
alone does not enable it. You are the one managing agent, not a second manager competing with the operator.
Read `rig context get skills/openrig-user`, "Manage subscriptions across a fleet", for the complete
move procedure and account-policy conversation. Use `rig <command> --help` for this installed version.
The first supported move is a whole Codex home on Linux, including its peers; not a subset of seats on
one home, Claude moves, or Mac restarts. If a route is missing, report it instead of improvising.
1. Load `rig context get account-policy` when it exists and record its manifest version. Follow the
person's standing permission without asking again. Without a policy, use the shipped procedure,
report no policy, and offer once to write one with them. Intent does not create missing access.
2. Read `rig provider status --all-hosts --json` and earlier `rig provider moves`. Keep machines'
readings separate; a shared allowance is one allowance. Unknown is never observed room. A known
reset may inform a plan only as **expected full since** that reset, not measured; get a new reading
when possible. A sign-in state of unknown is not needs-sign-in.
3. Balance machines across subscriptions rather than chasing the latest reset. Read
`rig seat restart <seat> --keep-conversation --plan` for the home and every peer. Skip busy or
service-bearing seats with a reason. Moving the home affects all peers, even those not restarted.
4. Confirm the selected home with `rig auth status --runtime codex --codex-home <home>`, then run
`rig auth switch <profile> --runtime codex --codex-home <home>`. For another machine, add
`--host <id>` to both auth commands and to every `rig seat restart` command in this practice.
Restart chosen seats with `rig seat restart <seat> --keep-conversation --reason "<why>"`.
Preserve their conversation and settings.
5. After a real tool-using turn, run `rig seat restart <seat> --keep-conversation --verify
--expect-account <target Account line>`. Only a matched account **and** an observed error-free tool
turn confirm uptake. Missing evidence is unconfirmed or unknown; a slow reply never warrants a
second restart. Record peers left alone as "not restarted; account unverified".
6. File `rig provider moves report --file <move.json> --to <whoever asked>` using its help for the
schema. Name from/to, host/home, reason, policy version or no policy, readings and their times/basis,
separate account and tool-turn evidence, missing facts, and peers. No credential values. A refused
report changes nothing about the move: correct the report, never repeat the switch or restart.
Follow up with `--follows <qitemId>` when evidence arrives; read it back with `rig provider moves`.
A known needs-sign-in gets one durable request to the person, naming machine and subscription; move
nothing onto it until resolved. Carry on with other work. If nothing has room, report reset times;
never use an API fallback, buy anything or create an account. The Fleet page shows these dated reports
separately from configured assignments and observed usage windows. A report is not a current login.
+2 -1
View File
@@ -16,11 +16,12 @@
## First: make sure the know-how is in this machine's library
Check `rig context list --json` for these three entries:
Check `rig context list --json` for these entries:
- `fleet-ops/recover-a-stranded-seat`
- `fleet-ops/read-usage-honestly`
- `fleet-ops/keep-context-healthy`
- `fleet-ops/manage-subscriptions`
Add only the ones that are missing, one at a time, from this rig's directory:
+48
View File
@@ -264,3 +264,51 @@ test("what wasn't read is named, never counted as zero rigs, a rig with no seats
assert.match(accAll, /No account has a readable window/);
assert.doesNotMatch(accAll, /No account is low or out|None of the/);
});
test("move history keeps reported outcomes, evidence and follow-ups separate from account readings", async () => {
const report = { version: 1, host: "box-a", home: "/homes/team", from: "old", to: "target", reason: "<keep working>", policy: { ref: "account-policy", version: "3" },
readings: [{ subject: "target", basis: "expected", expectedFullSince: "2030-01-02T11:00:00Z", summary: "reset passed" }],
seats: [{ seat: "impl@alpha", result: "moved", accountEvidence: { state: "matched", detail: "target login", at: "2030-01-02T11:01:00Z" }, toolTurn: { state: "observed", at: "2030-01-02T11:02:00Z" } },
{ seat: "review@alpha", result: "unconfirmed", missing: "no tool turn" }, { seat: "qa@alpha", result: "unknown", missing: "no capture" }],
peers: [{ seat: "lead@alpha", note: "not restarted; account unverified" }] };
const row = { qitemId: "move-1", reportedAt: "2030-01-02T11:03:00Z", reportedBy: "manager@fleet", reportedTo: "operator@kernel", state: "done", followedBy: ["move-2"], report };
const b = await daemon({ selfHostId: "box-b" });
const a = await daemon({ selfHostId: "box-a", hosts: [{ id: "second", transport: "http", url: b.url }], routes: { "/api/provider/moves": () => [200, { moves: [row, { ...row, qitemId: "move-2", followedBy: [], report: { ...report, follows: "move-1" } }], unreadable: [{ qitemId: "bad-1", reason: "invalid report" }], truncated: true }] } });
const fleet = await read(a.url); // Usage is off: move reports are still available.
const { all, body } = await render(fleet);
for (const value of ["Subscription moves", "old → target", "account-policy", "version 3", "impl@alpha", "matched", "2030-01-02T11:01:00Z", "observed", "2030-01-02T11:02:00Z", "unconfirmed", "no tool turn", "unknown", "no capture", "lead@alpha", "account unverified", "expected full since", "not measured", "bad-1", "invalid report", "older reports may exist"]) assert.ok(all.includes(value), value);
assert.match(all, /box-b: Move reports unavailable/);
assert.equal(b.calls.filter((c) => c.path === "/api/provider/moves").length, 1);
assert.match(body, /href="#move-box-a%2Fmove-2"/);
assert.match(body, /&lt;keep working&gt;/);
assert.doesNotMatch(body, /<keep working>/);
assert.ok(!(await fleetPage()).seatAccountsRows(fleet, NOW).some((r) => r.planned === "target"));
assert.ok(a.calls.some((c) => c.path === "/api/provider/moves" && c.query === "?limit=50"));
assert.ok(a.calls.every((c) => c.method === "GET"));
});
test("missing, guarded and malformed move routes say unavailable; only a read empty list says no recent reports", async () => {
for (const response of [[404, {}], [401, {}], [200, {}], [200, { moves: [], unreadable: [], truncated: false }]]) {
const a = await daemon({ selfHostId: "box-a", routes: { "/api/provider/moves": () => response } });
const { all } = await render(await read(a.url));
if (response[0] === 200 && Array.isArray(response[1].moves)) assert.match(all, /No recent move reports in this window/);
else { assert.match(all, /Move reports unavailable/); assert.doesNotMatch(all, /No recent move reports/); }
}
});
test("retained exhaustion, sign-in and reset expectations survive the reader without becoming measured room", async () => {
const observation = obs("local", 100, "exhausted");
Object.assign(observation.row, { lastObservedExhausted: { at: "2030-01-01T00:00:00Z", window: "weekly", evidence: "native reading" }, signIn: { state: "unknown", at: null, evidence: "not read" } });
observation.row.windows[0].expectation = { expectedFullSince: "2030-01-02T11:00:00Z", note: "not measured" };
const a = await daemon({ selfHostId: "box-a", usageHosts: usage([observation]) });
const fleet = await read(a.url), actual = fleet.subscriptions.data.allowances[0].observations[0];
assert.deepEqual(actual.lastObservedExhausted, observation.row.lastObservedExhausted);
assert.deepEqual(actual.signIn, observation.row.signIn);
assert.deepEqual(actual.windows[0].expectation, observation.row.windows[0].expectation);
const { all } = await render(fleet);
assert.match(all, /expected full since 2030-01-02T11:00:00Z/);
assert.match(all, /Sign-in: unknown/);
assert.match(all, /Last observed exhausted: 2030-01-01T00:00:00Z/);
assert.doesNotMatch(all, /has room/);
});
+11 -4
View File
@@ -52,6 +52,7 @@ export const SEARCHES = [
{ words: ["lost its login", "lost login", "login"], expect: "fleet-ops/recover-a-stranded-seat" },
{ words: ["subscription usage"], expect: "fleet-ops/read-usage-honestly" },
{ words: ["resets"], expect: "fleet-ops/read-usage-honestly" },
{ words: ["manage subscriptions"], expect: "fleet-ops/manage-subscriptions" },
{ words: ["context"], expect: "fleet-ops/keep-context-healthy" },
{ words: ["compaction"], expect: "fleet-ops/keep-context-healthy" },
];
@@ -72,8 +73,11 @@ const VERIFIED = {
"rig capture": [],
"rig send": [],
"rig provider status": ["--json", "--refresh", "--all-hosts", "--hosts", "--provider"],
"rig auth status": ["--runtime"],
"rig seat restart": ["--keep-conversation", "--reason", "--plan", "--json"],
"rig auth status": ["--runtime", "--codex-home", "--host"],
"rig auth switch": ["--runtime", "--codex-home", "--host"],
"rig provider moves report": ["--file", "--to", "--follows"],
"rig provider moves": ["--seat", "--limit"],
"rig seat restart": ["--keep-conversation", "--reason", "--plan", "--json", "--verify", "--expect-account", "--host"],
"rig seat launch": [],
"rig seat handover": ["--dry-run", "--reason", "--source", "--json"],
"rig up": ["--existing", "--plan", "--json"],
@@ -138,15 +142,18 @@ test("every command the know-how and the agent's text name exists, with those op
});
test("the know-how never prices usage, never measures it with a model request, and leaves moves to their owners", () => {
for (const [file, text] of knowHowTexts().slice(0, 3)) {
for (const [file, text] of knowHowTexts().filter(([file]) => file.endsWith("/practice.md"))) {
assert.doesNotMatch(text, /\$\s?\d|\bUSD\b|\bdollars?\b|\bcost of\b/i, `${file} mentions money`);
assert.doesNotMatch(text, /\/v1\/messages|api\.anthropic\.com|api\.openai\.com|curl\s/i, `${file} measures by a model request`);
assert.doesNotMatch(text, /`rig provider switch|`rig auth switch/, `${file} gives account-move steps`);
}
const usage = read(path.join(packsDir, "read-usage-honestly/practice.md"));
assert.match(usage, /Sending a model request to "check" usage\./);
assert.match(usage, /Adding readings from several machines into one total\./);
assert.match(usage, /Unknown is unknown\./);
const move = read(path.join(packsDir, "manage-subscriptions/practice.md"));
assert.match(move, /person has enabled subscription management/);
assert.match(move, /account \*\*and\*\* an observed error-free tool/);
assert.match(move, /never repeat the switch or restart/);
});
const FLEET_RIG = { project: "openrig-fleet-studio", port: 9450, providerRoute: "/api/fleet-view/" };
+3 -4
View File
@@ -82,18 +82,17 @@ test("a seat's configured link and its observed use stay in separate columns", a
assert.equal(sa.observed.kind, "observed");
const out = text(page.renderSubscriptions(fleet, NOW));
assert.deepEqual(sa.observed.differsFrom, ["dev-review@alpha"]);
assert.match(out, /dev-impl@alpha on box-a configured: team plan A active observed: windows differ from dev-review@alpha · sampled 60 min ago possibly a different login: this seat's status line reports windows that differ from another seat's \(a reset more than 60 s apart\); not a verdict no record/);
assert.match(out, /dev-impl@alpha on box-a configured: team plan A active observed: windows differ from dev-review@alpha · sampled 60 min ago possibly a different login: this seat's status line reports windows that differ from another seat's \(a reset more than 60 s apart\); not a verdict/);
assert.doesNotMatch(out, /differs from configured/);
assert.doesNotMatch(out, /current account|currently using/i);
});
test("moves are 'no record', and nothing is shown in money", async () => {
test("the Observatory points to Fleet for moves, and nothing is shown in money", async () => {
const page = await loadPage();
const a = await daemon({ selfHostId: "box-a", usageHosts: usage([obs("local", 10)]) });
const fleet = await readFleet({ daemonUrl: a.url, now, reads: ["bindings", "subscriptions"] });
assert.equal(fleet.subscriptions.moves.status, "no-record");
const out = text(page.renderSubscriptions(fleet, NOW));
assert.match(out, /Moves between accounts are not recorded yet\. Nothing here is in money\./);
assert.match(out, /Move history is shown in the Fleet page when installed\. Nothing here is in money\./);
assert.doesNotMatch(out, /\$|USD|dollar/i);
});