@oh-my-pi/pi-coding-agent 3.24.0 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +4 -4
  3. package/src/core/custom-commands/bundled/wt/index.ts +3 -0
  4. package/src/core/sdk.ts +7 -0
  5. package/src/core/tools/complete.ts +129 -0
  6. package/src/core/tools/index.test.ts +9 -1
  7. package/src/core/tools/index.ts +18 -5
  8. package/src/core/tools/jtd-to-json-schema.ts +252 -0
  9. package/src/core/tools/output.ts +125 -14
  10. package/src/core/tools/read.ts +4 -4
  11. package/src/core/tools/task/artifacts.ts +6 -9
  12. package/src/core/tools/task/executor.ts +189 -24
  13. package/src/core/tools/task/index.ts +23 -18
  14. package/src/core/tools/task/name-generator.ts +1577 -0
  15. package/src/core/tools/task/render.ts +137 -8
  16. package/src/core/tools/task/types.ts +26 -5
  17. package/src/core/tools/task/worker-protocol.ts +1 -0
  18. package/src/core/tools/task/worker.ts +136 -14
  19. package/src/core/tools/web-fetch-handlers/academic.test.ts +239 -0
  20. package/src/core/tools/web-fetch-handlers/artifacthub.ts +210 -0
  21. package/src/core/tools/web-fetch-handlers/arxiv.ts +84 -0
  22. package/src/core/tools/web-fetch-handlers/aur.ts +171 -0
  23. package/src/core/tools/web-fetch-handlers/biorxiv.ts +136 -0
  24. package/src/core/tools/web-fetch-handlers/bluesky.ts +277 -0
  25. package/src/core/tools/web-fetch-handlers/brew.ts +173 -0
  26. package/src/core/tools/web-fetch-handlers/business.test.ts +82 -0
  27. package/src/core/tools/web-fetch-handlers/cheatsh.ts +73 -0
  28. package/src/core/tools/web-fetch-handlers/chocolatey.ts +153 -0
  29. package/src/core/tools/web-fetch-handlers/coingecko.ts +179 -0
  30. package/src/core/tools/web-fetch-handlers/crates-io.ts +123 -0
  31. package/src/core/tools/web-fetch-handlers/dev-platforms.test.ts +254 -0
  32. package/src/core/tools/web-fetch-handlers/devto.ts +173 -0
  33. package/src/core/tools/web-fetch-handlers/discogs.ts +303 -0
  34. package/src/core/tools/web-fetch-handlers/dockerhub.ts +156 -0
  35. package/src/core/tools/web-fetch-handlers/documentation.test.ts +85 -0
  36. package/src/core/tools/web-fetch-handlers/finance-media.test.ts +144 -0
  37. package/src/core/tools/web-fetch-handlers/git-hosting.test.ts +272 -0
  38. package/src/core/tools/web-fetch-handlers/github-gist.ts +64 -0
  39. package/src/core/tools/web-fetch-handlers/github.ts +424 -0
  40. package/src/core/tools/web-fetch-handlers/gitlab.ts +444 -0
  41. package/src/core/tools/web-fetch-handlers/go-pkg.ts +271 -0
  42. package/src/core/tools/web-fetch-handlers/hackage.ts +89 -0
  43. package/src/core/tools/web-fetch-handlers/hackernews.ts +208 -0
  44. package/src/core/tools/web-fetch-handlers/hex.ts +121 -0
  45. package/src/core/tools/web-fetch-handlers/huggingface.ts +385 -0
  46. package/src/core/tools/web-fetch-handlers/iacr.ts +82 -0
  47. package/src/core/tools/web-fetch-handlers/index.ts +69 -0
  48. package/src/core/tools/web-fetch-handlers/lobsters.ts +186 -0
  49. package/src/core/tools/web-fetch-handlers/mastodon.ts +302 -0
  50. package/src/core/tools/web-fetch-handlers/maven.ts +147 -0
  51. package/src/core/tools/web-fetch-handlers/mdn.ts +174 -0
  52. package/src/core/tools/web-fetch-handlers/media.test.ts +138 -0
  53. package/src/core/tools/web-fetch-handlers/metacpan.ts +247 -0
  54. package/src/core/tools/web-fetch-handlers/npm.ts +107 -0
  55. package/src/core/tools/web-fetch-handlers/nuget.ts +201 -0
  56. package/src/core/tools/web-fetch-handlers/nvd.ts +238 -0
  57. package/src/core/tools/web-fetch-handlers/opencorporates.ts +273 -0
  58. package/src/core/tools/web-fetch-handlers/openlibrary.ts +313 -0
  59. package/src/core/tools/web-fetch-handlers/osv.ts +184 -0
  60. package/src/core/tools/web-fetch-handlers/package-managers-2.test.ts +199 -0
  61. package/src/core/tools/web-fetch-handlers/package-managers.test.ts +171 -0
  62. package/src/core/tools/web-fetch-handlers/package-registries.test.ts +259 -0
  63. package/src/core/tools/web-fetch-handlers/packagist.ts +170 -0
  64. package/src/core/tools/web-fetch-handlers/pub-dev.ts +185 -0
  65. package/src/core/tools/web-fetch-handlers/pubmed.ts +174 -0
  66. package/src/core/tools/web-fetch-handlers/pypi.ts +125 -0
  67. package/src/core/tools/web-fetch-handlers/readthedocs.ts +122 -0
  68. package/src/core/tools/web-fetch-handlers/reddit.ts +100 -0
  69. package/src/core/tools/web-fetch-handlers/repology.ts +257 -0
  70. package/src/core/tools/web-fetch-handlers/research.test.ts +107 -0
  71. package/src/core/tools/web-fetch-handlers/rfc.ts +205 -0
  72. package/src/core/tools/web-fetch-handlers/rubygems.ts +112 -0
  73. package/src/core/tools/web-fetch-handlers/sec-edgar.ts +269 -0
  74. package/src/core/tools/web-fetch-handlers/security.test.ts +103 -0
  75. package/src/core/tools/web-fetch-handlers/semantic-scholar.ts +190 -0
  76. package/src/core/tools/web-fetch-handlers/social-extended.test.ts +192 -0
  77. package/src/core/tools/web-fetch-handlers/social.test.ts +259 -0
  78. package/src/core/tools/web-fetch-handlers/spotify.ts +218 -0
  79. package/src/core/tools/web-fetch-handlers/stackexchange.test.ts +120 -0
  80. package/src/core/tools/web-fetch-handlers/stackoverflow.ts +123 -0
  81. package/src/core/tools/web-fetch-handlers/standards.test.ts +122 -0
  82. package/src/core/tools/web-fetch-handlers/terraform.ts +296 -0
  83. package/src/core/tools/web-fetch-handlers/tldr.ts +47 -0
  84. package/src/core/tools/web-fetch-handlers/twitter.ts +84 -0
  85. package/src/core/tools/web-fetch-handlers/types.ts +163 -0
  86. package/src/core/tools/web-fetch-handlers/utils.ts +91 -0
  87. package/src/core/tools/web-fetch-handlers/vimeo.ts +152 -0
  88. package/src/core/tools/web-fetch-handlers/wikidata.ts +349 -0
  89. package/src/core/tools/web-fetch-handlers/wikipedia.test.ts +73 -0
  90. package/src/core/tools/web-fetch-handlers/wikipedia.ts +91 -0
  91. package/src/core/tools/web-fetch-handlers/youtube.test.ts +198 -0
  92. package/src/core/tools/web-fetch-handlers/youtube.ts +319 -0
  93. package/src/core/tools/web-fetch.ts +152 -1324
  94. package/src/prompts/task.md +14 -50
  95. package/src/prompts/tools/output.md +2 -1
  96. package/src/prompts/tools/task.md +3 -1
  97. package/src/utils/tools-manager.ts +110 -8
@@ -69,6 +69,118 @@ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): s
69
69
  return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
70
70
  }
71
71
 
72
+ function formatJsonScalar(value: unknown): string {
73
+ if (value === null) return "null";
74
+ if (typeof value === "string") return `"${value}"`;
75
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
76
+ return "";
77
+ }
78
+
79
+ function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
80
+ return ancestors.map((hasNext) => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
81
+ }
82
+
83
+ function renderJsonTreeLines(
84
+ value: unknown,
85
+ theme: Theme,
86
+ maxDepth: number,
87
+ maxLines: number,
88
+ ): { lines: string[]; truncated: boolean } {
89
+ const lines: string[] = [];
90
+ let truncated = false;
91
+
92
+ const iconObject = theme.styledSymbol("icon.folder", "muted");
93
+ const iconArray = theme.styledSymbol("icon.package", "muted");
94
+ const iconScalar = theme.styledSymbol("icon.file", "muted");
95
+
96
+ const pushLine = (line: string) => {
97
+ if (lines.length >= maxLines) {
98
+ truncated = true;
99
+ return false;
100
+ }
101
+ lines.push(line);
102
+ return true;
103
+ };
104
+
105
+ const renderNode = (val: unknown, key: string | undefined, ancestors: boolean[], isLast: boolean, depth: number) => {
106
+ if (lines.length >= maxLines) {
107
+ truncated = true;
108
+ return;
109
+ }
110
+
111
+ const connector = isLast ? theme.tree.last : theme.tree.branch;
112
+ const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
113
+ const scalar = formatJsonScalar(val);
114
+
115
+ if (scalar) {
116
+ const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
117
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
118
+ return;
119
+ }
120
+
121
+ if (Array.isArray(val)) {
122
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
123
+ pushLine(`${prefix}${iconArray} ${header}`);
124
+ if (val.length === 0) {
125
+ pushLine(
126
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", "[]")}`,
127
+ );
128
+ return;
129
+ }
130
+ if (depth >= maxDepth) {
131
+ pushLine(
132
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", theme.format.ellipsis)}`,
133
+ );
134
+ return;
135
+ }
136
+ const nextAncestors = [...ancestors, !isLast];
137
+ for (let i = 0; i < val.length; i++) {
138
+ renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1);
139
+ if (lines.length >= maxLines) {
140
+ truncated = true;
141
+ return;
142
+ }
143
+ }
144
+ return;
145
+ }
146
+
147
+ if (val && typeof val === "object") {
148
+ const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
149
+ pushLine(`${prefix}${iconObject} ${header}`);
150
+ const entries = Object.entries(val as Record<string, unknown>);
151
+ if (entries.length === 0) {
152
+ pushLine(
153
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", "{}")}`,
154
+ );
155
+ return;
156
+ }
157
+ if (depth >= maxDepth) {
158
+ pushLine(
159
+ `${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.hook)} ${theme.fg("dim", theme.format.ellipsis)}`,
160
+ );
161
+ return;
162
+ }
163
+ const nextAncestors = [...ancestors, !isLast];
164
+ for (let i = 0; i < entries.length; i++) {
165
+ const [childKey, child] = entries[i];
166
+ renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1);
167
+ if (lines.length >= maxLines) {
168
+ truncated = true;
169
+ return;
170
+ }
171
+ }
172
+ return;
173
+ }
174
+
175
+ const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
176
+ pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", String(val))}`);
177
+ };
178
+
179
+ renderNode(value, undefined, [], true, 0);
180
+
181
+ return { lines, truncated };
182
+ }
183
+
72
184
  function renderOutputSection(
73
185
  output: string,
74
186
  continuePrefix: string,
@@ -78,11 +190,30 @@ function renderOutputSection(
78
190
  maxExpanded = 10,
79
191
  ): string[] {
80
192
  const lines: string[] = [];
81
- const outputLines = output.split("\n").filter((line) => line.trim());
82
- if (outputLines.length === 0) return lines;
193
+ const trimmedOutput = output.trim();
194
+ if (!trimmedOutput) return lines;
83
195
 
84
196
  lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
85
197
 
198
+ if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
199
+ try {
200
+ const parsed = JSON.parse(trimmedOutput);
201
+ const tree = renderJsonTreeLines(parsed, theme, expanded ? 6 : 2, expanded ? 24 : 6);
202
+ if (tree.lines.length > 0) {
203
+ for (const line of tree.lines) {
204
+ lines.push(`${continuePrefix} ${line}`);
205
+ }
206
+ if (tree.truncated) {
207
+ lines.push(`${continuePrefix} ${theme.fg("dim", theme.format.ellipsis)}`);
208
+ }
209
+ return lines;
210
+ }
211
+ } catch {
212
+ // Fall back to raw output
213
+ }
214
+ }
215
+
216
+ const outputLines = output.split("\n").filter((line) => line.trim());
86
217
  const previewCount = expanded ? maxExpanded : maxCollapsed;
87
218
  for (const line of outputLines.slice(0, previewCount)) {
88
219
  lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 70, theme.format.ellipsis))}`);
@@ -144,9 +275,8 @@ function renderAgentProgress(
144
275
  ? "error"
145
276
  : "accent";
146
277
 
147
- // Main status line - include index for Output tool ID derivation
148
- const agentId = `${progress.agent}(${progress.index})`;
149
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)}`;
278
+ // Main status line - use taskId for Output tool
279
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", progress.taskId)}`;
150
280
  const description = progress.description?.trim();
151
281
  if (description) {
152
282
  statusLine += ` ${theme.fg("muted", truncate(description, 40, theme.format.ellipsis))}`;
@@ -342,9 +472,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
342
472
  const iconColor = success ? "success" : "error";
343
473
  const statusText = aborted ? "aborted" : success ? "done" : "failed";
344
474
 
345
- // Main status line - include index for Output tool ID derivation
346
- const agentId = `${result.agent}(${result.index})`;
347
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)} ${formatBadge(statusText, iconColor, theme)}`;
475
+ // Main status line - use taskId for Output tool
476
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", result.taskId)} ${formatBadge(statusText, iconColor, theme)}`;
348
477
  const description = result.description?.trim();
349
478
  if (description) {
350
479
  statusLine += ` ${theme.fg("muted", truncate(description, 40, theme.format.ellipsis))}`;
@@ -4,20 +4,34 @@ import { type Static, Type } from "@sinclair/typebox";
4
4
  /** Source of an agent definition */
5
5
  export type AgentSource = "bundled" | "user" | "project";
6
6
 
7
+ function getEnv(name: string, defaultValue: number): number {
8
+ const value = process.env[name];
9
+ if (value === undefined) {
10
+ return defaultValue;
11
+ }
12
+ try {
13
+ const number = Number.parseInt(value, 10);
14
+ if (!Number.isNaN(number) && number > 0) {
15
+ return number;
16
+ }
17
+ } catch {}
18
+ return defaultValue;
19
+ }
20
+
7
21
  /** Maximum tasks per call */
8
- export const MAX_PARALLEL_TASKS = 32;
22
+ export const MAX_PARALLEL_TASKS = getEnv("OMP_TASK_MAX_PARALLEL", 32);
9
23
 
10
24
  /** Maximum concurrent workers */
11
- export const MAX_CONCURRENCY = 16;
25
+ export const MAX_CONCURRENCY = getEnv("OMP_TASK_MAX_CONCURRENCY", 16);
12
26
 
13
27
  /** Maximum output bytes per agent */
14
- export const MAX_OUTPUT_BYTES = 500_000;
28
+ export const MAX_OUTPUT_BYTES = getEnv("OMP_TASK_MAX_OUTPUT_BYTES", 500_000);
15
29
 
16
30
  /** Maximum output lines per agent */
17
- export const MAX_OUTPUT_LINES = 5000;
31
+ export const MAX_OUTPUT_LINES = getEnv("OMP_TASK_MAX_OUTPUT_LINES", 5000);
18
32
 
19
33
  /** Maximum agents to show in description */
20
- export const MAX_AGENTS_IN_DESCRIPTION = 10;
34
+ export const MAX_AGENTS_IN_DESCRIPTION = getEnv("OMP_TASK_MAX_AGENTS_IN_DESCRIPTION", 10);
21
35
 
22
36
  /** EventBus channel for raw subagent events */
23
37
  export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
@@ -38,6 +52,11 @@ export type TaskItem = Static<typeof taskItemSchema>;
38
52
  /** Task tool parameters */
39
53
  export const taskSchema = Type.Object({
40
54
  context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
55
+ output_schema: Type.Optional(
56
+ Type.Any({
57
+ description: "JSON schema for structured subagent output (used by the complete tool)",
58
+ }),
59
+ ),
41
60
  tasks: Type.Array(taskItemSchema, {
42
61
  description: "Tasks to run in parallel",
43
62
  maxItems: MAX_PARALLEL_TASKS,
@@ -85,6 +104,7 @@ export interface AgentDefinition {
85
104
  /** Progress tracking for a single agent */
86
105
  export interface AgentProgress {
87
106
  index: number;
107
+ taskId: string;
88
108
  agent: string;
89
109
  agentSource: AgentSource;
90
110
  status: "pending" | "running" | "completed" | "failed" | "aborted";
@@ -106,6 +126,7 @@ export interface AgentProgress {
106
126
  /** Result from a single agent execution */
107
127
  export interface SingleResult {
108
128
  index: number;
129
+ taskId: string;
109
130
  agent: string;
110
131
  agentSource: AgentSource;
111
132
  task: string;
@@ -6,6 +6,7 @@ export interface SubagentWorkerStartPayload {
6
6
  systemPrompt: string;
7
7
  model?: string;
8
8
  toolNames?: string[];
9
+ outputSchema?: unknown;
9
10
  sessionFile?: string | null;
10
11
  spawnsEnv?: string;
11
12
  }
@@ -24,7 +24,11 @@ import type { SubagentWorkerRequest, SubagentWorkerResponse, SubagentWorkerStart
24
24
  type PostMessageFn = (message: SubagentWorkerResponse) => void;
25
25
 
26
26
  const postMessageSafe: PostMessageFn = (message) => {
27
- (globalThis as typeof globalThis & { postMessage: PostMessageFn }).postMessage(message);
27
+ try {
28
+ (globalThis as typeof globalThis & { postMessage: PostMessageFn }).postMessage(message);
29
+ } catch {
30
+ // Parent may have terminated worker, nothing we can do
31
+ }
28
32
  };
29
33
 
30
34
  interface WorkerMessageEvent<T> {
@@ -51,7 +55,9 @@ const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent => {
51
55
 
52
56
  let running = false;
53
57
  let abortRequested = false;
58
+ let doneSent = false;
54
59
  let activeSession: { abort: () => Promise<void>; dispose: () => Promise<void> } | null = null;
60
+ let unsubscribe: (() => void) | null = null;
55
61
 
56
62
  /**
57
63
  * Resolve model string to Model object with optional thinking level.
@@ -123,6 +129,9 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
123
129
 
124
130
  // Create agent session (equivalent to CLI's createAgentSession)
125
131
  // Note: hasUI: false disables interactive features
132
+ const completionInstruction =
133
+ "When finished, call the complete tool exactly once. Do not end with a plain-text final answer.";
134
+
126
135
  const { session } = await createAgentSession({
127
136
  cwd: payload.cwd,
128
137
  authStorage,
@@ -130,8 +139,10 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
130
139
  model,
131
140
  thinkingLevel,
132
141
  toolNames: payload.toolNames,
142
+ outputSchema: payload.outputSchema,
143
+ requireCompleteTool: true,
133
144
  // Append system prompt (equivalent to CLI's --append-system-prompt)
134
- systemPrompt: (defaultPrompt) => `${defaultPrompt}\n\n${payload.systemPrompt}`,
145
+ systemPrompt: (defaultPrompt) => `${defaultPrompt}\n\n${payload.systemPrompt}\n\n${completionInstruction}`,
135
146
  sessionManager,
136
147
  hasUI: false,
137
148
  // Pass spawn restrictions to nested tasks
@@ -140,6 +151,17 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
140
151
 
141
152
  activeSession = session;
142
153
 
154
+ if (abortRequested) {
155
+ aborted = true;
156
+ exitCode = 1;
157
+ try {
158
+ await session.abort();
159
+ } catch {
160
+ // Ignore abort errors
161
+ }
162
+ return;
163
+ }
164
+
143
165
  // Initialize extensions (equivalent to CLI's extension initialization)
144
166
  // Note: Does not support --extension CLI flag or extension CLI flags
145
167
  const extensionRunner = session.extensionRunner;
@@ -164,16 +186,43 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
164
186
  await extensionRunner.emit({ type: "session_start" });
165
187
  }
166
188
 
189
+ // Track complete tool calls
190
+ const MAX_COMPLETE_RETRIES = 3;
191
+ let completeCalled = false;
192
+
167
193
  // Subscribe to events and forward to parent (equivalent to --mode json output)
168
- session.subscribe((event: AgentSessionEvent) => {
194
+ unsubscribe = session.subscribe((event: AgentSessionEvent) => {
169
195
  if (isAgentEvent(event)) {
170
196
  postMessageSafe({ type: "event", event });
197
+ // Track when complete tool is called
198
+ if (event.type === "tool_execution_end" && event.toolName === "complete") {
199
+ completeCalled = true;
200
+ }
171
201
  }
172
202
  });
173
203
 
174
204
  // Run the prompt (equivalent to --prompt flag)
175
205
  await session.prompt(payload.task);
176
206
 
207
+ // Retry loop if complete was not called
208
+ let retryCount = 0;
209
+ while (!completeCalled && retryCount < MAX_COMPLETE_RETRIES && !abortRequested) {
210
+ retryCount++;
211
+ const reminder = `<system-reminder>
212
+ CRITICAL: You stopped without calling the complete tool. This is reminder ${retryCount} of ${MAX_COMPLETE_RETRIES}.
213
+
214
+ You MUST call the complete tool to finish your task. Options:
215
+ 1. Call complete with your result data if you have completed the task
216
+ 2. Call complete with status="aborted" and an error message if you cannot complete the task
217
+
218
+ Failure to call complete after ${MAX_COMPLETE_RETRIES} reminders will result in task failure.
219
+ </system-reminder>
220
+
221
+ Call complete now.`;
222
+
223
+ await session.prompt(reminder);
224
+ }
225
+
177
226
  // Check if aborted during execution
178
227
  const lastMessage = session.state.messages[session.state.messages.length - 1];
179
228
  if (lastMessage?.role === "assistant" && lastMessage.stopReason === "aborted") {
@@ -190,24 +239,39 @@ async function runTask(payload: SubagentWorkerStartPayload): Promise<void> {
190
239
  if (exitCode === 0) exitCode = 1;
191
240
  }
192
241
 
193
- // Cleanup session
242
+ if (unsubscribe) {
243
+ try {
244
+ unsubscribe();
245
+ } catch {
246
+ // Ignore unsubscribe errors
247
+ }
248
+ unsubscribe = null;
249
+ }
250
+
251
+ // Cleanup session with timeout to prevent hanging
194
252
  if (activeSession) {
253
+ const session = activeSession;
254
+ activeSession = null;
195
255
  try {
196
- await activeSession.dispose();
256
+ await Promise.race([session.dispose(), new Promise<void>((resolve) => setTimeout(resolve, 5000))]);
197
257
  } catch {
198
258
  // Ignore cleanup errors
199
259
  }
200
- activeSession = null;
201
260
  }
202
261
 
203
- // Send completion message to parent
204
- postMessageSafe({
205
- type: "done",
206
- exitCode,
207
- durationMs: Date.now() - startTime,
208
- error,
209
- aborted,
210
- });
262
+ running = false;
263
+
264
+ // Send completion message to parent (only once)
265
+ if (!doneSent) {
266
+ doneSent = true;
267
+ postMessageSafe({
268
+ type: "done",
269
+ exitCode,
270
+ durationMs: Date.now() - startTime,
271
+ error,
272
+ aborted,
273
+ });
274
+ }
211
275
  }
212
276
  }
213
277
 
@@ -219,6 +283,64 @@ function handleAbort(): void {
219
283
  }
220
284
  }
221
285
 
286
+ // Global error handlers to ensure we always send a done message
287
+ // Using self instead of globalThis for proper worker scope typing
288
+ declare const self: {
289
+ addEventListener(type: "error", listener: (event: ErrorEvent) => void): void;
290
+ addEventListener(type: "unhandledrejection", listener: (event: { reason: unknown }) => void): void;
291
+ addEventListener(type: "messageerror", listener: (event: MessageEvent) => void): void;
292
+ };
293
+
294
+ self.addEventListener("error", (event) => {
295
+ if (!running || doneSent) return;
296
+ doneSent = true;
297
+ abortRequested = true;
298
+ if (activeSession) {
299
+ void activeSession.abort();
300
+ }
301
+ postMessageSafe({
302
+ type: "done",
303
+ exitCode: 1,
304
+ durationMs: 0,
305
+ error: `Uncaught error: ${event.message || "Unknown error"}`,
306
+ aborted: false,
307
+ });
308
+ });
309
+
310
+ self.addEventListener("unhandledrejection", (event) => {
311
+ if (!running || doneSent) return;
312
+ doneSent = true;
313
+ abortRequested = true;
314
+ if (activeSession) {
315
+ void activeSession.abort();
316
+ }
317
+ const reason = event.reason;
318
+ const message = reason instanceof Error ? reason.stack || reason.message : String(reason);
319
+ postMessageSafe({
320
+ type: "done",
321
+ exitCode: 1,
322
+ durationMs: 0,
323
+ error: `Unhandled rejection: ${message}`,
324
+ aborted: false,
325
+ });
326
+ });
327
+
328
+ self.addEventListener("messageerror", () => {
329
+ if (doneSent) return;
330
+ doneSent = true;
331
+ abortRequested = true;
332
+ if (activeSession) {
333
+ void activeSession.abort();
334
+ }
335
+ postMessageSafe({
336
+ type: "done",
337
+ exitCode: 1,
338
+ durationMs: 0,
339
+ error: "Failed to deserialize parent message",
340
+ aborted: false,
341
+ });
342
+ });
343
+
222
344
  // Message handler - receives start/abort commands from parent
223
345
  globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorkerRequest>) => {
224
346
  const message = event.data;