mirror of
https://github.com/mvschwarz/openrig-studio-apps.git
synced 2026-11-07 05:56:21 -10:00
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:
1 parent
fdaefb174e
commit
ef7dceab42
11 files changed
+178
-26
No files matched your search
@@ -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') {
|
||||
|
||||
@@ -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 --------------------------------------------------------------------------------
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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, /<keep working>/);
|
||||
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/);
|
||||
});
|
||||
@@ -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/" };
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user