@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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 (173) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +47 -22
  3. package/gui/dist/assets/index-BDjpkcRN.js +67 -0
  4. package/gui/dist/assets/index-BHsKRFh9.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/discovery.ts +4 -1
  10. package/src/adapters/cursor/effort-map.ts +3 -0
  11. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  12. package/src/adapters/cursor/protobuf-events.ts +24 -2
  13. package/src/adapters/cursor/protobuf-request.ts +1 -2
  14. package/src/adapters/cursor/tool-definitions.ts +68 -29
  15. package/src/adapters/google-wire-compiler.ts +4 -0
  16. package/src/adapters/google.ts +128 -2
  17. package/src/adapters/identity.ts +12 -2
  18. package/src/adapters/kiro.ts +64 -7
  19. package/src/adapters/mimo-free.ts +2 -0
  20. package/src/adapters/openai-responses.ts +246 -59
  21. package/src/claude/agents-inject.ts +5 -0
  22. package/src/claude/alias.ts +94 -14
  23. package/src/claude/inbound.ts +26 -9
  24. package/src/claude/outbound.ts +6 -3
  25. package/src/cli/account-auth.ts +1 -1
  26. package/src/cli/agent-driven.ts +37 -0
  27. package/src/cli/catalog-prewarm.ts +24 -0
  28. package/src/cli/claude.ts +35 -10
  29. package/src/cli/doctor.ts +71 -19
  30. package/src/cli/help.ts +42 -6
  31. package/src/cli/index.ts +93 -19
  32. package/src/cli/interactive-confirm.ts +133 -0
  33. package/src/cli/opencode.ts +701 -0
  34. package/src/cli/provider-runtime.ts +3 -0
  35. package/src/cli/provider.ts +31 -10
  36. package/src/cli/star-prompt.ts +79 -18
  37. package/src/cli/status.ts +47 -13
  38. package/src/cli/v2.ts +10 -1
  39. package/src/codex/account-id.ts +34 -0
  40. package/src/codex/account-lifecycle.ts +4 -1
  41. package/src/codex/account-namespace-match.ts +63 -0
  42. package/src/codex/account-namespaces.ts +149 -0
  43. package/src/codex/account-pause.ts +20 -0
  44. package/src/codex/account-store.ts +2 -0
  45. package/src/codex/account-usability.ts +6 -1
  46. package/src/codex/app-server-processes.ts +511 -0
  47. package/src/codex/auth-api.ts +293 -34
  48. package/src/codex/auth-collision.ts +2 -1
  49. package/src/codex/auth-context.ts +60 -17
  50. package/src/codex/catalog/bundled.ts +9 -2
  51. package/src/codex/catalog/parsing.ts +42 -2
  52. package/src/codex/catalog/provider-fetch.ts +264 -70
  53. package/src/codex/catalog/sync.ts +45 -8
  54. package/src/codex/catalog.ts +2 -2
  55. package/src/codex/features.ts +524 -5
  56. package/src/codex/history-provider.ts +145 -1
  57. package/src/codex/inject.ts +114 -14
  58. package/src/codex/main-account.ts +2 -8
  59. package/src/codex/pool-rotation.ts +186 -0
  60. package/src/codex/quota.ts +92 -2
  61. package/src/codex/routing.ts +695 -106
  62. package/src/codex/runtime.ts +10 -1
  63. package/src/codex/shim.ts +4 -1
  64. package/src/codex/subagent-defaults.ts +550 -0
  65. package/src/codex/subagent-model-fallback.ts +2 -0
  66. package/src/codex/sync.ts +3 -0
  67. package/src/config.ts +574 -25
  68. package/src/generated/jawcode-model-metadata.ts +12 -12
  69. package/src/github/star-state.ts +191 -0
  70. package/src/images/artifacts.ts +516 -0
  71. package/src/images/fulfill-video.ts +163 -0
  72. package/src/images/fulfill.ts +111 -0
  73. package/src/images/index.ts +4 -0
  74. package/src/images/loop.ts +789 -0
  75. package/src/images/plan.ts +133 -0
  76. package/src/images/synthetic-tool.ts +133 -0
  77. package/src/images/types.ts +41 -0
  78. package/src/images/xai-client.ts +141 -0
  79. package/src/images/xai-video-client.ts +163 -0
  80. package/src/lib/admin-secrets.ts +25 -0
  81. package/src/lib/bun-binary-validator.d.mts +3 -0
  82. package/src/lib/bun-binary-validator.mjs +18 -0
  83. package/src/lib/bun-runtime.ts +6 -20
  84. package/src/lib/config-ownership.ts +327 -0
  85. package/src/lib/crash-guard.ts +2 -0
  86. package/src/lib/destination-policy.ts +132 -7
  87. package/src/lib/pinned-http.ts +151 -0
  88. package/src/lib/process-control.ts +2 -2
  89. package/src/lib/provider-outbound.ts +167 -0
  90. package/src/lib/provider-url.ts +14 -0
  91. package/src/lib/proxy-env.ts +18 -0
  92. package/src/lib/shadow-call.ts +30 -0
  93. package/src/lib/test-home-guard.ts +90 -0
  94. package/src/lib/win-exec.ts +12 -2
  95. package/src/lib/windows-elevation.ts +81 -3
  96. package/src/lib/windows-secret-acl.ts +189 -12
  97. package/src/lib/winsw.ts +2 -0
  98. package/src/oauth/anthropic-routing.ts +570 -0
  99. package/src/oauth/health.ts +6 -0
  100. package/src/oauth/index.ts +310 -75
  101. package/src/oauth/key-providers.ts +38 -8
  102. package/src/oauth/kimi.ts +2 -0
  103. package/src/oauth/kiro-credentials.ts +373 -12
  104. package/src/oauth/kiro.ts +424 -43
  105. package/src/oauth/login-cli.ts +33 -6
  106. package/src/oauth/store.ts +56 -4
  107. package/src/oauth/types.ts +11 -0
  108. package/src/providers/alibaba-region-migration.ts +16 -3
  109. package/src/providers/antigravity-models.ts +3 -0
  110. package/src/providers/api-keys.ts +13 -6
  111. package/src/providers/derive.ts +8 -2
  112. package/src/providers/key-failover.ts +24 -4
  113. package/src/providers/model-discovery.ts +356 -0
  114. package/src/providers/quota.ts +233 -29
  115. package/src/providers/registry.ts +125 -3
  116. package/src/responses/parser.ts +11 -0
  117. package/src/responses/state.ts +22 -8
  118. package/src/responses/tool-groups.ts +19 -0
  119. package/src/router.ts +19 -7
  120. package/src/server/auth-cors.ts +114 -24
  121. package/src/server/claude-messages.ts +8 -1
  122. package/src/server/gui-static.ts +30 -6
  123. package/src/server/images.ts +303 -9
  124. package/src/server/index.ts +77 -9
  125. package/src/server/lifecycle.ts +25 -1
  126. package/src/server/live.ts +75 -25
  127. package/src/server/management/agent-settings-routes.ts +106 -8
  128. package/src/server/management/combo-routes.ts +7 -0
  129. package/src/server/management/config-routes.ts +22 -7
  130. package/src/server/management/context.ts +11 -1
  131. package/src/server/management/logs-usage-routes.ts +167 -3
  132. package/src/server/management/model-routes.ts +46 -13
  133. package/src/server/management/oauth-account-routes.ts +163 -17
  134. package/src/server/management/provider-routes.ts +73 -10
  135. package/src/server/management/shared.ts +2 -2
  136. package/src/server/management/sidebar-routes.ts +39 -0
  137. package/src/server/management/system-restart.ts +172 -0
  138. package/src/server/management/system-routes.ts +33 -10
  139. package/src/server/management-api.ts +5 -3
  140. package/src/server/management-auth.ts +216 -0
  141. package/src/server/proxy-liveness.ts +14 -3
  142. package/src/server/responses/compact.ts +21 -13
  143. package/src/server/responses/core.ts +614 -172
  144. package/src/server/responses/upstream-error.ts +48 -0
  145. package/src/server/responses-image-gen-repair.ts +118 -0
  146. package/src/server/responses-item-id-repair.ts +10 -85
  147. package/src/server/sse-payload-rewrite.ts +116 -0
  148. package/src/server/startup-action-control.ts +30 -14
  149. package/src/server/system-env.ts +28 -10
  150. package/src/service.ts +284 -19
  151. package/src/storage/cleanup-job.ts +57 -0
  152. package/src/storage/cleanup.ts +1504 -28
  153. package/src/storage/policy-job.ts +387 -0
  154. package/src/storage/policy-scheduler.ts +40 -0
  155. package/src/storage/policy-worker.ts +53 -0
  156. package/src/storage/policy.ts +522 -0
  157. package/src/storage/restore-job.ts +253 -0
  158. package/src/storage/restore-worker.ts +52 -0
  159. package/src/storage/storage-mutation-coordinator.ts +109 -0
  160. package/src/storage/worker-lifecycle.ts +81 -0
  161. package/src/tray/windows.ts +34 -4
  162. package/src/types.ts +107 -1
  163. package/src/update/badge.ts +72 -0
  164. package/src/update/index.ts +36 -18
  165. package/src/update/job.ts +111 -16
  166. package/src/update/npm-invocation.d.mts +23 -0
  167. package/src/update/npm-invocation.mjs +94 -0
  168. package/src/usage/debug.ts +2 -0
  169. package/src/usage/expected-prices.ts +6 -5
  170. package/src/usage/log.ts +12 -0
  171. package/src/web-search/loop.ts +57 -16
  172. package/gui/dist/assets/index-CjKFJHSC.js +0 -65
  173. package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/service.ts CHANGED
@@ -17,6 +17,7 @@ import { isWslRuntime } from "./codex/home";
17
17
  import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
18
18
  import { isProcessAlive, stopProxy } from "./lib/process-control";
19
19
  import { serviceApiTokenFilePath } from "./lib/service-secrets";
20
+ import { findLiveProxy } from "./server/proxy-liveness";
20
21
  import { randomUUID } from "node:crypto";
21
22
  import {
22
23
  ELEVATION_REQUEST_TIMEOUT_MS,
@@ -34,6 +35,8 @@ import {
34
35
  import { defaultWinswEntry, installWinswService, startWinswService, stopWinswService, statusWinswRaw, uninstallWinswService, winswStatusSummary, WINSW_SERVICE_ID, WINSW_SHA256, WINSW_VERSION } from "./lib/winsw";
35
36
  import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl";
36
37
  import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
38
+ import { recordOwnedConfigPath } from "./lib/config-ownership";
39
+ import { maybeShowStarPrompt } from "./cli/star-prompt";
37
40
 
38
41
  const LABEL = "com.opencodex.proxy";
39
42
  const TASK = "opencodex-proxy";
@@ -146,6 +149,7 @@ function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void {
146
149
  };
147
150
  for (const path of serviceStatePaths()) {
148
151
  const dir = dirname(path);
152
+ recordOwnedConfigPath(getConfigDir(), path);
149
153
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
150
154
  writeFileSync(path, JSON.stringify(state, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
151
155
  try { chmodSync(path, 0o600); } catch { /* best-effort */ }
@@ -254,6 +258,7 @@ function writeServiceApiTokenFile(): string | null {
254
258
  if (!token) return null;
255
259
  const path = serviceApiTokenFilePath();
256
260
  const dir = getConfigDir();
261
+ recordOwnedConfigPath(dir, path);
257
262
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
258
263
  if (process.platform === "win32") hardenSecretDir(dir, { required: true });
259
264
  writeFileSync(path, `${token}\n`, { encoding: "utf8", mode: 0o600 });
@@ -351,8 +356,41 @@ function sh(cmd: string): string {
351
356
  return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
352
357
  }
353
358
 
359
+ /**
360
+ * Decode schtasks stdout. `/query /xml` emits UTF-16LE (often with BOM) because the
361
+ * registered task document is UTF-16; reading that as UTF-8 makes every health check
362
+ * fail ("registration present but unhealthy") and rolls back a successful elevated create.
363
+ */
364
+ export function decodeSchtasksOutput(buffer: Buffer): string {
365
+ if (buffer.length === 0) return "";
366
+ const bomUtf16Le = buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe;
367
+ const bomUtf16Be = buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff;
368
+ const looksUtf16Le = buffer.length >= 4
369
+ && buffer[1] === 0x00
370
+ && buffer[3] === 0x00
371
+ && buffer[0] !== 0x00;
372
+ if (bomUtf16Le || looksUtf16Le) {
373
+ return buffer.toString("utf16le").replace(/^\uFEFF/, "").trim();
374
+ }
375
+ if (bomUtf16Be) {
376
+ // Swap pairs then decode as utf16le.
377
+ const swapped = Buffer.alloc(buffer.length - 2);
378
+ for (let i = 2; i + 1 < buffer.length; i += 2) {
379
+ swapped[i - 2] = buffer[i + 1]!;
380
+ swapped[i - 1] = buffer[i]!;
381
+ }
382
+ return swapped.toString("utf16le").trim();
383
+ }
384
+ return buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
385
+ }
386
+
354
387
  function runFile(file: string, args: string[]): string {
355
- return execFileSync(file, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
388
+ const buffer = execFileSync(file, args, {
389
+ encoding: "buffer",
390
+ stdio: ["ignore", "pipe", "pipe"],
391
+ windowsHide: true,
392
+ }) as Buffer;
393
+ return decodeSchtasksOutput(buffer);
356
394
  }
357
395
 
358
396
  function windowsSchtasks(): string {
@@ -390,6 +428,70 @@ export type WindowsSchedulerTaskProbe =
390
428
  | { status: "absent" }
391
429
  | { status: "unknown"; detail: string };
392
430
 
431
+ export type WindowsSchedulerProxyProbe =
432
+ | { status: "running"; port: number }
433
+ | { status: "not-running" }
434
+ | { status: "unknown" };
435
+
436
+ /**
437
+ * Render Task Scheduler status without exposing localized `schtasks` table output.
438
+ * The task probe answers installation state; the identity-checked health probe answers
439
+ * runtime state. Keep probe details out of this user-facing line because they can contain
440
+ * incorrectly decoded, locale-specific command output.
441
+ */
442
+ export function formatWindowsSchedulerServiceStatus(
443
+ task: WindowsSchedulerTaskProbe,
444
+ proxy: WindowsSchedulerProxyProbe,
445
+ ): string {
446
+ if (task.status === "present") {
447
+ if (proxy.status === "running") {
448
+ return `✅ service installed (Task Scheduler); OpenCodex proxy running on port ${proxy.port}.`;
449
+ }
450
+ if (proxy.status === "not-running") {
451
+ return "⚠️ service installed (Task Scheduler); OpenCodex proxy not running.";
452
+ }
453
+ return "⚠️ service installed (Task Scheduler); OpenCodex proxy status unknown.";
454
+ }
455
+ if (task.status === "absent") {
456
+ if (proxy.status === "running") {
457
+ return `❌ service not installed (Task Scheduler); OpenCodex proxy is running independently on port ${proxy.port}.`;
458
+ }
459
+ if (proxy.status === "unknown") {
460
+ return "❌ service not installed (Task Scheduler); OpenCodex proxy status unknown.";
461
+ }
462
+ return "❌ service not installed (Task Scheduler).";
463
+ }
464
+ if (proxy.status === "running") {
465
+ return `⚠️ Task Scheduler registration unknown; OpenCodex proxy running on port ${proxy.port}.`;
466
+ }
467
+ if (proxy.status === "not-running") {
468
+ return "⚠️ service status unknown (Task Scheduler query failed); OpenCodex proxy not running.";
469
+ }
470
+ return "⚠️ service status unknown (Task Scheduler and proxy checks failed).";
471
+ }
472
+
473
+ export async function inspectWindowsSchedulerServiceStatus(io: {
474
+ probeTask?: () => WindowsSchedulerTaskProbe;
475
+ findProxy?: () => Promise<{ port: number } | null>;
476
+ } = {}): Promise<string> {
477
+ let task: WindowsSchedulerTaskProbe;
478
+ try {
479
+ task = (io.probeTask ?? probeWindowsSchedulerTask)();
480
+ } catch (error) {
481
+ task = { status: "unknown", detail: schtasksErrorDetail(error) };
482
+ }
483
+
484
+ let proxy: WindowsSchedulerProxyProbe;
485
+ try {
486
+ const live = await (io.findProxy ?? findLiveProxy)();
487
+ proxy = live ? { status: "running", port: live.port } : { status: "not-running" };
488
+ } catch {
489
+ proxy = { status: "unknown" };
490
+ }
491
+
492
+ return formatWindowsSchedulerServiceStatus(task, proxy);
493
+ }
494
+
393
495
  function schtasksErrorDetail(error: unknown): string {
394
496
  return error instanceof Error ? error.message : String(error);
395
497
  }
@@ -483,7 +585,9 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
483
585
  : !assetsHealthy
484
586
  ? "Required scheduler service assets are missing."
485
587
  : !registrationHealthy
486
- ? "Task Scheduler registration is present but unhealthy."
588
+ ? (inputs.xml.trim()
589
+ ? "Task Scheduler registration is present but unhealthy."
590
+ : "Task Scheduler task is present but its XML could not be read.")
487
591
  : nativeStatusUnknown
488
592
  ? "The Task Scheduler task was created, but OpenCodex could not verify that the native WinSW service is absent."
489
593
  : "ok";
@@ -502,9 +606,18 @@ export function evaluateWindowsSchedulerInstallVerification(inputs: {
502
606
  /** Conflict-free postcondition check for an elevated scheduler install. */
503
607
  export function verifyWindowsSchedulerInstall(taskName = TASK): WindowsSchedulerInstallVerification {
504
608
  const taskInstalled = windowsSchedulerTaskInstalled(taskName);
505
- const xml = taskInstalled ? (() => {
506
- try { return querySchtasks(["/query", "/tn", taskName, "/xml"]); } catch { return ""; }
507
- })() : "";
609
+ let xml = "";
610
+ if (taskInstalled) {
611
+ try { xml = querySchtasks(["/query", "/tn", taskName, "/xml"]); } catch { xml = ""; }
612
+ }
613
+ // After elevated create, non-elevated `/query /xml` can fail or return empty while the
614
+ // task is still listed. Fall back to the on-disk document we registered.
615
+ if (taskInstalled && !xml.trim()) {
616
+ const diskPath = windowsTaskXmlPath();
617
+ if (existsSync(diskPath)) {
618
+ try { xml = decodeSchtasksOutput(readFileSync(diskPath)); } catch { /* keep empty */ }
619
+ }
620
+ }
508
621
  return evaluateWindowsSchedulerInstallVerification({
509
622
  taskInstalled,
510
623
  xml,
@@ -892,6 +1005,22 @@ function taskXmlString(value: string): string {
892
1005
  .replace(/'/g, "&apos;");
893
1006
  }
894
1007
 
1008
+ /**
1009
+ * RunLevel check. Schema default is LeastPrivilege (omitted on export). Elevated
1010
+ * `schtasks /create` often rewrites the registered task to HighestAvailable even when
1011
+ * the source XML asked for LeastPrivilege — still InteractiveToken / same user.
1012
+ * Keep accepting HighestAvailable here: rejecting it would false-fail healthy elevated
1013
+ * installs, and windowsTaskRegistrationHealthy tests encode that contract.
1014
+ */
1015
+ function taskXmlRunLevelAcceptable(principal: string): boolean {
1016
+ if (taskXmlHasPrefixedTag(principal, "RunLevel")) return false;
1017
+ const count = taskXmlElementCount(principal, "RunLevel");
1018
+ if (count === 0) return true;
1019
+ if (count > 1) return false;
1020
+ const value = new RegExp(`<RunLevel(?:\\s[^>]*?)?>\\s*([^<]*?)\\s*<\\/RunLevel>`, "i").exec(principal)?.[1]?.trim().toLowerCase();
1021
+ return value === "leastprivilege" || value === "highestavailable";
1022
+ }
1023
+
895
1024
  export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServiceListenPort()): string {
896
1025
  const { bun, cli } = entry;
897
1026
  const bunRuntime = durableBunRuntime();
@@ -1039,6 +1168,43 @@ function taskXmlHasPrefixedTag(xml: string, tag: string): boolean {
1039
1168
  * Absence means the documented schema default (#432); a present element must still
1040
1169
  * match exactly, so a malformed or explicitly unsafe value never reads as healthy.
1041
1170
  */
1171
+ /**
1172
+ * Decode XML's five predefined entities, exactly once.
1173
+ *
1174
+ * Task Scheduler re-encodes element text when it exports a task, so a needle we
1175
+ * escaped ourselves can never match its output (#608). Compare decoded values
1176
+ * instead of encoded ones.
1177
+ *
1178
+ * The single pass is the point: decoding twice would turn `&amp;quot;` into `"`,
1179
+ * letting a doubly-encoded value impersonate the expected launcher path.
1180
+ */
1181
+ function taskXmlDecodeEntities(value: string): string {
1182
+ return value.replace(/&(amp|lt|gt|quot|apos);/g, (_, name: string) => (
1183
+ name === "amp" ? "&"
1184
+ : name === "lt" ? "<"
1185
+ : name === "gt" ? ">"
1186
+ : name === "quot" ? "\""
1187
+ : "'"
1188
+ ));
1189
+ }
1190
+
1191
+ /**
1192
+ * Exactly one unprefixed `<tag>` whose DECODED text equals `expected`.
1193
+ *
1194
+ * Unlike taskXmlOptionalValueEquals(), an absent element is NOT a pass: these
1195
+ * elements name what actually gets executed, so a missing <Command>/<Arguments>
1196
+ * must fail the health check rather than inherit a schema default.
1197
+ */
1198
+ function taskXmlDecodedValueEquals(xml: string, tag: string, expected: string): boolean {
1199
+ // Same reasoning as the optional helper: `<t:Arguments>` must not read as absent.
1200
+ if (taskXmlHasPrefixedTag(xml, tag)) return false;
1201
+ if (taskXmlElementCount(xml, tag) !== 1) return false;
1202
+ // `[^<]*` refuses nested markup, so a decoy inside a child element cannot match.
1203
+ const value = new RegExp(`<${tag}(?:\\s[^>]*?)?>([^<]*)<\\/${tag}>`, "i").exec(xml)?.[1];
1204
+ if (value === undefined) return false;
1205
+ return taskXmlDecodeEntities(value).trim().toLowerCase() === expected.trim().toLowerCase();
1206
+ }
1207
+
1042
1208
  function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string): boolean {
1043
1209
  // Check the prefixed form first: treating `<t:Enabled>false</t:Enabled>` as an
1044
1210
  // omission would turn an explicitly disabled task into a healthy one.
@@ -1072,12 +1238,16 @@ export function windowsTaskRegistrationHealthy(
1072
1238
  return taskXmlElementCount(triggers, "LogonTrigger") > 0
1073
1239
  && taskXmlOptionalValueEquals(trigger, "Enabled", "true")
1074
1240
  && /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(principal)
1075
- && taskXmlOptionalValueEquals(principal, "RunLevel", "LeastPrivilege")
1241
+ && taskXmlRunLevelAcceptable(principal)
1076
1242
  && taskXmlOptionalValueEquals(settings, "Enabled", "true")
1077
1243
  && /<MultipleInstancesPolicy>\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings)
1078
1244
  && /<ExecutionTimeLimit>\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings)
1079
- && action.includes(`<Command>${taskXmlString(wscript)}</Command>`)
1080
- && action.includes(`<Arguments>${taskXmlString(`/b /nologo "${launcher}"`)}</Arguments>`);
1245
+ // Compare decoded VALUES, not encodings: Task Scheduler canonicalizes the
1246
+ // quotes we wrote as `&quot;` back to literal `"` on export, so an escaped
1247
+ // needle never matched and a healthy task read as permanently stale (#608).
1248
+ // Case-insensitive: elevated `schtasks /create` may rewrite System32 casing.
1249
+ && taskXmlDecodedValueEquals(action, "Command", wscript)
1250
+ && taskXmlDecodedValueEquals(action, "Arguments", `/b /nologo "${launcher}"`);
1081
1251
  }
1082
1252
 
1083
1253
  export interface WindowsSchedulerXmlState {
@@ -1112,6 +1282,7 @@ export function readWindowsSchedulerXmlState(
1112
1282
  function installLaunchd(): void {
1113
1283
  const dir = join(homedir(), "Library", "LaunchAgents");
1114
1284
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1285
+ recordOwnedConfigPath(getConfigDir(), serviceStatePath());
1115
1286
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
1116
1287
  writeServiceApiTokenFile();
1117
1288
  const p = plistPath();
@@ -1147,9 +1318,23 @@ function writeServiceAssetWithRetry(path: string, content: string, encoding: "ut
1147
1318
  }
1148
1319
  }
1149
1320
 
1150
- function installWindows(): void {
1321
+ /**
1322
+ * Rewrite on-disk scheduler assets (script/VBS/XML) without re-registering the task.
1323
+ * Used by fresh install (before schtasks /create) and by repair (no elevation).
1324
+ */
1325
+ function writeWindowsSchedulerAssets(): void {
1151
1326
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
1152
1327
  writeServiceApiTokenFile();
1328
+ const script = windowsServiceScriptPath();
1329
+ writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
1330
+ // UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
1331
+ // paths on some WSH/codepage combinations — same contract as the task XML below.
1332
+ writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
1333
+ writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
1334
+ }
1335
+
1336
+ function installWindows(): void {
1337
+ recordOwnedConfigPath(getConfigDir(), serviceStatePath());
1153
1338
  // Transactional backend switch: installing the scheduler backend removes a native
1154
1339
  // service first — two live managers would both respawn the proxy (conflict).
1155
1340
  if (statusWinswRaw() !== "nonexistent") {
@@ -1166,23 +1351,85 @@ function installWindows(): void {
1166
1351
  // End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
1167
1352
  // script mid-rewrite runs a torn batch file, and its open handle can fail the write.
1168
1353
  try { stopWindows(); } catch { /* not running */ }
1169
- const script = windowsServiceScriptPath();
1170
- writeServiceAssetWithRetry(script, buildWindowsServiceScript(), "utf8");
1171
- // UTF-16LE + BOM: a BOM-less UTF-8 VBS mis-decodes non-ASCII (e.g. Korean) profile
1172
- // paths on some WSH/codepage combinations — same contract as the task XML below.
1173
- writeServiceAssetWithRetry(windowsLauncherVbsPath(), `\uFEFF${buildWindowsLauncherVbs(script)}`, "utf16le");
1174
- writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
1175
- schtasks(buildWindowsSchtasksCreateArgs(script));
1354
+ writeWindowsSchedulerAssets();
1355
+ schtasks(buildWindowsSchtasksCreateArgs(windowsServiceScriptPath()));
1176
1356
  schtasks(["/run", "/tn", TASK]);
1177
1357
  writeServiceInstallState("scheduler");
1178
1358
  }
1179
1359
 
1360
+ export interface RepairServiceDeps {
1361
+ diagnose?: () => ServiceDiagnostic;
1362
+ assertEnv?: () => void;
1363
+ assertAuth?: () => void;
1364
+ writeSchedulerAssets?: () => void;
1365
+ stopScheduler?: () => void;
1366
+ startScheduler?: () => void;
1367
+ writeSchedulerState?: () => void;
1368
+ writeNativeState?: () => void;
1369
+ repairNative?: () => void | Promise<void>;
1370
+ repairLaunchd?: () => void;
1371
+ repairSystemd?: () => void;
1372
+ /** Test seam — defaults to process.platform so Linux CI cannot hit real installSystemd. */
1373
+ platform?: NodeJS.Platform;
1374
+ }
1375
+
1376
+ /**
1377
+ * Repair an already-installed background service without Task Scheduler re-registration.
1378
+ *
1379
+ * Windows scheduler: rewrite assets + stop/start — no `schtasks /create`, no UAC.
1380
+ * Windows native: WinSW asset rewrite + restart (skips `install /p` when present).
1381
+ * macOS/Linux: re-run the user-level install/reload path.
1382
+ */
1383
+ export async function repairService(deps: RepairServiceDeps = {}): Promise<void> {
1384
+ const diagnose = deps.diagnose ?? diagnoseService;
1385
+ const platform = deps.platform ?? process.platform;
1386
+ const diag = diagnose();
1387
+ if (!diag.supported) {
1388
+ throw new Error(`Background service is unsupported (${diag.summary}).`);
1389
+ }
1390
+ if (diag.conflict) {
1391
+ throw new Error(
1392
+ "Cannot repair while Task Scheduler and native WinSW are both present. "
1393
+ + "Run 'ocx service uninstall' then reinstall one backend with 'ocx service install'.",
1394
+ );
1395
+ }
1396
+ if (!diag.installed) {
1397
+ throw new Error("Background service is not installed. Run 'ocx service install' first.");
1398
+ }
1399
+
1400
+ (deps.assertEnv ?? assertServiceEnvironmentMatchesInstall)();
1401
+ (deps.assertAuth ?? assertServiceAuthEnvironment)();
1402
+
1403
+ if (platform === "win32") {
1404
+ if (diag.backend === "native") {
1405
+ await (deps.repairNative ?? (() => installWinswService(defaultWinswEntry(import.meta.dir))))();
1406
+ (deps.writeNativeState ?? (() => writeServiceInstallState("native")))();
1407
+ return;
1408
+ }
1409
+ try { (deps.stopScheduler ?? stopWindows)(); } catch { /* not running */ }
1410
+ (deps.writeSchedulerAssets ?? writeWindowsSchedulerAssets)();
1411
+ (deps.startScheduler ?? startWindows)();
1412
+ (deps.writeSchedulerState ?? (() => writeServiceInstallState("scheduler")))();
1413
+ return;
1414
+ }
1415
+ if (platform === "darwin") {
1416
+ (deps.repairLaunchd ?? installLaunchd)();
1417
+ return;
1418
+ }
1419
+ if (platform === "linux") {
1420
+ (deps.repairSystemd ?? installSystemd)();
1421
+ return;
1422
+ }
1423
+ throw new Error(`Background service repair is unsupported on ${platform}.`);
1424
+ }
1425
+
1180
1426
  /**
1181
1427
  * Opt-in native backend (`ocx service install --native`). Transactional: removes the
1182
1428
  * scheduler backend first; on failure the machine is left with NO service (explicitly
1183
1429
  * reported) — never a silent fallback to the scheduler.
1184
1430
  */
1185
1431
  async function installWindowsNative(): Promise<void> {
1432
+ recordOwnedConfigPath(getConfigDir(), serviceStatePath());
1186
1433
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
1187
1434
  writeServiceApiTokenFile();
1188
1435
  let hadScheduler = false;
@@ -1321,6 +1568,7 @@ function installSystemd(): void {
1321
1568
  ensureUserBusEnv(); // reach the user bus over a bare SSH session (F9)
1322
1569
  const dir = unitDir();
1323
1570
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1571
+ recordOwnedConfigPath(getConfigDir(), serviceStatePath());
1324
1572
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
1325
1573
  writeServiceApiTokenFile();
1326
1574
  writeFileSync(unitPath(), buildUnit(), "utf8");
@@ -1664,6 +1912,13 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
1664
1912
  console.error("--native (WinSW) is Windows-only.");
1665
1913
  process.exit(1);
1666
1914
  }
1915
+ if (command === "repair") {
1916
+ assertServiceEnvironmentMatchesInstall();
1917
+ assertServiceAuthEnvironment();
1918
+ await repairService();
1919
+ console.log("✅ opencodex background service repaired (assets refreshed, no Task Scheduler re-registration).");
1920
+ return;
1921
+ }
1667
1922
  // Non-install subcommands follow the backend recorded at install time (state v2).
1668
1923
  const backend: ServiceBackend = parsed.backend ?? (process.platform === "win32" ? readServiceBackend() : "scheduler");
1669
1924
  const ops = platformOps(backend);
@@ -1680,6 +1935,11 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
1680
1935
  ? "✅ opencodex native service installed + started (windowless, starts at boot, auto-restarts on crash)."
1681
1936
  : "✅ opencodex service installed + started (auto-starts on login, auto-restarts on crash).");
1682
1937
  if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
1938
+ // Service users never reach the `ocx start` prompt: the proxy they run is the
1939
+ // supervised child, which always carries OCX_SERVICE=1. This command, though, is
1940
+ // hand-typed in a real terminal, so it is the one interactive moment they get.
1941
+ // Same one-time marker and same guards (TTY, gh auth, agent deferral) apply.
1942
+ await maybeShowStarPrompt();
1683
1943
  break;
1684
1944
  case "start":
1685
1945
  ops.start();
@@ -1703,8 +1963,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
1703
1963
  }
1704
1964
  break;
1705
1965
  case "status": {
1706
- const s = ops.status();
1707
- console.log(s ? `✅ running:\n${s}` : "❌ service not installed/running.");
1966
+ if (process.platform === "win32" && backend === "scheduler") {
1967
+ console.log(await inspectWindowsSchedulerServiceStatus());
1968
+ } else {
1969
+ const s = ops.status();
1970
+ console.log(s ? `✅ running:\n${s}` : "❌ service not installed/running.");
1971
+ }
1708
1972
  console.log(`Diagnostics: ${serviceDiagnosticsSummary()}`);
1709
1973
  break;
1710
1974
  }
@@ -1736,8 +2000,9 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
1736
2000
  console.log("✅ service uninstalled.");
1737
2001
  break;
1738
2002
  default:
1739
- console.error("Usage: ocx service [install|start|stop|status|uninstall|remove] [--native|--scheduler]");
2003
+ console.error("Usage: ocx service [install|repair|start|stop|status|uninstall|remove] [--native|--scheduler]");
1740
2004
  console.error(" With no subcommand, installs/updates and starts the background service.");
2005
+ console.error(" repair: refresh assets and restart an already-installed service (no admin re-prompt).");
1741
2006
  console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
1742
2007
  process.exit(1);
1743
2008
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Single-flight wrapper for manual archived cleanup (management API).
3
+ *
4
+ * Shares the CODEX_HOME storage-mutation coordinator with trash restore and
5
+ * (Phase 3) policy-driven cleanup.
6
+ */
7
+ import { resolveCodexHomeDir } from "../codex/home";
8
+ import {
9
+ executeArchivedCleanup,
10
+ type CleanupResult,
11
+ type ExecuteCleanupOptions,
12
+ } from "./cleanup";
13
+ import {
14
+ resetStorageMutationCoordinatorForTests,
15
+ setStorageMutationCoordinatorTestHooks,
16
+ withStorageMutationSlot,
17
+ type StorageMutationCoordinatorTestHooks,
18
+ } from "./storage-mutation-coordinator";
19
+
20
+ export type CleanupJobTestHooks = StorageMutationCoordinatorTestHooks;
21
+
22
+ export function setArchivedCleanupJobTestHooks(hooks: CleanupJobTestHooks | null): void {
23
+ setStorageMutationCoordinatorTestHooks(hooks);
24
+ }
25
+
26
+ export function resetArchivedCleanupJobForTests(): void {
27
+ resetStorageMutationCoordinatorForTests();
28
+ }
29
+
30
+ /**
31
+ * Run archived cleanup under the shared storage-mutation gate.
32
+ * Returns immediately with `storage_mutation_busy` when restore or another
33
+ * cleanup is in flight for the same CODEX_HOME.
34
+ */
35
+ export async function runArchivedCleanupJob(
36
+ options: ExecuteCleanupOptions,
37
+ ): Promise<CleanupResult> {
38
+ const codexHome = options.codexHome ?? resolveCodexHomeDir();
39
+ const mode = options.mode;
40
+ const percent = options.percent;
41
+ const result = await withStorageMutationSlot("cleanup", codexHome, () =>
42
+ executeArchivedCleanup(options),
43
+ );
44
+ if (result && typeof result === "object" && "ok" in result && result.ok === false
45
+ && "error" in result && result.error === "storage_mutation_busy") {
46
+ return {
47
+ ok: false,
48
+ mode,
49
+ percent,
50
+ count: 0,
51
+ bytes: 0,
52
+ removedPaths: [],
53
+ error: "storage_mutation_busy",
54
+ };
55
+ }
56
+ return result as CleanupResult;
57
+ }