@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
@@ -5,6 +5,7 @@
5
5
 
6
6
  export interface GrpcWebTrailer {
7
7
  status: number;
8
+ statusValues: string[];
8
9
  statusMessage?: string;
9
10
  metadata: Record<string, string>;
10
11
  }
@@ -12,6 +13,8 @@ export interface GrpcWebTrailer {
12
13
  export interface DecodedGrpcWebResponse {
13
14
  messages: Uint8Array[];
14
15
  status: number;
16
+ statusValues: string[];
17
+ framingComplete: boolean;
15
18
  statusMessage?: string;
16
19
  trailers?: GrpcWebTrailer;
17
20
  }
@@ -51,6 +54,7 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
51
54
  const text = new TextDecoder("utf-8").decode(bytes);
52
55
  const lines = text.split(/\r?\n/);
53
56
  const metadata: Record<string, string> = {};
57
+ const statusValues: string[] = [];
54
58
  let status = 0;
55
59
  let statusMessage: string | undefined;
56
60
 
@@ -62,6 +66,7 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
62
66
  if (!key) continue;
63
67
  metadata[key] = value;
64
68
  if (key === "grpc-status") {
69
+ statusValues.push(value);
65
70
  const parsed = parseInt(value, 10);
66
71
  if (!Number.isNaN(parsed)) {
67
72
  status = parsed;
@@ -75,7 +80,7 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
75
80
  }
76
81
  }
77
82
 
78
- return { status, statusMessage, metadata };
83
+ return { status, statusValues, statusMessage, metadata };
79
84
  }
80
85
 
81
86
  /**
@@ -83,8 +88,11 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
83
88
  */
84
89
  export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse {
85
90
  const messages: Uint8Array[] = [];
91
+ const statusValues: string[] = [];
86
92
  let offset = 0;
87
93
  let trailer: GrpcWebTrailer | undefined;
94
+ let firstTrailerPayload: Uint8Array | undefined;
95
+ let framingComplete = true;
88
96
 
89
97
  while (offset + HEADER_SIZE <= bytes.length) {
90
98
  const flag = bytes[offset];
@@ -99,10 +107,22 @@ export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse
99
107
 
100
108
  const payload = bytes.subarray(frameStart, frameEnd);
101
109
 
110
+ // Preserve legacy decoding; callers needing confirmation inspect framingComplete.
102
111
  if (flag === FRAME_DATA) {
112
+ if (firstTrailerPayload !== undefined) framingComplete = false;
103
113
  messages.push(payload);
104
114
  } else if (flag === FRAME_TRAILER) {
115
+ const firstTrailer = firstTrailerPayload;
116
+ if (firstTrailer === undefined) {
117
+ firstTrailerPayload = payload;
118
+ } else if (payload.length !== firstTrailer.length
119
+ || payload.some((byte, index) => byte !== firstTrailer[index])) {
120
+ framingComplete = false;
121
+ }
105
122
  trailer = parseGrpcWebTrailers(payload);
123
+ for (const value of trailer.statusValues) statusValues.push(value);
124
+ } else {
125
+ framingComplete = false;
106
126
  }
107
127
 
108
128
  offset = frameEnd;
@@ -114,6 +134,8 @@ export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse
114
134
  return {
115
135
  messages,
116
136
  status: finalStatus,
137
+ statusValues,
138
+ framingComplete: framingComplete && offset === bytes.length,
117
139
  statusMessage: finalMessage,
118
140
  trailers: trailer,
119
141
  };
@@ -25,6 +25,8 @@ export interface GrokResetCouponOperationRecord {
25
25
  attemptedAt?: number;
26
26
  code?: string;
27
27
  settledAt?: number;
28
+ /** Durable status of a replayed record, so callers apply the terminal-replay predicate. */
29
+ status?: "attempted" | "settled" | "failed";
28
30
  }
29
31
 
30
32
  interface GrokResetCouponOperationState {
@@ -77,8 +79,15 @@ function readGrokCouponLedger(filePath: string): GrokResetCouponLedger {
77
79
  op.status = "attempted";
78
80
  delete op.code;
79
81
  }
82
+ // A v1 attempted record never carried a confirmed outcome.
83
+ if (op.status === "attempted") delete op.code;
80
84
  }
81
85
  }
86
+ // Only a settled or failed record carries an outcome. An open or attempted
87
+ // record with a code would replay an unconfirmed result as definitive.
88
+ for (const op of Object.values(parsed.operations)) {
89
+ if ((op.status === "open" || op.status === "attempted") && op.code !== undefined) throw new Error();
90
+ }
82
91
  return { version: 2, operations: parsed.operations };
83
92
  } catch {
84
93
  throw new Error("Grok coupon ledger is unreadable; existing records were preserved.");
@@ -163,6 +172,7 @@ export function openGrokResetCouponOperation(
163
172
  attemptedAt: existing.status === "attempted" ? existing.updatedAt : undefined,
164
173
  code: existing.code,
165
174
  settledAt: existing.updatedAt,
175
+ status: existing.status,
166
176
  };
167
177
  }
168
178
  return {
@@ -232,17 +242,37 @@ type GrokResetCouponPreflightResult =
232
242
  | { kind: "recorded" | "changed" }
233
243
  | { kind: "replay"; code: string; tokenId?: string; settledAt: number };
234
244
 
235
- const UNRESOLVED_COUPON_CODES = new Set(["redeem_failed", "attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed"]);
245
+ /**
246
+ * Codes that never prove an outcome. A record carrying one is not replayed as
247
+ * definitive, even if a restored or hand-edited ledger stores it as settled/failed.
248
+ */
249
+ const UNRESOLVED_COUPON_CODES = new Set([
250
+ "redeem_failed", "attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed",
251
+ "attempt_mark_failed", "operation_token_mismatch", "ledger_unavailable", "capacity",
252
+ ]);
253
+
254
+ /**
255
+ * Whether a durable record is a definitive outcome this account and token may replay:
256
+ * settled or failed, same account, same token when both are known, and a code that
257
+ * does not itself mean "unconfirmed".
258
+ */
259
+ export function isTerminalGrokCouponReplay(
260
+ record: { status?: string; accountId?: string; tokenId?: string; code?: string } | undefined,
261
+ accountId: string, tokenId: string | undefined,
262
+ ): boolean {
263
+ if (!record || record.accountId !== accountId) return false;
264
+ if (tokenId !== undefined && record.tokenId !== undefined && record.tokenId !== tokenId) return false;
265
+ return (record.status === "settled" || record.status === "failed")
266
+ && !!record.code && !UNRESOLVED_COUPON_CODES.has(record.code);
267
+ }
236
268
 
237
269
  /** The durable definitive outcome for this account and token, if the operation has one. */
238
270
  function terminalReplay(
239
271
  ledger: GrokResetCouponLedger, operationId: string, accountId: string, tokenId: string | undefined,
240
272
  ): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
241
273
  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 };
274
+ if (!existing || !isTerminalGrokCouponReplay(existing, accountId, tokenId)) return null;
275
+ return { kind: "replay", code: existing.code!, tokenId: existing.tokenId, settledAt: existing.updatedAt };
246
276
  }
247
277
 
248
278
  /** Settle a refusal or inspect its winner without releasing the mutation transaction. */
@@ -259,6 +289,46 @@ export function settleGrokResetCouponPreflightRefusal(
259
289
  });
260
290
  }
261
291
 
292
+ export type GrokResetCouponPreOpenRefusalResult =
293
+ | GrokResetCouponPreflightResult
294
+ | { kind: "identity-mismatch" | "capacity" };
295
+
296
+ /**
297
+ * Refuse a request that never opened its supplied operationId. Under the mutation lock
298
+ * an absent id is reserved as a failed record, so a later open of the same id replays
299
+ * the refusal and can never be claimed (within the 30-day retention window). An id
300
+ * that already exists is never overwritten: a definitive outcome replays, anything
301
+ * else reports "changed" so the caller keeps its hold.
302
+ */
303
+ export function refuseGrokResetCouponBeforeOpen(
304
+ refusal: { operationId: string; accountId: string; tokenId?: string; code: string },
305
+ now = Date.now(),
306
+ journalPath?: string,
307
+ ): GrokResetCouponPreOpenRefusalResult {
308
+ return withConfigMutationLockSync(() => {
309
+ const filePath = journalPath ?? grokCouponJournalPath();
310
+ const ledger = readGrokCouponLedger(filePath);
311
+ const retentionCutoff = now - 30 * 24 * 60 * 60_000;
312
+ ledger.operations = Object.fromEntries(
313
+ Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
314
+ );
315
+ const existing = ledger.operations[refusal.operationId];
316
+ if (existing) {
317
+ if (existing.accountId !== refusal.accountId) return { kind: "identity-mismatch" };
318
+ // Same token binding as a normal replay: a different requested coupon never
319
+ // replays another coupon's outcome.
320
+ return terminalReplay(ledger, refusal.operationId, refusal.accountId, refusal.tokenId) ?? { kind: "changed" };
321
+ }
322
+ if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) return { kind: "capacity" };
323
+ ledger.operations[refusal.operationId] = {
324
+ accountId: refusal.accountId, ...(refusal.tokenId !== undefined ? { tokenId: refusal.tokenId } : {}),
325
+ status: "failed", code: refusal.code, createdAt: now, updatedAt: now,
326
+ };
327
+ writeGrokCouponLedger(filePath, ledger, now);
328
+ return { kind: "recorded" };
329
+ });
330
+ }
331
+
262
332
  /**
263
333
  * After a lost claim: the definitive winner to replay, or null while the
264
334
  * operation is still unresolved. Terminal states never revert, so reading after
@@ -231,10 +231,12 @@ function buildGrokHeaders(accessToken: string): Record<string, string> {
231
231
  * Reads available Grok reset tokens for the authenticated xAI account.
232
232
  */
233
233
  function assertConfirmedCouponStatus(decoded: ReturnType<typeof decodeGrpcWebResponse>): void {
234
- const rawStatus = decoded.trailers?.metadata["grpc-status"];
234
+ const rawStatus = decoded.statusValues[0];
235
235
  // The shared decoder defaults a missing/malformed status to zero. Coupon
236
236
  // availability and an irreversible redemption need affirmative confirmation.
237
- if (rawStatus === undefined || !/^\d+$/.test(rawStatus) || !Number.isSafeInteger(Number(rawStatus))) {
237
+ if (decoded.framingComplete !== true || rawStatus === undefined
238
+ || decoded.statusValues.some((value) => !/^\d+$/.test(value)
239
+ || !Number.isSafeInteger(Number(value)) || value !== rawStatus)) {
238
240
  throw new Error("Grok coupon response has no confirmed gRPC status");
239
241
  }
240
242
  const status = Number(rawStatus);
@@ -32,6 +32,7 @@ import {
32
32
  assertDroidRecordedSettingsUnambiguous,
33
33
  isLoopbackOnly,
34
34
  resolveIntegrationPaths,
35
+ type ResolvedIntegrationPaths,
35
36
  restoreOwnershipCollision,
36
37
  type IntegrationClientId,
37
38
  } from "./registry";
@@ -567,7 +568,7 @@ export function restoreStoreDirectoryRefusal(
567
568
  if (!declared?.lockFile) return null;
568
569
  let storePath: string;
569
570
  try {
570
- storePath = declared.path(input.env, input.home);
571
+ storePath = declared.path(input.env, input.home, io);
571
572
  } catch (error) {
572
573
  if (error instanceof ClientPathError) return null;
573
574
  throw error;
@@ -602,10 +603,11 @@ export function observeRestore(
602
603
  const store = selectedOperation?.store ?? input.store ?? createIntegrationStateStore();
603
604
  let io = input.io ?? defaultIntegrationIO(store);
604
605
  const clientId = input.clientId;
605
- let resolved: { configPath: string; detectDir: string };
606
+ let resolved: ResolvedIntegrationPaths;
606
607
  try {
607
608
  const context = exportContextOf(input);
608
- resolved = input.resolvedPaths ?? resolveIntegrationPaths(clientId, input.env, input.home, context);
609
+ resolved = clientId === "zcode" || !input.resolvedPaths
610
+ ? resolveIntegrationPaths(clientId, input.env, input.home, context, io) : input.resolvedPaths;
609
611
  if (clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(resolved.detectDir, context);
610
612
  } catch (error) {
611
613
  if (!(error instanceof ClientPathError)) throw error;
@@ -630,7 +632,7 @@ export function observeRestore(
630
632
  * config file. The answer also carries the document shape those bytes are in.
631
633
  */
632
634
  const rowTarget = declaredIntegrationTarget({
633
- clientId, configPath, resolvedConfigPath: resolved.configPath, env: input.env, home: input.home,
635
+ clientId, configPath, resolvedConfigPath: resolved.configPath, env: input.env, home: input.home, io,
634
636
  });
635
637
  if (rowTarget === null) {
636
638
  return {
@@ -848,7 +850,7 @@ export interface IntegrationWriteInput {
848
850
  store?: IntegrationStateStore;
849
851
  io?: IntegrationIO;
850
852
  /** Frozen once by the async coordinator; synchronous callers may omit it. */
851
- resolvedPaths?: { configPath: string; detectDir: string };
853
+ resolvedPaths?: ResolvedIntegrationPaths;
852
854
  }
853
855
 
854
856
  /** A refusal in the planner's own vocabulary, so observation does not depend on the writer's result type. */
@@ -922,7 +924,8 @@ export function observeIntegration(
922
924
  * verify account 1 was installed and then write account 0's catalog.
923
925
  */
924
926
  const context = exportContextOf(input);
925
- const resolved = input.resolvedPaths ?? resolveIntegrationPaths(clientId, input.env, input.home, context);
927
+ const resolved = clientId === "zcode" || !input.resolvedPaths
928
+ ? resolveIntegrationPaths(clientId, input.env, input.home, context, io) : input.resolvedPaths;
926
929
  if (clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(resolved.detectDir, context);
927
930
  detectDir = resolved.detectDir;
928
931
  if (clientId === "cline") io = createClineIO(io, resolved.configPath, store, effects.recover);
@@ -945,9 +948,12 @@ export function observeIntegration(
945
948
  * collection route.
946
949
  */
947
950
  effective = resolveIntegrationTarget({
948
- clientId, configPath: recordedPath, io, record: stored, env: input.env, home: input.home,
951
+ clientId, configPath: recordedPath, io, record: stored, env: input.env, home: input.home, resolvedPaths: resolved,
949
952
  });
950
953
  configPath = effective.configPath;
954
+ if (effective.pathError && operation !== "disable") {
955
+ return { failed: observationFailure("unsafe", "unsafe", effective.pathError) } as const;
956
+ }
951
957
  } catch (error) {
952
958
  if (error instanceof ClineTransactionError) {
953
959
  return { failed: { ...observationFailure("unsafe", "unsafe", error.message, error.snapshotPath), residual: true } } as const;
@@ -12,6 +12,7 @@ import { homedir } from "node:os";
12
12
  import { assertDroidSettingsUnambiguous } from "./droid-settings";
13
13
  import { readPath } from "./merge";
14
14
  import type { OwnershipRecord } from "./ownership";
15
+ import type { IntegrationIO } from "./config-io";
15
16
  import { dirname, join } from "node:path";
16
17
  import {
17
18
  ClientPathError,
@@ -49,6 +50,8 @@ import {
49
50
  raycastConfigPath,
50
51
  zcodeConfigPath,
51
52
  zcodeHomeDir,
53
+ zcodeLegacyHomeDir,
54
+ ZcodeSettingsPathError,
52
55
  zcodeProviderStorePath,
53
56
  buildZcodeStoreContribution,
54
57
  zcodeStoreSchemaEstablished,
@@ -75,12 +78,21 @@ import {
75
78
  */
76
79
  export type IntegrationClientId = ExportClientId;
77
80
 
81
+ export interface ResolvedIntegrationPaths {
82
+ configPath: string;
83
+ detectDir: string;
84
+ /** Snapshot of a mutable store resolution from the same bootstrap read. */
85
+ storePath?: string;
86
+ /** The default config path is only a recovery hint when the active root is unknown. */
87
+ pathError?: string;
88
+ }
89
+
78
90
  export interface IntegrationClientSpec {
79
91
  id: IntegrationClientId;
80
92
  /** The client's config file, honoring that client's own environment override. */
81
- configPath: (env?: NodeJS.ProcessEnv, home?: string) => string;
93
+ configPath: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
82
94
  /** Directory whose existence is the cheap "is it installed?" signal. */
83
- detectDir: (env?: NodeJS.ProcessEnv, home?: string) => string;
95
+ detectDir: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
84
96
  /**
85
97
  * The provider store this client reads INSTEAD of `configPath`.
86
98
  *
@@ -98,7 +110,7 @@ export interface IntegrationClientSpec {
98
110
  * client, never merged into on a guess.
99
111
  */
100
112
  currentStore?: {
101
- path: (env?: NodeJS.ProcessEnv, home?: string) => string;
113
+ path: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
102
114
  format: ConfigFormat;
103
115
  establishes: (parsed: unknown) => boolean;
104
116
  buildContribution: BuildContribution;
@@ -139,7 +151,7 @@ export interface IntegrationClientSpec {
139
151
  * Droid uses the same seam to check competing settings against the export
140
152
  * context before status, preview, or mutation proceeds.
141
153
  */
142
- resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext) => { configPath: string; detectDir: string };
154
+ resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext, io?: Pick<IntegrationIO, "readText">) => ResolvedIntegrationPaths;
143
155
  /**
144
156
  * Where the client's config WOULD live, for a client whose real path cannot
145
157
  * be resolved yet.
@@ -183,10 +195,11 @@ export function resolveIntegrationPaths(
183
195
  env: NodeJS.ProcessEnv = process.env,
184
196
  home: string = homedir(),
185
197
  exportContext?: ExportContext,
186
- ): { configPath: string; detectDir: string } {
198
+ io?: Pick<IntegrationIO, "readText">,
199
+ ): ResolvedIntegrationPaths {
187
200
  const spec = INTEGRATION_CLIENTS[clientId];
188
- if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext);
189
- return { configPath: spec.configPath(env, home), detectDir: spec.detectDir(env, home) };
201
+ if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext, io);
202
+ return { configPath: spec.configPath(env, home, io), detectDir: spec.detectDir(env, home, io) };
190
203
  }
191
204
 
192
205
  export function assertDroidPathsUnambiguous(root: string, exportContext?: ExportContext): void {
@@ -357,15 +370,31 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
357
370
  },
358
371
  zcode: {
359
372
  id: "zcode",
360
- configPath: (env = process.env, home = homedir()) => zcodeConfigPath(env, home),
361
- detectDir: (env = process.env, home = homedir()) => zcodeHomeDir(env, home),
373
+ configPath: (env = process.env, home = homedir(), io) => zcodeConfigPath(env, home, io),
374
+ detectDir: (env = process.env, home = homedir(), io) => zcodeHomeDir(env, home, io),
375
+ resolvePaths: (env = process.env, home = homedir(), _context, io) => {
376
+ let root: string;
377
+ let pathError: string | undefined;
378
+ try { root = zcodeHomeDir(env, home, io); }
379
+ catch (error) {
380
+ if (!(error instanceof ZcodeSettingsPathError)) throw error;
381
+ root = zcodeLegacyHomeDir(env, home);
382
+ pathError = error.message;
383
+ }
384
+ // Passing the resolved root prevents a second bootstrap read. An explicit
385
+ // provider-file override outranks both the setting and ZCODE_DATA_DIR.
386
+ const storePath = zcodeProviderStorePath({ ...env, ZCODE_DATA_DIR: root }, home, io);
387
+ return { configPath: join(root, "v2", "config.json"), detectDir: root, storePath, ...(pathError ? { pathError } : {}) };
388
+ },
389
+ bindsDriftedRecord: (path, env = process.env, home = homedir()) =>
390
+ path === join(zcodeLegacyHomeDir(env, home), "v2", "config.json"),
362
391
  /*
363
392
  * ZCode 3.14 reads its providers from `v2/provider_config.json` and reaches
364
393
  * `v2/config.json` only through the import that seeded it. Where the new
365
394
  * file exists the import is spent, so our write is read by nobody (#5348).
366
395
  */
367
396
  currentStore: {
368
- path: (env = process.env, home = homedir()) => zcodeProviderStorePath(env, home),
397
+ path: (env = process.env, home = homedir(), io) => zcodeProviderStorePath(env, home, io),
369
398
  format: "json",
370
399
  establishes: zcodeStoreSchemaEstablished,
371
400
  buildContribution: buildZcodeStoreContribution,
@@ -496,7 +525,7 @@ export function boundIntegrationConfigPath(input: {
496
525
  if (
497
526
  record && record.clientId === input.clientId &&
498
527
  record.configPath !== input.resolvedPath &&
499
- input.statKind(record.configPath) === "file" &&
528
+ (input.clientId === "zcode" ? input.statKind(record.configPath) !== "missing" : input.statKind(record.configPath) === "file") &&
500
529
  INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord?.(record.configPath, input.env, input.home) === true
501
530
  ) {
502
531
  return record.configPath;
@@ -529,6 +558,11 @@ export function restoreOwnershipCollision(input: {
529
558
  }): string | null {
530
559
  const currentPath = input.currentPath;
531
560
  if (currentPath === null || currentPath === input.journaledPath) return null;
561
+ // A relocated ZCode store must not lose its single record to an older
562
+ // legacy undo, even when both journaled locations are legal for this home.
563
+ if (input.clientId === "zcode") {
564
+ return `that operation was recorded for ${input.journaledPath}, but ${currentPath} currently owns this integration`;
565
+ }
532
566
  const binds = INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord;
533
567
  if (!binds) return null;
534
568
  if (binds(input.journaledPath, input.env, input.home) !== true) return null;
@@ -39,6 +39,7 @@ import {
39
39
  assertDroidPathsUnambiguous,
40
40
  assertDroidRecordedSettingsUnambiguous,
41
41
  resolveIntegrationPaths,
42
+ type ResolvedIntegrationPaths,
42
43
  unresolvedPathHintFor,
43
44
  type IntegrationClientId,
44
45
  } from "./registry";
@@ -482,7 +483,7 @@ export interface IntegrationStateInput {
482
483
  store?: IntegrationStateStore;
483
484
  io?: IntegrationIO;
484
485
  /** Internal explicit profile target; never accepted as a caller-provided path. */
485
- resolvedPaths?: { configPath: string; detectDir: string };
486
+ resolvedPaths?: ResolvedIntegrationPaths;
486
487
  }
487
488
 
488
489
  export function exportContextOf(input: {
@@ -592,11 +593,11 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
592
593
  */
593
594
  let installed: boolean;
594
595
  let effective: IntegrationTarget;
595
- let record: OwnershipRecord | null;
596
+ let record: OwnershipRecord | null = null;
596
597
  try {
597
598
  // One resolution for both, so a client whose paths come from mutable state
598
599
  // cannot report one account's install beside another account's config path.
599
- const paths = resolveStatePaths(input);
600
+ const paths = resolveStatePaths(input, io);
600
601
  installed = io.statKind(paths.detectDir) === "dir";
601
602
  if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
602
603
  /*
@@ -611,7 +612,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
611
612
  statKind: io.statKind, env: input.env, home: input.home,
612
613
  });
613
614
  effective = resolveIntegrationTarget({
614
- clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home,
615
+ clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home, resolvedPaths: paths,
615
616
  });
616
617
  } catch (error) {
617
618
  if (!(error instanceof ClientPathError)) throw error;
@@ -626,6 +627,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
626
627
  * yet. A client that can name where its config would go gets `installed:
627
628
  * false` and that location, which reads as "not installed" in the UI.
628
629
  */
630
+ if (input.clientId === "zcode" && record) return {
631
+ clientId: input.clientId, state: "unsafe", installed: false, configPath: record.configPath,
632
+ reason: "unresolvable-path", appliedAt: record.appliedAt, lastOpId: record.opId, ...retention,
633
+ };
629
634
  const hint = unresolvedPathHintFor(input.clientId, input.env, input.home);
630
635
  return {
631
636
  clientId: input.clientId,
@@ -638,6 +643,12 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
638
643
  }
639
644
 
640
645
  const configPath = effective.configPath;
646
+ if (effective.pathError) return {
647
+ clientId: input.clientId, state: "unsafe", installed, configPath,
648
+ reason: "unresolvable-path",
649
+ ...(record?.configPath === configPath ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
650
+ ...retention,
651
+ };
641
652
  if (input.clientId === "kilo") {
642
653
  const candidates = inspectKiloCandidates({ io, selectedPath: configPath, env: input.env, home: input.home });
643
654
  if (candidates.kind !== "ok") return {
@@ -715,9 +726,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
715
726
  };
716
727
  }
717
728
 
718
- function resolveStatePaths(input: IntegrationStateInput): { configPath: string; detectDir: string } {
729
+ function resolveStatePaths(input: IntegrationStateInput, io: IntegrationIO): ResolvedIntegrationPaths {
719
730
  const context = exportContextOf(input);
720
- const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home, context);
731
+ const paths = input.clientId === "zcode" || !input.resolvedPaths
732
+ ? resolveIntegrationPaths(input.clientId, input.env, input.home, context, io) : input.resolvedPaths;
721
733
  if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
722
734
  return paths;
723
735
  }
@@ -727,7 +739,7 @@ export function readOwnedDroidReasoningDefaults(input: IntegrationStateInput): R
727
739
  const store = input.store ?? createIntegrationStateStore();
728
740
  const io = input.io ?? store.io();
729
741
  try {
730
- const paths = resolveStatePaths(input);
742
+ const paths = resolveStatePaths(input, io);
731
743
  const record = store.readRecords().droid;
732
744
  if (!record || record.clientId !== "droid" || record.configPath !== paths.configPath) return {};
733
745
  const effective = resolveIntegrationTarget({
@@ -23,11 +23,11 @@
23
23
  * nesting into a file holding the user's other providers would trade a
24
24
  * silent no-op for a silent loss.
25
25
  */
26
- import { EXPORT_CLIENTS, ClientPathError, type BuildContribution, type ConfigFormat } from "../clients/config-export";
26
+ import { EXPORT_CLIENTS, ClientPathError, zcodeLegacyHomeDir, zcodeProviderStorePath, type BuildContribution, type ConfigFormat } from "../clients/config-export";
27
27
  import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
28
28
  import { AmbiguousSelectorError, InvalidSelectorError, readPath } from "./merge";
29
29
  import type { OwnershipRecord } from "./ownership";
30
- import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
30
+ import { INTEGRATION_CLIENTS, resolveIntegrationPaths, type ResolvedIntegrationPaths, type IntegrationClientId } from "./registry";
31
31
 
32
32
  export interface IntegrationTarget {
33
33
  /** The file this operation reads, writes, journals and records ownership for. */
@@ -51,6 +51,8 @@ export interface IntegrationTarget {
51
51
  * to is as effective as it ever was.
52
52
  */
53
53
  readonly ineffective: IneffectiveWrite | null;
54
+ /** Active root unknown; the named config is only a historical removal target. */
55
+ readonly pathError?: string;
54
56
  }
55
57
 
56
58
  export interface IneffectiveWrite {
@@ -153,12 +155,44 @@ export function resolveIntegrationTarget(args: {
153
155
  record: OwnershipRecord | null;
154
156
  env?: NodeJS.ProcessEnv;
155
157
  home?: string;
158
+ resolvedPaths?: ResolvedIntegrationPaths;
156
159
  }): IntegrationTarget {
157
160
  const { clientId, configPath, io, record } = args;
158
161
  const declared = INTEGRATION_CLIENTS[clientId].currentStore;
159
162
  if (!declared) return configFileTarget(clientId, configPath, null);
160
- const storePath = declared.path(args.env, args.home);
163
+ const paths = clientId === "zcode"
164
+ ? args.resolvedPaths ?? resolveIntegrationPaths(clientId, args.env, args.home, undefined, io)
165
+ : undefined;
166
+ const storePath = paths?.storePath ?? declared.path(args.env, args.home, io);
161
167
  const kind = io.statKind(storePath);
168
+ const owned = record !== null && record.clientId === clientId ? record : null;
169
+ const historicalStore = clientId === "zcode"
170
+ ? zcodeProviderStorePath({ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, io)
171
+ : null;
172
+ if (clientId === "zcode" && owned && owned.configPath !== configPath && owned.configPath !== storePath
173
+ && owned.configPath !== historicalStore
174
+ && INTEGRATION_CLIENTS[clientId].bindsDriftedRecord?.(owned.configPath, args.env, args.home) !== true
175
+ && io.statKind(owned.configPath) !== "missing") {
176
+ throw new ClientPathError(`ZCode ownership remains at ${owned.configPath}. Its location cannot be bound to the current home or selectors; restore the original selectors and disable it before enabling a different data root. Ownership and recovery evidence are retained.`);
177
+ }
178
+ // An explicit provider-file selector can establish the active target even
179
+ // when the bootstrap is unreadable, but absence cannot justify a legacy write.
180
+ if (paths?.pathError && (!(args.env ?? process.env).ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim() || kind !== "file")) {
181
+ const removal = owned?.configPath === historicalStore
182
+ ? storeTarget(declared, historicalStore)
183
+ : configFileTarget(clientId, configPath, null);
184
+ return { ...removal, pathError: paths.pathError };
185
+ }
186
+ if (historicalStore && owned?.configPath === historicalStore && historicalStore !== storePath
187
+ && recordedBlockStillPresent(io, declared.format, historicalStore, owned)) {
188
+ return storeTarget(declared, historicalStore, { store: storePath, why: "owned-config-file" });
189
+ }
190
+ // Relocation must not orphan a block at the default historical path, even
191
+ // when the relocated store has not been created yet.
192
+ if (clientId === "zcode" && owned?.configPath === configPath && configPath !== paths?.configPath
193
+ && recordedBlockStillPresent(io, EXPORT_CLIENTS[clientId].format, configPath, owned)) {
194
+ return configFileTarget(clientId, configPath, { store: storePath, why: "owned-config-file" });
195
+ }
162
196
  if (kind === "missing") {
163
197
  const missing = declared.missingStore;
164
198
  return missing?.readsStore(storePath, path => io.statKind(path)) === true
@@ -182,7 +216,6 @@ export function resolveIntegrationTarget(args: {
182
216
  * file holds it. Disable has to remove what we wrote from where we wrote it,
183
217
  * and an apply must never leave a block in one file while writing another.
184
218
  */
185
- const owned = record !== null && record.clientId === clientId ? record : null;
186
219
  if (owned?.configPath === storePath && recordedBlockStillPresent(io, declared.format, storePath, owned)) {
187
220
  return storeTarget(declared, storePath, established ? null : unestablished);
188
221
  }
@@ -214,6 +247,7 @@ export function declaredIntegrationTarget(args: {
214
247
  resolvedConfigPath: string;
215
248
  env?: NodeJS.ProcessEnv;
216
249
  home?: string;
250
+ io?: Pick<IntegrationIO, "readText">;
217
251
  }): IntegrationTarget | null {
218
252
  const { clientId, configPath, resolvedConfigPath } = args;
219
253
  if (configPath === resolvedConfigPath) return configFileTarget(clientId, configPath, null);
@@ -222,8 +256,11 @@ export function declaredIntegrationTarget(args: {
222
256
  }
223
257
  const declared = INTEGRATION_CLIENTS[clientId].currentStore;
224
258
  if (!declared) return null;
259
+ if (clientId === "zcode" && configPath === zcodeProviderStorePath(
260
+ { ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, args.io,
261
+ )) return storeTarget(declared, configPath);
225
262
  try {
226
- if (declared.path(args.env, args.home) !== configPath) return null;
263
+ if (declared.path(args.env, args.home, args.io) !== configPath) return null;
227
264
  } catch (error) {
228
265
  // A store the operator relocated with a path we cannot resolve proves
229
266
  // nothing about the row, so the row is not a legal target.
@@ -39,6 +39,7 @@ import {
39
39
  assertDroidPathsUnambiguous,
40
40
  isLoopbackOnly,
41
41
  resolveIntegrationPaths,
42
+ type ResolvedIntegrationPaths,
42
43
  restoreOwnershipCollision,
43
44
  type IntegrationClientId,
44
45
  } from "./registry";
@@ -643,8 +644,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
643
644
  if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
644
645
 
645
646
  const clientId = entry.clientId;
646
- const resolvedPath = input.resolvedPaths?.configPath
647
- ?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
647
+ let resolvedPath: string;
648
+ try {
649
+ resolvedPath = clientId === "zcode" || !input.resolvedPaths
650
+ ? resolveIntegrationPaths(clientId, input.env, input.home, undefined, io).configPath
651
+ : input.resolvedPaths.configPath;
652
+ } catch (error) {
653
+ if (!(error instanceof ClientPathError)) throw error;
654
+ return refuse(clientId, "unsafe", "unsafe", error.message);
655
+ }
648
656
  const configPath = entry.configPath;
649
657
  /*
650
658
  * Restore acts on the path the operation was journaled against. Resolving a
@@ -655,7 +663,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
655
663
  * shape those bytes are in, which is what the state below is measured against.
656
664
  */
657
665
  const rowTarget = declaredIntegrationTarget({
658
- clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
666
+ clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home, io,
659
667
  });
660
668
  if (rowTarget === null) {
661
669
  return refuse(clientId, "conflict", "conflict",
@@ -814,7 +822,7 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
814
822
  io: IntegrationIO;
815
823
  env: NodeJS.ProcessEnv;
816
824
  home: string;
817
- resolvedPaths: { configPath: string; detectDir: string };
825
+ resolvedPaths: ResolvedIntegrationPaths;
818
826
  };
819
827
 
820
828
  /**
@@ -854,7 +862,7 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
854
862
  */
855
863
  const resolvedPaths = input.resolvedPaths
856
864
  ? { ...input.resolvedPaths }
857
- : resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }));
865
+ : resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }), io);
858
866
  /*
859
867
  * The configuration and the roster are seams like the others, and they were the two still held
860
868
  * by reference. A coordinated write plans from this input, awaits the writer lock and a
@@ -899,7 +907,7 @@ function storeLockFile(frozen: FrozenIntegrationInput): string | null {
899
907
  if (!declared?.lockFile) return null;
900
908
  let storePath: string;
901
909
  try {
902
- storePath = declared.path(frozen.env, frozen.home);
910
+ storePath = declared.path(frozen.env, frozen.home, frozen.io);
903
911
  } catch (error) {
904
912
  if (error instanceof ClientPathError) return null;
905
913
  throw error;