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,382 @@
|
|
|
1
|
+
import { DEFAULT_LOG_BYTES, normalizeMaxBytes, } from './common.js';
|
|
2
|
+
export const BG_REQUEST_CHANNEL = 'pi-background-tasks:request:v1';
|
|
3
|
+
export const BG_RESPONSE_CHANNEL = 'pi-background-tasks:response:v1';
|
|
4
|
+
export const BG_TERMINAL_CHANNEL = 'pi-background-tasks:terminal:v1';
|
|
5
|
+
export const BG_REQUEST_SCHEMA = 'pi-background-tasks.extension-request.v1';
|
|
6
|
+
export const BG_RESPONSE_SCHEMA = 'pi-background-tasks.extension-response.v1';
|
|
7
|
+
export const BG_TERMINAL_SCHEMA = 'pi-background-tasks.extension-terminal.v1';
|
|
8
|
+
export const BG_EXTENSION_SERVICE_CLOSED_CODE = 'pi_background_tasks_eventbus_closed';
|
|
9
|
+
export class BackgroundTaskExtensionServiceClosedError extends Error {
|
|
10
|
+
code = BG_EXTENSION_SERVICE_CLOSED_CODE;
|
|
11
|
+
constructor() {
|
|
12
|
+
super('pi-background-tasks EventBus service is closed');
|
|
13
|
+
this.name = 'BackgroundTaskExtensionServiceClosedError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const MAX_ERROR_CHARS = 240;
|
|
17
|
+
const MAX_REQUEST_ID_CHARS = 200;
|
|
18
|
+
export const BG_EXTENSION_CAPABILITIES = Object.freeze({
|
|
19
|
+
api_version: 1,
|
|
20
|
+
run: true,
|
|
21
|
+
run_is_agent: true,
|
|
22
|
+
run_completion_trigger: true,
|
|
23
|
+
status: true,
|
|
24
|
+
logs: true,
|
|
25
|
+
logs_bounded: true,
|
|
26
|
+
kill: true,
|
|
27
|
+
});
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
function hasOwn(record, key) {
|
|
32
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
33
|
+
}
|
|
34
|
+
function assertClosed(record, allowedKeys, label) {
|
|
35
|
+
const allowed = new Set(allowedKeys);
|
|
36
|
+
for (const key of Object.keys(record)) {
|
|
37
|
+
if (!allowed.has(key))
|
|
38
|
+
throw new Error(`${label} contains unknown key ${key}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function requireRecord(value, label) {
|
|
42
|
+
if (!isRecord(value))
|
|
43
|
+
throw new Error(`${label} must be an object`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function requireNonEmptyString(value, label) {
|
|
47
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
48
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function requireBoolean(value, label) {
|
|
53
|
+
if (typeof value !== 'boolean')
|
|
54
|
+
throw new Error(`${label} must be boolean`);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function requirePositiveInteger(value, label) {
|
|
58
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
59
|
+
throw new Error(`${label} must be a positive integer`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function requireOperation(value, label) {
|
|
64
|
+
if (value === 'capabilities' ||
|
|
65
|
+
value === 'run' ||
|
|
66
|
+
value === 'status' ||
|
|
67
|
+
value === 'logs' ||
|
|
68
|
+
value === 'kill') {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`${label} must be one of capabilities, run, status, logs, kill`);
|
|
72
|
+
}
|
|
73
|
+
function operationEcho(value) {
|
|
74
|
+
return typeof value === 'string' && value.length > 0 ? value : 'malformed';
|
|
75
|
+
}
|
|
76
|
+
function requestIdEcho(value) {
|
|
77
|
+
return typeof value === 'string' && value.length > 0 ? value : 'malformed';
|
|
78
|
+
}
|
|
79
|
+
function parseCapabilitiesPayload(value) {
|
|
80
|
+
const payload = requireRecord(value, 'capabilities.payload');
|
|
81
|
+
assertClosed(payload, [], 'capabilities.payload');
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
function parseRunPayload(value) {
|
|
85
|
+
const payload = requireRecord(value, 'run.payload');
|
|
86
|
+
assertClosed(payload, ['name', 'command', 'isAgent', 'timeoutSeconds', 'notifyOnCompletion', 'triggerOnCompletion'], 'run.payload');
|
|
87
|
+
const out = {
|
|
88
|
+
name: requireNonEmptyString(payload['name'], 'run.payload.name'),
|
|
89
|
+
command: requireNonEmptyString(payload['command'], 'run.payload.command'),
|
|
90
|
+
isAgent: requireBoolean(payload['isAgent'], 'run.payload.isAgent'),
|
|
91
|
+
notifyOnCompletion: requireBoolean(payload['notifyOnCompletion'], 'run.payload.notifyOnCompletion'),
|
|
92
|
+
triggerOnCompletion: requireBoolean(payload['triggerOnCompletion'], 'run.payload.triggerOnCompletion'),
|
|
93
|
+
};
|
|
94
|
+
if (hasOwn(payload, 'timeoutSeconds')) {
|
|
95
|
+
out.timeoutSeconds = requirePositiveInteger(payload['timeoutSeconds'], 'run.payload.timeoutSeconds');
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function parseStatusPayload(value) {
|
|
100
|
+
const payload = requireRecord(value, 'status.payload');
|
|
101
|
+
assertClosed(payload, ['taskId'], 'status.payload');
|
|
102
|
+
const out = {};
|
|
103
|
+
if (hasOwn(payload, 'taskId')) {
|
|
104
|
+
out.taskId = requireNonEmptyString(payload['taskId'], 'status.payload.taskId');
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function parseLogsPayload(value) {
|
|
109
|
+
const payload = requireRecord(value, 'logs.payload');
|
|
110
|
+
assertClosed(payload, ['taskId', 'maxBytes', 'tail'], 'logs.payload');
|
|
111
|
+
const out = {
|
|
112
|
+
taskId: requireNonEmptyString(payload['taskId'], 'logs.payload.taskId'),
|
|
113
|
+
};
|
|
114
|
+
if (hasOwn(payload, 'maxBytes')) {
|
|
115
|
+
out.maxBytes = requirePositiveInteger(payload['maxBytes'], 'logs.payload.maxBytes');
|
|
116
|
+
}
|
|
117
|
+
if (hasOwn(payload, 'tail'))
|
|
118
|
+
out.tail = requireBoolean(payload['tail'], 'logs.payload.tail');
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
function parseKillPayload(value) {
|
|
122
|
+
const payload = requireRecord(value, 'kill.payload');
|
|
123
|
+
assertClosed(payload, ['taskId'], 'kill.payload');
|
|
124
|
+
return { taskId: requireNonEmptyString(payload['taskId'], 'kill.payload.taskId') };
|
|
125
|
+
}
|
|
126
|
+
function parsePayload(operation, value) {
|
|
127
|
+
switch (operation) {
|
|
128
|
+
case 'capabilities':
|
|
129
|
+
return parseCapabilitiesPayload(value);
|
|
130
|
+
case 'run':
|
|
131
|
+
return parseRunPayload(value);
|
|
132
|
+
case 'status':
|
|
133
|
+
return parseStatusPayload(value);
|
|
134
|
+
case 'logs':
|
|
135
|
+
return parseLogsPayload(value);
|
|
136
|
+
case 'kill':
|
|
137
|
+
return parseKillPayload(value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function parseRequest(data) {
|
|
141
|
+
if (!isRecord(data)) {
|
|
142
|
+
return {
|
|
143
|
+
requestId: 'malformed',
|
|
144
|
+
operationEcho: 'malformed',
|
|
145
|
+
error: 'request frame must be an object',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const requestId = requestIdEcho(data['request_id']);
|
|
149
|
+
const opEcho = operationEcho(data['operation']);
|
|
150
|
+
try {
|
|
151
|
+
assertClosed(data, ['schema_version', 'request_id', 'operation', 'payload'], 'request');
|
|
152
|
+
if (data['schema_version'] !== BG_REQUEST_SCHEMA)
|
|
153
|
+
throw new Error('request schema_version mismatch');
|
|
154
|
+
const parsedRequestId = requireNonEmptyString(data['request_id'], 'request.request_id');
|
|
155
|
+
if (parsedRequestId.length > MAX_REQUEST_ID_CHARS) {
|
|
156
|
+
throw new Error(`request.request_id must be at most ${String(MAX_REQUEST_ID_CHARS)} characters`);
|
|
157
|
+
}
|
|
158
|
+
const operation = requireOperation(data['operation'], 'request.operation');
|
|
159
|
+
if (!hasOwn(data, 'payload'))
|
|
160
|
+
throw new Error('request.payload is required');
|
|
161
|
+
const payload = parsePayload(operation, data['payload']);
|
|
162
|
+
return {
|
|
163
|
+
requestId: parsedRequestId,
|
|
164
|
+
operationEcho: operation,
|
|
165
|
+
request: {
|
|
166
|
+
schema_version: BG_REQUEST_SCHEMA,
|
|
167
|
+
request_id: parsedRequestId,
|
|
168
|
+
operation,
|
|
169
|
+
payload,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return { requestId, operationEcho: opEcho, error: errorText(error) };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function createTerminalPublicationGate() {
|
|
178
|
+
let released = false;
|
|
179
|
+
let resolveGate = () => { };
|
|
180
|
+
const promise = new Promise((resolve) => {
|
|
181
|
+
resolveGate = resolve;
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
promise,
|
|
185
|
+
async releaseAfterResponse() {
|
|
186
|
+
if (released)
|
|
187
|
+
return;
|
|
188
|
+
released = true;
|
|
189
|
+
// Give response listeners one microtask turn to resolve their request promises
|
|
190
|
+
// and bind the returned task id before an early terminal event is emitted.
|
|
191
|
+
await Promise.resolve();
|
|
192
|
+
resolveGate();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function combineTerminalPublicationGates(existing, next) {
|
|
197
|
+
if (existing === undefined)
|
|
198
|
+
return next;
|
|
199
|
+
if (next === undefined)
|
|
200
|
+
return existing;
|
|
201
|
+
return Promise.all([existing, next]).then(() => undefined);
|
|
202
|
+
}
|
|
203
|
+
function errorText(error) {
|
|
204
|
+
return error instanceof Error ? error.message : String(error);
|
|
205
|
+
}
|
|
206
|
+
export function boundedBackgroundTaskError(error) {
|
|
207
|
+
const text = errorText(error).replace(/\s+/gu, ' ').trim();
|
|
208
|
+
if (text.length <= MAX_ERROR_CHARS)
|
|
209
|
+
return text;
|
|
210
|
+
return `${text.slice(0, MAX_ERROR_CHARS - 1)}…`;
|
|
211
|
+
}
|
|
212
|
+
function errorResponse(requestId, operation, error) {
|
|
213
|
+
return {
|
|
214
|
+
schema_version: BG_RESPONSE_SCHEMA,
|
|
215
|
+
request_id: requestId,
|
|
216
|
+
operation,
|
|
217
|
+
ok: false,
|
|
218
|
+
error: boundedBackgroundTaskError(error),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function successResponse(request, result) {
|
|
222
|
+
return {
|
|
223
|
+
schema_version: BG_RESPONSE_SCHEMA,
|
|
224
|
+
request_id: request.request_id,
|
|
225
|
+
operation: request.operation,
|
|
226
|
+
ok: true,
|
|
227
|
+
result,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function runPayload(value) {
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
function statusPayload(value) {
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
function logsPayload(value) {
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
function killPayload(value) {
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
class InstalledBackgroundTaskExtensionService {
|
|
243
|
+
events;
|
|
244
|
+
registry;
|
|
245
|
+
getContext;
|
|
246
|
+
isShuttingDown;
|
|
247
|
+
logger;
|
|
248
|
+
seenRequestIds = new Set();
|
|
249
|
+
unsubscribe;
|
|
250
|
+
serviceState = 'open';
|
|
251
|
+
constructor(options) {
|
|
252
|
+
this.events = options.events;
|
|
253
|
+
this.registry = options.registry;
|
|
254
|
+
this.getContext = options.getContext;
|
|
255
|
+
this.isShuttingDown = options.isShuttingDown;
|
|
256
|
+
this.logger = options.logger ?? console;
|
|
257
|
+
this.unsubscribe = this.events.on(BG_REQUEST_CHANNEL, (data) => {
|
|
258
|
+
void this.handle(data);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
get state() {
|
|
262
|
+
return this.serviceState;
|
|
263
|
+
}
|
|
264
|
+
isClosed() {
|
|
265
|
+
return this.serviceState === 'closed';
|
|
266
|
+
}
|
|
267
|
+
publishTerminal(task) {
|
|
268
|
+
if (this.serviceState === 'closed')
|
|
269
|
+
throw new BackgroundTaskExtensionServiceClosedError();
|
|
270
|
+
const terminal = {
|
|
271
|
+
schema_version: BG_TERMINAL_SCHEMA,
|
|
272
|
+
task,
|
|
273
|
+
};
|
|
274
|
+
this.events.emit(BG_TERMINAL_CHANNEL, terminal);
|
|
275
|
+
}
|
|
276
|
+
close() {
|
|
277
|
+
if (this.serviceState === 'closed')
|
|
278
|
+
return;
|
|
279
|
+
this.serviceState = 'closed';
|
|
280
|
+
this.registry.closeTerminalPublication('publisher_closed');
|
|
281
|
+
this.unsubscribe();
|
|
282
|
+
}
|
|
283
|
+
async handle(data) {
|
|
284
|
+
const parsed = parseRequest(data);
|
|
285
|
+
if (parsed.error !== undefined || parsed.request === undefined) {
|
|
286
|
+
this.emitResponse(errorResponse(parsed.requestId, parsed.operationEcho, parsed.error ?? 'malformed request'));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const request = parsed.request;
|
|
290
|
+
if (this.seenRequestIds.has(request.request_id)) {
|
|
291
|
+
this.emitResponse(errorResponse(request.request_id, request.operation, `duplicate request_id ${request.request_id}`));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.seenRequestIds.add(request.request_id);
|
|
295
|
+
const terminalGate = request.operation === 'run' || request.operation === 'kill'
|
|
296
|
+
? createTerminalPublicationGate()
|
|
297
|
+
: undefined;
|
|
298
|
+
try {
|
|
299
|
+
if (this.serviceState === 'closed')
|
|
300
|
+
throw new BackgroundTaskExtensionServiceClosedError();
|
|
301
|
+
if (this.isShuttingDown() || this.registry.isShuttingDown()) {
|
|
302
|
+
throw new Error('pi-background-tasks EventBus service is shutting down');
|
|
303
|
+
}
|
|
304
|
+
const ctx = this.getContext();
|
|
305
|
+
if (ctx === undefined) {
|
|
306
|
+
throw new Error('pi-background-tasks EventBus service is unavailable before session_start');
|
|
307
|
+
}
|
|
308
|
+
const result = await this.execute(ctx, request, terminalGate?.promise);
|
|
309
|
+
if (this.isClosed())
|
|
310
|
+
return;
|
|
311
|
+
if (this.isShuttingDown() || this.registry.isShuttingDown()) {
|
|
312
|
+
throw new Error('pi-background-tasks EventBus service is shutting down');
|
|
313
|
+
}
|
|
314
|
+
this.emitResponse(successResponse(request, result));
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
// A request accepted before close may report failure, but it must never
|
|
318
|
+
// report post-close success. Requests first emitted after close remain
|
|
319
|
+
// unhandled because the listener has already been removed.
|
|
320
|
+
this.emitResponse(errorResponse(request.request_id, request.operation, error));
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
await terminalGate?.releaseAfterResponse();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async execute(ctx, request, terminalPublicationGate) {
|
|
327
|
+
switch (request.operation) {
|
|
328
|
+
case 'capabilities':
|
|
329
|
+
return { ...BG_EXTENSION_CAPABILITIES };
|
|
330
|
+
case 'run': {
|
|
331
|
+
const payload = runPayload(request.payload);
|
|
332
|
+
const options = {
|
|
333
|
+
name: payload.name,
|
|
334
|
+
isAgent: payload.isAgent,
|
|
335
|
+
notifyOnCompletion: payload.notifyOnCompletion,
|
|
336
|
+
triggerOnCompletion: payload.triggerOnCompletion,
|
|
337
|
+
terminalPublicationGate,
|
|
338
|
+
};
|
|
339
|
+
if (payload.timeoutSeconds !== undefined)
|
|
340
|
+
options.timeoutSeconds = payload.timeoutSeconds;
|
|
341
|
+
const task = await this.registry.startTask(ctx, payload.command, options);
|
|
342
|
+
return this.registry.snapshot(task);
|
|
343
|
+
}
|
|
344
|
+
case 'status': {
|
|
345
|
+
const payload = statusPayload(request.payload);
|
|
346
|
+
const tasks = payload.taskId
|
|
347
|
+
? [this.registry.resolveTask(payload.taskId)]
|
|
348
|
+
: this.registry.allTasks();
|
|
349
|
+
return { tasks: tasks.map((task) => this.registry.snapshot(task)) };
|
|
350
|
+
}
|
|
351
|
+
case 'logs': {
|
|
352
|
+
const payload = logsPayload(request.payload);
|
|
353
|
+
const task = this.registry.resolveTask(payload.taskId);
|
|
354
|
+
const logs = await this.registry.getTaskLogs(task, normalizeMaxBytes(payload.maxBytes, DEFAULT_LOG_BYTES), payload.tail ?? true);
|
|
355
|
+
return { ...logs.details, text: logs.text };
|
|
356
|
+
}
|
|
357
|
+
case 'kill': {
|
|
358
|
+
const payload = killPayload(request.payload);
|
|
359
|
+
const task = this.registry.resolveTask(payload.taskId);
|
|
360
|
+
task.terminalPublicationGate = combineTerminalPublicationGates(task.terminalPublicationGate, terminalPublicationGate);
|
|
361
|
+
await this.registry.stopTask(task, 'user');
|
|
362
|
+
const snapshot = this.registry.snapshot(task);
|
|
363
|
+
return {
|
|
364
|
+
task: snapshot,
|
|
365
|
+
message: `Killed background task ${snapshot.name ?? snapshot.id} (${snapshot.id}). Output: ${snapshot.outputPath}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
emitResponse(response) {
|
|
371
|
+
try {
|
|
372
|
+
this.events.emit(BG_RESPONSE_CHANNEL, response);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
this.logger.error('[background-tasks] EventBus response emit failed:', error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
export function installBackgroundTaskExtensionApi(options) {
|
|
380
|
+
return new InstalledBackgroundTaskExtensionService(options);
|
|
381
|
+
}
|
|
382
|
+
//# sourceMappingURL=extension-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-api.js","sourceRoot":"","sources":["../../../src/core/extension-api.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EventBus } from '@earendil-works/pi-coding-agent';\nimport {\n DEFAULT_LOG_BYTES,\n normalizeMaxBytes,\n type BgLogsDetails,\n type BgTaskSnapshot,\n type JsonObject,\n type StartTaskOptions,\n} from './common.js';\nimport type { BackgroundTaskContext, BackgroundTaskRegistry } from './registry.js';\n\nexport const BG_REQUEST_CHANNEL = 'pi-background-tasks:request:v1';\nexport const BG_RESPONSE_CHANNEL = 'pi-background-tasks:response:v1';\nexport const BG_TERMINAL_CHANNEL = 'pi-background-tasks:terminal:v1';\nexport const BG_REQUEST_SCHEMA = 'pi-background-tasks.extension-request.v1';\nexport const BG_RESPONSE_SCHEMA = 'pi-background-tasks.extension-response.v1';\nexport const BG_TERMINAL_SCHEMA = 'pi-background-tasks.extension-terminal.v1';\nexport const BG_EXTENSION_SERVICE_CLOSED_CODE = 'pi_background_tasks_eventbus_closed';\n\nexport type BackgroundTaskExtensionServiceState = 'open' | 'closed';\n\nexport class BackgroundTaskExtensionServiceClosedError extends Error {\n readonly code = BG_EXTENSION_SERVICE_CLOSED_CODE;\n\n constructor() {\n super('pi-background-tasks EventBus service is closed');\n this.name = 'BackgroundTaskExtensionServiceClosedError';\n }\n}\n\nconst MAX_ERROR_CHARS = 240;\nconst MAX_REQUEST_ID_CHARS = 200;\n\nexport type BackgroundTaskExtensionOperation = 'capabilities' | 'run' | 'status' | 'logs' | 'kill';\n\nexport interface BackgroundTaskExtensionCapabilities {\n api_version: 1;\n run: boolean;\n run_is_agent: boolean;\n run_completion_trigger: boolean;\n status: boolean;\n logs: boolean;\n logs_bounded: boolean;\n kill: boolean;\n}\n\nexport const BG_EXTENSION_CAPABILITIES: BackgroundTaskExtensionCapabilities = Object.freeze({\n api_version: 1,\n run: true,\n run_is_agent: true,\n run_completion_trigger: true,\n status: true,\n logs: true,\n logs_bounded: true,\n kill: true,\n});\n\nexport interface BackgroundTaskExtensionRunPayload {\n name: string;\n command: string;\n isAgent: boolean;\n timeoutSeconds?: number | undefined;\n notifyOnCompletion: boolean;\n triggerOnCompletion: boolean;\n}\n\nexport interface BackgroundTaskExtensionStatusPayload {\n taskId?: string | undefined;\n}\n\nexport interface BackgroundTaskExtensionLogsPayload {\n taskId: string;\n maxBytes?: number | undefined;\n tail?: boolean | undefined;\n}\n\nexport interface BackgroundTaskExtensionKillPayload {\n taskId: string;\n}\n\nexport type BackgroundTaskExtensionPayload =\n | Record<PropertyKey, never>\n | BackgroundTaskExtensionRunPayload\n | BackgroundTaskExtensionStatusPayload\n | BackgroundTaskExtensionLogsPayload\n | BackgroundTaskExtensionKillPayload;\n\nexport interface BackgroundTaskExtensionRequest {\n schema_version: typeof BG_REQUEST_SCHEMA;\n request_id: string;\n operation: BackgroundTaskExtensionOperation;\n payload: BackgroundTaskExtensionPayload;\n}\n\nexport type BackgroundTaskExtensionResult =\n | BackgroundTaskExtensionCapabilities\n | BgTaskSnapshot\n | { tasks: BgTaskSnapshot[] }\n | (BgLogsDetails & { text: string })\n | { task: BgTaskSnapshot; message: string };\n\nexport type BackgroundTaskExtensionResponse =\n | {\n schema_version: typeof BG_RESPONSE_SCHEMA;\n request_id: string;\n operation: string;\n ok: true;\n result: BackgroundTaskExtensionResult;\n }\n | {\n schema_version: typeof BG_RESPONSE_SCHEMA;\n request_id: string;\n operation: string;\n ok: false;\n error: string;\n };\n\nexport interface BackgroundTaskExtensionTerminal {\n schema_version: typeof BG_TERMINAL_SCHEMA;\n task: BgTaskSnapshot;\n}\n\nexport interface BackgroundTaskExtensionService {\n readonly state: BackgroundTaskExtensionServiceState;\n publishTerminal(task: BgTaskSnapshot): void;\n close(): void;\n}\n\nexport interface BackgroundTaskExtensionServiceOptions {\n events: EventBus;\n registry: BackgroundTaskRegistry;\n getContext: () => BackgroundTaskContext | undefined;\n isShuttingDown: () => boolean;\n logger?: Pick<Console, 'error'> | undefined;\n}\n\ntype JsonRecord = JsonObject;\n\ninterface ParsedRequest {\n requestId: string;\n operationEcho: string;\n request?: BackgroundTaskExtensionRequest | undefined;\n error?: string | undefined;\n}\n\ninterface TerminalPublicationGate {\n promise: Promise<void>;\n releaseAfterResponse(): Promise<void>;\n}\n\nfunction isRecord(value: unknown): value is JsonRecord {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction hasOwn(record: JsonRecord, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(record, key);\n}\n\nfunction assertClosed(record: JsonRecord, allowedKeys: readonly string[], label: string): void {\n const allowed = new Set(allowedKeys);\n for (const key of Object.keys(record)) {\n if (!allowed.has(key)) throw new Error(`${label} contains unknown key ${key}`);\n }\n}\n\nfunction requireRecord(value: unknown, label: string): JsonRecord {\n if (!isRecord(value)) throw new Error(`${label} must be an object`);\n return value;\n}\n\nfunction requireNonEmptyString(value: unknown, label: string): string {\n if (typeof value !== 'string' || value.length === 0) {\n throw new Error(`${label} must be a non-empty string`);\n }\n return value;\n}\n\nfunction requireBoolean(value: unknown, label: string): boolean {\n if (typeof value !== 'boolean') throw new Error(`${label} must be boolean`);\n return value;\n}\n\nfunction requirePositiveInteger(value: unknown, label: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n return value;\n}\n\nfunction requireOperation(value: unknown, label: string): BackgroundTaskExtensionOperation {\n if (\n value === 'capabilities' ||\n value === 'run' ||\n value === 'status' ||\n value === 'logs' ||\n value === 'kill'\n ) {\n return value;\n }\n throw new Error(`${label} must be one of capabilities, run, status, logs, kill`);\n}\n\nfunction operationEcho(value: unknown): string {\n return typeof value === 'string' && value.length > 0 ? value : 'malformed';\n}\n\nfunction requestIdEcho(value: unknown): string {\n return typeof value === 'string' && value.length > 0 ? value : 'malformed';\n}\n\nfunction parseCapabilitiesPayload(value: unknown): Record<PropertyKey, never> {\n const payload = requireRecord(value, 'capabilities.payload');\n assertClosed(payload, [], 'capabilities.payload');\n return {};\n}\n\nfunction parseRunPayload(value: unknown): BackgroundTaskExtensionRunPayload {\n const payload = requireRecord(value, 'run.payload');\n assertClosed(\n payload,\n ['name', 'command', 'isAgent', 'timeoutSeconds', 'notifyOnCompletion', 'triggerOnCompletion'],\n 'run.payload',\n );\n const out: BackgroundTaskExtensionRunPayload = {\n name: requireNonEmptyString(payload['name'], 'run.payload.name'),\n command: requireNonEmptyString(payload['command'], 'run.payload.command'),\n isAgent: requireBoolean(payload['isAgent'], 'run.payload.isAgent'),\n notifyOnCompletion: requireBoolean(\n payload['notifyOnCompletion'],\n 'run.payload.notifyOnCompletion',\n ),\n triggerOnCompletion: requireBoolean(\n payload['triggerOnCompletion'],\n 'run.payload.triggerOnCompletion',\n ),\n };\n if (hasOwn(payload, 'timeoutSeconds')) {\n out.timeoutSeconds = requirePositiveInteger(\n payload['timeoutSeconds'],\n 'run.payload.timeoutSeconds',\n );\n }\n return out;\n}\n\nfunction parseStatusPayload(value: unknown): BackgroundTaskExtensionStatusPayload {\n const payload = requireRecord(value, 'status.payload');\n assertClosed(payload, ['taskId'], 'status.payload');\n const out: BackgroundTaskExtensionStatusPayload = {};\n if (hasOwn(payload, 'taskId')) {\n out.taskId = requireNonEmptyString(payload['taskId'], 'status.payload.taskId');\n }\n return out;\n}\n\nfunction parseLogsPayload(value: unknown): BackgroundTaskExtensionLogsPayload {\n const payload = requireRecord(value, 'logs.payload');\n assertClosed(payload, ['taskId', 'maxBytes', 'tail'], 'logs.payload');\n const out: BackgroundTaskExtensionLogsPayload = {\n taskId: requireNonEmptyString(payload['taskId'], 'logs.payload.taskId'),\n };\n if (hasOwn(payload, 'maxBytes')) {\n out.maxBytes = requirePositiveInteger(payload['maxBytes'], 'logs.payload.maxBytes');\n }\n if (hasOwn(payload, 'tail')) out.tail = requireBoolean(payload['tail'], 'logs.payload.tail');\n return out;\n}\n\nfunction parseKillPayload(value: unknown): BackgroundTaskExtensionKillPayload {\n const payload = requireRecord(value, 'kill.payload');\n assertClosed(payload, ['taskId'], 'kill.payload');\n return { taskId: requireNonEmptyString(payload['taskId'], 'kill.payload.taskId') };\n}\n\nfunction parsePayload(\n operation: BackgroundTaskExtensionOperation,\n value: unknown,\n): BackgroundTaskExtensionPayload {\n switch (operation) {\n case 'capabilities':\n return parseCapabilitiesPayload(value);\n case 'run':\n return parseRunPayload(value);\n case 'status':\n return parseStatusPayload(value);\n case 'logs':\n return parseLogsPayload(value);\n case 'kill':\n return parseKillPayload(value);\n }\n}\n\nfunction parseRequest(data: unknown): ParsedRequest {\n if (!isRecord(data)) {\n return {\n requestId: 'malformed',\n operationEcho: 'malformed',\n error: 'request frame must be an object',\n };\n }\n const requestId = requestIdEcho(data['request_id']);\n const opEcho = operationEcho(data['operation']);\n try {\n assertClosed(data, ['schema_version', 'request_id', 'operation', 'payload'], 'request');\n if (data['schema_version'] !== BG_REQUEST_SCHEMA)\n throw new Error('request schema_version mismatch');\n const parsedRequestId = requireNonEmptyString(data['request_id'], 'request.request_id');\n if (parsedRequestId.length > MAX_REQUEST_ID_CHARS) {\n throw new Error(\n `request.request_id must be at most ${String(MAX_REQUEST_ID_CHARS)} characters`,\n );\n }\n const operation = requireOperation(data['operation'], 'request.operation');\n if (!hasOwn(data, 'payload')) throw new Error('request.payload is required');\n const payload = parsePayload(operation, data['payload']);\n return {\n requestId: parsedRequestId,\n operationEcho: operation,\n request: {\n schema_version: BG_REQUEST_SCHEMA,\n request_id: parsedRequestId,\n operation,\n payload,\n },\n };\n } catch (error) {\n return { requestId, operationEcho: opEcho, error: errorText(error) };\n }\n}\n\nfunction createTerminalPublicationGate(): TerminalPublicationGate {\n let released = false;\n let resolveGate: () => void = () => {};\n const promise = new Promise<void>((resolve) => {\n resolveGate = resolve;\n });\n return {\n promise,\n async releaseAfterResponse() {\n if (released) return;\n released = true;\n // Give response listeners one microtask turn to resolve their request promises\n // and bind the returned task id before an early terminal event is emitted.\n await Promise.resolve();\n resolveGate();\n },\n };\n}\n\nfunction combineTerminalPublicationGates(\n existing: Promise<void> | undefined,\n next: Promise<void> | undefined,\n): Promise<void> | undefined {\n if (existing === undefined) return next;\n if (next === undefined) return existing;\n return Promise.all([existing, next]).then(() => undefined);\n}\n\nfunction errorText(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function boundedBackgroundTaskError(error: unknown): string {\n const text = errorText(error).replace(/\\s+/gu, ' ').trim();\n if (text.length <= MAX_ERROR_CHARS) return text;\n return `${text.slice(0, MAX_ERROR_CHARS - 1)}…`;\n}\n\nfunction errorResponse(\n requestId: string,\n operation: string,\n error: unknown,\n): BackgroundTaskExtensionResponse {\n return {\n schema_version: BG_RESPONSE_SCHEMA,\n request_id: requestId,\n operation,\n ok: false,\n error: boundedBackgroundTaskError(error),\n };\n}\n\nfunction successResponse(\n request: BackgroundTaskExtensionRequest,\n result: BackgroundTaskExtensionResult,\n): BackgroundTaskExtensionResponse {\n return {\n schema_version: BG_RESPONSE_SCHEMA,\n request_id: request.request_id,\n operation: request.operation,\n ok: true,\n result,\n };\n}\n\nfunction runPayload(value: BackgroundTaskExtensionPayload): BackgroundTaskExtensionRunPayload {\n return value as BackgroundTaskExtensionRunPayload;\n}\n\nfunction statusPayload(\n value: BackgroundTaskExtensionPayload,\n): BackgroundTaskExtensionStatusPayload {\n return value as BackgroundTaskExtensionStatusPayload;\n}\n\nfunction logsPayload(value: BackgroundTaskExtensionPayload): BackgroundTaskExtensionLogsPayload {\n return value as BackgroundTaskExtensionLogsPayload;\n}\n\nfunction killPayload(value: BackgroundTaskExtensionPayload): BackgroundTaskExtensionKillPayload {\n return value as BackgroundTaskExtensionKillPayload;\n}\n\nclass InstalledBackgroundTaskExtensionService implements BackgroundTaskExtensionService {\n private readonly events: EventBus;\n private readonly registry: BackgroundTaskRegistry;\n private readonly getContext: () => BackgroundTaskContext | undefined;\n private readonly isShuttingDown: () => boolean;\n private readonly logger: Pick<Console, 'error'>;\n private readonly seenRequestIds = new Set<string>();\n private readonly unsubscribe: () => void;\n private serviceState: BackgroundTaskExtensionServiceState = 'open';\n\n constructor(options: BackgroundTaskExtensionServiceOptions) {\n this.events = options.events;\n this.registry = options.registry;\n this.getContext = options.getContext;\n this.isShuttingDown = options.isShuttingDown;\n this.logger = options.logger ?? console;\n this.unsubscribe = this.events.on(BG_REQUEST_CHANNEL, (data) => {\n void this.handle(data);\n });\n }\n\n get state(): BackgroundTaskExtensionServiceState {\n return this.serviceState;\n }\n\n private isClosed(): boolean {\n return this.serviceState === 'closed';\n }\n\n publishTerminal(task: BgTaskSnapshot): void {\n if (this.serviceState === 'closed') throw new BackgroundTaskExtensionServiceClosedError();\n const terminal: BackgroundTaskExtensionTerminal = {\n schema_version: BG_TERMINAL_SCHEMA,\n task,\n };\n this.events.emit(BG_TERMINAL_CHANNEL, terminal);\n }\n\n close(): void {\n if (this.serviceState === 'closed') return;\n this.serviceState = 'closed';\n this.registry.closeTerminalPublication('publisher_closed');\n this.unsubscribe();\n }\n\n private async handle(data: unknown): Promise<void> {\n const parsed = parseRequest(data);\n if (parsed.error !== undefined || parsed.request === undefined) {\n this.emitResponse(\n errorResponse(parsed.requestId, parsed.operationEcho, parsed.error ?? 'malformed request'),\n );\n return;\n }\n const request = parsed.request;\n if (this.seenRequestIds.has(request.request_id)) {\n this.emitResponse(\n errorResponse(\n request.request_id,\n request.operation,\n `duplicate request_id ${request.request_id}`,\n ),\n );\n return;\n }\n this.seenRequestIds.add(request.request_id);\n const terminalGate =\n request.operation === 'run' || request.operation === 'kill'\n ? createTerminalPublicationGate()\n : undefined;\n try {\n if (this.serviceState === 'closed') throw new BackgroundTaskExtensionServiceClosedError();\n if (this.isShuttingDown() || this.registry.isShuttingDown()) {\n throw new Error('pi-background-tasks EventBus service is shutting down');\n }\n const ctx = this.getContext();\n if (ctx === undefined) {\n throw new Error('pi-background-tasks EventBus service is unavailable before session_start');\n }\n const result = await this.execute(ctx, request, terminalGate?.promise);\n if (this.isClosed()) return;\n if (this.isShuttingDown() || this.registry.isShuttingDown()) {\n throw new Error('pi-background-tasks EventBus service is shutting down');\n }\n this.emitResponse(successResponse(request, result));\n } catch (error) {\n // A request accepted before close may report failure, but it must never\n // report post-close success. Requests first emitted after close remain\n // unhandled because the listener has already been removed.\n this.emitResponse(errorResponse(request.request_id, request.operation, error));\n } finally {\n await terminalGate?.releaseAfterResponse();\n }\n }\n\n private async execute(\n ctx: BackgroundTaskContext,\n request: BackgroundTaskExtensionRequest,\n terminalPublicationGate?: Promise<void> | undefined,\n ): Promise<BackgroundTaskExtensionResult> {\n switch (request.operation) {\n case 'capabilities':\n return { ...BG_EXTENSION_CAPABILITIES };\n case 'run': {\n const payload = runPayload(request.payload);\n const options: StartTaskOptions = {\n name: payload.name,\n isAgent: payload.isAgent,\n notifyOnCompletion: payload.notifyOnCompletion,\n triggerOnCompletion: payload.triggerOnCompletion,\n terminalPublicationGate,\n };\n if (payload.timeoutSeconds !== undefined) options.timeoutSeconds = payload.timeoutSeconds;\n const task = await this.registry.startTask(ctx, payload.command, options);\n return this.registry.snapshot(task);\n }\n case 'status': {\n const payload = statusPayload(request.payload);\n const tasks = payload.taskId\n ? [this.registry.resolveTask(payload.taskId)]\n : this.registry.allTasks();\n return { tasks: tasks.map((task) => this.registry.snapshot(task)) };\n }\n case 'logs': {\n const payload = logsPayload(request.payload);\n const task = this.registry.resolveTask(payload.taskId);\n const logs = await this.registry.getTaskLogs(\n task,\n normalizeMaxBytes(payload.maxBytes, DEFAULT_LOG_BYTES),\n payload.tail ?? true,\n );\n return { ...logs.details, text: logs.text };\n }\n case 'kill': {\n const payload = killPayload(request.payload);\n const task = this.registry.resolveTask(payload.taskId);\n task.terminalPublicationGate = combineTerminalPublicationGates(\n task.terminalPublicationGate,\n terminalPublicationGate,\n );\n await this.registry.stopTask(task, 'user');\n const snapshot = this.registry.snapshot(task);\n return {\n task: snapshot,\n message: `Killed background task ${snapshot.name ?? snapshot.id} (${snapshot.id}). Output: ${snapshot.outputPath}`,\n };\n }\n }\n }\n\n private emitResponse(response: BackgroundTaskExtensionResponse): void {\n try {\n this.events.emit(BG_RESPONSE_CHANNEL, response);\n } catch (error) {\n this.logger.error('[background-tasks] EventBus response emit failed:', error);\n }\n }\n}\n\nexport function installBackgroundTaskExtensionApi(\n options: BackgroundTaskExtensionServiceOptions,\n): BackgroundTaskExtensionService {\n return new InstalledBackgroundTaskExtensionService(options);\n}\n"]}
|