@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
|
@@ -22,6 +22,25 @@ export interface AcceptanceLedger {
|
|
|
22
22
|
readonly status: "accepted" | "rejected" | "missing" | "malformed";
|
|
23
23
|
readonly criteria: readonly AcceptanceCriterionResult[];
|
|
24
24
|
readonly errors: readonly string[];
|
|
25
|
+
/** Child-authored judgment retained only for migration; never a runtime fact. */
|
|
26
|
+
readonly authority?: "model-self-attestation";
|
|
27
|
+
readonly deprecated?: {
|
|
28
|
+
readonly since: "0.5";
|
|
29
|
+
readonly removal: "1.0";
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ACCEPTANCE_DEPRECATION_WARNING =
|
|
34
|
+
"acceptance is deprecated since OpenPI 0.5 and will be removed in 1.0; it is model self-attestation, not runtime-verified evidence, and does not determine ok";
|
|
35
|
+
|
|
36
|
+
function ledger(
|
|
37
|
+
value: Omit<AcceptanceLedger, "authority" | "deprecated">,
|
|
38
|
+
): AcceptanceLedger {
|
|
39
|
+
return {
|
|
40
|
+
...value,
|
|
41
|
+
authority: "model-self-attestation",
|
|
42
|
+
deprecated: { since: "0.5", removal: "1.0" },
|
|
43
|
+
};
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -169,6 +188,12 @@ export function isAcceptanceLedger(value: unknown): value is AcceptanceLedger {
|
|
|
169
188
|
value.status === "rejected" ||
|
|
170
189
|
value.status === "missing" ||
|
|
171
190
|
value.status === "malformed") &&
|
|
191
|
+
(value.authority === undefined ||
|
|
192
|
+
value.authority === "model-self-attestation") &&
|
|
193
|
+
(value.deprecated === undefined ||
|
|
194
|
+
(record(value.deprecated) &&
|
|
195
|
+
value.deprecated.since === "0.5" &&
|
|
196
|
+
value.deprecated.removal === "1.0")) &&
|
|
172
197
|
value.errors.every((error) => typeof error === "string") &&
|
|
173
198
|
value.criteria.every(
|
|
174
199
|
(criterion) =>
|
|
@@ -187,7 +212,7 @@ export function acceptanceInstruction(contract: AcceptanceContract) {
|
|
|
187
212
|
`- ${criterion.id}: ${criterion.description}${criterion.requiredEvidence?.length ? `; required evidence labels: ${criterion.requiredEvidence.join(", ")}` : ""}`,
|
|
188
213
|
);
|
|
189
214
|
return [
|
|
190
|
-
"
|
|
215
|
+
"Deprecated compatibility protocol: this acceptance ledger is your own model self-attestation, not runtime-verified evidence, and it does not determine execution success.",
|
|
191
216
|
"Include an `acceptance.criteria` array in structured_output with exactly these ids. Mark rejected when the criterion is not demonstrated. Evidence entries must be concise labels or concrete references; do not invent evidence.",
|
|
192
217
|
...criteria,
|
|
193
218
|
].join("\n");
|
|
@@ -198,19 +223,19 @@ export function evaluateAcceptance(
|
|
|
198
223
|
structured: unknown,
|
|
199
224
|
): AcceptanceLedger {
|
|
200
225
|
if (!record(structured) || !record(structured.acceptance)) {
|
|
201
|
-
return {
|
|
226
|
+
return ledger({
|
|
202
227
|
status: "missing",
|
|
203
228
|
criteria: [],
|
|
204
229
|
errors: ["structured result omitted acceptance"],
|
|
205
|
-
};
|
|
230
|
+
});
|
|
206
231
|
}
|
|
207
232
|
const rawCriteria = structured.acceptance.criteria;
|
|
208
233
|
if (!Array.isArray(rawCriteria)) {
|
|
209
|
-
return {
|
|
234
|
+
return ledger({
|
|
210
235
|
status: "malformed",
|
|
211
236
|
criteria: [],
|
|
212
237
|
errors: ["acceptance.criteria is not an array"],
|
|
213
|
-
};
|
|
238
|
+
});
|
|
214
239
|
}
|
|
215
240
|
const errors: string[] = [];
|
|
216
241
|
const byId = new Map<string, AcceptanceCriterionResult>();
|
|
@@ -265,14 +290,15 @@ export function evaluateAcceptance(
|
|
|
265
290
|
errors.push(`unexpected acceptance criterion "${id}"`);
|
|
266
291
|
}
|
|
267
292
|
}
|
|
268
|
-
if (errors.length)
|
|
269
|
-
|
|
293
|
+
if (errors.length)
|
|
294
|
+
return ledger({ status: "malformed", criteria: results, errors });
|
|
295
|
+
return ledger({
|
|
270
296
|
status: results.every((result) => result.status === "accepted")
|
|
271
297
|
? "accepted"
|
|
272
298
|
: "rejected",
|
|
273
299
|
criteria: results,
|
|
274
300
|
errors: [],
|
|
275
|
-
};
|
|
301
|
+
});
|
|
276
302
|
}
|
|
277
303
|
|
|
278
304
|
export function applyAcceptance(options: {
|
|
@@ -284,15 +310,13 @@ export function applyAcceptance(options: {
|
|
|
284
310
|
const ledger = options.contract
|
|
285
311
|
? evaluateAcceptance(options.contract, options.structured)
|
|
286
312
|
: undefined;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
:
|
|
295
|
-
|
|
296
|
-
: (acceptanceError ?? "Agent failed");
|
|
297
|
-
return { ok, ...(ledger ? { ledger } : {}), ...(error ? { error } : {}) };
|
|
313
|
+
const ok = options.agentOk;
|
|
314
|
+
const error = ok ? undefined : (options.agentError ?? "Agent failed");
|
|
315
|
+
return {
|
|
316
|
+
ok,
|
|
317
|
+
...(ledger
|
|
318
|
+
? { ledger, acceptanceWarning: ACCEPTANCE_DEPRECATION_WARNING }
|
|
319
|
+
: {}),
|
|
320
|
+
...(error ? { error } : {}),
|
|
321
|
+
};
|
|
298
322
|
}
|
|
@@ -1,32 +1,40 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
type TranscriptEntry,
|
|
4
|
-
type WorkflowDetails,
|
|
5
|
-
} from "./model.ts";
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
6
3
|
import {
|
|
7
4
|
boundedJournal,
|
|
8
|
-
|
|
5
|
+
JOURNAL_MAX_BYTES,
|
|
9
6
|
type JournalEntry,
|
|
7
|
+
parseJournal,
|
|
10
8
|
type WorkflowJournal,
|
|
9
|
+
type WorkflowJournalAccumulator,
|
|
11
10
|
} from "./journal.ts";
|
|
12
11
|
import {
|
|
12
|
+
refreshWorkflowGraph,
|
|
13
|
+
type TranscriptEntry,
|
|
14
|
+
type WorkflowDelivery,
|
|
15
|
+
type WorkflowDetails,
|
|
16
|
+
} from "./model.ts";
|
|
17
|
+
import {
|
|
18
|
+
encodeCompleteJson,
|
|
13
19
|
safeStringify,
|
|
14
|
-
toSerializable,
|
|
15
20
|
truncateUtf8,
|
|
16
21
|
writeFileAtomic,
|
|
17
22
|
} from "./serialization.ts";
|
|
18
|
-
import * as fs from "node:fs";
|
|
19
|
-
import * as path from "node:path";
|
|
20
23
|
|
|
21
24
|
export const JOURNAL_FILE = "journal.json";
|
|
22
25
|
|
|
23
26
|
const ARTIFACT_TRANSCRIPT_MAX_BYTES = 32 * 1024;
|
|
24
27
|
const ARTIFACT_TRANSCRIPT_ENTRY_MAX_BYTES = 8 * 1024;
|
|
28
|
+
const AGENT_RESULT_ARTIFACT_MAX_BYTES = 2 * 1024 * 1024;
|
|
25
29
|
export const WORKFLOW_CHECKPOINT_INTERVAL_MS = 500;
|
|
26
30
|
const ENTRY_TRUNCATION_MARKER = "\n[entry truncated]";
|
|
27
31
|
const TRANSCRIPT_TRUNCATION_MARKER =
|
|
28
32
|
"[artifact transcript truncated: older entries omitted]";
|
|
29
33
|
|
|
34
|
+
type WorkflowJournalSource =
|
|
35
|
+
| readonly JournalEntry[]
|
|
36
|
+
| WorkflowJournalAccumulator;
|
|
37
|
+
|
|
30
38
|
function textBytes(text: string) {
|
|
31
39
|
return Buffer.byteLength(text, "utf8");
|
|
32
40
|
}
|
|
@@ -101,44 +109,61 @@ function writeRunFile(runDir: string, name: string, content: string) {
|
|
|
101
109
|
writeFileAtomic(path.join(runDir, name), content);
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
export function persistWorkflowTerminalState(
|
|
113
|
+
runDir: string,
|
|
114
|
+
details: WorkflowDetails,
|
|
115
|
+
) {
|
|
116
|
+
const terminalManifest: WorkflowDetails = {
|
|
117
|
+
...details,
|
|
118
|
+
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
|
|
119
|
+
};
|
|
120
|
+
delete terminalManifest.result;
|
|
121
|
+
delete terminalManifest.resultArtifact;
|
|
122
|
+
delete terminalManifest.transcriptArtifact;
|
|
123
|
+
writeRunFile(
|
|
124
|
+
runDir,
|
|
125
|
+
"workflow.json",
|
|
126
|
+
safeStringify(terminalManifest, { maxBytes: 1024 * 1024 }),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
104
130
|
/** Persist one successful child result before any handoff/context projection. */
|
|
105
131
|
export function persistWorkflowAgentResult(
|
|
106
132
|
runDir: string,
|
|
107
133
|
index: number,
|
|
108
134
|
result: { output: string; structured?: unknown },
|
|
109
135
|
) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
maxNodes: 100_000,
|
|
128
|
-
maxStringBytes: 2 * 1024 * 1024,
|
|
129
|
-
},
|
|
130
|
-
),
|
|
131
|
-
null,
|
|
132
|
-
2,
|
|
133
|
-
),
|
|
136
|
+
// Artifact references are persisted as portable workflow paths. The host
|
|
137
|
+
// filesystem join happens in writeRunFile; handoff validation and replay
|
|
138
|
+
// consumers intentionally use `/` regardless of the platform.
|
|
139
|
+
const artifact = `agent-results/agent-${String(index).padStart(4, "0")}.json`;
|
|
140
|
+
const encoded = encodeCompleteJson(
|
|
141
|
+
{
|
|
142
|
+
output: result.output,
|
|
143
|
+
...(result.structured !== undefined
|
|
144
|
+
? { structured: result.structured }
|
|
145
|
+
: {}),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
maxBytes: AGENT_RESULT_ARTIFACT_MAX_BYTES,
|
|
149
|
+
maxDepth: 32,
|
|
150
|
+
maxNodes: 100_000,
|
|
151
|
+
maxStringBytes: AGENT_RESULT_ARTIFACT_MAX_BYTES,
|
|
152
|
+
},
|
|
134
153
|
);
|
|
154
|
+
if (!encoded.ok) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Agent result artifact exceeded the ${AGENT_RESULT_ARTIFACT_MAX_BYTES}-byte budget (${encoded.limit} limit at ${encoded.path})`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
writeRunFile(runDir, artifact, encoded.json);
|
|
135
160
|
return artifact;
|
|
136
161
|
}
|
|
137
162
|
|
|
138
163
|
export function persistWorkflowJson(
|
|
139
164
|
runDir: string,
|
|
140
165
|
details: WorkflowDetails,
|
|
141
|
-
journal?:
|
|
166
|
+
journal?: WorkflowJournalSource,
|
|
142
167
|
) {
|
|
143
168
|
refreshWorkflowGraph(details);
|
|
144
169
|
const transcripts = Object.fromEntries(
|
|
@@ -147,6 +172,15 @@ export function persistWorkflowJson(
|
|
|
147
172
|
boundedArtifactTranscript(agent.transcript),
|
|
148
173
|
]),
|
|
149
174
|
);
|
|
175
|
+
|
|
176
|
+
// Publish terminal execution facts before dependent side artifacts. If a
|
|
177
|
+
// later artifact write fails, readers still see an explained terminal run
|
|
178
|
+
// instead of the previous `running` manifest. The final manifest below adds
|
|
179
|
+
// the artifact references once every dependent file has committed.
|
|
180
|
+
if (details.status !== "running") {
|
|
181
|
+
persistWorkflowTerminalState(runDir, details);
|
|
182
|
+
}
|
|
183
|
+
|
|
150
184
|
writeRunFile(
|
|
151
185
|
runDir,
|
|
152
186
|
"transcripts.json",
|
|
@@ -154,15 +188,20 @@ export function persistWorkflowJson(
|
|
|
154
188
|
);
|
|
155
189
|
// Written alongside the rest so it inherits atomic write, 500ms coalescing,
|
|
156
190
|
// and the final flush. Only present once a call has actually succeeded.
|
|
157
|
-
//
|
|
158
|
-
// JSON
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
// Accumulators already enforce the cap incrementally and can assemble the
|
|
192
|
+
// canonical JSON from their cached entry fragments. Plain arrays retain the
|
|
193
|
+
// compatibility path through boundedJournal; plain JSON.stringify is
|
|
194
|
+
// deliberate because safeStringify would swap an over-cap value for a
|
|
195
|
+
// preview stub, silently turning the journal into something unusable.
|
|
196
|
+
if (
|
|
197
|
+
journal &&
|
|
198
|
+
(journal.length > 0 || ("toJson" in journal && journal.dropped > 0))
|
|
199
|
+
) {
|
|
200
|
+
const content =
|
|
201
|
+
"toJson" in journal
|
|
202
|
+
? journal.toJson()
|
|
203
|
+
: JSON.stringify(boundedJournal(journal).journal, null, 2);
|
|
204
|
+
writeRunFile(runDir, JOURNAL_FILE, content);
|
|
166
205
|
}
|
|
167
206
|
if (details.result !== undefined) {
|
|
168
207
|
writeRunFile(
|
|
@@ -186,6 +225,25 @@ export function persistWorkflowJson(
|
|
|
186
225
|
);
|
|
187
226
|
}
|
|
188
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Update only the durable delivery receipt. Completion delivery may retain a
|
|
230
|
+
* compact memory projection after the run has been evicted, so rewriting the
|
|
231
|
+
* whole details object here would risk replacing exact result artifacts with
|
|
232
|
+
* that projection.
|
|
233
|
+
*/
|
|
234
|
+
export function persistWorkflowDeliveryState(
|
|
235
|
+
runDir: string,
|
|
236
|
+
delivery: WorkflowDelivery,
|
|
237
|
+
) {
|
|
238
|
+
const file = path.join(runDir, "workflow.json");
|
|
239
|
+
const raw: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
240
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
241
|
+
throw new Error(`Invalid persisted workflow details: ${file}`);
|
|
242
|
+
}
|
|
243
|
+
const next = { ...(raw as Record<string, unknown>), delivery };
|
|
244
|
+
writeFileAtomic(file, JSON.stringify(next));
|
|
245
|
+
}
|
|
246
|
+
|
|
189
247
|
/** Coalesce live checkpoints while keeping final persistence synchronous. */
|
|
190
248
|
export function createWorkflowPersistence(
|
|
191
249
|
runDir: string,
|
|
@@ -195,10 +253,10 @@ export function createWorkflowPersistence(
|
|
|
195
253
|
persist?: (
|
|
196
254
|
runDir: string,
|
|
197
255
|
details: WorkflowDetails,
|
|
198
|
-
journal?:
|
|
256
|
+
journal?: WorkflowJournalSource,
|
|
199
257
|
) => void;
|
|
200
|
-
/** Read at write time so callers only have to append to their
|
|
201
|
-
journal?: () =>
|
|
258
|
+
/** Read at write time so callers only have to append to their journal. */
|
|
259
|
+
journal?: () => WorkflowJournalSource;
|
|
202
260
|
} = {},
|
|
203
261
|
) {
|
|
204
262
|
const intervalMs = Math.max(
|
|
@@ -256,10 +314,42 @@ export function createWorkflowPersistence(
|
|
|
256
314
|
* turn into a new way for a run to fail.
|
|
257
315
|
*/
|
|
258
316
|
export function loadJournal(runDir: string): WorkflowJournal | undefined {
|
|
317
|
+
let descriptor: number | undefined;
|
|
259
318
|
try {
|
|
260
|
-
|
|
319
|
+
descriptor = fs.openSync(path.join(runDir, JOURNAL_FILE), "r");
|
|
320
|
+
const initialSize = fs.fstatSync(descriptor).size;
|
|
321
|
+
if (initialSize > JOURNAL_MAX_BYTES) return undefined;
|
|
322
|
+
|
|
323
|
+
// Read at most one byte beyond the cap. The descriptor pins the inspected
|
|
324
|
+
// file, while the extra byte catches growth after fstat without ever
|
|
325
|
+
// allocating or parsing an unbounded journal.
|
|
326
|
+
const bytes = Buffer.allocUnsafe(initialSize + 1);
|
|
327
|
+
let length = 0;
|
|
328
|
+
while (length < bytes.length) {
|
|
329
|
+
const read = fs.readSync(
|
|
330
|
+
descriptor,
|
|
331
|
+
bytes,
|
|
332
|
+
length,
|
|
333
|
+
bytes.length - length,
|
|
334
|
+
null,
|
|
335
|
+
);
|
|
336
|
+
if (read === 0) break;
|
|
337
|
+
length += read;
|
|
338
|
+
}
|
|
339
|
+
// Filling the extra byte means the file changed after inspection. Reject
|
|
340
|
+
// that race even when the new size would still fit the ordinary cap.
|
|
341
|
+
if (length === bytes.length) return undefined;
|
|
342
|
+
const raw = bytes.toString("utf8", 0, length);
|
|
261
343
|
return parseJournal(JSON.parse(raw));
|
|
262
344
|
} catch {
|
|
263
345
|
return undefined;
|
|
346
|
+
} finally {
|
|
347
|
+
if (descriptor !== undefined) {
|
|
348
|
+
try {
|
|
349
|
+
fs.closeSync(descriptor);
|
|
350
|
+
} catch {
|
|
351
|
+
// Loading a replay cache is optional and always fails open to a miss.
|
|
352
|
+
}
|
|
353
|
+
}
|
|
264
354
|
}
|
|
265
355
|
}
|