@selesai/code 0.8.6 → 0.8.7

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 (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/settings.json +1 -0
  16. package/dist/extensions/model-prompt-injector/config.json +10 -0
  17. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  18. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  19. package/dist/extensions/package.json +1 -0
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  21. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  22. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  23. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  24. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  25. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  26. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  27. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  28. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  29. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  30. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  31. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  32. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  33. package/dist/extensions/pi-subagents/install.mjs +2 -2
  34. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  35. package/dist/extensions/pi-subagents/package.json +1 -1
  36. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  37. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  38. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  39. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  40. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  41. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  42. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  43. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  45. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  46. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  47. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  48. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  49. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  50. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  51. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  52. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  53. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  54. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  55. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  56. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  57. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  58. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  59. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  60. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  61. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  62. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  64. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  66. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  67. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  70. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
  71. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  72. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  73. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  74. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  75. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  76. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  80. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  81. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  83. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  84. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  85. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  86. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  88. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  89. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  90. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  96. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  97. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  98. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  99. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  100. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  101. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  102. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  104. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  105. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  106. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  107. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  108. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  109. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  110. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  111. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  112. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  113. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  114. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  115. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  116. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  117. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  118. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  119. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  120. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  121. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  122. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  123. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  124. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  125. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  126. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  127. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  128. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  129. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  130. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  131. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  133. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  134. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  135. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  136. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  137. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  138. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  139. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  140. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  141. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  142. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  143. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  144. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  145. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  146. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  147. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  148. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  149. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  150. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  151. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  152. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  153. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  154. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  155. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  156. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  157. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  158. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  159. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  160. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  161. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  162. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  163. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  164. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  165. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  166. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  167. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  168. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  169. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  170. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  171. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  172. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  173. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  174. package/dist/extensions/question/index.ts +21 -6
  175. package/dist/extensions/question/question-list.ts +7 -1
  176. package/dist/extensions/question/row-layout.ts +3 -0
  177. package/dist/extensions/question/tests/exp.test.ts +70 -0
  178. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  179. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  180. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  181. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  182. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  183. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  184. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  185. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  186. package/dist/skills/pi-subagents/SKILL.md +1 -1
  187. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  188. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  189. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  190. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  191. package/docs/settings.md +6 -0
  192. package/package.json +3 -3
@@ -0,0 +1,376 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readSubagentEnv } from "../../shared/env.ts";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import type { Message } from "@earendil-works/pi-ai";
7
+ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
8
+ import { TEMP_ROOT_DIR, type OrcaProgressTabsConfig } from "../../shared/types.ts";
9
+ import { extractTextFromContent, extractToolArgsPreview, getAgentDir } from "../../shared/utils.ts";
10
+
11
+ const ORCA_CREATE_TIMEOUT_MS = 20_000;
12
+ const ORCA_KILL_GRACE_MS = 2_000;
13
+ const CLEANUP_DELAY_MS = 5 * 60_000;
14
+ const STALE_PROGRESS_MAX_AGE_MS = 24 * 60 * 60_000;
15
+ const VIEWER_POLL_MS = 150;
16
+ const MAX_MIRROR_BYTES = 1024 * 1024;
17
+ const MIRROR_FOOTER_RESERVE_BYTES = 16 * 1024;
18
+ const COUNTER_LOCK_STALE_MS = 30_000;
19
+ const COUNTER_LOCK_RETRIES = 200;
20
+ const COUNTER_LOCK_RETRY_MS = 10;
21
+
22
+ const ORCA_CREATE_WATCHDOG_SCRIPT = [
23
+ "const {spawn}=require('node:child_process');",
24
+ "const timeout=Number(process.argv[1]),grace=Number(process.argv[2]),command=process.argv[3],args=process.argv.slice(4);",
25
+ "const child=spawn(command,args,{stdio:'ignore',windowsHide:true});",
26
+ "let hardKill;",
27
+ "const timer=setTimeout(()=>{child.kill('SIGTERM');hardKill=setTimeout(()=>child.kill('SIGKILL'),grace)},timeout);",
28
+ "const clear=()=>{clearTimeout(timer);if(hardKill)clearTimeout(hardKill)};",
29
+ "child.once('error',()=>{clear();process.exitCode=1});",
30
+ "child.once('close',code=>{clear();process.exitCode=code===0?0:1});",
31
+ ].join("");
32
+
33
+ const ORCA_CLEANUP_WATCHDOG_SCRIPT = [
34
+ "const fs=require('node:fs');",
35
+ "const deadline=Date.now()+Number(process.argv[1]),files=process.argv.slice(2);",
36
+ "function check(){if(!files.some(file=>fs.existsSync(file)))process.exit(0);if(Date.now()<deadline)return;for(const file of files){try{fs.rmSync(file,{force:true})}catch{}}process.exit(0)}",
37
+ "setInterval(check,1000);check();",
38
+ ].join("");
39
+
40
+ export interface OrcaProgressTab {
41
+ append(text: string): void;
42
+ event(event: { type?: string; message?: Message; toolName?: string; args?: unknown }): void;
43
+ finish(status: "completed" | "failed" | "stopped", sessionFile?: string): void;
44
+ }
45
+
46
+ function executableFile(candidate: string): boolean {
47
+ try {
48
+ const stats = fs.statSync(candidate);
49
+ if (!stats.isFile()) return false;
50
+ if (process.platform === "win32") return true;
51
+ fs.accessSync(candidate, fs.constants.X_OK);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function executableNames(command: string, env: NodeJS.ProcessEnv): string[] {
59
+ if (process.platform !== "win32") return [command];
60
+ const extensions = (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean);
61
+ return [command, ...extensions.map((extension) => `${command}${extension.toLowerCase()}`), ...extensions.map((extension) => `${command}${extension.toUpperCase()}`)];
62
+ }
63
+
64
+ export function resolveOrcaCommand(env: NodeJS.ProcessEnv = process.env): string | undefined {
65
+ const override = readSubagentEnv(env, "SELESAI_SUBAGENT_ORCA_BINARY")?.trim();
66
+ if (override) return executableFile(override) ? override : undefined;
67
+ for (const directory of (env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
68
+ for (const name of executableNames("orca", env)) {
69
+ const candidate = path.join(directory, name);
70
+ if (executableFile(candidate)) return candidate;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ function shellQuote(value: string): string {
77
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
78
+ }
79
+
80
+ const VIEWER_SCRIPT = [
81
+ "const fs=require('fs'),{StringDecoder}=require('string_decoder');",
82
+ "const log=process.argv[1],done=process.argv[2],decoder=new StringDecoder('utf8');",
83
+ "let offset=0,finishing=false,state='text';",
84
+ "function sanitize(input){let output='';for(const char of input){const code=char.charCodeAt(0);",
85
+ "if(state==='text'){if(code===27){state='escape';continue}if(code===155){state='csi';continue}if(code===157){state='osc';continue}if(code===144||code===152||code===158||code===159){state='string';continue}if(code===10||(code>=32&&code!==127&&(code<128||code>159)))output+=char;continue}",
86
+ "if(state==='escape'){if(char==='[')state='csi';else if(char===']')state='osc';else if(char==='P'||char==='X'||char==='^'||char==='_')state='string';else if(code<32||code>47)state='text';continue}",
87
+ "if(state==='csi'){if(code>=64&&code<=126)state='text';continue}",
88
+ "if(state==='osc'){if(code===7)state='text';else if(code===27)state='osc-escape';continue}",
89
+ "if(state==='osc-escape'){state=char==='\\\\'?'text':code===27?'osc-escape':'osc';continue}",
90
+ "if(state==='string'){if(code===27)state='string-escape';continue}",
91
+ "if(state==='string-escape')state=char==='\\\\'?'text':code===27?'string-escape':'string';",
92
+ "}return output}",
93
+ "function write(buffer){const output=sanitize(decoder.write(buffer));if(output)process.stdout.write(output)}",
94
+ "function pump(){",
95
+ " try{const size=fs.statSync(log).size;if(size<offset)offset=0;if(size>offset){const fd=fs.openSync(log,'r');const b=Buffer.alloc(size-offset);fs.readSync(fd,b,0,b.length,offset);fs.closeSync(fd);offset=size;write(b);}}catch{}",
96
+ " if(!finishing&&fs.existsSync(done)){finishing=true;pump();setTimeout(()=>{const output=sanitize(decoder.end());if(output)process.stdout.write(output);try{fs.unlinkSync(done)}catch{}try{fs.unlinkSync(log)}catch{}process.exit(0)},250);}",
97
+ "}",
98
+ `const timer=setInterval(pump,${VIEWER_POLL_MS});`,
99
+ "pump();",
100
+ "process.on('exit',()=>clearInterval(timer));",
101
+ ].join("");
102
+
103
+ function viewerCommand(nodeExecutable: string, logPath: string, donePath: string): string {
104
+ const invocation = [nodeExecutable, "-e", VIEWER_SCRIPT, logPath, donePath].map(shellQuote).join(" ");
105
+ // Do not replace or exit Orca's interactive shell. The short-lived viewer may
106
+ // finish and delete its mirror files, while the completed tab remains open at
107
+ // the shell prompt until the user closes it.
108
+ return invocation;
109
+ }
110
+
111
+ function progressRoot(): string {
112
+ return path.join(TEMP_ROOT_DIR, "orca-progress");
113
+ }
114
+
115
+ function pruneStaleProgressFiles(root: string, now = Date.now()): void {
116
+ try {
117
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
118
+ if (!entry.isFile() || (!entry.name.endsWith(".log") && !entry.name.endsWith(".done"))) continue;
119
+ const file = path.join(root, entry.name);
120
+ try {
121
+ if (now - fs.statSync(file).mtimeMs > STALE_PROGRESS_MAX_AGE_MS) fs.rmSync(file, { force: true });
122
+ } catch { /* best-effort stale cleanup */ }
123
+ }
124
+ } catch {
125
+ // The observer remains optional when temp cleanup is unavailable.
126
+ }
127
+ }
128
+
129
+ function safeSegment(value: unknown, fallback = "subagent"): string {
130
+ if (typeof value !== "string") return fallback;
131
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || fallback;
132
+ }
133
+
134
+ function sleepSync(ms: number): void {
135
+ const signal = new Int32Array(new SharedArrayBuffer(4));
136
+ Atomics.wait(signal, 0, 0, ms);
137
+ }
138
+
139
+ function resolveSequenceScope(cwd: string): string {
140
+ let current = path.resolve(cwd);
141
+ try { current = fs.realpathSync(current); } catch { /* use the lexical cwd */ }
142
+ while (true) {
143
+ try {
144
+ if (fs.existsSync(path.join(current, ".git"))) return current;
145
+ } catch { /* keep walking */ }
146
+ const parent = path.dirname(current);
147
+ if (parent === current) return path.resolve(cwd);
148
+ current = parent;
149
+ }
150
+ }
151
+
152
+ function reserveTabSequence(root: string, cwd: string): number | undefined {
153
+ const key = createHash("sha256").update(resolveSequenceScope(cwd)).digest("hex").slice(0, 20);
154
+ const counterPath = path.join(root, `counter-${key}`);
155
+ const lockPath = `${counterPath}.lock`;
156
+ let locked = false;
157
+ for (let attempt = 0; attempt < COUNTER_LOCK_RETRIES; attempt++) {
158
+ try {
159
+ fs.mkdirSync(lockPath, { mode: 0o700 });
160
+ locked = true;
161
+ break;
162
+ } catch (error) {
163
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") break;
164
+ try {
165
+ if (Date.now() - fs.statSync(lockPath).mtimeMs > COUNTER_LOCK_STALE_MS) {
166
+ fs.rmSync(lockPath, { recursive: true, force: true });
167
+ continue;
168
+ }
169
+ } catch { /* another process released the lock */ }
170
+ sleepSync(COUNTER_LOCK_RETRY_MS);
171
+ }
172
+ }
173
+ if (!locked) return undefined;
174
+ try {
175
+ let current = 0;
176
+ try {
177
+ const parsed = Number.parseInt(fs.readFileSync(counterPath, "utf-8"), 10);
178
+ if (Number.isSafeInteger(parsed) && parsed >= 0) current = parsed;
179
+ } catch { /* first tab for this worktree */ }
180
+ const next = current + 1;
181
+ try {
182
+ fs.writeFileSync(counterPath, `${next}\n`, { encoding: "utf-8", mode: 0o600 });
183
+ return next;
184
+ } catch {
185
+ return undefined;
186
+ }
187
+ } finally {
188
+ try { fs.rmSync(lockPath, { recursive: true, force: true }); } catch { /* stale lock cleanup handles crashes */ }
189
+ }
190
+ }
191
+
192
+ export function resolvePiSessionId(sessionFile: string | undefined): string | undefined {
193
+ if (!sessionFile) return undefined;
194
+ try {
195
+ const fd = fs.openSync(sessionFile, "r");
196
+ try {
197
+ const buffer = Buffer.alloc(16 * 1024);
198
+ const bytes = fs.readSync(fd, buffer, 0, buffer.length, 0);
199
+ const line = buffer.toString("utf-8", 0, bytes).split("\n", 1)[0];
200
+ if (!line) return undefined;
201
+ const header = JSON.parse(line) as { type?: unknown; id?: unknown };
202
+ if (header.type === "session" && typeof header.id === "string" && /^[A-Za-z0-9-]{8,}$/.test(header.id)) return header.id;
203
+ return undefined;
204
+ } finally {
205
+ fs.closeSync(fd);
206
+ }
207
+ } catch {
208
+ return undefined;
209
+ }
210
+ }
211
+
212
+ function scheduleCleanup(nodeExecutable: string, paths: string[]): void {
213
+ try {
214
+ const watchdog = spawn(nodeExecutable, ["-e", ORCA_CLEANUP_WATCHDOG_SCRIPT, String(CLEANUP_DELAY_MS), ...paths], {
215
+ detached: true,
216
+ stdio: "ignore",
217
+ windowsHide: true,
218
+ });
219
+ watchdog.once("error", () => {});
220
+ watchdog.unref();
221
+ } catch {
222
+ // Cleanup remains best effort for this optional observer.
223
+ }
224
+ }
225
+
226
+ function loadOrcaProgressTabsConfig(): OrcaProgressTabsConfig | undefined {
227
+ try {
228
+ const configPath = path.join(getAgentDir(), "extensions", "subagent", "config.json");
229
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
230
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
231
+ const value = (parsed as Record<string, unknown>).orcaProgressTabs;
232
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
233
+ const config = value as Record<string, unknown>;
234
+ if (Object.keys(config).some((key) => key !== "enabled") || typeof config.enabled !== "boolean") return undefined;
235
+ return { enabled: config.enabled };
236
+ } catch {
237
+ return undefined;
238
+ }
239
+ }
240
+
241
+ export function createOrcaProgressTab(input: {
242
+ cwd: string;
243
+ runId: string;
244
+ agent: string;
245
+ index: number;
246
+ config?: OrcaProgressTabsConfig;
247
+ env?: NodeJS.ProcessEnv;
248
+ command?: string;
249
+ }): OrcaProgressTab | undefined {
250
+ const config = input.config ?? loadOrcaProgressTabsConfig();
251
+ if (config?.enabled !== true || process.platform === "win32") return undefined;
252
+ const command = input.command ?? resolveOrcaCommand(input.env);
253
+ if (!command) return undefined;
254
+ const nodeExecutable = resolveNodeExecutable();
255
+
256
+ const root = progressRoot();
257
+ try {
258
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
259
+ } catch {
260
+ return undefined;
261
+ }
262
+ pruneStaleProgressFiles(root);
263
+ const runId = safeSegment(input.runId, "run");
264
+ const agent = safeSegment(input.agent, "subagent");
265
+ const index = typeof input.index === "number" && Number.isInteger(input.index) && input.index >= 0 ? input.index : 0;
266
+ const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : process.cwd();
267
+ const tabSequence = reserveTabSequence(root, cwd);
268
+ if (tabSequence === undefined) return undefined;
269
+ const stem = `${runId}-${index}-${randomUUID()}`;
270
+ const logPath = path.join(root, `${stem}.log`);
271
+ const donePath = path.join(root, `${stem}.done`);
272
+ const title = `subagent · ${agent} · ${tabSequence}`;
273
+ try {
274
+ fs.writeFileSync(logPath, `pi-subagents / ${agent}\nrun ${runId} · child ${index + 1}\n${"─".repeat(48)}\n`, { encoding: "utf-8", mode: 0o600 });
275
+ fs.rmSync(donePath, { force: true });
276
+ } catch {
277
+ return undefined;
278
+ }
279
+
280
+ let available = true;
281
+ let truncated = false;
282
+ let scheduledBytes = fs.statSync(logPath).size;
283
+ const progressByteLimit = MAX_MIRROR_BYTES - MIRROR_FOOTER_RESERVE_BYTES;
284
+ const logStream = fs.createWriteStream(logPath, { flags: "a", mode: 0o600 });
285
+ const failObserver = () => {
286
+ if (!available) return;
287
+ available = false;
288
+ logStream.destroy();
289
+ try { fs.rmSync(logPath, { force: true }); } catch { /* best effort */ }
290
+ try { fs.rmSync(donePath, { force: true }); } catch { /* best effort */ }
291
+ };
292
+ logStream.once("error", failObserver);
293
+ const writeProgress = (text: string) => {
294
+ if (!available || !text || truncated) return;
295
+ const bytes = Buffer.byteLength(text);
296
+ if (scheduledBytes + bytes > progressByteLimit) {
297
+ truncated = true;
298
+ return;
299
+ }
300
+ scheduledBytes += bytes;
301
+ try {
302
+ logStream.write(text);
303
+ } catch {
304
+ failObserver();
305
+ }
306
+ };
307
+ try {
308
+ const watchdog = spawn(nodeExecutable, [
309
+ "-e", ORCA_CREATE_WATCHDOG_SCRIPT,
310
+ String(ORCA_CREATE_TIMEOUT_MS),
311
+ String(ORCA_KILL_GRACE_MS),
312
+ command,
313
+ "terminal", "create",
314
+ "--worktree", `path:${path.resolve(cwd)}`,
315
+ "--title", title,
316
+ "--command", viewerCommand(nodeExecutable, logPath, donePath),
317
+ "--json",
318
+ ], {
319
+ cwd,
320
+ detached: true,
321
+ stdio: "ignore",
322
+ windowsHide: true,
323
+ env: input.env ?? process.env,
324
+ });
325
+ watchdog.once("close", (code) => {
326
+ if (code !== 0) failObserver();
327
+ });
328
+ watchdog.once("error", failObserver);
329
+ watchdog.unref();
330
+ } catch {
331
+ failObserver();
332
+ return undefined;
333
+ }
334
+
335
+ let finished = false;
336
+ return {
337
+ append(text) {
338
+ if (finished) return;
339
+ writeProgress(text);
340
+ },
341
+ event(event) {
342
+ if (!available || finished) return;
343
+ if (event.type === "tool_execution_start" && event.toolName) {
344
+ const args = event.args && typeof event.args === "object" && !Array.isArray(event.args)
345
+ ? extractToolArgsPreview(event.args as Record<string, unknown>)
346
+ : "";
347
+ writeProgress(`\n› ${event.toolName}${args ? `: ${args}` : ""}\n`);
348
+ return;
349
+ }
350
+ if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
351
+ const text = extractTextFromContent(event.message.content);
352
+ if (text.trim()) writeProgress(`${text}${text.endsWith("\n") ? "" : "\n"}`);
353
+ }
354
+ },
355
+ finish(status, sessionFile) {
356
+ if (!available || finished) return;
357
+ finished = true;
358
+ const sessionId = status === "completed" ? resolvePiSessionId(sessionFile) : undefined;
359
+ let verifiedSessionFile: string | undefined;
360
+ if (sessionId && sessionFile) {
361
+ try { verifiedSessionFile = fs.realpathSync(sessionFile); } catch { /* the session is no longer available */ }
362
+ }
363
+ const terminalMessage = verifiedSessionFile
364
+ ? `completed. To remove the Pi session of this subagent, run rm -- ${shellQuote(verifiedSessionFile)}`
365
+ : status;
366
+ const truncation = truncated ? `\n[progress mirror truncated at ${MAX_MIRROR_BYTES} bytes]\n` : "";
367
+ let footer = `${truncation}\n${"─".repeat(48)}\n${terminalMessage}\n`;
368
+ if (scheduledBytes + Buffer.byteLength(footer) > MAX_MIRROR_BYTES) footer = `\n${status}\n`;
369
+ logStream.end(footer, () => {
370
+ if (!available) return;
371
+ try { fs.writeFileSync(donePath, `${status}\n`, { encoding: "utf-8", mode: 0o600 }); } catch { /* best effort */ }
372
+ scheduleCleanup(nodeExecutable, [logPath, donePath]);
373
+ });
374
+ },
375
+ };
376
+ }
@@ -41,6 +41,8 @@ export interface RunnerSubagentStep {
41
41
  sessionFile?: string;
42
42
  maxSubagentDepth?: number;
43
43
  timeoutMs?: number;
44
+ /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
45
+ toolTimeoutMs?: number;
44
46
  waitToolEnabled?: boolean;
45
47
  structuredOutput?: {
46
48
  schema: import("../../shared/types.ts").JsonSchemaObject;
@@ -6,6 +6,7 @@ import {
6
6
  type ControlNotificationChannel,
7
7
  type ResolvedControlConfig,
8
8
  } from "../../shared/types.ts";
9
+ import { isToolTimeoutExempt } from "./tool-timeout.ts";
9
10
 
10
11
  const CONTROL_EVENT_TYPES: ControlEventType[] = ["active_long_running", "needs_attention"];
11
12
  const CONTROL_NOTIFICATION_CHANNELS: ControlNotificationChannel[] = ["event", "async", "intercom"];
@@ -84,6 +85,18 @@ export function deriveActivityState(input: {
84
85
  return ageMs > input.config.needsAttentionAfterMs ? "needs_attention" : undefined;
85
86
  }
86
87
 
88
+ export function shouldEmitOpenToolAttention(input: {
89
+ config: ResolvedControlConfig;
90
+ currentTool?: string;
91
+ currentToolStartedAt?: number;
92
+ now?: number;
93
+ }): boolean {
94
+ if (!input.config.enabled || !input.currentTool || input.currentToolStartedAt === undefined) return false;
95
+ if (isToolTimeoutExempt(input.currentTool)) return false;
96
+ const now = input.now ?? Date.now();
97
+ return Math.max(0, now - input.currentToolStartedAt) >= input.config.activeNoticeAfterMs;
98
+ }
99
+
87
100
  export function buildControlEvent(input: {
88
101
  type?: ControlEventType;
89
102
  from?: ActivityState;
@@ -196,10 +209,12 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
196
209
  const supervisorHint = event.reason === "supervisor_request"
197
210
  ? "Supervisor request: reply to the pending request. If subagent_supervisor pending is empty, check intercom pending because an external intercom tool may own the request."
198
211
  : undefined;
212
+ const facts = formatLongRunningFacts(event);
199
213
  return [
200
214
  `Subagent needs attention: ${event.agent}`,
201
215
  `Run: ${runTarget}${event.index !== undefined ? ` step ${event.index + 1}` : ""}`,
202
216
  `Signal: ${event.message}`,
217
+ facts ? `Facts: ${facts}` : undefined,
203
218
  event.recentFailureSummary ? `Recent failures: ${event.recentFailureSummary}` : undefined,
204
219
  supervisorHint,
205
220
  "Hint: Inspect status first unless the run is clearly blocked. Use steer for a top-level live async child, routed resume for a live nested child, or resume to revive a paused/completed/failed child.",
@@ -580,16 +580,9 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
580
580
  } as unknown as SubagentState;
581
581
  if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, waitToolEnabled);
582
582
  let nativeSupervisorClientRegistered = false;
583
- let nativeSupervisorFallbackRegistered = false;
584
583
  const registerNativeSupervisorClientOnce = (): void => {
585
584
  if (nativeSupervisorClientRegistered) return;
586
585
  nativeSupervisorClientRegistered = true;
587
- registerNativeSupervisorClient(pi, { includeIntercomFallback: false });
588
- };
589
- const registerNativeSupervisorFallbackOnce = (): void => {
590
- registerNativeSupervisorClientOnce();
591
- if (nativeSupervisorFallbackRegistered) return;
592
- nativeSupervisorFallbackRegistered = true;
593
586
  registerNativeSupervisorClient(pi);
594
587
  };
595
588
  const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx?: ExtensionContext) => unknown) => void;
@@ -597,7 +590,6 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
597
590
  const sessionManager = (ctx as { sessionManager?: Parameters<typeof resolveCurrentSessionId>[0] } | undefined)?.sessionManager;
598
591
  waitState.currentSessionId = sessionManager ? resolveCurrentSessionId(sessionManager) : null;
599
592
  registerNativeSupervisorClientOnce();
600
- if (readRequiredChildTools()?.includes("intercom")) registerNativeSupervisorFallbackOnce();
601
593
  });
602
594
  onRuntimeEvent("agent_start", () => {
603
595
  refreshChildToolDiagnostic(pi);
@@ -650,7 +642,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
650
642
 
651
643
  onRuntimeEvent("before_agent_start", async (event: unknown) => {
652
644
  if (!event || typeof event !== "object" || !("systemPrompt" in event) || typeof event.systemPrompt !== "string") return undefined;
653
- registerNativeSupervisorFallbackOnce();
645
+ registerNativeSupervisorClientOnce();
654
646
  const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
655
647
  if (intercomSessionName && typeof pi.setSessionName === "function") {
656
648
  pi.setSessionName(intercomSessionName);
@@ -12,6 +12,7 @@ export const SUBAGENT_STARTUP_RETRY_DELAYS_MS = [250, 750, 1500] as const;
12
12
  export const MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS = 2000;
13
13
 
14
14
  const SIGKILL_PROCESS_SIGNAL_ERROR = formatProcessSignalError("SIGKILL");
15
+ const EXTENSION_REGISTRATION_CONFLICT_PATTERN = /Failed to load extension\b[^\n]*:\s*(?:Tool|Flag)\s+"[^"]+"\s+conflicts with\b/i;
15
16
 
16
17
  export interface SubagentStartupFailureEvidence {
17
18
  exitCode: number | null | undefined;
@@ -31,6 +32,17 @@ export interface SubagentStartupFailureEvidence {
31
32
  turnBudgetExceeded?: boolean;
32
33
  }
33
34
 
35
+ export function formatSubagentExtensionConflictError(
36
+ error: string | undefined,
37
+ input: { agent: string; ambientExtensionsEnabled: boolean },
38
+ ): string | undefined {
39
+ if (!error || !input.ambientExtensionsEnabled || !EXTENSION_REGISTRATION_CONFLICT_PATTERN.test(error)) return error;
40
+ const settings = JSON.stringify({
41
+ subagents: { agentOverrides: { [input.agent]: { extensions: [] } } },
42
+ });
43
+ return `${error}\n\nSubagent startup loaded conflicting ambient Pi extensions. Retry this run after disabling ambient extension discovery for agent ${JSON.stringify(input.agent)} in project .pi/settings.json: ${settings}. Replace the empty array with only the required extension entry paths when this agent needs extension tools.`;
44
+ }
45
+
34
46
  function hasUsage(usage: Usage): boolean {
35
47
  return usage.input !== 0
36
48
  || usage.output !== 0
@@ -0,0 +1,95 @@
1
+ import { readSubagentEnv } from "../../shared/env.ts";
2
+
3
+ export const TOOL_TIMEOUT_ENV = "SELESAI_SUBAGENT_TOOL_TIMEOUT_MS";
4
+
5
+ /** Maximum delay a Node.js timer accepts without overflow. */
6
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
7
+
8
+ export const DEFAULT_FAST_TOOL_TIMEOUT_MS = 300_000;
9
+
10
+ export const DEFAULT_FAST_TOOL_TIMEOUT_TOOLS = new Set([
11
+ "read",
12
+ "grep",
13
+ "find",
14
+ "ls",
15
+ "edit",
16
+ "write",
17
+ "structured_output",
18
+ ]);
19
+
20
+ /** Tools whose normal job can be to wait for a person or another run. */
21
+ export const TOOL_TIMEOUT_EXEMPT_TOOLS = new Set(["contact_supervisor", "intercom", "subagent_wait"]);
22
+
23
+ // Backward-compatible export name for existing callers/tests.
24
+ export const TOOL_TIMEOUT_ALLOWLIST = TOOL_TIMEOUT_EXEMPT_TOOLS;
25
+
26
+ export function isToolTimeoutExempt(toolName: string | undefined): boolean {
27
+ return toolName !== undefined && TOOL_TIMEOUT_EXEMPT_TOOLS.has(toolName);
28
+ }
29
+
30
+ export function defaultToolTimeoutMs(toolName: string | undefined): number | undefined {
31
+ return toolName !== undefined && DEFAULT_FAST_TOOL_TIMEOUT_TOOLS.has(toolName)
32
+ ? DEFAULT_FAST_TOOL_TIMEOUT_MS
33
+ : undefined;
34
+ }
35
+
36
+ export function effectiveToolTimeoutMs(toolName: string | undefined, configuredToolTimeoutMs: number | undefined): number | undefined {
37
+ if (isToolTimeoutExempt(toolName)) return undefined;
38
+ return configuredToolTimeoutMs ?? defaultToolTimeoutMs(toolName);
39
+ }
40
+
41
+ export function formatToolTimeoutMessage(toolName: string, timeoutMs: number): string {
42
+ return `Tool '${toolName}' exceeded its timeout of ${timeoutMs}ms.`;
43
+ }
44
+
45
+ export function toolTimeoutCallKey(event: { toolCallId?: unknown; toolName?: unknown }, fallbackId: number): string {
46
+ return typeof event.toolCallId === "string" && event.toolCallId.length > 0
47
+ ? `id:${event.toolCallId}`
48
+ : `anon:${String(event.toolName ?? "tool")}:${fallbackId}`;
49
+ }
50
+
51
+ export interface ToolTimeoutResolutionInput {
52
+ /** Per-call value from the subagent tool params (highest precedence). */
53
+ callValue?: unknown;
54
+ /** Agent frontmatter default (second precedence). */
55
+ agentValue?: number;
56
+ /** Global extension config.toolTimeoutMs (third precedence). */
57
+ configValue?: unknown;
58
+ /** SELESAI_SUBAGENT_TOOL_TIMEOUT_MS environment override (lowest precedence). */
59
+ envValue?: string | undefined;
60
+ }
61
+
62
+ /** Resolve the configured hard timeout. Default fast-tool timeouts apply later per tool name. */
63
+ export function resolveToolTimeoutMs(input: ToolTimeoutResolutionInput): { toolTimeoutMs?: number; error?: string } {
64
+ const candidates: Array<{ label: string; value: unknown }> = [
65
+ { label: "toolTimeoutMs", value: input.callValue },
66
+ { label: "agent.toolTimeoutMs", value: input.agentValue },
67
+ { label: "config.toolTimeoutMs", value: input.configValue },
68
+ ];
69
+ let winner: { label: string; value: unknown } | undefined;
70
+ for (const candidate of candidates) {
71
+ if (candidate.value === undefined) continue;
72
+ winner = candidate;
73
+ break;
74
+ }
75
+ if (winner === undefined && input.envValue !== undefined && input.envValue.trim() !== "") {
76
+ winner = { label: TOOL_TIMEOUT_ENV, value: input.envValue };
77
+ }
78
+ if (winner === undefined) return {};
79
+
80
+ let raw: unknown = winner.value;
81
+ let parsed: number;
82
+ if (winner.label === TOOL_TIMEOUT_ENV && typeof raw === "string") {
83
+ parsed = Number(raw);
84
+ if (raw.trim() !== "" && !Number.isNaN(parsed)) raw = parsed;
85
+ }
86
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0 || raw > MAX_TIMER_DELAY_MS) {
87
+ return { error: `${winner.label} must be a positive integer no larger than ${MAX_TIMER_DELAY_MS}.` };
88
+ }
89
+ return { toolTimeoutMs: raw };
90
+ }
91
+
92
+ /** Read the environment override without requiring callers to know the name. */
93
+ export function toolTimeoutFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {
94
+ return readSubagentEnv(env, TOOL_TIMEOUT_ENV);
95
+ }
@@ -0,0 +1,5 @@
1
+ import type { StreamFn } from "@earendil-works/pi-agent-core";
2
+
3
+ export function agentStreamOptions(streamFn: StreamFn): { streamFunction: StreamFn; streamFn: StreamFn } {
4
+ return { streamFunction: streamFn, streamFn };
5
+ }
@@ -65,10 +65,6 @@ function patternMatchesArtifactPath(pattern: string, artifactPath: string): bool
65
65
  || globMatchesPath(normalized, artifactPath);
66
66
  }
67
67
 
68
- function patternMatchesProjectArtifacts(pattern: string): boolean {
69
- return PROJECT_ARTIFACT_PATHS.some((artifactPath) => patternMatchesArtifactPath(pattern, artifactPath));
70
- }
71
-
72
68
  function ignoreFileExcludesProjectArtifacts(filePath: string): boolean {
73
69
  if (!fs.existsSync(filePath)) return false;
74
70
  try {
@@ -98,3 +98,53 @@ export function previewDisplayText(value: string, maxLength: number): string {
98
98
  if (maxLength <= 3) return truncateDisplayText(normalized, maxLength);
99
99
  return `${truncateDisplayText(normalized, maxLength - 3)}...`;
100
100
  }
101
+
102
+ const BINARY_CONTENT_PLACEHOLDER = "[binary content omitted for safe display]";
103
+
104
+ function isUnsafeTerminalCodePoint(codePoint: number): boolean {
105
+ const terminalControl = (codePoint <= 0x1f && codePoint !== 0x09 && codePoint !== 0x0a)
106
+ || (codePoint >= 0x7f && codePoint <= 0x9f);
107
+ const bidiControl = codePoint === 0x061c
108
+ || codePoint === 0x200e
109
+ || codePoint === 0x200f
110
+ || (codePoint >= 0x202a && codePoint <= 0x202e)
111
+ || (codePoint >= 0x2066 && codePoint <= 0x2069);
112
+ const privateUse = (codePoint >= 0xe000 && codePoint <= 0xf8ff)
113
+ || (codePoint >= 0xf0000 && codePoint <= 0xffffd)
114
+ || (codePoint >= 0x100000 && codePoint <= 0x10fffd);
115
+ const invalidScalar = codePoint >= 0xd800 && codePoint <= 0xdfff;
116
+ const nonCharacter = (codePoint >= 0xfdd0 && codePoint <= 0xfdef)
117
+ || (codePoint & 0xffff) === 0xfffe
118
+ || (codePoint & 0xffff) === 0xffff;
119
+ return terminalControl || bidiControl || privateUse || invalidScalar || nonCharacter;
120
+ }
121
+
122
+ function looksLikeBinaryContent(text: string): boolean {
123
+ if (text.includes("\0")) return true;
124
+ let suspiciousControls = 0;
125
+ let replacementCharacters = 0;
126
+ let codePoints = 0;
127
+ for (const character of text) {
128
+ codePoints++;
129
+ const codePoint = character.codePointAt(0) ?? 0;
130
+ if ((codePoint <= 0x08) || (codePoint >= 0x0e && codePoint <= 0x1f)) suspiciousControls++;
131
+ if (codePoint === 0xfffd) replacementCharacters++;
132
+ }
133
+ if (codePoints === 0) return false;
134
+ return (suspiciousControls >= 4 && suspiciousControls / codePoints >= 0.1)
135
+ || (replacementCharacters >= 3 && replacementCharacters / codePoints >= 0.1);
136
+ }
137
+
138
+ /** Escapes untrusted text before rendering it in a terminal while preserving safe newlines and tabs. */
139
+ export function safeTerminalText(value: string): string {
140
+ const normalized = value.replace(/\r\n/g, "\n");
141
+ if (looksLikeBinaryContent(normalized)) return BINARY_CONTENT_PLACEHOLDER;
142
+ let safe = "";
143
+ for (const character of normalized) {
144
+ const codePoint = character.codePointAt(0) ?? 0;
145
+ safe += isUnsafeTerminalCodePoint(codePoint)
146
+ ? `[U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}]`
147
+ : character;
148
+ }
149
+ return safe;
150
+ }
@@ -0,0 +1,21 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ function isNodeExecutableName(execPath: string): boolean {
5
+ const basename = path.basename(execPath).toLowerCase();
6
+ return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
7
+ }
8
+
9
+ function canUseNodeExecutable(execPath: string): boolean {
10
+ try {
11
+ fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ export function resolveNodeExecutable(execPath = process.execPath): string {
19
+ if (isNodeExecutableName(execPath) && canUseNodeExecutable(execPath)) return execPath;
20
+ return process.platform === "win32" ? "node.exe" : "node";
21
+ }