@tt-a1i/openpi 0.5.0 → 0.6.1

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 (80) hide show
  1. package/README.md +30 -20
  2. package/SETUP.md +10 -4
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/bin/openpi.js +25 -15
  5. package/extensions/ai-providers/LICENSE.upstream +23 -0
  6. package/extensions/ai-providers/README.md +65 -0
  7. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  8. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  9. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  10. package/extensions/ai-providers/antigravity/models.ts +84 -0
  11. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  12. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  13. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  14. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  15. package/extensions/ai-providers/cursor/constants.ts +5 -0
  16. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  17. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  18. package/extensions/ai-providers/cursor/input-images.ts +105 -0
  19. package/extensions/ai-providers/cursor/models.ts +45 -0
  20. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  21. package/extensions/ai-providers/cursor/proto.ts +1271 -0
  22. package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
  23. package/extensions/ai-providers/cursor/provider.ts +1431 -0
  24. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  25. package/extensions/ai-providers/cursor/tool-bridge.ts +68 -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 +8 -1
  31. package/extensions/background-terminals/src/manager.ts +3 -5
  32. package/extensions/background-terminals/src/result-delivery.ts +43 -23
  33. package/extensions/cron/index.ts +68 -27
  34. package/extensions/cron/schedule.ts +5 -1
  35. package/extensions/model-info/cache-diagnostics.ts +220 -0
  36. package/extensions/model-info/index.ts +45 -1
  37. package/extensions/plan-mode/index.ts +75 -4
  38. package/extensions/setup/index.ts +15 -3
  39. package/extensions/shared/child-session.ts +39 -5
  40. package/extensions/shared/completion-inbox.ts +193 -0
  41. package/extensions/shared/setup-config.ts +10 -1
  42. package/extensions/shared/structured-output.ts +154 -0
  43. package/extensions/subagents/index.ts +64 -7
  44. package/extensions/subagents/src/agent-types.ts +5 -17
  45. package/extensions/subagents/src/backends/pi.ts +130 -48
  46. package/extensions/subagents/src/backends/tool-preview.ts +29 -0
  47. package/extensions/subagents/src/domain.ts +16 -1
  48. package/extensions/subagents/src/manager.ts +7 -71
  49. package/extensions/subagents/src/prompt.ts +19 -5
  50. package/extensions/subagents/src/result-artifact.ts +32 -0
  51. package/extensions/subagents/src/result-delivery.ts +33 -14
  52. package/extensions/subagents/src/runtime.ts +10 -3
  53. package/extensions/ui-customization/footer.ts +16 -5
  54. package/extensions/user-input-fold/index.ts +42 -6
  55. package/extensions/web/index.ts +25 -2
  56. package/extensions/workflows/acceptance.ts +43 -19
  57. package/extensions/workflows/completion-projection.ts +3 -1
  58. package/extensions/workflows/dashboard.ts +147 -21
  59. package/extensions/workflows/index.ts +75 -20
  60. package/extensions/workflows/model.ts +5 -1
  61. package/extensions/workflows/progress-projection.ts +7 -1
  62. package/extensions/workflows/prompt.ts +4 -10
  63. package/extensions/workflows/result-delivery.ts +96 -22
  64. package/extensions/workflows/retention.ts +6 -0
  65. package/extensions/workflows/runner.ts +11 -233
  66. package/extensions/workflows/sandbox.ts +4 -0
  67. package/package.json +7 -7
  68. package/skills/subagents/REFERENCE.md +9 -9
  69. package/skills/subagents/SKILL.md +2 -1
  70. package/skills/workflows/REFERENCE.md +5 -3
  71. package/skills/workflows/SKILL.md +1 -1
  72. package/web/adapter/pi-adapter.ts +3 -0
  73. package/web/host/pi-coding-agent-entry.ts +162 -0
  74. package/web/host/web-host.ts +330 -50
  75. package/web/protocol/types.ts +5 -0
  76. package/web/runtime/pi-runtime.ts +240 -25
  77. package/web/runtime/types.ts +32 -1
  78. package/web/ui/app.js +343 -41
  79. package/web/ui/index.html +3 -0
  80. package/web/ui/styles.css +119 -37
@@ -53,8 +53,13 @@ import {
53
53
  formatActivityStatus,
54
54
  } from "../shared/activity-status.ts";
55
55
  import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
56
- import { waitBounded } from "../shared/child-session.ts";
56
+ import {
57
+ inheritedChildToolAllowlist,
58
+ resolveStandaloneChildProjectTrust,
59
+ waitBounded,
60
+ } from "../shared/child-session.ts";
57
61
  import { contextPercent } from "../shared/context-utilization.ts";
62
+ import { completionOwnerFor } from "../shared/completion-inbox.ts";
58
63
  import {
59
64
  registerEditorLayer,
60
65
  removeEditorLayer,
@@ -537,10 +542,13 @@ interface ScriptAgentResult {
537
542
  /** Opaque same-run handle for bounded downstream handoff. */
538
543
  ref?: string;
539
544
  acceptance?: AgentRecord["acceptance"];
545
+ /** Present only for the deprecated model self-attestation compatibility path. */
546
+ acceptanceWarning?: string;
540
547
  error?: string;
541
548
  }
542
549
 
543
550
  interface AgentCallOptions {
551
+ working_dir?: unknown;
544
552
  agent_type?: unknown;
545
553
  label?: unknown;
546
554
  phase?: unknown;
@@ -857,6 +865,8 @@ export default function workflows(
857
865
  };
858
866
  const resultDelivery = createWorkflowResultDelivery({
859
867
  isIdle: () => lastContext?.isIdle() ?? false,
868
+ owner: () =>
869
+ lastContext ? completionOwnerFor(lastContext.sessionManager) : undefined,
860
870
  persist: (details) => {
861
871
  if (!details.delivery)
862
872
  throw new Error("Workflow delivery identity is missing");
@@ -1276,6 +1286,8 @@ export default function workflows(
1276
1286
  agents: [],
1277
1287
  delivery: {
1278
1288
  id: `workflow:${runId}:terminal`,
1289
+ ownerSessionId: completionOwnerFor(ctx.sessionManager).sessionId,
1290
+ ownerEpoch: completionOwnerFor(ctx.sessionManager).epoch,
1279
1291
  state: launchMode === "inline" ? "held-for-inline" : "none",
1280
1292
  attempts: 0,
1281
1293
  updatedAt: now,
@@ -1332,11 +1344,12 @@ export default function workflows(
1332
1344
  structured: boolean,
1333
1345
  cwd: string,
1334
1346
  agentTypePrompt?: string,
1347
+ childProjectTrusted = projectTrusted,
1335
1348
  ) =>
1336
1349
  createWorkflowResources(
1337
1350
  cwd,
1338
1351
  structured ? "structured" : "plain",
1339
- projectTrusted,
1352
+ childProjectTrusted,
1340
1353
  agentTypePrompt,
1341
1354
  );
1342
1355
 
@@ -1631,6 +1644,36 @@ export default function workflows(
1631
1644
  );
1632
1645
  }
1633
1646
 
1647
+ const childTools = inheritedChildToolAllowlist(
1648
+ pi.getActiveTools(),
1649
+ agentType?.tools,
1650
+ );
1651
+ if (
1652
+ opts.working_dir !== undefined &&
1653
+ (typeof opts.working_dir !== "string" || !opts.working_dir.trim())
1654
+ ) {
1655
+ return fail(
1656
+ `agent "${label}": working_dir must be a non-empty string`,
1657
+ );
1658
+ }
1659
+ const requestedCwd = path.resolve(
1660
+ ctx.cwd,
1661
+ typeof opts.working_dir === "string" ? opts.working_dir : ".",
1662
+ );
1663
+ try {
1664
+ if (!fs.statSync(requestedCwd).isDirectory())
1665
+ throw new Error("not a directory");
1666
+ } catch {
1667
+ return fail(
1668
+ `agent "${label}": working_dir is not a directory: ${requestedCwd}`,
1669
+ );
1670
+ }
1671
+ const childProjectTrusted = resolveStandaloneChildProjectTrust({
1672
+ parentCwd: ctx.cwd,
1673
+ childCwd: requestedCwd,
1674
+ parentTrusted: projectTrusted,
1675
+ });
1676
+
1634
1677
  const explicitModel =
1635
1678
  typeof opts.model === "string" && opts.model.trim()
1636
1679
  ? opts.model.trim()
@@ -1701,11 +1744,14 @@ export default function workflows(
1701
1744
  const operatorFingerprint = operatorKey
1702
1745
  ? agentCallKey("workflow-operator", {
1703
1746
  execution: {
1747
+ cwd: requestedCwd,
1748
+ projectTrusted: childProjectTrusted,
1749
+ tools: childTools,
1704
1750
  agentType: agentType
1705
1751
  ? {
1706
1752
  name: agentType.name,
1707
1753
  body: agentType.body,
1708
- tools: agentType.tools,
1754
+ tools: childTools,
1709
1755
  }
1710
1756
  : undefined,
1711
1757
  model: model ? `${model.provider}/${model.id}` : undefined,
@@ -1725,7 +1771,7 @@ export default function workflows(
1725
1771
  const replaySafe =
1726
1772
  operatorKey === undefined &&
1727
1773
  isReplaySafeAgentCall({
1728
- tools: agentType?.tools,
1774
+ tools: agentType?.tools === undefined ? undefined : childTools,
1729
1775
  isolation: opts.isolation,
1730
1776
  });
1731
1777
  const replayLease = beginProcessReplayWorkspaceLease(replaySafe);
@@ -1737,13 +1783,14 @@ export default function workflows(
1737
1783
  try {
1738
1784
  replayResources = await getResources(
1739
1785
  effectiveSchema !== undefined,
1740
- ctx.cwd,
1786
+ requestedCwd,
1741
1787
  agentType?.body,
1788
+ childProjectTrusted,
1742
1789
  );
1743
1790
  replayIdentity = createReplayIdentity(
1744
- ctx.cwd,
1791
+ requestedCwd,
1745
1792
  replayResources.loader,
1746
- projectTrusted,
1793
+ childProjectTrusted,
1747
1794
  );
1748
1795
  } catch {
1749
1796
  // Fingerprinting is an optimization boundary. If resources cannot
@@ -1761,7 +1808,7 @@ export default function workflows(
1761
1808
  ? {
1762
1809
  name: agentType.name,
1763
1810
  body: agentType.body,
1764
- tools: agentType.tools,
1811
+ tools: childTools,
1765
1812
  }
1766
1813
  : undefined,
1767
1814
  model: model ? `${model.provider}/${model.id}` : undefined,
@@ -1880,6 +1927,9 @@ export default function workflows(
1880
1927
  : {}),
1881
1928
  ...(ref ? { ref } : {}),
1882
1929
  ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1930
+ ...(judged.acceptanceWarning
1931
+ ? { acceptanceWarning: judged.acceptanceWarning }
1932
+ : {}),
1883
1933
  };
1884
1934
  }
1885
1935
 
@@ -1915,7 +1965,7 @@ export default function workflows(
1915
1965
  );
1916
1966
  }
1917
1967
  const created = await createWorktree({
1918
- cwd: ctx.cwd,
1968
+ cwd: requestedCwd,
1919
1969
  label,
1920
1970
  id: `${details.runId}-${record.index}`,
1921
1971
  });
@@ -1927,18 +1977,19 @@ export default function workflows(
1927
1977
  worktree = created.worktree;
1928
1978
  if (!runSettled) record.worktreeBranch = worktree.branch;
1929
1979
  }
1930
- if (runSignal.aborted || runSettled) {
1931
- throw runSignal.reason instanceof Error
1932
- ? runSignal.reason
1933
- : new Error("Workflow was aborted");
1934
- }
1935
- const agentCwd = worktree?.path ?? ctx.cwd;
1980
+ const agentCwd = worktree?.path ?? requestedCwd;
1936
1981
 
1937
1982
  // Inside the try, not before it: building resources can throw
1938
1983
  // (bad settings, an unreadable skills dir), and a throw out here
1939
1984
  // would skip the finally and leak the worktree permanently —
1940
1985
  // nothing sweeps `.git/pi-worktrees/` afterwards.
1941
1986
  try {
1987
+ if (runSignal.aborted || runSettled) {
1988
+ throw runSignal.reason instanceof Error
1989
+ ? runSignal.reason
1990
+ : new Error("Workflow was aborted");
1991
+ }
1992
+
1942
1993
  let rejectResourceLoad: (() => void) | undefined;
1943
1994
  const resourceAbort = new Promise<never>((_resolve, reject) => {
1944
1995
  rejectResourceLoad = () =>
@@ -1958,6 +2009,7 @@ export default function workflows(
1958
2009
  effectiveSchema !== undefined,
1959
2010
  agentCwd,
1960
2011
  agentType?.body,
2012
+ childProjectTrusted,
1961
2013
  ),
1962
2014
  resourceAbort,
1963
2015
  ]).finally(() => {
@@ -1982,7 +2034,7 @@ export default function workflows(
1982
2034
  settingsManager: resources.settingsManager,
1983
2035
  ...(sessionManager ? { sessionManager } : {}),
1984
2036
  modelRegistry: ctx.modelRegistry,
1985
- ...(agentType?.tools ? { tools: agentType.tools } : {}),
2037
+ tools: childTools,
1986
2038
  ...(testAgentSessionFactory
1987
2039
  ? { sessionFactory: testAgentSessionFactory }
1988
2040
  : {}),
@@ -2098,9 +2150,9 @@ export default function workflows(
2098
2150
  // unfingerprintable calls always run for real.
2099
2151
  const completedIdentity = callKey
2100
2152
  ? createReplayIdentity(
2101
- ctx.cwd,
2153
+ requestedCwd,
2102
2154
  resources.loader,
2103
- projectTrusted,
2155
+ childProjectTrusted,
2104
2156
  )
2105
2157
  : undefined;
2106
2158
  const completedKey = completedIdentity
@@ -2131,6 +2183,9 @@ export default function workflows(
2131
2183
  : {}),
2132
2184
  ...(ref ? { ref } : {}),
2133
2185
  ...(acceptance ? { acceptance } : {}),
2186
+ ...(judged.acceptanceWarning
2187
+ ? { acceptanceWarning: judged.acceptanceWarning }
2188
+ : {}),
2134
2189
  ...(record.error !== undefined ? { error: record.error } : {}),
2135
2190
  };
2136
2191
  } finally {
@@ -2144,7 +2199,7 @@ export default function workflows(
2144
2199
  runId: details.runId,
2145
2200
  agentIndex: record.index,
2146
2201
  agentLabel: record.label,
2147
- repoCwd: ctx.cwd,
2202
+ repoCwd: requestedCwd,
2148
2203
  worktree,
2149
2204
  });
2150
2205
  let cleanup: WorktreeCleanup;
@@ -2161,7 +2216,7 @@ export default function workflows(
2161
2216
  const reclaimer =
2162
2217
  workflowLifecycleTestHooks?.reclaimWorktree ??
2163
2218
  reclaimWorktree;
2164
- cleanup = await reclaimer(ctx.cwd, worktree).catch(
2219
+ cleanup = await reclaimer(requestedCwd, worktree).catch(
2165
2220
  (error): WorktreeCleanup => ({
2166
2221
  removed: false,
2167
2222
  branchDeleted: false,
@@ -67,6 +67,10 @@ export type WorkflowDeliveryState =
67
67
  export interface WorkflowDelivery {
68
68
  /** Stable per-run idempotency identity, never a transport-batch id. */
69
69
  id: string;
70
+ /** Destination transcript identity; legacy records fall back to run.sessionId. */
71
+ ownerSessionId?: string;
72
+ /** Process-local generation of the Pi SessionManager owner. */
73
+ ownerEpoch?: number;
70
74
  state: WorkflowDeliveryState;
71
75
  attempts: number;
72
76
  updatedAt: number;
@@ -123,7 +127,7 @@ export interface AgentRecord {
123
127
  usage: AgentUsage;
124
128
  /** Replayed from a prior run's journal instead of actually executed. */
125
129
  replayed?: boolean;
126
- /** Explicit caller-supplied acceptance result; never inferred from role/task. */
130
+ /** Deprecated child self-attestation; never runtime-observed evidence. */
127
131
  acceptance?: AcceptanceLedger;
128
132
  /** Branch of the isolated worktree this agent ran in, when it holds commits. */
129
133
  worktreeBranch?: string;
@@ -250,8 +250,11 @@ export class AgentProgressProjection {
250
250
  snapshot(
251
251
  toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
252
252
  ): AgentProgressProjectionSnapshot {
253
+ // Reserve the initial task, then spend the remaining byte budget on the
254
+ // newest evidence. Forward selection would silently discard final errors
255
+ // after enough large tool results, even below the entry-count limit.
253
256
  const selected = this.firstEntry
254
- ? [this.firstEntry, ...this.tailEntries]
257
+ ? [this.firstEntry, ...this.tailEntries.slice().reverse()]
255
258
  : [];
256
259
  const transcript: TranscriptEntry[] = [];
257
260
  let totalBytes = 0;
@@ -277,6 +280,9 @@ export class AgentProgressProjection {
277
280
  : { timestamp: entry.timestamp }),
278
281
  });
279
282
  }
283
+ // Budgeting order is not display order: keep the retained tail chronological.
284
+ const newestFirstTail = transcript.splice(1);
285
+ transcript.push(...newestFirstTail.reverse());
280
286
  if (transcript.length < this.totalEntries) {
281
287
  transcript.push({
282
288
  role: "toolResult",
@@ -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
  }
@@ -1,8 +1,13 @@
1
- import type { WorkflowDetails } from "./model.ts";
1
+ import {
2
+ type CompletionEnvelope,
3
+ type CompletionOwner,
4
+ createCompletionInbox,
5
+ } from "../shared/completion-inbox.ts";
2
6
  import type {
3
7
  DurableResultDeliveryQueue,
4
8
  DurableResultDeliveryReceipt,
5
9
  } from "../shared/result-delivery.ts";
10
+ import type { WorkflowDetails } from "./model.ts";
6
11
 
7
12
  export interface WorkflowCompletionEnvelope {
8
13
  deliveryId: string;
@@ -12,6 +17,7 @@ export interface WorkflowCompletionEnvelope {
12
17
 
13
18
  export interface WorkflowResultDeliveryOptions {
14
19
  isIdle: () => boolean;
20
+ owner?: () => CompletionOwner | undefined;
15
21
  persist: (details: WorkflowDetails) => void;
16
22
  deliver: (
17
23
  envelopes: readonly WorkflowCompletionEnvelope[],
@@ -34,7 +40,9 @@ function errorText(error: unknown) {
34
40
  export function createWorkflowResultDelivery(
35
41
  options: WorkflowResultDeliveryOptions,
36
42
  ) {
37
- const pending = new Map<string, WorkflowCompletionEnvelope>();
43
+ const inbox = createCompletionInbox<WorkflowCompletionEnvelope>();
44
+ const currentOwner =
45
+ options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
38
46
  let flushing: Promise<void> | undefined;
39
47
  let flushRequested = false;
40
48
  let wakeRequested = false;
@@ -57,10 +65,44 @@ 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
+ };
62
101
  };
63
102
 
103
+ const enqueue = (envelope: WorkflowCompletionEnvelope) =>
104
+ inbox.defer(inboxEnvelope(envelope), currentOwner());
105
+
64
106
  const retainPending = (
65
107
  envelope: WorkflowCompletionEnvelope,
66
108
  patch: Partial<NonNullable<WorkflowDetails["delivery"]>>,
@@ -69,7 +111,7 @@ export function createWorkflowResultDelivery(
69
111
  // Memory owns the retry before persistence is attempted. A broken disk
70
112
  // must not make this envelope, or any sibling after it, disappear from the
71
113
  // current process.
72
- enqueue(envelope);
114
+ const admitted = enqueue(envelope);
73
115
  try {
74
116
  persistState(envelope.details, "pending", patch);
75
117
  } catch (error) {
@@ -83,6 +125,7 @@ export function createWorkflowResultDelivery(
83
125
  };
84
126
  }
85
127
  }
128
+ return admitted;
86
129
  };
87
130
 
88
131
  const flush = async (wake: boolean) => {
@@ -91,17 +134,16 @@ export function createWorkflowResultDelivery(
91
134
  wakeRequested ||= wake;
92
135
  return flushing;
93
136
  }
94
- if (pending.size === 0) return;
137
+ if (inbox.size() === 0) return;
95
138
 
96
139
  flushing = (async () => {
97
140
  let passWake = wake;
98
- while (pending.size > 0) {
141
+ while (inbox.size() > 0) {
99
142
  flushRequested = false;
100
143
  wakeRequested = false;
101
- const envelopes = [...pending.values()];
102
- for (const envelope of envelopes) {
103
- pending.delete(envelope.deliveryId);
104
- }
144
+ const claimed = inbox.claim(currentOwner());
145
+ const envelopes = claimed.map((envelope) => envelope.payload);
146
+ if (envelopes.length === 0) break;
105
147
 
106
148
  let receipts: readonly DurableResultDeliveryReceipt[] | undefined;
107
149
  try {
@@ -109,6 +151,7 @@ export function createWorkflowResultDelivery(
109
151
  } catch (error) {
110
152
  const message = errorText(error);
111
153
  for (const envelope of envelopes) {
154
+ inbox.acknowledge([envelope.deliveryId]);
112
155
  retainPending(
113
156
  envelope,
114
157
  {
@@ -127,6 +170,7 @@ export function createWorkflowResultDelivery(
127
170
  for (const envelope of envelopes) {
128
171
  const receipt = byId.get(envelope.deliveryId);
129
172
  if (receipt?.delivered) {
173
+ inbox.acknowledge([envelope.deliveryId]);
130
174
  try {
131
175
  persistState(envelope.details, "delivered", {
132
176
  attempts: (envelope.details.delivery?.attempts ?? 0) + 1,
@@ -150,6 +194,7 @@ export function createWorkflowResultDelivery(
150
194
  }
151
195
  continue;
152
196
  }
197
+ inbox.acknowledge([envelope.deliveryId]);
153
198
  retainPending(
154
199
  envelope,
155
200
  {
@@ -183,7 +228,7 @@ export function createWorkflowResultDelivery(
183
228
 
184
229
  /** Terminal won the wait/abort arbitration and will be returned inline. */
185
230
  consumeInline(details: WorkflowDetails) {
186
- if (details.delivery) pending.delete(details.delivery.id);
231
+ if (details.delivery) inbox.consumeDeliveryIds([details.delivery.id]);
187
232
  persistState(details, "consumed-inline", {
188
233
  deliveredAt: Date.now(),
189
234
  lastError: undefined,
@@ -214,18 +259,46 @@ export function createWorkflowResultDelivery(
214
259
  restore(envelope: WorkflowCompletionEnvelope) {
215
260
  const state = envelope.details.delivery?.state;
216
261
  if (state !== "pending" && state !== "held-for-inline") return false;
217
- // A process restart cannot still own the inline waiter. Deterministically
218
- // reconstruct pending delivery from the terminal artifact.
219
- if (state === "held-for-inline") {
220
- retainPending(
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(
221
287
  envelope,
222
- { lastError: "Inline waiter was not active after session restart" },
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
+ },
223
296
  "Restored delivery state persistence failed",
224
297
  );
225
- } else {
226
- enqueue(envelope);
227
298
  }
228
- 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);
229
302
  },
230
303
 
231
304
  retryPending() {
@@ -242,12 +315,13 @@ export function createWorkflowResultDelivery(
242
315
  },
243
316
 
244
317
  size() {
245
- return pending.size;
318
+ return inbox.size();
246
319
  },
247
320
 
248
321
  clear() {
249
- pending.clear();
322
+ inbox.clear();
250
323
  },
324
+ inspectDeadLetters: inbox.inspectDeadLetters,
251
325
  };
252
326
  return queue satisfies DurableResultDeliveryQueue<WorkflowCompletionEnvelope>;
253
327
  }
@@ -237,6 +237,12 @@ function makeProjection(
237
237
  ? {
238
238
  delivery: {
239
239
  id: details.delivery.id,
240
+ ...(details.delivery.ownerSessionId
241
+ ? { ownerSessionId: details.delivery.ownerSessionId }
242
+ : {}),
243
+ ...(details.delivery.ownerEpoch !== undefined
244
+ ? { ownerEpoch: details.delivery.ownerEpoch }
245
+ : {}),
240
246
  state: details.delivery.state,
241
247
  attempts: details.delivery.attempts,
242
248
  updatedAt: details.delivery.updatedAt,