pi-ultracode 0.1.2 → 0.3.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.
@@ -1,8 +1,15 @@
1
+ import * as crypto from "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import type { AgentTelemetryEvent, AgentUsage, ThinkingLevel } from "./agent-runner.ts";
4
5
  import { safeDisplayText, safeTranscriptText } from "./display-text.ts";
5
6
  import { preview, recompute, type WorkflowAgentStatus, type WorkflowSnapshot } from "./display.ts";
7
+ import {
8
+ appendArtifactFile,
9
+ ensurePrivateArtifactDirectory,
10
+ readArtifactFile,
11
+ writeArtifactFile,
12
+ } from "./run-artifacts.ts";
6
13
 
7
14
  export const RUN_DETAILS_VERSION = 1;
8
15
  export const MAX_LIVE_TASK_BYTES = 1024 * 1024;
@@ -66,6 +73,7 @@ export interface WorkflowTimelineEvent {
66
73
 
67
74
  export interface WorkflowTaskSummary {
68
75
  id: number;
76
+ callPath?: string;
69
77
  label: string;
70
78
  phase?: string;
71
79
  workflowPath: string[];
@@ -98,6 +106,7 @@ export interface WorkflowTaskDetail extends WorkflowTaskSummary {
98
106
 
99
107
  export interface StartWorkflowTaskInput {
100
108
  id: number;
109
+ callPath?: string;
101
110
  label: string;
102
111
  phase?: string;
103
112
  workflowPath?: string[];
@@ -261,6 +270,7 @@ function cloneEvent(event: WorkflowTimelineEvent): WorkflowTimelineEvent {
261
270
  function cloneSummary(task: InternalTask): WorkflowTaskSummary {
262
271
  return {
263
272
  id: task.id,
273
+ callPath: task.callPath,
264
274
  label: task.label,
265
275
  phase: task.phase,
266
276
  workflowPath: [...task.workflowPath],
@@ -299,6 +309,7 @@ export class WorkflowRunDetails {
299
309
  readonly manifestPath: string;
300
310
 
301
311
  private readonly tasks = new Map<number, InternalTask>();
312
+ private readonly previousGenerationTasks = new Map<string, InternalTask>();
302
313
  private readonly listeners = new Set<() => void>();
303
314
  private readonly streamingLineColumns = new Map<string, number>();
304
315
  private readonly artifactBudget: RunArtifactBudget;
@@ -311,24 +322,23 @@ export class WorkflowRunDetails {
311
322
  this.runsDir = input.runsDir;
312
323
  this.manifestPath = path.join(input.runsDir, `${input.runId}.details.json`);
313
324
  const taskDir = this.taskDir();
314
- try {
315
- fs.mkdirSync(taskDir, { recursive: true });
316
- } catch {
317
- // Artifact persistence is best-effort; live details remain available.
318
- }
325
+ ensurePrivateArtifactDirectory(this.runsDir);
326
+ ensurePrivateArtifactDirectory(taskDir);
319
327
  this.artifactBudget = new RunArtifactBudget(taskDir, MAX_RUN_TRANSCRIPT_PAYLOAD_BYTES);
320
328
  }
321
329
 
322
330
  static restore(manifestPath: string): { details: WorkflowRunDetails; snapshot: WorkflowSnapshot } | undefined {
323
331
  let manifest: WorkflowRunDetailsManifest;
324
332
  try {
325
- manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as WorkflowRunDetailsManifest;
333
+ manifest = JSON.parse(readArtifactFile(manifestPath, "workflow details manifest", MAX_DETAILS_MANIFEST_BYTES)) as WorkflowRunDetailsManifest;
326
334
  } catch {
327
335
  return undefined;
328
336
  }
337
+ const expectedFileName = `${manifest.runId}.details.json`;
329
338
  if (
330
339
  manifest.version !== RUN_DETAILS_VERSION
331
340
  || !/^wf_[a-z0-9_-]{1,120}$/i.test(manifest.runId ?? "")
341
+ || path.basename(manifestPath) !== expectedFileName
332
342
  || typeof manifest.name !== "string"
333
343
  || !manifest.snapshot
334
344
  ) return undefined;
@@ -381,20 +391,46 @@ export class WorkflowRunDetails {
381
391
  this.viewedTaskId = id;
382
392
  }
383
393
 
394
+ /** Start a resume generation while retaining prior transcripts only for cache hits. */
395
+ beginGeneration(): void {
396
+ this.previousGenerationTasks.clear();
397
+ for (const task of this.tasks.values()) {
398
+ this.previousGenerationTasks.set(task.callPath ?? `id:${task.id}`, task);
399
+ }
400
+ this.tasks.clear();
401
+ this.streamingLineColumns.clear();
402
+ this.sequence = 0;
403
+ this.viewedTaskId = undefined;
404
+ this.notify();
405
+ }
406
+
384
407
  startTask(input: StartWorkflowTaskInput): WorkflowTaskSummary {
385
- const restored = this.tasks.get(input.id);
408
+ const callPath = input.callPath ?? input.cachedRecord?.callPath ?? `id:${input.id}`;
409
+ const restored = this.tasks.get(input.id) ?? this.previousGenerationTasks.get(callPath);
386
410
  if (input.cached && restored) {
411
+ restored.id = input.id;
412
+ restored.callPath = callPath;
413
+ restored.label = safeDisplayText(input.label, 120) || `agent ${input.id}`;
414
+ restored.phase = input.phase ? safeDisplayText(input.phase, 120) : undefined;
415
+ restored.workflowPath = (input.workflowPath?.length ? input.workflowPath : [this.name])
416
+ .map((part) => safeDisplayText(part, 120))
417
+ .filter(Boolean);
387
418
  restored.status = "cached";
388
419
  restored.cached = true;
389
420
  restored.currentTurn = undefined;
421
+ this.previousGenerationTasks.delete(callPath);
422
+ this.tasks.set(input.id, restored);
423
+ this.sequence = Math.max(this.sequence, input.id);
390
424
  this.notify();
391
425
  return cloneSummary(restored);
392
426
  }
427
+ this.previousGenerationTasks.delete(callPath);
393
428
 
394
429
  const prompt = safeTranscriptText(input.prompt, MAX_TASK_TRANSCRIPT_BYTES);
395
430
  const cached = input.cachedRecord;
396
431
  const task: InternalTask = {
397
432
  id: input.id,
433
+ callPath,
398
434
  label: safeDisplayText(input.label, 120) || `agent ${input.id}`,
399
435
  phase: input.phase ? safeDisplayText(input.phase, 120) : undefined,
400
436
  workflowPath: (input.workflowPath?.length ? input.workflowPath : [this.name])
@@ -433,11 +469,14 @@ export class WorkflowRunDetails {
433
469
  if (!input.cached) {
434
470
  const writer = new BoundedTaskTranscriptWriter({
435
471
  dir: this.taskDir(),
472
+ artifactKey: transcriptArtifactKey(callPath),
473
+ runId: this.runId,
474
+ callPath,
436
475
  taskId: input.id,
437
476
  budget: this.artifactBudget,
438
477
  });
439
478
  writer.reset();
440
- this.persistPrompt(writer, input.id, prompt);
479
+ this.persistPrompt(writer, input.id, callPath, prompt);
441
480
  task.writer = writer;
442
481
  task.transcriptPath = writer.finalPath;
443
482
  }
@@ -709,17 +748,21 @@ export class WorkflowRunDetails {
709
748
  }
710
749
 
711
750
  persist(snapshot: WorkflowSnapshot): void {
751
+ const persistedTasks = this.tasks.size === 0 && this.previousGenerationTasks.size > 0
752
+ ? [...this.previousGenerationTasks.values()]
753
+ : [...this.tasks.values()];
754
+ persistedTasks.sort((a, b) => a.id - b.id);
712
755
  const manifest: WorkflowRunDetailsManifest = {
713
756
  version: RUN_DETAILS_VERSION,
714
757
  runId: this.runId,
715
758
  name: this.name,
716
759
  updatedAt: Date.now(),
717
760
  snapshot: snapshotForManifest(snapshot),
718
- tasks: this.listTasks().map(taskForManifest),
761
+ tasks: persistedTasks.map(taskForManifest),
719
762
  transcriptBytes: this.artifactBudget.usedBytes,
720
763
  };
721
764
  try {
722
- fs.mkdirSync(this.runsDir, { recursive: true });
765
+ ensurePrivateArtifactDirectory(this.runsDir);
723
766
  let serialized = JSON.stringify(manifest, null, 2);
724
767
  if (Buffer.byteLength(serialized, "utf8") > MAX_DETAILS_MANIFEST_BYTES) {
725
768
  manifest.snapshot.logs = [];
@@ -738,9 +781,7 @@ export class WorkflowRunDetails {
738
781
  serialized = JSON.stringify(manifest);
739
782
  }
740
783
  if (Buffer.byteLength(serialized, "utf8") > MAX_DETAILS_MANIFEST_BYTES) return;
741
- const temp = `${this.manifestPath}.tmp-${process.pid}`;
742
- fs.writeFileSync(temp, serialized, "utf8");
743
- fs.renameSync(temp, this.manifestPath);
784
+ writeArtifactFile(this.manifestPath, serialized, { trustedRoot: this.runsDir, overwrite: true });
744
785
  } catch {
745
786
  // Persistence failure does not affect the workflow result.
746
787
  }
@@ -782,7 +823,7 @@ export class WorkflowRunDetails {
782
823
  if (persist) this.persistTimelineEvent(task, event);
783
824
  }
784
825
 
785
- private persistPrompt(writer: BoundedTaskTranscriptWriter, taskId: number, prompt: string): void {
826
+ private persistPrompt(writer: BoundedTaskTranscriptWriter, taskId: number, callPath: string, prompt: string): void {
786
827
  const chunks = chunkText(safeMultiline(prompt), TRANSCRIPT_TEXT_CHUNK_CHARS);
787
828
  for (const [chunkIndex, text] of chunks.entries()) {
788
829
  writer.append({
@@ -790,6 +831,7 @@ export class WorkflowRunDetails {
790
831
  recordType: "prompt",
791
832
  runId: this.runId,
792
833
  taskId,
834
+ callPath,
793
835
  ts: Date.now(),
794
836
  chunkIndex,
795
837
  chunkCount: chunks.length,
@@ -814,6 +856,7 @@ export class WorkflowRunDetails {
814
856
  recordType: "event",
815
857
  runId: this.runId,
816
858
  taskId: task.id,
859
+ callPath: task.callPath,
817
860
  ts: Date.now(),
818
861
  baseSeq: safeEvent.seq,
819
862
  chunkIndex,
@@ -944,7 +987,7 @@ export class WorkflowRunDetails {
944
987
  const resolved = path.resolve(task.transcriptPath);
945
988
  const root = path.resolve(this.taskDir()) + path.sep;
946
989
  if (!resolved.startsWith(root)) return;
947
- content = fs.readFileSync(resolved, "utf8");
990
+ content = readArtifactFile(resolved, "workflow transcript", MAX_TASK_TRANSCRIPT_BYTES);
948
991
  } catch {
949
992
  return;
950
993
  }
@@ -957,6 +1000,11 @@ export class WorkflowRunDetails {
957
1000
  if (!line.trim()) continue;
958
1001
  try {
959
1002
  const record = JSON.parse(line) as any;
1003
+ if (
1004
+ record.version !== RUN_DETAILS_VERSION
1005
+ || record.runId !== this.runId
1006
+ || record.callPath !== task.callPath
1007
+ ) continue;
960
1008
  if (record.recordType === "prompt" && typeof record.text === "string") {
961
1009
  const index = Number.isFinite(record.chunkIndex) ? record.chunkIndex : 0;
962
1010
  promptChunks.set(index, record.text);
@@ -1013,7 +1061,7 @@ function restoreTaskSummary(value: unknown, fallbackName: string): WorkflowTaskS
1013
1061
  if (!value || typeof value !== "object") return undefined;
1014
1062
  const raw = value as Record<string, unknown>;
1015
1063
  const id = Number.isInteger(raw.id) ? Number(raw.id) : 0;
1016
- if (id < 1 || id > 1000) return undefined;
1064
+ if (id < 1 || id > 1024) return undefined;
1017
1065
  const statuses: WorkflowAgentStatus[] = ["running", "done", "error", "cancelled", "skipped", "cached"];
1018
1066
  const status = statuses.includes(raw.status as WorkflowAgentStatus) ? raw.status as WorkflowAgentStatus : "cancelled";
1019
1067
  const requestedEffort = normalizeThinkingLevel(raw.requestedEffort);
@@ -1023,6 +1071,7 @@ function restoreTaskSummary(value: unknown, fallbackName: string): WorkflowTaskS
1023
1071
  : [fallbackName];
1024
1072
  return taskForManifest({
1025
1073
  id,
1074
+ callPath: typeof raw.callPath === "string" ? raw.callPath : undefined,
1026
1075
  label: typeof raw.label === "string" ? raw.label : `agent ${id}`,
1027
1076
  phase: typeof raw.phase === "string" ? raw.phase : undefined,
1028
1077
  workflowPath,
@@ -1072,7 +1121,7 @@ function taskForManifest(task: WorkflowTaskSummary): WorkflowTaskSummary {
1072
1121
  }
1073
1122
 
1074
1123
  function snapshotForManifest(snapshot: WorkflowSnapshot): WorkflowSnapshot {
1075
- return {
1124
+ const projected: WorkflowSnapshot & { budgetTotal?: unknown } = {
1076
1125
  ...snapshot,
1077
1126
  name: safeDisplayText(snapshot.name, 120),
1078
1127
  description: snapshot.description ? safeDisplayText(snapshot.description, 240) : undefined,
@@ -1098,6 +1147,10 @@ function snapshotForManifest(snapshot: WorkflowSnapshot): WorkflowSnapshot {
1098
1147
  // potentially huge or hostile serialization in the UI manifest.
1099
1148
  result: undefined,
1100
1149
  };
1150
+ // Drop the removed run-level token-budget field when restoring or persisting
1151
+ // legacy manifests without broad unknown-field rewriting.
1152
+ delete projected.budgetTotal;
1153
+ return projected;
1101
1154
  }
1102
1155
 
1103
1156
  class RunArtifactBudget {
@@ -1128,6 +1181,10 @@ class RunArtifactBudget {
1128
1181
  }
1129
1182
  }
1130
1183
 
1184
+ function transcriptArtifactKey(callPath: string): string {
1185
+ return crypto.createHash("sha256").update(callPath).digest("hex").slice(0, 32);
1186
+ }
1187
+
1131
1188
  class BoundedTaskTranscriptWriter {
1132
1189
  readonly finalPath: string;
1133
1190
  readonly headPath: string;
@@ -1136,14 +1193,27 @@ class BoundedTaskTranscriptWriter {
1136
1193
  private headBytes = 0;
1137
1194
  private tailBytes = 0;
1138
1195
 
1139
- constructor(input: { dir: string; taskId: number; budget: RunArtifactBudget }) {
1196
+ constructor(input: {
1197
+ dir: string;
1198
+ artifactKey: string;
1199
+ runId: string;
1200
+ callPath: string;
1201
+ taskId: number;
1202
+ budget: RunArtifactBudget;
1203
+ }) {
1140
1204
  this.budget = input.budget;
1141
- this.finalPath = path.join(input.dir, `${input.taskId}.transcript.jsonl`);
1205
+ this.runId = input.runId;
1206
+ this.callPath = input.callPath;
1207
+ this.taskId = input.taskId;
1208
+ this.finalPath = path.join(input.dir, `${input.artifactKey}.transcript.jsonl`);
1142
1209
  this.headPath = `${this.finalPath}.head`;
1143
1210
  this.tailPath = `${this.finalPath}.tail`;
1144
1211
  }
1145
1212
 
1146
1213
  private readonly budget: RunArtifactBudget;
1214
+ private readonly runId: string;
1215
+ private readonly callPath: string;
1216
+ private readonly taskId: number;
1147
1217
 
1148
1218
  reset(): void {
1149
1219
  for (const target of [this.finalPath, this.headPath, this.tailPath]) {
@@ -1169,10 +1239,10 @@ class BoundedTaskTranscriptWriter {
1169
1239
  }
1170
1240
  try {
1171
1241
  if (this.headBytes + bytes <= TRANSCRIPT_HEAD_BYTES) {
1172
- fs.appendFileSync(this.headPath, line, "utf8");
1242
+ appendArtifactFile(this.headPath, line, { trustedRoot: path.dirname(this.finalPath) });
1173
1243
  this.headBytes += bytes;
1174
1244
  } else {
1175
- fs.appendFileSync(this.tailPath, line, "utf8");
1245
+ appendArtifactFile(this.tailPath, line, { trustedRoot: path.dirname(this.finalPath) });
1176
1246
  this.tailBytes += bytes;
1177
1247
  if (this.tailBytes > TRANSCRIPT_TAIL_BYTES) this.compactTail();
1178
1248
  }
@@ -1190,6 +1260,9 @@ class BoundedTaskTranscriptWriter {
1190
1260
  ? `${JSON.stringify({
1191
1261
  version: RUN_DETAILS_VERSION,
1192
1262
  recordType: "omitted",
1263
+ runId: this.runId,
1264
+ taskId: this.taskId,
1265
+ callPath: this.callPath,
1193
1266
  ts: Date.now(),
1194
1267
  omittedBytes: this.omittedBytes,
1195
1268
  message: `… ${formatBytes(this.omittedBytes)} of middle transcript omitted …`,
@@ -1201,10 +1274,11 @@ class BoundedTaskTranscriptWriter {
1201
1274
  : "";
1202
1275
  const combined = `${head}${marker}${tail}`;
1203
1276
  try {
1204
- fs.mkdirSync(path.dirname(this.finalPath), { recursive: true });
1205
- const temp = `${this.finalPath}.tmp-${process.pid}`;
1206
- fs.writeFileSync(temp, combined, "utf8");
1207
- fs.renameSync(temp, this.finalPath);
1277
+ ensurePrivateArtifactDirectory(path.dirname(this.finalPath));
1278
+ writeArtifactFile(this.finalPath, combined, {
1279
+ trustedRoot: path.dirname(this.finalPath),
1280
+ overwrite: true,
1281
+ });
1208
1282
  fs.rmSync(this.headPath, { force: true });
1209
1283
  fs.rmSync(this.tailPath, { force: true });
1210
1284
  this.headBytes = 0;
@@ -1232,7 +1306,10 @@ class BoundedTaskTranscriptWriter {
1232
1306
  }
1233
1307
  const removed = Math.max(0, this.tailBytes - keptBytes);
1234
1308
  try {
1235
- fs.writeFileSync(this.tailPath, kept.join(""), "utf8");
1309
+ writeArtifactFile(this.tailPath, kept.join(""), {
1310
+ trustedRoot: path.dirname(this.finalPath),
1311
+ overwrite: true,
1312
+ });
1236
1313
  this.tailBytes = keptBytes;
1237
1314
  this.omittedBytes += removed;
1238
1315
  this.budget.release(removed);
@@ -1255,7 +1332,7 @@ function isPathInside(candidate: string | undefined, rootDir: string): boolean {
1255
1332
 
1256
1333
  function readText(filePath: string): string {
1257
1334
  try {
1258
- return fs.readFileSync(filePath, "utf8");
1335
+ return readArtifactFile(filePath, "workflow transcript", MAX_TASK_TRANSCRIPT_BYTES);
1259
1336
  } catch {
1260
1337
  return "";
1261
1338
  }
@@ -1263,7 +1340,8 @@ function readText(filePath: string): string {
1263
1340
 
1264
1341
  function fileSize(filePath: string): number {
1265
1342
  try {
1266
- return fs.statSync(filePath).size;
1343
+ const stat = fs.lstatSync(filePath);
1344
+ return stat.isFile() && !stat.isSymbolicLink() ? stat.size : 0;
1267
1345
  } catch {
1268
1346
  return 0;
1269
1347
  }