@bitkyc08/opencodex 2.83.0 → 2.84.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.
- package/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +19 -0
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export.ts +46 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +311 -171
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +24 -15
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +14 -14
- package/src/providers/registry/entries-extended.ts +0 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +13 -0
- package/src/server/lifecycle.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +5 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/responses/adapter-dispatch.ts +232 -79
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -4
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +6 -8
- package/src/server/responses/sidecar-execution.ts +11 -2
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/usage/expected-prices.ts +4 -0
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
package/src/link/supervisor.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { createSshRunner, type SshChild, type SshRunner } from "./ssh-runner";
|
|
|
7
7
|
import {
|
|
8
8
|
classifySshStderr,
|
|
9
9
|
dueForSpawn,
|
|
10
|
+
failedTunnel,
|
|
11
|
+
HOME_TUNNEL_RETRY_POLICY,
|
|
10
12
|
IDLE,
|
|
11
13
|
reduceTunnel,
|
|
12
14
|
type TunnelState,
|
|
@@ -33,6 +35,13 @@ export interface LinkSupervisor {
|
|
|
33
35
|
ensureStarted(): Promise<void>;
|
|
34
36
|
reload(): Promise<void>;
|
|
35
37
|
stopLink(linkId: string): Promise<void>;
|
|
38
|
+
/** Reserve a link against reconnects and automatic spawns for a removal transaction. */
|
|
39
|
+
holdLink?(linkId: string): () => void;
|
|
40
|
+
/**
|
|
41
|
+
* Restart one Home-initiated tunnel now: stop its child, forget the backoff or failed state,
|
|
42
|
+
* and spawn a fresh attempt. False when the link is not a Home-initiated link of this store.
|
|
43
|
+
*/
|
|
44
|
+
reconnect(linkId: string): Promise<boolean>;
|
|
36
45
|
status(): readonly LinkTunnelStatus[];
|
|
37
46
|
notifyAuthenticatedRequest?(apiKeyId: string): void;
|
|
38
47
|
stop(): Promise<void>;
|
|
@@ -67,6 +76,20 @@ export interface LinkSupervisorDeps {
|
|
|
67
76
|
|
|
68
77
|
const TIMER_MS = 1_000;
|
|
69
78
|
const SPAWN_GRACE_MS = 5_000;
|
|
79
|
+
/**
|
|
80
|
+
* A retry runs while the network may still be down, and ssh can wait out its whole ConnectTimeout
|
|
81
|
+
* (10 s for the TCP connect, the SSH handshake and key exchange, see ssh-argv.ts) before it exits.
|
|
82
|
+
* A retry is promoted only once it has outlived that, so an attempt that never reached the Child
|
|
83
|
+
* does not read connected and restart the outage clock. Like the first attempt's five seconds,
|
|
84
|
+
* liveness is a heuristic: authentication and the forward request come after ConnectTimeout, so
|
|
85
|
+
* one that stalls past the grace reads connected until ssh exits.
|
|
86
|
+
*/
|
|
87
|
+
const RETRY_GRACE_MS = 15_000;
|
|
88
|
+
|
|
89
|
+
/** A retry whose ssh child is still running: reconnecting, or failed under the retry policy. */
|
|
90
|
+
function retryInFlight(state: TunnelState): boolean {
|
|
91
|
+
return (state.kind === "reconnecting" || state.kind === "failed") && state.inFlight === true;
|
|
92
|
+
}
|
|
70
93
|
|
|
71
94
|
function sameArgv(left: readonly string[], right: readonly string[]): boolean {
|
|
72
95
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
@@ -129,10 +152,19 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
129
152
|
let started = false;
|
|
130
153
|
let stopping = false;
|
|
131
154
|
let lifecycleFlight: Promise<void> | undefined;
|
|
155
|
+
let lifecycleRevision = 0;
|
|
132
156
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
133
157
|
const states = new Map<string, TunnelState>();
|
|
134
|
-
const children = new Map<string, { child: SshChild; argv: readonly string[] }>();
|
|
158
|
+
const children = new Map<string, { child: SshChild; argv: readonly string[]; spawnedAt: number }>();
|
|
159
|
+
/** Per-link generation, advanced by spawns and explicit stop/removal reservations. */
|
|
160
|
+
const generations = new Map<string, number>();
|
|
161
|
+
const linkRevisions = new Map<string, number>();
|
|
162
|
+
/** Reconnects in flight, so overlapping requests for one link restart it once. */
|
|
163
|
+
const reconnects = new Map<string, Promise<boolean>>();
|
|
164
|
+
const holds = new Map<string, number>();
|
|
135
165
|
const orphanUnverified = new Set<string>();
|
|
166
|
+
/** The generation of the child whose exit is still being classified, per link. */
|
|
167
|
+
const exiting = new Map<string, number>();
|
|
136
168
|
const recordInstances = new Map<string, string>();
|
|
137
169
|
|
|
138
170
|
const reconcileApiKeys = (): void => {
|
|
@@ -187,14 +219,17 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
187
219
|
};
|
|
188
220
|
|
|
189
221
|
const setEvent = (linkId: string, event: Parameters<typeof reduceTunnel>[1]): TunnelState => {
|
|
190
|
-
const next = reduceTunnel(states.get(linkId) ?? IDLE, event, deps.random);
|
|
222
|
+
const next = reduceTunnel(states.get(linkId) ?? IDLE, event, deps.random, HOME_TUNNEL_RETRY_POLICY);
|
|
191
223
|
states.set(linkId, next);
|
|
192
224
|
return next;
|
|
193
225
|
};
|
|
194
226
|
|
|
195
227
|
const spawnFor = (record: LinkRecord): void => {
|
|
196
|
-
if (stopping || record.direction !== "hub-initiated" || children.has(record.id)) return;
|
|
197
|
-
|
|
228
|
+
if (stopping || holds.has(record.id) || record.direction !== "hub-initiated" || children.has(record.id)) return;
|
|
229
|
+
// Only a fresh link or a due attempt spawns. A reload must neither skip the backoff nor start
|
|
230
|
+
// a second child while an exited one is still reporting its stderr.
|
|
231
|
+
const prior = states.get(record.id);
|
|
232
|
+
if (prior && prior.kind !== "idle" && !dueForSpawn(prior, now())) return;
|
|
198
233
|
if (store.listenerPort === null) {
|
|
199
234
|
states.set(record.id, { kind: "failed", since: now(), reason: "forward" });
|
|
200
235
|
return;
|
|
@@ -209,7 +244,11 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
209
244
|
knownHostsFile: linkKnownHostsPath(),
|
|
210
245
|
});
|
|
211
246
|
const child = runner.spawnTunnel(argv);
|
|
212
|
-
|
|
247
|
+
const generation = (generations.get(record.id) ?? 0) + 1;
|
|
248
|
+
generations.set(record.id, generation);
|
|
249
|
+
// An older child's exit still being classified no longer speaks for this link.
|
|
250
|
+
exiting.delete(record.id);
|
|
251
|
+
children.set(record.id, { child, argv, spawnedAt: now() });
|
|
213
252
|
orphanUnverified.delete(record.id);
|
|
214
253
|
setEvent(record.id, { type: "spawn", now: now() });
|
|
215
254
|
writePidfile(pidfilePath(record.id), { version: 1, linkId: record.id, pid: child.pid, argv });
|
|
@@ -218,9 +257,17 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
218
257
|
children.delete(record.id);
|
|
219
258
|
conditionalRemovePidfile(record.id, child.pid);
|
|
220
259
|
if (stopping) return;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
exiting.set(record.id, generation);
|
|
261
|
+
try {
|
|
262
|
+
let stderr = "";
|
|
263
|
+
// An unreadable stderr (the runner rejects it past its size cap) still reports the exit.
|
|
264
|
+
try { stderr = child.stderr ? await child.stderr : ""; } catch { /* classified as unknown */ }
|
|
265
|
+
// A reconnect may have started a fresh child meanwhile; its own lifecycle owns the state.
|
|
266
|
+
if (generations.get(record.id) !== generation) return;
|
|
267
|
+
setEvent(record.id, { type: "exit", now: now(), stderrClass: classifySshStderr(stderr) });
|
|
268
|
+
} finally {
|
|
269
|
+
if (exiting.get(record.id) === generation) exiting.delete(record.id);
|
|
270
|
+
}
|
|
224
271
|
}).catch(() => {
|
|
225
272
|
if (children.get(record.id)?.child !== child) return;
|
|
226
273
|
children.delete(record.id);
|
|
@@ -228,23 +275,40 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
228
275
|
if (!stopping) setEvent(record.id, { type: "exit", now: now(), stderrClass: "network" });
|
|
229
276
|
});
|
|
230
277
|
} catch {
|
|
231
|
-
|
|
278
|
+
const at = now();
|
|
279
|
+
states.set(record.id, failedTunnel("forward", at, HOME_TUNNEL_RETRY_POLICY, prior?.kind === "failed" ? prior.since : at));
|
|
232
280
|
}
|
|
233
281
|
};
|
|
234
282
|
|
|
235
283
|
const tick = (): void => {
|
|
236
|
-
if (stopping) return;
|
|
284
|
+
if (stopping || lifecycleFlight) return;
|
|
237
285
|
const current = now();
|
|
238
286
|
for (const record of store.links) {
|
|
239
287
|
if (record.direction !== "hub-initiated") continue;
|
|
240
|
-
|
|
288
|
+
if (holds.has(record.id)) continue;
|
|
289
|
+
// The exit is still being classified. Failing the link on the outage clock now would make
|
|
290
|
+
// a later host-key or auth exit land on a failed state that ignores it.
|
|
291
|
+
if (exiting.has(record.id)) continue;
|
|
292
|
+
const before = states.get(record.id) ?? IDLE;
|
|
241
293
|
const child = children.get(record.id);
|
|
242
|
-
|
|
294
|
+
// Promotion is checked before the outage limit, so a child that has outlived its grace by the
|
|
295
|
+
// time this tick runs (at the limit, or late after a sleep) is promoted rather than killed.
|
|
296
|
+
// An idle Child may send no catalog request for hours; for a retry, staying up past the
|
|
297
|
+
// retry grace is the proof instead, as five seconds is for a first attempt.
|
|
298
|
+
if (child && ((before.kind === "connecting" && current - before.since >= SPAWN_GRACE_MS)
|
|
299
|
+
|| (retryInFlight(before) && current - child.spawnedAt >= RETRY_GRACE_MS))) {
|
|
300
|
+
setEvent(record.id, { type: "ready", now: current });
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const next = setEvent(record.id, { type: "tick", now: current });
|
|
304
|
+
if (next.kind === "failed" && !next.inFlight && child && retryInFlight(before)) {
|
|
305
|
+
// A retry started just before the outage limit has not had its grace yet. It carries on as
|
|
306
|
+
// the failed state's retry instead of being killed with a link that may already be back.
|
|
307
|
+
states.set(record.id, { ...next, inFlight: true });
|
|
308
|
+
} else if (next.kind === "failed" && !next.inFlight && child) {
|
|
243
309
|
child.child.kill("SIGTERM");
|
|
244
310
|
children.delete(record.id);
|
|
245
311
|
conditionalRemovePidfile(record.id, child.child.pid);
|
|
246
|
-
} else if (next.kind === "connecting" && child && current - next.since >= SPAWN_GRACE_MS) {
|
|
247
|
-
setEvent(record.id, { type: "ready", now: current });
|
|
248
312
|
} else if (dueForSpawn(next, current)) {
|
|
249
313
|
spawnFor(record);
|
|
250
314
|
}
|
|
@@ -273,37 +337,45 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
273
337
|
syncTimer();
|
|
274
338
|
};
|
|
275
339
|
|
|
276
|
-
let queuedLifecycle: (() => void | Promise<void>) | undefined;
|
|
277
340
|
const runLifecycle = (operation: () => void | Promise<void>): Promise<void> => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
lifecycleFlight = (async () => {
|
|
283
|
-
let next: (() => void | Promise<void>) | undefined = operation;
|
|
284
|
-
while (next) {
|
|
285
|
-
await next();
|
|
286
|
-
next = queuedLifecycle;
|
|
287
|
-
queuedLifecycle = undefined;
|
|
288
|
-
}
|
|
289
|
-
})().finally(() => {
|
|
290
|
-
lifecycleFlight = undefined;
|
|
291
|
-
queuedLifecycle = undefined;
|
|
341
|
+
// Every caller keeps its own operation and completion; no later reload can replace a stop.
|
|
342
|
+
const flight = (lifecycleFlight ?? Promise.resolve()).catch(() => {}).then(operation).finally(() => {
|
|
343
|
+
if (lifecycleFlight === flight) lifecycleFlight = undefined;
|
|
292
344
|
});
|
|
293
|
-
|
|
345
|
+
lifecycleFlight = flight;
|
|
346
|
+
return flight;
|
|
294
347
|
};
|
|
295
348
|
|
|
296
|
-
const
|
|
349
|
+
const stopChild = async (linkId: string): Promise<void> => {
|
|
297
350
|
const current = children.get(linkId);
|
|
298
351
|
if (!current) {
|
|
299
352
|
states.set(linkId, IDLE);
|
|
300
353
|
return;
|
|
301
354
|
}
|
|
355
|
+
const generation = generations.get(linkId);
|
|
302
356
|
children.delete(linkId);
|
|
303
357
|
current.child.kill("SIGTERM");
|
|
304
358
|
await current.child.exited;
|
|
305
359
|
conditionalRemovePidfile(linkId, current.child.pid);
|
|
306
|
-
|
|
360
|
+
// A stop or removal reservation made while this child exited owns the next state.
|
|
361
|
+
if (generations.get(linkId) === generation) states.set(linkId, IDLE);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const invalidateLink = (linkId: string): void => {
|
|
365
|
+
linkRevisions.set(linkId, (linkRevisions.get(linkId) ?? 0) + 1);
|
|
366
|
+
generations.set(linkId, (generations.get(linkId) ?? 0) + 1);
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const holdLink = (linkId: string): (() => void) => {
|
|
370
|
+
holds.set(linkId, (holds.get(linkId) ?? 0) + 1);
|
|
371
|
+
invalidateLink(linkId);
|
|
372
|
+
let released = false;
|
|
373
|
+
return () => {
|
|
374
|
+
if (released) return;
|
|
375
|
+
released = true;
|
|
376
|
+
const remaining = (holds.get(linkId) ?? 1) - 1;
|
|
377
|
+
if (remaining) holds.set(linkId, remaining); else holds.delete(linkId);
|
|
378
|
+
};
|
|
307
379
|
};
|
|
308
380
|
|
|
309
381
|
return {
|
|
@@ -315,6 +387,7 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
315
387
|
return runLifecycle(begin);
|
|
316
388
|
},
|
|
317
389
|
async reload() {
|
|
390
|
+
lifecycleRevision += 1;
|
|
318
391
|
await runLifecycle(async () => {
|
|
319
392
|
if (stopping) return;
|
|
320
393
|
if (!started) {
|
|
@@ -332,7 +405,7 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
332
405
|
store = nextStore;
|
|
333
406
|
for (const [linkId] of [...children]) {
|
|
334
407
|
if (nextInstances.has(linkId) && !changed.has(linkId)) continue;
|
|
335
|
-
await
|
|
408
|
+
await stopChild(linkId);
|
|
336
409
|
states.delete(linkId);
|
|
337
410
|
orphanUnverified.delete(linkId);
|
|
338
411
|
recordInstances.delete(linkId);
|
|
@@ -353,12 +426,42 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
353
426
|
syncTimer();
|
|
354
427
|
});
|
|
355
428
|
},
|
|
356
|
-
stopLink
|
|
429
|
+
stopLink(linkId: string) {
|
|
430
|
+
invalidateLink(linkId);
|
|
431
|
+
return runLifecycle(() => stopChild(linkId));
|
|
432
|
+
},
|
|
433
|
+
holdLink,
|
|
434
|
+
reconnect(linkId: string) {
|
|
435
|
+
const inFlight = reconnects.get(linkId);
|
|
436
|
+
if (inFlight) return inFlight;
|
|
437
|
+
const linkRevision = linkRevisions.get(linkId);
|
|
438
|
+
let restarted = false;
|
|
439
|
+
const flight = runLifecycle(async () => {
|
|
440
|
+
if (stopping || !started || holds.has(linkId) || linkRevisions.get(linkId) !== linkRevision) return;
|
|
441
|
+
const record = store.links.find(link => link.id === linkId && link.direction === "hub-initiated");
|
|
442
|
+
if (!record) return;
|
|
443
|
+
// A preceding reload may have spawned a new generation; it is the current one we drain.
|
|
444
|
+
const generation = generations.get(linkId);
|
|
445
|
+
const revision = lifecycleRevision;
|
|
446
|
+
const instance = recordInstance(record, store.listenerPort);
|
|
447
|
+
await stopChild(linkId);
|
|
448
|
+
const latest = readStore();
|
|
449
|
+
const fresh = latest.links.find(link => link.id === linkId && link.direction === "hub-initiated");
|
|
450
|
+
if (stopping || holds.has(linkId) || generations.get(linkId) !== generation
|
|
451
|
+
|| lifecycleRevision !== revision || !fresh || recordInstance(fresh, latest.listenerPort) !== instance) return;
|
|
452
|
+
spawnFor(fresh);
|
|
453
|
+
restarted = true;
|
|
454
|
+
}).then(() => restarted).finally(() => { reconnects.delete(linkId); });
|
|
455
|
+
reconnects.set(linkId, flight);
|
|
456
|
+
return flight;
|
|
457
|
+
},
|
|
357
458
|
notifyAuthenticatedRequest(apiKeyId: string) {
|
|
358
459
|
for (const record of store.links) {
|
|
359
460
|
if (record.direction === "hub-initiated" && record.apiKeyId === apiKeyId && children.has(record.id)) {
|
|
360
461
|
const state = states.get(record.id);
|
|
361
|
-
if (state?.kind === "connecting" || state?.kind === "reconnecting"
|
|
462
|
+
if (state?.kind === "connecting" || state?.kind === "reconnecting" || (state && retryInFlight(state))) {
|
|
463
|
+
setEvent(record.id, { type: "ready", now: now() });
|
|
464
|
+
}
|
|
362
465
|
}
|
|
363
466
|
}
|
|
364
467
|
},
|
|
@@ -383,20 +486,12 @@ export function createLinkSupervisor(deps: LinkSupervisorDeps = {}): LinkSupervi
|
|
|
383
486
|
clearTimer(timer);
|
|
384
487
|
timer = undefined;
|
|
385
488
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
await current.child.exited;
|
|
393
|
-
conditionalRemovePidfile(linkId, current.child.pid);
|
|
394
|
-
states.set(linkId, IDLE);
|
|
395
|
-
}));
|
|
396
|
-
for (const record of store.links) {
|
|
397
|
-
if (record.direction === "hub-initiated") states.set(record.id, IDLE);
|
|
398
|
-
}
|
|
399
|
-
if (lifecycleFlight) await lifecycleFlight;
|
|
489
|
+
await runLifecycle(async () => {
|
|
490
|
+
await Promise.all([...children.keys()].map(stopChild));
|
|
491
|
+
for (const record of store.links) {
|
|
492
|
+
if (record.direction === "hub-initiated") states.set(record.id, IDLE);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
400
495
|
},
|
|
401
496
|
};
|
|
402
497
|
}
|
package/src/link/tunnel-state.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* - reconnecting: a transient failure; requests through the link fail with 503 meanwhile, and a
|
|
7
7
|
* new attempt is due at `retryAt`.
|
|
8
8
|
* - failed: auth, host key and forward failures, and a link that stayed down for FAILED_AFTER_MS.
|
|
9
|
-
* Without a retry policy (the Home's `-R` supervisor
|
|
10
|
-
* own `-L` tunnel) a reason that has a delay is tried again at `retryAt`, and
|
|
11
|
-
* runs with `inFlight` while the state still reads failed; a reason without a
|
|
12
|
-
* terminal.
|
|
9
|
+
* Without a retry policy it needs the user. With one (the Home's `-R` supervisor and the Child's
|
|
10
|
+
* own `-L` tunnel both run with one) a reason that has a delay is tried again at `retryAt`, and
|
|
11
|
+
* the retry attempt runs with `inFlight` while the state still reads failed; a reason without a
|
|
12
|
+
* delay stays terminal.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
export type TunnelFailure = "auth" | "hostkey" | "forward" | "timeout";
|
|
@@ -48,6 +48,13 @@ export const CLIENT_TUNNEL_RETRY_POLICY: TunnelRetryPolicy = {
|
|
|
48
48
|
retryFailedAfterMs: { timeout: 60_000, forward: 60_000, auth: 5 * 60_000 },
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The Home's `-R` supervisor dials the Child's sshd on the same cadence and limits. A sleep, an
|
|
53
|
+
* outage longer than FAILED_AFTER_MS, or a forward the Child's sshd still holds for the session
|
|
54
|
+
* that just died heals about a minute after it clears, instead of waiting for a proxy restart.
|
|
55
|
+
*/
|
|
56
|
+
export const HOME_TUNNEL_RETRY_POLICY: TunnelRetryPolicy = CLIENT_TUNNEL_RETRY_POLICY;
|
|
57
|
+
|
|
51
58
|
export const IDLE: TunnelState = { kind: "idle" };
|
|
52
59
|
|
|
53
60
|
/** Capped exponential backoff with ±20% jitter. `random` is injectable for tests. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { detectClaudeCodeToken, hasClaudeCredentialContinuity } from "./local-token-detect";
|
|
2
2
|
import { normalizeAnthropicIdentity, resolveAnthropicAccountIdentity, type AnthropicIdentityResolver } from "./anthropic-identity";
|
|
3
3
|
import { credentialGeneration, type AuthStore } from "./store";
|
|
4
|
+
import { mergeOAuthPlan } from "./plan";
|
|
4
5
|
import { REFRESH_SKEW_MS } from "./refresh-policy";
|
|
5
6
|
import type { OAuthCredentials, ProviderAccountSet } from "./types";
|
|
6
7
|
import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
@@ -30,7 +31,7 @@ export async function newerClaudeCredentialForInstance(
|
|
|
30
31
|
const before = normalizeAnthropicIdentity(stored.anthropicIdentity, stored.access);
|
|
31
32
|
if (hasClaudeCredentialContinuity(stored, disk)) {
|
|
32
33
|
return { kind: "adopt", diskGeneration: credentialGeneration(disk), credential: {
|
|
33
|
-
...stored, ...disk, anthropicIdentity: disk.access === stored.access ? before : undefined,
|
|
34
|
+
...stored, ...disk, plan: mergeOAuthPlan(disk, stored), anthropicIdentity: disk.access === stored.access ? before : undefined,
|
|
34
35
|
} };
|
|
35
36
|
}
|
|
36
37
|
// Independent observations share a deadline through the caller signal; neither extends the other.
|
|
@@ -42,7 +43,7 @@ export async function newerClaudeCredentialForInstance(
|
|
|
42
43
|
if (!oldIdentity || !newIdentity) return { kind: "unresolved", diskGeneration };
|
|
43
44
|
if (oldIdentity.accountUuid !== newIdentity.accountUuid) return { kind: "different", diskGeneration };
|
|
44
45
|
return { kind: "adopt", diskGeneration: credentialGeneration(disk), credential: {
|
|
45
|
-
...stored, ...disk, accountId: newIdentity.accountUuid, anthropicIdentity: newIdentity,
|
|
46
|
+
...stored, ...disk, accountId: newIdentity.accountUuid, anthropicIdentity: newIdentity, plan: newProof?.plan ?? null,
|
|
46
47
|
} };
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -75,5 +76,5 @@ export function captureAnthropicCredentialOwnerForInstance(instance: AnthropicIn
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function identityMetadata(credential: OAuthCredentials): string {
|
|
78
|
-
return JSON.stringify([credential.source, credential.accountId, credential.email, credential.anthropicIdentity]);
|
|
79
|
+
return JSON.stringify([credential.source, credential.accountId, credential.email, credential.anthropicIdentity, credential.plan]);
|
|
79
80
|
}
|
|
@@ -4,12 +4,13 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
5
5
|
import { CLAUDE_CLI_USER_AGENT } from "../providers/claude-cli-identity";
|
|
6
6
|
import type { OAuthCredentials } from "./types";
|
|
7
|
+
import { normalizeOAuthPlan } from "./plan";
|
|
7
8
|
|
|
8
9
|
const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
9
10
|
const PROFILE_TIMEOUT_MS = 10_000;
|
|
10
11
|
const PROFILE_MAX_BYTES = 65_536;
|
|
11
12
|
export type AnthropicIdentity = NonNullable<OAuthCredentials["anthropicIdentity"]>;
|
|
12
|
-
export type AnthropicIdentityResolver = (access: string, signal?: AbortSignal) => Promise<AnthropicIdentity | undefined>;
|
|
13
|
+
export type AnthropicIdentityResolver = (access: string, signal?: AbortSignal) => Promise<(AnthropicIdentity & { plan?: string }) | undefined>;
|
|
13
14
|
|
|
14
15
|
export function bindAnthropicIdentity(access: string, accountUuid: unknown): AnthropicIdentity | undefined {
|
|
15
16
|
if (!access || typeof accountUuid !== "string" || !accountUuid.length || accountUuid.length > 128
|
|
@@ -48,8 +49,18 @@ export const resolveAnthropicAccountIdentity: AnthropicIdentityResolver = async
|
|
|
48
49
|
const data: unknown = JSON.parse(body.text);
|
|
49
50
|
if (!data || typeof data !== "object" || !("account" in data)) return undefined;
|
|
50
51
|
const account = data.account;
|
|
51
|
-
|
|
52
|
+
const identity = account && typeof account === "object" && "uuid" in account
|
|
52
53
|
? bindAnthropicIdentity(access, account.uuid) : undefined;
|
|
54
|
+
if (!identity) return undefined;
|
|
55
|
+
const organization = "organization" in data ? data.organization : undefined;
|
|
56
|
+
const org = organization && typeof organization === "object"
|
|
57
|
+
? organization as Record<string, unknown> : undefined;
|
|
58
|
+
const strip = (value: unknown, prefix: string) => {
|
|
59
|
+
const label = normalizeOAuthPlan(value);
|
|
60
|
+
return label ? normalizeOAuthPlan(label.startsWith(prefix) ? label.slice(prefix.length) : label) : undefined;
|
|
61
|
+
};
|
|
62
|
+
const plan = strip(org?.rate_limit_tier, "default_claude_") ?? strip(org?.organization_type, "claude_");
|
|
63
|
+
return { ...identity, ...(plan ? { plan } : {}) };
|
|
53
64
|
} catch {
|
|
54
65
|
// Unavailable identity is an explicit unresolved result, never an ownership guess.
|
|
55
66
|
return undefined;
|
|
@@ -105,10 +105,22 @@ function createAnthropicModelQuota(instance: AnthropicInstanceId) {
|
|
|
105
105
|
if (!entry || !anthropicFamilyRejected(id, model, now)) return undefined;
|
|
106
106
|
return entry.busy ? now + 1000 : Math.min(entry.resetAt ?? Infinity, entry.seenAt + ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS);
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The requested family's exclusion as a usage-limit fact: whether a revalidation send is in flight
|
|
110
|
+
* (its answer may reopen the family within the second) and the upstream window reset, when known.
|
|
111
|
+
* The admission deadline `anthropicFamilyRetryAt` is not that reset: passive evidence also expires
|
|
112
|
+
* after thirty minutes.
|
|
113
|
+
*/
|
|
114
|
+
function anthropicFamilyHoldState(id: string, model?: string): { revalidating: boolean; resetAt?: number } | undefined {
|
|
115
|
+
const family = anthropicModelFamily(model);
|
|
116
|
+
const entry = family && ownedObservation(id, family);
|
|
117
|
+
return entry ? { revalidating: entry.busy, ...(entry.resetAt !== undefined ? { resetAt: entry.resetAt } : {}) } : undefined;
|
|
118
|
+
}
|
|
108
119
|
return Object.freeze({ instance, anthropicModelExhausted, anthropicFamilyQuotaGeneration,
|
|
109
120
|
captureAnthropicFamilyQuotaGeneration,
|
|
110
121
|
observeAnthropicFamilyQuota, anthropicFamilyRejected, claimAnthropicFamilyRevalidation,
|
|
111
|
-
clearAnthropicRequestedFamilyQuota, clearAnthropicFamilyQuota, anthropicFamilyRetryAt,
|
|
122
|
+
clearAnthropicRequestedFamilyQuota, clearAnthropicFamilyQuota, anthropicFamilyRetryAt, anthropicFamilyHoldState,
|
|
123
|
+
reconcileAnthropicFamilyQuota });
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
export type AnthropicModelQuota = ReturnType<typeof createAnthropicModelQuota>;
|