@swifty.js/swifty 0.0.35 → 0.0.36

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.
package/dist/lib/index.js CHANGED
@@ -10,8 +10,10 @@ import {
10
10
  ConversationManager,
11
11
  FileHistory,
12
12
  RecoveryState,
13
+ TOOL_RESULT_PREVIEW_CHARS,
13
14
  agent_exports,
14
15
  asImageMediaType,
16
+ buildPersistedOutputPreview,
15
17
  captureTelemetryError,
16
18
  compact_exports,
17
19
  conversation_exports,
@@ -27,6 +29,7 @@ import {
27
29
  loadSession,
28
30
  maybeResizeAndDownsampleImage,
29
31
  newSessionId,
32
+ persistLargeResult,
30
33
  plan_file_exports,
31
34
  plan_mode_exports,
32
35
  prompts_exports,
@@ -37,13 +40,14 @@ import {
37
40
  saveMessage,
38
41
  session_exports,
39
42
  shutdownTelemetry,
43
+ spillDir,
40
44
  telemetry_exports,
41
45
  tool_result_exports
42
- } from "./chunk-S5VWPTME.js";
46
+ } from "./chunk-YUDCYSKO.js";
43
47
  import {
44
48
  version,
45
49
  version_exports
46
- } from "./chunk-OJTISIYU.js";
50
+ } from "./chunk-PJ7354ZG.js";
47
51
  import {
48
52
  anthropic_exports,
49
53
  decideAndApply,
@@ -78,7 +82,7 @@ import {
78
82
  PermissionChecker,
79
83
  isSafeCommand,
80
84
  permissions_exports
81
- } from "./chunk-RUDYC7OS.js";
85
+ } from "./chunk-BHSNJT6Z.js";
82
86
  import {
83
87
  MCPToolWrapper,
84
88
  MCP_TOOL_PREFIX,
@@ -347,9 +351,9 @@ __export(server_exports, {
347
351
  RemoteServer: () => RemoteServer,
348
352
  createRemoteAgent: () => createRemoteAgent
349
353
  });
350
- import { readFileSync as readFileSync16, existsSync as existsSync16, statSync as statSync9 } from "fs";
354
+ import { readFileSync as readFileSync16, existsSync as existsSync16, statSync as statSync12 } from "fs";
351
355
  import { createServer } from "http";
352
- import { join as join20, extname as extname2, normalize } from "path";
356
+ import { join as join21, extname as extname2, normalize } from "path";
353
357
  import { cwd } from "process";
354
358
  import Koa from "koa";
355
359
  import { WebSocketServer, WebSocket } from "ws";
@@ -1927,11 +1931,14 @@ import { resolve as resolve4 } from "path";
1927
1931
  // src/tools/descriptions.ts
1928
1932
  var descriptions_exports = {};
1929
1933
  __export(descriptions_exports, {
1934
+ BASH_BACKGROUND_DESCRIPTION: () => BASH_BACKGROUND_DESCRIPTION,
1930
1935
  BASH_DESCRIPTION: () => BASH_DESCRIPTION,
1931
1936
  EDIT_FILE_DESCRIPTION: () => EDIT_FILE_DESCRIPTION,
1932
1937
  GLOB_DESCRIPTION: () => GLOB_DESCRIPTION,
1933
1938
  GREP_DESCRIPTION: () => GREP_DESCRIPTION,
1939
+ JAVASCRIPT_BACKGROUND_DESCRIPTION: () => JAVASCRIPT_BACKGROUND_DESCRIPTION,
1934
1940
  JAVASCRIPT_DESCRIPTION: () => JAVASCRIPT_DESCRIPTION,
1941
+ POWERSHELL_BACKGROUND_DESCRIPTION: () => POWERSHELL_BACKGROUND_DESCRIPTION,
1935
1942
  POWERSHELL_DESCRIPTION: () => POWERSHELL_DESCRIPTION,
1936
1943
  READ_FILE_DESCRIPTION: () => READ_FILE_DESCRIPTION,
1937
1944
  WEB_FETCH_DESCRIPTION: () => WEB_FETCH_DESCRIPTION,
@@ -1943,15 +1950,18 @@ var BASH_DESCRIPTION = `Execute command in Bash; return stdout and stderr. Prefe
1943
1950
  - Quote paths with spaces. To change directory, use cd "path" && command in the same call. Separate independent commands; chain dependent commands with &&, not ;.
1944
1951
  - Prefer dedicated file/search tools over cat, head, tail, sed, awk, echo, or find. Scope searches to a directory, never the filesystem root. Diagnose failures rather than retrying in sleep loops.
1945
1952
  ${GIT_GUIDANCE}`;
1953
+ var BASH_BACKGROUND_DESCRIPTION = `- Set run_in_background to true to run the command in the background. The call returns a task ID immediately and the result arrives later as a task notification; do not poll or sleep waiting for it. Use this for long-running commands you do not need the result of right away. Use TaskStop with the task_id to kill a background command early. A foreground command that exceeds its timeout is moved to the background automatically instead of being killed, unless it is a bare sleep.`;
1946
1954
  var POWERSHELL_DESCRIPTION = `Execute command in PowerShell; return stdout and stderr. Recommended on Windows (powershell.exe); uses pwsh elsewhere.
1947
1955
  - timeout is in seconds: default 120, maximum 600. Each call starts a fresh, independent shell in the Agent's working directory; location, variables, and options do not persist.
1948
1956
  - Quote paths with spaces; use Set-Location -LiteralPath "path" in the same call. Separate independent commands. For dependencies, check $LASTEXITCODE for native commands and use -ErrorAction Stop for cmdlets; ; does not stop on failure. Do not assume PowerShell 7 syntax.
1949
1957
  - Prefer dedicated file/search tools over Get-Content, Select-String, or Write-Output. Scope recursion to a directory, not a drive root. Diagnose failures rather than retrying in Start-Sleep loops.
1950
1958
  ${GIT_GUIDANCE}`;
1959
+ var POWERSHELL_BACKGROUND_DESCRIPTION = `- Set run_in_background to true to run the command in the background. The call returns a task ID immediately and the result arrives later as a task notification; do not poll or Start-Sleep waiting for it. Use this for long-running commands you do not need the result of right away. Use TaskStop with the task_id to kill a background command early. A foreground command that exceeds its timeout is moved to the background automatically instead of being killed, unless it is a bare Start-Sleep.`;
1951
1960
  var JAVASCRIPT_DESCRIPTION = `Execute JavaScript in a fresh isolated V8 context for pure computation.
1952
1961
  - Provide a function body and use return to produce a JSON-compatible result. Optional input is available as globalThis.input.
1953
1962
  - No filesystem, network, process, require, imports, timers, or host callbacks are available. Use Bash for Node.js or system operations.
1954
1963
  - timeout_ms defaults to 1000 (max 10000); memory_limit_mb defaults to 64 (max 256). Requires Node.js 24 or newer.`;
1964
+ var JAVASCRIPT_BACKGROUND_DESCRIPTION = `- Set run_in_background to true to evaluate in the background. The call returns a task ID immediately and the result arrives later as a task notification; do not poll. timeout_ms still caps the evaluation (max 10000). Use TaskStop with the task_id to abort it early.`;
1955
1965
  var READ_FILE_DESCRIPTION = `Read text with 1-based display line numbers, or images (png, jpg, jpeg, gif, webp) as visual content; not directories.
1956
1966
  - file_path is absolute or relative to the Agent's working directory. offset skips lines (0-based, default 0); limit defaults to 2000 lines, with a 50KB text output cap. Displayed line 101 starts at offset=100. Follow continuation/readback instructions for partial output.
1957
1967
  - Images ignore offset/limit. A successful read refreshes the file-state cache used by EditFile/WriteFile; re-read after an external-change error.`;
@@ -3178,7 +3188,9 @@ var MemoryManager = class {
3178
3188
  return this.loadAll();
3179
3189
  }
3180
3190
  /**
3181
- * Builds the memory index injected into the system prompt.
3191
+ * Builds the memory index injected into the conversation as a
3192
+ * system-reminder message (not the system prompt, which stays
3193
+ * project-independent to preserve prompt caching).
3182
3194
  *
3183
3195
  * This content is re-sent to the model on every conversation turn, so each additional index line
3184
3196
  * is a recurring cost. We therefore enforce both a line-count and a byte-size cap at the output
@@ -3268,8 +3280,9 @@ ${body}
3268
3280
  // ── Feature 2: findRelevantMemories ────────────────────────────────
3269
3281
  /**
3270
3282
  * Scans all memory headers from both dirs, asks the LLM to select the
3271
- * top 5 most relevant ones for the query, and returns the full content
3272
- * of those files. Best-effort: selector failures return an empty array.
3283
+ * top 5 most relevant ones for the query, and returns their paths (with
3284
+ * mtimes); the file contents are read later by renderReminder.
3285
+ * Best-effort: selector failures return an empty array.
3273
3286
  */
3274
3287
  async findRelevantMemories(query, client, recentTools = [], alreadySurfaced = /* @__PURE__ */ new Set()) {
3275
3288
  const allHeaders = [];
@@ -4543,27 +4556,37 @@ function parseAgentDefinition(content) {
4543
4556
  // src/subagent/task-manager.ts
4544
4557
  var task_manager_exports = {};
4545
4558
  __export(task_manager_exports, {
4559
+ TaskFailure: () => TaskFailure,
4546
4560
  TaskManager: () => TaskManager,
4547
4561
  formatAgentTaskNotification: () => formatAgentTaskNotification
4548
4562
  });
4563
+ var TaskFailure = class extends Error {
4564
+ constructor(output) {
4565
+ super("task failed");
4566
+ this.output = output;
4567
+ }
4568
+ output;
4569
+ };
4549
4570
  var TaskManager = class {
4550
4571
  tasks = /* @__PURE__ */ new Map();
4551
4572
  notifiedTaskIds = /* @__PURE__ */ new Set();
4552
4573
  listeners = /* @__PURE__ */ new Set();
4553
4574
  nextId = 1;
4575
+ pendingTaskIds = /* @__PURE__ */ new Set();
4554
4576
  create(name, runner, cancel, options = {}) {
4555
- const id = `agent-${String(this.nextId++)}`;
4577
+ const id = `${options.idPrefix ?? "agent"}-${String(this.nextId++)}`;
4556
4578
  const task = {
4557
4579
  id,
4558
4580
  name,
4559
4581
  ...options.originToolCallId ? { originToolCallId: options.originToolCallId } : {},
4582
+ ...options.kind ? { kind: options.kind } : {},
4560
4583
  status: "running",
4561
4584
  output: "",
4562
4585
  cancel,
4563
4586
  done: Promise.resolve()
4564
4587
  };
4565
4588
  this.tasks.set(id, task);
4566
- this.emitChange();
4589
+ this.pendingTaskIds.add(id);
4567
4590
  task.done = Promise.resolve().then(() => runner(task)).then((output) => {
4568
4591
  if (task.status === "running") {
4569
4592
  task.status = "completed";
@@ -4573,10 +4596,17 @@ var TaskManager = class {
4573
4596
  }).catch((error) => {
4574
4597
  if (task.status === "running") {
4575
4598
  task.status = "failed";
4576
- task.output = `Error: ${asErrorString(error)}`;
4599
+ task.output = error instanceof TaskFailure ? error.output : `Error: ${asErrorString(error)}`;
4600
+ this.emitChange();
4601
+ } else if (task.status === "cancelled" && error instanceof TaskFailure) {
4602
+ task.output = error.output;
4577
4603
  this.emitChange();
4578
4604
  }
4605
+ }).finally(() => {
4606
+ this.pendingTaskIds.delete(id);
4607
+ this.emitChange();
4579
4608
  });
4609
+ this.emitChange();
4580
4610
  return task;
4581
4611
  }
4582
4612
  get(id) {
@@ -4614,25 +4644,32 @@ var TaskManager = class {
4614
4644
  }
4615
4645
  async stopAndWait(id) {
4616
4646
  const task = this.tasks.get(id);
4617
- if (!task || !this.stop(id)) {
4647
+ if (!task) {
4618
4648
  return false;
4619
4649
  }
4650
+ const stopped = this.stop(id);
4620
4651
  await task.done;
4621
- return true;
4652
+ return stopped;
4622
4653
  }
4623
4654
  async stopAll() {
4624
- const running = this.list().filter((task) => task.status === "running");
4655
+ const running = this.list().filter((task) => this.pendingTaskIds.has(task.id));
4625
4656
  for (const task of running) {
4626
4657
  this.stop(task.id);
4627
4658
  }
4628
4659
  await Promise.allSettled(running.map((task) => task.done));
4629
4660
  }
4630
- async waitAll() {
4631
- await Promise.allSettled(this.list().map((task) => task.done));
4661
+ /**
4662
+ * Wait for tasks to settle. An optional filter selects which tasks to wait
4663
+ * for (e.g. print-mode waits only for agent-kind tasks: shell/js tasks may
4664
+ * run indefinitely and are stopped, not awaited, at exit).
4665
+ */
4666
+ async waitAll(filter) {
4667
+ const tasks = filter ? this.list().filter(filter) : this.list();
4668
+ await Promise.allSettled(tasks.map((task) => task.done));
4632
4669
  }
4633
4670
  drainNotifications() {
4634
4671
  const completed = this.list().filter(
4635
- (task) => task.status !== "running" && !this.notifiedTaskIds.has(task.id)
4672
+ (task) => task.status !== "running" && !this.pendingTaskIds.has(task.id) && !this.notifiedTaskIds.has(task.id)
4636
4673
  );
4637
4674
  for (const task of completed) {
4638
4675
  this.notifiedTaskIds.add(task.id);
@@ -6218,6 +6255,7 @@ async function spawnSubagent(definition, prompt, parentClient, parentRegistry, p
6218
6255
  const conversation = options.conversation ?? new ConversationManager();
6219
6256
  conversation.addSystemReminder(buildSubagentInstructions(definition));
6220
6257
  conversation.addUserMessage(prompt);
6258
+ const taskManager = options.backgroundTasks === false ? null : new TaskManager();
6221
6259
  const agent2 = new Agent({
6222
6260
  client,
6223
6261
  registry,
@@ -6230,53 +6268,59 @@ async function spawnSubagent(definition, prompt, parentClient, parentRegistry, p
6230
6268
  fileStateCache: new FileStateCache(),
6231
6269
  instructions: loadInstructions(workDir),
6232
6270
  contextWindow: getContextWindow(provider),
6233
- maxOutput: getMaxOutputTokens(provider)
6271
+ maxOutput: getMaxOutputTokens(provider),
6272
+ taskManager,
6273
+ notificationFn: taskManager ? () => taskManager.drainNotifications().map(formatAgentTaskNotification) : void 0
6234
6274
  });
6235
6275
  let output = "";
6236
6276
  let turn = 0;
6237
- for await (const event of agent2.run()) {
6238
- switch (event.type) {
6239
- case "stream_text":
6240
- output += event.text;
6241
- break;
6242
- case "tool_use":
6243
- onProgress?.({ lastTool: event.toolName });
6244
- onEvent?.({
6245
- type: "tool_use",
6246
- toolId: event.toolId,
6247
- toolName: event.toolName,
6248
- args: event.args
6249
- });
6250
- break;
6251
- case "tool_result":
6252
- onEvent?.({
6253
- type: "tool_result",
6254
- toolId: event.toolId
6255
- });
6256
- break;
6257
- case "usage":
6258
- onEvent?.({
6259
- type: "usage",
6260
- usage: {
6261
- inputTokens: event.usage.inputTokens,
6262
- outputTokens: event.usage.outputTokens
6277
+ try {
6278
+ for await (const event of agent2.run()) {
6279
+ switch (event.type) {
6280
+ case "stream_text":
6281
+ output += event.text;
6282
+ break;
6283
+ case "tool_use":
6284
+ onProgress?.({ lastTool: event.toolName });
6285
+ onEvent?.({
6286
+ type: "tool_use",
6287
+ toolId: event.toolId,
6288
+ toolName: event.toolName,
6289
+ args: event.args
6290
+ });
6291
+ break;
6292
+ case "tool_result":
6293
+ onEvent?.({
6294
+ type: "tool_result",
6295
+ toolId: event.toolId
6296
+ });
6297
+ break;
6298
+ case "usage":
6299
+ onEvent?.({
6300
+ type: "usage",
6301
+ usage: {
6302
+ inputTokens: event.usage.inputTokens,
6303
+ outputTokens: event.usage.outputTokens
6304
+ }
6305
+ });
6306
+ break;
6307
+ case "turn_complete":
6308
+ onProgress?.({ turn: ++turn });
6309
+ onEvent?.({ type: "turn_complete" });
6310
+ break;
6311
+ case "loop_complete":
6312
+ if (event.stopReason === "interrupted") {
6313
+ return `${output}${output ? "\n\n" : ""}${SUBAGENT_INTERRUPTED_MARKER}`;
6263
6314
  }
6264
- });
6265
- break;
6266
- case "turn_complete":
6267
- onProgress?.({ turn: ++turn });
6268
- onEvent?.({ type: "turn_complete" });
6269
- break;
6270
- case "loop_complete":
6271
- if (event.stopReason === "interrupted") {
6272
- return `${output}${output ? "\n\n" : ""}${SUBAGENT_INTERRUPTED_MARKER}`;
6273
- }
6274
- return output || "[No output]";
6275
- case "error":
6276
- throw event.error;
6315
+ return output || "[No output]";
6316
+ case "error":
6317
+ throw event.error;
6318
+ }
6277
6319
  }
6320
+ return output || "[No output]";
6321
+ } finally {
6322
+ await taskManager?.stopAll();
6278
6323
  }
6279
- return output || "[No output]";
6280
6324
  }
6281
6325
 
6282
6326
  // src/teams/coordinator.ts
@@ -7063,7 +7107,7 @@ var TaskStopTool = class {
7063
7107
  teamManager;
7064
7108
  taskManager;
7065
7109
  name = "TaskStop";
7066
- description = "Stop a running teammate or background Agent task. Pass exactly one of teammate or task_id.";
7110
+ description = "Stop a running teammate or background task (Agent, Bash, PowerShell or JavaScript). Pass exactly one of teammate or task_id.";
7067
7111
  category = "command";
7068
7112
  schema() {
7069
7113
  return {
@@ -7078,20 +7122,21 @@ var TaskStopTool = class {
7078
7122
  },
7079
7123
  task_id: {
7080
7124
  type: "string",
7081
- description: "ID of a background Agent task"
7125
+ description: "ID of a background task (Agent, Bash, PowerShell or JavaScript)"
7082
7126
  }
7083
7127
  }
7084
7128
  }
7085
7129
  };
7086
7130
  }
7087
- async execute(_ctx, args) {
7131
+ async execute(ctx, args) {
7088
7132
  const name = strArg(args, "teammate", "");
7089
7133
  const taskId = strArg(args, "task_id", "");
7090
7134
  if (!name && !taskId || name && taskId) {
7091
7135
  return { output: "Error: pass exactly one of teammate or task_id", isError: true };
7092
7136
  }
7093
7137
  if (taskId) {
7094
- const task = this.taskManager?.get(taskId);
7138
+ const manager = ctx.taskManager?.get(taskId) ? ctx.taskManager : this.taskManager;
7139
+ const task = manager?.get(taskId);
7095
7140
  if (!task) {
7096
7141
  return { output: `Error: background task '${taskId}' not found`, isError: true };
7097
7142
  }
@@ -7101,7 +7146,7 @@ var TaskStopTool = class {
7101
7146
  isError: false
7102
7147
  };
7103
7148
  }
7104
- await this.taskManager?.stopAndWait(taskId);
7149
+ await manager?.stopAndWait(taskId);
7105
7150
  return { output: `Background task '${taskId}' stopped.`, isError: false };
7106
7151
  }
7107
7152
  for (const team of this.teamManager.list()) {
@@ -7425,16 +7470,7 @@ ${prompt}` };
7425
7470
  shutdown: shutdownRequest("lead", "member deactivated")
7426
7471
  };
7427
7472
  }
7428
- /**
7429
- * Sends the teammate's completed plan to the Lead, blocks until approval is received,
7430
- * and returns the prompt to feed the model on the next turn.
7431
- *
7432
- * The teammate holds read-only permissions at this point, so no matter how long the
7433
- * wait, no damage can occur — hence no timeout is set here. Rather than timing out and
7434
- * autonomously modifying files, it is better to wait indefinitely and let the user
7435
- * drive progress from the Lead side. Returns null when the teammate has been
7436
- * deactivated; the caller should exit the main loop.
7437
- */
7473
+ /** Reads the teammate's plan file for review; returns a fallback note when empty or unreadable. */
7438
7474
  readPlanForReview() {
7439
7475
  try {
7440
7476
  const text = readFileSync14(getOrCreatePlanPath(this.workDir), "utf-8");
@@ -7445,6 +7481,16 @@ ${prompt}` };
7445
7481
  }
7446
7482
  return "(Plan file is empty \u2014 the teammate may not have written the plan as expected)";
7447
7483
  }
7484
+ /**
7485
+ * Sends the teammate's completed plan to the Lead, blocks until approval is received,
7486
+ * and returns the prompt to feed the model on the next turn.
7487
+ *
7488
+ * The teammate holds read-only permissions at this point, so no matter how long the
7489
+ * wait, no damage can occur — hence no timeout is set here. Rather than timing out and
7490
+ * autonomously modifying files, it is better to wait indefinitely and let the user
7491
+ * drive progress from the Lead side. Returns null when the teammate has been
7492
+ * deactivated; the caller should exit the main loop.
7493
+ */
7448
7494
  async runPlanApproval(member, plan) {
7449
7495
  const req = planApprovalRequest(member.name, plan);
7450
7496
  await this.leadMailbox.send(member.name, req.text, req);
@@ -8139,8 +8185,50 @@ __export(bash_exports, {
8139
8185
  BashTool: () => BashTool
8140
8186
  });
8141
8187
  import { spawn as spawn2 } from "child_process";
8188
+ import { statSync as statSync10 } from "fs";
8189
+
8190
+ // src/tools/shell-background.ts
8191
+ var shell_background_exports = {};
8192
+ __export(shell_background_exports, {
8193
+ BACKGROUNDABLE_TOOL_NAMES: () => BACKGROUNDABLE_TOOL_NAMES,
8194
+ BACKGROUND_MAX_OUTPUT_BYTES: () => BACKGROUND_MAX_OUTPUT_BYTES,
8195
+ BACKGROUND_NOTIFICATION_CHARS: () => BACKGROUND_NOTIFICATION_CHARS,
8196
+ SIZE_WATCHDOG_INTERVAL_MS: () => SIZE_WATCHDOG_INTERVAL_MS,
8197
+ attachBackgroundTaskManager: () => attachBackgroundTaskManager,
8198
+ backgroundAllForegroundTasks: () => backgroundAllForegroundTasks,
8199
+ backgroundMessage: () => backgroundMessage,
8200
+ backgroundTaskName: () => backgroundTaskName,
8201
+ buildBackgroundBody: () => buildBackgroundBody,
8202
+ createShellOutputFile: () => createShellOutputFile,
8203
+ discardFd: () => discardFd,
8204
+ formatFinalResult: () => formatFinalResult,
8205
+ hasAnyForegroundTasks: () => hasAnyForegroundTasks,
8206
+ isAutobackgroundingAllowed: () => isAutobackgroundingAllowed,
8207
+ openOutputFd: () => openOutputFd,
8208
+ readOutputFile: () => readOutputFile,
8209
+ sliceUtf8Safe: () => sliceUtf8Safe,
8210
+ unlinkQuiet: () => unlinkQuiet
8211
+ });
8212
+ import { randomBytes as randomBytes3 } from "crypto";
8213
+ import {
8214
+ closeSync as closeSync2,
8215
+ constants as fsConstants,
8216
+ fstatSync,
8217
+ mkdirSync as mkdirSync12,
8218
+ openSync as openSync2,
8219
+ readSync,
8220
+ statSync as statSync9,
8221
+ unlinkSync as unlinkSync5
8222
+ } from "fs";
8223
+ import { tmpdir } from "os";
8224
+ import { join as join19 } from "path";
8142
8225
 
8143
8226
  // src/tools/exit-code-hints.ts
8227
+ var exit_code_hints_exports = {};
8228
+ __export(exit_code_hints_exports, {
8229
+ exitCodeHint: () => exitCodeHint,
8230
+ extractBaseCmd: () => extractBaseCmd
8231
+ });
8144
8232
  function extractBaseCmd(command) {
8145
8233
  const lastSegment = command.split("|").pop()?.trim() ?? command;
8146
8234
  const tokens = lastSegment.split(/\s+/);
@@ -8372,9 +8460,201 @@ function exitCodeHint(command, exitCode) {
8372
8460
  return "";
8373
8461
  }
8374
8462
 
8463
+ // src/tools/shell-background.ts
8464
+ var BACKGROUND_NOTIFICATION_CHARS = 3e4;
8465
+ var BACKGROUND_MAX_OUTPUT_BYTES = 5 * 1024 * 1024 * 1024;
8466
+ var SIZE_WATCHDOG_INTERVAL_MS = 500;
8467
+ function isAutobackgroundingAllowed(command, disallowed) {
8468
+ const first = /^\S+/.exec(command.trimStart())?.[0] ?? "";
8469
+ const base = first.replace(/^.*\//, "").replace(/^["']|["']$/g, "").toLowerCase();
8470
+ return !disallowed.has(base);
8471
+ }
8472
+ function backgroundTaskName(command) {
8473
+ const flat = command.replace(/\s+/g, " ").trim();
8474
+ return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
8475
+ }
8476
+ function backgroundMessage(reason, taskId, timeout) {
8477
+ switch (reason) {
8478
+ case "explicit":
8479
+ return `Command running in background (task_id: ${taskId}). You will be notified when it completes; do not poll. Use TaskStop with task_id to kill it early.`;
8480
+ case "timeout":
8481
+ return `Command exceeded its ${String(timeout)}s timeout and was moved to the background (task_id: ${taskId}). It is still running \u2014 you will be notified when it completes.`;
8482
+ case "user":
8483
+ return `Command was manually backgrounded by the user (task_id: ${taskId}). It is still running \u2014 you will be notified when it completes.`;
8484
+ }
8485
+ }
8486
+ function sliceUtf8Safe(buf, maxBytes) {
8487
+ if (buf.length <= maxBytes) {
8488
+ return buf;
8489
+ }
8490
+ let end = maxBytes;
8491
+ while (end > 0 && (buf[end] & 192) === 128) {
8492
+ end--;
8493
+ }
8494
+ return buf.subarray(0, end);
8495
+ }
8496
+ function readOutputFile(path, maxBytes) {
8497
+ let fd;
8498
+ try {
8499
+ fd = openSync2(path, "r");
8500
+ } catch {
8501
+ return { text: "", size: 0, truncated: false };
8502
+ }
8503
+ try {
8504
+ const size = fstatSync(fd).size;
8505
+ const readLen = Math.min(size, maxBytes + 1);
8506
+ const buf = Buffer.alloc(readLen);
8507
+ let bytesRead = 0;
8508
+ while (bytesRead < readLen) {
8509
+ const count = readSync(fd, buf, bytesRead, readLen - bytesRead, bytesRead);
8510
+ if (count === 0) {
8511
+ break;
8512
+ }
8513
+ bytesRead += count;
8514
+ }
8515
+ return {
8516
+ text: sliceUtf8Safe(buf.subarray(0, bytesRead), maxBytes).toString("utf-8"),
8517
+ size,
8518
+ truncated: size > maxBytes
8519
+ };
8520
+ } finally {
8521
+ closeSync2(fd);
8522
+ }
8523
+ }
8524
+ function unlinkQuiet(path) {
8525
+ try {
8526
+ unlinkSync5(path);
8527
+ } catch {
8528
+ }
8529
+ }
8530
+ function discardFd(fd) {
8531
+ try {
8532
+ closeSync2(fd);
8533
+ } catch {
8534
+ }
8535
+ }
8536
+ function openOutputFd(path) {
8537
+ const flags = process.platform === "win32" ? "w" : fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_NOFOLLOW;
8538
+ return openSync2(path, flags, 384);
8539
+ }
8540
+ function createShellOutputFile(workDir, sessionId) {
8541
+ const fileName = `shell-${randomBytes3(8).toString("hex")}.output`;
8542
+ const candidates = [];
8543
+ try {
8544
+ const dir = spillDir(workDir, sessionId);
8545
+ mkdirSync12(dir, { recursive: true });
8546
+ candidates.push(join19(dir, fileName));
8547
+ } catch {
8548
+ }
8549
+ candidates.push(join19(tmpdir(), fileName));
8550
+ let lastError = new Error("no candidate output directory");
8551
+ for (const path of candidates) {
8552
+ try {
8553
+ return { path, fd: openOutputFd(path) };
8554
+ } catch (error) {
8555
+ lastError = error;
8556
+ }
8557
+ }
8558
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
8559
+ }
8560
+ function formatFinalResult(prompt, command, exit, merged, truncated, timeout) {
8561
+ if (exit.spawnError) {
8562
+ return {
8563
+ output: `Error executing command: ${exit.spawnError}`,
8564
+ isError: true
8565
+ };
8566
+ }
8567
+ if (exit.aborted || exit.timedOut) {
8568
+ const captured = merged || truncated ? formatShellOutput(prompt, command, merged, "", truncated) : "";
8569
+ const error = exit.aborted ? "Error: command interrupted" : `Error: command timed out after ${String(timeout)}s`;
8570
+ return {
8571
+ output: captured ? `${captured}
8572
+ ${error}` : error,
8573
+ isError: true
8574
+ };
8575
+ }
8576
+ const exitCode = exit.code ?? 0;
8577
+ let output = formatShellOutput(prompt, command, merged, "", truncated);
8578
+ if (!truncated) {
8579
+ if (exitCode !== 0) {
8580
+ const hint = exitCodeHint(command, exitCode);
8581
+ output += hint ? `
8582
+ Exit code ${String(exitCode)} (${hint})` : `
8583
+ Exit code ${String(exitCode)}`;
8584
+ }
8585
+ if (exit.code === null) {
8586
+ output += `
8587
+ Process terminated${exit.signal ? ` by ${exit.signal}` : " unexpectedly"}`;
8588
+ }
8589
+ }
8590
+ return { output, isError: truncated || exitCode !== 0 || exit.code === null };
8591
+ }
8592
+ function buildBackgroundBody(prompt, command, exit, outputPath, timeout, annotate) {
8593
+ let size = 0;
8594
+ try {
8595
+ size = statSync9(outputPath).size;
8596
+ } catch {
8597
+ }
8598
+ let result;
8599
+ if (size <= BACKGROUND_NOTIFICATION_CHARS) {
8600
+ const read = readOutputFile(outputPath, MAX_SHELL_OUTPUT_BYTES);
8601
+ const merged = annotate ? annotate(read.text) : read.text;
8602
+ result = formatFinalResult(prompt, command, exit, merged, read.truncated, timeout);
8603
+ unlinkQuiet(outputPath);
8604
+ } else {
8605
+ const header = formatFinalResult(prompt, command, exit, "", false, timeout);
8606
+ const preview2 = readOutputFile(outputPath, TOOL_RESULT_PREVIEW_CHARS).text;
8607
+ result = {
8608
+ output: `${header.output}
8609
+ ${buildPersistedOutputPreview(size, annotate ? annotate(preview2) : preview2, outputPath)}`,
8610
+ isError: header.isError
8611
+ };
8612
+ }
8613
+ if (exit.sizeKilled) {
8614
+ result.output += "\nBackground command killed: output file exceeded 5GB";
8615
+ result.isError = true;
8616
+ }
8617
+ return result;
8618
+ }
8619
+ var BACKGROUNDABLE_TOOL_NAMES = ["Bash", "PowerShell", "JavaScript"];
8620
+ function asBackgroundable(tool) {
8621
+ if (typeof tool === "object" && tool !== null && "taskManager" in tool && "backgroundForegroundTasks" in tool) {
8622
+ return tool;
8623
+ }
8624
+ return null;
8625
+ }
8626
+ function attachBackgroundTaskManager(registry, manager) {
8627
+ for (const name of BACKGROUNDABLE_TOOL_NAMES) {
8628
+ const tool = asBackgroundable(registry.get(name));
8629
+ if (tool) {
8630
+ tool.taskManager = manager;
8631
+ }
8632
+ }
8633
+ }
8634
+ function hasAnyForegroundTasks(registry) {
8635
+ for (const name of BACKGROUNDABLE_TOOL_NAMES) {
8636
+ const tool = asBackgroundable(registry.get(name));
8637
+ if (tool?.hasForegroundTasks()) {
8638
+ return true;
8639
+ }
8640
+ }
8641
+ return false;
8642
+ }
8643
+ function backgroundAllForegroundTasks(registry) {
8644
+ let count = 0;
8645
+ for (const name of BACKGROUNDABLE_TOOL_NAMES) {
8646
+ const tool = asBackgroundable(registry.get(name));
8647
+ if (tool) {
8648
+ count += tool.backgroundForegroundTasks();
8649
+ }
8650
+ }
8651
+ return count;
8652
+ }
8653
+
8375
8654
  // src/tools/bash.ts
8376
8655
  var MAX_TIMEOUT = 600;
8377
8656
  var KILL_GRACE_MS = 3e3;
8657
+ var DISALLOWED_AUTO_BACKGROUND_COMMANDS = /* @__PURE__ */ new Set(["sleep"]);
8378
8658
  var BashTool = class {
8379
8659
  // Use a hardcoded string instead of BashTool.name.replace("Tool", "")
8380
8660
  // because class names are not stable after minification — bundlers like
@@ -8390,6 +8670,18 @@ var BashTool = class {
8390
8670
  denyWrite: [],
8391
8671
  networkEnabled: true
8392
8672
  };
8673
+ /**
8674
+ * Background task registry, injected by the host (TUI / print mode / remote
8675
+ * server) — the same instance the Agent tool uses, so completion
8676
+ * notifications share one drain and TaskStop covers both. When null, Bash is
8677
+ * foreground-only: run_in_background disappears from the schema, timeouts
8678
+ * kill, and Ctrl+B is a no-op. Calls running inside a subagent loop carry
8679
+ * that loop's own manager in ctx.taskManager, which takes precedence.
8680
+ */
8681
+ taskManager = null;
8682
+ /** Running foreground executions eligible for manual backgrounding (Ctrl+B). */
8683
+ foreground = /* @__PURE__ */ new Map();
8684
+ nextForegroundId = 1;
8393
8685
  /**
8394
8686
  * Read-only commands can run concurrently with other read-only tools;
8395
8687
  * mutating commands must run exclusively.
@@ -8404,26 +8696,62 @@ var BashTool = class {
8404
8696
  const command = args.command;
8405
8697
  return typeof command === "string" && isSafeCommand(command);
8406
8698
  }
8699
+ /**
8700
+ * The background subsystem needs a task manager and can be disabled
8701
+ * wholesale with SWIFTY_DISABLE_BACKGROUND_TASKS=1 (schema parameter
8702
+ * removed, timeouts kill, Ctrl+B becomes a no-op). Instance-level gate used
8703
+ * by the schema; execute() re-checks with the ctx-resolved manager.
8704
+ */
8705
+ backgroundEnabled() {
8706
+ return this.taskManager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
8707
+ }
8708
+ /** True while at least one foreground Bash command runs (gates the Ctrl+B handler). */
8709
+ hasForegroundTasks() {
8710
+ return this.foreground.size > 0;
8711
+ }
8712
+ /**
8713
+ * Move every running foreground Bash command to the background (Ctrl+B).
8714
+ * Returns how many commands were actually backgrounded.
8715
+ */
8716
+ backgroundForegroundTasks() {
8717
+ let count = 0;
8718
+ for (const entry of [...this.foreground.values()]) {
8719
+ if (entry.background()) {
8720
+ count++;
8721
+ }
8722
+ }
8723
+ return count;
8724
+ }
8407
8725
  schema() {
8408
- const inputSchema = {
8409
- type: "object",
8410
- properties: {
8411
- command: {
8412
- type: "string",
8413
- description: "Shell command to execute"
8414
- },
8415
- timeout: {
8416
- type: "integer",
8417
- description: "Timeout in seconds (max 600)",
8418
- default: 120
8419
- }
8726
+ const properties = {
8727
+ command: {
8728
+ type: "string",
8729
+ description: "Shell command to execute"
8420
8730
  },
8421
- required: ["command"]
8731
+ timeout: {
8732
+ type: "integer",
8733
+ description: "Timeout in seconds (max 600)",
8734
+ default: 120
8735
+ }
8422
8736
  };
8737
+ let description = this.description;
8738
+ if (this.backgroundEnabled()) {
8739
+ properties.run_in_background = {
8740
+ type: "boolean",
8741
+ description: "Run the command in the background. Returns a task ID immediately; the result arrives later as a task notification.",
8742
+ default: false
8743
+ };
8744
+ description = `${this.description}
8745
+ ${BASH_BACKGROUND_DESCRIPTION}`;
8746
+ }
8423
8747
  return {
8424
8748
  name: this.name,
8425
- description: this.description,
8426
- input_schema: inputSchema
8749
+ description,
8750
+ input_schema: {
8751
+ type: "object",
8752
+ properties,
8753
+ required: ["command"]
8754
+ }
8427
8755
  };
8428
8756
  }
8429
8757
  async execute(ctx, args) {
@@ -8444,12 +8772,29 @@ var BashTool = class {
8444
8772
  if (timeout > MAX_TIMEOUT) {
8445
8773
  timeout = MAX_TIMEOUT;
8446
8774
  }
8775
+ const manager = ctx.taskManager !== void 0 ? ctx.taskManager : this.taskManager;
8776
+ const backgroundAvailable = manager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
8777
+ const runInBackground = boolArg(args, "run_in_background") && backgroundAvailable;
8778
+ let outputFile;
8779
+ try {
8780
+ outputFile = createShellOutputFile(ctx.workDir, ctx.sessionId ?? "");
8781
+ } catch (error) {
8782
+ return {
8783
+ output: `Error creating output file: ${asErrorString(error)}`,
8784
+ isError: true
8785
+ };
8786
+ }
8787
+ const discardOutputFile = () => {
8788
+ discardFd(outputFile.fd);
8789
+ unlinkQuiet(outputFile.path);
8790
+ };
8447
8791
  let prepared = {
8448
8792
  executable: "bash",
8449
8793
  args: ["-c", command]
8450
8794
  };
8451
8795
  if (this.sandboxRequired || this.sandbox) {
8452
8796
  if (!this.sandbox) {
8797
+ discardOutputFile();
8453
8798
  return {
8454
8799
  output: "Error: sandbox is enabled but unavailable; command was not executed",
8455
8800
  isError: true
@@ -8457,17 +8802,28 @@ var BashTool = class {
8457
8802
  }
8458
8803
  try {
8459
8804
  if (!await this.sandbox.available()) {
8805
+ discardOutputFile();
8460
8806
  return {
8461
8807
  output: `Error: ${this.sandbox.implementation} sandbox is unavailable; command was not executed`,
8462
8808
  isError: true
8463
8809
  };
8464
8810
  }
8465
- prepared = await this.sandbox.prepare(command, this.sandboxConfig, {
8466
- cwd: ctx.workDir,
8467
- abortSignal: ctx.abortSignal,
8468
- commandId: ctx.toolCallId
8469
- });
8811
+ prepared = await this.sandbox.prepare(
8812
+ command,
8813
+ {
8814
+ ...this.sandboxConfig,
8815
+ // The child writes its output file directly; grant write access to
8816
+ // that path even under a strict allowWrite config.
8817
+ allowWrite: [...this.sandboxConfig.allowWrite, outputFile.path]
8818
+ },
8819
+ {
8820
+ cwd: ctx.workDir,
8821
+ abortSignal: ctx.abortSignal,
8822
+ commandId: ctx.toolCallId
8823
+ }
8824
+ );
8470
8825
  } catch (error) {
8826
+ discardOutputFile();
8471
8827
  return {
8472
8828
  output: `Error preparing sandbox: ${asErrorString(error)}`,
8473
8829
  isError: true
@@ -8475,6 +8831,7 @@ var BashTool = class {
8475
8831
  }
8476
8832
  }
8477
8833
  if (ctx.abortSignal?.aborted) {
8834
+ discardOutputFile();
8478
8835
  try {
8479
8836
  await prepared.cleanup?.();
8480
8837
  } catch {
@@ -8484,22 +8841,58 @@ var BashTool = class {
8484
8841
  isError: true
8485
8842
  };
8486
8843
  }
8487
- return new Promise((resolve12) => {
8488
- let timedOut = false;
8844
+ const handle = this.startCommand(ctx, prepared, command, timeout, outputFile, manager);
8845
+ if (runInBackground) {
8846
+ const taskId = handle.background("explicit");
8847
+ if (taskId !== null) {
8848
+ return {
8849
+ output: backgroundMessage("explicit", taskId, timeout),
8850
+ isError: false
8851
+ };
8852
+ }
8853
+ }
8854
+ return handle.result;
8855
+ }
8856
+ /**
8857
+ * Spawn the command with stdout+stderr writing directly into the output
8858
+ * file (ccb's file-descriptor mode): output never flows through JS, so
8859
+ * backgrounding is a bookkeeping switch — no re-spawn, no buffer handover.
8860
+ * The returned handle exposes the tool-call promise plus a background()
8861
+ * trigger that transitions the running command into a TaskManager task.
8862
+ */
8863
+ startCommand(ctx, prepared, command, timeout, outputFile, manager) {
8864
+ let backgroundFn;
8865
+ const result = new Promise((resolve12) => {
8489
8866
  let aborted = false;
8490
8867
  let terminating = false;
8491
8868
  let settled = false;
8869
+ let backgrounded = false;
8870
+ let sizeKilled = false;
8492
8871
  let escalateTimer = null;
8493
- const child = spawn2(prepared.executable, prepared.args, {
8494
- cwd: ctx.workDir,
8495
- detached: true,
8496
- env: prepared.env,
8497
- stdio: ["ignore", "pipe", "pipe"]
8872
+ let child;
8873
+ try {
8874
+ child = spawn2(prepared.executable, prepared.args, {
8875
+ cwd: ctx.workDir,
8876
+ detached: true,
8877
+ env: prepared.env,
8878
+ // stdout and stderr share one O_APPEND fd: each write lands on disk
8879
+ // atomically and the streams interleave chronologically.
8880
+ stdio: ["ignore", outputFile.fd, outputFile.fd]
8881
+ });
8882
+ } catch (error) {
8883
+ discardFd(outputFile.fd);
8884
+ unlinkQuiet(outputFile.path);
8885
+ resolve12({
8886
+ output: `Error executing command: ${asErrorString(error)}`,
8887
+ isError: true
8888
+ });
8889
+ return;
8890
+ }
8891
+ discardFd(outputFile.fd);
8892
+ let doneResolve;
8893
+ const done = new Promise((resolveDone) => {
8894
+ doneResolve = resolveDone;
8498
8895
  });
8499
- let stdout = "";
8500
- let stderr = "";
8501
- let total = 0;
8502
- let outputTruncated = false;
8503
8896
  const killTree = (signal) => {
8504
8897
  if (typeof child.pid !== "number") {
8505
8898
  return;
@@ -8521,41 +8914,35 @@ var BashTool = class {
8521
8914
  killTree("SIGTERM");
8522
8915
  escalateTimer = setTimeout(() => {
8523
8916
  killTree("SIGKILL");
8524
- child.stdout.destroy();
8525
- child.stderr.destroy();
8526
8917
  }, KILL_GRACE_MS);
8527
8918
  escalateTimer.unref();
8528
8919
  };
8529
- const appendChunk = (chunk, target) => {
8530
- if (outputTruncated) {
8920
+ const watchdog = setInterval(() => {
8921
+ let size = 0;
8922
+ try {
8923
+ size = statSync10(outputFile.path).size;
8924
+ } catch {
8531
8925
  return;
8532
8926
  }
8533
- const remaining = MAX_SHELL_OUTPUT_BYTES - total;
8534
- const piece = takeUtf8Prefix(chunk, remaining);
8535
- total += utf8ByteLength(piece);
8536
- if (piece.length < chunk.length) {
8537
- outputTruncated = true;
8927
+ const cap = backgrounded ? BACKGROUND_MAX_OUTPUT_BYTES : MAX_SHELL_OUTPUT_BYTES;
8928
+ if (size > cap) {
8929
+ sizeKilled = backgrounded;
8930
+ clearInterval(watchdog);
8538
8931
  terminate();
8539
8932
  }
8540
- if (target === "stdout") {
8541
- stdout += piece;
8542
- } else {
8543
- stderr += piece;
8544
- }
8545
- };
8546
- child.stdout.setEncoding("utf-8");
8547
- child.stdout.on("data", (chunk) => {
8548
- appendChunk(chunk, "stdout");
8549
- });
8550
- child.stderr.setEncoding("utf-8");
8551
- child.stderr.on("data", (chunk) => {
8552
- appendChunk(chunk, "stderr");
8553
- });
8933
+ }, SIZE_WATCHDOG_INTERVAL_MS);
8934
+ watchdog.unref();
8554
8935
  const onAbort = () => {
8555
8936
  aborted = true;
8556
8937
  terminate();
8557
8938
  };
8939
+ const backgroundAvailableHere = manager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
8940
+ const autoBackgroundAllowed = backgroundAvailableHere && isAutobackgroundingAllowed(command, DISALLOWED_AUTO_BACKGROUND_COMMANDS);
8941
+ let timedOut = false;
8558
8942
  const timeoutTimer = setTimeout(() => {
8943
+ if (autoBackgroundAllowed && backgroundExecution("timeout") !== null) {
8944
+ return;
8945
+ }
8559
8946
  timedOut = true;
8560
8947
  terminate();
8561
8948
  }, timeout * 1e3);
@@ -8564,14 +8951,17 @@ var BashTool = class {
8564
8951
  if (ctx.abortSignal?.aborted) {
8565
8952
  onAbort();
8566
8953
  }
8954
+ const foregroundKey = `bash-${String(this.nextForegroundId++)}`;
8567
8955
  const cleanup = () => {
8568
8956
  clearTimeout(timeoutTimer);
8957
+ clearInterval(watchdog);
8569
8958
  if (escalateTimer) {
8570
8959
  clearTimeout(escalateTimer);
8571
8960
  }
8572
8961
  ctx.abortSignal?.removeEventListener("abort", onAbort);
8962
+ this.foreground.delete(foregroundKey);
8573
8963
  };
8574
- const settle = (result) => {
8964
+ const settle = (finalResult) => {
8575
8965
  if (settled) {
8576
8966
  return;
8577
8967
  }
@@ -8582,54 +8972,105 @@ var BashTool = class {
8582
8972
  };
8583
8973
  void finalize().then(
8584
8974
  () => {
8585
- resolve12(result);
8975
+ resolve12(finalResult);
8586
8976
  },
8587
8977
  (error) => {
8588
8978
  resolve12({
8589
- output: `${result.output}
8979
+ output: `${finalResult.output}
8590
8980
  Error cleaning up sandbox: ${asErrorString(error)}`,
8591
8981
  isError: true
8592
8982
  });
8593
8983
  }
8594
8984
  );
8595
8985
  };
8596
- child.on("error", (error) => {
8597
- settle({
8598
- output: `Error executing command: ${error.message}`,
8599
- isError: true
8600
- });
8601
- });
8602
- child.on("close", (code, signal) => {
8603
- stderr = prepared.annotateStderr?.(stderr) ?? stderr;
8604
- if (aborted || timedOut) {
8605
- const captured = stdout || stderr || outputTruncated ? formatShellOutput("$ ", command, stdout, stderr, outputTruncated) : "";
8606
- const error = aborted ? "Error: command interrupted" : `Error: command timed out after ${String(timeout)}s`;
8607
- settle({
8608
- output: captured ? `${captured}
8609
- ${error}` : error,
8610
- isError: true
8611
- });
8612
- return;
8613
- }
8614
- const exitCode = code ?? 0;
8615
- let output = formatShellOutput("$ ", command, stdout, stderr, outputTruncated);
8616
- if (outputTruncated) {
8617
- settle({ output, isError: true });
8986
+ const settleExit = (exit) => {
8987
+ if (backgrounded) {
8618
8988
  return;
8619
8989
  }
8620
- if (exitCode !== 0) {
8621
- const hint = exitCodeHint(command, exitCode);
8622
- output += hint ? `
8623
- Exit code ${String(exitCode)} (${hint})` : `
8624
- Exit code ${String(exitCode)}`;
8990
+ const read = readOutputFile(outputFile.path, MAX_SHELL_OUTPUT_BYTES);
8991
+ const merged = prepared.annotateStderr?.(read.text) ?? read.text;
8992
+ const finalResult = formatFinalResult("$ ", command, exit, merged, read.truncated, timeout);
8993
+ unlinkQuiet(outputFile.path);
8994
+ settle(finalResult);
8995
+ };
8996
+ const backgroundExecution = (reason) => {
8997
+ if (backgrounded || settled || terminating || !manager || !backgroundAvailableHere) {
8998
+ return null;
8625
8999
  }
8626
- if (code === null) {
8627
- output += `
8628
- Process terminated${signal ? ` by ${signal}` : " unexpectedly"}`;
9000
+ backgrounded = true;
9001
+ settled = true;
9002
+ clearTimeout(timeoutTimer);
9003
+ ctx.abortSignal?.removeEventListener("abort", onAbort);
9004
+ this.foreground.delete(foregroundKey);
9005
+ const task = manager.create(
9006
+ backgroundTaskName(command),
9007
+ async () => {
9008
+ const exit = await done;
9009
+ try {
9010
+ await prepared.cleanup?.();
9011
+ } catch {
9012
+ }
9013
+ const annotate = prepared.annotateStderr ? (text) => prepared.annotateStderr?.(text) ?? text : void 0;
9014
+ const body = buildBackgroundBody(
9015
+ "$ ",
9016
+ command,
9017
+ exit,
9018
+ outputFile.path,
9019
+ timeout,
9020
+ annotate
9021
+ );
9022
+ if (body.isError) {
9023
+ throw new TaskFailure(body.output);
9024
+ }
9025
+ return body.output;
9026
+ },
9027
+ () => {
9028
+ killTree("SIGKILL");
9029
+ },
9030
+ { originToolCallId: ctx.toolCallId, idPrefix: "bash", kind: "shell" }
9031
+ );
9032
+ resolve12({
9033
+ output: backgroundMessage(reason, task.id, timeout),
9034
+ isError: false
9035
+ });
9036
+ return task.id;
9037
+ };
9038
+ if (backgroundAvailableHere) {
9039
+ this.foreground.set(foregroundKey, {
9040
+ background: () => backgroundExecution("user") !== null
9041
+ });
9042
+ }
9043
+ const stopTimers = () => {
9044
+ clearInterval(watchdog);
9045
+ if (escalateTimer) {
9046
+ clearTimeout(escalateTimer);
8629
9047
  }
8630
- settle({ output, isError: exitCode !== 0 || code === null });
9048
+ };
9049
+ child.on("error", (error) => {
9050
+ stopTimers();
9051
+ const exit = {
9052
+ code: null,
9053
+ signal: null,
9054
+ aborted,
9055
+ timedOut,
9056
+ sizeKilled,
9057
+ spawnError: error.message
9058
+ };
9059
+ doneResolve?.(exit);
9060
+ settleExit(exit);
9061
+ });
9062
+ child.on("close", (code, signal) => {
9063
+ stopTimers();
9064
+ const exit = { code, signal, aborted, timedOut, sizeKilled };
9065
+ doneResolve?.(exit);
9066
+ settleExit(exit);
8631
9067
  });
9068
+ backgroundFn = backgroundExecution;
8632
9069
  });
9070
+ return {
9071
+ result,
9072
+ background: (reason) => backgroundFn?.(reason) ?? null
9073
+ };
8633
9074
  }
8634
9075
  };
8635
9076
 
@@ -8640,8 +9081,8 @@ __export(computer_use_exports, {
8640
9081
  });
8641
9082
  import { spawn as spawn3 } from "child_process";
8642
9083
  import { mkdtemp, readFile as readFile4, rm, writeFile as writeFile2 } from "fs/promises";
8643
- import { tmpdir } from "os";
8644
- import { join as join19 } from "path";
9084
+ import { tmpdir as tmpdir2 } from "os";
9085
+ import { join as join20 } from "path";
8645
9086
  import { setTimeout as delay } from "timers/promises";
8646
9087
  import sharp from "sharp";
8647
9088
  import { safeParse as safeParse2, z as z14 } from "zod";
@@ -9718,9 +10159,9 @@ var ComputerUseTool = class {
9718
10159
  }
9719
10160
  }
9720
10161
  async compileMacHelper(signal) {
9721
- const directory = await mkdtemp(join19(tmpdir(), "swifty-computer-helper-"));
9722
- const sourcePath = join19(directory, "main.swift");
9723
- const executablePath = join19(directory, "computer-helper");
10162
+ const directory = await mkdtemp(join20(tmpdir2(), "swifty-computer-helper-"));
10163
+ const sourcePath = join20(directory, "main.swift");
10164
+ const executablePath = join20(directory, "computer-helper");
9724
10165
  try {
9725
10166
  await writeFile2(sourcePath, MACOS_SNIPPET, "utf8");
9726
10167
  const result = await this.run(
@@ -9937,8 +10378,8 @@ var ComputerUseTool = class {
9937
10378
  return { output, contentBlocks: [imageBlock], isError: false };
9938
10379
  }
9939
10380
  async captureScreenshot(signal) {
9940
- const directory = await mkdtemp(join19(tmpdir(), "swifty-computer-"));
9941
- const screenshotPath = join19(directory, "screenshot.png");
10381
+ const directory = await mkdtemp(join20(tmpdir2(), "swifty-computer-"));
10382
+ const screenshotPath = join20(directory, "screenshot.png");
9942
10383
  try {
9943
10384
  switch (this.platform) {
9944
10385
  case "darwin": {
@@ -10215,8 +10656,10 @@ __export(powershell_exports, {
10215
10656
  PowerShellTool: () => PowerShellTool
10216
10657
  });
10217
10658
  import { execFile as execFile2, spawn as spawn4 } from "child_process";
10659
+ import { statSync as statSync11 } from "fs";
10218
10660
  var MAX_TIMEOUT2 = 600;
10219
10661
  var KILL_GRACE_MS2 = 3e3;
10662
+ var DISALLOWED_AUTO_BACKGROUND_COMMANDS2 = /* @__PURE__ */ new Set(["start-sleep", "sleep"]);
10220
10663
  var PowerShellTool = class {
10221
10664
  // Use a hardcoded string instead of PowerShellTool.name.replace("Tool", "")
10222
10665
  // because class names are not stable after minification — bundlers like
@@ -10224,57 +10667,130 @@ var PowerShellTool = class {
10224
10667
  name = "PowerShell";
10225
10668
  description = POWERSHELL_DESCRIPTION;
10226
10669
  category = "command";
10670
+ /**
10671
+ * Background task registry, injected by the host — same contract as
10672
+ * BashTool.taskManager (schema gating, timeout auto-background, Ctrl+B).
10673
+ * Calls running inside a subagent loop carry that loop's own manager in
10674
+ * ctx.taskManager, which takes precedence.
10675
+ */
10676
+ taskManager = null;
10677
+ /** Running foreground executions eligible for manual backgrounding (Ctrl+B). */
10678
+ foreground = /* @__PURE__ */ new Map();
10679
+ nextForegroundId = 1;
10680
+ /** Instance-level background gate; see BashTool.backgroundEnabled. */
10681
+ backgroundEnabled() {
10682
+ return this.taskManager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
10683
+ }
10684
+ /** True while at least one foreground PowerShell command runs. */
10685
+ hasForegroundTasks() {
10686
+ return this.foreground.size > 0;
10687
+ }
10688
+ /** Move every running foreground command to the background (Ctrl+B). */
10689
+ backgroundForegroundTasks() {
10690
+ let count = 0;
10691
+ for (const entry of [...this.foreground.values()]) {
10692
+ if (entry.background()) {
10693
+ count++;
10694
+ }
10695
+ }
10696
+ return count;
10697
+ }
10227
10698
  schema() {
10228
- const inputSchema = {
10229
- type: "object",
10230
- properties: {
10231
- command: {
10232
- type: "string",
10233
- description: "PowerShell command to execute"
10234
- },
10235
- timeout: {
10236
- type: "integer",
10237
- description: "Timeout in seconds (max 600)",
10238
- default: 120
10239
- }
10699
+ const properties = {
10700
+ command: {
10701
+ type: "string",
10702
+ description: "PowerShell command to execute"
10240
10703
  },
10241
- required: ["command"]
10704
+ timeout: {
10705
+ type: "integer",
10706
+ description: "Timeout in seconds (max 600)",
10707
+ default: 120
10708
+ }
10242
10709
  };
10710
+ let description = this.description;
10711
+ if (this.backgroundEnabled()) {
10712
+ properties.run_in_background = {
10713
+ type: "boolean",
10714
+ description: "Run the command in the background. Returns a task ID immediately; the result arrives later as a task notification.",
10715
+ default: false
10716
+ };
10717
+ description = `${this.description}
10718
+ ${POWERSHELL_BACKGROUND_DESCRIPTION}`;
10719
+ }
10243
10720
  return {
10244
10721
  name: this.name,
10245
- description: this.description,
10246
- input_schema: inputSchema
10722
+ description,
10723
+ input_schema: {
10724
+ type: "object",
10725
+ properties,
10726
+ required: ["command"]
10727
+ }
10247
10728
  };
10248
10729
  }
10249
- execute(ctx, args) {
10730
+ async execute(ctx, args) {
10250
10731
  const command = strArg(args, "command");
10251
10732
  if (!command) {
10252
- return Promise.resolve({
10733
+ return {
10253
10734
  output: "Error: command is required",
10254
10735
  isError: true
10255
- });
10736
+ };
10256
10737
  }
10257
10738
  let timeout = intArg(args, "timeout", 120);
10258
10739
  if (!Number.isFinite(timeout) || timeout <= 0) {
10259
- return Promise.resolve({
10740
+ return {
10260
10741
  output: "Error: timeout must be a finite number greater than 0 seconds",
10261
10742
  isError: true
10262
- });
10743
+ };
10263
10744
  }
10264
10745
  if (timeout > MAX_TIMEOUT2) {
10265
10746
  timeout = MAX_TIMEOUT2;
10266
10747
  }
10748
+ const manager = ctx.taskManager !== void 0 ? ctx.taskManager : this.taskManager;
10749
+ const backgroundAvailable = manager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
10750
+ const runInBackground = boolArg(args, "run_in_background") && backgroundAvailable;
10267
10751
  const shell = process.platform === "win32" ? "powershell.exe" : "pwsh";
10752
+ let outputFile;
10753
+ try {
10754
+ outputFile = createShellOutputFile(ctx.workDir, ctx.sessionId ?? "");
10755
+ } catch (error) {
10756
+ return {
10757
+ output: `Error creating output file: ${asErrorString(error)}`,
10758
+ isError: true
10759
+ };
10760
+ }
10268
10761
  if (ctx.abortSignal?.aborted) {
10269
- return Promise.resolve({
10762
+ discardFd(outputFile.fd);
10763
+ unlinkQuiet(outputFile.path);
10764
+ return {
10270
10765
  output: "Error: command interrupted",
10271
10766
  isError: true
10272
- });
10767
+ };
10273
10768
  }
10274
- return new Promise((resolve12) => {
10275
- let timedOut = false;
10769
+ const handle = this.startCommand(ctx, shell, command, timeout, outputFile, manager);
10770
+ if (runInBackground) {
10771
+ const taskId = handle.background("explicit");
10772
+ if (taskId !== null) {
10773
+ return {
10774
+ output: backgroundMessage("explicit", taskId, timeout),
10775
+ isError: false
10776
+ };
10777
+ }
10778
+ }
10779
+ return handle.result;
10780
+ }
10781
+ /**
10782
+ * Spawn PowerShell with stdout+stderr writing directly into the output file
10783
+ * (fd mode, shared with BashTool): output never flows through JS, so
10784
+ * backgrounding is a bookkeeping switch — no re-spawn, no buffer handover.
10785
+ */
10786
+ startCommand(ctx, shell, command, timeout, outputFile, manager) {
10787
+ let backgroundFn;
10788
+ const result = new Promise((resolve12) => {
10276
10789
  let aborted = false;
10277
10790
  let terminating = false;
10791
+ let settled = false;
10792
+ let backgrounded = false;
10793
+ let sizeKilled = false;
10278
10794
  let escalateTimer = null;
10279
10795
  const shellCommand = process.platform === "win32" ? "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n" + command : command;
10280
10796
  const shellArgs = process.platform === "win32" ? [
@@ -10285,17 +10801,31 @@ var PowerShellTool = class {
10285
10801
  "-Command",
10286
10802
  shellCommand
10287
10803
  ] : ["-NoProfile", "-NonInteractive", "-Command", shellCommand];
10288
- const child = spawn4(shell, shellArgs, {
10289
- cwd: ctx.workDir,
10290
- // Only POSIX needs its own process group for kill(-pid); the Windows
10291
- // tree kill goes through taskkill and needs no new group.
10292
- detached: process.platform !== "win32",
10293
- stdio: ["ignore", "pipe", "pipe"]
10804
+ let child;
10805
+ try {
10806
+ child = spawn4(shell, shellArgs, {
10807
+ cwd: ctx.workDir,
10808
+ // Only POSIX needs its own process group for kill(-pid); the Windows
10809
+ // tree kill goes through taskkill and needs no new group.
10810
+ detached: process.platform !== "win32",
10811
+ // stdout and stderr share one O_APPEND fd: each write lands on disk
10812
+ // atomically and the streams interleave chronologically.
10813
+ stdio: ["ignore", outputFile.fd, outputFile.fd]
10814
+ });
10815
+ } catch (error) {
10816
+ discardFd(outputFile.fd);
10817
+ unlinkQuiet(outputFile.path);
10818
+ resolve12({
10819
+ output: `Error executing command: ${asErrorString(error)}`,
10820
+ isError: true
10821
+ });
10822
+ return;
10823
+ }
10824
+ discardFd(outputFile.fd);
10825
+ let doneResolve;
10826
+ const done = new Promise((resolveDone) => {
10827
+ doneResolve = resolveDone;
10294
10828
  });
10295
- let stdout = "";
10296
- let stderr = "";
10297
- let total = 0;
10298
- let outputTruncated = false;
10299
10829
  const alreadyExited = () => child.exitCode !== null || child.signalCode !== null;
10300
10830
  const killTree = (signal) => {
10301
10831
  if (typeof child.pid !== "number") {
@@ -10333,41 +10863,35 @@ var PowerShellTool = class {
10333
10863
  killTree("SIGTERM");
10334
10864
  escalateTimer = setTimeout(() => {
10335
10865
  killTree("SIGKILL");
10336
- child.stdout.destroy();
10337
- child.stderr.destroy();
10338
10866
  }, KILL_GRACE_MS2);
10339
10867
  escalateTimer.unref();
10340
10868
  };
10341
- const appendChunk = (chunk, target) => {
10342
- if (outputTruncated) {
10869
+ const watchdog = setInterval(() => {
10870
+ let size = 0;
10871
+ try {
10872
+ size = statSync11(outputFile.path).size;
10873
+ } catch {
10343
10874
  return;
10344
10875
  }
10345
- const remaining = MAX_SHELL_OUTPUT_BYTES - total;
10346
- const piece = takeUtf8Prefix(chunk, remaining);
10347
- total += utf8ByteLength(piece);
10348
- if (piece.length < chunk.length) {
10349
- outputTruncated = true;
10876
+ const cap = backgrounded ? BACKGROUND_MAX_OUTPUT_BYTES : MAX_SHELL_OUTPUT_BYTES;
10877
+ if (size > cap) {
10878
+ sizeKilled = backgrounded;
10879
+ clearInterval(watchdog);
10350
10880
  terminate();
10351
10881
  }
10352
- if (target === "stdout") {
10353
- stdout += piece;
10354
- } else {
10355
- stderr += piece;
10356
- }
10357
- };
10358
- child.stdout.setEncoding("utf-8");
10359
- child.stdout.on("data", (chunk) => {
10360
- appendChunk(chunk, "stdout");
10361
- });
10362
- child.stderr.setEncoding("utf-8");
10363
- child.stderr.on("data", (chunk) => {
10364
- appendChunk(chunk, "stderr");
10365
- });
10882
+ }, SIZE_WATCHDOG_INTERVAL_MS);
10883
+ watchdog.unref();
10366
10884
  const onAbort = () => {
10367
10885
  aborted = true;
10368
10886
  terminate();
10369
10887
  };
10888
+ const backgroundAvailableHere = manager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
10889
+ const autoBackgroundAllowed = backgroundAvailableHere && isAutobackgroundingAllowed(command, DISALLOWED_AUTO_BACKGROUND_COMMANDS2);
10890
+ let timedOut = false;
10370
10891
  const timeoutTimer = setTimeout(() => {
10892
+ if (autoBackgroundAllowed && backgroundExecution("timeout") !== null) {
10893
+ return;
10894
+ }
10371
10895
  timedOut = true;
10372
10896
  terminate();
10373
10897
  }, timeout * 1e3);
@@ -10376,60 +10900,107 @@ var PowerShellTool = class {
10376
10900
  if (ctx.abortSignal?.aborted) {
10377
10901
  onAbort();
10378
10902
  }
10903
+ const foregroundKey = `ps-${String(this.nextForegroundId++)}`;
10379
10904
  const cleanup = () => {
10380
10905
  clearTimeout(timeoutTimer);
10906
+ clearInterval(watchdog);
10381
10907
  if (escalateTimer) {
10382
10908
  clearTimeout(escalateTimer);
10383
10909
  }
10384
10910
  ctx.abortSignal?.removeEventListener("abort", onAbort);
10911
+ this.foreground.delete(foregroundKey);
10385
10912
  };
10386
- child.on("error", (err) => {
10387
- cleanup();
10388
- const hint = strArg(asRecord(err), "code") === "ENOENT" && process.platform !== "win32" ? " (pwsh is required on macOS/Linux \u2014 install PowerShell Core)" : "";
10389
- resolve12({
10390
- output: `Error executing command: ${err.message}${hint}`,
10391
- isError: true
10392
- });
10393
- });
10394
- child.on("close", (code, signal) => {
10395
- cleanup();
10396
- if (aborted) {
10397
- const captured = stdout || stderr || outputTruncated ? formatShellOutput("PS> ", command, stdout, stderr, outputTruncated) : "";
10398
- resolve12({
10399
- output: captured ? `${captured}
10400
- Error: command interrupted` : "Error: command interrupted",
10401
- isError: true
10402
- });
10403
- return;
10404
- }
10405
- if (timedOut) {
10406
- const captured = stdout || stderr || outputTruncated ? formatShellOutput("PS> ", command, stdout, stderr, outputTruncated) : "";
10407
- resolve12({
10408
- output: captured ? `${captured}
10409
- Error: command timed out after ${String(timeout)}s` : `Error: command timed out after ${String(timeout)}s`,
10410
- isError: true
10411
- });
10913
+ const settle = (finalResult) => {
10914
+ if (settled) {
10412
10915
  return;
10413
10916
  }
10414
- const exitCode = code ?? 0;
10415
- let output = formatShellOutput("PS> ", command, stdout, stderr, outputTruncated);
10416
- if (outputTruncated) {
10417
- resolve12({ output, isError: true });
10917
+ settled = true;
10918
+ cleanup();
10919
+ resolve12(finalResult);
10920
+ };
10921
+ const settleExit = (exit) => {
10922
+ if (backgrounded) {
10418
10923
  return;
10419
10924
  }
10420
- if (exitCode !== 0) {
10421
- const hint = exitCodeHint(command, exitCode);
10422
- output += hint ? `
10423
- Exit code ${String(exitCode)} (${hint})` : `
10424
- Exit code ${String(exitCode)}`;
10925
+ const read = readOutputFile(outputFile.path, MAX_SHELL_OUTPUT_BYTES);
10926
+ const finalResult = formatFinalResult(
10927
+ "PS> ",
10928
+ command,
10929
+ exit,
10930
+ read.text,
10931
+ read.truncated,
10932
+ timeout
10933
+ );
10934
+ unlinkQuiet(outputFile.path);
10935
+ settle(finalResult);
10936
+ };
10937
+ const backgroundExecution = (reason) => {
10938
+ if (backgrounded || settled || terminating || !manager || !backgroundAvailableHere) {
10939
+ return null;
10425
10940
  }
10426
- if (code === null) {
10427
- output += `
10428
- Process terminated${signal ? ` by ${signal}` : " unexpectedly"}`;
10941
+ backgrounded = true;
10942
+ settled = true;
10943
+ clearTimeout(timeoutTimer);
10944
+ ctx.abortSignal?.removeEventListener("abort", onAbort);
10945
+ this.foreground.delete(foregroundKey);
10946
+ const task = manager.create(
10947
+ backgroundTaskName(command),
10948
+ async () => {
10949
+ const exit = await done;
10950
+ const body = buildBackgroundBody("PS> ", command, exit, outputFile.path, timeout);
10951
+ if (body.isError) {
10952
+ throw new TaskFailure(body.output);
10953
+ }
10954
+ return body.output;
10955
+ },
10956
+ () => {
10957
+ killTree("SIGKILL");
10958
+ },
10959
+ { originToolCallId: ctx.toolCallId, idPrefix: "ps", kind: "shell" }
10960
+ );
10961
+ resolve12({
10962
+ output: backgroundMessage(reason, task.id, timeout),
10963
+ isError: false
10964
+ });
10965
+ return task.id;
10966
+ };
10967
+ if (backgroundAvailableHere) {
10968
+ this.foreground.set(foregroundKey, {
10969
+ background: () => backgroundExecution("user") !== null
10970
+ });
10971
+ }
10972
+ const stopTimers = () => {
10973
+ clearInterval(watchdog);
10974
+ if (escalateTimer) {
10975
+ clearTimeout(escalateTimer);
10429
10976
  }
10430
- resolve12({ output, isError: exitCode !== 0 || code === null });
10977
+ };
10978
+ child.on("error", (err) => {
10979
+ stopTimers();
10980
+ const hint = strArg(asRecord(err), "code") === "ENOENT" && process.platform !== "win32" ? " (pwsh is required on macOS/Linux \u2014 install PowerShell Core)" : "";
10981
+ const exit = {
10982
+ code: null,
10983
+ signal: null,
10984
+ aborted,
10985
+ timedOut,
10986
+ sizeKilled,
10987
+ spawnError: `${err.message}${hint}`
10988
+ };
10989
+ doneResolve?.(exit);
10990
+ settleExit(exit);
10991
+ });
10992
+ child.on("close", (code, signal) => {
10993
+ stopTimers();
10994
+ const exit = { code, signal, aborted, timedOut, sizeKilled };
10995
+ doneResolve?.(exit);
10996
+ settleExit(exit);
10431
10997
  });
10998
+ backgroundFn = backgroundExecution;
10432
10999
  });
11000
+ return {
11001
+ result,
11002
+ background: (reason) => backgroundFn?.(reason) ?? null
11003
+ };
10433
11004
  }
10434
11005
  };
10435
11006
 
@@ -10621,7 +11192,7 @@ var AskUserResponseSchema = z15.object({
10621
11192
  id: z15.string(),
10622
11193
  answers: z15.record(z15.string(), z15.string())
10623
11194
  });
10624
- var FE_DIST = join20(import.meta.dirname, "fe", "dist");
11195
+ var FE_DIST = join21(import.meta.dirname, "fe", "dist");
10625
11196
  var MIME_TYPES = {
10626
11197
  ".html": "text/html; charset=utf-8",
10627
11198
  ".js": "application/javascript; charset=utf-8",
@@ -10634,11 +11205,11 @@ var MIME_TYPES = {
10634
11205
  };
10635
11206
  function serveStatic(path) {
10636
11207
  const cleanPath = normalize(path).replace(/^(\.\.[/\\])+/, "");
10637
- const fullPath = join20(FE_DIST, cleanPath);
11208
+ const fullPath = join21(FE_DIST, cleanPath);
10638
11209
  if (!fullPath.startsWith(FE_DIST)) {
10639
11210
  return null;
10640
11211
  }
10641
- if (!existsSync16(fullPath) || !statSync9(fullPath).isFile()) {
11212
+ if (!existsSync16(fullPath) || !statSync12(fullPath).isFile()) {
10642
11213
  return null;
10643
11214
  }
10644
11215
  const body = readFileSync16(fullPath);
@@ -10814,31 +11385,38 @@ async function createRemoteAgent(opts) {
10814
11385
  if (!client) {
10815
11386
  throw new Error("no llm client (provider not initialized)");
10816
11387
  }
10817
- const { PermissionChecker: PC } = await import("./permissions-BW4QPZXV.js");
10818
- const { Agent: AgentClass } = await import("./agent-TC4HSVMS.js");
11388
+ const { PermissionChecker: PC } = await import("./permissions-77UJQITR.js");
11389
+ const { Agent: AgentClass } = await import("./agent-ZMIS6IH6.js");
10819
11390
  const subConv = new ConversationManager();
10820
11391
  subConv.addUserMessage(prompt);
11392
+ const taskManager = new TaskManager();
10821
11393
  const subAgent = new AgentClass({
10822
11394
  client,
10823
11395
  registry: filterToolsForAgent(registry, void 0, void 0, false),
10824
11396
  checker: new PC(workDir, "acceptEdits"),
10825
11397
  conversation: subConv,
10826
11398
  workDir,
10827
- maxIterations: 200
11399
+ maxIterations: 200,
11400
+ taskManager,
11401
+ notificationFn: () => taskManager.drainNotifications().map(formatAgentTaskNotification)
10828
11402
  });
10829
11403
  let output = "";
10830
- for await (const event of subAgent.run()) {
10831
- switch (event.type) {
10832
- case "stream_text":
10833
- output += event.text;
10834
- break;
10835
- case "loop_complete":
10836
- return output || "[No output]";
10837
- case "error":
10838
- throw event.error;
11404
+ try {
11405
+ for await (const event of subAgent.run()) {
11406
+ switch (event.type) {
11407
+ case "stream_text":
11408
+ output += event.text;
11409
+ break;
11410
+ case "loop_complete":
11411
+ return output || "[No output]";
11412
+ case "error":
11413
+ throw event.error;
11414
+ }
10839
11415
  }
11416
+ return output || "[No output]";
11417
+ } finally {
11418
+ await taskManager.stopAll();
10840
11419
  }
10841
- return output || "[No output]";
10842
11420
  }
10843
11421
  };
10844
11422
  registry.register(new LoadSkillTool(catalog, skillHost, skillForkHost));
@@ -10856,10 +11434,12 @@ async function createRemoteAgent(opts) {
10856
11434
  onEvent,
10857
11435
  void 0,
10858
11436
  teamChecker,
10859
- { abortSignal }
11437
+ // Teammates stay purely foreground: see SubagentRunOptions.backgroundTasks.
11438
+ { abortSignal, backgroundTasks: false }
10860
11439
  );
10861
11440
  const teamManager = new TeamManager(workDir);
10862
11441
  const backgroundTaskManager = new TaskManager();
11442
+ attachBackgroundTaskManager(registry, backgroundTaskManager);
10863
11443
  registry.register(new TeamCreateTool(teamManager));
10864
11444
  registry.register(new SendMessageTool(teamManager));
10865
11445
  registry.register(new TeamDeleteTool(teamManager));
@@ -10898,6 +11478,7 @@ async function createRemoteAgent(opts) {
10898
11478
  const forkClient = modelOverride ? await createClient({ ...provider, model: resolvedModel }, forkSystemPrompt) : client;
10899
11479
  const checker = context?.permissionChecker ?? new PermissionChecker(forkWorkDir, "acceptEdits");
10900
11480
  forkConv.addUserMessage(prompt);
11481
+ const forkTaskManager = new TaskManager();
10901
11482
  const agent2 = new Agent({
10902
11483
  client: forkClient,
10903
11484
  registry: forkRegistry,
@@ -10909,23 +11490,29 @@ async function createRemoteAgent(opts) {
10909
11490
  onPermissionRequest: context?.onPermissionRequest,
10910
11491
  fileStateCache: new FileStateCache(),
10911
11492
  instructions,
10912
- memoryContent: memReminder
11493
+ memoryContent: memReminder,
11494
+ taskManager: forkTaskManager,
11495
+ notificationFn: () => forkTaskManager.drainNotifications().map(formatAgentTaskNotification)
10913
11496
  });
10914
11497
  let output = "";
10915
- for await (const event of agent2.run()) {
10916
- switch (event.type) {
10917
- case "stream_text":
10918
- output += event.text;
10919
- break;
10920
- case "loop_complete":
10921
- return output || "[No output]";
10922
- case "error":
10923
- return output ? `${output}
11498
+ try {
11499
+ for await (const event of agent2.run()) {
11500
+ switch (event.type) {
11501
+ case "stream_text":
11502
+ output += event.text;
11503
+ break;
11504
+ case "loop_complete":
11505
+ return output || "[No output]";
11506
+ case "error":
11507
+ return output ? `${output}
10924
11508
 
10925
11509
  [Error: ${event.error.message}]` : `Error: ${event.error.message}`;
11510
+ }
10926
11511
  }
11512
+ return output || "[No output]";
11513
+ } finally {
11514
+ await forkTaskManager.stopAll();
10927
11515
  }
10928
- return output || "[No output]";
10929
11516
  },
10930
11517
  backgroundTaskManager
10931
11518
  );
@@ -12423,6 +13010,7 @@ __export(javascript_exports, {
12423
13010
  evaluateJavaScript: () => evaluateJavaScript,
12424
13011
  isolatedVmSupported: () => isolatedVmSupported
12425
13012
  });
13013
+ import { randomBytes as randomBytes4 } from "crypto";
12426
13014
  import { z as z16 } from "zod";
12427
13015
  var DEFAULT_TIMEOUT_MS = 1e3;
12428
13016
  var MAX_TIMEOUT_MS = 1e4;
@@ -12435,7 +13023,10 @@ var JavaScriptArgumentsSchema = z16.object({
12435
13023
  code: z16.string().min(1),
12436
13024
  input: z16.json().optional(),
12437
13025
  timeout_ms: z16.number().int().min(1).max(MAX_TIMEOUT_MS).default(DEFAULT_TIMEOUT_MS),
12438
- memory_limit_mb: z16.number().int().min(8).max(MAX_MEMORY_LIMIT_MB).default(DEFAULT_MEMORY_LIMIT_MB)
13026
+ memory_limit_mb: z16.number().int().min(8).max(MAX_MEMORY_LIMIT_MB).default(DEFAULT_MEMORY_LIMIT_MB),
13027
+ // Accepted even when backgrounding is unavailable (then simply ignored):
13028
+ // the JSON schema gates what the model sees per host.
13029
+ run_in_background: z16.boolean().optional()
12439
13030
  });
12440
13031
  var JavaScriptEvaluationOptionsSchema = z16.object({
12441
13032
  input: z16.json().optional(),
@@ -12471,14 +13062,22 @@ async function runJavaScript(code, input, timeoutMs, memoryLimitMb, abortSignal)
12471
13062
  }
12472
13063
  abortSignal?.throwIfAborted();
12473
13064
  const module = await import("isolated-vm");
13065
+ abortSignal?.throwIfAborted();
12474
13066
  const ivm = module.default;
12475
13067
  const isolate = new ivm.Isolate({ memoryLimit: memoryLimitMb });
12476
13068
  const context = await isolate.createContext();
12477
13069
  const onAbort = () => {
12478
- isolate.dispose();
13070
+ if (!isolate.isDisposed) {
13071
+ isolate.dispose();
13072
+ }
12479
13073
  };
12480
13074
  abortSignal?.addEventListener("abort", onAbort, { once: true });
13075
+ let wallTimer;
12481
13076
  try {
13077
+ if (abortSignal?.aborted) {
13078
+ onAbort();
13079
+ abortSignal.throwIfAborted();
13080
+ }
12482
13081
  await context.global.set("input", input ?? null, { copy: true });
12483
13082
  const source = `
12484
13083
  (async () => {
@@ -12523,12 +13122,19 @@ ${code}
12523
13122
  return __payload;
12524
13123
  })()
12525
13124
  `;
12526
- const raw = await context.eval(source, {
13125
+ const evaluation = context.eval(source, {
12527
13126
  timeout: timeoutMs,
12528
13127
  promise: true,
12529
13128
  copy: true,
12530
13129
  filename: "swifty-sandbox.js"
12531
13130
  });
13131
+ const deadline = new Promise((_, reject) => {
13132
+ wallTimer = setTimeout(() => {
13133
+ reject(new Error(`JavaScript evaluation timed out after ${String(timeoutMs)}ms`));
13134
+ onAbort();
13135
+ }, timeoutMs);
13136
+ });
13137
+ const raw = await Promise.race([evaluation, deadline]);
12532
13138
  if (typeof raw !== "string") {
12533
13139
  throw new Error("JavaScript sandbox returned an invalid result");
12534
13140
  }
@@ -12538,6 +13144,7 @@ ${code}
12538
13144
  const decoded = JSON.parse(raw);
12539
13145
  return JavaScriptEvaluationSchema.parse(decoded);
12540
13146
  } finally {
13147
+ clearTimeout(wallTimer);
12541
13148
  abortSignal?.removeEventListener("abort", onAbort);
12542
13149
  try {
12543
13150
  context.release();
@@ -12548,37 +13155,94 @@ ${code}
12548
13155
  }
12549
13156
  }
12550
13157
  }
13158
+ function formatEvaluation(result) {
13159
+ const sections = [];
13160
+ if (result.logs.length > 0) {
13161
+ sections.push(`Console:
13162
+ ${result.logs.join("\n")}`);
13163
+ }
13164
+ if (result.logsTruncated) {
13165
+ sections.push("Console output truncated");
13166
+ }
13167
+ sections.push(`Result:
13168
+ ${JSON.stringify(result.value, null, 2)}`);
13169
+ return sections.join("\n\n");
13170
+ }
13171
+ function jsBackgroundMessage(reason, taskId) {
13172
+ return reason === "user" ? `JavaScript evaluation was manually backgrounded by the user (task_id: ${taskId}). It is still running \u2014 you will be notified when it completes.` : `JavaScript running in background (task_id: ${taskId}). You will be notified when it completes; do not poll. Use TaskStop with task_id to abort it early.`;
13173
+ }
12551
13174
  var JavaScriptTool = class {
12552
13175
  name = "JavaScript";
12553
13176
  description = JAVASCRIPT_DESCRIPTION;
12554
13177
  category = "command";
13178
+ /**
13179
+ * Background task registry, injected by the host — same contract as
13180
+ * BashTool.taskManager. Unlike the shell tools there is no timeout
13181
+ * auto-background: timeout_ms is the isolate's hard safety cap — V8 kills a
13182
+ * synchronous script at the cap, and a wall-clock deadline disposes the
13183
+ * isolate when an evaluation is suspended on a promise — so nothing is left
13184
+ * to keep running.
13185
+ */
13186
+ taskManager = null;
13187
+ /** Running foreground evaluations eligible for manual backgrounding (Ctrl+B). */
13188
+ foreground = /* @__PURE__ */ new Map();
13189
+ nextForegroundId = 1;
13190
+ /** Instance-level background gate; see BashTool.backgroundEnabled. */
13191
+ backgroundEnabled() {
13192
+ return this.taskManager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
13193
+ }
13194
+ /** True while at least one foreground evaluation runs. */
13195
+ hasForegroundTasks() {
13196
+ return this.foreground.size > 0;
13197
+ }
13198
+ /** Move every running foreground evaluation to the background (Ctrl+B). */
13199
+ backgroundForegroundTasks() {
13200
+ let count = 0;
13201
+ for (const entry of [...this.foreground.values()]) {
13202
+ if (entry.background()) {
13203
+ count++;
13204
+ }
13205
+ }
13206
+ return count;
13207
+ }
12555
13208
  schema() {
13209
+ const properties = {
13210
+ code: {
13211
+ type: "string",
13212
+ description: "JavaScript function body to execute; use return to produce a result"
13213
+ },
13214
+ input: {
13215
+ description: "Optional JSON-compatible value exposed as globalThis.input"
13216
+ },
13217
+ timeout_ms: {
13218
+ type: "integer",
13219
+ minimum: 1,
13220
+ maximum: MAX_TIMEOUT_MS,
13221
+ default: DEFAULT_TIMEOUT_MS
13222
+ },
13223
+ memory_limit_mb: {
13224
+ type: "integer",
13225
+ minimum: 8,
13226
+ maximum: MAX_MEMORY_LIMIT_MB,
13227
+ default: DEFAULT_MEMORY_LIMIT_MB
13228
+ }
13229
+ };
13230
+ let description = this.description;
13231
+ if (this.backgroundEnabled()) {
13232
+ properties.run_in_background = {
13233
+ type: "boolean",
13234
+ description: "Run the evaluation in the background. Returns a task ID immediately; the result arrives later as a task notification.",
13235
+ default: false
13236
+ };
13237
+ description = `${this.description}
13238
+ ${JAVASCRIPT_BACKGROUND_DESCRIPTION}`;
13239
+ }
12556
13240
  return {
12557
13241
  name: this.name,
12558
- description: this.description,
13242
+ description,
12559
13243
  input_schema: {
12560
13244
  type: "object",
12561
- properties: {
12562
- code: {
12563
- type: "string",
12564
- description: "JavaScript function body to execute; use return to produce a result"
12565
- },
12566
- input: {
12567
- description: "Optional JSON-compatible value exposed as globalThis.input"
12568
- },
12569
- timeout_ms: {
12570
- type: "integer",
12571
- minimum: 1,
12572
- maximum: MAX_TIMEOUT_MS,
12573
- default: DEFAULT_TIMEOUT_MS
12574
- },
12575
- memory_limit_mb: {
12576
- type: "integer",
12577
- minimum: 8,
12578
- maximum: MAX_MEMORY_LIMIT_MB,
12579
- default: DEFAULT_MEMORY_LIMIT_MB
12580
- }
12581
- },
13245
+ properties,
12582
13246
  required: ["code"],
12583
13247
  additionalProperties: false
12584
13248
  }
@@ -12589,32 +13253,128 @@ var JavaScriptTool = class {
12589
13253
  if (!parsed.success) {
12590
13254
  return { output: `Error: ${parsed.error.message}`, isError: true };
12591
13255
  }
12592
- try {
12593
- const result = await runJavaScript(
12594
- parsed.data.code,
12595
- parsed.data.input,
12596
- parsed.data.timeout_ms,
12597
- parsed.data.memory_limit_mb,
12598
- ctx.abortSignal
12599
- );
12600
- const sections = [];
12601
- if (result.logs.length > 0) {
12602
- sections.push(`Console:
12603
- ${result.logs.join("\n")}`);
12604
- }
12605
- if (result.logsTruncated) {
12606
- sections.push("Console output truncated");
13256
+ const manager = ctx.taskManager !== void 0 ? ctx.taskManager : this.taskManager;
13257
+ const backgroundAvailable = manager !== null && process.env.SWIFTY_DISABLE_BACKGROUND_TASKS !== "1";
13258
+ const runInBackground = parsed.data.run_in_background === true && backgroundAvailable;
13259
+ const handle = this.startEvaluation(ctx, parsed.data, manager, backgroundAvailable);
13260
+ if (runInBackground) {
13261
+ const taskId = handle.background("explicit");
13262
+ if (taskId !== null) {
13263
+ return {
13264
+ output: jsBackgroundMessage("explicit", taskId),
13265
+ isError: false
13266
+ };
12607
13267
  }
12608
- sections.push(`Result:
12609
- ${JSON.stringify(result.value, null, 2)}`);
12610
- return { output: sections.join("\n\n"), isError: false };
12611
- } catch (error) {
12612
- return { output: `Error executing JavaScript: ${asErrorString(error)}`, isError: true };
12613
13268
  }
13269
+ return handle.result;
13270
+ }
13271
+ /**
13272
+ * Start the isolated evaluation. The returned handle mirrors the shell
13273
+ * tools: background() transitions the pending evaluation into a TaskManager
13274
+ * task — the isolate keeps running on its own thread, the tool call
13275
+ * resolves immediately, and completion arrives as a task notification.
13276
+ * While foreground, the caller's abort signal (Esc) disposes the isolate;
13277
+ * once backgrounded, only TaskStop or session shutdown can abort it.
13278
+ */
13279
+ startEvaluation(ctx, data, manager, backgroundAvailable) {
13280
+ let backgroundFn;
13281
+ const result = new Promise((resolve12) => {
13282
+ let settled = false;
13283
+ let backgrounded = false;
13284
+ const controller = new AbortController();
13285
+ const onCallerAbort = () => {
13286
+ controller.abort();
13287
+ };
13288
+ if (ctx.abortSignal?.aborted) {
13289
+ resolve12({ output: "Error: command interrupted", isError: true });
13290
+ return;
13291
+ }
13292
+ ctx.abortSignal?.addEventListener("abort", onCallerAbort, { once: true });
13293
+ const evalPromise = runJavaScript(
13294
+ data.code,
13295
+ data.input,
13296
+ data.timeout_ms,
13297
+ data.memory_limit_mb,
13298
+ controller.signal
13299
+ );
13300
+ const foregroundKey = `js-${String(this.nextForegroundId++)}`;
13301
+ const settle = (finalResult) => {
13302
+ if (settled) {
13303
+ return;
13304
+ }
13305
+ settled = true;
13306
+ ctx.abortSignal?.removeEventListener("abort", onCallerAbort);
13307
+ this.foreground.delete(foregroundKey);
13308
+ resolve12(finalResult);
13309
+ };
13310
+ const backgroundExecution = (reason) => {
13311
+ if (backgrounded || settled || controller.signal.aborted || !manager || !backgroundAvailable) {
13312
+ return null;
13313
+ }
13314
+ backgrounded = true;
13315
+ settled = true;
13316
+ ctx.abortSignal?.removeEventListener("abort", onCallerAbort);
13317
+ this.foreground.delete(foregroundKey);
13318
+ const task = manager.create(
13319
+ backgroundTaskName(data.code),
13320
+ async (backgroundTask) => {
13321
+ const cap = (text) => text.length > BACKGROUND_NOTIFICATION_CHARS ? persistLargeResult(
13322
+ ctx.workDir,
13323
+ ctx.sessionId ?? "",
13324
+ `${backgroundTask.id}-${randomBytes4(4).toString("hex")}`,
13325
+ text
13326
+ ) : text;
13327
+ try {
13328
+ const evaluation = await evalPromise;
13329
+ return cap(formatEvaluation(evaluation));
13330
+ } catch (error) {
13331
+ if (backgroundTask.status === "cancelled") {
13332
+ throw error;
13333
+ }
13334
+ throw new TaskFailure(cap(`Error executing JavaScript: ${asErrorString(error)}`));
13335
+ }
13336
+ },
13337
+ () => {
13338
+ controller.abort();
13339
+ },
13340
+ { originToolCallId: ctx.toolCallId, idPrefix: "js", kind: "js" }
13341
+ );
13342
+ resolve12({
13343
+ output: jsBackgroundMessage(reason, task.id),
13344
+ isError: false
13345
+ });
13346
+ return task.id;
13347
+ };
13348
+ if (backgroundAvailable) {
13349
+ this.foreground.set(foregroundKey, {
13350
+ background: () => backgroundExecution("user") !== null
13351
+ });
13352
+ }
13353
+ evalPromise.then(
13354
+ (evaluation) => {
13355
+ settle({ output: formatEvaluation(evaluation), isError: false });
13356
+ },
13357
+ (error) => {
13358
+ settle({
13359
+ output: `Error executing JavaScript: ${asErrorString(error)}`,
13360
+ isError: true
13361
+ });
13362
+ }
13363
+ );
13364
+ backgroundFn = backgroundExecution;
13365
+ });
13366
+ return {
13367
+ result,
13368
+ background: (reason) => backgroundFn?.(reason) ?? null
13369
+ };
12614
13370
  }
12615
13371
  };
12616
13372
 
12617
13373
  // src/tools/web-fetch.ts
13374
+ var web_fetch_exports = {};
13375
+ __export(web_fetch_exports, {
13376
+ WebFetchTool: () => WebFetchTool
13377
+ });
12618
13378
  var log27 = createChildLogger({ module: "tools" });
12619
13379
  var MAX_CONTENT_BYTES = 10 * 1024 * 1024;
12620
13380
  var FETCH_TIMEOUT_MS = 6e4;
@@ -13615,8 +14375,8 @@ __export(manager_exports3, {
13615
14375
  CodeReviewManager: () => CodeReviewManager,
13616
14376
  createDefaultCodeReviewTeam: () => createDefaultCodeReviewTeam
13617
14377
  });
13618
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync11, existsSync as existsSync18, mkdirSync as mkdirSync12 } from "fs";
13619
- import { join as join21 } from "path";
14378
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync11, existsSync as existsSync18, mkdirSync as mkdirSync13 } from "fs";
14379
+ import { join as join22 } from "path";
13620
14380
  import z17 from "zod";
13621
14381
  var log30 = createChildLogger({ module: "code-review" });
13622
14382
  var CodeReviewMemberSchema = z17.object({
@@ -13639,7 +14399,7 @@ var CodeReviewManager = class {
13639
14399
  workDir;
13640
14400
  constructor(workDir, teamManager) {
13641
14401
  this.workDir = workDir;
13642
- this.configPath = join21(workDir, ".swifty", "code-review-teams.json");
14402
+ this.configPath = join22(workDir, ".swifty", "code-review-teams.json");
13643
14403
  this.teamManager = teamManager;
13644
14404
  this.loadTeams();
13645
14405
  }
@@ -13658,9 +14418,9 @@ var CodeReviewManager = class {
13658
14418
  }
13659
14419
  }
13660
14420
  saveTeams() {
13661
- const dir = join21(this.workDir, ".swifty");
14421
+ const dir = join22(this.workDir, ".swifty");
13662
14422
  if (!existsSync18(dir)) {
13663
- mkdirSync12(dir, { recursive: true });
14423
+ mkdirSync13(dir, { recursive: true });
13664
14424
  }
13665
14425
  const teams = [...this.teams.values()];
13666
14426
  writeFileSync11(this.configPath, JSON.stringify(teams, null, 2));
@@ -13806,8 +14566,8 @@ var usage_tracker_exports = {};
13806
14566
  __export(usage_tracker_exports, {
13807
14567
  CommandUsageTracker: () => CommandUsageTracker
13808
14568
  });
13809
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync13, existsSync as existsSync19 } from "fs";
13810
- import { join as join22, dirname as dirname9 } from "path";
14569
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync14, existsSync as existsSync19 } from "fs";
14570
+ import { join as join23, dirname as dirname9 } from "path";
13811
14571
  import { safeParse as safeParse3, z as z18 } from "zod";
13812
14572
  var log31 = createChildLogger({ module: "commands" });
13813
14573
  var UsageEntrySchema = z18.object({
@@ -13818,10 +14578,10 @@ var CommandUsageTracker = class {
13818
14578
  usage = /* @__PURE__ */ new Map();
13819
14579
  filePath;
13820
14580
  constructor(workDir) {
13821
- const dir = join22(workDir, ".swifty");
13822
- this.filePath = join22(dir, "command_usage.json");
14581
+ const dir = join23(workDir, ".swifty");
14582
+ this.filePath = join23(dir, "command_usage.json");
13823
14583
  if (!existsSync19(dir)) {
13824
- mkdirSync13(dir, { recursive: true });
14584
+ mkdirSync14(dir, { recursive: true });
13825
14585
  }
13826
14586
  this.load();
13827
14587
  }
@@ -13862,7 +14622,7 @@ var CommandUsageTracker = class {
13862
14622
  }
13863
14623
  save() {
13864
14624
  try {
13865
- mkdirSync13(dirname9(this.filePath), { recursive: true });
14625
+ mkdirSync14(dirname9(this.filePath), { recursive: true });
13866
14626
  writeFileSync12(this.filePath, JSON.stringify(Object.fromEntries(this.usage), null, 2));
13867
14627
  } catch (err) {
13868
14628
  log31.error({ err }, "commands operation failed");
@@ -13885,15 +14645,15 @@ __export(history_exports, {
13885
14645
  append: () => append,
13886
14646
  load: () => load
13887
14647
  });
13888
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, existsSync as existsSync20 } from "fs";
13889
- import { join as join23 } from "path";
14648
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, mkdirSync as mkdirSync15, existsSync as existsSync20 } from "fs";
14649
+ import { join as join24 } from "path";
13890
14650
  import { parse as parse8, z as z19 } from "zod";
13891
14651
  var log32 = createChildLogger({ module: "history" });
13892
14652
  var MAX_HISTORY_ENTRIES = 200;
13893
14653
  var FILENAME = "prompt_history.jsonl";
13894
14654
  var JSONLSchema = z19.looseObject({ text: z19.string() });
13895
14655
  function load(dir) {
13896
- const filePath = join23(dir, FILENAME);
14656
+ const filePath = join24(dir, FILENAME);
13897
14657
  if (!existsSync20(filePath)) {
13898
14658
  return [];
13899
14659
  }
@@ -13915,8 +14675,8 @@ function load(dir) {
13915
14675
  }
13916
14676
  }
13917
14677
  function append(dir, text) {
13918
- const filePath = join23(dir, FILENAME);
13919
- mkdirSync14(dir, { recursive: true });
14678
+ const filePath = join24(dir, FILENAME);
14679
+ mkdirSync15(dir, { recursive: true });
13920
14680
  const entries = load(dir);
13921
14681
  if (entries.length === 0 || entries[entries.length - 1] !== text) {
13922
14682
  entries.push(text);
@@ -14132,17 +14892,17 @@ __export(install_tool_exports, {
14132
14892
  });
14133
14893
  import { randomUUID as randomUUID2 } from "crypto";
14134
14894
  import {
14135
- mkdirSync as mkdirSync15,
14895
+ mkdirSync as mkdirSync16,
14136
14896
  writeFileSync as writeFileSync14,
14137
14897
  readFileSync as readFileSync20,
14138
14898
  lstatSync,
14139
14899
  realpathSync,
14140
- openSync as openSync2,
14141
- closeSync as closeSync2,
14900
+ openSync as openSync3,
14901
+ closeSync as closeSync3,
14142
14902
  renameSync as renameSync2,
14143
14903
  rmSync as rmSync2
14144
14904
  } from "fs";
14145
- import { join as join24, resolve as resolve10 } from "path";
14905
+ import { join as join25, resolve as resolve10 } from "path";
14146
14906
  import yaml7 from "js-yaml";
14147
14907
  var log33 = createChildLogger({ module: "skills" });
14148
14908
  var InstallSkillTool = class {
@@ -14234,28 +14994,28 @@ ${parsed.body}
14234
14994
  ctx.abortSignal?.throwIfAborted();
14235
14995
  let dir = realpathSync(this.workDir);
14236
14996
  for (const segment of [".agents", "skills", name]) {
14237
- dir = join24(dir, segment);
14997
+ dir = join25(dir, segment);
14238
14998
  const stat4 = lstatSync(dir, { throwIfNoEntry: false });
14239
14999
  if (stat4) {
14240
15000
  if (stat4.isSymbolicLink() || !stat4.isDirectory()) {
14241
15001
  throw new Error(`Installation directory must be a real directory: ${dir}`);
14242
15002
  }
14243
15003
  } else {
14244
- mkdirSync15(dir);
15004
+ mkdirSync16(dir);
14245
15005
  }
14246
15006
  }
14247
- const destination = join24(dir, "SKILL.md");
15007
+ const destination = join25(dir, "SKILL.md");
14248
15008
  const stat3 = lstatSync(destination, { throwIfNoEntry: false });
14249
15009
  if (stat3 && (stat3.isSymbolicLink() || !stat3.isFile())) {
14250
15010
  throw new Error(`Installation target must be a regular file: ${destination}`);
14251
15011
  }
14252
- const temporary = join24(dir, `.SKILL-${randomUUID2()}.tmp`);
14253
- const fd = openSync2(temporary, "wx", stat3?.mode ?? 438);
15012
+ const temporary = join25(dir, `.SKILL-${randomUUID2()}.tmp`);
15013
+ const fd = openSync3(temporary, "wx", stat3?.mode ?? 438);
14254
15014
  try {
14255
15015
  try {
14256
15016
  writeFileSync14(fd, content, "utf-8");
14257
15017
  } finally {
14258
- closeSync2(fd);
15018
+ closeSync3(fd);
14259
15019
  }
14260
15020
  renameSync2(temporary, destination);
14261
15021
  } finally {
@@ -14298,6 +15058,7 @@ __export(tools_exports3, {
14298
15058
  Diff: () => diff_exports,
14299
15059
  EditFile: () => edit_file_exports,
14300
15060
  EnterWorktree: () => enter_worktree_exports,
15061
+ ExitCodeHints: () => exit_code_hints_exports,
14301
15062
  ExitPlanMode: () => exit_plan_mode_exports,
14302
15063
  ExitWorktree: () => exit_worktree_exports,
14303
15064
  FileMutationQueue: () => file_mutation_queue_exports,
@@ -14310,11 +15071,13 @@ __export(tools_exports3, {
14310
15071
  PowerShell: () => powershell_exports,
14311
15072
  ReadFile: () => read_file_exports,
14312
15073
  Registry: () => registry_exports,
15074
+ ShellBackground: () => shell_background_exports,
14313
15075
  ShellOutput: () => shell_output_exports,
14314
15076
  Snippets: () => snippets_exports,
14315
15077
  SyntheticOutput: () => synthetic_output_exports,
14316
15078
  ToolSearch: () => tool_search_exports,
14317
15079
  Types: () => types_exports,
15080
+ WebFetchTool: () => web_fetch_exports,
14318
15081
  WriteFile: () => write_file_exports
14319
15082
  });
14320
15083
 
@@ -14600,7 +15363,7 @@ __export(lockfile_exports, {
14600
15363
  });
14601
15364
  import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
14602
15365
  import { homedir as homedir10 } from "os";
14603
- import { join as join25, resolve as resolve11, sep as sep2 } from "path";
15366
+ import { join as join26, resolve as resolve11, sep as sep2 } from "path";
14604
15367
  import { z as z21 } from "zod";
14605
15368
  var log34 = createChildLogger({ module: "vscode" });
14606
15369
  var LockfileSchema = z21.object({
@@ -14611,8 +15374,8 @@ var LockfileSchema = z21.object({
14611
15374
  authToken: z21.string().optional()
14612
15375
  });
14613
15376
  function ideLockDir() {
14614
- const configDir = process.env.CLAUDE_CONFIG_DIR ?? join25(homedir10(), ".claude");
14615
- return join25(configDir, "ide");
15377
+ const configDir = process.env.CLAUDE_CONFIG_DIR ?? join26(homedir10(), ".claude");
15378
+ return join26(configDir, "ide");
14616
15379
  }
14617
15380
  function cwdInWorkspace(cwd2, workspaceFolders) {
14618
15381
  const normalizedCwd = cwd2.normalize("NFC");
@@ -14630,7 +15393,7 @@ async function readLockfile(dir, filename) {
14630
15393
  return null;
14631
15394
  }
14632
15395
  try {
14633
- const content = await readFile5(join25(dir, filename), "utf-8");
15396
+ const content = await readFile5(join26(dir, filename), "utf-8");
14634
15397
  const parsed = LockfileSchema.parse(JSON.parse(content));
14635
15398
  if (parsed.transport !== "ws") {
14636
15399
  return null;
@@ -14975,6 +15738,7 @@ async function runPrintMode(args) {
14975
15738
  registry.register(new ToolSearchTool(registry));
14976
15739
  const teamManager = new TeamManager(workDir);
14977
15740
  const backgroundTaskManager = new TaskManager();
15741
+ attachBackgroundTaskManager(registry, backgroundTaskManager);
14978
15742
  registry.register(new TeamCreateTool(teamManager));
14979
15743
  registry.register(new SendMessageTool(teamManager));
14980
15744
  registry.register(new TeamDeleteTool(teamManager));
@@ -15036,7 +15800,8 @@ async function runPrintMode(args) {
15036
15800
  onEvent,
15037
15801
  void 0,
15038
15802
  teamChecker,
15039
- { abortSignal }
15803
+ // Teammates stay purely foreground: see SubagentRunOptions.backgroundTasks.
15804
+ { abortSignal, backgroundTasks: false }
15040
15805
  ),
15041
15806
  provider.base_url
15042
15807
  );
@@ -15124,7 +15889,7 @@ Error: ${event.error.message}`);
15124
15889
  break;
15125
15890
  }
15126
15891
  }
15127
- await backgroundTaskManager.waitAll();
15892
+ await backgroundTaskManager.waitAll((task) => (task.kind ?? "agent") === "agent");
15128
15893
  const backgroundNotifications = backgroundTaskManager.drainNotifications();
15129
15894
  const durationMs = Date.now() - startTime;
15130
15895
  if (args.outputFormat === "text" && resultText && !resultText.endsWith("\n")) {
@@ -15217,13 +15982,13 @@ __export(recover_exports, {
15217
15982
  recordExit: () => recordExit,
15218
15983
  recover: () => recover
15219
15984
  });
15220
- import { appendFileSync, mkdirSync as mkdirSync16 } from "fs";
15221
- import { join as join26 } from "path";
15985
+ import { appendFileSync, mkdirSync as mkdirSync17 } from "fs";
15986
+ import { join as join27 } from "path";
15222
15987
  var LOG_DIR = ".swifty";
15223
- var LOG_PATH = join26(LOG_DIR, "crash.log");
15988
+ var LOG_PATH = join27(LOG_DIR, "crash.log");
15224
15989
  function record(text) {
15225
15990
  try {
15226
- mkdirSync16(LOG_DIR, { recursive: true });
15991
+ mkdirSync17(LOG_DIR, { recursive: true });
15227
15992
  appendFileSync(LOG_PATH, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${text}
15228
15993
  `, "utf8");
15229
15994
  } catch {
@@ -15303,7 +16068,7 @@ __export(teammate_exports, {
15303
16068
  parseTeammateFlags: () => parseTeammateFlags,
15304
16069
  runTeammate: () => runTeammate
15305
16070
  });
15306
- import { basename as basename6, dirname as dirname10, join as join27 } from "path";
16071
+ import { basename as basename6, dirname as dirname10, join as join28 } from "path";
15307
16072
  function parseTeammateFlags(args) {
15308
16073
  if (!args.includes("--teammate")) {
15309
16074
  return null;
@@ -15399,7 +16164,7 @@ async function runTeammate(args) {
15399
16164
  initLogger({
15400
16165
  sessionId: `teammate-${safeMemberName}-${Date.now().toString(36)}`,
15401
16166
  mode: "teammate",
15402
- logDir: join27(dirname10(args.teamDir), "logs"),
16167
+ logDir: join28(dirname10(args.teamDir), "logs"),
15403
16168
  skipCleanup: true
15404
16169
  });
15405
16170
  process.on("exit", closeLogger);