@indigoai-us/hq-cli 5.345.110 → 5.345.111

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.
@@ -53,6 +53,7 @@ import { containsNoReplySentinel, type ProgressPosterOptions } from "./progress.
53
53
  import { type BotLogger } from "./log.js";
54
54
  import { type CodingToolActivity, type DetectCodingToolActivityInput } from "./coding-tool-activity.js";
55
55
  import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult } from "./runtime/index.js";
56
+ import { type BotSessionScope } from "./paths.js";
56
57
  import type { VaultObjectDownloader } from "../agent-kit/attachment-download.js";
57
58
  import { type HeartbeatAgentUidSource, type HeartbeatCredsSource, type HeartbeatIdentityMismatchReporter } from "../heartbeat-identity.js";
58
59
  import { type BotState, type BotStatusFile } from "./status.js";
@@ -79,6 +80,8 @@ export declare function apiPollDelayMs(opts: {
79
80
  }): number;
80
81
  export declare const FAILURE_WINDOW_MS: number;
81
82
  export declare const FAILURE_LIMIT = 5;
83
+ /** Resumed-session failures with the model CLI's unknown error, in a row, before the session is cleared. */
84
+ export declare const RESUMED_UNKNOWN_ERROR_LIMIT = 2;
82
85
  export declare const MAX_ATTEMPTS_PER_MESSAGE = 3;
83
86
  export declare const THINKING_EMOJI = "\uD83D\uDC40";
84
87
  export declare const ROOM_THINKING_STATUS = "is thinking\u2026";
@@ -239,6 +242,13 @@ export interface BotHandle {
239
242
  * An item that never reaches a model turn gets a lane of its own.
240
243
  */
241
244
  export declare function inboxLaneKey(item: InboxItem, config: Pick<BotConfig, "ownerUid" | "agentUid" | "name">): string;
245
+ /**
246
+ * The session an owner DM turn runs in. A Create brief job the desktop app
247
+ * sent as a DM gets a fresh session of its own (one per run), so it neither
248
+ * replays the owner's conversation nor leaves its sweep in it; every other DM
249
+ * resumes the sender's session.
250
+ */
251
+ export declare function dmTurnSessionScope(item: Pick<InboxItem, "fromPersonUid" | "text">, ownerUid: string): BotSessionScope;
242
252
  export declare function preflightCreds(dir: string, config: BotConfig): string | null;
243
253
  export declare function runBot(deps: BotRunDeps): Promise<BotHandle>;
244
254
  /** True for a setup note the desktop app sent, not words the person typed. */
@@ -72,6 +72,7 @@ import { setupKeptHereNote, setupOfferAnsweredInWordsNote, setupToolOfferAnswer,
72
72
  import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled, onPath } from "./runtime/claude.js";
73
73
  import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
74
74
  import { botAttachmentsDir, dmSessionScope, isDmSessionScope } from "./paths.js";
75
+ import { briefJobRunId, briefJobSessionScope, isBriefJobSessionScope } from "./brief-job.js";
75
76
  import { appendDmHistory, dmHistoryHasModelThread, markDmHistoryModelTurn, readDmHistory, withRecentDmHistory } from "./dm-history.js";
76
77
  import { attachmentTurnBlock, downloadTurnAttachments } from "./attachments.js";
77
78
  import { inboxEntryAttachments } from "../agent-kit/inbox-state.js";
@@ -122,6 +123,10 @@ export function apiPollDelayMs(opts) {
122
123
  }
123
124
  export const FAILURE_WINDOW_MS = 10 * 60 * 1000;
124
125
  export const FAILURE_LIMIT = 5;
126
+ /** Resumed-session failures with the model CLI's unknown error, in a row, before the session is cleared. */
127
+ export const RESUMED_UNKNOWN_ERROR_LIMIT = 2;
128
+ /** The model CLI's own message, exactly; "Unexpected token" in a stack trace is not it. */
129
+ const RESUMED_UNKNOWN_ERROR = /an unknown error occurred \(unexpected\)/i;
125
130
  export const MAX_ATTEMPTS_PER_MESSAGE = 3;
126
131
  export const THINKING_EMOJI = "👀";
127
132
  export const ROOM_THINKING_STATUS = "is thinking…";
@@ -238,12 +243,23 @@ export const RESTARTED_REASON = "I was restarted while I was working on it";
238
243
  */
239
244
  export function inboxLaneKey(item, config) {
240
245
  if (isDmItem(item))
241
- return dmSessionScope(item.fromPersonUid ?? "", config.ownerUid);
246
+ return dmTurnSessionScope(item, config.ownerUid);
242
247
  const target = resolveReplyTarget(item, config);
243
248
  if (target?.kind === "room" || target?.kind === "job")
244
249
  return target.sessionScope;
245
250
  return `item:${item.messageId}`;
246
251
  }
252
+ /**
253
+ * The session an owner DM turn runs in. A Create brief job the desktop app
254
+ * sent as a DM gets a fresh session of its own (one per run), so it neither
255
+ * replays the owner's conversation nor leaves its sweep in it; every other DM
256
+ * resumes the sender's session.
257
+ */
258
+ export function dmTurnSessionScope(item, ownerUid) {
259
+ const sender = item.fromPersonUid ?? "";
260
+ const runId = sender === ownerUid ? briefJobRunId(item.text) : null;
261
+ return runId ? briefJobSessionScope(runId) : dmSessionScope(sender, ownerUid);
262
+ }
247
263
  class FailureWindow {
248
264
  windowMs;
249
265
  limit;
@@ -942,6 +958,7 @@ export async function runBot(deps) {
942
958
  markSignInWorks();
943
959
  // A good turn: the last recorded failure is no longer the bot's state.
944
960
  setStatus({ recentFailures: failures.count(now().getTime()), lastError: undefined });
961
+ resumedUnknownErrors.delete(sessionScope);
945
962
  if (turn.modelFallback) {
946
963
  const { from, to } = turn.modelFallback;
947
964
  log("warn", `${runtime.id} on this computer is too old for model ${from || "(its configured default)"}; this turn ran on ${to} instead. Update ${runtime.id} to use ${from || "that model"} again`);
@@ -951,7 +968,8 @@ export async function runBot(deps) {
951
968
  setStatus({ runtimeModelFallback: null });
952
969
  }
953
970
  log("info", `model turn done in ${Math.round((now().getTime() - tTurnStart) / 1000)}s (${Math.round((tTurnStart - tReceived) / 1000)}s before it)`);
954
- if (turn.sessionId) {
971
+ // A brief job's session is one run long; nothing resumes it.
972
+ if (turn.sessionId && !isBriefJobSessionScope(sessionScope)) {
955
973
  writeBotSession(dir, {
956
974
  v: 1,
957
975
  runtime: config.runtime,
@@ -983,16 +1001,37 @@ export async function runBot(deps) {
983
1001
  }
984
1002
  };
985
1003
  /** Failure bookkeeping shared by every model turn: window, health, status, log, stale session. */
1004
+ /** Consecutive resumed-session failures with the model CLI's unknown error, per scope. */
1005
+ const resumedUnknownErrors = new Map();
986
1006
  const recordTurnFailure = (err, sessionScope) => {
987
1007
  const excerpt = err instanceof RuntimeError ? err.stderrExcerpt : err instanceof Error ? err.message : String(err);
988
1008
  const count = failures.record(now().getTime());
989
1009
  modelHealth = count >= FAILURE_LIMIT ? "failed" : "degraded";
990
1010
  setStatus({ restarts: (readStatus()?.restarts ?? 0) + 1, recentFailures: count, lastError: excerpt.slice(0, 400) });
991
1011
  log("error", `model turn failed (${count}/${FAILURE_LIMIT} in window, ${classifyRuntimeError(err)}): ${excerpt}`);
992
- // A resumed session that no longer exists must not poison every turn.
1012
+ // A resumed session that no longer exists must not poison every turn: the
1013
+ // next turn starts fresh with the recent DM history prepended. The model
1014
+ // CLI's "An unknown error occurred (Unexpected)" (every turn of a
1015
+ // long-lived process, 2026-10-12) is not a session error on its face, so a
1016
+ // single one never clears anything; the session is cleared only once that
1017
+ // error repeats on the same resumed session (RESUMED_UNKNOWN_ERROR_LIMIT
1018
+ // in a row, the count reset by any good turn in that scope).
993
1019
  const resumed = Boolean(err?.resumedSession);
994
- if (resumed && /session|resume|not found/i.test(excerpt))
1020
+ if (resumed && /session|resume|not found/i.test(excerpt)) {
995
1021
  clearBotSession(dir, sessionScope);
1022
+ resumedUnknownErrors.delete(sessionScope);
1023
+ }
1024
+ else if (resumed && RESUMED_UNKNOWN_ERROR.test(excerpt)) {
1025
+ const streak = (resumedUnknownErrors.get(sessionScope) ?? 0) + 1;
1026
+ if (streak >= RESUMED_UNKNOWN_ERROR_LIMIT) {
1027
+ log("warn", `resumed session in ${sessionScope} failed ${streak} times in a row with the model CLI's unknown error; starting fresh next turn`);
1028
+ clearBotSession(dir, sessionScope);
1029
+ resumedUnknownErrors.delete(sessionScope);
1030
+ }
1031
+ else {
1032
+ resumedUnknownErrors.set(sessionScope, streak);
1033
+ }
1034
+ }
996
1035
  return { excerpt, count };
997
1036
  };
998
1037
  /**
@@ -1953,9 +1992,10 @@ export async function runBot(deps) {
1953
1992
  const block = attachmentTurnBlock(files);
1954
1993
  text = text ? `${text}\n\n${block}` : block;
1955
1994
  }
1956
- const sessionScope = target.kind === "room" ? target.sessionScope : dmSessionScope(target.peerUid, config.ownerUid);
1995
+ const sessionScope = target.kind === "room" ? target.sessionScope : dmTurnSessionScope(item, config.ownerUid);
1957
1996
  if (target.kind === "dm") {
1958
- rememberDm(sessionScope, {
1997
+ // The owner's history still records that the job was sent.
1998
+ rememberDm(dmSessionScope(target.peerUid, config.ownerUid), {
1959
1999
  role: "user",
1960
2000
  from: (item.fromDisplayName ?? "").trim() || item.fromPersonUid || "someone",
1961
2001
  ...(item.fromPersonUid ? { fromPersonUid: item.fromPersonUid } : {}),
@@ -2077,7 +2117,9 @@ export async function runBot(deps) {
2077
2117
  clearInflight(dir, id);
2078
2118
  attempts.delete(id);
2079
2119
  markSignInBroken(outcome.excerpt);
2080
- const conversation = markerTarget.kind === "dm" ? sessionScope : `room:${markerTarget.channelId}:${item.rootEventId ?? ""}`;
2120
+ // One notice per DM conversation: a brief job's scope is per run, so
2121
+ // the notice keys on the peer's DM scope, not the turn's scope.
2122
+ const conversation = markerTarget.kind === "dm" ? dmSessionScope(markerTarget.peerUid, config.ownerUid) : `room:${markerTarget.channelId}:${item.rootEventId ?? ""}`;
2081
2123
  await tellSignInNeeded(conversation, (notice) => postRepair(markerTarget, notice, item.rootEventId), false, item.eventId ?? id);
2082
2124
  log("warn", `kept ${id} until ${runtime.id} can sign in again`);
2083
2125
  return; // un-acked: answered once the sign-in works
@@ -2191,8 +2233,11 @@ export async function runBot(deps) {
2191
2233
  const pending = new Set();
2192
2234
  const running = new Set();
2193
2235
  const canStart = () => !stopping && state === "running" && !hasPromotionHold(dir);
2194
- /** Job turns yield to people: one at a time, and never the last free slot. */
2195
- const isJobLane = (lane) => lane.startsWith("job:");
2236
+ /**
2237
+ * Job turns yield to people: one at a time, and never the last free slot.
2238
+ * A brief job is a person's click, so it is scheduled like the owner's DM.
2239
+ */
2240
+ const isJobLane = (lane) => lane.startsWith("job:") && !isBriefJobSessionScope(lane);
2196
2241
  const jobLaneBudget = Math.max(1, maxParallelTurns - 1);
2197
2242
  const canStartJobLane = () => [...busyLanes].filter(isJobLane).length < MAX_PARALLEL_JOB_TURNS && busyLanes.size < jobLaneBudget;
2198
2243
  const pump = () => {
@@ -5,10 +5,26 @@
5
5
  * `Claude Code-credentials`, plus `Claude Code-credentials-<hash>` seats,
6
6
  * or the credentials JSON under ~/.claude/ on Linux). GETs
7
7
  * https://api.anthropic.com/api/oauth/usage with the oauth-2025-04-20 beta
8
- * header. This module does not refresh the token, write the Keychain or
9
- * ~/.claude, call an OAuth token endpoint, or invoke the claude CLI except
8
+ * header. This module does not invoke the claude CLI except
10
9
  * `claude --version` when the caller does not pass cliVersion.
11
10
  *
11
+ * US-014 wires in a token-refresh gate. The gate uses two seams,
12
+ * `defaultListLiveLanes` and `defaultListProcesses`, to decide whether a
13
+ * refresh is safe (no running Claude Code holds the home). Both seams
14
+ * FAIL CLOSED: an error, a timeout, or an unexpected JSON shape returns an
15
+ * error-signalling throw and the caller records `skipped-live` /
16
+ * `skipped-process` and does NOT rotate the refresh token.
17
+ *
18
+ * Residual blind spot on darwin: `ps -axo pid=,command=` cannot read a
19
+ * child process's environment, so on darwin a running `claude` process is
20
+ * conservatively treated as holding the DEFAULT home AND blocking any
21
+ * non-default home refresh. A sibling that sets `CLAUDE_CONFIG_DIR` to a
22
+ * different home still blocks the gate; this is the safe side of the
23
+ * precision/safety tradeoff. On linux `/proc/<pid>/environ` lets the gate
24
+ * read the per-process `CLAUDE_CONFIG_DIR`, so it only blocks the matching
25
+ * home. Windows and other platforms get no process gate; the lane gate is
26
+ * authoritative there.
27
+ *
12
28
  * The legacy shape (seven_day_opus populated, no limits[]) is drift.
13
29
  * observedAt is the response time. One collect appends every account's
14
30
  * readings and failures in one rebuild.
@@ -30,6 +46,17 @@ export declare const CLAUDE_OAUTH_FETCH_TIMEOUT_MS = 10000;
30
46
  export declare const CLAUDE_OAUTH_SECURITY_TIMEOUT_MS = 5000;
31
47
  export declare const CLAUDE_OAUTH_POLL_FILE = "claude-oauth-last-call.json";
32
48
  export declare const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
49
+ /** Claude Code's refresh endpoint. The URL and the client_id are the ones Claude Code itself posts. */
50
+ export declare const CLAUDE_OAUTH_REFRESH_URL = "https://console.anthropic.com/v1/oauth/token";
51
+ export declare const CLAUDE_OAUTH_REFRESH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
52
+ /** Minimum gap between refresh attempts for one home when the previous attempt did not revoke. */
53
+ export declare const CLAUDE_OAUTH_REFRESH_BACKOFF_MS: number;
54
+ /** Minimum gap between refresh attempts for one home after an invalid_grant. */
55
+ export declare const CLAUDE_OAUTH_REFRESH_REVOKED_BACKOFF_MS: number;
56
+ /** Try a refresh when the token is this close to expiry, so one tick does not land on a stale token. */
57
+ export declare const CLAUDE_OAUTH_REFRESH_PREFLIGHT_MS: number;
58
+ export declare const CLAUDE_OAUTH_REFRESH_TIMEOUT_MS = 10000;
59
+ export declare const CLAUDE_OAUTH_REFRESH_LAST_FILE = "claude-oauth-refresh-last.json";
33
60
  export interface ClaudeOAuthFetchInit {
34
61
  method: "GET";
35
62
  headers: Record<string, string>;
@@ -43,6 +70,21 @@ export type ClaudeOAuthFetch = (url: string, init: ClaudeOAuthFetchInit) => Prom
43
70
  get(name: string): string | null;
44
71
  };
45
72
  }>;
73
+ export interface ClaudeOAuthRefreshFetchInit {
74
+ method: "POST";
75
+ headers: Record<string, string>;
76
+ body: string;
77
+ redirect: "manual";
78
+ signal?: AbortSignal;
79
+ }
80
+ /** Separate seam from the usage fetch: POST + body, same response shape. */
81
+ export type ClaudeOAuthRefreshFetch = (url: string, init: ClaudeOAuthRefreshFetchInit) => Promise<{
82
+ status: number;
83
+ text: () => Promise<string>;
84
+ headers?: {
85
+ get(name: string): string | null;
86
+ };
87
+ }>;
46
88
  export interface SecurityExecResult {
47
89
  status: number | null;
48
90
  stdout: string;
@@ -58,12 +100,77 @@ export interface ClaudeCredentialAccount {
58
100
  expiresAt?: number | null;
59
101
  scopes?: readonly string[] | null;
60
102
  planType?: string | null;
103
+ /** Opaque refresh token. Never logged, never surfaced. Passed to writeCredentials verbatim. */
104
+ refreshToken?: string | null;
105
+ /** Full parsed credential JSON. Preserved so writeCredentials can round-trip mcpOAuth and siblings. */
106
+ credentialDocument?: Record<string, unknown> | null;
107
+ /** The config dir this credential belongs to; needed to resolve the Keychain service or file path. */
108
+ home?: string | null;
61
109
  }
62
110
  export interface ReadClaudeCredentialsResult {
63
111
  keychain: ClaudeKeychainStatus;
64
112
  accounts: ClaudeCredentialAccount[];
65
113
  }
66
114
  export type ReadClaudeCredentials = () => ReadClaudeCredentialsResult;
115
+ export interface WriteClaudeCredentialsRequest {
116
+ home: string;
117
+ /** macOS Keychain service name; empty on non-darwin. */
118
+ service: string;
119
+ platform: NodeJS.Platform;
120
+ /** The full new credential JSON object. The caller preserved sibling keys. */
121
+ document: Record<string, unknown>;
122
+ /** The refreshToken we read before POSTing to the token endpoint. Used to detect rotation races. */
123
+ previousRefreshToken: string | null;
124
+ }
125
+ export interface WriteClaudeCredentialsResult {
126
+ ok: boolean;
127
+ /** True when the on-disk refresh token no longer matches `previousRefreshToken`. */
128
+ raceDetected?: boolean;
129
+ invalidGrant?: boolean;
130
+ /** Human-readable, already scrubbed of tokens. */
131
+ error?: string;
132
+ }
133
+ export type WriteClaudeCredentials = (request: WriteClaudeCredentialsRequest) => Promise<WriteClaudeCredentialsResult>;
134
+ /**
135
+ * One row per lane the gate must consider.
136
+ * - `isLive` is true for state ∈ {queued, running, awaiting_input}: a running
137
+ * Claude Code may hold the home; a refresh would race and get invalid_grant.
138
+ * - `isAuthFailed` is true when the lane terminated with reason
139
+ * `provider_auth_failed` (src/lib/lanes/watcher.ts classifies 401/403 that
140
+ * way). The gate treats this as proof the account is already signed out and
141
+ * records revoked without hitting the token endpoint.
142
+ * - `home` is the resolved per-host provider home for the account; the gate
143
+ * compares it to the home being refreshed with `path.resolve` so symlinks
144
+ * do not slip through. `null` is accepted only when the row is not live.
145
+ */
146
+ export interface LiveLane {
147
+ home: string | null;
148
+ account: string | null;
149
+ provider: string;
150
+ state: string;
151
+ reason: string | null;
152
+ isLive: boolean;
153
+ isAuthFailed: boolean;
154
+ }
155
+ /** Fail-closed: throw on error/timeout/unexpected shape; the caller skips refresh. */
156
+ export type ListLiveLanes = () => Promise<readonly LiveLane[]>;
157
+ /**
158
+ * One row per Claude Code process the gate must consider.
159
+ * - `probe === "env"` means the probe read `/proc/<pid>/environ` and the
160
+ * `env.CLAUDE_CONFIG_DIR` field is authoritative (undefined means default).
161
+ * - `probe === "command-only"` means we only saw the command line (darwin
162
+ * `ps -axo pid=,command=`), with no visibility into the process env. The
163
+ * gate treats such a row as a conservative blocker for every home.
164
+ */
165
+ export interface ClaudeProcess {
166
+ pid: number;
167
+ env: {
168
+ CLAUDE_CONFIG_DIR?: string;
169
+ };
170
+ probe: "env" | "command-only";
171
+ }
172
+ /** Fail-closed: throw on error/timeout; the caller skips refresh. */
173
+ export type ListProcesses = () => Promise<readonly ClaudeProcess[]>;
67
174
  export interface ClaudeOAuthCollectResult {
68
175
  readings: ReadingInput[];
69
176
  failures: ParserFailureEvent[];
@@ -99,6 +206,20 @@ export interface CollectClaudeOAuthOptions {
99
206
  }[];
100
207
  /** One-shot collect reads while the resident collector stays disabled. */
101
208
  allowDisabled?: boolean;
209
+ /**
210
+ * Writes a refreshed credential back to disk / Keychain. Default implementation uses
211
+ * `security add-generic-password -U` on darwin and tmp+rename at mode 0600 on linux,
212
+ * preserving all sibling fields (including `mcpOAuth`).
213
+ */
214
+ writeCredentials?: WriteClaudeCredentials;
215
+ /** Returns homes that live lanes are using. Default implementation reads `hq lanes list --json`. */
216
+ listLiveLanes?: ListLiveLanes;
217
+ /** Returns processes whose `CLAUDE_CONFIG_DIR` identifies a home. Default reads `ps -ewwo pid,command`. */
218
+ listProcesses?: ListProcesses;
219
+ /** Overrides the Claude Code refresh endpoint. Tests set this to a 127.0.0.1 stub. */
220
+ tokenEndpoint?: string;
221
+ /** Overrides the POST transport for the refresh. Default wraps `fetch` with a 10s abort. */
222
+ refreshFetch?: ClaudeOAuthRefreshFetch;
102
223
  }
103
224
  interface MapOptions {
104
225
  observedAt: string;
@@ -141,5 +262,107 @@ export declare function claudeSeatAccountId(seat: string, orgId: string | null):
141
262
  export declare function readInstalledClaudeVersion(readVersion?: () => string): string;
142
263
  /** Async `claude --version` for the daemon. The sync reader stays for one-shot callers. */
143
264
  export declare function readInstalledClaudeVersionAsync(): Promise<string>;
265
+ /** Per-home record of the last refresh attempt. */
266
+ export interface ClaudeRefreshLastEntry {
267
+ attemptedAt: string;
268
+ outcome: "refreshed" | "revoked" | "error" | "skipped-race" | "skipped-live" | "skipped-live-check-failed" | "skipped-process" | "skipped-process-check-failed" | "skipped-backoff" | "skipped-revoked";
269
+ revokedRefreshTokenFingerprint?: string;
270
+ /** Short, token-free diagnostic (never a response body). */
271
+ errorMessage?: string;
272
+ /** Fixed classification string for refresh errors; no response body is stored anywhere. */
273
+ errorClass?: "invalid_grant" | "revoked" | "server_error" | "timeout" | "network" | "shape";
274
+ httpStatus?: number | null;
275
+ backoffUntil?: string;
276
+ /** ISO expiry of the token the entry describes. Set on a successful refresh. */
277
+ tokenExpiresAt?: string;
278
+ }
279
+ export interface RevokedAccountEntry {
280
+ at: string;
281
+ fingerprint?: string;
282
+ }
283
+ export declare function claudeOAuthRefreshLastPath(root: string): string;
284
+ /** Not the token: a 16 char sha256 prefix. */
285
+ export declare function tokenFingerprint(token: string): string;
286
+ /** Reads the last refresh outcome for a home. Exposed for the doctor command. */
287
+ export declare function readClaudeRefreshLast(root: string, home: string): ClaudeRefreshLastEntry | null;
288
+ /** @deprecated prefer {@link revokedMarkerForCurrentCredential}; this is the pre-fingerprint shim. */
289
+ export declare function isAccountRevokedPersisted(root: string, accountId: string): boolean;
290
+ /**
291
+ * Compare the persisted revoked marker (if any) against the refresh token the
292
+ * caller is about to use.
293
+ *
294
+ * - `"match"`: the marker was written for this exact refresh token. The caller
295
+ * must still block (nothing has changed since the account was marked signed
296
+ * out).
297
+ * - `"stale"`: a marker exists but the stored fingerprint does not match the
298
+ * current refresh token, meaning the human signed in again and the token
299
+ * rotated. The caller should clear the marker and proceed; a fresh 401 will
300
+ * re-mark it.
301
+ * - `"none"`: no marker is persisted.
302
+ *
303
+ * A marker written by an older version carries no fingerprint. We treat that
304
+ * as `"stale"` so a current sign-in can recover; if the token really is still
305
+ * revoked the usage fetch will set a fingerprinted marker on the next 401.
306
+ */
307
+ export declare function revokedMarkerForCurrentCredential(root: string, accountId: string, refreshToken: string | null | undefined): "match" | "stale" | "none";
308
+ /**
309
+ * Shape of a single lane row emitted by `hq lanes list --json`.
310
+ * See src/commands/lanes.ts (withoutProviderHome strips `provider_home_at_spawn`
311
+ * from CLI output; we must consult the accounts registry to map
312
+ * account id -> effective_home).
313
+ *
314
+ * Note: the lane watcher writes a provider-auth-failed signal to the
315
+ * `terminal_reason` field (watcher.ts:~729). The `terminal_details` record
316
+ * carries the structured `account` and `provider` of the rejection
317
+ * (provider-rejection.ts:providerRejectionDetails). Lane records never
318
+ * surface a top-level `reason` for the auth-failed state, so this is the
319
+ * only shape we read.
320
+ */
321
+ interface RawLaneRow {
322
+ state?: unknown;
323
+ account?: unknown;
324
+ provider?: unknown;
325
+ terminal_reason?: unknown;
326
+ terminal_details?: unknown;
327
+ }
328
+ interface RawAccountRow {
329
+ id?: unknown;
330
+ home?: unknown;
331
+ effective_home?: unknown;
332
+ auth_state?: unknown;
333
+ provider?: unknown;
334
+ }
335
+ /**
336
+ * Parse `hq lanes list --json` payload into typed rows.
337
+ * Exported for tests; fail-closed semantics live in defaultListLiveLanes.
338
+ */
339
+ export declare function parseLanesListPayload(payload: unknown): readonly RawLaneRow[];
340
+ /**
341
+ * Parse `hq lanes accounts list --json` payload into typed rows.
342
+ * Exported for tests; fail-closed semantics live in defaultListLiveLanes.
343
+ */
344
+ export declare function parseAccountsListPayload(payload: unknown): readonly RawAccountRow[];
345
+ /**
346
+ * Join lanes rows against accounts rows to produce LiveLane records.
347
+ * Exported for tests.
348
+ */
349
+ export declare function buildLiveLanesFromPayloads(lanes: readonly RawLaneRow[], accounts: readonly RawAccountRow[]): readonly LiveLane[];
350
+ /**
351
+ * Parse `ps -axo pid=,command=` into rows that mention the `claude` executable.
352
+ * Exported for tests. Returned rows carry `probe: "command-only"` so the gate
353
+ * knows it cannot read per-process CLAUDE_CONFIG_DIR on darwin.
354
+ */
355
+ export declare function parsePsClaudeOutput(stdout: string): readonly ClaudeProcess[];
356
+ export interface ClaudeTokenRefreshResult {
357
+ outcome: "refreshed" | "revoked" | "error" | "skipped-race" | "skipped-live" | "skipped-live-check-failed" | "skipped-process" | "skipped-process-check-failed" | "skipped-backoff" | "skipped-revoked";
358
+ newAccessToken?: string;
359
+ newExpiresAt?: number;
360
+ newRefreshToken?: string;
361
+ newScopes?: readonly string[] | null;
362
+ errorMessage?: string;
363
+ errorClass?: ClaudeRefreshErrorClass;
364
+ httpStatus?: number | null;
365
+ }
366
+ export type ClaudeRefreshErrorClass = "invalid_grant" | "revoked" | "server_error" | "timeout" | "network" | "shape";
144
367
  export {};
145
368
  //# sourceMappingURL=claude-oauth.d.ts.map