@tt-a1i/openpi 0.4.0 → 0.5.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 (109) hide show
  1. package/README.md +102 -40
  2. package/SETUP.md +22 -6
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/background-terminals/index.ts +30 -2
  6. package/extensions/background-terminals/src/domain.ts +2 -0
  7. package/extensions/background-terminals/src/manager.ts +486 -106
  8. package/extensions/background-terminals/src/output.ts +33 -0
  9. package/extensions/background-terminals/src/prompt.ts +13 -5
  10. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  11. package/extensions/clear-context/index.ts +83 -0
  12. package/extensions/context-pivot/index.ts +16 -6
  13. package/extensions/cron/schedule.ts +7 -1
  14. package/extensions/file-mutation-display/render.ts +17 -257
  15. package/extensions/file-search/src/binaries.ts +57 -41
  16. package/extensions/git-read/index.ts +1 -3
  17. package/extensions/model-info/index.ts +21 -33
  18. package/extensions/model-info/session-metrics.ts +96 -0
  19. package/extensions/plan-mode/bash-policy.ts +54 -9
  20. package/extensions/plan-mode/index.ts +7 -2
  21. package/extensions/post-edit/index.ts +16 -6
  22. package/extensions/sessions/git-stats.ts +258 -72
  23. package/extensions/sessions/index.ts +153 -86
  24. package/extensions/sessions/preview-cache.ts +104 -0
  25. package/extensions/sessions/preview-loader.ts +856 -0
  26. package/extensions/sessions/sessions.ts +43 -4
  27. package/extensions/setup/index.ts +123 -127
  28. package/extensions/shared/activity-status.ts +30 -0
  29. package/extensions/shared/agent-session-page.ts +319 -0
  30. package/extensions/shared/agent-tool-renderer.ts +218 -0
  31. package/extensions/shared/agent-transcript.ts +524 -0
  32. package/extensions/shared/capability-intent.ts +1 -1
  33. package/extensions/shared/child-session.ts +437 -21
  34. package/extensions/shared/result-delivery.ts +34 -0
  35. package/extensions/shared/setup-config.ts +73 -33
  36. package/extensions/shared/setup-episode-state.ts +1 -1
  37. package/extensions/shared/terminal-text.ts +110 -23
  38. package/extensions/shared/text-projection.ts +72 -15
  39. package/extensions/shared/tool-activity.ts +382 -0
  40. package/extensions/shared/tool-surface.ts +29 -2
  41. package/extensions/shared/transcript-viewport.ts +46 -0
  42. package/extensions/shared/web-observer-registry.ts +390 -0
  43. package/extensions/shared/worktree.ts +11 -0
  44. package/extensions/subagents/index.ts +270 -59
  45. package/extensions/subagents/navigation.ts +34 -5
  46. package/extensions/subagents/src/backend.ts +12 -1
  47. package/extensions/subagents/src/backends/pi.ts +375 -66
  48. package/extensions/subagents/src/domain.ts +5 -0
  49. package/extensions/subagents/src/manager.ts +34 -2
  50. package/extensions/subagents/src/prompt.ts +32 -4
  51. package/extensions/subagents/src/result-artifact.ts +4 -0
  52. package/extensions/subagents/src/result-delivery.ts +7 -1
  53. package/extensions/subagents/src/runtime.ts +15 -1
  54. package/extensions/subagents/src/ui/takeover.ts +73 -257
  55. package/extensions/subagents/src/ui/transcript.ts +38 -535
  56. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  57. package/extensions/suggestions/src/ui.ts +10 -4
  58. package/extensions/tasks/index.ts +0 -3
  59. package/extensions/ui-customization/footer.ts +0 -40
  60. package/extensions/ui-customization/index.ts +0 -4
  61. package/extensions/user-input-fold/index.ts +1 -1
  62. package/extensions/web/index.ts +234 -0
  63. package/extensions/workflows/artifacts.ts +137 -47
  64. package/extensions/workflows/completion-projection.ts +457 -0
  65. package/extensions/workflows/coordinator.ts +8 -10
  66. package/extensions/workflows/dashboard.ts +167 -228
  67. package/extensions/workflows/handoff.ts +70 -16
  68. package/extensions/workflows/index.ts +488 -198
  69. package/extensions/workflows/journal.ts +148 -13
  70. package/extensions/workflows/model.ts +74 -4
  71. package/extensions/workflows/navigation.ts +32 -8
  72. package/extensions/workflows/progress-projection.ts +306 -0
  73. package/extensions/workflows/prompt.ts +66 -6
  74. package/extensions/workflows/replay-safety.ts +42 -21
  75. package/extensions/workflows/result-delivery.ts +128 -64
  76. package/extensions/workflows/retention.ts +593 -0
  77. package/extensions/workflows/runner.ts +388 -279
  78. package/extensions/workflows/sandbox-child.cjs +25 -3
  79. package/extensions/workflows/sandbox.ts +62 -8
  80. package/extensions/workflows/serialization.ts +325 -17
  81. package/extensions/workflows/tool-renderer.ts +22 -0
  82. package/extensions/workflows/transcript.ts +149 -0
  83. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  84. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  85. package/package.json +28 -8
  86. package/skills/subagents/REFERENCE.md +189 -0
  87. package/skills/subagents/SKILL.md +1 -1
  88. package/skills/workflows/REFERENCE.md +4 -2
  89. package/web/adapter/pi-adapter.ts +661 -0
  90. package/web/host/browser-launcher.ts +20 -0
  91. package/web/host/static-assets.ts +4 -0
  92. package/web/host/terminal-status.ts +38 -0
  93. package/web/host/web-host.ts +789 -0
  94. package/web/http-dispatcher.ts +125 -0
  95. package/web/protocol/types.ts +462 -0
  96. package/web/runtime/pi-runtime.ts +991 -0
  97. package/web/runtime/types.ts +71 -0
  98. package/web/runtime/web-host-lease.ts +497 -0
  99. package/web/trace.ts +18 -0
  100. package/web/ui/app.js +1398 -0
  101. package/web/ui/index.html +139 -0
  102. package/web/ui/styles.css +598 -0
  103. package/web/vite.config.mjs +34 -0
  104. package/extensions/execution-convergence/active-evidence.ts +0 -129
  105. package/extensions/execution-convergence/index.ts +0 -442
  106. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  107. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  108. package/extensions/setup/intercom.ts +0 -603
  109. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -1,8 +1,8 @@
1
- import { sanitizeTerminalText } from "../shared/terminal-text.ts";
2
1
  import {
3
2
  allocateResultBudgets,
4
3
  type ParentContextUsage,
5
4
  } from "../shared/result-budget.ts";
5
+ import { sanitizeTerminalText } from "../shared/terminal-text.ts";
6
6
  import { projectText } from "../shared/text-projection.ts";
7
7
  import {
8
8
  countStates,
@@ -18,15 +18,15 @@ export const WORKFLOW_PARAMETER_DESCRIPTIONS = {
18
18
  "JavaScript workflow script. May start with `export const meta = {...}`, then use phase(), agent(), parallel(), args, and a final `return`.",
19
19
  args: "Optional JSON string exposed to the script as `args` (parsed when valid JSON, otherwise passed through as the raw string).",
20
20
  background:
21
- "Deprecated compatibility alias: true means wait=false; false means wait=true. Do not provide both background and wait.",
21
+ "Deprecated compatibility alias for published callers only; new calls must use wait. Replace true with wait=false and false with wait=true. Do not provide both fields. The alias will be removed in the next announced breaking release.",
22
22
  wait: "Wait for the final result in this tool call. Interactive sessions default to false and deliver completion later; print/automation defaults to true. Interrupting the wait does not cancel the workflow.",
23
23
  resumeFromRunId:
24
24
  "Optional prior run id or unique suffix for safe read-only replay. See the workflows Skill for matching rules.",
25
25
  };
26
26
 
27
- /** Describes stopping a running background workflow, mirroring subagent_cancel/bg_kill. */
27
+ /** Describes stopping a running workflow, mirroring subagent_cancel/bg_kill. */
28
28
  export const WORKFLOW_STOP_TOOL_DESCRIPTION =
29
- "Cancel a running background workflow by its run id (from the workflow launch result). This aborts its remaining agents and settles the run; partial results and artifacts are preserved. Only background runs need this — a blocking workflow is already cancelled by interrupting the turn.";
29
+ "Cancel a running workflow by its run id (from the workflow launch result). This aborts its remaining agents and settles the run; partial results and artifacts are preserved.";
30
30
 
31
31
  /** Model-facing schema description for the workflow run id to stop. */
32
32
  export const WORKFLOW_STOP_PARAMETER_DESCRIPTIONS = {
@@ -214,6 +214,16 @@ export function buildProjectedWorkflowCompletionBatch(
214
214
  }[],
215
215
  usage?: ParentContextUsage | null,
216
216
  ) {
217
+ const deliveryFacts = JSON.stringify(
218
+ entries.map(({ deliveryId, details, runDir }) => ({
219
+ deliveryId,
220
+ runId: details.runId,
221
+ evidence: shortenHome(runDir),
222
+ })),
223
+ );
224
+ const manifest =
225
+ "Workflow completion delivery facts (stable across retries; deduplicate by deliveryId):\n" +
226
+ deliveryFacts;
217
227
  const full = entries.map(({ deliveryId, details, runDir }) =>
218
228
  buildBackgroundWorkflowFollowUp({
219
229
  runId: details.runId,
@@ -223,18 +233,30 @@ export function buildProjectedWorkflowCompletionBatch(
223
233
  }),
224
234
  );
225
235
  const separatorBytes = Math.max(0, entries.length - 1) * 2;
236
+ const manifestSeparatorBytes = entries.length > 0 ? 2 : 0;
237
+ const fixedBytes =
238
+ Buffer.byteLength(manifest, "utf8") +
239
+ separatorBytes +
240
+ manifestSeparatorBytes;
241
+ const bodyBudget = 48 * 1024 - fixedBytes;
242
+ if (bodyBudget < 0) {
243
+ throw new Error(
244
+ "Workflow completion delivery facts exceed the transport payload limit",
245
+ );
246
+ }
226
247
  const allocation = allocateResultBudgets(
227
248
  full.map((message) => Buffer.byteLength(message, "utf8")),
228
249
  usage,
229
250
  {
230
- maxBatchBytes: 48 * 1024 - separatorBytes,
251
+ maxBatchBytes: bodyBudget,
231
252
  maxResultBytes: 48 * 1024,
232
253
  minResultBytes: 1024,
233
254
  headroomShare: 0.25,
234
255
  estimatedBytesPerToken: 4,
256
+ fixedBytes,
235
257
  },
236
258
  );
237
- return full
259
+ const projected = full
238
260
  .map((message, index) =>
239
261
  projectText(message, {
240
262
  maxBytes: allocation.budgets[index] ?? 1024,
@@ -243,6 +265,44 @@ export function buildProjectedWorkflowCompletionBatch(
243
265
  }),
244
266
  )
245
267
  .join("\n\n");
268
+ return projected ? `${manifest}\n\n${projected}` : manifest;
269
+ }
270
+
271
+ /** Split transport batches so every manifest and projected body fit together. */
272
+ export function buildProjectedWorkflowCompletionBatches(
273
+ entries: readonly {
274
+ deliveryId: string;
275
+ details: WorkflowDetails;
276
+ runDir: string;
277
+ }[],
278
+ usage?: ParentContextUsage | null,
279
+ ) {
280
+ const batches: Array<{
281
+ entries: (typeof entries)[number][];
282
+ content: string;
283
+ }> = [];
284
+ let current: (typeof entries)[number][] = [];
285
+ for (const entry of entries) {
286
+ const candidate = [...current, entry];
287
+ try {
288
+ buildProjectedWorkflowCompletionBatch(candidate, usage);
289
+ current = candidate;
290
+ } catch (error) {
291
+ if (current.length === 0) throw error;
292
+ batches.push({
293
+ entries: current,
294
+ content: buildProjectedWorkflowCompletionBatch(current, usage),
295
+ });
296
+ current = [entry];
297
+ }
298
+ }
299
+ if (current.length > 0) {
300
+ batches.push({
301
+ entries: current,
302
+ content: buildProjectedWorkflowCompletionBatch(current, usage),
303
+ });
304
+ }
305
+ return batches;
246
306
  }
247
307
 
248
308
  /** Builds the background-launch result and tells the parent model how to inspect or stop the run. */
@@ -423,13 +423,44 @@ function canonicalPath(value: string) {
423
423
  return fs.realpathSync.native(value);
424
424
  }
425
425
 
426
- function repositoryFingerprint(cwd: string) {
426
+ function deadlineBoundGitRunner() {
427
+ // One budget per fingerprint call: every git invocation shares a single
428
+ // deadline.
427
429
  const deadline = Date.now() + REPLAY_IDENTITY_TIMEOUT_MS;
428
- const git = (gitCwd: string, args: readonly string[]) =>
430
+ return (gitCwd: string, args: readonly string[]) =>
429
431
  boundedGit(gitCwd, args, deadline);
430
- const root = canonicalPath(git(cwd, ["rev-parse", "--show-toplevel"]).trim());
431
- const head = git(root, ["rev-parse", "--verify", "HEAD"]).trim();
432
- const diff = git(root, [
432
+ }
433
+
434
+ export function repositoryFingerprint(
435
+ cwd: string,
436
+ runGit: (
437
+ gitCwd: string,
438
+ args: readonly string[],
439
+ ) => string = deadlineBoundGitRunner(),
440
+ ) {
441
+ const root = canonicalPath(
442
+ runGit(cwd, ["rev-parse", "--show-toplevel"]).trim(),
443
+ );
444
+ const assertNoIgnoredFiles = () => {
445
+ // Ignored files are observable to a read-capable child but are commonly
446
+ // too large or secret-bearing to hash (node_modules, .env, build output).
447
+ // A complete identity cannot pretend they do not exist, so disable replay.
448
+ const ignoredOutput = runGit(root, [
449
+ "ls-files",
450
+ "--others",
451
+ "--ignored",
452
+ "--exclude-standard",
453
+ "--directory",
454
+ "-z",
455
+ ]);
456
+ if (ignoredOutput.length > 0) {
457
+ throw new Error("ignored files make the replay identity incomplete");
458
+ }
459
+ };
460
+ // The common disqualifying case exits before the expensive binary diff.
461
+ assertNoIgnoredFiles();
462
+ const head = runGit(root, ["rev-parse", "--verify", "HEAD"]).trim();
463
+ const diff = runGit(root, [
433
464
  "diff",
434
465
  "--no-ext-diff",
435
466
  "--no-textconv",
@@ -437,7 +468,7 @@ function repositoryFingerprint(cwd: string) {
437
468
  "HEAD",
438
469
  "--",
439
470
  ]);
440
- const trackedModes = git(root, ["ls-files", "-s", "-z"]);
471
+ const trackedModes = runGit(root, ["ls-files", "-s", "-z"]);
441
472
  // Tracked symlinks can expose changing content outside Git, while gitlinks
442
473
  // can expose submodule state absent from the parent diff. Neither has a
443
474
  // complete bounded identity here, so execute instead of replaying.
@@ -450,21 +481,11 @@ function repositoryFingerprint(cwd: string) {
450
481
  ) {
451
482
  throw new Error("symlinks and gitlinks make replay identity incomplete");
452
483
  }
453
- // Ignored files are observable to a read-capable child but are commonly too
454
- // large or secret-bearing to hash (node_modules, .env, build output). A
455
- // complete identity cannot pretend they do not exist, so disable replay.
456
- const ignoredOutput = git(root, [
457
- "ls-files",
458
- "--others",
459
- "--ignored",
460
- "--exclude-standard",
461
- "--directory",
462
- "-z",
463
- ]);
464
- if (ignoredOutput.length > 0) {
465
- throw new Error("ignored files make the replay identity incomplete");
466
- }
467
- const untrackedOutput = git(root, [
484
+ // Preserve the original post-diff safety observation: an ignored file can
485
+ // appear while the synchronous diff is running, including from another
486
+ // process. The early check is only an optimization, never the final gate.
487
+ assertNoIgnoredFiles();
488
+ const untrackedOutput = runGit(root, [
468
489
  "ls-files",
469
490
  "--others",
470
491
  "--exclude-standard",
@@ -1,4 +1,8 @@
1
1
  import type { WorkflowDetails } from "./model.ts";
2
+ import type {
3
+ DurableResultDeliveryQueue,
4
+ DurableResultDeliveryReceipt,
5
+ } from "../shared/result-delivery.ts";
2
6
 
3
7
  export interface WorkflowCompletionEnvelope {
4
8
  deliveryId: string;
@@ -6,19 +10,13 @@ export interface WorkflowCompletionEnvelope {
6
10
  details: WorkflowDetails;
7
11
  }
8
12
 
9
- export interface WorkflowDeliveryReceipt {
10
- deliveryId: string;
11
- delivered: boolean;
12
- error?: string;
13
- }
14
-
15
13
  export interface WorkflowResultDeliveryOptions {
16
14
  isIdle: () => boolean;
17
15
  persist: (details: WorkflowDetails) => void;
18
16
  deliver: (
19
17
  envelopes: readonly WorkflowCompletionEnvelope[],
20
18
  wake: boolean,
21
- ) => Promise<readonly WorkflowDeliveryReceipt[]>;
19
+ ) => Promise<readonly DurableResultDeliveryReceipt[]>;
22
20
  }
23
21
 
24
22
  function errorText(error: unknown) {
@@ -38,6 +36,8 @@ export function createWorkflowResultDelivery(
38
36
  ) {
39
37
  const pending = new Map<string, WorkflowCompletionEnvelope>();
40
38
  let flushing: Promise<void> | undefined;
39
+ let flushRequested = false;
40
+ let wakeRequested = false;
41
41
 
42
42
  const persistState = (
43
43
  details: WorkflowDetails,
@@ -61,63 +61,113 @@ export function createWorkflowResultDelivery(
61
61
  pending.set(envelope.deliveryId, envelope);
62
62
  };
63
63
 
64
+ const retainPending = (
65
+ envelope: WorkflowCompletionEnvelope,
66
+ patch: Partial<NonNullable<WorkflowDetails["delivery"]>>,
67
+ persistenceFailure: string,
68
+ ) => {
69
+ // Memory owns the retry before persistence is attempted. A broken disk
70
+ // must not make this envelope, or any sibling after it, disappear from the
71
+ // current process.
72
+ enqueue(envelope);
73
+ try {
74
+ persistState(envelope.details, "pending", patch);
75
+ } catch (error) {
76
+ const delivery = envelope.details.delivery;
77
+ if (delivery) {
78
+ envelope.details.delivery = {
79
+ ...delivery,
80
+ state: "pending",
81
+ updatedAt: Date.now(),
82
+ lastError: `${persistenceFailure}: ${errorText(error)}`,
83
+ };
84
+ }
85
+ }
86
+ };
87
+
64
88
  const flush = async (wake: boolean) => {
65
- if (flushing) return flushing;
89
+ if (flushing) {
90
+ flushRequested = true;
91
+ wakeRequested ||= wake;
92
+ return flushing;
93
+ }
66
94
  if (pending.size === 0) return;
67
- const envelopes = [...pending.values()];
68
- for (const envelope of envelopes) pending.delete(envelope.deliveryId);
69
95
 
70
96
  flushing = (async () => {
71
- let receipts: readonly WorkflowDeliveryReceipt[];
72
- try {
73
- receipts = await options.deliver(envelopes, wake);
74
- } catch (error) {
75
- const message = errorText(error);
97
+ let passWake = wake;
98
+ while (pending.size > 0) {
99
+ flushRequested = false;
100
+ wakeRequested = false;
101
+ const envelopes = [...pending.values()];
76
102
  for (const envelope of envelopes) {
77
- enqueue(envelope);
78
- persistState(envelope.details, "pending", {
79
- attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
80
- lastError: message,
81
- });
103
+ pending.delete(envelope.deliveryId);
82
104
  }
83
- return;
84
- }
85
105
 
86
- const byId = new Map(
87
- receipts.map((receipt) => [receipt.deliveryId, receipt] as const),
88
- );
89
- for (const envelope of envelopes) {
90
- const receipt = byId.get(envelope.deliveryId);
91
- if (receipt?.delivered) {
92
- try {
93
- persistState(envelope.details, "delivered", {
94
- attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
95
- deliveredAt: Date.now(),
96
- lastError: undefined,
97
- });
98
- } catch (error) {
99
- // The transport already accepted the message, but the durable
100
- // receipt did not commit. Retain it for at-least-once recovery;
101
- // the visible stable id lets the parent recognize a rare replay.
102
- enqueue(envelope);
103
- const delivery = envelope.details.delivery;
104
- if (delivery) {
105
- envelope.details.delivery = {
106
- ...delivery,
107
- state: "pending",
108
- updatedAt: Date.now(),
109
- lastError: `Delivery receipt persistence failed: ${errorText(error)}`,
110
- };
106
+ let receipts: readonly DurableResultDeliveryReceipt[] | undefined;
107
+ try {
108
+ receipts = await options.deliver(envelopes, passWake);
109
+ } catch (error) {
110
+ const message = errorText(error);
111
+ for (const envelope of envelopes) {
112
+ retainPending(
113
+ envelope,
114
+ {
115
+ attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
116
+ lastError: message,
117
+ },
118
+ "Pending delivery persistence failed",
119
+ );
120
+ }
121
+ }
122
+
123
+ if (receipts !== undefined) {
124
+ const byId = new Map(
125
+ receipts.map((receipt) => [receipt.deliveryId, receipt] as const),
126
+ );
127
+ for (const envelope of envelopes) {
128
+ const receipt = byId.get(envelope.deliveryId);
129
+ if (receipt?.delivered) {
130
+ try {
131
+ persistState(envelope.details, "delivered", {
132
+ attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
133
+ deliveredAt: Date.now(),
134
+ lastError: undefined,
135
+ });
136
+ } catch (error) {
137
+ // The transport already accepted the message, but the durable
138
+ // receipt did not commit. Retain it for at-least-once recovery;
139
+ // the visible stable id lets the parent recognize a rare replay.
140
+ enqueue(envelope);
141
+ const delivery = envelope.details.delivery;
142
+ if (delivery) {
143
+ envelope.details.delivery = {
144
+ ...delivery,
145
+ state: "pending",
146
+ updatedAt: Date.now(),
147
+ lastError: `Delivery receipt persistence failed: ${errorText(error)}`,
148
+ };
149
+ }
150
+ }
151
+ continue;
111
152
  }
153
+ retainPending(
154
+ envelope,
155
+ {
156
+ attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
157
+ lastError:
158
+ receipt?.error ?? "Completion delivery was not acknowledged",
159
+ },
160
+ "Pending delivery persistence failed",
161
+ );
112
162
  }
113
- continue;
114
163
  }
115
- enqueue(envelope);
116
- persistState(envelope.details, "pending", {
117
- attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
118
- lastError:
119
- receipt?.error ?? "Completion delivery was not acknowledged",
120
- });
164
+
165
+ // A completion or lifecycle edge joined this pass while transport was
166
+ // in flight. Drain once more so that request is not lost. Failures
167
+ // retained above do not request their own immediate retry, preventing
168
+ // an unavailable transport from creating a busy loop.
169
+ if (!flushRequested) break;
170
+ passWake ||= wakeRequested;
121
171
  }
122
172
  })().finally(() => {
123
173
  flushing = undefined;
@@ -125,7 +175,7 @@ export function createWorkflowResultDelivery(
125
175
  return flushing;
126
176
  };
127
177
 
128
- return {
178
+ const queue = {
129
179
  /** Register inline interest before the run starts. */
130
180
  holdInline(details: WorkflowDetails) {
131
181
  persistState(details, "held-for-inline");
@@ -142,15 +192,21 @@ export function createWorkflowResultDelivery(
142
192
 
143
193
  /** Abort won the wait/terminal arbitration; deliver the result later. */
144
194
  releaseInline(envelope: WorkflowCompletionEnvelope) {
145
- persistState(envelope.details, "pending", { lastError: undefined });
146
- enqueue(envelope);
195
+ retainPending(
196
+ envelope,
197
+ { lastError: undefined },
198
+ "Initial delivery persistence failed",
199
+ );
147
200
  if (options.isIdle()) void flush(true);
148
201
  },
149
202
 
150
- /** Queue a detached run after terminal status and pending are persisted. */
203
+ /** Queue a detached run with in-memory retry ownership before persistence. */
151
204
  defer(envelope: WorkflowCompletionEnvelope) {
152
- persistState(envelope.details, "pending", { lastError: undefined });
153
- enqueue(envelope);
205
+ retainPending(
206
+ envelope,
207
+ { lastError: undefined },
208
+ "Initial delivery persistence failed",
209
+ );
154
210
  if (options.isIdle()) void flush(true);
155
211
  },
156
212
 
@@ -161,14 +217,21 @@ export function createWorkflowResultDelivery(
161
217
  // A process restart cannot still own the inline waiter. Deterministically
162
218
  // reconstruct pending delivery from the terminal artifact.
163
219
  if (state === "held-for-inline") {
164
- persistState(envelope.details, "pending", {
165
- lastError: "Inline waiter was not active after session restart",
166
- });
220
+ retainPending(
221
+ envelope,
222
+ { lastError: "Inline waiter was not active after session restart" },
223
+ "Restored delivery state persistence failed",
224
+ );
225
+ } else {
226
+ enqueue(envelope);
167
227
  }
168
- enqueue(envelope);
169
228
  return true;
170
229
  },
171
230
 
231
+ retryPending() {
232
+ return flush(true) ?? Promise.resolve();
233
+ },
234
+
172
235
  parentSettled() {
173
236
  return flush(true);
174
237
  },
@@ -186,4 +249,5 @@ export function createWorkflowResultDelivery(
186
249
  pending.clear();
187
250
  },
188
251
  };
252
+ return queue satisfies DurableResultDeliveryQueue<WorkflowCompletionEnvelope>;
189
253
  }