@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
package/dist/index.js CHANGED
@@ -1,20 +1,24 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
- import { createGitContextCache, probeGitBranch, probeGitContext } from "./git-metadata.js";
4
- import { loadSettings as loadSettingsFromFile, buildAllowlist, reloadPinetRuntimeSafely, buildPinetOwnerToken, resolveAgentIdentity, resolveBrokerStableId, resolveAgentStableId, resolveAllowAllWorkspaceUsers, trackBrokerInboundThread, } from "./helpers.js";
3
+ import { probeGitBranch, probeGitContext } from "./git-metadata.js";
4
+ import { loadSettings as loadSettingsFromFile, buildAllowlist, resolvePinetMeshAuth, reloadPinetRuntimeInPlaceSafely, reloadPinetRuntimeSafely, buildPinetOwnerToken, resolveAgentIdentity, resolveBrokerStableId, resolveAgentStableId, resolveAllowAllWorkspaceUsers, trackBrokerInboundThread, isAbortError, } from "./helpers.js";
5
5
  import { buildSecurityPrompt } from "./guardrails.js";
6
6
  import { TtlCache, TtlSet } from "./ttl-cache.js";
7
7
  import { resolveReactionCommands } from "./reaction-triggers.js";
8
- import { DEFAULT_SOCKET_PATH } from "./broker/client.js";
8
+ import { inspectBrokerLock, probeBrokerSocket, replaceBrokerOwner, } from "./broker/index.js";
9
+ import { readMeshSecret } from "./broker/auth.js";
10
+ import { collectAgentLifecycleStatuses, } from "@pinet/broker-core";
11
+ import { resolveHibernationSettings } from "./hibernation-config.js";
9
12
  import { createCommandRegistrationRuntime } from "./command-registration-runtime.js";
10
13
  import { createToolRegistrationRuntime } from "./tool-registration-runtime.js";
11
14
  import { createSlackRuntimeAccess } from "./slack-runtime-access.js";
12
15
  import { createThreadConfirmationPolicy } from "./thread-confirmations.js";
13
16
  import { persistDeliveredInboundMessage } from "./broker-inbound-persistence.js";
17
+ import { createCompactionGate } from "./compaction-gate.js";
14
18
  import { createIMessageAdapter, detectIMessageMvpEnvironment, formatIMessageMvpReadiness, } from "@pinet/imessage-bridge";
15
19
  import { SLACK_SOCKET_DELIVERY_DEDUP_MAX_SIZE, SLACK_SOCKET_DELIVERY_DEDUP_TTL_MS, } from "./slack-access.js";
16
20
  import { createFollowerDeliveryState, markFollowerInboxIdsDelivered, queueFollowerInboxIds, } from "./follower-delivery.js";
17
- import { createFollowerRuntime } from "./follower-runtime.js";
21
+ import { createFollowerRuntime, resolveBrokerSocketPath, } from "./follower-runtime.js";
18
22
  import { createSinglePlayerRuntime, } from "./single-player-runtime.js";
19
23
  import { createBrokerRuntime } from "./broker-runtime.js";
20
24
  import { createSlackPinetRuntimeAdapterFactory } from "./slack-pinet-runtime-adapter.js";
@@ -42,7 +46,8 @@ import { formatSlackAgentsDashboard, formatSlackAgentsUsage, isSlackAgentCommand
42
46
  import { createPinetRegistrationGate } from "./pinet-registration-gate.js";
43
47
  import { createBrokerRuntimeAccess } from "./broker-runtime-access.js";
44
48
  import { createInboxDrainRuntime } from "./inbox-drain-runtime.js";
45
- import { createSubtreeBrokerRuntime } from "./subtree-broker-runtime.js";
49
+ import { createSubtreeBrokerRuntime, getExtensionEntryPath, SUBTREE_INHERITED_ENV_KEYS, } from "./subtree-broker-runtime.js";
50
+ import { routeHibernationCommand } from "./broker/hibernation-command-router.js";
46
51
  import { createAgentCompletionRuntime } from "./agent-completion-runtime.js";
47
52
  import { sendBrokerMessage } from "./broker/message-send.js";
48
53
  import { SlackThreadStatusManager } from "./slack-thread-status.js";
@@ -144,13 +149,20 @@ export default function (pi) {
144
149
  },
145
150
  });
146
151
  const { updateBadge, setExtStatus, maybeDrainInboxIfIdle } = sessionUiRuntime;
152
+ const compactionGate = createCompactionGate(() => {
153
+ maybeDrainInboxIfIdle();
154
+ const ctx = sessionUiRuntime.getExtensionContext();
155
+ if (ctx)
156
+ subtreeBrokerRuntime.drainInbox(ctx);
157
+ });
158
+ const isIdleAndNotCompacting = () => !compactionGate.isActive() && (sessionUiRuntime.getExtensionContext()?.isIdle?.() ?? true);
147
159
  let reportAgentStatus = async () => { };
148
160
  let deliverTrackedSlackFollowUpMessage = () => false;
149
161
  const inboxDrainRuntime = createInboxDrainRuntime({
150
162
  sendUserMessage: (body) => {
151
163
  pi.sendUserMessage(body);
152
164
  },
153
- isIdle: () => sessionUiRuntime.getExtensionContext()?.isIdle?.() ?? true,
165
+ isIdle: isIdleAndNotCompacting,
154
166
  takeInboxMessages: (maxMessages) => inbox.splice(0, maxMessages ?? inbox.length),
155
167
  restoreInboxMessages: (messages) => {
156
168
  inbox.unshift(...messages);
@@ -166,12 +178,31 @@ export default function (pi) {
166
178
  markBrokerInboxIdsDelivered: (inboxIds) => {
167
179
  brokerRuntime.markDelivered(inboxIds);
168
180
  },
181
+ markSubtreeInboxIdsDelivered: (inboxIds) => {
182
+ subtreeBrokerRuntime.markDelivered(inboxIds);
183
+ },
169
184
  getFollowerDeliveryState: () => followerDeliveryState,
170
185
  });
171
186
  const { deliverFollowUpMessage, flushDeliveredFollowerAcks, drainInbox } = inboxDrainRuntime;
172
187
  drainInboxPort = drainInbox;
188
+ function deliverSteeringMessage(text, ctx) {
189
+ try {
190
+ return compactionGate.tryDeliver(() => {
191
+ if (ctx.isIdle?.() ?? true) {
192
+ pi.sendUserMessage(text);
193
+ }
194
+ else {
195
+ pi.sendUserMessage(text, { deliverAs: "steer" });
196
+ }
197
+ });
198
+ }
199
+ catch (error) {
200
+ console.error(`[slack-bridge] Pinet steering delivery failed: ${msg(error)}`);
201
+ return false;
202
+ }
203
+ }
173
204
  // ─── Helpers ─────────────────────────────────────────
174
- const gitContextCache = createGitContextCache(() => probeGitContext(process.cwd()));
205
+ let gitContext = null;
175
206
  const runtimeAgentContext = createRuntimeAgentContext({
176
207
  cwd: process.cwd(),
177
208
  getSettings: () => settings,
@@ -230,7 +261,10 @@ export default function (pi) {
230
261
  getExtensionContext: sessionUiRuntime.getExtensionContext,
231
262
  persistState,
232
263
  updateBadge,
233
- getGitContext: () => gitContextCache.get(),
264
+ getGitContext: async (signal) => {
265
+ gitContext ??= await probeGitContext(process.cwd(), undefined, signal);
266
+ return gitContext;
267
+ },
234
268
  });
235
269
  const { isUserAllowed, maybeWarnSlackUserAccess, maybeWarnSlackGuardrailPosture, applyLocalAgentIdentity, refreshSettings, snapshotReloadableRuntime, restoreReloadableRuntime, getAgentMetadata, getMeshRoleFromMetadata, buildSkinMetadata, applyRegistrationIdentity, } = runtimeAgentContext;
236
270
  async function refreshSlackScopeDiagnostics(ctx) {
@@ -371,7 +405,6 @@ export default function (pi) {
371
405
  beginThreadStatus: (channel, threadTs, status) => slackThreadStatuses.begin(channel, threadTs, status),
372
406
  updateThreadStatus: (channel, threadTs, status) => slackThreadStatuses.update(channel, threadTs, status),
373
407
  clearThreadStatus: (channel, threadTs) => slackThreadStatuses.clear(channel, threadTs),
374
- beforeAgentStart: agentPromptGuidance.beforeAgentStart,
375
408
  onCompletionAgentEnd: agentCompletionRuntime.onAgentEnd,
376
409
  setDeliverTrackedSlackFollowUpMessage: (deliver) => {
377
410
  deliverTrackedSlackFollowUpMessage = deliver;
@@ -380,7 +413,7 @@ export default function (pi) {
380
413
  const pinetMaintenanceDelivery = createPinetMaintenanceDelivery({
381
414
  getActiveBrokerDb,
382
415
  getActiveBrokerSelfId,
383
- isIdle: () => sessionUiRuntime.getExtensionContext()?.isIdle?.() ?? true,
416
+ isIdle: isIdleAndNotCompacting,
384
417
  sendUserMessage: (body) => {
385
418
  pi.sendUserMessage(body);
386
419
  },
@@ -420,8 +453,15 @@ export default function (pi) {
420
453
  pushInboxMessages: (messages) => {
421
454
  inbox.push(...messages);
422
455
  },
456
+ discardQueuedInboxMessages: () => {
457
+ for (let index = inbox.length - 1; index >= 0; index -= 1) {
458
+ if (inbox[index]?.brokerInboxOrigin === "subtree")
459
+ inbox.splice(index, 1);
460
+ }
461
+ },
423
462
  updateBadge,
424
463
  maybeDrainInboxIfIdle,
464
+ deliverSteeringMessage,
425
465
  requestRemoteControl,
426
466
  runRemoteControl,
427
467
  formatError: msg,
@@ -499,6 +539,9 @@ export default function (pi) {
499
539
  let currentRuntimeMode = "off";
500
540
  let brokerRole = null;
501
541
  let brokerClient = null;
542
+ let pinetLifecycleTail = Promise.resolve();
543
+ let pinetReloadPromise = null;
544
+ let pendingStartup = null;
502
545
  let followerRuntimeDiagnostic = null;
503
546
  const followerDeliveryState = createFollowerDeliveryState();
504
547
  let desiredAgentStatus = "idle";
@@ -531,6 +574,7 @@ export default function (pi) {
531
574
  getAppToken: () => appToken,
532
575
  getAllowedUsers: () => allowedUsers,
533
576
  shouldAllowAllWorkspaceUsers: () => resolveAllowAllWorkspaceUsers(settings, process.env.SLACK_ALLOW_ALL_WORKSPACE_USERS),
577
+ setExtStatus,
534
578
  onAppHomeOpened: handleBrokerAppHomeOpened,
535
579
  onSlashCommand: handleBrokerSlackSlashCommand,
536
580
  });
@@ -615,6 +659,7 @@ export default function (pi) {
615
659
  },
616
660
  updateBadge,
617
661
  maybeDrainInboxIfIdle,
662
+ deliverSteeringMessage,
618
663
  requestRemoteControl,
619
664
  deferControlAck: deferBrokerControlAck,
620
665
  runRemoteControl,
@@ -644,6 +689,14 @@ export default function (pi) {
644
689
  buildControlPlaneDashboardSnapshot: (input) => buildBrokerControlPlaneDashboardSnapshot(input),
645
690
  buildCurrentDashboardSnapshot: async (openedAt) => buildCurrentBrokerControlPlaneDashboardSnapshot(openedAt),
646
691
  createAdapterBindings: [slackPinetAdapterFactory],
692
+ onAdminShutdownRequested: async (ctx) => {
693
+ // `/pinet start replace` from another local session: stop being the
694
+ // broker but keep this session alive (issue #951).
695
+ ctx.ui.notify("Pinet broker shutdown requested by another local session (/pinet start replace). Stopping the broker runtime in this session.", "warning");
696
+ await runPinetLifecycle(() => stopPinetRuntime(ctx, { releaseIdentity: true }));
697
+ slackRequestRuntime.reset();
698
+ singlePlayerRuntime.resetShutdownState();
699
+ },
647
700
  onMaintenanceResult: (ctx, { result, previousSignature, signature }) => {
648
701
  if (signature && signature !== previousSignature) {
649
702
  ctx.ui.notify(`Pinet broker: ${result.anomalies.join("; ")}`, "warning");
@@ -734,6 +787,25 @@ export default function (pi) {
734
787
  },
735
788
  });
736
789
  const { sendPinetAgentMessage, sendPinetBroadcastMessage, scheduleBrokerWakeup, scheduleFollowerWakeup, listBrokerAgents, listFollowerAgents, searchPinetSessions, } = pinetMeshOps;
790
+ async function runHibernationCommand(command, target, reason) {
791
+ // Thin wrapper: DB-topology resolution + executor composition live in the
792
+ // extracted, independently-tested production router, so the live extension and
793
+ // the production-route E2E drive the exact same integration code. `brokerRole`,
794
+ // the runtime-activation authority (via the router's internal
795
+ // `hibernationRuntimeActive()` gate), the real subtree control accessor, and the
796
+ // central-DB fallback are all threaded through unchanged.
797
+ return routeHibernationCommand({
798
+ command,
799
+ target,
800
+ ...(reason !== undefined ? { reason } : {}),
801
+ brokerRole,
802
+ hib: resolveHibernationSettings(settings),
803
+ getRuntimeControl: () => subtreeBrokerRuntime.getHibernationRuntimeControl(),
804
+ getFallbackDb: () => getActiveBrokerDb(),
805
+ extensionEntryPath: getExtensionEntryPath(),
806
+ inheritedEnvKeys: SUBTREE_INHERITED_ENV_KEYS,
807
+ });
808
+ }
737
809
  async function listPinetLanes(options) {
738
810
  if (brokerRole === "broker") {
739
811
  const db = getActiveBrokerDb();
@@ -880,104 +952,185 @@ export default function (pi) {
880
952
  }
881
953
  throw new Error("Pinet is in an unexpected state.");
882
954
  }
883
- async function transitionToRuntimeMode(ctx, mode) {
884
- if (currentRuntimeMode === mode) {
885
- if (mode === "off") {
886
- setExtStatus(ctx, "off");
887
- }
955
+ function runPinetLifecycle(operation) {
956
+ const run = pinetLifecycleTail.then(operation, operation);
957
+ pinetLifecycleTail = run.then(() => undefined, () => undefined);
958
+ return run;
959
+ }
960
+ async function cancelPendingStartup() {
961
+ const startup = pendingStartup;
962
+ if (!startup)
888
963
  return;
889
- }
890
- if (currentRuntimeMode !== "off") {
891
- await stopPinetRuntime(ctx, { releaseIdentity: true });
892
- // Runtime transitions keep the extension alive in-process, so restore a
893
- // fresh top-level Slack request tracker after tearing the prior runtime down.
964
+ startup.controller.abort();
965
+ await startup.promise;
966
+ }
967
+ function runRuntimeModeTransition(ctx, mode, signal) {
968
+ return runPinetLifecycle(async () => {
969
+ signal?.throwIfAborted();
970
+ if (currentRuntimeMode === mode) {
971
+ if (mode === "off") {
972
+ setExtStatus(ctx, "off");
973
+ }
974
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
975
+ return;
976
+ }
977
+ if (currentRuntimeMode !== "off") {
978
+ if (pinetReloadPromise) {
979
+ await pinetReloadPromise.catch(() => undefined);
980
+ }
981
+ await stopPinetRuntime(ctx, { releaseIdentity: true });
982
+ signal?.throwIfAborted();
983
+ }
894
984
  slackRequestRuntime.reset();
895
985
  singlePlayerRuntime.resetShutdownState();
896
- }
897
- if (mode === "off") {
898
- currentRuntimeMode = "off";
899
- setExtStatus(ctx, "off");
900
- return;
901
- }
902
- if (mode === "single") {
903
- currentRuntimeMode = "single";
904
- setExtStatus(ctx, "reconnecting");
905
- await singlePlayerRuntime.connect(ctx);
906
- botUserId = singlePlayerRuntime.getBotUserId() ?? botUserId;
986
+ if (mode === "off") {
987
+ currentRuntimeMode = "off";
988
+ setExtStatus(ctx, "off");
989
+ return;
990
+ }
991
+ if (mode === "single") {
992
+ currentRuntimeMode = "single";
993
+ setExtStatus(ctx, "reconnecting");
994
+ await singlePlayerRuntime.connect(ctx, signal);
995
+ signal?.throwIfAborted();
996
+ botUserId = singlePlayerRuntime.getBotUserId() ?? botUserId;
997
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
998
+ signal?.throwIfAborted();
999
+ void ensureSlackScopeDiagnostics(ctx);
1000
+ return;
1001
+ }
1002
+ if (mode === "broker") {
1003
+ await connectAsBroker(ctx, {}, signal);
1004
+ signal?.throwIfAborted();
1005
+ void ensureSlackScopeDiagnostics(ctx);
1006
+ return;
1007
+ }
1008
+ await connectAsFollower(ctx, signal);
1009
+ signal?.throwIfAborted();
907
1010
  void ensureSlackScopeDiagnostics(ctx);
908
- return;
1011
+ });
1012
+ }
1013
+ async function transitionToRuntimeMode(ctx, mode) {
1014
+ await cancelPendingStartup();
1015
+ await runRuntimeModeTransition(ctx, mode);
1016
+ }
1017
+ /**
1018
+ * Resolve the local mesh secret value for client-side broker RPCs
1019
+ * (graceful takeover). Returns null when no secret is configured or the
1020
+ * secret file is unreadable — callers degrade to fenced termination.
1021
+ */
1022
+ function resolveLocalMeshSecretValue() {
1023
+ const meshAuth = resolvePinetMeshAuth(settings);
1024
+ if (meshAuth.meshSecret) {
1025
+ return meshAuth.meshSecret;
909
1026
  }
910
- if (mode === "broker") {
911
- await connectAsBroker(ctx);
912
- void ensureSlackScopeDiagnostics(ctx);
913
- return;
1027
+ try {
1028
+ return meshAuth.meshSecretPath ? readMeshSecret(meshAuth.meshSecretPath) : readMeshSecret();
1029
+ }
1030
+ catch {
1031
+ return null;
914
1032
  }
915
- await connectAsFollower(ctx);
916
- void ensureSlackScopeDiagnostics(ctx);
917
1033
  }
918
1034
  async function stopPinetRuntime(ctx, options) {
919
1035
  flushPersist();
920
1036
  await subtreeBrokerRuntime.stop({ releaseIdentity: options.releaseIdentity });
921
1037
  await brokerRuntime.disconnect({ releaseIdentity: options.releaseIdentity });
922
- if (brokerClient) {
923
- if (options.releaseIdentity) {
924
- await disconnectFollower(ctx).catch(() => {
925
- /* best effort */
926
- });
927
- }
928
- else {
929
- await followerRuntime.disconnect(ctx, { releaseIdentity: false }).catch(() => {
930
- /* best effort */
931
- });
932
- brokerClient = null;
933
- desiredAgentStatus = "idle";
934
- brokerRole = null;
935
- pinetEnabled = false;
936
- }
937
- }
1038
+ await followerRuntime
1039
+ .disconnect(ctx, { releaseIdentity: options.releaseIdentity })
1040
+ .catch(() => {
1041
+ /* best effort */
1042
+ });
1043
+ brokerClient = null;
1044
+ followerRuntimeDiagnostic = null;
938
1045
  await singlePlayerRuntime.disconnect();
939
1046
  brokerRole = null;
940
1047
  pinetEnabled = false;
941
1048
  desiredAgentStatus = "idle";
942
1049
  currentRuntimeMode = "off";
943
1050
  setExtStatus(ctx, "off");
1051
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
944
1052
  }
945
1053
  async function reloadPinetRuntime(ctx) {
946
- await reloadPinetRuntimeSafely({
947
- getCurrentRole: () => brokerRole,
948
- snapshotState: () => snapshotReloadableRuntime(),
949
- restoreState: (snapshot) => {
950
- restoreReloadableRuntime(snapshot);
951
- },
952
- refreshState: () => {
953
- refreshSettings();
954
- },
955
- validateRefreshedState: () => {
1054
+ await cancelPendingStartup();
1055
+ if (pinetReloadPromise) {
1056
+ await pinetReloadPromise;
1057
+ return;
1058
+ }
1059
+ const reloadWork = runPinetLifecycle(async () => {
1060
+ let reloadBrokerInPlace = false;
1061
+ const validateRefreshedState = () => {
956
1062
  if (!botToken || !appToken) {
957
1063
  throw new Error("Slack tokens are not configured after reload.");
958
1064
  }
959
- },
960
- stopRuntime: async () => {
961
- await stopPinetRuntime(ctx, { releaseIdentity: false });
962
- // Reload intentionally keeps the extension alive in-process, so restore a
963
- // fresh top-level Slack request tracker after aborting the previous
964
- // generation. This preserves shutdown abort semantics without leaving
965
- // top-level Slack tools permanently stuck in "shutdown in progress".
966
- slackRequestRuntime.reset();
967
- singlePlayerRuntime.resetShutdownState();
968
- setExtStatus(ctx, "reconnecting");
969
- },
970
- startRuntime: async (role) => {
971
- if (role === "broker") {
972
- await connectAsBroker(ctx);
973
- return;
1065
+ if (brokerRole === "broker") {
1066
+ reloadBrokerInPlace = brokerRuntime.canReloadInPlace();
974
1067
  }
975
- await connectAsFollower(ctx);
976
- },
1068
+ };
1069
+ if (brokerRole === "broker") {
1070
+ await reloadPinetRuntimeInPlaceSafely({
1071
+ snapshotState: () => snapshotReloadableRuntime(),
1072
+ restoreState: (snapshot) => {
1073
+ restoreReloadableRuntime(snapshot);
1074
+ },
1075
+ refreshState: () => {
1076
+ refreshSettings();
1077
+ },
1078
+ validateRefreshedState,
1079
+ reloadRuntime: async () => {
1080
+ if (reloadBrokerInPlace) {
1081
+ const result = await brokerRuntime.reloadAdapters(ctx);
1082
+ botUserId = result.botUserId;
1083
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
1084
+ return;
1085
+ }
1086
+ await stopPinetRuntime(ctx, { releaseIdentity: false });
1087
+ slackRequestRuntime.reset();
1088
+ singlePlayerRuntime.resetShutdownState();
1089
+ setExtStatus(ctx, "reconnecting");
1090
+ await connectAsBroker(ctx, { refreshSettings: false });
1091
+ },
1092
+ });
1093
+ return;
1094
+ }
1095
+ await reloadPinetRuntimeSafely({
1096
+ getCurrentRole: () => brokerRole,
1097
+ snapshotState: () => snapshotReloadableRuntime(),
1098
+ restoreState: (snapshot) => {
1099
+ restoreReloadableRuntime(snapshot);
1100
+ },
1101
+ refreshState: () => {
1102
+ refreshSettings();
1103
+ },
1104
+ validateRefreshedState,
1105
+ stopRuntime: async () => {
1106
+ await stopPinetRuntime(ctx, { releaseIdentity: false });
1107
+ // Reload intentionally keeps the extension alive in-process, so restore a
1108
+ // fresh top-level Slack request tracker after aborting the previous
1109
+ // generation. This preserves shutdown abort semantics without leaving
1110
+ // top-level Slack tools permanently stuck in "shutdown in progress".
1111
+ slackRequestRuntime.reset();
1112
+ singlePlayerRuntime.resetShutdownState();
1113
+ setExtStatus(ctx, "reconnecting");
1114
+ },
1115
+ startRuntime: async (role) => {
1116
+ if (role === "broker") {
1117
+ await connectAsBroker(ctx);
1118
+ return;
1119
+ }
1120
+ await connectAsFollower(ctx);
1121
+ },
1122
+ });
1123
+ }).finally(() => {
1124
+ if (pinetReloadPromise === reloadWork) {
1125
+ pinetReloadPromise = null;
1126
+ }
977
1127
  });
1128
+ pinetReloadPromise = reloadWork;
1129
+ await reloadWork;
978
1130
  }
979
1131
  // ─── Tools ──────────────────────────────────────────
980
1132
  const toolRegistrationRuntime = createToolRegistrationRuntime({
1133
+ buildPromptGuidelines: agentPromptGuidance.buildPromptGuidelines,
981
1134
  slackTools: {
982
1135
  getBotToken: () => {
983
1136
  if (!botToken) {
@@ -994,6 +1147,9 @@ export default function (pi) {
994
1147
  getAgentOwnerToken: () => agentOwnerToken,
995
1148
  getLastDmChannel: () => lastDmChannel,
996
1149
  updateBadge,
1150
+ markSubtreeInboxIdsDelivered: (inboxIds) => {
1151
+ subtreeBrokerRuntime.markDelivered(inboxIds);
1152
+ },
997
1153
  resolveUser,
998
1154
  threadContext: singlePlayerRuntime.getThreadContextPort(),
999
1155
  resolveChannel,
@@ -1002,6 +1158,7 @@ export default function (pi) {
1002
1158
  getBotUserId: () => botUserId,
1003
1159
  registerConfirmationRequest,
1004
1160
  pinetDelivery: {
1161
+ isEnabled: () => pinetEnabled,
1005
1162
  isAvailable: () => pinetEnabled && brokerRole !== null,
1006
1163
  sendSlackMessage: async (input) => {
1007
1164
  const content = {
@@ -1093,6 +1250,21 @@ export default function (pi) {
1093
1250
  ralphSnoozeStatus: () => currentRuntimeMode === "broker" ? brokerRuntime.getRalphSnoozeStatus() : null,
1094
1251
  snoozeRalphLoop: (input) => brokerRuntime.snoozeRalphLoop(input),
1095
1252
  clearRalphSnooze: () => brokerRuntime.clearRalphSnooze(),
1253
+ runHibernateCommand: ({ target, reason }) => runHibernationCommand("hibernate", target, reason),
1254
+ runWakeCommand: ({ target, reason }) => runHibernationCommand("wake", target, reason),
1255
+ getAgentLifecycleProjection: (agentIds) => {
1256
+ if (brokerRole !== "broker")
1257
+ return [];
1258
+ const db = getActiveBrokerDb();
1259
+ if (!db)
1260
+ return [];
1261
+ const hib = resolveHibernationSettings(settings);
1262
+ return collectAgentLifecycleStatuses(db, {
1263
+ ...(agentIds ? { agentIds } : {}),
1264
+ maxConcurrentWakes: hib.maxConcurrentWakes,
1265
+ maxConcurrentWakesPerRepo: hib.maxConcurrentWakesPerRepo,
1266
+ });
1267
+ },
1096
1268
  },
1097
1269
  iMessageTools: {
1098
1270
  pinetEnabled: () => pinetEnabled,
@@ -1122,11 +1294,16 @@ export default function (pi) {
1122
1294
  });
1123
1295
  toolRegistrationRuntime.register(pi);
1124
1296
  // ─── Commands ───────────────────────────────────────
1125
- async function connectAsBroker(ctx) {
1126
- refreshSettings();
1297
+ async function connectAsBroker(ctx, options = {}, signal) {
1298
+ signal?.throwIfAborted();
1299
+ setExtStatus(ctx, "reconnecting");
1300
+ if (options.refreshSettings !== false) {
1301
+ refreshSettings();
1302
+ }
1127
1303
  maybeWarnSlackUserAccess(ctx);
1128
1304
  maybeWarnSlackGuardrailPosture(ctx);
1129
- const { botUserId: brokerBotUserId, recoveredBrokerMessages, recoveredTargetedBacklogCount, releasedBrokerClaims, } = await brokerRuntime.connect(ctx);
1305
+ const { botUserId: brokerBotUserId, recoveredBrokerMessages, recoveredTargetedBacklogCount, releasedBrokerClaims, } = await brokerRuntime.connect(ctx, signal);
1306
+ signal?.throwIfAborted();
1130
1307
  const broker = brokerRuntime.getBroker();
1131
1308
  if (!broker) {
1132
1309
  throw new Error("Broker runtime failed to initialize.");
@@ -1148,8 +1325,18 @@ export default function (pi) {
1148
1325
  else {
1149
1326
  try {
1150
1327
  const imessageAdapter = createIMessageAdapter();
1151
- await imessageAdapter.connect();
1152
- broker.addAdapter(imessageAdapter);
1328
+ const abortConnect = () => {
1329
+ void imessageAdapter.disconnect().catch(() => undefined);
1330
+ };
1331
+ signal?.addEventListener("abort", abortConnect, { once: true });
1332
+ try {
1333
+ await imessageAdapter.connect();
1334
+ signal?.throwIfAborted();
1335
+ broker.addAdapter(imessageAdapter);
1336
+ }
1337
+ finally {
1338
+ signal?.removeEventListener("abort", abortConnect);
1339
+ }
1153
1340
  if (environment.blockers.length > 0) {
1154
1341
  ctx.ui.notify(`iMessage send-first mode enabled — ${readinessSummary}`, "warning");
1155
1342
  brokerRuntime.logActivity({
@@ -1173,11 +1360,13 @@ export default function (pi) {
1173
1360
  }
1174
1361
  }
1175
1362
  }
1363
+ signal?.throwIfAborted();
1176
1364
  broker.server.setOutboundMessageAdapters?.(broker.adapters);
1177
1365
  brokerRole = "broker";
1178
1366
  pinetEnabled = true;
1179
1367
  desiredAgentStatus = "idle";
1180
1368
  currentRuntimeMode = "broker";
1369
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
1181
1370
  if (recoveredBrokerMessages > 0 || releasedBrokerClaims > 0) {
1182
1371
  const recoveredTargetedDetail = recoveredTargetedBacklogCount > 0
1183
1372
  ? ` including ${recoveredTargetedBacklogCount} recovered targeted backlog item${recoveredTargetedBacklogCount === 1 ? "" : "s"}`
@@ -1185,7 +1374,6 @@ export default function (pi) {
1185
1374
  ctx.ui.notify(`Pinet broker recovered ${recoveredBrokerMessages} pending message${recoveredBrokerMessages === 1 ? "" : "s"}${recoveredTargetedDetail} and released ${releasedBrokerClaims} broker-owned thread claim${releasedBrokerClaims === 1 ? "" : "s"}`, "info");
1186
1375
  }
1187
1376
  brokerRuntime.startObservability(ctx);
1188
- setExtStatus(ctx, "ok");
1189
1377
  brokerRuntime.logActivity({
1190
1378
  kind: "broker_started",
1191
1379
  level: "actions",
@@ -1233,6 +1421,11 @@ export default function (pi) {
1233
1421
  },
1234
1422
  getAgentMetadata,
1235
1423
  applyRegistrationIdentity,
1424
+ onRegistrationIdentityApplied: async () => {
1425
+ if (currentRuntimeMode === "follower") {
1426
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
1427
+ }
1428
+ },
1236
1429
  persistState,
1237
1430
  updateBadge,
1238
1431
  maybeDrainInboxIfIdle,
@@ -1240,6 +1433,7 @@ export default function (pi) {
1240
1433
  deferControlAck: deferFollowerControlAck,
1241
1434
  runRemoteControl,
1242
1435
  deliverFollowUpMessage,
1436
+ deliverSteeringMessage,
1243
1437
  setExtStatus,
1244
1438
  getRuntimeDiagnostic: () => followerRuntimeDiagnostic,
1245
1439
  setRuntimeDiagnostic: (diagnostic) => {
@@ -1292,7 +1486,15 @@ export default function (pi) {
1292
1486
  lastBrokerControlPlaneHomeTabRefreshAt: () => brokerRuntime.getLastHomeTabRefreshAt(),
1293
1487
  lastBrokerControlPlaneHomeTabError: () => brokerRuntime.getLastHomeTabError(),
1294
1488
  subtreeBrokerStatus: () => subtreeBrokerRuntime.getStatus(),
1489
+ inspectGlobalBroker: async () => {
1490
+ const lock = inspectBrokerLock();
1491
+ if (lock.state !== "alive") {
1492
+ return { lock, probe: null };
1493
+ }
1494
+ return { lock, probe: await probeBrokerSocket() };
1495
+ },
1295
1496
  getPinetRegistrationBlockReason: pinetRegistrationGate.getBlockReason,
1497
+ replaceGlobalBroker: async () => replaceBrokerOwner({ meshSecret: resolveLocalMeshSecretValue() }),
1296
1498
  connectAsBroker: (ctx) => transitionToRuntimeMode(ctx, "broker"),
1297
1499
  connectAsFollower: (ctx) => transitionToRuntimeMode(ctx, "follower"),
1298
1500
  reloadPinetRuntime,
@@ -1302,21 +1504,27 @@ export default function (pi) {
1302
1504
  spawnSubtreeWorker: (ctx, input) => subtreeBrokerRuntime.spawnWorker(ctx, input),
1303
1505
  sendPinetAgentMessage,
1304
1506
  signalAgentFree,
1305
- applyLocalAgentIdentity,
1507
+ applyLocalAgentIdentity: async (name, emoji, personality) => {
1508
+ applyLocalAgentIdentity(name, emoji, personality);
1509
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
1510
+ },
1306
1511
  setExtStatus,
1307
1512
  setExtCtx: sessionUiRuntime.setExtCtx,
1308
1513
  },
1309
1514
  });
1310
1515
  commandRegistrationRuntime.register(pi);
1311
- async function connectAsFollower(ctx) {
1516
+ async function connectAsFollower(ctx, signal) {
1517
+ signal?.throwIfAborted();
1312
1518
  pinetRegistrationGate.assertCanRegister();
1313
- const clientRef = await followerRuntime.connect(ctx);
1519
+ const clientRef = await followerRuntime.connect(ctx, signal);
1520
+ signal?.throwIfAborted();
1314
1521
  brokerClient = clientRef;
1315
1522
  followerRuntimeDiagnostic = null;
1316
1523
  brokerRole = "follower";
1317
1524
  pinetEnabled = true;
1318
1525
  desiredAgentStatus = "idle";
1319
1526
  currentRuntimeMode = "follower";
1527
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
1320
1528
  setExtStatus(ctx, "ok");
1321
1529
  }
1322
1530
  async function disconnectFollower(ctx, options = {}) {
@@ -1326,6 +1534,7 @@ export default function (pi) {
1326
1534
  brokerRole = null;
1327
1535
  pinetEnabled = false;
1328
1536
  currentRuntimeMode = "off";
1537
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
1329
1538
  if (!options.preserveErrorState) {
1330
1539
  followerRuntimeDiagnostic = null;
1331
1540
  setExtStatus(ctx, "off");
@@ -1334,6 +1543,8 @@ export default function (pi) {
1334
1543
  }
1335
1544
  // ─── Lifecycle ──────────────────────────────────────
1336
1545
  pi.on("session_start", async (_event, ctx) => {
1546
+ await cancelPendingStartup();
1547
+ compactionGate.reset();
1337
1548
  singlePlayerRuntime.resetShutdownState();
1338
1549
  slackRequestRuntime.reset();
1339
1550
  resetRemoteControlState();
@@ -1346,43 +1557,65 @@ export default function (pi) {
1346
1557
  console.log("[slack-bridge] detected local subagent context; skipping Pinet registration");
1347
1558
  currentRuntimeMode = "off";
1348
1559
  setExtStatus(ctx, "off");
1560
+ await toolRegistrationRuntime.sync(pi, currentRuntimeMode);
1349
1561
  return;
1350
1562
  }
1351
1563
  refreshSettings();
1352
1564
  maybeWarnSlackUserAccess(ctx);
1353
1565
  const startupMode = resolveSlackBridgeStartupRuntimeMode(settings, {
1354
- brokerSocketExists: fs.existsSync(DEFAULT_SOCKET_PATH),
1566
+ brokerSocketExists: fs.existsSync(resolveBrokerSocketPath()),
1355
1567
  brokerManagedFollowerLaunch: isBrokerManagedFollowerLaunch(),
1356
1568
  });
1357
- try {
1569
+ if (startupMode === "off") {
1358
1570
  await transitionToRuntimeMode(ctx, startupMode);
1359
- if (startupMode === "single") {
1360
- maybeWarnSlackGuardrailPosture(ctx);
1361
- console.log("[slack-bridge] runtime mode: single");
1571
+ return;
1572
+ }
1573
+ await toolRegistrationRuntime.sync(pi, "off");
1574
+ setExtStatus(ctx, "reconnecting");
1575
+ const controller = new AbortController();
1576
+ const startupPromise = (async () => {
1577
+ try {
1578
+ await runRuntimeModeTransition(ctx, startupMode, controller.signal);
1579
+ if (startupMode === "single") {
1580
+ maybeWarnSlackGuardrailPosture(ctx);
1581
+ }
1582
+ console.log(`[slack-bridge] runtime mode: ${startupMode}`);
1362
1583
  }
1363
- else if (startupMode === "follower") {
1364
- console.log("[slack-bridge] runtime mode: follower");
1584
+ catch (error) {
1585
+ const cancelled = controller.signal.aborted || isAbortError(error);
1586
+ if (!cancelled) {
1587
+ console.error(`[slack-bridge] runtime start (${startupMode}) failed: ${msg(error)}`);
1588
+ }
1589
+ await runPinetLifecycle(() => stopPinetRuntime(ctx, { releaseIdentity: !cancelled })).catch((cleanupError) => {
1590
+ console.error(`[slack-bridge] runtime cleanup failed: ${msg(cleanupError)}`);
1591
+ });
1592
+ slackRequestRuntime.reset();
1593
+ singlePlayerRuntime.resetShutdownState();
1365
1594
  }
1366
- else if (startupMode === "broker") {
1367
- console.log("[slack-bridge] runtime mode: broker");
1595
+ finally {
1596
+ if (pendingStartup?.controller === controller)
1597
+ pendingStartup = null;
1368
1598
  }
1369
- }
1370
- catch (err) {
1371
- console.error(`[slack-bridge] runtime start (${startupMode}) failed: ${msg(err)}`);
1372
- currentRuntimeMode = "off";
1373
- setExtStatus(ctx, "off");
1374
- }
1599
+ })().catch((error) => {
1600
+ console.error(`[slack-bridge] background runtime start failed: ${msg(error)}`);
1601
+ });
1602
+ pendingStartup = { controller, promise: startupPromise };
1375
1603
  });
1376
1604
  // ─── Agent event wiring ──────────────────────────────
1377
1605
  agentEventRuntime.register(pi);
1378
- pi.on("session_compact", async (_event, ctx) => {
1379
- maybeDrainInboxIfIdle(ctx);
1606
+ pi.on("session_before_compact", (event) => {
1607
+ compactionGate.begin(event.signal);
1608
+ });
1609
+ pi.on("session_compact", () => {
1610
+ compactionGate.end();
1380
1611
  });
1381
1612
  pi.on("session_shutdown", async (_event, ctx) => {
1613
+ compactionGate.reset();
1382
1614
  resetRemoteControlState();
1383
1615
  resetPendingRemoteControlAcks();
1384
1616
  sessionUiRuntime.cleanupForSessionShutdown();
1385
- await stopPinetRuntime(ctx, { releaseIdentity: true });
1617
+ await cancelPendingStartup();
1618
+ await runPinetLifecycle(() => stopPinetRuntime(ctx, { releaseIdentity: true }));
1386
1619
  pinetRegistrationGate.reset();
1387
1620
  });
1388
1621
  }