@pinet/slack-bridge 0.2.2 → 0.2.6

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.
Files changed (93) hide show
  1. package/README.md +316 -451
  2. package/dist/activity-log.d.ts +10 -3
  3. package/dist/agent-event-runtime.d.ts +0 -2
  4. package/dist/agent-event-runtime.js +0 -1
  5. package/dist/agent-prompt-guidance.d.ts +1 -6
  6. package/dist/agent-prompt-guidance.js +1 -6
  7. package/dist/broker/adapters/slack.d.ts +4 -1
  8. package/dist/broker/adapters/slack.js +16 -9
  9. package/dist/broker/client.d.ts +21 -4
  10. package/dist/broker/client.js +65 -25
  11. package/dist/broker/hibernation-activation-authority.d.ts +22 -0
  12. package/dist/broker/hibernation-activation-authority.js +51 -0
  13. package/dist/broker/hibernation-activation.d.ts +75 -0
  14. package/dist/broker/hibernation-activation.js +95 -0
  15. package/dist/broker/hibernation-command-router.d.ts +54 -0
  16. package/dist/broker/hibernation-command-router.js +124 -0
  17. package/dist/broker/hibernation-runtime-adapters.d.ts +79 -0
  18. package/dist/broker/hibernation-runtime-adapters.js +473 -0
  19. package/dist/broker/hibernation-runtime-helpers.d.ts +146 -0
  20. package/dist/broker/hibernation-runtime-helpers.js +243 -0
  21. package/dist/broker/index.d.ts +16 -1
  22. package/dist/broker/index.js +68 -10
  23. package/dist/broker/lock-conflict.d.ts +109 -0
  24. package/dist/broker/lock-conflict.js +409 -0
  25. package/dist/broker/socket-server.d.ts +26 -0
  26. package/dist/broker/socket-server.js +287 -4
  27. package/dist/broker-delivery.d.ts +1 -0
  28. package/dist/broker-delivery.js +10 -1
  29. package/dist/broker-runtime.d.ts +14 -2
  30. package/dist/broker-runtime.js +114 -3
  31. package/dist/canvases.d.ts +12 -2
  32. package/dist/canvases.js +1 -3
  33. package/dist/compaction-gate.d.ts +7 -0
  34. package/dist/compaction-gate.js +35 -0
  35. package/dist/deploy-manifest.d.ts +8 -2
  36. package/dist/deploy-manifest.js +9 -7
  37. package/dist/follower-runtime.d.ts +5 -2
  38. package/dist/follower-runtime.js +39 -5
  39. package/dist/git-metadata.d.ts +5 -9
  40. package/dist/git-metadata.js +23 -42
  41. package/dist/guardrails.js +5 -0
  42. package/dist/helpers.d.ts +77 -27
  43. package/dist/helpers.js +150 -39
  44. package/dist/hibernation-config.d.ts +13 -0
  45. package/dist/hibernation-config.js +14 -0
  46. package/dist/home-tab.d.ts +13 -2
  47. package/dist/imessage-tools.d.ts +4 -1
  48. package/dist/inbox-drain-runtime.d.ts +1 -0
  49. package/dist/inbox-drain-runtime.js +5 -1
  50. package/dist/index.js +341 -108
  51. package/dist/pinet-commands.d.ts +14 -1
  52. package/dist/pinet-commands.js +93 -22
  53. package/dist/pinet-mesh-ops.d.ts +6 -4
  54. package/dist/pinet-mesh-ops.js +9 -1
  55. package/dist/pinet-runtime-composition.d.ts +2 -1
  56. package/dist/pinet-runtime-composition.js +14 -3
  57. package/dist/pinet-session-formatting.d.ts +26 -2
  58. package/dist/pinet-session-formatting.js +2 -0
  59. package/dist/pinet-tools.d.ts +14 -0
  60. package/dist/pinet-tools.js +355 -50
  61. package/dist/prompts/broker/default.md +3 -3
  62. package/dist/prompts/broker/tmux.md +3 -3
  63. package/dist/runtime-agent-context.d.ts +3 -3
  64. package/dist/runtime-agent-context.js +12 -6
  65. package/dist/runtime-mode.js +4 -0
  66. package/dist/session-ui-runtime.js +3 -1
  67. package/dist/single-player-runtime.d.ts +1 -1
  68. package/dist/single-player-runtime.js +21 -5
  69. package/dist/slack-access.d.ts +10 -2
  70. package/dist/slack-access.js +64 -22
  71. package/dist/slack-block-kit.d.ts +44 -2
  72. package/dist/slack-block-kit.js +5 -5
  73. package/dist/slack-file-access.d.ts +12 -1
  74. package/dist/slack-file-access.js +4 -4
  75. package/dist/slack-message-context.d.ts +9 -2
  76. package/dist/slack-modals.d.ts +4 -1
  77. package/dist/slack-modals.js +5 -5
  78. package/dist/slack-pinet-runtime-adapter.d.ts +1 -0
  79. package/dist/slack-pinet-runtime-adapter.js +10 -0
  80. package/dist/slack-presence.d.ts +5 -4
  81. package/dist/slack-request-runtime.d.ts +3 -2
  82. package/dist/slack-scope-diagnostics.d.ts +27 -2
  83. package/dist/slack-socket-dedup.d.ts +8 -5
  84. package/dist/slack-tools.d.ts +10 -0
  85. package/dist/slack-tools.js +51 -64
  86. package/dist/slack-upload.d.ts +17 -1
  87. package/dist/subtree-broker-runtime.d.ts +83 -2
  88. package/dist/subtree-broker-runtime.js +774 -109
  89. package/dist/tool-output-limits.d.ts +11 -0
  90. package/dist/tool-output-limits.js +20 -0
  91. package/dist/tool-registration-runtime.d.ts +3 -0
  92. package/dist/tool-registration-runtime.js +27 -0
  93. package/package.json +8 -8
@@ -1,16 +1,24 @@
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
1
2
  import os from "node:os";
3
+ import path from "node:path";
2
4
  import { buildCompactPinetReadDetails, formatPinetReadResultCompact, formatPinetReadResultFull, } from "@pinet/pinet-core/pinet-read-formatting";
3
5
  import { normalizePinetOutputOptions, } from "@pinet/pinet-core/output-options";
4
6
  import { parseScheduledWakeupDelay, resolveScheduledWakeupFireAt, } from "@pinet/pinet-core/scheduled-wakeups";
5
7
  import { Text } from "@earendil-works/pi-tui";
6
8
  import { Type } from "@sinclair/typebox";
9
+ import { formatAgentLifecycleStatus, formatAgentLifecycleTag, formatHibernationCommandResult, sanitizeOperatorReason, sanitizeOperatorTarget, } from "@pinet/broker-core";
7
10
  import { buildAgentDisplayInfo, filterAgentsForMeshVisibility, formatAgentList, rankAgentsForRouting, } from "./helpers.js";
8
11
  import { isBroadcastChannelTarget } from "./broker/agent-messaging.js";
9
12
  import { DEFAULT_HEARTBEAT_TIMEOUT_MS } from "./broker/socket-server.js";
10
13
  import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
14
+ import { SubtreeSpawnLaunchError, SubtreeSpawnRegistrationTimeoutError, } from "./subtree-broker-runtime.js";
15
+ import { DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_MAX_OUTPUT_LINES, formatOutputSize, measureOutputLimits, } from "./tool-output-limits.js";
11
16
  import { buildPinetSessionCompactDetails, buildPinetSessionFullDetails, getPinetSessionFilename, getPinetSessionPath, summarizePinetStableId, } from "./pinet-session-formatting.js";
12
17
  const PINET_DISPATCHER_EXAMPLES = {
13
- send: [{ action: "send", args: { to: "@worker", message: "Please review PR #123" } }],
18
+ send: [
19
+ { action: "send", args: { to: "@worker", message: "Please review PR #123" } },
20
+ { action: "send", args: { to: "@worker", message: "/steer stop polling" } },
21
+ ],
14
22
  read: [
15
23
  // Routine inbox drain: defaults are `unread_only: true` + `mark_read: true`,
16
24
  // so this returns and consumes only this agent's unread rows. The compact
@@ -60,6 +68,8 @@ const PINET_DISPATCHER_EXAMPLES = {
60
68
  ],
61
69
  reload: [{ action: "reload", args: { target: "@worker" } }],
62
70
  exit: [{ action: "exit", args: { target: "@worker" } }],
71
+ hibernate: [{ action: "hibernate", args: { target: "@worker", reason: "idle overnight" } }],
72
+ wake: [{ action: "wake", args: { target: "@worker" } }],
63
73
  };
64
74
  const PINET_OUTPUT_OPTION_PARAMETERS = {
65
75
  format: Type.Optional(Type.String({ description: 'Response presentation format: "cli" (default) or "json".' })),
@@ -72,7 +82,7 @@ function getRecordString(record, key) {
72
82
  const value = record?.[key];
73
83
  return typeof value === "string" && value.trim() ? value : undefined;
74
84
  }
75
- function isRecord(value) {
85
+ function isPinetToolObject(value) {
76
86
  return typeof value === "object" && value !== null && !Array.isArray(value);
77
87
  }
78
88
  function normalizeDispatcherAction(value) {
@@ -100,6 +110,8 @@ function normalizeDispatcherAction(value) {
100
110
  "spawn",
101
111
  "reload",
102
112
  "exit",
113
+ "hibernate",
114
+ "wake",
103
115
  ];
104
116
  if (!allowed.includes(normalized)) {
105
117
  throw new Error(`Unknown Pinet action: ${normalized}`);
@@ -118,6 +130,14 @@ function getErrorMessage(error) {
118
130
  }
119
131
  }
120
132
  function classifyPinetError(message) {
133
+ if (message.includes("state=launched_unregistered")) {
134
+ return {
135
+ class: "runtime",
136
+ message,
137
+ retryable: true,
138
+ hint: "Retry spawn with retry_handle to clean up the exact recorded worker runtime before relaunching.",
139
+ };
140
+ }
121
141
  if (message.includes("requires confirmation for action")) {
122
142
  return {
123
143
  class: "state",
@@ -126,7 +146,9 @@ function classifyPinetError(message) {
126
146
  hint: 'Call slack with action "confirm_action" first in the original thread.',
127
147
  };
128
148
  }
129
- if (message.includes("is not running") || message.includes("unexpected state")) {
149
+ if (message.includes("is not running") ||
150
+ message.includes("not initialized") ||
151
+ message.includes("unexpected state")) {
130
152
  return {
131
153
  class: "state",
132
154
  message,
@@ -173,7 +195,7 @@ function buildPinetDispatcherEnvelope(status, data, errors = [], warnings = [])
173
195
  function formatPinetHelpCliText(data) {
174
196
  const actions = Array.isArray(data.actions)
175
197
  ? data.actions
176
- .map((action) => isRecord(action) && typeof action.action === "string" ? action.action : null)
198
+ .map((action) => isPinetToolObject(action) && typeof action.action === "string" ? action.action : null)
177
199
  .filter((action) => Boolean(action))
178
200
  : [];
179
201
  if (actions.length > 0) {
@@ -189,7 +211,7 @@ function formatPinetHelpCliText(data) {
189
211
  return null;
190
212
  }
191
213
  function getPinetEnvelopeCliText(envelope) {
192
- if (envelope.status === "succeeded" && isRecord(envelope.data)) {
214
+ if (envelope.status === "succeeded" && isPinetToolObject(envelope.data)) {
193
215
  const text = envelope.data.text;
194
216
  if (typeof text === "string" && text.length > 0)
195
217
  return text;
@@ -207,7 +229,7 @@ function getPinetEnvelopeCliText(envelope) {
207
229
  return `Pinet ${envelope.status}. Use args.format="json" for the compact envelope, or args.full=true for verbose details.`;
208
230
  }
209
231
  function getTolerantPinetOutputOptions(value) {
210
- if (!isRecord(value))
232
+ if (!isPinetToolObject(value))
211
233
  return { format: "cli", full: false };
212
234
  const rawFormat = value.format ?? value.f ?? value["-f"];
213
235
  const normalizedFormat = rawFormat == null ? "cli" : String(rawFormat).trim().toLowerCase();
@@ -216,7 +238,7 @@ function getTolerantPinetOutputOptions(value) {
216
238
  return { format, full: rawFull === true };
217
239
  }
218
240
  function getOptionalPinetOutputOptions(value) {
219
- if (!isRecord(value))
241
+ if (!isPinetToolObject(value))
220
242
  return { format: "cli", full: false };
221
243
  try {
222
244
  return normalizePinetOutputOptions(value);
@@ -232,28 +254,134 @@ function shouldRenderStructuredPinetEnvelope(envelope, output) {
232
254
  return false;
233
255
  if (envelope.status === "failed")
234
256
  return true;
235
- if (!isRecord(envelope.data))
257
+ if (!isPinetToolObject(envelope.data))
236
258
  return true;
237
259
  const text = envelope.data.text;
238
260
  return typeof text !== "string" || text.length === 0;
239
261
  }
240
- function wrapDispatcherEnvelope(envelope, output = { format: "cli", full: false }, expandedText, displayText) {
262
+ function wrapDispatcherEnvelope(envelope, output = { format: "cli", full: false }, expandedText, displayText, compactDetailsForSpill) {
263
+ const structuredEnvelopeText = JSON.stringify(envelope, null, output.full ? 2 : 0);
264
+ const renderStructured = shouldRenderStructuredPinetEnvelope(envelope, output);
265
+ const renderedText = renderStructured
266
+ ? structuredEnvelopeText
267
+ : getPinetEnvelopeCliText(envelope);
268
+ const data = typeof envelope.data === "object" && envelope.data !== null && !Array.isArray(envelope.data)
269
+ ? envelope.data
270
+ : null;
271
+ const action = typeof data?.action === "string" ? data.action : undefined;
272
+ const fullOutputText = output.format === "json"
273
+ ? renderedText
274
+ : `${renderedText}\n\n--- Pinet dispatcher envelope (JSON) ---\n${JSON.stringify(envelope, null, 2)}`;
275
+ const outputMeasurement = measureOutputLimits(fullOutputText);
276
+ let returnedEnvelope = envelope;
277
+ let returnedText = renderedText;
278
+ let returnedDisplayText = displayText;
279
+ if (outputMeasurement.exceedsLimit) {
280
+ const fileToken = (action ?? "result")
281
+ .trim()
282
+ .toLowerCase()
283
+ .replace(/[^a-z0-9_-]+/g, "-")
284
+ .replace(/^-+|-+$/g, "") || "result";
285
+ const dir = mkdtempSync(path.join(os.tmpdir(), "pinet-output-"));
286
+ const extension = output.format === "json" ? "json" : "txt";
287
+ const fullOutputPath = path.join(dir, `${Date.now()}-${fileToken}.${extension}`);
288
+ writeFileSync(fullOutputPath, fullOutputText, "utf8");
289
+ const originalDetails = data?.details;
290
+ let compactDetailRecord = {};
291
+ if (typeof compactDetailsForSpill === "object" &&
292
+ compactDetailsForSpill !== null &&
293
+ !Array.isArray(compactDetailsForSpill)) {
294
+ compactDetailRecord = compactDetailsForSpill;
295
+ }
296
+ else if (compactDetailsForSpill != null) {
297
+ const compactDetailValue = Array.isArray(compactDetailsForSpill)
298
+ ? { count: compactDetailsForSpill.length }
299
+ : typeof compactDetailsForSpill === "string" ||
300
+ typeof compactDetailsForSpill === "number" ||
301
+ typeof compactDetailsForSpill === "boolean"
302
+ ? compactDetailsForSpill
303
+ : String(compactDetailsForSpill);
304
+ compactDetailRecord = { compactDetails: compactDetailValue };
305
+ }
306
+ else if (typeof originalDetails === "object" &&
307
+ originalDetails !== null &&
308
+ !Array.isArray(originalDetails)) {
309
+ compactDetailRecord = Object.fromEntries(Object.entries(originalDetails)
310
+ .slice(0, PINET_COMPACT_LIST_LIMIT)
311
+ .map(([key, value]) => {
312
+ if (value == null ||
313
+ typeof value === "string" ||
314
+ typeof value === "number" ||
315
+ typeof value === "boolean") {
316
+ return [key, value];
317
+ }
318
+ if (Array.isArray(value))
319
+ return [key, { count: value.length }];
320
+ return [key, typeof value === "object" ? "{...}" : String(value)];
321
+ }));
322
+ }
323
+ const count = compactDetailRecord.laneCount ??
324
+ compactDetailRecord.count ??
325
+ compactDetailRecord.total ??
326
+ compactDetailRecord.messageCount ??
327
+ compactDetailRecord.sessionCount;
328
+ const subject = action ? `Pinet ${action}` : `Pinet ${envelope.status}`;
329
+ let summary;
330
+ if (typeof count === "number" && action === "lanes") {
331
+ summary = `Pinet lanes: ${count} tracked.`;
332
+ }
333
+ else if (typeof count === "number" && action === "agents") {
334
+ summary = `Pinet agents: ${count} visible.`;
335
+ }
336
+ else if (typeof count === "number" && action === "sessions") {
337
+ summary = `Pinet sessions: ${count} found.`;
338
+ }
339
+ else if (typeof count === "number" && action === "ports") {
340
+ summary = `Pinet port leases: ${count}.`;
341
+ }
342
+ else if (typeof count === "number" && action === "read") {
343
+ summary = `Pinet read: ${count} message${count === 1 ? "" : "s"}.`;
344
+ }
345
+ else {
346
+ summary = `${subject} output exceeded the inline limit.`;
347
+ }
348
+ const spillNotice = `[Output truncated: original was ${outputMeasurement.totalLines} line${outputMeasurement.totalLines === 1 ? "" : "s"} (${formatOutputSize(outputMeasurement.totalBytes)}), exceeding the inline limit of ${DEFAULT_MAX_OUTPUT_LINES} lines or ${formatOutputSize(DEFAULT_MAX_OUTPUT_BYTES)}. Full output saved to: ${fullOutputPath}]`;
349
+ const warning = `Pinet output exceeded ${formatOutputSize(DEFAULT_MAX_OUTPUT_BYTES)} / ${DEFAULT_MAX_OUTPUT_LINES} lines; full output saved to ${fullOutputPath}.`;
350
+ returnedEnvelope = buildPinetDispatcherEnvelope(envelope.status, {
351
+ ...(action ? { action } : {}),
352
+ text: `${summary}\n\n${spillNotice}`,
353
+ details: {
354
+ ...compactDetailRecord,
355
+ fullOutputPath,
356
+ truncation: {
357
+ truncated: true,
358
+ totalLines: outputMeasurement.totalLines,
359
+ totalBytes: outputMeasurement.totalBytes,
360
+ maxLines: DEFAULT_MAX_OUTPUT_LINES,
361
+ maxBytes: DEFAULT_MAX_OUTPUT_BYTES,
362
+ },
363
+ outputFormat: output.format,
364
+ },
365
+ }, envelope.errors, [...envelope.warnings, warning]);
366
+ returnedText = renderStructured
367
+ ? JSON.stringify(returnedEnvelope, null, output.full ? 2 : 0)
368
+ : getPinetEnvelopeCliText(returnedEnvelope);
369
+ returnedDisplayText = getPinetEnvelopeCliText(returnedEnvelope);
370
+ }
241
371
  return {
242
372
  content: [
243
373
  {
244
374
  type: "text",
245
- text: shouldRenderStructuredPinetEnvelope(envelope, output)
246
- ? JSON.stringify(envelope, null, output.full ? 2 : 0)
247
- : getPinetEnvelopeCliText(envelope),
375
+ text: returnedText,
248
376
  },
249
377
  ],
250
- details: envelope,
251
- ...(expandedText ? { expandedText } : {}),
252
- ...(displayText ? { displayText } : {}),
378
+ details: returnedEnvelope,
379
+ ...(outputMeasurement.exceedsLimit ? {} : expandedText ? { expandedText } : {}),
380
+ ...(returnedDisplayText ? { displayText: returnedDisplayText } : {}),
253
381
  };
254
382
  }
255
383
  function isPinetDispatcherEnvelope(value) {
256
- if (!isRecord(value))
384
+ if (!isPinetToolObject(value))
257
385
  return false;
258
386
  if (value.status !== "succeeded" && value.status !== "failed")
259
387
  return false;
@@ -355,13 +483,13 @@ function formatPinetEnvelopeExpandedText(envelope, expandedText) {
355
483
  else if (expandedText && expandedText.trim().length > 0) {
356
484
  lines.push(...expandedText.split("\n"));
357
485
  }
358
- else if (isRecord(envelope.data)) {
486
+ else if (isPinetToolObject(envelope.data)) {
359
487
  lines.push(`status: ${envelope.status}`);
360
488
  const action = typeof envelope.data.action === "string" ? envelope.data.action : undefined;
361
489
  if (action)
362
490
  lines.push(`action: ${action}`);
363
491
  const details = envelope.data.details;
364
- if (isRecord(details)) {
492
+ if (isPinetToolObject(details)) {
365
493
  lines.push("details:");
366
494
  lines.push(...formatPrimitiveDetails(details).map((line) => ` ${line}`));
367
495
  }
@@ -664,36 +792,59 @@ function runPinetSnoozeAction(params, deps, toolName) {
664
792
  }
665
793
  function runPinetSpawnAction(params, deps, toolName, output) {
666
794
  return (async () => {
667
- const task = getMaybeString(params, "task");
668
- const repo = getMaybeString(params, "repo");
669
- const role = getMaybeString(params, "role") ?? "subworker";
670
- const laneId = getMaybeString(params, "lane_id");
671
- deps.requireToolPolicy(toolName, undefined, `task=${task ?? ""} | repo=${repo ?? ""} | role=${role} | lane_id=${laneId ?? ""}`);
672
795
  if (!deps.pinetEnabled()) {
673
796
  throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
674
797
  }
675
798
  if (deps.brokerRole() !== "follower") {
676
799
  throw new Error("spawn is worker-only; the broker should launch top-level followers, not own subtrees.");
677
800
  }
801
+ if (!deps.spawnSubtreeWorker) {
802
+ throw new Error("subtree worker launcher is unavailable in this runtime. Start a worker subtree with /pinet subtree start and retry.");
803
+ }
804
+ const task = getMaybeString(params, "task");
805
+ const repo = getMaybeString(params, "repo");
806
+ const role = getMaybeString(params, "role") ?? "subworker";
807
+ const laneId = getMaybeString(params, "lane_id");
808
+ const retryHandleValue = params.retry_handle;
809
+ let cleanupHandle;
810
+ if (retryHandleValue !== undefined) {
811
+ if (!isPinetToolObject(retryHandleValue)) {
812
+ throw new Error("retry_handle must be a subtree spawn launch handle");
813
+ }
814
+ const launchId = getRecordString(retryHandleValue, "launchId");
815
+ const tmuxSessionName = getRecordString(retryHandleValue, "tmuxSessionName");
816
+ const socketPath = getRecordString(retryHandleValue, "socketPath");
817
+ if (!launchId ||
818
+ !tmuxSessionName ||
819
+ !socketPath ||
820
+ retryHandleValue.state !== "launched_unregistered") {
821
+ throw new Error('retry_handle requires launchId, tmuxSessionName, socketPath, and state="launched_unregistered"');
822
+ }
823
+ cleanupHandle = {
824
+ launchId,
825
+ tmuxSessionName,
826
+ socketPath,
827
+ state: "launched_unregistered",
828
+ };
829
+ }
830
+ deps.requireToolPolicy(toolName, undefined, `task=${task ?? ""} | repo=${repo ?? ""} | role=${role} | lane_id=${laneId ?? ""}`);
678
831
  if (!task)
679
832
  throw new Error("spawn requires task");
680
833
  if (!repo)
681
834
  throw new Error("spawn requires repo");
682
- if (!deps.spawnSubtreeWorker) {
683
- throw new Error("subtree worker launcher is unavailable in this runtime. Start a worker subtree with /pinet subtree start and retry.");
684
- }
685
835
  const result = await deps.spawnSubtreeWorker({
686
836
  task,
687
837
  repo,
688
838
  role,
689
839
  ...(laneId ? { laneId } : {}),
840
+ ...(cleanupHandle ? { cleanupHandle } : {}),
690
841
  });
691
842
  return {
692
843
  content: [
693
844
  {
694
845
  type: "text",
695
846
  text: output.full
696
- ? `Pinet subtree worker started: ${result.agentName} (${result.agentId}) in tmux session ${result.sessionName}. Task message ${result.messageId} delivered. Monitor: ${result.monitorCommand}`
847
+ ? `Pinet subtree worker started: ${result.agentName} (${result.agentId}) using ${result.runtimeKind} runtime ${result.sessionName}. Task message ${result.messageId} delivered. Monitor: ${result.monitorCommand}`
697
848
  : `Pinet subtree worker started: ${result.agentName} (${result.agentId}). Task message ${result.messageId} delivered.`,
698
849
  },
699
850
  ],
@@ -728,6 +879,49 @@ function runPinetRemoteControlAction(params, deps, toolName, command) {
728
879
  };
729
880
  })();
730
881
  }
882
+ function runPinetHibernationCommandAction(target, reason, deps, toolName, command) {
883
+ return (async () => {
884
+ if (!target) {
885
+ throw new Error("target is required");
886
+ }
887
+ // Sanitize the operator-authored target/reason before they enter the policy
888
+ // context, which can surface verbatim in confirmation prompts and error
889
+ // text. `sanitizeOperatorTarget` fingerprints anything that is not a plain
890
+ // broker-safe identifier slug — a stable-id/session/cwd target such as
891
+ // `host:session:<ref>` (which embeds the session-resume identity), a private
892
+ // worktree path, or a `KEY=secret` shape — to an opaque `target:#<hash>`
893
+ // rather than echoing it. The reason is path/secret-redacted. The raw
894
+ // `target` still flows to the broker runner, which resolves it server-side
895
+ // and redacts any unknown-target echo.
896
+ const safeTarget = sanitizeOperatorTarget(target);
897
+ const safeReason = sanitizeOperatorReason(reason) ?? "";
898
+ deps.requireToolPolicy(toolName, undefined, `command=${command} | target=${safeTarget} | reason=${safeReason}`);
899
+ if (!deps.pinetEnabled()) {
900
+ throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
901
+ }
902
+ const runner = command === "hibernate" ? deps.runHibernateCommand : deps.runWakeCommand;
903
+ if (!runner) {
904
+ throw new Error(`pinet ${command} is broker-managed. Connect this session as the broker (/pinet start) to hibernate or wake workers.`);
905
+ }
906
+ const result = await runner({ target, reason });
907
+ const text = formatHibernationCommandResult(result);
908
+ return {
909
+ content: [{ type: "text", text }],
910
+ details: result,
911
+ compactDetails: {
912
+ command: result.command,
913
+ agentId: result.agentId,
914
+ outcome: result.outcome,
915
+ reason: result.reason,
916
+ state: result.state,
917
+ ...(result.runtimeGeneration != null
918
+ ? { runtimeGeneration: result.runtimeGeneration }
919
+ : {}),
920
+ },
921
+ fullDetails: result,
922
+ };
923
+ })();
924
+ }
731
925
  function runPinetScheduleAction(params, deps, toolName, output) {
732
926
  return (async () => {
733
927
  const delay = typeof params.delay === "string" ? params.delay : undefined;
@@ -995,7 +1189,7 @@ function getMaybeMetadata(params) {
995
1189
  const value = params.metadata;
996
1190
  if (value === null)
997
1191
  return null;
998
- return isRecord(value) ? value : undefined;
1192
+ return isPinetToolObject(value) ? value : undefined;
999
1193
  }
1000
1194
  function buildCompactPortLeaseDetails(leases) {
1001
1195
  const shownLeases = leases.slice(0, PINET_COMPACT_LIST_LIMIT);
@@ -1185,7 +1379,12 @@ function runPinetPortsAction(params, deps, toolName, output) {
1185
1379
  function runPinetLanesAction(params, deps, toolName, output) {
1186
1380
  return (async () => {
1187
1381
  const op = getMaybeString(params, "op")?.toLowerCase() ?? "list";
1188
- deps.requireToolPolicy(toolName, undefined, `op=${op} | format=${output.format}`);
1382
+ const policyContext = `op=${op} | format=${output.format}`;
1383
+ const threadTs = getMaybeString(params, "thread_ts");
1384
+ deps.requireToolPolicy(toolName, threadTs, policyContext);
1385
+ if (op === "upsert" || op === "participant") {
1386
+ deps.requireToolPolicy(`${toolName}:write`, threadTs, policyContext);
1387
+ }
1189
1388
  if (!deps.pinetEnabled()) {
1190
1389
  throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
1191
1390
  }
@@ -1297,6 +1496,32 @@ function runPinetLanesAction(params, deps, toolName, output) {
1297
1496
  throw new Error("lanes op must be list, upsert, or participant");
1298
1497
  })();
1299
1498
  }
1499
+ /**
1500
+ * Render the operator-safe hibernation lifecycle section appended to the
1501
+ * existing `agents`/`sessions` read output. Compact output uses a scannable
1502
+ * per-agent tag (state · gen · queue · checkpoint · refusal/quarantine); full
1503
+ * output uses the complete redacted status block. Returns "" when there is
1504
+ * nothing hibernation-relevant to show, so ordinary output is unchanged.
1505
+ */
1506
+ function formatHibernationLifecycleSection(statuses, full) {
1507
+ if (statuses.length === 0)
1508
+ return "";
1509
+ const body = full
1510
+ ? statuses.map(formatAgentLifecycleStatus).join("\n\n")
1511
+ : statuses
1512
+ .map((status) => ` ${status.agentId}: ${formatAgentLifecycleTag(status)}`)
1513
+ .join("\n");
1514
+ return `\n\nHibernation lifecycle:\n${body}`;
1515
+ }
1516
+ function buildCompactLifecycleDetails(statuses) {
1517
+ return statuses.map((status) => ({
1518
+ agentId: status.agentId,
1519
+ state: status.state,
1520
+ quarantined: status.quarantined,
1521
+ tag: formatAgentLifecycleTag(status),
1522
+ ...(status.wake.queued ? { wakeQueuePosition: status.wake.position } : {}),
1523
+ }));
1524
+ }
1300
1525
  function runPinetAgentsAction(params, deps, toolName, output) {
1301
1526
  return (async () => {
1302
1527
  const scope = params.scope === "children" || params.scope === "subtree" || params.scope === "all"
@@ -1361,14 +1586,26 @@ function runPinetAgentsAction(params, deps, toolName, output) {
1361
1586
  recentDisconnectWindowMs: recentGhostWindowMs,
1362
1587
  }).map(toDisplay);
1363
1588
  const agents = rankAgentsForRouting(visibleAgents, hint);
1589
+ // Read-only hibernation lifecycle projection wired into the existing agents
1590
+ // read path. Compact output shows a scannable per-agent tag; full/JSON
1591
+ // exposes the complete redacted lifecycle structure. Broker-only (the dep
1592
+ // is undefined on followers) and sanitized by construction. Called with NO
1593
+ // id filter so durable hibernated/quarantined runtimes — which are
1594
+ // disconnected and therefore absent from the visible roster — are still
1595
+ // surfaced (this is exactly when operators need lifecycle visibility).
1596
+ const lifecycle = deps.getAgentLifecycleProjection ? deps.getAgentLifecycleProjection() : [];
1364
1597
  const header = hasHint && output.full ? `${buildPinetAgentsHintText(hint)}\n\n` : "";
1365
- const text = `${header}${output.full ? formatAgentList(agents, os.homedir()) : formatCompactAgentList(agents, hint)}`;
1598
+ const text = `${header}${output.full ? formatAgentList(agents, os.homedir()) : formatCompactAgentList(agents, hint)}${formatHibernationLifecycleSection(lifecycle, output.full)}`;
1599
+ const expandedText = `${formatPinetAgentsExpanded(agents)}${formatHibernationLifecycleSection(lifecycle, true)}`;
1366
1600
  return {
1367
1601
  content: [{ type: "text", text }],
1368
- details: { agents, hint },
1369
- compactDetails: buildCompactAgentDetails(agents, hint),
1370
- fullDetails: { agents, hint },
1371
- expandedText: formatPinetAgentsExpanded(agents),
1602
+ details: { agents, hint, ...(lifecycle.length > 0 ? { lifecycle } : {}) },
1603
+ compactDetails: {
1604
+ ...buildCompactAgentDetails(agents, hint),
1605
+ ...(lifecycle.length > 0 ? { lifecycle: buildCompactLifecycleDetails(lifecycle) } : {}),
1606
+ },
1607
+ fullDetails: { agents, hint, ...(lifecycle.length > 0 ? { lifecycle } : {}) },
1608
+ expandedText,
1372
1609
  };
1373
1610
  })();
1374
1611
  }
@@ -1385,6 +1622,9 @@ function buildPinetSessionSearchOptions(params) {
1385
1622
  ...(getMaybeString(params, "worktree_path")
1386
1623
  ? { worktreePath: getMaybeString(params, "worktree_path") }
1387
1624
  : {}),
1625
+ ...(getMaybeString(params, "runtime_locator")
1626
+ ? { runtimeLocator: getMaybeString(params, "runtime_locator") }
1627
+ : {}),
1388
1628
  ...(getMaybeString(params, "tmux_session")
1389
1629
  ? { tmuxSession: getMaybeString(params, "tmux_session") }
1390
1630
  : {}),
@@ -1400,6 +1640,7 @@ function formatPinetSessionSearchHeader(sessions, options) {
1400
1640
  options.threadId ? `thread_id=${options.threadId}` : null,
1401
1641
  options.repo ? `repo=${options.repo}` : null,
1402
1642
  options.worktreePath ? `worktree=${options.worktreePath}` : null,
1643
+ options.runtimeLocator ? `runtime=${options.runtimeLocator}` : null,
1403
1644
  options.tmuxSession ? `tmux=${options.tmuxSession}` : null,
1404
1645
  options.since ? `since=${options.since}` : null,
1405
1646
  options.until ? `until=${options.until}` : null,
@@ -1413,7 +1654,7 @@ function formatPinetSessionLine(session, full) {
1413
1654
  const where = [
1414
1655
  session.repo ?? null,
1415
1656
  session.branch ? `branch=${session.branch}` : null,
1416
- session.tmuxSession ? `tmux=${session.tmuxSession}` : null,
1657
+ session.runtimeLocator ? `${session.runtimeKind ?? "runtime"}=${session.runtimeLocator}` : null,
1417
1658
  ].filter((item) => Boolean(item));
1418
1659
  const threads = session.relatedThreadIds.slice(0, full ? 10 : 3).join(", ");
1419
1660
  const suffix = session.relatedThreadIds.length > (full ? 10 : 3) ? " …" : "";
@@ -1466,24 +1707,37 @@ function runPinetSessionsAction(params, deps, toolName, output) {
1466
1707
  throw new Error("sessions op must be search");
1467
1708
  }
1468
1709
  const options = buildPinetSessionSearchOptions(params);
1469
- deps.requireToolPolicy(toolName, undefined, `agent_name=${options.agentName ?? ""} | agent_id=${options.agentId ?? ""} | thread_id=${options.threadId ?? ""} | repo=${options.repo ?? ""} | worktree_path=${options.worktreePath ?? ""} | tmux_session=${options.tmuxSession ?? ""} | since=${options.since ?? ""} | until=${options.until ?? ""} | limit=${options.limit ?? ""} | format=${output.format} | full=${output.full}`);
1710
+ deps.requireToolPolicy(toolName, undefined, `agent_name=${options.agentName ?? ""} | agent_id=${options.agentId ?? ""} | thread_id=${options.threadId ?? ""} | repo=${options.repo ?? ""} | worktree_path=${options.worktreePath ?? ""} | runtime_locator=${options.runtimeLocator ?? ""} | tmux_session=${options.tmuxSession ?? ""} | since=${options.since ?? ""} | until=${options.until ?? ""} | limit=${options.limit ?? ""} | format=${output.format} | full=${output.full}`);
1470
1711
  if (!deps.pinetEnabled()) {
1471
1712
  throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
1472
1713
  }
1473
1714
  const sessions = await deps.searchPinetSessions(options);
1474
- const text = formatPinetSessionSearch(sessions, options, output.full);
1715
+ // Read-only lifecycle projection for the session's live agents, wired into
1716
+ // the existing sessions read path. Broker-only and sanitized by
1717
+ // construction; empty for historical/disconnected agents.
1718
+ const lifecycle = deps.getAgentLifecycleProjection && sessions.length > 0
1719
+ ? deps.getAgentLifecycleProjection(sessions.map((session) => session.agentId))
1720
+ : [];
1721
+ const text = `${formatPinetSessionSearch(sessions, options, output.full)}${formatHibernationLifecycleSection(lifecycle, output.full)}`;
1475
1722
  return {
1476
1723
  content: [{ type: "text", text }],
1477
- details: { count: sessions.length, sessions, options },
1724
+ details: {
1725
+ count: sessions.length,
1726
+ sessions,
1727
+ options,
1728
+ ...(lifecycle.length > 0 ? { lifecycle } : {}),
1729
+ },
1478
1730
  compactDetails: {
1479
1731
  count: sessions.length,
1480
1732
  options,
1481
1733
  sessions: sessions.map(buildPinetSessionCompactDetails),
1734
+ ...(lifecycle.length > 0 ? { lifecycle: buildCompactLifecycleDetails(lifecycle) } : {}),
1482
1735
  },
1483
1736
  fullDetails: {
1484
1737
  count: sessions.length,
1485
1738
  options,
1486
1739
  sessions: sessions.map(buildPinetSessionFullDetails),
1740
+ ...(lifecycle.length > 0 ? { lifecycle } : {}),
1487
1741
  },
1488
1742
  expandedText: text,
1489
1743
  };
@@ -1496,12 +1750,14 @@ export function registerPinetTools(pi, deps) {
1496
1750
  }
1497
1751
  registerAction({
1498
1752
  name: "send",
1499
- description: "Send a message to a connected Pinet agent or broker-only broadcast channel.",
1753
+ description: "Send a queued message to a connected Pinet agent or broker-only broadcast channel. Prefix the body with /steer to interrupt a busy recipient with explicit steering.",
1500
1754
  parameters: Type.Object({
1501
1755
  to: Type.String({
1502
1756
  description: "Target agent name/ID, or a broker-only broadcast channel like #extensions. Avoid #all for repo-specific issue/policy announcements.",
1503
1757
  }),
1504
- message: Type.String({ description: "Message body" }),
1758
+ message: Type.String({
1759
+ description: "Message body. Use /steer <instruction> only when you explicitly need to steer a busy recipient immediately; otherwise messages remain queued.",
1760
+ }),
1505
1761
  transfer_thread_id: Type.Optional(Type.String({
1506
1762
  description: "Broker-only: transfer ownership of this existing Slack thread to the direct recipient after delivery (for example a Slack thread_ts).",
1507
1763
  })),
@@ -1545,12 +1801,18 @@ export function registerPinetTools(pi, deps) {
1545
1801
  });
1546
1802
  registerAction({
1547
1803
  name: "spawn",
1548
- description: "Worker-only: launch a tmux-backed child worker into this worker's active subtree broker and deliver the task over Pinet.",
1804
+ description: "Worker-only: launch a child worker with the configured runtime into this worker's active subtree broker and deliver the task over Pinet.",
1549
1805
  parameters: Type.Object({
1550
1806
  task: Type.String({ description: "Scoped task prompt for the child worker" }),
1551
1807
  repo: Type.String({ description: "Repo/workspace scope for the child worker" }),
1552
1808
  role: Type.Optional(Type.String({ description: "Child subtree role, e.g. reviewer" })),
1553
1809
  lane_id: Type.Optional(Type.String({ description: "Optional durable Pinet lane id" })),
1810
+ retry_handle: Type.Optional(Type.Object({
1811
+ launchId: Type.String(),
1812
+ tmuxSessionName: Type.String(),
1813
+ socketPath: Type.String(),
1814
+ state: Type.Literal("launched_unregistered"),
1815
+ })),
1554
1816
  ...PINET_OUTPUT_OPTION_PARAMETERS,
1555
1817
  }),
1556
1818
  execute: (_id, params, output) => runPinetSpawnAction(params, deps, "pinet:spawn", output),
@@ -1573,6 +1835,42 @@ export function registerPinetTools(pi, deps) {
1573
1835
  }),
1574
1836
  execute: (_id, params, _output) => runPinetRemoteControlAction(params, deps, "pinet:exit", "/exit"),
1575
1837
  });
1838
+ registerAction({
1839
+ name: "hibernate",
1840
+ description: "Broker-managed: checkpoint and hibernate an idle broker-managed worker. Default-off; returns a safe refusal when hibernation is disabled or observe-only.",
1841
+ parameters: Type.Object({
1842
+ target: Type.String({
1843
+ description: "Target worker: display name, agent id, or stable id",
1844
+ }),
1845
+ reason: Type.Optional(Type.String({ description: "Optional operator reason recorded in lifecycle telemetry" })),
1846
+ ...PINET_OUTPUT_OPTION_PARAMETERS,
1847
+ }),
1848
+ execute: (_id, params, _output) => {
1849
+ const target = typeof params.target === "string" ? params.target.trim() : "";
1850
+ const reason = typeof params.reason === "string" && params.reason.trim().length > 0
1851
+ ? params.reason.trim()
1852
+ : undefined;
1853
+ return runPinetHibernationCommandAction(target, reason, deps, "pinet:hibernate", "hibernate");
1854
+ },
1855
+ });
1856
+ registerAction({
1857
+ name: "wake",
1858
+ description: "Broker-managed: wake a hibernated worker under a fresh fenced runtime and drain its queued messages in order.",
1859
+ parameters: Type.Object({
1860
+ target: Type.String({
1861
+ description: "Target worker: display name, agent id, or stable id",
1862
+ }),
1863
+ reason: Type.Optional(Type.String({ description: "Optional operator reason recorded in lifecycle telemetry" })),
1864
+ ...PINET_OUTPUT_OPTION_PARAMETERS,
1865
+ }),
1866
+ execute: (_id, params, _output) => {
1867
+ const target = typeof params.target === "string" ? params.target.trim() : "";
1868
+ const reason = typeof params.reason === "string" && params.reason.trim().length > 0
1869
+ ? params.reason.trim()
1870
+ : undefined;
1871
+ return runPinetHibernationCommandAction(target, reason, deps, "pinet:wake", "wake");
1872
+ },
1873
+ });
1576
1874
  registerAction({
1577
1875
  name: "schedule",
1578
1876
  description: "Schedule a future wake-up for this Pinet agent.",
@@ -1606,7 +1904,7 @@ export function registerPinetTools(pi, deps) {
1606
1904
  });
1607
1905
  registerAction({
1608
1906
  name: "sessions",
1609
- description: "Search live and historical Pinet worker Pi sessions by display name, agent id, thread, repo/worktree, tmux session, or time range.",
1907
+ description: "Search live and historical Pinet worker Pi sessions by display name, agent id, thread, repo/worktree, runtime locator (tmux session or herdr pane), or time range.",
1610
1908
  parameters: Type.Object({
1611
1909
  op: Type.Optional(Type.String({ description: "Operation: search (default)" })),
1612
1910
  agent_name: Type.Optional(Type.String({ description: "Worker display name, e.g. Frozen Hazel Whale" })),
@@ -1615,6 +1913,7 @@ export function registerPinetTools(pi, deps) {
1615
1913
  thread_id: Type.Optional(Type.String({ description: "Related Pinet/Slack/A2A thread id" })),
1616
1914
  repo: Type.Optional(Type.String({ description: "Repo name or path fragment" })),
1617
1915
  worktree_path: Type.Optional(Type.String({ description: "Worktree/cwd path fragment" })),
1916
+ runtime_locator: Type.Optional(Type.String({ description: "Broker-managed runtime locator (tmux session or herdr pane)" })),
1618
1917
  tmux_session: Type.Optional(Type.String({ description: "Broker-managed tmux session name" })),
1619
1918
  since: Type.Optional(Type.String({ description: "Only sessions active after this ISO time" })),
1620
1919
  until: Type.Optional(Type.String({ description: "Only sessions started before this ISO time" })),
@@ -1645,9 +1944,10 @@ export function registerPinetTools(pi, deps) {
1645
1944
  });
1646
1945
  registerAction({
1647
1946
  name: "lanes",
1648
- description: "List or update durable Pinet lane metadata for PM-mode and complex coordination visibility.",
1947
+ description: "List or update durable Pinet lane metadata for PM-mode and complex coordination visibility. Upsert and participant operations additionally require guardrail policy pinet:lanes:write.",
1649
1948
  parameters: Type.Object({
1650
1949
  op: Type.Optional(Type.String({ description: "Operation: list, upsert, or participant" })),
1950
+ thread_ts: Type.Optional(Type.String({ description: "Slack thread timestamp for confirmation-required mutations" })),
1651
1951
  lane_id: Type.Optional(Type.String({ description: "Stable lane id, e.g. issue-688" })),
1652
1952
  name: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Human-readable lane name" })),
1653
1953
  task: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Short lane task description" })),
@@ -1694,10 +1994,11 @@ export function registerPinetTools(pi, deps) {
1694
1994
  name: "pinet",
1695
1995
  label: "Pinet Dispatcher",
1696
1996
  description: "Dispatch Pinet operations by action with compact help and schema discovery.",
1697
- promptSnippet: 'Use this compact dispatcher for Pinet actions: send, read, free, snooze, schedule, agents, sessions, lanes, ports, reload, exit, spawn, and help. Use /pinet start, /pinet follow, /pinet unfollow, and /pinet subtree start for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" for the compact envelope or args.full=true for verbose/debug detail.',
1997
+ promptSnippet: 'Use this compact dispatcher for Pinet actions: send, read, free, snooze, schedule, agents, sessions, lanes, ports, reload, exit, hibernate, wake, spawn, and help. Use /pinet start, /pinet follow, /pinet unfollow, and /pinet subtree start for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" for the compact envelope or args.full=true for verbose/debug detail.',
1998
+ promptGuidelines: deps.promptGuidelines,
1698
1999
  parameters: Type.Object({
1699
2000
  action: Type.String({
1700
- description: "Action name: help, send, read, free, snooze, schedule, agents, sessions, lanes, ports, reload, or exit. Also supports spawn for launching worker-owned subtree children.",
2001
+ description: "Action name: help, send, read, free, snooze, schedule, agents, sessions, lanes, ports, reload, exit, hibernate (broker-managed checkpoint+hibernate), or wake (broker-managed fenced wake). Also supports spawn for launching worker-owned subtree children.",
1701
2002
  }),
1702
2003
  args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
1703
2004
  description: 'Action arguments. Add format="cli"|"json" (or f/"-f") for presentation, and full=true (or "--full": true) only for verbose/debug details. Default cli and non-full json keep data.details compact.',
@@ -1706,7 +2007,7 @@ export function registerPinetTools(pi, deps) {
1706
2007
  renderCall(args, theme) {
1707
2008
  const action = typeof args.action === "string" && args.action.trim() ? args.action.trim() : "?";
1708
2009
  let suffix = "";
1709
- if (isRecord(args.args)) {
2010
+ if (isPinetToolObject(args.args)) {
1710
2011
  const topic = typeof args.args.topic === "string" ? args.args.topic.trim() : "";
1711
2012
  const op = typeof args.args.op === "string" ? args.args.op.trim() : "";
1712
2013
  if (topic)
@@ -1748,7 +2049,7 @@ export function registerPinetTools(pi, deps) {
1748
2049
  ]), getOptionalPinetOutputOptions(params.args));
1749
2050
  }
1750
2051
  if (normalizedAction === "help") {
1751
- const args = isRecord(params.args) ? params.args : {};
2052
+ const args = isPinetToolObject(params.args) ? params.args : {};
1752
2053
  let output;
1753
2054
  try {
1754
2055
  output = normalizePinetOutputOptions(args);
@@ -1776,7 +2077,7 @@ export function registerPinetTools(pi, deps) {
1776
2077
  },
1777
2078
  ]), getOptionalPinetOutputOptions(params.args));
1778
2079
  }
1779
- if (!isRecord(params.args)) {
2080
+ if (!isPinetToolObject(params.args)) {
1780
2081
  return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [
1781
2082
  {
1782
2083
  class: "input",
@@ -1810,14 +2111,18 @@ export function registerPinetTools(pi, deps) {
1810
2111
  action: definition.name,
1811
2112
  text: output.format === "json" ? (result.machineText ?? displayText) : displayText,
1812
2113
  details: selectPinetResultDetails(result, output),
1813
- }), output, expandedText, displayText);
2114
+ }), output, expandedText, displayText, result.compactDetails);
1814
2115
  }
1815
2116
  catch (error) {
1816
2117
  if (error instanceof Error && error.name === "AbortError") {
1817
2118
  throw error;
1818
2119
  }
1819
2120
  const message = getErrorMessage(error);
1820
- return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [classifyPinetError(message)]), output);
2121
+ const launchHandle = error instanceof SubtreeSpawnRegistrationTimeoutError ||
2122
+ error instanceof SubtreeSpawnLaunchError
2123
+ ? error.handle
2124
+ : null;
2125
+ return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", launchHandle ? { action: definition.name, details: { launchHandle } } : null, [classifyPinetError(message)]), output);
1821
2126
  }
1822
2127
  },
1823
2128
  });