@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { atomicWriteFile } from "../config/atomic-write";
1
+ import { closeSync, fsyncSync, openSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { atomicWriteFileStreamed } from "../config/atomic-write";
4
+ import { withConfigMutationLockSync } from "../config/mutation-lock";
4
5
  import { getConfigDir } from "../config/paths";
5
6
 
6
- export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "capacity";
7
+ export type GrokResetCouponOperationKind = "execute" | "replay" | "identity-mismatch" | "token-mismatch" | "capacity";
7
8
 
8
9
  export interface GrokResetCouponOperationIdentity {
9
10
  accountId: string;
@@ -16,6 +17,12 @@ export interface GrokResetCouponOperationRecord {
16
17
  operationId: string;
17
18
  accountId?: string;
18
19
  tokenId?: string;
20
+ /** Token expiry (ms) captured at claim time as journal metadata. Remaining
21
+ * availability or expiry cannot establish this operation's outcome. */
22
+ tokenValidityEnd?: number;
23
+ /** When the attempt was marked (ms) — set only on an "attempted" replay so
24
+ * the route can defer inspection while the attempt may still be in flight. */
25
+ attemptedAt?: number;
19
26
  code?: string;
20
27
  settledAt?: number;
21
28
  }
@@ -23,14 +30,18 @@ export interface GrokResetCouponOperationRecord {
23
30
  interface GrokResetCouponOperationState {
24
31
  accountId: string;
25
32
  tokenId?: string;
26
- status: "open" | "settled" | "failed";
33
+ // "attempted" sits between open and settled: the spend call may have fired,
34
+ // so the operation must never execute again. Its replay carries no code —
35
+ // the route reconciles it against upstream instead of trusting a status.
36
+ status: "open" | "attempted" | "settled" | "failed";
27
37
  code?: string;
38
+ tokenValidityEnd?: number;
28
39
  createdAt: number;
29
40
  updatedAt: number;
30
41
  }
31
42
 
32
43
  interface GrokResetCouponLedger {
33
- version: 1;
44
+ version: 2;
34
45
  operations: Record<string, GrokResetCouponOperationState>;
35
46
  }
36
47
 
@@ -40,95 +51,249 @@ export function grokCouponJournalPath(customDir?: string): string {
40
51
  }
41
52
 
42
53
  function readGrokCouponLedger(filePath: string): GrokResetCouponLedger {
43
- if (!existsSync(filePath)) {
44
- return { version: 1, operations: {} };
54
+ let raw: string;
55
+ try { raw = readFileSync(filePath, "utf-8"); }
56
+ catch (error) {
57
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 2, operations: {} };
58
+ throw new Error("Grok coupon ledger is unavailable; existing records were preserved.");
45
59
  }
46
60
  try {
47
- const raw = readFileSync(filePath, "utf-8");
48
- const parsed = JSON.parse(raw) as GrokResetCouponLedger;
49
- return parsed && parsed.version === 1 && parsed.operations && typeof parsed.operations === "object"
50
- ? parsed
51
- : { version: 1, operations: {} };
61
+ const parsed = JSON.parse(raw) as Omit<GrokResetCouponLedger, "version"> & { version: unknown };
62
+ if (!parsed || (parsed.version !== 1 && parsed.version !== 2) || !parsed.operations
63
+ || typeof parsed.operations !== "object" || Array.isArray(parsed.operations)) throw new Error();
64
+ for (const op of Object.values(parsed.operations)) {
65
+ if (!op || typeof op.accountId !== "string" || !op.accountId
66
+ || !["open", "attempted", "settled", "failed"].includes(op.status)
67
+ || !Number.isFinite(op.createdAt) || !Number.isFinite(op.updatedAt)
68
+ || (op.tokenId !== undefined && typeof op.tokenId !== "string")
69
+ || (op.code !== undefined && typeof op.code !== "string")
70
+ || (op.tokenValidityEnd !== undefined && !Number.isFinite(op.tokenValidityEnd))) throw new Error();
71
+ }
72
+ if (parsed.version === 1) {
73
+ for (const op of Object.values(parsed.operations)) {
74
+ // Old opens (and transport failures recorded as failed) may already
75
+ // have dispatched. Only version-two opens prove pre-dispatch state.
76
+ if (op.status === "open" || (op.status === "failed" && op.code === "redeem_failed")) {
77
+ op.status = "attempted";
78
+ delete op.code;
79
+ }
80
+ }
81
+ }
82
+ return { version: 2, operations: parsed.operations };
52
83
  } catch {
53
- return { version: 1, operations: {} };
84
+ throw new Error("Grok coupon ledger is unreadable; existing records were preserved.");
54
85
  }
55
86
  }
56
87
 
57
88
  function writeGrokCouponLedger(filePath: string, ledger: GrokResetCouponLedger, now = Date.now()): void {
58
- // Prune settled/failed operations older than 30 days to avoid unbounded growth
89
+ // Prune every record past the 30-day window — including "open" and
90
+ // "attempted". Idempotency retention is explicitly bounded to 30 days;
91
+ // operations are not retry authorities after that window.
59
92
  const retentionCutoff = now - 30 * 24 * 60 * 60_000;
60
93
  ledger.operations = Object.fromEntries(
61
- Object.entries(ledger.operations).filter(
62
- ([, op]) => op.status === "open" || op.updatedAt > retentionCutoff,
63
- ),
94
+ Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
64
95
  );
65
- atomicWriteFile(filePath, JSON.stringify(ledger, null, 2));
96
+ atomicWriteFileStreamed(filePath, descriptor => writeFileSync(descriptor, JSON.stringify(ledger, null, 2)), {
97
+ afterRename: target => {
98
+ // The shared streamed writer flushes the temp. A spend claim must also
99
+ // refuse on a POSIX directory-sync failure before dispatch, rather than
100
+ // relying on that writer's best-effort parent sync. Windows has no
101
+ // portable directory descriptor and retains the writer's platform contract.
102
+ if (process.platform === "win32") return;
103
+ const descriptor = openSync(dirname(target), "r");
104
+ try { fsyncSync(descriptor); }
105
+ finally { closeSync(descriptor); }
106
+ },
107
+ });
66
108
  }
67
109
 
68
110
  const MAX_GROK_RESET_COUPON_OPERATION_IDS = 256;
69
111
 
112
+ /** Resolve an omitted-account retry from its durable identity, within retention. */
113
+ export function getGrokResetCouponOperationAccountId(
114
+ operationId: string,
115
+ now = Date.now(),
116
+ journalPath?: string,
117
+ ): string | undefined {
118
+ return withConfigMutationLockSync(() => {
119
+ const record = readGrokCouponLedger(journalPath ?? grokCouponJournalPath()).operations[operationId];
120
+ if (!record || record.updatedAt <= now - 30 * 24 * 60 * 60_000) return undefined;
121
+ return record.accountId;
122
+ });
123
+ }
124
+
70
125
  export function openGrokResetCouponOperation(
71
126
  identity: GrokResetCouponOperationIdentity,
72
127
  now = Date.now(),
73
128
  journalPath?: string,
74
129
  ): GrokResetCouponOperationRecord {
75
- const filePath = journalPath ?? grokCouponJournalPath();
76
- const ledger = readGrokCouponLedger(filePath);
130
+ // Read→decide→write under the shared config mutation lock: two concurrent
131
+ // coupon requests on the same home must not lose an operation record — the
132
+ // ledger exists precisely to make an irreversible spend replay-safe.
133
+ return withConfigMutationLockSync(() => {
134
+ const filePath = journalPath ?? grokCouponJournalPath();
135
+ const ledger = readGrokCouponLedger(filePath);
77
136
 
78
- if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) {
79
- return { kind: "capacity", operationId: identity.operationId };
80
- }
137
+ // Prune BEFORE the capacity check: expiry is enforced on write, so a ledger
138
+ // full of stale records would otherwise reject every new operation forever
139
+ // (the prune path inside writeGrokCouponLedger is never reached when all
140
+ // openings are rejected).
141
+ const retentionCutoff = now - 30 * 24 * 60 * 60_000;
142
+ ledger.operations = Object.fromEntries(
143
+ Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
144
+ );
81
145
 
82
- const existing = ledger.operations[identity.operationId];
83
- if (existing) {
84
- if (existing.accountId !== identity.accountId) {
85
- return { kind: "identity-mismatch", operationId: identity.operationId };
86
- }
87
- if (existing.status !== "open") {
88
- // Durably settled already: replay the recorded outcome instead of
89
- // trusting upstream idempotency for an irreversible spend.
146
+ const existing = ledger.operations[identity.operationId];
147
+ if (existing) {
148
+ if (existing.accountId !== identity.accountId) {
149
+ return { kind: "identity-mismatch", operationId: identity.operationId };
150
+ }
151
+ if (identity.tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== identity.tokenId) {
152
+ return { kind: "token-mismatch", operationId: identity.operationId };
153
+ }
154
+ if (existing.status !== "open") {
155
+ // Non-open: replay the recorded outcome. "attempted" records carry no
156
+ // code — the caller must reconcile them against upstream, never re-run.
157
+ return {
158
+ kind: "replay",
159
+ operationId: identity.operationId,
160
+ accountId: existing.accountId,
161
+ tokenId: existing.tokenId,
162
+ tokenValidityEnd: existing.tokenValidityEnd,
163
+ attemptedAt: existing.status === "attempted" ? existing.updatedAt : undefined,
164
+ code: existing.code,
165
+ settledAt: existing.updatedAt,
166
+ };
167
+ }
90
168
  return {
91
- kind: "replay",
169
+ kind: "execute",
92
170
  operationId: identity.operationId,
93
171
  accountId: existing.accountId,
94
172
  tokenId: existing.tokenId,
95
- code: existing.code,
96
- settledAt: existing.updatedAt,
97
173
  };
98
174
  }
175
+
176
+ if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) {
177
+ return { kind: "capacity", operationId: identity.operationId };
178
+ }
179
+
180
+ ledger.operations[identity.operationId] = {
181
+ accountId: identity.accountId,
182
+ ...(identity.tokenId === undefined ? {} : { tokenId: identity.tokenId }),
183
+ status: "open",
184
+ createdAt: now,
185
+ updatedAt: now,
186
+ };
187
+ writeGrokCouponLedger(filePath, ledger, now);
99
188
  return {
100
189
  kind: "execute",
101
190
  operationId: identity.operationId,
102
- accountId: existing.accountId,
103
- tokenId: existing.tokenId,
191
+ accountId: identity.accountId,
192
+ tokenId: identity.tokenId,
104
193
  };
105
- }
194
+ });
195
+ }
106
196
 
107
- ledger.operations[identity.operationId] = {
108
- accountId: identity.accountId,
109
- ...(identity.tokenId === undefined ? {} : { tokenId: identity.tokenId }),
110
- status: "open",
111
- createdAt: now,
112
- updatedAt: now,
113
- };
114
- writeGrokCouponLedger(filePath, ledger, now);
115
- return {
116
- kind: "execute",
117
- operationId: identity.operationId,
118
- accountId: identity.accountId,
119
- tokenId: identity.tokenId,
120
- };
197
+ /**
198
+ * Flip an open operation to `attempted` BEFORE the upstream spend call. The
199
+ * write happens while nothing is irreversible yet: if it throws, the caller
200
+ * aborts without spending; if a later settle write dies instead, the
201
+ * operation still refuses blind re-execution — the next open returns a
202
+ * code-less replay the route resolves via the upstream remaining-resets list.
203
+ */
204
+ export function markGrokResetCouponAttempt(
205
+ operationId: string,
206
+ tokenId: string,
207
+ now = Date.now(),
208
+ journalPath?: string,
209
+ tokenValidityEnd?: number,
210
+ ): boolean {
211
+ return withConfigMutationLockSync(() => {
212
+ const filePath = journalPath ?? grokCouponJournalPath();
213
+ const ledger = readGrokCouponLedger(filePath);
214
+ const existing = ledger.operations[operationId];
215
+ if (!existing || existing.status !== "open"
216
+ || (existing.tokenId !== undefined && existing.tokenId !== tokenId)) return false;
217
+ existing.status = "attempted";
218
+ existing.tokenId = tokenId;
219
+ if (tokenValidityEnd !== undefined) existing.tokenValidityEnd = tokenValidityEnd;
220
+ existing.updatedAt = now;
221
+ writeGrokCouponLedger(filePath, ledger, now);
222
+ return true;
223
+ });
224
+ }
225
+
226
+ type GrokResetCouponSettlement = { operationId: string; accountId: string; code: string } & (
227
+ | { tokenId?: string; status: "failed"; expectedStatus: "open" }
228
+ | { tokenId: string; status: "success"; expectedStatus: "attempted" }
229
+ );
230
+
231
+ type GrokResetCouponPreflightResult =
232
+ | { kind: "recorded" | "changed" }
233
+ | { kind: "replay"; code: string; tokenId?: string; settledAt: number };
234
+
235
+ const UNRESOLVED_COUPON_CODES = new Set(["redeem_failed", "attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed"]);
236
+
237
+ /** The durable definitive outcome for this account and token, if the operation has one. */
238
+ function terminalReplay(
239
+ ledger: GrokResetCouponLedger, operationId: string, accountId: string, tokenId: string | undefined,
240
+ ): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
241
+ const existing = ledger.operations[operationId];
242
+ if (!existing || existing.accountId !== accountId) return null;
243
+ if (tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== tokenId) return null;
244
+ if ((existing.status !== "settled" && existing.status !== "failed") || !existing.code || UNRESOLVED_COUPON_CODES.has(existing.code)) return null;
245
+ return { kind: "replay", code: existing.code, tokenId: existing.tokenId, settledAt: existing.updatedAt };
121
246
  }
122
247
 
248
+ /** Settle a refusal or inspect its winner without releasing the mutation transaction. */
249
+ export function settleGrokResetCouponPreflightRefusal(
250
+ settlement: Extract<GrokResetCouponSettlement, { status: "failed" }>,
251
+ now = Date.now(),
252
+ journalPath?: string,
253
+ ): GrokResetCouponPreflightResult {
254
+ return withConfigMutationLockSync(() => {
255
+ const filePath = journalPath ?? grokCouponJournalPath();
256
+ const ledger = readGrokCouponLedger(filePath);
257
+ if (recordSettlement(ledger, filePath, settlement, now)) return { kind: "recorded" };
258
+ return terminalReplay(ledger, settlement.operationId, settlement.accountId, settlement.tokenId) ?? { kind: "changed" };
259
+ });
260
+ }
261
+
262
+ /**
263
+ * After a lost claim: the definitive winner to replay, or null while the
264
+ * operation is still unresolved. Terminal states never revert, so reading after
265
+ * the failed claim cannot replay an outcome that is later replaced.
266
+ */
267
+ export function readGrokResetCouponTerminalReplay(
268
+ operationId: string, accountId: string, tokenId?: string, journalPath?: string,
269
+ ): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
270
+ return withConfigMutationLockSync(() => {
271
+ const filePath = journalPath ?? grokCouponJournalPath();
272
+ return terminalReplay(readGrokCouponLedger(filePath), operationId, accountId, tokenId);
273
+ });
274
+ }
275
+
276
+ /** A preflight refusal cannot replace a claim or a terminal result. */
123
277
  export function recordGrokResetCouponSettlement(
124
- settlement: { operationId: string; tokenId?: string; code: string; status: "success" | "failed" },
278
+ settlement: GrokResetCouponSettlement,
125
279
  now = Date.now(),
126
280
  journalPath?: string,
127
- ): void {
128
- const filePath = journalPath ?? grokCouponJournalPath();
129
- const ledger = readGrokCouponLedger(filePath);
281
+ ): boolean {
282
+ return withConfigMutationLockSync(() => {
283
+ const filePath = journalPath ?? grokCouponJournalPath();
284
+ const ledger = readGrokCouponLedger(filePath);
285
+ return recordSettlement(ledger, filePath, settlement, now);
286
+ });
287
+ }
288
+
289
+ function recordSettlement(
290
+ ledger: GrokResetCouponLedger, filePath: string, settlement: GrokResetCouponSettlement, now: number,
291
+ ): boolean {
130
292
  const existing = ledger.operations[settlement.operationId];
131
- if (!existing) return;
293
+ if (!existing || existing.status !== settlement.expectedStatus
294
+ || existing.accountId !== settlement.accountId
295
+ || (settlement.tokenId !== undefined && existing.tokenId !== undefined && existing.tokenId !== settlement.tokenId)
296
+ || (settlement.status === "success" && existing.tokenId !== settlement.tokenId)) return false;
132
297
 
133
298
  existing.status = settlement.status === "success" ? "settled" : "failed";
134
299
  existing.code = settlement.code;
@@ -136,4 +301,5 @@ export function recordGrokResetCouponSettlement(
136
301
  existing.updatedAt = now;
137
302
 
138
303
  writeGrokCouponLedger(filePath, ledger, now);
304
+ return true;
139
305
  }
@@ -230,6 +230,17 @@ function buildGrokHeaders(accessToken: string): Record<string, string> {
230
230
  /**
231
231
  * Reads available Grok reset tokens for the authenticated xAI account.
232
232
  */
233
+ function assertConfirmedCouponStatus(decoded: ReturnType<typeof decodeGrpcWebResponse>): void {
234
+ const rawStatus = decoded.trailers?.metadata["grpc-status"];
235
+ // The shared decoder defaults a missing/malformed status to zero. Coupon
236
+ // availability and an irreversible redemption need affirmative confirmation.
237
+ if (rawStatus === undefined || !/^\d+$/.test(rawStatus) || !Number.isSafeInteger(Number(rawStatus))) {
238
+ throw new Error("Grok coupon response has no confirmed gRPC status");
239
+ }
240
+ const status = Number(rawStatus);
241
+ if (status !== 0) throw new GrpcWebError(status, decoded.statusMessage ?? "Unknown gRPC error");
242
+ }
243
+
233
244
  export async function getGrokRemainingResets(options: GetRemainingResetsOptions): Promise<{ tokens: GrokResetCoupon[] }> {
234
245
  const fetchImpl = options.fetchFn ?? globalThis.fetch;
235
246
  const endpoint = options.endpoint ?? GROK_GET_REMAINING_RESETS_ENDPOINT;
@@ -251,9 +262,7 @@ export async function getGrokRemainingResets(options: GetRemainingResetsOptions)
251
262
  const rawBytes = new Uint8Array(await res.arrayBuffer());
252
263
  const decoded = decodeGrpcWebResponse(rawBytes);
253
264
 
254
- if (decoded.status !== 0) {
255
- throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
256
- }
265
+ assertConfirmedCouponStatus(decoded);
257
266
 
258
267
  if (decoded.messages.length === 0) {
259
268
  return { tokens: [] };
@@ -285,9 +294,7 @@ export async function redeemGrokResetCoupon(options: RedeemResetOptions): Promis
285
294
  const rawBytes = new Uint8Array(await res.arrayBuffer());
286
295
  const decoded = decodeGrpcWebResponse(rawBytes);
287
296
 
288
- if (decoded.status !== 0) {
289
- throw new GrpcWebError(decoded.status, decoded.statusMessage ?? "Unknown gRPC error");
290
- }
297
+ assertConfirmedCouponStatus(decoded);
291
298
 
292
299
  return {
293
300
  success: true,
package/src/grok/sync.ts CHANGED
@@ -31,7 +31,7 @@ async function defaultFetchAllModels(config: OcxConfig): Promise<CatalogModel[]>
31
31
  export async function syncGrokConfig(
32
32
  port: number,
33
33
  config: OcxConfig,
34
- opts: { hostname?: string; grokHome?: string } = {},
34
+ opts: { hostname?: string; grokHome?: string; refreshOnly?: { admit: () => boolean } } = {},
35
35
  deps: GrokSyncDeps = { fetchAllModels: defaultFetchAllModels, injectGrokConfig },
36
36
  ): Promise<GrokInjectResult> {
37
37
  let projection: ReturnType<typeof projectGrokCatalog>;
@@ -58,6 +58,7 @@ export async function syncGrokConfig(
58
58
  ? (opts.hostname ?? config.hostname)
59
59
  : targetUrl.hostname,
60
60
  ...(opts.grokHome !== undefined ? { grokHome: opts.grokHome } : {}),
61
+ ...opts.refreshOnly ? { refreshOnly: opts.refreshOnly } : {},
61
62
  excluded: new Set(config.grokExcludedModels ?? []),
62
63
  catalogModelIds: projection.catalogModelIds,
63
64
  disabledProviderNamespaces: projection.disabledProviderNamespaces,
@@ -300,6 +300,11 @@ export interface ImageBridgeDeps {
300
300
  onRequestBuilt?: (request: AdapterRequest) => void;
301
301
  abortSignal?: AbortSignal;
302
302
  onFirstOutput?: () => void;
303
+ /** Transfer prepaid cleanup before asynchronous inference can outlive Response construction. */
304
+ onProducerStart?: () => void;
305
+ onProducerEnd?: () => void;
306
+ /** Release a prepaid recovery that did not dispatch during this fetch iteration. */
307
+ onIterationEnd?: () => void;
303
308
  /** Max image-generation rounds before forcing a final answer. Defaults to 3; clamped to [0, 10]. */
304
309
  maxRounds?: number;
305
310
  /** Connect / response-header budget for non-runTurn iterations. */
@@ -309,7 +314,7 @@ export interface ImageBridgeDeps {
309
314
  /** Provider-specific fetch (e.g. xAI transport wrapper). Falls back to global fetch. */
310
315
  fetchImpl?: typeof globalThis.fetch;
311
316
  /** Bind physical dispatch to this iteration's built request; pacing remains owned by the loop. */
312
- fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest) => typeof globalThis.fetch;
317
+ fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter) => typeof globalThis.fetch;
313
318
  /** Reserve the routed provider's next request-start slot before each adapter dispatch. */
314
319
  waitForRequestSlot?: (signal?: AbortSignal) => Promise<ProviderRequestSlot | void>;
315
320
  /** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
@@ -372,6 +377,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
372
377
  let paidImageCalls = 0;
373
378
  let paidVideoCalls = 0;
374
379
  let hiddenUsage: OcxUsage | undefined;
380
+ let activeProducers = 0;
375
381
 
376
382
  const addUsage = (a: OcxUsage | undefined, b: OcxUsage | undefined): OcxUsage | undefined => {
377
383
  if (!a) return b;
@@ -514,19 +520,25 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
514
520
  try {
515
521
  // Attempt telemetry must fire at dispatch time, not after collection.
516
522
  deps.onAttemptSend?.();
517
- void adapter.runTurn(iterParsed, {
518
- headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
519
- ...(deps.incomingMeta.providerName ? { providerName: deps.incomingMeta.providerName } : {}),
520
- abortSignal: signal,
521
- translatorBudget,
522
- pacingSlot,
523
- }, emit).then(closeOnAbort).catch(err => {
523
+ activeProducers++;
524
+ let producer: Promise<void>;
525
+ try {
526
+ producer = adapter.runTurn(iterParsed, {
527
+ ...deps.incomingMeta,
528
+ headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
529
+ ...(deps.incomingMeta.providerName ? { providerName: deps.incomingMeta.providerName } : {}),
530
+ abortSignal: signal,
531
+ translatorBudget,
532
+ pacingSlot,
533
+ }, emit);
534
+ } catch (err) { producer = Promise.reject(err); }
535
+ void producer.then(closeOnAbort).catch(err => {
524
536
  if (accepting) {
525
537
  collectionError = err;
526
538
  if (isTranslatorBudgetExceededError(err)) internalAbort.abort(err);
527
539
  }
528
540
  closeOnAbort();
529
- });
541
+ }).finally(() => { activeProducers--; deps.onProducerEnd?.(); });
530
542
  idle.reset();
531
543
  for await (const event of queue.stream()) {
532
544
  if (timedOut) break;
@@ -602,7 +614,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
602
614
  cachedRequest = request;
603
615
  cachedAdapter = requestAdapter;
604
616
  }
605
- const requestFetch = deps.fetchForRequest?.(request, iterParsed) ?? fetchImpl;
617
+ const requestFetch = deps.fetchForRequest?.(request, iterParsed, requestAdapter) ?? fetchImpl;
606
618
  let response: Response;
607
619
  try {
608
620
  if (requestAdapter.fetchResponse) {
@@ -615,6 +627,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
615
627
  returnRawErrors: true,
616
628
  stream: true,
617
629
  executor: requestFetch,
630
+ sendBudget: deps.incomingMeta.sendBudget,
631
+ onPhysicalSend: deps.incomingMeta.onPhysicalSend,
632
+ onRecoveryWithheld: deps.incomingMeta.onRecoveryWithheld,
618
633
  }));
619
634
  } finally {
620
635
  releaseProviderRequestSlot(slot);
@@ -689,7 +704,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
689
704
  }
690
705
  // 429 key-failover parity with web-search / normal routed path.
691
706
  while ((prepared.response.status === 429
692
- || (prepared.response.status === 403 && isAnthropicInstanceId(deps.incomingMeta?.providerName))
707
+ || ((prepared.response.status === 403 || prepared.response.status === 401) && isAnthropicInstanceId(deps.incomingMeta?.providerName))
693
708
  || (iterParsed._kiroAuthContext && (prepared.response.status === 400 || prepared.response.status === 403))) && deps.on429) {
694
709
  const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
695
710
  iterParsed, prepared.response);
@@ -734,6 +749,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
734
749
  throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
735
750
  } finally {
736
751
  headerDeadline.clear();
752
+ deps.onIterationEnd?.();
737
753
  }
738
754
  };
739
755
 
@@ -796,6 +812,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
796
812
  // non-2xx JSON. Skip for runTurn adapters: their "headers" are synthetic, and awaiting
797
813
  // queue.collect() before returning SSE starves clients of headers/heartbeats on slow first turns.
798
814
  const skipEagerDrain = !!adapter.runTurn;
815
+ if (skipEagerDrain) deps.onProducerStart?.();
799
816
  let firstPrepared: IterationResponse | undefined;
800
817
  if (!skipEagerDrain) {
801
818
  try {
@@ -1056,6 +1073,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
1056
1073
  }
1057
1074
  }
1058
1075
  } finally {
1076
+ if (skipEagerDrain && activeProducers === 0) deps.onProducerEnd?.();
1059
1077
  if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
1060
1078
  }
1061
1079
  }
@@ -8,7 +8,7 @@ import { readIntegrationState, type IntegrationState, type IntegrationStatus } f
8
8
  import {
9
9
  applyIntegrationCoordinated, disableIntegrationCoordinated,
10
10
  overwriteIntegrationCoordinated, refreshIntegrationCoordinated,
11
- type IntegrationWriteInput,
11
+ type CoordinatedIntegrationOptions, type IntegrationWriteInput,
12
12
  } from "./writer";
13
13
  import {
14
14
  asideProfileEnabled, asideProfileFailure, asideProfileScope, asideWriteInput,
@@ -260,7 +260,10 @@ export function mutateAsideProfiles(
260
260
  });
261
261
  }
262
262
 
263
- export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
263
+ export function refreshAsideProfiles(
264
+ input: AsideProfilesInput,
265
+ options?: { refreshOnly?: boolean; guard?: CoordinatedIntegrationOptions["guard"] },
266
+ ): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
264
267
  const policy = input.config.asideProfileSync;
265
268
  const selected = Object.values(policy?.profiles ?? {}).some(enabled => enabled === true);
266
269
  if (!selected && (policy?.allProfiles === false
@@ -272,11 +275,15 @@ export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<O
272
275
  try {
273
276
  const scope = asideProfileScope(ctx, profile);
274
277
  const owned = scope.store.readRecords().aside !== undefined;
278
+ if (options?.refreshOnly && !owned) continue;
275
279
  const bound = await asideWriteInput(ctx, scope);
276
280
  // A surviving ownership record means a removed block stays removed.
277
281
  // A newly discovered, enabled profile may receive its first safe apply.
278
282
  const operation = owned ? refreshIntegrationCoordinated : applyIntegrationCoordinated;
279
- const result = await operation(bound, { lockSeams: input.lockSeams });
283
+ const result = await operation(bound, {
284
+ lockSeams: input.lockSeams,
285
+ ...(options?.guard ? { guard: options.guard } : {}),
286
+ });
280
287
  outcomes.push({
281
288
  client: "aside", profileId: profile.id, ok: result.ok,
282
289
  ...(result.ok ? { changed: result.changed } : {}),
@@ -2,6 +2,7 @@ import { siblingOfLivePort } from "../codex/sibling-start";
2
2
  import { redactSecretString } from "../lib/redact";
3
3
  import type { ExportModel } from "../clients/config-export";
4
4
  import type { IntegrationClientId } from "./registry";
5
+ import type { CoordinatedIntegrationOptions, WriteOutcome } from "./writer";
5
6
  import {
6
7
  refreshOwnedIntegration,
7
8
  type OwnedIntegrationRefreshInput,
@@ -12,6 +13,7 @@ import {
12
13
  export async function refreshOwnedCatalogIntegrations(
13
14
  input: Omit<OwnedIntegrationRefreshInput, "clientId">,
14
15
  clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"],
16
+ options: { refreshOnly?: boolean; admit?: () => boolean } = {},
15
17
  ): Promise<OwnedIntegrationRefreshOutcome[]> {
16
18
  // Client files are shared with the live proxy a sibling instance runs beside; their entries
17
19
  // point at the owner's port, and refreshing them here would re-point them at this one.
@@ -20,14 +22,28 @@ export async function refreshOwnedCatalogIntegrations(
20
22
  const loadModels = () => models ??= Promise.resolve().then(() =>
21
23
  typeof input.models === "function" ? input.models() : input.models);
22
24
  const outcomes: OwnedIntegrationRefreshOutcome[] = [];
25
+ const admit = options.admit;
26
+ const writeOptions: CoordinatedIntegrationOptions | undefined = admit ? {
27
+ guard: (frozen): WriteOutcome | null => admit() ? null : {
28
+ ok: false, reason: "superseded_store", state: "current", clientId: frozen.clientId,
29
+ message: "Background refresh superseded",
30
+ },
31
+ } : undefined;
23
32
  for (const clientId of clientIds) {
33
+ if (admit?.() === false) break;
24
34
  try {
25
35
  if (clientId === "aside") {
26
36
  const { refreshAsideProfiles } = await import("./aside-profiles");
27
- outcomes.push(...await refreshAsideProfiles({ ...input, models: loadModels }));
37
+ const asideOptions = options.refreshOnly === undefined ? writeOptions
38
+ : { ...writeOptions, refreshOnly: options.refreshOnly };
39
+ const bound = { ...input, models: loadModels };
40
+ outcomes.push(...await (asideOptions === undefined
41
+ ? refreshAsideProfiles(bound) : refreshAsideProfiles(bound, asideOptions)));
28
42
  continue;
29
43
  }
30
- const result = await refreshOwnedIntegration({ ...input, clientId, models: loadModels });
44
+ const bound = { ...input, clientId, models: loadModels };
45
+ const result = await (writeOptions === undefined
46
+ ? refreshOwnedIntegration(bound) : refreshOwnedIntegration(bound, writeOptions));
31
47
  if (result) outcomes.push(result);
32
48
  } catch (error) {
33
49
  const busy = error !== null && typeof error === "object"
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Detection is read-only and injectable: the proxy never writes anything into a Cursor
11
11
  * install, its state database, or its keychain entries (the T20 exclusion in
12
- * devlog/_plan/260822_senpi_cursor_transfer/090), and the tests run against a temp tree.
12
+ * devlog/_fin/260822_senpi_cursor_transfer/090), and the tests run against a temp tree.
13
13
  */
14
14
  import { existsSync, readdirSync, readFileSync } from "node:fs";
15
15
  import { homedir } from "node:os";