@bitkyc08/opencodex 2.37.0 → 2.38.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 (66) hide show
  1. package/bin/ocx.mjs +69 -10
  2. package/gui/dist/assets/{index-CowztZdo.js → index-C14iCj_Q.js} +13 -13
  3. package/gui/dist/assets/index-D7PIz7_g.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/aside.svg +3 -0
  6. package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
  7. package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
  8. package/gui/dist/provider-icons/openclaw.svg +54 -0
  9. package/gui/dist/provider-icons/prime-agent.svg +21 -0
  10. package/gui/dist/provider-icons/zcode.svg +219 -0
  11. package/package.json +1 -1
  12. package/src/adapters/cursor/protobuf-request.ts +4 -1
  13. package/src/adapters/cursor/tool-definitions.ts +36 -4
  14. package/src/cli/capabilities.ts +14 -0
  15. package/src/cli/codex-cli-update.ts +96 -0
  16. package/src/cli/codex-shim-autorestore.ts +3 -0
  17. package/src/cli/export-command.ts +18 -17
  18. package/src/cli/help.ts +2 -2
  19. package/src/cli/index.ts +3 -2
  20. package/src/cli/launcher-context.ts +53 -2
  21. package/src/cli/opencode.ts +126 -33
  22. package/src/cli/registry.ts +16 -10
  23. package/src/cli/system-command.ts +6 -1
  24. package/src/clients/config-export.ts +293 -28
  25. package/src/codex/account-store.ts +10 -4
  26. package/src/codex/autostart-health.ts +3 -3
  27. package/src/codex/catalog/provider-fetch.ts +20 -1
  28. package/src/codex/catalog/sync.ts +4 -3
  29. package/src/codex/cli-install-provenance.ts +795 -0
  30. package/src/codex/convergence.ts +4 -3
  31. package/src/codex/credential-mutation-epoch.ts +11 -0
  32. package/src/codex/main-account.ts +2 -0
  33. package/src/codex/model-entitlements.ts +430 -27
  34. package/src/codex/native-profile-manager.ts +4 -0
  35. package/src/codex/reset-credit-operation-ledger.ts +1411 -0
  36. package/src/codex/reset-credit-recovery.ts +20 -2
  37. package/src/codex/shim.ts +204 -18
  38. package/src/codex/user-identity.ts +2 -1
  39. package/src/config/paths.ts +18 -3
  40. package/src/config.ts +23 -0
  41. package/src/generated/compatibility-version.json +81 -45
  42. package/src/integrations/registry.ts +112 -0
  43. package/src/integrations/state.ts +67 -5
  44. package/src/integrations/writer.ts +25 -9
  45. package/src/lib/bounded-subprocess.ts +36 -0
  46. package/src/lib/strict-semver.ts +47 -0
  47. package/src/lib/windows-elevation.ts +32 -1
  48. package/src/lib/windows-secret-acl.ts +47 -25
  49. package/src/lib/windows-service-mutation-lock.ts +133 -0
  50. package/src/lib/windows-user-principal.ts +15 -17
  51. package/src/responses/spill-store.ts +334 -29
  52. package/src/responses/state.ts +488 -7
  53. package/src/server/index.ts +4 -3
  54. package/src/server/lifecycle.ts +5 -1
  55. package/src/server/management/model-rows.ts +11 -2
  56. package/src/server/management/provider-routes.ts +4 -0
  57. package/src/server/management/system-restart.ts +5 -5
  58. package/src/server/management-api.ts +7 -2
  59. package/src/server/startup-action-control.ts +3 -2
  60. package/src/service.ts +594 -33
  61. package/src/sidecar/candidates.ts +1 -1
  62. package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
  63. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  64. package/src/update/index.ts +3 -2
  65. package/src/update/job.ts +10 -11
  66. package/gui/dist/assets/index-jqE_VOKI.css +0 -1
@@ -15,9 +15,17 @@ import {
15
15
  writeSync,
16
16
  } from "node:fs";
17
17
  import { createHash, randomBytes } from "node:crypto";
18
- import { join } from "node:path";
18
+ import { dirname, join } from "node:path";
19
19
  import { getConfigDir } from "../config";
20
- import { forgetEphemeralSecretPath, forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
20
+ import {
21
+ forgetEphemeralSecretPath,
22
+ forgetHardenedSecretPath,
23
+ hardenSecretDir,
24
+ hardenSecretDirAsync,
25
+ hardenSecretPath,
26
+ hardenSecretPathAsync,
27
+ windowsSecretAclApplies,
28
+ } from "../lib/windows-secret-acl";
21
29
  import { isValidProviderContinuationOwner } from "./provider-continuation";
22
30
  import type { OcxProviderContinuationState } from "../types";
23
31
 
@@ -110,11 +118,47 @@ export interface ResponseSpillIoForTest {
110
118
 
111
119
  let spillIoForTest: ResponseSpillIoForTest | null = null;
112
120
  let spillGeneration = 0;
121
+ let spillNowOverride: (() => number) | null = null;
122
+
123
+ interface ResponseSpillWriteOptions {
124
+ retryTimedOutOnce?: boolean;
125
+ /** Total caller-owned ACL budget shared by every harden in this publication. */
126
+ aclBudgetMs?: number;
127
+ publicationControl?: ResponseSpillPublicationControl;
128
+ }
129
+
130
+ export interface ResponseSpillPublicationControl {
131
+ superseded: boolean;
132
+ tempPath: string | null;
133
+ destinationPath: string | null;
134
+ }
135
+
136
+ interface SpillAclBudget {
137
+ deadline: number;
138
+ perCallMs: number;
139
+ }
140
+
141
+ export function createResponseSpillPublicationControl(): ResponseSpillPublicationControl {
142
+ return { superseded: false, tempPath: null, destinationPath: null };
143
+ }
144
+
145
+ export function markResponseSpillPublicationSuperseded(control: ResponseSpillPublicationControl): void {
146
+ control.superseded = true;
147
+ }
113
148
 
114
149
  export function setSpillIoForTest(io: ResponseSpillIoForTest | null): void {
115
150
  spillIoForTest = io;
116
151
  }
117
152
 
153
+ /** Test-only: inject the spill deadline clock. */
154
+ export function setResponseSpillNowForTests(now: (() => number) | null): void {
155
+ spillNowOverride = now;
156
+ }
157
+
158
+ function spillNow(): number {
159
+ return spillNowOverride?.() ?? Date.now();
160
+ }
161
+
118
162
  function record(event: "write" | "fsync" | "close" | "harden" | "publish" | "dir-fsync" | "stub-swap"): void {
119
163
  spillIoForTest?.record?.(event);
120
164
  }
@@ -175,16 +219,62 @@ function canUseExclusiveCopyFallback(error: unknown): boolean {
175
219
  .some(code => isErrno(error, code));
176
220
  }
177
221
 
178
- function harden(path: string, mode: number): void {
222
+ function spillAclBudget(totalMs: number | undefined): SpillAclBudget | undefined {
223
+ if (totalMs === undefined) return undefined;
224
+ const bounded = Math.max(1, Math.floor(totalMs));
225
+ return { deadline: spillNow() + bounded, perCallMs: Math.max(1, Math.floor(bounded / 2)) };
226
+ }
227
+
228
+ function nextSpillHardenDeadlineMs(budget: SpillAclBudget | undefined): number | undefined {
229
+ if (!budget) return undefined;
230
+ const remaining = budget.deadline - spillNow();
231
+ if (remaining <= 0) {
232
+ throw Object.assign(new Error("Response spill ACL budget exhausted"), { code: "ETIMEDOUT" });
233
+ }
234
+ return Math.min(budget.perCallMs, remaining);
235
+ }
236
+
237
+ function harden(path: string, mode: number, budget?: SpillAclBudget): void {
238
+ const aclApplies = budget ? windowsSecretAclApplies() : process.platform === "win32";
239
+ try {
240
+ chmodSync(path, mode);
241
+ } catch {
242
+ if (!aclApplies) throw new Error("Response spill permission hardening failed");
243
+ }
244
+ if (aclApplies) {
245
+ const deadlineMs = nextSpillHardenDeadlineMs(budget);
246
+ const options = {
247
+ required: true,
248
+ ...(deadlineMs !== undefined ? { deadlineMs } : {}),
249
+ };
250
+ const result = mode === 0o700
251
+ ? hardenSecretDir(path, options)
252
+ : hardenSecretPath(path, options);
253
+ if (!result.ok) throw new Error("Response spill permission hardening failed");
254
+ }
255
+ }
256
+
257
+ async function hardenAsync(
258
+ path: string,
259
+ mode: number,
260
+ budget: SpillAclBudget,
261
+ retryTimedOutOnce = false,
262
+ ): Promise<void> {
179
263
  try {
180
264
  chmodSync(path, mode);
181
265
  } catch {
182
- if (process.platform !== "win32") throw new Error("Response spill permission hardening failed");
266
+ if (!windowsSecretAclApplies()) throw new Error("Response spill permission hardening failed");
183
267
  }
184
- if (process.platform === "win32") {
268
+ if (windowsSecretAclApplies()) {
269
+ const deadlineMs = nextSpillHardenDeadlineMs(budget);
270
+ const options = {
271
+ required: true,
272
+ retryTimedOutOnce,
273
+ ...(deadlineMs !== undefined ? { deadlineMs } : {}),
274
+ };
185
275
  const result = mode === 0o700
186
- ? hardenSecretDir(path, { required: true })
187
- : hardenSecretPath(path, { required: true });
276
+ ? await hardenSecretDirAsync(path, options)
277
+ : await hardenSecretPathAsync(path, options);
188
278
  if (!result.ok) throw new Error("Response spill permission hardening failed");
189
279
  }
190
280
  }
@@ -231,7 +321,94 @@ function unlinkEphemeral(path: string): void {
231
321
  unlink(path, true);
232
322
  }
233
323
 
234
- function publishNoReplace(tempPath: string, destinationPath: string): void {
324
+ function supersededPublicationError(): NodeJS.ErrnoException {
325
+ return Object.assign(new Error("Response spill publication superseded"), { code: "ECANCELED" });
326
+ }
327
+
328
+ function throwIfPublicationSuperseded(control: ResponseSpillPublicationControl | undefined): void {
329
+ if (control?.superseded) throw supersededPublicationError();
330
+ }
331
+
332
+ function clearOwnedPath(
333
+ control: ResponseSpillPublicationControl,
334
+ key: "tempPath" | "destinationPath",
335
+ ephemeral: boolean,
336
+ ): unknown {
337
+ const path = control[key];
338
+ if (!path) return null;
339
+ try {
340
+ if (ephemeral) unlinkEphemeral(path);
341
+ else unlink(path);
342
+ control[key] = null;
343
+ return null;
344
+ } catch (error) {
345
+ if (isErrno(error, "ENOENT")) {
346
+ control[key] = null;
347
+ return null;
348
+ }
349
+ return error;
350
+ }
351
+ }
352
+
353
+ /** Claim and remove every path still owned by an abandoned async publication. */
354
+ export function cleanupSupersededResponseSpillPublication(
355
+ control: ResponseSpillPublicationControl,
356
+ ): Error | null {
357
+ control.superseded = true;
358
+ const ownedDir = control.destinationPath
359
+ ? dirname(control.destinationPath)
360
+ : control.tempPath
361
+ ? dirname(control.tempPath)
362
+ : null;
363
+ const destinationError = clearOwnedPath(control, "destinationPath", false);
364
+ const tempError = clearOwnedPath(control, "tempPath", true);
365
+ if (ownedDir) fsyncDirectoryBestEffort(ownedDir);
366
+ const cleanupError = destinationError ?? tempError;
367
+ return cleanupError ? responseSpillWriteError(cleanupError) : null;
368
+ }
369
+
370
+ function publishNoReplace(
371
+ tempPath: string,
372
+ destinationPath: string,
373
+ budget?: SpillAclBudget,
374
+ ): void {
375
+ try {
376
+ if (spillIoForTest?.link) spillIoForTest.link(tempPath, destinationPath);
377
+ else linkSync(tempPath, destinationPath);
378
+ } catch (error) {
379
+ if (isErrno(error, "EEXIST")) throw error;
380
+ if (!canUseExclusiveCopyFallback(error)) throw error;
381
+ let copied = false;
382
+ try {
383
+ if (spillIoForTest?.copyFileExcl) spillIoForTest.copyFileExcl(tempPath, destinationPath);
384
+ else copyFileSync(tempPath, destinationPath, constants.COPYFILE_EXCL);
385
+ copied = true;
386
+ harden(destinationPath, 0o600, budget);
387
+ const copyFd = openSync(destinationPath, "r");
388
+ try {
389
+ if (spillIoForTest?.fsync) spillIoForTest.fsync(copyFd);
390
+ else fsyncSync(copyFd);
391
+ } finally {
392
+ closeSync(copyFd);
393
+ }
394
+ } catch (copyError) {
395
+ if (copied) {
396
+ try { unlink(destinationPath); } catch { /* startup GC reclaims an incomplete publication */ }
397
+ }
398
+ throw copyError;
399
+ }
400
+ }
401
+ record("publish");
402
+ }
403
+
404
+ async function publishNoReplaceAsync(
405
+ tempPath: string,
406
+ destinationPath: string,
407
+ budget: SpillAclBudget,
408
+ retryTimedOutOnce: boolean,
409
+ publicationControl?: ResponseSpillPublicationControl,
410
+ ): Promise<void> {
411
+ throwIfPublicationSuperseded(publicationControl);
235
412
  try {
236
413
  if (spillIoForTest?.link) spillIoForTest.link(tempPath, destinationPath);
237
414
  else linkSync(tempPath, destinationPath);
@@ -243,7 +420,8 @@ function publishNoReplace(tempPath: string, destinationPath: string): void {
243
420
  if (spillIoForTest?.copyFileExcl) spillIoForTest.copyFileExcl(tempPath, destinationPath);
244
421
  else copyFileSync(tempPath, destinationPath, constants.COPYFILE_EXCL);
245
422
  copied = true;
246
- harden(destinationPath, 0o600);
423
+ await hardenAsync(destinationPath, 0o600, budget, retryTimedOutOnce);
424
+ throwIfPublicationSuperseded(publicationControl);
247
425
  const copyFd = openSync(destinationPath, "r");
248
426
  try {
249
427
  if (spillIoForTest?.fsync) spillIoForTest.fsync(copyFd);
@@ -258,9 +436,48 @@ function publishNoReplace(tempPath: string, destinationPath: string): void {
258
436
  throw copyError;
259
437
  }
260
438
  }
439
+ throwIfPublicationSuperseded(publicationControl);
261
440
  record("publish");
262
441
  }
263
442
 
443
+ function serializedSpill(
444
+ responseId: string,
445
+ state: Omit<ResponseSpillPayload, "version" | "responseId">,
446
+ ): {
447
+ bytes: Buffer;
448
+ digest: string;
449
+ idDigest: string;
450
+ contentDigest: string;
451
+ } {
452
+ const payload: ResponseSpillPayload = {
453
+ version: 1,
454
+ responseId,
455
+ createdAt: state.createdAt,
456
+ ...(state.clientThreadId ? { clientThreadId: state.clientThreadId } : {}),
457
+ items: state.items,
458
+ ...(state.providerOutputStart !== undefined ? { providerOutputStart: state.providerOutputStart } : {}),
459
+ ...(state.providers ? { providers: state.providers } : {}),
460
+ };
461
+ const serialized = JSON.stringify(payload);
462
+ if (serialized === undefined) throw new Error("Response spill serialization failed");
463
+ const bytes = Buffer.from(serialized, "utf8");
464
+ const digest = sha256(bytes);
465
+ return {
466
+ bytes,
467
+ digest,
468
+ idDigest: sha256(responseId).slice(0, 12),
469
+ contentDigest: digest.slice(0, 24),
470
+ };
471
+ }
472
+
473
+ function responseSpillWriteError(cause: unknown): NodeJS.ErrnoException {
474
+ const error = new Error("Response spill write failed", { cause }) as NodeJS.ErrnoException;
475
+ if (cause && typeof cause === "object" && "code" in cause) {
476
+ error.code = String((cause as { code?: unknown }).code);
477
+ }
478
+ return error;
479
+ }
480
+
264
481
  function validSpillRef(ref: ResponseSpillRef): boolean {
265
482
  return ref.version === 1
266
483
  && OWNED_SPILL_NAME.test(ref.fileName)
@@ -302,28 +519,16 @@ function validPayload(value: unknown, responseId: string): value is ResponseSpil
302
519
  export function writeResponseSpillDurably(
303
520
  responseId: string,
304
521
  state: Omit<ResponseSpillPayload, "version" | "responseId">,
522
+ options: ResponseSpillWriteOptions = {},
305
523
  ): ResponseSpillRef {
306
524
  let tempPath: string | null = null;
307
525
  let fd: number | null = null;
308
526
  try {
309
- const payload: ResponseSpillPayload = {
310
- version: 1,
311
- responseId,
312
- createdAt: state.createdAt,
313
- ...(state.clientThreadId ? { clientThreadId: state.clientThreadId } : {}),
314
- items: state.items,
315
- ...(state.providerOutputStart !== undefined ? { providerOutputStart: state.providerOutputStart } : {}),
316
- ...(state.providers ? { providers: state.providers } : {}),
317
- };
318
- const serialized = JSON.stringify(payload);
319
- if (serialized === undefined) throw new Error("Response spill serialization failed");
320
- const bytes = Buffer.from(serialized, "utf8");
321
- const digest = sha256(bytes);
322
- const idDigest = sha256(responseId).slice(0, 12);
323
- const contentDigest = digest.slice(0, 24);
527
+ const aclBudget = spillAclBudget(options.aclBudgetMs);
528
+ const { bytes, digest, idDigest, contentDigest } = serializedSpill(responseId, state);
324
529
  const dir = responseSpillDirectory();
325
530
  mkdirSync(dir, { recursive: true, mode: 0o700 });
326
- harden(dir, 0o700);
531
+ harden(dir, 0o700, aclBudget);
327
532
 
328
533
  tempPath = join(dir, `.response-spill.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
329
534
  fd = openSync(tempPath, "wx", 0o600);
@@ -331,7 +536,7 @@ export function writeResponseSpillDurably(
331
536
  fsyncFile(fd);
332
537
  closeFile(fd);
333
538
  fd = null;
334
- harden(tempPath, 0o600);
539
+ harden(tempPath, 0o600, aclBudget);
335
540
  record("harden");
336
541
  const publishTempPath = tempPath;
337
542
 
@@ -341,7 +546,7 @@ export function writeResponseSpillDurably(
341
546
  if (!OWNED_SPILL_NAME.test(fileName)) throw new Error("Response spill name allocation failed");
342
547
  const destinationPath = join(dir, fileName);
343
548
  try {
344
- publishNoReplace(publishTempPath, destinationPath);
549
+ publishNoReplace(publishTempPath, destinationPath, aclBudget);
345
550
  fsyncDirectoryBestEffort(dir);
346
551
  unlinkEphemeral(publishTempPath);
347
552
  tempPath = null;
@@ -352,14 +557,114 @@ export function writeResponseSpillDurably(
352
557
  }
353
558
  }
354
559
  throw new Error("Response spill publication retries exhausted");
355
- } catch {
560
+ } catch (cause) {
356
561
  if (fd !== null) {
357
562
  try { closeSync(fd); } catch { /* best effort */ }
358
563
  }
359
564
  if (tempPath) {
360
565
  try { unlinkEphemeral(tempPath); } catch { /* best effort */ }
361
566
  }
362
- throw new Error("Response spill write failed");
567
+ throw responseSpillWriteError(cause);
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Windows runtime counterpart of `writeResponseSpillDurably`.
573
+ *
574
+ * The filesystem publication contract stays identical, but required NTFS ACL subprocesses are
575
+ * awaited through Bun.spawn instead of Bun.spawnSync. State ownership and serialization remain in
576
+ * `state.ts`; callers must compare the resident generation again before installing the returned
577
+ * reference because another response can replace it while ACL hardening is pending.
578
+ */
579
+ export async function writeResponseSpillDurablyAsync(
580
+ responseId: string,
581
+ state: Omit<ResponseSpillPayload, "version" | "responseId">,
582
+ options: ResponseSpillWriteOptions & { aclBudgetMs: number },
583
+ ): Promise<ResponseSpillRef> {
584
+ const publicationControl = options.publicationControl;
585
+ const aclBudget = spillAclBudget(options.aclBudgetMs);
586
+ if (!aclBudget) throw new Error("Response spill async ACL budget is required");
587
+ let tempPath: string | null = null;
588
+ let fd: number | null = null;
589
+ try {
590
+ throwIfPublicationSuperseded(publicationControl);
591
+ const { bytes, digest, idDigest, contentDigest } = serializedSpill(responseId, state);
592
+ const dir = responseSpillDirectory();
593
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
594
+ await hardenAsync(dir, 0o700, aclBudget, options.retryTimedOutOnce === true);
595
+ throwIfPublicationSuperseded(publicationControl);
596
+
597
+ tempPath = join(dir, `.response-spill.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
598
+ fd = openSync(tempPath, "wx", 0o600);
599
+ if (publicationControl) publicationControl.tempPath = tempPath;
600
+ writeAll(fd, bytes);
601
+ fsyncFile(fd);
602
+ closeFile(fd);
603
+ fd = null;
604
+ await hardenAsync(tempPath, 0o600, aclBudget, options.retryTimedOutOnce === true);
605
+ throwIfPublicationSuperseded(publicationControl);
606
+ record("harden");
607
+ const publishTempPath = tempPath;
608
+
609
+ for (let attempt = 0; attempt < RESPONSE_SPILL_PUBLISH_RETRIES; attempt++) {
610
+ throwIfPublicationSuperseded(publicationControl);
611
+ spillGeneration += 1;
612
+ const fileName = `${sanitizeResponseId(responseId)}.${idDigest}.${contentDigest}.${spillGeneration}.${bytes.byteLength}.spill.json`;
613
+ if (!OWNED_SPILL_NAME.test(fileName)) throw new Error("Response spill name allocation failed");
614
+ const destinationPath = join(dir, fileName);
615
+ if (publicationControl) publicationControl.destinationPath = destinationPath;
616
+ try {
617
+ throwIfPublicationSuperseded(publicationControl);
618
+ await publishNoReplaceAsync(
619
+ publishTempPath,
620
+ destinationPath,
621
+ aclBudget,
622
+ options.retryTimedOutOnce === true,
623
+ publicationControl,
624
+ );
625
+ throwIfPublicationSuperseded(publicationControl);
626
+ fsyncDirectoryBestEffort(dir);
627
+ unlinkEphemeral(publishTempPath);
628
+ tempPath = null;
629
+ if (publicationControl) {
630
+ publicationControl.tempPath = null;
631
+ publicationControl.destinationPath = null;
632
+ }
633
+ return { version: 1, fileName, digest, payloadBytes: bytes.byteLength };
634
+ } catch (error) {
635
+ if (publicationControl?.superseded) throw error;
636
+ if (publicationControl) publicationControl.destinationPath = null;
637
+ if (isErrno(error, "EEXIST")) continue;
638
+ throw error;
639
+ }
640
+ }
641
+ throw new Error("Response spill publication retries exhausted");
642
+ } catch (cause) {
643
+ if (fd !== null) {
644
+ try { closeSync(fd); } catch { /* best effort */ }
645
+ }
646
+ if (tempPath) {
647
+ try {
648
+ unlinkEphemeral(tempPath);
649
+ if (publicationControl?.tempPath === tempPath) publicationControl.tempPath = null;
650
+ } catch (error) {
651
+ if (isErrno(error, "ENOENT") && publicationControl?.tempPath === tempPath) {
652
+ publicationControl.tempPath = null;
653
+ }
654
+ }
655
+ }
656
+ if (publicationControl) {
657
+ const destinationPath = publicationControl.destinationPath;
658
+ if (destinationPath) {
659
+ try {
660
+ unlink(destinationPath);
661
+ publicationControl.destinationPath = null;
662
+ } catch (error) {
663
+ if (isErrno(error, "ENOENT")) publicationControl.destinationPath = null;
664
+ }
665
+ }
666
+ }
667
+ throw responseSpillWriteError(cause);
363
668
  }
364
669
  }
365
670