@oh-my-pi/pi-ai 18.2.0 → 18.2.2

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +2 -0
  3. package/dist/types/auth/sqlite-credential-store.d.ts +2 -1
  4. package/dist/types/auth-broker/remote-store.d.ts +17 -0
  5. package/dist/types/auth-gateway/index.d.ts +1 -0
  6. package/dist/types/auth-gateway/session-state.d.ts +118 -0
  7. package/dist/types/auth-storage.d.ts +17 -0
  8. package/dist/types/error/body-error.d.ts +15 -0
  9. package/dist/types/error/flags.d.ts +16 -0
  10. package/dist/types/error/index.d.ts +1 -0
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/types/oneshot-retry.d.ts +6 -0
  13. package/dist/types/provider-session-state.d.ts +46 -0
  14. package/dist/types/providers/amazon-bedrock.d.ts +3 -0
  15. package/dist/types/providers/aws-sigv4.d.ts +12 -0
  16. package/dist/types/providers/openai-codex/request-transformer.d.ts +27 -0
  17. package/dist/types/providers/openai-responses.d.ts +15 -0
  18. package/dist/types/providers/openai-shared.d.ts +20 -3
  19. package/dist/types/registry/oauth/perplexity.d.ts +1 -7
  20. package/dist/types/registry/oauth/types.d.ts +8 -0
  21. package/dist/types/stream.d.ts +2 -0
  22. package/dist/types/types.d.ts +3 -1
  23. package/dist/types/usage/openai-codex.d.ts +3 -1
  24. package/dist/types/usage.d.ts +11 -1
  25. package/dist/types/utils/block-symbols.d.ts +36 -0
  26. package/dist/types/utils/openai-http.d.ts +2 -0
  27. package/dist/types/utils/retry-after.d.ts +2 -0
  28. package/dist/types/utils/schema/wire.d.ts +4 -5
  29. package/dist/types/utils.d.ts +9 -0
  30. package/package.json +6 -6
  31. package/src/auth/sqlite-credential-store.ts +8 -33
  32. package/src/auth-broker/remote-store.ts +73 -8
  33. package/src/auth-broker/wire-schemas.ts +1 -0
  34. package/src/auth-gateway/index.ts +1 -0
  35. package/src/auth-gateway/server.ts +186 -74
  36. package/src/auth-gateway/session-state.ts +312 -0
  37. package/src/auth-storage.ts +146 -15
  38. package/src/error/body-error.ts +310 -0
  39. package/src/error/flags.ts +63 -13
  40. package/src/error/index.ts +1 -0
  41. package/src/error/retryable.ts +2 -0
  42. package/src/index.ts +1 -0
  43. package/src/oneshot-retry.ts +13 -3
  44. package/src/provider-session-state.ts +56 -0
  45. package/src/providers/amazon-bedrock.ts +20 -3
  46. package/src/providers/anthropic-messages-server.ts +104 -23
  47. package/src/providers/anthropic-signature.ts +5 -2
  48. package/src/providers/anthropic.ts +101 -15
  49. package/src/providers/aws-sigv4.ts +16 -5
  50. package/src/providers/cursor.ts +60 -10
  51. package/src/providers/devin.ts +82 -28
  52. package/src/providers/openai-chat-server.ts +4 -0
  53. package/src/providers/openai-codex/request-transformer.ts +36 -0
  54. package/src/providers/openai-codex-responses.ts +35 -12
  55. package/src/providers/openai-completions.ts +49 -12
  56. package/src/providers/openai-reasoning-fallback.ts +6 -6
  57. package/src/providers/openai-responses-server.ts +2 -1
  58. package/src/providers/openai-responses.ts +52 -4
  59. package/src/providers/openai-shared.ts +199 -51
  60. package/src/registry/oauth/perplexity.ts +94 -28
  61. package/src/registry/oauth/types.ts +9 -0
  62. package/src/stream.ts +23 -2
  63. package/src/types.ts +3 -0
  64. package/src/usage/claude.ts +33 -0
  65. package/src/usage/google-antigravity.ts +8 -2
  66. package/src/usage/openai-codex.ts +94 -11
  67. package/src/usage.ts +8 -1
  68. package/src/utils/block-symbols.ts +57 -0
  69. package/src/utils/http-inspector.ts +20 -0
  70. package/src/utils/openai-http.ts +39 -3
  71. package/src/utils/retry-after.ts +12 -0
  72. package/src/utils/schema/normalize.ts +3 -3
  73. package/src/utils/schema/stamps.ts +33 -45
  74. package/src/utils/schema/wire.ts +9 -7
  75. package/src/utils.ts +67 -22
@@ -5,6 +5,8 @@ import type { CredentialRankingStrategy, UsageProvider, UsageReport } from "../u
5
5
  * ingesting them lets credential selection block an exhausted account before
6
6
  * the next request burns a wire 429.
7
7
  */
8
- export declare function parseCodexRateLimitHeaders(headers: Record<string, string>, now?: number): UsageReport | null;
8
+ export declare function parseCodexRateLimitHeaders(headers: Record<string, string>, now?: number, context?: {
9
+ responseStatus?: number;
10
+ }): UsageReport | null;
9
11
  export declare const openaiCodexUsageProvider: UsageProvider;
10
12
  export declare const codexRankingStrategy: CredentialRankingStrategy;
@@ -43,6 +43,8 @@ export interface UsageScope {
43
43
  tier?: string;
44
44
  windowId?: string;
45
45
  shared?: boolean;
46
+ /** Stable identity shared by routing-specific copies of one upstream quota. */
47
+ sharedGroup?: string;
46
48
  }
47
49
  /** Normalized limit entry for a single window or quota bucket. */
48
50
  export interface UsageLimit {
@@ -241,6 +243,7 @@ export declare const usageScopeSchema: import("@oh-my-pi/omptype").FluentType<{
241
243
  projectId?: string | undefined;
242
244
  provider: string;
243
245
  shared?: boolean | undefined;
246
+ sharedGroup?: string | undefined;
244
247
  tier?: string | undefined;
245
248
  windowId?: string | undefined;
246
249
  }, {
@@ -250,6 +253,7 @@ export declare const usageScopeSchema: import("@oh-my-pi/omptype").FluentType<{
250
253
  projectId?: string | undefined;
251
254
  provider: string;
252
255
  shared?: boolean | undefined;
256
+ sharedGroup?: string | undefined;
253
257
  tier?: string | undefined;
254
258
  windowId?: string | undefined;
255
259
  }>;
@@ -272,6 +276,7 @@ export declare const usageLimitSchema: import("@oh-my-pi/omptype").FluentType<{
272
276
  projectId?: string | undefined;
273
277
  provider: string;
274
278
  shared?: boolean | undefined;
279
+ sharedGroup?: string | undefined;
275
280
  tier?: string | undefined;
276
281
  windowId?: string | undefined;
277
282
  };
@@ -302,6 +307,7 @@ export declare const usageLimitSchema: import("@oh-my-pi/omptype").FluentType<{
302
307
  projectId?: string | undefined;
303
308
  provider: string;
304
309
  shared?: boolean | undefined;
310
+ sharedGroup?: string | undefined;
305
311
  tier?: string | undefined;
306
312
  windowId?: string | undefined;
307
313
  };
@@ -359,6 +365,7 @@ export declare const usageReportSchema: import("@oh-my-pi/omptype").FluentType<{
359
365
  projectId?: string | undefined;
360
366
  provider: string;
361
367
  shared?: boolean | undefined;
368
+ sharedGroup?: string | undefined;
362
369
  tier?: string | undefined;
363
370
  windowId?: string | undefined;
364
371
  };
@@ -404,6 +411,7 @@ export declare const usageReportSchema: import("@oh-my-pi/omptype").FluentType<{
404
411
  projectId?: string | undefined;
405
412
  provider: string;
406
413
  shared?: boolean | undefined;
414
+ sharedGroup?: string | undefined;
407
415
  tier?: string | undefined;
408
416
  windowId?: string | undefined;
409
417
  };
@@ -472,7 +480,9 @@ export interface UsageProvider {
472
480
  id: Provider;
473
481
  fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null>;
474
482
  /** Parse provider rate-limit response headers (lowercased keys) into a usage report, if supported. */
475
- parseRateLimitHeaders?(headers: Record<string, string>, now?: number): UsageReport | null;
483
+ parseRateLimitHeaders?(headers: Record<string, string>, now?: number, context?: {
484
+ responseStatus?: number;
485
+ }): UsageReport | null;
476
486
  supports?(params: UsageFetchParams): boolean;
477
487
  /** True when fetchUsage contacts upstream and can authenticate the credential for health checks. */
478
488
  validatesCredentials?: boolean;
@@ -113,3 +113,39 @@ export type SyntheticUserCarrier = object & {
113
113
  };
114
114
  /** True for `user` messages synthesized by message transformation. */
115
115
  export declare function isSyntheticUser(message: SyntheticUserCarrier | null | undefined): boolean;
116
+ /**
117
+ * Marks a message synthesized by a per-call context transform rather than
118
+ * loaded from persisted conversation history.
119
+ *
120
+ * Prompt-cache boundaries must skip these messages: their content is rebuilt
121
+ * for each request and cannot anchor a prefix reused by the next turn.
122
+ * Symbol-keyed so the marker never persists or reaches the provider wire.
123
+ */
124
+ export declare const kPerCallContextMessage: unique symbol;
125
+ /** Carries per-call context provenance without exposing a string-keyed property. */
126
+ export type PerCallContextMessageCarrier = object & {
127
+ [kPerCallContextMessage]?: true;
128
+ };
129
+ /** Marks a message as synthesized for the current provider call. */
130
+ export declare function markPerCallContextMessage(message: PerCallContextMessageCarrier): void;
131
+ /** Copies per-call context provenance to a converted or projected message. */
132
+ export declare function copyPerCallContextMessage(target: PerCallContextMessageCarrier, source: PerCallContextMessageCarrier): void;
133
+ /** True when a message was synthesized for the current provider call. */
134
+ export declare function isPerCallContextMessage(message: PerCallContextMessageCarrier | null | undefined): boolean;
135
+ /**
136
+ * Original history position carried by a context message clone.
137
+ *
138
+ * Object-spread transforms retain this symbol, allowing the extension runner
139
+ * to distinguish byte-identical historical copies from inserted messages.
140
+ */
141
+ export declare const kContextHistoryIndex: unique symbol;
142
+ /** Carries a context message's original history position. */
143
+ export type ContextHistoryIndexCarrier = object & {
144
+ [kContextHistoryIndex]?: number;
145
+ };
146
+ /** Reads a context message's original history position. */
147
+ export declare function getContextHistoryIndex(message: ContextHistoryIndexCarrier | null | undefined): number | undefined;
148
+ /** Records a context message's original history position. */
149
+ export declare function setContextHistoryIndex(message: ContextHistoryIndexCarrier, index: number): void;
150
+ /** Removes context-history tracking before provider conversion. */
151
+ export declare function clearContextHistoryIndex(message: ContextHistoryIndexCarrier): void;
@@ -26,6 +26,8 @@ export interface OpenAIStreamRequestInit {
26
26
  body: unknown;
27
27
  signal: AbortSignal;
28
28
  fetch?: FetchImpl;
29
+ /** Optional caller-specific gate composed with shared transport retry exclusions. */
30
+ shouldRetryResponse?: (response: Response, bodyText: string) => boolean | Promise<boolean>;
29
31
  /** Raw wire-frame observer (`onSseEvent` debug pipeline). */
30
32
  onSseEvent?: SseEventObserver;
31
33
  }
@@ -1,4 +1,6 @@
1
1
  export type HeadersLike = Headers | Record<string, string | undefined> | undefined | null;
2
+ /** Extracts retry timing using the provider's catalog-declared timestamp timezone. */
3
+ export declare function extractProviderRetryHint(provider: string | undefined, message: string | undefined): number | undefined;
2
4
  export declare function formatErrorMessageWithRetryAfter(error: unknown, headers?: HeadersLike): string;
3
5
  export declare function getRetryAfterMsFromHeaders(headers: HeadersLike): number | undefined;
4
6
  export declare function getHeadersFromError(error: unknown): HeadersLike;
@@ -41,10 +41,9 @@ export declare function arkToWireSchema(schema: Type): Record<string, unknown>;
41
41
  export declare function toolWireSchema(tool: Tool): Record<string, unknown>;
42
42
  /**
43
43
  * Return a deep clone of `schema` with every `description` annotation removed.
44
- * The result is memoized on the input via a non-enumerable symbol (`stamp`) so
45
- * repeated provider requests reuse the same stripped object; the input is never
46
- * mutated, so the stamped `toolWireSchema` cache stays intact for
47
- * system-prompt/UI rendering.
44
+ * The result is memoized against the input (`stamp`) so repeated provider
45
+ * requests reuse the same stripped object; the input is never mutated, so the
46
+ * memoized `toolWireSchema` result stays intact for system-prompt/UI rendering.
48
47
  */
49
48
  export declare function stripSchemaDescriptions(schema: Record<string, unknown>): Record<string, unknown>;
50
49
  /**
@@ -53,6 +52,6 @@ export declare function stripSchemaDescriptions(schema: Record<string, unknown>)
53
52
  * Used when the full tool catalog is rendered into the system prompt instead, so
54
53
  * the descriptions ride the wire once (in the prompt) rather than duplicated on
55
54
  * every tool definition. Parameters are resolved to wire JSON Schema and cloned,
56
- * leaving the original tool objects and the stamped schema cache untouched.
55
+ * leaving the original tool objects and the memoized schema cache untouched.
57
56
  */
58
57
  export declare function stripToolDescriptions(tools: readonly Tool[]): Tool[];
@@ -48,6 +48,15 @@ export declare function stripUnpairedOpenAIResponsesComputerReasoningIdsForRepla
48
48
  * Returns `undefined` for hidden-empty turns that only contain reasoning and an
49
49
  * empty assistant message, allowing callers to rebuild visible transcript
50
50
  * history instead of replaying stale native state.
51
+ *
52
+ * When the turn does carry replayable output, whitespace-only assistant
53
+ * messages are dropped rather than replayed. gpt-5.6 Codex ends a turn whose
54
+ * text all landed in the `commentary` phase with an empty `final_answer`
55
+ * message; replaying that item verbatim seeds the next turn with an empty slot
56
+ * the model then fills - `""` → `"\n\n"` → stray words → non-Latin residue -
57
+ * and each contaminated item is replayed in turn, so the drift compounds until
58
+ * the visible answer collapses. Codex CLI replays the empty item too
59
+ * (openai/codex#32389); this is the layer where omp can refuse to.
51
60
  */
52
61
  export declare function sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(items: Array<Record<string, unknown>>, options?: OpenAIResponsesReplaySanitizeOptions): ResponseInput | undefined;
53
62
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.2.0",
3
+ "version": "18.2.2",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -124,11 +124,11 @@
124
124
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
125
125
  },
126
126
  "dependencies": {
127
- "@oh-my-pi/omptype": "18.2.0",
128
- "@oh-my-pi/pi-catalog": "18.2.0",
129
- "@oh-my-pi/pi-natives": "18.2.0",
130
- "@oh-my-pi/pi-utils": "18.2.0",
131
- "@oh-my-pi/pi-wire": "18.2.0"
127
+ "@oh-my-pi/omptype": "18.2.2",
128
+ "@oh-my-pi/pi-catalog": "18.2.2",
129
+ "@oh-my-pi/pi-natives": "18.2.2",
130
+ "@oh-my-pi/pi-utils": "18.2.2",
131
+ "@oh-my-pi/pi-wire": "18.2.2"
132
132
  },
133
133
  "devDependencies": {
134
134
  "@types/bun": "^1.3.14"
@@ -4,7 +4,7 @@
4
4
  * The public AuthCredentialStore interface remains in ../auth-storage so local
5
5
  * and remote stores share the same contract.
6
6
  */
7
- import { Database, type Statement } from "bun:sqlite";
7
+ import type { Database, Statement } from "bun:sqlite";
8
8
  import * as fs from "node:fs/promises";
9
9
  import * as path from "node:path";
10
10
  import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
@@ -15,6 +15,7 @@ import {
15
15
  isSqliteBusyError,
16
16
  isSqliteCorruptionError,
17
17
  logger,
18
+ openSqliteDatabase,
18
19
  } from "@oh-my-pi/pi-utils";
19
20
  import type {
20
21
  AuthCredential,
@@ -25,7 +26,6 @@ import type {
25
26
  StoredAuthCredential,
26
27
  StoredCredentialBlock,
27
28
  } from "../auth-storage";
28
- import * as AIError from "../error";
29
29
  import type { OAuthCredentials } from "../registry/oauth/types";
30
30
  import type { Provider } from "../types";
31
31
  import type {
@@ -509,6 +509,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
509
509
  );
510
510
  }
511
511
 
512
+ /** Opens credential storage with bounded busy retries and one-shot corruption recovery. */
512
513
  static async open(dbPath: string = getAgentDbPath()): Promise<SqliteAuthCredentialStore> {
513
514
  const dir = path.dirname(dbPath);
514
515
  const dirExists = await fs
@@ -519,23 +520,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
519
520
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
520
521
  }
521
522
 
522
- // Concurrent omp startups can race against WAL recovery and the schema
523
- // init's first lock-taking statement. Bun's default `busy_timeout` is 0,
524
- // so retry the open on `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY` with bounded
525
- // exponential backoff before surfacing the failure. See issue #2421.
526
- const maxAttempts = 4;
527
- const baseDelayMs = 100;
528
- let lastBusyError: Error | undefined;
529
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
530
- let db: Database | undefined;
531
- try {
532
- db = new Database(dbPath);
533
- // Install the busy handler BEFORE the first lock-taking statement
534
- // on this connection. The leases DDL below and the constructor's
535
- // schema init both acquire locks during WAL recovery; without a
536
- // non-zero `busy_timeout` they fail immediately with SQLITE_BUSY.
537
- // See issue #2421.
538
- SqliteAuthCredentialStore.#installBusyTimeout(db);
523
+ return openSqliteDatabase(
524
+ dbPath,
525
+ async db => {
539
526
  try {
540
527
  await fs.chmod(dbPath, 0o600);
541
528
  } catch {
@@ -543,20 +530,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
543
530
  }
544
531
  SqliteAuthCredentialStore.#ensureAuthCredentialRefreshLeasesTable(db);
545
532
  return new SqliteAuthCredentialStore(db);
546
- } catch (err) {
547
- db?.close();
548
- if (!isSqliteBusyError(err)) {
549
- throw err;
550
- }
551
- lastBusyError = err instanceof Error ? err : new Error(String(err));
552
- if (attempt < maxAttempts - 1) {
553
- await Bun.sleep(baseDelayMs * 2 ** attempt);
554
- }
555
- }
556
- }
557
- throw new AIError.ConfigurationError(
558
- `Failed to open auth database at '${dbPath}' after ${maxAttempts} attempts: ${lastBusyError?.message}`,
559
- { cause: lastBusyError },
533
+ },
534
+ { recoverCorruption: true },
560
535
  );
561
536
  }
562
537
 
@@ -255,6 +255,18 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
255
255
  #snapshot: SnapshotResponse = emptySnapshot();
256
256
  #snapshotReceivedAt = Date.now();
257
257
  #generation = 0;
258
+ /**
259
+ * Content fingerprint of the credential set in {@link #snapshot} (id +
260
+ * provider + credential material), recomputed after every snapshot mutation.
261
+ * Drives {@link #credentialRevision} independently of the broker's numeric
262
+ * generation, which is an in-memory counter that resets when the broker
263
+ * process restarts and so cannot be trusted for change detection.
264
+ */
265
+ #credentialFingerprint = "";
266
+ /** Monotonic local counter bumped whenever {@link #credentialFingerprint} changes. */
267
+ #credentialRevision = 0;
268
+ /** Revision last reported as "seen" by {@link pollExternalChanges}; seeded from the initial snapshot. */
269
+ #acknowledgedRevision = 0;
258
270
  #usageOverlays: Map<string, UsageReport> = new Map();
259
271
  #backgroundAbort = new AbortController();
260
272
  readonly #backgroundIdleMs: number;
@@ -300,6 +312,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
300
312
  ? new Map([...opts.accountPool].map(([provider, identities]) => [provider, new Set(identities)]))
301
313
  : undefined;
302
314
  this.#applySnapshot(opts.initialSnapshot ?? emptySnapshot(), opts.initialSnapshot?.generation ?? 0);
315
+ this.#acknowledgedRevision = this.#credentialRevision;
303
316
  this.#onSnapshot = opts.onSnapshot;
304
317
  void this.#runBackground();
305
318
  }
@@ -325,6 +338,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
325
338
  this.#snapshot = { ...snapshot, credentials };
326
339
  this.#generation = generation;
327
340
  this.#snapshotReceivedAt = nowMs;
341
+ this.#refreshCredentialRevision();
328
342
  const onSnapshot = this.#onSnapshot;
329
343
  if (!onSnapshot) return;
330
344
  try {
@@ -333,6 +347,34 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
333
347
  logger.debug("auth-broker snapshot callback failed", { error: String(error) });
334
348
  }
335
349
  }
350
+
351
+ /**
352
+ * Recompute the credential-content fingerprint and bump
353
+ * {@link #credentialRevision} when it changes. Called after every snapshot
354
+ * mutation so {@link pollExternalChanges} detects add/remove/replace even
355
+ * when the broker's numeric generation repeats (e.g. after a broker
356
+ * restart resets its in-memory counter).
357
+ */
358
+ #refreshCredentialRevision(): void {
359
+ const fingerprint = this.#computeCredentialFingerprint();
360
+ if (fingerprint === this.#credentialFingerprint) return;
361
+ this.#credentialFingerprint = fingerprint;
362
+ this.#credentialRevision += 1;
363
+ }
364
+
365
+ /**
366
+ * Order-independent digest of the routable credential material — exactly the
367
+ * fields {@link listAuthCredentials} exposes (id, provider, credential). A
368
+ * token rotation or an add/remove changes it; credential blocks and usage
369
+ * overlays do not.
370
+ */
371
+ #computeCredentialFingerprint(): string {
372
+ const parts = this.#snapshot.credentials.map(
373
+ entry => `${entry.id}\u0000${entry.provider}\u0000${JSON.stringify(entry.credential)}`,
374
+ );
375
+ parts.sort();
376
+ return parts.join("\u0001");
377
+ }
336
378
  #protectNewSnapshotBlocks(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[], nowMs: number): void {
337
379
  const previousBlocksByKey = new Map<string, string>();
338
380
  for (const entry of previous) {
@@ -499,15 +541,13 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
499
541
  #applyStreamEvent(event: SnapshotStreamEvent): void {
500
542
  switch (event.kind) {
501
543
  case "snapshot": {
502
- // Strip the discriminator so we store the wire-shape SnapshotResponse.
544
+ // The first frame of every SSE connection is a full authoritative
545
+ // snapshot. Always adopt it as the new generation baseline: the
546
+ // broker's in-memory generation counter resets on restart and may
547
+ // therefore be lower than the previous stream's last value.
548
+ // Subsequent entry/removal frames remain guarded against reordering
549
+ // relative to this new baseline below.
503
550
  const { kind: _kind, ...snapshot } = event;
504
- if (snapshot.generation < this.#generation) {
505
- logger.debug("auth-broker stream snapshot older than local; ignoring", {
506
- local: this.#generation,
507
- incoming: snapshot.generation,
508
- });
509
- return;
510
- }
511
551
  this.#applySnapshot(snapshot, snapshot.generation);
512
552
  return;
513
553
  }
@@ -548,6 +588,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
548
588
  this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
549
589
  this.#generation = generation;
550
590
  this.#snapshotReceivedAt = Date.now();
591
+ this.#refreshCredentialRevision();
551
592
  }
552
593
 
553
594
  #removeStreamCredential(
@@ -564,6 +605,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
564
605
  this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
565
606
  this.#generation = generation;
566
607
  this.#snapshotReceivedAt = Date.now();
608
+ this.#refreshCredentialRevision();
567
609
  }
568
610
 
569
611
  /** Re-hydrate the in-memory snapshot from the broker. */
@@ -574,6 +616,29 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
574
616
  return this.#snapshot;
575
617
  }
576
618
 
619
+ /**
620
+ * Stateful probe for broker-side credential changes, mirroring
621
+ * {@link SqliteAuthCredentialStore.pollExternalChanges} so long-lived broker
622
+ * clients (notably `auth-gateway serve`) pick up logins/logouts made by
623
+ * another process without a restart.
624
+ *
625
+ * Compares a local content revision, not the broker's numeric generation:
626
+ * generation is an in-memory counter that resets when the broker process
627
+ * restarts, so a reconnecting stream can deliver a different credential set
628
+ * under a repeated (or lower) generation. {@link #refreshCredentialRevision}
629
+ * bumps the revision whenever the applied credential material actually
630
+ * changes, catching those cases too. Records foreground activity first: a
631
+ * low-traffic client's background sync parks after `#backgroundIdleMs`, and
632
+ * without this wakeup it would never fetch the new snapshot to report in the
633
+ * first place.
634
+ */
635
+ pollExternalChanges(): boolean {
636
+ this.#noteActivity();
637
+ if (this.#credentialRevision === this.#acknowledgedRevision) return false;
638
+ this.#acknowledgedRevision = this.#credentialRevision;
639
+ return true;
640
+ }
641
+
577
642
  listAuthCredentials(provider?: string): StoredAuthCredential[] {
578
643
  this.#noteActivity();
579
644
  const out: StoredAuthCredential[] = [];
@@ -220,6 +220,7 @@ const usageScopeSchema = type({
220
220
  "tier?": "string",
221
221
  "windowId?": "string",
222
222
  "shared?": "boolean",
223
+ "sharedGroup?": "string",
223
224
  });
224
225
 
225
226
  const usageLimitSchema = type({
@@ -1,3 +1,4 @@
1
1
  export * from "./http";
2
+ export * from "./session-state";
2
3
  export * from "./server";
3
4
  export * from "./types";