pi-background-tasks 2.5.0 → 2.6.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/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/README.md +70 -25
- package/TESTING.md +42 -2
- package/TEST_PLAN.md +7 -4
- package/dist/extensions/anthropic-attribution-child.js +4 -0
- package/dist/extensions/anthropic-attribution-child.js.map +1 -0
- package/dist/extensions/anthropic-attribution.js +107 -0
- package/dist/extensions/anthropic-attribution.js.map +1 -0
- package/dist/extensions/background-tasks.js +2 -0
- package/dist/extensions/background-tasks.js.map +1 -0
- package/dist/extensions/delegate-child.js +2 -0
- package/dist/extensions/delegate-child.js.map +1 -0
- package/dist/extensions/fusion-child.js +2 -0
- package/dist/extensions/fusion-child.js.map +1 -0
- package/dist/package.json +5 -0
- package/dist/src/core/anthropic-attribution-path.js +24 -0
- package/dist/src/core/anthropic-attribution-path.js.map +1 -0
- package/dist/src/core/anthropic-attribution.js +2247 -0
- package/dist/src/core/anthropic-attribution.js.map +1 -0
- package/dist/src/core/attested-pi-contract.js +5 -0
- package/dist/src/core/attested-pi-contract.js.map +1 -0
- package/dist/src/core/attested-pi-run.js +749 -0
- package/dist/src/core/attested-pi-run.js.map +1 -0
- package/dist/src/core/canonical-json.js +19 -0
- package/dist/src/core/canonical-json.js.map +1 -0
- package/dist/src/core/common.js +787 -0
- package/dist/src/core/common.js.map +1 -0
- package/dist/src/core/config.js +78 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/context/parent-snapshot.js +75 -0
- package/dist/src/core/context/parent-snapshot.js.map +1 -0
- package/dist/src/core/context/token-budget.js +631 -0
- package/dist/src/core/context/token-budget.js.map +1 -0
- package/dist/src/core/context/visible-conversation-v2.js +390 -0
- package/dist/src/core/context/visible-conversation-v2.js.map +1 -0
- package/dist/src/core/delegate/artifacts.js +357 -0
- package/dist/src/core/delegate/artifacts.js.map +1 -0
- package/dist/src/core/delegate/budget.js +266 -0
- package/dist/src/core/delegate/budget.js.map +1 -0
- package/dist/src/core/delegate/facade-contract.js +7 -0
- package/dist/src/core/delegate/facade-contract.js.map +1 -0
- package/dist/src/core/delegate/hook-contract-evidence.json +18 -0
- package/dist/src/core/delegate/hook-contract.js +124 -0
- package/dist/src/core/delegate/hook-contract.js.map +1 -0
- package/dist/src/core/delegate/launch.js +366 -0
- package/dist/src/core/delegate/launch.js.map +1 -0
- package/dist/src/core/delegate/result-package.js +293 -0
- package/dist/src/core/delegate/result-package.js.map +1 -0
- package/dist/src/core/delegate/runner.js +398 -0
- package/dist/src/core/delegate/runner.js.map +1 -0
- package/dist/src/core/delegate/seed.js +334 -0
- package/dist/src/core/delegate/seed.js.map +1 -0
- package/dist/src/core/delegate/types.js +103 -0
- package/dist/src/core/delegate/types.js.map +1 -0
- package/dist/src/core/durable-fs.js +372 -0
- package/dist/src/core/durable-fs.js.map +1 -0
- package/dist/src/core/extension-api.js +382 -0
- package/dist/src/core/extension-api.js.map +1 -0
- package/dist/src/core/fusion/artifacts.js +654 -0
- package/dist/src/core/fusion/artifacts.js.map +1 -0
- package/dist/src/core/fusion/budget.js +949 -0
- package/dist/src/core/fusion/budget.js.map +1 -0
- package/dist/src/core/fusion/child-protocol.js +176 -0
- package/dist/src/core/fusion/child-protocol.js.map +1 -0
- package/dist/src/core/fusion/claude-cache.js +156 -0
- package/dist/src/core/fusion/claude-cache.js.map +1 -0
- package/dist/src/core/fusion/clean-context.js +64 -0
- package/dist/src/core/fusion/clean-context.js.map +1 -0
- package/dist/src/core/fusion/config.js +349 -0
- package/dist/src/core/fusion/config.js.map +1 -0
- package/dist/src/core/fusion/context.js +184 -0
- package/dist/src/core/fusion/context.js.map +1 -0
- package/dist/src/core/fusion/evaluation.js +696 -0
- package/dist/src/core/fusion/evaluation.js.map +1 -0
- package/dist/src/core/fusion/facade-contract.js +3 -0
- package/dist/src/core/fusion/facade-contract.js.map +1 -0
- package/dist/src/core/fusion/orchestrator.js +867 -0
- package/dist/src/core/fusion/orchestrator.js.map +1 -0
- package/dist/src/core/fusion/output-contract.js +27 -0
- package/dist/src/core/fusion/output-contract.js.map +1 -0
- package/dist/src/core/fusion/pi-child.js +1741 -0
- package/dist/src/core/fusion/pi-child.js.map +1 -0
- package/dist/src/core/fusion/prompts.js +260 -0
- package/dist/src/core/fusion/prompts.js.map +1 -0
- package/dist/src/core/fusion/result-package.js +857 -0
- package/dist/src/core/fusion/result-package.js.map +1 -0
- package/dist/src/core/fusion/source-policy.js +259 -0
- package/dist/src/core/fusion/source-policy.js.map +1 -0
- package/dist/src/core/fusion/types.js +207 -0
- package/dist/src/core/fusion/types.js.map +1 -0
- package/dist/src/core/fusion/web-fetch.js +777 -0
- package/dist/src/core/fusion/web-fetch.js.map +1 -0
- package/dist/src/core/fusion/workflows.js +126 -0
- package/dist/src/core/fusion/workflows.js.map +1 -0
- package/dist/src/core/lazy-module.js +181 -0
- package/dist/src/core/lazy-module.js.map +1 -0
- package/dist/src/core/pi-launch.js +431 -0
- package/dist/src/core/pi-launch.js.map +1 -0
- package/dist/src/core/registry.js +3331 -0
- package/dist/src/core/registry.js.map +1 -0
- package/dist/src/core/reload-shell-owner.js +1396 -0
- package/dist/src/core/reload-shell-owner.js.map +1 -0
- package/dist/src/core/shell-policy.js +80 -0
- package/dist/src/core/shell-policy.js.map +1 -0
- package/dist/src/core/task-durable.js +51 -0
- package/dist/src/core/task-durable.js.map +1 -0
- package/dist/src/core/update-check.js +92 -0
- package/dist/src/core/update-check.js.map +1 -0
- package/dist/src/core/windows-taskkill.js +185 -0
- package/dist/src/core/windows-taskkill.js.map +1 -0
- package/dist/src/delegate-child-extension.js +776 -0
- package/dist/src/delegate-child-extension.js.map +1 -0
- package/dist/src/delegate-extension.js +746 -0
- package/dist/src/delegate-extension.js.map +1 -0
- package/dist/src/extension.js +953 -0
- package/dist/src/extension.js.map +1 -0
- package/dist/src/fusion-child-extension.js +760 -0
- package/dist/src/fusion-child-extension.js.map +1 -0
- package/dist/src/fusion-extension.js +1030 -0
- package/dist/src/fusion-extension.js.map +1 -0
- package/dist/src/ui/background-tasks-manager.js +689 -0
- package/dist/src/ui/background-tasks-manager.js.map +1 -0
- package/dist/src/ui/fusion-model-selector.js +277 -0
- package/dist/src/ui/fusion-model-selector.js.map +1 -0
- package/docs/INDEX.md +35 -33
- package/docs/api/eventbus-v1.md +21 -5
- package/docs/choose-a-workflow.md +2 -0
- package/docs/commands/bg-clear.md +3 -3
- package/docs/commands/bg-update.md +3 -3
- package/docs/commands/bg.md +18 -6
- package/docs/commands/claude-cache.md +4 -4
- package/docs/commands/fusion-models.md +49 -4
- package/docs/commands/fusion.md +3 -3
- package/docs/commands/jobs.md +3 -3
- package/docs/commands/kill.md +3 -3
- package/docs/commands/logs.md +3 -3
- package/docs/commands/task-manager.md +5 -5
- package/docs/concepts/completion-delivery.md +6 -2
- package/docs/getting-started.md +7 -1
- package/docs/manifest.json +228 -42
- package/docs/operations/configuration.md +58 -5
- package/docs/operations/releasing.md +4 -2
- package/docs/operations/testing.md +22 -0
- package/docs/operations/troubleshooting.md +6 -2
- package/docs/read-before-edit.md +44 -31
- package/docs/reference/runtime-contracts.md +87 -51
- package/docs/reference/shortcuts-and-dock.md +28 -14
- package/docs/subsystems/anthropic-attribution.md +23 -11
- package/docs/subsystems/attested-pi-runs.md +12 -7
- package/docs/subsystems/background-task-runtime.md +53 -11
- package/docs/subsystems/child-launch-durability-and-safety.md +28 -9
- package/docs/subsystems/delegation.md +16 -3
- package/docs/subsystems/docs-freshness-gate.md +14 -7
- package/docs/subsystems/fusion.md +36 -11
- package/docs/subsystems/host-ui-and-telemetry.md +22 -9
- package/docs/tools/bg_delegate.md +3 -1
- package/docs/tools/bg_kill.md +3 -1
- package/docs/tools/bg_logs.md +3 -1
- package/docs/tools/bg_result.md +5 -3
- package/docs/tools/bg_run.md +31 -5
- package/docs/tools/bg_run_pi_attested.md +3 -1
- package/docs/tools/bg_status.md +3 -1
- package/docs/tools/fusion_investigate.md +3 -1
- package/docs/tools/fusion_reason.md +3 -1
- package/docs/tools/fusion_research.md +3 -1
- package/docs/tools/fusion_validate.md +3 -1
- package/extensions/anthropic-attribution-child.ts +3 -0
- package/extensions/anthropic-attribution.ts +166 -1
- package/package.json +12 -10
- package/src/core/anthropic-attribution-path.ts +5 -4
- package/src/core/anthropic-attribution.ts +293 -113
- package/src/core/attested-pi-contract.ts +4 -0
- package/src/core/attested-pi-run.ts +468 -81
- package/src/core/canonical-json.ts +21 -0
- package/src/core/common.ts +450 -40
- package/src/core/config.ts +121 -0
- package/src/core/context/visible-conversation-v2.ts +3 -6
- package/src/core/delegate/artifacts.ts +13 -9
- package/src/core/delegate/budget.ts +13 -6
- package/src/core/delegate/facade-contract.ts +6 -0
- package/src/core/delegate/result-package.ts +32 -14
- package/src/core/delegate/runner.ts +129 -58
- package/src/core/delegate/seed.ts +5 -7
- package/src/core/durable-fs.ts +178 -30
- package/src/core/extension-api.ts +36 -8
- package/src/core/fusion/artifacts.ts +17 -16
- package/src/core/fusion/clean-context.ts +8 -5
- package/src/core/fusion/config.ts +2 -1
- package/src/core/fusion/context.ts +2 -6
- package/src/core/fusion/facade-contract.ts +2 -0
- package/src/core/fusion/orchestrator.ts +3 -3
- package/src/core/fusion/prompts.ts +1 -1
- package/src/core/fusion/result-package.ts +259 -72
- package/src/core/fusion/source-policy.ts +43 -17
- package/src/core/lazy-module.ts +215 -0
- package/src/core/pi-launch.ts +390 -65
- package/src/core/registry.ts +2010 -381
- package/src/core/reload-shell-owner.ts +1662 -0
- package/src/core/shell-policy.ts +134 -0
- package/src/core/task-durable.ts +67 -0
- package/src/delegate-extension.ts +404 -144
- package/src/extension.ts +387 -114
- package/src/fusion-extension.ts +202 -96
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { accessSync, constants, statSync } from 'node:fs';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { basename, delimiter, extname, isAbsolute, join, resolve, win32 } from 'node:path';
|
|
4
|
+
import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
export const TASK_STATUS_VALUES = ['running', 'completed', 'failed', 'killed'];
|
|
6
|
+
export const TERMINAL_TASK_STATUS_VALUES = ['completed', 'failed', 'killed'];
|
|
7
|
+
export class ReloadSurvivalError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(`${code}: ${message}`);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'ReloadSurvivalError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function rejectSurvivalForTaskKind(value, kind) {
|
|
16
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'surviveReload'))
|
|
17
|
+
return;
|
|
18
|
+
throw new ReloadSurvivalError('pi_bg_survive_reload_unsupported_task_kind', `${kind} does not support surviveReload; only ordinary isAgent:false shell tasks may survive reload`);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Describe the actual parent-agent completion path for one bg_run launch.
|
|
22
|
+
* A wake request cannot take effect without the notification that carries it.
|
|
23
|
+
*/
|
|
24
|
+
export function deriveCompletionDeliveryGuidance(notifyOnCompletion, triggerOnCompletion) {
|
|
25
|
+
if (notifyOnCompletion && triggerOnCompletion) {
|
|
26
|
+
return {
|
|
27
|
+
mode: 'notification-and-wake',
|
|
28
|
+
notificationEnabled: true,
|
|
29
|
+
automaticWakeEnabled: true,
|
|
30
|
+
text: [
|
|
31
|
+
'Terminal notification: enabled.',
|
|
32
|
+
'Automatic follow-up turn: enabled.',
|
|
33
|
+
'Next action: do not poll or sleep merely to wait; continue only independent useful work, otherwise end this turn and wait for <background-task-notification>.',
|
|
34
|
+
].join('\n'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (notifyOnCompletion) {
|
|
38
|
+
return {
|
|
39
|
+
mode: 'notification-only',
|
|
40
|
+
notificationEnabled: true,
|
|
41
|
+
automaticWakeEnabled: false,
|
|
42
|
+
text: [
|
|
43
|
+
'Terminal notification: enabled.',
|
|
44
|
+
'Automatic follow-up turn: disabled. The terminal notification will be delivered, but it will not start an agent turn.',
|
|
45
|
+
'Next action: automatic wake-up was explicitly disabled; use bg_status/bg_logs only when deliberate monitoring is required, without tight polling.',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
mode: 'manual-monitoring',
|
|
51
|
+
notificationEnabled: false,
|
|
52
|
+
automaticWakeEnabled: false,
|
|
53
|
+
text: [
|
|
54
|
+
'Terminal notification: disabled.',
|
|
55
|
+
triggerOnCompletion
|
|
56
|
+
? 'Automatic follow-up turn: disabled because terminal notifications are disabled. triggerOnCompletion has no effect while notifyOnCompletion is false.'
|
|
57
|
+
: 'Automatic follow-up turn: disabled.',
|
|
58
|
+
'Next action: completion delivery was explicitly disabled; use bg_status/bg_logs only for deliberate manual monitoring, without tight polling.',
|
|
59
|
+
].join('\n'),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export const DEFAULT_LOG_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
|
|
63
|
+
export const MAX_LOG_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
|
|
64
|
+
export const COMMAND_PREVIEW_CHARS = 90;
|
|
65
|
+
const parseJsonValue = globalThis.JSON.parse;
|
|
66
|
+
export function isJsonObject(value) {
|
|
67
|
+
return typeof value === 'object' && value !== null;
|
|
68
|
+
}
|
|
69
|
+
export function parseJsonText(text) {
|
|
70
|
+
return parseJsonValue(text);
|
|
71
|
+
}
|
|
72
|
+
export function sanitizePathSegment(value) {
|
|
73
|
+
const sanitized = value.replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
74
|
+
return sanitized || 'session';
|
|
75
|
+
}
|
|
76
|
+
export function stripMatchingQuotes(value) {
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (trimmed.length >= 2) {
|
|
79
|
+
const first = trimmed[0];
|
|
80
|
+
const last = trimmed[trimmed.length - 1];
|
|
81
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
82
|
+
return trimmed.slice(1, -1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return trimmed;
|
|
86
|
+
}
|
|
87
|
+
export function compactWhitespace(value) {
|
|
88
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
89
|
+
}
|
|
90
|
+
export function truncateChars(value, maxChars) {
|
|
91
|
+
if (value.length <= maxChars)
|
|
92
|
+
return value;
|
|
93
|
+
return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
94
|
+
}
|
|
95
|
+
export function normalizeTaskName(value) {
|
|
96
|
+
if (typeof value !== 'string')
|
|
97
|
+
return undefined;
|
|
98
|
+
const normalized = compactWhitespace(stripMatchingQuotes(value));
|
|
99
|
+
if (!normalized)
|
|
100
|
+
return undefined;
|
|
101
|
+
return truncateChars(normalized, 80);
|
|
102
|
+
}
|
|
103
|
+
export function deriveTaskNameFromCommand(command) {
|
|
104
|
+
const normalized = compactWhitespace(stripMatchingQuotes(command));
|
|
105
|
+
if (!normalized)
|
|
106
|
+
return 'Background task';
|
|
107
|
+
const packageScript = /^(npm|pnpm|yarn|bun)\s+(?:(run)\s+)?([^\s;&|]+)/.exec(normalized);
|
|
108
|
+
if (packageScript) {
|
|
109
|
+
const runner = packageScript[1] ?? 'npm';
|
|
110
|
+
const run = packageScript[2] !== undefined ? ' run' : '';
|
|
111
|
+
const script = packageScript[3] ?? '';
|
|
112
|
+
return truncateChars(`${runner}${run} ${script}`, 48);
|
|
113
|
+
}
|
|
114
|
+
const words = normalized.split(/\s+/).slice(0, 5).join(' ');
|
|
115
|
+
return truncateChars(words.length > 0 ? words : normalized, 48);
|
|
116
|
+
}
|
|
117
|
+
export function taskDisplayName(task) {
|
|
118
|
+
const commandName = task.command && task.command.length > 0 ? deriveTaskNameFromCommand(task.command) : undefined;
|
|
119
|
+
return (normalizeTaskName(task.name) ??
|
|
120
|
+
normalizeTaskName(task.description) ??
|
|
121
|
+
commandName ??
|
|
122
|
+
task.id ??
|
|
123
|
+
'Background task');
|
|
124
|
+
}
|
|
125
|
+
function parseNameValueAndRest(valueAndRest) {
|
|
126
|
+
const input = valueAndRest.trimStart();
|
|
127
|
+
if (!input)
|
|
128
|
+
return undefined;
|
|
129
|
+
const quote = input[0];
|
|
130
|
+
if (quote === '"' || quote === "'") {
|
|
131
|
+
let escaped = false;
|
|
132
|
+
let value = '';
|
|
133
|
+
for (let i = 1; i < input.length; i++) {
|
|
134
|
+
const char = input.charAt(i);
|
|
135
|
+
if (escaped) {
|
|
136
|
+
value += char;
|
|
137
|
+
escaped = false;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (char === '\\') {
|
|
141
|
+
escaped = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (char === quote) {
|
|
145
|
+
return { value, rest: input.slice(i + 1).trimStart() };
|
|
146
|
+
}
|
|
147
|
+
value += char;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(input);
|
|
152
|
+
if (!match)
|
|
153
|
+
return undefined;
|
|
154
|
+
const parsedValue = match[1];
|
|
155
|
+
if (parsedValue === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
return { value: parsedValue, rest: match[2]?.trimStart() ?? '' };
|
|
158
|
+
}
|
|
159
|
+
export function parseBgCommandArgs(args) {
|
|
160
|
+
let input = args.trim();
|
|
161
|
+
let name;
|
|
162
|
+
let isAgent = false;
|
|
163
|
+
let surviveReload = false;
|
|
164
|
+
while (input) {
|
|
165
|
+
let consumed = false;
|
|
166
|
+
for (const prefix of ['--name=', '-n=']) {
|
|
167
|
+
if (input.startsWith(prefix)) {
|
|
168
|
+
const parsed = parseNameValueAndRest(input.slice(prefix.length));
|
|
169
|
+
if (!parsed)
|
|
170
|
+
throw new Error(`${prefix.slice(0, -1)} requires a task name`);
|
|
171
|
+
name = normalizeTaskName(parsed.value);
|
|
172
|
+
input = parsed.rest;
|
|
173
|
+
consumed = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (consumed)
|
|
178
|
+
continue;
|
|
179
|
+
for (const prefix of ['--name', '-n']) {
|
|
180
|
+
if (input === prefix || input.startsWith(`${prefix} `) || input.startsWith(`${prefix}\t`)) {
|
|
181
|
+
const parsed = parseNameValueAndRest(input.slice(prefix.length));
|
|
182
|
+
if (!parsed)
|
|
183
|
+
throw new Error(`${prefix} requires a task name`);
|
|
184
|
+
name = normalizeTaskName(parsed.value);
|
|
185
|
+
input = parsed.rest;
|
|
186
|
+
consumed = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (consumed)
|
|
191
|
+
continue;
|
|
192
|
+
for (const flag of ['--agent', '--llm-agent']) {
|
|
193
|
+
if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
|
|
194
|
+
isAgent = true;
|
|
195
|
+
input = input.slice(flag.length).trimStart();
|
|
196
|
+
consumed = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (consumed)
|
|
201
|
+
continue;
|
|
202
|
+
for (const flag of ['--script', '--no-agent']) {
|
|
203
|
+
if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
|
|
204
|
+
isAgent = false;
|
|
205
|
+
input = input.slice(flag.length).trimStart();
|
|
206
|
+
consumed = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (consumed)
|
|
211
|
+
continue;
|
|
212
|
+
if (input === '--survive-reload' ||
|
|
213
|
+
input.startsWith('--survive-reload ') ||
|
|
214
|
+
input.startsWith('--survive-reload\t')) {
|
|
215
|
+
if (surviveReload) {
|
|
216
|
+
throw new ReloadSurvivalError('pi_bg_survive_reload_invalid', '/bg accepts --survive-reload at most once');
|
|
217
|
+
}
|
|
218
|
+
surviveReload = true;
|
|
219
|
+
input = input.slice('--survive-reload'.length).trimStart();
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (input.startsWith('--survive-reload=')) {
|
|
223
|
+
throw new ReloadSurvivalError('pi_bg_survive_reload_invalid', '/bg accepts only the bare --survive-reload flag');
|
|
224
|
+
}
|
|
225
|
+
if (input === '--') {
|
|
226
|
+
input = '';
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (input.startsWith('-- ')) {
|
|
230
|
+
input = input.slice(3).trimStart();
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (surviveReload && isAgent) {
|
|
236
|
+
throw new ReloadSurvivalError('pi_bg_survive_reload_requires_non_agent', 'surviveReload requires isAgent:false');
|
|
237
|
+
}
|
|
238
|
+
return name
|
|
239
|
+
? { name, command: input, isAgent, surviveReload }
|
|
240
|
+
: { command: input, isAgent, surviveReload };
|
|
241
|
+
}
|
|
242
|
+
export function formatDuration(ms) {
|
|
243
|
+
if (ms < 1000)
|
|
244
|
+
return `${String(ms)}ms`;
|
|
245
|
+
const seconds = Math.floor(ms / 1000);
|
|
246
|
+
if (seconds < 60)
|
|
247
|
+
return `${String(seconds)}s`;
|
|
248
|
+
const minutes = Math.floor(seconds / 60);
|
|
249
|
+
const remSeconds = seconds % 60;
|
|
250
|
+
if (minutes < 60)
|
|
251
|
+
return `${String(minutes)}m${remSeconds > 0 ? `${String(remSeconds)}s` : ''}`;
|
|
252
|
+
const hours = Math.floor(minutes / 60);
|
|
253
|
+
const remMinutes = minutes % 60;
|
|
254
|
+
return `${String(hours)}h${remMinutes > 0 ? `${String(remMinutes)}m` : ''}`;
|
|
255
|
+
}
|
|
256
|
+
export function formatCompactNumber(count) {
|
|
257
|
+
const normalized = Math.max(0, Math.floor(count));
|
|
258
|
+
if (normalized < 1000)
|
|
259
|
+
return normalized.toString();
|
|
260
|
+
if (normalized < 10000)
|
|
261
|
+
return `${(normalized / 1000).toFixed(1)}k`;
|
|
262
|
+
if (normalized < 1000000)
|
|
263
|
+
return `${String(Math.round(normalized / 1000))}k`;
|
|
264
|
+
if (normalized < 10000000)
|
|
265
|
+
return `${(normalized / 1000000).toFixed(1)}M`;
|
|
266
|
+
return `${String(Math.round(normalized / 1000000))}M`;
|
|
267
|
+
}
|
|
268
|
+
export function formatContextUsageSummary(usage) {
|
|
269
|
+
if (usage?.contextWindow === undefined || usage.contextWindow <= 0)
|
|
270
|
+
return undefined;
|
|
271
|
+
const window = formatCompactNumber(usage.contextWindow);
|
|
272
|
+
if (usage.percent === null || usage.tokens === null)
|
|
273
|
+
return `ctx=?/${window}`;
|
|
274
|
+
return `ctx=${usage.percent.toFixed(1)}%/${window}`;
|
|
275
|
+
}
|
|
276
|
+
export function formatTokenUsageSummary(usage) {
|
|
277
|
+
if (!usage || usage.totalTokens <= 0)
|
|
278
|
+
return undefined;
|
|
279
|
+
return `tokens=${formatCompactNumber(usage.totalTokens)}`;
|
|
280
|
+
}
|
|
281
|
+
export function formatToolUsageSummary(usage) {
|
|
282
|
+
if (!usage || (usage.total <= 0 && usage.failed <= 0))
|
|
283
|
+
return undefined;
|
|
284
|
+
const failed = usage.failed > 0 ? ` failed=${String(usage.failed)}` : '';
|
|
285
|
+
return `tools=${String(usage.total)}${failed}`;
|
|
286
|
+
}
|
|
287
|
+
export function formatModelSummary(model) {
|
|
288
|
+
if (!model)
|
|
289
|
+
return undefined;
|
|
290
|
+
return `model=${model}`;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Human-readable activity transcript for telemetry-wrapped Pi agents.
|
|
294
|
+
*
|
|
295
|
+
* The wrapper emits one `background-task-activity` control line per meaningful
|
|
296
|
+
* child-agent event (assistant text, reasoning, tool start, tool end) so the
|
|
297
|
+
* registry can render "what the agent is actually doing" into the task output
|
|
298
|
+
* file instead of leaking raw telemetry JSON. Both the parser and the formatter
|
|
299
|
+
* are pure so the visible transcript is fully unit-testable.
|
|
300
|
+
*/
|
|
301
|
+
export const AGENT_ACTIVITY_TYPE = 'background-task-activity';
|
|
302
|
+
const AGENT_ACTIVITY_DETAIL_MAX = 80;
|
|
303
|
+
function readActivityString(record, key) {
|
|
304
|
+
const value = record[key];
|
|
305
|
+
return typeof value === 'string' ? value : undefined;
|
|
306
|
+
}
|
|
307
|
+
/** Narrow a parsed `background-task-activity` control payload into a typed {@link AgentActivity}. */
|
|
308
|
+
export function parseAgentActivity(payload) {
|
|
309
|
+
if (!isJsonObject(payload))
|
|
310
|
+
return undefined;
|
|
311
|
+
const record = payload;
|
|
312
|
+
if (record.type !== AGENT_ACTIVITY_TYPE)
|
|
313
|
+
return undefined;
|
|
314
|
+
const kind = record.kind;
|
|
315
|
+
if (kind === 'assistant_text' || kind === 'reasoning') {
|
|
316
|
+
const text = readActivityString(record, 'text');
|
|
317
|
+
if (text === undefined)
|
|
318
|
+
return undefined;
|
|
319
|
+
return { kind, text };
|
|
320
|
+
}
|
|
321
|
+
if (kind === 'tool_start') {
|
|
322
|
+
const tool = readActivityString(record, 'tool');
|
|
323
|
+
if (!tool)
|
|
324
|
+
return undefined;
|
|
325
|
+
return { kind, tool, argsSummary: readActivityString(record, 'argsSummary') ?? '' };
|
|
326
|
+
}
|
|
327
|
+
if (kind === 'tool_end') {
|
|
328
|
+
const tool = readActivityString(record, 'tool');
|
|
329
|
+
if (!tool)
|
|
330
|
+
return undefined;
|
|
331
|
+
const activity = { kind, tool, isError: record.isError === true };
|
|
332
|
+
const error = readActivityString(record, 'error');
|
|
333
|
+
if (error !== undefined && error.trim().length > 0)
|
|
334
|
+
activity.error = error;
|
|
335
|
+
return activity;
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Render an {@link AgentActivity} into a single transcript line, or `undefined`
|
|
341
|
+
* when the event carries nothing worth showing (blank text, a successful tool
|
|
342
|
+
* end). Successful tool ends are intentionally silent: the matching `→` start
|
|
343
|
+
* line already announced the call, and the next line implies completion.
|
|
344
|
+
*/
|
|
345
|
+
export function formatAgentActivityLine(activity) {
|
|
346
|
+
if (activity.kind === 'assistant_text') {
|
|
347
|
+
const text = activity.text.replace(/\s+$/u, '');
|
|
348
|
+
return text.trim().length > 0 ? text : undefined;
|
|
349
|
+
}
|
|
350
|
+
if (activity.kind === 'reasoning') {
|
|
351
|
+
const text = activity.text.replace(/\s+$/u, '');
|
|
352
|
+
return text.trim().length > 0 ? `\u2026 ${text}` : undefined;
|
|
353
|
+
}
|
|
354
|
+
if (activity.kind === 'tool_start') {
|
|
355
|
+
const summary = compactWhitespace(activity.argsSummary);
|
|
356
|
+
const suffix = summary.length > 0 ? ` ${truncateChars(summary, AGENT_ACTIVITY_DETAIL_MAX)}` : '';
|
|
357
|
+
return `\u2192 ${activity.tool}${suffix}`;
|
|
358
|
+
}
|
|
359
|
+
if (!activity.isError)
|
|
360
|
+
return undefined;
|
|
361
|
+
const detail = activity.error
|
|
362
|
+
? `: ${truncateChars(compactWhitespace(activity.error), AGENT_ACTIVITY_DETAIL_MAX)}`
|
|
363
|
+
: '';
|
|
364
|
+
return `\u2717 ${activity.tool} failed${detail}`;
|
|
365
|
+
}
|
|
366
|
+
export function shellQuote(value) {
|
|
367
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
368
|
+
}
|
|
369
|
+
export class ShellInvocationError extends Error {
|
|
370
|
+
code = 'pi_bg_shell_invalid';
|
|
371
|
+
constructor(message) {
|
|
372
|
+
super(`pi_bg_shell_invalid: ${message}`);
|
|
373
|
+
this.name = 'ShellInvocationError';
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const POSIX_FUNCTION_SHELLS = new Set([
|
|
377
|
+
'sh',
|
|
378
|
+
'dash',
|
|
379
|
+
'ash',
|
|
380
|
+
'ksh',
|
|
381
|
+
'ksh93',
|
|
382
|
+
'mksh',
|
|
383
|
+
'pdksh',
|
|
384
|
+
'zsh',
|
|
385
|
+
'yash',
|
|
386
|
+
'posh',
|
|
387
|
+
]);
|
|
388
|
+
function failShellInvocation(message) {
|
|
389
|
+
throw new ShellInvocationError(message);
|
|
390
|
+
}
|
|
391
|
+
function shellErrorMessage(error) {
|
|
392
|
+
return error instanceof Error ? error.message : String(error);
|
|
393
|
+
}
|
|
394
|
+
function freezeShellPolicy(policy) {
|
|
395
|
+
return Object.freeze({
|
|
396
|
+
...policy,
|
|
397
|
+
argvPrefix: Object.freeze([...policy.argvPrefix]),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
export function shellPolicySnapshot(policy) {
|
|
401
|
+
return Object.freeze({
|
|
402
|
+
policy: policy.policy,
|
|
403
|
+
executable: policy.executable,
|
|
404
|
+
argvPrefix: Object.freeze([...policy.argvPrefix]),
|
|
405
|
+
dialect: policy.dialect,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function isWindowsExecutablePath(path) {
|
|
409
|
+
const extension = extname(path).toLowerCase();
|
|
410
|
+
return extension === '.exe' || extension === '.com';
|
|
411
|
+
}
|
|
412
|
+
function validateWindowsShellPath(path, label) {
|
|
413
|
+
if (path.length === 0)
|
|
414
|
+
failShellInvocation(`${label} is empty`);
|
|
415
|
+
if (!isAbsolute(path) && !win32.isAbsolute(path)) {
|
|
416
|
+
failShellInvocation(`${label} must be an absolute path`);
|
|
417
|
+
}
|
|
418
|
+
if (!isWindowsExecutablePath(path)) {
|
|
419
|
+
failShellInvocation(`${label} must point to a .exe or .com file`);
|
|
420
|
+
}
|
|
421
|
+
let stats;
|
|
422
|
+
try {
|
|
423
|
+
stats = statSync(path);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
failShellInvocation(`${label} stat failed: ${shellErrorMessage(error)}`);
|
|
427
|
+
}
|
|
428
|
+
if (!stats.isFile())
|
|
429
|
+
failShellInvocation(`${label} must point to a regular file`);
|
|
430
|
+
return path;
|
|
431
|
+
}
|
|
432
|
+
function inspectWindowsShellCandidate(path) {
|
|
433
|
+
if (!isWindowsExecutablePath(path)) {
|
|
434
|
+
return { found: false, diagnostic: `${path} is not a .exe or .com path` };
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
const stats = statSync(path);
|
|
438
|
+
if (stats.isFile())
|
|
439
|
+
return { found: true };
|
|
440
|
+
return { found: false, diagnostic: `${path} is not a regular file` };
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
return { found: false, diagnostic: `${path}: ${shellErrorMessage(error)}` };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function windowsPathValue(env) {
|
|
447
|
+
return env['PATH'] ?? env['Path'] ?? env['path'] ?? '';
|
|
448
|
+
}
|
|
449
|
+
function resolveWindowsBash(env) {
|
|
450
|
+
const pathValue = windowsPathValue(env);
|
|
451
|
+
const diagnostics = [];
|
|
452
|
+
for (const dir of pathValue.split(';').filter((entry) => entry.length > 0)) {
|
|
453
|
+
for (const name of ['bash.exe', 'bash.com']) {
|
|
454
|
+
const candidate = join(dir, name);
|
|
455
|
+
const result = inspectWindowsShellCandidate(candidate);
|
|
456
|
+
if (result.found)
|
|
457
|
+
return candidate;
|
|
458
|
+
diagnostics.push(result.diagnostic);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const suffix = diagnostics.length > 0 ? `: ${diagnostics.join('; ')}` : '';
|
|
462
|
+
failShellInvocation(`PI_BG_SHELL=bash could not resolve bash.exe or bash.com on PATH${suffix}`);
|
|
463
|
+
}
|
|
464
|
+
function validatePosixShellPath(path, label) {
|
|
465
|
+
if (path.length === 0)
|
|
466
|
+
failShellInvocation(`${label} is empty`);
|
|
467
|
+
if (!isAbsolute(path))
|
|
468
|
+
failShellInvocation(`${label} must be an absolute path`);
|
|
469
|
+
let stats;
|
|
470
|
+
try {
|
|
471
|
+
stats = statSync(path);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
failShellInvocation(`${label} stat failed: ${shellErrorMessage(error)}`);
|
|
475
|
+
}
|
|
476
|
+
if (!stats.isFile())
|
|
477
|
+
failShellInvocation(`${label} must point to a regular file`);
|
|
478
|
+
try {
|
|
479
|
+
accessSync(path, constants.X_OK);
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
failShellInvocation(`${label} must be executable: ${shellErrorMessage(error)}`);
|
|
483
|
+
}
|
|
484
|
+
return path;
|
|
485
|
+
}
|
|
486
|
+
function inspectPosixShellCandidate(path) {
|
|
487
|
+
try {
|
|
488
|
+
const stats = statSync(path);
|
|
489
|
+
if (!stats.isFile())
|
|
490
|
+
return false;
|
|
491
|
+
accessSync(path, constants.X_OK);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function resolvePosixExecutable(name, env, activationCwd) {
|
|
499
|
+
const binCandidate = `/bin/${name}`;
|
|
500
|
+
if (inspectPosixShellCandidate(binCandidate))
|
|
501
|
+
return binCandidate;
|
|
502
|
+
const pathValue = env['PATH'] ?? '';
|
|
503
|
+
for (const entry of pathValue.split(delimiter)) {
|
|
504
|
+
if (entry.length === 0)
|
|
505
|
+
continue;
|
|
506
|
+
const directory = isAbsolute(entry) ? entry : resolve(activationCwd, entry);
|
|
507
|
+
const candidate = join(directory, name);
|
|
508
|
+
if (inspectPosixShellCandidate(candidate))
|
|
509
|
+
return candidate;
|
|
510
|
+
}
|
|
511
|
+
failShellInvocation(`PI_BG_POSIX_SHELL=${name} could not resolve executable ${binCandidate} or ${name} on PATH`);
|
|
512
|
+
}
|
|
513
|
+
function inheritedPosixDialect(executable) {
|
|
514
|
+
const name = basename(executable).toLowerCase();
|
|
515
|
+
if (name === 'bash')
|
|
516
|
+
return 'bash';
|
|
517
|
+
return POSIX_FUNCTION_SHELLS.has(name) ? 'posix' : 'user-non-posix';
|
|
518
|
+
}
|
|
519
|
+
/** Resolve one activation-stable policy. New POSIX variables are intentionally ignored on Windows. */
|
|
520
|
+
export function resolveShellPolicy(platform = process.platform, env = process.env, activationCwd = process.cwd()) {
|
|
521
|
+
if (platform === 'win32') {
|
|
522
|
+
const requestedShell = env['PI_BG_SHELL'];
|
|
523
|
+
const requestedPath = env['PI_BG_SHELL_PATH'];
|
|
524
|
+
if (requestedShell === undefined) {
|
|
525
|
+
if (requestedPath !== undefined)
|
|
526
|
+
failShellInvocation('PI_BG_SHELL_PATH requires PI_BG_SHELL');
|
|
527
|
+
const comSpec = env['ComSpec'];
|
|
528
|
+
return freezeShellPolicy({
|
|
529
|
+
policy: 'cmd',
|
|
530
|
+
executable: comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe',
|
|
531
|
+
argvPrefix: ['/d', '/s', '/c'],
|
|
532
|
+
dialect: 'cmd',
|
|
533
|
+
supportsPosixFunctionWrapper: false,
|
|
534
|
+
windowsVerbatimArguments: true,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (requestedShell !== 'cmd' && requestedShell !== 'bash') {
|
|
538
|
+
failShellInvocation('PI_BG_SHELL must be exactly cmd or bash');
|
|
539
|
+
}
|
|
540
|
+
const explicitPath = requestedPath !== undefined
|
|
541
|
+
? validateWindowsShellPath(requestedPath, 'PI_BG_SHELL_PATH')
|
|
542
|
+
: undefined;
|
|
543
|
+
if (requestedShell === 'cmd') {
|
|
544
|
+
const comSpec = env['ComSpec'];
|
|
545
|
+
return freezeShellPolicy({
|
|
546
|
+
policy: 'cmd',
|
|
547
|
+
executable: explicitPath ?? (comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe'),
|
|
548
|
+
argvPrefix: ['/d', '/s', '/c'],
|
|
549
|
+
dialect: 'cmd',
|
|
550
|
+
supportsPosixFunctionWrapper: false,
|
|
551
|
+
windowsVerbatimArguments: true,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return freezeShellPolicy({
|
|
555
|
+
policy: 'bash',
|
|
556
|
+
executable: explicitPath ?? resolveWindowsBash(env),
|
|
557
|
+
argvPrefix: ['-c'],
|
|
558
|
+
dialect: 'bash',
|
|
559
|
+
supportsPosixFunctionWrapper: true,
|
|
560
|
+
windowsVerbatimArguments: false,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const configuredPolicy = env['PI_BG_POSIX_SHELL'];
|
|
564
|
+
if (configuredPolicy !== undefined &&
|
|
565
|
+
configuredPolicy !== 'inherit' &&
|
|
566
|
+
configuredPolicy !== 'bash' &&
|
|
567
|
+
configuredPolicy !== 'sh') {
|
|
568
|
+
failShellInvocation('PI_BG_POSIX_SHELL must be exactly inherit, bash, or sh');
|
|
569
|
+
}
|
|
570
|
+
const policy = configuredPolicy ?? 'inherit';
|
|
571
|
+
const configuredPath = env['PI_BG_POSIX_SHELL_PATH'];
|
|
572
|
+
if (policy === 'inherit') {
|
|
573
|
+
if (configuredPath !== undefined) {
|
|
574
|
+
failShellInvocation('PI_BG_POSIX_SHELL_PATH requires PI_BG_POSIX_SHELL=bash or PI_BG_POSIX_SHELL=sh');
|
|
575
|
+
}
|
|
576
|
+
const inherited = env['SHELL'];
|
|
577
|
+
const executable = inherited && inherited.length > 0 ? inherited : '/bin/sh';
|
|
578
|
+
const dialect = inheritedPosixDialect(executable);
|
|
579
|
+
return freezeShellPolicy({
|
|
580
|
+
policy,
|
|
581
|
+
executable,
|
|
582
|
+
argvPrefix: ['-c'],
|
|
583
|
+
dialect,
|
|
584
|
+
supportsPosixFunctionWrapper: dialect === 'bash' || dialect === 'posix',
|
|
585
|
+
windowsVerbatimArguments: false,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const executable = configuredPath !== undefined
|
|
589
|
+
? validatePosixShellPath(configuredPath, 'PI_BG_POSIX_SHELL_PATH')
|
|
590
|
+
: resolvePosixExecutable(policy, env, activationCwd);
|
|
591
|
+
return freezeShellPolicy({
|
|
592
|
+
policy,
|
|
593
|
+
executable,
|
|
594
|
+
argvPrefix: ['-c'],
|
|
595
|
+
dialect: policy === 'bash' ? 'bash' : 'posix',
|
|
596
|
+
supportsPosixFunctionWrapper: true,
|
|
597
|
+
windowsVerbatimArguments: false,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
export function shellInvocationForPolicy(command, policy) {
|
|
601
|
+
const dialect = policy.dialect === 'bash' ? 'posix' : policy.dialect;
|
|
602
|
+
return {
|
|
603
|
+
shell: policy.executable,
|
|
604
|
+
args: policy.dialect === 'cmd'
|
|
605
|
+
? [...policy.argvPrefix, `"${command}"`]
|
|
606
|
+
: [...policy.argvPrefix, command],
|
|
607
|
+
dialect,
|
|
608
|
+
windowsVerbatimArguments: policy.windowsVerbatimArguments,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
export function shellInvocation(command, platform = process.platform, env = process.env) {
|
|
612
|
+
return shellInvocationForPolicy(command, resolveShellPolicy(platform, env));
|
|
613
|
+
}
|
|
614
|
+
export function normalizeMaxBytes(value, fallback = DEFAULT_LOG_BYTES) {
|
|
615
|
+
const raw = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : fallback;
|
|
616
|
+
return Math.max(1, Math.min(MAX_LOG_BYTES, raw));
|
|
617
|
+
}
|
|
618
|
+
export function snapshot(task) {
|
|
619
|
+
return {
|
|
620
|
+
id: task.id,
|
|
621
|
+
name: taskDisplayName(task),
|
|
622
|
+
command: task.command,
|
|
623
|
+
description: task.description,
|
|
624
|
+
status: task.status,
|
|
625
|
+
outputPath: task.outputPath,
|
|
626
|
+
cwd: task.cwd,
|
|
627
|
+
startTime: task.startTime,
|
|
628
|
+
endTime: task.endTime,
|
|
629
|
+
exitCode: task.exitCode,
|
|
630
|
+
signal: task.signal,
|
|
631
|
+
pid: task.pid,
|
|
632
|
+
bytesWritten: task.bytesWritten,
|
|
633
|
+
isAgent: task.isAgent,
|
|
634
|
+
surviveReload: task.surviveReload === true,
|
|
635
|
+
reloadSurvival: task.reloadSurvival,
|
|
636
|
+
error: task.error,
|
|
637
|
+
notified: task.notified,
|
|
638
|
+
notifyOnCompletion: task.notifyOnCompletion,
|
|
639
|
+
triggerOnCompletion: task.triggerOnCompletion,
|
|
640
|
+
timeoutSeconds: task.timeoutSeconds,
|
|
641
|
+
contextUsage: task.contextUsage,
|
|
642
|
+
tokenUsage: task.tokenUsage,
|
|
643
|
+
toolUsage: task.toolUsage,
|
|
644
|
+
model: task.model,
|
|
645
|
+
telemetryUnavailableReason: task.telemetryUnavailableReason,
|
|
646
|
+
shellPolicy: task.shellPolicy,
|
|
647
|
+
attestationPath: task.attestationPath,
|
|
648
|
+
delegate: task.delegate,
|
|
649
|
+
fusion: task.fusion,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
export function formatSnapshotList(tasks, now = Date.now()) {
|
|
653
|
+
if (tasks.length === 0)
|
|
654
|
+
return 'No background tasks in this Pi extension runtime.';
|
|
655
|
+
return tasks
|
|
656
|
+
.map((task) => {
|
|
657
|
+
const statusIcon = task.status === 'running'
|
|
658
|
+
? '▶'
|
|
659
|
+
: task.status === 'completed'
|
|
660
|
+
? '✓'
|
|
661
|
+
: task.status === 'killed'
|
|
662
|
+
? '■'
|
|
663
|
+
: '✗';
|
|
664
|
+
const age = formatDuration((task.endTime ?? now) - task.startTime);
|
|
665
|
+
const code = task.exitCode !== undefined ? ` exit=${String(task.exitCode)}` : '';
|
|
666
|
+
const pid = task.pid !== undefined ? ` pid=${String(task.pid)}` : '';
|
|
667
|
+
const error = task.error ? ` error=${truncateChars(task.error, 80)}` : '';
|
|
668
|
+
const telemetry = [
|
|
669
|
+
formatContextUsageSummary(task.contextUsage),
|
|
670
|
+
formatModelSummary(task.model),
|
|
671
|
+
formatTokenUsageSummary(task.tokenUsage),
|
|
672
|
+
formatToolUsageSummary(task.toolUsage),
|
|
673
|
+
]
|
|
674
|
+
.filter(Boolean)
|
|
675
|
+
.join(' ');
|
|
676
|
+
const telemetryText = telemetry ? ` ${telemetry}` : '';
|
|
677
|
+
return `${statusIcon} ${task.id} ${task.status} ${age}${code}${pid}${telemetryText} — ${truncateChars(taskDisplayName(task), COMMAND_PREVIEW_CHARS)}${error}\n output: ${task.outputPath}`;
|
|
678
|
+
})
|
|
679
|
+
.join('\n');
|
|
680
|
+
}
|
|
681
|
+
export async function boundedRead(filePath, maxBytes, tail) {
|
|
682
|
+
const stats = statSync(filePath);
|
|
683
|
+
const totalBytes = stats.size;
|
|
684
|
+
const bytesToRead = Math.min(totalBytes, maxBytes);
|
|
685
|
+
if (bytesToRead === 0)
|
|
686
|
+
return { content: '', truncated: false, bytesRead: 0, totalBytes };
|
|
687
|
+
const file = await open(filePath, 'r');
|
|
688
|
+
try {
|
|
689
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
690
|
+
const position = tail ? Math.max(0, totalBytes - bytesToRead) : 0;
|
|
691
|
+
const { bytesRead } = await file.read(buffer, 0, bytesToRead, position);
|
|
692
|
+
return {
|
|
693
|
+
content: buffer.subarray(0, bytesRead).toString('utf8'),
|
|
694
|
+
truncated: totalBytes > bytesRead,
|
|
695
|
+
bytesRead,
|
|
696
|
+
totalBytes,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
await file.close();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
export function escapeXml(value) {
|
|
704
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
705
|
+
}
|
|
706
|
+
export const UPDATE_COMMAND = '/bg-update';
|
|
707
|
+
const SEMVER_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
708
|
+
export function parseSemver(value) {
|
|
709
|
+
if (typeof value !== 'string')
|
|
710
|
+
return undefined;
|
|
711
|
+
const match = SEMVER_PATTERN.exec(value.trim());
|
|
712
|
+
if (!match)
|
|
713
|
+
return undefined;
|
|
714
|
+
const majorRaw = match[1];
|
|
715
|
+
const minorRaw = match[2];
|
|
716
|
+
const patchRaw = match[3];
|
|
717
|
+
if (majorRaw === undefined || minorRaw === undefined || patchRaw === undefined)
|
|
718
|
+
return undefined;
|
|
719
|
+
const major = Number(majorRaw);
|
|
720
|
+
const minor = Number(minorRaw);
|
|
721
|
+
const patch = Number(patchRaw);
|
|
722
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch))
|
|
723
|
+
return undefined;
|
|
724
|
+
const prerelease = match[4] !== undefined ? match[4].split('.') : [];
|
|
725
|
+
return { major, minor, patch, prerelease };
|
|
726
|
+
}
|
|
727
|
+
function comparePrerelease(a, b) {
|
|
728
|
+
if (a.length === 0 && b.length === 0)
|
|
729
|
+
return 0;
|
|
730
|
+
// A version without prerelease identifiers outranks the same core with prerelease identifiers.
|
|
731
|
+
if (a.length === 0)
|
|
732
|
+
return 1;
|
|
733
|
+
if (b.length === 0)
|
|
734
|
+
return -1;
|
|
735
|
+
const shared = Math.min(a.length, b.length);
|
|
736
|
+
for (let i = 0; i < shared; i++) {
|
|
737
|
+
const idA = a[i];
|
|
738
|
+
const idB = b[i];
|
|
739
|
+
if (idA === undefined || idB === undefined)
|
|
740
|
+
break;
|
|
741
|
+
if (idA === idB)
|
|
742
|
+
continue;
|
|
743
|
+
const numericA = /^\d+$/.test(idA);
|
|
744
|
+
const numericB = /^\d+$/.test(idB);
|
|
745
|
+
if (numericA && numericB) {
|
|
746
|
+
const diff = Number(idA) - Number(idB);
|
|
747
|
+
if (diff !== 0)
|
|
748
|
+
return diff < 0 ? -1 : 1;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
// Numeric identifiers always have lower precedence than non-numeric identifiers.
|
|
752
|
+
if (numericA)
|
|
753
|
+
return -1;
|
|
754
|
+
if (numericB)
|
|
755
|
+
return 1;
|
|
756
|
+
return idA < idB ? -1 : 1;
|
|
757
|
+
}
|
|
758
|
+
if (a.length === b.length)
|
|
759
|
+
return 0;
|
|
760
|
+
return a.length < b.length ? -1 : 1;
|
|
761
|
+
}
|
|
762
|
+
/** Compare two semver strings. Returns -1/0/1, or undefined when either side is not valid semver. */
|
|
763
|
+
export function compareSemver(a, b) {
|
|
764
|
+
const left = parseSemver(a);
|
|
765
|
+
const right = parseSemver(b);
|
|
766
|
+
if (!left || !right)
|
|
767
|
+
return undefined;
|
|
768
|
+
if (left.major !== right.major)
|
|
769
|
+
return left.major < right.major ? -1 : 1;
|
|
770
|
+
if (left.minor !== right.minor)
|
|
771
|
+
return left.minor < right.minor ? -1 : 1;
|
|
772
|
+
if (left.patch !== right.patch)
|
|
773
|
+
return left.patch < right.patch ? -1 : 1;
|
|
774
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
775
|
+
}
|
|
776
|
+
export function isNewerVersion(latest, current) {
|
|
777
|
+
return compareSemver(latest, current) === 1;
|
|
778
|
+
}
|
|
779
|
+
/** Footer segment shown only when a newer published version exists; undefined otherwise. */
|
|
780
|
+
export function formatUpdateSegment(latest, current) {
|
|
781
|
+
if (!latest)
|
|
782
|
+
return undefined;
|
|
783
|
+
if (!isNewerVersion(latest, current))
|
|
784
|
+
return undefined;
|
|
785
|
+
return `\u2b06 v${latest} ${UPDATE_COMMAND}`;
|
|
786
|
+
}
|
|
787
|
+
//# sourceMappingURL=common.js.map
|