@tokenfactory/acc-runner 0.40.24 → 0.40.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * CHAT-HYGIENE AC2 — the companion/fleet DB-POLL FALLBACK
3
+ * for chat turn discovery.
4
+ *
5
+ * WHY THIS EXISTS. A companion (chat-companion.ts) / fleet lane
6
+ * (watch-chat/fleet-chat-lane.ts) discovers which conversations to serve from
7
+ * TWO Realtime-backed signals: the browser's `conversation_active` announce on
8
+ * the `identity:<user>` topic (companion.ts createConversationListener, FU-4)
9
+ * and the `chat:<conversation_id>` turn broadcasts. Both ride Supabase Realtime,
10
+ * which is BEST-EFFORT (no ack, no retry). During a Realtime DELIVERY BLACKOUT —
11
+ * a dropped socket, a Realtime outage, a missed announce — a freshly queued turn
12
+ * is never surfaced, so the companion never claims it and the operator's chat
13
+ * hangs with no reply (the 2026-07-21/22 incident shape).
14
+ *
15
+ * The claim PUMP already runs on its own interval, but it only claims across the
16
+ * conversations already in the served set — a conversation the blackout hid from
17
+ * discovery is never pumped. This module closes that gap with a discovery path
18
+ * that does NOT depend on Realtime at all: a heartbeat poller that queries the DB
19
+ * directly for the bound identity's OWNED conversations carrying a `queued` turn,
20
+ * and feeds them to a sink (in production: fold into the owner-affinity source +
21
+ * nudge the pump). It is the DB twin of the Realtime discovery signals — the
22
+ * safety net that guarantees a queued turn is served within one poll interval
23
+ * even with Realtime entirely dead.
24
+ *
25
+ * Pure primitive: every I/O seam (the DB query, the on-due sink, the clock) is
26
+ * injected, so the whole fallback is exercised deterministically
27
+ * (tests/unit/chat-hygiene, tests/e2e/chat-hygiene) with no network and no
28
+ * Supabase Realtime. Wiring it into `acc-runner companion` / `watch` is a
29
+ * follow-up (mirrors the FLEET-SERVE watch-chat primitives-then-wire split); the
30
+ * primitive + chaos proof land here.
31
+ */
32
+ /** Env var that opts OUT of the DB-poll fallback (default: on). */
33
+ export declare const CHAT_POLL_ENABLED_ENV = "ACC_RUNNER_CHAT_POLL";
34
+ /** Env var carrying the DB-poll heartbeat cadence (ms). */
35
+ export declare const CHAT_POLL_INTERVAL_MS_ENV = "ACC_RUNNER_CHAT_POLL_MS";
36
+ /**
37
+ * Default poll cadence (ms). Sits in the same ~15-30s band as the companion's
38
+ * FU-10 rescan backstop: short enough that a Realtime-lost queued turn is
39
+ * served within a barely-perceptible delay, long enough that the bounded
40
+ * owner-affinity scan stays trivially cheap.
41
+ */
42
+ export declare const DEFAULT_CHAT_POLL_INTERVAL_MS = 15000;
43
+ /** Clamp bounds so a fat-fingered env can't hammer the DB or effectively disable the net. */
44
+ export declare const MIN_CHAT_POLL_INTERVAL_MS = 2000;
45
+ export declare const MAX_CHAT_POLL_INTERVAL_MS = 300000;
46
+ /** The queued-turn status the fallback polls for (the only one the pump can act on). */
47
+ export declare const CHAT_POLL_QUEUED_STATUS: "queued";
48
+ /** Is the DB-poll fallback enabled? Disabled by `ACC_RUNNER_CHAT_POLL=0`. */
49
+ export declare function chatPollEnabled(env?: NodeJS.ProcessEnv): boolean;
50
+ /**
51
+ * Resolve the poll cadence (ms). Blank/absent/non-numeric ⇒ default; a numeric
52
+ * value is clamped to [MIN, MAX] so a typo can neither DDoS the DB nor make the
53
+ * fallback effectively never fire.
54
+ */
55
+ export declare function resolveChatPollIntervalMs(raw: string | undefined | null): number;
56
+ /**
57
+ * Minimal structural shape of the Supabase client the poll query needs — the
58
+ * PostgREST query-builder chain used by companion.ts's backfill. A real
59
+ * RunnerSupabaseClient satisfies it; tests inject a recording fake.
60
+ */
61
+ export interface ChatPollClient {
62
+ from(table: string): any;
63
+ }
64
+ export interface PollQueuedConversationsOpts {
65
+ /** Best-effort log sink. */
66
+ onLog?: (line: string) => void;
67
+ /** Override the queued-turn status filter (defaults to CHAT_POLL_QUEUED_STATUS). */
68
+ status?: string;
69
+ }
70
+ /**
71
+ * Query the DB for the conversations OWNED BY the bound user that currently have
72
+ * a `queued` chat turn — the Realtime-independent discovery read at the heart of
73
+ * the fallback.
74
+ *
75
+ * Owner-affinity: acc.chat_turns' SELECT policy is ORG-scoped (0254), so a
76
+ * candidate conversation_id may belong to a fellow org member. We re-confirm
77
+ * ownership against acc.chat_conversations (filtered to the bound user) exactly
78
+ * as backfillActiveConversations does, so a companion never surfaces — and never
79
+ * claims — another operator's conversation. Best-effort: any query error is
80
+ * logged and yields an empty result (the fallback is a safety net; it must never
81
+ * throw into the poll loop).
82
+ */
83
+ export declare function pollQueuedConversations(client: ChatPollClient, boundUserId: string, opts?: PollQueuedConversationsOpts): Promise<string[]>;
84
+ export interface ChatTurnPollerDeps {
85
+ /**
86
+ * Discover the conversation ids that currently have queued work for the bound
87
+ * identity — Realtime-independent. Typically
88
+ * `() => pollQueuedConversations(client, boundUserId, { onLog })`.
89
+ */
90
+ poll: () => Promise<string[]>;
91
+ /**
92
+ * Act on the discovered conversations: fold them into the served set and nudge
93
+ * the claim pump so their queued turns drain this interval. Best-effort — a
94
+ * throw is caught and logged; the next tick retries. Omit ⇒ discovery-only
95
+ * (the poller just reports what it found, for observability/tests).
96
+ */
97
+ onDue?: (conversationIds: string[]) => Promise<void> | void;
98
+ /** Poll cadence (ms). Default DEFAULT_CHAT_POLL_INTERVAL_MS; clamped to a sane floor. */
99
+ intervalMs?: number;
100
+ /** Best-effort log sink. */
101
+ onLog?: (line: string) => void;
102
+ }
103
+ /** The result of one poll pass, for observability + tests. */
104
+ export interface ChatPollResult {
105
+ /** Conversation ids the DB reported as carrying queued work this pass. */
106
+ due: string[];
107
+ /** True when the onDue sink ran without throwing (or was absent). */
108
+ handled: boolean;
109
+ }
110
+ /**
111
+ * The DB-poll fallback loop. `pollOnce()` runs exactly one Realtime-independent
112
+ * discovery pass (query → onDue); `start()` schedules it on the heartbeat
113
+ * interval; `stop()` halts the loop. Single-flight: a slow pass can never stack
114
+ * on the next tick — an overlapping tick is coalesced. The whole loop is
115
+ * best-effort: a query or sink throw is caught + logged and the loop rides on,
116
+ * so the fallback can never crash the companion process.
117
+ */
118
+ export declare class ChatTurnPoller {
119
+ private readonly deps;
120
+ private timer;
121
+ private stopped;
122
+ private polling;
123
+ private readonly intervalMs;
124
+ constructor(deps: ChatTurnPollerDeps);
125
+ /** Run one discovery pass. Best-effort + single-flight; never throws. */
126
+ pollOnce(): Promise<ChatPollResult>;
127
+ /** Start the heartbeat poll loop (does not block; timer is unref'd). */
128
+ start(): void;
129
+ /** Stop the loop. Idempotent. */
130
+ stop(): void;
131
+ }
132
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/chat-poll/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,yBAAyB,CAAC;AAC5D,2DAA2D;AAC3D,eAAO,MAAM,yBAAyB,4BAA4B,CAAC;AACnE;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,QAAS,CAAC;AACpD,6FAA6F;AAC7F,eAAO,MAAM,yBAAyB,OAAQ,CAAC;AAC/C,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD,wFAAwF;AACxF,eAAO,MAAM,uBAAuB,EAAG,QAAiB,CAAC;AAQzD,6EAA6E;AAC7E,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE7E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAIhF;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAE7B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC;CAC1B;AAED,MAAM,WAAW,2BAA2B;IAC1C,4BAA4B;IAC5B,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,2BAAgC,GACrC,OAAO,CAAC,MAAM,EAAE,CAAC,CAoDnB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9B;;;;;OAKG;IACH,KAAK,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5D,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,0EAA0E;IAC1E,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,qBAAa,cAAc;IAMb,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAEP,IAAI,EAAE,kBAAkB;IAOrD,yEAAyE;IACnE,QAAQ,IAAI,OAAO,CAAC,cAAc,CAAC;IAmCzC,wEAAwE;IACxE,KAAK,IAAI,IAAI;IAWb,iCAAiC;IACjC,IAAI,IAAI,IAAI;CAOb"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * CHAT-HYGIENE AC2 — the companion/fleet DB-POLL FALLBACK
3
+ * for chat turn discovery.
4
+ *
5
+ * WHY THIS EXISTS. A companion (chat-companion.ts) / fleet lane
6
+ * (watch-chat/fleet-chat-lane.ts) discovers which conversations to serve from
7
+ * TWO Realtime-backed signals: the browser's `conversation_active` announce on
8
+ * the `identity:<user>` topic (companion.ts createConversationListener, FU-4)
9
+ * and the `chat:<conversation_id>` turn broadcasts. Both ride Supabase Realtime,
10
+ * which is BEST-EFFORT (no ack, no retry). During a Realtime DELIVERY BLACKOUT —
11
+ * a dropped socket, a Realtime outage, a missed announce — a freshly queued turn
12
+ * is never surfaced, so the companion never claims it and the operator's chat
13
+ * hangs with no reply (the 2026-07-21/22 incident shape).
14
+ *
15
+ * The claim PUMP already runs on its own interval, but it only claims across the
16
+ * conversations already in the served set — a conversation the blackout hid from
17
+ * discovery is never pumped. This module closes that gap with a discovery path
18
+ * that does NOT depend on Realtime at all: a heartbeat poller that queries the DB
19
+ * directly for the bound identity's OWNED conversations carrying a `queued` turn,
20
+ * and feeds them to a sink (in production: fold into the owner-affinity source +
21
+ * nudge the pump). It is the DB twin of the Realtime discovery signals — the
22
+ * safety net that guarantees a queued turn is served within one poll interval
23
+ * even with Realtime entirely dead.
24
+ *
25
+ * Pure primitive: every I/O seam (the DB query, the on-due sink, the clock) is
26
+ * injected, so the whole fallback is exercised deterministically
27
+ * (tests/unit/chat-hygiene, tests/e2e/chat-hygiene) with no network and no
28
+ * Supabase Realtime. Wiring it into `acc-runner companion` / `watch` is a
29
+ * follow-up (mirrors the FLEET-SERVE watch-chat primitives-then-wire split); the
30
+ * primitive + chaos proof land here.
31
+ */
32
+ /** Env var that opts OUT of the DB-poll fallback (default: on). */
33
+ export const CHAT_POLL_ENABLED_ENV = "ACC_RUNNER_CHAT_POLL";
34
+ /** Env var carrying the DB-poll heartbeat cadence (ms). */
35
+ export const CHAT_POLL_INTERVAL_MS_ENV = "ACC_RUNNER_CHAT_POLL_MS";
36
+ /**
37
+ * Default poll cadence (ms). Sits in the same ~15-30s band as the companion's
38
+ * FU-10 rescan backstop: short enough that a Realtime-lost queued turn is
39
+ * served within a barely-perceptible delay, long enough that the bounded
40
+ * owner-affinity scan stays trivially cheap.
41
+ */
42
+ export const DEFAULT_CHAT_POLL_INTERVAL_MS = 15_000;
43
+ /** Clamp bounds so a fat-fingered env can't hammer the DB or effectively disable the net. */
44
+ export const MIN_CHAT_POLL_INTERVAL_MS = 2_000;
45
+ export const MAX_CHAT_POLL_INTERVAL_MS = 300_000;
46
+ /** The queued-turn status the fallback polls for (the only one the pump can act on). */
47
+ export const CHAT_POLL_QUEUED_STATUS = "queued";
48
+ /** True when the value is a recognised falsy flag ("0"/"false"/"no"/"off"). */
49
+ function isFalsyFlag(raw) {
50
+ const v = (raw ?? "").trim().toLowerCase();
51
+ return v === "0" || v === "false" || v === "no" || v === "off";
52
+ }
53
+ /** Is the DB-poll fallback enabled? Disabled by `ACC_RUNNER_CHAT_POLL=0`. */
54
+ export function chatPollEnabled(env = process.env) {
55
+ return !isFalsyFlag(env[CHAT_POLL_ENABLED_ENV]);
56
+ }
57
+ /**
58
+ * Resolve the poll cadence (ms). Blank/absent/non-numeric ⇒ default; a numeric
59
+ * value is clamped to [MIN, MAX] so a typo can neither DDoS the DB nor make the
60
+ * fallback effectively never fire.
61
+ */
62
+ export function resolveChatPollIntervalMs(raw) {
63
+ const n = Number.parseInt((raw ?? "").trim(), 10);
64
+ if (!Number.isFinite(n))
65
+ return DEFAULT_CHAT_POLL_INTERVAL_MS;
66
+ return Math.min(MAX_CHAT_POLL_INTERVAL_MS, Math.max(MIN_CHAT_POLL_INTERVAL_MS, n));
67
+ }
68
+ /**
69
+ * Query the DB for the conversations OWNED BY the bound user that currently have
70
+ * a `queued` chat turn — the Realtime-independent discovery read at the heart of
71
+ * the fallback.
72
+ *
73
+ * Owner-affinity: acc.chat_turns' SELECT policy is ORG-scoped (0254), so a
74
+ * candidate conversation_id may belong to a fellow org member. We re-confirm
75
+ * ownership against acc.chat_conversations (filtered to the bound user) exactly
76
+ * as backfillActiveConversations does, so a companion never surfaces — and never
77
+ * claims — another operator's conversation. Best-effort: any query error is
78
+ * logged and yields an empty result (the fallback is a safety net; it must never
79
+ * throw into the poll loop).
80
+ */
81
+ export async function pollQueuedConversations(client, boundUserId, opts = {}) {
82
+ const status = opts.status ?? CHAT_POLL_QUEUED_STATUS;
83
+ try {
84
+ const turnsRes = await client
85
+ .from("chat_turns")
86
+ .select("conversation_id")
87
+ .eq("status", status);
88
+ if (turnsRes?.error) {
89
+ opts.onLog?.(`[acc-runner] chat-poll: chat_turns scan failed: ${turnsRes.error.message}`);
90
+ return [];
91
+ }
92
+ const turnRows = Array.isArray(turnsRes?.data) ? turnsRes.data : [];
93
+ const candidateIds = [
94
+ ...new Set(turnRows
95
+ .map((r) => {
96
+ const v = r?.conversation_id;
97
+ return typeof v === "string" ? v.trim() : "";
98
+ })
99
+ .filter((id) => id.length > 0)),
100
+ ];
101
+ if (candidateIds.length === 0)
102
+ return [];
103
+ const ownedRes = await client
104
+ .from("chat_conversations")
105
+ .select("id")
106
+ .eq("user_id", boundUserId)
107
+ .in("id", candidateIds);
108
+ if (ownedRes?.error) {
109
+ opts.onLog?.(`[acc-runner] chat-poll: ownership check failed: ${ownedRes.error.message}`);
110
+ return [];
111
+ }
112
+ const ownedRows = Array.isArray(ownedRes?.data) ? ownedRes.data : [];
113
+ return [
114
+ ...new Set(ownedRows
115
+ .map((r) => {
116
+ const v = r?.id;
117
+ return typeof v === "string" ? v.trim() : "";
118
+ })
119
+ .filter((id) => id.length > 0)),
120
+ ];
121
+ }
122
+ catch (err) {
123
+ opts.onLog?.(`[acc-runner] chat-poll query failed: ${err.message}`);
124
+ return [];
125
+ }
126
+ }
127
+ /**
128
+ * The DB-poll fallback loop. `pollOnce()` runs exactly one Realtime-independent
129
+ * discovery pass (query → onDue); `start()` schedules it on the heartbeat
130
+ * interval; `stop()` halts the loop. Single-flight: a slow pass can never stack
131
+ * on the next tick — an overlapping tick is coalesced. The whole loop is
132
+ * best-effort: a query or sink throw is caught + logged and the loop rides on,
133
+ * so the fallback can never crash the companion process.
134
+ */
135
+ export class ChatTurnPoller {
136
+ deps;
137
+ timer = null;
138
+ stopped = false;
139
+ polling = false;
140
+ intervalMs;
141
+ constructor(deps) {
142
+ this.deps = deps;
143
+ this.intervalMs = Math.max(MIN_CHAT_POLL_INTERVAL_MS, deps.intervalMs ?? DEFAULT_CHAT_POLL_INTERVAL_MS);
144
+ }
145
+ /** Run one discovery pass. Best-effort + single-flight; never throws. */
146
+ async pollOnce() {
147
+ if (this.stopped || this.polling)
148
+ return { due: [], handled: false };
149
+ this.polling = true;
150
+ try {
151
+ let due = [];
152
+ try {
153
+ due = await this.deps.poll();
154
+ }
155
+ catch (err) {
156
+ this.deps.onLog?.(`[acc-runner] chat-poll pass failed: ${err.message}`);
157
+ return { due: [], handled: false };
158
+ }
159
+ if (this.stopped)
160
+ return { due, handled: false };
161
+ if (due.length === 0 || !this.deps.onDue) {
162
+ return { due, handled: true };
163
+ }
164
+ try {
165
+ await this.deps.onDue(due);
166
+ this.deps.onLog?.(`[acc-runner] chat-poll fallback surfaced ${due.length} queued conversation(s) ` +
167
+ `(Realtime-independent)`);
168
+ return { due, handled: true };
169
+ }
170
+ catch (err) {
171
+ this.deps.onLog?.(`[acc-runner] chat-poll onDue sink failed: ${err.message}`);
172
+ return { due, handled: false };
173
+ }
174
+ }
175
+ finally {
176
+ this.polling = false;
177
+ }
178
+ }
179
+ /** Start the heartbeat poll loop (does not block; timer is unref'd). */
180
+ start() {
181
+ if (this.timer || this.stopped)
182
+ return;
183
+ this.timer = setInterval(() => {
184
+ void this.pollOnce();
185
+ }, this.intervalMs);
186
+ this.timer.unref?.();
187
+ // Kick one immediate pass so a blackout in progress at startup self-heals
188
+ // without waiting a full interval.
189
+ void this.pollOnce();
190
+ }
191
+ /** Stop the loop. Idempotent. */
192
+ stop() {
193
+ this.stopped = true;
194
+ if (this.timer) {
195
+ clearInterval(this.timer);
196
+ this.timer = null;
197
+ }
198
+ }
199
+ }
200
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/chat-poll/index.ts"],"names":[],"mappings":"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"}
package/dist/doctor.d.ts CHANGED
@@ -125,8 +125,9 @@ export interface DoctorOptions {
125
125
  /**
126
126
  * Treat advisory WARN outcomes as hard failures. Used by
127
127
  * `scripts/provision-runner.sh` and CI so a runner only reports healthy when
128
- * the environment is fully green — no missing ANTHROPIC_API_KEY, no stale
129
- * binary, no claude/version drift, no unconfirmable foreign watch process.
128
+ * the environment is fully green — subscription/BYOK serving auth resolved
129
+ * (REVIEW-SUBS), no stale binary, no claude/version drift, no unconfirmable
130
+ * foreign watch process.
130
131
  * Off by default so interactive `doctor` stays lenient about advisories.
131
132
  */
132
133
  strict?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAEL,KAAK,oBAAoB,EAUzB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAiBrB,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,4BAA4B,CAAC;AAKpC,eAAO,MAAM,aAAa,gCAAgC,CAAC;AAC3D,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,YAAY,EAAE,MAAM,CAYxD,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,YAAY,EAAE,MAAM,CAY5D,CAAC;AAoEF,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAkFD;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CACjC,KAAK,GAAE,MAAM,OAAO,CAAC,qBAAqB,CAAgD,GACzF,OAAO,CAAC,YAAY,CAAC,CAsBvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,YAAY,CAAC,CA+B9D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1C;AAED,wBAAsB,oBAAoB,CAAC,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWzF;AA0FD;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,YAAY,CAAC,CA8D3D;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAMhF;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,oBAAoB,GAAG,IAAI,GACrC,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAmOD,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC/C;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,YAAY,GAAG,IAAI,EACxB,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,YAAY,CAAC,CA+CvB;AAQD,wBAAsB,oBAAoB,CACxC,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA4BvB;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,YAAY,GAAG,IAAI,GACvB,OAAO,CAAC,YAAY,CAAC,CAevB;AAED,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAED,wBAAsB,gBAAgB,CACpC,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,YAAY,CAAC,CA6BvB;AAiGD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAuFtF;AAgBD,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAmF1F;AAkED,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;IAC5C;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAiBD,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0FhF"}
package/dist/doctor.js CHANGED
@@ -9,7 +9,7 @@ import path from "node:path";
9
9
  import { execa } from "execa";
10
10
  import chalk from "chalk";
11
11
  import { buildEnv } from "./bin-resolve.js";
12
- import { resolveAuthMode, describeAuthMode, authPrecedenceHint } from "./provider-auth.js";
12
+ import { authPrecedenceHint, servingAuthKind, describeServingAuthKind, anthropicApiKey, forwardsAmbientKey, } from "./provider-auth.js";
13
13
  import { openaiApiKey, openaiApiKeyFromKeychain, describeCodexAuthMode, } from "./provider-auth.js";
14
14
  import { codexEngineEnabled, resolveCodexSpawnMode } from "./engines/codex.js";
15
15
  import { claudeCodeEngine } from "./engines/claude-code.js";
@@ -148,29 +148,50 @@ async function checkClaude() {
148
148
  };
149
149
  }
150
150
  /**
151
- * v0.74-B (T-1780800000740201): advisory check that ANTHROPIC_API_KEY is set.
152
- * When present, spawned claude sessions authenticate via the API account,
153
- * which has no per-session cap. When absent claude falls back to the
154
- * operator's interactive OAuth session, which is subject to 429 session caps
155
- * during long autonomous runs — worth flagging but not a hard environment
156
- * failure, so this is a WARN (advisory, never fails the doctor exit code).
151
+ * Report how spawned claude sessions will authenticate, under the
152
+ * subscription-only serving policy. NO ambient API-key billing: a host
153
+ * ANTHROPIC_API_KEY is forwarded to a spawn ONLY when the serving identity is
154
+ * BYOK `api_key`; otherwise it is stripped and the session serves on the
155
+ * operator's subscription OAuth login. So a MISSING key is now the healthy
156
+ * default, not a warning — the advice is `claude login`, never "export a key".
157
157
  */
158
158
  async function checkAnthropicKey() {
159
- const mode = resolveAuthMode();
160
- if (mode === "api-key") {
161
- const hint = authPrecedenceHint();
159
+ const kind = servingAuthKind();
160
+ const keyPresent = !!anthropicApiKey();
161
+ const name = "Anthropic serving auth";
162
+ if (forwardsAmbientKey()) {
163
+ // BYOK api_key identity: the host key IS forwarded. Missing key = misconfig.
164
+ if (keyPresent) {
165
+ const hint = authPrecedenceHint();
166
+ return {
167
+ name,
168
+ ok: true,
169
+ detail: hint
170
+ ? `${describeServingAuthKind(kind)} — ${hint}`
171
+ : describeServingAuthKind(kind),
172
+ };
173
+ }
162
174
  return {
163
- name: "ANTHROPIC_API_KEY set",
164
- ok: true,
165
- detail: hint ? `${describeAuthMode(mode)} — ${hint}` : describeAuthMode(mode),
175
+ name,
176
+ ok: false,
177
+ warn: true,
178
+ detail: `${describeServingAuthKind(kind)} but no ANTHROPIC_API_KEY present`,
179
+ fix: "this runner serves a BYOK api_key identity — ensure the v9 broker delivers ANTHROPIC_API_KEY, or switch the identity to subscription serving.",
166
180
  };
167
181
  }
182
+ // Subscription-first (the desired default). A missing key is correct; a
183
+ // present-but-stripped key is noted so the operator knows it is NOT billed.
168
184
  return {
169
- name: "ANTHROPIC_API_KEY set",
170
- ok: false,
171
- warn: true,
172
- detail: describeAuthMode(mode),
173
- fix: "export ANTHROPIC_API_KEY=sk-ant-… so spawned claude sessions bill against the API account and avoid 429 session caps.",
185
+ name,
186
+ ok: true,
187
+ detail: keyPresent
188
+ ? `${describeServingAuthKind(kind)}; a host ANTHROPIC_API_KEY is present but will be STRIPPED from spawns (not billed)`
189
+ : describeServingAuthKind(kind),
190
+ ...(keyPresent
191
+ ? {
192
+ fix: "a stray ANTHROPIC_API_KEY is set but ignored under subscription-only serving; unset it to avoid confusion. Run `claude login` if the subscription session ever lapses.",
193
+ }
194
+ : {}),
174
195
  };
175
196
  }
176
197
  /**
@@ -195,7 +216,7 @@ export async function checkChatAuth(probe = () => claudeCodeChatEngine.credentia
195
216
  ok: false,
196
217
  warn: true,
197
218
  detail: `chat lane unavailable — ${result.detail}`,
198
- fix: "export ANTHROPIC_API_KEY=sk-ant-… or run an interactive `claude` login so the chat lane can serve turns.",
219
+ fix: "run an interactive `claude` login so the chat lane can serve turns on the operator's subscription (REVIEW-SUBS: ambient ANTHROPIC_API_KEY is no longer used).",
199
220
  };
200
221
  }
201
222
  const draftOnly = result.tier === "draft-only";