pi-simple-background-tasks 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORK-CHANGES.md +13 -6
- package/README.md +20 -10
- package/package.json +4 -3
- package/src/core/common.ts +26 -207
- package/src/core/detached-spawn.ts +317 -0
- package/src/core/registry.ts +586 -1203
- package/src/core/task-cleanup.ts +6 -0
- package/src/core/windows-taskkill.ts +4 -11
- package/src/core/windows-wmi-spawn.ts +192 -0
- package/src/extension.ts +123 -225
- package/src/ui/background-tasks-manager.ts +156 -35
- package/src/core/reload-shell-owner.ts +0 -1512
package/src/extension.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { realpathSync } from 'node:fs';
|
|
3
1
|
import type {
|
|
4
2
|
ExtensionAPI,
|
|
5
3
|
ExtensionCommandContext,
|
|
@@ -9,7 +7,13 @@ import type {
|
|
|
9
7
|
ToolRenderResultOptions,
|
|
10
8
|
} from '@earendil-works/pi-coding-agent';
|
|
11
9
|
import { formatSize } from '@earendil-works/pi-coding-agent';
|
|
12
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type KeyId,
|
|
12
|
+
type OverlayHandle,
|
|
13
|
+
type OverlayOptions,
|
|
14
|
+
Text,
|
|
15
|
+
type TUI,
|
|
16
|
+
} from '@earendil-works/pi-tui';
|
|
13
17
|
import { type Static, Type } from 'typebox';
|
|
14
18
|
import {
|
|
15
19
|
type BgKillDetails,
|
|
@@ -26,10 +30,6 @@ import {
|
|
|
26
30
|
normalizeMaxBytes,
|
|
27
31
|
normalizeTaskName,
|
|
28
32
|
parseBgCommandArgs,
|
|
29
|
-
type ReloadShellActivationClaimV1,
|
|
30
|
-
type ReloadShellActivationLeaseV1,
|
|
31
|
-
type ReloadShellIdentityV1,
|
|
32
|
-
ReloadSurvivalError,
|
|
33
33
|
type StartTaskOptions,
|
|
34
34
|
taskDisplayName,
|
|
35
35
|
truncateChars,
|
|
@@ -41,13 +41,14 @@ import {
|
|
|
41
41
|
} from './core/extension-api.js';
|
|
42
42
|
import { LazyModule, SynchronousActivationCloseFence } from './core/lazy-module.js';
|
|
43
43
|
import { type BackgroundTaskContext, BackgroundTaskRegistry } from './core/registry.js';
|
|
44
|
-
import {
|
|
45
|
-
getProcessReloadShellOwnerV1,
|
|
46
|
-
makeReloadShellIdentity,
|
|
47
|
-
} from './core/reload-shell-owner.js';
|
|
48
44
|
import { createShellPolicyGuidanceHandler, initializeShellPolicy } from './core/shell-policy.js';
|
|
49
45
|
import type { CleanupScope, CleanupSummary } from './core/task-cleanup.js';
|
|
50
|
-
import
|
|
46
|
+
import {
|
|
47
|
+
type BackgroundTaskForUi,
|
|
48
|
+
type BackgroundTasksManager,
|
|
49
|
+
FULLSCREEN_MARGINS,
|
|
50
|
+
type TaskManagerOptions,
|
|
51
|
+
} from './ui/background-tasks-manager.js';
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Project-local Pi background task manager.
|
|
@@ -55,9 +56,11 @@ import type { BackgroundTaskForUi, TaskManagerResult } from './ui/background-tas
|
|
|
55
56
|
* Scope:
|
|
56
57
|
* - Explicit background shell jobs only: /bg and bg_run spawn commands directly.
|
|
57
58
|
* - No Ctrl+B support for backgrounding an already-running built-in bash tool.
|
|
58
|
-
* -
|
|
59
|
-
*
|
|
60
|
-
* -
|
|
59
|
+
* - Tasks always survive /reload and Pi restarts: every task spawns detached,
|
|
60
|
+
* its ownership facts live in its on-disk metadata, and each activation
|
|
61
|
+
* adopts still-running records from disk. Pi session shutdown kills all
|
|
62
|
+
* running tasks, so exit with care.
|
|
63
|
+
* - No flag negotiates any of this: there is exactly one launch path.
|
|
61
64
|
*/
|
|
62
65
|
|
|
63
66
|
const COMMAND_PREVIEW_CHARS = 90;
|
|
@@ -77,6 +80,7 @@ interface BgToolArgumentRecord {
|
|
|
77
80
|
readonly timeoutSeconds?: unknown;
|
|
78
81
|
readonly notifyOnCompletion?: unknown;
|
|
79
82
|
readonly triggerOnCompletion?: unknown;
|
|
83
|
+
/** Legacy argument, silently ignored: survival is now unconditional. */
|
|
80
84
|
readonly surviveReload?: unknown;
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -108,12 +112,6 @@ const BgRunParams = Type.Object({
|
|
|
108
112
|
'Whether that notification should automatically trigger a follow-up agent turn. Default: true for bg_run; requires notifyOnCompletion.',
|
|
109
113
|
}),
|
|
110
114
|
),
|
|
111
|
-
surviveReload: Type.Optional(
|
|
112
|
-
Type.Boolean({
|
|
113
|
-
description:
|
|
114
|
-
'Opt in to retaining this exact shell execution across a real same-process Pi reload. Default: false.',
|
|
115
|
-
}),
|
|
116
|
-
),
|
|
117
115
|
});
|
|
118
116
|
|
|
119
117
|
const BgStatusParams = Type.Object({
|
|
@@ -202,15 +200,8 @@ function formatCleanMessage(summary: CleanupSummary): string {
|
|
|
202
200
|
export default async function backgroundTasksExtension(pi: ExtensionAPI): Promise<void> {
|
|
203
201
|
const config = resolveBackgroundTasksConfig(process.cwd());
|
|
204
202
|
const shellPolicy = initializeShellPolicy();
|
|
205
|
-
const reloadShellOwner = getProcessReloadShellOwnerV1();
|
|
206
203
|
pi.on('before_agent_start', createShellPolicyGuidanceHandler(shellPolicy));
|
|
207
|
-
let currentRegistryCtx: BackgroundTaskContext | undefined;
|
|
208
|
-
let activationLease: ReloadShellActivationLeaseV1 | undefined;
|
|
209
|
-
let activationIdentity: ReloadShellIdentityV1 | undefined;
|
|
210
|
-
let pendingActivationClaim: ReloadShellActivationClaimV1 | undefined;
|
|
211
204
|
let disposed = false;
|
|
212
|
-
let shutdownCleanupStarted = false;
|
|
213
|
-
let reloadHandoffFailed = false;
|
|
214
205
|
let shutdownReason = 'shutdown';
|
|
215
206
|
const activationCloseFence = new SynchronousActivationCloseFence();
|
|
216
207
|
const taskManagerLoader = new LazyModule(() => import('./ui/background-tasks-manager.js'));
|
|
@@ -229,59 +220,22 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
229
220
|
},
|
|
230
221
|
shellPolicy,
|
|
231
222
|
tasksDir: config.tasksDir,
|
|
232
|
-
reloadShellOwner,
|
|
233
223
|
});
|
|
234
224
|
const eventService: BackgroundTaskExtensionService = installBackgroundTaskExtensionApi({
|
|
235
225
|
events: pi.events,
|
|
236
226
|
registry,
|
|
237
227
|
});
|
|
238
228
|
|
|
239
|
-
const beginSessionShutdown = (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (pendingActivationClaim !== undefined) {
|
|
245
|
-
const claim = pendingActivationClaim;
|
|
246
|
-
pendingActivationClaim = undefined;
|
|
247
|
-
registry.abortReloadActivation(claim);
|
|
248
|
-
try {
|
|
249
|
-
reloadShellOwner.abortActivation(
|
|
250
|
-
claim,
|
|
251
|
-
new ReloadSurvivalError(
|
|
252
|
-
'pi_bg_reload_owner_stale_claim',
|
|
253
|
-
'activation shut down while its reload claim was staging',
|
|
254
|
-
),
|
|
255
|
-
);
|
|
256
|
-
} catch (error) {
|
|
257
|
-
handoffError = error;
|
|
258
|
-
}
|
|
259
|
-
} else if (reason === 'reload' && activationLease !== undefined) {
|
|
260
|
-
const lease = activationLease;
|
|
261
|
-
try {
|
|
262
|
-
registry.prepareReloadHandoff(lease);
|
|
263
|
-
activationLease = undefined;
|
|
264
|
-
activationIdentity = undefined;
|
|
265
|
-
} catch (error) {
|
|
266
|
-
reloadHandoffFailed = true;
|
|
267
|
-
handoffError = error;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
registry.setShuttingDown(true);
|
|
271
|
-
eventService.close();
|
|
272
|
-
}
|
|
273
|
-
// Teardown remains active on repeated calls so even a handle assigned by a
|
|
274
|
-
// racing continuation is still disposed rather than hidden by idempotence.
|
|
275
|
-
currentRegistryCtx = undefined;
|
|
276
|
-
if (handoffError !== undefined) throw handoffError;
|
|
229
|
+
const beginSessionShutdown = (): void => {
|
|
230
|
+
if (disposed) return;
|
|
231
|
+
disposed = true;
|
|
232
|
+
registry.setShuttingDown(true);
|
|
233
|
+
eventService.close();
|
|
277
234
|
};
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
// asynchronous cleanup. The core callback performs an eligible reload
|
|
281
|
-
// handoff first; every lazy lane then closes on the same call stack before
|
|
282
|
-
// Pi can await any later cleanup handler.
|
|
235
|
+
// Join the one synchronous all-lane barrier: every lazy lane closes on the
|
|
236
|
+
// same call stack before Pi can await any later cleanup handler.
|
|
283
237
|
activationCloseFence.add(() => {
|
|
284
|
-
beginSessionShutdown(
|
|
238
|
+
beginSessionShutdown();
|
|
285
239
|
});
|
|
286
240
|
activationCloseFence.add(() => {
|
|
287
241
|
taskManagerLoader.close();
|
|
@@ -291,72 +245,21 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
291
245
|
activationCloseFence.close();
|
|
292
246
|
});
|
|
293
247
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
248
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
249
|
+
// A Pi reload binds a fresh extension instance. Adoptions run before any
|
|
250
|
+
// task can start so notifications continue for tasks running across the
|
|
251
|
+
// reload, and the poller takes over ownership detection from disk records.
|
|
252
|
+
if (disposed) return;
|
|
253
|
+
registry.setShuttingDown(false);
|
|
254
|
+
await registry.ensureRuntimeDir(registryContext(ctx));
|
|
297
255
|
if (disposed) return;
|
|
298
|
-
const nextRegistryCtx = registryContext(ctx);
|
|
299
|
-
const identity = makeReloadShellIdentity(
|
|
300
|
-
nextRegistryCtx.sessionId ?? '',
|
|
301
|
-
realpathSync(ctx.cwd),
|
|
302
|
-
);
|
|
303
|
-
if (activationLease !== undefined && registry.hasCurrentReloadLease()) {
|
|
304
|
-
if (
|
|
305
|
-
activationIdentity?.hostPid !== identity.hostPid ||
|
|
306
|
-
activationIdentity.sessionId !== identity.sessionId ||
|
|
307
|
-
activationIdentity.cwdRealpath !== identity.cwdRealpath
|
|
308
|
-
) {
|
|
309
|
-
throw new ReloadSurvivalError(
|
|
310
|
-
'pi_bg_reload_owner_activation_conflict',
|
|
311
|
-
'a repeated session_start changed the bound reload owner identity without shutdown',
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
currentRegistryCtx = nextRegistryCtx;
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
const activationNonce = randomBytes(16).toString('hex');
|
|
318
|
-
const claim = reloadShellOwner.beginActivation(identity, event.reason, activationNonce);
|
|
319
|
-
pendingActivationClaim = claim;
|
|
320
256
|
try {
|
|
321
|
-
|
|
322
|
-
if (pendingActivationClaim !== claim) {
|
|
323
|
-
// The synchronous shutdown barrier already aborted this claim and
|
|
324
|
-
// detached staged records while the durable import awaited.
|
|
325
|
-
registry.abortReloadActivation(claim);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (disposed) {
|
|
329
|
-
registry.abortReloadActivation(claim);
|
|
330
|
-
pendingActivationClaim = undefined;
|
|
331
|
-
reloadShellOwner.abortActivation(
|
|
332
|
-
claim,
|
|
333
|
-
new ReloadSurvivalError(
|
|
334
|
-
'pi_bg_reload_owner_stale_claim',
|
|
335
|
-
'activation was disposed before reload claim commit',
|
|
336
|
-
),
|
|
337
|
-
);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
const lease = reloadShellOwner.commitActivation(claim, adapter);
|
|
341
|
-
pendingActivationClaim = undefined;
|
|
342
|
-
activationLease = lease;
|
|
343
|
-
activationIdentity = identity;
|
|
344
|
-
currentRegistryCtx = nextRegistryCtx;
|
|
257
|
+
await registry.reconcileTasksFromDisk();
|
|
345
258
|
} catch (error) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
} catch (abortError) {
|
|
351
|
-
if (
|
|
352
|
-
typeof abortError !== 'object' ||
|
|
353
|
-
abortError === null ||
|
|
354
|
-
Reflect.get(abortError, 'code') !== 'pi_bg_reload_owner_stale_claim'
|
|
355
|
-
) {
|
|
356
|
-
throw new AggregateError([error, abortError], 'Reload activation claim and abort failed');
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
throw error;
|
|
259
|
+
console.error(
|
|
260
|
+
'[background-tasks] failed to reconcile background tasks from disk:',
|
|
261
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
262
|
+
);
|
|
360
263
|
}
|
|
361
264
|
});
|
|
362
265
|
|
|
@@ -365,9 +268,7 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
365
268
|
command: string,
|
|
366
269
|
options: StartTaskOptions = {},
|
|
367
270
|
): Promise<BgTask> {
|
|
368
|
-
|
|
369
|
-
currentRegistryCtx = nextRegistryCtx;
|
|
370
|
-
return registry.startTask(nextRegistryCtx, command, options);
|
|
271
|
+
return registry.startTask(registryContext(ctx), command, options);
|
|
371
272
|
}
|
|
372
273
|
|
|
373
274
|
async function openTaskManager(
|
|
@@ -381,55 +282,87 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
381
282
|
);
|
|
382
283
|
return;
|
|
383
284
|
}
|
|
285
|
+
const normalOptions: OverlayOptions = {
|
|
286
|
+
anchor: 'top-right',
|
|
287
|
+
width: 96,
|
|
288
|
+
minWidth: 64,
|
|
289
|
+
margin: { top: 1, left: 1, right: 1 },
|
|
290
|
+
};
|
|
291
|
+
// Pi resolves overlayOptions once (at overlay creation), so a fullscreen toggle
|
|
292
|
+
// re-creates the overlay with the same manager instance and fresh options.
|
|
293
|
+
const fullscreenOptions = (): OverlayOptions => ({
|
|
294
|
+
anchor: 'top-left',
|
|
295
|
+
width: '100%',
|
|
296
|
+
maxHeight: Math.max(
|
|
297
|
+
1,
|
|
298
|
+
process.stdout.rows - FULLSCREEN_MARGINS.top - FULLSCREEN_MARGINS.bottom,
|
|
299
|
+
),
|
|
300
|
+
margin: FULLSCREEN_MARGINS,
|
|
301
|
+
});
|
|
302
|
+
|
|
384
303
|
const { BackgroundTasksManager } = await taskManagerLoader.run((runtime) => runtime);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
showOutputPath: (task: BackgroundTaskForUi) => {
|
|
410
|
-
ctx.ui.notify(
|
|
411
|
-
`Output path for ${taskDisplayName(task)} (${task.id}):\n${task.outputPath}`,
|
|
412
|
-
'info',
|
|
413
|
-
);
|
|
414
|
-
},
|
|
415
|
-
deleteTask: async (task: BackgroundTaskForUi) => {
|
|
416
|
-
await registry.cleanupFinishedTasks('one', task.id);
|
|
417
|
-
},
|
|
304
|
+
let manager: BackgroundTasksManager | null = null;
|
|
305
|
+
let currentHandle: OverlayHandle | null = null;
|
|
306
|
+
let rawTui: TUI | null = null;
|
|
307
|
+
|
|
308
|
+
const rePresent = (options: OverlayOptions) => {
|
|
309
|
+
if (!manager || !rawTui) return;
|
|
310
|
+
currentHandle?.hide();
|
|
311
|
+
currentHandle = rawTui.showOverlay(manager, options);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const managerOptions: TaskManagerOptions = {
|
|
315
|
+
getTasks: () => registry.allTasks(),
|
|
316
|
+
stopTask: async (task: BackgroundTaskForUi) => {
|
|
317
|
+
await registry.stopTask(registry.resolveTask(task.id), 'user');
|
|
318
|
+
},
|
|
319
|
+
stopAllRunning: async () => {
|
|
320
|
+
const result = await registry.stopAllRunning('user');
|
|
321
|
+
return result;
|
|
322
|
+
},
|
|
323
|
+
rerunTask: async (task: BackgroundTaskForUi) => {
|
|
324
|
+
const rerunOptions: StartTaskOptions = {
|
|
325
|
+
name: taskDisplayName(task),
|
|
326
|
+
notifyOnCompletion: true,
|
|
327
|
+
triggerOnCompletion: false,
|
|
418
328
|
};
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
329
|
+
if (task.description !== undefined) rerunOptions.description = task.description;
|
|
330
|
+
if (task.timeoutSeconds !== undefined) rerunOptions.timeoutSeconds = task.timeoutSeconds;
|
|
331
|
+
const rerun = await startTask(ctx, task.command, rerunOptions);
|
|
332
|
+
return rerun;
|
|
333
|
+
},
|
|
334
|
+
showOutputPath: (task: BackgroundTaskForUi) => {
|
|
335
|
+
ctx.ui.notify(
|
|
336
|
+
`Output path for ${taskDisplayName(task)} (${task.id}):\n${task.outputPath}`,
|
|
337
|
+
'info',
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
deleteTask: async (task: BackgroundTaskForUi) => {
|
|
341
|
+
await registry.cleanupFinishedTasks('one', task.id);
|
|
342
|
+
},
|
|
343
|
+
onFullscreenToggle: (fullscreen) =>
|
|
344
|
+
rePresent(fullscreen ? fullscreenOptions() : normalOptions),
|
|
345
|
+
onClose: () => {
|
|
346
|
+
currentHandle?.hide();
|
|
347
|
+
currentHandle = null;
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
await ctx.ui.custom(
|
|
352
|
+
(tui, theme, _keybindings, done) => {
|
|
353
|
+
rawTui = tui;
|
|
354
|
+
const created = new BackgroundTasksManager(tui, theme, done, {
|
|
355
|
+
...managerOptions,
|
|
356
|
+
...(initialTaskId ? { initialTaskId } : {}),
|
|
357
|
+
});
|
|
358
|
+
manager = created;
|
|
359
|
+
return created;
|
|
425
360
|
},
|
|
426
361
|
{
|
|
427
362
|
overlay: true,
|
|
428
|
-
overlayOptions:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
minWidth: 64,
|
|
432
|
-
margin: { top: 1, left: 1, right: 1 },
|
|
363
|
+
overlayOptions: normalOptions,
|
|
364
|
+
onHandle: (handle) => {
|
|
365
|
+
currentHandle = handle;
|
|
433
366
|
},
|
|
434
367
|
},
|
|
435
368
|
);
|
|
@@ -460,26 +393,12 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
460
393
|
},
|
|
461
394
|
);
|
|
462
395
|
|
|
463
|
-
pi.on('session_start', async (_event, ctx) => {
|
|
464
|
-
// Pi replacement binds a fresh extension instance. Never revive this old
|
|
465
|
-
// activation if a late lifecycle dispatch reaches it after shutdown.
|
|
466
|
-
if (disposed) return;
|
|
467
|
-
registry.setShuttingDown(false);
|
|
468
|
-
const nextRegistryCtx = currentRegistryCtx ?? registryContext(ctx);
|
|
469
|
-
currentRegistryCtx = nextRegistryCtx;
|
|
470
|
-
await registry.ensureRuntimeDir(nextRegistryCtx);
|
|
471
|
-
if (disposed) return;
|
|
472
|
-
});
|
|
473
|
-
|
|
474
396
|
pi.on('session_shutdown', async (event, ctx) => {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
397
|
+
if (shutdownReason.length === 0) shutdownReason = event.reason;
|
|
398
|
+
// Explicitly user-visible sessions always kill running tasks, while /reload,
|
|
399
|
+
// a Pi restart or a crash cannot reach this handler and thus never kill: the
|
|
400
|
+
// tasks survive and the next activation adopts them from disk.
|
|
478
401
|
try {
|
|
479
|
-
// Admission closure aborts cooperative preflight and the drain retains
|
|
480
|
-
// ownership until subprocess/file/managed cleanup settles. Any admitted
|
|
481
|
-
// child is inserted synchronously before spawn and is visible below.
|
|
482
|
-
await registry.waitForTaskAdmissions();
|
|
483
402
|
const running = registry.allTasks().filter((task) => task.status === 'running');
|
|
484
403
|
if (running.length === 0) return;
|
|
485
404
|
|
|
@@ -504,30 +423,15 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
504
423
|
}
|
|
505
424
|
} finally {
|
|
506
425
|
eventService.close();
|
|
507
|
-
if ((event.reason !== 'reload' || reloadHandoffFailed) && activationLease !== undefined) {
|
|
508
|
-
try {
|
|
509
|
-
await registry.waitForReloadHostSettlement();
|
|
510
|
-
} catch (error) {
|
|
511
|
-
console.error(
|
|
512
|
-
'[background-tasks] reload owner host settlement failed during shutdown:',
|
|
513
|
-
error,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
registry.releaseReloadActivation(activationLease);
|
|
517
|
-
activationLease = undefined;
|
|
518
|
-
activationIdentity = undefined;
|
|
519
|
-
}
|
|
520
426
|
}
|
|
521
427
|
});
|
|
522
428
|
|
|
523
429
|
pi.registerCommand('bg', {
|
|
524
|
-
description:
|
|
525
|
-
'Start a tracked shell command: /bg [--survive-reload] [--name "Task name"] <command>',
|
|
430
|
+
description: 'Start a tracked shell command: /bg [--name "Task name"] <command>',
|
|
526
431
|
handler: async (args, ctx) => {
|
|
527
432
|
try {
|
|
528
433
|
const parsed = parseBgCommandArgs(args);
|
|
529
434
|
const taskOptions: StartTaskOptions = {
|
|
530
|
-
surviveReload: parsed.surviveReload,
|
|
531
435
|
notifyOnCompletion: true,
|
|
532
436
|
triggerOnCompletion: false,
|
|
533
437
|
};
|
|
@@ -703,12 +607,8 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
703
607
|
if (!args || typeof args !== 'object') throw new Error('bg_run arguments must be an object');
|
|
704
608
|
const input = args as BgToolArgumentRecord;
|
|
705
609
|
if (typeof input.command !== 'string') throw new Error('bg_run requires command string');
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
'pi_bg_survive_reload_invalid',
|
|
709
|
-
'bg_run surviveReload must be boolean when present',
|
|
710
|
-
);
|
|
711
|
-
}
|
|
610
|
+
// Legacy surviveReload values are silently ignored: task survival across
|
|
611
|
+
// /reload is now the only behavior, so the knob has nothing left to set.
|
|
712
612
|
const prepared: BgRunParamsValue = {
|
|
713
613
|
command: input.command,
|
|
714
614
|
name:
|
|
@@ -722,13 +622,11 @@ export default async function backgroundTasksExtension(pi: ExtensionAPI): Promis
|
|
|
722
622
|
prepared.notifyOnCompletion = input.notifyOnCompletion;
|
|
723
623
|
if (typeof input.triggerOnCompletion === 'boolean')
|
|
724
624
|
prepared.triggerOnCompletion = input.triggerOnCompletion;
|
|
725
|
-
if (typeof input.surviveReload === 'boolean') prepared.surviveReload = input.surviveReload;
|
|
726
625
|
return prepared;
|
|
727
626
|
},
|
|
728
627
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
729
628
|
const taskOptions: StartTaskOptions = {
|
|
730
629
|
name: params.name,
|
|
731
|
-
surviveReload: params.surviveReload ?? false,
|
|
732
630
|
notifyOnCompletion: params.notifyOnCompletion ?? true,
|
|
733
631
|
triggerOnCompletion: params.triggerOnCompletion ?? true,
|
|
734
632
|
};
|