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,121 @@
|
|
|
1
|
+
export const PI_BG_FEATURE_VALUES = Object.freeze([
|
|
2
|
+
'process',
|
|
3
|
+
'delegate',
|
|
4
|
+
'fusion',
|
|
5
|
+
'attested',
|
|
6
|
+
'attribution',
|
|
7
|
+
] as const);
|
|
8
|
+
export type PiBackgroundFeature = (typeof PI_BG_FEATURE_VALUES)[number];
|
|
9
|
+
|
|
10
|
+
export const PI_BG_DEFAULT_FEATURES = PI_BG_FEATURE_VALUES;
|
|
11
|
+
|
|
12
|
+
export const PI_BG_DOCK_SHORTCUT_VALUES = Object.freeze([
|
|
13
|
+
'shift+down',
|
|
14
|
+
'ctrl+alt+b',
|
|
15
|
+
'off',
|
|
16
|
+
] as const);
|
|
17
|
+
export type PiBackgroundDockShortcut = (typeof PI_BG_DOCK_SHORTCUT_VALUES)[number];
|
|
18
|
+
|
|
19
|
+
export const PI_BG_DEFAULT_DOCK_SHORTCUT: PiBackgroundDockShortcut = 'shift+down';
|
|
20
|
+
|
|
21
|
+
export interface PiBackgroundFeatureSelection {
|
|
22
|
+
readonly process: true;
|
|
23
|
+
readonly delegate: boolean;
|
|
24
|
+
readonly fusion: boolean;
|
|
25
|
+
readonly attested: boolean;
|
|
26
|
+
readonly attribution: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PiBackgroundConfig {
|
|
30
|
+
readonly features: PiBackgroundFeatureSelection;
|
|
31
|
+
readonly dockShortcut: PiBackgroundDockShortcut;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CONFIG_VALUE_EXCERPT_CHARS = 96;
|
|
35
|
+
|
|
36
|
+
function boundedConfigValue(value: string): string {
|
|
37
|
+
if (value.length <= CONFIG_VALUE_EXCERPT_CHARS) return JSON.stringify(value);
|
|
38
|
+
return `${JSON.stringify(value.slice(0, CONFIG_VALUE_EXCERPT_CHARS))}… (${String(value.length)} chars)`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function invalidConfig(variable: string, reason: string, value: string): never {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`pi_bg_config_invalid: ${variable} ${reason}; received ${boundedConfigValue(value)}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseFeatures(rawValue: string | undefined): PiBackgroundFeatureSelection {
|
|
48
|
+
const raw = rawValue ?? PI_BG_DEFAULT_FEATURES.join(',');
|
|
49
|
+
const accepted = PI_BG_FEATURE_VALUES.join(',');
|
|
50
|
+
if (raw.length === 0) {
|
|
51
|
+
invalidConfig('PI_BG_FEATURES', `must not be empty; accepted tokens: ${accepted}`, raw);
|
|
52
|
+
}
|
|
53
|
+
if (/\s/u.test(raw)) {
|
|
54
|
+
invalidConfig(
|
|
55
|
+
'PI_BG_FEATURES',
|
|
56
|
+
`must contain no whitespace; accepted tokens: ${accepted}`,
|
|
57
|
+
raw,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const tokens = raw.split(',');
|
|
61
|
+
if (tokens.some((token) => token.length === 0)) {
|
|
62
|
+
invalidConfig(
|
|
63
|
+
'PI_BG_FEATURES',
|
|
64
|
+
`contains a blank comma token; accepted tokens: ${accepted}`,
|
|
65
|
+
raw,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const selected = new Set<PiBackgroundFeature>();
|
|
70
|
+
for (const token of tokens) {
|
|
71
|
+
if (!(PI_BG_FEATURE_VALUES as readonly string[]).includes(token)) {
|
|
72
|
+
invalidConfig(
|
|
73
|
+
'PI_BG_FEATURES',
|
|
74
|
+
`contains unknown token ${boundedConfigValue(token)}; accepted tokens: ${accepted}; bg_result is derived from delegate or fusion`,
|
|
75
|
+
raw,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const feature = token as PiBackgroundFeature;
|
|
79
|
+
if (selected.has(feature)) {
|
|
80
|
+
invalidConfig('PI_BG_FEATURES', `contains duplicate token ${feature}`, raw);
|
|
81
|
+
}
|
|
82
|
+
selected.add(feature);
|
|
83
|
+
}
|
|
84
|
+
if (!selected.has('process')) {
|
|
85
|
+
invalidConfig('PI_BG_FEATURES', 'must include mandatory token process', raw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
process: true,
|
|
90
|
+
delegate: selected.has('delegate'),
|
|
91
|
+
fusion: selected.has('fusion'),
|
|
92
|
+
attested: selected.has('attested'),
|
|
93
|
+
attribution: selected.has('attribution'),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseDockShortcut(rawValue: string | undefined): PiBackgroundDockShortcut {
|
|
98
|
+
const raw = rawValue ?? PI_BG_DEFAULT_DOCK_SHORTCUT;
|
|
99
|
+
if (!(PI_BG_DOCK_SHORTCUT_VALUES as readonly string[]).includes(raw)) {
|
|
100
|
+
invalidConfig(
|
|
101
|
+
'PI_BG_DOCK_SHORTCUT',
|
|
102
|
+
`accepted values are exactly ${PI_BG_DOCK_SHORTCUT_VALUES.join(',')}`,
|
|
103
|
+
raw,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return raw as PiBackgroundDockShortcut;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function parseBackgroundTasksConfig(
|
|
110
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
111
|
+
): PiBackgroundConfig {
|
|
112
|
+
const features = parseFeatures(env['PI_BG_FEATURES']);
|
|
113
|
+
const dockShortcut = parseDockShortcut(env['PI_BG_DOCK_SHORTCUT']);
|
|
114
|
+
return Object.freeze({ features, dockShortcut });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function dockShortcutFooterHint(shortcut: PiBackgroundDockShortcut): string {
|
|
118
|
+
if (shortcut === 'shift+down') return 'Shift↓';
|
|
119
|
+
if (shortcut === 'ctrl+alt+b') return 'CtrlAltB';
|
|
120
|
+
return '/tasks';
|
|
121
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import type { Message } from '@earendil-works/pi-ai';
|
|
3
|
-
import { canonicalJson } from '../
|
|
3
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Frozen `visible-conversation-ledger-v2` transform.
|
|
@@ -22,8 +22,7 @@ import { canonicalJson } from '../attested-pi-run.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export const VISIBLE_CONVERSATION_TRANSFORM_ID = 'visible-conversation-ledger-v2';
|
|
24
24
|
|
|
25
|
-
export const CONVERSATION_IMAGE_OMISSION_PREFIX =
|
|
26
|
-
'[Image omitted from fusion text transcript: ';
|
|
25
|
+
export const CONVERSATION_IMAGE_OMISSION_PREFIX = '[Image omitted from fusion text transcript: ';
|
|
27
26
|
|
|
28
27
|
export const OMITTED_EVENT_KINDS = [
|
|
29
28
|
'assistant_thinking',
|
|
@@ -141,9 +140,7 @@ export class UnsupportedConversationBlockError extends Error {
|
|
|
141
140
|
readonly label: string;
|
|
142
141
|
|
|
143
142
|
constructor(label: string) {
|
|
144
|
-
super(
|
|
145
|
-
`fusion context projection encountered an unsupported conversation block: ${label}`,
|
|
146
|
-
);
|
|
143
|
+
super(`fusion context projection encountered an unsupported conversation block: ${label}`);
|
|
147
144
|
this.name = 'UnsupportedConversationBlockError';
|
|
148
145
|
this.label = label;
|
|
149
146
|
}
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { chmod, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
|
|
4
4
|
import { basename, isAbsolute, join, relative, sep } from 'node:path';
|
|
5
|
-
import { canonicalJson } from '../
|
|
5
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
6
6
|
import { sanitizePathSegment } from '../common.js';
|
|
7
7
|
import { replaceFileDurable, writeFileDurable } from '../durable-fs.js';
|
|
8
8
|
import {
|
|
@@ -92,11 +92,14 @@ function sha256Bytes(bytes: Buffer): string {
|
|
|
92
92
|
|
|
93
93
|
function pathInside(parent: string, child: string): boolean {
|
|
94
94
|
const rel = relative(parent, child);
|
|
95
|
-
return
|
|
95
|
+
return (
|
|
96
|
+
rel === '' || (!rel.startsWith('..') && !isAbsolute(rel) && !rel.split(sep).includes('..'))
|
|
97
|
+
);
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
function artifactError(message: string, cause?: unknown): DelegateError {
|
|
99
|
-
const suffix =
|
|
101
|
+
const suffix =
|
|
102
|
+
cause === undefined ? '' : `: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
100
103
|
return new DelegateError(`${message}${suffix}`, {
|
|
101
104
|
code: 'artifact_error',
|
|
102
105
|
childCreated: true,
|
|
@@ -150,9 +153,7 @@ export class DelegateArtifactStore {
|
|
|
150
153
|
* directory is created with `recursive: false` so a pre-existing directory is
|
|
151
154
|
* a loud failure rather than a silent reuse.
|
|
152
155
|
*/
|
|
153
|
-
static async create(
|
|
154
|
-
options: CreateDelegateArtifactStoreOptions,
|
|
155
|
-
): Promise<DelegateArtifactStore> {
|
|
156
|
+
static async create(options: CreateDelegateArtifactStoreOptions): Promise<DelegateArtifactStore> {
|
|
156
157
|
const sessionSegment = sanitizePathSegment(
|
|
157
158
|
options.sessionId ?? `session-${String(process.pid)}`,
|
|
158
159
|
);
|
|
@@ -260,9 +261,12 @@ export class DelegateArtifactStore {
|
|
|
260
261
|
taskId: this.manifest.task_id,
|
|
261
262
|
artifactDir: this.rootDisplay,
|
|
262
263
|
preserved: diagnosticNames,
|
|
263
|
-
remediation:
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
remediation:
|
|
265
|
+
diagnosticNames.length === 0
|
|
266
|
+
? [
|
|
267
|
+
'No diagnostic control artifact exists; inspect the background task merged output if one was created.',
|
|
268
|
+
]
|
|
269
|
+
: [`Inspect the existing delegate control artifacts: ${diagnosticNames.join(', ')}.`],
|
|
266
270
|
},
|
|
267
271
|
);
|
|
268
272
|
}
|
|
@@ -14,6 +14,12 @@ import {
|
|
|
14
14
|
type TokenBudgetFamilyCalibration,
|
|
15
15
|
type TokenBudgetRateSource,
|
|
16
16
|
} from '../context/token-budget.js';
|
|
17
|
+
import {
|
|
18
|
+
DELEGATE_DEFAULT_MAX_TOOL_CALLS,
|
|
19
|
+
DELEGATE_DEFAULT_MAX_TURNS,
|
|
20
|
+
DELEGATE_DEFAULT_TIMEOUT_SECONDS,
|
|
21
|
+
DELEGATE_INLINE_ANSWER_BYTES,
|
|
22
|
+
} from './facade-contract.js';
|
|
17
23
|
import {
|
|
18
24
|
DELEGATE_BUDGET_PLAN_SCHEMA_VERSION,
|
|
19
25
|
DelegateError,
|
|
@@ -21,6 +27,13 @@ import {
|
|
|
21
27
|
type DelegatePinnedRoute,
|
|
22
28
|
} from './types.js';
|
|
23
29
|
|
|
30
|
+
export {
|
|
31
|
+
DELEGATE_DEFAULT_MAX_TOOL_CALLS,
|
|
32
|
+
DELEGATE_DEFAULT_MAX_TURNS,
|
|
33
|
+
DELEGATE_DEFAULT_TIMEOUT_SECONDS,
|
|
34
|
+
DELEGATE_INLINE_ANSWER_BYTES,
|
|
35
|
+
} from './facade-contract.js';
|
|
36
|
+
|
|
24
37
|
/**
|
|
25
38
|
* Delegate budgeting.
|
|
26
39
|
*
|
|
@@ -51,9 +64,6 @@ export const DELEGATE_MIN_CONTEXT_WINDOW_TOKENS =
|
|
|
51
64
|
DELEGATE_FRAMING_RESERVE_TOKENS +
|
|
52
65
|
DELEGATE_SAFETY_RESERVE_TOKENS;
|
|
53
66
|
|
|
54
|
-
export const DELEGATE_DEFAULT_MAX_TURNS = 24;
|
|
55
|
-
export const DELEGATE_DEFAULT_MAX_TOOL_CALLS = 120;
|
|
56
|
-
export const DELEGATE_DEFAULT_TIMEOUT_SECONDS = 1200;
|
|
57
67
|
export const DELEGATE_MAX_TOOL_RESULT_BYTES = 64 * 1024;
|
|
58
68
|
export const DELEGATE_MAX_TOTAL_TOOL_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
59
69
|
export const DELEGATE_MAX_ANSWER_BYTES = 4 * 1024 * 1024;
|
|
@@ -61,9 +71,6 @@ export const DELEGATE_MAX_ANSWER_BYTES = 4 * 1024 * 1024;
|
|
|
61
71
|
export const DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS = 32 * 1024;
|
|
62
72
|
/** Remaining retained-growth runway at which the child disables tools. */
|
|
63
73
|
export const DELEGATE_FINALIZATION_TRIGGER_TOKENS = 8 * 1024;
|
|
64
|
-
/** Answers at or under this serialize inline; larger ones degrade explicitly. */
|
|
65
|
-
export const DELEGATE_INLINE_ANSWER_BYTES = 48 * 1024;
|
|
66
|
-
|
|
67
74
|
export const DELEGATE_BUDGET_POLICY_ID = 'delegate-budget-policy-v3';
|
|
68
75
|
|
|
69
76
|
export interface DelegateBudgetPolicyDescriptor {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Light registration constants shared by the delegate facade and budget engine. */
|
|
2
|
+
export const DELEGATE_DEFAULT_MAX_TURNS = 24;
|
|
3
|
+
export const DELEGATE_DEFAULT_MAX_TOOL_CALLS = 120;
|
|
4
|
+
export const DELEGATE_DEFAULT_TIMEOUT_SECONDS = 1200;
|
|
5
|
+
/** Answers at or under this serialize inline; larger ones degrade explicitly. */
|
|
6
|
+
export const DELEGATE_INLINE_ANSWER_BYTES = 48 * 1024;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { canonicalJson } from '../
|
|
2
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
3
3
|
import {
|
|
4
4
|
DELEGATE_RECEIPT_SCHEMA_VERSION,
|
|
5
5
|
DELEGATE_RESULT_PACKAGE_SCHEMA_VERSION,
|
|
@@ -87,9 +87,7 @@ export function buildDelegateResultPackage(
|
|
|
87
87
|
data_base64: bytes.toString('base64'),
|
|
88
88
|
};
|
|
89
89
|
});
|
|
90
|
-
const aggregate = Buffer.concat(
|
|
91
|
-
blocks.map((block) => Buffer.from(block.data_base64, 'base64')),
|
|
92
|
-
);
|
|
90
|
+
const aggregate = Buffer.concat(blocks.map((block) => Buffer.from(block.data_base64, 'base64')));
|
|
93
91
|
return {
|
|
94
92
|
schema_version: DELEGATE_RESULT_PACKAGE_SCHEMA_VERSION,
|
|
95
93
|
task_id: input.taskId,
|
|
@@ -183,7 +181,11 @@ function parseUsage(value: unknown, taskId: string): DelegateUsageReport {
|
|
|
183
181
|
);
|
|
184
182
|
const usage = value['usage'];
|
|
185
183
|
if (!isRecord(usage))
|
|
186
|
-
fail(
|
|
184
|
+
fail(
|
|
185
|
+
'delegate result package observed usage must be an object',
|
|
186
|
+
'child_result_invalid',
|
|
187
|
+
taskId,
|
|
188
|
+
);
|
|
187
189
|
const cost = usage['cost'];
|
|
188
190
|
if (!isRecord(cost))
|
|
189
191
|
fail(
|
|
@@ -220,10 +222,7 @@ function parseUsage(value: unknown, taskId: string): DelegateUsageReport {
|
|
|
220
222
|
};
|
|
221
223
|
}
|
|
222
224
|
|
|
223
|
-
function parseAttestations(
|
|
224
|
-
value: unknown,
|
|
225
|
-
taskId: string,
|
|
226
|
-
): readonly DelegateRouteAttestation[] {
|
|
225
|
+
function parseAttestations(value: unknown, taskId: string): readonly DelegateRouteAttestation[] {
|
|
227
226
|
if (!Array.isArray(value))
|
|
228
227
|
fail(
|
|
229
228
|
'delegate result package route_attestations must be an array',
|
|
@@ -264,7 +263,11 @@ function parseSpillContentFormat(
|
|
|
264
263
|
|
|
265
264
|
function parseSpillReceipts(value: unknown, taskId: string): readonly DelegateSpillReceipt[] {
|
|
266
265
|
if (!Array.isArray(value))
|
|
267
|
-
fail(
|
|
266
|
+
fail(
|
|
267
|
+
'delegate result package spilled_artifacts must be an array',
|
|
268
|
+
'child_result_invalid',
|
|
269
|
+
taskId,
|
|
270
|
+
);
|
|
268
271
|
return value.map((entry) => {
|
|
269
272
|
if (!isRecord(entry))
|
|
270
273
|
fail('delegate spill receipt must be an object', 'child_result_invalid', taskId);
|
|
@@ -332,7 +335,11 @@ export function verifyDelegateResultPackage(
|
|
|
332
335
|
|
|
333
336
|
const routeRecord = parsed['route'];
|
|
334
337
|
if (!isRecord(routeRecord))
|
|
335
|
-
fail(
|
|
338
|
+
fail(
|
|
339
|
+
'delegate result package route must be an object',
|
|
340
|
+
'child_result_invalid',
|
|
341
|
+
expected.taskId,
|
|
342
|
+
);
|
|
336
343
|
const provider = requireString(routeRecord, 'provider', expected.taskId);
|
|
337
344
|
const model = requireString(routeRecord, 'model', expected.taskId);
|
|
338
345
|
if (provider !== expected.route.provider || model !== expected.route.model)
|
|
@@ -343,7 +350,10 @@ export function verifyDelegateResultPackage(
|
|
|
343
350
|
);
|
|
344
351
|
const attestations = parseAttestations(parsed['route_attestations'], expected.taskId);
|
|
345
352
|
for (const attestation of attestations) {
|
|
346
|
-
if (
|
|
353
|
+
if (
|
|
354
|
+
attestation.provider !== expected.route.provider ||
|
|
355
|
+
attestation.model !== expected.route.model
|
|
356
|
+
)
|
|
347
357
|
fail(
|
|
348
358
|
`delegate child produced an assistant message on ${attestation.provider}/${attestation.model}, but the pinned route is ${expected.route.provider}/${expected.route.model}`,
|
|
349
359
|
'route_mismatch',
|
|
@@ -353,7 +363,11 @@ export function verifyDelegateResultPackage(
|
|
|
353
363
|
|
|
354
364
|
const answerRecord = parsed['answer'];
|
|
355
365
|
if (!isRecord(answerRecord))
|
|
356
|
-
fail(
|
|
366
|
+
fail(
|
|
367
|
+
'delegate result package answer must be an object',
|
|
368
|
+
'child_result_invalid',
|
|
369
|
+
expected.taskId,
|
|
370
|
+
);
|
|
357
371
|
if (answerRecord['encoding'] !== 'utf-8')
|
|
358
372
|
fail(
|
|
359
373
|
'delegate result package answer encoding must be utf-8',
|
|
@@ -364,7 +378,11 @@ export function verifyDelegateResultPackage(
|
|
|
364
378
|
const declaredSha = requireSha256(answerRecord, 'sha256', expected.taskId);
|
|
365
379
|
const blocksValue = answerRecord['blocks'];
|
|
366
380
|
if (!Array.isArray(blocksValue))
|
|
367
|
-
fail(
|
|
381
|
+
fail(
|
|
382
|
+
'delegate result package answer blocks must be an array',
|
|
383
|
+
'child_result_invalid',
|
|
384
|
+
expected.taskId,
|
|
385
|
+
);
|
|
368
386
|
|
|
369
387
|
const blocks: DelegateAnswerBlock[] = [];
|
|
370
388
|
const buffers: Buffer[] = [];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { canonicalJson } from '../
|
|
2
|
+
import { readFile, rmdir } from 'node:fs/promises';
|
|
3
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
4
4
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
5
5
|
import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attribution-path.js';
|
|
6
|
-
import { join } from 'node:path';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
7
|
import { DelegateArtifactStore, discardDelegateArtifactRoot } from './artifacts.js';
|
|
8
8
|
import {
|
|
9
9
|
buildDelegateChildArgv,
|
|
@@ -14,9 +14,7 @@ import {
|
|
|
14
14
|
type DelegatePreflightInput,
|
|
15
15
|
type DelegatePreflightResult,
|
|
16
16
|
} from './launch.js';
|
|
17
|
-
import {
|
|
18
|
-
DELEGATE_INLINE_ANSWER_BYTES,
|
|
19
|
-
} from './budget.js';
|
|
17
|
+
import { DELEGATE_INLINE_ANSWER_BYTES } from './budget.js';
|
|
20
18
|
import { verifyDelegateResultPackage, type VerifiedDelegateResult } from './result-package.js';
|
|
21
19
|
import {
|
|
22
20
|
DelegateError,
|
|
@@ -44,6 +42,8 @@ export interface PreparedDelegateLaunch {
|
|
|
44
42
|
seedPathAbs: string;
|
|
45
43
|
/** Exact prompt bytes delivered to the child over stdin. */
|
|
46
44
|
stdinBytes: Buffer;
|
|
45
|
+
/** Remove this complete preparation while it is still unowned by a task. */
|
|
46
|
+
rollback: () => Promise<void>;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export interface PrepareDelegateLaunchInput extends DelegatePreflightInput {
|
|
@@ -55,6 +55,57 @@ export interface PrepareDelegateLaunchInput extends DelegatePreflightInput {
|
|
|
55
55
|
attributionExtensionPath?: string | undefined;
|
|
56
56
|
env?: NodeJS.ProcessEnv | undefined;
|
|
57
57
|
now?: (() => Date) | undefined;
|
|
58
|
+
/** Activation closure cancels preparation before task ownership transfers. */
|
|
59
|
+
signal?: AbortSignal | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const PREPARATION_CLEANUP_ERROR_MAX_CHARS = 320;
|
|
63
|
+
|
|
64
|
+
function boundedPreparationError(error: unknown): string {
|
|
65
|
+
const text = (error instanceof Error ? error.message : String(error))
|
|
66
|
+
.replace(/\s+/gu, ' ')
|
|
67
|
+
.trim();
|
|
68
|
+
if (text.length <= PREPARATION_CLEANUP_ERROR_MAX_CHARS) return text;
|
|
69
|
+
return `${text.slice(0, PREPARATION_CLEANUP_ERROR_MAX_CHARS)}…`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function filesystemErrorCode(error: unknown): string | undefined {
|
|
73
|
+
if (typeof error !== 'object' || error === null) return undefined;
|
|
74
|
+
const code: unknown = Reflect.get(error, 'code');
|
|
75
|
+
return typeof code === 'string' ? code : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function removeEmptyDirectory(path: string): Promise<boolean> {
|
|
79
|
+
try {
|
|
80
|
+
await rmdir(path);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const code = filesystemErrorCode(error);
|
|
84
|
+
if (code === 'ENOENT') return true;
|
|
85
|
+
if (code === 'ENOTEMPTY' || code === 'EEXIST') return false;
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function discardUnownedDelegatePreparation(rootAbs: string): Promise<void> {
|
|
91
|
+
await discardDelegateArtifactRoot(rootAbs);
|
|
92
|
+
const runParent = dirname(rootAbs);
|
|
93
|
+
if (!(await removeEmptyDirectory(runParent))) return;
|
|
94
|
+
await removeEmptyDirectory(dirname(runParent));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function throwIfPreparationAborted(signal: AbortSignal | undefined): void {
|
|
98
|
+
if (signal?.aborted !== true) return;
|
|
99
|
+
const reason: unknown = signal.reason;
|
|
100
|
+
if (reason instanceof Error) throw reason;
|
|
101
|
+
throw new Error('delegate launch preparation was cancelled before task registration');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function preparationCleanupFailure(original: unknown, cleanup: unknown): AggregateError {
|
|
105
|
+
return new AggregateError(
|
|
106
|
+
[original, cleanup],
|
|
107
|
+
`delegate launch preparation failed and rollback also failed: ${boundedPreparationError(cleanup)}`,
|
|
108
|
+
);
|
|
58
109
|
}
|
|
59
110
|
|
|
60
111
|
/**
|
|
@@ -62,16 +113,16 @@ export interface PrepareDelegateLaunchInput extends DelegatePreflightInput {
|
|
|
62
113
|
*
|
|
63
114
|
* Preflight runs first and completes entirely before the artifact directory is
|
|
64
115
|
* created, so every admission refusal leaves zero children AND zero artifacts.
|
|
65
|
-
* When a step after directory creation fails
|
|
66
|
-
*
|
|
116
|
+
* When a step after directory creation fails or activation cancellation is
|
|
117
|
+
* observed, the producer removes the unowned directory before rejecting.
|
|
67
118
|
*/
|
|
68
119
|
export async function prepareDelegateLaunch(
|
|
69
120
|
input: PrepareDelegateLaunchInput,
|
|
70
121
|
): Promise<PreparedDelegateLaunch> {
|
|
122
|
+
throwIfPreparationAborted(input.signal);
|
|
71
123
|
// Resolve the guard extension before anything is created: a package missing
|
|
72
124
|
// its child guard must refuse rather than spawn an unguarded child.
|
|
73
|
-
const childExtensionPath =
|
|
74
|
-
input.childExtensionPath ?? resolveDelegateChildExtensionPath();
|
|
125
|
+
const childExtensionPath = input.childExtensionPath ?? resolveDelegateChildExtensionPath();
|
|
75
126
|
let attributionExtensionPath: string | undefined;
|
|
76
127
|
if (input.route.provider === 'anthropic') {
|
|
77
128
|
try {
|
|
@@ -89,41 +140,46 @@ export async function prepareDelegateLaunch(
|
|
|
89
140
|
}
|
|
90
141
|
}
|
|
91
142
|
|
|
143
|
+
throwIfPreparationAborted(input.signal);
|
|
92
144
|
const preflight = preflightDelegateLaunch(input);
|
|
145
|
+
throwIfPreparationAborted(input.signal);
|
|
93
146
|
|
|
94
|
-
|
|
95
|
-
cwd: input.cwd,
|
|
96
|
-
taskId: preflight.taskId,
|
|
97
|
-
launchNonce: preflight.launchNonce,
|
|
98
|
-
sessionId: input.sessionId,
|
|
99
|
-
childSessionId: preflight.childSessionId,
|
|
100
|
-
childSessionDir: '',
|
|
101
|
-
extensionMode: input.extensionMode,
|
|
102
|
-
route: input.route,
|
|
103
|
-
limits: preflight.limits,
|
|
104
|
-
seedSha256: preflight.seed.sha256,
|
|
105
|
-
...(input.now === undefined ? {} : { now: input.now }),
|
|
106
|
-
});
|
|
107
|
-
|
|
147
|
+
let store: DelegateArtifactStore | undefined;
|
|
108
148
|
try {
|
|
149
|
+
store = await DelegateArtifactStore.create({
|
|
150
|
+
cwd: input.cwd,
|
|
151
|
+
taskId: preflight.taskId,
|
|
152
|
+
launchNonce: preflight.launchNonce,
|
|
153
|
+
sessionId: input.sessionId,
|
|
154
|
+
childSessionId: preflight.childSessionId,
|
|
155
|
+
childSessionDir: '',
|
|
156
|
+
extensionMode: input.extensionMode,
|
|
157
|
+
route: input.route,
|
|
158
|
+
limits: preflight.limits,
|
|
159
|
+
seedSha256: preflight.seed.sha256,
|
|
160
|
+
...(input.now === undefined ? {} : { now: input.now }),
|
|
161
|
+
});
|
|
162
|
+
throwIfPreparationAborted(input.signal);
|
|
163
|
+
|
|
109
164
|
const seedRef = await store.writeSeed(preflight.seed.serialized);
|
|
165
|
+
throwIfPreparationAborted(input.signal);
|
|
110
166
|
// The persisted seed bytes are the bytes the child reads. Nothing
|
|
111
167
|
// re-serializes them between here and the child, and the child verifies the
|
|
112
168
|
// hash before its first model call.
|
|
113
169
|
if (seedRef.sha256 !== preflight.seed.sha256) {
|
|
114
|
-
throw new DelegateError(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
artifactDir: store.artifactDir,
|
|
121
|
-
},
|
|
122
|
-
);
|
|
170
|
+
throw new DelegateError('delegate seed hash changed between construction and persistence', {
|
|
171
|
+
code: 'seed_persist_failed',
|
|
172
|
+
childCreated: false,
|
|
173
|
+
taskId: preflight.taskId,
|
|
174
|
+
artifactDir: store.artifactDir,
|
|
175
|
+
});
|
|
123
176
|
}
|
|
124
177
|
await store.writeLedger(preflight.seed.ledger);
|
|
178
|
+
throwIfPreparationAborted(input.signal);
|
|
125
179
|
await store.writeBudgetPlan(preflight.plan);
|
|
180
|
+
throwIfPreparationAborted(input.signal);
|
|
126
181
|
const childSessionDirAbs = await ensureDelegateChildSessionDir(store.artifactDirAbs);
|
|
182
|
+
throwIfPreparationAborted(input.signal);
|
|
127
183
|
const seedPathAbs = join(store.artifactDirAbs, 'seed.json');
|
|
128
184
|
const argv = buildDelegateChildArgv({
|
|
129
185
|
route: input.route,
|
|
@@ -169,6 +225,13 @@ export async function prepareDelegateLaunch(
|
|
|
169
225
|
// The persisted prompt bytes must equal the bytes sent to the child, so the
|
|
170
226
|
// artifact is evidence of what the child actually received.
|
|
171
227
|
await store.writeChildPrompt(stdinBytes);
|
|
228
|
+
throwIfPreparationAborted(input.signal);
|
|
229
|
+
const artifactDirAbs = store.artifactDirAbs;
|
|
230
|
+
let rollbackPromise: Promise<void> | undefined;
|
|
231
|
+
const rollback = (): Promise<void> => {
|
|
232
|
+
rollbackPromise ??= discardUnownedDelegatePreparation(artifactDirAbs);
|
|
233
|
+
return rollbackPromise;
|
|
234
|
+
};
|
|
172
235
|
return {
|
|
173
236
|
preflight,
|
|
174
237
|
store,
|
|
@@ -178,10 +241,15 @@ export async function prepareDelegateLaunch(
|
|
|
178
241
|
childSessionDirAbs,
|
|
179
242
|
seedPathAbs,
|
|
180
243
|
stdinBytes,
|
|
244
|
+
rollback,
|
|
181
245
|
};
|
|
182
246
|
} catch (error) {
|
|
183
|
-
|
|
184
|
-
|
|
247
|
+
if (store === undefined) throw error;
|
|
248
|
+
try {
|
|
249
|
+
await discardUnownedDelegatePreparation(store.artifactDirAbs);
|
|
250
|
+
} catch (cleanupError) {
|
|
251
|
+
throw preparationCleanupFailure(error, cleanupError);
|
|
252
|
+
}
|
|
185
253
|
throw error;
|
|
186
254
|
}
|
|
187
255
|
}
|
|
@@ -246,17 +314,19 @@ async function adjudicateDelegateTerminal(
|
|
|
246
314
|
const resultPath = join(input.artifactDirAbs, 'result.json');
|
|
247
315
|
const terminalPath = join(input.artifactDirAbs, 'child-terminal.json');
|
|
248
316
|
if (!existsSync(resultPath)) {
|
|
249
|
-
const recorded = existsSync(terminalPath)
|
|
250
|
-
? await readChildTerminal(terminalPath)
|
|
251
|
-
: undefined;
|
|
317
|
+
const recorded = existsSync(terminalPath) ? await readChildTerminal(terminalPath) : undefined;
|
|
252
318
|
const cancelled = input.taskStatus === 'killed';
|
|
253
319
|
const code = recorded?.code ?? (cancelled ? 'child_cancelled' : 'child_exited_without_commit');
|
|
254
320
|
const detail =
|
|
255
321
|
recorded?.message ??
|
|
256
322
|
input.taskError ??
|
|
257
323
|
'the delegate child exited without committing a result package';
|
|
258
|
-
const preserved = [
|
|
259
|
-
.
|
|
324
|
+
const preserved = [
|
|
325
|
+
'seed.json',
|
|
326
|
+
'budget-plan.json',
|
|
327
|
+
'child-terminal.json',
|
|
328
|
+
'runtime-budget.json',
|
|
329
|
+
].filter((name) => existsSync(join(input.artifactDirAbs, name)));
|
|
260
330
|
if (
|
|
261
331
|
input.taskOutputPath !== undefined &&
|
|
262
332
|
input.taskOutputAbsPath !== undefined &&
|
|
@@ -265,25 +335,26 @@ async function adjudicateDelegateTerminal(
|
|
|
265
335
|
preserved.push(input.taskOutputPath);
|
|
266
336
|
}
|
|
267
337
|
const diagnosticTargets = preserved.filter(
|
|
268
|
-
(name) =>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
: `Inspect the preserved diagnostic evidence: ${diagnosticTargets.join(', ')}.`;
|
|
273
|
-
const error = new DelegateError(
|
|
274
|
-
`bg_delegate produced no committed answer: ${detail}`,
|
|
275
|
-
{
|
|
276
|
-
code: isDelegateErrorCode(code) ? code : 'child_exited_without_commit',
|
|
277
|
-
childCreated: true,
|
|
278
|
-
taskId: input.taskId,
|
|
279
|
-
artifactDir: input.artifactDirAbs,
|
|
280
|
-
preserved,
|
|
281
|
-
remediation: [
|
|
282
|
-
diagnostic,
|
|
283
|
-
'No partial answer is returned; nothing was truncated to look like success.',
|
|
284
|
-
],
|
|
285
|
-
},
|
|
338
|
+
(name) =>
|
|
339
|
+
name === 'child-terminal.json' ||
|
|
340
|
+
name === 'runtime-budget.json' ||
|
|
341
|
+
name === input.taskOutputPath,
|
|
286
342
|
);
|
|
343
|
+
const diagnostic =
|
|
344
|
+
diagnosticTargets.length === 0
|
|
345
|
+
? 'No child terminal record or merged task output exists; inspect the preserved launch artifacts listed above.'
|
|
346
|
+
: `Inspect the preserved diagnostic evidence: ${diagnosticTargets.join(', ')}.`;
|
|
347
|
+
const error = new DelegateError(`bg_delegate produced no committed answer: ${detail}`, {
|
|
348
|
+
code: isDelegateErrorCode(code) ? code : 'child_exited_without_commit',
|
|
349
|
+
childCreated: true,
|
|
350
|
+
taskId: input.taskId,
|
|
351
|
+
artifactDir: input.artifactDirAbs,
|
|
352
|
+
preserved,
|
|
353
|
+
remediation: [
|
|
354
|
+
diagnostic,
|
|
355
|
+
'No partial answer is returned; nothing was truncated to look like success.',
|
|
356
|
+
],
|
|
357
|
+
});
|
|
287
358
|
const outcome: DelegateTaskOutcome = {
|
|
288
359
|
status: cancelled ? 'cancelled' : 'failed',
|
|
289
360
|
errorCode: error.code,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { canonicalJson } from '../
|
|
2
|
+
import { canonicalJson } from '../canonical-json.js';
|
|
3
3
|
import {
|
|
4
4
|
UnsupportedConversationBlockError,
|
|
5
5
|
projectVisibleConversationV2,
|
|
@@ -241,11 +241,7 @@ export function verifyDelegateSeedBytes(
|
|
|
241
241
|
return rebuildSeed(parsed, expected.taskId);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
function requireString(
|
|
245
|
-
record: Record<PropertyKey, unknown>,
|
|
246
|
-
key: string,
|
|
247
|
-
taskId: string,
|
|
248
|
-
): string {
|
|
244
|
+
function requireString(record: Record<PropertyKey, unknown>, key: string, taskId: string): string {
|
|
249
245
|
const value = record[key];
|
|
250
246
|
if (typeof value !== 'string') {
|
|
251
247
|
throw new DelegateError(`delegate seed field ${key} must be a string`, {
|
|
@@ -404,7 +400,9 @@ function rebuildSeed(record: Record<PropertyKey, unknown>, taskId: string): Dele
|
|
|
404
400
|
* so it is carried through without re-deriving values that would only duplicate
|
|
405
401
|
* the parent's computation.
|
|
406
402
|
*/
|
|
407
|
-
function readDelegateProjection(
|
|
403
|
+
function readDelegateProjection(
|
|
404
|
+
record: Record<PropertyKey, unknown>,
|
|
405
|
+
): DelegateConversationProjection {
|
|
408
406
|
const projection: unknown = record;
|
|
409
407
|
if (!isDelegateProjection(projection)) {
|
|
410
408
|
throw new DelegateError('delegate seed conversation_projection is malformed', {
|