From 97f8871b8b201f658cafba00bf2fd327272e862a Mon Sep 17 00:00:00 2001 From: "developing-today[bot]" <5454422+developing-today[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 01:44:36 -0600 Subject: [PATCH 1/6] quota: gatherer reads only the opencode quota subcommand (#6127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-source rewire (Drew 2026-11-05, program #6125): the tick's only quota read is now two invocations of DESCRIPTION OpenCode command line interface USAGE opencode [flags] [] ARGUMENTS directory string Directory to start OpenCode in (optional) FLAGS --standalone Run with a private server instead of the background service --server string Connect to a server URL instead of the background service --auto Auto-approve permissions that are not explicitly denied --continue, -c Continue the last session --session, -s string Session ID to continue, or to create if it does not exist --prompt string Prompt to use GLOBAL FLAGS --help, -h Show help information --version, -v Show version information --wizard Start wizard mode for a command --completions Print shell completion script (choices: bash, zsh, fish, sh) --log-level Sets the minimum log level (choices: all, trace, debug, info, warn, warning, error, fatal, none) --print-logs Print logs to stderr (server logs require --standalone) SUBCOMMANDS upgrade, update Upgrade OpenCode to the latest or a specific version uninstall Uninstall OpenCode, keeping session data, configuration, and state acp Start an Agent Client Protocol server api Make a request to the running server debug Debugging and troubleshooting tools auth manage integrations and credentials mcp Manage MCP (Model Context Protocol) servers plugin Manage plugins models List all available models stats Show shareable usage statistics mini Start the minimal interactive interface run Run OpenCode with a message session Manage sessions msg Send, read and search inter-session messages service Manage the background server reload Reload configuration pair Print one-time links to connect a browser or app serve Start the v2 API and web server (configured listing + explicit ask for the route-disabled live pools meta/command-code), mapped through the unchanged quota-gather-map contract. Retired from the gatherer: the in-process OpenChamber module import, the gatherer-side Turso read, the native ClinePass adapter fill, the file-export family, and the Claude expiry side-read (the subcommand results carry the metadata now). Kept: the OpenDesign session keeper (credential maintenance, runs before the read), #1268 last-known retention, #1331 pool splits, Command Code plan-window mapping, fail-soft heartbeat contract. quota-current.json source becomes "opencode-quota". Added: Supabase sink — one agent.quota_records row per pool entry per tick (source "opencode-quota"), replacing the "pm-pool" writer that died 2026-10-29. Fail-soft after the file write. QUOTA_SUBCOMMAND env overrides the whole command (interim fork builds); the systemd drop-in points at the fork-built binary until the nix switch ships the new system opencode. --- pm-stack/aggregator/README.md | 39 +- pm-stack/aggregator/scripts/gather-quota.ts | 927 ++++++-------------- pm-stack/aggregator/src/quota-gather-map.ts | 2 +- 3 files changed, 290 insertions(+), 678 deletions(-) diff --git a/pm-stack/aggregator/README.md b/pm-stack/aggregator/README.md index 46bcaff4c..c4ba2e9ad 100644 --- a/pm-stack/aggregator/README.md +++ b/pm-stack/aggregator/README.md @@ -600,31 +600,38 @@ clone (`~/git/code`) and writes its outputs under Each tick: -- imports the OpenChamber quota module in-process (from the laptop's - OpenChamber checkout) and fetches every configured provider - per-credential, with credentials read **directly from OpenCode's - credential store** (`opencode.db`, read-only) — no OpenCode server - needs to be listening. One provider failing or exceeding the 15s - cap degrades only its own rows. -- ingests laptop **file exports** in the Meta Phase-1 convention - (#1301): a producer's `latest.json` (today: Meta, - `~/atlas/meta-quota-export/latest.json`) maps onto the same - contract; a failure-status document or a file older than 30 - minutes is an error entry, never zeros. -- writes **`quota-current.json`** — the latest-state contract +- runs the OpenDesign session keeper (Drew's provider browser-auth + auto-renewal rule): the one browser-session credential in the + system is read, slid, or re-captured over CDP before it is used, + with its horizon recorded in `opendesign-renewal.json`. The keeper + maintains a credential; it is not a quota source. +- invokes the ONE quota source - the opencode fork's quota + subcommand, `opencode quota --json` (#6125/#6127) - whose + per-provider quota functions live in the providers that own the + credentials. A second invocation asks explicitly for the + route-disabled live pools (`meta`, `command-code`), which a named + ask always fetches. The gatherer fetches no provider itself and + imports no quota module; a provider failing degrades only its own + rows, and #1268 retention keeps last-known rows for errored runs. +- writes **`quota-current.json`** - the latest-state contract (`QuotaCurrentFile` in `src/model.ts`: per provider/credential, windows carrying value, % of limit, reset, measured-at, source, - state) — and `quota-gatherer-heartbeat.json` (tick time, - per-provider state/duration; the heartbeat also rides inside + state; the file's `source` is `opencode-quota`) - and + `quota-gatherer-heartbeat.json` (tick time, per-provider + state/duration; the heartbeat also rides inside quota-current.json), both in the aggregator state directory beside `fleet-feed.json`. -- appends §4 snapshot records for windows whose value **moved** +- appends S4 snapshot records for windows whose value **moved** since the last appended record, directly to the snapshot store (`SnapshotStore.append` dedupes; change-tracking state in `quota-gatherer-state.json`). Setting `USAGE_SNAPSHOT_OUTBOX_PATH` reroutes records to the writers' outbox instead. +- appends one Supabase `agent.quota_records` row per pool entry to + muse-dev2 (source `opencode-quota`), so the readings land in + Supabase on the same tick as the dashboard file. Fail-soft: a sink + failure is logged and never fails the tick. -Fail-soft: a module-import or credential-store failure writes a +Fail-soft: a subcommand failure or unparseable output writes a heartbeat with the error, exits non-zero, and leaves the previous `quota-current.json` untouched. **Remaining hop:** the aggregator does not read `quota-current.json` yet — `Source` pickup (freshness diff --git a/pm-stack/aggregator/scripts/gather-quota.ts b/pm-stack/aggregator/scripts/gather-quota.ts index 71c824311..27ab47e2b 100755 --- a/pm-stack/aggregator/scripts/gather-quota.ts +++ b/pm-stack/aggregator/scripts/gather-quota.ts @@ -1,94 +1,53 @@ #!/usr/bin/env bun -// Quota gatherer (Stage 2, #1151): the PM stack's 15-minute quota -// tick. Deployed on the LAPTOP (systemd user timer, units in -// pm-stack/systemd/, installed by install.sh) beside the aggregator -// and the snapshot store. Two input families: +// Quota gatherer (Stage 2, #1151; single-source rewire #6127): the PM +// stack's 15-minute quota tick. Deployed on the LAPTOP (systemd user +// timer, units in pm-stack/systemd/, installed by install.sh) beside +// the aggregator and the snapshot store. // -// 1. The OpenChamber quota module, imported in-process from the -// laptop's OpenChamber checkout, fetching every configured -// provider per credential. Credentials come from ASKING THE -// OPENCODE SERVER (Drew's ruling, 2026-11-03: the quota ask -// terminates at the OpenCode server — never at a standalone -// proxy program and never at a direct database read when the -// server can answer): the module's own openCodeCredentialSource -// against GET /api/credential on the standing fork server -// (service-prod.json under ~/.local/state/opencode/ names its -// URL and password; the password is read at tick time and never -// logged) — the quota CLI's primary path (#2490). The DIRECT -// read of OpenCode's credential store (the `credential` table -// of opencode.db, opened read-only, the module's -// openCodeLocalCredentialSource) remains ONLY as the -// transport-failure fallback, the CLI's exact combined-source -// shape: a stopped server switches the tick's reads to the -// local store instead of zeroing the tick (#1251), and when -// even that fails the tick fails loudly and the previous -// quota-current.json stands, with #1268 retaining last-known -// rows per provider. A server that ANSWERS and refuses is -// authoritative — no fallback. The fallback's projection (rows -// ordered (active, time_created, id), the last row per -// integration selected, unreadable rows surfaced rather than -// silently dropped) is the one every surface gets. -// Environment-variable keys are not store rows and never appear -// here — stored credentials win, which is the order OpenCode -// itself uses. -// One leg does NOT terminate at the server: OpenDesign. Its -// provider reads a managed session credential (a Better Auth -// session cookie + workspace id, in OpenChamber's own quota -// credential store) because the billing usage API it answers -// from accepts only that session — the credential the OpenCode -// server holds for `opendesign` is the AMR inference API key, -// which that API refuses (verified 403 as Bearer and as session -// cookie, 2026-11-03), and the server exposes no quota/usage -// surface at all (its route table has credential/session/etc. -// groups, no quota group). The gap is filed as code#5060. -// 3. The Turso org usage API (#1049): usage + subscription over -// the verified route, token from Bitwarden by name. -// 2. File exports (the Phase-1 convention, #1301): producers the -// module cannot reach write latest.json documents on the laptop; -// the gatherer ingests them in the same tick. A document older -// than 30 minutes means the producer is unreachable — an error -// entry, never zeros. The v1 producer (Meta's dev.meta.ai -// console scrape) was retired 2026-11-03: Drew's ruling is that -// quota is gathered by asking OpenCode — family 5 below is its -// replacement, and the producer list is empty today. -// 4. The native ClinePass token adapter (#1159, src/clinepass- -// adapter.ts on the #1153 framework): fills the ClinePass row -// when the OpenChamber module did not already produce a fresh -// one this tick, and only when CLINEPASS_CREDENTIAL_REF names -// a credential reference on this host. An expired token is -// session-expired / re-login with the last-known row retained -// (#1268) — never a 0% row. -// 5. The Meta module ask (2026-11-03): the provider listing -// (family 1) filters providers whose session door is disabled -// in the opencode config, and `meta` is disabled as a direct -// door — sessions reach the pool through model routes — so the -// listing never names it. The gatherer asks the module for meta -// explicitly (runMetaModule): the read runs through OpenCode's -// own credential machinery, which is what gathering quota by -// asking OpenCode means for a route-only pool. +// THE SINGLE SOURCE (Drew's order, 2026-11-05, #6125): every quota +// reading in this tick comes from ONE producer — the opencode fork's +// quota subcommand, `opencode quota --json`, whose per-provider quota +// functions live in the providers that own the credentials +// (developing-today-forks/opencode#1718). The gatherer invokes the +// subcommand; it never fetches a provider itself. What the tick does +// around that single read: // -// 6. The OpenDesign session keeper (src/opendesign-renewal.ts, +// 1. The OpenDesign session keeper (src/opendesign-renewal.ts, // Drew's 2026-11-05 provider browser-auth auto-renewal rule): -// family 1's OpenDesign leg rides on a Better Auth session in -// OpenChamber's managed credential store — the one credential -// in the tick that is a browser session rather than a token. -// Before the provider runs each tick, the keeper reads the -// stored session (authenticated reads slide its expiry once -// the update interval elapses — verified live 2026-11-05: -// expiresAt re-extended to read-time + 7 days), records the +// OpenDesign's quota function rides on a Better Auth session in +// a managed credential store (the one credential in the system +// that is a browser session rather than a token). Before the +// subcommand runs each tick, the keeper reads the stored +// session (authenticated reads slide its expiry), records the // horizon in opendesign-renewal.json beside quota-current.json, // re-captures the cookie from the standing laptop Helium over // CDP when the stored session fails or its horizon runs low -// (validating the candidate before it is stored), and raises -// an alert in that state file while days remain when renewal -// cannot complete. Fail-soft: a keeper error never fails the -// tick. +// (validating the candidate before it is stored), and raises an +// alert in that state file while days remain when renewal +// cannot complete. The keeper maintains a CREDENTIAL; it is not +// a quota source. Fail-soft: a keeper error never fails the tick. +// (Its credential-store accessors live in the OpenChamber lib +// the store was born in — store plumbing, not a reading path.) +// 2. The subcommand, twice: the configured listing +// (`opencode quota --json`) and the explicit ask +// (`opencode quota --json meta command-code`) — providers whose +// routing doors are disabled in the opencode config but whose +// pools are live are always fetched when named, the same +// discipline the gatherer's out-of-band asks kept before the +// rewire. An explicit-ask result replaces a listing result for +// the same provider + credential. The subcommand exits non-zero +// when a requested provider produced no successful result; that +// is a per-provider failure, not a tick failure — stdout still +// carries the results, and only an unparseable/empty stdout +// fails the tick (loudly; the previous quota-current.json +// stands, #1268). // // Outputs: // - quota-current.json — the latest-state contract (model.ts // QuotaCurrentFile) in the deployed aggregator dir, beside -// fleet-feed.json. (Aggregator pickup of this file is the -// remaining hop; see the README's quota-gatherer section.) +// fleet-feed.json. Its `source` is "opencode-quota" +// (GATHERER_SOURCE): readings are the subcommand's, and the file +// says so. // - quota-gatherer-heartbeat.json — the tick's health record // (also embedded in quota-current.json). // - §4 snapshot records for windows whose value moved since the @@ -96,43 +55,44 @@ // snapshot store (SnapshotStore.append dedupes). Setting // USAGE_SNAPSHOT_OUTBOX_PATH switches the sink to the writers' // outbox JSONL instead (the sandbox collector's drain path). +// - Supabase agent.quota_records rows (muse-dev2), one per pool +// entry per tick, source "opencode-quota" — Drew's rule that all +// quota info lands in Supabase on a regular basis in addition to +// the dashboard. (The previous writer of pool rows, source +// "pm-pool", died 2026-10-29; this sink replaces it.) Fail-soft: +// a sink failure is logged and never fails the tick — +// quota-current.json is already written by then. // -// Fail-soft contract: if the module import fails or the credential -// store cannot be read, the tick writes a heartbeat carrying the -// error and exits non-zero WITHOUT touching quota-current.json. A -// single provider failing or exceeding its cap degrades only its own -// rows; the tick still succeeds. A provider whose run errors keeps -// its last-known ok entries from the previous tick's -// quota-current.json (#1268) — retained verbatim, so their original -// measuredAt makes the hub classify them stale instead of the fresh -// error rows replacing the last good data. +// Fail-soft contract: if the subcommand cannot be run or its output +// cannot be parsed, the tick writes a heartbeat carrying the error +// and exits non-zero WITHOUT touching quota-current.json. A single +// provider failing degrades only its own rows; the tick still +// succeeds. A provider whose run errors keeps its last-known ok +// entries from the previous tick's quota-current.json (#1268) — +// retained verbatim, so their original measuredAt makes the hub +// classify them stale instead of the fresh error rows replacing the +// last good data. // // Usage: bun run scripts/gather-quota.ts -// Env: OPENCHAMBER_LIB_DIR (default: first of ~/git/devtoday- -// openchamber, ~/git/openchamber-fork containing the module), -// OPENCODE_DB_PATH (~/.local/share/opencode/opencode.db), -// OPENCODE_URL (default: the service-prod.json URL, else -// http://127.0.0.1:13420), OPENCODE_AUTH_FILE -// (~/.local/state/opencode/service-prod.json — the standing -// server's URL + password, read at tick time, never logged), -// OPENCODE_SERVER_PASSWORD (overrides the file's password), +// Env: QUOTA_SUBCOMMAND (full command override, e.g. a fork-built +// binary path + args, shell-split on spaces; default: +// "opencode quota --json" / the explicit ask appends provider +// ids), QUOTA_SUBCOMMAND_TIMEOUT_MS (240000 — the whole-exec +// cap; the subcommand self-limits per provider), // QUOTA_CURRENT_PATH, QUOTA_HEARTBEAT_PATH, // QUOTA_GATHERER_STATE_PATH, QUOTA_SNAPSHOTS_DIR (./snapshots), // USAGE_SNAPSHOT_OUTBOX_PATH, -// QUOTA_GATHER_TIMEOUT_MS (15000), -// QUOTA_GATHER_CONCURRENCY (6), TURSO_API_TOKEN (else the -// Bitwarden secret turso.platform-api-token-muse-cli, resolved -// by name via ~/tools/bws-env conventions — BWS_BIN and -// BWS_ACCESS_TOKEN_FILE override its plumbing), -// TURSO_ORG_SLUG (dezren39), TURSO_API_BASE -// (https://api.turso.tech), -// CLINEPASS_CREDENTIAL_REF (bitwarden: or file: — -// a CredentialRef for the native ClinePass adapter (#1159), -// never a credential value; unset = not configured on this -// host, live verification gated on #1155). +// QUOTA_GATHER_CONCURRENCY (retained for interface stability; +// the subcommand owns fetch concurrency now), +// OPENCHAMBER_LIB_DIR (ONLY the OpenDesign keeper's credential +// store accessors resolve from here; no quota reading does), +// SUPABASE_QUOTA_SINK=0 disables the Supabase sink (default on; +// token + project ref resolved by name via the resolveBwsSecret +// plumbing: supabase.muse-dev2.access-token-muse-cli / +// supabase.muse-dev2.project-ref). import { existsSync } from "node:fs"; import { execFile } from "node:child_process"; -import { access, appendFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises"; +import { access, appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -142,25 +102,12 @@ import { entriesWithPoolSplits, entryFromResult, entryHasMeasuredWindow, - entryFromFileExport, - entryFromTurso, lastKnownEntries, - mapWithConcurrency, markRetained, recordsFromEntries, - type FileExportDoc, type QuotaProviderResult, - type TursoSubscriptionDoc, - type TursoUsageDoc, } from "../src/quota-gather-map.ts"; import { SnapshotStore, isSnapshotRecord, snapshotKey } from "../src/snapshots.ts"; -import { - CLINEPASS_PROVIDER_ID, - clinePassErrorEntry, - credentialRefFromConfig, - gatherClinePass, -} from "../src/clinepass-adapter.ts"; -import type { CredentialRef } from "../src/token-adapter.ts"; import { safeProviderReason } from "../src/credential-lifecycle.ts"; import { renewOpenDesignSession, type OpenDesignCredentialLike } from "../src/opendesign-renewal.ts"; @@ -168,6 +115,10 @@ const PKG = join(dirname(fileURLToPath(import.meta.url)), ".."); const HOME = homedir(); const HIDDEN = join(HOME, "workspace/goals/keep-the-four-repos-healthy/hidden_files"); +/** The OpenDesign keeper's credential-store accessors live in the + * OpenChamber lib (the managed store's home). Store plumbing for ONE + * credential the keeper maintains — never a quota reading path: all + * readings come from the subcommand (see the header). */ function resolveLibDir(): string { const candidates = [ process.env.OPENCHAMBER_LIB_DIR, @@ -180,88 +131,21 @@ function resolveLibDir(): string { return candidates[0]; } const libDir = resolveLibDir(); -const dbPath = process.env.OPENCODE_DB_PATH ?? join(HOME, ".local/share/opencode/opencode.db"); const currentPath = process.env.QUOTA_CURRENT_PATH ?? join(PKG, "quota-current.json"); const heartbeatPath = process.env.QUOTA_HEARTBEAT_PATH ?? join(PKG, "quota-gatherer-heartbeat.json"); const statePath = process.env.QUOTA_GATHERER_STATE_PATH ?? join(PKG, "quota-gatherer-state.json"); const snapshotsDir = process.env.QUOTA_SNAPSHOTS_DIR ?? join(PKG, "snapshots"); const outboxPath = process.env.USAGE_SNAPSHOT_OUTBOX_PATH ?? null; -const timeoutMs = Number(process.env.QUOTA_GATHER_TIMEOUT_MS ?? 15_000); -const gatherConcurrency = Number(process.env.QUOTA_GATHER_CONCURRENCY ?? 6); +const subcommandTimeoutMs = Number(process.env.QUOTA_SUBCOMMAND_TIMEOUT_MS ?? 240_000); const OBSERVED_BY = "quota-gatherer"; -/** - * File-export producers ingested each tick (the #1301 convention). - * The v1 producer — Meta, ~/atlas/meta-quota-export/latest.json, a - * dev.meta.ai console scrape — was retired 2026-11-03: quota is - * gathered by asking OpenCode (Drew's ruling), and the module's own - * Meta provider replaced the scrape (see runMetaModule). The list - * stays, empty, for the next producer the module cannot reach; - * entryFromFileExport keeps the convention's mapping tested. - */ -const FILE_EXPORTS: { key: string; path: string; staleMs: number }[] = []; - -interface QuotaModule { - listConfiguredQuotaProviders: (opts?: { includeDisabled?: boolean }) => Promise; - fetchQuotaForProviderInstances: (providerId: string) => Promise; -} -interface AuthModule { - configureOpenCodeCredentials: (source: unknown) => void; - openCodeCredentialSource: (opts: { - buildOpenCodeUrl: (requestPath?: string) => string; - getOpenCodeAuthHeaders: () => Record; - }) => CredentialSourceLike; - // The shared direct credential-store read from lib/opencode/auth.js - // (#2490 phase 3): same projection and failure semantics as the CLI - // fallback — it throws when the store is missing or holds rows but - // none are readable, and that throw reaches main()'s failTick via - // listConfiguredQuotaProviders, so a bad store fails the tick loudly - // instead of producing an empty "not configured" tick. It has no - // listEnvironmentKeys: stored credentials are the whole story for - // this reader, as before. Since 2026-11-03 it is wired only BEHIND - // the server source above (transport-failure fallback), never as - // the primary read. - openCodeLocalCredentialSource: (opts: { dbPath: string }) => CredentialSourceLike; -} - -interface CredentialSourceLike { - list: () => Promise; - create?: (input: unknown) => Promise; - remove?: (credentialID: string) => Promise; - listEnvironmentKeys?: () => Promise>; - usesLocalStore?: () => boolean; - getLaunchEnvironment?: () => unknown; -} - -interface ClaudeCredentialSource { - loadClaudeCredential: () => Promise<{ - expiresAt?: number | null; - source?: "keychain" | "credentials-file" | "opencode-auth" | "env"; - } | null>; -} - -/** Read only Claude expiry metadata; credential values stay local and are discarded. */ -async function claudeExpiryMetadata(): Promise<{ - expiresAt?: string; - source?: "claude-code" | "opencode" | "environment"; -} | null> { - try { - const auth = (await import(join(libDir, "quota/providers/claude/auth.js"))) as unknown as ClaudeCredentialSource; - const credential = await auth.loadClaudeCredential(); - if (!credential) return null; - const metadata: { expiresAt?: string; source?: "claude-code" | "opencode" | "environment" } = {}; - if (typeof credential.expiresAt === "number" && Number.isFinite(credential.expiresAt)) { - const expiry = new Date(credential.expiresAt); - if (Number.isFinite(expiry.getTime())) metadata.expiresAt = expiry.toISOString(); - } - if (credential.source === "keychain" || credential.source === "credentials-file") metadata.source = "claude-code"; - else if (credential.source === "opencode-auth") metadata.source = "opencode"; - else if (credential.source === "env") metadata.source = "environment"; - return metadata; - } catch { - return null; - } -} +/** Providers always asked explicitly, by name, every tick: their + * routing doors are disabled in the opencode config (definitions + * kept, flipped off), so a listing can omit them even though their + * credentials and plans are live pools. A named ask always fetches. + * (Meta: route-only pool, #6079. Command Code: disabled door, live + * plan — its read is the PLAN read; see keepCommandCodePlanWindows.) */ +const EXPLICIT_ASK_PROVIDERS = ["meta", "command-code"]; async function writeAtomic(path: string, text: string): Promise { await mkdir(dirname(path), { recursive: true }); @@ -287,22 +171,6 @@ async function failTick(tickAt: string, started: number, error: string): Promise return 1; } -async function withTimeout(p: Promise, ms: number, label: string): Promise { - let timer: ReturnType | undefined; - const timeout = new Promise((_, reject) => { - timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); - }); - try { - return await Promise.race([p, timeout]); - } finally { - clearTimeout(timer); - } -} - -/** Bitwarden secret NAME for the Turso platform token (#1049). - * The value is resolved at tick time and never logged or stored. */ -const TURSO_SECRET_NAME = "turso.platform-api-token-muse-cli"; - /** * Resolve one Bitwarden Secrets Manager secret by key, without ever * logging its value (the collect-fleet-feed.ts pattern). Plumbing: @@ -373,82 +241,7 @@ async function resolveBwsSecret(key: string): Promise { } } -/** - * Turso org usage (#1049): usage + subscription reads over the - * verified org route, mapped by entryFromTurso. Any failure (no - * token, HTTP error, timeout) degrades to this run's error entry — - * per-provider fail-soft like every other source; never zeros. - */ -async function runTurso(tickIso: string): Promise { - const t0 = Date.now(); - const done = (entry: QuotaCurrentEntry): ProviderRun => ({ - id: "turso", - state: entry.state === "ok" ? "ok" : "error", - ms: Date.now() - t0, - entries: [entry], - ...(entry.error ? { error: entry.error } : {}), - }); - const token = process.env.TURSO_API_TOKEN?.trim() || (await resolveBwsSecret(TURSO_SECRET_NAME)); - if (!token) { - return done( - entryFromTurso(null, null, { - measuredAt: tickIso, - error: `turso token unavailable (Bitwarden secret ${TURSO_SECRET_NAME})`, - }), - ); - } - const base = process.env.TURSO_API_BASE ?? "https://api.turso.tech"; - const slug = process.env.TURSO_ORG_SLUG ?? "dezren39"; - try { - const get = async (path: string): Promise => { - // Fetch AND body parse inside the timeout, with an abort - // signal: a body that stalls after prompt headers must not - // hang the tick (Codex review, PR #2366). - return withTimeout( - (async () => { - const res = await fetch(`${base}/v1/organizations/${slug}${path}`, { - headers: { Authorization: `Bearer ${token}` }, - signal: AbortSignal.timeout(timeoutMs), - }); - if (!res.ok) throw new Error(`turso ${path} HTTP ${res.status}`); - return res.json(); - })(), - timeoutMs, - `turso ${path}`, - ); - }; - const usage = (await get("/usage")) as TursoUsageDoc; - const subscription = (await get("/subscription")) as TursoSubscriptionDoc; - return done(entryFromTurso(usage, subscription, { measuredAt: tickIso })); - } catch (err) { - return done( - entryFromTurso(null, null, { - measuredAt: tickIso, - error: safeProviderReason(err instanceof Error ? err.message : String(err)), - }), - ); - } -} - -/** - * Resolve a ClinePass CredentialRef to its value at tick time - * (#1159): Bitwarden by name (the resolveBwsSecret plumbing), or a - * file whose owner-only mode is enforced, not assumed. The value is - * handed to the adapter's fetch and never logged or stored. - */ -async function resolveClinePassCredentialValue(ref: CredentialRef): Promise { - if (ref.kind === "bitwarden") return resolveBwsSecret(ref.name); - try { - const st = await stat(ref.path); - if ((st.mode & 0o077) !== 0) return null; - const v = (await readFile(ref.path, "utf8")).trim(); - return v || null; - } catch { - return null; - } -} - -/** Managed-credential accessors the OpenChamber quota module exposes (libDir). */ +/** Managed-credential accessors for the OpenDesign keeper (libDir). */ interface OpenDesignCredentialsModule { readManagedCredential: (providerId: string) => OpenDesignCredentialLike | null; writeManagedCredential: (providerId: string, value: OpenDesignCredentialLike) => unknown; @@ -458,10 +251,13 @@ interface OpenDesignProviderModule { } /** - * The OpenDesign session keeper run (family 6 above). Runs before - * the provider listing is fetched so a credential renewed this tick - * is the one the tick's OpenDesign read uses. Fail-soft: any error - * is logged and the tick proceeds with the stored credential as-is. + * The OpenDesign session keeper run. Runs before the subcommand so a + * credential renewed this tick is the one the tick's OpenDesign read + * uses. The validate step performs a quota fetch with the candidate + * credential purely to prove the credential works before it is + * stored — a credential check, not a quota reading (no value from it + * reaches any output). Fail-soft: any error is logged and the tick + * proceeds with the stored credential as-is. */ async function runOpenDesignRenewal(tickAt: string): Promise { if (process.env.OPENDESIGN_RENEWAL_DISABLED === "1") return; @@ -492,130 +288,6 @@ async function runOpenDesignRenewal(tickAt: string): Promise { } } -/** - * The native ClinePass adapter run (#1159). Null when no credential - * reference is configured on this host — the adapter is registered - * but dormant until #1155 names the provenance. A misconfigured - * reference is an error row naming the defect, never a value. - * Previous-value retention is the tick's #1268 pass below, so the - * adapter is collected with no previous here. - */ -async function runClinePass(tickIso: string): Promise { - const t0 = Date.now(); - let ref: CredentialRef | null; - try { - ref = credentialRefFromConfig(process.env.CLINEPASS_CREDENTIAL_REF); - } catch (err) { - const detail = err instanceof Error ? err.message : String(err); - const entry = clinePassErrorEntry(safeProviderReason(detail), tickIso); - entry.credentialWatch = true; - return { - id: CLINEPASS_PROVIDER_ID, - state: "error", - ms: Date.now() - t0, - entries: [entry], - error: safeProviderReason(detail), - }; - } - if (!ref) return null; - const outcome = await gatherClinePass({ - now: new Date(tickIso), - previous: null, - credentialRef: ref, - resolveCredentialValue: resolveClinePassCredentialValue, - }); - const entry = - outcome.value ?? clinePassErrorEntry(outcome.detail ?? `cline-pass adapter: ${outcome.status}`, tickIso); - entry.credentialWatch = true; - return { - id: CLINEPASS_PROVIDER_ID, - state: outcome.status === "ok" ? "ok" : "error", - ms: Date.now() - t0, - entries: [entry], - ...(outcome.status !== "ok" ? { error: outcome.detail ?? outcome.status } : {}), - }; -} - -/** - * The Meta module ask (2026-11-03, Drew's ask-OpenCode ruling): the - * quota module's provider listing filters out providers whose - * session door is disabled in the opencode config, and `meta` is - * disabled as a direct door (sessions reach the pool through model - * routes), so the listing never names it and the plain tick never - * asked — the board's Meta row went dark even though OpenCode holds - * the pool's credential and the module's Meta provider reads the - * plan's own usage endpoint with it. The gatherer asks explicitly, - * the same out-of-band registration pattern as the native ClinePass - * adapter. No outer timeout wraps the ask: the provider self-times - * out per request (15s), and its account-token fallback is a second - * request — clipping the pair at the generic 15s cap could turn a - * slow success into a false error row. - */ -async function runMetaModule(quota: QuotaModule, tickIso: string): Promise { - const t0 = Date.now(); - try { - // This explicit ask IS the OpenCode-side mechanism, in-process: - // it is the same fetch the OpenChamber CLI's quota subcommand - // runs for a named provider (`openchamber quota meta --json`, - // packages/web/bin/lib/commands-quota.js in the OpenChamber - // fork) — credentials asked from the OpenCode server - // (GET /api/credential, family 1), and an explicit ask is always - // fetched even when the provider is turned off in routing - // (the listing filter governs listing, never an explicit ask). - // The provider then reads Meta's own plan endpoint - // (POST https://api.meta.ai/muse-code/key) with the OpenCode - // `meta` credential, falling back to the Muse CLI account - // token in ~/.config/muse/auth.json when OpenCode holds only - // the minted inference key. OpenCode's internal route_attempt - // history is NOT a substitute: it meters tokens/cost, and Meta's - // plan percentage exists only on Meta's endpoint. - const results = await quota.fetchQuotaForProviderInstances("meta"); - const entries = results.map((r) => entryFromResult(r, tickIso)); - return finalizeRun("meta", entries, t0); - } catch (err) { - return { - id: "meta", - state: "error", - ms: Date.now() - t0, - entries: [], - error: safeProviderReason(err instanceof Error ? err.message : String(err)), - }; - } -} - -/** - * The Command Code explicit ask: the Meta pattern, for the same - * reason. `commandcode` is disabled as a routing door in the - * OpenCode config (definitions kept, flipped off in - * disabled_providers), so the provider listing can drop - * `command-code` even though its credential and plan are live — - * and a provider absent from the listing produced no run at all: - * no error, no #1268 retention, just a silently missing row. - * Drew's read was "working" because an explicit ask always works: - * the module fetches a named provider even when routing turns it - * off (`openchamber quota command-code --json` is the CLI form). - * The read is the PLAN read (see keepCommandCodePlanWindows): - * monthly from the included-credits bucket, 5h/weekly from - * windowLimits — never the purchased/free credit balances. - * No outer timeout, mirroring runMetaModule. - */ -async function runCommandCodeModule(quota: QuotaModule, tickIso: string): Promise { - const t0 = Date.now(); - try { - const results = await quota.fetchQuotaForProviderInstances("command-code"); - const entries = results.map((r) => entryFromResult(r, tickIso)); - return keepCommandCodePlanWindows(finalizeRun("command-code", entries, t0)); - } catch (err) { - return { - id: "command-code", - state: "error", - ms: Date.now() - t0, - entries: [], - error: safeProviderReason(err instanceof Error ? err.message : String(err)), - }; - } -} - interface ProviderRun { id: string; state: "ok" | "error"; @@ -636,7 +308,7 @@ interface ProviderRun { * last good percentages. Downgrade it to an error run carrying the * mapper's no-data reason, flip its entries to error so retention * can replace them with last-known rows, and let the heartbeat say - * why. The entries' failureClass / lastSuccessAt (when the module + * why. The entries' failureClass / lastSuccessAt (when the provider * provides them) ride along in the reason. */ function finalizeRun(id: string, entries: QuotaCurrentEntry[], t0: number): ProviderRun { @@ -663,13 +335,14 @@ function finalizeRun(id: string, entries: QuotaCurrentEntry[], t0: number): Prov } /** Command Code plan windows (Drew's rule, 2026-11-01: Command Code - * reads use PLAN, not BALANCE). The module's provider also returns - * balance windows (purchased_credits / free_credits: label-only, no + * reads use PLAN, not BALANCE). The provider also returns balance + * windows (purchased_credits / free_credits: label-only, no * percentage); those are balances, never quota, so they are stripped * from the entry's windows — the raw payload stays on entry.usage — * and no consumer or snapshot writer can mistake a balance for a * plan reading or read the plan windows as "empty" because the - * first window is a balance. */ + * first window is a balance. This is a mapping rule over the single + * source's output, not a second read. */ const COMMAND_CODE_PLAN_WINDOWS = new Set(["5h", "weekly", "monthly"]); function keepCommandCodePlanWindows(run: ProviderRun): ProviderRun { for (const entry of run.entries) { @@ -678,56 +351,6 @@ function keepCommandCodePlanWindows(run: ProviderRun): ProviderRun { return run; } -async function runProvider(quota: QuotaModule, id: string, tickIso: string): Promise { - const t0 = Date.now(); - try { - const results = await withTimeout(quota.fetchQuotaForProviderInstances(id), timeoutMs, id); - const entries = results.map((r) => entryFromResult(r, tickIso)); - // A provider is ok when at least one credential row answered - // with a measured window; a failing row (e.g. Codex's - // token-sharing login, which OpenAI refuses by design) keeps its - // error on its own entry, and an ok-but-null row degrades the - // run (see finalizeRun). - return finalizeRun(id, entries, t0); - } catch (err) { - return { - id, - state: "error", - ms: Date.now() - t0, - entries: [], - error: safeProviderReason(err instanceof Error ? err.message : String(err)), - }; - } -} - -/** Ingest one file-export document into a quota-current entry. */ -async function runFileExport(fx: (typeof FILE_EXPORTS)[number]): Promise { - const t0 = Date.now(); - let entry: QuotaCurrentEntry; - try { - const st = await stat(fx.path); - let doc: FileExportDoc | null = null; - try { - doc = JSON.parse(await readFile(fx.path, "utf8")) as FileExportDoc; - } catch { - doc = null; - } - entry = entryFromFileExport(doc, { - fileMtimeIso: st.mtime.toISOString(), - stale: Date.now() - st.mtimeMs > fx.staleMs, - }); - } catch { - entry = entryFromFileExport(null, { fileMtimeIso: null, stale: false }); - } - return { - id: fx.key, - state: entry.state === "ok" ? "ok" : "error", - ms: Date.now() - t0, - entries: [entry], - ...(entry.error ? { error: entry.error } : {}), - }; -} - /** The previous tick's quota-current entries, for #1268 last-known * retention. Any read/parse failure means no retention this tick. */ async function readPreviousEntries(): Promise { @@ -754,222 +377,199 @@ async function readState(): Promise> { return new Map(); } -/** - * The standing OpenCode fork server's coordinates (Drew's ruling, - * 2026-11-03: the quota ask terminates at the OpenCode server). The - * service file is the fleet's standing record — the dispatcher's - * OPENCODE_AUTH_FILE convention (pm-stack/dispatch/src/config.ts) — - * naming the server's URL and its password. OPENCODE_URL / - * OPENCODE_SERVER_PASSWORD override it. The password is held in this - * closure only: never logged, never written, never placed in a URL. - * A missing/unreadable file is not fatal: the default loopback URL is - * used with no auth header, and the local-store fallback behind the - * server source covers a server that cannot be reached at all. - */ -async function resolveOpenCodeServer(): Promise<{ baseUrl: string; authHeaders: () => Record }> { - let url = process.env.OPENCODE_URL?.trim() || ""; - let password = process.env.OPENCODE_SERVER_PASSWORD?.trim() || ""; - if (!url || !password) { - try { - const filePath = process.env.OPENCODE_AUTH_FILE ?? join(HOME, ".local", "state", "opencode", "service-prod.json"); - const svc = JSON.parse(await readFile(filePath, "utf8")) as { url?: unknown; password?: unknown }; - if (!url && typeof svc.url === "string" && svc.url.trim()) url = svc.url.trim(); - if (!password && typeof svc.password === "string") password = svc.password; - } catch { - // no service file — the defaults below stand - } - } - const baseUrl = (url || "http://127.0.0.1:13420").replace(/\/+$/, ""); - return { - baseUrl, - authHeaders: () => - password ? { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` } : {}, - }; +/** The subcommand invocation: QUOTA_SUBCOMMAND overrides the whole + * command (interim fork builds, tests); the default is the deployed + * opencode binary's quota subcommand. Explicit provider ids are + * appended as positional args. */ +function subcommandArgv(extraArgs: string[]): string[] { + const override = process.env.QUOTA_SUBCOMMAND?.trim(); + const base = override ? override.split(/\s+/) : ["opencode", "quota", "--json"]; + return [...base, ...extraArgs]; } -const TRANSPORT_ERROR_CODES = new Set([ - "ECONNREFUSED", - "ECONNRESET", - "ECONNABORTED", - "ENOTFOUND", - "EAI_AGAIN", - "ETIMEDOUT", - "EPIPE", -]); +/** Run the subcommand once. A non-zero exit WITH stdout is a + * per-provider failure signal, not an exec failure — the results on + * stdout stand. An exec failure (spawn error, timeout, no stdout) + * rejects. */ +function execSubcommand(argv: string[]): Promise { + return new Promise((resolve, reject) => { + execFile( + argv[0], + argv.slice(1), + { maxBuffer: 64 * 1024 * 1024, timeout: subcommandTimeoutMs, env: process.env }, + (err, stdout) => { + if (err && !stdout) { + reject(err); + return; + } + resolve(stdout ?? ""); + }, + ); + }); +} -/** - * Whether a credential read failed because the OpenCode server could - * not be reached at all, as opposed to the server answering and - * refusing. Ported from the quota CLI's commands-quota.js - * isTransportFailure (#2490): the OpenCode client marks its own - * transport failures with `reason: 'Transport'` (an HTTP answer is a - * different reason and stays authoritative); the walk down the cause - * chain covers the fetch and socket errors underneath. - */ -function isTransportFailure(error: unknown): boolean { - let current: unknown = error; - for (let depth = 0; current && depth < 8; depth += 1) { - const rec = current as { reason?: unknown; code?: unknown; message?: unknown; cause?: unknown }; - if (rec.reason === "Transport") return true; - if (typeof rec.code === "string" && TRANSPORT_ERROR_CODES.has(rec.code)) return true; - const message = typeof rec.message === "string" ? rec.message : ""; - if (message.startsWith("Transport:") || message === "fetch failed") return true; - current = rec.cause; +/** Parse one subcommand run's stdout into provider results. The + * contract (opencode#1718): --json emits an array of result objects, + * one per credential instance; an object keyed by providerId is + * also accepted. Anything else is a tick-failing defect — a silent + * empty tick is exactly what the single-source rule exists to + * prevent. */ +function parseSubcommandResults(stdout: string, label: string): QuotaProviderResult[] { + let parsed: unknown; + try { + parsed = JSON.parse(stdout); + } catch { + throw new Error(`opencode quota (${label}): unparseable output`); } - return false; + const arr = Array.isArray(parsed) + ? parsed + : typeof parsed === "object" && parsed !== null + ? Object.values(parsed as Record) + : null; + if (!arr) throw new Error(`opencode quota (${label}): output is not a result set`); + const results = arr.filter( + (r): r is QuotaProviderResult => + typeof r === "object" && r !== null && typeof (r as QuotaProviderResult).providerId === "string", + ); + if (results.length === 0) throw new Error(`opencode quota (${label}): no provider results`); + return results; } /** - * The server-first credential source wired into the quota module: - * the OpenCode server answers (GET /api/credential via the module's - * openCodeCredentialSource); the direct local-store read stands - * behind it ONLY for transport failures — the quota CLI's - * createFallbackCredentialSource shape (#2490), kept here so the - * gatherer does not import CLI plumbing. The first transport failure - * switches every later read this tick to the local store (a server - * that is down does not come back mid-tick) and is logged once, so a - * tick's provenance is never ambiguous. This is the #1251 resilience - * contract: server-down degrades to the store read, and if the store - * also fails the error propagates to failTick — the tick fails - * loudly and last-known rows stand (#1268), never zeros. + * THE tick's only quota read: the subcommand's configured listing, + * plus the explicit ask for the route-disabled live pools + * (EXPLICIT_ASK_PROVIDERS). Explicit results replace listing results + * for the same provider + credential. Results are grouped into one + * run per provider id, mapped through the unchanged quota-gather-map + * contract. */ -function createServerFirstCredentialSource( - primary: CredentialSourceLike, - fallback: CredentialSourceLike, -): CredentialSourceLike { - let fellBack = false; - const source: CredentialSourceLike = { - usesLocalStore: () => primary.usesLocalStore?.() ?? false, - getLaunchEnvironment: () => primary.getLaunchEnvironment?.() ?? null, - list: async () => { - if (fellBack) return fallback.list(); - try { - return await primary.list(); - } catch (error) { - if (!isTransportFailure(error)) throw error; - fellBack = true; - console.log( - "quota-gatherer: OpenCode server unreachable — credentials from the local OpenCode store this tick (transport fallback)", - ); - try { - return await fallback.list(); - } catch (fallbackError) { - const reason = error instanceof Error ? error.message : "OpenCode server unreachable"; - const storeReason = - fallbackError instanceof Error ? fallbackError.message : "local credential store unreadable"; - throw new Error(`${reason}; the local credential store could not be read either (${storeReason})`); - } - } - }, - }; - if (primary.create) source.create = (input) => primary.create!(input); - if (primary.remove) source.remove = (credentialID) => primary.remove!(credentialID); - if (primary.listEnvironmentKeys) source.listEnvironmentKeys = () => primary.listEnvironmentKeys!(); - return source; +async function collectRuns(tickAt: string): Promise { + const listingT0 = Date.now(); + const listingStdout = await execSubcommand(subcommandArgv([])); + const listing = parseSubcommandResults(listingStdout, "listing"); + const listingMs = Date.now() - listingT0; + + const askT0 = Date.now(); + const askStdout = await execSubcommand(subcommandArgv(EXPLICIT_ASK_PROVIDERS)); + const asked = parseSubcommandResults(askStdout, "explicit ask"); + const askMs = Date.now() - askT0; + + const keyOf = (r: QuotaProviderResult) => `${r.providerId} ${r.credentialId ?? ""}`; + const byKey = new Map(); + for (const r of listing) byKey.set(keyOf(r), { result: r, ms: listingMs }); + for (const r of asked) byKey.set(keyOf(r), { result: r, ms: askMs }); + + const byProvider = new Map(); + for (const { result, ms } of byKey.values()) { + const group = byProvider.get(result.providerId) ?? { results: [], ms }; + group.results.push(result); + group.ms = Math.max(group.ms, ms); + byProvider.set(result.providerId, group); + } + + const runs: ProviderRun[] = []; + for (const [id, group] of byProvider) { + const t0 = Date.now() - group.ms; + const entries = group.results.map((r) => entryFromResult(r, tickAt)); + let run = finalizeRun(id, entries, t0); + if (id === "command-code") run = keepCommandCodePlanWindows(run); + runs.push(run); + } + return runs; } -async function main(): Promise { - const started = Date.now(); - const tickAt = new Date(started).toISOString(); +const SUPABASE_TOKEN_NAME = "supabase.muse-dev2.access-token-muse-cli"; +const SUPABASE_REF_NAME = "supabase.muse-dev2.project-ref"; + +function sqlString(v: string): string { + return `'${v.replace(/'/g, "''")}'`; +} - let quota: QuotaModule; +/** + * The Supabase sink: one agent.quota_records row per pool entry that + * carries a reading (at least one window), in the shape the retired + * "pm-pool" writer used (payload = {id, ts, plan, state, source, + * windows[{name, percent, resetsAt}], provider}), with source = + * GATHERER_SOURCE naming the single source. Runs AFTER + * quota-current.json is written; fail-soft by contract (a sink + * failure is logged, never fails the tick, never zeroes a row). + */ +async function writeSupabaseRecords(entries: QuotaCurrentEntry[], tickAt: string): Promise { + if (process.env.SUPABASE_QUOTA_SINK === "0") return; try { - const auth = (await import(join(libDir, "opencode/auth.js"))) as unknown as AuthModule; - // Ask the OpenCode server first (GET /api/credential on the - // standing fork server); the direct store read is only the - // transport-failure fallback behind it. See - // createServerFirstCredentialSource and the header (family 1). - const server = await resolveOpenCodeServer(); - const serverSource = auth.openCodeCredentialSource({ - buildOpenCodeUrl: (requestPath?: string) => `${server.baseUrl}${requestPath ?? ""}`, - getOpenCodeAuthHeaders: server.authHeaders, + const token = await resolveBwsSecret(SUPABASE_TOKEN_NAME); + const ref = await resolveBwsSecret(SUPABASE_REF_NAME); + if (!token || !ref) { + console.log("quota-gatherer: supabase sink skipped — token/ref unresolved by name"); + return; + } + const readable = entries.filter((e) => e.windows.length > 0); + if (!readable.length) { + console.log("quota-gatherer: supabase sink — no entries with windows this tick"); + return; + } + const values = readable.map((e) => { + const weekly = e.windows.find((w) => w.name === "weekly"); + const payload = { + id: e.serviceId, + ts: e.measuredAt, + plan: e.planLabel ?? null, + state: e.state, + source: GATHERER_SOURCE, + windows: e.windows.map((w) => ({ name: w.name, percent: w.pctOfLimit, resetsAt: w.resetsAt })), + provider: e.providerName, + }; + const id = `${e.serviceId}:${tickAt}`; + return `(${sqlString(id)}, ${sqlString(GATHERER_SOURCE)}, ${sqlString(e.measuredAt)}, ${ + e.planLabel ? sqlString(e.planLabel) : "NULL" + }, ${weekly?.pctOfLimit ?? "NULL"}, ${weekly?.resetsAt ? sqlString(weekly.resetsAt) : "NULL"}, ${sqlString( + JSON.stringify(payload), + )}::jsonb)`; }); - auth.configureOpenCodeCredentials( - createServerFirstCredentialSource(serverSource, auth.openCodeLocalCredentialSource({ dbPath })), - ); - quota = (await import(join(libDir, "quota/index.js"))) as unknown as QuotaModule; + const query = + "insert into agent.quota_records (id, source, ts, plan, weekly_percent, resets, payload) values " + + values.join(", ") + + " on conflict (id) do nothing;"; + const res = await fetch(`https://api.supabase.com/v1/projects/${ref}/database/query`, { + method: "POST", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ query }), + }); + if (!res.ok) { + console.log(`quota-gatherer: supabase sink failed-soft: HTTP ${res.status}`); + return; + } + console.log(`quota-gatherer: supabase sink wrote ${readable.length} quota_records rows (source=${GATHERER_SOURCE})`); } catch (err) { - return failTick(tickAt, started, `quota module import failed: ${err instanceof Error ? err.message : String(err)}`); + console.log( + `quota-gatherer: supabase sink failed-soft: ${safeProviderReason(err instanceof Error ? err.message : String(err))}`, + ); } +} - // Family 6: renew the OpenDesign session before its read. +async function main(): Promise { + const started = Date.now(); + const tickAt = new Date(started).toISOString(); + + // The OpenDesign keeper runs first: a session renewed this tick is + // the one the subcommand's OpenDesign read uses. await runOpenDesignRenewal(tickAt); - let providerIds: string[]; + // The single source: every reading this tick comes from the + // opencode quota subcommand. A failure to run it, or output that + // cannot be parsed, fails the tick loudly and the previous + // quota-current.json stands. + let runs: ProviderRun[]; try { - providerIds = await quota.listConfiguredQuotaProviders(); + runs = await collectRuns(tickAt); } catch (err) { return failTick( tickAt, started, - `credential store read failed: ${err instanceof Error ? err.message : String(err)}`, + `opencode quota subcommand failed: ${err instanceof Error ? err.message : String(err)}`, ); } - // #1268: bounded provider concurrency (default 6, matching the - // OpenChamber CLI) instead of one Promise.all over the catalogue. - const runs = await mapWithConcurrency(providerIds, gatherConcurrency, (id) => runProvider(quota, id, tickAt)); - for (const fx of FILE_EXPORTS) runs.push(await runFileExport(fx)); - runs.push(await runTurso(tickAt)); - - // #1159: register the native ClinePass adapter. Exactly one - // cline-pass run survives: a fresh module row wins and the native - // adapter is never even started (no duplicate credential resolve - // or dashboard fetch that tick); otherwise the native run - // replaces the module's errored run so #1268 retention below - // applies to one run only. - const clineIdx = runs.findIndex((r) => r.id === CLINEPASS_PROVIDER_ID); - if (clineIdx !== -1 && runs[clineIdx].entries.some((e) => e.state === "ok")) { - console.log("quota-gatherer: cline-pass ok via openchamber module - native adapter run skipped"); - } else { - const clineRun = await runClinePass(tickAt); - if (clineRun) { - if (clineIdx === -1) runs.push(clineRun); - else runs[clineIdx] = clineRun; - } - } - - // 2026-11-03: the Meta module ask (see runMetaModule). Exactly one - // meta run survives, mirroring the ClinePass discipline above: a - // fresh listed run (the door re-enabled someday) wins and the ask - // is skipped; otherwise the ask replaces the listed run, or is - // appended when the listing never named meta. - const metaIdx = runs.findIndex((r) => r.id === "meta"); - if (metaIdx !== -1 && runs[metaIdx].entries.some((e) => e.state === "ok")) { - console.log("quota-gatherer: meta ok via provider listing - explicit module ask skipped"); - } else { - const metaRun = await runMetaModule(quota, tickAt); - if (metaIdx === -1) runs.push(metaRun); - else runs[metaIdx] = metaRun; - } - - // Command Code: exactly one run survives, mirroring Meta above. - // A listed run with a measured plan window wins (balance windows - // stripped); otherwise the explicit ask replaces or appends it, - // so a routing-disabled provider still gets its plan read. - const ccIdx = runs.findIndex((r) => r.id === "command-code"); - if (ccIdx !== -1 && runs[ccIdx].entries.some((e) => e.state === "ok" && entryHasMeasuredWindow(e))) { - keepCommandCodePlanWindows(runs[ccIdx]); - console.log("quota-gatherer: command-code ok via provider listing - explicit module ask skipped"); - } else { - const ccRun = await runCommandCodeModule(quota, tickAt); - if (ccIdx === -1) runs.push(ccRun); - else runs[ccIdx] = ccRun; - } - - // The Claude auth source exposes an expiry timestamp. Persist only that - // metadata and its source class, never the credential object or values. - const claudeRun = runs.find((run) => run.id === "claude"); - if (claudeRun?.entries.length) { - const metadata = await claudeExpiryMetadata(); - if (metadata) { - for (const entry of claudeRun.entries) { - if (metadata.expiresAt) entry.credentialExpiresAt = metadata.expiresAt; - if (metadata.source) entry.credentialSource = metadata.source; - } - } - } - // #1268: an errored run with no ok row of its own retains the // provider's last-known ok entries from the previous tick instead // of replacing them with fresh error rows (or with nothing, on a @@ -1048,6 +648,11 @@ async function main(): Promise { console.error(`quota-gatherer: state write failed (${err}); next tick may re-append`), ); } + + // Supabase: the same readings land in agent.quota_records on the + // same tick (Drew's regular-basis rule). Fail-soft, after the file. + await writeSupabaseRecords(entries, tickAt); + const okCount = runs.filter((r) => r.state === "ok").length; console.log( `quota-gatherer: tick ${tickAt} providers=${runs.length} ok=${okCount} records=+${fresh.length} durationMs=${heartbeat.durationMs} -> ${currentPath}`, diff --git a/pm-stack/aggregator/src/quota-gather-map.ts b/pm-stack/aggregator/src/quota-gather-map.ts index 5540bb724..abf41e5fe 100644 --- a/pm-stack/aggregator/src/quota-gather-map.ts +++ b/pm-stack/aggregator/src/quota-gather-map.ts @@ -107,7 +107,7 @@ export function serviceIdFor(providerId: string): string { } } -export const GATHERER_SOURCE = "openchamber-quota"; +export const GATHERER_SOURCE = "opencode-quota"; /** * Split pool service ids the gatherer emits alongside the combined rows From cad503115064238cd524ee2c5753dd6e6fafcbfc Mon Sep 17 00:00:00 2001 From: "developing-today[bot]" <5454422+developing-today[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:13:45 -0600 Subject: [PATCH 2/6] quota: supabase sink conflict target matches quota_records PK (source, id, ts) --- pm-stack/aggregator/scripts/gather-quota.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pm-stack/aggregator/scripts/gather-quota.ts b/pm-stack/aggregator/scripts/gather-quota.ts index 27ab47e2b..8633f4539 100755 --- a/pm-stack/aggregator/scripts/gather-quota.ts +++ b/pm-stack/aggregator/scripts/gather-quota.ts @@ -529,7 +529,7 @@ async function writeSupabaseRecords(entries: QuotaCurrentEntry[], tickAt: string const query = "insert into agent.quota_records (id, source, ts, plan, weekly_percent, resets, payload) values " + values.join(", ") + - " on conflict (id) do nothing;"; + " on conflict (source, id, ts) do nothing;"; const res = await fetch(`https://api.supabase.com/v1/projects/${ref}/database/query`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, From 9dadf77c4f72cc8731df3deab343ce743aec863a Mon Sep 17 00:00:00 2001 From: "developing-today[bot]" <5454422+developing-today[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:32:28 -0600 Subject: [PATCH 3/6] quota: explicit ask for deepseek until the subcommand listing includes it (opencode#1729) --- pm-stack/aggregator/scripts/gather-quota.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pm-stack/aggregator/scripts/gather-quota.ts b/pm-stack/aggregator/scripts/gather-quota.ts index 8633f4539..043144686 100755 --- a/pm-stack/aggregator/scripts/gather-quota.ts +++ b/pm-stack/aggregator/scripts/gather-quota.ts @@ -144,8 +144,11 @@ const OBSERVED_BY = "quota-gatherer"; * kept, flipped off), so a listing can omit them even though their * credentials and plans are live pools. A named ask always fetches. * (Meta: route-only pool, #6079. Command Code: disabled door, live - * plan — its read is the PLAN read; see keepCommandCodePlanWindows.) */ -const EXPLICIT_ASK_PROVIDERS = ["meta", "command-code"]; + * plan — its read is the PLAN read; see keepCommandCodePlanWindows. + * DeepSeek: named ask resolves it but the subcommand's listing + * currently omits it — opencode#1729; remove it here when the + * listing and the named ask agree on the configured set.) */ +const EXPLICIT_ASK_PROVIDERS = ["meta", "command-code", "deepseek"]; async function writeAtomic(path: string, text: string): Promise { await mkdir(dirname(path), { recursive: true }); From f385dcde7a5f6bc2b9f1409826f691c0bc4a2925 Mon Sep 17 00:00:00 2001 From: "developing-today[bot]" <5454422+developing-today[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:33:40 -0600 Subject: [PATCH 4/6] quota: explicit ask for mistral until the subcommand cap fits its CDP read (opencode#1729) --- pm-stack/aggregator/scripts/gather-quota.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pm-stack/aggregator/scripts/gather-quota.ts b/pm-stack/aggregator/scripts/gather-quota.ts index 043144686..b835972ba 100755 --- a/pm-stack/aggregator/scripts/gather-quota.ts +++ b/pm-stack/aggregator/scripts/gather-quota.ts @@ -147,8 +147,13 @@ const OBSERVED_BY = "quota-gatherer"; * plan — its read is the PLAN read; see keepCommandCodePlanWindows. * DeepSeek: named ask resolves it but the subcommand's listing * currently omits it — opencode#1729; remove it here when the - * listing and the named ask agree on the configured set.) */ -const EXPLICIT_ASK_PROVIDERS = ["meta", "command-code", "deepseek"]; + * listing and the named ask agree on the configured set. + * Mistral: its console budget read is CDP-backed and exceeds the + * subcommand's 30s per-provider cap under the full listing's + * concurrency (opencode#1729); the small explicit ask gives it + * room, and its result replaces the listing's timed-out one. + * Remove it here when the cap is raised.) */ +const EXPLICIT_ASK_PROVIDERS = ["meta", "command-code", "deepseek", "mistral"]; async function writeAtomic(path: string, text: string): Promise { await mkdir(dirname(path), { recursive: true }); From 1d71faddd3d7652ef4fe82f3fcd4d4cdcc302915 Mon Sep 17 00:00:00 2001 From: "developing-today[bot]" <5454422+developing-today[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:35:50 -0600 Subject: [PATCH 5/6] quota: per-provider group choice between listing and explicit ask (no double-published pools) --- pm-stack/aggregator/scripts/gather-quota.ts | 44 ++++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/pm-stack/aggregator/scripts/gather-quota.ts b/pm-stack/aggregator/scripts/gather-quota.ts index b835972ba..ab17ccef1 100755 --- a/pm-stack/aggregator/scripts/gather-quota.ts +++ b/pm-stack/aggregator/scripts/gather-quota.ts @@ -462,17 +462,41 @@ async function collectRuns(tickAt: string): Promise { const asked = parseSubcommandResults(askStdout, "explicit ask"); const askMs = Date.now() - askT0; - const keyOf = (r: QuotaProviderResult) => `${r.providerId} ${r.credentialId ?? ""}`; - const byKey = new Map(); - for (const r of listing) byKey.set(keyOf(r), { result: r, ms: listingMs }); - for (const r of asked) byKey.set(keyOf(r), { result: r, ms: askMs }); - + // Per-provider group choice, the pre-rewire discipline generalized: + // a listing group with an ok row wins (the ask is not consulted for + // it); otherwise a fresher ask group with an ok row replaces it; + // otherwise the ask group (fresher error) stands over the listing's. + // The two invocations key credentials differently (the listing + // enumerates stored credentials; a named ask may answer with a + // credential-less result), so merging per-credential would publish + // the same pool twice — once as each invocation saw it. + const groupByProvider = (results: QuotaProviderResult[], ms: number) => { + const groups = new Map(); + for (const result of results) { + const group = groups.get(result.providerId) ?? { results: [], ms }; + group.results.push(result); + groups.set(result.providerId, group); + } + return groups; + }; + const listingGroups = groupByProvider(listing, listingMs); + const askGroups = groupByProvider(asked, askMs); const byProvider = new Map(); - for (const { result, ms } of byKey.values()) { - const group = byProvider.get(result.providerId) ?? { results: [], ms }; - group.results.push(result); - group.ms = Math.max(group.ms, ms); - byProvider.set(result.providerId, group); + for (const [id, group] of listingGroups) byProvider.set(id, group); + for (const [id, askGroup] of askGroups) { + const listed = byProvider.get(id); + if (!listed) { + byProvider.set(id, askGroup); + continue; + } + const listedOk = listed.results.some((r) => r.ok); + const askOk = askGroup.results.some((r) => r.ok); + if (!listedOk) byProvider.set(id, askGroup); + else if (!askOk) { + console.log(`quota-gatherer: ${id} ok via listing — explicit ask's error row dropped`); + } else { + byProvider.set(id, askGroup); + } } const runs: ProviderRun[] = []; From 2ce613d05d7ff042e717af3acf9e7fb0cc0c74f2 Mon Sep 17 00:00:00 2001 From: "developing-today[bot]" <5454422+developing-today[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:36:06 -0600 Subject: [PATCH 6/6] quota: listing group stands when both listing and ask are ok --- pm-stack/aggregator/scripts/gather-quota.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pm-stack/aggregator/scripts/gather-quota.ts b/pm-stack/aggregator/scripts/gather-quota.ts index ab17ccef1..69e50df42 100755 --- a/pm-stack/aggregator/scripts/gather-quota.ts +++ b/pm-stack/aggregator/scripts/gather-quota.ts @@ -494,9 +494,10 @@ async function collectRuns(tickAt: string): Promise { if (!listedOk) byProvider.set(id, askGroup); else if (!askOk) { console.log(`quota-gatherer: ${id} ok via listing — explicit ask's error row dropped`); - } else { - byProvider.set(id, askGroup); } + // Both ok: the listing group stands — it is the canonical, + // credential-keyed enumeration; the ask exists to rescue a + // missing or errored listing group, not to override a good one. } const runs: ProviderRun[] = [];