@nettee/beacon 0.6.0 → 0.6.1
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/dist/daemon/run/create-pi-orchestrator.js +4 -1
- package/dist/daemon/run/orchestrator.d.ts +2 -0
- package/dist/daemon/run/orchestrator.js +3 -1
- package/dist/daemon/runtime/pi-worker-lifecycle.d.ts +14 -2
- package/dist/daemon/runtime/pi-worker-lifecycle.js +50 -6
- package/dist/daemon/runtime/pi-worker-main.js +6 -4
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@ import { DEFAULT_ORPHAN_AFTER_MS } from "../runtime/supervisor-lease.js";
|
|
|
4
4
|
import { RunOrchestrator } from "./orchestrator.js";
|
|
5
5
|
export function createPiRunOrchestrator(options) {
|
|
6
6
|
const detachRuns = options.detachRuns ?? true;
|
|
7
|
-
const
|
|
7
|
+
const stateDirectory = join(options.config.homeDirectory, "state");
|
|
8
|
+
const supervisorLeasePath = join(stateDirectory, "supervisor.lease");
|
|
9
|
+
const outcomesDirectory = join(stateDirectory, "outcomes");
|
|
8
10
|
return new RunOrchestrator({
|
|
9
11
|
profile: options.profile,
|
|
10
12
|
store: options.store,
|
|
@@ -26,6 +28,7 @@ export function createPiRunOrchestrator(options) {
|
|
|
26
28
|
? {
|
|
27
29
|
detached: {
|
|
28
30
|
supervisorLeasePath,
|
|
31
|
+
outcomesDirectory,
|
|
29
32
|
orphanAfterMs: options.orphanAfterMs ?? DEFAULT_ORPHAN_AFTER_MS,
|
|
30
33
|
executable: options.config.pi.executable,
|
|
31
34
|
timeoutMs: options.config.runs.timeoutSeconds * 1_000,
|
|
@@ -11,6 +11,8 @@ export type DeliveryAdapter = {
|
|
|
11
11
|
};
|
|
12
12
|
export type DetachedRunOptions = {
|
|
13
13
|
supervisorLeasePath: string;
|
|
14
|
+
/** Directory for short per-run Outcome sockets (e.g. ~/.beacon/state/outcomes). */
|
|
15
|
+
outcomesDirectory: string;
|
|
14
16
|
orphanAfterMs?: number | undefined;
|
|
15
17
|
executable?: string | undefined;
|
|
16
18
|
timeoutMs?: number | undefined;
|
|
@@ -3,7 +3,7 @@ import { isAbsolute, join } from "node:path";
|
|
|
3
3
|
import { isMissingReplyOutcomeError, MISSING_REPLY_OUTCOME_SUMMARY, } from "../outcome/content.js";
|
|
4
4
|
import { inboundOutOfRoleReply } from "../outcome/instructions.js";
|
|
5
5
|
import { PiRuntimeError, RUNTIME_TIMEOUT_OUTCOME_MISSING_NOTE, RUNTIME_TIMEOUT_OUTCOME_SUBMITTED_NOTE, } from "../runtime/pi-rpc.js";
|
|
6
|
-
import { isWorkerAdoptable, signalWorker, spawnDetachedPiWorker, waitForWorkerCompletion, } from "../runtime/pi-worker-lifecycle.js";
|
|
6
|
+
import { isWorkerAdoptable, resolveWorkerOutcomeSocketPath, signalWorker, spawnDetachedPiWorker, waitForWorkerCompletion, } from "../runtime/pi-worker-lifecycle.js";
|
|
7
7
|
import { DEFAULT_ORPHAN_AFTER_MS } from "../runtime/supervisor-lease.js";
|
|
8
8
|
import { agentSystemPromptTrigger, buildAgentSystemPrompt, profilePromptDigest, } from "../runtime/system-prompt.js";
|
|
9
9
|
function promptFor(input) {
|
|
@@ -383,6 +383,7 @@ export class RunOrchestrator {
|
|
|
383
383
|
const lifecycleDirectory = join(sessionPath, "beacon-runtime");
|
|
384
384
|
const runToken = randomBytes(32).toString("base64url");
|
|
385
385
|
const orphanAfterMs = detached.orphanAfterMs ?? DEFAULT_ORPHAN_AFTER_MS;
|
|
386
|
+
const outcomeSocketPath = resolveWorkerOutcomeSocketPath(detached.outcomesDirectory, runId);
|
|
386
387
|
const handle = await spawnDetachedPiWorker(lifecycleDirectory, {
|
|
387
388
|
prompt: promptFor(input),
|
|
388
389
|
playbook: this.options.profile.playbook,
|
|
@@ -394,6 +395,7 @@ export class RunOrchestrator {
|
|
|
394
395
|
path: sessionPath,
|
|
395
396
|
name: `Beacon ${this.options.profile.id} ${runId}`,
|
|
396
397
|
},
|
|
398
|
+
outcomeSocketPath,
|
|
397
399
|
...(this.options.profile.skills
|
|
398
400
|
? { skills: this.options.profile.skills }
|
|
399
401
|
: {}),
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ProfileSkills } from "../config/profile.js";
|
|
2
2
|
import type { FeedbackRecord, FinalOutcomeContent, ObservabilityFeedbackRecord } from "../domain/types.js";
|
|
3
3
|
import type { PiRunResult, PiRuntimeOptions } from "./pi-rpc.js";
|
|
4
|
+
/**
|
|
5
|
+
* Usable AF_UNIX path length on macOS (`sun_path` is 104 including NUL).
|
|
6
|
+
* Enforced on all platforms so Linux CI catches oversize paths too.
|
|
7
|
+
*/
|
|
8
|
+
export declare const UNIX_SOCKET_PATH_MAX_BYTES = 103;
|
|
4
9
|
export type PiWorkerRequest = {
|
|
5
10
|
prompt: string;
|
|
6
11
|
playbook: string;
|
|
@@ -15,6 +20,8 @@ export type PiWorkerRequest = {
|
|
|
15
20
|
skills?: ProfileSkills | undefined;
|
|
16
21
|
beaconCliPath: string;
|
|
17
22
|
runToken: string;
|
|
23
|
+
/** Short absolute path under ~/.beacon/state/outcomes/<16hex>.sock */
|
|
24
|
+
outcomeSocketPath: string;
|
|
18
25
|
supervisorLeasePath: string;
|
|
19
26
|
orphanAfterMs?: number | undefined;
|
|
20
27
|
executable?: string | undefined;
|
|
@@ -24,6 +31,8 @@ export type PiWorkerRequest = {
|
|
|
24
31
|
environment?: NodeJS.ProcessEnv | undefined;
|
|
25
32
|
runtimeEnvironment?: NodeJS.ProcessEnv | undefined;
|
|
26
33
|
};
|
|
34
|
+
/** Stable, short per-run Outcome socket under the daemon outcomes directory. */
|
|
35
|
+
export declare function resolveWorkerOutcomeSocketPath(outcomesDirectory: string, runId: string): string;
|
|
27
36
|
export type PiWorkerCompletion = {
|
|
28
37
|
ok: true;
|
|
29
38
|
result: PiRunResult;
|
|
@@ -55,9 +64,12 @@ export declare function lifecyclePaths(lifecycleDirectory: string): {
|
|
|
55
64
|
completion: string;
|
|
56
65
|
heartbeat: string;
|
|
57
66
|
pid: string;
|
|
58
|
-
|
|
67
|
+
/** @deprecated 0.6.0 bound here; 0.6.1+ uses resolveWorkerOutcomeSocketPath. */
|
|
68
|
+
legacyOutcomeSocket: string;
|
|
59
69
|
};
|
|
60
|
-
export declare function prepareLifecycleDirectory(lifecycleDirectory: string
|
|
70
|
+
export declare function prepareLifecycleDirectory(lifecycleDirectory: string, options?: {
|
|
71
|
+
outcomeSocketPath?: string | undefined;
|
|
72
|
+
}): Promise<void>;
|
|
61
73
|
export declare function writeWorkerRequest(lifecycleDirectory: string, request: PiWorkerRequest): Promise<void>;
|
|
62
74
|
export declare function readWorkerRequest(lifecycleDirectory: string): Promise<PiWorkerRequest>;
|
|
63
75
|
export declare function writeWorkerHeartbeat(lifecycleDirectory: string): Promise<void>;
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { access, chmod, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { DEFAULT_ORPHAN_AFTER_MS, isSupervisorLeaseFresh, } from "./supervisor-lease.js";
|
|
7
|
+
/**
|
|
8
|
+
* Usable AF_UNIX path length on macOS (`sun_path` is 104 including NUL).
|
|
9
|
+
* Enforced on all platforms so Linux CI catches oversize paths too.
|
|
10
|
+
*/
|
|
11
|
+
export const UNIX_SOCKET_PATH_MAX_BYTES = 103;
|
|
12
|
+
/** Stable, short per-run Outcome socket under the daemon outcomes directory. */
|
|
13
|
+
export function resolveWorkerOutcomeSocketPath(outcomesDirectory, runId) {
|
|
14
|
+
if (!isAbsolute(outcomesDirectory)) {
|
|
15
|
+
throw new Error("Worker outcome socket directory must be absolute");
|
|
16
|
+
}
|
|
17
|
+
if (!runId) {
|
|
18
|
+
throw new Error("Worker outcome socket requires a run id");
|
|
19
|
+
}
|
|
20
|
+
const shortId = createHash("sha256").update(runId).digest("hex").slice(0, 16);
|
|
21
|
+
const socketPath = join(outcomesDirectory, `${shortId}.sock`);
|
|
22
|
+
const bytes = Buffer.byteLength(socketPath, "utf8");
|
|
23
|
+
if (bytes > UNIX_SOCKET_PATH_MAX_BYTES) {
|
|
24
|
+
throw new Error(`Worker outcome socket path exceeds Unix sun_path limit (${bytes} > ${UNIX_SOCKET_PATH_MAX_BYTES} bytes): ${socketPath}`);
|
|
25
|
+
}
|
|
26
|
+
return socketPath;
|
|
27
|
+
}
|
|
6
28
|
const REQUEST_FILE = "request.json";
|
|
7
29
|
const COMPLETION_FILE = "completion.json";
|
|
8
30
|
const HEARTBEAT_FILE = "worker.heartbeat";
|
|
@@ -13,10 +35,11 @@ export function lifecyclePaths(lifecycleDirectory) {
|
|
|
13
35
|
completion: join(lifecycleDirectory, COMPLETION_FILE),
|
|
14
36
|
heartbeat: join(lifecycleDirectory, HEARTBEAT_FILE),
|
|
15
37
|
pid: join(lifecycleDirectory, PID_FILE),
|
|
16
|
-
|
|
38
|
+
/** @deprecated 0.6.0 bound here; 0.6.1+ uses resolveWorkerOutcomeSocketPath. */
|
|
39
|
+
legacyOutcomeSocket: join(lifecycleDirectory, "outcomes.sock"),
|
|
17
40
|
};
|
|
18
41
|
}
|
|
19
|
-
export async function prepareLifecycleDirectory(lifecycleDirectory) {
|
|
42
|
+
export async function prepareLifecycleDirectory(lifecycleDirectory, options = {}) {
|
|
20
43
|
if (!isAbsolute(lifecycleDirectory)) {
|
|
21
44
|
throw new Error("Pi worker lifecycle directory must be absolute");
|
|
22
45
|
}
|
|
@@ -26,10 +49,25 @@ export async function prepareLifecycleDirectory(lifecycleDirectory) {
|
|
|
26
49
|
await rm(paths.completion, { force: true });
|
|
27
50
|
await rm(paths.heartbeat, { force: true });
|
|
28
51
|
await rm(paths.pid, { force: true });
|
|
29
|
-
await rm(paths.
|
|
52
|
+
await rm(paths.legacyOutcomeSocket, { force: true });
|
|
53
|
+
if (options.outcomeSocketPath) {
|
|
54
|
+
if (!isAbsolute(options.outcomeSocketPath)) {
|
|
55
|
+
throw new Error("Worker outcome socket path must be absolute");
|
|
56
|
+
}
|
|
57
|
+
await mkdir(dirname(options.outcomeSocketPath), {
|
|
58
|
+
recursive: true,
|
|
59
|
+
mode: 0o700,
|
|
60
|
+
});
|
|
61
|
+
await rm(options.outcomeSocketPath, { force: true });
|
|
62
|
+
}
|
|
30
63
|
}
|
|
31
64
|
export async function writeWorkerRequest(lifecycleDirectory, request) {
|
|
32
|
-
|
|
65
|
+
if (!request.outcomeSocketPath || !isAbsolute(request.outcomeSocketPath)) {
|
|
66
|
+
throw new Error("Pi worker request requires an absolute outcomeSocketPath");
|
|
67
|
+
}
|
|
68
|
+
await prepareLifecycleDirectory(lifecycleDirectory, {
|
|
69
|
+
outcomeSocketPath: request.outcomeSocketPath,
|
|
70
|
+
});
|
|
33
71
|
const temporary = `${lifecyclePaths(lifecycleDirectory).request}.tmp`;
|
|
34
72
|
await writeFile(temporary, `${JSON.stringify(request, null, 2)}\n`, {
|
|
35
73
|
mode: 0o600,
|
|
@@ -38,9 +76,15 @@ export async function writeWorkerRequest(lifecycleDirectory, request) {
|
|
|
38
76
|
}
|
|
39
77
|
export async function readWorkerRequest(lifecycleDirectory) {
|
|
40
78
|
const raw = JSON.parse(await readFile(lifecyclePaths(lifecycleDirectory).request, "utf8"));
|
|
41
|
-
if (!raw.beaconCliPath ||
|
|
79
|
+
if (!raw.beaconCliPath ||
|
|
80
|
+
!raw.runToken ||
|
|
81
|
+
!raw.supervisorLeasePath ||
|
|
82
|
+
!raw.outcomeSocketPath) {
|
|
42
83
|
throw new Error("Pi worker request is missing required fields");
|
|
43
84
|
}
|
|
85
|
+
if (!isAbsolute(raw.outcomeSocketPath)) {
|
|
86
|
+
throw new Error("Pi worker outcomeSocketPath must be absolute");
|
|
87
|
+
}
|
|
44
88
|
return raw;
|
|
45
89
|
}
|
|
46
90
|
export async function writeWorkerHeartbeat(lifecycleDirectory) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { chmod, mkdir, rm } from "node:fs/promises";
|
|
2
2
|
import { createServer } from "node:net";
|
|
3
|
+
import { dirname } from "node:path";
|
|
3
4
|
import { completeOutcome, mergeOutcome, parseOutcomePatch, } from "../outcome/content.js";
|
|
4
5
|
import { PiRuntimeError, runPiAgent } from "./pi-rpc.js";
|
|
5
|
-
import { assertSupervisorOrThrow,
|
|
6
|
+
import { assertSupervisorOrThrow, readWorkerRequest, writeWorkerCompletion, writeWorkerHeartbeat, } from "./pi-worker-lifecycle.js";
|
|
6
7
|
import { DEFAULT_ORPHAN_AFTER_MS } from "./supervisor-lease.js";
|
|
7
8
|
function respond(socket, response) {
|
|
8
9
|
socket.end(`${JSON.stringify(response)}\n`);
|
|
@@ -123,8 +124,9 @@ async function startWorkerOutcomeServer(socketPath, runToken, maxRequestBytes =
|
|
|
123
124
|
async function runWorker(lifecycleDirectory) {
|
|
124
125
|
const request = await readWorkerRequest(lifecycleDirectory);
|
|
125
126
|
const orphanAfterMs = request.orphanAfterMs ?? DEFAULT_ORPHAN_AFTER_MS;
|
|
126
|
-
const
|
|
127
|
+
const outcomeSocketPath = request.outcomeSocketPath;
|
|
127
128
|
await mkdir(lifecycleDirectory, { recursive: true, mode: 0o700 });
|
|
129
|
+
await mkdir(dirname(outcomeSocketPath), { recursive: true, mode: 0o700 });
|
|
128
130
|
const abort = new AbortController();
|
|
129
131
|
const heartbeat = setInterval(() => {
|
|
130
132
|
void writeWorkerHeartbeat(lifecycleDirectory).catch(() => undefined);
|
|
@@ -137,7 +139,7 @@ async function runWorker(lifecycleDirectory) {
|
|
|
137
139
|
}, 5_000);
|
|
138
140
|
heartbeat.unref?.();
|
|
139
141
|
await writeWorkerHeartbeat(lifecycleDirectory);
|
|
140
|
-
const outcomes = await startWorkerOutcomeServer(
|
|
142
|
+
const outcomes = await startWorkerOutcomeServer(outcomeSocketPath, request.runToken);
|
|
141
143
|
const finish = async (completion) => {
|
|
142
144
|
clearInterval(heartbeat);
|
|
143
145
|
await writeWorkerCompletion(lifecycleDirectory, completion);
|
|
@@ -154,7 +156,7 @@ async function runWorker(lifecycleDirectory) {
|
|
|
154
156
|
session: request.session,
|
|
155
157
|
skills: request.skills,
|
|
156
158
|
outcome: {
|
|
157
|
-
socketPath:
|
|
159
|
+
socketPath: outcomeSocketPath,
|
|
158
160
|
runToken: request.runToken,
|
|
159
161
|
cliPath: request.beaconCliPath,
|
|
160
162
|
},
|