@tt-a1i/openpi 0.1.0 → 0.2.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -389
  3. package/SETUP.md +24 -22
  4. package/THIRD_PARTY_NOTICES.md +3 -4
  5. package/assets/readme-hero-mobile.svg +2 -2
  6. package/assets/readme-hero.svg +10 -10
  7. package/extensions/ask-user/handoff.ts +5 -1
  8. package/extensions/ask-user/index.ts +44 -0
  9. package/extensions/background-terminals/index.ts +118 -29
  10. package/extensions/background-terminals/src/domain.ts +5 -1
  11. package/extensions/background-terminals/src/manager.ts +2 -1
  12. package/extensions/background-terminals/src/prompt.ts +35 -0
  13. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  14. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  15. package/extensions/capabilities/index.ts +198 -0
  16. package/extensions/context-pivot/index.ts +21 -0
  17. package/extensions/cron/index.ts +42 -15
  18. package/extensions/execution-convergence/active-evidence.ts +129 -0
  19. package/extensions/execution-convergence/index.ts +442 -0
  20. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  21. package/extensions/file-search/index.ts +8 -1
  22. package/extensions/file-search/src/binaries.ts +2 -1
  23. package/extensions/git-info/src/runtime.ts +1 -1
  24. package/extensions/goal/controller.ts +2 -1
  25. package/extensions/goal/index.ts +20 -1
  26. package/extensions/plan-mode/index.ts +12 -0
  27. package/extensions/setup/index.ts +241 -45
  28. package/extensions/setup/intercom-fs-helper.cjs +130 -0
  29. package/extensions/setup/intercom.ts +603 -0
  30. package/extensions/shared/child-session.ts +42 -5
  31. package/extensions/shared/setup-config.ts +27 -1
  32. package/extensions/shared/setup-episode-state.ts +7 -0
  33. package/extensions/shared/tool-surface.ts +435 -0
  34. package/extensions/subagents/index.ts +16 -1
  35. package/extensions/subagents/src/manager.ts +13 -11
  36. package/extensions/subagents/src/prompt.ts +1 -1
  37. package/extensions/tasks/index.ts +39 -12
  38. package/extensions/ui-customization/footer.ts +6 -1
  39. package/extensions/workflows/artifacts.ts +6 -1
  40. package/extensions/workflows/dashboard.ts +138 -27
  41. package/extensions/workflows/graph-projection.ts +240 -0
  42. package/extensions/workflows/handoff.ts +194 -0
  43. package/extensions/workflows/index.ts +258 -56
  44. package/extensions/workflows/invocation-ledger.ts +368 -0
  45. package/extensions/workflows/model.ts +57 -1
  46. package/extensions/workflows/operator.ts +131 -0
  47. package/extensions/workflows/prompt.ts +10 -38
  48. package/extensions/workflows/replay-safety.ts +9 -8
  49. package/extensions/workflows/runner.ts +10 -2
  50. package/extensions/workflows/sandbox.ts +5 -0
  51. package/package.json +15 -15
  52. package/skills/subagents/SKILL.md +6 -0
  53. package/skills/workflows/EXAMPLES.md +58 -0
  54. package/skills/workflows/REFERENCE.md +44 -0
  55. package/skills/workflows/SKILL.md +39 -0
@@ -38,6 +38,7 @@ import {
38
38
  getMarkdownTheme,
39
39
  keyHint,
40
40
  type ExtensionAPI,
41
+ type SessionManager,
41
42
  type ExtensionContext,
42
43
  } from "@earendil-works/pi-coding-agent";
43
44
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
@@ -45,6 +46,10 @@ import { Type, type Static } from "typebox";
45
46
  import { formatActivityStatus } from "../shared/activity-status.ts";
46
47
  import { waitBounded } from "../shared/child-session.ts";
47
48
  import { loadSetupConfig } from "../shared/setup-config.ts";
49
+ import {
50
+ OPENPI_TOOL_SURFACE,
51
+ patchOwnedTools,
52
+ } from "../shared/tool-surface.ts";
48
53
  import {
49
54
  loadAgentTypes,
50
55
  resolveAgentModel,
@@ -62,6 +67,17 @@ import {
62
67
  loadJournal,
63
68
  persistWorkflowJson,
64
69
  } from "./artifacts.ts";
70
+ import { createWorkflowHandoffRegistry } from "./handoff.ts";
71
+ import {
72
+ classifyInterruptedInvocation,
73
+ createInvocationIdentity,
74
+ requestInvocation,
75
+ transitionInvocation,
76
+ } from "./invocation-ledger.ts";
77
+ import {
78
+ normalizeWorkflowOperatorKey,
79
+ WorkflowOperatorRegistry,
80
+ } from "./operator.ts";
65
81
  import {
66
82
  agentCallKey,
67
83
  createReplayCache,
@@ -71,6 +87,7 @@ import {
71
87
  import { RunController } from "./controller.ts";
72
88
  import {
73
89
  normalizePersistedWorkflowDetails,
90
+ recoverStaleWorkflowDetails,
74
91
  sessionWorkflowRunIds,
75
92
  showWorkflowDashboard,
76
93
  } from "./dashboard.ts";
@@ -98,6 +115,7 @@ import {
98
115
  statusColor,
99
116
  statusWord,
100
117
  createUsageReader,
118
+ refreshWorkflowGraph,
101
119
  SQUARE,
102
120
  type AgentRecord,
103
121
  type WorkflowDetails,
@@ -166,6 +184,8 @@ interface ScriptAgentResult {
166
184
  ok: boolean;
167
185
  output: string;
168
186
  structured?: unknown;
187
+ /** Opaque same-run handle for bounded downstream handoff. */
188
+ ref?: string;
169
189
  acceptance?: AgentRecord["acceptance"];
170
190
  error?: string;
171
191
  }
@@ -180,6 +200,10 @@ interface AgentCallOptions {
180
200
  provider?: unknown;
181
201
  effort?: unknown;
182
202
  isolation?: unknown;
203
+ /** Reuse one in-memory child Session within this workflow run. */
204
+ operator?: unknown;
205
+ /** Same-run result refs to hydrate as bounded untrusted input data. */
206
+ inputs?: unknown;
183
207
  }
184
208
 
185
209
  const WorkflowParams = Type.Object({
@@ -401,6 +425,14 @@ export default function workflows(pi: ExtensionAPI) {
401
425
  [...activeRuns].map(([runId, run]) => [runId, run.details] as const),
402
426
  );
403
427
  const settledRuns = new Map<string, WorkflowDetails>();
428
+ const hideLifecycleTools = () =>
429
+ patchOwnedTools(pi, "workflows", {
430
+ disable: OPENPI_TOOL_SURFACE.workflows.deferred,
431
+ });
432
+ const showLifecycleTools = () =>
433
+ patchOwnedTools(pi, "workflows", {
434
+ enable: OPENPI_TOOL_SURFACE.workflows.deferred,
435
+ });
404
436
  const stripState = new WorkflowStripState();
405
437
  const widgetKey = "workflow-navigation";
406
438
 
@@ -557,6 +589,7 @@ export default function workflows(pi: ExtensionAPI) {
557
589
  };
558
590
 
559
591
  pi.on("session_start", (_event, ctx) => {
592
+ hideLifecycleTools();
560
593
  if (ctx.hasUI) lastContext = ctx;
561
594
  agentTypes = loadAgentTypes({
562
595
  agentDir: getAgentDir(),
@@ -745,6 +778,8 @@ export default function workflows(pi: ExtensionAPI) {
745
778
  workflowConfig.concurrency,
746
779
  workflowConfig.maxAgentCalls,
747
780
  );
781
+ const handoffs = createWorkflowHandoffRegistry();
782
+ const operators = new WorkflowOperatorRegistry();
748
783
 
749
784
  // Each concurrent child gets its own extension runtime. Children use the
750
785
  // parent's live trust decision; an isolated child gets its own cwd (its
@@ -779,6 +814,7 @@ export default function workflows(pi: ExtensionAPI) {
779
814
  };
780
815
  const emit = (checkpoint = true) => {
781
816
  if (runSettled) return;
817
+ refreshWorkflowGraph(details);
782
818
  if (checkpoint) persistence.checkpoint();
783
819
  if (emitTimer) return;
784
820
  emitTimer = setTimeout(
@@ -809,6 +845,16 @@ export default function workflows(pi: ExtensionAPI) {
809
845
  );
810
846
  for (const record of details.agents) {
811
847
  if (record.state !== "running") continue;
848
+ if (
849
+ record.invocation &&
850
+ record.invocation.executionState !== "settled" &&
851
+ record.invocation.executionState !== "uncertain"
852
+ ) {
853
+ record.invocation = classifyInterruptedInvocation(
854
+ record.invocation,
855
+ Date.now(),
856
+ );
857
+ }
812
858
  record.state = "error";
813
859
  record.error =
814
860
  record.error ?? "Agent did not settle before run cleanup";
@@ -816,6 +862,7 @@ export default function workflows(pi: ExtensionAPI) {
816
862
  }
817
863
  details.status = status;
818
864
  details.finishedAt = Date.now();
865
+ refreshWorkflowGraph(details);
819
866
  if (error) details.error = sanitizeWorkflowDisplayLine(error);
820
867
  return true;
821
868
  };
@@ -874,8 +921,15 @@ export default function workflows(pi: ExtensionAPI) {
874
921
  error: "Workflow was aborted before this agent started",
875
922
  };
876
923
  }
924
+ const startedAt = Date.now();
925
+ const callId = `${details.runId}:call:${index}`;
877
926
  const record: AgentRecord = {
878
927
  index,
928
+ callId,
929
+ invocation: requestInvocation(
930
+ createInvocationIdentity(details.runId, index),
931
+ startedAt,
932
+ ),
879
933
  label,
880
934
  phase:
881
935
  typeof opts.phase === "string"
@@ -884,20 +938,49 @@ export default function workflows(pi: ExtensionAPI) {
884
938
  state: "running",
885
939
  model: ctx.model?.id,
886
940
  contextWindow: ctx.model?.contextWindow,
887
- startedAt: Date.now(),
941
+ startedAt,
888
942
  preview: "",
889
943
  usage: emptyUsage(),
890
944
  transcript: [],
891
945
  };
892
946
  details.agents.push(record);
947
+ refreshWorkflowGraph(details);
893
948
  persistence.checkpoint({ immediate: true });
894
949
  emit(false);
895
950
 
896
951
  const fail = (error: string): ScriptAgentResult => {
897
952
  if (record.state === "running" && !runSettled) {
953
+ const at = Date.now();
954
+ if (
955
+ record.invocation?.admissionState === "pending" &&
956
+ record.invocation.executionState === "pending"
957
+ ) {
958
+ record.invocation = transitionInvocation(record.invocation, {
959
+ status: "rejected",
960
+ at,
961
+ });
962
+ } else if (
963
+ record.invocation?.admissionState === "claimed" &&
964
+ record.invocation.executionState === "running"
965
+ ) {
966
+ record.invocation = transitionInvocation(record.invocation, {
967
+ status: "settled",
968
+ outcome: "error",
969
+ at,
970
+ });
971
+ } else if (
972
+ record.invocation &&
973
+ record.invocation.executionState !== "settled" &&
974
+ record.invocation.executionState !== "uncertain"
975
+ ) {
976
+ record.invocation = classifyInterruptedInvocation(
977
+ record.invocation,
978
+ at,
979
+ );
980
+ }
898
981
  record.state = "error";
899
982
  record.error = sanitizeWorkflowDisplayLine(error);
900
- record.finishedAt = Date.now();
983
+ record.finishedAt = at;
901
984
  emit();
902
985
  }
903
986
  return { ok: false, output: "", error };
@@ -909,6 +992,45 @@ export default function workflows(pi: ExtensionAPI) {
909
992
  : String(promptValue ?? "");
910
993
  if (!basePrompt.trim())
911
994
  return fail("agent() requires a non-empty prompt string");
995
+
996
+ let operatorKey: string | undefined;
997
+ try {
998
+ if (opts.operator !== undefined) {
999
+ operatorKey = normalizeWorkflowOperatorKey(String(opts.operator));
1000
+ if (opts.isolation !== undefined) {
1001
+ throw new Error(
1002
+ "workflow operators cannot use per-call worktree isolation",
1003
+ );
1004
+ }
1005
+ record.operatorKey = operatorKey;
1006
+ }
1007
+ } catch (error) {
1008
+ return fail(`agent "${label}": ${errorText(error)}`);
1009
+ }
1010
+
1011
+ let inputRefs: string[] = [];
1012
+ let promptWithHandoffs = basePrompt;
1013
+ try {
1014
+ if (opts.inputs !== undefined) {
1015
+ if (
1016
+ !Array.isArray(opts.inputs) ||
1017
+ !opts.inputs.every((value) => typeof value === "string")
1018
+ ) {
1019
+ throw new Error(
1020
+ "inputs must be an array of workflow result refs",
1021
+ );
1022
+ }
1023
+ inputRefs = [...opts.inputs];
1024
+ }
1025
+ const entries = handoffs.resolveEntries(inputRefs);
1026
+ record.inputCallIds = entries.flatMap((entry) =>
1027
+ entry.callId ? [entry.callId] : [],
1028
+ );
1029
+ promptWithHandoffs = handoffs.appendToPrompt(basePrompt, inputRefs);
1030
+ } catch (error) {
1031
+ return fail(`agent "${label}": ${errorText(error)}`);
1032
+ }
1033
+
912
1034
  let acceptanceContract: ReturnType<typeof parseAcceptanceContract>;
913
1035
  let effectiveSchema: unknown;
914
1036
  try {
@@ -921,8 +1043,8 @@ export default function workflows(pi: ExtensionAPI) {
921
1043
  }
922
1044
  const prompt = buildWorkflowAgentPrompt(
923
1045
  acceptanceContract
924
- ? `${basePrompt}\n\n${acceptanceInstruction(acceptanceContract)}`
925
- : basePrompt,
1046
+ ? `${promptWithHandoffs}\n\n${acceptanceInstruction(acceptanceContract)}`
1047
+ : promptWithHandoffs,
926
1048
  );
927
1049
  if (controller.signal.aborted)
928
1050
  return fail("Workflow was aborted before this agent started");
@@ -1012,19 +1134,40 @@ export default function workflows(pi: ExtensionAPI) {
1012
1134
  }
1013
1135
  record.model = model?.id;
1014
1136
  record.contextWindow = model?.contextWindow;
1137
+ const operatorFingerprint = operatorKey
1138
+ ? agentCallKey("workflow-operator", {
1139
+ execution: {
1140
+ agentType: agentType
1141
+ ? {
1142
+ name: agentType.name,
1143
+ body: agentType.body,
1144
+ tools: agentType.tools,
1145
+ }
1146
+ : undefined,
1147
+ model: model ? `${model.provider}/${model.id}` : undefined,
1148
+ effort: thinkingLevel,
1149
+ structured: effectiveSchema !== undefined,
1150
+ },
1151
+ })
1152
+ : undefined;
1015
1153
 
1016
1154
  // Replay is deliberately narrower than execution: only a named type
1017
1155
  // whose effective tool allowlist is entirely known read-only can be
1018
1156
  // cached. General-purpose children inherit bash/edit/write, custom
1019
1157
  // tools have unknown effects, and worktrees have state a string result
1020
1158
  // cannot restore.
1021
- const replaySafe = isReplaySafeAgentCall({
1022
- tools: agentType?.tools,
1023
- isolation: opts.isolation,
1024
- });
1159
+ // A reused operator's later activation depends on prior in-memory
1160
+ // conversation state, which a cached string cannot reconstruct.
1161
+ const replaySafe =
1162
+ operatorKey === undefined &&
1163
+ isReplaySafeAgentCall({
1164
+ tools: agentType?.tools,
1165
+ isolation: opts.isolation,
1166
+ });
1025
1167
  const replayLease = beginProcessReplayWorkspaceLease(replaySafe);
1026
1168
  let replayResources:
1027
- Awaited<ReturnType<typeof getResources>> | undefined;
1169
+ | Awaited<ReturnType<typeof getResources>>
1170
+ | undefined;
1028
1171
  let replayIdentity: ReturnType<typeof createReplayIdentity> | undefined;
1029
1172
  if (replaySafe) {
1030
1173
  try {
@@ -1070,9 +1213,14 @@ export default function workflows(pi: ExtensionAPI) {
1070
1213
  const cached =
1071
1214
  callKey && replayLease.canReplay ? replay?.take(callKey) : undefined;
1072
1215
  if (cached) {
1216
+ const finishedAt = Date.now();
1217
+ record.invocation = transitionInvocation(record.invocation!, {
1218
+ status: "replayed",
1219
+ at: finishedAt,
1220
+ });
1073
1221
  record.state = "done";
1074
1222
  record.replayed = true;
1075
- record.finishedAt = Date.now();
1223
+ record.finishedAt = finishedAt;
1076
1224
  record.preview = sanitizeWorkflowDisplayText(
1077
1225
  cached.output,
1078
1226
  PREVIEW_LENGTH,
@@ -1083,6 +1231,16 @@ export default function workflows(pi: ExtensionAPI) {
1083
1231
  cached.structured,
1084
1232
  );
1085
1233
  }
1234
+ const ref = handoffs.register({
1235
+ callId,
1236
+ settled: true,
1237
+ ok: true,
1238
+ output: cached.output,
1239
+ ...(cached.structured !== undefined
1240
+ ? { structured: cached.structured }
1241
+ : {}),
1242
+ });
1243
+ if (ref) record.resultRef = ref;
1086
1244
  emit();
1087
1245
  // Re-journal so a chain of resumes keeps working: run C resuming from
1088
1246
  // B still finds what B replayed from A.
@@ -1094,12 +1252,24 @@ export default function workflows(pi: ExtensionAPI) {
1094
1252
  ...(cached.structured !== undefined
1095
1253
  ? { structured: cached.structured }
1096
1254
  : {}),
1255
+ ...(ref ? { ref } : {}),
1097
1256
  ...(record.acceptance ? { acceptance: record.acceptance } : {}),
1098
1257
  };
1099
1258
  }
1100
1259
 
1101
1260
  return controller
1102
1261
  .schedule(async (runSignal) => {
1262
+ const claimedAt = Date.now();
1263
+ record.invocation = transitionInvocation(record.invocation!, {
1264
+ status: "claimed",
1265
+ at: claimedAt,
1266
+ });
1267
+ refreshWorkflowGraph(details);
1268
+ persistence.checkpoint({ immediate: true });
1269
+ record.invocation = transitionInvocation(record.invocation, {
1270
+ status: "running",
1271
+ at: Date.now(),
1272
+ });
1103
1273
  record.model = model?.id;
1104
1274
  record.contextWindow = model?.contextWindow;
1105
1275
  emit();
@@ -1172,45 +1342,58 @@ export default function workflows(pi: ExtensionAPI) {
1172
1342
  if (runSettled) {
1173
1343
  throw new Error("Workflow was settled before agent creation");
1174
1344
  }
1175
- const outcome = await runAgent({
1176
- prompt,
1177
- schema: effectiveSchema,
1178
- model,
1179
- thinkingLevel,
1180
- // Replay-safe calls use the same canonical cwd as the
1181
- // identity and filesystem boundary, so a symlink spelling of
1182
- // the checkout cannot retarget relative tool paths.
1183
- cwd: replayIdentity?.cwd ?? agentCwd,
1184
- loader: resources.loader,
1185
- settingsManager: resources.settingsManager,
1186
- modelRegistry: ctx.modelRegistry,
1187
- ...(agentType?.tools ? { tools: agentType.tools } : {}),
1188
- ...(replayIdentity
1189
- ? {
1190
- replayFilesystemBoundary: {
1191
- repositoryRoot: replayIdentity.repositoryRoot,
1192
- cwd: replayIdentity.cwd,
1193
- onViolation: () => {
1194
- replayBoundaryViolated = true;
1345
+ const runChild = (sessionManager?: SessionManager) =>
1346
+ runAgent({
1347
+ prompt,
1348
+ schema: effectiveSchema,
1349
+ model,
1350
+ thinkingLevel,
1351
+ // Replay-safe calls use the same canonical cwd as the
1352
+ // identity and filesystem boundary, so a symlink spelling of
1353
+ // the checkout cannot retarget relative tool paths.
1354
+ cwd: replayIdentity?.cwd ?? agentCwd,
1355
+ loader: resources.loader,
1356
+ settingsManager: resources.settingsManager,
1357
+ ...(sessionManager ? { sessionManager } : {}),
1358
+ modelRegistry: ctx.modelRegistry,
1359
+ ...(agentType?.tools ? { tools: agentType.tools } : {}),
1360
+ ...(replayIdentity
1361
+ ? {
1362
+ replayFilesystemBoundary: {
1363
+ repositoryRoot: replayIdentity.repositoryRoot,
1364
+ cwd: replayIdentity.cwd,
1365
+ onViolation: () => {
1366
+ replayBoundaryViolated = true;
1367
+ },
1195
1368
  },
1196
- },
1197
- }
1198
- : {}),
1199
- signal: runSignal,
1200
- onProgress: (progress) => {
1201
- if (runSettled || record.state !== "running") return;
1202
- record.preview = sanitizeWorkflowDisplayText(
1203
- progress.preview,
1204
- PREVIEW_LENGTH,
1205
- );
1206
- record.usage = progress.usage;
1207
- record.model = progress.model ?? record.model;
1208
- record.contextWindow =
1209
- progress.contextWindow ?? record.contextWindow;
1210
- record.transcript = progress.transcript;
1211
- emit();
1212
- },
1213
- });
1369
+ }
1370
+ : {}),
1371
+ signal: runSignal,
1372
+ onProgress: (progress) => {
1373
+ if (runSettled || record.state !== "running") return;
1374
+ record.preview = sanitizeWorkflowDisplayText(
1375
+ progress.preview,
1376
+ PREVIEW_LENGTH,
1377
+ );
1378
+ record.usage = progress.usage;
1379
+ record.model = progress.model ?? record.model;
1380
+ record.contextWindow =
1381
+ progress.contextWindow ?? record.contextWindow;
1382
+ record.transcript = progress.transcript;
1383
+ emit();
1384
+ },
1385
+ });
1386
+ const outcome = operatorKey
1387
+ ? await operators.activate(
1388
+ {
1389
+ key: operatorKey,
1390
+ fingerprint: operatorFingerprint!,
1391
+ cwd: agentCwd,
1392
+ signal: runSignal,
1393
+ },
1394
+ runChild,
1395
+ )
1396
+ : await runChild();
1214
1397
 
1215
1398
  if (runSettled || record.state !== "running") {
1216
1399
  return {
@@ -1228,7 +1411,8 @@ export default function workflows(pi: ExtensionAPI) {
1228
1411
  outcome.output || record.preview,
1229
1412
  PREVIEW_LENGTH,
1230
1413
  );
1231
- record.finishedAt = Date.now();
1414
+ const finishedAt = Date.now();
1415
+ record.finishedAt = finishedAt;
1232
1416
  const judged = applyAcceptance({
1233
1417
  contract: acceptanceContract,
1234
1418
  structured: outcome.structured,
@@ -1238,12 +1422,27 @@ export default function workflows(pi: ExtensionAPI) {
1238
1422
  const acceptance = judged.ledger;
1239
1423
  if (acceptance) record.acceptance = acceptance;
1240
1424
  const outcomeOk = judged.ok;
1425
+ record.invocation = transitionInvocation(record.invocation!, {
1426
+ status: "settled",
1427
+ outcome: outcomeOk ? "success" : "error",
1428
+ at: finishedAt,
1429
+ });
1241
1430
  record.state = outcomeOk ? "done" : "error";
1242
1431
  if (outcomeOk) delete record.error;
1243
1432
  else
1244
1433
  record.error = judged.error
1245
1434
  ? sanitizeWorkflowDisplayLine(judged.error)
1246
1435
  : undefined;
1436
+ const ref = handoffs.register({
1437
+ callId,
1438
+ settled: true,
1439
+ ok: outcomeOk,
1440
+ output: outcome.output,
1441
+ ...(outcome.structured !== undefined
1442
+ ? { structured: outcome.structured }
1443
+ : {}),
1444
+ });
1445
+ if (ref) record.resultRef = ref;
1247
1446
  emit();
1248
1447
 
1249
1448
  // Only provably read-only successes with a complete, stable
@@ -1285,6 +1484,7 @@ export default function workflows(pi: ExtensionAPI) {
1285
1484
  ...(outcome.structured !== undefined
1286
1485
  ? { structured: outcome.structured }
1287
1486
  : {}),
1487
+ ...(ref ? { ref } : {}),
1288
1488
  ...(acceptance ? { acceptance } : {}),
1289
1489
  ...(record.error !== undefined ? { error: record.error } : {}),
1290
1490
  };
@@ -1379,12 +1579,16 @@ export default function workflows(pi: ExtensionAPI) {
1379
1579
  const settled = await controller.settle({
1380
1580
  abort: status !== "completed",
1381
1581
  });
1582
+ const operatorsSettled = await waitBounded(operators.close(), 1_000);
1382
1583
  if (runSettled) return;
1383
- if (!settled) {
1584
+ if (!settled || !operatorsSettled) {
1384
1585
  status = "failed";
1586
+ const cleanupError = !settled
1587
+ ? "agent shutdown deadline exceeded"
1588
+ : "workflow operator cleanup deadline exceeded";
1385
1589
  details.error = details.error
1386
- ? `${details.error}; agent shutdown deadline exceeded`
1387
- : "Agent shutdown deadline exceeded";
1590
+ ? `${details.error}; ${cleanupError}`
1591
+ : cleanupError[0]!.toUpperCase() + cleanupError.slice(1);
1388
1592
  }
1389
1593
  if (runSettled) return;
1390
1594
  terminalize(status, details.error);
@@ -1453,6 +1657,7 @@ export default function workflows(pi: ExtensionAPI) {
1453
1657
  // Session may be shutting down.
1454
1658
  }
1455
1659
  });
1660
+ showLifecycleTools();
1456
1661
  return {
1457
1662
  content: [
1458
1663
  {
@@ -1744,10 +1949,7 @@ export default function workflows(pi: ExtensionAPI) {
1744
1949
  // shutdown settle deadline.
1745
1950
  return {
1746
1951
  ok: true,
1747
- details:
1748
- details.status === "running"
1749
- ? { ...details, status: "aborted" as const }
1750
- : details,
1952
+ details: recoverStaleWorkflowDetails(details),
1751
1953
  } as const;
1752
1954
  } catch {
1753
1955
  return {