pi-agent-flow 2.2.14 → 2.2.16

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 (254) hide show
  1. package/README.md +12 -0
  2. package/dist/batch/apply-patch.d.ts +2 -1
  3. package/dist/batch/apply-patch.d.ts.map +1 -1
  4. package/dist/batch/batch-bash.d.ts.map +1 -1
  5. package/dist/batch/batch-bash.js +9 -43
  6. package/dist/batch/batch-bash.js.map +1 -1
  7. package/dist/batch/constants.d.ts +9 -1
  8. package/dist/batch/constants.d.ts.map +1 -1
  9. package/dist/batch/constants.js +7 -0
  10. package/dist/batch/constants.js.map +1 -1
  11. package/dist/batch/execute.d.ts +1 -13
  12. package/dist/batch/execute.d.ts.map +1 -1
  13. package/dist/batch/execute.js +87 -28
  14. package/dist/batch/execute.js.map +1 -1
  15. package/dist/batch/index.d.ts.map +1 -1
  16. package/dist/batch/index.js +16 -14
  17. package/dist/batch/index.js.map +1 -1
  18. package/dist/batch/render.d.ts.map +1 -1
  19. package/dist/batch/render.js.map +1 -1
  20. package/dist/batch/summary.d.ts +0 -6
  21. package/dist/batch/summary.d.ts.map +1 -1
  22. package/dist/batch/summary.js +3 -3
  23. package/dist/batch/summary.js.map +1 -1
  24. package/dist/batch/symbols.d.ts.map +1 -1
  25. package/dist/batch/symbols.js +3 -1
  26. package/dist/batch/symbols.js.map +1 -1
  27. package/dist/batch/truncate-output.d.ts +31 -0
  28. package/dist/batch/truncate-output.d.ts.map +1 -0
  29. package/dist/batch/truncate-output.js +120 -0
  30. package/dist/batch/truncate-output.js.map +1 -0
  31. package/dist/config/config.d.ts +2 -1
  32. package/dist/config/config.d.ts.map +1 -1
  33. package/dist/config/config.js +10 -24
  34. package/dist/config/config.js.map +1 -1
  35. package/dist/config/models.d.ts.map +1 -1
  36. package/dist/config/models.js +7 -5
  37. package/dist/config/models.js.map +1 -1
  38. package/dist/config/paths.d.ts +3 -0
  39. package/dist/config/paths.d.ts.map +1 -0
  40. package/dist/config/paths.js +9 -0
  41. package/dist/config/paths.js.map +1 -0
  42. package/dist/config/resolver-helpers.d.ts +44 -0
  43. package/dist/config/resolver-helpers.d.ts.map +1 -0
  44. package/dist/config/resolver-helpers.js +135 -0
  45. package/dist/config/resolver-helpers.js.map +1 -0
  46. package/dist/config/settings-resolver.d.ts +0 -6
  47. package/dist/config/settings-resolver.d.ts.map +1 -1
  48. package/dist/config/settings-resolver.js +27 -256
  49. package/dist/config/settings-resolver.js.map +1 -1
  50. package/dist/core2/snapshot.d.ts +0 -2
  51. package/dist/core2/snapshot.d.ts.map +1 -1
  52. package/dist/core2/snapshot.js +91 -24
  53. package/dist/core2/snapshot.js.map +1 -1
  54. package/dist/flow/agents.d.ts +1 -1
  55. package/dist/flow/agents.d.ts.map +1 -1
  56. package/dist/flow/agents.js +18 -9
  57. package/dist/flow/agents.js.map +1 -1
  58. package/dist/flow/audit-formatters.d.ts +20 -0
  59. package/dist/flow/audit-formatters.d.ts.map +1 -0
  60. package/dist/flow/audit-formatters.js +123 -0
  61. package/dist/flow/audit-formatters.js.map +1 -0
  62. package/dist/flow/complexity.d.ts +3 -2
  63. package/dist/flow/complexity.d.ts.map +1 -1
  64. package/dist/flow/complexity.js +1 -1
  65. package/dist/flow/complexity.js.map +1 -1
  66. package/dist/flow/continuation.d.ts.map +1 -1
  67. package/dist/flow/continuation.js +6 -4
  68. package/dist/flow/continuation.js.map +1 -1
  69. package/dist/flow/cycle-guard.d.ts +20 -0
  70. package/dist/flow/cycle-guard.d.ts.map +1 -0
  71. package/dist/flow/cycle-guard.js +56 -0
  72. package/dist/flow/cycle-guard.js.map +1 -0
  73. package/dist/flow/depth.d.ts +0 -1
  74. package/dist/flow/depth.d.ts.map +1 -1
  75. package/dist/flow/depth.js +3 -2
  76. package/dist/flow/depth.js.map +1 -1
  77. package/dist/flow/dump-io.d.ts +15 -0
  78. package/dist/flow/dump-io.d.ts.map +1 -0
  79. package/dist/flow/dump-io.js +169 -0
  80. package/dist/flow/dump-io.js.map +1 -0
  81. package/dist/flow/execute-single.d.ts +8 -0
  82. package/dist/flow/execute-single.d.ts.map +1 -0
  83. package/dist/flow/execute-single.js +132 -0
  84. package/dist/flow/execute-single.js.map +1 -0
  85. package/dist/flow/executor.d.ts +3 -62
  86. package/dist/flow/executor.d.ts.map +1 -1
  87. package/dist/flow/executor.js +36 -342
  88. package/dist/flow/executor.js.map +1 -1
  89. package/dist/flow/flow-args.d.ts +14 -0
  90. package/dist/flow/flow-args.d.ts.map +1 -0
  91. package/dist/flow/flow-args.js +119 -0
  92. package/dist/flow/flow-args.js.map +1 -0
  93. package/dist/flow/flow-live.d.ts +18 -0
  94. package/dist/flow/flow-live.d.ts.map +1 -0
  95. package/dist/flow/flow-live.js +46 -0
  96. package/dist/flow/flow-live.js.map +1 -0
  97. package/dist/flow/process-lifecycle.d.ts +28 -0
  98. package/dist/flow/process-lifecycle.d.ts.map +1 -0
  99. package/dist/flow/process-lifecycle.js +114 -0
  100. package/dist/flow/process-lifecycle.js.map +1 -0
  101. package/dist/flow/runner.d.ts +4 -54
  102. package/dist/flow/runner.d.ts.map +1 -1
  103. package/dist/flow/runner.js +55 -517
  104. package/dist/flow/runner.js.map +1 -1
  105. package/dist/flow/settings-command.d.ts +2 -24
  106. package/dist/flow/settings-command.d.ts.map +1 -1
  107. package/dist/flow/settings-command.js +14 -716
  108. package/dist/flow/settings-command.js.map +1 -1
  109. package/dist/flow/settings-handler.d.ts +9 -0
  110. package/dist/flow/settings-handler.d.ts.map +1 -0
  111. package/dist/flow/settings-handler.js +377 -0
  112. package/dist/flow/settings-handler.js.map +1 -0
  113. package/dist/flow/settings-items.d.ts +42 -0
  114. package/dist/flow/settings-items.d.ts.map +1 -0
  115. package/dist/flow/settings-items.js +380 -0
  116. package/dist/flow/settings-items.js.map +1 -0
  117. package/dist/flow/spawn-utils.d.ts +17 -0
  118. package/dist/flow/spawn-utils.d.ts.map +1 -0
  119. package/dist/flow/spawn-utils.js +40 -0
  120. package/dist/flow/spawn-utils.js.map +1 -0
  121. package/dist/flow/store.d.ts.map +1 -1
  122. package/dist/flow/store.js +2 -8
  123. package/dist/flow/store.js.map +1 -1
  124. package/dist/flow/transition.d.ts +0 -1
  125. package/dist/flow/transition.d.ts.map +1 -1
  126. package/dist/flow/transition.js +0 -3
  127. package/dist/flow/transition.js.map +1 -1
  128. package/dist/index.d.ts +3 -0
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +94 -57
  131. package/dist/index.js.map +1 -1
  132. package/dist/io/atomic-write.d.ts +10 -0
  133. package/dist/io/atomic-write.d.ts.map +1 -0
  134. package/dist/io/atomic-write.js +30 -0
  135. package/dist/io/atomic-write.js.map +1 -0
  136. package/dist/notify/notify.d.ts.map +1 -1
  137. package/dist/notify/notify.js +1 -4
  138. package/dist/notify/notify.js.map +1 -1
  139. package/dist/snapshot/cli-args.d.ts +1 -2
  140. package/dist/snapshot/cli-args.d.ts.map +1 -1
  141. package/dist/snapshot/runner-events.d.ts +10 -0
  142. package/dist/snapshot/runner-events.d.ts.map +1 -1
  143. package/dist/snapshot/runner-events.js +112 -30
  144. package/dist/snapshot/runner-events.js.map +1 -1
  145. package/dist/snapshot/structured-output.d.ts.map +1 -1
  146. package/dist/snapshot/structured-output.js +3 -1
  147. package/dist/snapshot/structured-output.js.map +1 -1
  148. package/dist/snapshot/trace-output.d.ts.map +1 -1
  149. package/dist/snapshot/trace-output.js +8 -4
  150. package/dist/snapshot/trace-output.js.map +1 -1
  151. package/dist/steering/sliding-prompt.d.ts.map +1 -1
  152. package/dist/steering/sliding-prompt.js +12 -15
  153. package/dist/steering/sliding-prompt.js.map +1 -1
  154. package/dist/steering/tool-utils.d.ts +3 -3
  155. package/dist/steering/tool-utils.d.ts.map +1 -1
  156. package/dist/steering/tool-utils.js +3 -3
  157. package/dist/steering/tool-utils.js.map +1 -1
  158. package/dist/tools/ask-user.js.map +1 -1
  159. package/dist/tools/timed-bash.d.ts.map +1 -1
  160. package/dist/tools/timed-bash.js +8 -3
  161. package/dist/tools/timed-bash.js.map +1 -1
  162. package/dist/tools/trace.d.ts +0 -1
  163. package/dist/tools/trace.d.ts.map +1 -1
  164. package/dist/tools/trace.js +79 -62
  165. package/dist/tools/trace.js.map +1 -1
  166. package/dist/tools/web-ops.d.ts +0 -49
  167. package/dist/tools/web-ops.d.ts.map +1 -1
  168. package/dist/tools/web-ops.js +9 -9
  169. package/dist/tools/web-ops.js.map +1 -1
  170. package/dist/tui/body-render.d.ts +31 -0
  171. package/dist/tui/body-render.d.ts.map +1 -0
  172. package/dist/tui/body-render.js +589 -0
  173. package/dist/tui/body-render.js.map +1 -0
  174. package/dist/tui/context-display.d.ts +18 -0
  175. package/dist/tui/context-display.d.ts.map +1 -0
  176. package/dist/tui/context-display.js +29 -0
  177. package/dist/tui/context-display.js.map +1 -0
  178. package/dist/tui/flow-colors.d.ts +6 -1
  179. package/dist/tui/flow-colors.d.ts.map +1 -1
  180. package/dist/tui/flow-colors.js +1 -0
  181. package/dist/tui/flow-colors.js.map +1 -1
  182. package/dist/tui/flow-live-state.d.ts +36 -0
  183. package/dist/tui/flow-live-state.d.ts.map +1 -0
  184. package/dist/tui/flow-live-state.js +53 -0
  185. package/dist/tui/flow-live-state.js.map +1 -0
  186. package/dist/tui/grouping.d.ts +27 -0
  187. package/dist/tui/grouping.d.ts.map +1 -0
  188. package/dist/tui/grouping.js +191 -0
  189. package/dist/tui/grouping.js.map +1 -0
  190. package/dist/tui/header.d.ts +25 -0
  191. package/dist/tui/header.d.ts.map +1 -0
  192. package/dist/tui/header.js +95 -0
  193. package/dist/tui/header.js.map +1 -0
  194. package/dist/tui/render-utils.d.ts +1 -0
  195. package/dist/tui/render-utils.d.ts.map +1 -1
  196. package/dist/tui/render-utils.js +4 -5
  197. package/dist/tui/render-utils.js.map +1 -1
  198. package/dist/tui/render.d.ts +19 -33
  199. package/dist/tui/render.d.ts.map +1 -1
  200. package/dist/tui/render.js +112 -1014
  201. package/dist/tui/render.js.map +1 -1
  202. package/dist/tui/scramble/animation-status.d.ts +5 -0
  203. package/dist/tui/scramble/animation-status.d.ts.map +1 -0
  204. package/dist/tui/scramble/animation-status.js +93 -0
  205. package/dist/tui/scramble/animation-status.js.map +1 -0
  206. package/dist/tui/scramble/constants.d.ts +0 -4
  207. package/dist/tui/scramble/constants.d.ts.map +1 -1
  208. package/dist/tui/scramble/constants.js +3 -3
  209. package/dist/tui/scramble/constants.js.map +1 -1
  210. package/dist/tui/scramble/dynamic-text.d.ts +10 -0
  211. package/dist/tui/scramble/dynamic-text.d.ts.map +1 -0
  212. package/dist/tui/scramble/dynamic-text.js +19 -0
  213. package/dist/tui/scramble/dynamic-text.js.map +1 -0
  214. package/dist/tui/scramble/index.d.ts +8 -2
  215. package/dist/tui/scramble/index.d.ts.map +1 -1
  216. package/dist/tui/scramble/index.js +8 -1
  217. package/dist/tui/scramble/index.js.map +1 -1
  218. package/dist/tui/scramble/lifecycle.d.ts +4 -0
  219. package/dist/tui/scramble/lifecycle.d.ts.map +1 -0
  220. package/dist/tui/scramble/lifecycle.js +75 -0
  221. package/dist/tui/scramble/lifecycle.js.map +1 -0
  222. package/dist/tui/scramble/line-state.d.ts +5 -0
  223. package/dist/tui/scramble/line-state.d.ts.map +1 -0
  224. package/dist/tui/scramble/line-state.js +99 -0
  225. package/dist/tui/scramble/line-state.js.map +1 -0
  226. package/dist/tui/scramble/manager.d.ts +3 -14
  227. package/dist/tui/scramble/manager.d.ts.map +1 -1
  228. package/dist/tui/scramble/manager.js +22 -405
  229. package/dist/tui/scramble/manager.js.map +1 -1
  230. package/dist/tui/scramble/singleton.d.ts +3 -0
  231. package/dist/tui/scramble/singleton.d.ts.map +1 -0
  232. package/dist/tui/scramble/singleton.js +3 -0
  233. package/dist/tui/scramble/singleton.js.map +1 -0
  234. package/dist/tui/scramble/timer.d.ts +6 -0
  235. package/dist/tui/scramble/timer.d.ts.map +1 -0
  236. package/dist/tui/scramble/timer.js +28 -0
  237. package/dist/tui/scramble/timer.js.map +1 -0
  238. package/dist/tui/scramble/utils.d.ts +0 -2
  239. package/dist/tui/scramble/utils.d.ts.map +1 -1
  240. package/dist/tui/scramble/utils.js +0 -9
  241. package/dist/tui/scramble/utils.js.map +1 -1
  242. package/dist/tui/scramble/value-flash.d.ts +6 -0
  243. package/dist/tui/scramble/value-flash.d.ts.map +1 -0
  244. package/dist/tui/scramble/value-flash.js +141 -0
  245. package/dist/tui/scramble/value-flash.js.map +1 -0
  246. package/dist/tui/traces.d.ts +13 -0
  247. package/dist/tui/traces.d.ts.map +1 -0
  248. package/dist/tui/traces.js +97 -0
  249. package/dist/tui/traces.js.map +1 -0
  250. package/dist/types/ui.d.ts +0 -4
  251. package/dist/types/ui.d.ts.map +1 -1
  252. package/dist/types/ui.js +1 -30
  253. package/dist/types/ui.js.map +1 -1
  254. package/package.json +5 -1
@@ -9,12 +9,21 @@ import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import { getFlowTier } from "./agents.js";
12
- import { getInheritedCliArgs } from "../snapshot/cli-args.js";
13
- import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate, drainToolCallEstimate, drainCtxEstimate, updateSmoothedTps, drainSmoothedTps } from "../snapshot/runner-events.js";
12
+ import { processFlowJsonLine, drainStreamingText, drainStreamingEstimate, drainToolCallEstimate, drainCtxEstimate, updateSmoothedTps, drainSmoothedTps, getCtxState } from "../snapshot/runner-events.js";
14
13
  import { emptyFlowUsage, getFlowOutput, normalizeFlowResult, } from "../types/flow.js";
14
+ import { parseSharedContext } from "../core2/snapshot.js";
15
15
  import { extractStructuredOutput, generateCommandsFromHistory } from "../snapshot/structured-output.js";
16
+ import { computeInitialContextTokens, mergeStreamingContextTokens } from "../tui/context-display.js";
16
17
  import { logWarn, logError } from '../config/log.js';
18
+ import { atomicWriteFileSync } from "../io/atomic-write.js";
17
19
  import { DEFAULT_COMPLEXITY, getComplexityTimeoutMs } from "./complexity.js";
20
+ import { makeUniqueDumpPath, makeUniqueDumpTxtPath, cleanupStaleDumps, cleanupStaleDebugDumps, writeReminderFile, getDebugDir, resolveDumpMaxAgeHours, FLOW_DUMP_SNAPSHOT_ENV, } from "./dump-io.js";
21
+ import { registerChildGroup, unregisterChildGroup, terminateChildProcess, SIGKILL_TIMEOUT_MS, isWindows, } from "./process-lifecycle.js";
22
+ import { buildFlowArgs, inheritedCliArgs } from "./flow-args.js";
23
+ import { resolveFlowSpawn, writeFlowSessionToTempFile, cleanupFlowTempDir } from "./spawn-utils.js";
24
+ export { cleanupStaleDumps } from "./dump-io.js";
25
+ export { terminateAllChildGroups } from "./process-lifecycle.js";
26
+ export { buildFlowArgs, getOptimizedTools } from "./flow-args.js";
18
27
  function getEnvInt(name, fallback) {
19
28
  const raw = process.env[name];
20
29
  if (!raw)
@@ -22,81 +31,27 @@ function getEnvInt(name, fallback) {
22
31
  const parsed = parseInt(raw, 10);
23
32
  return Number.isFinite(parsed) ? parsed : fallback;
24
33
  }
25
- const isWindows = process.platform === "win32";
26
- const SIGKILL_TIMEOUT_MS = getEnvInt("PI_FLOW_SIGKILL_TIMEOUT_MS", 5000);
27
- const FINISH_KILL_GRACE_MS = getEnvInt("PI_FLOW_FINISH_KILL_GRACE_MS", 5_000); // wait 5s after finish() before force-killing the child process
34
+ const FINISH_KILL_GRACE_MS = getEnvInt("PI_FLOW_FINISH_KILL_GRACE_MS", 5_000);
28
35
  const AGENT_END_GRACE_MS = getEnvInt("PI_FLOW_AGENT_END_GRACE_MS", 2000);
29
- const FLOW_TIME_BUDGET_WARNING_MS = getEnvInt("PI_FLOW_TIME_BUDGET_WARNING_MS", 2 * 60 * 1000); // warn 2 min before kill
30
- const FLOW_FINAL_URGE_MS = getEnvInt("PI_FLOW_FINAL_URGE_MS", 135 * 1000); // final urge 135 s (2m15s) before kill (increased from 30s for wider summary window)
31
- const REPORTING_GRACE_MS = getEnvInt("PI_FLOW_REPORTING_GRACE_MS", 90_000); // grace period after timeout for agent to report findings (increased from 10s to 90s)
32
- const SNAP_THRESHOLD_MS = getEnvInt("PI_FLOW_SNAP_THRESHOLD_MS", 120_000); // threshold for proportional short-budget timer logic
33
- const FLOW_TOOL_SUMMARY_GRACE_MS = FLOW_FINAL_URGE_MS; // bash/tool abort lead time so the agent can summarize
36
+ const FLOW_TIME_BUDGET_WARNING_MS = getEnvInt("PI_FLOW_TIME_BUDGET_WARNING_MS", 2 * 60 * 1000);
37
+ const FLOW_FINAL_URGE_MS = getEnvInt("PI_FLOW_FINAL_URGE_MS", 135 * 1000);
38
+ const REPORTING_GRACE_MS = getEnvInt("PI_FLOW_REPORTING_GRACE_MS", 90_000);
39
+ const SNAP_THRESHOLD_MS = getEnvInt("PI_FLOW_SNAP_THRESHOLD_MS", 120_000);
40
+ const FLOW_TOOL_SUMMARY_GRACE_MS = FLOW_FINAL_URGE_MS;
34
41
  import { FLOW_DEPTH_ENV, FLOW_MAX_DEPTH_ENV, FLOW_STACK_ENV, FLOW_PREVENT_CYCLES_ENV, FLOW_TOOL_OPTIMIZE_ENV, } from "./depth.js";
35
- import { computeTransitionState, buildGuardLine, buildFlowListSection, buildLineage, computeChildPropagation, } from "./transition.js";
42
+ import { computeChildPropagation, } from "./transition.js";
36
43
  const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
37
44
  const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
38
45
  const PI_OFFLINE_ENV = "PI_OFFLINE";
39
46
  const FLOW_REMINDER_FILE_ENV = "PI_FLOW_REMINDER_FILE";
40
- const FLOW_DUMP_SNAPSHOT_ENV = "PI_FLOW_DUMP_SNAPSHOT";
41
47
  const packageJsonPath = path.join(path.dirname(new URL(import.meta.url).pathname), "../..", "package.json");
42
48
  let pipelineVersion = "0.0.0";
43
49
  try {
44
50
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
45
51
  pipelineVersion = pkg.version ?? "0.0.0";
46
52
  }
47
- catch {
48
- /* best-effort: fallback to 0.0.0 if package.json is missing or malformed */
49
- }
50
- // ---------------------------------------------------------------------------
51
- // Global child process group tracking for signal propagation
52
- // ---------------------------------------------------------------------------
53
- /** Track child process groups so we can kill them on parent exit / signal. */
54
- const runningChildGroups = new Map();
55
- /** Register a child process group for cleanup on shutdown. */
56
- export function registerChildGroup(pid, name) {
57
- if (pid > 0 && !isWindows) {
58
- runningChildGroups.set(pid, { groupPid: pid, name });
59
- }
60
- }
61
- /** Unregister a completed/stopped child process group. */
62
- export function unregisterChildGroup(pid) {
63
- runningChildGroups.delete(pid);
64
- }
65
- /**
66
- * Terminate all registered child process groups via SIGTERM (then SIGKILL after timeout).
67
- * Called on parent exit, SIGINT, SIGTERM, or pi-agent-flow:shutdown.
68
- */
69
- export function terminateAllChildGroups() {
70
- if (runningChildGroups.size === 0)
71
- return;
72
- const pids = Array.from(runningChildGroups.keys());
73
- for (const pid of pids) {
74
- try {
75
- process.kill(-pid, "SIGTERM");
76
- }
77
- catch {
78
- try {
79
- process.kill(pid, "SIGTERM");
80
- }
81
- catch { /* gone */ }
82
- }
83
- }
84
- // Hard kill after timeout
85
- const sigkillTimer = setTimeout(() => {
86
- for (const pid of pids) {
87
- try {
88
- process.kill(-pid, "SIGKILL");
89
- }
90
- catch {
91
- try {
92
- process.kill(pid, "SIGKILL");
93
- }
94
- catch { /* gone */ }
95
- }
96
- }
97
- runningChildGroups.clear();
98
- }, 5000);
99
- sigkillTimer.unref();
53
+ catch (err) {
54
+ logWarn(`[pi-agent-flow] Failed to read package.json: ${err}`);
100
55
  }
101
56
  /**
102
57
  * Merge actual usage with streaming estimates.
@@ -110,384 +65,14 @@ function mergeStreamingUsage(actual, estimatedOutputTokens, ctxEstimate, smoothe
110
65
  return {
111
66
  ...actual,
112
67
  ...(estimatedOutputTokens > 0 ? { output: Math.max(actual.output, estimatedOutputTokens) } : {}),
113
- ...(ctxEstimate > 0 ? { contextTokens: Math.max(actual.contextTokens, ctxEstimate) } : {}),
114
- // Show the live EMA value so the dashboard can rise and fall smoothly.
68
+ ...(ctxEstimate > 0 || actual.contextTokens > 0
69
+ ? { contextTokens: mergeStreamingContextTokens(actual, ctxEstimate) }
70
+ : {}),
115
71
  ...(smoothedTps > 0 ? { smoothedTps } : {}),
116
72
  };
117
73
  }
118
- // ---------------------------------------------------------------------------
119
- // Process helpers
120
- // ---------------------------------------------------------------------------
121
- /**
122
- * Derive the spawn command from the current process context so child invocations
123
- * work on Unix and Windows without going through a shell wrapper.
124
- */
125
- function resolveFlowSpawn() {
126
- // Support PI_FLOW_SPAWN_COMMAND env var override for exotic runtime
127
- // environments (e.g. bundled with pkg/nexe where process.argv[1] is unreliable).
128
- const envOverride = process.env["PI_FLOW_SPAWN_COMMAND"];
129
- if (envOverride && envOverride.trim()) {
130
- return { command: envOverride.trim(), prefixArgs: [] };
131
- }
132
- const isNode = /[\\/]node(?:\.exe)?$/i.test(process.execPath);
133
- if (isNode && process.argv[1]) {
134
- return { command: process.execPath, prefixArgs: [process.argv[1]] };
135
- }
136
- return { command: process.execPath, prefixArgs: [] };
137
- }
138
- // ---------------------------------------------------------------------------
139
- // Temp file helpers
140
- // ---------------------------------------------------------------------------
141
- function writeFlowSessionToTempFile(flowName, sessionJsonl) {
142
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-flow-"));
143
- const safeName = flowName.replace(/[^\w.-]+/g, "_");
144
- const filePath = path.join(tmpDir, `flow-${safeName}.jsonl`);
145
- fs.writeFileSync(filePath, sessionJsonl, { encoding: "utf-8", mode: 0o600 });
146
- return { dir: tmpDir, filePath };
147
- }
148
- function cleanupFlowTempDir(dir) {
149
- if (!dir)
150
- return;
151
- try {
152
- fs.rmSync(dir, { recursive: true, force: true });
153
- }
154
- catch (err) {
155
- logWarn(`[pi-agent-flow] cleanupFlowTempDir failed: ${err}`);
156
- }
157
- }
158
- // ---------------------------------------------------------------------------
159
- // Dump path helpers
160
- // ---------------------------------------------------------------------------
161
- function makeUniqueDumpPath(basePath, flowName) {
162
- const ext = path.extname(basePath);
163
- const base = ext ? basePath.slice(0, -ext.length) : basePath;
164
- const timestamp = Date.now();
165
- const safeFlowName = flowName.replace(/[^\w.-]+/g, "_");
166
- return `${base}.${safeFlowName}.${timestamp}.md`;
167
- }
168
- function makeUniqueDumpTxtPath(mdPath) {
169
- return mdPath.replace(/\.md$/, ".txt");
170
- }
171
- function atomicWriteFileSync(targetPath, data) {
172
- const dir = path.dirname(targetPath);
173
- const tmpPath = path.join(dir, `.tmp-${path.basename(targetPath)}.${Date.now()}`);
174
- fs.writeFileSync(tmpPath, data, { encoding: "utf-8", mode: 0o600 });
175
- fs.renameSync(tmpPath, targetPath);
176
- }
177
- function parseSessionSnapshotInfo(jsonl) {
178
- const result = {};
179
- if (!jsonl)
180
- return result;
181
- for (const line of jsonl.split("\n")) {
182
- if (!line.trim())
183
- continue;
184
- try {
185
- const entry = JSON.parse(line);
186
- if (entry.type === "session" && entry.id && typeof entry.id === "string") {
187
- result.sessionId = entry.id;
188
- }
189
- if (entry.type === "message" &&
190
- entry.message &&
191
- typeof entry.message === "object" &&
192
- !result.firstUserText) {
193
- const msg = entry.message;
194
- if (msg.role === "user" && msg.content) {
195
- let text = "";
196
- if (typeof msg.content === "string") {
197
- text = msg.content;
198
- }
199
- else if (Array.isArray(msg.content)) {
200
- const firstText = msg.content.find((c) => c && typeof c === "object" && c.type === "text");
201
- text = typeof firstText?.text === "string" ? firstText.text : "";
202
- }
203
- if (text.trim()) {
204
- result.firstUserText = text.trim();
205
- }
206
- }
207
- }
208
- }
209
- catch {
210
- /* ignore non-JSON lines */
211
- }
212
- }
213
- return result;
214
- }
215
- function sanitizeForFilesystem(text, maxLength) {
216
- const safe = text
217
- .slice(0, maxLength)
218
- .replace(/[^a-zA-Z0-9_-]/g, "_")
219
- .replace(/^_+|_+$/g, "");
220
- return safe || "unknown";
221
- }
222
- function getDebugDir(cwd, jsonl) {
223
- const { sessionId, firstUserText } = parseSessionSnapshotInfo(jsonl);
224
- const safeText = sanitizeForFilesystem(firstUserText ?? "", 20);
225
- const idShort = sanitizeForFilesystem((sessionId ?? "unknown").slice(0, 8), 8);
226
- return path.join(cwd, "tmp", `session-${safeText}-${idShort}`);
227
- }
228
- // ---------------------------------------------------------------------------
229
- // Dump TTL cleanup
230
- // ---------------------------------------------------------------------------
231
- /**
232
- * Delete stale dump files from the dump directory.
233
- * Called once at the start of each dump block to prevent unbounded accumulation.
234
- * Silently skips on any error (defensive).
235
- */
236
- function cleanupStaleDumps(dumpPath, maxAgeHours = 168) {
237
- try {
238
- const dir = path.dirname(dumpPath);
239
- const baseName = path.basename(dumpPath);
240
- const ext = path.extname(baseName);
241
- const base = ext ? baseName.slice(0, -ext.length) : baseName;
242
- const entries = fs.readdirSync(dir);
243
- const nowMs = Date.now();
244
- const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
245
- let deleted = 0;
246
- for (const entry of entries) {
247
- // Match both pi-dump.* and snapshot-dump.* families, plus .txt twins
248
- const isLegacyDump = entry.startsWith("snapshot-dump");
249
- if (!entry.startsWith(base) && !isLegacyDump)
250
- continue;
251
- const entryPath = path.join(dir, entry);
252
- try {
253
- const stats = fs.statSync(entryPath);
254
- if (nowMs - stats.mtimeMs > maxAgeMs) {
255
- fs.unlinkSync(entryPath);
256
- deleted++;
257
- }
258
- }
259
- catch { /* ignore per-entry errors */ }
260
- }
261
- if (deleted > 0) {
262
- logError(`[pi-agent-flow] Cleaned ${deleted} stale dump file(s) from ${dir}`);
263
- }
264
- }
265
- catch (err) {
266
- logWarn(`[pi-agent-flow] cleanupStaleDumps failed: ${err}`);
267
- }
268
- }
269
- /**
270
- * Delete stale debug dump files from session directories under the repo tmp/ path.
271
- * Called once at the start of each debug block to prevent unbounded accumulation.
272
- * Silently skips on any error (defensive).
273
- */
274
- function cleanupStaleDebugDumps(cwd, maxAgeHours = 168) {
275
- try {
276
- const baseDir = path.join(cwd, "tmp");
277
- if (!fs.existsSync(baseDir))
278
- return;
279
- const entries = fs.readdirSync(baseDir);
280
- const nowMs = Date.now();
281
- const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
282
- let deleted = 0;
283
- for (const entry of entries) {
284
- if (!entry.startsWith("session-"))
285
- continue;
286
- const sessionDir = path.join(baseDir, entry);
287
- try {
288
- const stat = fs.statSync(sessionDir);
289
- if (!stat.isDirectory())
290
- continue;
291
- const files = fs.readdirSync(sessionDir);
292
- for (const file of files) {
293
- const filePath = path.join(sessionDir, file);
294
- try {
295
- const stats = fs.statSync(filePath);
296
- if (nowMs - stats.mtimeMs > maxAgeMs) {
297
- fs.unlinkSync(filePath);
298
- deleted++;
299
- }
300
- }
301
- catch { /* ignore per-file errors */ }
302
- }
303
- // Remove empty session directories
304
- if (fs.readdirSync(sessionDir).length === 0) {
305
- fs.rmdirSync(sessionDir);
306
- }
307
- }
308
- catch { /* ignore per-directory errors */ }
309
- }
310
- if (deleted > 0) {
311
- logWarn(`[pi-agent-flow] Cleaned ${deleted} stale debug file(s) from ${baseDir}`);
312
- }
313
- }
314
- catch (err) {
315
- logWarn(`[pi-agent-flow] cleanupStaleDebugDumps failed: ${err}`);
316
- }
317
- }
318
- // ---------------------------------------------------------------------------
319
- // Reminder file helpers
320
- // ---------------------------------------------------------------------------
321
- /**
322
- * Write a reminder message to the reminder file so the flow state can see it
323
- * via the timed-bash wrapper before its next tool call.
324
- * Creates the file if it doesn't exist; appends the message.
325
- */
326
- function writeReminderFile(reminderFilePath, message) {
327
- if (!reminderFilePath)
328
- return;
329
- try {
330
- fs.writeFileSync(reminderFilePath, message + "\n", { encoding: "utf-8", flag: "a" });
331
- }
332
- catch {
333
- /* best-effort */
334
- }
335
- }
336
- // ---------------------------------------------------------------------------
337
- // Build pi CLI arguments (fork-only)
338
- // ---------------------------------------------------------------------------
339
- const inheritedCliArgs = getInheritedCliArgs();
340
- /**
341
- * Transform a flow's tool list when toolOptimize is enabled.
342
- * Replaces separate read/write/edit tools with the unified batch tool.
343
- */
344
- export function getOptimizedTools(flowTools, toolOptimize) {
345
- if (!toolOptimize || !flowTools)
346
- return flowTools;
347
- const hasLegacyTools = flowTools.some((t) => t === "read" || t === "write" || t === "edit");
348
- if (!hasLegacyTools)
349
- return flowTools;
350
- const filtered = flowTools.filter((t) => t !== "read" && t !== "write" && t !== "edit" && t !== "batch" && t !== "batch_read");
351
- return filtered.includes("batch")
352
- ? filtered
353
- : [...filtered, "batch"];
354
- }
355
- function buildFlowArgs(flow, intent, forkSessionPath, model, parentDepth = 0, maxDepth = 0, toolOptimize = false, structuredOutput = true, complexity = DEFAULT_COMPLEXITY, sessionTimeoutMs = getComplexityTimeoutMs(complexity), acceptance, discoveredFlows = [], parentFlowStack = [], preventCycles = true, goalContext, cwd, tools, preDispatchResults) {
356
- const args = [
357
- "--mode",
358
- "json",
359
- ...inheritedCliArgs.extensionArgs,
360
- ...inheritedCliArgs.alwaysProxy,
361
- ];
362
- // Fork mode: always use --session
363
- if (forkSessionPath) {
364
- args.push("--session", forkSessionPath);
365
- }
366
- if (inheritedCliArgs.flowModelConfig) {
367
- args.push("--flow-model-config", inheritedCliArgs.flowModelConfig);
368
- }
369
- if (inheritedCliArgs.flowComplexity) {
370
- args.push("--flow-complexity", inheritedCliArgs.flowComplexity);
371
- }
372
- if (inheritedCliArgs.tieredModels?.lite) {
373
- args.push("--flow-lite-model", inheritedCliArgs.tieredModels.lite);
374
- }
375
- if (inheritedCliArgs.tieredModels?.flash) {
376
- args.push("--flow-flash-model", inheritedCliArgs.tieredModels.flash);
377
- }
378
- if (inheritedCliArgs.tieredModels?.full) {
379
- args.push("--flow-full-model", inheritedCliArgs.tieredModels.full);
380
- }
381
- const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
382
- if (resolvedModel)
383
- args.push("--model", resolvedModel);
384
- // Opt out of appending the structured JSON appendix to the child `-p` mission.
385
- // Set `PI_FLOW_SKIP_STRUCTURED_DIRECTIVE=1` if a provider rejects that prompt shape.
386
- const rawSkipSo = process.env["PI_FLOW_SKIP_STRUCTURED_DIRECTIVE"];
387
- const skipStructuredDirective = rawSkipSo !== undefined && ["1", "true", "yes"].includes(rawSkipSo.trim().toLowerCase());
388
- // Do not inherit the parent CLI `--thinking` level. Child flows often use a
389
- // different tier/model than the root state; inheriting `--thinking high` can
390
- // be incompatible with the child model.
391
- const thinking = flow.thinking;
392
- if (thinking)
393
- args.push("--thinking", thinking);
394
- // Compute transition depth before building tool list — children that can
395
- // transition need the "flow" tool in their available set.
396
- const { currentDepth, effectiveMaxDepth, canTransition } = computeTransitionState(parentDepth, maxDepth);
397
- // Default tools for child flows. Legacy read/write/edit are NOT registered
398
- // for children — only batch (which includes read/write ops) is available.
399
- // The flow's frontmatter `tools` field overrides this default when set.
400
- const defaultTools = toolOptimize
401
- ? canTransition
402
- ? ["batch", "bash", "flow"]
403
- : ["batch", "bash"]
404
- : canTransition
405
- ? ["batch", "bash", "flow"]
406
- : ["batch", "bash"];
407
- // getOptimizedTools replaces legacy read/write/edit with batch when
408
- // toolOptimize is on. If the flow's frontmatter explicitly lists "flow",
409
- // it passes through; otherwise the defaultTools above handle it.
410
- const sourceTools = tools ?? flow.tools;
411
- const optimizedTools = getOptimizedTools(sourceTools, toolOptimize) ?? defaultTools;
412
- let harnessTools = optimizedTools;
413
- // If the flow explicitly listed only tools that got filtered (e.g. just
414
- // "web"), or the remaining tools lack essentials (batch/bash), fall back
415
- // to defaultTools so the child isn't orphaned.
416
- const hasEssentials = harnessTools.some((t) => t === "batch" || t === "bash" || t === "batch_read");
417
- if (harnessTools.length === 0 || !hasEssentials) {
418
- harnessTools = [...new Set([...defaultTools, ...harnessTools])];
419
- }
420
- args.push("--tools", harnessTools.join(","));
421
- // No --append-system-prompt: child inherits parent's system prompt for cache hits.
422
- // Flow instructions go in the intent message instead.
423
- const availableTools = harnessTools.join(", ");
424
- // Phase 1: Context seal — sharp boundary declaring history sealed
425
- const contextSeal = `<context-seal>\n` +
426
- `The conversation above is sealed — it is your session history for situational awareness only.\n` +
427
- `Your task begins NOW. Do not respond to or continue anything from the history.\n` +
428
- `</context-seal>`;
429
- // Phase 2: Activation — role, tools, depth, transition rules (dynamically generated)
430
- const guardLine = buildGuardLine(currentDepth, effectiveMaxDepth, preventCycles, parentFlowStack);
431
- const flowListSection = buildFlowListSection(canTransition, discoveredFlows);
432
- const effectiveTier = flow.tier ?? getFlowTier(flow.name);
433
- const lineage = buildLineage(flow.name, parentFlowStack);
434
- const activation = `\n\n<activation flow="${flow.name}" depth="${currentDepth}" tools="${availableTools}" tier="${effectiveTier}" lineage="${lineage}">\n` +
435
- `You are a [${flow.name}] agent operating at depth ${currentDepth}.\n` +
436
- `${flowListSection}` +
437
- `Do not attempt to use any tool outside the available set — it will fail.\n` +
438
- `</activation>`;
439
- // Phase 3: Directive — the flow's system prompt (renamed from <system-directive>)
440
- let directiveBody = flow.systemPrompt.trim();
441
- // Append structured output instructions when enabled (unless opted out via env).
442
- const isTrace = flow.name.toLowerCase() === "trace";
443
- if (structuredOutput && directiveBody && !skipStructuredDirective && !isTrace) {
444
- const isAudit = flow.name.toLowerCase() === "audit";
445
- const auditFields = isAudit
446
- ? " Also include `verdict: 'pass' | 'rework'` and `feedback: string` (required when verdict is 'rework', optional when 'pass'). When auditing multiple builds, also include `builds: { index: number, verdict: 'pass' | 'rework', feedback?: string }[]` with per-build verdicts."
447
- : "";
448
- directiveBody +=
449
- `\n\n## Structured Output\n` +
450
- `End with a \`\`\`json block: { version, status, summary, files[], actions[], notDone[], nextSteps[], reasoning[], notes[]${isAudit ? ", verdict, feedback, builds" : ""} }. Commands auto-extracted; omit empty arrays. Keep snippets under 300 chars. List at most 10 items per array.${auditFields}`;
451
- }
452
- const directive = directiveBody
453
- ? `\n\n<directive>\n${directiveBody}\n</directive>`
454
- : "";
455
- // Phase 4: Mission — the intent (and optional acceptance criteria)
456
- const acceptanceLine = acceptance ? `\nAcceptance: ${acceptance}` : "";
457
- const mission = `\n\n<mission>\n${intent}${acceptanceLine}\n` +
458
- `\nExecute this mission. Use only your available tools. If blocked, report why — do not guess.\n` +
459
- `Follow the output format specified in your directive.\n` +
460
- `</mission>`;
461
- // Phase 4.5: Flow goal context (optional)
462
- let goalSection = "";
463
- if (goalContext?.objective) {
464
- const completedSummary = goalContext.completedFlows?.length
465
- ? `\nCompleted steps:\n${goalContext.completedFlows.map(f => `- [${f.type}] ${f.aim}`).join("\n")}`
466
- : "";
467
- goalSection = `\n\n<flow>\nObjective: ${goalContext.objective}\n${goalContext.acceptance ? `Acceptance: ${goalContext.acceptance}\n` : ""}${goalContext.maxFlows !== undefined ? `Progress: ${goalContext.flowCount ?? 0}/${goalContext.maxFlows} flows used.\n` : ""}${completedSummary}\n</flow>`;
468
- }
469
- // Phase 4.6: Pre-dispatch results (optional)
470
- const preDispatchSection = preDispatchResults
471
- ? `\n\n<pre-dispatch>\nThe following tool calls were already executed on your behalf. Continue your exploration from there, or synthesize the outputs, pick up necessary tool id to keep.\n\n${preDispatchResults}\n</pre-dispatch>`
472
- : "";
473
- // -p must immediately precede the prompt so the CLI parser binds it correctly
474
- args.push("-p", `${contextSeal}${activation}${directive}${mission}${goalSection}${preDispatchSection}`);
475
- return args;
476
- }
477
- /**
478
- * Spawn a single flow process with forked session context.
479
- *
480
- * Returns a SingleResult even on failure (exitCode > 0, stderr populated).
481
- *
482
- * Why `spawn` instead of `pi.exec()`:
483
- * - `pi.exec()` is designed for simple command execution (one-shot, wait for exit).
484
- * - Flows need process-group isolation (detached mode on Unix), signal propagation,
485
- * soft-timeout with background continuation, streaming stdout parsing, and
486
- * mid-flight kill semantics. `spawn` gives full control over stdio, process
487
- * groups, and lifecycle that `pi.exec()` does not expose.
488
- */
489
74
  export async function runFlow(opts) {
490
- const { cwd, flows, flowName, intent, aim, taskCwd, forkSessionSnapshotJsonl, parentDepth, parentFlowStack, maxDepth, preventCycles, toolOptimize = false, structuredOutput = true, model, signal, onUpdate, makeDetails, } = opts;
75
+ const { cwd, flows, flowName, intent, aim, taskCwd, forkSessionSnapshotJsonl, parentDepth, parentFlowStack, maxDepth, preventCycles, toolOptimize = false, structuredOutput = true, model, signal, onUpdate, makeDetails, toolCallId, } = opts;
491
76
  const normalizedFlowName = flowName.toLowerCase();
492
77
  const flow = flows.find((f) => f.name === normalizedFlowName);
493
78
  if (!flow) {
@@ -511,6 +96,8 @@ export async function runFlow(opts) {
511
96
  const deadlineAtMs = effectiveTimeout > 0 ? startedAtMs + effectiveTimeout : undefined;
512
97
  const resolvedModel = model ?? flow.model ?? inheritedCliArgs.fallbackModel;
513
98
  const resolvedMaxContextTokens = opts.maxContextTokens ?? inheritedCliArgs.maxContextTokens;
99
+ const sharedContext = forkSessionSnapshotJsonl ? parseSharedContext(forkSessionSnapshotJsonl) : undefined;
100
+ const initialContextTokens = computeInitialContextTokens(sharedContext, intent);
514
101
  const result = {
515
102
  type: normalizedFlowName,
516
103
  agentSource: flow.source,
@@ -520,12 +107,19 @@ export async function runFlow(opts) {
520
107
  exitCode: -1,
521
108
  messages: [],
522
109
  stderr: "",
523
- usage: emptyFlowUsage(),
110
+ usage: {
111
+ ...emptyFlowUsage(),
112
+ contextTokens: initialContextTokens,
113
+ },
524
114
  model: resolvedModel,
525
115
  startedAtMs,
526
116
  ...(deadlineAtMs !== undefined ? { deadlineAtMs } : {}),
527
117
  ...(resolvedMaxContextTokens !== undefined ? { maxContextTokens: resolvedMaxContextTokens } : {}),
528
118
  };
119
+ if (initialContextTokens > 0) {
120
+ const ctxState = getCtxState(result);
121
+ ctxState.baseline = initialContextTokens;
122
+ }
529
123
  let liveStreamingText = "";
530
124
  let liveEstimatedOutputTokens = 0;
531
125
  let lastActualOutputTokens = result.usage.output;
@@ -533,7 +127,6 @@ export async function runFlow(opts) {
533
127
  const streamingDelta = drainStreamingText(result);
534
128
  if (streamingDelta)
535
129
  liveStreamingText += streamingDelta;
536
- // Live text is stored per-toolCallId by the executor's emitProgress, not here.
537
130
  const estimatedTokens = drainStreamingEstimate(result);
538
131
  const toolCallTokens = drainToolCallEstimate(result);
539
132
  if (result.usage.output !== lastActualOutputTokens) {
@@ -544,7 +137,6 @@ export async function runFlow(opts) {
544
137
  const ctxEst = drainCtxEstimate(result);
545
138
  updateSmoothedTps(result, estimatedTokens);
546
139
  const smoothedTps = drainSmoothedTps(result);
547
- // Fallback TPS for providers that don't emit streaming deltas (e.g., Fireworks k2p6).
548
140
  const elapsedSec = (Date.now() - startedAtMs) / 1000;
549
141
  const fallbackTps = elapsedSec > 0.5 && smoothedTps <= 0 ? result.usage.output / elapsedSec : 0;
550
142
  const displayTps = smoothedTps > 0 ? smoothedTps : fallbackTps;
@@ -557,9 +149,9 @@ export async function runFlow(opts) {
557
149
  },
558
150
  ],
559
151
  details: makeDetails([{ ...result, usage: mergedUsage, streamingText: liveStreamingText || undefined }]),
152
+ ...(toolCallId ? { _toolCallId: toolCallId } : {}),
560
153
  });
561
154
  };
562
- // Write forked session snapshot to temp file only when provided
563
155
  let forkSessionTmpDir = null;
564
156
  let forkSessionTmpPath = null;
565
157
  if (forkSessionSnapshotJsonl) {
@@ -567,8 +159,6 @@ export async function runFlow(opts) {
567
159
  forkSessionTmpDir = forkTmp.dir;
568
160
  forkSessionTmpPath = forkTmp.filePath;
569
161
  }
570
- // Create a temp dir for the reminder file so the flow state can read timeout warnings
571
- // via the timed-bash wrapper before its next tool call.
572
162
  let reminderTmpDir = null;
573
163
  let reminderFilePath = null;
574
164
  if (effectiveTimeout > 0) {
@@ -584,14 +174,17 @@ export async function runFlow(opts) {
584
174
  }
585
175
  try {
586
176
  const piArgs = buildFlowArgs(flow, intent, forkSessionTmpPath, model, parentDepth, maxDepth, toolOptimize, structuredOutput, effectiveComplexity, effectiveTimeout, opts.acceptance, flows, parentFlowStack, preventCycles, opts.goalContext, cwd, opts.tools, opts.preDispatchResults);
587
- // Extract the activation prompt once for reuse by both dump and debug blocks.
588
177
  const promptIndex = piArgs.indexOf("-p");
589
178
  const prompt = promptIndex >= 0 ? piArgs[promptIndex + 1] : "";
590
- // Dump verbatim child payload to disk for debugging when requested.
179
+ result.usage.contextTokens = computeInitialContextTokens(sharedContext, intent, prompt);
180
+ const ctxState = getCtxState(result);
181
+ ctxState.baseline = result.usage.contextTokens;
182
+ if (onUpdate) {
183
+ emitUpdate();
184
+ }
591
185
  const dumpPath = process.env[FLOW_DUMP_SNAPSHOT_ENV] || inheritedCliArgs.dumpPath;
592
186
  if (dumpPath) {
593
- const maxAgeHours = Number(process.env.PI_FLOW_DUMP_MAX_AGE_HOURS);
594
- cleanupStaleDumps(dumpPath, Number.isFinite(maxAgeHours) && maxAgeHours > 0 ? maxAgeHours : 168);
187
+ cleanupStaleDumps(dumpPath, resolveDumpMaxAgeHours());
595
188
  const effectiveTier = flow.tier ?? getFlowTier(flow.name);
596
189
  const sanitizationHeader = `<!-- pi-agent-flow dump | Flow: ${flow.name} | Tier: ${effectiveTier} | Pipeline: ${pipelineVersion} | Generated: ${new Date().toISOString()} -->`;
597
190
  const markdownParts = [
@@ -602,7 +195,6 @@ export async function runFlow(opts) {
602
195
  markdownParts.push(`## Session Snapshot (JSONL)`, ``, ...forkSessionSnapshotJsonl.split("\n"), ``);
603
196
  }
604
197
  markdownParts.push(`## Activation Prompt (-p)`, ``, prompt);
605
- // Compression Stats section removed — zeroed values provide no signal
606
198
  const markdown = markdownParts.join("\n");
607
199
  const uniqueDumpPath = makeUniqueDumpPath(dumpPath, flow.name);
608
200
  const uniqueTxtPath = makeUniqueDumpTxtPath(uniqueDumpPath);
@@ -615,10 +207,8 @@ export async function runFlow(opts) {
615
207
  logError(`[pi-agent-flow] Snapshot dump FAILED: ${err}`);
616
208
  }
617
209
  }
618
- // Debug mode: write the activation prompt and shared session snapshot to a dedicated temp file.
619
210
  if (opts.debugMode) {
620
- const maxAgeHours = Number(process.env.PI_FLOW_DUMP_MAX_AGE_HOURS);
621
- cleanupStaleDebugDumps(cwd, Number.isFinite(maxAgeHours) && maxAgeHours > 0 ? maxAgeHours : 168);
211
+ cleanupStaleDebugDumps(cwd, resolveDumpMaxAgeHours());
622
212
  const safeFlowName = flow.name.replace(/[^\w.-]+/g, "_");
623
213
  const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
624
214
  const debugDir = getDebugDir(cwd, forkSessionSnapshotJsonl);
@@ -665,7 +255,8 @@ export async function runFlow(opts) {
665
255
  const distMtime = fs.statSync(path.join(distDir, distFile)).mtimeMs;
666
256
  return srcMtime > distMtime;
667
257
  }
668
- catch {
258
+ catch (e) {
259
+ logWarn(`[pi-agent-flow] checkStale failed for ${srcFile}: ${e}`);
669
260
  return false;
670
261
  }
671
262
  };
@@ -677,7 +268,6 @@ export async function runFlow(opts) {
677
268
  cwd: taskCwd ?? cwd,
678
269
  shell: false,
679
270
  stdio: ["pipe", "pipe", "pipe"],
680
- // Process group on Unix so we can kill all descendants on timeout/abort.
681
271
  detached: !isWindows,
682
272
  ...(signal ? { signal } : {}),
683
273
  env: {
@@ -699,7 +289,6 @@ export async function runFlow(opts) {
699
289
  }),
700
290
  },
701
291
  });
702
- // Register the child process group for global cleanup on signal/exit
703
292
  if (proc.pid !== undefined && !isWindows) {
704
293
  registerChildGroup(proc.pid, normalizedFlowName);
705
294
  }
@@ -711,10 +300,12 @@ export async function runFlow(opts) {
711
300
  try {
712
301
  proc.stdin.end();
713
302
  }
714
- catch { /* ignore */ }
303
+ catch (e) {
304
+ logWarn(`[pi-agent-flow] Failed to end child stdin: ${e}`);
305
+ }
715
306
  };
716
- proc.stdin.on("error", () => {
717
- /* ignore broken pipe on fast exits */
307
+ proc.stdin.on("error", (err) => {
308
+ logWarn(`[pi-agent-flow] Child stdin error: ${err}`);
718
309
  });
719
310
  proc.stdin.end();
720
311
  let abortHandler;
@@ -745,44 +336,11 @@ export async function runFlow(opts) {
745
336
  }
746
337
  };
747
338
  const terminateChild = () => {
748
- endStdin();
749
- if (isWindows) {
750
- if (proc.pid !== undefined) {
751
- const killer = spawn("taskkill", ["/T", "/F", "/PID", String(proc.pid)], {
752
- stdio: "ignore",
753
- });
754
- killer.unref();
755
- }
756
- return;
757
- }
758
- // Kill the entire process group (negative PID).
759
- if (proc.pid === undefined) {
760
- proc.kill("SIGTERM");
761
- }
762
- else {
763
- try {
764
- process.kill(-proc.pid, "SIGTERM");
765
- }
766
- catch {
767
- proc.kill("SIGTERM");
768
- }
769
- }
770
- const sigkillTimer = setTimeout(() => {
771
- if (!didClose) {
772
- if (proc.pid === undefined) {
773
- proc.kill("SIGKILL");
774
- }
775
- else {
776
- try {
777
- process.kill(-proc.pid, "SIGKILL");
778
- }
779
- catch {
780
- proc.kill("SIGKILL");
781
- }
782
- }
783
- }
784
- }, SIGKILL_TIMEOUT_MS);
785
- sigkillTimer.unref();
339
+ terminateChildProcess(proc, {
340
+ endStdin,
341
+ timeoutMs: SIGKILL_TIMEOUT_MS,
342
+ skipIfClosed: () => didClose,
343
+ });
786
344
  };
787
345
  const clearFinishKillTimer = () => {
788
346
  if (finishKillTimer) {
@@ -801,8 +359,6 @@ export async function runFlow(opts) {
801
359
  if (signal && abortHandler) {
802
360
  signal.removeEventListener("abort", abortHandler);
803
361
  }
804
- // Soft-kill: give the child a short grace to exit naturally after stdin close.
805
- // If it hasn't closed by then, force-kill to prevent orphaned processes.
806
362
  clearFinishKillTimer();
807
363
  finishKillTimer = setTimeout(() => {
808
364
  if (!didClose) {
@@ -883,7 +439,6 @@ export async function runFlow(opts) {
883
439
  result.stderr = err.message;
884
440
  finish(1);
885
441
  });
886
- // Abort handling
887
442
  if (signal) {
888
443
  abortHandler = () => {
889
444
  if (didClose || settled)
@@ -897,12 +452,7 @@ export async function runFlow(opts) {
897
452
  else
898
453
  signal.addEventListener("abort", abortHandler, { once: true });
899
454
  }
900
- // Execution timeout — two-stage: parent-side warnings, tool-level deadline abort, then grace, then hard kill
901
455
  if (effectiveTimeout > 0) {
902
- // Warning timer: notify the parent UI that the child is about to be killed.
903
- // NOTE: True mid-flight injection into the child's context requires pi-core
904
- // support for out-of-band messages. Until then, the agent only knows its
905
- // budget from the initial prompt; this warning is parent-side only.
906
456
  const warningMs = effectiveTimeout - FLOW_TIME_BUDGET_WARNING_MS;
907
457
  if (warningMs > 0) {
908
458
  const warnTimer = setTimeout(() => {
@@ -911,14 +461,11 @@ export async function runFlow(opts) {
911
461
  const remainingSec = Math.round(FLOW_TIME_BUDGET_WARNING_MS / 1000);
912
462
  const warnMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. The agent should wrap up now.`;
913
463
  result.stderr += warnMsg;
914
- // Write to reminder file so the flow state sees it on its next bash call.
915
464
  writeReminderFile(reminderFilePath, `[Flow warning] ${remainingSec}s remaining before hard timeout. Wrap up your work and output structured findings.`);
916
- // Force an update so the parent UI shows the warning immediately.
917
465
  emitUpdate();
918
466
  }, warningMs);
919
467
  warnTimer.unref();
920
468
  }
921
- // Final urge timer: stronger warning 45 s before hard timeout
922
469
  const urgeMs = effectiveTimeout - FLOW_FINAL_URGE_MS;
923
470
  if (urgeMs > 0) {
924
471
  const urgeTimer = setTimeout(() => {
@@ -927,7 +474,6 @@ export async function runFlow(opts) {
927
474
  const remainingSec = Math.round(FLOW_FINAL_URGE_MS / 1000);
928
475
  const urgeMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. Stop all work and output your structured findings.`;
929
476
  result.stderr += urgeMsg;
930
- // Write to reminder file so the flow state sees it on its next bash call.
931
477
  writeReminderFile(reminderFilePath, `[Flow urge] ${remainingSec}s remaining before hard timeout. STOP all tool use and output your structured findings NOW.`);
932
478
  emitUpdate();
933
479
  }, urgeMs);
@@ -939,7 +485,6 @@ export async function runFlow(opts) {
939
485
  timeoutFired = true;
940
486
  result.stderr += `\nFlow timed out after ${Math.round(effectiveTimeout / 1000)}s.`;
941
487
  emitUpdate();
942
- // Grace period before hard kill
943
488
  const graceTimer = setTimeout(() => {
944
489
  if (didClose || settled)
945
490
  return;
@@ -953,9 +498,6 @@ export async function runFlow(opts) {
953
498
  }
954
499
  });
955
500
  result.exitCode = exitCode;
956
- // Persist final smoothed TPS into the result's usage so it survives after streaming ends.
957
- // During streaming, emitUpdate() only merges smoothedTps into a temporary display object;
958
- // without this, result.usage.smoothedTps stays at 0 and the UI shows a dash.
959
501
  const finalSmoothedTps = drainSmoothedTps(result);
960
502
  const finalElapsedSec = (Date.now() - startedAtMs) / 1000;
961
503
  const finalTps = finalSmoothedTps > 0 ? finalSmoothedTps
@@ -964,7 +506,6 @@ export async function runFlow(opts) {
964
506
  result.usage.smoothedTps = finalTps;
965
507
  }
966
508
  const normalized = normalizeFlowResult(result, wasAborted);
967
- // Extract structured JSON output from the final assistant text
968
509
  if (structuredOutput) {
969
510
  const flowText = getFlowOutput(normalized.messages);
970
511
  const extracted = extractStructuredOutput(flowText);
@@ -983,9 +524,6 @@ export async function runFlow(opts) {
983
524
  // ---------------------------------------------------------------------------
984
525
  // Concurrency helper
985
526
  // ---------------------------------------------------------------------------
986
- /**
987
- * Map over items with a bounded number of concurrent workers.
988
- */
989
527
  export async function mapFlowConcurrent(items, concurrency, fn) {
990
528
  if (items.length === 0)
991
529
  return [];