@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.
- package/README.md +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -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 +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- 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/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -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 +82 -6
- 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 +138 -130
- 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 +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- 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 +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- 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 +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- 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 +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -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 = {
|
|
@@ -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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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,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
|
|
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
|
|
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
|
|
61
|
-
|
|
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)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
248
|
+
/** Queue a detached run with in-memory retry ownership before persistence. */
|
|
151
249
|
defer(envelope: WorkflowCompletionEnvelope) {
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
318
|
+
return inbox.size();
|
|
183
319
|
},
|
|
184
320
|
|
|
185
321
|
clear() {
|
|
186
|
-
|
|
322
|
+
inbox.clear();
|
|
187
323
|
},
|
|
324
|
+
inspectDeadLetters: inbox.inspectDeadLetters,
|
|
188
325
|
};
|
|
326
|
+
return queue satisfies DurableResultDeliveryQueue<WorkflowCompletionEnvelope>;
|
|
189
327
|
}
|