@tt-a1i/openpi 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -143,18 +143,54 @@ export function readPersistedWorkflowDetails(
143
143
  runId: string,
144
144
  options: ReadPersistedRunOptions = {},
145
145
  ): WorkflowDetails | undefined {
146
- let details: WorkflowDetails | undefined;
146
+ const details = normalizeReadRecord(
147
+ runId,
148
+ readPersistedWorkflowRecord(runId),
149
+ );
150
+ if (!details) return undefined;
151
+ if (options.hydrateArtifacts) hydrateRunArtifacts(runId, details);
152
+ return details;
153
+ }
154
+
155
+ function normalizeReadRecord(runId: string, raw: unknown) {
156
+ try {
157
+ return normalizePersistedWorkflowDetails(runId, raw);
158
+ } catch {
159
+ return undefined;
160
+ }
161
+ }
162
+
163
+ function readPersistedWorkflowRecord(runId: string) {
147
164
  try {
148
165
  const raw: unknown = JSON.parse(
149
166
  fs.readFileSync(path.join(runsDir(), runId, "workflow.json"), "utf8"),
150
167
  );
151
- details = normalizePersistedWorkflowDetails(runId, raw);
168
+ return raw && typeof raw === "object"
169
+ ? (raw as Record<string, unknown>)
170
+ : undefined;
152
171
  } catch {
153
172
  return undefined;
154
173
  }
155
- if (!details) return undefined;
156
- if (options.hydrateArtifacts) hydrateRunArtifacts(runId, details);
157
- return details;
174
+ }
175
+
176
+ function matchesRunScope(
177
+ record: { startedAt?: unknown; finishedAt?: unknown; sessionId?: unknown },
178
+ runId: string,
179
+ sessionId: string,
180
+ referencedRunIds: ReadonlySet<string>,
181
+ startedSince: number,
182
+ fromRetention = false,
183
+ ) {
184
+ const touchedAt = Math.max(
185
+ typeof record.startedAt === "number" ? record.startedAt : 0,
186
+ typeof record.finishedAt === "number" ? record.finishedAt : 0,
187
+ );
188
+ return (
189
+ touchedAt >= startedSince &&
190
+ (fromRetention ||
191
+ record.sessionId === sessionId ||
192
+ referencedRunIds.has(runId))
193
+ );
158
194
  }
159
195
 
160
196
  function isWorktreeCleanup(
@@ -581,27 +617,41 @@ export function loadRunEntries(
581
617
  retained: ReadonlyMap<string, WorkflowDetails> = new Map(),
582
618
  ): RunEntry[] {
583
619
  const entries: RunEntry[] = [];
584
- const runIds = new Set([...listPersistedRunIds(), ...retained.keys()]);
620
+ const runIds = new Set([
621
+ ...listPersistedRunIds(),
622
+ ...retained.keys(),
623
+ ...active.keys(),
624
+ ]);
585
625
  for (const runId of runIds) {
586
626
  const live = active.get(runId);
587
627
  if (live) {
588
628
  entries.push({ runId, details: live, live: true });
589
629
  continue;
590
630
  }
591
- const persisted = readPersistedWorkflowDetails(runId, {
592
- hydrateArtifacts: true,
593
- });
631
+ // Reject unrelated history before normalizing potentially large inline
632
+ // transcripts. Side artifacts belong to explicit detail navigation.
633
+ const raw = readPersistedWorkflowRecord(runId);
634
+ if (
635
+ raw &&
636
+ !matchesRunScope(raw, runId, sessionId, referencedRunIds, startedSince)
637
+ ) {
638
+ continue;
639
+ }
640
+ const persisted = normalizeReadRecord(runId, raw);
594
641
  const retainedDetails = retained.get(runId);
595
642
  const details = persisted ?? retainedDetails;
596
643
  if (!details) continue;
597
644
  const fromRetention =
598
645
  persisted === undefined && retainedDetails !== undefined;
599
- const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
600
646
  if (
601
- touchedAt < startedSince ||
602
- (!fromRetention &&
603
- details.sessionId !== sessionId &&
604
- !referencedRunIds.has(runId))
647
+ !matchesRunScope(
648
+ details,
649
+ runId,
650
+ sessionId,
651
+ referencedRunIds,
652
+ startedSince,
653
+ fromRetention,
654
+ )
605
655
  ) {
606
656
  continue;
607
657
  }
@@ -718,6 +768,9 @@ type DetailFocus = "phases" | "agents";
718
768
  export class WorkflowDashboard {
719
769
  private view: View = "list";
720
770
  private entries: RunEntry[] = [];
771
+ private historyLoaded = false;
772
+ private seenRetainedRunIds = new Set<string>();
773
+ private hydratedRunIds = new Set<string>();
721
774
  private listIndex = 0;
722
775
  private phaseIndex = 0;
723
776
  private agentIndex = 0;
@@ -826,13 +879,68 @@ export class WorkflowDashboard {
826
879
 
827
880
  private refresh() {
828
881
  const selected = this.entries[this.listIndex]?.runId;
829
- this.entries = loadRunEntries(
830
- this.getActive(),
831
- this.sessionId,
832
- this.referencedRunIds,
833
- this.startedSince,
834
- this.getRetained(),
835
- );
882
+ const active = this.getActive();
883
+ const retained = this.getRetained();
884
+ if (!this.historyLoaded) {
885
+ this.entries = loadRunEntries(
886
+ active,
887
+ this.sessionId,
888
+ this.referencedRunIds,
889
+ this.startedSince,
890
+ retained,
891
+ );
892
+ this.historyLoaded = true;
893
+ } else {
894
+ // Animation ticks reuse historical projections. Only a newly settled run
895
+ // needs one canonical read; stable frames never scan or reread history.
896
+ const entries = new Map(
897
+ this.entries.map((entry) => [entry.runId, entry]),
898
+ );
899
+ const settledIds = new Set([
900
+ ...this.entries
901
+ .filter((entry) => entry.live && !active.has(entry.runId))
902
+ .map((entry) => entry.runId),
903
+ ...[...retained.keys()].filter(
904
+ (runId) =>
905
+ !this.seenRetainedRunIds.has(runId) &&
906
+ !entries.has(runId) &&
907
+ !active.has(runId),
908
+ ),
909
+ ]);
910
+ for (const runId of settledIds) {
911
+ const persisted = readPersistedWorkflowDetails(runId);
912
+ const details =
913
+ persisted ?? retained.get(runId) ?? entries.get(runId)?.details;
914
+ if (!details) continue;
915
+ if (
916
+ !matchesRunScope(
917
+ details,
918
+ runId,
919
+ this.sessionId,
920
+ this.referencedRunIds,
921
+ this.startedSince,
922
+ !persisted,
923
+ )
924
+ ) {
925
+ entries.delete(runId);
926
+ continue;
927
+ }
928
+ // Recovery operates on a projection, never on the former live owner.
929
+ const recovered = recoverStaleWorkflowDetails({
930
+ ...details,
931
+ agents: details.agents.map((agent) => ({ ...agent })),
932
+ });
933
+ entries.set(runId, { runId, details: recovered, live: false });
934
+ this.hydratedRunIds.delete(runId);
935
+ }
936
+ for (const [runId, details] of active) {
937
+ entries.set(runId, { runId, details, live: true });
938
+ }
939
+ this.entries = [...entries.values()].sort(
940
+ (a, b) => b.details.startedAt - a.details.startedAt,
941
+ );
942
+ }
943
+ for (const runId of retained.keys()) this.seenRetainedRunIds.add(runId);
836
944
  if (selected) {
837
945
  const index = this.entries.findIndex((e) => e.runId === selected);
838
946
  if (index >= 0) this.listIndex = index;
@@ -847,6 +955,7 @@ export class WorkflowDashboard {
847
955
  );
848
956
  if (refreshed) this.current = refreshed;
849
957
  }
958
+ if (this.view === "transcript") this.hydrateCurrent();
850
959
  if (this.notice && Date.now() - this.noticeAt > NOTICE_TTL_MS)
851
960
  this.notice = undefined;
852
961
  }
@@ -882,7 +991,15 @@ export class WorkflowDashboard {
882
991
  this.agentIndex = Math.min(this.agentIndex, Math.max(0, agents.length - 1));
883
992
  }
884
993
 
994
+ private hydrateCurrent() {
995
+ const entry = this.current;
996
+ if (!entry || entry.live || this.hydratedRunIds.has(entry.runId)) return;
997
+ hydrateRunArtifacts(entry.runId, entry.details);
998
+ this.hydratedRunIds.add(entry.runId);
999
+ }
1000
+
885
1001
  private saveReport() {
1002
+ this.hydrateCurrent();
886
1003
  const entry = this.current;
887
1004
  if (!entry) return;
888
1005
  const target = path.join(runsDir(), entry.runId, "report.md");
@@ -1023,6 +1140,7 @@ export class WorkflowDashboard {
1023
1140
  }
1024
1141
 
1025
1142
  private openTranscriptPage() {
1143
+ this.hydrateCurrent();
1026
1144
  const transcriptAdapter = new WorkflowTranscriptAdapter();
1027
1145
  this.view = "transcript";
1028
1146
  this.transcriptPage = new AgentSessionPage(
@@ -53,7 +53,11 @@ import {
53
53
  formatActivityStatus,
54
54
  } from "../shared/activity-status.ts";
55
55
  import { fitNavigationSides } from "../shared/below-editor-navigation.ts";
56
- import { waitBounded } from "../shared/child-session.ts";
56
+ import {
57
+ inheritedChildToolAllowlist,
58
+ resolveStandaloneChildProjectTrust,
59
+ waitBounded,
60
+ } from "../shared/child-session.ts";
57
61
  import { contextPercent } from "../shared/context-utilization.ts";
58
62
  import { completionOwnerFor } from "../shared/completion-inbox.ts";
59
63
  import {
@@ -544,6 +548,7 @@ interface ScriptAgentResult {
544
548
  }
545
549
 
546
550
  interface AgentCallOptions {
551
+ working_dir?: unknown;
547
552
  agent_type?: unknown;
548
553
  label?: unknown;
549
554
  phase?: unknown;
@@ -1339,11 +1344,12 @@ export default function workflows(
1339
1344
  structured: boolean,
1340
1345
  cwd: string,
1341
1346
  agentTypePrompt?: string,
1347
+ childProjectTrusted = projectTrusted,
1342
1348
  ) =>
1343
1349
  createWorkflowResources(
1344
1350
  cwd,
1345
1351
  structured ? "structured" : "plain",
1346
- projectTrusted,
1352
+ childProjectTrusted,
1347
1353
  agentTypePrompt,
1348
1354
  );
1349
1355
 
@@ -1638,6 +1644,36 @@ export default function workflows(
1638
1644
  );
1639
1645
  }
1640
1646
 
1647
+ const childTools = inheritedChildToolAllowlist(
1648
+ pi.getActiveTools(),
1649
+ agentType?.tools,
1650
+ );
1651
+ if (
1652
+ opts.working_dir !== undefined &&
1653
+ (typeof opts.working_dir !== "string" || !opts.working_dir.trim())
1654
+ ) {
1655
+ return fail(
1656
+ `agent "${label}": working_dir must be a non-empty string`,
1657
+ );
1658
+ }
1659
+ const requestedCwd = path.resolve(
1660
+ ctx.cwd,
1661
+ typeof opts.working_dir === "string" ? opts.working_dir : ".",
1662
+ );
1663
+ try {
1664
+ if (!fs.statSync(requestedCwd).isDirectory())
1665
+ throw new Error("not a directory");
1666
+ } catch {
1667
+ return fail(
1668
+ `agent "${label}": working_dir is not a directory: ${requestedCwd}`,
1669
+ );
1670
+ }
1671
+ const childProjectTrusted = resolveStandaloneChildProjectTrust({
1672
+ parentCwd: ctx.cwd,
1673
+ childCwd: requestedCwd,
1674
+ parentTrusted: projectTrusted,
1675
+ });
1676
+
1641
1677
  const explicitModel =
1642
1678
  typeof opts.model === "string" && opts.model.trim()
1643
1679
  ? opts.model.trim()
@@ -1708,11 +1744,14 @@ export default function workflows(
1708
1744
  const operatorFingerprint = operatorKey
1709
1745
  ? agentCallKey("workflow-operator", {
1710
1746
  execution: {
1747
+ cwd: requestedCwd,
1748
+ projectTrusted: childProjectTrusted,
1749
+ tools: childTools,
1711
1750
  agentType: agentType
1712
1751
  ? {
1713
1752
  name: agentType.name,
1714
1753
  body: agentType.body,
1715
- tools: agentType.tools,
1754
+ tools: childTools,
1716
1755
  }
1717
1756
  : undefined,
1718
1757
  model: model ? `${model.provider}/${model.id}` : undefined,
@@ -1732,7 +1771,7 @@ export default function workflows(
1732
1771
  const replaySafe =
1733
1772
  operatorKey === undefined &&
1734
1773
  isReplaySafeAgentCall({
1735
- tools: agentType?.tools,
1774
+ tools: agentType?.tools === undefined ? undefined : childTools,
1736
1775
  isolation: opts.isolation,
1737
1776
  });
1738
1777
  const replayLease = beginProcessReplayWorkspaceLease(replaySafe);
@@ -1744,13 +1783,14 @@ export default function workflows(
1744
1783
  try {
1745
1784
  replayResources = await getResources(
1746
1785
  effectiveSchema !== undefined,
1747
- ctx.cwd,
1786
+ requestedCwd,
1748
1787
  agentType?.body,
1788
+ childProjectTrusted,
1749
1789
  );
1750
1790
  replayIdentity = createReplayIdentity(
1751
- ctx.cwd,
1791
+ requestedCwd,
1752
1792
  replayResources.loader,
1753
- projectTrusted,
1793
+ childProjectTrusted,
1754
1794
  );
1755
1795
  } catch {
1756
1796
  // Fingerprinting is an optimization boundary. If resources cannot
@@ -1768,7 +1808,7 @@ export default function workflows(
1768
1808
  ? {
1769
1809
  name: agentType.name,
1770
1810
  body: agentType.body,
1771
- tools: agentType.tools,
1811
+ tools: childTools,
1772
1812
  }
1773
1813
  : undefined,
1774
1814
  model: model ? `${model.provider}/${model.id}` : undefined,
@@ -1925,7 +1965,7 @@ export default function workflows(
1925
1965
  );
1926
1966
  }
1927
1967
  const created = await createWorktree({
1928
- cwd: ctx.cwd,
1968
+ cwd: requestedCwd,
1929
1969
  label,
1930
1970
  id: `${details.runId}-${record.index}`,
1931
1971
  });
@@ -1937,18 +1977,19 @@ export default function workflows(
1937
1977
  worktree = created.worktree;
1938
1978
  if (!runSettled) record.worktreeBranch = worktree.branch;
1939
1979
  }
1940
- if (runSignal.aborted || runSettled) {
1941
- throw runSignal.reason instanceof Error
1942
- ? runSignal.reason
1943
- : new Error("Workflow was aborted");
1944
- }
1945
- const agentCwd = worktree?.path ?? ctx.cwd;
1980
+ const agentCwd = worktree?.path ?? requestedCwd;
1946
1981
 
1947
1982
  // Inside the try, not before it: building resources can throw
1948
1983
  // (bad settings, an unreadable skills dir), and a throw out here
1949
1984
  // would skip the finally and leak the worktree permanently —
1950
1985
  // nothing sweeps `.git/pi-worktrees/` afterwards.
1951
1986
  try {
1987
+ if (runSignal.aborted || runSettled) {
1988
+ throw runSignal.reason instanceof Error
1989
+ ? runSignal.reason
1990
+ : new Error("Workflow was aborted");
1991
+ }
1992
+
1952
1993
  let rejectResourceLoad: (() => void) | undefined;
1953
1994
  const resourceAbort = new Promise<never>((_resolve, reject) => {
1954
1995
  rejectResourceLoad = () =>
@@ -1968,6 +2009,7 @@ export default function workflows(
1968
2009
  effectiveSchema !== undefined,
1969
2010
  agentCwd,
1970
2011
  agentType?.body,
2012
+ childProjectTrusted,
1971
2013
  ),
1972
2014
  resourceAbort,
1973
2015
  ]).finally(() => {
@@ -1992,7 +2034,7 @@ export default function workflows(
1992
2034
  settingsManager: resources.settingsManager,
1993
2035
  ...(sessionManager ? { sessionManager } : {}),
1994
2036
  modelRegistry: ctx.modelRegistry,
1995
- ...(agentType?.tools ? { tools: agentType.tools } : {}),
2037
+ tools: childTools,
1996
2038
  ...(testAgentSessionFactory
1997
2039
  ? { sessionFactory: testAgentSessionFactory }
1998
2040
  : {}),
@@ -2108,9 +2150,9 @@ export default function workflows(
2108
2150
  // unfingerprintable calls always run for real.
2109
2151
  const completedIdentity = callKey
2110
2152
  ? createReplayIdentity(
2111
- ctx.cwd,
2153
+ requestedCwd,
2112
2154
  resources.loader,
2113
- projectTrusted,
2155
+ childProjectTrusted,
2114
2156
  )
2115
2157
  : undefined;
2116
2158
  const completedKey = completedIdentity
@@ -2157,7 +2199,7 @@ export default function workflows(
2157
2199
  runId: details.runId,
2158
2200
  agentIndex: record.index,
2159
2201
  agentLabel: record.label,
2160
- repoCwd: ctx.cwd,
2202
+ repoCwd: requestedCwd,
2161
2203
  worktree,
2162
2204
  });
2163
2205
  let cleanup: WorktreeCleanup;
@@ -2174,7 +2216,7 @@ export default function workflows(
2174
2216
  const reclaimer =
2175
2217
  workflowLifecycleTestHooks?.reclaimWorktree ??
2176
2218
  reclaimWorktree;
2177
- cleanup = await reclaimer(ctx.cwd, worktree).catch(
2219
+ cleanup = await reclaimer(requestedCwd, worktree).catch(
2178
2220
  (error): WorktreeCleanup => ({
2179
2221
  removed: false,
2180
2222
  branchDeleted: false,
@@ -250,8 +250,11 @@ export class AgentProgressProjection {
250
250
  snapshot(
251
251
  toolTimings: ReadonlyMap<string, ProgressToolTiming> = new Map(),
252
252
  ): AgentProgressProjectionSnapshot {
253
+ // Reserve the initial task, then spend the remaining byte budget on the
254
+ // newest evidence. Forward selection would silently discard final errors
255
+ // after enough large tool results, even below the entry-count limit.
253
256
  const selected = this.firstEntry
254
- ? [this.firstEntry, ...this.tailEntries]
257
+ ? [this.firstEntry, ...this.tailEntries.slice().reverse()]
255
258
  : [];
256
259
  const transcript: TranscriptEntry[] = [];
257
260
  let totalBytes = 0;
@@ -277,6 +280,9 @@ export class AgentProgressProjection {
277
280
  : { timestamp: entry.timestamp }),
278
281
  });
279
282
  }
283
+ // Budgeting order is not display order: keep the retained tail chronological.
284
+ const newestFirstTail = transcript.splice(1);
285
+ transcript.push(...newestFirstTail.reverse());
280
286
  if (transcript.length < this.totalEntries) {
281
287
  transcript.push({
282
288
  role: "toolResult",
@@ -50,7 +50,6 @@ import { truncateUtf8 } from "./serialization.ts";
50
50
  import { bindWorkflowToolRenderer } from "./tool-renderer.ts";
51
51
 
52
52
  const AGENT_OUTPUT_MAX_BYTES = 64 * 1024;
53
- export const MODEL_PROGRESS_TIMEOUT_MS = 45_000;
54
53
 
55
54
  export type WorkflowModel = NonNullable<ExtensionContext["model"]>;
56
55
  export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
@@ -111,8 +110,6 @@ export interface RunAgentOptions {
111
110
  replayFilesystemBoundary?: ReplayFilesystemBoundaryOptions;
112
111
  /** Test-only override for the per-tool execution timeout. */
113
112
  toolCallTimeoutMs?: number;
114
- /** Test-only override for the per-provider-turn model-progress timeout. */
115
- modelProgressTimeoutMs?: number;
116
113
  /** Test-only override for the end-to-end abort/shutdown deadline. */
117
114
  shutdownTimeoutMs?: number;
118
115
  /** Test seam for lifecycle races; production always uses createAgentSession. */
@@ -245,119 +242,6 @@ function errorText(error: unknown): string {
245
242
  );
246
243
  }
247
244
 
248
- function formatTimeout(timeoutMs: number) {
249
- return timeoutMs % 1_000 === 0
250
- ? `${timeoutMs / 1_000} seconds`
251
- : `${timeoutMs} ms`;
252
- }
253
-
254
- export function resolveModelProgressTimeoutMs(
255
- settingsManager: SettingsManager,
256
- override?: number,
257
- ) {
258
- if (override !== undefined) return override;
259
- const configured =
260
- settingsManager.getProjectSettings().httpIdleTimeoutMs ??
261
- settingsManager.getGlobalSettings().httpIdleTimeoutMs;
262
- return typeof configured === "number" && Number.isFinite(configured)
263
- ? Math.max(MODEL_PROGRESS_TIMEOUT_MS, Math.floor(configured))
264
- : MODEL_PROGRESS_TIMEOUT_MS;
265
- }
266
-
267
- /** Abort any provider turn that stops producing model-visible progress. */
268
- export function createModelProgressWatchdog(
269
- onTimeout: (error: Error) => Promise<unknown>,
270
- options: { timeoutMs?: number; model?: string } = {},
271
- ) {
272
- const timeoutMs = options.timeoutMs ?? MODEL_PROGRESS_TIMEOUT_MS;
273
- let timer: ReturnType<typeof setTimeout> | undefined;
274
- let activeTurn = false;
275
- let closed = false;
276
- let rejectTimeout!: (error: Error) => void;
277
- const timeout = new Promise<never>((_resolve, reject) => {
278
- rejectTimeout = reject;
279
- });
280
-
281
- const clear = () => {
282
- if (timer) clearTimeout(timer);
283
- timer = undefined;
284
- };
285
- const schedule = () => {
286
- clear();
287
- if (!activeTurn || closed) return;
288
- // This timer owns the awaited watchdog outcome. Keep it referenced so a
289
- // short-lived Node 22 process cannot exit with the promise still pending.
290
- timer = setTimeout(() => {
291
- timer = undefined;
292
- activeTurn = false;
293
- closed = true;
294
- const model = options.model ? ` for ${options.model}` : "";
295
- const error = new Error(
296
- `Agent provider turn${model} produced no model-visible progress for ${formatTimeout(timeoutMs)}; the provider request may be stalled. Retry the workflow.`,
297
- );
298
- rejectTimeout(error);
299
- try {
300
- void onTimeout(error).catch(() => {});
301
- } catch {
302
- // The timeout result remains authoritative even if abort throws before
303
- // returning its promise; bounded shutdown below gets another chance.
304
- }
305
- }, timeoutMs);
306
- };
307
- const armTurn = () => {
308
- if (closed) return;
309
- activeTurn = true;
310
- schedule();
311
- };
312
- const markProgress = () => {
313
- if (!activeTurn || closed) return;
314
- schedule();
315
- };
316
- const completeTurn = () => {
317
- activeTurn = false;
318
- clear();
319
- };
320
- const cancel = () => {
321
- closed = true;
322
- activeTurn = false;
323
- clear();
324
- };
325
-
326
- return {
327
- armTurn,
328
- markProgress,
329
- completeTurn,
330
- cancel,
331
- async waitFor<T>(operation: Promise<T>) {
332
- try {
333
- return await Promise.race([operation, timeout]);
334
- } finally {
335
- cancel();
336
- }
337
- },
338
- };
339
- }
340
-
341
- function isModelVisibleProgress(event: AgentSessionEvent) {
342
- if (event.type !== "message_update" || event.message.role !== "assistant") {
343
- return false;
344
- }
345
- // Raw transport heartbeats never become AgentSession events. Empty stream,
346
- // text, and thinking starts likewise cannot keep a provider turn alive.
347
- const update = event.assistantMessageEvent;
348
- if (
349
- update.type === "text_delta" ||
350
- update.type === "thinking_delta" ||
351
- update.type === "toolcall_delta"
352
- ) {
353
- return update.delta.length > 0;
354
- }
355
- if (update.type === "text_end" || update.type === "thinking_end") {
356
- return update.content.length > 0;
357
- }
358
- return update.type === "toolcall_start" || update.type === "toolcall_end";
359
- }
360
-
361
245
  export async function runAgent(
362
246
  options: RunAgentOptions,
363
247
  ): Promise<AgentOutcome> {
@@ -367,8 +251,6 @@ export async function runAgent(
367
251
  let session: AgentSession | undefined;
368
252
  let unsubscribeToolGuards: (() => void) | undefined;
369
253
  let aborted = false;
370
- let terminalCause: "abort" | "model-progress-timeout" | undefined;
371
- let modelProgressTimeoutMessage: string | undefined;
372
254
  let abortOperation: Promise<unknown> | undefined;
373
255
  let rejectForAbort: ((error: Error) => void) | undefined;
374
256
  let rejectForProjectionFailure: ((error: Error) => void) | undefined;
@@ -389,7 +271,6 @@ export async function runAgent(
389
271
  const onAbort = () => {
390
272
  if (aborted) return;
391
273
  aborted = true;
392
- terminalCause ??= "abort";
393
274
  if (session) {
394
275
  try {
395
276
  abortOperation ??= session.abort();
@@ -594,10 +475,6 @@ export async function runAgent(
594
475
  });
595
476
  };
596
477
 
597
- let armModelProgress = () => {};
598
- let markModelProgress = () => {};
599
- let completeModelTurn = () => {};
600
- let cancelModelProgressWatchdog = () => {};
601
478
  let compactionReconcileQueued = false;
602
479
  const queueCompactionReconcile = () => {
603
480
  if (compactionReconcileQueued) return;
@@ -625,7 +502,6 @@ export async function runAgent(
625
502
  };
626
503
  const unsubscribe = childSession.subscribe((event) => {
627
504
  if (settled) return;
628
- if (event.type === "turn_start") armModelProgress();
629
505
  if (event.type === "tool_execution_start") {
630
506
  toolRenderer.start(
631
507
  event.toolCallId,
@@ -648,10 +524,6 @@ export async function runAgent(
648
524
  event.isError,
649
525
  );
650
526
  }
651
- if (isModelVisibleProgress(event)) markModelProgress();
652
- if (event.type === "message_end" && event.message.role === "assistant") {
653
- completeModelTurn();
654
- }
655
527
  if (event.type === "message_end") {
656
528
  assistantSettlement = observeAssistantSettlement(
657
529
  assistantSettlement,
@@ -686,32 +558,10 @@ export async function runAgent(
686
558
  captureToolRenderData(childSession.messages);
687
559
  snapshotProjection();
688
560
  if (!aborted) {
689
- const watchdog = createModelProgressWatchdog(
690
- (error) => {
691
- terminalCause ??= "model-progress-timeout";
692
- if (terminalCause === "model-progress-timeout") {
693
- modelProgressTimeoutMessage ??= error.message;
694
- }
695
- abortOperation ??= childSession.abort();
696
- void abortOperation.catch(() => {});
697
- return abortOperation;
698
- },
699
- {
700
- timeoutMs: resolveModelProgressTimeoutMs(
701
- options.settingsManager,
702
- options.modelProgressTimeoutMs,
703
- ),
704
- model: modelId,
705
- },
706
- );
707
- armModelProgress = watchdog.armTurn;
708
- markModelProgress = watchdog.markProgress;
709
- completeModelTurn = watchdog.completeTurn;
710
- cancelModelProgressWatchdog = watchdog.cancel;
561
+ // Pi owns transport liveness and retries. Quiet model output is not
562
+ // evidence of a stalled request (thinking and retry backoff can be silent).
711
563
  await Promise.race([
712
- watchdog.waitFor(
713
- childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
714
- ),
564
+ childSession.prompt(buildWorkflowAgentPrompt(options.prompt)),
715
565
  abortRace,
716
566
  projectionFailureRace,
717
567
  ]);
@@ -719,7 +569,6 @@ export async function runAgent(
719
569
  } catch (error) {
720
570
  promptErrorMessage ??= errorText(error);
721
571
  } finally {
722
- cancelModelProgressWatchdog();
723
572
  options.signal?.removeEventListener("abort", onAbort);
724
573
  settled = true;
725
574
  unsubscribe();
@@ -758,11 +607,7 @@ export async function runAgent(
758
607
  ? `Cleanup failed: ${cleanupErrors.join("; ")}`
759
608
  : undefined;
760
609
 
761
- if (
762
- terminalCause === "abort" ||
763
- (terminalCause === undefined &&
764
- assistantSettlement?.stopReason === "aborted")
765
- ) {
610
+ if (aborted || assistantSettlement?.stopReason === "aborted") {
766
611
  return {
767
612
  ok: false,
768
613
  output,
@@ -779,9 +624,7 @@ export async function runAgent(
779
624
  }
780
625
 
781
626
  const failureMessage =
782
- (terminalCause === "model-progress-timeout"
783
- ? modelProgressTimeoutMessage
784
- : agentFailureMessage(assistantSettlement, promptErrorMessage)) ??
627
+ agentFailureMessage(assistantSettlement, promptErrorMessage) ??
785
628
  cleanupError;
786
629
  if (failureMessage !== undefined) {
787
630
  return {