pi-extensible-workflows 3.3.0 → 3.4.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/src/cli.ts CHANGED
@@ -323,6 +323,8 @@ class CliProgress {
323
323
  #frame = 0;
324
324
  #run: PersistedRun | undefined;
325
325
  #runId: string | undefined;
326
+ #runtimeStartedAt = 0;
327
+ #runtimeBaseMs = 0;
326
328
  #timer: ReturnType<typeof setInterval> | undefined;
327
329
  #interactive: boolean;
328
330
  #styles: WorkflowProgressStyles;
@@ -331,7 +333,15 @@ class CliProgress {
331
333
  this.#styles = terminalProgressStyles(this.#interactive);
332
334
  }
333
335
  update(run: PersistedRun): void {
334
- if (this.#runId !== run.id) { this.#runId = run.id; this.onRunId(run.id); }
336
+ if (this.#runId !== run.id) {
337
+ this.#runId = run.id;
338
+ this.onRunId(run.id);
339
+ this.#runtimeStartedAt = Date.now();
340
+ this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
341
+ } else if (this.#run && this.#run.state !== "running" && run.state === "running") {
342
+ this.#runtimeStartedAt = Date.now();
343
+ this.#runtimeBaseMs = run.usage?.durationMs ?? 0;
344
+ }
335
345
  this.#run = run;
336
346
  if (!this.#interactive) {
337
347
  this.#timer ??= setInterval(() => { this.render(); }, 1000);
@@ -345,14 +355,15 @@ class CliProgress {
345
355
  }
346
356
  render(): void {
347
357
  if (!this.#run) return;
358
+ const run = this.#run.state !== "running" ? this.#run : { ...this.#run, usage: { ...(this.#run.usage ?? { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 }), durationMs: Math.max(this.#run.usage?.durationMs ?? 0, this.#runtimeBaseMs + Date.now() - this.#runtimeStartedAt) } };
348
359
  if (!this.#interactive) {
349
- const stable = formatWorkflowProgress(this.#run, "◇", this.#styles);
360
+ const stable = formatWorkflowProgress(run, "◇", this.#styles);
350
361
  if (stable !== this.#lastStable) { this.#lastStable = stable; this.stderr(`${stable}\n`); }
351
362
  return;
352
363
  }
353
364
  const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
354
365
  const width = process.stderr.columns || 80;
355
- const text = truncateWorkflowProgress(formatWorkflowProgress(this.#run, spinner, this.#styles), width).join("\n");
366
+ const text = truncateWorkflowProgress(formatWorkflowProgress(run, spinner, this.#styles), width).join("\n");
356
367
  this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
357
368
  this.#lines = text.split("\n").length;
358
369
  }
@@ -46,12 +46,43 @@ function accounting(value: unknown, label: string): void { if (!object(value)) t
46
46
  function resourceExclusions(value: unknown, label: string): void { if (value === undefined) return; if (!object(value)) throw new Error(`${label} is invalid`); if (value.skills !== undefined) stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined) stringList(value.extensions, `${label}.extensions`); }
47
47
  function agentDefinition(value: unknown, label: string): void { if (!object(value)) throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking as string)) throw new Error(`${label}.thinking is invalid`); if (value.tools !== undefined) stringList(value.tools, `${label}.tools`); resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`); }
48
48
  function validateScheduledOptions(value: unknown, label: string): void { if (!object(value) || typeof value.label !== "string" || !value.label || typeof value.cwd !== "string" || !value.cwd) throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value.thinking as string)) throw new Error(`${label}.thinking is invalid`); optionalString(value.role, `${label}.role`); if (value.schema !== undefined) validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined) nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null) positiveInteger(value.timeoutMs, `${label}.timeoutMs`); if (value.agentOptions !== undefined && (!object(value.agentOptions) || !jsonValue(value.agentOptions))) throw new Error(`${label}.agentOptions is invalid`); if (value.agentIdentity !== undefined) { if (!object(value.agentIdentity) || !Array.isArray(value.agentIdentity.structuralPath) || value.agentIdentity.structuralPath.some((part) => typeof part !== "string") || typeof value.agentIdentity.callSite !== "string") throw new Error(`${label}.agentIdentity is invalid`); positiveInteger(value.agentIdentity.occurrence, `${label}.agentIdentity.occurrence`); optionalString(value.agentIdentity.parentBreadcrumb, `${label}.agentIdentity.parentBreadcrumb`); optionalString(value.agentIdentity.worktreeOwner, `${label}.agentIdentity.worktreeOwner`); } }
49
- function validateAgent(value: unknown, label: string): void { if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || typeof value.state !== "string" || !AGENT_STATES.has(value.state)) throw new Error(`${label} is invalid`); optionalString(value.label, `${label}.label`); optionalString(value.parentId, `${label}.parentId`); if (value.structuralPath !== undefined) stringList(value.structuralPath, `${label}.structuralPath`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.role, `${label}.role`); optionalString(value.requestedModel, `${label}.requestedModel`); model(value.model, `${label}.model`); stringList(value.tools, `${label}.tools`); nonNegativeInteger(value.attempts, `${label}.attempts`); if (value.lastEventAt !== undefined) finiteNumber(value.lastEventAt, `${label}.lastEventAt`); if (value.attemptDetails !== undefined) { if (!Array.isArray(value.attemptDetails)) throw new Error(`${label}.attemptDetails is invalid`); for (const [index, attempt] of value.attemptDetails.entries()) { const at = `${label}.attemptDetails[${String(index)}]`; if (!object(attempt) || !Number.isSafeInteger(attempt.attempt) || Number(attempt.attempt) < 1 || typeof attempt.sessionId !== "string" || !attempt.sessionId || typeof attempt.sessionFile !== "string" || !attempt.sessionFile) throw new Error(`${at} is invalid`); accounting(attempt.accounting, `${at}.accounting`); if (attempt.error !== undefined && (!object(attempt.error) || typeof attempt.error.code !== "string" || typeof attempt.error.message !== "string")) throw new Error(`${at}.error is invalid`); if (attempt.setup !== undefined) { if (!object(attempt.setup) || !Array.isArray(attempt.setup.hookNames) || attempt.setup.hookNames.some((name) => typeof name !== "string") || typeof attempt.setup.cwd !== "string") throw new Error(`${at}.setup is invalid`); model(attempt.setup.model, `${at}.setup.model`); stringList(attempt.setup.tools, `${at}.setup.tools`); resourceExclusions(attempt.setup.disabledAgentResources, `${at}.setup.disabledAgentResources`); } } } if (value.accounting !== undefined) accounting(value.accounting, `${label}.accounting`); if (value.toolCalls !== undefined) { if (!Array.isArray(value.toolCalls)) throw new Error(`${label}.toolCalls is invalid`); for (const call of value.toolCalls) if (!object(call) || typeof call.id !== "string" || typeof call.name !== "string" || !["running", "completed", "failed"].includes(call.state as string)) throw new Error(`${label}.toolCalls is invalid`); } if (value.activity !== undefined && (!object(value.activity) || !["reasoning", "tool", "text"].includes(value.activity.kind as string) || typeof value.activity.text !== "string")) throw new Error(`${label}.activity is invalid`); }
49
+ function validateSessionReference(value: unknown, label: string): void {
50
+ if (!object(value) || typeof value.transport !== "string" || !value.transport || typeof value.sessionId !== "string" || !value.sessionId || (value.locator !== undefined && !jsonValue(value.locator))) throw new Error(`${label} is invalid`);
51
+ }
52
+ function validateAgentSetup(value: unknown, label: string): void {
53
+ if (!object(value)) throw new Error(`${label} is invalid`);
54
+ stringList(value.hookNames, `${label}.hookNames`);
55
+ model(value.model, `${label}.model`);
56
+ stringList(value.tools, `${label}.tools`);
57
+ if (typeof value.cwd !== "string" || !value.cwd) throw new Error(`${label}.cwd is invalid`);
58
+ resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
59
+ }
60
+ function validateAgent(value: unknown, label: string): void {
61
+ if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || typeof value.state !== "string" || !AGENT_STATES.has(value.state)) throw new Error(`${label} is invalid`);
62
+ optionalString(value.systemPrompt, `${label}.systemPrompt`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.label, `${label}.label`); optionalString(value.parentId, `${label}.parentId`);
63
+ if (value.structuralPath !== undefined) stringList(value.structuralPath, `${label}.structuralPath`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.role, `${label}.role`); optionalString(value.requestedModel, `${label}.requestedModel`);
64
+ model(value.model, `${label}.model`); stringList(value.tools, `${label}.tools`); nonNegativeInteger(value.attempts, `${label}.attempts`);
65
+ if (value.attemptDetails !== undefined) {
66
+ if (!Array.isArray(value.attemptDetails)) throw new Error(`${label}.attemptDetails is invalid`);
67
+ for (const [index, attempt] of value.attemptDetails.entries()) {
68
+ const at = `${label}.attemptDetails[${String(index)}]`;
69
+ if (!object(attempt) || !Number.isSafeInteger(attempt.attempt) || Number(attempt.attempt) < 1 || typeof attempt.transport !== "string" || !attempt.transport || Object.hasOwn(attempt, "sessionId") || Object.hasOwn(attempt, "sessionFile")) throw new Error(`${at} is invalid`);
70
+ validateAgentSetup(attempt.setup, `${at}.setup`);
71
+ if (attempt.session !== undefined) { validateSessionReference(attempt.session, `${at}.session`); if ((attempt.session as Record<string, unknown>).transport !== attempt.transport) throw new Error(`${at}.session transport does not match attempt transport`); }
72
+ accounting(attempt.accounting, `${at}.accounting`);
73
+ if (attempt.error !== undefined && (!object(attempt.error) || typeof attempt.error.code !== "string" || typeof attempt.error.message !== "string")) throw new Error(`${at}.error is invalid`);
74
+ }
75
+ }
76
+ if (value.accounting !== undefined) accounting(value.accounting, `${label}.accounting`);
77
+ if (value.toolCalls !== undefined) { if (!Array.isArray(value.toolCalls)) throw new Error(`${label}.toolCalls is invalid`); for (const [index, call] of value.toolCalls.entries()) if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !["running", "completed", "failed"].includes(call.state as string)) throw new Error(`${label}.toolCalls[${String(index)}] is invalid`); }
78
+ if (value.activity !== undefined) { if (!object(value.activity) || !["reasoning", "tool", "text"].includes(value.activity.kind as string) || typeof value.activity.text !== "string") throw new Error(`${label}.activity is invalid`); }
79
+ if (value.lastEventAt !== undefined) finiteNumber(value.lastEventAt, `${label}.lastEventAt`);
80
+ }
50
81
  function validateUsage(value: unknown, label: string): void { if (!object(value)) throw new Error(`${label} is invalid`); for (const key of ["tokens", "costUsd", "durationMs", "agentLaunches"]) finiteNumber(value[key], `${label}.${key}`); }
51
82
  function validateBudgetEvents(value: unknown): void { if (value === undefined) return; if (!Array.isArray(value)) throw new Error("Persisted budget events are invalid"); for (const [index, event] of value.entries()) { const label = `budgetEvents[${String(index)}]`; if (!object(event) || typeof event.type !== "string" || !BUDGET_EVENT_TYPES.has(event.type) || !Number.isSafeInteger(event.budgetVersion) || Number(event.budgetVersion) < 1 || !Array.isArray(event.dimensions) || event.dimensions.some((dimension) => typeof dimension !== "string" || !BUDGET_DIMENSIONS.has(dimension)) || typeof event.at !== "number" || !Number.isFinite(event.at) || event.limits === undefined) throw new Error(`${label} is invalid`); validateUsage(event.usage, `${label}.usage`); validateBudget(event.limits); } }
52
83
  function validateRunRecord(run: PersistedRun): void {
53
84
  const value = run as unknown;
54
- if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state as RunState) || !Array.isArray(value.agents) || !Array.isArray(value.nativeSessions)) throw new Error("Persisted run state is invalid");
85
+ if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state as RunState) || !Array.isArray(value.agents) || !Array.isArray(value.agentSessions) || Object.hasOwn(value, "nativeSessions")) throw new Error("Persisted run state is invalid");
55
86
  const agents = value.agents as Record<string, unknown>[];
56
87
  agents.forEach((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); });
57
88
  const agentIds = new Set<string>();
@@ -72,7 +103,8 @@ function validateRunRecord(run: PersistedRun): void {
72
103
  parent = typeof parentAgent?.parentId === "string" ? parentAgent.parentId : undefined;
73
104
  }
74
105
  }
75
- for (const [index, session] of (value.nativeSessions as unknown[]).entries()) if (!object(session) || typeof session.sessionId !== "string" || !session.sessionId || typeof session.sessionFile !== "string" || !session.sessionFile) throw new Error(`nativeSessions[${String(index)}] is invalid`);
106
+ const sessions = value.agentSessions as unknown[];
107
+ for (const [index, session] of sessions.entries()) validateSessionReference(session, `agentSessions[${String(index)}]`);
76
108
  optionalString(value.parentRunId, "Persisted parent run");
77
109
  optionalString(value.failedAt, "Persisted failed path");
78
110
  if (value.retry !== undefined) {
@@ -146,6 +178,7 @@ async function validateRunArtifacts(store: RunStore, workflowScript: string, sta
146
178
  if (!object(record) || typeof record.id !== "string" || !record.id || ownershipIds.has(record.id) || typeof record.label !== "string" || !record.label || typeof record.state !== "string" || !SCHEDULER_STATES.has(record.state)) throw new Error(`${label} is invalid`);
147
179
  ownershipIds.add(record.id);
148
180
  optionalString(record.parentId, `${label}.parentId`);
181
+ optionalString(record.prompt, `${label}.prompt`);
149
182
  validateScheduledOptions(record.options, `${label}.options`);
150
183
  }
151
184
  for (const record of ownership) if (object(record) && record.parentId !== undefined && (typeof record.parentId !== "string" || !ownershipIds.has(record.parentId))) throw new Error("Persisted ownership parent is missing");
package/src/execution.ts CHANGED
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import { RunStore, structuralPath as operationPath } from "./persistence.js";
7
7
  import type { AgentAttempt } from "./agent-execution.js";
8
- import type { AgentIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
8
+ import type { AgentIdentity, AgentAttemptSummary, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowAgentSessionReference, WorkflowBridge, WorkflowErrorCode, WorkflowExecution } from "./types.js";
9
9
  import { WorkflowError } from "./types.js";
10
10
  import { asWorkflowError, errorText, fail, isWorkflowAuthored, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
11
11
  import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
@@ -13,6 +13,7 @@ import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validat
13
13
  export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
14
14
  type WorkerErrorShape = { code?: string; message: string; authored?: boolean; failedAt?: string };
15
15
  export const HEARTBEAT_TIMEOUT_MS = 5000;
16
+ const HEARTBEAT_INTERVAL_MS = 1000;
16
17
 
17
18
  const OUTCOME_ERRORS = new Set<string>(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
18
19
  const WORK_RESULT_BRAND = "__workResult";
@@ -64,7 +65,7 @@ process.on("message", raw => {
64
65
  if (message.ok) request.resolve(message.value);
65
66
  else request.reject(workflowError(message.error));
66
67
  });
67
- const heartbeat = setInterval(() => send({ type: "heartbeat" }), 1000);
68
+ const heartbeat = setInterval(() => send({ type: "heartbeat" }), ${HEARTBEAT_INTERVAL_MS});
68
69
  send({ type: "heartbeat" });
69
70
  const BRAND = "${WORK_RESULT_BRAND}";
70
71
  const workError = (code, message) => Object.assign(new Error(message), { code });
@@ -416,7 +417,16 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
416
417
  const controller = new AbortController();
417
418
  let settled = false;
418
419
  let rejectResult: (error: WorkflowError) => void = () => undefined;
419
- let watchdog = setTimeout(() => { stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat"); }, HEARTBEAT_TIMEOUT_MS);
420
+ let lastHeartbeatAt = performance.now();
421
+ let lastWatchdogCheckAt = lastHeartbeatAt;
422
+ const watchdog = setInterval(() => {
423
+ const now = performance.now();
424
+ // A check delayed enough to bridge the normal heartbeat margin makes elapsed time unreliable.
425
+ const watchdogCheckWasDelayed = now - lastWatchdogCheckAt >= HEARTBEAT_TIMEOUT_MS - HEARTBEAT_INTERVAL_MS;
426
+ lastWatchdogCheckAt = now;
427
+ if (watchdogCheckWasDelayed) { lastHeartbeatAt = now; return; }
428
+ if (now - lastHeartbeatAt >= HEARTBEAT_TIMEOUT_MS) stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat");
429
+ }, HEARTBEAT_INTERVAL_MS);
420
430
  const result = new Promise<JsonValue>((resolve, reject) => {
421
431
  rejectResult = reject;
422
432
  child.on("message", (raw: unknown) => {
@@ -424,7 +434,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
424
434
  if (typeof raw !== "string" || Buffer.byteLength(raw) > RPC_LIMIT_BYTES) fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
425
435
  const message = JSON.parse(raw) as { type?: string; id?: number; method?: string; args?: JsonValue[]; ok?: boolean; value?: JsonValue; error?: WorkerErrorShape };
426
436
  if (!jsonValue(message)) fail("RPC_LIMIT_EXCEEDED", "Worker RPC must contain JSON-compatible values");
427
- if (message.type === "heartbeat") { clearTimeout(watchdog); watchdog = setTimeout(() => { stop("WORKER_UNRESPONSIVE", "Workflow worker missed its five-second heartbeat"); }, HEARTBEAT_TIMEOUT_MS); return; }
437
+ if (message.type === "heartbeat") { lastHeartbeatAt = performance.now(); return; }
428
438
  if (message.type === "result") { encoded(message.value); finish(); resolve(message.value ?? null); return; }
429
439
  if (message.type === "error") { finish(); reject(workflowErrorFromWorker(message.error ?? { code: "INTERNAL_ERROR", message: "Worker failed" })); return; }
430
440
  if (message.type === "rpc" && message.id !== undefined) void handleRpc(message.id, message.method ?? "", message.args ?? []);
@@ -439,7 +449,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
439
449
  setTimeout(() => { if (!child.killed) child.kill("SIGKILL"); }, 1000).unref();
440
450
  }
441
451
  }
442
- function finish() { settled = true; clearTimeout(watchdog); signal?.removeEventListener("abort", cancel); killChild(); rmSync(childDir, { recursive: true, force: true }); }
452
+ function finish() { settled = true; clearInterval(watchdog); signal?.removeEventListener("abort", cancel); killChild(); rmSync(childDir, { recursive: true, force: true }); }
443
453
  function stop(code: WorkflowErrorCode, message: string) { if (settled) return; controller.abort(); finish(); rejectResult(new WorkflowError(code, message)); }
444
454
  function branded(result: Record<string, JsonValue>): JsonValue { return { ...result, [WORK_RESULT_BRAND]: true }; }
445
455
  async function handleRpc(id: number, method: string, values: JsonValue[]) {
@@ -520,18 +530,19 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
520
530
  if (signal?.aborted) cancel(); else signal?.addEventListener("abort", cancel, { once: true });
521
531
  return { result, cancel };
522
532
  }
523
- function nativeSessionReference(attempt: Pick<AgentAttempt, "sessionId" | "sessionFile">): { sessionId: string; sessionFile: string } {
524
- return { sessionId: attempt.sessionId, sessionFile: attempt.sessionFile };
533
+ function attemptSummary(attempt: AgentAttempt, accounting = attempt.accounting): AgentAttemptSummary {
534
+ return { attempt: attempt.attempt, transport: attempt.transport, ...(attempt.session ? { session: attempt.session } : {}), ...(attempt.error ? { error: attempt.error } : {}), accounting, setup: attempt.setup };
525
535
  }
526
-
527
- export async function persistActiveAgentAttempt(store: RunStore, id: string, active: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">): Promise<void> {
536
+ export async function persistActiveAgentAttempt(store: RunStore, id: string, active: AgentAttempt): Promise<void> {
528
537
  await store.updateState((run) => {
529
538
  const agent = run.agents.find((candidate) => candidate.id === id);
530
539
  if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
531
540
  const accounting = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
532
- const details = [...(agent.attemptDetails ?? []).filter((candidate) => candidate.attempt !== active.attempt), { ...active, accounting }];
533
- const nativeSessions = run.nativeSessions.some(({ sessionId }) => sessionId === active.sessionId) ? run.nativeSessions : [...run.nativeSessions, nativeSessionReference(active)];
534
- return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: Math.max(candidate.attempts, active.attempt), attemptDetails: details } : candidate), nativeSessions };
541
+ const detail = attemptSummary(active, accounting);
542
+ const details = [...(agent.attemptDetails ?? []).filter((candidate) => candidate.attempt !== active.attempt), detail];
543
+ const session = active.session;
544
+ const agentSessions = session && !run.agentSessions.some(({ transport, sessionId }) => transport === session.transport && sessionId === session.sessionId) ? [...run.agentSessions, session] : run.agentSessions;
545
+ return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: Math.max(candidate.attempts, active.attempt), attemptDetails: details } : candidate), agentSessions };
535
546
  });
536
547
  }
537
548
 
@@ -540,9 +551,10 @@ export async function persistAgentAttempts(store: RunStore, id: string, attempts
540
551
  const agent = run.agents.find((candidate) => candidate.id === id);
541
552
  if (!agent) throw new WorkflowError("INTERNAL_ERROR", `Missing production ownership record: ${id}`);
542
553
  const total = attempts.reduce((sum, attempt) => ({ input: sum.input + attempt.accounting.input, output: sum.output + attempt.accounting.output, cacheRead: sum.cacheRead + attempt.accounting.cacheRead, cacheWrite: sum.cacheWrite + attempt.accounting.cacheWrite, cost: sum.cost + attempt.accounting.cost }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
543
- const attemptDetails = attempts.map(({ attempt, sessionId, sessionFile, error, accounting, setup }) => ({ attempt, sessionId, sessionFile, ...(error ? { error } : {}), accounting, ...(setup ? { setup } : {}) }));
544
- const sessionIds = new Set(attempts.map(({ sessionId }) => sessionId));
545
- return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: attempts.length, attemptDetails, accounting: total } : candidate), nativeSessions: [...run.nativeSessions.filter(({ sessionId }) => !sessionIds.has(sessionId)), ...attempts.map((attempt) => nativeSessionReference(attempt))] };
554
+ const attemptDetails = attempts.map((attempt) => attemptSummary(attempt));
555
+ const sessions = attempts.map((attempt) => attempt.session).filter((session): session is WorkflowAgentSessionReference => session !== undefined);
556
+ const sessionKeys = new Set(sessions.map(({ transport, sessionId }) => `${transport}:${sessionId}`));
557
+ return { ...run, agents: run.agents.map((candidate) => candidate.id === id ? { ...candidate, attempts: attempts.length, attemptDetails, accounting: total } : candidate), agentSessions: [...run.agentSessions.filter(({ transport, sessionId }) => !sessionKeys.has(`${transport}:${sessionId}`)), ...sessions] };
546
558
  });
547
559
  }
548
560