@tt-a1i/openpi 0.4.0 → 0.6.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 (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. 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 = {
@@ -56,7 +56,7 @@ export const WORKFLOW_TOOL_DESCRIPTION = [
56
56
  "Interactive sessions launch in the background by default and deliver completion later. Set wait: true only when this tool call must return the final result inline.",
57
57
  "Derive fan-out from independent verifiable work items and task difficulty. Concurrency is a runtime ceiling, not a target or the total-call limit; user cost, count, model, and effort constraints take precedence.",
58
58
  "For concurrent writers use isolation: 'worktree' and tell each agent to commit. Read-only work should normally stay in the shared checkout.",
59
- "Read the workflows Skill before a nontrivial script; it covers the restricted sandbox, full DSL, acceptance, result refs, replay, background lifecycle, limits, and examples.",
59
+ "Read the workflows Skill before a nontrivial script; it covers the restricted sandbox, full DSL, result refs, replay, background lifecycle, limits, and examples.",
60
60
  ].join("\n");
61
61
 
62
62
  /** Adds workflow orchestration primitives and background execution to the model's tool prompt. */
@@ -75,14 +75,6 @@ export function buildWorkflowAgentPrompt(prompt: string) {
75
75
  return prompt;
76
76
  }
77
77
 
78
- /** Instructs structured workflow children to terminate with exactly one structured_output call. */
79
- export const STRUCTURED_OUTPUT_SYSTEM_INSTRUCTION =
80
- "When your task is complete, call the `structured_output` tool exactly once as your final action, with fields matching the required schema. Do not write any other text after it.";
81
-
82
- /** Describes the terminating structured_output tool and its final-action contract. */
83
- export const STRUCTURED_OUTPUT_TOOL_DESCRIPTION =
84
- "Return your final result as structured data matching the required schema. Call this exactly once, as your last action; do not write any other text after it.";
85
-
86
78
  /** Builds the workflow completion report returned to the parent model. */
87
79
  export function buildWorkflowResultMessage(
88
80
  details: WorkflowDetails,
@@ -157,7 +149,9 @@ export function buildWorkflowResultMessage(
157
149
  : "running";
158
150
  lines.push(
159
151
  `- [${agent.label}]${agent.phase ? ` (${agent.phase})` : ""} ${status}` +
160
- (agent.acceptance ? ` · acceptance ${agent.acceptance.status}` : "") +
152
+ (agent.acceptance
153
+ ? ` · deprecated model self-attestation ${agent.acceptance.status}`
154
+ : "") +
161
155
  (agent.error ? ` — ${agent.error}` : ""),
162
156
  );
163
157
  }
@@ -214,6 +208,16 @@ export function buildProjectedWorkflowCompletionBatch(
214
208
  }[],
215
209
  usage?: ParentContextUsage | null,
216
210
  ) {
211
+ const deliveryFacts = JSON.stringify(
212
+ entries.map(({ deliveryId, details, runDir }) => ({
213
+ deliveryId,
214
+ runId: details.runId,
215
+ evidence: shortenHome(runDir),
216
+ })),
217
+ );
218
+ const manifest =
219
+ "Workflow completion delivery facts (stable across retries; deduplicate by deliveryId):\n" +
220
+ deliveryFacts;
217
221
  const full = entries.map(({ deliveryId, details, runDir }) =>
218
222
  buildBackgroundWorkflowFollowUp({
219
223
  runId: details.runId,
@@ -223,18 +227,30 @@ export function buildProjectedWorkflowCompletionBatch(
223
227
  }),
224
228
  );
225
229
  const separatorBytes = Math.max(0, entries.length - 1) * 2;
230
+ const manifestSeparatorBytes = entries.length > 0 ? 2 : 0;
231
+ const fixedBytes =
232
+ Buffer.byteLength(manifest, "utf8") +
233
+ separatorBytes +
234
+ manifestSeparatorBytes;
235
+ const bodyBudget = 48 * 1024 - fixedBytes;
236
+ if (bodyBudget < 0) {
237
+ throw new Error(
238
+ "Workflow completion delivery facts exceed the transport payload limit",
239
+ );
240
+ }
226
241
  const allocation = allocateResultBudgets(
227
242
  full.map((message) => Buffer.byteLength(message, "utf8")),
228
243
  usage,
229
244
  {
230
- maxBatchBytes: 48 * 1024 - separatorBytes,
245
+ maxBatchBytes: bodyBudget,
231
246
  maxResultBytes: 48 * 1024,
232
247
  minResultBytes: 1024,
233
248
  headroomShare: 0.25,
234
249
  estimatedBytesPerToken: 4,
250
+ fixedBytes,
235
251
  },
236
252
  );
237
- return full
253
+ const projected = full
238
254
  .map((message, index) =>
239
255
  projectText(message, {
240
256
  maxBytes: allocation.budgets[index] ?? 1024,
@@ -243,6 +259,44 @@ export function buildProjectedWorkflowCompletionBatch(
243
259
  }),
244
260
  )
245
261
  .join("\n\n");
262
+ return projected ? `${manifest}\n\n${projected}` : manifest;
263
+ }
264
+
265
+ /** Split transport batches so every manifest and projected body fit together. */
266
+ export function buildProjectedWorkflowCompletionBatches(
267
+ entries: readonly {
268
+ deliveryId: string;
269
+ details: WorkflowDetails;
270
+ runDir: string;
271
+ }[],
272
+ usage?: ParentContextUsage | null,
273
+ ) {
274
+ const batches: Array<{
275
+ entries: (typeof entries)[number][];
276
+ content: string;
277
+ }> = [];
278
+ let current: (typeof entries)[number][] = [];
279
+ for (const entry of entries) {
280
+ const candidate = [...current, entry];
281
+ try {
282
+ buildProjectedWorkflowCompletionBatch(candidate, usage);
283
+ current = candidate;
284
+ } catch (error) {
285
+ if (current.length === 0) throw error;
286
+ batches.push({
287
+ entries: current,
288
+ content: buildProjectedWorkflowCompletionBatch(current, usage),
289
+ });
290
+ current = [entry];
291
+ }
292
+ }
293
+ if (current.length > 0) {
294
+ batches.push({
295
+ entries: current,
296
+ content: buildProjectedWorkflowCompletionBatch(current, usage),
297
+ });
298
+ }
299
+ return batches;
246
300
  }
247
301
 
248
302
  /** 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,3 +1,12 @@
1
+ import {
2
+ type CompletionEnvelope,
3
+ type CompletionOwner,
4
+ createCompletionInbox,
5
+ } from "../shared/completion-inbox.ts";
6
+ import type {
7
+ DurableResultDeliveryQueue,
8
+ DurableResultDeliveryReceipt,
9
+ } from "../shared/result-delivery.ts";
1
10
  import type { WorkflowDetails } from "./model.ts";
2
11
 
3
12
  export interface WorkflowCompletionEnvelope {
@@ -6,19 +15,14 @@ export interface WorkflowCompletionEnvelope {
6
15
  details: WorkflowDetails;
7
16
  }
8
17
 
9
- export interface WorkflowDeliveryReceipt {
10
- deliveryId: string;
11
- delivered: boolean;
12
- error?: string;
13
- }
14
-
15
18
  export interface WorkflowResultDeliveryOptions {
16
19
  isIdle: () => boolean;
20
+ owner?: () => CompletionOwner | undefined;
17
21
  persist: (details: WorkflowDetails) => void;
18
22
  deliver: (
19
23
  envelopes: readonly WorkflowCompletionEnvelope[],
20
24
  wake: boolean,
21
- ) => Promise<readonly WorkflowDeliveryReceipt[]>;
25
+ ) => Promise<readonly DurableResultDeliveryReceipt[]>;
22
26
  }
23
27
 
24
28
  function errorText(error: unknown) {
@@ -36,8 +40,12 @@ function errorText(error: unknown) {
36
40
  export function createWorkflowResultDelivery(
37
41
  options: WorkflowResultDeliveryOptions,
38
42
  ) {
39
- const pending = new Map<string, WorkflowCompletionEnvelope>();
43
+ const inbox = createCompletionInbox<WorkflowCompletionEnvelope>();
44
+ const currentOwner =
45
+ options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
40
46
  let flushing: Promise<void> | undefined;
47
+ let flushRequested = false;
48
+ let wakeRequested = false;
41
49
 
42
50
  const persistState = (
43
51
  details: WorkflowDetails,
@@ -57,67 +65,154 @@ export function createWorkflowResultDelivery(
57
65
  options.persist(details);
58
66
  };
59
67
 
60
- const enqueue = (envelope: WorkflowCompletionEnvelope) => {
61
- pending.set(envelope.deliveryId, envelope);
68
+ const inboxEnvelope = (
69
+ envelope: WorkflowCompletionEnvelope,
70
+ ): CompletionEnvelope<WorkflowCompletionEnvelope> => {
71
+ const delivery = envelope.details.delivery;
72
+ const deliverySessionId = delivery?.ownerSessionId;
73
+ const detailsSessionId = envelope.details.sessionId;
74
+
75
+ // Conflicting owner fields invalidate delivery ownership fail-closed.
76
+ const hasConflict =
77
+ deliverySessionId !== undefined &&
78
+ detailsSessionId !== undefined &&
79
+ deliverySessionId !== detailsSessionId;
80
+
81
+ const ownerSessionId = hasConflict ? undefined : deliverySessionId;
82
+ return {
83
+ deliveryId: envelope.deliveryId,
84
+ owner: {
85
+ sessionId:
86
+ ownerSessionId && ownerSessionId !== "unowned"
87
+ ? ownerSessionId
88
+ : "unowned",
89
+ epoch: delivery?.ownerEpoch ?? 0,
90
+ },
91
+ producer: "workflow",
92
+ producerId: envelope.runId,
93
+ terminalRef: {
94
+ kind: "workflow-terminal",
95
+ runId: envelope.runId,
96
+ status: envelope.details.status,
97
+ },
98
+ wake: "producer-policy",
99
+ payload: envelope,
100
+ };
101
+ };
102
+
103
+ const enqueue = (envelope: WorkflowCompletionEnvelope) =>
104
+ inbox.defer(inboxEnvelope(envelope), currentOwner());
105
+
106
+ const retainPending = (
107
+ envelope: WorkflowCompletionEnvelope,
108
+ patch: Partial<NonNullable<WorkflowDetails["delivery"]>>,
109
+ persistenceFailure: string,
110
+ ) => {
111
+ // Memory owns the retry before persistence is attempted. A broken disk
112
+ // must not make this envelope, or any sibling after it, disappear from the
113
+ // current process.
114
+ const admitted = enqueue(envelope);
115
+ try {
116
+ persistState(envelope.details, "pending", patch);
117
+ } catch (error) {
118
+ const delivery = envelope.details.delivery;
119
+ if (delivery) {
120
+ envelope.details.delivery = {
121
+ ...delivery,
122
+ state: "pending",
123
+ updatedAt: Date.now(),
124
+ lastError: `${persistenceFailure}: ${errorText(error)}`,
125
+ };
126
+ }
127
+ }
128
+ return admitted;
62
129
  };
63
130
 
64
131
  const flush = async (wake: boolean) => {
65
- if (flushing) return flushing;
66
- if (pending.size === 0) return;
67
- const envelopes = [...pending.values()];
68
- for (const envelope of envelopes) pending.delete(envelope.deliveryId);
132
+ if (flushing) {
133
+ flushRequested = true;
134
+ wakeRequested ||= wake;
135
+ return flushing;
136
+ }
137
+ if (inbox.size() === 0) return;
69
138
 
70
139
  flushing = (async () => {
71
- let receipts: readonly WorkflowDeliveryReceipt[];
72
- try {
73
- receipts = await options.deliver(envelopes, wake);
74
- } catch (error) {
75
- const message = errorText(error);
76
- 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
- });
140
+ let passWake = wake;
141
+ while (inbox.size() > 0) {
142
+ flushRequested = false;
143
+ wakeRequested = false;
144
+ const claimed = inbox.claim(currentOwner());
145
+ const envelopes = claimed.map((envelope) => envelope.payload);
146
+ if (envelopes.length === 0) break;
147
+
148
+ let receipts: readonly DurableResultDeliveryReceipt[] | undefined;
149
+ try {
150
+ receipts = await options.deliver(envelopes, passWake);
151
+ } catch (error) {
152
+ const message = errorText(error);
153
+ for (const envelope of envelopes) {
154
+ inbox.acknowledge([envelope.deliveryId]);
155
+ retainPending(
156
+ envelope,
157
+ {
158
+ attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
159
+ lastError: message,
160
+ },
161
+ "Pending delivery persistence failed",
162
+ );
163
+ }
82
164
  }
83
- return;
84
- }
85
165
 
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
- };
166
+ if (receipts !== undefined) {
167
+ const byId = new Map(
168
+ receipts.map((receipt) => [receipt.deliveryId, receipt] as const),
169
+ );
170
+ for (const envelope of envelopes) {
171
+ const receipt = byId.get(envelope.deliveryId);
172
+ if (receipt?.delivered) {
173
+ inbox.acknowledge([envelope.deliveryId]);
174
+ try {
175
+ persistState(envelope.details, "delivered", {
176
+ attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
177
+ deliveredAt: Date.now(),
178
+ lastError: undefined,
179
+ });
180
+ } catch (error) {
181
+ // The transport already accepted the message, but the durable
182
+ // receipt did not commit. Retain it for at-least-once recovery;
183
+ // the visible stable id lets the parent recognize a rare replay.
184
+ enqueue(envelope);
185
+ const delivery = envelope.details.delivery;
186
+ if (delivery) {
187
+ envelope.details.delivery = {
188
+ ...delivery,
189
+ state: "pending",
190
+ updatedAt: Date.now(),
191
+ lastError: `Delivery receipt persistence failed: ${errorText(error)}`,
192
+ };
193
+ }
194
+ }
195
+ continue;
111
196
  }
197
+ inbox.acknowledge([envelope.deliveryId]);
198
+ retainPending(
199
+ envelope,
200
+ {
201
+ attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
202
+ lastError:
203
+ receipt?.error ?? "Completion delivery was not acknowledged",
204
+ },
205
+ "Pending delivery persistence failed",
206
+ );
112
207
  }
113
- continue;
114
208
  }
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
- });
209
+
210
+ // A completion or lifecycle edge joined this pass while transport was
211
+ // in flight. Drain once more so that request is not lost. Failures
212
+ // retained above do not request their own immediate retry, preventing
213
+ // an unavailable transport from creating a busy loop.
214
+ if (!flushRequested) break;
215
+ passWake ||= wakeRequested;
121
216
  }
122
217
  })().finally(() => {
123
218
  flushing = undefined;
@@ -125,7 +220,7 @@ export function createWorkflowResultDelivery(
125
220
  return flushing;
126
221
  };
127
222
 
128
- return {
223
+ const queue = {
129
224
  /** Register inline interest before the run starts. */
130
225
  holdInline(details: WorkflowDetails) {
131
226
  persistState(details, "held-for-inline");
@@ -133,7 +228,7 @@ export function createWorkflowResultDelivery(
133
228
 
134
229
  /** Terminal won the wait/abort arbitration and will be returned inline. */
135
230
  consumeInline(details: WorkflowDetails) {
136
- if (details.delivery) pending.delete(details.delivery.id);
231
+ if (details.delivery) inbox.consumeDeliveryIds([details.delivery.id]);
137
232
  persistState(details, "consumed-inline", {
138
233
  deliveredAt: Date.now(),
139
234
  lastError: undefined,
@@ -142,15 +237,21 @@ export function createWorkflowResultDelivery(
142
237
 
143
238
  /** Abort won the wait/terminal arbitration; deliver the result later. */
144
239
  releaseInline(envelope: WorkflowCompletionEnvelope) {
145
- persistState(envelope.details, "pending", { lastError: undefined });
146
- enqueue(envelope);
240
+ retainPending(
241
+ envelope,
242
+ { lastError: undefined },
243
+ "Initial delivery persistence failed",
244
+ );
147
245
  if (options.isIdle()) void flush(true);
148
246
  },
149
247
 
150
- /** Queue a detached run after terminal status and pending are persisted. */
248
+ /** Queue a detached run with in-memory retry ownership before persistence. */
151
249
  defer(envelope: WorkflowCompletionEnvelope) {
152
- persistState(envelope.details, "pending", { lastError: undefined });
153
- enqueue(envelope);
250
+ retainPending(
251
+ envelope,
252
+ { lastError: undefined },
253
+ "Initial delivery persistence failed",
254
+ );
154
255
  if (options.isIdle()) void flush(true);
155
256
  },
156
257
 
@@ -158,15 +259,50 @@ export function createWorkflowResultDelivery(
158
259
  restore(envelope: WorkflowCompletionEnvelope) {
159
260
  const state = envelope.details.delivery?.state;
160
261
  if (state !== "pending" && state !== "held-for-inline") return false;
161
- // A process restart cannot still own the inline waiter. Deterministically
162
- // reconstruct pending delivery from the terminal artifact.
163
- if (state === "held-for-inline") {
164
- persistState(envelope.details, "pending", {
165
- lastError: "Inline waiter was not active after session restart",
166
- });
262
+ const owner = currentOwner();
263
+ const deliverySessionId = envelope.details.delivery?.ownerSessionId;
264
+ const detailsSessionId = envelope.details.sessionId;
265
+ const hasConflict =
266
+ deliverySessionId !== undefined &&
267
+ detailsSessionId !== undefined &&
268
+ deliverySessionId !== detailsSessionId;
269
+ const storedSessionId = hasConflict
270
+ ? undefined
271
+ : (deliverySessionId ?? detailsSessionId);
272
+
273
+ // Restoring canonical producer state is the explicit owner-revival
274
+ // boundary. Rebind only the same transcript to this process-local
275
+ // SessionManager generation; a different Session still dead-letters.
276
+ if (
277
+ owner &&
278
+ storedSessionId !== undefined &&
279
+ storedSessionId === owner.sessionId
280
+ ) {
281
+ envelope.details.delivery = {
282
+ ...envelope.details.delivery!,
283
+ ownerSessionId: owner.sessionId,
284
+ ownerEpoch: owner.epoch,
285
+ };
286
+ return retainPending(
287
+ envelope,
288
+ {
289
+ ownerSessionId: owner.sessionId,
290
+ ownerEpoch: owner.epoch,
291
+ lastError:
292
+ state === "held-for-inline"
293
+ ? "Inline waiter was not active after session restart"
294
+ : undefined,
295
+ },
296
+ "Restored delivery state persistence failed",
297
+ );
167
298
  }
168
- enqueue(envelope);
169
- return true;
299
+ // Keep the canonical terminal artifact pending, but record that this
300
+ // process is not its transcript owner instead of redirecting it.
301
+ return enqueue(envelope);
302
+ },
303
+
304
+ retryPending() {
305
+ return flush(true) ?? Promise.resolve();
170
306
  },
171
307
 
172
308
  parentSettled() {
@@ -179,11 +315,13 @@ export function createWorkflowResultDelivery(
179
315
  },
180
316
 
181
317
  size() {
182
- return pending.size;
318
+ return inbox.size();
183
319
  },
184
320
 
185
321
  clear() {
186
- pending.clear();
322
+ inbox.clear();
187
323
  },
324
+ inspectDeadLetters: inbox.inspectDeadLetters,
188
325
  };
326
+ return queue satisfies DurableResultDeliveryQueue<WorkflowCompletionEnvelope>;
189
327
  }