@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.
- package/README.md +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- 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
|
|
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
|
|
27
|
+
/** Describes stopping a running workflow, mirroring subagent_cancel/bg_kill. */
|
|
28
28
|
export const WORKFLOW_STOP_TOOL_DESCRIPTION =
|
|
29
|
-
"Cancel a running
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
430
|
+
return (gitCwd: string, args: readonly string[]) =>
|
|
429
431
|
boundedGit(gitCwd, args, deadline);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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 =
|
|
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
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
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)
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
203
|
+
/** Queue a detached run with in-memory retry ownership before persistence. */
|
|
151
204
|
defer(envelope: WorkflowCompletionEnvelope) {
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
}
|