pi-better-background-tasks 0.2.4 → 0.2.6

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/src/tools.ts CHANGED
@@ -5,6 +5,7 @@ import { refreshBackgroundTasksNavigator } from "./navigator-provider.js";
5
5
  import { cancelCallbackBatch } from "./shared-callback-batcher.js";
6
6
  import { listMetas, readMeta, writeMeta } from "./registry.js";
7
7
  import { resumeRunningTask, spawnTask, startWatchTask, stopTask } from "./runtime.js";
8
+ import { ForegroundSandboxBlockedError } from "./sandbox.js";
8
9
  import type { BackgroundTaskCallbackOrigin, BackgroundTaskMeta } from "./types.js";
9
10
  import { isTerminalStatus } from "./types.js";
10
11
 
@@ -68,7 +69,7 @@ const ListParams = Type.Object({
68
69
  });
69
70
  const LogParams = Type.Object({
70
71
  id: Type.String({ description: "Background task id." }),
71
- tail_lines: Type.Optional(Type.Number({ description: "Number of trailing lines. Default 20 for compact model ingestion. Set <=0 only when the full log is explicitly required." })),
72
+ tail_lines: Type.Optional(Type.Number({ description: "Number of trailing lines. Default 5 for compact model ingestion. Set <=0 only when the full log is explicitly required." })),
72
73
  });
73
74
 
74
75
  const ActionParams = Type.Object({
@@ -121,26 +122,26 @@ export function registerTools(pi: ExtensionAPI): void {
121
122
  pi.registerTool({
122
123
  name: "bg_task_spawn",
123
124
  label: "BG Spawn",
124
- description: "Start a long-running background process and return immediately with its task id. For remote work, prefer structured ssh: pass ssh:{host,user} and put the remote command in command; spawn defaults to a remote tmux session with durable local logs and real remote stop. If tmux is missing, the preset attempts to install tmux non-interactively and fails closed with operator guidance when setup cannot proceed. Explicit remote.session=direct skips tmux, but direct mode has weaker stop semantics and may leave the remote process running. Never wait or poll in the foreground.",
125
+ description: "Start a long-running background process and return immediately with its task id. For remote work, prefer structured ssh: pass ssh:{host,user} and put the remote command in command; spawn defaults to a remote tmux session with durable local logs and real remote stop. For short synchronous remote commands that should return output now, use remote_bash from pi-better-ssh. If tmux is missing, the preset attempts to install tmux non-interactively and fails closed with operator guidance when setup cannot proceed. Explicit remote.session=direct skips tmux, but direct mode has weaker stop semantics and may leave the remote process running. Never wait or poll in the foreground.",
125
126
  parameters: SpawnParams,
126
127
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
127
128
  activeSession = getCallbackOrigin(ctx);
128
- const meta = spawnTask(pi, params, ctx.cwd, activeSession, getActiveSession);
129
+ const launched = reportLaunch(() => spawnTask(pi, params, ctx.cwd, activeSession, getActiveSession));
129
130
  refreshBackgroundTasksNavigator(ctx);
130
- return text(formatLaunch(meta));
131
+ return text(launched);
131
132
  },
132
133
  });
133
134
 
134
135
  pi.registerTool({
135
136
  name: "bg_task_watch",
136
137
  label: "BG Watch",
137
- description: "Poll a command in the background until success_when, failure_when, or timeout matches. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command; each interval opens a direct one-shot SSH poll without tmux installation. Returns immediately with its task id. Default timeout 900 seconds; pass timeout_seconds:0 to disable.",
138
+ description: "Poll a command in the background until success_when, failure_when, or timeout matches. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command; each interval opens a direct one-shot SSH poll without tmux installation. For short synchronous remote commands that should return output now, use remote_bash from pi-better-ssh. Returns immediately with its task id. Default timeout 900 seconds; pass timeout_seconds:0 to disable.",
138
139
  parameters: WatchParams,
139
140
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
140
141
  activeSession = getCallbackOrigin(ctx);
141
- const meta = startWatchTask(pi, params, ctx.cwd, activeSession, getActiveSession);
142
+ const launched = reportLaunch(() => startWatchTask(pi, params, ctx.cwd, activeSession, getActiveSession));
142
143
  refreshBackgroundTasksNavigator(ctx);
143
- return text(formatLaunch(meta));
144
+ return text(launched);
144
145
  },
145
146
  });
146
147
 
@@ -168,7 +169,7 @@ export function registerTools(pi: ExtensionAPI): void {
168
169
  pi.registerTool({
169
170
  name: "bg_task_log",
170
171
  label: "BG Log",
171
- description: "Read a background task log. Default output is a compact 20-line terminal-aware tail for model ingestion. Pass tail_lines for a bounded tail; tail_lines:0 returns the retained raw log, capped at 512 KiB for safe recovery. Nonblocking.",
172
+ description: "Read a background task log. Default output is a compact 5-line terminal-aware tail for model ingestion. Pass tail_lines for a bounded tail; tail_lines:0 returns the retained raw log, capped at 512 KiB for safe recovery. Nonblocking.",
172
173
  parameters: LogParams,
173
174
  renderResult(result: unknown, options: unknown, theme: unknown) {
174
175
  return renderBackgroundTaskLogDisplay(result, options, theme);
@@ -194,7 +195,7 @@ export function registerTools(pi: ExtensionAPI): void {
194
195
  pi.registerTool({
195
196
  name: "bg_task",
196
197
  label: "BG Task",
197
- description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command. SSH spawn defaults to durable tmux; SSH watches use direct one-shot polls without tmux installation; remote.session=direct is a weaker-stop spawn escape hatch. Spawn/watch return immediately; do not poll in foreground. For action:status, default compact output and use verbose:true only for full metadata. For action:log, default compact tail and use tail_lines:0 only for explicit full logs.",
198
+ description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command. For short synchronous remote commands that should return output now, use remote_bash from pi-better-ssh. SSH spawn defaults to durable tmux; SSH watches use direct one-shot polls without tmux installation; remote.session=direct is a weaker-stop spawn escape hatch. Spawn/watch return immediately; do not poll in foreground. For action:status, default compact output and use verbose:true only for full metadata. For action:log, default compact tail and use tail_lines:0 only for explicit full logs.",
198
199
  parameters: ActionParams,
199
200
  renderResult(result: unknown, options: unknown, theme: unknown) {
200
201
  return renderBackgroundTaskLogDisplay(result, options, theme);
@@ -246,10 +247,10 @@ async function runAction(
246
247
  ): Promise<string> {
247
248
  switch (params.action) {
248
249
  case "spawn":
249
- return withNavigatorRefresh(ctx, formatLaunch(spawnTask(pi, params, ctx.cwd, callbackOrigin, getActiveSession)));
250
+ return withNavigatorRefresh(ctx, reportLaunch(() => spawnTask(pi, params, ctx.cwd, callbackOrigin, getActiveSession)));
250
251
  case "watch":
251
252
  if (!params.success_when) return "Invalid parameters: watch requires success_when.";
252
- return withNavigatorRefresh(ctx, formatLaunch(startWatchTask(pi, params as never, ctx.cwd, callbackOrigin, getActiveSession)));
253
+ return withNavigatorRefresh(ctx, reportLaunch(() => startWatchTask(pi, params as never, ctx.cwd, callbackOrigin, getActiveSession)));
253
254
  case "list":
254
255
  return formatList(resolveList(params.status as string[] | undefined, params.limit as number | undefined));
255
256
  case "status":
@@ -268,6 +269,21 @@ async function runAction(
268
269
  }
269
270
  }
270
271
 
272
+ /**
273
+ * Report a launch, or why the foreground sandbox refused it.
274
+ *
275
+ * A blocked launch is an operator-facing answer, not a tool crash: the task was
276
+ * never started, and nothing about it is retried unconfined.
277
+ */
278
+ function reportLaunch(launch: () => BackgroundTaskMeta): string {
279
+ try {
280
+ return formatLaunch(launch());
281
+ } catch (error) {
282
+ if (error instanceof ForegroundSandboxBlockedError) return error.message;
283
+ throw error;
284
+ }
285
+ }
286
+
271
287
  function withNavigatorRefresh(ctx: ExtensionContext, result: string): string {
272
288
  refreshBackgroundTasksNavigator(ctx);
273
289
  return result;
@@ -338,7 +354,7 @@ function formatCompactStatus(meta: BackgroundTaskMeta): string {
338
354
  if (meta.lastState !== undefined) lines.push(`last state: ${oneLine(meta.lastState, 800)}`);
339
355
  if (meta.logDiscardedBytes) lines.push(`log retention: ${meta.logDiscardedBytes} bytes discarded in ${meta.logRetentionEvents ?? 1} compaction(s).`);
340
356
  lines.push(`log: ${meta.logPath}`);
341
- lines.push(`For full metadata use bg_task_status id=${meta.id} verbose=true. For logs use bg_task_log id=${meta.id} tail_lines=20, or tail_lines=0 for the retained raw log.`);
357
+ lines.push(`For full metadata use bg_task_status id=${meta.id} verbose=true. For logs use bg_task_log id=${meta.id} tail_lines=5, or tail_lines=0 for the retained raw log.`);
342
358
  return lines.join("\n");
343
359
  }
344
360
 
@@ -360,7 +376,7 @@ function oneLine(value: unknown, maxLength: number): string {
360
376
  function formatLog(id: string, tailLines?: number): string {
361
377
  const meta = readMeta(id);
362
378
  if (!meta) return `No background task found for id ${id}.`;
363
- const log = readLog(meta.logPath, tailLines ?? 20);
379
+ const log = readLog(meta.logPath, tailLines ?? 5);
364
380
  const prefix = log.truncated ? `[showing tail of ${meta.logPath}]\n` : `[${meta.logPath}]\n`;
365
381
  return prefix + (log.text || "(log is empty)");
366
382
  }
package/src/types.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import type { ResolvedSshIdentity, SshConnectionParams } from "./shared-ssh-core/index.js";
2
+
3
+ export type { ResolvedSshIdentity, SshConnectionParams };
4
+
1
5
  export type BackgroundTaskStatus =
2
6
  | "running"
3
7
  | "succeeded"
@@ -32,31 +36,12 @@ export interface CommandResult {
32
36
  timedOut?: boolean;
33
37
  }
34
38
 
35
- export interface SshConnectionParams {
36
- host: string;
37
- user?: string;
38
- port?: number;
39
- identity_file?: string;
40
- jump?: string;
41
- options?: Record<string, string>;
42
- }
43
-
44
39
  export interface RemoteTaskParams {
45
40
  session?: "tmux" | "direct";
46
41
  install_tmux?: boolean;
47
42
  workdir?: string;
48
43
  }
49
44
 
50
- export interface ResolvedSshIdentity {
51
- host: string;
52
- user?: string;
53
- port?: number;
54
- identityFile?: string;
55
- jump?: string;
56
- options?: Record<string, string>;
57
- target: string;
58
- }
59
-
60
45
  export interface ResolvedRemoteTaskMetadata {
61
46
  command: string;
62
47
  session?: "tmux" | "direct";
@@ -105,6 +90,15 @@ export interface BackgroundTaskMeta {
105
90
  shell?: boolean;
106
91
  cwd: string;
107
92
  env?: Record<string, string>;
93
+ /**
94
+ * The executable and argv this task was actually launched with, when that
95
+ * differs from `command`/`argv` above — today, an OS write-sandbox wrapper
96
+ * captured from the foreground policy at launch. Re-running a watch poll uses
97
+ * it verbatim, which is how a running task keeps the policy it started under
98
+ * even after the foreground policy changes. `command`/`argv` stay the operator's
99
+ * own request, so status, navigator, and goal surfaces read unchanged.
100
+ */
101
+ launchArgv?: string[];
108
102
  maxLogBytes?: number;
109
103
  logDiscardedBytes?: number;
110
104
  logRetentionEvents?: number;