@cortexkit/opencode-openai-auth 0.8.0 → 0.9.1
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.
- package/README.md +1 -1
- package/dist/{index-3w4vmckx.js → index-4effjmr6.js} +2 -2
- package/dist/{index-507jh8bv.js → index-9ehhb051.js} +1 -1
- package/dist/index-g3hsdjh4.js +2 -0
- package/dist/index-hh52v8sb.js +2 -0
- package/dist/{index-944gsj1s.js → index-jwn4r3sw.js} +1 -1
- package/dist/{index-02h7g5f9.js → index-sdt2d2ra.js} +1 -1
- package/dist/index-sq36b120.js +211 -0
- package/dist/index.d.ts +172 -341
- package/dist/index.js +65 -65
- package/dist/internal-BTndPmW2.d.ts +914 -0
- package/dist/quota-normalize-08hpp51v.js +1 -0
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +1 -1
- package/dist/rpc/rpc-client.d.ts +2 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state-CoKYKcLn.d.ts +220 -0
- package/dist/sidebar-state.d.ts +2 -172
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-jdvpwk60.js → token-9823y0pc.js} +1 -1
- package/dist/token-util-ypgz7rge.js +1 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +27 -5
- package/package.json +3 -2
- package/src/tui-compiled/raw/core-custody-state.ts +243 -0
- package/src/tui-compiled/raw/sidebar-state.ts +168 -12
- package/src/tui-compiled/raw/tui-command-dialogs.tsx +89 -39
- package/src/tui-compiled/raw/tui.tsx +138 -18
- package/src/tui-compiled/runtime/core-custody-state.ts +243 -0
- package/src/tui-compiled/runtime/sidebar-state.ts +168 -12
- package/src/tui-compiled/runtime/tui-command-dialogs.tsx +70 -33
- package/src/tui-compiled/runtime/tui.tsx +241 -143
- package/src/tui-compiled/shared/logger.ts +9 -2
- package/dist/index-2xppkvkx.js +0 -2
- package/dist/index-nshbgkz1.js +0 -198
- package/dist/index-xyg86jgt.js +0 -2
- package/dist/protocol-Cv9ZmU0p.d.ts +0 -24
- package/dist/quota-normalize-wap0rne8.js +0 -1
- package/dist/token-util-zzm7paz9.js +0 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
export type CustodyMode = 'local' | 'claustrum'
|
|
2
|
+
|
|
3
|
+
export type ManifestCustodyState = 'absent' | 'present' | 'unreadable'
|
|
4
|
+
|
|
5
|
+
export type LocalCustodyState =
|
|
6
|
+
| 'real'
|
|
7
|
+
| 'tombstone'
|
|
8
|
+
| 'empty'
|
|
9
|
+
| 'gone'
|
|
10
|
+
| 'slot-absent'
|
|
11
|
+
|
|
12
|
+
export type VaultCustodyState =
|
|
13
|
+
| 'serves'
|
|
14
|
+
| 'cold'
|
|
15
|
+
| 'needs_reauth'
|
|
16
|
+
| 'identity_mismatch'
|
|
17
|
+
| 'no_handle'
|
|
18
|
+
|
|
19
|
+
export const CUSTODY_INERT_REASONS = [
|
|
20
|
+
'needs-login',
|
|
21
|
+
'mode-mismatch',
|
|
22
|
+
'corrupt-under-binding',
|
|
23
|
+
'manifest-unreadable',
|
|
24
|
+
'unbound-under-claustrum',
|
|
25
|
+
'orphan-tombstone',
|
|
26
|
+
'vault-cold',
|
|
27
|
+
'vault-reauth',
|
|
28
|
+
'identity-mismatch',
|
|
29
|
+
'no-handle',
|
|
30
|
+
'takeover-incomplete',
|
|
31
|
+
'new-local-family-under-claustrum',
|
|
32
|
+
'takeover-incomplete/vault-unavailable',
|
|
33
|
+
'takeover-incomplete/slot-absent',
|
|
34
|
+
] as const
|
|
35
|
+
|
|
36
|
+
export type CustodyInertReason = (typeof CUSTODY_INERT_REASONS)[number]
|
|
37
|
+
|
|
38
|
+
export type CustodyVerdict =
|
|
39
|
+
| { kind: 'LOCAL' }
|
|
40
|
+
| {
|
|
41
|
+
kind: 'VAULT'
|
|
42
|
+
canonicalize?: boolean
|
|
43
|
+
installTombstone?: boolean
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
kind: 'INERT'
|
|
47
|
+
reason: CustodyInertReason
|
|
48
|
+
installTombstone?: boolean
|
|
49
|
+
canonicalize?: boolean
|
|
50
|
+
}
|
|
51
|
+
| { kind: 'NEEDS_LOGIN'; reason?: 'corrupt' }
|
|
52
|
+
|
|
53
|
+
export interface EvaluateCustodyStartupInput {
|
|
54
|
+
mode: CustodyMode
|
|
55
|
+
manifest: ManifestCustodyState
|
|
56
|
+
local: LocalCustodyState
|
|
57
|
+
vault: () => VaultCustodyState
|
|
58
|
+
manifestFailureReason?: string
|
|
59
|
+
fingerprintMatch?: boolean
|
|
60
|
+
verifiedInProcessLogin: boolean
|
|
61
|
+
isMain?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function unreachable(value: never): never {
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function withCanonicalization<
|
|
69
|
+
T extends Extract<CustodyVerdict, { kind: 'VAULT' | 'INERT' }>,
|
|
70
|
+
>(local: LocalCustodyState, verdict: T): T | (T & { canonicalize: true }) {
|
|
71
|
+
return local === 'empty' ? { ...verdict, canonicalize: true } : verdict
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function evaluateLocal(local: LocalCustodyState): CustodyVerdict {
|
|
75
|
+
switch (local) {
|
|
76
|
+
case 'real':
|
|
77
|
+
return { kind: 'LOCAL' }
|
|
78
|
+
case 'tombstone':
|
|
79
|
+
case 'empty':
|
|
80
|
+
case 'slot-absent':
|
|
81
|
+
return { kind: 'NEEDS_LOGIN' }
|
|
82
|
+
case 'gone':
|
|
83
|
+
return { kind: 'NEEDS_LOGIN', reason: 'corrupt' }
|
|
84
|
+
}
|
|
85
|
+
return unreachable(local)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function evaluateLocalWithBinding(
|
|
89
|
+
local: LocalCustodyState,
|
|
90
|
+
verifiedInProcessLogin: boolean,
|
|
91
|
+
): CustodyVerdict {
|
|
92
|
+
switch (local) {
|
|
93
|
+
case 'real':
|
|
94
|
+
return verifiedInProcessLogin
|
|
95
|
+
? { kind: 'LOCAL' }
|
|
96
|
+
: { kind: 'INERT', reason: 'needs-login' }
|
|
97
|
+
case 'tombstone':
|
|
98
|
+
case 'empty':
|
|
99
|
+
case 'slot-absent':
|
|
100
|
+
return { kind: 'INERT', reason: 'mode-mismatch' }
|
|
101
|
+
case 'gone':
|
|
102
|
+
return { kind: 'INERT', reason: 'corrupt-under-binding' }
|
|
103
|
+
}
|
|
104
|
+
return unreachable(local)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function evaluateUnboundClaustrum(local: LocalCustodyState): CustodyVerdict {
|
|
108
|
+
switch (local) {
|
|
109
|
+
case 'tombstone':
|
|
110
|
+
case 'empty':
|
|
111
|
+
return { kind: 'INERT', reason: 'orphan-tombstone' }
|
|
112
|
+
case 'real':
|
|
113
|
+
case 'gone':
|
|
114
|
+
case 'slot-absent':
|
|
115
|
+
return { kind: 'INERT', reason: 'unbound-under-claustrum' }
|
|
116
|
+
}
|
|
117
|
+
return unreachable(local)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function evaluateTombstone(
|
|
121
|
+
local: Extract<LocalCustodyState, 'tombstone' | 'empty'>,
|
|
122
|
+
vault: VaultCustodyState,
|
|
123
|
+
): CustodyVerdict {
|
|
124
|
+
switch (vault) {
|
|
125
|
+
case 'serves':
|
|
126
|
+
return withCanonicalization(local, { kind: 'VAULT' })
|
|
127
|
+
case 'cold':
|
|
128
|
+
return withCanonicalization(local, {
|
|
129
|
+
kind: 'INERT',
|
|
130
|
+
reason: 'vault-cold',
|
|
131
|
+
})
|
|
132
|
+
case 'needs_reauth':
|
|
133
|
+
return withCanonicalization(local, {
|
|
134
|
+
kind: 'INERT',
|
|
135
|
+
reason: 'vault-reauth',
|
|
136
|
+
})
|
|
137
|
+
case 'identity_mismatch':
|
|
138
|
+
return withCanonicalization(local, {
|
|
139
|
+
kind: 'INERT',
|
|
140
|
+
reason: 'identity-mismatch',
|
|
141
|
+
})
|
|
142
|
+
case 'no_handle':
|
|
143
|
+
return withCanonicalization(local, { kind: 'INERT', reason: 'no-handle' })
|
|
144
|
+
}
|
|
145
|
+
return unreachable(vault)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function evaluateReal(
|
|
149
|
+
vault: VaultCustodyState,
|
|
150
|
+
fingerprintMatch: boolean | undefined,
|
|
151
|
+
): CustodyVerdict {
|
|
152
|
+
switch (vault) {
|
|
153
|
+
case 'serves':
|
|
154
|
+
return fingerprintMatch === false
|
|
155
|
+
? { kind: 'INERT', reason: 'new-local-family-under-claustrum' }
|
|
156
|
+
: { kind: 'INERT', reason: 'takeover-incomplete' }
|
|
157
|
+
case 'cold':
|
|
158
|
+
case 'needs_reauth':
|
|
159
|
+
return { kind: 'INERT', reason: 'takeover-incomplete/vault-unavailable' }
|
|
160
|
+
case 'identity_mismatch':
|
|
161
|
+
return { kind: 'INERT', reason: 'identity-mismatch' }
|
|
162
|
+
case 'no_handle':
|
|
163
|
+
return { kind: 'INERT', reason: 'no-handle' }
|
|
164
|
+
}
|
|
165
|
+
return unreachable(vault)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function evaluateGone(vault: VaultCustodyState): CustodyVerdict {
|
|
169
|
+
switch (vault) {
|
|
170
|
+
case 'serves':
|
|
171
|
+
return { kind: 'VAULT', installTombstone: true }
|
|
172
|
+
case 'cold':
|
|
173
|
+
return { kind: 'INERT', reason: 'vault-cold', installTombstone: true }
|
|
174
|
+
case 'needs_reauth':
|
|
175
|
+
return { kind: 'INERT', reason: 'vault-reauth', installTombstone: true }
|
|
176
|
+
case 'identity_mismatch':
|
|
177
|
+
return { kind: 'INERT', reason: 'identity-mismatch' }
|
|
178
|
+
case 'no_handle':
|
|
179
|
+
return { kind: 'INERT', reason: 'no-handle' }
|
|
180
|
+
}
|
|
181
|
+
return unreachable(vault)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function evaluateSlotAbsent(vault: VaultCustodyState): CustodyVerdict {
|
|
185
|
+
switch (vault) {
|
|
186
|
+
case 'serves':
|
|
187
|
+
case 'cold':
|
|
188
|
+
case 'needs_reauth':
|
|
189
|
+
return { kind: 'INERT', reason: 'takeover-incomplete/slot-absent' }
|
|
190
|
+
case 'identity_mismatch':
|
|
191
|
+
return { kind: 'INERT', reason: 'identity-mismatch' }
|
|
192
|
+
case 'no_handle':
|
|
193
|
+
return { kind: 'INERT', reason: 'no-handle' }
|
|
194
|
+
}
|
|
195
|
+
return unreachable(vault)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function evaluatePresentClaustrum(
|
|
199
|
+
local: LocalCustodyState,
|
|
200
|
+
vault: VaultCustodyState,
|
|
201
|
+
fingerprintMatch: boolean | undefined,
|
|
202
|
+
): CustodyVerdict {
|
|
203
|
+
switch (local) {
|
|
204
|
+
case 'tombstone':
|
|
205
|
+
case 'empty':
|
|
206
|
+
return evaluateTombstone(local, vault)
|
|
207
|
+
case 'real':
|
|
208
|
+
return evaluateReal(vault, fingerprintMatch)
|
|
209
|
+
case 'gone':
|
|
210
|
+
return evaluateGone(vault)
|
|
211
|
+
case 'slot-absent':
|
|
212
|
+
return evaluateSlotAbsent(vault)
|
|
213
|
+
}
|
|
214
|
+
return unreachable(local)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function evaluateCustodyStartup(
|
|
218
|
+
input: EvaluateCustodyStartupInput,
|
|
219
|
+
): CustodyVerdict {
|
|
220
|
+
if (input.manifest === 'unreadable') {
|
|
221
|
+
return { kind: 'INERT', reason: 'manifest-unreadable' }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
switch (input.mode) {
|
|
225
|
+
case 'local':
|
|
226
|
+
return input.manifest === 'present'
|
|
227
|
+
? evaluateLocalWithBinding(input.local, input.verifiedInProcessLogin)
|
|
228
|
+
: evaluateLocal(input.local)
|
|
229
|
+
case 'claustrum':
|
|
230
|
+
if (input.manifest === 'absent') {
|
|
231
|
+
return evaluateUnboundClaustrum(input.local)
|
|
232
|
+
}
|
|
233
|
+
if (input.manifest === 'present') {
|
|
234
|
+
return evaluatePresentClaustrum(
|
|
235
|
+
input.local,
|
|
236
|
+
input.vault(),
|
|
237
|
+
input.fingerprintMatch,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
return unreachable(input.manifest)
|
|
241
|
+
}
|
|
242
|
+
return unreachable(input.mode)
|
|
243
|
+
}
|
|
@@ -6,10 +6,25 @@ export interface QuotaWindow {
|
|
|
6
6
|
windowMinutes?: number
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface SpendControlReading {
|
|
10
|
+
limit: number
|
|
11
|
+
used: number
|
|
12
|
+
remaining: number
|
|
13
|
+
usedPercent: number
|
|
14
|
+
remainingPercent: number
|
|
15
|
+
resetsAt?: string
|
|
16
|
+
unit?: string
|
|
17
|
+
source?: string
|
|
18
|
+
reached: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
export interface AccountQuota {
|
|
10
22
|
checkedAt?: number
|
|
11
23
|
primary?: QuotaWindow
|
|
12
24
|
secondary?: QuotaWindow
|
|
25
|
+
spendControl?: SpendControlReading
|
|
26
|
+
// The source reported that no credit budget exists; see OAuthQuotaSnapshot.
|
|
27
|
+
spendControlCleared?: true
|
|
13
28
|
resetCreditsAvailable?: number
|
|
14
29
|
}
|
|
15
30
|
|
|
@@ -88,6 +103,17 @@ export interface SidebarAccountState {
|
|
|
88
103
|
killed: boolean
|
|
89
104
|
enabled: boolean
|
|
90
105
|
resetCredits?: number
|
|
106
|
+
custody?: SidebarAccountCustody
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type SidebarCustodyState = 'vault' | 'needsLogin' | 'local' | 'inert'
|
|
110
|
+
|
|
111
|
+
export type SidebarCustodyReason = CustodyInertReason | 'corrupt'
|
|
112
|
+
|
|
113
|
+
export interface SidebarAccountCustody {
|
|
114
|
+
state: SidebarCustodyState
|
|
115
|
+
reason?: SidebarCustodyReason
|
|
116
|
+
recordVersion?: number
|
|
91
117
|
}
|
|
92
118
|
|
|
93
119
|
export interface ActiveRoutingEntry {
|
|
@@ -132,6 +158,7 @@ export interface SidebarState {
|
|
|
132
158
|
quota: AccountQuota | null
|
|
133
159
|
/** ChatGPT identity of the main account this quota belongs to. */
|
|
134
160
|
mainAccountId?: string
|
|
161
|
+
custody?: SidebarAccountCustody
|
|
135
162
|
killed: boolean
|
|
136
163
|
quotaBackedOff?: boolean
|
|
137
164
|
quotaBackoffUntil?: number
|
|
@@ -156,6 +183,11 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
|
156
183
|
import { tmpdir } from 'node:os'
|
|
157
184
|
import { dirname, join } from 'node:path'
|
|
158
185
|
import { acquireRefreshFileLock } from '../shared/internal.ts'
|
|
186
|
+
import {
|
|
187
|
+
CUSTODY_INERT_REASONS,
|
|
188
|
+
type CustodyInertReason,
|
|
189
|
+
type CustodyVerdict,
|
|
190
|
+
} from './core-custody-state.ts'
|
|
159
191
|
import { createLogger } from './logger.ts'
|
|
160
192
|
|
|
161
193
|
const logSb = createLogger('sidebar')
|
|
@@ -183,6 +215,49 @@ function resetCreditsField(value: unknown): { resetCredits?: number } {
|
|
|
183
215
|
return credits !== undefined ? { resetCredits: credits } : {}
|
|
184
216
|
}
|
|
185
217
|
|
|
218
|
+
const CUSTODY_STATES = new Set<SidebarCustodyState>([
|
|
219
|
+
'vault',
|
|
220
|
+
'needsLogin',
|
|
221
|
+
'local',
|
|
222
|
+
'inert',
|
|
223
|
+
])
|
|
224
|
+
|
|
225
|
+
const CUSTODY_REASONS = new Set<SidebarCustodyReason>([
|
|
226
|
+
...CUSTODY_INERT_REASONS,
|
|
227
|
+
'corrupt',
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Tolerant reader for the per-fallback `custody` projection. Unknown state
|
|
232
|
+
* or reason values are dropped (NOT replaced with a default — a stale state
|
|
233
|
+
* file with an experimental `vaultHealing` value must not silently render
|
|
234
|
+
* as `local`, it must render as no-projection-at-all). Valid values round-
|
|
235
|
+
* trip byte-identical. The output contains only `state` and `reason`.
|
|
236
|
+
*/
|
|
237
|
+
function normalizeSidebarCustody(
|
|
238
|
+
value: unknown,
|
|
239
|
+
): SidebarAccountCustody | undefined {
|
|
240
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
241
|
+
return undefined
|
|
242
|
+
}
|
|
243
|
+
const c = value as Record<string, unknown>
|
|
244
|
+
if (
|
|
245
|
+
typeof c.state !== 'string' ||
|
|
246
|
+
!CUSTODY_STATES.has(c.state as SidebarCustodyState)
|
|
247
|
+
) {
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
const state = c.state as SidebarCustodyState
|
|
251
|
+
const out: SidebarAccountCustody = { state }
|
|
252
|
+
if (
|
|
253
|
+
typeof c.reason === 'string' &&
|
|
254
|
+
CUSTODY_REASONS.has(c.reason as SidebarCustodyReason)
|
|
255
|
+
) {
|
|
256
|
+
out.reason = c.reason as SidebarCustodyReason
|
|
257
|
+
}
|
|
258
|
+
return out
|
|
259
|
+
}
|
|
260
|
+
|
|
186
261
|
function normalizeActiveRouting(value: unknown): ActiveRoutingMap | undefined {
|
|
187
262
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
188
263
|
return undefined
|
|
@@ -276,6 +351,23 @@ export function getSidebarStateFile(): string {
|
|
|
276
351
|
return process.env[STATE_FILE_ENV] || DEFAULT_STATE_FILE
|
|
277
352
|
}
|
|
278
353
|
|
|
354
|
+
export function projectCustodyForSidebar(
|
|
355
|
+
verdict: CustodyVerdict,
|
|
356
|
+
): SidebarAccountCustody {
|
|
357
|
+
switch (verdict.kind) {
|
|
358
|
+
case 'LOCAL':
|
|
359
|
+
return { state: 'local' }
|
|
360
|
+
case 'VAULT':
|
|
361
|
+
return { state: 'vault' }
|
|
362
|
+
case 'INERT':
|
|
363
|
+
return { state: 'inert', reason: verdict.reason }
|
|
364
|
+
case 'NEEDS_LOGIN':
|
|
365
|
+
return verdict.reason === 'corrupt'
|
|
366
|
+
? { state: 'needsLogin', reason: 'corrupt' }
|
|
367
|
+
: { state: 'needsLogin' }
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
279
371
|
export const DEFAULT_SIDEBAR_STATE: SidebarState = {
|
|
280
372
|
main: { quota: null, killed: false },
|
|
281
373
|
fallbacks: [],
|
|
@@ -315,6 +407,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
315
407
|
...(typeof m.mainAccountId === 'string'
|
|
316
408
|
? { mainAccountId: m.mainAccountId }
|
|
317
409
|
: {}),
|
|
410
|
+
...(() => {
|
|
411
|
+
const custody = normalizeSidebarCustody(m.custody)
|
|
412
|
+
return custody ? { custody } : {}
|
|
413
|
+
})(),
|
|
318
414
|
// Preserve optional backoff fields if present
|
|
319
415
|
...(typeof m.quotaBackedOff === 'boolean'
|
|
320
416
|
? { quotaBackedOff: m.quotaBackedOff }
|
|
@@ -347,17 +443,21 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
347
443
|
!Array.isArray(entry) &&
|
|
348
444
|
typeof (entry as Record<string, unknown>).id === 'string',
|
|
349
445
|
)
|
|
350
|
-
.map((e) =>
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
446
|
+
.map((e) => {
|
|
447
|
+
const custody = normalizeSidebarCustody(e.custody)
|
|
448
|
+
return {
|
|
449
|
+
id: e.id as string,
|
|
450
|
+
label: typeof e.label === 'string' ? e.label : undefined,
|
|
451
|
+
...(typeof e.accountId === 'string'
|
|
452
|
+
? { accountId: e.accountId }
|
|
453
|
+
: {}),
|
|
454
|
+
quota: ('quota' in e ? e.quota : null) as AccountQuota | null,
|
|
455
|
+
killed: typeof e.killed === 'boolean' ? e.killed : false,
|
|
456
|
+
enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
|
|
457
|
+
...resetCreditsField(e.resetCredits),
|
|
458
|
+
...(custody ? { custody } : {}),
|
|
459
|
+
}
|
|
460
|
+
})
|
|
361
461
|
: []
|
|
362
462
|
|
|
363
463
|
// activeId — string or undefined
|
|
@@ -428,6 +528,27 @@ export function isUsableRoutingEntry(
|
|
|
428
528
|
)
|
|
429
529
|
}
|
|
430
530
|
|
|
531
|
+
// The credit budget's own exhaustion signal, shared by admission and sticky
|
|
532
|
+
// migration so both agree on what "spent" means. `reached` is the provider's
|
|
533
|
+
// authoritative boolean — the percentage is only a display approximation — and
|
|
534
|
+
// the check fails open on a missing or lapsed reset exactly like a rate-limit
|
|
535
|
+
// window, so a stale or corrupt reading never blocks.
|
|
536
|
+
export function spendControlExhaustedResetAt(
|
|
537
|
+
quota: AccountQuota | null | undefined,
|
|
538
|
+
now = Date.now(),
|
|
539
|
+
): { resetsAt: string; resetAtMs: number } | undefined {
|
|
540
|
+
const spendControl = quota?.spendControl
|
|
541
|
+
if (
|
|
542
|
+
spendControl?.reached !== true ||
|
|
543
|
+
typeof spendControl.resetsAt !== 'string'
|
|
544
|
+
) {
|
|
545
|
+
return undefined
|
|
546
|
+
}
|
|
547
|
+
const resetAtMs = Date.parse(spendControl.resetsAt)
|
|
548
|
+
if (!Number.isFinite(resetAtMs) || resetAtMs <= now) return undefined
|
|
549
|
+
return { resetsAt: spendControl.resetsAt, resetAtMs }
|
|
550
|
+
}
|
|
551
|
+
|
|
431
552
|
// Earliest future reset among the quota's exhausted windows, or undefined when
|
|
432
553
|
// no present window is exhausted. Every present window is evaluated — matching
|
|
433
554
|
// the admission policy, which rejects an account when ANY live window is below
|
|
@@ -454,6 +575,12 @@ export function exhaustedQuotaResetAt(
|
|
|
454
575
|
earliest = { resetsAt: window.resetsAt, resetAtMs }
|
|
455
576
|
}
|
|
456
577
|
}
|
|
578
|
+
// The credit budget is a third axis on its own reset clock (a month, not
|
|
579
|
+
// 5h/7d), judged by the same shared signal sticky migration uses.
|
|
580
|
+
const spendReset = spendControlExhaustedResetAt(quota, now)
|
|
581
|
+
if (spendReset && (!earliest || spendReset.resetAtMs < earliest.resetAtMs)) {
|
|
582
|
+
earliest = spendReset
|
|
583
|
+
}
|
|
457
584
|
return earliest
|
|
458
585
|
}
|
|
459
586
|
|
|
@@ -979,6 +1106,19 @@ function freshestWindow(
|
|
|
979
1106
|
// of the merged result. Only safe when both snapshots share an account identity
|
|
980
1107
|
// (sameAccountIdentity) — an identity switch must whole-pick (freshestQuota) so
|
|
981
1108
|
// windows from two accounts are never combined.
|
|
1109
|
+
// The fresher side decides the budget when it knows anything about it: its own
|
|
1110
|
+
// reading, or its report that no budget exists. Only when it says nothing (a
|
|
1111
|
+
// header or WebSocket push, which never carries spend control) does the other
|
|
1112
|
+
// side's reading survive, and even then not if that side reported none.
|
|
1113
|
+
function mergeSpendControl(
|
|
1114
|
+
fresher: AccountQuota,
|
|
1115
|
+
older: AccountQuota,
|
|
1116
|
+
): SpendControlReading | undefined {
|
|
1117
|
+
if (fresher.spendControl !== undefined) return fresher.spendControl
|
|
1118
|
+
if (fresher.spendControlCleared === true) return undefined
|
|
1119
|
+
return older.spendControl
|
|
1120
|
+
}
|
|
1121
|
+
|
|
982
1122
|
function mergeQuotaByWindow(
|
|
983
1123
|
incoming: AccountQuota | null,
|
|
984
1124
|
existing: AccountQuota | null,
|
|
@@ -1004,6 +1144,13 @@ function mergeQuotaByWindow(
|
|
|
1004
1144
|
incoming.checkedAt,
|
|
1005
1145
|
existing.checkedAt,
|
|
1006
1146
|
)
|
|
1147
|
+
const spendControl = existingSnapshotIsFresher
|
|
1148
|
+
? mergeSpendControl(existing, incoming)
|
|
1149
|
+
: mergeSpendControl(incoming, existing)
|
|
1150
|
+
const spendControlCleared =
|
|
1151
|
+
spendControl === undefined &&
|
|
1152
|
+
(incoming.spendControlCleared === true ||
|
|
1153
|
+
existing.spendControlCleared === true)
|
|
1007
1154
|
let checkedAt: number | undefined
|
|
1008
1155
|
for (const stamp of [
|
|
1009
1156
|
finiteWindowCheckedAt(primary),
|
|
@@ -1013,10 +1160,19 @@ function mergeQuotaByWindow(
|
|
|
1013
1160
|
checkedAt = checkedAt === undefined ? stamp : Math.max(checkedAt, stamp)
|
|
1014
1161
|
}
|
|
1015
1162
|
}
|
|
1163
|
+
// Both budget fields are decided above, so neither may leak in from the
|
|
1164
|
+
// spread: an older incoming budget must not survive a fresher "no budget".
|
|
1165
|
+
const {
|
|
1166
|
+
spendControl: _incomingSpendControl,
|
|
1167
|
+
spendControlCleared: _incomingCleared,
|
|
1168
|
+
...incomingRest
|
|
1169
|
+
} = incoming
|
|
1016
1170
|
return {
|
|
1017
|
-
...
|
|
1171
|
+
...incomingRest,
|
|
1018
1172
|
primary,
|
|
1019
1173
|
secondary,
|
|
1174
|
+
...(spendControl !== undefined ? { spendControl } : {}),
|
|
1175
|
+
...(spendControlCleared ? { spendControlCleared: true as const } : {}),
|
|
1020
1176
|
checkedAt: checkedAt ?? incoming.checkedAt,
|
|
1021
1177
|
}
|
|
1022
1178
|
}
|
|
@@ -619,7 +619,7 @@ export function openCommandDialog(
|
|
|
619
619
|
}
|
|
620
620
|
|
|
621
621
|
if (payload.command === 'openai-account') {
|
|
622
|
-
openAccountDialog(api, apply, sessionId)
|
|
622
|
+
openAccountDialog(api, apply, sessionId, payload)
|
|
623
623
|
return
|
|
624
624
|
}
|
|
625
625
|
|
|
@@ -676,12 +676,71 @@ export function buildAccountDialogRows(
|
|
|
676
676
|
]
|
|
677
677
|
}
|
|
678
678
|
|
|
679
|
+
export function accountDialogModeOption(mode: unknown) {
|
|
680
|
+
const claustrum = mode === 'claustrum'
|
|
681
|
+
return {
|
|
682
|
+
title: claustrum ? 'Leave Claustrum' : 'Enter Claustrum',
|
|
683
|
+
value: claustrum ? '__local__' : '__claustrum__',
|
|
684
|
+
description: claustrum
|
|
685
|
+
? 'Require fresh local logins before local refresh resumes'
|
|
686
|
+
: 'Verify every enabled account before custody takes over',
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function buildFallbackAccountOptions(input: {
|
|
691
|
+
id: string
|
|
692
|
+
label?: string
|
|
693
|
+
enabled: boolean
|
|
694
|
+
index: number
|
|
695
|
+
count: number
|
|
696
|
+
}) {
|
|
697
|
+
const name = input.label ?? input.id
|
|
698
|
+
const options: Array<{
|
|
699
|
+
title: string
|
|
700
|
+
value: string
|
|
701
|
+
description: string
|
|
702
|
+
}> = [
|
|
703
|
+
{
|
|
704
|
+
title: input.enabled ? 'Disable' : 'Enable',
|
|
705
|
+
value: input.enabled ? 'disable' : 'enable',
|
|
706
|
+
description: `${input.enabled ? 'Disable' : 'Enable'} ${name}`,
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
title: 'Remove',
|
|
710
|
+
value: 'remove',
|
|
711
|
+
description: `Remove ${name}`,
|
|
712
|
+
},
|
|
713
|
+
]
|
|
714
|
+
if (input.index > 0) {
|
|
715
|
+
options.push({
|
|
716
|
+
title: 'Move up',
|
|
717
|
+
value: 'move_up',
|
|
718
|
+
description: 'Swap with the preceding fallback',
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
if (input.index < input.count - 1) {
|
|
722
|
+
options.push({
|
|
723
|
+
title: 'Move down',
|
|
724
|
+
value: 'move_down',
|
|
725
|
+
description: 'Swap with the following fallback',
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
options.push({
|
|
729
|
+
title: 'Back',
|
|
730
|
+
value: 'back',
|
|
731
|
+
description: 'Return to account list',
|
|
732
|
+
})
|
|
733
|
+
return options
|
|
734
|
+
}
|
|
735
|
+
|
|
679
736
|
function openAccountDialog(
|
|
680
737
|
api: TuiPluginApi,
|
|
681
738
|
apply: ApplyFn,
|
|
682
739
|
sessionId?: string,
|
|
740
|
+
payload?: OpenDialogPayload,
|
|
683
741
|
) {
|
|
684
742
|
const DialogConfirm = api.ui.DialogConfirm
|
|
743
|
+
let claustrumMode = payload?.knobs.claustrumMode
|
|
685
744
|
|
|
686
745
|
function showL1() {
|
|
687
746
|
void getSidebarState().then((state) => {
|
|
@@ -691,6 +750,7 @@ function openAccountDialog(
|
|
|
691
750
|
<DialogSelectInner
|
|
692
751
|
title='OpenAI Accounts'
|
|
693
752
|
options={[
|
|
753
|
+
accountDialogModeOption(claustrumMode),
|
|
694
754
|
...buildAccountDialogRows(state, sessionId),
|
|
695
755
|
{
|
|
696
756
|
title: 'Add account\u2026',
|
|
@@ -699,6 +759,19 @@ function openAccountDialog(
|
|
|
699
759
|
},
|
|
700
760
|
]}
|
|
701
761
|
onSelect={(option) => {
|
|
762
|
+
if (
|
|
763
|
+
option.value === '__claustrum__' ||
|
|
764
|
+
option.value === '__local__'
|
|
765
|
+
) {
|
|
766
|
+
const args =
|
|
767
|
+
option.value === '__claustrum__' ? 'claustrum' : 'local'
|
|
768
|
+
void apply('openai-account', args).then((r) => {
|
|
769
|
+
api.ui.toast({ message: r.text })
|
|
770
|
+
claustrumMode = r.knobs.claustrumMode
|
|
771
|
+
showL1()
|
|
772
|
+
})
|
|
773
|
+
return
|
|
774
|
+
}
|
|
702
775
|
if (option.value === '__add__') {
|
|
703
776
|
showAddFlow()
|
|
704
777
|
return
|
|
@@ -722,44 +795,12 @@ function openAccountDialog(
|
|
|
722
795
|
showL1()
|
|
723
796
|
return
|
|
724
797
|
}
|
|
725
|
-
const options
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
options.push({
|
|
732
|
-
title: 'Remove',
|
|
733
|
-
value: 'remove',
|
|
734
|
-
description: `Remove ${fb.label ?? fb.id}`,
|
|
735
|
-
})
|
|
736
|
-
|
|
737
|
-
if (fbIndex > 0) {
|
|
738
|
-
const neighbor = state.fallbacks[fbIndex - 1]
|
|
739
|
-
if (neighbor) {
|
|
740
|
-
options.push({
|
|
741
|
-
title: 'Move up',
|
|
742
|
-
value: 'move_up',
|
|
743
|
-
description: `Swap with ${neighbor.label ?? neighbor.id}`,
|
|
744
|
-
})
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
if (fbIndex < state.fallbacks.length - 1) {
|
|
749
|
-
const neighbor = state.fallbacks[fbIndex + 1]
|
|
750
|
-
if (neighbor) {
|
|
751
|
-
options.push({
|
|
752
|
-
title: 'Move down',
|
|
753
|
-
value: 'move_down',
|
|
754
|
-
description: `Swap with ${neighbor.label ?? neighbor.id}`,
|
|
755
|
-
})
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
options.push({
|
|
760
|
-
title: 'Back',
|
|
761
|
-
value: 'back',
|
|
762
|
-
description: 'Return to account list',
|
|
798
|
+
const options = buildFallbackAccountOptions({
|
|
799
|
+
id: fb.id,
|
|
800
|
+
label: fb.label,
|
|
801
|
+
enabled: fb.enabled,
|
|
802
|
+
index: fbIndex,
|
|
803
|
+
count: state.fallbacks.length,
|
|
763
804
|
})
|
|
764
805
|
|
|
765
806
|
api.ui.dialog.setSize('xlarge')
|
|
@@ -785,6 +826,15 @@ function openAccountDialog(
|
|
|
785
826
|
))
|
|
786
827
|
return
|
|
787
828
|
}
|
|
829
|
+
if (option.value === 'enable' || option.value === 'disable') {
|
|
830
|
+
void apply('openai-account', `${option.value} ${id}`).then(
|
|
831
|
+
(r) => {
|
|
832
|
+
api.ui.toast({ message: r.text })
|
|
833
|
+
showL1()
|
|
834
|
+
},
|
|
835
|
+
)
|
|
836
|
+
return
|
|
837
|
+
}
|
|
788
838
|
if (option.value === 'move_up') {
|
|
789
839
|
const neighbor = state.fallbacks[fbIndex - 1]
|
|
790
840
|
if (neighbor) {
|