@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.
- package/README.md +30 -20
- package/SETUP.md +10 -4
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +65 -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 +105 -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 +1271 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
- package/extensions/ai-providers/cursor/provider.ts +1431 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -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 +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +39 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +64 -7
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +130 -48
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +7 -71
- package/extensions/subagents/src/prompt.ts +19 -5
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +147 -21
- package/extensions/workflows/index.ts +75 -20
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +11 -233
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +9 -9
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +5 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- 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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1786
|
+
requestedCwd,
|
|
1741
1787
|
agentType?.body,
|
|
1788
|
+
childProjectTrusted,
|
|
1742
1789
|
);
|
|
1743
1790
|
replayIdentity = createReplayIdentity(
|
|
1744
|
-
|
|
1791
|
+
requestedCwd,
|
|
1745
1792
|
replayResources.loader,
|
|
1746
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2153
|
+
requestedCwd,
|
|
2102
2154
|
resources.loader,
|
|
2103
|
-
|
|
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:
|
|
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(
|
|
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
|
-
/**
|
|
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,
|
|
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
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import
|
|
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
|
|
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
|
|
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
|
+
};
|
|
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 (
|
|
137
|
+
if (inbox.size() === 0) return;
|
|
95
138
|
|
|
96
139
|
flushing = (async () => {
|
|
97
140
|
let passWake = wake;
|
|
98
|
-
while (
|
|
141
|
+
while (inbox.size() > 0) {
|
|
99
142
|
flushRequested = false;
|
|
100
143
|
wakeRequested = false;
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
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)
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
318
|
+
return inbox.size();
|
|
246
319
|
},
|
|
247
320
|
|
|
248
321
|
clear() {
|
|
249
|
-
|
|
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,
|