@bitkyc08/opencodex 2.43.0 → 2.44.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 (138) hide show
  1. package/README.md +25 -17
  2. package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
  3. package/gui/dist/assets/index-ltx3L-WS.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/command-code.ts +23 -2
  7. package/src/adapters/cursor/native-exec.ts +63 -11
  8. package/src/adapters/cursor/tool-definitions.ts +1 -1
  9. package/src/adapters/cursor/tool-schemas.ts +59 -0
  10. package/src/adapters/kiro.ts +54 -8
  11. package/src/adapters/openai-chat.ts +23 -6
  12. package/src/adapters/openai-responses.ts +3 -1
  13. package/src/adapters/opencode-go.ts +35 -0
  14. package/src/claude/agents-inject.ts +12 -5
  15. package/src/claude/desktop-3p-library.ts +89 -0
  16. package/src/claude/desktop-3p.ts +166 -95
  17. package/src/claude/desktop-discovery-inputs.ts +44 -0
  18. package/src/claude/desktop-profile.ts +1 -1
  19. package/src/claude/desktop-remote-store-artifact.ts +185 -0
  20. package/src/claude/desktop-remote-store-io.ts +91 -0
  21. package/src/claude/desktop-remote-store-state.ts +127 -0
  22. package/src/claude/desktop-remote-store.ts +269 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound-records.ts +7 -0
  25. package/src/claude/inbound.ts +1 -1
  26. package/src/claude/model-info.ts +16 -3
  27. package/src/cli/account-api.ts +7 -1
  28. package/src/cli/aside-profiles.ts +17 -0
  29. package/src/cli/capabilities.ts +39 -0
  30. package/src/cli/claude-agent-startup-sync.ts +3 -1
  31. package/src/cli/claude-desktop.ts +127 -18
  32. package/src/cli/connect.ts +27 -14
  33. package/src/cli/dispatch.ts +30 -16
  34. package/src/cli/export-command.ts +3 -1
  35. package/src/cli/index.ts +28 -16
  36. package/src/cli/integrations.ts +46 -12
  37. package/src/cli/opencode.ts +11 -4
  38. package/src/cli/runtime-api.ts +7 -2
  39. package/src/cli/uninstall-client-state.ts +78 -0
  40. package/src/client/connect.ts +476 -223
  41. package/src/client/hub-client.ts +81 -0
  42. package/src/client/hub-relay.ts +38 -6
  43. package/src/client/lifecycle-lock.ts +129 -0
  44. package/src/client/state.ts +71 -42
  45. package/src/clients/aside-profiles.ts +224 -0
  46. package/src/codex/account-lifecycle.ts +5 -9
  47. package/src/codex/auth-api.ts +50 -4
  48. package/src/codex/catalog/metadata.ts +0 -2
  49. package/src/codex/catalog/provider-fetch.ts +7 -5
  50. package/src/codex/catalog/sync.ts +88 -24
  51. package/src/codex/convergence.ts +2 -0
  52. package/src/codex/inject.ts +45 -14
  53. package/src/codex/journal.ts +6 -6
  54. package/src/codex/quota-refresh-outcome.ts +27 -0
  55. package/src/codex/shim.ts +7 -1
  56. package/src/combos/resolve.ts +10 -7
  57. package/src/config.ts +18 -5
  58. package/src/generated/compatibility-version.json +209 -105
  59. package/src/images/loop.ts +5 -2
  60. package/src/integrations/aside-profile-context.ts +270 -0
  61. package/src/integrations/aside-profile-journal.ts +229 -0
  62. package/src/integrations/aside-profiles.ts +176 -0
  63. package/src/integrations/catalog-refresh.ts +40 -0
  64. package/src/integrations/config-io.ts +16 -1
  65. package/src/integrations/owned-refresh.ts +14 -2
  66. package/src/integrations/state.ts +3 -1
  67. package/src/integrations/writer.ts +3 -1
  68. package/src/lab/fabric/producer-isolate.ts +84 -46
  69. package/src/lib/account-selection-events.ts +32 -0
  70. package/src/lib/errors.ts +9 -1
  71. package/src/lib/provider-outbound.ts +2 -45
  72. package/src/lib/proxy-env.ts +67 -0
  73. package/src/oauth/account-quota-rank.ts +9 -1
  74. package/src/oauth/anthropic-routing.ts +91 -23
  75. package/src/oauth/generic-account-failover.ts +15 -53
  76. package/src/oauth/index.ts +67 -68
  77. package/src/oauth/pool-settings-capability.ts +2 -1
  78. package/src/oauth/store.ts +91 -7
  79. package/src/oauth/types.ts +8 -0
  80. package/src/providers/api-key-selection.ts +110 -0
  81. package/src/providers/api-keys.ts +38 -31
  82. package/src/providers/context-cap.ts +27 -3
  83. package/src/providers/key-failover.ts +27 -17
  84. package/src/providers/provider-id-rewrite.ts +10 -8
  85. package/src/providers/registry.ts +1 -0
  86. package/src/responses/custom-tool-compat.ts +10 -0
  87. package/src/responses/function-call-compat.ts +173 -0
  88. package/src/responses/namespace-tool-compat.ts +27 -4
  89. package/src/responses/parser.ts +8 -0
  90. package/src/responses/task-input.ts +36 -0
  91. package/src/responses/tool-name-aliases.ts +79 -0
  92. package/src/router.ts +2 -0
  93. package/src/routing/capability.ts +10 -3
  94. package/src/routing/compatibility/assemble.ts +16 -1
  95. package/src/routing/evaluator.ts +5 -1
  96. package/src/server/auth-cors.ts +3 -0
  97. package/src/server/chat-native.ts +44 -9
  98. package/src/server/claude-messages.ts +71 -36
  99. package/src/server/grok-responses-snapshot-repair.ts +333 -0
  100. package/src/server/index.ts +26 -10
  101. package/src/server/management/account-selection-stream.ts +70 -0
  102. package/src/server/management/aside-profile-routes.ts +231 -0
  103. package/src/server/management/config-routes.ts +36 -41
  104. package/src/server/management/context.ts +3 -0
  105. package/src/server/management/integration-routes.ts +61 -3
  106. package/src/server/management/logs-usage-routes.ts +1 -0
  107. package/src/server/management/model-routes.ts +20 -9
  108. package/src/server/management/model-rows.ts +10 -2
  109. package/src/server/management/oauth-account-routes.ts +7 -0
  110. package/src/server/management/provider-routes.ts +10 -8
  111. package/src/server/management/route-registry.ts +20 -2
  112. package/src/server/management-auth.ts +16 -0
  113. package/src/server/relay-eager.ts +17 -5
  114. package/src/server/relay.ts +43 -4
  115. package/src/server/request-log.ts +29 -1
  116. package/src/server/responses/agent-task-recovery-cache.ts +11 -0
  117. package/src/server/responses/agent-task-recovery.ts +24 -4
  118. package/src/server/responses/codex-ws-pool.ts +4 -4
  119. package/src/server/responses/codex-ws-session.ts +2 -2
  120. package/src/server/responses/collaboration.ts +2 -1
  121. package/src/server/responses/combo-stream-preflight.ts +26 -7
  122. package/src/server/responses/core.ts +668 -151
  123. package/src/server/responses/fetch-helpers.ts +6 -1
  124. package/src/server/responses/ws-upstream.ts +8 -3
  125. package/src/server/responses-custom-tool-repair.ts +29 -9
  126. package/src/server/responses-function-tool-repair.ts +183 -0
  127. package/src/server/responses-snapshot-codec.ts +14 -0
  128. package/src/server/responses-snapshot-repair.ts +1 -13
  129. package/src/server/responses-undeclared-tool-guard.ts +1 -68
  130. package/src/server/sse-payload-rewrite.ts +3 -1
  131. package/src/service.ts +6 -2
  132. package/src/types/config.ts +23 -16
  133. package/src/types/provider.ts +14 -3
  134. package/src/types/request.ts +2 -0
  135. package/src/usage/log.ts +9 -0
  136. package/src/web-search/loop.ts +5 -2
  137. package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
  138. package/gui/dist/assets/index-Djowl68T.js +0 -112
@@ -0,0 +1,91 @@
1
+ import { chmodSync, lstatSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { atomicWriteFile, getConfigDir } from "../config";
4
+ import { hardenSecretDir } from "../lib/windows-secret-acl";
5
+ import { isRecord, resolveDesktop3pConfigLibraryPath, SAFE_DESKTOP_PROFILE_ID, type Desktop3pMetadata } from "./desktop-3p-library";
6
+ import { DesktopStoreError, digest } from "./desktop-remote-store-state";
7
+
8
+ export const MAX_DESKTOP_METADATA_ENTRIES = 256;
9
+
10
+ export interface JsonFile { value: Record<string, unknown>; hash: string; identity: string }
11
+ const identity = (s: NonNullable<ReturnType<typeof lstatSync>>): string => `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}`;
12
+ function missing(error: unknown): boolean { return (error as NodeJS.ErrnoException)?.code === "ENOENT"; }
13
+ export function canonicalDirectory(path: string): string {
14
+ const absolute = resolve(path);
15
+ try {
16
+ const stat = lstatSync(absolute);
17
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new DesktopStoreError("unsafe");
18
+ return realpathSync(absolute);
19
+ } catch (error) {
20
+ if (!missing(error)) throw error;
21
+ const parent = dirname(absolute);
22
+ return parent === absolute ? absolute : join(canonicalDirectory(parent), basename(absolute));
23
+ }
24
+ }
25
+ export function storePaths() {
26
+ const home = canonicalDirectory(getConfigDir());
27
+ const library = canonicalDirectory(resolveDesktop3pConfigLibraryPath());
28
+ const root = canonicalDirectory(join(home, "desktop-remote"));
29
+ return { home, library, root, state: join(root, "state.json"), baseline: join(root, "baseline.json"), disconnect: join(root, "disconnect.json") };
30
+ }
31
+ export class StoreIO {
32
+ changed = false;
33
+ private bytes = 0;
34
+ private readonly seenSizes = new Map<string, number>();
35
+ read(path: string, maxBytes = 1024 * 1024, privateFile = false): JsonFile | null {
36
+ let stat: ReturnType<typeof lstatSync>;
37
+ try { stat = lstatSync(path); }
38
+ catch (error) { if (missing(error)) return null; throw new DesktopStoreError("unsafe"); }
39
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxBytes
40
+ || (privateFile && process.platform !== "win32" && (stat.mode & 0o077) !== 0)) throw new DesktopStoreError("unsafe");
41
+ const counted = this.seenSizes.get(path) ?? 0;
42
+ this.bytes += Math.max(0, stat.size - counted);
43
+ this.seenSizes.set(path, Math.max(counted, stat.size));
44
+ if (this.bytes > 8 * 1024 * 1024) throw new DesktopStoreError("unsafe");
45
+ try {
46
+ const bytes = readFileSync(path);
47
+ if (bytes.byteLength > maxBytes || identity(lstatSync(path)) !== identity(stat)) throw new DesktopStoreError("conflict");
48
+ const value: unknown = JSON.parse(bytes.toString("utf8"));
49
+ if (!isRecord(value)) throw new DesktopStoreError("unsafe");
50
+ return { value, hash: digest(bytes.toString("utf8")), identity: identity(stat) };
51
+ } catch (error) { if (error instanceof DesktopStoreError) throw error; throw new DesktopStoreError("unsafe"); }
52
+ }
53
+ ensureDirectory(path: string): void {
54
+ canonicalDirectory(path);
55
+ mkdirSync(path, { recursive: true, mode: 0o700 });
56
+ canonicalDirectory(path);
57
+ chmodSync(path, 0o700);
58
+ if (process.platform === "win32") hardenSecretDir(path, { required: true });
59
+ }
60
+ compare(path: string, expected: JsonFile | null): void {
61
+ const fresh = this.read(path);
62
+ if (fresh?.hash !== expected?.hash || fresh?.identity !== expected?.identity) throw new DesktopStoreError("conflict");
63
+ }
64
+ write(path: string, value: unknown, expected: JsonFile | null, maxBytes = 1024 * 1024): void {
65
+ const bytes = JSON.stringify(value, null, 2) + "\n";
66
+ if (Buffer.byteLength(bytes) > maxBytes) throw new DesktopStoreError("unsafe");
67
+ this.compare(path, expected);
68
+ this.ensureDirectory(dirname(path));
69
+ atomicWriteFile(path, bytes, undefined, { validateBeforeRename: () => this.compare(path, expected) });
70
+ this.changed = true;
71
+ }
72
+ remove(path: string, expected: JsonFile): void {
73
+ this.compare(path, expected);
74
+ unlinkSync(path);
75
+ this.changed = true;
76
+ }
77
+ }
78
+ export function metadata(io: StoreIO, library: string): { file: JsonFile | null; value: Desktop3pMetadata } {
79
+ const file = io.read(join(library, "_meta.json"));
80
+ if (!file) return { file, value: { entries: [] } };
81
+ const value = file.value;
82
+ if (!Array.isArray(value.entries) || value.entries.length > MAX_DESKTOP_METADATA_ENTRIES) throw new DesktopStoreError("unsafe");
83
+ const seen = new Set<string>();
84
+ for (const entry of value.entries) {
85
+ if (!isRecord(entry) || typeof entry.id !== "string" || !SAFE_DESKTOP_PROFILE_ID.test(entry.id)
86
+ || seen.has(entry.id) || typeof entry.name !== "string") throw new DesktopStoreError("unsafe");
87
+ seen.add(entry.id);
88
+ }
89
+ if (value.appliedId !== undefined && (typeof value.appliedId !== "string" || !seen.has(value.appliedId))) throw new DesktopStoreError("unsafe");
90
+ return { file, value: value as unknown as Desktop3pMetadata };
91
+ }
@@ -0,0 +1,127 @@
1
+ import { createHash } from "node:crypto";
2
+ import { OPENCODEX_DESKTOP_PROFILE_KEYS, SAFE_DESKTOP_PROFILE_ID, isRecord } from "./desktop-3p-library";
3
+
4
+ export type DesktopRemoteOwner = { serverUrl: string; apiKeyId: string; connectedAt: string };
5
+ export type DesktopStoreResult =
6
+ | { ok: true; changed: boolean; status: "absent" | "applied" | "updated" | "restored";
7
+ baselineKind?: "known" | "standard_fallback";
8
+ restoration?: "owned_projection" | "standard_fallback" | "selection_preserved";
9
+ retainedForeignData?: boolean; restartRequired: boolean; path?: string; fingerprint?: string }
10
+ | { ok: false; changed: boolean; reason: "busy" | "conflict" | "unsafe" | "recovery_required" | "cleanup_pending" | "desired_disabled" };
11
+ export type StoreReason = Extract<DesktopStoreResult, { ok: false }>["reason"];
12
+ export class DesktopStoreError extends Error {
13
+ constructor(readonly reason: StoreReason) { super(`desktop_store_${reason}`); }
14
+ }
15
+ export type Projection = Record<string, unknown>;
16
+ export interface Baseline {
17
+ version: 1; owner: DesktopRemoteOwner; home: string; library: string; targetId: string;
18
+ kind: "known" | "standard_fallback"; targetExisted: boolean; projection: Projection;
19
+ priorSelection: { id: string; hash: string } | null;
20
+ }
21
+ export interface ArtifactPending {
22
+ kind: "apply" | "rotate" | "restore"; before: string; after: string;
23
+ beforeSelection: string | null; afterSelection: string | null;
24
+ tokenFingerprint: string;
25
+ }
26
+ export interface StoreState {
27
+ version: 1; owner: DesktopRemoteOwner; home: string; library: string; targetId: string;
28
+ baselineRef: "baseline.json"; baselineHash: string; baselineKind: Baseline["kind"];
29
+ phase: "prepared" | "active" | "restored" | "cleaned";
30
+ lastProjectionHash: string; tokenFingerprint: string; pending?: ArtifactPending;
31
+ }
32
+ export interface DesktopDisconnectReceipt {
33
+ version: 1; owner: DesktopRemoteOwner; tokenFingerprint: string; keepCatalog: boolean;
34
+ phase: "prepared" | "desktop_restored" | "catalog_settled" | "removing_token" | "token_removed"
35
+ | "clearing_connection" | "connection_cleared" | "complete";
36
+ desktopAfterFingerprint?: string;
37
+ catalogAfter?: { kind: "absent" } | { kind: "file"; fingerprint: string };
38
+ }
39
+ export const DISCONNECT_PHASES = ["prepared", "desktop_restored", "catalog_settled", "removing_token", "token_removed", "clearing_connection", "connection_cleared", "complete"] as const;
40
+ export function canonical(value: unknown): string {
41
+ const sort = (v: unknown): unknown => Array.isArray(v) ? v.map(sort) : isRecord(v)
42
+ ? Object.fromEntries(Object.keys(v).sort().map(k => [k, sort(v[k])])) : v;
43
+ const text = JSON.stringify(sort(value));
44
+ if (text === undefined) throw new DesktopStoreError("unsafe");
45
+ return text;
46
+ }
47
+ export function digest(value: string): string { return createHash("sha256").update(value).digest("hex"); }
48
+ export function projection(value: Record<string, unknown>): Projection {
49
+ return Object.fromEntries(Object.entries(value).filter(([key]) => OPENCODEX_DESKTOP_PROFILE_KEYS.has(key)));
50
+ }
51
+ export function projectionHash(value: Record<string, unknown> | null): string {
52
+ return digest(canonical(value === null ? null : projection(value)));
53
+ }
54
+ export function mergeProjection(current: Record<string, unknown>, owned: Projection): Record<string, unknown> {
55
+ return { ...Object.fromEntries(Object.entries(current).filter(([k]) => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(k))), ...owned };
56
+ }
57
+ export function sameOwner(a: DesktopRemoteOwner, b: DesktopRemoteOwner): boolean { return canonical(a) === canonical(b); }
58
+ export function exact(value: Record<string, unknown>, keys: string[]): void {
59
+ if (Object.keys(value).some(k => !keys.includes(k))) throw new DesktopStoreError("unsafe");
60
+ }
61
+ export function origin(value: unknown): string {
62
+ if (typeof value !== "string") throw new DesktopStoreError("unsafe");
63
+ let url: URL;
64
+ try { url = new URL(value); } catch { throw new DesktopStoreError("unsafe"); }
65
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash
66
+ || !['/', '/v1', '/v1/'].includes(url.pathname)) throw new DesktopStoreError("unsafe");
67
+ return url.origin;
68
+ }
69
+ export function parseOwner(value: unknown): DesktopRemoteOwner {
70
+ if (!isRecord(value)) throw new DesktopStoreError("unsafe");
71
+ exact(value, ["serverUrl", "apiKeyId", "connectedAt"]);
72
+ if (typeof value.apiKeyId !== "string" || !value.apiKeyId || value.apiKeyId.length > 256
73
+ || typeof value.connectedAt !== "string" || !Number.isFinite(Date.parse(value.connectedAt))
74
+ || origin(value.serverUrl) !== value.serverUrl) throw new DesktopStoreError("unsafe");
75
+ return value as DesktopRemoteOwner;
76
+ }
77
+ export function isHash(value: unknown): value is string { return typeof value === "string" && /^[a-f0-9]{64}$/.test(value); }
78
+ export function parseState(value: unknown): StoreState {
79
+ if (!isRecord(value)) throw new DesktopStoreError("unsafe");
80
+ exact(value, ["version", "owner", "home", "library", "targetId", "baselineRef", "baselineHash", "baselineKind", "phase", "lastProjectionHash", "tokenFingerprint", "pending"]);
81
+ parseOwner(value.owner);
82
+ if (value.version !== 1 || typeof value.home !== "string" || typeof value.library !== "string"
83
+ || typeof value.targetId !== "string" || !SAFE_DESKTOP_PROFILE_ID.test(value.targetId)
84
+ || value.baselineRef !== "baseline.json" || !isHash(value.baselineHash) || !isHash(value.lastProjectionHash)
85
+ || !isHash(value.tokenFingerprint) || !["known", "standard_fallback"].includes(String(value.baselineKind))
86
+ || !["prepared", "active", "restored", "cleaned"].includes(String(value.phase))) throw new DesktopStoreError("unsafe");
87
+ if (value.pending !== undefined) {
88
+ const p = value.pending;
89
+ if (!isRecord(p)) throw new DesktopStoreError("unsafe");
90
+ exact(p, ["kind", "before", "after", "beforeSelection", "afterSelection", "tokenFingerprint"]);
91
+ if (!['apply', 'rotate', 'restore'].includes(String(p.kind)) || !isHash(p.before) || !isHash(p.after) || !isHash(p.tokenFingerprint)
92
+ || [p.beforeSelection, p.afterSelection].some(id => id !== null && (typeof id !== "string" || !SAFE_DESKTOP_PROFILE_ID.test(id)))) throw new DesktopStoreError("unsafe");
93
+ }
94
+ return value as unknown as StoreState;
95
+ }
96
+ export function parseBaseline(value: unknown): Baseline {
97
+ if (!isRecord(value)) throw new DesktopStoreError("unsafe");
98
+ exact(value, ["version", "owner", "home", "library", "targetId", "kind", "targetExisted", "projection", "priorSelection"]);
99
+ parseOwner(value.owner);
100
+ if (value.version !== 1 || typeof value.home !== "string" || typeof value.library !== "string"
101
+ || typeof value.targetId !== "string" || !SAFE_DESKTOP_PROFILE_ID.test(value.targetId)
102
+ || !["known", "standard_fallback"].includes(String(value.kind)) || typeof value.targetExisted !== "boolean"
103
+ || !isRecord(value.projection) || Object.keys(value.projection).some(k => !OPENCODEX_DESKTOP_PROFILE_KEYS.has(k))) throw new DesktopStoreError("unsafe");
104
+ if (value.kind === "standard_fallback" && Object.keys(value.projection).length) throw new DesktopStoreError("unsafe");
105
+ if (value.priorSelection !== null) {
106
+ const p = value.priorSelection;
107
+ if (!isRecord(p)) throw new DesktopStoreError("unsafe");
108
+ exact(p, ["id", "hash"]);
109
+ if (typeof p.id !== "string" || !SAFE_DESKTOP_PROFILE_ID.test(p.id) || !isHash(p.hash)) throw new DesktopStoreError("unsafe");
110
+ }
111
+ return value as unknown as Baseline;
112
+ }
113
+ export function parseDisconnect(value: unknown): DesktopDisconnectReceipt {
114
+ if (!isRecord(value)) throw new DesktopStoreError("unsafe");
115
+ exact(value, ["version", "owner", "tokenFingerprint", "keepCatalog", "phase", "desktopAfterFingerprint", "catalogAfter"]);
116
+ parseOwner(value.owner);
117
+ if (value.version !== 1 || !isHash(value.tokenFingerprint) || typeof value.keepCatalog !== "boolean"
118
+ || !DISCONNECT_PHASES.includes(value.phase as DesktopDisconnectReceipt['phase'])
119
+ || (value.desktopAfterFingerprint !== undefined && !isHash(value.desktopAfterFingerprint))) throw new DesktopStoreError("unsafe");
120
+ if (value.catalogAfter !== undefined) {
121
+ const c = value.catalogAfter;
122
+ if (!isRecord(c)) throw new DesktopStoreError("unsafe");
123
+ exact(c, c.kind === "absent" ? ["kind"] : ["kind", "fingerprint"]);
124
+ if (c.kind !== "absent" && (c.kind !== "file" || !isHash(c.fingerprint))) throw new DesktopStoreError("unsafe");
125
+ }
126
+ return value as unknown as DesktopDisconnectReceipt;
127
+ }
@@ -0,0 +1,269 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readConfigDiagnostics, withConfigMutationLockSync } from "../config";
4
+ import { assertClientLifecycleHeld, type ClientLifecycleHeld } from "../client/lifecycle-lock";
5
+ import { readServiceApiTokenState, serviceApiTokenFingerprint } from "../lib/service-secrets";
6
+ import { assertDesktop3pModelsValid } from "./desktop-3p-guard";
7
+ import { profilePath } from "./desktop-3p-library";
8
+ import type { Desktop3pConfigMode, Desktop3pModelEntry } from "./desktop-3p";
9
+ import { StoreIO, metadata, storePaths } from "./desktop-remote-store-io";
10
+ import {
11
+ DesktopStoreError, DISCONNECT_PHASES, canonical, origin, parseDisconnect, parseOwner,
12
+ projection, projectionHash, sameOwner, type DesktopDisconnectReceipt, type DesktopRemoteOwner, type DesktopStoreResult,
13
+ } from "./desktop-remote-store-state";
14
+ import {
15
+ artifact, cleanBackup, currentConnection, establish, loadBundle, locateLegacy, profileCredential,
16
+ requireOwner, saveState, selectedFile, serviceGenerations, type Bundle,
17
+ } from "./desktop-remote-store-artifact";
18
+ export type { DesktopDisconnectReceipt, DesktopRemoteOwner, DesktopStoreResult } from "./desktop-remote-store-state";
19
+
20
+ function mutation(held: ClientLifecycleHeld, operation: (io: StoreIO) => DesktopStoreResult): DesktopStoreResult {
21
+ assertClientLifecycleHeld(held);
22
+ const io = new StoreIO();
23
+ try { return withConfigMutationLockSync(() => operation(io)); }
24
+ catch (error) { return { ok: false, changed: io.changed, reason: error instanceof DesktopStoreError ? error.reason : io.changed ? "recovery_required" : "unsafe" }; }
25
+ }
26
+ function absent(io?: StoreIO): DesktopStoreResult { return { ok: true, changed: io?.changed ?? false, status: "absent", restartRequired: false }; }
27
+ function success(io: StoreIO, bundle: Bundle, status: "applied" | "updated" | "restored", extra: Partial<Extract<DesktopStoreResult, { ok: true }>> = {}): DesktopStoreResult {
28
+ return { ok: true, changed: io.changed, status, baselineKind: bundle.state!.baselineKind,
29
+ restartRequired: io.changed, path: profilePath(bundle.paths.library, bundle.state!.targetId),
30
+ fingerprint: bundle.state!.lastProjectionHash, ...extra };
31
+ }
32
+ function receipt(io: StoreIO): DesktopDisconnectReceipt | null {
33
+ const file = io.read(storePaths().disconnect, 64 * 1024, true);
34
+ return file ? parseDisconnect(file.value) : null;
35
+ }
36
+ function noDisconnect(io: StoreIO): void {
37
+ const value = receipt(io);
38
+ if (value && value.phase !== "complete") throw new DesktopStoreError("conflict");
39
+ }
40
+ function knownGeneration(expected: string, candidates: readonly string[]): void {
41
+ if (!candidates.includes(expected)) throw new DesktopStoreError("conflict");
42
+ }
43
+ function recordedGenerations(io: StoreIO, owner: DesktopRemoteOwner): string[] {
44
+ const bundle = loadBundle(io);
45
+ const terminal = receipt(io);
46
+ if (bundle.state?.phase === "cleaned" && terminal?.phase === "complete"
47
+ && sameOwner(bundle.state.owner, terminal.owner) && !sameOwner(owner, terminal.owner)) return serviceGenerations();
48
+ requireOwner(bundle, owner);
49
+ return [...serviceGenerations(), ...(bundle.state ? [bundle.state.tokenFingerprint] : []),
50
+ ...(bundle.state?.pending ? [bundle.state.pending.tokenFingerprint] : [])];
51
+ }
52
+
53
+ export function inspectRemoteDesktopStore(owner: DesktopRemoteOwner): {
54
+ kind: "absent" | "active" | "pending" | "restored" | "legacy_current_connection" | "conflict" | "unsafe";
55
+ } {
56
+ try {
57
+ parseOwner(owner);
58
+ const io = new StoreIO(), bundle = loadBundle(io);
59
+ if (bundle.state?.phase === "cleaned") {
60
+ const terminal = receipt(io);
61
+ if (terminal?.phase === "complete" && sameOwner(bundle.state.owner, terminal.owner) && !sameOwner(owner, terminal.owner)) return { kind: "absent" };
62
+ }
63
+ requireOwner(bundle, owner);
64
+ if (bundle.state) {
65
+ if (bundle.state.phase === "prepared" || bundle.state.pending) return { kind: "pending" };
66
+ if (bundle.state.phase === "cleaned") return { kind: "restored" };
67
+ const meta = metadata(io, bundle.paths.library);
68
+ selectedFile(io, bundle.paths.library, meta.value);
69
+ if (!meta.value.entries.some(e => e.id === bundle.state!.targetId && e.name === "opencodex")) return { kind: "conflict" };
70
+ const file = io.read(profilePath(bundle.paths.library, bundle.state.targetId));
71
+ if (projectionHash(file?.value ?? null) !== bundle.state.lastProjectionHash) return { kind: "conflict" };
72
+ return { kind: bundle.state.phase === "restored" ? "restored" : "active" };
73
+ }
74
+ if (existsSync(bundle.paths.root) && !receipt(io)) throw new DesktopStoreError("unsafe");
75
+ const legacy = locateLegacy(io, owner, serviceGenerations());
76
+ if (legacy) currentConnection(owner);
77
+ return { kind: legacy ? "legacy_current_connection" : "absent" };
78
+ } catch (error) { return { kind: error instanceof DesktopStoreError && error.reason === "conflict" ? "conflict" : "unsafe" }; }
79
+ }
80
+
81
+ export function applyRemoteDesktopStore(held: ClientLifecycleHeld, options: {
82
+ owner: DesktopRemoteOwner; expectedTokenFingerprint: string;
83
+ baseUrl: string; apiKey: string; mode: Desktop3pConfigMode; models: Desktop3pModelEntry[];
84
+ }): DesktopStoreResult {
85
+ return mutation(held, io => {
86
+ const { config, client } = currentConnection(options.owner);
87
+ noDisconnect(io);
88
+ if (client.pendingOperation) throw new DesktopStoreError("conflict");
89
+ const token = readServiceApiTokenState();
90
+ if (token.kind !== "present" || token.fingerprint !== options.expectedTokenFingerprint || token.fingerprint !== client.tokenFingerprint
91
+ || serviceApiTokenFingerprint(options.apiKey) !== token.fingerprint || origin(options.baseUrl) !== options.owner.serverUrl) throw new DesktopStoreError("conflict");
92
+ if (config.clientIntegrations?.["claude-desktop"] === false) throw new DesktopStoreError("desired_disabled");
93
+ if (!options.models.length || !["static", "hybrid", "discovery"].includes(options.mode)) throw new DesktopStoreError("unsafe");
94
+ assertDesktop3pModelsValid(options.models);
95
+ const bundle = establish(io, options.owner, [token.fingerprint, ...serviceGenerations()], false)!;
96
+ const value = {
97
+ inferenceProvider: "gateway", inferenceCredentialKind: "static", inferenceGatewayBaseUrl: options.owner.serverUrl,
98
+ inferenceGatewayApiKey: options.apiKey, modelDiscoveryEnabled: options.mode !== "static",
99
+ ...(options.mode === "discovery" ? {} : { inferenceModels: options.models }),
100
+ };
101
+ artifact(io, bundle, value, "apply", token.fingerprint, bundle.state!.targetId);
102
+ cleanBackup(io, bundle, [token.fingerprint, ...serviceGenerations()]);
103
+ return success(io, bundle, "applied");
104
+ });
105
+ }
106
+
107
+ export function replaceRemoteDesktopCredential(held: ClientLifecycleHeld, options: {
108
+ owner: DesktopRemoteOwner; expectedTokenFingerprint: string; replacementKey: string;
109
+ }): DesktopStoreResult {
110
+ return mutation(held, io => {
111
+ const { config, client } = currentConnection(options.owner);
112
+ noDisconnect(io);
113
+ const token = readServiceApiTokenState();
114
+ const generations = recordedGenerations(io, options.owner);
115
+ const replacement = serviceApiTokenFingerprint(options.replacementKey);
116
+ if (token.kind !== "present" || replacement !== token.fingerprint
117
+ || (token.fingerprint !== client.tokenFingerprint && !client.pendingOperation)) throw new DesktopStoreError("conflict");
118
+ knownGeneration(options.expectedTokenFingerprint, generations);
119
+ const bundle = establish(io, options.owner, [options.expectedTokenFingerprint, ...generations], true);
120
+ if (!bundle) return absent(io);
121
+ if (bundle.state!.phase === "restored") return restoreArtifact(io, bundle, generations);
122
+ if (config.clientIntegrations?.["claude-desktop"] === false) return restoreArtifact(io, bundle, generations);
123
+ const file = io.read(profilePath(bundle.paths.library, bundle.state!.targetId));
124
+ if (!file) throw new DesktopStoreError("unsafe");
125
+ const credential = profileCredential(file.value);
126
+ if (!credential || credential.origin !== options.owner.serverUrl
127
+ || ![options.expectedTokenFingerprint, replacement].includes(credential.fingerprint)) throw new DesktopStoreError("conflict");
128
+ artifact(io, bundle, { ...projection(file.value), inferenceGatewayApiKey: options.replacementKey }, "rotate", replacement);
129
+ cleanBackup(io, bundle, [options.expectedTokenFingerprint, ...generations]);
130
+ return success(io, bundle, "updated");
131
+ });
132
+ }
133
+
134
+ function restoreArtifact(io: StoreIO, bundle: Bundle, known: readonly string[]): DesktopStoreResult {
135
+ let state = bundle.state!;
136
+ const baseline = bundle.baseline;
137
+ if (!baseline) throw new DesktopStoreError("recovery_required");
138
+ const meta = metadata(io, bundle.paths.library);
139
+ selectedFile(io, bundle.paths.library, meta.value);
140
+ let selection = meta.value.appliedId ?? state.targetId;
141
+ let restoration: "owned_projection" | "standard_fallback" | "selection_preserved" = baseline.kind === "standard_fallback" ? "standard_fallback" : "owned_projection";
142
+ if (selection !== state.targetId) restoration = "selection_preserved";
143
+ else if (baseline.priorSelection && baseline.priorSelection.id !== state.targetId) {
144
+ const priorSelection = baseline.priorSelection;
145
+ const prior = io.read(profilePath(bundle.paths.library, priorSelection.id));
146
+ if (!prior || prior.hash !== priorSelection.hash || !meta.value.entries.some(e => e.id === priorSelection.id)) throw new DesktopStoreError("conflict");
147
+ const key = profileCredential(prior.value);
148
+ if (key && known.includes(key.fingerprint)) throw new DesktopStoreError("conflict");
149
+ selection = priorSelection.id;
150
+ }
151
+ const originalKey = profileCredential(baseline.projection);
152
+ if (originalKey && known.includes(originalKey.fingerprint)) throw new DesktopStoreError("conflict");
153
+ const current = io.read(profilePath(bundle.paths.library, state.targetId));
154
+ const retainedForeignData = current !== null && Object.keys(current.value).length > Object.keys(projection(current.value)).length;
155
+ if (state.pending && state.pending.kind !== "restore") {
156
+ const observed = projectionHash(current?.value ?? null);
157
+ if (observed !== state.pending.before && observed !== state.pending.after) throw new DesktopStoreError("conflict");
158
+ const targetEntry = meta.value.entries.find(e => e.id === state.targetId);
159
+ const uncommittedCreation = !baseline.targetExisted && state.lastProjectionHash === projectionHash(null);
160
+ if ((targetEntry && targetEntry.name !== "opencodex") || (!targetEntry && !uncommittedCreation)) throw new DesktopStoreError("conflict");
161
+ const tokenFingerprint = observed === state.pending.after ? state.pending.tokenFingerprint : state.tokenFingerprint;
162
+ // Transition the intent, not the last committed projection. A profile may
163
+ // already exist while its new metadata row has never been committed.
164
+ saveState(io, bundle, { ...state, tokenFingerprint, pending: {
165
+ kind: "restore", before: observed, after: projectionHash(baseline.projection),
166
+ beforeSelection: meta.value.appliedId ?? null, afterSelection: selection, tokenFingerprint,
167
+ } });
168
+ state = bundle.state!;
169
+ }
170
+ artifact(io, bundle, baseline.projection, "restore", state.tokenFingerprint, selection);
171
+ cleanBackup(io, bundle, [...known, state.tokenFingerprint]);
172
+ return success(io, bundle, "restored", { restoration, retainedForeignData: !baseline.targetExisted && retainedForeignData });
173
+ }
174
+
175
+ export function restoreRemoteDesktopStore(held: ClientLifecycleHeld, options: {
176
+ owner: DesktopRemoteOwner; knownTokenFingerprints: readonly string[];
177
+ }): DesktopStoreResult {
178
+ return mutation(held, io => {
179
+ currentConnection(options.owner);
180
+ const generations = recordedGenerations(io, options.owner);
181
+ if (options.knownTokenFingerprints.some(hash => !generations.includes(hash))) throw new DesktopStoreError("conflict");
182
+ const bundle = establish(io, options.owner, generations, true);
183
+ if (!bundle) return absent(io);
184
+ return restoreArtifact(io, bundle, [...generations, ...options.knownTokenFingerprints]);
185
+ });
186
+ }
187
+
188
+ export function readDesktopDisconnectReceipt():
189
+ | { kind: "absent" } | { kind: "valid"; value: DesktopDisconnectReceipt } | { kind: "unsafe" } {
190
+ try { const value = receipt(new StoreIO()); return value ? { kind: "valid", value } : { kind: "absent" }; }
191
+ catch { return { kind: "unsafe" }; }
192
+ }
193
+ export function writeDesktopDisconnectReceipt(held: ClientLifecycleHeld, expected: DesktopDisconnectReceipt | null, next: DesktopDisconnectReceipt): void {
194
+ assertClientLifecycleHeld(held);
195
+ try {
196
+ withConfigMutationLockSync(() => {
197
+ parseDisconnect(next);
198
+ if (expected) parseDisconnect(expected);
199
+ const io = new StoreIO(), paths = storePaths();
200
+ const file = io.read(paths.disconnect, 64 * 1024, true);
201
+ const current = file ? parseDisconnect(file.value) : null;
202
+ if (canonical(current) !== canonical(expected)) throw new DesktopStoreError("conflict");
203
+ const rollover = current?.phase === "complete" && next.phase === "prepared" && !sameOwner(current.owner, next.owner);
204
+ if (!current || rollover) {
205
+ if (next.phase !== "prepared") throw new DesktopStoreError("conflict");
206
+ const { client } = currentConnection(next.owner);
207
+ const token = readServiceApiTokenState();
208
+ if (client.pendingOperation || token.kind !== "present" || token.fingerprint !== next.tokenFingerprint || client.tokenFingerprint !== token.fingerprint) throw new DesktopStoreError("conflict");
209
+ if (rollover) {
210
+ const bundle = loadBundle(io);
211
+ if (bundle.baselineFile || (bundle.state && (bundle.state.phase !== "cleaned" || !sameOwner(bundle.state.owner, current!.owner)))) throw new DesktopStoreError("conflict");
212
+ if (bundle.stateFile) io.remove(paths.state, bundle.stateFile);
213
+ }
214
+ } else {
215
+ if (!sameOwner(current.owner, next.owner) || current.tokenFingerprint !== next.tokenFingerprint || current.keepCatalog !== next.keepCatalog) throw new DesktopStoreError("conflict");
216
+ const delta = DISCONNECT_PHASES.indexOf(next.phase) - DISCONNECT_PHASES.indexOf(current.phase);
217
+ if (delta !== 0 && delta !== 1) throw new DesktopStoreError("conflict");
218
+ if ((current.desktopAfterFingerprint && current.desktopAfterFingerprint !== next.desktopAfterFingerprint)
219
+ || (current.catalogAfter && canonical(current.catalogAfter) !== canonical(next.catalogAfter))) throw new DesktopStoreError("conflict");
220
+ }
221
+ io.write(paths.disconnect, next, file, 64 * 1024);
222
+ });
223
+ } catch (error) {
224
+ throw new Error(error instanceof DesktopStoreError && error.reason === "conflict"
225
+ ? "desktop_disconnect_receipt_conflict" : error instanceof DesktopStoreError && error.reason === "unsafe"
226
+ ? "desktop_disconnect_receipt_unsafe" : "desktop_disconnect_receipt_write_failed");
227
+ }
228
+ }
229
+
230
+ export function inspectRemoteDesktopCleanup():
231
+ | { kind: "absent" } | { kind: "active" | "restored" | "pending"; owner: DesktopRemoteOwner } | { kind: "unsafe" } {
232
+ try {
233
+ const io = new StoreIO(), bundle = loadBundle(io), r = receipt(io);
234
+ if (r && bundle.state && !sameOwner(r.owner, bundle.state.owner)) throw new DesktopStoreError("unsafe");
235
+ if (bundle.state) {
236
+ const state = bundle.state;
237
+ if (state.phase === "cleaned" && r?.phase === "complete") {
238
+ return bundle.baselineFile ? { kind: "pending", owner: state.owner } : { kind: "absent" };
239
+ }
240
+ if (state.pending || state.phase === "prepared") return { kind: "pending", owner: state.owner };
241
+ if (r && r.phase !== "complete" && state.phase === "cleaned") return { kind: "pending", owner: state.owner };
242
+ return { kind: state.phase === "restored" || state.phase === "cleaned" ? "restored" : "active", owner: state.owner };
243
+ }
244
+ if (r && r.phase !== "complete") return { kind: "pending", owner: r.owner };
245
+ if (!r && existsSync(bundle.paths.root)) throw new DesktopStoreError("unsafe");
246
+ return { kind: "absent" };
247
+ } catch { return { kind: "unsafe" }; }
248
+ }
249
+
250
+ export function finishRemoteDesktopCleanup(held: ClientLifecycleHeld, owner: DesktopRemoteOwner): DesktopStoreResult {
251
+ return mutation(held, io => {
252
+ const r = receipt(io);
253
+ if (!r || !sameOwner(r.owner, owner) || !["connection_cleared", "complete"].includes(r.phase)) throw new DesktopStoreError("conflict");
254
+ const diagnostics = readConfigDiagnostics();
255
+ if (diagnostics.source === "fallback" || diagnostics.config.client || diagnostics.config.runtimeRole === "client"
256
+ || readServiceApiTokenState().kind !== "absent") throw new DesktopStoreError("conflict");
257
+ const bundle = loadBundle(io);
258
+ requireOwner(bundle, owner);
259
+ if (!bundle.state) return absent(io);
260
+ if (bundle.state.phase !== "restored" && bundle.state.phase !== "cleaned") throw new DesktopStoreError("conflict");
261
+ const target = io.read(profilePath(bundle.paths.library, bundle.state.targetId));
262
+ if (projectionHash(target?.value ?? null) !== bundle.state.lastProjectionHash) throw new DesktopStoreError("conflict");
263
+ cleanBackup(io, bundle, [r.tokenFingerprint, bundle.state.tokenFingerprint]);
264
+ // Mark cleanup before unlink so a crash between the two remains recoverable.
265
+ if (bundle.state.phase !== "cleaned") saveState(io, bundle, { ...bundle.state, phase: "cleaned" });
266
+ if (bundle.baselineFile) io.remove(bundle.paths.baseline, bundle.baselineFile);
267
+ return success(io, bundle, "restored", { restartRequired: false });
268
+ });
269
+ }
@@ -2,8 +2,9 @@ import type { OcxClaudeCodeConfig } from "../types";
2
2
  import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema";
3
3
  import { resolveAlias } from "./alias";
4
4
  import { stripOneMillionMarker } from "./context-windows";
5
- import { resolveDesktop3pAlias } from "./desktop-3p";
6
- import { isRec, type Rec } from "./inbound-records";
5
+ import { isUnresolvedDesktop3pAlias, resolveDesktop3pAlias } from "./desktop-3p";
6
+ import { validDateAlias } from "./desktop-profile";
7
+ import { AnthropicRequestError, DesktopModelMappingUnavailableError, isRec, type Rec } from "./inbound-records";
7
8
 
8
9
  function isClaudeClassifierModel(model: string): boolean {
9
10
  const stripped = model.replace(/-\d{8}$/, "");
@@ -54,6 +55,13 @@ export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): st
54
55
  const map = cc?.modelMap ?? {};
55
56
  const exact = map[model];
56
57
  if (typeof exact === "string" && exact.length > 0) return exact;
58
+ if (isUnresolvedDesktop3pAlias(model)) {
59
+ const base = model.endsWith("--fast") ? model.slice(0, -"--fast".length) : model;
60
+ // A missing date-shaped ID is ambiguous even after a successful but partial
61
+ // discovery. Never infer that a genuine native model is invalid or reroute it.
62
+ if (validDateAlias(base)) throw new DesktopModelMappingUnavailableError();
63
+ throw new AnthropicRequestError("Unknown Claude Desktop alias; reapply the Desktop profile from the connected hub");
64
+ }
57
65
  const stripped = model.replace(/-\d{8}$/, "");
58
66
  const dateless = map[stripped];
59
67
  if (typeof dateless === "string" && dateless.length > 0) return dateless;
@@ -1,5 +1,12 @@
1
1
  export class AnthropicRequestError extends Error {}
2
2
 
3
+ /** A date-shaped Desktop ID can also name a genuine native model absent from discovery. */
4
+ export class DesktopModelMappingUnavailableError extends AnthropicRequestError {
5
+ constructor() {
6
+ super("Claude Desktop model mapping is unavailable; refresh model discovery or reapply the connected hub profile");
7
+ }
8
+ }
9
+
3
10
  export type Rec = Record<string, unknown>;
4
11
 
5
12
  export function isRec(v: unknown): v is Rec {
@@ -12,7 +12,7 @@
12
12
  import type { OcxClaudeCodeConfig } from "../types";
13
13
  import { createHash } from "node:crypto";
14
14
 
15
- export { AnthropicRequestError } from "./inbound-records";
15
+ export { AnthropicRequestError, DesktopModelMappingUnavailableError } from "./inbound-records";
16
16
  export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, extractOcxRouteDirective, extractOcxEffortDirective } from "./inbound-model-options";
17
17
  import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
18
18
  import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound-model-options";
@@ -143,14 +143,19 @@ export function buildAnthropicModelInfos(
143
143
  // the auto-context widening that let a 372K route carry the marker (and be
144
144
  // over-filled) is the #854 defect and does not come back. Guards (audit R1#11):
145
145
  // same dedupe set, never double-suffix.
146
- const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined, maxInputTokens?: number) => {
146
+ const push1mVariant = (
147
+ base: AnthropicModelInfo,
148
+ contextWindow: number | undefined,
149
+ maxInputTokens?: number,
150
+ selectorId?: string,
151
+ ) => {
147
152
  // The [1m] marker makes Claude Code account 1e6 tokens for the row, so it
148
153
  // may only name models whose AUTHORITATIVE effective window is >= 1M —
149
154
  // never the auto-context widening, which would mark a 372K route and have
150
155
  // Claude Code over-fill it (the #854 defect).
151
156
  if (contextWindow === undefined || contextWindow < ONE_MILLION) return;
152
157
  if (base.id.includes("[1m]")) return;
153
- const id = `${base.id}[1m]`;
158
+ const id = selectorId ?? `${base.id}[1m]`;
154
159
  if (seen.has(id)) return;
155
160
  seen.add(id);
156
161
  // The marker fixes Claude Code's accounting at 1e6, but a model may accept less input
@@ -220,7 +225,15 @@ export function buildAnthropicModelInfos(
220
225
  out.push(info);
221
226
  // Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
222
227
  // routes — only a genuine >=1M window earns the variant row there.
223
- push1mVariant(info, m.contextWindow, routedMaxInput);
228
+ // Claude Code groups canonical Fable ids before it compares the [1m] marker. This
229
+ // reversible alias only separates picker families; it is not an OpenAI-native route.
230
+ // The Messages ingress restores the canonical Anthropic id before passthrough.
231
+ const oneMillionSelector = idStyle === "readable"
232
+ && m.provider === "anthropic"
233
+ && listedModelId.startsWith("claude-fable-")
234
+ ? `${claudeCodeNativeAlias(listedModelId)}[1m]`
235
+ : undefined;
236
+ push1mVariant(info, m.contextWindow, routedMaxInput, oneMillionSelector);
224
237
  // The whole model is passed, not a (provider, id) pair: a combo row lives in its own
225
238
  // namespace with no config.providers entry, so the caller classifies it from the
226
239
  // aggregated supportsServiceTier the row already carries.
@@ -8,6 +8,7 @@ import { runningProxyUpdateHeaders } from "../oauth/login-cli";
8
8
  import { isPublicOAuthProvider } from "../oauth/index";
9
9
  import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
10
10
  import type { OcxConfig } from "../types";
11
+ import { projectCodexQuotaRefreshOutcome, type CodexQuotaRefreshOutcome } from "../codex/quota-refresh-outcome";
11
12
 
12
13
  export type AccountType = "codex" | "oauth" | "api-key";
13
14
 
@@ -24,6 +25,7 @@ export interface AccountRow {
24
25
  /** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
25
26
  priority?: number;
26
27
  quota?: CodexQuotaDto | null;
28
+ quotaRefresh?: CodexQuotaRefreshOutcome;
27
29
  /**
28
30
  * Whether the pool is holding this account out of rotation.
29
31
  *
@@ -237,6 +239,7 @@ interface CodexAccountDto {
237
239
  needsReauth?: boolean;
238
240
  priority?: number;
239
241
  quota?: CodexQuotaDto | null;
242
+ quotaRefresh?: unknown;
240
243
  paused?: boolean;
241
244
  }
242
245
 
@@ -299,7 +302,10 @@ export async function fetchCodexRows(
299
302
  needsReauth: a.needsReauth,
300
303
  priority: typeof a.priority === "number" ? a.priority : 0,
301
304
  paused: a.paused === true,
302
- ...(includeQuota ? { quota: projectQuota(a.quota) } : {}),
305
+ ...(includeQuota ? {
306
+ quota: projectQuota(a.quota),
307
+ quotaRefresh: projectCodexQuotaRefreshOutcome(a.quotaRefresh),
308
+ } : {}),
303
309
  }));
304
310
  return { rows, activeId, autoSwitchThreshold, status: 200 };
305
311
  }
@@ -0,0 +1,17 @@
1
+ import type { OwnedIntegrationRefreshOutcome } from "../integrations/owned-refresh";
2
+ import { runtimeRequest, RuntimeApiError, type RuntimeApiDeps } from "./runtime-api";
3
+
4
+ /** Aside policy and file writes share the running server's mutation owner. Never fall back locally. */
5
+ export async function refreshAsideProfilesThroughServer(
6
+ deps: RuntimeApiDeps = {},
7
+ ): Promise<OwnedIntegrationRefreshOutcome[]> {
8
+ const result = await runtimeRequest<{ results?: OwnedIntegrationRefreshOutcome[] }>(
9
+ "/api/client-integrations/aside/sync",
10
+ { method: "POST", body: "{}" },
11
+ deps,
12
+ );
13
+ if (!Array.isArray(result.results)) {
14
+ throw new RuntimeApiError("The running proxy does not support Aside profile synchronization", 502, result);
15
+ }
16
+ return result.results;
17
+ }