agl 40.4.0 → 40.5.0

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 (149) hide show
  1. package/changelog.md +27 -0
  2. package/dist_serve/bundle.js +2228 -2177
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/aglapi/aglapisurface.document.js +2 -2
  6. package/dist_ts/classes.accountsactions.d.ts +11 -1
  7. package/dist_ts/classes.accountsactions.js +59 -2
  8. package/dist_ts/classes.controller.js +5 -5
  9. package/dist_ts/classes.flexaccountaccess.js +3 -2
  10. package/dist_ts/classes.harnessreleases.d.ts +14 -4
  11. package/dist_ts/classes.harnessreleases.js +28 -4
  12. package/dist_ts/classes.inferencegateway.d.ts +28 -3
  13. package/dist_ts/classes.inferencegateway.js +177 -61
  14. package/dist_ts/classes.inferenceplacement.d.ts +5 -3
  15. package/dist_ts/classes.inferenceplacement.js +2 -2
  16. package/dist_ts/classes.inferencerouter.d.ts +349 -91
  17. package/dist_ts/classes.inferencerouter.js +804 -297
  18. package/dist_ts/classes.inferenceservice.d.ts +14 -2
  19. package/dist_ts/classes.inferenceservice.js +53 -14
  20. package/dist_ts/classes.inferencespend.d.ts +339 -0
  21. package/dist_ts/classes.inferencespend.js +815 -0
  22. package/dist_ts/classes.inferenceusagecache.d.ts +8 -0
  23. package/dist_ts/classes.inferenceusagecache.js +9 -5
  24. package/dist_ts/classes.inferenceusagetap.d.ts +50 -9
  25. package/dist_ts/classes.inferenceusagetap.js +185 -31
  26. package/dist_ts/classes.proxycontrol.d.ts +3 -2
  27. package/dist_ts/classes.proxycontrol.js +1 -2
  28. package/dist_ts/classes.proxydaemon.js +3 -2
  29. package/dist_ts/classes.proxystore.d.ts +6 -0
  30. package/dist_ts/classes.proxystore.js +11 -2
  31. package/dist_ts/fleet/classes.claudeworkers.d.ts +5 -4
  32. package/dist_ts/fleet/classes.claudeworkers.js +10 -9
  33. package/dist_ts/functions.claudecoderelease.d.ts +12 -6
  34. package/dist_ts/functions.claudecoderelease.js +13 -7
  35. package/dist_ts/functions.inferencecli.js +12 -8
  36. package/dist_ts/interfaces.auth.d.ts +1 -1
  37. package/dist_ts/interfaces.auth.js +3 -1
  38. package/dist_ts/plugins.d.ts +3 -1
  39. package/dist_ts/plugins.js +7 -2
  40. package/dist_ts_authswitch/authority-apikeys.d.ts +32 -0
  41. package/dist_ts_authswitch/authority-apikeys.js +65 -0
  42. package/dist_ts_authswitch/authority-contract.d.ts +142 -28
  43. package/dist_ts_authswitch/authority-contract.js +48 -1
  44. package/dist_ts_authswitch/authority-overview.js +8 -1
  45. package/dist_ts_authswitch/authority-placement.d.ts +7 -2
  46. package/dist_ts_authswitch/authority-placement.js +10 -3
  47. package/dist_ts_authswitch/authority-runtime-contract.d.ts +2 -2
  48. package/dist_ts_authswitch/authority-spend.d.ts +103 -0
  49. package/dist_ts_authswitch/authority-spend.js +110 -0
  50. package/dist_ts_authswitch/authority-subscription.d.ts +12 -1
  51. package/dist_ts_authswitch/authority-subscription.js +24 -1
  52. package/dist_ts_authswitch/authority-usagelimit.d.ts +7 -4
  53. package/dist_ts_authswitch/authority-usagelimit.js +89 -39
  54. package/dist_ts_authswitch/authority-usagewindows.d.ts +4 -1
  55. package/dist_ts_authswitch/authority-usagewindows.js +41 -16
  56. package/dist_ts_authswitch/classes.authoritybroker.d.ts +30 -19
  57. package/dist_ts_authswitch/classes.authoritybroker.js +152 -61
  58. package/dist_ts_authswitch/classes.authoritycli.d.ts +9 -4
  59. package/dist_ts_authswitch/classes.authoritycli.js +94 -29
  60. package/dist_ts_authswitch/classes.authorityclient.d.ts +24 -7
  61. package/dist_ts_authswitch/classes.authorityclient.js +31 -11
  62. package/dist_ts_authswitch/classes.authoritydaemon.js +39 -12
  63. package/dist_ts_authswitch/classes.authoritydatabase.d.ts +28 -5
  64. package/dist_ts_authswitch/classes.authoritydatabase.js +171 -9
  65. package/dist_ts_authswitch/classes.authorityimport.js +2 -2
  66. package/dist_ts_authswitch/classes.authoritymodels.d.ts +71 -3
  67. package/dist_ts_authswitch/classes.authoritymodels.js +249 -20
  68. package/dist_ts_authswitch/classes.authorityregistry.js +4 -2
  69. package/dist_ts_authswitch/classes.authorityusage.d.ts +69 -7
  70. package/dist_ts_authswitch/classes.authorityusage.js +118 -4
  71. package/dist_ts_authswitch/classes.claudenative.d.ts +16 -7
  72. package/dist_ts_authswitch/classes.claudenative.js +29 -16
  73. package/dist_ts_authswitch/classes.claudesignin.js +5 -2
  74. package/dist_ts_authswitch/testsupport.d.ts +24 -5
  75. package/dist_ts_authswitch/testsupport.js +56 -12
  76. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.d.ts +12 -0
  77. package/dist_ts_authswitch/ts_migration/0011_account_api_key_fields.js +21 -0
  78. package/dist_ts_authswitch/ts_migration/index.js +3 -1
  79. package/dist_ts_interfaces/functions.authorityscope.d.ts +18 -7
  80. package/dist_ts_interfaces/functions.authorityscope.js +35 -8
  81. package/dist_ts_interfaces/interfaces.d.ts +1 -1
  82. package/dist_ts_interfaces/interfaces.js +1 -1
  83. package/dist_ts_interfaces/plugins.d.ts +2 -1
  84. package/dist_ts_interfaces/plugins.js +7 -4
  85. package/dist_ts_interfaces/requests.accounts.d.ts +56 -0
  86. package/dist_ts_interfaces/requests.accounts.js +11 -1
  87. package/dist_ts_interfaces/requests.d.ts +2 -2
  88. package/dist_ts_interfaces/requests.js +1 -1
  89. package/docs/authswitch.md +75 -12
  90. package/package.json +5 -5
  91. package/readme.md +198 -21
  92. package/ts/00_commitinfo_data.ts +1 -1
  93. package/ts/aglapi/aglapisurface.document.ts +2 -2
  94. package/ts/classes.accountsactions.ts +75 -1
  95. package/ts/classes.controller.ts +4 -4
  96. package/ts/classes.flexaccountaccess.ts +2 -1
  97. package/ts/classes.harnessreleases.ts +25 -4
  98. package/ts/classes.inferencegateway.ts +184 -64
  99. package/ts/classes.inferenceplacement.ts +6 -4
  100. package/ts/classes.inferencerouter.ts +1026 -314
  101. package/ts/classes.inferenceservice.ts +56 -13
  102. package/ts/classes.inferencespend.ts +870 -0
  103. package/ts/classes.inferenceusagecache.ts +16 -4
  104. package/ts/classes.inferenceusagetap.ts +159 -34
  105. package/ts/classes.proxycontrol.ts +3 -3
  106. package/ts/classes.proxydaemon.ts +2 -1
  107. package/ts/classes.proxystore.ts +11 -1
  108. package/ts/fleet/classes.claudeworkers.ts +9 -8
  109. package/ts/functions.claudecoderelease.ts +13 -6
  110. package/ts/functions.inferencecli.ts +11 -7
  111. package/ts/interfaces.auth.ts +2 -0
  112. package/ts/plugins.ts +7 -0
  113. package/ts_authswitch/authority-apikeys.ts +85 -0
  114. package/ts_authswitch/authority-contract.ts +167 -29
  115. package/ts_authswitch/authority-overview.ts +7 -0
  116. package/ts_authswitch/authority-placement.ts +12 -4
  117. package/ts_authswitch/authority-runtime-contract.ts +2 -2
  118. package/ts_authswitch/authority-spend.ts +168 -0
  119. package/ts_authswitch/authority-subscription.ts +28 -1
  120. package/ts_authswitch/authority-usagelimit.ts +95 -40
  121. package/ts_authswitch/authority-usagewindows.ts +42 -17
  122. package/ts_authswitch/classes.authoritybroker.ts +161 -68
  123. package/ts_authswitch/classes.authoritycli.ts +108 -32
  124. package/ts_authswitch/classes.authorityclient.ts +41 -10
  125. package/ts_authswitch/classes.authoritydaemon.ts +43 -13
  126. package/ts_authswitch/classes.authoritydatabase.ts +178 -8
  127. package/ts_authswitch/classes.authorityimport.ts +1 -1
  128. package/ts_authswitch/classes.authoritymodels.ts +165 -19
  129. package/ts_authswitch/classes.authorityregistry.ts +3 -0
  130. package/ts_authswitch/classes.authorityusage.ts +171 -10
  131. package/ts_authswitch/classes.claudenative.ts +28 -15
  132. package/ts_authswitch/classes.claudesignin.ts +4 -1
  133. package/ts_authswitch/native-contracts.md +81 -20
  134. package/ts_authswitch/readme.md +1 -1
  135. package/ts_authswitch/testsupport.ts +58 -11
  136. package/ts_authswitch/ts_migration/0011_account_api_key_fields.ts +20 -0
  137. package/ts_authswitch/ts_migration/index.ts +2 -0
  138. package/ts_interfaces/functions.authorityscope.ts +43 -8
  139. package/ts_interfaces/interfaces.ts +1 -1
  140. package/ts_interfaces/plugins.ts +6 -3
  141. package/ts_interfaces/requests.accounts.ts +48 -0
  142. package/ts_interfaces/requests.ts +4 -0
  143. package/ts_web/00_commitinfo_data.ts +1 -1
  144. package/ts_web/accounts.plugins.ts +3 -1
  145. package/ts_web/accounts.templates.ts +20 -11
  146. package/ts_web/classes.accountsreadmodel.ts +322 -70
  147. package/ts_web/elements.accounts.ts +219 -20
  148. package/ts_web/elements.codexconnections.ts +3 -1
  149. package/ts_web/plugins.ts +4 -2
@@ -8,6 +8,9 @@ import { authSwitchCombineUsageObservations, authSwitchObservedUsage,
8
8
  type TAuthSwitchUsageObservation } from './authority-usageheaders.js';
9
9
  import { usageWindowPeriod } from './accounts.js';
10
10
  import type { IAuthSwitchTokenUsage } from './authority-usagetokens.js';
11
+ import { authSwitchSpendPeriod, type IAuthSwitchSpendReading } from './authority-spend.js';
12
+ import type { IAuthSwitchSpendAnswer, TAuthSwitchPaymentProblemCause as TCause } from './authority-contract.js';
13
+ import { authSwitchPaymentProblemHeld } from './authority-subscription.js';
11
14
  import { AuthSwitchUsageEstimator, authSwitchRolledOpenAiLimits, type IAuthSwitchUsageEstimateState,
12
15
  type IAuthSwitchUsageEstimatorOptions, type TAuthSwitchEstimateBasis } from './classes.authorityusageestimate.js';
13
16
 
@@ -16,12 +19,12 @@ type TRateLimits = IOpenAiAccountRateLimits;
16
19
  /**
17
20
  * Why a usage reading is missing or incomplete. `held_in_container`: the Claude login runs in a registered container
18
21
  * home, whose files are the container's; the authority reads its usage again once the login is moved out. `pending`:
19
- * the account's first read has not finished yet.
22
+ * the account's first read has not finished yet. `spend_unavailable`: an API key's spend ledger could not be read.
20
23
  */
21
24
  export type TAuthSwitchUsageProblem =
22
25
  | 'none' | 'unsupported' | 'access_unavailable' | 'provider_unavailable'
23
26
  | 'provider_rejected' | 'profile_unavailable' | 'usage_unavailable'
24
- | 'context_changed' | 'expired' | 'held_in_container' | 'pending';
27
+ | 'context_changed' | 'expired' | 'held_in_container' | 'pending' | 'spend_unavailable';
25
28
 
26
29
  export interface IAuthSwitchNativeUsageSource {
27
30
  homeId: string;
@@ -69,10 +72,18 @@ export interface IAuthSwitchClaudeStatus {
69
72
  rateLimit: { retryAt: string | null } | null;
70
73
  }
71
74
 
72
- export type TAuthSwitchUsageReading =
75
+ /** A usage reading the authority reads from a provider for a subscription sign-in, and stores between its runs. */
76
+ export type TAuthSwitchProviderUsageReading =
73
77
  | { kind: 'openai'; rateLimits: TRateLimits }
74
78
  | { kind: 'anthropic'; status: IAuthSwitchClaudeStatus };
75
79
 
80
+ /**
81
+ * A login's usage reading: a subscription sign-in's, as its provider stated it, or an API key's monthly spend
82
+ * (`spend`), which the authority answers from what AGL's inference proxy reported and never reads from a provider.
83
+ */
84
+ export type TAuthSwitchUsageReading = TAuthSwitchProviderUsageReading
85
+ | { kind: 'spend'; spend: IAuthSwitchSpendReading };
86
+
76
87
  export interface IAuthSwitchUsageAccess {
77
88
  accessToken: string;
78
89
  accountId: string;
@@ -96,10 +107,33 @@ export interface IAuthSwitchUsageAccess {
96
107
  export interface IAuthSwitchPaymentProblem {
97
108
  at: string;
98
109
  subscription: IClaudeSubscription | null;
110
+ /**
111
+ * What the provider refused for (`TAuthSwitchPaymentProblemCause`), present only for `spend_cap` and `quota`, which
112
+ * only an API key's login is held with; absent, the cause is `billing`, on a Claude login and an API key alike. A
113
+ * problem with a cause has no subscription.
114
+ */
115
+ cause?: Exclude<TAuthSwitchPaymentProblemCause, 'billing'>;
116
+ /**
117
+ * When a `spend_cap` lifts by itself: the end of the month in UTC `at` falls in (`authSwitchSpendPeriod(at).end`),
118
+ * present exactly for that cause; once it passed the problem holds nothing (`authSwitchPaymentProblemHeld`). Every
119
+ * other problem holds until it is cleared: a Claude login's by a read, an API key's by a re-check (`usage.recheck`).
120
+ */
121
+ until?: string;
99
122
  }
100
123
 
101
124
  /**
102
- * How a re-check's read went (`AuthSwitchAuthorityUsage.recheck`): `healthy` -- the profile was found, the sign-in
125
+ * Why a provider refused inference on an API key for money, as AGL's inference proxy read the refusal: `billing`, the
126
+ * organization's billing (Anthropic HTTP 402 `billing_error`, and every Claude sign-in's payment problem); `spend_cap`,
127
+ * the monthly spend cap of the key's usage tier, which lifts at 00:00 UTC on the first of the next month (Anthropic 429
128
+ * `enforced_spend_limit_reached`); `quota`, the organization's or project's credit or spend limit (OpenAI 429
129
+ * `insufficient_quota`).
130
+ */
131
+ export type TAuthSwitchPaymentProblemCause = 'billing' | 'spend_cap' | 'quota';
132
+
133
+ /**
134
+ * How a re-check went (`AuthSwitchAuthorityUsage.recheck`). `cleared`: the login is an API key, whose hold a re-check
135
+ * lifts without asking the provider, which states no billing or quota on a model list; the next request shows whether the
136
+ * provider takes the key again. Every other outcome is a Claude login's read: `healthy` -- the profile was found, the sign-in
103
137
  * accepted and the identity matched -- clears a payment problem; every other outcome clears nothing. `credential_rejected`:
104
138
  * Anthropic rejected the sign-in, renewed too. `token_unusable`: no access token usable for a profile read (it could not
105
139
  * be resolved, is not fresh enough, or lacks the `user:profile` scope). `profile_missing`: the profile read failed or its
@@ -108,7 +142,7 @@ export interface IAuthSwitchPaymentProblem {
108
142
  * lies ahead.
109
143
  */
110
144
  export type TAuthSwitchRecheckOutcome = 'healthy' | 'credential_rejected' | 'token_unusable' | 'profile_missing'
111
- | 'mismatch' | 'read_failed';
145
+ | 'mismatch' | 'read_failed' | 'cleared';
112
146
 
113
147
  /** A re-check's answer: the usage the authority holds once its read finished, and how that read went. */
114
148
  export interface IAuthSwitchUsageRecheck {
@@ -120,7 +154,7 @@ export interface IAuthSwitchUsageRecheck {
120
154
  export interface IAuthSwitchStoredUsage {
121
155
  context: IAuthSwitchUsageContext;
122
156
  updateId: string;
123
- reading: TAuthSwitchUsageReading | null;
157
+ reading: TAuthSwitchProviderUsageReading | null;
124
158
  observedAt: string | null;
125
159
  currentUntil: string | null;
126
160
  staleUntil: string | null;
@@ -262,6 +296,15 @@ export interface IAuthSwitchAuthorityUsageOptions {
262
296
  estimate?: IAuthSwitchUsageEstimatorOptions;
263
297
  /** Where the reader's notes go, one line each: standard error, the service's journal, unless set. */
264
298
  log?: (lineArg: string) => void;
299
+ /**
300
+ * An API key's spend ledger: its month's reading at `nowArg`, and a spend report recorded in one transaction
301
+ * (`AuthSwitchAuthorityDatabase.readSpend`, `.recordSpend`). Without it the authority answers no key's usage.
302
+ */
303
+ spend?: {
304
+ read(accountId: string, loginId: string, nowArg: number): Promise<IAuthSwitchSpendReading>;
305
+ record(accountId: string, loginId: string, answers: readonly IAuthSwitchSpendAnswer[],
306
+ nowArg: number): Promise<IAuthSwitchSpendReading>;
307
+ };
265
308
  }
266
309
 
267
310
  /** What a client that served requests on a login reports about the account: see `report`. */
@@ -345,6 +388,15 @@ const usable = (context: IAuthSwitchUsageContext): boolean =>
345
388
  || (context.owner === 'claude_native' && context.nativeSource !== null
346
389
  && context.grantState === 'native'))));
347
390
 
391
+ /** An API key of its account's provider, held or removed: its spend is recorded either way. */
392
+ const apiKeyOfAccount = (context: IAuthSwitchUsageContext): boolean => context.workspaceId === 'api-key'
393
+ && ((context.providerId === 'anthropic' && context.purpose === 'anthropic_api_key')
394
+ || (context.providerId === 'openai' && context.purpose === 'openai_api_key'));
395
+
396
+ /** An API key's login the authority holds: a key nothing else renews. Its usage is its spend ledger. */
397
+ const apiKeyLogin = (context: IAuthSwitchUsageContext): boolean => apiKeyOfAccount(context) && context.owner === 'daemon'
398
+ && context.nativeSource === null;
399
+
348
400
  /** A login read through Claude Code's own home: its context names the home only once that home is opened for a read. */
349
401
  const native = (context: IAuthSwitchUsageContext): boolean => context.providerId === 'anthropic'
350
402
  && context.purpose === 'claude_host_native' && context.owner === 'claude_native' && context.grantState === 'native';
@@ -664,7 +716,8 @@ export class AuthSwitchAuthorityUsage {
664
716
  * carries it. Refused for any login but a Claude login the authority renews, and for one with no usage record of its
665
717
  * sign-in yet, which the proxy never routed to. The provider is not asked.
666
718
  */
667
- public reportPaymentProblem(accountId: string, loginId: string, observedAt: string): Promise<IAuthSwitchUsageSnapshot> {
719
+ public reportPaymentProblem(accountId: string, loginId: string, observedAt: string,
720
+ cause: TCause = 'billing'): Promise<IAuthSwitchUsageSnapshot> {
668
721
  if (this.closed) return Promise.reject(new Error('Authority usage helper is closed.'));
669
722
  return this.admit((async () => {
670
723
  if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId) || !Number.isFinite(Date.parse(observedAt))) {
@@ -672,9 +725,21 @@ export class AuthSwitchAuthorityUsage {
672
725
  }
673
726
  for (let attempt = 1; ; attempt++) {
674
727
  const context = await this.currentContext(accountId, loginId);
728
+ if (context && context.accountId === accountId && context.loginId === loginId && !context.removed
729
+ && apiKeyLogin(context) && context.grantState === 'ready') {
730
+ if (await this.holdApiKey(context, observedAt, cause)) return this.spendAnswer(context);
731
+ if (attempt >= paymentProblemWriteAttempts) {
732
+ throw new AuthSwitchRefusal('account_changed',
733
+ 'The key\'s record kept changing, so its payment problem was not written; report it again.');
734
+ }
735
+ continue;
736
+ }
675
737
  if (!context || context.accountId !== accountId || context.loginId !== loginId || !paymentProblemLogin(context)) {
676
738
  throw new AuthSwitchRefusal('login_unavailable',
677
- 'Only a Claude login the account service renews is held with a payment problem.');
739
+ 'Only a Claude login the account service renews, or an API key it holds, is held with a payment problem.');
740
+ }
741
+ if (cause !== 'billing') {
742
+ throw new AuthSwitchRefusal('login_unavailable', 'A Claude login is held only for a billing error.');
678
743
  }
679
744
  const stored = await this.options.store.read(accountId, loginId);
680
745
  const held = stored && authSwitchSameUsageSource(context, stored.context) ? stored : null;
@@ -714,9 +779,13 @@ export class AuthSwitchAuthorityUsage {
714
779
  throw new Error('Invalid authority usage identity.');
715
780
  }
716
781
  const context = await this.currentContext(accountId, loginId);
782
+ if (context && context.accountId === accountId && context.loginId === loginId && !context.removed
783
+ && apiKeyLogin(context)) {
784
+ return this.clearApiKey(context);
785
+ }
717
786
  if (!context || context.accountId !== accountId || context.loginId !== loginId
718
787
  || context.providerId !== 'anthropic' || context.purpose !== 'claude_host_native') {
719
- throw new AuthSwitchRefusal('login_unavailable', 'Only a Claude login is re-checked.');
788
+ throw new AuthSwitchRefusal('login_unavailable', 'Only a Claude login or an API key is re-checked.');
720
789
  }
721
790
  const key = this.key(accountId, loginId);
722
791
  await this.answer(accountId, loginId, null);
@@ -737,6 +806,97 @@ export class AuthSwitchAuthorityUsage {
737
806
  })());
738
807
  }
739
808
 
809
+ /**
810
+ * Records what answers relayed on an API key's login cost (`authswitch.authority.usage.spend`) and answers the key's
811
+ * usage once the report committed. A removed key's spend is recorded too; its usage then answers unavailable.
812
+ */
813
+ public recordSpend(accountId: string, loginId: string,
814
+ answers: readonly IAuthSwitchSpendAnswer[]): Promise<IAuthSwitchUsageSnapshot> {
815
+ if (this.closed) return Promise.reject(new Error('Authority usage helper is closed.'));
816
+ return this.admit((async () => {
817
+ if (!/^[a-f0-9]{64}$/.test(accountId) || !/^[a-f0-9]{64}$/.test(loginId)) {
818
+ throw new Error('Invalid authority usage identity.');
819
+ }
820
+ const context = await this.currentContext(accountId, loginId);
821
+ const spend = this.options.spend;
822
+ if (!spend || !context || context.accountId !== accountId || context.loginId !== loginId || !apiKeyOfAccount(context)) {
823
+ throw new AuthSwitchRefusal('login_unavailable', 'Only an API key\'s login records spend.');
824
+ }
825
+ await spend.record(accountId, loginId, answers, this.now());
826
+ return this.answer(accountId, loginId, null);
827
+ })());
828
+ }
829
+
830
+ /**
831
+ * An API key's usage: its month's spend from the ledger, current as of now, which stays current until the month ends
832
+ * or a report or a limit change moves it, and the payment problem it is held with, while it holds.
833
+ */
834
+ private async spendAnswer(context: IAuthSwitchUsageContext): Promise<IAuthSwitchUsageSnapshot> {
835
+ const { accountId, loginId } = context;
836
+ if (context.removed || !this.options.spend) {
837
+ return this.unavailable(accountId, loginId, context.removed ? 'access_unavailable' : 'unsupported');
838
+ }
839
+ const now = this.now();
840
+ let spend: IAuthSwitchSpendReading;
841
+ let held: IAuthSwitchStoredUsage | null;
842
+ try {
843
+ [spend, held] = await Promise.all([this.options.spend.read(accountId, loginId, now),
844
+ this.options.store.read(accountId, loginId)]);
845
+ } catch {
846
+ this.log('Authswitch could not read an API key\'s spend; the key is answered without a reading.');
847
+ return this.unavailable(accountId, loginId, 'spend_unavailable');
848
+ }
849
+ const paymentProblem = held && authSwitchSameUsageSource(context, held.context) && held.paymentProblem
850
+ && authSwitchPaymentProblemHeld(held.paymentProblem, now) ? held.paymentProblem : undefined;
851
+ const readAt = timestamp(now);
852
+ return { accountId, loginId, state: 'current', reading: { kind: 'spend', spend }, observedAt: readAt, readAt,
853
+ currentUntil: spend.resetAt, staleUntil: spend.resetAt, lastAttemptAt: null, nextAttemptAt: null,
854
+ lastReadError: null, problem: 'none', ...(paymentProblem ? { paymentProblem } : {}) };
855
+ }
856
+
857
+ /**
858
+ * Holds an API key with a payment problem on its usage record, which holds nothing else: when it was seen -- a time
859
+ * ahead of the authority's clock counts as now -- and its cause; `billing` is written as no cause, and a `spend_cap`
860
+ * lifts at the end of the month it was seen in. False when another write of the record landed first.
861
+ */
862
+ private async holdApiKey(context: IAuthSwitchUsageContext, observedAt: string, cause: TCause): Promise<boolean> {
863
+ const stored = await this.options.store.read(context.accountId, context.loginId);
864
+ const held = stored && authSwitchSameUsageSource(context, stored.context) ? stored : null;
865
+ const now = this.now();
866
+ const at = timestamp(Math.min(Date.parse(observedAt), now));
867
+ const paymentProblem: IAuthSwitchPaymentProblem = cause === 'spend_cap'
868
+ ? { at, subscription: null, cause, until: authSwitchSpendPeriod(Date.parse(at)).end }
869
+ : cause === 'quota' ? { at, subscription: null, cause } : { at, subscription: null };
870
+ const next: IAuthSwitchStoredUsage = { context, updateId: plugins.crypto.randomUUID(), reading: null,
871
+ observedAt: null, currentUntil: null, staleUntil: null, lastAttemptAt: at, nextAttemptAt: at, failures: 0,
872
+ problem: 'none', paymentProblem };
873
+ return this.options.store.publishIfCurrent(context, held && sameContext(context, held.context) ? held.updateId : null,
874
+ next);
875
+ }
876
+
877
+ /**
878
+ * A re-check of an API key: lifts the payment problem it is held with, without asking the provider, whose model list
879
+ * states no billing or quota; the next request shows whether the provider takes the key again.
880
+ */
881
+ private async clearApiKey(context: IAuthSwitchUsageContext): Promise<IAuthSwitchUsageRecheck> {
882
+ for (let attempt = 1; ; attempt++) {
883
+ const stored = await this.options.store.read(context.accountId, context.loginId);
884
+ const held = stored && authSwitchSameUsageSource(context, stored.context) && stored.paymentProblem ? stored : null;
885
+ if (!held) return { usage: await this.spendAnswer(context), outcome: 'cleared' };
886
+ // A record without the member: the store's transition drops a problem the draft does not hold.
887
+ const { paymentProblem: _lifted, ...rest } = held;
888
+ const next: IAuthSwitchStoredUsage = { ...rest, context, updateId: plugins.crypto.randomUUID() };
889
+ if (await this.options.store.publishIfCurrent(context, sameContext(context, held.context) ? held.updateId : null,
890
+ next)) {
891
+ return { usage: await this.spendAnswer(context), outcome: 'cleared' };
892
+ }
893
+ if (attempt >= paymentProblemWriteAttempts) {
894
+ throw new AuthSwitchRefusal('account_changed', 'The key\'s record kept changing, so its hold was not lifted; '
895
+ + 're-check it again.');
896
+ }
897
+ }
898
+ }
899
+
740
900
  /** Logs a Claude subscription status the first time this run reads it, up to `maximumLoggedSubscriptionStatuses`. */
741
901
  private noteSubscriptionStatus(statusArg: string | null): void {
742
902
  if (statusArg === null || this.subscriptionStatusesCapped || this.loggedSubscriptionStatuses.has(statusArg)) return;
@@ -774,6 +934,7 @@ export class AuthSwitchAuthorityUsage {
774
934
  if (context.accountId !== accountId || context.loginId !== loginId) {
775
935
  return this.unavailable(accountId, loginId, 'context_changed');
776
936
  }
937
+ if (apiKeyLogin(context)) return this.spendAnswer(context);
777
938
  if (!readable(context)) {
778
939
  this.unschedule(accountId, loginId);
779
940
  return this.unavailable(accountId, loginId,
@@ -999,7 +1160,7 @@ export class AuthSwitchAuthorityUsage {
999
1160
  previous = latest && sameContext(context, latest.context) ? latest : null;
1000
1161
  held = latest && authSwitchSameUsageSource(context, latest.context) ? latest : null;
1001
1162
  };
1002
- let reading: TAuthSwitchUsageReading | null = null;
1163
+ let reading: TAuthSwitchProviderUsageReading | null = null;
1003
1164
  /** Whether the read stated the account's windows: only then is it a reading the schedule waits a full slot after. */
1004
1165
  let windowsRead = false;
1005
1166
  let rateLimited = false;
@@ -14,11 +14,20 @@ export const CLAUDE_NATIVE_MINIMUM_VERSION = '2.1.273';
14
14
  /**
15
15
  * The releases whose lock table, live credential pickup and native-login selector were read from their own bundle
16
16
  * (native-contracts.md). The usage of an adopted home's login is read from the home only while every Claude Code
17
- * session running there runs one of these or a later release than the newest of them, which is read as that one
18
- * (`claudeNativeReleaseReadsAsVerified`, owner ruling 2026-10-22, AGL-685); an installed release between the floor and
19
- * the newest that is not listed is read from only while no session runs in that home.
17
+ * session running there runs one of these or a release from `CLAUDE_NATIVE_READ_AS_VERIFIED_FROM` on
18
+ * (`claudeNativeReleaseReadsAsVerified`, owner ruling 2026-10-22, AGL-685); an installed release below that line that
19
+ * is not listed is read from only while no session runs in that home. The controller's verified release (2.1.306,
20
+ * AGL-686) and its fleet floor (2.1.282) are listed. 2.1.288, the release AGL's own Claude chats run through the Agent
21
+ * SDK, had these items read from the SDK's binary (AGL-688) and is listed for the usage read only: the fleet never ran
22
+ * it, so it is never the fleet's floor or the verified release.
20
23
  */
21
- export const CLAUDE_NATIVE_VERIFIED_RELEASES: readonly string[] = ['2.1.273', '2.1.282'];
24
+ export const CLAUDE_NATIVE_VERIFIED_RELEASES: readonly string[] = ['2.1.273', '2.1.282', '2.1.288', '2.1.306'];
25
+ /**
26
+ * The line from which the usage read takes any release, listed or not, as verified (owner ruling 2026-10-22, AGL-685):
27
+ * the controller's fleet floor (`claudeCodeFleetFloor`), which a test holds equal to it. It does not move when a newer
28
+ * release is verified.
29
+ */
30
+ export const CLAUDE_NATIVE_READ_AS_VERIFIED_FROM = '2.1.282';
22
31
  /**
23
32
  * The releases whose credential custody contract was read from their own bundle (native-contracts.md): the OAuth
24
33
  * refresh lock and its legacy twin, the credential store's write lock and the global config lock; the store read
@@ -126,7 +135,8 @@ export class ClaudeNativeEffectiveAuthUnsupportedError extends Error {
126
135
  public readonly sessions: readonly IClaudeNativeRefusedSession[] = []) {
127
136
  super((reason === 'unsupported_release'
128
137
  ? `Reading Claude Code's own login requires Claude Code ${CLAUDE_NATIVE_MINIMUM_VERSION} or newer, and a session `
129
- + `running in this home must run a verified release (${CLAUDE_NATIVE_VERIFIED_RELEASES.join(', ')}) or a newer one.`
138
+ + `running in this home must run a verified release (${CLAUDE_NATIVE_VERIFIED_RELEASES.join(', ')}) or any release `
139
+ + `from ${CLAUDE_NATIVE_READ_AS_VERIFIED_FROM} on.`
130
140
  : `Cannot prove that Claude Code uses its native claude.ai login (${reason}).`)
131
141
  + (sessions.length ? ` Refused: ${sessions.map(describeClaudeNativeRefusedSession).join('; ')}.` : ''));
132
142
  }
@@ -251,17 +261,15 @@ const atLeast = (version: [number, number, number], minimum: [number, number, nu
251
261
  const claudeBuildPattern = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-[0-9a-f]{1,64})?$/;
252
262
  /**
253
263
  * Whether a session of this build is read like a verified release when its home's login is read for usage: a listed
254
- * release, or a build of a release no older than the newest listed one, which is read as that one. A hashed build is
255
- * ordered by its release. Custody never uses this: it matches whole build names (`CLAUDE_NATIVE_CUSTODY_RELEASES`).
264
+ * release, or a build of a release from `CLAUDE_NATIVE_READ_AS_VERIFIED_FROM` on. A hashed build is ordered by its
265
+ * release. Custody never uses this: it matches whole build names (`CLAUDE_NATIVE_CUSTODY_RELEASES`).
256
266
  */
257
267
  export const claudeNativeReleaseReadsAsVerified = (build: string | null): boolean => {
258
268
  if (build === null) return false;
259
269
  if (CLAUDE_NATIVE_VERIFIED_RELEASES.includes(build)) return true;
260
270
  const match = claudeBuildPattern.exec(build);
261
271
  if (!match) return false;
262
- const newest = CLAUDE_NATIVE_VERIFIED_RELEASES.map(release => parseClaudeVersion(release)!)
263
- .reduce((left, right) => atLeast(left, right) ? left : right);
264
- return atLeast([Number(match[1]), Number(match[2]), Number(match[3])], newest);
272
+ return atLeast([Number(match[1]), Number(match[2]), Number(match[3])], parseClaudeVersion(CLAUDE_NATIVE_READ_AS_VERIFIED_FROM)!);
265
273
  };
266
274
 
267
275
  /**
@@ -458,10 +466,15 @@ export const claudeNativeCredentialStoreDirectory = (env: NodeJS.ProcessEnv): st
458
466
  ? plugins.path.join(env.HOME, '.claude').normalize('NFC') : null;
459
467
  };
460
468
 
469
+ /**
470
+ * An environment selects this home and keeps its credential store there too: a store elsewhere
471
+ * (`CLAUDE_SECURESTORAGE_CONFIG_DIR`) selects another login, so it is refused as an override.
472
+ */
461
473
  const assertNativeHome = (env: NodeJS.ProcessEnv, configDir: string): void => {
462
- const selected = selectedNativeHome(env);
463
- if (!selected || !plugins.path.isAbsolute(selected) || plugins.path.normalize(selected) !== configDir) {
464
- throw new ClaudeNativeEffectiveAuthUnsupportedError('override');
474
+ for (const selected of [selectedNativeHome(env), claudeNativeCredentialStoreDirectory(env)]) {
475
+ if (!selected || !plugins.path.isAbsolute(selected) || plugins.path.normalize(selected) !== configDir) {
476
+ throw new ClaudeNativeEffectiveAuthUnsupportedError('override');
477
+ }
465
478
  }
466
479
  };
467
480
 
@@ -908,8 +921,8 @@ export class ClaudeNativeAdapter {
908
921
 
909
922
  /**
910
923
  * The whole support check of reading the login Claude Code selects in the home: the home's custody, and the proof
911
- * that Claude Code selects its native login there -- for the daemon's own environment and settings and for every
912
- * process running on the home.
924
+ * that Claude Code selects its native login there -- for the daemon's own environment (its home and its credential
925
+ * store are both this home) and settings, and for every process running on the home.
913
926
  */
914
927
  private assertSupported(): IClaudeNativeSupport {
915
928
  this.assertHomeDirectories();
@@ -107,8 +107,11 @@ export class ClaudeSignInAuthority {
107
107
  const now = this.now();
108
108
  const signIn: IClaudeSignIn = {
109
109
  operation: { id: operationId, state: 'pending',
110
+ // The credential store is pinned to the home as well: one an inherited CLAUDE_SECURESTORAGE_CONFIG_DIR names
111
+ // would take the new login, over whatever login it holds, and the home would never get it.
110
112
  prompt: { flow: 'terminal', directory,
111
- command: `CLAUDE_CONFIG_DIR=${shellWord(directory)} claude ${authSwitchClaudeSignInArguments.join(' ')}` },
113
+ command: `CLAUDE_CONFIG_DIR=${shellWord(directory)} CLAUDE_SECURESTORAGE_CONFIG_DIR=${shellWord(directory)} `
114
+ + `claude ${authSwitchClaudeSignInArguments.join(' ')}` },
112
115
  result: null, error: null, createdAt: new Date(now).toISOString(), updatedAt: new Date(now).toISOString(),
113
116
  expiresAt: new Date(now + signInWindowMs).toISOString(), finishedAt: null, revision: 1 },
114
117
  directory, parent, timer: null, commit: null,
@@ -312,10 +312,11 @@ custody item, so it is on `CLAUDE_NATIVE_CUSTODY_RELEASES`:
312
312
  - Logout `performLogout` (@34545518) clears two account caches more than 2.1.282's, `metricsStatusCache` and
313
313
  `metricsStatusCacheByPrincipal`. Both are now on `CLAUDE_NATIVE_ACCOUNT_CACHES`.
314
314
 
315
- 2.1.288 is not on `CLAUDE_NATIVE_VERIFIED_RELEASES`. The newest entry of that list is the fleet's floor
316
- (`claudeCodeVerifiedRelease`), and the fleet items of the check need Claude Code run. The fleet never runs the SDK's
317
- binary, and the usage read already takes 2.1.288 as 2.1.282. The custody list names releases, so a natively installed
318
- 2.1.288 is admitted like the SDK's build; only the SDK's `linux-x64` binary was read. The SDK's binary is named through
315
+ Its lock table, live pickup and selector are the usage read's items too, so 2.1.288 is on
316
+ `CLAUDE_NATIVE_VERIFIED_RELEASES` (AGL-686) for the usage read only. The fleet never runs the SDK's binary, so its fleet
317
+ items were never confirmed: 2.1.288 is never the fleet's floor (`claudeCodeFleetFloor`) or the verified release
318
+ (`claudeCodeVerifiedRelease`). Both lists name releases, so a natively installed 2.1.288 is admitted like the SDK's
319
+ build; only the SDK's `linux-x64` binary was read. The SDK's binary is named through
319
320
  `CLAUDE_NATIVE_AGENT_SDK_RELEASES` (SDK 0.3.288 bundles 2.1.288), see process detection below.
320
321
 
321
322
  Claude Code 2.1.306 (AGL-686, read 2026-10-22). Linux ELF bundle `~/.local/share/claude/versions/2.1.306`, BuildID
@@ -407,6 +408,11 @@ process to a home by both directories (`claudeNativeHomeRelation`):
407
408
  as `override`.
408
409
  - A process whose home or store cannot be resolved is refused as uninspectable (`running_session` in the usage read).
409
410
  - The container-home and sign-in checks count a process whose store is their home.
411
+ - The usage read requires the accounts service's own environment to keep its store on the home as well
412
+ (`assertNativeHome` applies `claudeNativeCredentialStoreDirectory` to it): a store elsewhere is refused as `override`.
413
+ - A sign-in pins the variable to its home, in the command it prints and when `account add claude` starts Claude Code.
414
+ Inherited, the variable would send the new login to another store, over the login that store holds, while the sign-in
415
+ home never gets a `.credentials.json` and the sign-in expires.
410
416
 
411
417
  Which credential a 2.1.282 process uses (`jc` @16151504, `Cc` @16150518): `ANTHROPIC_AUTH_TOKEN` and `apiKeyHelper`
412
418
  outrank the stored claude.ai login and disable it for requests, though the stored login is still read (`dF`) and side
@@ -423,10 +429,12 @@ Until 38.0.0 a switch of an adopted home proved an installed release above the 2
423
429
  switch: from 38.0.0 on authswitch never moves a login into Claude Code's own home, and reads an adopted home's login for
424
430
  usage only while no session of such a release runs there.
425
431
 
426
- Newer releases (owner ruling 2026-10-22, AGL-685): the usage read takes a release at or after the newest verified one
427
- (2.1.282) as that one (`claudeNativeReleaseReadsAsVerified`), installed or running, and a remote agent's hashed build
428
- (`2.1.295-8b2928c330c4`) is ordered by its release there. An unlisted release between the floor and the newest verified
429
- one stays refused, since its selector was never read. Custody does not follow: it matches whole build names against
432
+ Newer releases (owner ruling 2026-10-22, AGL-685): the usage read takes a listed release and every release from
433
+ 2.1.282 on as verified (`claudeNativeReleaseReadsAsVerified`, line `CLAUDE_NATIVE_READ_AS_VERIFIED_FROM`, held equal to
434
+ the fleet's floor by a test), installed or running, and a remote agent's hashed build (`2.1.295-8b2928c330c4`) is
435
+ ordered by its release there. The line does not move when a newer release is verified (arbiter ruling 2026-10-23,
436
+ AGL-686). An unlisted release below it (2.1.274-2.1.281) stays refused, since its selector was never read. Custody does
437
+ not follow: it matches whole build names against
430
438
  `CLAUDE_NATIVE_CUSTODY_RELEASES`, so a newer release and every hashed build stay refused until their custody contract is
431
439
  read, and the refusal names each process by pid, start and build. AGL mentions each newer release it meets once to the
432
440
  driver, who files the compatibility check below.
@@ -504,17 +512,20 @@ can invalidate the grant. Requests to Claude account endpoints report an HTTP 42
504
512
 
505
513
  ## Claude Code compatibility check
506
514
 
507
- AGL verified Claude Code 2.1.282 (`claudeCodeVerifiedRelease` in `ts/functions.claudecoderelease.ts`, the newest of
508
- `CLAUDE_NATIVE_VERIFIED_RELEASES`). It runs a newer release optimistically -- fleet workers, Claude Code sessions in the
509
- session feed and the usage read -- refuses an older one in the fleet, and keeps custody (handing Claude Code's own
515
+ AGL verified Claude Code 2.1.306 (`claudeCodeVerifiedRelease` in `ts/functions.claudecoderelease.ts`, on
516
+ `CLAUDE_NATIVE_VERIFIED_RELEASES`; 2.1.282 before it). Its fleet's floor is a separate constant, `claudeCodeFleetFloor`
517
+ (2.1.282): the oldest release whose fleet output the tests still pin. It rises only when the adapter stops handling an
518
+ older release's output, never because a newer release was verified (arbiter ruling 2026-10-23, AGL-686). AGL runs a
519
+ release newer than the verified one optimistically -- fleet workers, Claude Code sessions in the session feed and the
520
+ usage read -- refuses one older than the floor in the fleet, and keeps custody (handing Claude Code's own
510
521
  sign-in to the accounts service) refused on it until its custody items were read (`CLAUDE_NATIVE_CUSTODY_RELEASES`, which
511
522
  may take a release before its fleet items are confirmed, as it took 2.1.306). The first time the controller's fleet or configuration probe meets a
512
523
  newer release it records it (`agl_harness_releases`) and publishes one `harness.release.discovered` fleet event to every
513
524
  owner's watch and the built-in Driver, so the driver files a compatibility check for that release on the board. The
514
525
  check works through this list against the release's own bundle (`~/.local/share/claude/versions/<release>`) and
515
- disposable homes, never a credential file. A release that keeps every item moves the verified releases (the controller
516
- constant, `CLAUDE_NATIVE_VERIFIED_RELEASES`, and `CLAUDE_NATIVE_CUSTODY_RELEASES` for the custody items), with its BuildID
517
- and SHA-256 recorded here and the tests that pin 2.1.282 output moved to it.
526
+ disposable homes, never a credential file. A release that keeps every item moves the verified release (the controller
527
+ constant `claudeCodeVerifiedRelease`, `CLAUDE_NATIVE_VERIFIED_RELEASES`, and `CLAUDE_NATIVE_CUSTODY_RELEASES` for the
528
+ custody items), with its BuildID and SHA-256 recorded here and its output added to the tests beside the floor's.
518
529
 
519
530
  An Agent SDK update gets the same check against the binary its platform package ships, the custody and usage-read items
520
531
  at least, since AGL's own chats run that binary on the controller's home. Record the SDK version and the release it
@@ -542,9 +553,9 @@ are marked:
542
553
  - Relay stream-json: `system`/`init` and `result` (`subtype`, `is_error`, `permission_denials`, `api_error_status`,
543
554
  `terminal_reason`, `errors`).
544
555
  - Transcript growth against the 4 MiB window: the fleet reads a transcript's last 4 MiB and refuses an incremental read
545
- of more as an observation gap. 2.1.306 writes `api-request`, `api-request-shape` and `api-request-blob` records, about
546
- 41 % of the bytes after its first record in a small sample on 2026-10-22; measure the share again and check that the
547
- Agent SDK's chain reader (`getSessionMessages`, used for history) skips record types it does not know.
556
+ of more as an observation gap. Measure what the release adds and how much time and how many turns the window still
557
+ covers, and check that the Agent SDK's chain reader (`getSessionMessages`, used for history) skips record types it does
558
+ not know.
548
559
 
549
560
  The managed policy hook (`ts/configuration/policyhook/claude.ts`): the PreToolUse payload's `tool_name` (`Bash`,
550
561
  `mcp__agl__job_start`), `tool_input.command` and `tool_input.argv`, and exit code 2 as a denial. A renamed `tool_name`,
@@ -587,9 +598,59 @@ offsets, as for 2.1.280-2.1.282 above:
587
598
  "diskless" session (`ee()`, a launch option a hosting server sets; off on the host). A new mode like it must not let a
588
599
  process that has a disk refresh without the lock.
589
600
 
590
- 2.1.306's bundle (AGL-686): its custody and usage-read items were read on 2026-10-22, and its fleet items wait for
591
- transcripts that 2.1.306 processes write under AGL 40.4.0. It is on `CLAUDE_NATIVE_CUSTODY_RELEASES`, not yet on
592
- `CLAUDE_NATIVE_VERIFIED_RELEASES` or the fleet's floor.
601
+ 2.1.306's fleet items (AGL-686, 2026-10-23). Sources: its bundle's `strings -n 6` dump (offsets as in its entry above),
602
+ compared with 2.1.282's after renaming minified names; 56 transcripts 2.1.306 processes wrote on this host (6 sessions,
603
+ 50 subagents, 18,732 records with `version` 2.1.306); and the live controller on AGL 40.4.0, which watched the fleet's two
604
+ 2.1.306 driver workers from 23:35 UTC on 2026-10-22. AGL's own parsers (`parseFleetClaudeTranscript`,
605
+ `fleetClaudeAssistantTexts`, `fleetClaudeCredentialFailures`, `fleetClaudePermissionMode`) and the history projection
606
+ read every one of those records, and each file's 4 MiB window, without an error. Every item holds, so 2.1.306 is the
607
+ verified release and on `CLAUDE_NATIVE_VERIFIED_RELEASES`; the fleet's floor stays 2.1.282. Items marked bundle only
608
+ did not occur on 2.1.306 after the upgrade; the first natural occurrence gets a confirming line here, and one that
609
+ contradicts the bundle read gets a fix ticket:
610
+ - Inventory. `printAgentsJson` (@56964267) is 2.1.282's after renaming. A status goes through `p` (@56963968): `idle`,
611
+ `waiting`, anything else `busy`, so the session registry's `shell` reads `busy`. A pid and a status come from the same
612
+ live session entry, and a job's kind is `background`. The controller read the inventory at every poll after the
613
+ upgrade and published no Claude `worker.observation.failed`.
614
+ - `--bg` and wake output, all 2.1.282's:
615
+ - `backgrounded · <id>[ · <name>]` (`lqe` @31980520);
616
+ - the wake note (@31978597), printed only when the job saved options;
617
+ - the copy note (`xn` @31973173, case `own-options`);
618
+ - the message saved for the job's next start (@31977716);
619
+ - `--mcp-config <configs...>` (@35482194), still variadic;
620
+ - the trust key `hasTrustDialogAccepted`.
621
+
622
+ New: `Couldn't tell whether <flag> is a flag or text here, so nothing was started` (@31974605, exit 1) when no `--bg`
623
+ flag can be stripped. The fleet passes `--bg` first, and exit 1 fails visibly. A wake's text is recorded as
624
+ `origin.kind` `human` with string content (three wakes on 2026-10-22). The output itself is bundle only: the fleet
625
+ started, woke and relayed nothing on 2.1.306 after the upgrade.
626
+ - Held and delivery notices (bundle only). `Held peer message — from …` (@47687878) is unchanged. It is still appended to the session's
627
+ transcript as a `system`/`informational` record with `level: "warning"`, built like the release's other transcript
628
+ notices, and the loggable check `Mw` (@25426540) keeps a system record unless it says `persist:false`. Delivery notices (@47169054) still start
629
+ with `[Cross-session delivery notice]`. The hold now reads `… was held by that session. NOT delivered: …` instead of
630
+ `… held for the recipient user's approval …`. No 2.1.306 session held a message here, so neither text is in a 2.1.306
631
+ transcript yet.
632
+ - SendMessage receipts. A peer send by name answers `{success: true, msg_id, message}` (6 in 2.1.306 transcripts) or
633
+ `{success: false, message}` (1). The caveat (@50880854) now reads `; in that session's inbox, not yet read by its
634
+ Claude — a [Cross-session delivery notice] follows if …`. It sits in the tool result, which the relay check skips. The
635
+ tool's input gained `recipient_kind`; `to` and `message` are unchanged.
636
+ - API error records: `isApiErrorMessage: true` with `error` (9 records, `rate_limit`), built at @29824350. The error list
637
+ (@17985651) is 2.1.282's, `authentication_failed` and `oauth_org_not_allowed` included. `apiErrorStatus` and
638
+ `apiErrorIsTransient` are new beside them.
639
+ - `system`/`turn_duration` records: 342 written. The 15 the two driver workers wrote after the upgrade matched the
640
+ controller's 15 `worker.turn.ended` events one for one.
641
+ - `permission-mode` records (no `version` field; 435, one per turn as before), and `user.origin.kind` `human` and `peer`
642
+ with `origin.body` (4 peer messages). Copies keeping their uuids are bundle only: no copy was seen, and the copy path
643
+ of the dispatcher is 2.1.282's.
644
+ - Relay stream-json (bundle only): `system`/`init` (@33617187), and the `result` fields `permission_denials`, `api_error_status`,
645
+ `terminal_reason`, `errors` and `is_api_error_message` are in the bundle. No relay ran on 2.1.306.
646
+ - Transcript growth. `api-request`, `api-request-shape` and `api-request-blob` records are 27 % of the bytes written after
647
+ each file's first 2.1.306 record (26 of 95 MB, three quarters of it blobs), and 26 % of the two driver workers' 4 MiB
648
+ windows.
649
+ - Those windows still cover 4.5 h and 105 turns, and 3.7 h and 61 turns. Without the new records they would cover 4.9 h
650
+ and 4.2 h; two 2.1.282 drivers' windows covered 2.4 h and 7.8 h.
651
+ - The most written within 2 s was 0.7 MB, far from the 4 MiB gap, and every window held a `permission-mode` record.
652
+ - The Agent SDK's chain reader (`jM` in SDK 0.3.288) keeps only `user`, `assistant`, `progress`, `system` and
653
+ `attachment` records with a `uuid`, so it skips the new records.
593
654
 
594
655
  ## Managed Codex app-server contract (Codex 0.157.0, 2026-09-25)
595
656
 
@@ -8,6 +8,6 @@ Keep the authority in its own process. The controller never owns refresh grants.
8
8
 
9
9
  Native credential files belong to their tools: the authority writes one only through a verified, journaled handoff, and never destroys or overwrites a native credential it has not first proven it can restore. Every operation that stops the owned Codex app-server restarts it on both success and failure. Structured authority state uses the existing NoSQLDB models and versioned migrations.
10
10
 
11
- Tests isolate all harness homes and use injected provider/process adapters. Never run account-mutating tests against the developer's own installations. Module import, CLI help/version and the browser leaves (`authority-contract.ts`, `authority-usagewindows.ts`, `authority-usagelimit.ts`, `authority-usageheaders.ts`, `authority-usagetokens.ts`, `authority-placement.ts`, `authority-subscription.ts`, `preuse-text.ts`) must not initialize a controller or touch credentials. `authority-subscription.ts` holds the one map of what Claude's `subscription_status` values mean (empty until a value is confirmed) and the words of a payment problem and a re-check, which the command line and the Accounts screen share; `classes.authorityusage.ts` holds a payment problem on the usage record (`reportPaymentProblem`) and clears it on a read that finds a changed subscription or a healthy re-check (`recheck`); a problem held without a subscription takes the one the next healthy read finds, and that read clears nothing. `authority-placement.ts` is the one rule of an account's lifecycle against its `lapsesAt` and of the placement score the proxy places new sessions by. `authority-usageheaders.ts` reads the usage a provider's answer headers state and joins it to the authority's own reading; the authority holds such observations in memory and answers every usage read with them (`authswitch.authority.usage.observe`). `classes.authorityusage.ts` owns the one usage-read schedule: each account is read from the provider every 10 minutes on the authority's timer and at once after an inference 429, a failed read keeps the held reading `stale` and waits for `retry-after` or the next slot, and no question waits for a read or an unseal. `authority-usagetokens.ts` (browser-safe) weighs the tokens the proxy reports by Anthropic's published prices, and `classes.authorityusageestimate.ts` carries each Claude window forward between reads with a ratio learned per account and window, in memory only. `authority-usagelimit.ts` is the one rule of whether an account's usage, against its `maxUsagePercent`, lets it take work; the proxy, Flex, the CLI and the Accounts screen all call it rather than restating it.
11
+ Tests isolate all harness homes and use injected provider/process adapters. Never run account-mutating tests against the developer's own installations. Module import, CLI help/version and the browser leaves (`authority-contract.ts`, `authority-spend.ts`, `authority-usagewindows.ts`, `authority-usagelimit.ts`, `authority-usageheaders.ts`, `authority-usagetokens.ts`, `authority-placement.ts`, `authority-subscription.ts`, `preuse-text.ts`) must not initialize a controller or touch credentials. `authority-subscription.ts` holds the one map of what Claude's `subscription_status` values mean (empty until a value is confirmed) and the words of a payment problem and a re-check, which the command line and the Accounts screen share; `classes.authorityusage.ts` holds a payment problem on the usage record (`reportPaymentProblem`) and clears it on a read that finds a changed subscription or a healthy re-check (`recheck`); a problem held without a subscription takes the one the next healthy read finds, and that read clears nothing. `authority-placement.ts` is the one rule of an account's lifecycle against its `lapsesAt` and of the placement score the proxy places new sessions by. `authority-usageheaders.ts` reads the usage a provider's answer headers state and joins it to the authority's own reading; the authority holds such observations in memory and answers every usage read with them (`authswitch.authority.usage.observe`). `classes.authorityusage.ts` owns the one usage-read schedule: each account is read from the provider every 10 minutes on the authority's timer and at once after an inference 429, a failed read keeps the held reading `stale` and waits for `retry-after` or the next slot, and no question waits for a read or an unseal. `authority-usagetokens.ts` (browser-safe) weighs the tokens the proxy reports by Anthropic's published prices, and `classes.authorityusageestimate.ts` carries each Claude window forward between reads with a ratio learned per account and window, in memory only. `authority-usagelimit.ts` is the one rule of whether an account's usage, against its `maxUsagePercent`, lets it take work -- a switch on the reading's `kind`, where an API key's `spend` reading is judged by its spend limit alone; the proxy, Flex, the CLI and the Accounts screen all call it rather than restating it. `authority-spend.ts` (imports nothing) is the calendar month in UTC a key's spend counts in (`authSwitchSpendPeriod`, the one calendar function), the spend reading, the spend limit rule and dollar amounts. `authority-apikeys.ts` holds the key verifier (`authSwitchApiKeyVerifier`, a read of the provider's model list) and the guard against a key of the other provider; `AuthSwitchAuthorityDatabase.recordSpend` / `readSpend` keep the monthly ledger (`authswitch_authority_spend_entries`, `authswitch_authority_spend_periods`), and `classes.authorityusage.ts` answers a key's usage from it and holds a key's payment problem, by cause, on the key's usage record. A sealed key is `{ kind: 'anthropicApiKey' | 'openaiApiKey', apiKey }`, one kind per provider; never add a kind a reader must keep forever.
12
12
 
13
13
  Imported from authswitch 11.4.1, commit `c3fed26`; future fixes and releases belong to AGL. OpenAI adaptations retain their headers and the notices in the repository's `third-party-notices.md` and `openai-codex-license.txt`.