@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.
Files changed (216) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/ollama-native.ts +29 -1
  10. package/src/adapters/openai-chat/messages.ts +8 -4
  11. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  12. package/src/adapters/openai-responses/passthrough.ts +3 -0
  13. package/src/adapters/openai-responses/request-strips.ts +10 -2
  14. package/src/adapters/qoder/adapter.ts +16 -14
  15. package/src/bridge/errors.ts +19 -0
  16. package/src/bridge.ts +1 -1
  17. package/src/claude/context-windows.ts +54 -1
  18. package/src/claude/intercept/connect-proxy.ts +10 -3
  19. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  20. package/src/claude/intercept/picker-listener.ts +49 -1
  21. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  22. package/src/claude/intercept/settings.ts +101 -9
  23. package/src/cli/account-api.ts +65 -0
  24. package/src/cli/account-auth.ts +26 -2
  25. package/src/cli/account.ts +41 -5
  26. package/src/cli/capabilities-access-remote.ts +8 -0
  27. package/src/cli/capabilities-base.ts +13 -1
  28. package/src/cli/catalog-command-result.ts +2 -1
  29. package/src/cli/claude.ts +15 -6
  30. package/src/cli/config-command.ts +8 -2
  31. package/src/cli/debug.ts +8 -0
  32. package/src/cli/dispatch.ts +16 -3
  33. package/src/cli/export-command.ts +14 -1
  34. package/src/cli/hub-disable.ts +99 -0
  35. package/src/cli/hub.ts +4 -2
  36. package/src/cli/index.ts +13 -4
  37. package/src/cli/link-child.ts +104 -0
  38. package/src/cli/link-enrollment.ts +96 -0
  39. package/src/cli/link.ts +39 -24
  40. package/src/cli/observe-stream.ts +6 -1
  41. package/src/cli/observe.ts +10 -2
  42. package/src/cli/provider.ts +12 -0
  43. package/src/cli/registry.ts +5 -4
  44. package/src/cli/runtime-api.ts +27 -2
  45. package/src/cli/status.ts +104 -20
  46. package/src/client/connect.ts +39 -4
  47. package/src/client/hub-client.ts +14 -7
  48. package/src/client/hub-state.ts +25 -14
  49. package/src/client/link-home.ts +248 -0
  50. package/src/client/link-management-relay.ts +61 -0
  51. package/src/client/link-relay.ts +6 -1
  52. package/src/client/link-status.ts +14 -0
  53. package/src/client/machine-api.ts +14 -7
  54. package/src/client/machine-listener.ts +29 -4
  55. package/src/clients/config-export.ts +46 -16
  56. package/src/clients/omo-role-models.ts +6 -3
  57. package/src/codex/agent-role-models.ts +26 -8
  58. package/src/codex/auth-api/account-list.ts +17 -2
  59. package/src/codex/auth-api/account-selection.ts +17 -7
  60. package/src/codex/auth-api/routes.ts +5 -1
  61. package/src/codex/autostart-health.ts +3 -1
  62. package/src/codex/catalog/bundled.ts +5 -1
  63. package/src/codex/catalog/effort.ts +2 -2
  64. package/src/codex/catalog/metadata.ts +4 -2
  65. package/src/codex/catalog/native-models.ts +16 -2
  66. package/src/codex/native-profile-startup.ts +4 -1
  67. package/src/codex/prompt-journal.ts +20 -2
  68. package/src/codex/prompt-layers/encoding.ts +49 -0
  69. package/src/codex/prompt-layers/import-source.ts +30 -0
  70. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  71. package/src/codex/prompt-layers/toml-read.ts +52 -4
  72. package/src/codex/prompt-layers.ts +383 -20
  73. package/src/codex/prompt-lock-claim.ts +13 -3
  74. package/src/codex/prompt-lock-evidence.ts +3 -3
  75. package/src/codex/prompt-lock-owner.ts +14 -6
  76. package/src/codex/prompt-text-probe.ts +34 -5
  77. package/src/codex/quota.ts +13 -6
  78. package/src/codex/role-auto-assign.ts +2 -5
  79. package/src/codex/routing/active-account.ts +21 -0
  80. package/src/codex/routing.ts +1 -1
  81. package/src/codex/runtime.ts +196 -8
  82. package/src/codex/shim-overlay.ts +8 -4
  83. package/src/codex/shim.ts +1 -1
  84. package/src/combos/failover.ts +3 -0
  85. package/src/config/api-key-reconcile.ts +34 -0
  86. package/src/config/diagnostics.ts +16 -0
  87. package/src/config/feature-flags.ts +5 -0
  88. package/src/config/live-reconcile.ts +43 -29
  89. package/src/config/live-topology.ts +29 -0
  90. package/src/config/load-degrade.ts +2 -1
  91. package/src/config/paths.ts +3 -0
  92. package/src/config/salvage.ts +3 -1
  93. package/src/config/schema/config-schema.ts +2 -0
  94. package/src/generated/compatibility-version.json +311 -171
  95. package/src/grok/grpc-web.ts +23 -1
  96. package/src/grok/reset-coupon-ledger.ts +75 -5
  97. package/src/grok/reset-coupons.ts +4 -2
  98. package/src/integrations/mutation-plan.ts +13 -7
  99. package/src/integrations/registry.ts +45 -11
  100. package/src/integrations/state.ts +19 -7
  101. package/src/integrations/target.ts +42 -5
  102. package/src/integrations/writer.ts +14 -6
  103. package/src/lib/app-owned-memory-stores.ts +4 -0
  104. package/src/lib/app-owned-memory.ts +11 -0
  105. package/src/lib/json-byte-size.ts +31 -0
  106. package/src/lib/optional-shutdown-hooks.ts +35 -0
  107. package/src/lib/outbound-credential-registry.ts +210 -0
  108. package/src/lib/passive-read.ts +35 -0
  109. package/src/lib/shadow-call.ts +44 -5
  110. package/src/link/codex-settings.ts +155 -0
  111. package/src/link/management-routes.ts +89 -0
  112. package/src/link/remote-probe.ts +38 -0
  113. package/src/link/ssh-argv.ts +26 -5
  114. package/src/link/ssh-runner.ts +58 -27
  115. package/src/link/status-projection.ts +22 -4
  116. package/src/link/supervisor.ts +144 -49
  117. package/src/link/tunnel-state.ts +11 -4
  118. package/src/oauth/anthropic-continuity.ts +4 -3
  119. package/src/oauth/anthropic-identity.ts +13 -2
  120. package/src/oauth/anthropic-model-quota.ts +13 -1
  121. package/src/oauth/anthropic-routing.ts +230 -23
  122. package/src/oauth/anthropic.ts +18 -4
  123. package/src/oauth/google-antigravity.ts +24 -7
  124. package/src/oauth/index.ts +24 -15
  125. package/src/oauth/kiro-account-identity.ts +106 -0
  126. package/src/oauth/kiro.ts +48 -5
  127. package/src/oauth/plan.ts +29 -0
  128. package/src/oauth/store.ts +40 -3
  129. package/src/oauth/types.ts +2 -0
  130. package/src/plugins/loader.ts +11 -0
  131. package/src/providers/anthropic-billing-ownership.ts +74 -0
  132. package/src/providers/anthropic-billing-recovery.ts +31 -0
  133. package/src/providers/anthropic-billing-refusal.ts +55 -0
  134. package/src/providers/anthropic-reset-admission.ts +62 -0
  135. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  136. package/src/providers/api-key-resolve.ts +17 -2
  137. package/src/providers/key-failover.ts +52 -5
  138. package/src/providers/model-rename-startup.ts +5 -3
  139. package/src/providers/quota/account-cache.ts +11 -0
  140. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  141. package/src/providers/quota/report-cache.ts +14 -0
  142. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  143. package/src/providers/quota.ts +4 -1
  144. package/src/providers/registry/entries-core.ts +14 -14
  145. package/src/providers/registry/entries-extended.ts +0 -1
  146. package/src/providers/registry/model-seeds.ts +17 -0
  147. package/src/providers/request-pacing.ts +9 -0
  148. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  149. package/src/responses/compaction.ts +14 -0
  150. package/src/responses/parser.ts +7 -1
  151. package/src/server/auth-cors.ts +42 -0
  152. package/src/server/configured-credentials.ts +194 -0
  153. package/src/server/index/link-management.ts +58 -0
  154. package/src/server/index/optional-listeners.ts +5 -1
  155. package/src/server/index/serve-options.ts +13 -0
  156. package/src/server/lifecycle.ts +2 -1
  157. package/src/server/management/agent-settings-routes.ts +28 -10
  158. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  159. package/src/server/management/codex-agent-role-routes.ts +19 -6
  160. package/src/server/management/codex-prompt-routes.ts +136 -5
  161. package/src/server/management/codex-role-auto-assign.ts +1 -1
  162. package/src/server/management/grok-coupon-routes.ts +78 -27
  163. package/src/server/management/link-enrollment.ts +192 -0
  164. package/src/server/management/link-route-state.ts +185 -0
  165. package/src/server/management/link-routes.ts +82 -402
  166. package/src/server/management/model-rows.ts +3 -1
  167. package/src/server/management/provider-overwrite-carry.ts +10 -0
  168. package/src/server/management/provider-routes.ts +1 -0
  169. package/src/server/management/route-registry.ts +5 -3
  170. package/src/server/management/shared.ts +3 -0
  171. package/src/server/management-api.ts +3 -1
  172. package/src/server/messages-native.ts +68 -42
  173. package/src/server/relay-eager.ts +2 -0
  174. package/src/server/request-log-filter.ts +6 -1
  175. package/src/server/request-log-terminal-status.ts +63 -6
  176. package/src/server/responses/adapter-dispatch.ts +232 -79
  177. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  178. package/src/server/responses/codex-ws-exchange.ts +147 -6
  179. package/src/server/responses/codex-ws-wire.ts +203 -0
  180. package/src/server/responses/collaboration.ts +36 -7
  181. package/src/server/responses/compact.ts +126 -48
  182. package/src/server/responses/context-overflow.ts +13 -1
  183. package/src/server/responses/core-codex-account.ts +105 -1
  184. package/src/server/responses/core-combo-failure.ts +14 -0
  185. package/src/server/responses/core-combo.ts +7 -2
  186. package/src/server/responses/core-errors.ts +29 -1
  187. package/src/server/responses/core-options.ts +2 -0
  188. package/src/server/responses/deferred-collab-surface.ts +158 -0
  189. package/src/server/responses/deferred-send-reporter.ts +25 -0
  190. package/src/server/responses/fetch-helpers.ts +12 -1
  191. package/src/server/responses/passthrough-delivery.ts +4 -1
  192. package/src/server/responses/passthrough-dispatch.ts +15 -4
  193. package/src/server/responses/request-prepare.ts +6 -4
  194. package/src/server/responses/request-transport.ts +6 -8
  195. package/src/server/responses/sidecar-execution.ts +11 -2
  196. package/src/server/responses/ws-upstream.ts +9 -1
  197. package/src/server/startup-action-control.ts +22 -2
  198. package/src/server/startup-health-cache.ts +4 -0
  199. package/src/server/system-env-shell.ts +7 -2
  200. package/src/server/system-env.ts +11 -1
  201. package/src/service/cli.ts +5 -1
  202. package/src/service/desktop-startup.ts +147 -34
  203. package/src/service/desktop-supervision.d.mts +27 -1
  204. package/src/service/desktop-supervision.mjs +111 -5
  205. package/src/service/health-sentinel-launch.ts +176 -0
  206. package/src/service/health-sentinel-main.ts +447 -0
  207. package/src/service/health-sentinel-policy.ts +241 -0
  208. package/src/service/health-sentinel-probe.ts +54 -0
  209. package/src/service/health-sentinel-windows.ts +87 -0
  210. package/src/service/linux-linger.ts +20 -0
  211. package/src/types/config.ts +5 -0
  212. package/src/usage/expected-prices.ts +4 -0
  213. package/src/usage/log.ts +19 -1
  214. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  215. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  216. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
@@ -1,4 +1,5 @@
1
1
  import { SendBudgetExhaustedError } from "../../lib/upstream-retry";
2
+ import { anthropicBillingPhysicalFetch } from "../../providers/anthropic-billing-ownership";
2
3
  import { rebindPhysicalSend } from "../../lib/request-execution-budget";
3
4
  import { anthropicModelQuotaFor } from "../../oauth/anthropic-model-quota";
4
5
  import { anthropicRatePolicyFor } from "../../oauth/anthropic-rate-limit-policy";
@@ -87,7 +88,7 @@ import {
87
88
  import type { AttemptRecoveryKind } from "../../usage/log";
88
89
  import { bindAttemptDeliveryRecorder } from "../../usage/attempt-delivery";
89
90
  import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
90
- import { clientCancelledResponse } from "./core-errors";
91
+ import { anthropicPoolCooldownResponse, clientCancelledResponse } from "./core-errors";
91
92
  import { acquireAccountLease, KIRO_ACCOUNT_WAIT_MS } from "../../oauth/kiro-account-load";
92
93
 
93
94
  /** Return a renewed Chat wire to the native owner before sending inference. */
@@ -697,7 +698,7 @@ export async function prepareResponsesTransport(
697
698
  }
698
699
  commitKeyAttemptSend();
699
700
  response = await sendWithConnectionPolicy(
700
- fetchImpl,
701
+ anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider),
701
702
  destination,
702
703
  { ...dispatchInit, redirect: "manual" },
703
704
  { providerName: route.providerName, provider: route.provider },
@@ -784,12 +785,9 @@ export async function prepareResponsesTransport(
784
785
  // Route names may resemble account IDs; log only the matched rule position.
785
786
  if (anthropicRouteDecision) console.warn(`[${anthropicInstance}-pool] route:#${anthropicRouteDecision.position} ${selection.reason}; answering locally`);
786
787
  if (selection.reason === "all-cooled") {
787
- const retryAfterSec = anthropicRouting!.getAnthropicPoolRetryAfterSeconds(Date.now(), anthropicRouteDecision, route.modelId);
788
- return formatErrorResponse(
789
- 429,
790
- "rate_limit_error",
791
- anthropicRouteDecision ? "Anthropic OAuth accounts for this model route are temporarily rate-limited" : "All Anthropic OAuth accounts are temporarily rate-limited",
792
- retryAfterSec !== null ? { retryAfter: String(retryAfterSec) } : undefined,
788
+ return anthropicPoolCooldownResponse(
789
+ anthropicRouting!.anthropicPoolCooldownError(Date.now(), anthropicRouteDecision, route.modelId),
790
+ { comboAttempt: options.comboAttempt, logCtx },
793
791
  );
794
792
  }
795
793
  return formatErrorResponse(401, "authentication_error", anthropicRouteDecision ? "No eligible Anthropic OAuth account for this model route" : "No eligible Anthropic OAuth account available");
@@ -32,7 +32,7 @@ import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
32
32
  import { isNonReplayableResponse } from "../../lib/upstream-retry";
33
33
  import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
34
34
  import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
35
- import { readDisplaySafeErrorText } from "./core-errors";
35
+ import { anthropicPoolCooldownResponse, readDisplaySafeErrorText } from "./core-errors";
36
36
  import {
37
37
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
38
38
  anthropicRoutingFor,
@@ -555,7 +555,7 @@ export async function executeResponsesSidecars(
555
555
  modelId: route.modelId,
556
556
  ...sidecarBudget.fetchOptions(transportState.adapter),
557
557
  })(input, init)) as typeof globalThis.fetch;
558
- const wsResponse = await runWithWebSearch({
558
+ const loopResponse = await runWithWebSearch({
559
559
  parsed, adapter: transportState.adapter,
560
560
  onIterationEnd: sidecarBudget.releaseUnsentHop,
561
561
  fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
@@ -622,6 +622,15 @@ export async function executeResponsesSidecars(
622
622
  notifyResponseComplete(response);
623
623
  },
624
624
  });
625
+ // A routed Anthropic model whose last pool account refused on spent quota fails the loop's
626
+ // first iteration with a generic 429 before any search runs. Answer the usage limit instead,
627
+ // as the main path does (#6991), so Codex stops rather than retrying the whole turn.
628
+ const poolUsageLimit = loopResponse.status === 429 && anthropicInstance && route.provider.authMode === "oauth"
629
+ && !options.comboAttempt
630
+ ? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
631
+ : null;
632
+ if (poolUsageLimit) void loopResponse.body?.cancel().catch(() => {});
633
+ const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx }) : loopResponse;
625
634
  // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
626
635
  // in-flight web-search turns instead of skipping them during graceful shutdown.
627
636
  if (wsResponse.body) {
@@ -23,8 +23,9 @@ import { CODEX_RESPONSES_HTTP_URL, CODEX_RESPONSES_WS_URL, CODEX_WS_FRAME_HEADER
23
23
  import { codexWsExchange } from "./codex-ws-exchange";
24
24
  import { CodexWsSession } from "./codex-ws-session";
25
25
  import { codexWsPool, codexWsReuseIdentity } from "./codex-ws-pool";
26
- import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
26
+ import { codexWsCreateFrameExceedsLimit, type CodexWsPreOutputRefusalHold } from "./codex-ws-wire";
27
27
  import { isLoopbackUrl, rewriteWebSocketDial } from "../../plugins/upstream-hooks";
28
+ import { outboundCredentials } from "../../lib/outbound-credential-registry";
28
29
  export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
29
30
  MAX_CODEX_WS_CREATE_FRAME_BYTES, CODEX_WS_CREATE_FRAME_LIMIT_BYTES, codexWsCreateFrameExceedsLimit,
30
31
  isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse, isCodexWsPreludeProjection } from "./codex-ws-wire";
@@ -162,6 +163,8 @@ export async function codexWsUpstreamFetch(
162
163
  nativeControl?: NativeResponseControl,
163
164
  beforeContinuation?: () => Promise<void>,
164
165
  onPhysicalDispatch?: () => void,
166
+ /** Opt-in pre-output refusal hold (#6969); see codexWsExchange. */
167
+ preOutputRefusalHold?: CodexWsPreOutputRefusalHold,
165
168
  ): Promise<Response> {
166
169
  const prepared = prepareCodexWsRequest(url, init);
167
170
  if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
@@ -221,6 +224,10 @@ export async function codexWsUpstreamFetch(
221
224
  // reused for a different destination or with stale plugin headers.
222
225
  const dial = planCodexWsDial(wsUrl, headers, proxy);
223
226
  if (!dial) return sseFallback(url, init);
227
+ // The final dial headers, after plugin rewrites, are what the WebSocket upstream receives; the
228
+ // per-turn headers planning drops from the upgrade still reach it inside the frame metadata.
229
+ outboundCredentials().remember(headers);
230
+ outboundCredentials().remember(dial.headers);
224
231
  const identity = control ? null : codexWsReuseIdentity(url, dial.headers, frameText, dial.proxy, dial.url);
225
232
  const reuseAcrossTurns = identity !== null && codexWsReuseAcrossTurnsEnabled();
226
233
  const pooled = identity
@@ -243,6 +250,7 @@ export async function codexWsUpstreamFetch(
243
250
  session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
244
251
  nativeControl: control,
245
252
  beforeContinuation, onPhysicalDispatch,
253
+ preOutputRefusalHold,
246
254
  bunVersion: typeof runtime === "string" ? runtime : runtime.version,
247
255
  });
248
256
  }
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
3
  import { join } from "node:path";
4
4
  import { durableBunPath } from "../lib/bun-runtime";
5
+ import { selfLaunchArgv } from "../lib/self-launch-argv";
5
6
  import {
6
7
  WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER,
7
8
  isWindowsSchtasksCreateAccessDenied,
@@ -61,6 +62,18 @@ export function setStartupInstallFinalizeForTests(next: FinalizeFn | null): void
61
62
  finalizeImpl = next ?? finalizeWindowsSchedulerServiceRegistration;
62
63
  }
63
64
 
65
+ interface StartupInstallLaunch {
66
+ execFile?: typeof execFile;
67
+ /** Overrides Bun's own standalone detection, which a source-mode test cannot flip. */
68
+ isStandaloneExecutable?: boolean;
69
+ }
70
+ let installLaunch: StartupInstallLaunch = {};
71
+
72
+ /** Test-only seam: observe the installer child's argv and simulate a compiled `ocx`. */
73
+ export function setStartupInstallLaunchForTests(next: StartupInstallLaunch | null): void {
74
+ installLaunch = next ?? {};
75
+ }
76
+
64
77
  /** Snapshot of the install lock for diagnostics and tests. */
65
78
  export function getStartupInstallState(): StartupInstallState {
66
79
  return installState;
@@ -162,12 +175,19 @@ function runCliInstall(
162
175
  ): Promise<{ stdout: string; stderr: string }> {
163
176
  const bun = durableBunPath();
164
177
  const cli = join(import.meta.dir, "..", "cli", "index.ts");
165
- const argv = [cli, ...startupInstallArgv(action, options)];
178
+ // The Desktop app and the Linux deb run a compiled `ocx`, where `process.execPath` is the CLI
179
+ // itself: prefixing the source entrypoint made it an unknown command and every dashboard
180
+ // Install/Repair failed (#6963). Same builder as the startup-health probe.
181
+ const argv = selfLaunchArgv(startupInstallArgv(action, options), {
182
+ sourceEntrypoint: cli,
183
+ isStandaloneExecutable: installLaunch.isStandaloneExecutable,
184
+ });
185
+ const run = installLaunch.execFile ?? execFile;
166
186
  return new Promise((resolve, reject) => {
167
187
  const timeout = process.platform === "win32" && action === "install-service"
168
188
  ? 0
169
189
  : 60_000;
170
- execFile(bun, argv, {
190
+ run(bun, argv, {
171
191
  encoding: "utf8",
172
192
  env: process.env,
173
193
  // A fresh Windows scheduler install now owns its UAC prompt inside this CLI
@@ -9,6 +9,7 @@ import { selfLaunchArgv } from "../lib/self-launch-argv";
9
9
  import { desktopStartupOwnership } from "../service/desktop-startup";
10
10
  import type { OcxConfig } from "../types";
11
11
  import { truncateRetainedUtf8 } from "../lib/admission";
12
+ import { isPassiveRead } from "../lib/passive-read";
12
13
 
13
14
  const CACHE_TTL_MS = 30_000;
14
15
  const PROBE_TIMEOUT_MS = startupHealthProbeBudgetMs();
@@ -69,6 +70,8 @@ export function getStartupHealthSnapshot(
69
70
  ): StartupHealth {
70
71
  const now = deps.now ?? Date.now;
71
72
  if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
73
+ // A passive read (a Remote Link Child's, #6959) starts no diagnostic process on this machine.
74
+ if (isPassiveRead()) return staleOrFallback(config);
72
75
  refreshInBackground(config, deps);
73
76
  return staleOrFallback(config);
74
77
  }
@@ -189,6 +192,7 @@ export async function getCachedStartupHealth(
189
192
  ): Promise<StartupHealth> {
190
193
  const now = deps.now ?? Date.now;
191
194
  if (cached && now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
195
+ if (isPassiveRead()) return staleOrFallback(config);
192
196
  refreshInBackground(config, deps);
193
197
  // An expired or empty read is an explicit protection check. Wait for the
194
198
  // isolated probe instead of presenting a synthetic failure while that probe
@@ -1,7 +1,7 @@
1
1
  import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
2
2
  import { delimiter, join } from "node:path";
3
3
  import { getConfigDir } from "../config";
4
- import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
4
+ import { claudeTierDisplayName, claudeToolSearchEnv, isClaudeTierLabelSlot, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
5
5
  import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
6
6
  import { resolveClaudeAuthMode } from "../claude/auth-mode";
7
7
  import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
@@ -115,7 +115,12 @@ export function writeShellEnvFile(
115
115
  }
116
116
  for (const [name, value] of Object.entries(modelEnv)) {
117
117
  if (name === "ANTHROPIC_MODEL") continue;
118
- lines.push(conditional(name, value));
118
+ // An alias tier slot carries its picker label (#6926) in the same branch, so the label is
119
+ // exported only when this file writes the slot and the shell has no label of its own.
120
+ const label = isClaudeTierLabelSlot(name) ? claudeTierDisplayName(value) : undefined;
121
+ lines.push(label === undefined
122
+ ? conditional(name, value)
123
+ : `${conditional(name, value)} && [ -z "\${${name}_NAME+x}" ] && export ${name}_NAME=${shellValue(label)}`);
119
124
  }
120
125
  const maxCtx = config.claudeCode?.maxContextTokens;
121
126
  if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { getConfigDir } from "../config";
5
- import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
5
+ import { CLAUDE_TIER_LABEL_SLOTS, claudeTierDisplayName, claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
6
6
  import { PROXY_MARKER } from "../claude/auth-detect";
7
7
  import { isProxyAdmissionSecret } from "./auth-cors";
8
8
  import type { OcxConfig } from "../types";
@@ -30,6 +30,8 @@ const MANAGED_SYSTEM_ENV_NAMES = new Set<string>([
30
30
  "ANTHROPIC_DEFAULT_FABLE_MODEL",
31
31
  "ANTHROPIC_DEFAULT_HAIKU_MODEL",
32
32
  "ANTHROPIC_SMALL_FAST_MODEL",
33
+ // Picker labels paired with an alias tier slot this instance injected (#6926).
34
+ ...CLAUDE_TIER_LABEL_SLOTS.map(slot => `${slot}_NAME`),
33
35
  "CLAUDE_CODE_MAX_CONTEXT_TOKENS",
34
36
  "DISABLE_COMPACT",
35
37
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW",
@@ -315,6 +317,14 @@ export async function injectSystemEnv(
315
317
  if (name === "ANTHROPIC_MODEL") continue; // legacy slot handled by shell file only (back-compat)
316
318
  injectLever(name, value);
317
319
  }
320
+ // Picker labels (#6926) only for alias tier slots this instance owns after the loop above: a
321
+ // launchd slot the user set keeps whatever label it has, and a label nothing produces any
322
+ // more is unset by the produced-key sweep below like any other tracked lever.
323
+ for (const slot of CLAUDE_TIER_LABEL_SLOTS) {
324
+ const value = modelEnv[slot];
325
+ const label = value === undefined ? undefined : claudeTierDisplayName(value);
326
+ if (label !== undefined && injectedKeys.includes(slot)) injectLever(`${slot}_NAME`, label);
327
+ }
318
328
  const maxCtx = config.claudeCode?.maxContextTokens;
319
329
  if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
320
330
  injectLever("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)));
@@ -27,6 +27,8 @@ import { win32 } from "node:path";
27
27
  import { serviceDiagnosticsSummary } from "./diagnostics";
28
28
  import { runServiceClaim } from "./claim";
29
29
  import { createSupervisionLatch, desktopServiceCommandRefusal } from "./desktop-command-guard";
30
+ import { linuxServiceBootHint } from "./linux-linger";
31
+ import { diagnoseDesktopStartup, desktopLoginStatusSummary } from "./desktop-startup";
30
32
 
31
33
  /**
32
34
  * `restart` is NO LONGER folded into `repair`.
@@ -336,7 +338,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
336
338
  // and the baked one cannot have diverged yet — unlike `start`, which reads the
337
339
  // installed artifact instead.
338
340
  await reportServiceServing("installed", { port: resolveServiceListenPort() });
339
- if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
341
+ if (process.platform === "linux") console.log(linuxServiceBootHint());
340
342
  // Service users never reach the `ocx start` prompt: the proxy they run is the
341
343
  // supervised child, which always carries OCX_SERVICE=1. This command, though, is
342
344
  // hand-typed in a real terminal, so it is the one interactive moment they get.
@@ -433,6 +435,8 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
433
435
  const lease = ownershipMutationLeaseStatusLine(serviceStatePaths());
434
436
  if (lease) console.log(lease);
435
437
  console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
438
+ const desktop = diagnoseDesktopStartup();
439
+ if (desktop) console.log(desktopLoginStatusSummary(desktop));
436
440
  break;
437
441
  }
438
442
  case "uninstall":
@@ -1,18 +1,24 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
2
+ import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
5
5
  import { readPid } from "../config/process-state";
6
6
  import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
7
- import { inspectDesktopSupervision, processIdentity, procfs, type ProcReader } from "./desktop-supervision.mjs";
7
+ import { inspectDesktopSupervision, processIdentity, procfs, type DesktopSupervisionDeps, type ProcReader } from "./desktop-supervision.mjs";
8
8
 
9
9
  export interface DesktopStartupDiagnostic {
10
10
  /** Durable desktop claim; failed supervision does not release ownership. */
11
11
  owned: boolean;
12
12
  loginEnabled: boolean;
13
+ /** Verified enabled macOS registration; absence means registration is not credited. */
14
+ loginRegistration?: "loaded" | "registered-not-loaded";
13
15
  running: boolean;
14
16
  viable: boolean;
15
- /** Live parentage when no durable claim exists; never converts supervision into ownership. */
17
+ /**
18
+ * Proven live parentage; never converts supervision into ownership. macOS and Linux report it
19
+ * only without a durable claim. Windows also reports it beside a durable Desktop claim, because
20
+ * older Windows runtimes never published a Desktop diagnostic that status could defer to.
21
+ */
16
22
  supervisor?: { supervisorPid: number; runtimePid: number; app: string };
17
23
  }
18
24
 
@@ -21,6 +27,14 @@ export function deriveDesktopStartup(facts: Omit<DesktopStartupDiagnostic, "viab
21
27
  return { ...facts, viable: (facts.owned || facts.supervisor !== undefined) && facts.loginEnabled && facts.running };
22
28
  }
23
29
 
30
+ export function desktopLoginStatusSummary(desktop: DesktopStartupDiagnostic): string {
31
+ if (!desktop.loginEnabled) return "Desktop Start at Login: not registered, disabled, or could not be verified.";
32
+ if (desktop.loginRegistration === "registered-not-loaded") {
33
+ return "Desktop Start at Login: registered and enabled for next login; not loaded in this session.";
34
+ }
35
+ return `Desktop Start at Login: enabled${desktop.loginRegistration === "loaded" ? " and loaded in this session" : ""}.`;
36
+ }
37
+
24
38
  function run(command: string, args: string[]): string {
25
39
  return execFileSync(command, args, { encoding: "utf8", timeout: 750, maxBuffer: 128 * 1024, stdio: ["ignore", "pipe", "pipe"] }).trim();
26
40
  }
@@ -35,12 +49,16 @@ interface DesktopStartupDeps {
35
49
  run?: typeof run;
36
50
  env?: NodeJS.ProcessEnv;
37
51
  proc?: ProcReader;
52
+ windowsProcesses?: DesktopSupervisionDeps["windowsProcesses"];
53
+ now?: () => number;
54
+ /** The Desktop's own install id (Tauri app_config_dir); null when unreadable. */
55
+ readWindowsInstallId?: () => string | null;
38
56
  }
39
57
 
40
58
  /** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
41
59
  export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
42
60
  const platform = deps.platform ?? process.platform;
43
- if (platform !== "darwin" && platform !== "linux") return undefined;
61
+ if (platform !== "darwin" && platform !== "linux" && platform !== "win32") return undefined;
44
62
  const owner = (deps.ownership ?? resolveServiceOwnership)();
45
63
  return owner.kind === "owned" && owner.ownership.owner === "desktop"
46
64
  ? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
@@ -49,33 +67,85 @@ export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopS
49
67
  function macLoginRegistration(deps: DesktopStartupDeps) {
50
68
  const execute = deps.run ?? run;
51
69
  const path = join(deps.home ?? homedir(), "Library", "LaunchAgents", "OpenCodex.plist");
52
- const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
53
- const args = plist.ProgramArguments;
54
- if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
55
- || args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
56
- || !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
57
- || (plist.Program !== undefined && plist.Program !== args[0])) return null;
58
- const app = realpathSync(args[0]);
59
- const proxy = realpathSync(join(dirname(app), "ocx"));
60
- accessSync(app, constants.X_OK);
61
- accessSync(proxy, constants.X_OK);
62
70
  const domain = `gui/${deps.uid ?? process.getuid!()}`;
63
- const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
64
- const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
71
+ const identity = (file: string) => {
72
+ const stat = lstatSync(file, { bigint: true });
73
+ return [stat.dev, stat.ino, stat.size, stat.mode, stat.mtimeNs, stat.ctimeNs].map(String).join(":");
74
+ };
75
+ const capture = () => {
76
+ const registrationPath = realpathSync(path);
77
+ const plistIdentity = identity(path), targetIdentity = identity(registrationPath);
78
+ const definition = execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]);
79
+ const plist = JSON.parse(definition);
80
+ const args = plist.ProgramArguments;
81
+ if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
82
+ || args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
83
+ || !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
84
+ || (plist.Program !== undefined && plist.Program !== args[0])
85
+ || (plist.Disabled !== undefined && typeof plist.Disabled !== "boolean")) return null;
86
+ const app = realpathSync(args[0]);
87
+ const proxy = realpathSync(join(dirname(app), "ocx"));
88
+ accessSync(app, constants.X_OK);
89
+ accessSync(proxy, constants.X_OK);
90
+ const appIdentity = identity(app), proxyIdentity = identity(proxy);
91
+ const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
92
+ const mentions = disabled.match(/"OpenCodex"/g)?.length ?? 0;
93
+ const override = /"OpenCodex"\s*=>\s*(enabled|false|disabled|true)(?=\s*(?:\r?\n|[,;}]|$))/.exec(disabled)?.[1];
94
+ const loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
95
+ && (mentions === 0 ? plist.Disabled !== true : mentions === 1 && (override === "enabled" || override === "false"));
96
+ if (registrationPath !== realpathSync(path) || plistIdentity !== identity(path) || targetIdentity !== identity(registrationPath)
97
+ || app !== realpathSync(args[0]) || proxy !== realpathSync(join(dirname(app), "ocx"))
98
+ || appIdentity !== identity(app) || proxyIdentity !== identity(proxy)) return null;
99
+ return { app, proxy, loginEnabled, registrationPath, plistIdentity, targetIdentity, definition, appIdentity, proxyIdentity, disabled };
100
+ };
101
+ const first = capture();
102
+ if (!first) return null;
103
+ const { app, proxy } = first;
104
+ if (!first.loginEnabled) return { app, proxy, loginEnabled: false };
105
+ let loaded: string;
106
+ try { loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]); }
107
+ catch (error) {
108
+ // execFileSync's specific service-not-found result is absence, not a failed probe.
109
+ // Timeout, permission denial, missing tools and another domain remain unverified.
110
+ const failure = error as { status?: number; stderr?: unknown; signal?: unknown; code?: unknown } | null;
111
+ const stderr = typeof failure?.stderr === "string" ? failure.stderr
112
+ : Buffer.isBuffer(failure?.stderr) ? failure.stderr.toString("utf8") : "";
113
+ const message = `Could not find service "OpenCodex" in domain for user gui: ${domain.slice(4)}`;
114
+ const missing = failure?.status === 113 && !failure.signal && !failure.code
115
+ && [message, `Bad request.\n${message}`].includes(stderr.trim());
116
+ if (!missing) throw error;
117
+ // The absent job supplies no durable registration evidence. Re-read the entire
118
+ // registration and enabled state; replacement or ambiguity revokes login credit.
119
+ const final = capture();
120
+ if (!final || JSON.stringify(first) !== JSON.stringify(final)) return { app, proxy, loginEnabled: false };
121
+ return { app, proxy, loginEnabled: true, loginRegistration: "registered-not-loaded" as const };
122
+ }
65
123
  const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
66
124
  const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
67
- const loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
68
- && !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
69
- && program !== undefined && realpathSync(program) === app
70
- && loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
71
- return { app, proxy, loginEnabled };
125
+ const loginEnabled = program !== undefined && realpathSync(program) === app
126
+ && loadedPath !== undefined && realpathSync(loadedPath) === first.registrationPath;
127
+ return { app, proxy, loginEnabled, ...(loginEnabled ? { loginRegistration: "loaded" as const } : {}) };
128
+ }
129
+
130
+ type DesktopLoginFacts = Pick<DesktopStartupDiagnostic, "loginEnabled" | "loginRegistration">;
131
+
132
+ /**
133
+ * Start at Login credit for one Desktop app copy. The tray's Start at Login item ports this
134
+ * judgment (desktop/src-tauri/src/login_registration.rs); both run the cases in
135
+ * tests/fixtures/desktop-login-registration-cases.json. Throws when evidence is unreadable.
136
+ */
137
+ export function macLoginFactsFor(app: string, deps: DesktopStartupDeps): DesktopLoginFacts {
138
+ const login = macLoginRegistration(deps);
139
+ return login !== null && login.app === app
140
+ ? { loginEnabled: login.loginEnabled, ...(login.loginRegistration ? { loginRegistration: login.loginRegistration } : {}) }
141
+ : { loginEnabled: false };
72
142
  }
73
143
 
74
- function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) => boolean): DesktopStartupDiagnostic | undefined {
144
+ function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) => DesktopLoginFacts): DesktopStartupDiagnostic | undefined {
75
145
  const evidence = inspectDesktopSupervision(deps);
76
146
  if (evidence.kind !== "desktop") return undefined;
77
- let loginEnabled = false;
78
- try { loginEnabled = loginFor(evidence.app); }
147
+ let login: DesktopLoginFacts = { loginEnabled: false };
148
+ try { login = loginFor(evidence.app); }
79
149
  catch { /* Login failure revokes restart viability, not the observed live supervisor. */ }
80
150
  if ((deps.ownership ?? resolveServiceOwnership)().kind !== "none") return undefined;
81
151
  // Login probes can outlive the runtime or its parent; only the same live chain gets credit.
@@ -86,7 +156,7 @@ function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: numb
86
156
  const finalOwner = (deps.ownership ?? resolveServiceOwnership)();
87
157
  if (finalOwner.kind !== "none" || finalOwner.revision !== ownerRevision) return undefined;
88
158
  const { supervisorPid, runtimePid, app } = evidence;
89
- return deriveDesktopStartup({ owned: false, loginEnabled, running: true, supervisor: { supervisorPid, runtimePid, app } });
159
+ return deriveDesktopStartup({ owned: false, ...login, running: true, supervisor: { supervisorPid, runtimePid, app } });
90
160
  }
91
161
 
92
162
  /** Read-only macOS desktop ownership, login registration and live parent/child checks. */
@@ -94,12 +164,9 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
94
164
  if ((deps.platform ?? process.platform) !== "darwin") return undefined;
95
165
  const ownership = deps.ownership ?? resolveServiceOwnership;
96
166
  const owner = ownership();
97
- if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
98
- const login = macLoginRegistration(deps);
99
- return login !== null && login.app === app && login.loginEnabled;
100
- });
167
+ if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => macLoginFactsFor(app, deps));
101
168
  if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
102
- const facts = { owned: true, loginEnabled: false, running: false };
169
+ const facts: Omit<DesktopStartupDiagnostic, "viable"> = { owned: true, loginEnabled: false, running: false };
103
170
  const execute = deps.run ?? run;
104
171
  const pidReader = deps.readPid ?? readPid;
105
172
  try {
@@ -110,6 +177,7 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
110
177
  if (!login) return deriveDesktopStartup(facts);
111
178
  const { app, proxy } = login;
112
179
  facts.loginEnabled = login.loginEnabled;
180
+ if (login.loginRegistration) facts.loginRegistration = login.loginRegistration;
113
181
  const pid = pidReader();
114
182
  if (pid !== null) {
115
183
  const child = processIdentity(pid, execute);
@@ -156,10 +224,10 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
156
224
  const configured = (deps.env ?? process.env).XDG_CONFIG_HOME;
157
225
  const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
158
226
  // auto-launch's HOME entry must remain inside the session's XDG autostart search path.
159
- if (resolve(config) !== resolve(home, ".config")) return false;
227
+ if (resolve(config) !== resolve(home, ".config")) return { loginEnabled: false };
160
228
  const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
161
229
  const login = linuxLoginApp(entry);
162
- return login !== null && realpathSync(login) === app;
230
+ return { loginEnabled: login !== null && realpathSync(login) === app };
163
231
  });
164
232
  if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
165
233
  const facts = { owned: true, loginEnabled: false, running: false };
@@ -212,5 +280,50 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
212
280
 
213
281
  /** Platform dispatch for the full (probing) desktop startup diagnostic. */
214
282
  export function diagnoseDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
215
- return (deps.platform ?? process.platform) === "linux" ? diagnoseLinuxDesktopStartup(deps) : diagnoseMacDesktopStartup(deps);
283
+ const platform = deps.platform ?? process.platform;
284
+ if (platform === "linux") return diagnoseLinuxDesktopStartup(deps);
285
+ if (platform === "win32") return diagnoseWindowsDesktopStartup(deps);
286
+ return diagnoseMacDesktopStartup(deps);
287
+ }
288
+
289
+ /** Tauri resolves app_config_dir to the roaming AppData folder plus the bundle identifier. */
290
+ function readWindowsInstallId(env: NodeJS.ProcessEnv): string | null {
291
+ const appData = env.APPDATA;
292
+ if (!appData || !/^[A-Za-z]:[\\/]/.test(appData)) return null;
293
+ try {
294
+ return readFileSync(win32.join(appData, "com.opencodex.desktop", "install-id"), "utf8").trim() || null;
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Read-only Windows desktop ownership and live parent/child checks. Windows Start at Login is
302
+ * not verified, so it is never credited and Desktop never reads as startup protection here;
303
+ * the diagnostic still names the supervisor so status and doctor stop recommending service repairs.
304
+ */
305
+ export function diagnoseWindowsDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
306
+ if ((deps.platform ?? process.platform) !== "win32") return undefined;
307
+ const ownership = deps.ownership ?? resolveServiceOwnership;
308
+ const owner = ownership();
309
+ if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, () => ({ loginEnabled: false }));
310
+ if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
311
+ const facts = { owned: true, loginEnabled: false, running: false };
312
+ try {
313
+ const installId = deps.readWindowsInstallId ? deps.readWindowsInstallId() : readWindowsInstallId(deps.env ?? process.env);
314
+ if (installId !== owner.ownership.installId) return deriveDesktopStartup(facts);
315
+ const evidence = inspectDesktopSupervision(deps);
316
+ if (evidence.kind !== "desktop") return deriveDesktopStartup(facts);
317
+ // Only the same live chain, read again after the claim, earns running and the supervisor.
318
+ const final = inspectDesktopSupervision({ ...deps, targetPid: evidence.runtimePid });
319
+ const current = ownership();
320
+ if (final.kind !== "desktop" || final.runtimePid !== evidence.runtimePid
321
+ || final.supervisorPid !== evidence.supervisorPid || final.app !== evidence.app
322
+ || current.kind === "unknown" || !sameServiceOwnershipSubject(owner, current)) return deriveDesktopStartup(facts);
323
+ const { supervisorPid, runtimePid, app } = final;
324
+ return deriveDesktopStartup({ ...facts, running: true, supervisor: { supervisorPid, runtimePid, app } });
325
+ } catch {
326
+ // Unreadable evidence never grants running or releases the claim.
327
+ return deriveDesktopStartup(facts);
328
+ }
216
329
  }
@@ -9,18 +9,44 @@ export interface ProcReader {
9
9
  parent(pid: number): number;
10
10
  }
11
11
 
12
+ /** One Win32_Process row; `created` is UTC ticks as a decimal string (exceeds 2^53). */
13
+ export interface WindowsProcessRow {
14
+ pid: number;
15
+ parent: number;
16
+ exe: string | null;
17
+ session: number;
18
+ created: string;
19
+ }
20
+
21
+ export interface WindowsProcessPair {
22
+ child: WindowsProcessRow | null;
23
+ parent: WindowsProcessRow | null;
24
+ }
25
+
26
+ export interface WindowsProcessReadOptions {
27
+ /** Called with the parent image before row validation, so partial Desktop evidence keeps its veto. */
28
+ onExecutable?: (path: string) => void;
29
+ timeoutMs?: number;
30
+ env?: NodeJS.ProcessEnv;
31
+ }
32
+
12
33
  export interface DesktopSupervisionDeps {
13
34
  targetPid?: number;
14
35
  platform?: NodeJS.Platform;
15
- run?: (command: string, args: string[]) => string;
36
+ run?: (command: string, args: string[], timeoutMs?: number) => string;
16
37
  proc?: ProcReader;
17
38
  readPid?: () => number | null;
18
39
  readRuntimePortPid?: () => number | null;
40
+ windowsProcesses?: (pid: number, options: Pick<WindowsProcessReadOptions, "onExecutable" | "timeoutMs">) => WindowsProcessPair | null;
41
+ env?: NodeJS.ProcessEnv;
42
+ now?: () => number;
19
43
  }
20
44
 
21
45
  export function desktopSupervisionPaths(): { pid: string; runtimePort: string };
22
46
  export const procfs: ProcReader;
23
47
  export function processIdentity(pid: number, execute?: DesktopSupervisionDeps["run"], onExecutable?: (path: string) => void):
24
48
  { parent: number; executable: string } | null;
49
+ export function windowsPowerShellPath(env?: NodeJS.ProcessEnv): string;
50
+ export function windowsProcessPair(pid: number, execute?: DesktopSupervisionDeps["run"], options?: WindowsProcessReadOptions): WindowsProcessPair;
25
51
  export function inspectDesktopSupervision(deps?: DesktopSupervisionDeps): SupervisionEvidence;
26
52
  export function createSupervisionLatch(): { observe(evidence: SupervisionEvidence): boolean };