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/README.md +35 -1
- package/package.json +7 -4
- package/src/index.ts +4 -0
- package/src/process.ts +68 -17
- package/src/registry.ts +11 -0
- package/src/remote-task-preset.ts +53 -571
- package/src/runtime.ts +35 -2
- package/src/sandbox.ts +285 -0
- package/src/shared-sandbox-core.ts +462 -0
- package/src/shared-ssh-core/index.ts +902 -0
- package/src/tools.ts +29 -13
- package/src/types.ts +13 -19
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
|
|
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
|
|
129
|
+
const launched = reportLaunch(() => spawnTask(pi, params, ctx.cwd, activeSession, getActiveSession));
|
|
129
130
|
refreshBackgroundTasksNavigator(ctx);
|
|
130
|
-
return text(
|
|
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
|
|
142
|
+
const launched = reportLaunch(() => startWatchTask(pi, params, ctx.cwd, activeSession, getActiveSession));
|
|
142
143
|
refreshBackgroundTasksNavigator(ctx);
|
|
143
|
-
return text(
|
|
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
|
|
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,
|
|
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,
|
|
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=
|
|
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 ??
|
|
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;
|