@pinet/slack-bridge 0.2.4 → 0.2.7
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/README.md +26 -1
- package/dist/activity-log.d.ts +10 -3
- package/dist/agent-event-runtime.d.ts +0 -2
- package/dist/agent-event-runtime.js +0 -1
- package/dist/agent-prompt-guidance.d.ts +1 -6
- package/dist/agent-prompt-guidance.js +1 -6
- package/dist/broker/adapters/slack.d.ts +4 -1
- package/dist/broker/adapters/slack.js +16 -9
- package/dist/broker/client.d.ts +21 -4
- package/dist/broker/client.js +65 -25
- package/dist/broker/hibernation-activation-authority.d.ts +22 -0
- package/dist/broker/hibernation-activation-authority.js +51 -0
- package/dist/broker/hibernation-activation.d.ts +75 -0
- package/dist/broker/hibernation-activation.js +95 -0
- package/dist/broker/hibernation-command-router.d.ts +54 -0
- package/dist/broker/hibernation-command-router.js +124 -0
- package/dist/broker/hibernation-runtime-adapters.d.ts +79 -0
- package/dist/broker/hibernation-runtime-adapters.js +473 -0
- package/dist/broker/hibernation-runtime-helpers.d.ts +146 -0
- package/dist/broker/hibernation-runtime-helpers.js +243 -0
- package/dist/broker/index.d.ts +16 -1
- package/dist/broker/index.js +68 -10
- package/dist/broker/lock-conflict.d.ts +109 -0
- package/dist/broker/lock-conflict.js +409 -0
- package/dist/broker/socket-server.d.ts +25 -0
- package/dist/broker/socket-server.js +234 -4
- package/dist/broker-delivery.d.ts +1 -0
- package/dist/broker-delivery.js +10 -1
- package/dist/broker-runtime.d.ts +14 -2
- package/dist/broker-runtime.js +114 -3
- package/dist/canvases.d.ts +12 -2
- package/dist/canvases.js +1 -3
- package/dist/compaction-gate.d.ts +7 -0
- package/dist/compaction-gate.js +35 -0
- package/dist/deploy-manifest.d.ts +8 -2
- package/dist/deploy-manifest.js +9 -7
- package/dist/follower-runtime.d.ts +5 -2
- package/dist/follower-runtime.js +39 -5
- package/dist/git-metadata.d.ts +5 -9
- package/dist/git-metadata.js +23 -42
- package/dist/guardrails.js +5 -0
- package/dist/helpers.d.ts +77 -27
- package/dist/helpers.js +150 -39
- package/dist/hibernation-config.d.ts +13 -0
- package/dist/hibernation-config.js +14 -0
- package/dist/home-tab.d.ts +13 -2
- package/dist/imessage-tools.d.ts +4 -1
- package/dist/inbox-drain-runtime.d.ts +1 -0
- package/dist/inbox-drain-runtime.js +5 -1
- package/dist/index.js +340 -108
- package/dist/pinet-commands.d.ts +14 -1
- package/dist/pinet-commands.js +93 -22
- package/dist/pinet-mesh-ops.d.ts +6 -4
- package/dist/pinet-mesh-ops.js +8 -1
- package/dist/pinet-runtime-composition.d.ts +2 -1
- package/dist/pinet-runtime-composition.js +14 -3
- package/dist/pinet-session-formatting.d.ts +26 -2
- package/dist/pinet-session-formatting.js +2 -0
- package/dist/pinet-tools.d.ts +14 -0
- package/dist/pinet-tools.js +355 -50
- package/dist/prompts/broker/default.md +3 -3
- package/dist/prompts/broker/tmux.md +3 -3
- package/dist/runtime-agent-context.d.ts +3 -3
- package/dist/runtime-agent-context.js +12 -6
- package/dist/runtime-mode.js +4 -0
- package/dist/session-ui-runtime.js +3 -1
- package/dist/single-player-runtime.d.ts +1 -1
- package/dist/single-player-runtime.js +21 -5
- package/dist/slack-access.d.ts +10 -2
- package/dist/slack-access.js +64 -22
- package/dist/slack-block-kit.d.ts +44 -2
- package/dist/slack-block-kit.js +5 -5
- package/dist/slack-file-access.d.ts +12 -1
- package/dist/slack-file-access.js +4 -4
- package/dist/slack-message-context.d.ts +9 -2
- package/dist/slack-modals.d.ts +4 -1
- package/dist/slack-modals.js +5 -5
- package/dist/slack-pinet-runtime-adapter.d.ts +1 -0
- package/dist/slack-pinet-runtime-adapter.js +10 -0
- package/dist/slack-presence.d.ts +5 -4
- package/dist/slack-request-runtime.d.ts +3 -2
- package/dist/slack-scope-diagnostics.d.ts +27 -2
- package/dist/slack-socket-dedup.d.ts +8 -5
- package/dist/slack-tools.d.ts +3 -1
- package/dist/slack-tools.js +26 -16
- package/dist/slack-upload.d.ts +17 -1
- package/dist/subtree-broker-runtime.d.ts +83 -2
- package/dist/subtree-broker-runtime.js +774 -109
- package/dist/tool-output-limits.d.ts +11 -0
- package/dist/tool-output-limits.js +20 -0
- package/dist/tool-registration-runtime.d.ts +3 -0
- package/dist/tool-registration-runtime.js +27 -0
- package/package.json +8 -8
|
@@ -1,17 +1,84 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { sleep } from "@pinet/transport-core/async";
|
|
7
8
|
import { summarizePinetStableId } from "./pinet-session-formatting.js";
|
|
8
9
|
import { dispatchDirectAgentMessage, resolveDirectAgentTarget } from "./broker/agent-messaging.js";
|
|
9
10
|
import { startBroker } from "./broker/index.js";
|
|
10
11
|
import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
|
|
11
|
-
import { buildPinetOwnerToken, generateAgentName, normalizeOutgoingPinetControlMessage, resolvePinetMeshAuth, syncBrokerInboxEntries, } from "./helpers.js";
|
|
12
|
+
import { buildPinetOwnerToken, formatPinetSteeringMessage, generateAgentName, normalizeOutgoingPinetControlMessage, normalizeOutgoingPinetSteeringMessage, resolvePinetMeshAuth, syncBrokerInboxEntries, } from "./helpers.js";
|
|
13
|
+
import { resolveHibernationSettings } from "./hibernation-config.js";
|
|
14
|
+
import { hibernationRuntimeActive, createHibernationOrchestrator, persistSpawnedRuntimeSpec, recoverStrandedWakesBeforeRegistrations, } from "./broker/hibernation-activation.js";
|
|
15
|
+
import { freezeHibernationActivationAuthority } from "./broker/hibernation-activation-authority.js";
|
|
12
16
|
const execFileAsync = promisify(execFile);
|
|
13
17
|
const DEFAULT_SPAWN_REGISTRATION_TIMEOUT_MS = 45_000;
|
|
14
18
|
const SUBTREE_CHILD_EXIT_GRACE_MS = 5_000;
|
|
19
|
+
const PENDING_HERDR_RUNTIMES_SETTING = "pinet.subtreePendingHerdrRuntimes";
|
|
20
|
+
function readPendingHerdrRuntimes(db) {
|
|
21
|
+
const records = db.getSetting(PENDING_HERDR_RUNTIMES_SETTING);
|
|
22
|
+
if (!Array.isArray(records))
|
|
23
|
+
return [];
|
|
24
|
+
return records.filter((record) => record !== null &&
|
|
25
|
+
typeof record === "object" &&
|
|
26
|
+
record.runtimeKind === "herdr" &&
|
|
27
|
+
typeof record.launchId === "string" &&
|
|
28
|
+
record.launchId.length > 0 &&
|
|
29
|
+
typeof record.sessionName === "string" &&
|
|
30
|
+
record.sessionName.length > 0 &&
|
|
31
|
+
typeof record.herdrSession === "string" &&
|
|
32
|
+
record.herdrSession.length > 0 &&
|
|
33
|
+
typeof record.herdrConfigDir === "string" &&
|
|
34
|
+
record.herdrConfigDir.length > 0 &&
|
|
35
|
+
typeof record.herdrPaneId === "string" &&
|
|
36
|
+
record.herdrPaneId.length > 0 &&
|
|
37
|
+
typeof record.herdrShellPid === "number" &&
|
|
38
|
+
Number.isInteger(record.herdrShellPid) &&
|
|
39
|
+
record.herdrShellPid > 0);
|
|
40
|
+
}
|
|
41
|
+
function updatePendingHerdrRuntime(db, launchId, spec) {
|
|
42
|
+
const records = readPendingHerdrRuntimes(db).filter((record) => record.launchId !== launchId);
|
|
43
|
+
if (spec) {
|
|
44
|
+
if (!spec.herdrPaneId || spec.herdrShellPid === null) {
|
|
45
|
+
throw new Error(`Herdr runtime ${spec.sessionName} has no cleanup identity`);
|
|
46
|
+
}
|
|
47
|
+
records.push({
|
|
48
|
+
...spec,
|
|
49
|
+
launchId,
|
|
50
|
+
herdrPaneId: spec.herdrPaneId,
|
|
51
|
+
herdrShellPid: spec.herdrShellPid,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (records.length === 0)
|
|
55
|
+
db.deleteSetting(PENDING_HERDR_RUNTIMES_SETTING);
|
|
56
|
+
else
|
|
57
|
+
db.setSetting(PENDING_HERDR_RUNTIMES_SETTING, records);
|
|
58
|
+
}
|
|
59
|
+
export class SubtreeSpawnRegistrationTimeoutError extends Error {
|
|
60
|
+
handle;
|
|
61
|
+
constructor(timeoutMs, handle) {
|
|
62
|
+
const runtimeKind = handle.runtimeKind ?? "tmux";
|
|
63
|
+
const monitorHint = handle.monitorCommand ? `; monitor=${handle.monitorCommand}` : "";
|
|
64
|
+
super(`subtree ${runtimeKind} worker ${handle.tmuxSessionName} started but did not register within ${timeoutMs}ms; ` +
|
|
65
|
+
`launchId=${handle.launchId}; tmuxSessionName=${handle.tmuxSessionName}; ` +
|
|
66
|
+
`socketPath=${handle.socketPath}; state=${handle.state}${monitorHint}`);
|
|
67
|
+
this.name = "SubtreeSpawnRegistrationTimeoutError";
|
|
68
|
+
this.handle = handle;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class SubtreeSpawnLaunchError extends Error {
|
|
72
|
+
handle;
|
|
73
|
+
constructor(error, handle, runtimeKind = "tmux") {
|
|
74
|
+
const message = error.message;
|
|
75
|
+
super(`subtree worker session ${handle.tmuxSessionName} may have started after ${runtimeKind} reported: ${message}; ` +
|
|
76
|
+
`launchId=${handle.launchId}; tmuxSessionName=${handle.tmuxSessionName}; ` +
|
|
77
|
+
`socketPath=${handle.socketPath}; state=${handle.state}`, { cause: error });
|
|
78
|
+
this.name = "SubtreeSpawnLaunchError";
|
|
79
|
+
this.handle = handle;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
15
82
|
function sanitizePathSegment(value) {
|
|
16
83
|
const sanitized = value.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
17
84
|
return sanitized || "agent";
|
|
@@ -19,11 +86,6 @@ function sanitizePathSegment(value) {
|
|
|
19
86
|
function randomSuffix() {
|
|
20
87
|
return Math.random().toString(36).slice(2, 8);
|
|
21
88
|
}
|
|
22
|
-
function delay(ms) {
|
|
23
|
-
return new Promise((resolve) => {
|
|
24
|
-
setTimeout(resolve, ms);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
89
|
export function buildSubtreeBrokerPaths(stableId) {
|
|
28
90
|
const rootDir = path.join(os.homedir(), ".pi", "pinet-subtrees", sanitizePathSegment(stableId));
|
|
29
91
|
return {
|
|
@@ -40,10 +102,11 @@ function buildChildLaunchEnv(paths, selfAgentId, input = {}) {
|
|
|
40
102
|
return {
|
|
41
103
|
PINET_SOCKET_PATH: paths.socketPath,
|
|
42
104
|
PINET_BROKER_MANAGED: "1",
|
|
105
|
+
PINET_BROKER_AGENT_ID: selfAgentId,
|
|
43
106
|
PINET_PARENT_AGENT_ID: selfAgentId,
|
|
44
107
|
PINET_ROOT_AGENT_ID: selfAgentId,
|
|
45
108
|
PINET_SPAWNED_BY_AGENT_ID: selfAgentId,
|
|
46
|
-
PINET_LAUNCH_SOURCE: "subtree-broker-tmux",
|
|
109
|
+
PINET_LAUNCH_SOURCE: input.runtimeKind === "herdr" ? "subtree-broker-herdr" : "subtree-broker-tmux",
|
|
47
110
|
...(input.launchId ? { PINET_LAUNCH_ID: input.launchId } : {}),
|
|
48
111
|
...(input.role ? { PINET_SUBTREE_ROLE: input.role } : {}),
|
|
49
112
|
...(input.laneId ? { PINET_LANE_ID: input.laneId } : {}),
|
|
@@ -124,11 +187,253 @@ function buildTmuxMonitorCommand(sessionName, socketPath) {
|
|
|
124
187
|
const socketArgs = socketPath ? `-S ${quoteShellValue(socketPath)} ` : "";
|
|
125
188
|
return `tmux ${socketArgs}attach -t ${quoteShellValue(sessionName)}`;
|
|
126
189
|
}
|
|
127
|
-
function
|
|
190
|
+
function isMissingTmuxTarget(error) {
|
|
191
|
+
const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
|
|
192
|
+
return /can't find session:|no server running|error connecting to .*\(no such file or directory\)/i.test(`${error.message}\n${stderr}`);
|
|
193
|
+
}
|
|
194
|
+
function herdrErrorText(error) {
|
|
195
|
+
const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
|
|
196
|
+
return `${error.message}\n${stderr}`;
|
|
197
|
+
}
|
|
198
|
+
function isMissingHerdrSession(error) {
|
|
199
|
+
return /Error:\s*Os\s*\{\s*code:\s*2,\s*kind:\s*NotFound,\s*message:\s*"No such file or directory"\s*\}/.test(herdrErrorText(error));
|
|
200
|
+
}
|
|
201
|
+
// agent-standards-ignore prefer-inline-single-use-helper: precise Herdr absence classifier
|
|
202
|
+
function isMissingHerdrPane(error) {
|
|
203
|
+
return /"code"\s*:\s*"pane_not_found"/.test(herdrErrorText(error));
|
|
204
|
+
}
|
|
205
|
+
// agent-standards-ignore prefer-inline-single-use-helper: runtime backend construction seam
|
|
206
|
+
function createTmuxWorkerRuntimeController(runTmuxCommand) {
|
|
207
|
+
return {
|
|
208
|
+
createLaunchSpec: (sessionName) => ({
|
|
209
|
+
runtimeKind: "tmux",
|
|
210
|
+
sessionName,
|
|
211
|
+
tmuxSocketPath: findTmuxSocketPath(),
|
|
212
|
+
}),
|
|
213
|
+
monitorCommand: (spec) => buildTmuxMonitorCommand(spec.sessionName, spec.tmuxSocketPath),
|
|
214
|
+
launch: async (spec, launcherPath) => {
|
|
215
|
+
await runTmuxCommand([
|
|
216
|
+
...buildTmuxBaseArgs(spec.tmuxSocketPath),
|
|
217
|
+
"new-session",
|
|
218
|
+
"-d",
|
|
219
|
+
"-s",
|
|
220
|
+
spec.sessionName,
|
|
221
|
+
launcherPath,
|
|
222
|
+
]);
|
|
223
|
+
},
|
|
224
|
+
cleanup: async (spec) => {
|
|
225
|
+
const exactTarget = `=${spec.sessionName}`;
|
|
226
|
+
try {
|
|
227
|
+
await runTmuxCommand([
|
|
228
|
+
...buildTmuxBaseArgs(spec.tmuxSocketPath),
|
|
229
|
+
"has-session",
|
|
230
|
+
"-t",
|
|
231
|
+
exactTarget,
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof Error && isMissingTmuxTarget(error))
|
|
236
|
+
return;
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await runTmuxCommand([
|
|
241
|
+
...buildTmuxBaseArgs(spec.tmuxSocketPath),
|
|
242
|
+
"kill-session",
|
|
243
|
+
"-t",
|
|
244
|
+
exactTarget,
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (!(error instanceof Error) || !isMissingTmuxTarget(error))
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function runHerdrSessionCommand(runner, spec, args, detached = false) {
|
|
255
|
+
return runner(["--session", spec.herdrSession, ...args], {
|
|
256
|
+
env: { ...process.env, XDG_CONFIG_HOME: spec.herdrConfigDir },
|
|
257
|
+
...(detached ? { detached: true } : {}),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// agent-standards-ignore prefer-inline-single-use-helper: dedicated Herdr ownership boundary
|
|
261
|
+
function ensureHerdrConfig(configDir) {
|
|
262
|
+
const herdrDir = path.join(configDir, "herdr");
|
|
263
|
+
const configPath = path.join(herdrDir, "config.toml");
|
|
264
|
+
fs.mkdirSync(herdrDir, { recursive: true });
|
|
265
|
+
if (!fs.existsSync(configPath)) {
|
|
266
|
+
fs.writeFileSync(configPath, "[experimental]\npane_history = true\n", { mode: 0o600 });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export function createHerdrWorkerRuntimeController(runHerdrCommand, options) {
|
|
270
|
+
return {
|
|
271
|
+
createLaunchSpec: (sessionName) => ({
|
|
272
|
+
runtimeKind: "herdr",
|
|
273
|
+
sessionName,
|
|
274
|
+
herdrSession: options.herdrSession,
|
|
275
|
+
herdrConfigDir: options.herdrConfigDir,
|
|
276
|
+
herdrPaneId: null,
|
|
277
|
+
herdrShellPid: null,
|
|
278
|
+
}),
|
|
279
|
+
monitorCommand: (spec) => `XDG_CONFIG_HOME=${quoteShellValue(spec.herdrConfigDir)} herdr session attach ${quoteShellValue(spec.herdrSession)}`,
|
|
280
|
+
launch: async (spec, launcherPath, launchEnv, onRuntimeOwned) => {
|
|
281
|
+
ensureHerdrConfig(spec.herdrConfigDir);
|
|
282
|
+
try {
|
|
283
|
+
await runHerdrSessionCommand(runHerdrCommand, spec, ["pane", "list"]);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
287
|
+
throw new Error("Herdr worker runtime is configured but the 'herdr' executable is unavailable.", { cause: error });
|
|
288
|
+
}
|
|
289
|
+
if (!(error instanceof Error) || !isMissingHerdrSession(error))
|
|
290
|
+
throw error;
|
|
291
|
+
await runHerdrSessionCommand(runHerdrCommand, spec, ["server"], true);
|
|
292
|
+
let ready = false;
|
|
293
|
+
let lastMissingError = error;
|
|
294
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
295
|
+
try {
|
|
296
|
+
await runHerdrSessionCommand(runHerdrCommand, spec, ["pane", "list"]);
|
|
297
|
+
ready = true;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
catch (probeError) {
|
|
301
|
+
if (!(probeError instanceof Error) || !isMissingHerdrSession(probeError)) {
|
|
302
|
+
throw probeError;
|
|
303
|
+
}
|
|
304
|
+
lastMissingError = probeError;
|
|
305
|
+
await sleep(50);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!ready) {
|
|
309
|
+
throw new Error(`Herdr session ${spec.herdrSession} did not become ready`, {
|
|
310
|
+
cause: lastMissingError,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const createOutput = await runHerdrSessionCommand(runHerdrCommand, spec, [
|
|
315
|
+
"workspace",
|
|
316
|
+
"create",
|
|
317
|
+
"--cwd",
|
|
318
|
+
path.dirname(launcherPath),
|
|
319
|
+
"--label",
|
|
320
|
+
spec.sessionName,
|
|
321
|
+
...Object.entries(launchEnv).flatMap(([key, value]) => ["--env", `${key}=${value}`]),
|
|
322
|
+
...PER_LAUNCH_PINET_ENV_KEYS.filter((key) => !(key in launchEnv)).flatMap((key) => [
|
|
323
|
+
"--env",
|
|
324
|
+
`${key}=`,
|
|
325
|
+
]),
|
|
326
|
+
...SUBTREE_INHERITED_ENV_KEYS.flatMap((key) => ["--env", `${key}=`]),
|
|
327
|
+
...HERDR_UNSET_ENV_KEYS.flatMap((key) => ["--env", `${key}=`]),
|
|
328
|
+
"--no-focus",
|
|
329
|
+
]);
|
|
330
|
+
const createResponse = JSON.parse(createOutput);
|
|
331
|
+
const paneId = createResponse.result?.root_pane?.pane_id;
|
|
332
|
+
if (typeof paneId !== "string" || paneId.length === 0) {
|
|
333
|
+
throw new Error("Herdr workspace create returned no pane id");
|
|
334
|
+
}
|
|
335
|
+
spec.herdrPaneId = paneId;
|
|
336
|
+
try {
|
|
337
|
+
const processOutput = await runHerdrSessionCommand(runHerdrCommand, spec, [
|
|
338
|
+
"pane",
|
|
339
|
+
"process-info",
|
|
340
|
+
"--pane",
|
|
341
|
+
paneId,
|
|
342
|
+
]);
|
|
343
|
+
const processResponse = JSON.parse(processOutput);
|
|
344
|
+
const shellPid = processResponse.result?.process_info?.shell_pid;
|
|
345
|
+
if (typeof shellPid !== "number" || !Number.isInteger(shellPid) || shellPid <= 0) {
|
|
346
|
+
throw new Error(`Herdr pane ${paneId} returned no shell PID`);
|
|
347
|
+
}
|
|
348
|
+
spec.herdrShellPid = shellPid;
|
|
349
|
+
onRuntimeOwned?.(spec);
|
|
350
|
+
}
|
|
351
|
+
catch (launchError) {
|
|
352
|
+
try {
|
|
353
|
+
await runHerdrSessionCommand(runHerdrCommand, spec, ["pane", "close", paneId]);
|
|
354
|
+
spec.herdrPaneId = null;
|
|
355
|
+
spec.herdrShellPid = null;
|
|
356
|
+
}
|
|
357
|
+
catch (rollbackError) {
|
|
358
|
+
if (rollbackError instanceof Error &&
|
|
359
|
+
(isMissingHerdrPane(rollbackError) || isMissingHerdrSession(rollbackError))) {
|
|
360
|
+
spec.herdrPaneId = null;
|
|
361
|
+
spec.herdrShellPid = null;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
|
|
365
|
+
throw new Error(`Herdr launch failed before PID capture for pane ${paneId}, and rollback failed: ${rollbackMessage}`, { cause: launchError });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw launchError;
|
|
369
|
+
}
|
|
370
|
+
await runHerdrSessionCommand(runHerdrCommand, spec, [
|
|
371
|
+
"pane",
|
|
372
|
+
"run",
|
|
373
|
+
paneId,
|
|
374
|
+
quoteShellValue(launcherPath),
|
|
375
|
+
]);
|
|
376
|
+
},
|
|
377
|
+
cleanup: async (spec) => {
|
|
378
|
+
const paneId = spec.herdrPaneId;
|
|
379
|
+
const recordedShellPid = spec.herdrShellPid;
|
|
380
|
+
if (!paneId && recordedShellPid === null)
|
|
381
|
+
return;
|
|
382
|
+
if (!paneId || recordedShellPid === null) {
|
|
383
|
+
throw new Error(`Herdr runtime ${spec.sessionName} has an incomplete pane generation`);
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await runHerdrSessionCommand(runHerdrCommand, spec, ["pane", "get", paneId]);
|
|
387
|
+
const processOutput = await runHerdrSessionCommand(runHerdrCommand, spec, [
|
|
388
|
+
"pane",
|
|
389
|
+
"process-info",
|
|
390
|
+
"--pane",
|
|
391
|
+
paneId,
|
|
392
|
+
]);
|
|
393
|
+
const processResponse = JSON.parse(processOutput);
|
|
394
|
+
const observedShellPid = processResponse.result?.process_info?.shell_pid;
|
|
395
|
+
if (typeof observedShellPid !== "number" ||
|
|
396
|
+
!Number.isInteger(observedShellPid) ||
|
|
397
|
+
observedShellPid <= 0) {
|
|
398
|
+
throw new Error(`Herdr pane ${paneId} returned no shell PID`);
|
|
399
|
+
}
|
|
400
|
+
if (observedShellPid !== recordedShellPid) {
|
|
401
|
+
throw new Error(`Refusing to clean up Herdr pane ${paneId}: recorded shell PID ${recordedShellPid}, observed ${observedShellPid}`);
|
|
402
|
+
}
|
|
403
|
+
await runHerdrSessionCommand(runHerdrCommand, spec, ["pane", "close", paneId]);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (error instanceof Error && (isMissingHerdrPane(error) || isMissingHerdrSession(error))) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
export function getExtensionEntryPath() {
|
|
128
415
|
const currentPath = fileURLToPath(import.meta.url);
|
|
129
416
|
const extension = path.extname(currentPath) || ".js";
|
|
130
417
|
return path.join(path.dirname(currentPath), `index${extension}`);
|
|
131
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Broker env var NAMES (never values) a spawned — or later WOKEN — worker
|
|
421
|
+
* re-exports to re-establish itself. Single source of truth shared by the
|
|
422
|
+
* ordinary spawn launcher and the Phase B wake path so both stay in lockstep.
|
|
423
|
+
*/
|
|
424
|
+
export const SUBTREE_INHERITED_ENV_KEYS = [
|
|
425
|
+
"PI_CODING_AGENT_DIR",
|
|
426
|
+
"PI_CODING_AGENT_SESSION_DIR",
|
|
427
|
+
"PI_OFFLINE",
|
|
428
|
+
"PI_SETTINGS_PATH",
|
|
429
|
+
"PINET_MESH_SECRET",
|
|
430
|
+
"PINET_MESH_SECRET_PATH",
|
|
431
|
+
"PINET_HIBERNATION_RUNTIME_ACTIVATION",
|
|
432
|
+
"SLACK_ALLOW_ALL_WORKSPACE_USERS",
|
|
433
|
+
"SLACK_ALLOWED_USERS",
|
|
434
|
+
"SLACK_APP_TOKEN",
|
|
435
|
+
"SLACK_BOT_TOKEN",
|
|
436
|
+
];
|
|
132
437
|
function childStartupPrompt(parentAgentId) {
|
|
133
438
|
return [
|
|
134
439
|
`You are a Pinet subtree child supervised by ${parentAgentId}.`,
|
|
@@ -136,31 +441,47 @@ function childStartupPrompt(parentAgentId) {
|
|
|
136
441
|
"If you are not following Pinet yet, wait for the launcher to run /pinet follow.",
|
|
137
442
|
].join(" ");
|
|
138
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Per-launch PINET identity env var NAMES. A child launch must either set
|
|
446
|
+
* these explicitly or have them cleared at the pane/launcher boundary, so a
|
|
447
|
+
* child can never inherit stale identity (broker owner, lane, tmux locator,
|
|
448
|
+
* parentage) from the parent process environment or a shared, long-lived
|
|
449
|
+
* Herdr server started by an earlier agent.
|
|
450
|
+
*/
|
|
451
|
+
const PER_LAUNCH_PINET_ENV_KEYS = [
|
|
452
|
+
"PINET_BROKER_AGENT_ID",
|
|
453
|
+
"PINET_BROKER_MANAGED",
|
|
454
|
+
"PINET_LANE_ID",
|
|
455
|
+
"PINET_LAUNCH_ID",
|
|
456
|
+
"PINET_LAUNCH_SOURCE",
|
|
457
|
+
"PINET_PARENT_AGENT_ID",
|
|
458
|
+
"PINET_ROOT_AGENT_ID",
|
|
459
|
+
"PINET_SOCKET_PATH",
|
|
460
|
+
"PINET_SPAWNED_BY_AGENT_ID",
|
|
461
|
+
"PINET_SUBTREE_ROLE",
|
|
462
|
+
"PINET_TMUX_SESSION",
|
|
463
|
+
"PINET_WAKE_CORRELATION_ID",
|
|
464
|
+
"PINET_WAKE_FENCE_TOKEN",
|
|
465
|
+
"PINET_WAKE_LEASE_ID",
|
|
466
|
+
"PINET_WAKE_RESERVED_GENERATION",
|
|
467
|
+
"PINET_WAKE_RESERVATION_NONCE",
|
|
468
|
+
];
|
|
469
|
+
const HERDR_UNSET_ENV_KEYS = ["CLAUDE_TMUX_SOCKET_DIR", "TMUX", "TMUX_PANE"];
|
|
139
470
|
function buildLauncherScript(input) {
|
|
140
|
-
const
|
|
141
|
-
"PI_CODING_AGENT_DIR",
|
|
142
|
-
"PI_CODING_AGENT_SESSION_DIR",
|
|
143
|
-
"PI_OFFLINE",
|
|
144
|
-
"PI_SETTINGS_PATH",
|
|
145
|
-
"PINET_MESH_SECRET",
|
|
146
|
-
"PINET_MESH_SECRET_PATH",
|
|
147
|
-
"SLACK_APP_TOKEN",
|
|
148
|
-
"SLACK_BOT_TOKEN",
|
|
149
|
-
];
|
|
150
|
-
const inheritedExports = inheritedEnvKeys
|
|
151
|
-
.map((key) => {
|
|
471
|
+
const inheritedEnv = SUBTREE_INHERITED_ENV_KEYS.map((key) => {
|
|
152
472
|
const value = process.env[key];
|
|
153
|
-
return value ? `export ${key}=${quoteShellValue(value)}` :
|
|
154
|
-
})
|
|
155
|
-
.filter((line) => Boolean(line));
|
|
473
|
+
return value ? `export ${key}=${quoteShellValue(value)}` : `unset ${key}`;
|
|
474
|
+
});
|
|
156
475
|
const envExports = Object.entries(input.env).map(([key, value]) => `export ${key}=${quoteShellValue(value)}`);
|
|
157
476
|
const nickname = `Subtree ${input.env.PINET_SUBTREE_ROLE ?? "Worker"} ${input.env.PINET_LAUNCH_ID ?? randomSuffix()}`;
|
|
158
477
|
return [
|
|
159
478
|
"#!/bin/bash",
|
|
160
479
|
"set -euo pipefail",
|
|
161
480
|
`cd ${quoteShellValue(input.repoPath)}`,
|
|
162
|
-
...
|
|
481
|
+
...(input.runtimeKind === "herdr" ? HERDR_UNSET_ENV_KEYS.map((key) => `unset ${key}`) : []),
|
|
482
|
+
...inheritedEnv,
|
|
163
483
|
...envExports,
|
|
484
|
+
...PER_LAUNCH_PINET_ENV_KEYS.filter((key) => !(key in input.env)).map((key) => `unset ${key}`),
|
|
164
485
|
`export PI_NICKNAME=${quoteShellValue(nickname)}`,
|
|
165
486
|
`exec pi -e ${quoteShellValue(input.extensionEntryPath)} ${quoteShellValue(input.startupPrompt)}`,
|
|
166
487
|
"",
|
|
@@ -199,7 +520,47 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
199
520
|
let startedAt = null;
|
|
200
521
|
let activePaths = null;
|
|
201
522
|
let heartbeatTimer = null;
|
|
523
|
+
let brokerStartPromise = null;
|
|
202
524
|
const spawnedWorkers = new Map();
|
|
525
|
+
const fencedLaunchIds = new Set();
|
|
526
|
+
const pendingInboxIds = new Set();
|
|
527
|
+
const runTmuxCommand = deps.runTmuxCommand ??
|
|
528
|
+
(async (args) => {
|
|
529
|
+
await execFileAsync("tmux", args);
|
|
530
|
+
});
|
|
531
|
+
const runHerdrCommand = deps.runHerdrCommand ??
|
|
532
|
+
(async (args, options) => {
|
|
533
|
+
if (!options.detached) {
|
|
534
|
+
const { stdout } = await execFileAsync("herdr", args, { env: options.env });
|
|
535
|
+
return stdout;
|
|
536
|
+
}
|
|
537
|
+
await new Promise((resolve, reject) => {
|
|
538
|
+
const child = spawn("herdr", args, {
|
|
539
|
+
detached: true,
|
|
540
|
+
env: options.env,
|
|
541
|
+
stdio: "ignore",
|
|
542
|
+
});
|
|
543
|
+
child.once("error", reject);
|
|
544
|
+
child.once("spawn", () => {
|
|
545
|
+
child.unref();
|
|
546
|
+
resolve();
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
return "";
|
|
550
|
+
});
|
|
551
|
+
const workerRuntimeControllers = {
|
|
552
|
+
tmux: createTmuxWorkerRuntimeController(runTmuxCommand),
|
|
553
|
+
herdr: createHerdrWorkerRuntimeController(runHerdrCommand, {
|
|
554
|
+
herdrSession: "pinet-workers",
|
|
555
|
+
herdrConfigDir: path.join(os.homedir(), ".pi", "pinet-herdr-config"),
|
|
556
|
+
}),
|
|
557
|
+
};
|
|
558
|
+
// agent-standards-ignore prefer-inline-single-use-helper: discriminated runtime cleanup dispatch
|
|
559
|
+
function cleanupWorkerRuntime(spec) {
|
|
560
|
+
return spec.runtimeKind === "tmux"
|
|
561
|
+
? workerRuntimeControllers.tmux.cleanup(spec)
|
|
562
|
+
: workerRuntimeControllers.herdr.cleanup(spec);
|
|
563
|
+
}
|
|
203
564
|
function stopHeartbeat() {
|
|
204
565
|
if (!heartbeatTimer)
|
|
205
566
|
return;
|
|
@@ -218,6 +579,15 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
218
579
|
}, HEARTBEAT_INTERVAL_MS);
|
|
219
580
|
heartbeatTimer.unref?.();
|
|
220
581
|
}
|
|
582
|
+
function fenceLaunch(broker, launchId) {
|
|
583
|
+
fencedLaunchIds.add(launchId);
|
|
584
|
+
for (const agent of broker.db.getAllAgents()) {
|
|
585
|
+
if (metadataString(agent.metadata, "launchId") === launchId) {
|
|
586
|
+
broker.server.disconnectAgentConnections(agent.id);
|
|
587
|
+
broker.db.unregisterAgent(agent.id);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
221
591
|
function currentChildren() {
|
|
222
592
|
const broker = activeBroker;
|
|
223
593
|
const agentId = selfAgentId;
|
|
@@ -241,7 +611,10 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
241
611
|
};
|
|
242
612
|
}
|
|
243
613
|
function drainSelfInbox(ctx, broker, agentId) {
|
|
244
|
-
const entries = broker.db
|
|
614
|
+
const entries = broker.db
|
|
615
|
+
.getInbox(agentId)
|
|
616
|
+
.filter((item) => !pendingInboxIds.has(item.entry.id))
|
|
617
|
+
.map(toFollowerInboxEntry);
|
|
245
618
|
if (entries.length === 0)
|
|
246
619
|
return;
|
|
247
620
|
const synced = syncBrokerInboxEntries(entries);
|
|
@@ -263,12 +636,46 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
263
636
|
if (handledControlInboxIds.size > 0) {
|
|
264
637
|
broker.db.markDelivered([...handledControlInboxIds], agentId);
|
|
265
638
|
}
|
|
639
|
+
const steeredInboxIds = [];
|
|
640
|
+
for (const entry of synced.steeringEntries) {
|
|
641
|
+
try {
|
|
642
|
+
if (deps.deliverSteeringMessage(formatPinetSteeringMessage(entry), ctx)) {
|
|
643
|
+
steeredInboxIds.push(entry.inboxId);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
ctx.ui.notify(`Subtree Pinet steering failed: ${deps.formatError(error)}`, "error");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (steeredInboxIds.length > 0) {
|
|
651
|
+
broker.db.markDelivered(steeredInboxIds, agentId);
|
|
652
|
+
}
|
|
266
653
|
if (synced.inboxMessages.length === 0)
|
|
267
654
|
return;
|
|
268
|
-
|
|
655
|
+
const inboxMessages = synced.inboxMessages.map((message) => ({
|
|
656
|
+
...message,
|
|
657
|
+
brokerInboxOrigin: "subtree",
|
|
658
|
+
}));
|
|
659
|
+
deps.pushInboxMessages(inboxMessages);
|
|
660
|
+
for (const message of inboxMessages) {
|
|
661
|
+
if (message.brokerInboxId != null)
|
|
662
|
+
pendingInboxIds.add(message.brokerInboxId);
|
|
663
|
+
}
|
|
269
664
|
deps.updateBadge();
|
|
270
665
|
deps.maybeDrainInboxIfIdle(ctx);
|
|
271
666
|
}
|
|
667
|
+
function drainInbox(ctx) {
|
|
668
|
+
if (!activeBroker || !selfAgentId)
|
|
669
|
+
return;
|
|
670
|
+
drainSelfInbox(ctx, activeBroker, selfAgentId);
|
|
671
|
+
}
|
|
672
|
+
function markDelivered(inboxIds) {
|
|
673
|
+
if (!activeBroker || !selfAgentId)
|
|
674
|
+
return;
|
|
675
|
+
activeBroker.db.markDelivered(inboxIds, selfAgentId);
|
|
676
|
+
for (const inboxId of inboxIds)
|
|
677
|
+
pendingInboxIds.delete(inboxId);
|
|
678
|
+
}
|
|
272
679
|
function readInbox(options = {}) {
|
|
273
680
|
if (!activeBroker || !selfAgentId)
|
|
274
681
|
return null;
|
|
@@ -294,9 +701,10 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
294
701
|
const targetAgent = resolveDirectAgentTarget(activeBroker.db.getAgents(), target);
|
|
295
702
|
if (!targetAgent || targetAgent.id === selfAgentId)
|
|
296
703
|
return null;
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
const
|
|
704
|
+
const normalized = normalizeOutgoingPinetControlMessage(body, metadata) ??
|
|
705
|
+
normalizeOutgoingPinetSteeringMessage(body, metadata);
|
|
706
|
+
const finalBody = normalized?.body ?? body;
|
|
707
|
+
const finalMetadata = normalized?.metadata ?? metadata;
|
|
300
708
|
const identity = deps.getAgentIdentity();
|
|
301
709
|
const result = dispatchDirectAgentMessage(activeBroker.db, {
|
|
302
710
|
senderAgentId: selfAgentId,
|
|
@@ -320,7 +728,7 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
320
728
|
return filtered.map((agent) => toSubtreeAgentRecord(broker.db, agent));
|
|
321
729
|
}
|
|
322
730
|
async function sendFollowCommand(sessionName, tmuxBaseArgs) {
|
|
323
|
-
await
|
|
731
|
+
await runTmuxCommand([
|
|
324
732
|
...tmuxBaseArgs,
|
|
325
733
|
"send-keys",
|
|
326
734
|
"-t",
|
|
@@ -329,7 +737,7 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
329
737
|
"--",
|
|
330
738
|
"/pinet follow",
|
|
331
739
|
]);
|
|
332
|
-
await
|
|
740
|
+
await runTmuxCommand([...tmuxBaseArgs, "send-keys", "-t", sessionName, "Enter"]);
|
|
333
741
|
}
|
|
334
742
|
async function waitForSpawnedAgent(input) {
|
|
335
743
|
const deadline = Date.now() + input.timeoutMs;
|
|
@@ -337,41 +745,84 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
337
745
|
while (Date.now() < deadline) {
|
|
338
746
|
const agent = input.broker.db
|
|
339
747
|
.getAllAgents()
|
|
340
|
-
.find((candidate) => metadataString(candidate.metadata, "launchId") === input.launchId);
|
|
748
|
+
.find((candidate) => metadataString(candidate.metadata, "launchId") === input.handle.launchId);
|
|
341
749
|
if (agent)
|
|
342
750
|
return agent;
|
|
343
|
-
if (Date.now() - lastFollowAttemptAt > 6_000) {
|
|
751
|
+
if (input.runtimeSpec.runtimeKind === "tmux" && Date.now() - lastFollowAttemptAt > 6_000) {
|
|
344
752
|
lastFollowAttemptAt = Date.now();
|
|
345
|
-
await sendFollowCommand(input.sessionName, input.
|
|
753
|
+
await sendFollowCommand(input.runtimeSpec.sessionName, buildTmuxBaseArgs(input.runtimeSpec.tmuxSocketPath)).catch(() => {
|
|
346
754
|
// The session may still be starting; the loop retries until timeout.
|
|
347
755
|
});
|
|
348
756
|
}
|
|
349
|
-
await
|
|
757
|
+
await sleep(Math.min(1_000, Math.max(1, deadline - Date.now())));
|
|
350
758
|
}
|
|
351
|
-
|
|
759
|
+
fenceLaunch(input.broker, input.handle.launchId);
|
|
760
|
+
throw new SubtreeSpawnRegistrationTimeoutError(input.timeoutMs, input.handle);
|
|
352
761
|
}
|
|
353
762
|
async function requestChildExit(agent) {
|
|
354
763
|
await sendMessage(agent.id, "/exit", { subtreeLifecycle: "stop" }).catch(() => null);
|
|
355
764
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
765
|
+
// agent-standards-ignore prefer-inline-single-use-helper: restart-safe runtime cleanup resolution
|
|
766
|
+
function childRuntimeSpecs(broker, agentId) {
|
|
767
|
+
const specs = new Map();
|
|
768
|
+
const addSpec = (spec) => {
|
|
769
|
+
const key = spec.runtimeKind === "tmux"
|
|
770
|
+
? `tmux:${spec.tmuxSocketPath ?? ""}:${spec.sessionName}`
|
|
771
|
+
: `herdr:${spec.herdrConfigDir}:${spec.herdrSession}:${spec.herdrPaneId ?? ""}:${spec.herdrShellPid ?? ""}`;
|
|
772
|
+
specs.set(key, spec);
|
|
773
|
+
};
|
|
774
|
+
const workers = [...spawnedWorkers.values()];
|
|
775
|
+
for (const worker of workers)
|
|
776
|
+
addSpec(worker);
|
|
777
|
+
const activeLaunchIds = new Set(spawnedWorkers.keys());
|
|
778
|
+
for (const { launchId, ...spec } of readPendingHerdrRuntimes(broker.db)) {
|
|
779
|
+
if (!activeLaunchIds.has(launchId))
|
|
780
|
+
addSpec(spec);
|
|
366
781
|
}
|
|
782
|
+
const recordedAgentIds = new Set(workers.flatMap((worker) => (worker.agentId ? [worker.agentId] : [])));
|
|
367
783
|
for (const agent of broker.db.getAllAgents()) {
|
|
368
|
-
if (!isSubtreeChildAgent(agent, agentId))
|
|
784
|
+
if (!isSubtreeChildAgent(agent, agentId) || recordedAgentIds.has(agent.id))
|
|
785
|
+
continue;
|
|
786
|
+
const durableSpec = broker.db.getAgentRuntimeSpec(agent.id);
|
|
787
|
+
if (durableSpec?.runtimeKind === "tmux") {
|
|
788
|
+
addSpec({
|
|
789
|
+
runtimeKind: "tmux",
|
|
790
|
+
sessionName: durableSpec.tmuxSession,
|
|
791
|
+
tmuxSocketPath: durableSpec.tmuxSocket,
|
|
792
|
+
});
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (durableSpec?.runtimeKind === "herdr") {
|
|
796
|
+
addSpec({
|
|
797
|
+
runtimeKind: "herdr",
|
|
798
|
+
sessionName: agent.id,
|
|
799
|
+
herdrSession: durableSpec.herdrSession,
|
|
800
|
+
herdrConfigDir: durableSpec.herdrConfigDir,
|
|
801
|
+
herdrPaneId: durableSpec.herdrPaneId,
|
|
802
|
+
herdrShellPid: durableSpec.herdrShellPid,
|
|
803
|
+
});
|
|
369
804
|
continue;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
805
|
+
}
|
|
806
|
+
const tmuxSession = metadataString(agent.metadata, "tmuxSession");
|
|
807
|
+
const metadataRuntimeKind = metadataString(agent.metadata, "runtimeKind");
|
|
808
|
+
const launchSource = metadataString(agent.metadata, "launchSource");
|
|
809
|
+
if (metadataRuntimeKind === "herdr" ||
|
|
810
|
+
launchSource === "broker-herdr" ||
|
|
811
|
+
launchSource === "subtree-broker-herdr") {
|
|
812
|
+
if (tmuxSession) {
|
|
813
|
+
console.warn(`[slack-bridge] Ignoring inherited tmuxSession metadata for Herdr child ${agent.id}; no durable Herdr runtime spec is available for cleanup.`);
|
|
814
|
+
}
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (tmuxSession) {
|
|
818
|
+
addSpec({
|
|
819
|
+
runtimeKind: "tmux",
|
|
820
|
+
sessionName: tmuxSession,
|
|
821
|
+
tmuxSocketPath: findTmuxSocketPath(),
|
|
822
|
+
});
|
|
823
|
+
}
|
|
373
824
|
}
|
|
374
|
-
return [...
|
|
825
|
+
return [...specs.values()];
|
|
375
826
|
}
|
|
376
827
|
async function stopChildren(broker, agentId) {
|
|
377
828
|
const children = broker.db
|
|
@@ -379,11 +830,20 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
379
830
|
.filter((agent) => isSubtreeChildAgent(agent, agentId));
|
|
380
831
|
await Promise.all(children.map(requestChildExit));
|
|
381
832
|
if (children.length > 0) {
|
|
382
|
-
await
|
|
833
|
+
await sleep(SUBTREE_CHILD_EXIT_GRACE_MS);
|
|
834
|
+
}
|
|
835
|
+
const cleanupResults = await Promise.all(childRuntimeSpecs(broker, agentId).map(async (spec) => {
|
|
836
|
+
try {
|
|
837
|
+
await cleanupWorkerRuntime(spec);
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}));
|
|
844
|
+
if (cleanupResults.every(Boolean)) {
|
|
845
|
+
broker.db.deleteSetting(PENDING_HERDR_RUNTIMES_SETTING);
|
|
383
846
|
}
|
|
384
|
-
const tmuxSocketPath = findTmuxSocketPath();
|
|
385
|
-
const tmuxBaseArgs = buildTmuxBaseArgs(tmuxSocketPath);
|
|
386
|
-
await Promise.all(childTmuxSessions(broker, agentId).map((sessionName) => killTmuxSession(sessionName, tmuxBaseArgs).catch(() => undefined)));
|
|
387
847
|
}
|
|
388
848
|
async function stop(options = {}) {
|
|
389
849
|
stopHeartbeat();
|
|
@@ -409,102 +869,209 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
409
869
|
startedAt = null;
|
|
410
870
|
activePaths = null;
|
|
411
871
|
spawnedWorkers.clear();
|
|
872
|
+
fencedLaunchIds.clear();
|
|
873
|
+
deps.discardQueuedInboxMessages();
|
|
874
|
+
pendingInboxIds.clear();
|
|
875
|
+
deps.updateBadge();
|
|
412
876
|
}
|
|
413
877
|
}
|
|
414
|
-
|
|
878
|
+
// agent-standards-ignore prefer-inline-single-use-helper: single-flight start implementation
|
|
879
|
+
async function startOnce(ctx) {
|
|
415
880
|
if (activeBroker)
|
|
416
881
|
return getStatus();
|
|
417
882
|
const stableId = deps.getCentralAgentId() ?? deps.getAgentStableId();
|
|
418
883
|
const paths = buildSubtreeBrokerPaths(stableId);
|
|
419
884
|
fs.mkdirSync(paths.rootDir, { recursive: true });
|
|
420
885
|
const meshAuth = resolvePinetMeshAuth(deps.getSettings());
|
|
886
|
+
// Capture the durable, process-lifetime activation authority at broker start
|
|
887
|
+
// (frozen; never re-read from reloadable settings) and resolve the self id up
|
|
888
|
+
// front so Phase B, Seam 3 startup stranded-wake recovery can run inside
|
|
889
|
+
// `beforeListen` — strictly BEFORE the socket accepts any registration.
|
|
890
|
+
freezeHibernationActivationAuthority();
|
|
891
|
+
const selfId = buildSelfAgentId(stableId);
|
|
892
|
+
const runtimeActive = hibernationRuntimeActive();
|
|
893
|
+
const startupHib = resolveHibernationSettings(deps.getSettings());
|
|
421
894
|
const broker = await startBroker({
|
|
422
895
|
dbPath: paths.dbPath,
|
|
423
896
|
socketPath: paths.socketPath,
|
|
424
897
|
lockPath: paths.lockPath,
|
|
425
898
|
...(meshAuth.meshSecret ? { meshSecret: meshAuth.meshSecret } : {}),
|
|
426
899
|
...(meshAuth.meshSecretPath ? { meshSecretPath: meshAuth.meshSecretPath } : {}),
|
|
900
|
+
beforeListen: async ({ db }) => {
|
|
901
|
+
const pendingHerdrRuntimes = readPendingHerdrRuntimes(db);
|
|
902
|
+
for (const pending of pendingHerdrRuntimes) {
|
|
903
|
+
const spec = pending;
|
|
904
|
+
await cleanupWorkerRuntime(spec);
|
|
905
|
+
}
|
|
906
|
+
if (pendingHerdrRuntimes.length > 0) {
|
|
907
|
+
db.deleteSetting(PENDING_HERDR_RUNTIMES_SETTING);
|
|
908
|
+
}
|
|
909
|
+
if (runtimeActive) {
|
|
910
|
+
recoverStrandedWakesBeforeRegistrations(createHibernationOrchestrator({
|
|
911
|
+
db,
|
|
912
|
+
brokerInstanceId: selfId,
|
|
913
|
+
extensionEntryPath: getExtensionEntryPath(),
|
|
914
|
+
baseLaunchEnv: buildChildLaunchEnv(paths, selfId),
|
|
915
|
+
inheritedEnvKeys: SUBTREE_INHERITED_ENV_KEYS,
|
|
916
|
+
config: {
|
|
917
|
+
handshakeTimeoutMs: startupHib.handshakeTimeoutMs,
|
|
918
|
+
wakeLeaseMs: startupHib.wakeLeaseMs,
|
|
919
|
+
maxConcurrentWakes: startupHib.maxConcurrentWakes,
|
|
920
|
+
maxConcurrentWakesPerRepo: startupHib.maxConcurrentWakesPerRepo,
|
|
921
|
+
},
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
924
|
+
},
|
|
427
925
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
};
|
|
436
|
-
const selfAgent = broker.db.registerAgent(selfId, name ? `Subtree Broker ${name}` : "Subtree Broker", emoji || "🌳", process.pid, metadata, `${stableId}:subtree-broker`);
|
|
437
|
-
broker.server.setAgentRegistrationResolver((registration) => {
|
|
438
|
-
const role = deps.getMeshRoleFromMetadata(registration.metadata, "worker");
|
|
439
|
-
const identity = generateAgentName(registration.stableId ?? registration.agentId, role);
|
|
440
|
-
return {
|
|
441
|
-
name: registration.name || identity.name,
|
|
442
|
-
emoji: registration.emoji || identity.emoji,
|
|
443
|
-
metadata: {
|
|
444
|
-
...(registration.metadata ?? {}),
|
|
445
|
-
subtreeBrokerAgentId: selfAgent.id,
|
|
446
|
-
subtreeRootAgentId: selfAgent.id,
|
|
447
|
-
},
|
|
926
|
+
try {
|
|
927
|
+
const { name, emoji } = deps.getAgentIdentity();
|
|
928
|
+
const metadata = {
|
|
929
|
+
...(await deps.getAgentMetadata("broker")),
|
|
930
|
+
subtreeBroker: true,
|
|
931
|
+
upstreamAgentId: deps.getCentralAgentId(),
|
|
932
|
+
subtreeSocketPath: paths.socketPath,
|
|
448
933
|
};
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
934
|
+
const selfAgent = broker.db.registerAgent(selfId, name ? `Subtree Broker ${name}` : "Subtree Broker", emoji || "🌳", process.pid, metadata, `${stableId}:subtree-broker`);
|
|
935
|
+
broker.server.setAgentRegistrationResolver((registration) => {
|
|
936
|
+
const launchId = metadataString(registration.metadata, "launchId");
|
|
937
|
+
if (launchId && fencedLaunchIds.has(launchId)) {
|
|
938
|
+
throw new Error("spawn launch has already been cleaned up");
|
|
939
|
+
}
|
|
940
|
+
const role = deps.getMeshRoleFromMetadata(registration.metadata, "worker");
|
|
941
|
+
const identity = generateAgentName(registration.stableId ?? registration.agentId, role);
|
|
942
|
+
const worker = launchId ? spawnedWorkers.get(launchId) : undefined;
|
|
943
|
+
return {
|
|
944
|
+
name: registration.name || identity.name,
|
|
945
|
+
emoji: registration.emoji || identity.emoji,
|
|
946
|
+
metadata: {
|
|
947
|
+
...(registration.metadata ?? {}),
|
|
948
|
+
...(worker
|
|
949
|
+
? {
|
|
950
|
+
runtimeKind: worker.runtimeKind,
|
|
951
|
+
runtimeLocator: worker.runtimeKind === "tmux"
|
|
952
|
+
? worker.sessionName
|
|
953
|
+
: (worker.herdrPaneId ?? worker.sessionName),
|
|
954
|
+
}
|
|
955
|
+
: {}),
|
|
956
|
+
subtreeBrokerAgentId: selfAgent.id,
|
|
957
|
+
subtreeRootAgentId: selfAgent.id,
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
});
|
|
961
|
+
broker.server.onAgentMessage((targetAgentId) => {
|
|
962
|
+
if (targetAgentId !== selfAgent.id)
|
|
963
|
+
return;
|
|
964
|
+
drainSelfInbox(ctx, broker, selfAgent.id);
|
|
965
|
+
});
|
|
966
|
+
activeBroker = broker;
|
|
967
|
+
selfAgentId = selfAgent.id;
|
|
968
|
+
startedAt = new Date().toISOString();
|
|
969
|
+
activePaths = paths;
|
|
970
|
+
startHeartbeat(broker, selfAgent.id);
|
|
971
|
+
broker.db.recoverPendingTargetedBacklog(selfAgent.id);
|
|
453
972
|
drainSelfInbox(ctx, broker, selfAgent.id);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
973
|
+
broker.db.setSetting("pinet.subtreeBrokerParentStableId", deps.getAgentStableId());
|
|
974
|
+
broker.db.setSetting("pinet.subtreeBrokerOwnerToken", buildPinetOwnerToken(stableId));
|
|
975
|
+
return getStatus();
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
stopHeartbeat();
|
|
979
|
+
activeBroker = null;
|
|
980
|
+
selfAgentId = null;
|
|
981
|
+
startedAt = null;
|
|
982
|
+
activePaths = null;
|
|
983
|
+
deps.discardQueuedInboxMessages();
|
|
984
|
+
pendingInboxIds.clear();
|
|
985
|
+
deps.updateBadge();
|
|
986
|
+
await broker.stop().catch(() => undefined);
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function start(ctx) {
|
|
991
|
+
if (activeBroker)
|
|
992
|
+
return Promise.resolve(getStatus());
|
|
993
|
+
if (!brokerStartPromise) {
|
|
994
|
+
brokerStartPromise = startOnce(ctx).finally(() => {
|
|
995
|
+
brokerStartPromise = null;
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
return brokerStartPromise;
|
|
463
999
|
}
|
|
464
1000
|
async function spawnWorker(ctx, input) {
|
|
465
1001
|
if (!input.task.trim())
|
|
466
1002
|
throw new Error("spawn requires task");
|
|
467
1003
|
if (!input.repo.trim())
|
|
468
1004
|
throw new Error("spawn requires repo");
|
|
469
|
-
|
|
470
|
-
await start(ctx);
|
|
471
|
-
}
|
|
1005
|
+
await start(ctx);
|
|
472
1006
|
if (!activeBroker || !activePaths || !selfAgentId) {
|
|
473
1007
|
throw new Error("Subtree broker is not running.");
|
|
474
1008
|
}
|
|
1009
|
+
const broker = activeBroker;
|
|
1010
|
+
if (input.cleanupHandle) {
|
|
1011
|
+
const handle = input.cleanupHandle;
|
|
1012
|
+
if (!activePaths || handle.socketPath !== activePaths.socketPath) {
|
|
1013
|
+
throw new Error("spawn cleanup handle does not belong to this subtree broker");
|
|
1014
|
+
}
|
|
1015
|
+
const worker = spawnedWorkers.get(handle.launchId);
|
|
1016
|
+
if (!worker) {
|
|
1017
|
+
throw new Error(fencedLaunchIds.has(handle.launchId)
|
|
1018
|
+
? "spawn cleanup handle has already been consumed"
|
|
1019
|
+
: "spawn cleanup handle does not belong to this subtree broker");
|
|
1020
|
+
}
|
|
1021
|
+
if (worker.sessionName !== handle.tmuxSessionName) {
|
|
1022
|
+
throw new Error("spawn cleanup handle does not match its recorded tmux session");
|
|
1023
|
+
}
|
|
1024
|
+
fenceLaunch(activeBroker, handle.launchId);
|
|
1025
|
+
spawnedWorkers.delete(handle.launchId);
|
|
1026
|
+
try {
|
|
1027
|
+
await cleanupWorkerRuntime(worker);
|
|
1028
|
+
if (worker.runtimeKind === "herdr") {
|
|
1029
|
+
updatePendingHerdrRuntime(activeBroker.db, handle.launchId, null);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
spawnedWorkers.set(handle.launchId, worker);
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const socketPath = activePaths.socketPath;
|
|
475
1038
|
const repoPath = resolveRepoPath(input.repo, deps.cwd);
|
|
476
1039
|
const role = normalizeRole(input.role);
|
|
477
1040
|
const launchId = `subtree-${Date.now().toString(36)}-${randomSuffix()}`;
|
|
478
1041
|
const sessionName = buildTmuxSessionName(repoPath, role, launchId);
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
const
|
|
1042
|
+
const configuredRuntime = deps.getSettings()
|
|
1043
|
+
.subtreeWorkerRuntime;
|
|
1044
|
+
const runtimeKind = configuredRuntime === undefined ? "tmux" : configuredRuntime;
|
|
1045
|
+
if (runtimeKind !== "tmux" && runtimeKind !== "herdr") {
|
|
1046
|
+
const invalidValue = JSON.stringify(runtimeKind);
|
|
1047
|
+
throw new Error(`Invalid subtreeWorkerRuntime setting ${invalidValue}; valid options are "tmux" and "herdr".`);
|
|
1048
|
+
}
|
|
1049
|
+
const runtimeSpec = runtimeKind === "tmux"
|
|
1050
|
+
? workerRuntimeControllers.tmux.createLaunchSpec(sessionName)
|
|
1051
|
+
: workerRuntimeControllers.herdr.createLaunchSpec(sessionName);
|
|
1052
|
+
const monitorCommand = runtimeSpec.runtimeKind === "tmux"
|
|
1053
|
+
? workerRuntimeControllers.tmux.monitorCommand(runtimeSpec)
|
|
1054
|
+
: workerRuntimeControllers.herdr.monitorCommand(runtimeSpec);
|
|
482
1055
|
const childLaunchEnv = buildChildLaunchEnv(activePaths, selfAgentId, {
|
|
483
1056
|
launchId,
|
|
484
1057
|
role,
|
|
1058
|
+
runtimeKind: runtimeSpec.runtimeKind,
|
|
485
1059
|
...(input.laneId ? { laneId: input.laneId } : {}),
|
|
486
|
-
tmuxSession: sessionName,
|
|
1060
|
+
...(runtimeSpec.runtimeKind === "tmux" ? { tmuxSession: sessionName } : {}),
|
|
487
1061
|
});
|
|
488
1062
|
const launchersDir = path.join(activePaths.rootDir, "launchers");
|
|
489
1063
|
fs.mkdirSync(launchersDir, { recursive: true });
|
|
490
1064
|
const launcherPath = path.join(launchersDir, `${sessionName}.sh`);
|
|
491
1065
|
fs.writeFileSync(launcherPath, buildLauncherScript({
|
|
1066
|
+
runtimeKind: runtimeSpec.runtimeKind,
|
|
492
1067
|
repoPath,
|
|
493
1068
|
env: childLaunchEnv,
|
|
494
1069
|
extensionEntryPath: getExtensionEntryPath(),
|
|
495
1070
|
startupPrompt: childStartupPrompt(selfAgentId),
|
|
496
1071
|
}), { mode: 0o700 });
|
|
497
|
-
await execFileAsync("tmux", [
|
|
498
|
-
...tmuxBaseArgs,
|
|
499
|
-
"new-session",
|
|
500
|
-
"-d",
|
|
501
|
-
"-s",
|
|
502
|
-
sessionName,
|
|
503
|
-
launcherPath,
|
|
504
|
-
]);
|
|
505
1072
|
const workerRecord = {
|
|
1073
|
+
...runtimeSpec,
|
|
506
1074
|
launchId,
|
|
507
|
-
sessionName,
|
|
508
1075
|
repoPath,
|
|
509
1076
|
role,
|
|
510
1077
|
laneId: input.laneId ?? null,
|
|
@@ -512,16 +1079,101 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
512
1079
|
startedAt: new Date().toISOString(),
|
|
513
1080
|
monitorCommand,
|
|
514
1081
|
};
|
|
1082
|
+
const launchHandle = {
|
|
1083
|
+
launchId,
|
|
1084
|
+
tmuxSessionName: sessionName,
|
|
1085
|
+
socketPath,
|
|
1086
|
+
state: "launched_unregistered",
|
|
1087
|
+
runtimeKind: workerRecord.runtimeKind,
|
|
1088
|
+
monitorCommand: workerRecord.monitorCommand,
|
|
1089
|
+
};
|
|
515
1090
|
spawnedWorkers.set(launchId, workerRecord);
|
|
1091
|
+
try {
|
|
1092
|
+
if (workerRecord.runtimeKind === "tmux") {
|
|
1093
|
+
await workerRuntimeControllers.tmux.launch(workerRecord, launcherPath, childLaunchEnv);
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
await workerRuntimeControllers.herdr.launch(workerRecord, launcherPath, childLaunchEnv, (spec) => updatePendingHerdrRuntime(broker.db, launchId, spec));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
catch (error) {
|
|
1100
|
+
fenceLaunch(activeBroker, launchId);
|
|
1101
|
+
throw new SubtreeSpawnLaunchError(error instanceof Error ? error : new Error(String(error)), launchHandle, workerRecord.runtimeKind);
|
|
1102
|
+
}
|
|
516
1103
|
const agent = await waitForSpawnedAgent({
|
|
517
1104
|
broker: activeBroker,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
tmuxBaseArgs,
|
|
1105
|
+
handle: launchHandle,
|
|
1106
|
+
runtimeSpec: workerRecord,
|
|
521
1107
|
timeoutMs: input.waitForRegistrationMs ?? DEFAULT_SPAWN_REGISTRATION_TIMEOUT_MS,
|
|
522
1108
|
});
|
|
523
1109
|
const updatedRecord = { ...workerRecord, agentId: agent.id };
|
|
524
1110
|
spawnedWorkers.set(launchId, updatedRecord);
|
|
1111
|
+
const commonRuntimeFacts = {
|
|
1112
|
+
agentId: agent.id,
|
|
1113
|
+
stableId: agent.stableId ?? "",
|
|
1114
|
+
brokerOwnerId: selfAgentId,
|
|
1115
|
+
cwd: repoPath,
|
|
1116
|
+
repoRoot: repoPath,
|
|
1117
|
+
worktreePath: repoPath,
|
|
1118
|
+
extensionEntryPath: getExtensionEntryPath(),
|
|
1119
|
+
envAllowlist: Object.keys(childLaunchEnv),
|
|
1120
|
+
configFingerprint: `subtree-broker-${workerRecord.runtimeKind}`,
|
|
1121
|
+
expectedUser: os.userInfo().username,
|
|
1122
|
+
launchSource: childLaunchEnv.PINET_LAUNCH_SOURCE,
|
|
1123
|
+
};
|
|
1124
|
+
if (workerRecord.runtimeKind === "tmux") {
|
|
1125
|
+
if (hibernationRuntimeActive()) {
|
|
1126
|
+
await persistSpawnedRuntimeSpec(activeBroker.db, {
|
|
1127
|
+
...commonRuntimeFacts,
|
|
1128
|
+
runtimeKind: "tmux",
|
|
1129
|
+
tmuxSocket: workerRecord.tmuxSocketPath ?? "",
|
|
1130
|
+
tmuxSession: workerRecord.sessionName,
|
|
1131
|
+
tmuxTarget: workerRecord.sessionName,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
let persisted = false;
|
|
1137
|
+
let persistenceError = null;
|
|
1138
|
+
if (workerRecord.herdrPaneId && workerRecord.herdrShellPid !== null) {
|
|
1139
|
+
try {
|
|
1140
|
+
persisted = Boolean(await persistSpawnedRuntimeSpec(activeBroker.db, {
|
|
1141
|
+
...commonRuntimeFacts,
|
|
1142
|
+
runtimeKind: "herdr",
|
|
1143
|
+
herdrSession: workerRecord.herdrSession,
|
|
1144
|
+
herdrConfigDir: workerRecord.herdrConfigDir,
|
|
1145
|
+
herdrPaneId: workerRecord.herdrPaneId,
|
|
1146
|
+
herdrShellPid: workerRecord.herdrShellPid,
|
|
1147
|
+
}));
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
persistenceError = error instanceof Error ? error : new Error(String(error));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (persisted) {
|
|
1154
|
+
updatePendingHerdrRuntime(activeBroker.db, launchId, null);
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
const failureMessage = `Herdr worker ${agent.id} has no durable restart-safe cleanup identity`;
|
|
1158
|
+
const failure = persistenceError
|
|
1159
|
+
? new Error(failureMessage, { cause: persistenceError })
|
|
1160
|
+
: new Error(failureMessage);
|
|
1161
|
+
fenceLaunch(activeBroker, launchId);
|
|
1162
|
+
await requestChildExit(agent);
|
|
1163
|
+
try {
|
|
1164
|
+
await cleanupWorkerRuntime(workerRecord);
|
|
1165
|
+
updatePendingHerdrRuntime(activeBroker.db, launchId, null);
|
|
1166
|
+
spawnedWorkers.delete(launchId);
|
|
1167
|
+
activeBroker.db.unregisterAgent(agent.id);
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
throw new SubtreeSpawnLaunchError(new Error(`${failure.message}; cleanup also failed`, {
|
|
1171
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
1172
|
+
}), launchHandle, "herdr");
|
|
1173
|
+
}
|
|
1174
|
+
throw new Error(`${failure.message}; worker stopped`, { cause: failure });
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
525
1177
|
const messageResult = await sendMessage(agent.id, input.task, {
|
|
526
1178
|
subtreeTask: true,
|
|
527
1179
|
launchId,
|
|
@@ -534,6 +1186,7 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
534
1186
|
return {
|
|
535
1187
|
status: "started",
|
|
536
1188
|
launchId,
|
|
1189
|
+
runtimeKind: workerRecord.runtimeKind,
|
|
537
1190
|
sessionName,
|
|
538
1191
|
repoPath,
|
|
539
1192
|
role,
|
|
@@ -548,10 +1201,22 @@ export function createSubtreeBrokerRuntime(deps) {
|
|
|
548
1201
|
childLaunchEnv,
|
|
549
1202
|
};
|
|
550
1203
|
}
|
|
1204
|
+
function getHibernationRuntimeControl() {
|
|
1205
|
+
if (!activeBroker || !selfAgentId || !activePaths)
|
|
1206
|
+
return null;
|
|
1207
|
+
return {
|
|
1208
|
+
db: activeBroker.db,
|
|
1209
|
+
brokerInstanceId: selfAgentId,
|
|
1210
|
+
baseLaunchEnv: buildChildLaunchEnv(activePaths, selfAgentId),
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
551
1213
|
return {
|
|
552
1214
|
start,
|
|
1215
|
+
getHibernationRuntimeControl,
|
|
553
1216
|
stop,
|
|
554
1217
|
getStatus,
|
|
1218
|
+
drainInbox,
|
|
1219
|
+
markDelivered,
|
|
555
1220
|
readInbox,
|
|
556
1221
|
sendMessage,
|
|
557
1222
|
listAgents,
|