@tt-a1i/openpi 0.6.0 → 0.7.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 +13 -11
- package/SETUP.md +2 -2
- package/THIRD_PARTY_NOTICES.md +242 -0
- package/extensions/ai-providers/README.md +12 -6
- package/extensions/ai-providers/cursor/connect-frame-reader.ts +76 -0
- package/extensions/ai-providers/cursor/input-images.ts +2 -3
- package/extensions/ai-providers/cursor/proto.ts +218 -11
- package/extensions/ai-providers/cursor/protobuf.ts +12 -2
- package/extensions/ai-providers/cursor/provider.ts +280 -37
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/index.ts +3 -3
- package/extensions/file-mutation-display/index.ts +13 -9
- package/extensions/shared/agent-transcript.ts +3 -2
- package/extensions/shared/child-session.ts +14 -0
- package/extensions/subagents/index.ts +20 -3
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +70 -59
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/manager.ts +2 -71
- package/extensions/subagents/src/prompt.ts +2 -2
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/web/index.ts +69 -5
- package/extensions/workflows/artifacts.ts +362 -23
- package/extensions/workflows/dashboard.ts +141 -21
- package/extensions/workflows/index.ts +62 -20
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/runner.ts +5 -162
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +28 -4
- package/skills/subagents/REFERENCE.md +6 -7
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +3 -1
- package/web/dist/app.js +87 -0
- package/web/dist/favicon.svg +9 -0
- package/web/dist/index.html +15 -0
- package/web/dist/styles.css +3 -0
- package/web/host/web-host.ts +6 -9
- package/web/ui/index.html +3 -131
- package/web/ui/public/favicon.svg +9 -0
- package/web/ui/src/app/App.tsx +134 -0
- package/web/ui/src/app/providers.tsx +38 -0
- package/web/ui/src/components/Markdown.tsx +58 -0
- package/web/ui/src/components/OpenPiLogo.tsx +41 -0
- package/web/ui/src/features/activity/ActivityBar.tsx +120 -0
- package/web/ui/src/features/composer/Composer.tsx +237 -0
- package/web/ui/src/features/sessions/SessionSidebar.tsx +418 -0
- package/web/ui/src/features/transcript/Transcript.tsx +860 -0
- package/web/ui/src/i18n.ts +159 -0
- package/web/ui/src/lib/format.ts +57 -0
- package/web/ui/src/main.tsx +16 -0
- package/web/ui/src/protocol/client.ts +199 -0
- package/web/ui/src/protocol/event-stream.ts +88 -0
- package/web/ui/src/store/web-store.ts +926 -0
- package/web/ui/src/styles.css +420 -0
- package/web/ui/tsconfig.json +12 -0
- package/web/ui/vite-env.d.ts +1 -0
- package/web/vite.config.mjs +21 -1
- package/web/host/static-assets.ts +0 -4
- package/web/ui/app.js +0 -1700
- package/web/ui/styles.css +0 -680
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import {
|
|
@@ -22,10 +23,14 @@ import {
|
|
|
22
23
|
} from "./serialization.ts";
|
|
23
24
|
|
|
24
25
|
export const JOURNAL_FILE = "journal.json";
|
|
26
|
+
export const WORKFLOW_COMMIT_FILE = ".workflow-commit.json";
|
|
25
27
|
|
|
26
28
|
const ARTIFACT_TRANSCRIPT_MAX_BYTES = 32 * 1024;
|
|
27
29
|
const ARTIFACT_TRANSCRIPT_ENTRY_MAX_BYTES = 8 * 1024;
|
|
28
30
|
const AGENT_RESULT_ARTIFACT_MAX_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
const WORKFLOW_MANIFEST_MAX_BYTES = 1024 * 1024;
|
|
32
|
+
const WORKFLOW_TRANSCRIPTS_MAX_BYTES = 2 * 1024 * 1024;
|
|
33
|
+
const WORKFLOW_COMMIT_MAX_BYTES = 3 * 1024 * 1024;
|
|
29
34
|
export const WORKFLOW_CHECKPOINT_INTERVAL_MS = 500;
|
|
30
35
|
const ENTRY_TRUNCATION_MARKER = "\n[entry truncated]";
|
|
31
36
|
const TRANSCRIPT_TRUNCATION_MARKER =
|
|
@@ -35,10 +40,317 @@ type WorkflowJournalSource =
|
|
|
35
40
|
| readonly JournalEntry[]
|
|
36
41
|
| WorkflowJournalAccumulator;
|
|
37
42
|
|
|
43
|
+
interface WorkflowArtifactWrite {
|
|
44
|
+
name: typeof JOURNAL_FILE | "result.json" | "transcripts.json";
|
|
45
|
+
content: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface WorkflowCommitArtifact {
|
|
49
|
+
name: WorkflowArtifactWrite["name"];
|
|
50
|
+
bytes: number;
|
|
51
|
+
sha256: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface WorkflowCommitMarker {
|
|
55
|
+
version: 1;
|
|
56
|
+
runId: string;
|
|
57
|
+
manifest: string;
|
|
58
|
+
predecessorSha256: string;
|
|
59
|
+
artifacts: WorkflowCommitArtifact[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type WorkflowCommitRecovery =
|
|
63
|
+
| "none"
|
|
64
|
+
| "recovered"
|
|
65
|
+
| "already-committed"
|
|
66
|
+
| "incomplete"
|
|
67
|
+
| "invalid"
|
|
68
|
+
| "failed";
|
|
69
|
+
|
|
70
|
+
const artifactLimits = new Map<WorkflowArtifactWrite["name"], number>([
|
|
71
|
+
["transcripts.json", WORKFLOW_TRANSCRIPTS_MAX_BYTES],
|
|
72
|
+
["result.json", WORKFLOW_MANIFEST_MAX_BYTES],
|
|
73
|
+
[JOURNAL_FILE, JOURNAL_MAX_BYTES],
|
|
74
|
+
]);
|
|
75
|
+
|
|
38
76
|
function textBytes(text: string) {
|
|
39
77
|
return Buffer.byteLength(text, "utf8");
|
|
40
78
|
}
|
|
41
79
|
|
|
80
|
+
function sha256(content: string | Buffer) {
|
|
81
|
+
return createHash("sha256").update(content).digest("hex");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeWorkflowCommit(runDir: string, strict = false) {
|
|
85
|
+
try {
|
|
86
|
+
fs.unlinkSync(path.join(runDir, WORKFLOW_COMMIT_FILE));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
89
|
+
if (strict) throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function workflowCommitMarker(
|
|
94
|
+
details: WorkflowDetails,
|
|
95
|
+
manifest: string,
|
|
96
|
+
artifacts: WorkflowArtifactWrite[],
|
|
97
|
+
predecessorSha256: string,
|
|
98
|
+
): WorkflowCommitMarker {
|
|
99
|
+
for (const { name, content } of artifacts) {
|
|
100
|
+
const bytes = textBytes(content);
|
|
101
|
+
const limit = artifactLimits.get(name);
|
|
102
|
+
if (limit === undefined || bytes > limit) {
|
|
103
|
+
throw new Error(`Workflow artifact ${name} exceeded its commit budget`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
version: 1,
|
|
108
|
+
runId: details.runId,
|
|
109
|
+
manifest,
|
|
110
|
+
predecessorSha256,
|
|
111
|
+
artifacts: artifacts.map(({ name, content }) => ({
|
|
112
|
+
name,
|
|
113
|
+
bytes: textBytes(content),
|
|
114
|
+
sha256: sha256(content),
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function serializeWorkflowCommitMarker(
|
|
120
|
+
details: WorkflowDetails,
|
|
121
|
+
manifest: string,
|
|
122
|
+
artifacts: WorkflowArtifactWrite[],
|
|
123
|
+
predecessorSha256: string,
|
|
124
|
+
) {
|
|
125
|
+
const content = JSON.stringify(
|
|
126
|
+
workflowCommitMarker(details, manifest, artifacts, predecessorSha256),
|
|
127
|
+
);
|
|
128
|
+
if (textBytes(content) > WORKFLOW_COMMIT_MAX_BYTES) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"Workflow artifact commit receipt exceeded its byte budget",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return content;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseWorkflowCommitMarker(
|
|
137
|
+
runDir: string,
|
|
138
|
+
): WorkflowCommitMarker | "none" | "invalid" {
|
|
139
|
+
const markerPath = path.join(runDir, WORKFLOW_COMMIT_FILE);
|
|
140
|
+
let stat: fs.Stats;
|
|
141
|
+
try {
|
|
142
|
+
stat = fs.lstatSync(markerPath);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
145
|
+
? "none"
|
|
146
|
+
: "invalid";
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
!stat.isFile() ||
|
|
150
|
+
stat.isSymbolicLink() ||
|
|
151
|
+
stat.size <= 0 ||
|
|
152
|
+
stat.size > WORKFLOW_COMMIT_MAX_BYTES
|
|
153
|
+
) {
|
|
154
|
+
return "invalid";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let raw: unknown;
|
|
158
|
+
try {
|
|
159
|
+
raw = JSON.parse(fs.readFileSync(markerPath, "utf8"));
|
|
160
|
+
} catch {
|
|
161
|
+
return "invalid";
|
|
162
|
+
}
|
|
163
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "invalid";
|
|
164
|
+
const record = raw as Record<string, unknown>;
|
|
165
|
+
if (
|
|
166
|
+
record.version !== 1 ||
|
|
167
|
+
typeof record.runId !== "string" ||
|
|
168
|
+
record.runId !== path.basename(runDir) ||
|
|
169
|
+
typeof record.predecessorSha256 !== "string" ||
|
|
170
|
+
!/^[0-9a-f]{64}$/u.test(record.predecessorSha256) ||
|
|
171
|
+
typeof record.manifest !== "string" ||
|
|
172
|
+
textBytes(record.manifest) > WORKFLOW_MANIFEST_MAX_BYTES ||
|
|
173
|
+
!Array.isArray(record.artifacts) ||
|
|
174
|
+
record.artifacts.length < 1 ||
|
|
175
|
+
record.artifacts.length > artifactLimits.size
|
|
176
|
+
) {
|
|
177
|
+
return "invalid";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let manifest: Record<string, unknown>;
|
|
181
|
+
try {
|
|
182
|
+
const parsed: unknown = JSON.parse(record.manifest);
|
|
183
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
184
|
+
return "invalid";
|
|
185
|
+
}
|
|
186
|
+
manifest = parsed as Record<string, unknown>;
|
|
187
|
+
} catch {
|
|
188
|
+
return "invalid";
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
manifest.runId !== record.runId ||
|
|
192
|
+
!["completed", "failed", "aborted", "uncertain"].includes(
|
|
193
|
+
String(manifest.status),
|
|
194
|
+
) ||
|
|
195
|
+
manifest.transcriptArtifact !== "transcripts.json" ||
|
|
196
|
+
(manifest.resultArtifact !== undefined &&
|
|
197
|
+
manifest.resultArtifact !== "result.json")
|
|
198
|
+
) {
|
|
199
|
+
return "invalid";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const artifacts: WorkflowCommitArtifact[] = [];
|
|
203
|
+
const names = new Set<string>();
|
|
204
|
+
for (const value of record.artifacts) {
|
|
205
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
206
|
+
return "invalid";
|
|
207
|
+
}
|
|
208
|
+
const artifact = value as Record<string, unknown>;
|
|
209
|
+
if (
|
|
210
|
+
typeof artifact.name !== "string" ||
|
|
211
|
+
!artifactLimits.has(artifact.name as WorkflowArtifactWrite["name"]) ||
|
|
212
|
+
names.has(artifact.name) ||
|
|
213
|
+
typeof artifact.bytes !== "number" ||
|
|
214
|
+
!Number.isInteger(artifact.bytes) ||
|
|
215
|
+
artifact.bytes < 0 ||
|
|
216
|
+
artifact.bytes >
|
|
217
|
+
(artifactLimits.get(artifact.name as WorkflowArtifactWrite["name"]) ??
|
|
218
|
+
-1) ||
|
|
219
|
+
typeof artifact.sha256 !== "string" ||
|
|
220
|
+
!/^[0-9a-f]{64}$/u.test(artifact.sha256)
|
|
221
|
+
) {
|
|
222
|
+
return "invalid";
|
|
223
|
+
}
|
|
224
|
+
names.add(artifact.name);
|
|
225
|
+
artifacts.push({
|
|
226
|
+
name: artifact.name as WorkflowArtifactWrite["name"],
|
|
227
|
+
bytes: artifact.bytes,
|
|
228
|
+
sha256: artifact.sha256,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
!names.has("transcripts.json") ||
|
|
233
|
+
(manifest.resultArtifact === "result.json") !== names.has("result.json")
|
|
234
|
+
) {
|
|
235
|
+
return "invalid";
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
version: 1,
|
|
239
|
+
runId: record.runId,
|
|
240
|
+
manifest: record.manifest,
|
|
241
|
+
predecessorSha256: record.predecessorSha256,
|
|
242
|
+
artifacts,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hasCommittedManifest(
|
|
247
|
+
manifestPath: string,
|
|
248
|
+
marker: WorkflowCommitMarker,
|
|
249
|
+
) {
|
|
250
|
+
try {
|
|
251
|
+
const stat = fs.lstatSync(manifestPath);
|
|
252
|
+
if (
|
|
253
|
+
!stat.isFile() ||
|
|
254
|
+
stat.isSymbolicLink() ||
|
|
255
|
+
stat.size > WORKFLOW_MANIFEST_MAX_BYTES
|
|
256
|
+
)
|
|
257
|
+
return false;
|
|
258
|
+
const content = fs.readFileSync(manifestPath);
|
|
259
|
+
if (content.byteLength > WORKFLOW_MANIFEST_MAX_BYTES) return false;
|
|
260
|
+
const parsed: unknown = JSON.parse(content.toString("utf8"));
|
|
261
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const manifest = parsed as Record<string, unknown>;
|
|
265
|
+
const markerManifest = JSON.parse(marker.manifest) as Record<
|
|
266
|
+
string,
|
|
267
|
+
unknown
|
|
268
|
+
>;
|
|
269
|
+
return (
|
|
270
|
+
manifest.runId === marker.runId &&
|
|
271
|
+
manifest.status === markerManifest.status &&
|
|
272
|
+
manifest.transcriptArtifact === markerManifest.transcriptArtifact &&
|
|
273
|
+
manifest.resultArtifact === markerManifest.resultArtifact
|
|
274
|
+
);
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Complete a terminal artifact commit only when every prepared file matches
|
|
282
|
+
* the exact bounded receipt written before the side-artifact sequence began.
|
|
283
|
+
*/
|
|
284
|
+
export function recoverPendingWorkflowCommit(
|
|
285
|
+
runDir: string,
|
|
286
|
+
): WorkflowCommitRecovery {
|
|
287
|
+
const marker = parseWorkflowCommitMarker(runDir);
|
|
288
|
+
if (marker === "none" || marker === "invalid") return marker;
|
|
289
|
+
|
|
290
|
+
for (const artifact of marker.artifacts) {
|
|
291
|
+
const artifactPath = path.join(runDir, artifact.name);
|
|
292
|
+
let stat: fs.Stats;
|
|
293
|
+
let content: Buffer;
|
|
294
|
+
try {
|
|
295
|
+
stat = fs.lstatSync(artifactPath);
|
|
296
|
+
if (
|
|
297
|
+
!stat.isFile() ||
|
|
298
|
+
stat.isSymbolicLink() ||
|
|
299
|
+
stat.size !== artifact.bytes
|
|
300
|
+
) {
|
|
301
|
+
return "incomplete";
|
|
302
|
+
}
|
|
303
|
+
content = fs.readFileSync(artifactPath);
|
|
304
|
+
} catch {
|
|
305
|
+
return "incomplete";
|
|
306
|
+
}
|
|
307
|
+
if (
|
|
308
|
+
content.byteLength !== artifact.bytes ||
|
|
309
|
+
sha256(content) !== artifact.sha256
|
|
310
|
+
) {
|
|
311
|
+
return "incomplete";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const manifestPath = path.join(runDir, "workflow.json");
|
|
316
|
+
try {
|
|
317
|
+
if (
|
|
318
|
+
fs.existsSync(manifestPath) &&
|
|
319
|
+
hasCommittedManifest(manifestPath, marker)
|
|
320
|
+
) {
|
|
321
|
+
removeWorkflowCommit(runDir);
|
|
322
|
+
return "already-committed";
|
|
323
|
+
}
|
|
324
|
+
// A later terminal/cleanup/delivery publication supersedes this receipt.
|
|
325
|
+
// Artifact validity alone cannot authorize replacing canonical facts.
|
|
326
|
+
let predecessor: Buffer;
|
|
327
|
+
try {
|
|
328
|
+
const stat = fs.lstatSync(manifestPath);
|
|
329
|
+
if (
|
|
330
|
+
!stat.isFile() ||
|
|
331
|
+
stat.isSymbolicLink() ||
|
|
332
|
+
stat.size > WORKFLOW_MANIFEST_MAX_BYTES
|
|
333
|
+
) {
|
|
334
|
+
return "invalid";
|
|
335
|
+
}
|
|
336
|
+
predecessor = fs.readFileSync(manifestPath);
|
|
337
|
+
} catch {
|
|
338
|
+
return "invalid";
|
|
339
|
+
}
|
|
340
|
+
if (
|
|
341
|
+
predecessor.byteLength > WORKFLOW_MANIFEST_MAX_BYTES ||
|
|
342
|
+
sha256(predecessor) !== marker.predecessorSha256
|
|
343
|
+
) {
|
|
344
|
+
return "invalid";
|
|
345
|
+
}
|
|
346
|
+
writeFileAtomic(manifestPath, marker.manifest);
|
|
347
|
+
removeWorkflowCommit(runDir);
|
|
348
|
+
return "recovered";
|
|
349
|
+
} catch {
|
|
350
|
+
return "failed";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
42
354
|
function boundEntry(entry: TranscriptEntry, maxBytes: number) {
|
|
43
355
|
if (textBytes(entry.text) <= maxBytes) return { ...entry };
|
|
44
356
|
const markerBytes = textBytes(ENTRY_TRUNCATION_MARKER);
|
|
@@ -120,11 +432,11 @@ export function persistWorkflowTerminalState(
|
|
|
120
432
|
delete terminalManifest.result;
|
|
121
433
|
delete terminalManifest.resultArtifact;
|
|
122
434
|
delete terminalManifest.transcriptArtifact;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
);
|
|
435
|
+
const content = safeStringify(terminalManifest, {
|
|
436
|
+
maxBytes: WORKFLOW_MANIFEST_MAX_BYTES,
|
|
437
|
+
});
|
|
438
|
+
writeRunFile(runDir, "workflow.json", content);
|
|
439
|
+
return sha256(content);
|
|
128
440
|
}
|
|
129
441
|
|
|
130
442
|
/** Persist one successful child result before any handoff/context projection. */
|
|
@@ -177,15 +489,24 @@ export function persistWorkflowJson(
|
|
|
177
489
|
// later artifact write fails, readers still see an explained terminal run
|
|
178
490
|
// instead of the previous `running` manifest. The final manifest below adds
|
|
179
491
|
// the artifact references once every dependent file has committed.
|
|
180
|
-
|
|
181
|
-
|
|
492
|
+
const terminal = details.status !== "running";
|
|
493
|
+
let predecessorSha256: string | undefined;
|
|
494
|
+
if (terminal) {
|
|
495
|
+
// A retry supersedes an older unfinished receipt before it publishes a new
|
|
496
|
+
// terminal fact. Failing to remove it must stop the new commit rather than
|
|
497
|
+
// let a concurrent reader promote stale artifact identities.
|
|
498
|
+
removeWorkflowCommit(runDir, true);
|
|
499
|
+
predecessorSha256 = persistWorkflowTerminalState(runDir, details);
|
|
182
500
|
}
|
|
183
501
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
502
|
+
const artifactWrites: WorkflowArtifactWrite[] = [
|
|
503
|
+
{
|
|
504
|
+
name: "transcripts.json",
|
|
505
|
+
content: safeStringify(transcripts, {
|
|
506
|
+
maxBytes: WORKFLOW_TRANSCRIPTS_MAX_BYTES,
|
|
507
|
+
}),
|
|
508
|
+
},
|
|
509
|
+
];
|
|
189
510
|
// Written alongside the rest so it inherits atomic write, 500ms coalescing,
|
|
190
511
|
// and the final flush. Only present once a call has actually succeeded.
|
|
191
512
|
// Accumulators already enforce the cap incrementally and can assemble the
|
|
@@ -201,14 +522,15 @@ export function persistWorkflowJson(
|
|
|
201
522
|
"toJson" in journal
|
|
202
523
|
? journal.toJson()
|
|
203
524
|
: JSON.stringify(boundedJournal(journal).journal, null, 2);
|
|
204
|
-
|
|
525
|
+
artifactWrites.push({ name: JOURNAL_FILE, content });
|
|
205
526
|
}
|
|
206
527
|
if (details.result !== undefined) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
528
|
+
artifactWrites.push({
|
|
529
|
+
name: "result.json",
|
|
530
|
+
content: safeStringify(details.result, {
|
|
531
|
+
maxBytes: WORKFLOW_MANIFEST_MAX_BYTES,
|
|
532
|
+
}),
|
|
533
|
+
});
|
|
212
534
|
}
|
|
213
535
|
const compact: WorkflowDetails = {
|
|
214
536
|
...details,
|
|
@@ -218,11 +540,27 @@ export function persistWorkflowJson(
|
|
|
218
540
|
transcriptArtifact: "transcripts.json",
|
|
219
541
|
agents: details.agents.map((agent) => ({ ...agent, transcript: [] })),
|
|
220
542
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
543
|
+
const manifest = safeStringify(compact, {
|
|
544
|
+
maxBytes: WORKFLOW_MANIFEST_MAX_BYTES,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (predecessorSha256 !== undefined) {
|
|
548
|
+
writeRunFile(
|
|
549
|
+
runDir,
|
|
550
|
+
WORKFLOW_COMMIT_FILE,
|
|
551
|
+
serializeWorkflowCommitMarker(
|
|
552
|
+
details,
|
|
553
|
+
manifest,
|
|
554
|
+
artifactWrites,
|
|
555
|
+
predecessorSha256,
|
|
556
|
+
),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
for (const artifact of artifactWrites) {
|
|
560
|
+
writeRunFile(runDir, artifact.name, artifact.content);
|
|
561
|
+
}
|
|
562
|
+
writeRunFile(runDir, "workflow.json", manifest);
|
|
563
|
+
if (terminal) removeWorkflowCommit(runDir);
|
|
226
564
|
}
|
|
227
565
|
|
|
228
566
|
/**
|
|
@@ -235,6 +573,7 @@ export function persistWorkflowDeliveryState(
|
|
|
235
573
|
runDir: string,
|
|
236
574
|
delivery: WorkflowDelivery,
|
|
237
575
|
) {
|
|
576
|
+
recoverPendingWorkflowCommit(runDir);
|
|
238
577
|
const file = path.join(runDir, "workflow.json");
|
|
239
578
|
const raw: unknown = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
240
579
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { SPINNER_INTERVAL_MS, spinnerFrame } from "../shared/spinner.ts";
|
|
32
32
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
33
33
|
import { isAcceptanceLedger } from "./acceptance.ts";
|
|
34
|
+
import { recoverPendingWorkflowCommit } from "./artifacts.ts";
|
|
34
35
|
import { projectWorkflowGraph } from "./graph-projection.ts";
|
|
35
36
|
import {
|
|
36
37
|
classifyInterruptedInvocation,
|
|
@@ -143,18 +144,55 @@ export function readPersistedWorkflowDetails(
|
|
|
143
144
|
runId: string,
|
|
144
145
|
options: ReadPersistedRunOptions = {},
|
|
145
146
|
): WorkflowDetails | undefined {
|
|
146
|
-
|
|
147
|
+
const details = normalizeReadRecord(
|
|
148
|
+
runId,
|
|
149
|
+
readPersistedWorkflowRecord(runId),
|
|
150
|
+
);
|
|
151
|
+
if (!details) return undefined;
|
|
152
|
+
if (options.hydrateArtifacts) hydrateRunArtifacts(runId, details);
|
|
153
|
+
return details;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeReadRecord(runId: string, raw: unknown) {
|
|
157
|
+
try {
|
|
158
|
+
return normalizePersistedWorkflowDetails(runId, raw);
|
|
159
|
+
} catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readPersistedWorkflowRecord(runId: string) {
|
|
165
|
+
recoverPendingWorkflowCommit(path.join(runsDir(), runId));
|
|
147
166
|
try {
|
|
148
167
|
const raw: unknown = JSON.parse(
|
|
149
168
|
fs.readFileSync(path.join(runsDir(), runId, "workflow.json"), "utf8"),
|
|
150
169
|
);
|
|
151
|
-
|
|
170
|
+
return raw && typeof raw === "object"
|
|
171
|
+
? (raw as Record<string, unknown>)
|
|
172
|
+
: undefined;
|
|
152
173
|
} catch {
|
|
153
174
|
return undefined;
|
|
154
175
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function matchesRunScope(
|
|
179
|
+
record: { startedAt?: unknown; finishedAt?: unknown; sessionId?: unknown },
|
|
180
|
+
runId: string,
|
|
181
|
+
sessionId: string,
|
|
182
|
+
referencedRunIds: ReadonlySet<string>,
|
|
183
|
+
startedSince: number,
|
|
184
|
+
fromRetention = false,
|
|
185
|
+
) {
|
|
186
|
+
const touchedAt = Math.max(
|
|
187
|
+
typeof record.startedAt === "number" ? record.startedAt : 0,
|
|
188
|
+
typeof record.finishedAt === "number" ? record.finishedAt : 0,
|
|
189
|
+
);
|
|
190
|
+
return (
|
|
191
|
+
touchedAt >= startedSince &&
|
|
192
|
+
(fromRetention ||
|
|
193
|
+
record.sessionId === sessionId ||
|
|
194
|
+
referencedRunIds.has(runId))
|
|
195
|
+
);
|
|
158
196
|
}
|
|
159
197
|
|
|
160
198
|
function isWorktreeCleanup(
|
|
@@ -581,27 +619,41 @@ export function loadRunEntries(
|
|
|
581
619
|
retained: ReadonlyMap<string, WorkflowDetails> = new Map(),
|
|
582
620
|
): RunEntry[] {
|
|
583
621
|
const entries: RunEntry[] = [];
|
|
584
|
-
const runIds = new Set([
|
|
622
|
+
const runIds = new Set([
|
|
623
|
+
...listPersistedRunIds(),
|
|
624
|
+
...retained.keys(),
|
|
625
|
+
...active.keys(),
|
|
626
|
+
]);
|
|
585
627
|
for (const runId of runIds) {
|
|
586
628
|
const live = active.get(runId);
|
|
587
629
|
if (live) {
|
|
588
630
|
entries.push({ runId, details: live, live: true });
|
|
589
631
|
continue;
|
|
590
632
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
633
|
+
// Reject unrelated history before normalizing potentially large inline
|
|
634
|
+
// transcripts. Side artifacts belong to explicit detail navigation.
|
|
635
|
+
const raw = readPersistedWorkflowRecord(runId);
|
|
636
|
+
if (
|
|
637
|
+
raw &&
|
|
638
|
+
!matchesRunScope(raw, runId, sessionId, referencedRunIds, startedSince)
|
|
639
|
+
) {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const persisted = normalizeReadRecord(runId, raw);
|
|
594
643
|
const retainedDetails = retained.get(runId);
|
|
595
644
|
const details = persisted ?? retainedDetails;
|
|
596
645
|
if (!details) continue;
|
|
597
646
|
const fromRetention =
|
|
598
647
|
persisted === undefined && retainedDetails !== undefined;
|
|
599
|
-
const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
|
|
600
648
|
if (
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
649
|
+
!matchesRunScope(
|
|
650
|
+
details,
|
|
651
|
+
runId,
|
|
652
|
+
sessionId,
|
|
653
|
+
referencedRunIds,
|
|
654
|
+
startedSince,
|
|
655
|
+
fromRetention,
|
|
656
|
+
)
|
|
605
657
|
) {
|
|
606
658
|
continue;
|
|
607
659
|
}
|
|
@@ -718,6 +770,9 @@ type DetailFocus = "phases" | "agents";
|
|
|
718
770
|
export class WorkflowDashboard {
|
|
719
771
|
private view: View = "list";
|
|
720
772
|
private entries: RunEntry[] = [];
|
|
773
|
+
private historyLoaded = false;
|
|
774
|
+
private seenRetainedRunIds = new Set<string>();
|
|
775
|
+
private hydratedRunIds = new Set<string>();
|
|
721
776
|
private listIndex = 0;
|
|
722
777
|
private phaseIndex = 0;
|
|
723
778
|
private agentIndex = 0;
|
|
@@ -826,13 +881,68 @@ export class WorkflowDashboard {
|
|
|
826
881
|
|
|
827
882
|
private refresh() {
|
|
828
883
|
const selected = this.entries[this.listIndex]?.runId;
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
this.
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
884
|
+
const active = this.getActive();
|
|
885
|
+
const retained = this.getRetained();
|
|
886
|
+
if (!this.historyLoaded) {
|
|
887
|
+
this.entries = loadRunEntries(
|
|
888
|
+
active,
|
|
889
|
+
this.sessionId,
|
|
890
|
+
this.referencedRunIds,
|
|
891
|
+
this.startedSince,
|
|
892
|
+
retained,
|
|
893
|
+
);
|
|
894
|
+
this.historyLoaded = true;
|
|
895
|
+
} else {
|
|
896
|
+
// Animation ticks reuse historical projections. Only a newly settled run
|
|
897
|
+
// needs one canonical read; stable frames never scan or reread history.
|
|
898
|
+
const entries = new Map(
|
|
899
|
+
this.entries.map((entry) => [entry.runId, entry]),
|
|
900
|
+
);
|
|
901
|
+
const settledIds = new Set([
|
|
902
|
+
...this.entries
|
|
903
|
+
.filter((entry) => entry.live && !active.has(entry.runId))
|
|
904
|
+
.map((entry) => entry.runId),
|
|
905
|
+
...[...retained.keys()].filter(
|
|
906
|
+
(runId) =>
|
|
907
|
+
!this.seenRetainedRunIds.has(runId) &&
|
|
908
|
+
!entries.has(runId) &&
|
|
909
|
+
!active.has(runId),
|
|
910
|
+
),
|
|
911
|
+
]);
|
|
912
|
+
for (const runId of settledIds) {
|
|
913
|
+
const persisted = readPersistedWorkflowDetails(runId);
|
|
914
|
+
const details =
|
|
915
|
+
persisted ?? retained.get(runId) ?? entries.get(runId)?.details;
|
|
916
|
+
if (!details) continue;
|
|
917
|
+
if (
|
|
918
|
+
!matchesRunScope(
|
|
919
|
+
details,
|
|
920
|
+
runId,
|
|
921
|
+
this.sessionId,
|
|
922
|
+
this.referencedRunIds,
|
|
923
|
+
this.startedSince,
|
|
924
|
+
!persisted,
|
|
925
|
+
)
|
|
926
|
+
) {
|
|
927
|
+
entries.delete(runId);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
// Recovery operates on a projection, never on the former live owner.
|
|
931
|
+
const recovered = recoverStaleWorkflowDetails({
|
|
932
|
+
...details,
|
|
933
|
+
agents: details.agents.map((agent) => ({ ...agent })),
|
|
934
|
+
});
|
|
935
|
+
entries.set(runId, { runId, details: recovered, live: false });
|
|
936
|
+
this.hydratedRunIds.delete(runId);
|
|
937
|
+
}
|
|
938
|
+
for (const [runId, details] of active) {
|
|
939
|
+
entries.set(runId, { runId, details, live: true });
|
|
940
|
+
}
|
|
941
|
+
this.entries = [...entries.values()].sort(
|
|
942
|
+
(a, b) => b.details.startedAt - a.details.startedAt,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
for (const runId of retained.keys()) this.seenRetainedRunIds.add(runId);
|
|
836
946
|
if (selected) {
|
|
837
947
|
const index = this.entries.findIndex((e) => e.runId === selected);
|
|
838
948
|
if (index >= 0) this.listIndex = index;
|
|
@@ -847,6 +957,7 @@ export class WorkflowDashboard {
|
|
|
847
957
|
);
|
|
848
958
|
if (refreshed) this.current = refreshed;
|
|
849
959
|
}
|
|
960
|
+
if (this.view === "transcript") this.hydrateCurrent();
|
|
850
961
|
if (this.notice && Date.now() - this.noticeAt > NOTICE_TTL_MS)
|
|
851
962
|
this.notice = undefined;
|
|
852
963
|
}
|
|
@@ -882,7 +993,15 @@ export class WorkflowDashboard {
|
|
|
882
993
|
this.agentIndex = Math.min(this.agentIndex, Math.max(0, agents.length - 1));
|
|
883
994
|
}
|
|
884
995
|
|
|
996
|
+
private hydrateCurrent() {
|
|
997
|
+
const entry = this.current;
|
|
998
|
+
if (!entry || entry.live || this.hydratedRunIds.has(entry.runId)) return;
|
|
999
|
+
hydrateRunArtifacts(entry.runId, entry.details);
|
|
1000
|
+
this.hydratedRunIds.add(entry.runId);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
885
1003
|
private saveReport() {
|
|
1004
|
+
this.hydrateCurrent();
|
|
886
1005
|
const entry = this.current;
|
|
887
1006
|
if (!entry) return;
|
|
888
1007
|
const target = path.join(runsDir(), entry.runId, "report.md");
|
|
@@ -1023,6 +1142,7 @@ export class WorkflowDashboard {
|
|
|
1023
1142
|
}
|
|
1024
1143
|
|
|
1025
1144
|
private openTranscriptPage() {
|
|
1145
|
+
this.hydrateCurrent();
|
|
1026
1146
|
const transcriptAdapter = new WorkflowTranscriptAdapter();
|
|
1027
1147
|
this.view = "transcript";
|
|
1028
1148
|
this.transcriptPage = new AgentSessionPage(
|