@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
@@ -79,6 +79,16 @@ export function createSlackPinetRuntimeAdapterFactory(deps) {
79
79
  onAppHomeOpened: async ({ userId }) => {
80
80
  await deps.onAppHomeOpened(userId, ctx);
81
81
  },
82
+ onSocketOpen: () => deps.setExtStatus(ctx, "ok"),
83
+ onSocketReconnectScheduled: () => deps.setExtStatus(ctx, "reconnecting"),
84
+ onSocketError: (message, source) => {
85
+ if (source === "connection") {
86
+ deps.setExtStatus(ctx, "error");
87
+ }
88
+ else {
89
+ ctx.ui.notify(`Slack event failed: ${message}`, "error");
90
+ }
91
+ },
82
92
  onSlashCommand: deps.onSlashCommand
83
93
  ? (event) => deps.onSlashCommand?.(event, ctx) ?? null
84
94
  : undefined,
@@ -21,11 +21,12 @@ export interface SlackPresenceSnapshot {
21
21
  lastActivity?: number;
22
22
  online?: boolean;
23
23
  }
24
+ export type SlackPresenceTimestampValue = string | number | null | undefined;
24
25
  export interface SlackDndInfoLike {
25
- dnd_enabled?: unknown;
26
- next_dnd_end_ts?: unknown;
27
- snooze_enabled?: unknown;
28
- snooze_endtime?: unknown;
26
+ dnd_enabled?: boolean | null;
27
+ next_dnd_end_ts?: SlackPresenceTimestampValue;
28
+ snooze_enabled?: boolean | null;
29
+ snooze_endtime?: SlackPresenceTimestampValue;
29
30
  }
30
31
  export declare function stripSlackUserReference(value: string): string;
31
32
  export declare function isSlackUserId(value: string): boolean;
@@ -1,5 +1,6 @@
1
- import { type SlackResult } from "./slack-api.js";
2
- export type SlackRequestCall = (method: string, token: string, body?: Record<string, unknown>) => Promise<SlackResult>;
1
+ import { callSlackApi, type SlackResult } from "./slack-api.js";
2
+ export type SlackRequestBody = NonNullable<Parameters<typeof callSlackApi>[2]>;
3
+ export type SlackRequestCall = (method: string, token: string, body?: SlackRequestBody) => Promise<SlackResult>;
3
4
  export interface SlackRequestRuntime {
4
5
  slack: SlackRequestCall;
5
6
  reset: () => void;
@@ -1,7 +1,31 @@
1
+ export type SlackScopeDiagnosticsSurface = "files" | "bookmarks" | "pins";
2
+ export type SlackScopeProbeBody = Record<string, unknown>;
3
+ interface SlackFileInfoScopeProbeBody extends SlackScopeProbeBody {
4
+ file: string;
5
+ }
6
+ interface SlackCompleteUploadScopeProbeFile {
7
+ id: string;
8
+ title: string;
9
+ }
10
+ interface SlackCompleteUploadScopeProbeBody extends SlackScopeProbeBody {
11
+ files: SlackCompleteUploadScopeProbeFile[];
12
+ channel_id: string;
13
+ }
14
+ interface SlackChannelScopeProbeBody extends SlackScopeProbeBody {
15
+ channel_id: string;
16
+ }
17
+ interface SlackBookmarkRemoveScopeProbeBody extends SlackChannelScopeProbeBody {
18
+ bookmark_id: string;
19
+ }
20
+ interface SlackPinnedItemScopeProbeBody extends SlackScopeProbeBody {
21
+ channel: string;
22
+ timestamp?: string;
23
+ }
24
+ export type SlackScopeProbeRequestBody = SlackFileInfoScopeProbeBody | SlackCompleteUploadScopeProbeBody | SlackChannelScopeProbeBody | SlackBookmarkRemoveScopeProbeBody | SlackPinnedItemScopeProbeBody;
1
25
  export type SlackScopeDiagnosticsStatus = "not_checked" | "pending" | "healthy" | "drift" | "unavailable";
2
26
  export interface SlackScopeProbeResult {
3
27
  key: string;
4
- surface: "files" | "bookmarks" | "pins";
28
+ surface: SlackScopeDiagnosticsSurface;
5
29
  method: string;
6
30
  expectedScopes: string[];
7
31
  status: "ok" | "missing" | "unavailable";
@@ -14,7 +38,7 @@ export interface SlackScopeDiagnostics {
14
38
  status: SlackScopeDiagnosticsStatus;
15
39
  checkedAt: string | null;
16
40
  summary: string;
17
- surfaces: Array<"files" | "bookmarks" | "pins">;
41
+ surfaces: SlackScopeDiagnosticsSurface[];
18
42
  missingScopes: string[];
19
43
  results: SlackScopeProbeResult[];
20
44
  error?: string;
@@ -29,3 +53,4 @@ export declare function createPendingSlackScopeDiagnostics(): SlackScopeDiagnost
29
53
  export declare function detectSlackScopeDiagnostics(options: DetectSlackScopeDiagnosticsOptions): Promise<SlackScopeDiagnostics>;
30
54
  export declare function formatSlackScopeDiagnosticsStatus(diagnostics: SlackScopeDiagnostics): string;
31
55
  export declare function buildSlackScopeDriftWarning(diagnostics: SlackScopeDiagnostics): string | null;
56
+ export {};
@@ -1,7 +1,10 @@
1
1
  export declare const SLACK_SOCKET_DELIVERY_DEDUP_TTL_MS: number;
2
2
  export declare const SLACK_SOCKET_DELIVERY_DEDUP_MAX_SIZE = 10000;
3
- export declare function extractSlackEventDedupKey(evt: Record<string, unknown>): string | null;
4
- export declare function extractSlackBlockActionDedupKey(payload: Record<string, unknown>): string | null;
5
- export declare function extractSlackViewSubmissionDedupKey(payload: Record<string, unknown>): string | null;
6
- export declare function extractSlackInteractiveDedupKey(payload: Record<string, unknown>): string | null;
7
- export declare function extractSlackSocketDedupKey(frame: Record<string, unknown>): string | null;
3
+ export type SlackDedupEventPayload = Record<string, unknown>;
4
+ export type SlackInteractiveDedupPayload = Record<string, unknown>;
5
+ export type SlackSocketDedupFrame = Record<string, unknown>;
6
+ export declare function extractSlackEventDedupKey(evt: SlackDedupEventPayload): string | null;
7
+ export declare function extractSlackBlockActionDedupKey(payload: SlackInteractiveDedupPayload): string | null;
8
+ export declare function extractSlackViewSubmissionDedupKey(payload: SlackInteractiveDedupPayload): string | null;
9
+ export declare function extractSlackInteractiveDedupKey(payload: SlackInteractiveDedupPayload): string | null;
10
+ export declare function extractSlackSocketDedupKey(frame: SlackSocketDedupFrame): string | null;
@@ -26,10 +26,19 @@ export interface SlackPinetDeliveryResult {
26
26
  source: string;
27
27
  }
28
28
  export interface SlackPinetDeliveryPort {
29
+ /**
30
+ * True when Pinet is configured/enabled for this session, regardless of
31
+ * live broker connectivity. When true, threaded Slack replies MUST route
32
+ * through the broker; direct Slack fallback is refused even if the broker
33
+ * is momentarily unavailable, to preserve broker-enforced thread
34
+ * ownership (see gugu91/pinet#855).
35
+ */
36
+ isEnabled: () => boolean;
29
37
  isAvailable: () => boolean;
30
38
  sendSlackMessage: (input: SlackPinetDeliveryInput) => Promise<SlackPinetDeliveryResult>;
31
39
  }
32
40
  export interface RegisterSlackToolsDeps {
41
+ additionalSendPromptGuidelines?: string[];
33
42
  getBotToken: () => string;
34
43
  getDefaultChannel: () => string | undefined;
35
44
  getSecurityPrompt: () => string;
@@ -40,6 +49,7 @@ export interface RegisterSlackToolsDeps {
40
49
  getAgentOwnerToken: () => string;
41
50
  getLastDmChannel: () => string | null;
42
51
  updateBadge: () => void;
52
+ markSubtreeInboxIdsDelivered: (inboxIds: number[]) => void;
43
53
  resolveUser: (userId: string) => Promise<string>;
44
54
  threadContext: SlackToolsThreadContextPort;
45
55
  resolveChannel: (nameOrId: string) => Promise<string>;
@@ -1,5 +1,6 @@
1
1
  import os from "node:os";
2
2
  import { Type } from "@sinclair/typebox";
3
+ import { getSubtreeInboxIds } from "./broker-delivery.js";
3
4
  import { buildSlackCanvasCreateRequest, buildSlackCanvasEditRequest, buildSlackCanvasSectionsLookupRequest, extractSlackCanvasCommentsPage, extractSlackChannelCanvasId, normalizeSlackCanvasCommentsLimit, normalizeSlackCanvasCreateKind, normalizeSlackCanvasUpdateMode, pickSlackCanvasSectionId, } from "./canvases.js";
4
5
  import { normalizeSlackBlocksInput, summarizeSlackBlocksForPolicy } from "./slack-block-kit.js";
5
6
  import { encodeSlackModalPrivateMetadata, normalizeSlackModalViewInput } from "./slack-modals.js";
@@ -146,7 +147,7 @@ const SLACK_DISPATCHER_EXAMPLES = {
146
147
  },
147
148
  ],
148
149
  };
149
- function isRecord(value) {
150
+ function isSlackToolObject(value) {
150
151
  return typeof value === "object" && value !== null && !Array.isArray(value);
151
152
  }
152
153
  function normalizeSlackDispatcherActionName(value) {
@@ -180,25 +181,6 @@ function isAbortError(error) {
180
181
  function getErrorMessage(error) {
181
182
  return error instanceof Error ? error.message : String(error);
182
183
  }
183
- function isPinetDeliveryFallbackError(error) {
184
- const lower = getErrorMessage(error).toLowerCase();
185
- if (lower.includes("already owned"))
186
- return false;
187
- return (lower.includes("not running") ||
188
- lower.includes("unexpected state") ||
189
- lower.includes("unavailable") ||
190
- lower.includes("not connected") ||
191
- lower.includes("disconnected") ||
192
- lower.includes("timeout") ||
193
- lower.includes("timed out") ||
194
- lower.includes("econn") ||
195
- lower.includes("socket") ||
196
- lower.includes("no transport source") ||
197
- lower.includes("no transport channel") ||
198
- lower.includes("only allows local file paths") ||
199
- lower.includes("no adapter") ||
200
- lower.includes("identity is unavailable"));
201
- }
202
184
  function classifySlackDispatcherError(error) {
203
185
  const message = getErrorMessage(error);
204
186
  const lower = message.toLowerCase();
@@ -311,11 +293,11 @@ function classifySlackDispatcherError(error) {
311
293
  };
312
294
  }
313
295
  function extractToolResponseText(response) {
314
- if (!isRecord(response) || !Array.isArray(response.content))
296
+ if (!isSlackToolObject(response) || !Array.isArray(response.content))
315
297
  return undefined;
316
298
  const lines = response.content
317
299
  .map((item) => {
318
- if (!isRecord(item))
300
+ if (!isSlackToolObject(item))
319
301
  return undefined;
320
302
  return typeof item.text === "string" ? item.text : undefined;
321
303
  })
@@ -323,18 +305,20 @@ function extractToolResponseText(response) {
323
305
  return lines.length > 0 ? lines.join("\n") : undefined;
324
306
  }
325
307
  function extractToolResponseDetails(response) {
326
- return isRecord(response) && "details" in response ? response.details : undefined;
308
+ return isSlackToolObject(response) && "details" in response ? response.details : undefined;
327
309
  }
328
310
  function extractToolResponseFullDetails(response) {
329
- return isRecord(response) && "fullDetails" in response ? response.fullDetails : undefined;
311
+ return isSlackToolObject(response) && "fullDetails" in response
312
+ ? response.fullDetails
313
+ : undefined;
330
314
  }
331
315
  function normalizeSlackOutputOptions(action, args) {
332
- if (args != null && !isRecord(args)) {
316
+ if (args != null && !isSlackToolObject(args)) {
333
317
  throw new Error("slack args must be an object.");
334
318
  }
335
319
  const ownsFormatArg = SLACK_ACTIONS_WITH_FORMAT_ARG.has(action);
336
- const rawResponseFormat = isRecord(args) ? args.response_format : undefined;
337
- const rawFormat = rawResponseFormat ?? (isRecord(args) && !ownsFormatArg ? args.format : undefined);
320
+ const rawResponseFormat = isSlackToolObject(args) ? args.response_format : undefined;
321
+ const rawFormat = rawResponseFormat ?? (isSlackToolObject(args) && !ownsFormatArg ? args.format : undefined);
338
322
  const normalizedFormat = rawFormat == null ? "cli" : String(rawFormat).trim().toLowerCase();
339
323
  if (normalizedFormat !== "cli" && normalizedFormat !== "json") {
340
324
  throw new Error(ownsFormatArg
@@ -342,14 +326,14 @@ function normalizeSlackOutputOptions(action, args) {
342
326
  : 'format must be "cli" or "json" when provided.');
343
327
  }
344
328
  const format = normalizedFormat;
345
- const rawFull = isRecord(args) ? args.full : undefined;
329
+ const rawFull = isSlackToolObject(args) ? args.full : undefined;
346
330
  if (rawFull != null && typeof rawFull !== "boolean") {
347
331
  throw new Error("full must be a boolean when provided.");
348
332
  }
349
333
  return { format, full: rawFull === true };
350
334
  }
351
335
  function getSlackEnvelopeCliText(envelope) {
352
- if (envelope.status === "succeeded" && isRecord(envelope.data)) {
336
+ if (envelope.status === "succeeded" && isSlackToolObject(envelope.data)) {
353
337
  const text = envelope.data.text;
354
338
  if (typeof text === "string" && text.length > 0)
355
339
  return text;
@@ -423,6 +407,7 @@ function buildSlackSendPromptGuidelines() {
423
407
  return [
424
408
  "Use slack_send for replies in the current Slack assistant thread; always reply where the task came from.",
425
409
  "For rich Block Kit JSON examples or modal/canvas patterns, load the slack-bridge skill instead of relying on tool schemas.",
410
+ "If slack_send fails with 'broker is unavailable' or 'already owned by another agent', do NOT retry via post_channel or a different tool \u2014 report the blocker in the same thread (or to the broker) and wait for ownership to be transferred; direct posting would bypass thread ownership (#855).",
426
411
  ];
427
412
  }
428
413
  function getSlackCanvasSummary(markdown) {
@@ -624,32 +609,31 @@ export function registerSlackTools(pi, deps) {
624
609
  }
625
610
  async function deliverSlackMessage(input) {
626
611
  const blocks = input.blocks ? normalizeSlackBlocksInput(input.blocks) : undefined;
627
- let fallbackReason;
628
- if (input.threadTs && pinetDelivery) {
629
- try {
630
- if (pinetDelivery.isAvailable()) {
631
- const result = await pinetDelivery.sendSlackMessage({
632
- threadId: input.threadTs,
633
- channel: input.channel,
634
- text: input.text,
635
- ...(blocks ? { blocks } : {}),
636
- ...(input.files ? { files: input.files } : {}),
637
- });
638
- return {
639
- threadTs: input.threadTs,
640
- channel: result.channel,
641
- blocksCount: blocks?.length ?? 0,
642
- delivery: "pinet",
643
- adapter: result.adapter,
644
- messageId: result.messageId,
645
- };
646
- }
647
- }
648
- catch (error) {
649
- if (!isPinetDeliveryFallbackError(error))
650
- throw error;
651
- fallbackReason = getErrorMessage(error);
652
- }
612
+ // Threaded Slack replies routed through Pinet MUST NOT fall back to direct
613
+ // Slack when the broker is unavailable or the delivery errors: direct
614
+ // Slack posting bypasses broker thread-ownership enforcement and lets any
615
+ // worker with a valid bot token take over a Slack thread it does not own.
616
+ // See gugu91/pinet#855. Single-player mode (isEnabled === false) and
617
+ // top-level channel posts (no threadTs) remain unaffected.
618
+ if (input.threadTs && pinetDelivery?.isEnabled()) {
619
+ if (!pinetDelivery.isAvailable()) {
620
+ throw new Error("Cannot post to Slack thread: Pinet broker is unavailable and direct Slack fallback would bypass thread-ownership enforcement. Wait for the broker to reconnect, or ask the broker to transfer this thread.");
621
+ }
622
+ const result = await pinetDelivery.sendSlackMessage({
623
+ threadId: input.threadTs,
624
+ channel: input.channel,
625
+ text: input.text,
626
+ ...(blocks ? { blocks } : {}),
627
+ ...(input.files ? { files: input.files } : {}),
628
+ });
629
+ return {
630
+ threadTs: input.threadTs,
631
+ channel: result.channel,
632
+ blocksCount: blocks?.length ?? 0,
633
+ delivery: "pinet",
634
+ adapter: result.adapter,
635
+ messageId: result.messageId,
636
+ };
653
637
  }
654
638
  if (input.files && input.files.length > 0) {
655
639
  if (blocks && blocks.length > 0) {
@@ -676,7 +660,6 @@ export function registerSlackTools(pi, deps) {
676
660
  channel: input.channel,
677
661
  blocksCount: blocks?.length ?? 0,
678
662
  delivery: "slack",
679
- ...(fallbackReason ? { fallbackReason } : {}),
680
663
  };
681
664
  }
682
665
  const body = {
@@ -693,14 +676,13 @@ export function registerSlackTools(pi, deps) {
693
676
  if (input.threadTs)
694
677
  body.thread_ts = input.threadTs;
695
678
  const response = await slack("chat.postMessage", getBotToken(), body);
696
- const message = isRecord(response.message) ? response.message : null;
679
+ const message = isSlackToolObject(response.message) ? response.message : null;
697
680
  const ts = typeof message?.ts === "string" ? message.ts : undefined;
698
681
  return {
699
682
  ...(ts ? { ts } : {}),
700
683
  channel: input.channel,
701
684
  blocksCount: blocks?.length ?? 0,
702
685
  delivery: "slack",
703
- ...(fallbackReason ? { fallbackReason } : {}),
704
686
  };
705
687
  }
706
688
  const slackActionRegistry = new Map();
@@ -712,10 +694,10 @@ export function registerSlackTools(pi, deps) {
712
694
  slackActionRegistry.set(action, definition);
713
695
  }
714
696
  function buildSlackDispatcherHelpEnvelope(args) {
715
- if (args != null && !isRecord(args)) {
697
+ if (args != null && !isSlackToolObject(args)) {
716
698
  return buildSlackActionFailureEnvelope(new Error("slack help args must be an object."));
717
699
  }
718
- const topic = isRecord(args) && typeof args.topic === "string" ? args.topic : undefined;
700
+ const topic = isSlackToolObject(args) && typeof args.topic === "string" ? args.topic : undefined;
719
701
  if (topic) {
720
702
  const action = normalizeSlackDispatcherActionName(topic);
721
703
  const definition = slackActionRegistry.get(action);
@@ -790,7 +772,7 @@ export function registerSlackTools(pi, deps) {
790
772
  if (action === "help") {
791
773
  return buildSlackDispatcherResponse(buildSlackDispatcherHelpEnvelope(args), output);
792
774
  }
793
- if (!isRecord(args)) {
775
+ if (!isSlackToolObject(args)) {
794
776
  return buildSlackDispatcherResponse(buildSlackActionFailureEnvelope(new Error("slack args must be an object.")), output);
795
777
  }
796
778
  const definition = slackActionRegistry.get(action);
@@ -1268,6 +1250,10 @@ export function registerSlackTools(pi, deps) {
1268
1250
  : "";
1269
1251
  lines.push(`${prefix} (${message.timestamp}): ${message.text}${metadataSuffix}`);
1270
1252
  }
1253
+ const subtreeInboxIds = getSubtreeInboxIds(pending);
1254
+ if (subtreeInboxIds.length > 0) {
1255
+ deps.markSubtreeInboxIdsDelivered(subtreeInboxIds);
1256
+ }
1271
1257
  return {
1272
1258
  content: [
1273
1259
  {
@@ -1425,7 +1411,10 @@ export function registerSlackTools(pi, deps) {
1425
1411
  label: "Slack Send",
1426
1412
  description: "Send a message in a Slack assistant thread.",
1427
1413
  promptSnippet: "Reply in a Slack assistant thread. When you receive a task: ACK briefly, do the work, report blockers immediately, report the outcome when done. Always reply where the task came from.",
1428
- promptGuidelines: buildSlackSendPromptGuidelines(),
1414
+ promptGuidelines: [
1415
+ ...buildSlackSendPromptGuidelines(),
1416
+ ...(deps.additionalSendPromptGuidelines ?? []),
1417
+ ],
1429
1418
  parameters: Type.Object({
1430
1419
  text: Type.String({ description: "Message text (Slack markdown)" }),
1431
1420
  thread_ts: Type.Optional(Type.String({
@@ -1479,7 +1468,6 @@ export function registerSlackTools(pi, deps) {
1479
1468
  filesCount: Array.isArray(params.files) ? params.files.length : 0,
1480
1469
  ...(delivery.adapter ? { adapter: delivery.adapter } : {}),
1481
1470
  ...(delivery.messageId ? { messageId: delivery.messageId } : {}),
1482
- ...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
1483
1471
  },
1484
1472
  };
1485
1473
  },
@@ -2194,7 +2182,6 @@ export function registerSlackTools(pi, deps) {
2194
2182
  filesCount: Array.isArray(params.files) ? params.files.length : 0,
2195
2183
  ...(delivery.adapter ? { adapter: delivery.adapter } : {}),
2196
2184
  ...(delivery.messageId ? { messageId: delivery.messageId } : {}),
2197
- ...(delivery.fallbackReason ? { fallbackReason: delivery.fallbackReason } : {}),
2198
2185
  },
2199
2186
  };
2200
2187
  },
@@ -17,8 +17,24 @@ export interface PreparedSlackUpload {
17
17
  source: "content" | "path";
18
18
  resolvedPath?: string;
19
19
  }
20
+ export interface SlackUploadMetadataPayload extends Record<string, unknown> {
21
+ filename: string;
22
+ length: number;
23
+ snippet_type?: string;
24
+ }
25
+ export interface SlackCompleteUploadFilePayload {
26
+ id: string;
27
+ title: string;
28
+ }
29
+ export interface SlackCompleteUploadPayload extends Record<string, unknown> {
30
+ files: SlackCompleteUploadFilePayload[];
31
+ channel_id: string;
32
+ thread_ts?: string;
33
+ initial_comment?: string;
34
+ }
35
+ export type SlackUploadApiBody = SlackUploadMetadataPayload | SlackCompleteUploadPayload;
20
36
  export interface SlackUploadDeps {
21
- slack: (method: string, token: string, body?: Record<string, unknown>) => Promise<SlackResult>;
37
+ slack: (method: string, token: string, body?: SlackUploadApiBody) => Promise<SlackResult>;
22
38
  token: string;
23
39
  fetchImpl?: (url: string, init: RequestInit) => Promise<Pick<Response, "ok" | "status" | "statusText" | "text">>;
24
40
  }
@@ -2,21 +2,51 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AgentSessionSummary } from "./broker/types.js";
3
3
  import type { PinetReadOptions, PinetReadResult } from "@pinet/pinet-core/pinet-read-formatting";
4
4
  import { type InboxMessage, type PinetControlCommand, type PinetRemoteControlRequestResult, type SlackBridgeSettings } from "./helpers.js";
5
+ import type { BrokerDB } from "./broker/schema.js";
5
6
  export interface SubtreeBrokerPaths {
6
7
  rootDir: string;
7
8
  socketPath: string;
8
9
  dbPath: string;
9
10
  lockPath: string;
10
11
  }
11
- export interface SubtreeWorkerRecord {
12
- launchId: string;
12
+ export type WorkerRuntimeSpec = {
13
+ runtimeKind: "tmux";
14
+ sessionName: string;
15
+ tmuxSocketPath: string | null;
16
+ } | {
17
+ runtimeKind: "herdr";
13
18
  sessionName: string;
19
+ herdrSession: string;
20
+ herdrConfigDir: string;
21
+ herdrPaneId: string | null;
22
+ herdrShellPid: number | null;
23
+ };
24
+ type HerdrWorkerRuntimeSpec = Extract<WorkerRuntimeSpec, {
25
+ runtimeKind: "herdr";
26
+ }>;
27
+ export type SubtreeWorkerRecord = WorkerRuntimeSpec & {
28
+ launchId: string;
14
29
  repoPath: string;
15
30
  role: string;
16
31
  laneId: string | null;
17
32
  agentId: string | null;
18
33
  startedAt: string;
19
34
  monitorCommand: string;
35
+ };
36
+ interface WorkerRuntimeController<T extends WorkerRuntimeSpec> {
37
+ createLaunchSpec: (sessionName: string) => T;
38
+ monitorCommand: (spec: T) => string;
39
+ launch: (spec: T, launcherPath: string, launchEnv: Record<string, string>, onRuntimeOwned?: (spec: T) => void) => Promise<void>;
40
+ cleanup: (spec: T) => Promise<void>;
41
+ }
42
+ export interface HerdrCommandOptions {
43
+ env: NodeJS.ProcessEnv;
44
+ detached?: boolean;
45
+ }
46
+ export type HerdrCommandRunner = (args: string[], options: HerdrCommandOptions) => Promise<string>;
47
+ export interface HerdrWorkerRuntimeControllerOptions {
48
+ herdrSession: string;
49
+ herdrConfigDir: string;
20
50
  }
21
51
  export interface SubtreeBrokerStatus {
22
52
  active: boolean;
@@ -50,16 +80,34 @@ export interface SubtreeAgentRecord {
50
80
  subtreeRole?: string | null;
51
81
  laneId?: string | null;
52
82
  }
83
+ export interface SubtreeSpawnLaunchHandle {
84
+ launchId: string;
85
+ tmuxSessionName: string;
86
+ socketPath: string;
87
+ state: "launched_unregistered";
88
+ runtimeKind?: WorkerRuntimeSpec["runtimeKind"];
89
+ monitorCommand?: string;
90
+ }
91
+ export declare class SubtreeSpawnRegistrationTimeoutError extends Error {
92
+ readonly handle: SubtreeSpawnLaunchHandle;
93
+ constructor(timeoutMs: number, handle: SubtreeSpawnLaunchHandle);
94
+ }
95
+ export declare class SubtreeSpawnLaunchError extends Error {
96
+ readonly handle: SubtreeSpawnLaunchHandle;
97
+ constructor(error: Error, handle: SubtreeSpawnLaunchHandle, runtimeKind?: WorkerRuntimeSpec["runtimeKind"]);
98
+ }
53
99
  export interface SubtreeSpawnInput {
54
100
  task: string;
55
101
  repo: string;
56
102
  role?: string;
57
103
  laneId?: string;
58
104
  waitForRegistrationMs?: number;
105
+ cleanupHandle?: SubtreeSpawnLaunchHandle;
59
106
  }
60
107
  export interface SubtreeSpawnResult {
61
108
  status: "started";
62
109
  launchId: string;
110
+ runtimeKind: WorkerRuntimeSpec["runtimeKind"];
63
111
  sessionName: string;
64
112
  repoPath: string;
65
113
  role: string;
@@ -85,19 +133,43 @@ export interface SubtreeBrokerRuntimeDeps {
85
133
  getAgentMetadata: (role: "broker" | "worker") => Promise<Record<string, unknown>>;
86
134
  getMeshRoleFromMetadata: (metadata: Record<string, unknown> | undefined, fallback?: "broker" | "worker") => "broker" | "worker";
87
135
  pushInboxMessages: (messages: InboxMessage[]) => void;
136
+ discardQueuedInboxMessages: () => void;
88
137
  updateBadge: () => void;
89
138
  maybeDrainInboxIfIdle: (ctx: ExtensionContext) => boolean;
139
+ deliverSteeringMessage: (text: string, ctx: ExtensionContext) => boolean;
90
140
  requestRemoteControl: (command: PinetControlCommand, ctx: ExtensionContext) => PinetRemoteControlRequestResult;
91
141
  runRemoteControl: (command: PinetControlCommand, ctx: ExtensionContext) => void;
92
142
  formatError: (error: unknown) => string;
143
+ runTmuxCommand?: (args: string[]) => Promise<void>;
144
+ runHerdrCommand?: HerdrCommandRunner;
145
+ }
146
+ /**
147
+ * The authoritative hibernation runtime surface for this process's subtree
148
+ * broker. The subtree broker is the authority that spawns and OWNS its workers
149
+ * and authors their durable runtime specs, so hibernate/wake must resolve the
150
+ * target, read its authz spec, and drive the orchestrator against THIS same
151
+ * authoritative DB end to end. The explicit trust boundary: the operator command
152
+ * may only address workers this subtree broker owns. Null when no subtree broker
153
+ * is running (nothing is command-addressable).
154
+ */
155
+ export interface SubtreeHibernationRuntimeControl {
156
+ /** The single authoritative DB that owns the spawned workers + their specs. */
157
+ db: BrokerDB;
158
+ /** Broker instance id recorded on lifecycle leases; matches startup recovery. */
159
+ brokerInstanceId: string;
160
+ /** Base PINET_* env re-establishing the mesh connection for a woken worker. */
161
+ baseLaunchEnv: Record<string, string>;
93
162
  }
94
163
  export interface SubtreeBrokerRuntime {
95
164
  start: (ctx: ExtensionContext) => Promise<SubtreeBrokerStatus>;
165
+ getHibernationRuntimeControl: () => SubtreeHibernationRuntimeControl | null;
96
166
  stop: (options?: {
97
167
  releaseIdentity?: boolean;
98
168
  stopChildren?: boolean;
99
169
  }) => Promise<void>;
100
170
  getStatus: () => SubtreeBrokerStatus;
171
+ drainInbox: (ctx: ExtensionContext) => void;
172
+ markDelivered: (inboxIds: number[]) => void;
101
173
  readInbox: (options?: PinetReadOptions) => PinetReadResult | null;
102
174
  sendMessage: (target: string, body: string, metadata?: Record<string, unknown>) => Promise<{
103
175
  messageId: number;
@@ -109,4 +181,13 @@ export interface SubtreeBrokerRuntime {
109
181
  isActive: () => boolean;
110
182
  }
111
183
  export declare function buildSubtreeBrokerPaths(stableId: string): SubtreeBrokerPaths;
184
+ export declare function createHerdrWorkerRuntimeController(runHerdrCommand: HerdrCommandRunner, options: HerdrWorkerRuntimeControllerOptions): WorkerRuntimeController<HerdrWorkerRuntimeSpec>;
185
+ export declare function getExtensionEntryPath(): string;
186
+ /**
187
+ * Broker env var NAMES (never values) a spawned — or later WOKEN — worker
188
+ * re-exports to re-establish itself. Single source of truth shared by the
189
+ * ordinary spawn launcher and the Phase B wake path so both stay in lockstep.
190
+ */
191
+ export declare const SUBTREE_INHERITED_ENV_KEYS: string[];
112
192
  export declare function createSubtreeBrokerRuntime(deps: SubtreeBrokerRuntimeDeps): SubtreeBrokerRuntime;
193
+ export {};