@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2
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/LICENSE +21 -0
- package/README.md +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { collaborationCommandSchema, collaborationGoalSchema, collaborationPreparedSchema, } from '../schemas/collaboration.js';
|
|
5
|
+
import { admitLaunch, reserveLaunch, settleLaunch, } from '../collab/admission.js';
|
|
6
|
+
import { buildMessage, sha256File } from '../collab/envelope.js';
|
|
7
|
+
import { findAttempt } from '../collab/lifecycle.js';
|
|
8
|
+
import { submitMessage } from '../collab/messages.js';
|
|
9
|
+
import { claimResources, releaseClaim } from '../collab/resources.js';
|
|
10
|
+
import { initTask, nowIso, readRecord, taskRootFor } from '../collab/store.js';
|
|
11
|
+
import { observePath, resolveRealpath } from '../infra/fs.js';
|
|
12
|
+
import { isPathInside, samePath } from '../schemas/paths.js';
|
|
13
|
+
import { ensureStatePathBoundary } from '../install/ownership.js';
|
|
14
|
+
import { startOwnedPiRpc } from '../hosts/pi/rpc.js';
|
|
15
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
16
|
+
const LAUNCH_TOKENS = new WeakMap();
|
|
17
|
+
function failure(status, code, message, nextAction = null, diagnostics = []) {
|
|
18
|
+
return { ok: false, status, code, message, nextAction, diagnostics };
|
|
19
|
+
}
|
|
20
|
+
function boundedJson(value, maxBytes) {
|
|
21
|
+
let encoded;
|
|
22
|
+
try {
|
|
23
|
+
encoded = JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return Buffer.byteLength(encoded) <= maxBytes ? encoded : null;
|
|
29
|
+
}
|
|
30
|
+
function readBoundedFile(file, maxBytes = MAX_FILE_BYTES) {
|
|
31
|
+
const stat = fs.lstatSync(file);
|
|
32
|
+
if (!stat.isFile() || stat.size > maxBytes) {
|
|
33
|
+
throw new Error(`unsupported file: ${file}`);
|
|
34
|
+
}
|
|
35
|
+
return fs.readFileSync(file, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
function readJsonFile(file) {
|
|
38
|
+
return JSON.parse(readBoundedFile(file));
|
|
39
|
+
}
|
|
40
|
+
function namespaceFailure(code, message, nextAction) {
|
|
41
|
+
return { ok: false, failure: failure('conflict', code, message, nextAction) };
|
|
42
|
+
}
|
|
43
|
+
async function directoryIdentity(target, code, label) {
|
|
44
|
+
const observed = await observePath(target);
|
|
45
|
+
if (observed.kind !== 'directory' || observed.stats === null) {
|
|
46
|
+
return namespaceFailure(code, `${label} is not a directory: ${target}`, 'Inspect or remove the redirected path; no metadata was adopted.');
|
|
47
|
+
}
|
|
48
|
+
const canonical = await resolveRealpath(target);
|
|
49
|
+
if (canonical === null || !samePath(canonical, target)) {
|
|
50
|
+
return namespaceFailure(code, `${label} is redirected or not canonical: ${target}`, 'Inspect or remove the symlink explicitly; no metadata was written.');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
value: {
|
|
55
|
+
path: target,
|
|
56
|
+
canonical,
|
|
57
|
+
dev: observed.stats.dev,
|
|
58
|
+
ino: observed.stats.ino,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function identityStillCurrent(identity) {
|
|
63
|
+
const observed = await observePath(identity.path);
|
|
64
|
+
if (observed.kind !== 'directory' || observed.stats === null)
|
|
65
|
+
return false;
|
|
66
|
+
if (observed.stats.dev !== identity.dev ||
|
|
67
|
+
observed.stats.ino !== identity.ino) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const canonical = await resolveRealpath(identity.path);
|
|
71
|
+
return canonical !== null && samePath(canonical, identity.path);
|
|
72
|
+
}
|
|
73
|
+
async function namespaceStillCurrent(namespace) {
|
|
74
|
+
for (const [label, identity] of [
|
|
75
|
+
['domain', namespace.domain],
|
|
76
|
+
['task', namespace.taskRoot],
|
|
77
|
+
['private', namespace.privateDir],
|
|
78
|
+
]) {
|
|
79
|
+
if (!(await identityStillCurrent(identity))) {
|
|
80
|
+
return failure('conflict', 'private_namespace_redirected', `The ${label} namespace changed during preparation or admission.`, 'Inspect the redirected path and reconcile the task record before retrying.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function namespaceSyncFailure(namespace) {
|
|
86
|
+
for (const [label, identity] of [
|
|
87
|
+
['domain', namespace.domain],
|
|
88
|
+
['task', namespace.taskRoot],
|
|
89
|
+
['private', namespace.privateDir],
|
|
90
|
+
]) {
|
|
91
|
+
try {
|
|
92
|
+
const observed = fs.lstatSync(identity.path);
|
|
93
|
+
if (!observed.isDirectory() ||
|
|
94
|
+
observed.dev !== identity.dev ||
|
|
95
|
+
observed.ino !== identity.ino) {
|
|
96
|
+
return `${label} namespace identity changed`;
|
|
97
|
+
}
|
|
98
|
+
const canonical = fs.realpathSync(identity.path);
|
|
99
|
+
if (!samePath(canonical, identity.path)) {
|
|
100
|
+
return `${label} namespace is redirected`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return `${label} namespace is unavailable`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
async function capturePreparedNamespace(preparedFile) {
|
|
110
|
+
const privateDir = path.dirname(preparedFile);
|
|
111
|
+
const taskRoot = path.dirname(privateDir);
|
|
112
|
+
const tasksDir = path.dirname(taskRoot);
|
|
113
|
+
const domainRoot = path.dirname(tasksDir);
|
|
114
|
+
const domain = await directoryIdentity(domainRoot, 'prepared_namespace_redirected', 'Domain root');
|
|
115
|
+
if (!domain.ok)
|
|
116
|
+
return domain;
|
|
117
|
+
const task = await directoryIdentity(taskRoot, 'prepared_namespace_redirected', 'Task root');
|
|
118
|
+
if (!task.ok)
|
|
119
|
+
return task;
|
|
120
|
+
const privateIdentity = await directoryIdentity(privateDir, 'prepared_namespace_redirected', 'Prepared private directory');
|
|
121
|
+
if (!privateIdentity.ok)
|
|
122
|
+
return privateIdentity;
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
value: {
|
|
126
|
+
domain: domain.value,
|
|
127
|
+
taskRoot: task.value,
|
|
128
|
+
privateDir: privateIdentity.value,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function initialNamespace(domainRoot, taskRoot) {
|
|
133
|
+
const domain = await directoryIdentity(domainRoot, 'domain_namespace_redirected', 'Domain root');
|
|
134
|
+
if (!domain.ok)
|
|
135
|
+
return domain;
|
|
136
|
+
if (!samePath(path.dirname(taskRoot), path.join(domain.value.path, 'tasks'))) {
|
|
137
|
+
return namespaceFailure('task_namespace_redirected', `Task root is not a direct child of the private domain: ${taskRoot}`, 'Use the task root derived from the explicit shared domain.');
|
|
138
|
+
}
|
|
139
|
+
const boundary = await ensureStatePathBoundary(domain.value.path, taskRoot);
|
|
140
|
+
if (!boundary.ok) {
|
|
141
|
+
return namespaceFailure(boundary.code, boundary.message, boundary.nextAction ?? 'Inspect the private task path.');
|
|
142
|
+
}
|
|
143
|
+
const observed = await observePath(taskRoot);
|
|
144
|
+
if (observed.kind === 'symlink' ||
|
|
145
|
+
observed.kind === 'file' ||
|
|
146
|
+
observed.kind === 'other') {
|
|
147
|
+
return namespaceFailure('task_namespace_redirected', `Task root is not a real directory: ${taskRoot}`, 'Inspect or remove the redirected task path; no task metadata was written.');
|
|
148
|
+
}
|
|
149
|
+
if (observed.kind === 'missing') {
|
|
150
|
+
return { ok: true, value: { domain: domain.value, taskRoot: null } };
|
|
151
|
+
}
|
|
152
|
+
const task = await directoryIdentity(taskRoot, 'task_namespace_redirected', 'Task root');
|
|
153
|
+
if (!task.ok)
|
|
154
|
+
return task;
|
|
155
|
+
return { ok: true, value: { domain: domain.value, taskRoot: task.value } };
|
|
156
|
+
}
|
|
157
|
+
async function preparePrivateNamespace(domainRoot, taskRoot, privateDir) {
|
|
158
|
+
const domain = await directoryIdentity(domainRoot, 'domain_namespace_redirected', 'Domain root');
|
|
159
|
+
if (!domain.ok)
|
|
160
|
+
return domain;
|
|
161
|
+
const task = await directoryIdentity(taskRoot, 'task_namespace_redirected', 'Task root');
|
|
162
|
+
if (!task.ok)
|
|
163
|
+
return task;
|
|
164
|
+
const boundary = await ensureStatePathBoundary(taskRoot, privateDir);
|
|
165
|
+
if (!boundary.ok) {
|
|
166
|
+
return namespaceFailure(boundary.code, boundary.message, boundary.nextAction ?? 'Inspect the private task path.');
|
|
167
|
+
}
|
|
168
|
+
const observed = await observePath(privateDir);
|
|
169
|
+
if (observed.kind === 'symlink' ||
|
|
170
|
+
observed.kind === 'file' ||
|
|
171
|
+
observed.kind === 'other') {
|
|
172
|
+
return namespaceFailure('private_namespace_redirected', `Prepared private directory is not a directory: ${privateDir}`, 'Inspect or remove the redirected path; no task artifacts were adopted.');
|
|
173
|
+
}
|
|
174
|
+
if (observed.kind === 'missing') {
|
|
175
|
+
try {
|
|
176
|
+
fs.mkdirSync(privateDir, { mode: 0o700 });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
failure: failure('failed', 'private_directory_create_failed', error instanceof Error ? error.message : String(error), 'Inspect the task root before retrying.'),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const privateIdentity = await directoryIdentity(privateDir, 'private_namespace_redirected', 'Prepared private directory');
|
|
186
|
+
if (!privateIdentity.ok)
|
|
187
|
+
return privateIdentity;
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
value: {
|
|
191
|
+
domain: domain.value,
|
|
192
|
+
taskRoot: task.value,
|
|
193
|
+
privateDir: privateIdentity.value,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function preparedFilePathFailure(preparedFile) {
|
|
198
|
+
const privateDir = path.dirname(preparedFile);
|
|
199
|
+
const taskRoot = path.dirname(privateDir);
|
|
200
|
+
const tasksDir = path.dirname(taskRoot);
|
|
201
|
+
const domainRoot = path.dirname(tasksDir);
|
|
202
|
+
if (path.basename(privateDir) !== 'p10' ||
|
|
203
|
+
path.basename(tasksDir) !== 'tasks' ||
|
|
204
|
+
path.basename(preparedFile) !== 'prepared.json') {
|
|
205
|
+
return failure('conflict', 'prepared_namespace_unrecognized', 'Prepared file is not inside the private task namespace.', 'Use a prepared file published under an explicit domain/tasks/<task>/p10 directory.');
|
|
206
|
+
}
|
|
207
|
+
for (const [target, label] of [
|
|
208
|
+
[domainRoot, 'domain'],
|
|
209
|
+
[tasksDir, 'tasks'],
|
|
210
|
+
[taskRoot, 'task'],
|
|
211
|
+
[privateDir, 'private'],
|
|
212
|
+
]) {
|
|
213
|
+
let observed;
|
|
214
|
+
let canonical;
|
|
215
|
+
try {
|
|
216
|
+
observed = fs.lstatSync(target);
|
|
217
|
+
canonical = fs.realpathSync(target);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return failure('conflict', 'prepared_namespace_missing', `Prepared ${label} namespace is unavailable: ${target}`, 'Inspect the prepared task path before retrying.');
|
|
221
|
+
}
|
|
222
|
+
if (!observed.isDirectory() || !samePath(canonical, target)) {
|
|
223
|
+
return failure('conflict', 'prepared_namespace_redirected', `Prepared ${label} namespace is redirected: ${target}`, 'Inspect or remove the symlink explicitly; no artifacts were read.');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
async function writeInNamespace(namespace, file, bytes) {
|
|
229
|
+
const before = await namespaceStillCurrent(namespace);
|
|
230
|
+
if (before !== null)
|
|
231
|
+
return before;
|
|
232
|
+
try {
|
|
233
|
+
writeOrVerify(file, bytes);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
return failure('failed', 'prepared_write_failed', error instanceof Error ? error.message : String(error), 'Inspect the private task directory and residual files.');
|
|
237
|
+
}
|
|
238
|
+
return namespaceStillCurrent(namespace);
|
|
239
|
+
}
|
|
240
|
+
function publishCurrentResult(namespace, file, bytes) {
|
|
241
|
+
const before = namespaceSyncFailure(namespace);
|
|
242
|
+
if (before !== null)
|
|
243
|
+
return { ok: false, error: before };
|
|
244
|
+
if (!isPathInside(namespace.privateDir.path, file)) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
error: 'current-result evidence target is outside the private namespace',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const existing = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
252
|
+
if (existing) {
|
|
253
|
+
if (!existing.isFile() || existing.size > MAX_FILE_BYTES * 2) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
error: 'current-result evidence target is not an owned regular file',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const beforeRead = namespaceSyncFailure(namespace);
|
|
260
|
+
if (beforeRead !== null)
|
|
261
|
+
return { ok: false, error: beforeRead };
|
|
262
|
+
const content = fs.readFileSync(file);
|
|
263
|
+
if (!content.equals(Buffer.from(bytes))) {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
error: 'current-result evidence target has different content',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
|
|
272
|
+
}
|
|
273
|
+
const afterWrite = namespaceSyncFailure(namespace);
|
|
274
|
+
if (afterWrite !== null)
|
|
275
|
+
return { ok: false, error: afterWrite };
|
|
276
|
+
const after = fs.lstatSync(file);
|
|
277
|
+
if (!after.isFile()) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
error: 'current-result evidence publication could not be verified',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const beforeVerifyRead = namespaceSyncFailure(namespace);
|
|
284
|
+
if (beforeVerifyRead !== null) {
|
|
285
|
+
return { ok: false, error: beforeVerifyRead };
|
|
286
|
+
}
|
|
287
|
+
if (fs.readFileSync(file).length !== Buffer.byteLength(bytes)) {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
error: 'current-result evidence publication could not be verified',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const namespaceProblem = namespaceSyncFailure(namespace);
|
|
294
|
+
if (namespaceProblem !== null) {
|
|
295
|
+
return { ok: false, error: namespaceProblem };
|
|
296
|
+
}
|
|
297
|
+
return { ok: true, ref: file, digest: sha256File(file) };
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
error: error instanceof Error ? error.message : String(error),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function isRecord(value) {
|
|
307
|
+
return typeof value === 'object' && value !== null;
|
|
308
|
+
}
|
|
309
|
+
function assistantRecord(record) {
|
|
310
|
+
if (!isRecord(record))
|
|
311
|
+
return null;
|
|
312
|
+
const nested = isRecord(record['message']) ? record['message'] : record;
|
|
313
|
+
if (nested['role'] !== 'assistant')
|
|
314
|
+
return null;
|
|
315
|
+
const stopReason = typeof nested['stopReason'] === 'string'
|
|
316
|
+
? nested['stopReason'].toLowerCase()
|
|
317
|
+
: '';
|
|
318
|
+
const errorMessage = typeof nested['errorMessage'] === 'string' ? nested['errorMessage'] : '';
|
|
319
|
+
const content = nested['content'];
|
|
320
|
+
let text = false;
|
|
321
|
+
let tool = false;
|
|
322
|
+
if (typeof content === 'string')
|
|
323
|
+
text = content.trim() !== '';
|
|
324
|
+
else if (Array.isArray(content)) {
|
|
325
|
+
for (const part of content) {
|
|
326
|
+
if (!isRecord(part))
|
|
327
|
+
continue;
|
|
328
|
+
const type = typeof part['type'] === 'string' ? part['type'] : '';
|
|
329
|
+
if (type === 'text' &&
|
|
330
|
+
typeof part['text'] === 'string' &&
|
|
331
|
+
part['text'].trim() !== '') {
|
|
332
|
+
text = true;
|
|
333
|
+
}
|
|
334
|
+
if (type.includes('tool'))
|
|
335
|
+
tool = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const terminal = stopReason === 'stop' ||
|
|
339
|
+
stopReason === 'end_turn' ||
|
|
340
|
+
stopReason === 'completed' ||
|
|
341
|
+
stopReason === 'complete' ||
|
|
342
|
+
stopReason === 'success' ||
|
|
343
|
+
stopReason === 'finish' ||
|
|
344
|
+
stopReason === 'finished';
|
|
345
|
+
return {
|
|
346
|
+
text,
|
|
347
|
+
tool,
|
|
348
|
+
terminal,
|
|
349
|
+
error: errorMessage.trim() !== '' ||
|
|
350
|
+
stopReason.includes('error') ||
|
|
351
|
+
stopReason.includes('failed'),
|
|
352
|
+
aborted: stopReason.includes('abort') ||
|
|
353
|
+
stopReason.includes('cancel') ||
|
|
354
|
+
stopReason.includes('interrupt'),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function currentNativeResult(file, baseline) {
|
|
358
|
+
if (file === null ||
|
|
359
|
+
baseline === null ||
|
|
360
|
+
!path.isAbsolute(file) ||
|
|
361
|
+
!Number.isSafeInteger(baseline) ||
|
|
362
|
+
baseline < 0) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const stat = fs.lstatSync(file);
|
|
367
|
+
if (!stat.isFile() || stat.size < baseline)
|
|
368
|
+
return null;
|
|
369
|
+
const available = stat.size - baseline;
|
|
370
|
+
const length = Math.min(available, MAX_FILE_BYTES);
|
|
371
|
+
const truncated = available > length;
|
|
372
|
+
const buffer = Buffer.alloc(length);
|
|
373
|
+
const fd = fs.openSync(file, 'r');
|
|
374
|
+
try {
|
|
375
|
+
fs.readSync(fd, buffer, 0, length, baseline);
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
fs.closeSync(fd);
|
|
379
|
+
}
|
|
380
|
+
const text = buffer.toString('utf8');
|
|
381
|
+
// The captured baseline is always the byte length of a previously complete
|
|
382
|
+
// session file, so every byte after it belongs to the current prompt. A
|
|
383
|
+
// malformed line is reported as invalid current evidence, never skipped as
|
|
384
|
+
// if it were part of an older record.
|
|
385
|
+
const lines = text.split('\n');
|
|
386
|
+
const records = [];
|
|
387
|
+
let invalid = false;
|
|
388
|
+
for (const line of lines) {
|
|
389
|
+
if (line.trim() === '')
|
|
390
|
+
continue;
|
|
391
|
+
try {
|
|
392
|
+
const parsed = JSON.parse(line);
|
|
393
|
+
if (!isRecord(parsed)) {
|
|
394
|
+
invalid = true;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
records.push(parsed);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
invalid = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (truncated) {
|
|
404
|
+
return {
|
|
405
|
+
records,
|
|
406
|
+
status: 'unknown',
|
|
407
|
+
reason: 'current_result_truncated',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (invalid) {
|
|
411
|
+
return {
|
|
412
|
+
records,
|
|
413
|
+
status: 'unknown',
|
|
414
|
+
reason: 'current_result_invalid',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const assistant = records
|
|
418
|
+
.map((record) => assistantRecord(record))
|
|
419
|
+
.filter((record) => record !== null);
|
|
420
|
+
if (assistant.length === 0) {
|
|
421
|
+
return {
|
|
422
|
+
records,
|
|
423
|
+
status: 'unknown',
|
|
424
|
+
reason: 'no_current_assistant_message',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const last = assistant[assistant.length - 1];
|
|
428
|
+
if (last.error) {
|
|
429
|
+
return {
|
|
430
|
+
records,
|
|
431
|
+
status: 'failed',
|
|
432
|
+
reason: 'current_assistant_error',
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (last.aborted) {
|
|
436
|
+
return {
|
|
437
|
+
records,
|
|
438
|
+
status: 'cancelled',
|
|
439
|
+
reason: 'current_assistant_aborted',
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (!last.terminal || last.tool) {
|
|
443
|
+
return {
|
|
444
|
+
records,
|
|
445
|
+
status: 'unknown',
|
|
446
|
+
reason: last.tool
|
|
447
|
+
? 'current_assistant_tool_intermediate'
|
|
448
|
+
: 'current_assistant_incomplete_terminal',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (last.text) {
|
|
452
|
+
return { records, status: 'completed', reason: null };
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
records,
|
|
456
|
+
status: 'unknown',
|
|
457
|
+
reason: 'current_assistant_incomplete',
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function regularFileSize(file) {
|
|
465
|
+
if (file === null || !path.isAbsolute(file))
|
|
466
|
+
return null;
|
|
467
|
+
try {
|
|
468
|
+
const observed = fs.lstatSync(file);
|
|
469
|
+
return observed.isFile() ? observed.size : null;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function writeNewFile(file, bytes) {
|
|
476
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
477
|
+
fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
|
|
478
|
+
}
|
|
479
|
+
function writeOrVerify(file, bytes) {
|
|
480
|
+
try {
|
|
481
|
+
writeNewFile(file, bytes);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (!(typeof error === 'object' && error !== null && 'code' in error) ||
|
|
485
|
+
error.code !== 'EEXIST' ||
|
|
486
|
+
!fs.lstatSync(file).isFile() ||
|
|
487
|
+
fs.readFileSync(file, 'utf8') !== bytes) {
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function prepDir(taskRoot) {
|
|
493
|
+
return path.join(taskRoot, 'p10');
|
|
494
|
+
}
|
|
495
|
+
function createToken(facts) {
|
|
496
|
+
const token = Object.freeze({});
|
|
497
|
+
LAUNCH_TOKENS.set(token, facts);
|
|
498
|
+
return token;
|
|
499
|
+
}
|
|
500
|
+
export function validateLaunchToken(token, expected) {
|
|
501
|
+
if (token === null || typeof token !== 'object')
|
|
502
|
+
return false;
|
|
503
|
+
const facts = LAUNCH_TOKENS.get(token);
|
|
504
|
+
return (facts !== undefined &&
|
|
505
|
+
facts.taskRoot === expected.taskRoot &&
|
|
506
|
+
facts.assignmentDigest === expected.assignmentDigest &&
|
|
507
|
+
facts.subtaskId === expected.subtaskId &&
|
|
508
|
+
facts.attempt === expected.attempt);
|
|
509
|
+
}
|
|
510
|
+
function consumeToken(token, expected) {
|
|
511
|
+
if (!validateLaunchToken(token, expected)) {
|
|
512
|
+
throw new Error('genuine admission token required');
|
|
513
|
+
}
|
|
514
|
+
const facts = LAUNCH_TOKENS.get(token);
|
|
515
|
+
if (!facts)
|
|
516
|
+
throw new Error('genuine admission token required');
|
|
517
|
+
LAUNCH_TOKENS.delete(token);
|
|
518
|
+
return facts;
|
|
519
|
+
}
|
|
520
|
+
function preparedBundle(preparedFile) {
|
|
521
|
+
try {
|
|
522
|
+
const namespaceProblem = preparedFilePathFailure(preparedFile);
|
|
523
|
+
if (namespaceProblem !== null)
|
|
524
|
+
return namespaceProblem;
|
|
525
|
+
const prepared = collaborationPreparedSchema.parse(readJsonFile(preparedFile));
|
|
526
|
+
const preparedDir = path.dirname(preparedFile);
|
|
527
|
+
if (prepared.prepared_dir !== preparedDir) {
|
|
528
|
+
return failure('conflict', 'prepared_dir_mismatch', 'Prepared directory does not match its published location.');
|
|
529
|
+
}
|
|
530
|
+
for (const artifact of [
|
|
531
|
+
prepared.assignment_path,
|
|
532
|
+
prepared.prompt_path,
|
|
533
|
+
prepared.goal_path,
|
|
534
|
+
]) {
|
|
535
|
+
if (path.dirname(artifact) !== preparedDir ||
|
|
536
|
+
!fs.lstatSync(artifact).isFile()) {
|
|
537
|
+
return failure('conflict', 'prepared_artifact_redirected', 'Prepared artifacts must be regular files inside the private directory.');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (sha256File(prepared.assignment_path) !== prepared.assignment_digest) {
|
|
541
|
+
return failure('conflict', 'assignment_digest_mismatch', 'Prepared assignment digest no longer matches.');
|
|
542
|
+
}
|
|
543
|
+
if (sha256File(prepared.prompt_path) !== prepared.prompt_digest) {
|
|
544
|
+
return failure('conflict', 'prompt_digest_mismatch', 'Prepared prompt digest no longer matches.');
|
|
545
|
+
}
|
|
546
|
+
const goal = collaborationGoalSchema.parse(readJsonFile(prepared.goal_path));
|
|
547
|
+
if (goal.task_id !== prepared.task_id ||
|
|
548
|
+
goal.subtask_id !== prepared.subtask_id) {
|
|
549
|
+
return failure('conflict', 'prepared_identity_mismatch', 'Prepared identity is inconsistent.');
|
|
550
|
+
}
|
|
551
|
+
const assignment = readJsonFile(prepared.assignment_path);
|
|
552
|
+
return { ok: true, value: { prepared, goal, assignment } };
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
return failure('invalid', 'prepared_invalid', error instanceof Error ? error.message : String(error));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
export async function prepareCollaboration(input, options) {
|
|
559
|
+
let goal;
|
|
560
|
+
const clock = options.clock ?? Date.now();
|
|
561
|
+
try {
|
|
562
|
+
goal = collaborationGoalSchema.parse(input);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
return failure('invalid', 'goal_invalid', error instanceof Error ? error.message : String(error));
|
|
566
|
+
}
|
|
567
|
+
if (!path.isAbsolute(options.domainRoot)) {
|
|
568
|
+
return failure('invalid', 'domain_root_required', 'Domain root must be absolute.');
|
|
569
|
+
}
|
|
570
|
+
if (!fs.existsSync(goal.assignee.cwd)) {
|
|
571
|
+
return failure('needs_input', 'cwd_unavailable', 'Assignee cwd does not exist.');
|
|
572
|
+
}
|
|
573
|
+
let taskRoot;
|
|
574
|
+
let domainRoot;
|
|
575
|
+
try {
|
|
576
|
+
taskRoot = taskRootFor(options.domainRoot, goal.task_id);
|
|
577
|
+
domainRoot = fs.realpathSync(options.domainRoot);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
return failure('failed', 'task_init_failed', error instanceof Error ? error.message : String(error));
|
|
581
|
+
}
|
|
582
|
+
const initial = await initialNamespace(domainRoot, taskRoot);
|
|
583
|
+
if (!initial.ok)
|
|
584
|
+
return initial.failure;
|
|
585
|
+
try {
|
|
586
|
+
initTask(taskRoot, {
|
|
587
|
+
taskId: goal.task_id,
|
|
588
|
+
coordinator: {
|
|
589
|
+
agent_id: goal.coordinator.agent_id,
|
|
590
|
+
session_id: goal.coordinator.session_id,
|
|
591
|
+
harness: goal.coordinator.harness,
|
|
592
|
+
owner_epoch: goal.coordinator.owner_epoch,
|
|
593
|
+
since: nowIso(clock),
|
|
594
|
+
previous: null,
|
|
595
|
+
},
|
|
596
|
+
domainRoot,
|
|
597
|
+
clock: clock,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
return failure('failed', 'task_init_failed', error instanceof Error ? error.message : String(error));
|
|
602
|
+
}
|
|
603
|
+
const taskIdentity = await directoryIdentity(taskRoot, 'task_namespace_redirected', 'Task root');
|
|
604
|
+
if (!taskIdentity.ok)
|
|
605
|
+
return taskIdentity.failure;
|
|
606
|
+
const preparedDir = prepDir(taskRoot);
|
|
607
|
+
const privateNamespace = await preparePrivateNamespace(domainRoot, taskRoot, preparedDir);
|
|
608
|
+
if (!privateNamespace.ok)
|
|
609
|
+
return privateNamespace.failure;
|
|
610
|
+
const namespace = privateNamespace.value;
|
|
611
|
+
const existingPrepared = path.join(preparedDir, 'prepared.json');
|
|
612
|
+
if (fs.existsSync(existingPrepared)) {
|
|
613
|
+
const existing = preparedBundle(existingPrepared);
|
|
614
|
+
if (!existing.ok)
|
|
615
|
+
return existing;
|
|
616
|
+
if (JSON.stringify(existing.value.goal) !== JSON.stringify(goal)) {
|
|
617
|
+
return failure('conflict', 'goal_changed', 'The task already has a prepared goal with different content.');
|
|
618
|
+
}
|
|
619
|
+
return existing;
|
|
620
|
+
}
|
|
621
|
+
const claimIds = [];
|
|
622
|
+
const resourceEpochs = {};
|
|
623
|
+
for (const resource of goal.resources) {
|
|
624
|
+
const claimed = await claimResources(taskRoot, {
|
|
625
|
+
resources: [{ kind: 'path', path: resource.path }],
|
|
626
|
+
access: resource.access,
|
|
627
|
+
holder: {
|
|
628
|
+
agent_id: goal.assignee.agent_id,
|
|
629
|
+
session_id: goal.assignee.session_id,
|
|
630
|
+
harness: goal.assignee.harness,
|
|
631
|
+
},
|
|
632
|
+
subtaskId: goal.subtask_id,
|
|
633
|
+
attempt: goal.attempt,
|
|
634
|
+
clock,
|
|
635
|
+
});
|
|
636
|
+
if (!claimed.ok) {
|
|
637
|
+
for (const claimId of claimIds) {
|
|
638
|
+
releaseClaim(taskRoot, {
|
|
639
|
+
claimId,
|
|
640
|
+
reason: 'prepare resource claim failed',
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return failure(claimed.reason === 'resource_conflict' ? 'conflict' : 'failed', claimed.reason, 'Required resources could not be claimed.');
|
|
644
|
+
}
|
|
645
|
+
const requested = claimed.claims.find((claim) => claim.path === resource.path) ??
|
|
646
|
+
claimed.claims[0];
|
|
647
|
+
if (requested === undefined) {
|
|
648
|
+
for (const claimId of claimIds) {
|
|
649
|
+
releaseClaim(taskRoot, {
|
|
650
|
+
claimId,
|
|
651
|
+
reason: 'prepare resource epoch missing',
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return failure('failed', 'resource_epoch_missing', 'No resource epoch was returned.');
|
|
655
|
+
}
|
|
656
|
+
claimIds.push(...claimed.claims.map((claim) => claim.claim_id));
|
|
657
|
+
resourceEpochs[resource.path] = requested.resource_epoch;
|
|
658
|
+
}
|
|
659
|
+
const goalBytes = `${JSON.stringify(goal, null, 2)}\n`;
|
|
660
|
+
const promptBytes = `${goal.prompt}\n`;
|
|
661
|
+
const goalFile = path.join(preparedDir, 'goal.json');
|
|
662
|
+
const promptFile = path.join(preparedDir, 'prompt.txt');
|
|
663
|
+
const goalProblem = await writeInNamespace(namespace, goalFile, goalBytes);
|
|
664
|
+
if (goalProblem !== null) {
|
|
665
|
+
if (goalProblem.code === 'prepared_write_failed') {
|
|
666
|
+
for (const claimId of claimIds) {
|
|
667
|
+
releaseClaim(taskRoot, {
|
|
668
|
+
claimId,
|
|
669
|
+
reason: 'prepare failed before assignment',
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return goalProblem;
|
|
674
|
+
}
|
|
675
|
+
const promptProblem = await writeInNamespace(namespace, promptFile, promptBytes);
|
|
676
|
+
if (promptProblem !== null) {
|
|
677
|
+
if (promptProblem.code === 'prepared_write_failed') {
|
|
678
|
+
for (const claimId of claimIds) {
|
|
679
|
+
releaseClaim(taskRoot, {
|
|
680
|
+
claimId,
|
|
681
|
+
reason: 'prepare failed before assignment',
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return promptProblem;
|
|
686
|
+
}
|
|
687
|
+
const promptDigest = sha256File(promptFile);
|
|
688
|
+
const goalDigest = sha256File(goalFile);
|
|
689
|
+
const assignment = {
|
|
690
|
+
schema: 'harness-collab/assignment/1',
|
|
691
|
+
task_id: goal.task_id,
|
|
692
|
+
assignment_id: goal.subtask_id,
|
|
693
|
+
attempt: goal.attempt,
|
|
694
|
+
objective: goal.objective,
|
|
695
|
+
acceptance: goal.acceptance,
|
|
696
|
+
roles: ['implementer'],
|
|
697
|
+
coordinator: {
|
|
698
|
+
agent_id: goal.coordinator.agent_id,
|
|
699
|
+
session_id: goal.coordinator.session_id,
|
|
700
|
+
harness: goal.coordinator.harness,
|
|
701
|
+
owner_epoch: goal.coordinator.owner_epoch,
|
|
702
|
+
},
|
|
703
|
+
assignee: {
|
|
704
|
+
agent_id: goal.assignee.agent_id,
|
|
705
|
+
session_id: goal.assignee.session_id,
|
|
706
|
+
harness: goal.assignee.harness,
|
|
707
|
+
surface: goal.assignee.surface,
|
|
708
|
+
cwd: goal.assignee.cwd,
|
|
709
|
+
configuration_id: goal.assignee.configuration_id,
|
|
710
|
+
},
|
|
711
|
+
inputs: [
|
|
712
|
+
{ ref: promptFile, digest: promptDigest },
|
|
713
|
+
{ ref: goalFile, digest: goalDigest },
|
|
714
|
+
],
|
|
715
|
+
resources: goal.resources.map((item) => ({
|
|
716
|
+
path: item.path,
|
|
717
|
+
access: item.access,
|
|
718
|
+
resource_epoch: resourceEpochs[item.path],
|
|
719
|
+
})),
|
|
720
|
+
runner: {
|
|
721
|
+
name: 'pi-cli',
|
|
722
|
+
mode: 'native',
|
|
723
|
+
omitted_capabilities: [],
|
|
724
|
+
},
|
|
725
|
+
required: goal.required,
|
|
726
|
+
preferred: goal.preferred,
|
|
727
|
+
phase: goal.phase,
|
|
728
|
+
};
|
|
729
|
+
const assignmentFile = path.join(preparedDir, 'assignment.json');
|
|
730
|
+
const assignmentBytes = `${JSON.stringify(assignment, null, 2)}\n`;
|
|
731
|
+
const assignmentProblem = await writeInNamespace(namespace, assignmentFile, assignmentBytes);
|
|
732
|
+
if (assignmentProblem !== null) {
|
|
733
|
+
if (assignmentProblem.code === 'prepared_write_failed') {
|
|
734
|
+
for (const claimId of claimIds) {
|
|
735
|
+
releaseClaim(taskRoot, {
|
|
736
|
+
claimId,
|
|
737
|
+
reason: 'prepare failed before assignment',
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return assignmentProblem;
|
|
742
|
+
}
|
|
743
|
+
const beforeAssignment = await namespaceStillCurrent(namespace);
|
|
744
|
+
if (beforeAssignment !== null)
|
|
745
|
+
return beforeAssignment;
|
|
746
|
+
const assignmentDigest = sha256File(assignmentFile);
|
|
747
|
+
const assignmentMessage = buildMessage({
|
|
748
|
+
type: 'ASSIGN',
|
|
749
|
+
task_id: goal.task_id,
|
|
750
|
+
from: {
|
|
751
|
+
agent_id: goal.coordinator.agent_id,
|
|
752
|
+
session_id: goal.coordinator.session_id,
|
|
753
|
+
harness: goal.coordinator.harness,
|
|
754
|
+
},
|
|
755
|
+
to: {
|
|
756
|
+
agent_id: goal.assignee.agent_id,
|
|
757
|
+
session_id: goal.assignee.session_id,
|
|
758
|
+
harness: goal.assignee.harness,
|
|
759
|
+
},
|
|
760
|
+
subtask_id: goal.subtask_id,
|
|
761
|
+
attempt: goal.attempt,
|
|
762
|
+
owner_epoch: goal.coordinator.owner_epoch,
|
|
763
|
+
body: {
|
|
764
|
+
assignment_ref: assignmentFile,
|
|
765
|
+
assignment_digest: assignmentDigest,
|
|
766
|
+
input_revision: promptDigest,
|
|
767
|
+
},
|
|
768
|
+
}, { clock: clock });
|
|
769
|
+
const submitted = submitMessage(taskRoot, assignmentMessage, {
|
|
770
|
+
clock: clock,
|
|
771
|
+
});
|
|
772
|
+
if (!submitted.ok) {
|
|
773
|
+
for (const claimId of claimIds) {
|
|
774
|
+
releaseClaim(taskRoot, {
|
|
775
|
+
claimId,
|
|
776
|
+
reason: 'assignment registration failed',
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return failure('failed', submitted.reason, 'Assignment could not be registered.');
|
|
780
|
+
}
|
|
781
|
+
const afterAssignment = await namespaceStillCurrent(namespace);
|
|
782
|
+
if (afterAssignment !== null) {
|
|
783
|
+
return failure('conflict', 'assignment_registered_namespace_changed', `${afterAssignment.message}; assignment is registered and claims remain held`, 'Inspect the private task path and task/claim state before reconciliation.');
|
|
784
|
+
}
|
|
785
|
+
const record = readRecord(taskRoot);
|
|
786
|
+
const prepared = {
|
|
787
|
+
schema: 'pappra/collaboration-prepared/1',
|
|
788
|
+
task_id: goal.task_id,
|
|
789
|
+
subtask_id: goal.subtask_id,
|
|
790
|
+
attempt: goal.attempt,
|
|
791
|
+
phase: goal.phase,
|
|
792
|
+
task_root: taskRoot,
|
|
793
|
+
domain_root: fs.realpathSync(options.domainRoot),
|
|
794
|
+
prepared_dir: preparedDir,
|
|
795
|
+
assignment_path: assignmentFile,
|
|
796
|
+
prompt_path: promptFile,
|
|
797
|
+
goal_path: goalFile,
|
|
798
|
+
assignment_digest: assignmentDigest,
|
|
799
|
+
prompt_digest: promptDigest,
|
|
800
|
+
resource_epochs: Object.fromEntries(goal.resources.map((item) => [item.path, resourceEpochs[item.path]])),
|
|
801
|
+
claim_ids: claimIds,
|
|
802
|
+
state_revision: record?.state_revision ?? submitted.state_revision ?? 0,
|
|
803
|
+
created_at: nowIso(clock),
|
|
804
|
+
};
|
|
805
|
+
const preparedFile = path.join(preparedDir, 'prepared.json');
|
|
806
|
+
const publicationProblem = await writeInNamespace(namespace, preparedFile, `${JSON.stringify(prepared, null, 2)}\n`);
|
|
807
|
+
if (publicationProblem !== null) {
|
|
808
|
+
return failure(publicationProblem.status, publicationProblem.code === 'private_namespace_redirected'
|
|
809
|
+
? 'assignment_registered_namespace_changed'
|
|
810
|
+
: 'prepared_publication_failed', `${publicationProblem.message}; assignment is registered and claims remain held; prepared residual: ${preparedFile}`, 'Inspect the prepared residual and task/claim state before reconciliation.');
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
ok: true,
|
|
814
|
+
value: { prepared, goal, assignment },
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
export async function admitPrepared(preparedFile, capabilitiesFile, options = {}) {
|
|
818
|
+
const bundle = preparedBundle(preparedFile);
|
|
819
|
+
if (!bundle.ok)
|
|
820
|
+
return bundle;
|
|
821
|
+
const { prepared, goal, assignment } = bundle.value;
|
|
822
|
+
const card = (() => {
|
|
823
|
+
try {
|
|
824
|
+
return JSON.parse(fs.readFileSync(capabilitiesFile, 'utf8'));
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
return error;
|
|
828
|
+
}
|
|
829
|
+
})();
|
|
830
|
+
if (card instanceof Error) {
|
|
831
|
+
return failure('invalid', 'capabilities_invalid', card.message);
|
|
832
|
+
}
|
|
833
|
+
const admissionOptions = {
|
|
834
|
+
assignmentFile: prepared.assignment_path,
|
|
835
|
+
capabilitiesFile,
|
|
836
|
+
taskRoot: prepared.task_root,
|
|
837
|
+
cwd: goal.assignee.cwd,
|
|
838
|
+
sessionId: goal.assignee.session_id,
|
|
839
|
+
harness: goal.assignee.harness,
|
|
840
|
+
launchKind: 'first',
|
|
841
|
+
now: options.clock ?? Date.now(),
|
|
842
|
+
};
|
|
843
|
+
const admission = admitLaunch(admissionOptions);
|
|
844
|
+
if (!admission.ok) {
|
|
845
|
+
return failure(admission.reason === 'resource_conflict' ? 'conflict' : 'needs_input', admission.reason, 'Launch admission refused.', 'Inspect admission checks and supply fresh capability evidence.');
|
|
846
|
+
}
|
|
847
|
+
const token = createToken({
|
|
848
|
+
taskRoot: prepared.task_root,
|
|
849
|
+
assignmentDigest: prepared.assignment_digest,
|
|
850
|
+
subtaskId: prepared.subtask_id,
|
|
851
|
+
attempt: prepared.attempt,
|
|
852
|
+
reservationId: null,
|
|
853
|
+
assignee: assignment.assignee,
|
|
854
|
+
});
|
|
855
|
+
return {
|
|
856
|
+
ok: true,
|
|
857
|
+
value: { prepared, goal, assignment, admission, token },
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
export async function startPrepared(preparedFile, options) {
|
|
861
|
+
const clock = options.clock ?? Date.now();
|
|
862
|
+
const admitted = await admitPrepared(preparedFile, options.capabilitiesFile, {
|
|
863
|
+
clock,
|
|
864
|
+
});
|
|
865
|
+
if (!admitted.ok)
|
|
866
|
+
return admitted;
|
|
867
|
+
const { prepared, goal, assignment } = admitted.value;
|
|
868
|
+
const namespaceProblem = preparedFilePathFailure(preparedFile);
|
|
869
|
+
if (namespaceProblem !== null)
|
|
870
|
+
return namespaceProblem;
|
|
871
|
+
const captured = await capturePreparedNamespace(preparedFile);
|
|
872
|
+
if (!captured.ok)
|
|
873
|
+
return captured.failure;
|
|
874
|
+
let token = admitted.value.token;
|
|
875
|
+
let reservationId = null;
|
|
876
|
+
if (options.controllerId === 'fixture') {
|
|
877
|
+
const reservation = reserveLaunch(prepared.task_root, {
|
|
878
|
+
expectRevision: prepared.state_revision,
|
|
879
|
+
clock,
|
|
880
|
+
reservation: {
|
|
881
|
+
reservation_id: `p10-${crypto.randomUUID()}`,
|
|
882
|
+
subtask_id: prepared.subtask_id,
|
|
883
|
+
attempt: prepared.attempt,
|
|
884
|
+
kind: 'first',
|
|
885
|
+
mode: 'work',
|
|
886
|
+
phase: prepared.phase,
|
|
887
|
+
holder: assignment.assignee,
|
|
888
|
+
pid: process.pid,
|
|
889
|
+
requested_session_id: assignment.assignee.session_id,
|
|
890
|
+
transport: 'fixture',
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
if (!reservation.ok) {
|
|
894
|
+
return failure(reservation.reason === 'launch_already_reserved'
|
|
895
|
+
? 'conflict'
|
|
896
|
+
: 'needs_input', reservation.reason, 'Only one dispatch may reserve this attempt.');
|
|
897
|
+
}
|
|
898
|
+
reservationId = reservation.reservation_id;
|
|
899
|
+
const reservedToken = createToken({
|
|
900
|
+
taskRoot: prepared.task_root,
|
|
901
|
+
assignmentDigest: prepared.assignment_digest,
|
|
902
|
+
subtaskId: prepared.subtask_id,
|
|
903
|
+
attempt: prepared.attempt,
|
|
904
|
+
reservationId,
|
|
905
|
+
assignee: assignment.assignee,
|
|
906
|
+
});
|
|
907
|
+
LAUNCH_TOKENS.delete(token);
|
|
908
|
+
token = reservedToken;
|
|
909
|
+
}
|
|
910
|
+
const promptNamespaceProblem = preparedFilePathFailure(preparedFile);
|
|
911
|
+
if (promptNamespaceProblem !== null) {
|
|
912
|
+
return promptNamespaceProblem;
|
|
913
|
+
}
|
|
914
|
+
let prompt;
|
|
915
|
+
try {
|
|
916
|
+
prompt = readBoundedFile(prepared.prompt_path, 64 * 1024);
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
if (reservationId !== null) {
|
|
920
|
+
settleLaunch(prepared.task_root, {
|
|
921
|
+
reservationId,
|
|
922
|
+
outcome: 'not_started',
|
|
923
|
+
detail: 'prepared prompt unavailable',
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return failure('invalid', 'prepared_prompt_invalid', error instanceof Error ? error.message : String(error));
|
|
927
|
+
}
|
|
928
|
+
const request = {
|
|
929
|
+
prepared,
|
|
930
|
+
goal,
|
|
931
|
+
assignment,
|
|
932
|
+
capabilitiesFile: options.capabilitiesFile,
|
|
933
|
+
prompt,
|
|
934
|
+
promptFile: prepared.prompt_path,
|
|
935
|
+
taskRoot: prepared.task_root,
|
|
936
|
+
evidenceDir: path.join(prepared.task_root, 'p10', 'evidence'),
|
|
937
|
+
namespace: captured.value,
|
|
938
|
+
};
|
|
939
|
+
let started;
|
|
940
|
+
try {
|
|
941
|
+
started = await options.controller.start(token, request);
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
started = {
|
|
945
|
+
ok: false,
|
|
946
|
+
status: 'failed',
|
|
947
|
+
code: 'controller_start_failed',
|
|
948
|
+
message: error instanceof Error ? error.message : String(error),
|
|
949
|
+
nextAction: 'Inspect the controller and native process state.',
|
|
950
|
+
possible_start: true,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
if (!started.ok) {
|
|
954
|
+
if (reservationId !== null) {
|
|
955
|
+
settleLaunch(prepared.task_root, {
|
|
956
|
+
reservationId,
|
|
957
|
+
outcome: started.possible_start === true ? 'spawn_failed' : 'not_started',
|
|
958
|
+
detail: started.message,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
return failure(started.status, started.code, started.message, started.nextAction);
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
ok: true,
|
|
965
|
+
value: {
|
|
966
|
+
prepared,
|
|
967
|
+
goal,
|
|
968
|
+
assignment,
|
|
969
|
+
token,
|
|
970
|
+
controller: options.controller,
|
|
971
|
+
session: started.session,
|
|
972
|
+
reservationId,
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
export function submitResultReport(started, finish, { clock = Date.now() } = {}) {
|
|
977
|
+
const messageId = `report-${started.prepared.task_id}-${started.prepared.subtask_id}-${started.prepared.attempt}`;
|
|
978
|
+
const outputs = finish.outputs.slice(0, 32);
|
|
979
|
+
const body = {
|
|
980
|
+
status: finish.status,
|
|
981
|
+
outputs,
|
|
982
|
+
native_ref: finish.nativeRef,
|
|
983
|
+
evidence_ref: finish.evidenceRef,
|
|
984
|
+
};
|
|
985
|
+
if (boundedJson(body, 64 * 1024) === null) {
|
|
986
|
+
return {
|
|
987
|
+
ok: false,
|
|
988
|
+
messageId,
|
|
989
|
+
duplicate: false,
|
|
990
|
+
classification: null,
|
|
991
|
+
detail: 'result exceeds the bounded report size',
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
const message = buildMessage({
|
|
995
|
+
type: 'REPORT',
|
|
996
|
+
message_id: messageId,
|
|
997
|
+
task_id: started.prepared.task_id,
|
|
998
|
+
from: {
|
|
999
|
+
agent_id: started.assignment.assignee.agent_id,
|
|
1000
|
+
session_id: started.assignment.assignee.session_id,
|
|
1001
|
+
harness: started.assignment.assignee.harness,
|
|
1002
|
+
},
|
|
1003
|
+
to: {
|
|
1004
|
+
agent_id: started.goal.coordinator.agent_id,
|
|
1005
|
+
session_id: started.goal.coordinator.session_id,
|
|
1006
|
+
harness: started.goal.coordinator.harness,
|
|
1007
|
+
},
|
|
1008
|
+
subtask_id: started.prepared.subtask_id,
|
|
1009
|
+
attempt: started.prepared.attempt,
|
|
1010
|
+
owner_epoch: started.goal.coordinator.owner_epoch,
|
|
1011
|
+
body,
|
|
1012
|
+
}, { clock });
|
|
1013
|
+
const submitted = submitMessage(started.prepared.task_root, message, {
|
|
1014
|
+
clock,
|
|
1015
|
+
});
|
|
1016
|
+
if (!submitted.ok) {
|
|
1017
|
+
return {
|
|
1018
|
+
ok: false,
|
|
1019
|
+
messageId,
|
|
1020
|
+
duplicate: false,
|
|
1021
|
+
classification: null,
|
|
1022
|
+
detail: submitted.reason,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const record = readRecord(started.prepared.task_root);
|
|
1026
|
+
const stored = record?.results?.find((entry) => entry.message_id === messageId &&
|
|
1027
|
+
entry.subtask_id === started.prepared.subtask_id &&
|
|
1028
|
+
entry.attempt === started.prepared.attempt);
|
|
1029
|
+
const appliedMessage = record?.messages?.find((entry) => entry.message_id === messageId);
|
|
1030
|
+
const effect = submitted.applied.result;
|
|
1031
|
+
const classification = stored?.classification ??
|
|
1032
|
+
(effect && typeof effect.classification === 'string'
|
|
1033
|
+
? effect.classification
|
|
1034
|
+
: null);
|
|
1035
|
+
const fixedContext = stored !== undefined &&
|
|
1036
|
+
appliedMessage !== undefined &&
|
|
1037
|
+
appliedMessage.from.agent_id === started.assignment.assignee.agent_id &&
|
|
1038
|
+
appliedMessage.from.session_id === started.assignment.assignee.session_id &&
|
|
1039
|
+
appliedMessage.from.harness === started.assignment.assignee.harness &&
|
|
1040
|
+
appliedMessage.owner_epoch === started.goal.coordinator.owner_epoch &&
|
|
1041
|
+
stored.input_revision === started.prepared.prompt_digest &&
|
|
1042
|
+
stored.status === finish.status;
|
|
1043
|
+
const finalStatus = ['completed', 'failed', 'cancelled'].includes(finish.status);
|
|
1044
|
+
const accepted = finalStatus && fixedContext && classification === 'applied';
|
|
1045
|
+
return {
|
|
1046
|
+
ok: accepted,
|
|
1047
|
+
messageId,
|
|
1048
|
+
duplicate: submitted.duplicate === true,
|
|
1049
|
+
classification,
|
|
1050
|
+
detail: accepted
|
|
1051
|
+
? null
|
|
1052
|
+
: `result_not_accepted:${classification ?? 'unknown'}`,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
export async function finishStarted(started) {
|
|
1056
|
+
let finish;
|
|
1057
|
+
try {
|
|
1058
|
+
finish = await started.session.finish();
|
|
1059
|
+
}
|
|
1060
|
+
catch (error) {
|
|
1061
|
+
return failure('failed', 'native_finish_failed', error instanceof Error ? error.message : String(error));
|
|
1062
|
+
}
|
|
1063
|
+
if (!['completed', 'failed', 'cancelled', 'needs_input', 'unknown'].includes(finish.status) ||
|
|
1064
|
+
finish.outputs.length > 32 ||
|
|
1065
|
+
boundedJson(finish.outputs, 64 * 1024) === null) {
|
|
1066
|
+
return failure('failed', 'native_result_invalid', 'Native result is malformed.');
|
|
1067
|
+
}
|
|
1068
|
+
if (finish.sessionId !== null &&
|
|
1069
|
+
finish.sessionId !== started.assignment.assignee.session_id) {
|
|
1070
|
+
return failure('conflict', 'native_session_drift', 'Native result belongs to a different session.');
|
|
1071
|
+
}
|
|
1072
|
+
for (const output of finish.outputs) {
|
|
1073
|
+
if (output === null ||
|
|
1074
|
+
typeof output !== 'object' ||
|
|
1075
|
+
!('ref' in output) ||
|
|
1076
|
+
!('digest' in output) ||
|
|
1077
|
+
typeof output.ref !== 'string' ||
|
|
1078
|
+
!path.isAbsolute(output.ref) ||
|
|
1079
|
+
typeof output.digest !== 'string' ||
|
|
1080
|
+
!/^sha256:[0-9a-f]{64}$/.test(output.digest)) {
|
|
1081
|
+
return failure('invalid', 'result_output_invalid', 'Every result output must be an absolute ref with a SHA-256 digest.');
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
if (sha256File(output.ref) !== output.digest) {
|
|
1085
|
+
return failure('conflict', 'result_output_changed', 'Result output digest does not match.');
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
return failure('failed', 'result_output_unavailable', 'Result output is unavailable.');
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (started.reservationId !== null) {
|
|
1093
|
+
const settled = settleLaunch(started.prepared.task_root, {
|
|
1094
|
+
reservationId: started.reservationId,
|
|
1095
|
+
outcome: finish.outcome === 'exited' ||
|
|
1096
|
+
finish.outcome === 'exited_nonzero' ||
|
|
1097
|
+
finish.outcome === 'timed_out'
|
|
1098
|
+
? finish.outcome
|
|
1099
|
+
: 'exited_nonzero',
|
|
1100
|
+
exitCode: finish.exitCode,
|
|
1101
|
+
sessionId: finish.sessionId,
|
|
1102
|
+
runner: started.controller.id,
|
|
1103
|
+
evidenceRef: finish.evidenceRef,
|
|
1104
|
+
detail: finish.status,
|
|
1105
|
+
});
|
|
1106
|
+
if (!settled.ok) {
|
|
1107
|
+
const record = readRecord(started.prepared.task_root);
|
|
1108
|
+
const attempt = record
|
|
1109
|
+
? findAttempt(record, started.prepared.subtask_id, started.prepared.attempt)
|
|
1110
|
+
: null;
|
|
1111
|
+
if (attempt === null ||
|
|
1112
|
+
!['completed', 'failed', 'cancelled'].includes(attempt.state)) {
|
|
1113
|
+
return failure('failed', 'settlement_failed', `Native settlement failed: ${settled.reason}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return { ok: true, value: finish };
|
|
1118
|
+
}
|
|
1119
|
+
export async function driveRun(started, commands, emit, { clock = Date.now() } = {}) {
|
|
1120
|
+
let sequence = 0;
|
|
1121
|
+
const seenRequests = new Set();
|
|
1122
|
+
const send = (kind, requestId, status, detail, data) => {
|
|
1123
|
+
sequence += 1;
|
|
1124
|
+
const normalizedStatus = status.trim() === '' ? 'unknown' : status;
|
|
1125
|
+
const safeStatus = normalizedStatus.length > 64
|
|
1126
|
+
? `${normalizedStatus.slice(0, 61)}...`
|
|
1127
|
+
: normalizedStatus;
|
|
1128
|
+
const safeDetail = detail === null
|
|
1129
|
+
? null
|
|
1130
|
+
: detail.length > 4096
|
|
1131
|
+
? `${detail.slice(0, 4093)}...`
|
|
1132
|
+
: detail;
|
|
1133
|
+
const safeData = data === null || boundedJson(data, 8192) !== null
|
|
1134
|
+
? data
|
|
1135
|
+
: { truncated: true, detail: 'observation exceeded the event budget' };
|
|
1136
|
+
emit({
|
|
1137
|
+
schema: 'pappra/collaboration-event/1',
|
|
1138
|
+
sequence,
|
|
1139
|
+
request_id: requestId,
|
|
1140
|
+
kind,
|
|
1141
|
+
at: nowIso(clock),
|
|
1142
|
+
status: safeStatus,
|
|
1143
|
+
detail: safeDetail,
|
|
1144
|
+
data: safeData,
|
|
1145
|
+
});
|
|
1146
|
+
};
|
|
1147
|
+
try {
|
|
1148
|
+
send('started', null, 'started', null, {
|
|
1149
|
+
task_id: started.prepared.task_id,
|
|
1150
|
+
subtask_id: started.prepared.subtask_id,
|
|
1151
|
+
attempt: started.prepared.attempt,
|
|
1152
|
+
controller: started.controller.id,
|
|
1153
|
+
reservation_id: started.reservationId,
|
|
1154
|
+
});
|
|
1155
|
+
for await (const raw of commands) {
|
|
1156
|
+
let command;
|
|
1157
|
+
try {
|
|
1158
|
+
command = collaborationCommandSchema.parse(raw);
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
send('refused', null, 'invalid', error instanceof Error ? error.message : String(error), null);
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
if (seenRequests.has(command.request_id)) {
|
|
1165
|
+
send('refused', command.request_id, 'duplicate_request', 'request_id was already processed by this run', null);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
seenRequests.add(command.request_id);
|
|
1169
|
+
if (command.action === 'query') {
|
|
1170
|
+
try {
|
|
1171
|
+
const observation = await started.session.query();
|
|
1172
|
+
send('query', command.request_id, observation.status, observation.detail, observation.data);
|
|
1173
|
+
}
|
|
1174
|
+
catch (error) {
|
|
1175
|
+
send('refused', command.request_id, 'failed', error instanceof Error ? error.message : String(error), null);
|
|
1176
|
+
}
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (command.action === 'cancel') {
|
|
1180
|
+
const intent = buildMessage({
|
|
1181
|
+
type: 'CANCEL',
|
|
1182
|
+
task_id: started.prepared.task_id,
|
|
1183
|
+
from: {
|
|
1184
|
+
agent_id: started.goal.coordinator.agent_id,
|
|
1185
|
+
session_id: started.goal.coordinator.session_id,
|
|
1186
|
+
harness: started.goal.coordinator.harness,
|
|
1187
|
+
},
|
|
1188
|
+
to: {
|
|
1189
|
+
agent_id: started.assignment.assignee.agent_id,
|
|
1190
|
+
session_id: started.assignment.assignee.session_id,
|
|
1191
|
+
harness: started.assignment.assignee.harness,
|
|
1192
|
+
},
|
|
1193
|
+
subtask_id: started.prepared.subtask_id,
|
|
1194
|
+
attempt: started.prepared.attempt,
|
|
1195
|
+
owner_epoch: started.goal.coordinator.owner_epoch,
|
|
1196
|
+
body: { reason: 'operator cancellation request' },
|
|
1197
|
+
}, { clock });
|
|
1198
|
+
const submitted = submitMessage(started.prepared.task_root, intent, {
|
|
1199
|
+
clock,
|
|
1200
|
+
});
|
|
1201
|
+
if (!submitted.ok) {
|
|
1202
|
+
send('cancel', command.request_id, 'cancel-intent-failed', 'P01 cancellation intent was not recorded', {
|
|
1203
|
+
intent: false,
|
|
1204
|
+
native: {
|
|
1205
|
+
called: false,
|
|
1206
|
+
reason: submitted.reason,
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
const cancel = await started.session.cancel();
|
|
1213
|
+
send('cancel', command.request_id, 'cancel-requested', null, {
|
|
1214
|
+
intent: true,
|
|
1215
|
+
native: cancel,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
send('refused', command.request_id, 'failed', error instanceof Error ? error.message : String(error), { intent: true, native: { called: true } });
|
|
1220
|
+
}
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const finished = await finishStarted(started);
|
|
1224
|
+
if (!finished.ok) {
|
|
1225
|
+
send('refused', command.request_id, finished.status, finished.message, null);
|
|
1226
|
+
return finished;
|
|
1227
|
+
}
|
|
1228
|
+
if (finished.value.status !== command.status) {
|
|
1229
|
+
send('refused', command.request_id, 'conflict', 'finish status does not match the native result', null);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const report = submitResultReport(started, finished.value, { clock });
|
|
1233
|
+
send('finished', command.request_id, report.ok ? 'finished' : 'failed', report.detail, {
|
|
1234
|
+
status: finished.value.status,
|
|
1235
|
+
outcome: finished.value.outcome,
|
|
1236
|
+
report,
|
|
1237
|
+
});
|
|
1238
|
+
return report.ok
|
|
1239
|
+
? { ok: true, value: report }
|
|
1240
|
+
: failure('failed', 'report_failed', report.detail ?? 'REPORT failed.');
|
|
1241
|
+
}
|
|
1242
|
+
return failure('needs_input', 'run_closed', 'Run input closed before a final REPORT.');
|
|
1243
|
+
}
|
|
1244
|
+
catch (error) {
|
|
1245
|
+
send('refused', null, 'failed', error instanceof Error ? error.message : String(error), null);
|
|
1246
|
+
return failure('failed', 'run_stream_failed', error instanceof Error ? error.message : String(error));
|
|
1247
|
+
}
|
|
1248
|
+
finally {
|
|
1249
|
+
await started.session.dispose?.().catch(() => undefined);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
export class FixtureController {
|
|
1253
|
+
options;
|
|
1254
|
+
id = 'fixture';
|
|
1255
|
+
constructor(options = {}) {
|
|
1256
|
+
this.options = options;
|
|
1257
|
+
}
|
|
1258
|
+
async start(token, request) {
|
|
1259
|
+
let facts;
|
|
1260
|
+
try {
|
|
1261
|
+
facts = consumeToken(token, {
|
|
1262
|
+
taskRoot: request.taskRoot,
|
|
1263
|
+
assignmentDigest: request.prepared.assignment_digest,
|
|
1264
|
+
subtaskId: request.prepared.subtask_id,
|
|
1265
|
+
attempt: request.prepared.attempt,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
catch (error) {
|
|
1269
|
+
return {
|
|
1270
|
+
ok: false,
|
|
1271
|
+
status: 'invalid',
|
|
1272
|
+
code: 'admission_token_required',
|
|
1273
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1274
|
+
nextAction: 'Use startPrepared; a fabricated token is not authority.',
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
const status = this.options.status ?? 'completed';
|
|
1278
|
+
return {
|
|
1279
|
+
ok: true,
|
|
1280
|
+
session: {
|
|
1281
|
+
query: async () => ({
|
|
1282
|
+
status: 'live',
|
|
1283
|
+
detail: 'fixture controller has no native process',
|
|
1284
|
+
data: { fixture: true, reservation_id: facts.reservationId },
|
|
1285
|
+
}),
|
|
1286
|
+
cancel: async () => ({
|
|
1287
|
+
acknowledged: true,
|
|
1288
|
+
terminal_observed: false,
|
|
1289
|
+
stop_evidence: { kind: 'fixture', stopped: false },
|
|
1290
|
+
}),
|
|
1291
|
+
finish: async () => ({
|
|
1292
|
+
outcome: status === 'completed' ? 'exited' : 'exited_nonzero',
|
|
1293
|
+
status,
|
|
1294
|
+
exitCode: status === 'completed' ? 0 : 1,
|
|
1295
|
+
sessionId: this.options.sessionId ?? request.assignment.assignee.session_id,
|
|
1296
|
+
outputs: this.options.outputs ?? [],
|
|
1297
|
+
nativeRef: {
|
|
1298
|
+
kind: 'fixture',
|
|
1299
|
+
session_id: request.assignment.assignee.session_id,
|
|
1300
|
+
},
|
|
1301
|
+
evidenceRef: null,
|
|
1302
|
+
}),
|
|
1303
|
+
dispose: async () => undefined,
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
export class PiRpcController {
|
|
1309
|
+
id = 'pi-rpc';
|
|
1310
|
+
async start(token, request) {
|
|
1311
|
+
if (!validateLaunchToken(token, {
|
|
1312
|
+
taskRoot: request.taskRoot,
|
|
1313
|
+
assignmentDigest: request.prepared.assignment_digest,
|
|
1314
|
+
subtaskId: request.prepared.subtask_id,
|
|
1315
|
+
attempt: request.prepared.attempt,
|
|
1316
|
+
})) {
|
|
1317
|
+
return {
|
|
1318
|
+
ok: false,
|
|
1319
|
+
status: 'invalid',
|
|
1320
|
+
code: 'admission_token_required',
|
|
1321
|
+
message: 'genuine admission token required',
|
|
1322
|
+
nextAction: 'Use startPrepared; a fabricated token is not authority.',
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const started = await startOwnedPiRpc({
|
|
1326
|
+
taskRoot: request.taskRoot,
|
|
1327
|
+
cwd: request.assignment.assignee.cwd,
|
|
1328
|
+
assignmentFile: request.prepared.assignment_path,
|
|
1329
|
+
capabilitiesFile: request.capabilitiesFile,
|
|
1330
|
+
promptFile: request.promptFile,
|
|
1331
|
+
evidenceDir: request.evidenceDir,
|
|
1332
|
+
profile: 'daily',
|
|
1333
|
+
phase: request.goal.phase,
|
|
1334
|
+
sessionId: request.assignment.assignee.session_id,
|
|
1335
|
+
launchKind: 'first',
|
|
1336
|
+
...(request.namespace === undefined
|
|
1337
|
+
? {}
|
|
1338
|
+
: {
|
|
1339
|
+
namespaceGuard: () => {
|
|
1340
|
+
const issue = namespaceSyncFailure(request.namespace);
|
|
1341
|
+
if (issue !== null)
|
|
1342
|
+
throw new Error(issue);
|
|
1343
|
+
},
|
|
1344
|
+
}),
|
|
1345
|
+
});
|
|
1346
|
+
if (!started.ok) {
|
|
1347
|
+
const reason = 'reason' in started && typeof started.reason === 'string'
|
|
1348
|
+
? started.reason
|
|
1349
|
+
: 'native_refused';
|
|
1350
|
+
const stage = 'stage' in started && typeof started.stage === 'string'
|
|
1351
|
+
? started.stage
|
|
1352
|
+
: 'native';
|
|
1353
|
+
return {
|
|
1354
|
+
ok: false,
|
|
1355
|
+
status: reason.includes('conflict') ? 'conflict' : 'needs_input',
|
|
1356
|
+
code: reason,
|
|
1357
|
+
message: `Pi RPC refused at ${stage}`,
|
|
1358
|
+
nextAction: 'Inspect the native admission and reservation evidence.',
|
|
1359
|
+
possible_start: stage === 'identity',
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
const nativeSessionFile = started.session.sessionFile();
|
|
1363
|
+
const nativeSessionBaseline = regularFileSize(nativeSessionFile);
|
|
1364
|
+
try {
|
|
1365
|
+
await started.session.prompt();
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
await started.session.finish().catch(() => undefined);
|
|
1369
|
+
return {
|
|
1370
|
+
ok: false,
|
|
1371
|
+
status: 'failed',
|
|
1372
|
+
code: 'native_prompt_failed',
|
|
1373
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1374
|
+
nextAction: 'The owned RPC child was shut down; inspect native evidence before reconciliation.',
|
|
1375
|
+
possible_start: true,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
let disposed = false;
|
|
1379
|
+
let currentResultEvidenceFile = null;
|
|
1380
|
+
const liveNamespaceProblem = () => request.namespace === undefined
|
|
1381
|
+
? null
|
|
1382
|
+
: namespaceSyncFailure(request.namespace);
|
|
1383
|
+
return {
|
|
1384
|
+
ok: true,
|
|
1385
|
+
session: {
|
|
1386
|
+
query: async () => {
|
|
1387
|
+
if (disposed) {
|
|
1388
|
+
return {
|
|
1389
|
+
status: 'disposed',
|
|
1390
|
+
detail: 'owned Pi RPC child was closed by this run',
|
|
1391
|
+
data: null,
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
const namespaceProblem = liveNamespaceProblem();
|
|
1395
|
+
if (namespaceProblem !== null) {
|
|
1396
|
+
return {
|
|
1397
|
+
status: 'unknown',
|
|
1398
|
+
detail: `private namespace changed: ${namespaceProblem}`,
|
|
1399
|
+
data: null,
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
try {
|
|
1403
|
+
const data = await started.session.query();
|
|
1404
|
+
const after = liveNamespaceProblem();
|
|
1405
|
+
if (after !== null) {
|
|
1406
|
+
return {
|
|
1407
|
+
status: 'unknown',
|
|
1408
|
+
detail: `private namespace changed during query: ${after}`,
|
|
1409
|
+
data: null,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
return {
|
|
1413
|
+
status: 'live',
|
|
1414
|
+
detail: 'owned Pi RPC handle',
|
|
1415
|
+
data,
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
catch (error) {
|
|
1419
|
+
return {
|
|
1420
|
+
status: 'unknown',
|
|
1421
|
+
detail: error instanceof Error ? error.message : 'owned query failed',
|
|
1422
|
+
data: null,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
cancel: async () => {
|
|
1427
|
+
const namespaceProblem = liveNamespaceProblem();
|
|
1428
|
+
if (namespaceProblem !== null) {
|
|
1429
|
+
return {
|
|
1430
|
+
acknowledged: false,
|
|
1431
|
+
terminal_observed: false,
|
|
1432
|
+
stop_evidence: {
|
|
1433
|
+
namespace_changed: true,
|
|
1434
|
+
detail: namespaceProblem,
|
|
1435
|
+
},
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
const cancel = await started.session.cancel();
|
|
1439
|
+
const entry = cancel !== null && typeof cancel === 'object'
|
|
1440
|
+
? cancel
|
|
1441
|
+
: {};
|
|
1442
|
+
const abort = entry['abort'] !== null && typeof entry['abort'] === 'object'
|
|
1443
|
+
? entry['abort']
|
|
1444
|
+
: {};
|
|
1445
|
+
return {
|
|
1446
|
+
acknowledged: abort['success'] === true,
|
|
1447
|
+
terminal_observed: entry['agent_settled'] !== null &&
|
|
1448
|
+
entry['agent_settled'] !== undefined,
|
|
1449
|
+
stop_evidence: cancel,
|
|
1450
|
+
};
|
|
1451
|
+
},
|
|
1452
|
+
finish: async () => {
|
|
1453
|
+
const initialNamespaceProblem = liveNamespaceProblem();
|
|
1454
|
+
if (initialNamespaceProblem !== null) {
|
|
1455
|
+
disposed = true;
|
|
1456
|
+
await started.session.shutdown().catch(() => undefined);
|
|
1457
|
+
return {
|
|
1458
|
+
outcome: 'unknown',
|
|
1459
|
+
status: 'unknown',
|
|
1460
|
+
exitCode: null,
|
|
1461
|
+
sessionId: null,
|
|
1462
|
+
outputs: [],
|
|
1463
|
+
nativeRef: {
|
|
1464
|
+
reason: 'private_namespace_redirected',
|
|
1465
|
+
detail: initialNamespaceProblem,
|
|
1466
|
+
current_result_published: false,
|
|
1467
|
+
},
|
|
1468
|
+
evidenceRef: null,
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
try {
|
|
1472
|
+
await started.session.awaitSettled();
|
|
1473
|
+
}
|
|
1474
|
+
catch {
|
|
1475
|
+
// finish() below still records the bounded settlement/failure.
|
|
1476
|
+
}
|
|
1477
|
+
const result = await started.session.finish();
|
|
1478
|
+
const value = result !== null && typeof result === 'object'
|
|
1479
|
+
? result
|
|
1480
|
+
: {};
|
|
1481
|
+
const exit = value['exit'] !== null && typeof value['exit'] === 'object'
|
|
1482
|
+
? value['exit']
|
|
1483
|
+
: {};
|
|
1484
|
+
const evidence = value['evidence'] !== null && typeof value['evidence'] === 'object'
|
|
1485
|
+
? value['evidence']
|
|
1486
|
+
: {};
|
|
1487
|
+
const settlement = value['settlement'] !== null &&
|
|
1488
|
+
typeof value['settlement'] === 'object'
|
|
1489
|
+
? value['settlement']
|
|
1490
|
+
: {};
|
|
1491
|
+
const nativeResult = value['result'] !== null && typeof value['result'] === 'object'
|
|
1492
|
+
? value['result']
|
|
1493
|
+
: {};
|
|
1494
|
+
const outcome = typeof value['outcome'] === 'string' ? value['outcome'] : 'unknown';
|
|
1495
|
+
const evidenceWritten = evidence['written'] === true;
|
|
1496
|
+
const ok = value['ok'] === true &&
|
|
1497
|
+
outcome === 'exited' &&
|
|
1498
|
+
value['identity'] === 'match' &&
|
|
1499
|
+
evidenceWritten &&
|
|
1500
|
+
settlement['ok'] === true &&
|
|
1501
|
+
nativeResult['ok'] === true;
|
|
1502
|
+
const executionFile = typeof evidence['execution_file'] === 'string'
|
|
1503
|
+
? evidence['execution_file']
|
|
1504
|
+
: null;
|
|
1505
|
+
const proofFile = typeof evidence['proof_file'] === 'string'
|
|
1506
|
+
? evidence['proof_file']
|
|
1507
|
+
: null;
|
|
1508
|
+
const sessionFile = typeof value['session_file'] === 'string' &&
|
|
1509
|
+
path.isAbsolute(value['session_file'])
|
|
1510
|
+
? value['session_file']
|
|
1511
|
+
: nativeSessionFile;
|
|
1512
|
+
const afterLowerProblem = liveNamespaceProblem();
|
|
1513
|
+
const sessionFileInside = request.namespace === undefined ||
|
|
1514
|
+
(sessionFile !== null &&
|
|
1515
|
+
isPathInside(request.namespace.privateDir.path, sessionFile));
|
|
1516
|
+
const current = afterLowerProblem === null && sessionFileInside
|
|
1517
|
+
? currentNativeResult(sessionFile, nativeSessionBaseline)
|
|
1518
|
+
: null;
|
|
1519
|
+
const currentStatus = current?.status ?? 'unknown';
|
|
1520
|
+
const currentReason = current?.reason ??
|
|
1521
|
+
afterLowerProblem ??
|
|
1522
|
+
(sessionFileInside
|
|
1523
|
+
? 'current_result_unavailable'
|
|
1524
|
+
: 'native session file is outside the private namespace');
|
|
1525
|
+
let publicationError = afterLowerProblem ??
|
|
1526
|
+
(sessionFileInside
|
|
1527
|
+
? null
|
|
1528
|
+
: 'native session file is outside the private namespace');
|
|
1529
|
+
if (current !== null) {
|
|
1530
|
+
const resultFile = path.join(path.dirname(sessionFile ?? ''), `${path.basename(sessionFile ?? 'session')}.current-result.json`);
|
|
1531
|
+
const resultBytes = `${JSON.stringify({
|
|
1532
|
+
schema: 'harness-collab/native-current-result/1',
|
|
1533
|
+
session_file: sessionFile,
|
|
1534
|
+
baseline_bytes: nativeSessionBaseline,
|
|
1535
|
+
status: currentStatus,
|
|
1536
|
+
reason: currentReason,
|
|
1537
|
+
record_count: current.records.length,
|
|
1538
|
+
records: current.records,
|
|
1539
|
+
}, null, 2)}\n`;
|
|
1540
|
+
if (request.namespace === undefined) {
|
|
1541
|
+
publicationError =
|
|
1542
|
+
'captured private namespace is unavailable for current-result evidence';
|
|
1543
|
+
currentResultEvidenceFile = null;
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
const published = publishCurrentResult(request.namespace, resultFile, resultBytes);
|
|
1547
|
+
if (published.ok) {
|
|
1548
|
+
currentResultEvidenceFile = published.ref;
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
publicationError = published.error;
|
|
1552
|
+
currentResultEvidenceFile = null;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (afterLowerProblem !== null)
|
|
1557
|
+
currentResultEvidenceFile = null;
|
|
1558
|
+
const outputs = [];
|
|
1559
|
+
for (const ref of [
|
|
1560
|
+
afterLowerProblem === null ? executionFile : null,
|
|
1561
|
+
afterLowerProblem === null ? proofFile : null,
|
|
1562
|
+
currentResultEvidenceFile,
|
|
1563
|
+
]) {
|
|
1564
|
+
if (ref === null || !path.isAbsolute(ref))
|
|
1565
|
+
continue;
|
|
1566
|
+
const hashNamespaceProblem = liveNamespaceProblem();
|
|
1567
|
+
if (hashNamespaceProblem !== null) {
|
|
1568
|
+
publicationError = publicationError ?? hashNamespaceProblem;
|
|
1569
|
+
currentResultEvidenceFile = null;
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
outputs.push({ ref, digest: sha256File(ref) });
|
|
1574
|
+
}
|
|
1575
|
+
catch {
|
|
1576
|
+
// Missing evidence cannot be claimed as a result output.
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
const finalNamespaceProblem = liveNamespaceProblem();
|
|
1580
|
+
if (finalNamespaceProblem !== null) {
|
|
1581
|
+
publicationError = publicationError ?? finalNamespaceProblem;
|
|
1582
|
+
currentResultEvidenceFile = null;
|
|
1583
|
+
}
|
|
1584
|
+
const deliveryStatus = publicationError === null ? currentStatus : 'unknown';
|
|
1585
|
+
disposed = true;
|
|
1586
|
+
return {
|
|
1587
|
+
outcome,
|
|
1588
|
+
status: !ok
|
|
1589
|
+
? outcome === 'timed_out' ||
|
|
1590
|
+
outcome === 'spawn_failed' ||
|
|
1591
|
+
!evidenceWritten
|
|
1592
|
+
? 'unknown'
|
|
1593
|
+
: 'failed'
|
|
1594
|
+
: deliveryStatus,
|
|
1595
|
+
exitCode: typeof exit['exit_code'] === 'number' ? exit['exit_code'] : null,
|
|
1596
|
+
sessionId: typeof value['session_id'] === 'string'
|
|
1597
|
+
? value['session_id']
|
|
1598
|
+
: null,
|
|
1599
|
+
outputs,
|
|
1600
|
+
nativeRef: {
|
|
1601
|
+
...value,
|
|
1602
|
+
task_result: current === null
|
|
1603
|
+
? null
|
|
1604
|
+
: {
|
|
1605
|
+
status: currentStatus,
|
|
1606
|
+
reason: currentReason,
|
|
1607
|
+
record_count: current.records.length,
|
|
1608
|
+
},
|
|
1609
|
+
current_result_published: currentResultEvidenceFile !== null,
|
|
1610
|
+
current_result_publication_error: publicationError,
|
|
1611
|
+
},
|
|
1612
|
+
evidenceRef: ok && finalNamespaceProblem === null ? executionFile : null,
|
|
1613
|
+
};
|
|
1614
|
+
},
|
|
1615
|
+
dispose: async () => {
|
|
1616
|
+
disposed = true;
|
|
1617
|
+
if (liveNamespaceProblem() !== null) {
|
|
1618
|
+
await started.session.shutdown().catch(() => undefined);
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
await started.session.finish().catch(() => undefined);
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
},
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
export function inspectPreparedHistory(preparedFile) {
|
|
1629
|
+
const bundle = preparedBundle(preparedFile);
|
|
1630
|
+
if (!bundle.ok)
|
|
1631
|
+
return bundle;
|
|
1632
|
+
const record = readRecord(bundle.value.prepared.task_root);
|
|
1633
|
+
if (record === null) {
|
|
1634
|
+
return failure('failed', 'task_record_missing', 'Task record is missing.');
|
|
1635
|
+
}
|
|
1636
|
+
const attempt = findAttempt(record, bundle.value.prepared.subtask_id, bundle.value.prepared.attempt);
|
|
1637
|
+
return {
|
|
1638
|
+
ok: true,
|
|
1639
|
+
value: {
|
|
1640
|
+
schema: 'pappra/collaboration-history/1',
|
|
1641
|
+
task_id: bundle.value.prepared.task_id,
|
|
1642
|
+
subtask_id: bundle.value.prepared.subtask_id,
|
|
1643
|
+
attempt: bundle.value.prepared.attempt,
|
|
1644
|
+
live_owner: 'unknown',
|
|
1645
|
+
state: attempt?.state ?? null,
|
|
1646
|
+
native: attempt?.native ?? null,
|
|
1647
|
+
reconciliation_required: attempt?.native?.observation !== 'confirmed',
|
|
1648
|
+
},
|
|
1649
|
+
};
|
|
1650
|
+
}
|