@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
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CliUsageError,
3
+ RuntimeApiError,
3
4
  csv,
4
5
  printData,
5
6
  rejectArgs,
@@ -28,10 +29,25 @@ const GROK_USAGE = `Usage:
28
29
  ocx grok apply [--json]`;
29
30
 
30
31
  const CLIENT_USAGE = `Usage:
31
- ocx integration client [status] [--client <id>] [--json]
32
- ocx integration client <enable|disable> --client <id> [--overwrite-conflict] [--json]
33
- ocx integration client history [--client <id>] [--json]
34
- ocx integration client restore --op <opId> [--confirm-drift] [--json]`;
32
+ ocx integration client [status] [--client <id>] [--profile <id>] [--json]
33
+ ocx integration client <enable|disable> --client <id> [--profile <id>] [--overwrite-conflict] [--json]
34
+ ocx integration client history [--client <id>] [--profile <id>] [--json]
35
+ ocx integration client restore --op <opId> [--client aside --profile <id>] [--confirm-drift] [--json]
36
+ --profile selects one Aside account-backed profile; omitted Aside toggles affect all profiles.`;
37
+
38
+ function validateAsideProfile(profile: string | undefined, client: string | undefined): void {
39
+ if (profile === undefined) return;
40
+ if (client !== "aside") throw new CliUsageError("--profile requires --client aside", CLIENT_USAGE);
41
+ if (!/^(0|[1-9][0-9]*)$/.test(profile) || !Number.isSafeInteger(Number(profile))) {
42
+ throw new CliUsageError("--profile must be a nonnegative integer account ID", CLIENT_USAGE);
43
+ }
44
+
45
+ }
46
+
47
+ function clientIntegrationPath(client: string, profile?: string): string {
48
+ const base = `/api/client-integrations/${encodeURIComponent(client)}`;
49
+ return client === "aside" ? `${base}/profiles${profile === undefined ? "" : `/${encodeURIComponent(profile)}`}` : base;
50
+ }
35
51
 
36
52
  function parseMap(raw: string): Record<string, string> {
37
53
  if (raw === "-") return {};
@@ -163,16 +179,23 @@ export async function handleClientIntegrationCommand(
163
179
  const args = [...argv];
164
180
  const action = (args.shift() ?? "status").toLowerCase();
165
181
  const wantsJson = takeFlag(args, "--json");
182
+ const profile = takeOption(args, "--profile");
166
183
 
167
184
  if (action === "status" || action === "show" || action === "list") {
168
185
  const client = takeOption(args, "--client");
186
+ validateAsideProfile(profile, client);
169
187
  rejectArgs(args, CLIENT_USAGE);
170
188
  const path = client
171
- ? `/api/client-integrations/${encodeURIComponent(client)}`
189
+ ? clientIntegrationPath(client, profile)
172
190
  : "/api/client-integrations";
173
191
  const result = await runtimeRequest(path, {}, deps);
174
192
  const rows = (result as { clients?: Array<Record<string, unknown>> }).clients;
175
- printData(result, wantsJson, rows
193
+ const profiles = (result as { profiles?: Array<Record<string, unknown>> }).profiles;
194
+ printData(result, wantsJson, profiles
195
+ ? profiles.length > 0
196
+ ? profiles.map(row => `${String(row.profileId)} ${String(row.name ?? "Aside")}: ${row.enabled ? "on" : "off"} (${String(row.state)})${row.current ? " [current]" : ""}`)
197
+ : [String((result as { error?: string }).error ?? "No Aside profiles found.")]
198
+ : rows
176
199
  ? rows.map(row => `${String(row.clientId)}: ${String(row.state)}${row.installed ? "" : " (not installed)"}`)
177
200
  : summaryLines(result));
178
201
  return;
@@ -180,9 +203,11 @@ export async function handleClientIntegrationCommand(
180
203
 
181
204
  if (action === "history" || action === "journal") {
182
205
  const client = takeOption(args, "--client");
206
+ validateAsideProfile(profile, client);
183
207
  rejectArgs(args, CLIENT_USAGE);
184
- const query = client ? `?client=${encodeURIComponent(client)}` : "";
185
- const result = await runtimeRequest(`/api/client-integrations/journal${query}`, {}, deps);
208
+ const path = client === "aside" ? `${clientIntegrationPath(client, profile)}/journal`
209
+ : `/api/client-integrations/journal${client ? `?client=${encodeURIComponent(client)}` : ""}`;
210
+ const result = await runtimeRequest(path, {}, deps);
186
211
  const operations = (result as { operations?: Array<Record<string, unknown>> }).operations ?? [];
187
212
  printData(result, wantsJson, operations.length === 0
188
213
  ? ["No integration operations recorded yet."]
@@ -190,7 +215,8 @@ export async function handleClientIntegrationCommand(
190
215
  // `snapshot` is resolved against the disk by the route, so "expired"
191
216
  // here means the bytes are genuinely gone, not merely old.
192
217
  const backup = row.snapshot === "expired" ? "backup expired" : `op ${String(row.opId)}`;
193
- return `${String(row.at)} ${String(row.clientId)} ${String(row.kind)} (${backup})`;
218
+ const owner = row.profileId === undefined ? String(row.clientId) : `${String(row.clientId)}:${String(row.profileId)}`;
219
+ return `${String(row.at)} ${owner} ${String(row.kind)} (${backup})`;
194
220
  }));
195
221
  return;
196
222
  }
@@ -198,9 +224,12 @@ export async function handleClientIntegrationCommand(
198
224
  if (action === "restore") {
199
225
  const opId = takeOption(args, "--op") ?? takeOption(args, "--op-id");
200
226
  const confirmDrift = takeFlag(args, "--confirm-drift");
227
+ const client = takeOption(args, "--client");
228
+ validateAsideProfile(profile, client);
229
+ if (client !== undefined && profile === undefined) throw new CliUsageError("restore --client requires --profile", CLIENT_USAGE);
201
230
  rejectArgs(args, CLIENT_USAGE);
202
231
  if (!opId) throw new CliUsageError("--op <opId> is required", CLIENT_USAGE);
203
- const result = await runtimeRequest("/api/client-integrations/restore", {
232
+ const result = await runtimeRequest(profile === undefined ? "/api/client-integrations/restore" : `${clientIntegrationPath("aside", profile)}/restore`, {
204
233
  method: "POST",
205
234
  body: JSON.stringify({ opId, confirmDrift }),
206
235
  }, deps);
@@ -212,6 +241,7 @@ export async function handleClientIntegrationCommand(
212
241
  throw new CliUsageError(`unknown client integration command ${action}`, CLIENT_USAGE);
213
242
  }
214
243
  const client = takeOption(args, "--client");
244
+ validateAsideProfile(profile, client);
215
245
  /*
216
246
  * The conflict escape hatch, spelled the way `restore --confirm-drift` is: the
217
247
  * refusal is the default and the waiver has to be typed.
@@ -232,7 +262,7 @@ export async function handleClientIntegrationCommand(
232
262
  if (overwriteConflict && action === "disable") {
233
263
  throw new CliUsageError("--overwrite-conflict applies only to enable", CLIENT_USAGE);
234
264
  }
235
- const result = await runtimeRequest(`/api/client-integrations/${encodeURIComponent(client)}`, {
265
+ const result = await runtimeRequest(clientIntegrationPath(client, profile), {
236
266
  method: "PUT",
237
267
  // Sent only when asked for, so a proxy on an older build sees the request it
238
268
  // has always seen rather than an unknown field.
@@ -240,7 +270,11 @@ export async function handleClientIntegrationCommand(
240
270
  ? { enabled: true, overwriteConflict: true }
241
271
  : { enabled: action === "enable" }),
242
272
  }, deps);
243
- printData(result, wantsJson, [String((result as Record<string, unknown>).message ?? `${client} ${action}d.`)]);
273
+ const batch = result as { ok?: boolean; message?: string; results?: Array<Record<string, unknown>> };
274
+ printData(result, wantsJson, batch.results
275
+ ? batch.results.map(row => `aside:${String(row.profileId)} ${String(row.message ?? (row.ok ? "updated" : "refused"))}${row.residual === true ? " Recovery did not finish." : ""}${typeof row.snapshotPath === "string" ? ` Backup: ${row.snapshotPath}` : ""}`)
276
+ : [String(batch.message ?? `${client} ${action}d.`)]);
277
+ if (batch.ok === false) throw new RuntimeApiError(batch.message ?? "Some Aside profiles could not be updated", 207, result);
244
278
  });
245
279
  }
246
280
 
@@ -39,7 +39,7 @@ import type {
39
39
  OpencodeProviderBlocks,
40
40
  OpencodeV2ProviderBlock,
41
41
  } from "../clients/config-export";
42
- import { visibleNativeSlugs } from "../codex/catalog";
42
+ import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
43
43
  import { commandInvocation } from "../lib/win-exec";
44
44
  import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
45
45
  import { providerCodexAccountMode } from "../providers/registry";
@@ -376,12 +376,17 @@ export function opencodeCatalogFromProxyRows(
376
376
  config: OcxConfig,
377
377
  ): OpencodeCatalogModel[] {
378
378
  const omitNative = providerCodexAccountMode("openai", config.providers?.openai) === "direct";
379
+ const routedRows = rows.filter((row): row is OpencodeProxyModelRow & { provider: string; id: string } =>
380
+ row.native !== true && typeof row.provider === "string" && typeof row.id === "string");
381
+ const visibleRouted = new Set<OpencodeProxyModelRow>(filterCatalogVisibleModels(routedRows, config));
379
382
  const seen = new Set<string>();
380
383
  const catalog: OpencodeCatalogModel[] = [];
381
384
  for (const row of rows) {
382
385
  const namespaced = row.namespaced?.trim();
383
386
  if (!namespaced || row.disabled === true) continue;
384
387
  if (omitNative && row.native === true) continue;
388
+ if (row.native !== true && typeof row.provider === "string" && typeof row.id === "string"
389
+ && !visibleRouted.has(row)) continue;
385
390
  if (seen.has(namespaced)) continue;
386
391
  seen.add(namespaced);
387
392
  catalog.push({
@@ -632,14 +637,14 @@ export function opencodeNotFoundHint(
632
637
  }
633
638
 
634
639
  export async function cmdOpencode(args: string[]): Promise<number> {
635
- const config = loadConfig();
636
- const live = await ensureProxyForOpencode(config);
640
+ const startupConfig = loadConfig();
641
+ const live = await ensureProxyForOpencode(startupConfig);
637
642
  if (!live) {
638
643
  console.error("❌ Proxy did not become healthy after starting.");
639
644
  return 1;
640
645
  }
641
646
 
642
- const apiKey = opencodeApiKey(config);
647
+ const apiKey = opencodeApiKey(startupConfig);
643
648
  let proxyModels: OpencodeProxyModelRow[];
644
649
  try {
645
650
  proxyModels = await fetchOpencodeProxyModels(live, apiKey);
@@ -648,6 +653,8 @@ export async function cmdOpencode(args: string[]): Promise<number> {
648
653
  console.error(`❌ Could not fetch the model catalog from the proxy: ${reason}`);
649
654
  return 1;
650
655
  }
656
+ // /api/models may have completed and persisted initial provider selection.
657
+ const config = loadConfig();
651
658
  const catalog = opencodeCatalogFromProxyRows(proxyModels, config);
652
659
  const blocks = buildOpencodeProviderBlocksFromCatalog(live.port, catalog, live.hostname, config);
653
660
  const baseUrl = blocks.v1.options.baseURL;
@@ -44,7 +44,7 @@ export class RuntimeApiError extends Error {
44
44
 
45
45
  export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string> {
46
46
  if (deps.baseUrl) return deps.baseUrl.replace(/\/$/, "");
47
- const live = await findLiveProxy();
47
+ const live = await (deps.findLiveProxy ?? findLiveProxy)();
48
48
  if (!live) throw new RuntimeApiError("Proxy is not running. Start it with: ocx start", 503, null);
49
49
  return `http://${probeHostname(live.hostname)}:${live.port}`;
50
50
  }
@@ -79,7 +79,12 @@ function responseMessage(body: unknown, status: number): string {
79
79
  if (reason && reason !== primary) parts.push(`reason: ${reason}`);
80
80
  const hint = stringField(record, "hint");
81
81
  if (hint && hint !== primary) parts.push(`hint: ${hint}`);
82
- return parts.join("\n").slice(0, 1200);
82
+ const snapshotPath = stringField(record, "snapshotPath");
83
+ const recovery = [
84
+ ...(snapshotPath ? [`Backup: ${snapshotPath.slice(0, 32768)}`] : []),
85
+ ...(record.residual === true ? ["Automatic recovery did not finish; check the client configuration before retrying."] : []),
86
+ ];
87
+ return [parts.join("\n").slice(0, 1200), ...recovery].join("\n");
83
88
  }
84
89
 
85
90
  export async function runtimeRequest<T = unknown>(
@@ -0,0 +1,78 @@
1
+ import { getConfigDir } from "../config";
2
+ import { disconnectClient } from "../client/connect";
3
+ import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
4
+ import { assertClientLifecycleHeld, withClientLifecycle } from "../client/lifecycle-lock";
5
+ import { inspectRemoteDesktopCleanup, readDesktopDisconnectReceipt } from "../claude/desktop-remote-store";
6
+ import { removeOwnedConfigState, type ConfigRemovalResult } from "../lib/config-ownership";
7
+ import { sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
8
+
9
+ export interface UninstallClientStateDeps {
10
+ readConnection: typeof readClientConnectionState;
11
+ inspectDesktop: typeof inspectRemoteDesktopCleanup;
12
+ readReceipt: typeof readDesktopDisconnectReceipt;
13
+ disconnect: (options?: Parameters<typeof disconnectClient>[0]) => Promise<unknown>;
14
+ withLifecycle: typeof withClientLifecycle;
15
+ remove: () => ConfigRemovalResult;
16
+ }
17
+
18
+ const defaults: UninstallClientStateDeps = {
19
+ readConnection: readClientConnectionState,
20
+ inspectDesktop: inspectRemoteDesktopCleanup,
21
+ readReceipt: readDesktopDisconnectReceipt,
22
+ disconnect: options => disconnectClient(options),
23
+ withLifecycle: withClientLifecycle,
24
+ remove: () => removeOwnedConfigState(getConfigDir()),
25
+ };
26
+
27
+ /** Restore connection-owned client artifacts before removing their ownership/recovery records. */
28
+ export async function removeOwnedConfigAfterDesktopCleanup(
29
+ observed: UninstallObservation,
30
+ deps: UninstallClientStateDeps = defaults,
31
+ ): Promise<ConfigRemovalResult> {
32
+ if (!sharedTeardownAuthorized(observed)) {
33
+ throw new Error("Client cleanup refused: service or proxy teardown is not proven.");
34
+ }
35
+ const state = deps.readConnection();
36
+ if (state.kind !== "connected" && state.kind !== "disconnected") {
37
+ throw new Error("Client cleanup refused: connection state is invalid or mismatched.");
38
+ }
39
+ const desktop = deps.inspectDesktop();
40
+ const receipt = deps.readReceipt();
41
+ if (desktop.kind === "unsafe" || receipt.kind === "unsafe") {
42
+ throw new Error("Client cleanup refused: Desktop recovery state is unsafe.");
43
+ }
44
+ const interrupted = receipt.kind === "valid" && receipt.value.phase !== "complete" ? receipt.value : undefined;
45
+ if ((state.kind === "connected" && desktop.kind !== "absent" && !sameClientConnectionOwner(state.value, desktop.owner))
46
+ || (interrupted && state.kind === "connected" && !sameClientConnectionOwner(state.value, interrupted.owner))
47
+ || (interrupted && desktop.kind !== "absent" && !sameClientConnectionOwner(desktop.owner, interrupted.owner))) {
48
+ throw new Error("Client cleanup refused: Desktop recovery ownership is mismatched.");
49
+ }
50
+ if (state.kind === "connected" || interrupted) {
51
+ // Disconnect owns its N/L/C ordering. Do not hold L across this call, and
52
+ // preserve the frozen catalog choice when resuming an interrupted operation.
53
+ await deps.disconnect({
54
+ expectedOwner: state.kind === "connected" ? {
55
+ serverUrl: state.value.serverUrl, apiKeyId: state.value.apiKeyId, connectedAt: state.value.connectedAt,
56
+ } : interrupted!.owner,
57
+ ...(interrupted ? { keepCatalog: interrupted.keepCatalog } : {}),
58
+ });
59
+ } else if (desktop.kind !== "absent") {
60
+ throw new Error("Client cleanup refused: finish Desktop recovery before uninstalling.");
61
+ }
62
+
63
+ // A connection can appear while asynchronous cleanup is finishing. The final
64
+ // inspection and actual remover share L, but never hold C while deleting its DB.
65
+ return deps.withLifecycle(async held => {
66
+ assertClientLifecycleHeld(held);
67
+ const latest = deps.readConnection();
68
+ const latestDesktop = deps.inspectDesktop();
69
+ const latestReceipt = deps.readReceipt();
70
+ if (latest.kind !== "disconnected"
71
+ || latestDesktop.kind !== "absent"
72
+ || latestReceipt.kind === "unsafe"
73
+ || (latestReceipt.kind === "valid" && latestReceipt.value.phase !== "complete")) {
74
+ throw new Error("Client cleanup refused: connection or Desktop state changed before removal.");
75
+ }
76
+ return deps.remove();
77
+ });
78
+ }