@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.
- package/README.md +316 -451
- package/dist/activity-log.d.ts +10 -3
- package/dist/agent-event-runtime.d.ts +0 -2
- package/dist/agent-event-runtime.js +0 -1
- package/dist/agent-prompt-guidance.d.ts +1 -6
- package/dist/agent-prompt-guidance.js +1 -6
- package/dist/broker/adapters/slack.d.ts +4 -1
- package/dist/broker/adapters/slack.js +16 -9
- package/dist/broker/client.d.ts +21 -4
- package/dist/broker/client.js +65 -25
- package/dist/broker/hibernation-activation-authority.d.ts +22 -0
- package/dist/broker/hibernation-activation-authority.js +51 -0
- package/dist/broker/hibernation-activation.d.ts +75 -0
- package/dist/broker/hibernation-activation.js +95 -0
- package/dist/broker/hibernation-command-router.d.ts +54 -0
- package/dist/broker/hibernation-command-router.js +124 -0
- package/dist/broker/hibernation-runtime-adapters.d.ts +79 -0
- package/dist/broker/hibernation-runtime-adapters.js +473 -0
- package/dist/broker/hibernation-runtime-helpers.d.ts +146 -0
- package/dist/broker/hibernation-runtime-helpers.js +243 -0
- package/dist/broker/index.d.ts +16 -1
- package/dist/broker/index.js +68 -10
- package/dist/broker/lock-conflict.d.ts +109 -0
- package/dist/broker/lock-conflict.js +409 -0
- package/dist/broker/socket-server.d.ts +26 -0
- package/dist/broker/socket-server.js +287 -4
- package/dist/broker-delivery.d.ts +1 -0
- package/dist/broker-delivery.js +10 -1
- package/dist/broker-runtime.d.ts +14 -2
- package/dist/broker-runtime.js +114 -3
- package/dist/canvases.d.ts +12 -2
- package/dist/canvases.js +1 -3
- package/dist/compaction-gate.d.ts +7 -0
- package/dist/compaction-gate.js +35 -0
- package/dist/deploy-manifest.d.ts +8 -2
- package/dist/deploy-manifest.js +9 -7
- package/dist/follower-runtime.d.ts +5 -2
- package/dist/follower-runtime.js +39 -5
- package/dist/git-metadata.d.ts +5 -9
- package/dist/git-metadata.js +23 -42
- package/dist/guardrails.js +5 -0
- package/dist/helpers.d.ts +77 -27
- package/dist/helpers.js +150 -39
- package/dist/hibernation-config.d.ts +13 -0
- package/dist/hibernation-config.js +14 -0
- package/dist/home-tab.d.ts +13 -2
- package/dist/imessage-tools.d.ts +4 -1
- package/dist/inbox-drain-runtime.d.ts +1 -0
- package/dist/inbox-drain-runtime.js +5 -1
- package/dist/index.js +341 -108
- package/dist/pinet-commands.d.ts +14 -1
- package/dist/pinet-commands.js +93 -22
- package/dist/pinet-mesh-ops.d.ts +6 -4
- package/dist/pinet-mesh-ops.js +9 -1
- package/dist/pinet-runtime-composition.d.ts +2 -1
- package/dist/pinet-runtime-composition.js +14 -3
- package/dist/pinet-session-formatting.d.ts +26 -2
- package/dist/pinet-session-formatting.js +2 -0
- package/dist/pinet-tools.d.ts +14 -0
- package/dist/pinet-tools.js +355 -50
- package/dist/prompts/broker/default.md +3 -3
- package/dist/prompts/broker/tmux.md +3 -3
- package/dist/runtime-agent-context.d.ts +3 -3
- package/dist/runtime-agent-context.js +12 -6
- package/dist/runtime-mode.js +4 -0
- package/dist/session-ui-runtime.js +3 -1
- package/dist/single-player-runtime.d.ts +1 -1
- package/dist/single-player-runtime.js +21 -5
- package/dist/slack-access.d.ts +10 -2
- package/dist/slack-access.js +64 -22
- package/dist/slack-block-kit.d.ts +44 -2
- package/dist/slack-block-kit.js +5 -5
- package/dist/slack-file-access.d.ts +12 -1
- package/dist/slack-file-access.js +4 -4
- package/dist/slack-message-context.d.ts +9 -2
- package/dist/slack-modals.d.ts +4 -1
- package/dist/slack-modals.js +5 -5
- package/dist/slack-pinet-runtime-adapter.d.ts +1 -0
- package/dist/slack-pinet-runtime-adapter.js +10 -0
- package/dist/slack-presence.d.ts +5 -4
- package/dist/slack-request-runtime.d.ts +3 -2
- package/dist/slack-scope-diagnostics.d.ts +27 -2
- package/dist/slack-socket-dedup.d.ts +8 -5
- package/dist/slack-tools.d.ts +10 -0
- package/dist/slack-tools.js +51 -64
- package/dist/slack-upload.d.ts +17 -1
- package/dist/subtree-broker-runtime.d.ts +83 -2
- package/dist/subtree-broker-runtime.js +774 -109
- package/dist/tool-output-limits.d.ts +11 -0
- package/dist/tool-output-limits.js +20 -0
- package/dist/tool-registration-runtime.d.ts +3 -0
- package/dist/tool-registration-runtime.js +27 -0
- package/package.json +8 -8
package/dist/activity-log.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface ActivityLogEntry {
|
|
|
18
18
|
export interface LoggedActivityLogEntry extends ActivityLogEntry {
|
|
19
19
|
timestamp: string;
|
|
20
20
|
}
|
|
21
|
+
export type ActivityLogSlackBlock = Record<string, unknown>;
|
|
22
|
+
export interface ActivityLogPostMessageBody extends Record<string, unknown> {
|
|
23
|
+
channel: string;
|
|
24
|
+
text: string;
|
|
25
|
+
blocks: ActivityLogSlackBlock[];
|
|
26
|
+
thread_ts?: string;
|
|
27
|
+
}
|
|
21
28
|
export interface SlackActivityLoggerDeps {
|
|
22
29
|
getBotToken: () => string | undefined;
|
|
23
30
|
getLogChannel: () => string | undefined;
|
|
@@ -25,7 +32,7 @@ export interface SlackActivityLoggerDeps {
|
|
|
25
32
|
getAgentName: () => string;
|
|
26
33
|
getAgentEmoji: () => string;
|
|
27
34
|
resolveChannel: (nameOrId: string) => Promise<string>;
|
|
28
|
-
slack: (method: string, token: string, body?:
|
|
35
|
+
slack: (method: string, token: string, body?: ActivityLogPostMessageBody) => Promise<SlackResult>;
|
|
29
36
|
onError?: (error: unknown) => void;
|
|
30
37
|
now?: () => Date;
|
|
31
38
|
maxRecentEntries?: number;
|
|
@@ -34,10 +41,10 @@ export declare function normalizeActivityLogLevel(value: string | undefined): Ac
|
|
|
34
41
|
export declare function shouldLogActivity(configuredLevel: ActivityLogLevel, eventLevel: ActivityLogLevel): boolean;
|
|
35
42
|
export declare function redactSensitiveText(value: string): string;
|
|
36
43
|
export declare function buildActivityLogText(agentName: string, agentEmoji: string, entry: LoggedActivityLogEntry): string;
|
|
37
|
-
export declare function buildActivityLogBlocks(agentName: string, agentEmoji: string, entry: LoggedActivityLogEntry):
|
|
44
|
+
export declare function buildActivityLogBlocks(agentName: string, agentEmoji: string, entry: LoggedActivityLogEntry): ActivityLogSlackBlock[];
|
|
38
45
|
export declare function buildActivityLogThreadHeader(agentName: string, agentEmoji: string, dateKey: string): {
|
|
39
46
|
text: string;
|
|
40
|
-
blocks:
|
|
47
|
+
blocks: ActivityLogSlackBlock[];
|
|
41
48
|
};
|
|
42
49
|
export declare function formatRecentActivityLogEntries(entries: ReadonlyArray<LoggedActivityLogEntry>): string;
|
|
43
50
|
export declare class SlackActivityLogger {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AgentCompletionRuntime } from "./agent-completion-runtime.js";
|
|
3
|
-
import type { AgentPromptGuidance } from "./agent-prompt-guidance.js";
|
|
4
3
|
import { type SlackToolPolicyRuntime, type SlackToolPolicyRuntimeDeps } from "./slack-tool-policy-runtime.js";
|
|
5
4
|
export interface AgentEventRuntimeDeps extends SlackToolPolicyRuntimeDeps {
|
|
6
|
-
beforeAgentStart: AgentPromptGuidance["beforeAgentStart"];
|
|
7
5
|
onCompletionAgentEnd: AgentCompletionRuntime["onAgentEnd"];
|
|
8
6
|
setDeliverTrackedSlackFollowUpMessage: (deliver: SlackToolPolicyRuntime["deliverTrackedSlackFollowUpMessage"]) => void;
|
|
9
7
|
}
|
|
@@ -18,7 +18,6 @@ export function createAgentEventRuntime(deps) {
|
|
|
18
18
|
pi.on("turn_end", slackToolPolicyRuntime.onTurnEnd);
|
|
19
19
|
pi.on("agent_end", slackToolPolicyRuntime.onAgentEnd);
|
|
20
20
|
pi.on("tool_call", slackToolPolicyRuntime.onToolCall);
|
|
21
|
-
pi.on("before_agent_start", deps.beforeAgentStart);
|
|
22
21
|
pi.on("agent_end", deps.onCompletionAgentEnd);
|
|
23
22
|
}
|
|
24
23
|
return {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { type BrokerPromptLoadResult } from "./broker-prompt-loader.js";
|
|
2
|
-
export interface BeforeAgentStartEvent {
|
|
3
|
-
systemPrompt: string;
|
|
4
|
-
}
|
|
5
2
|
export interface AgentPromptGuidanceDeps {
|
|
6
3
|
getIdentityGuidelines: () => string[];
|
|
7
4
|
getAgentName: () => string;
|
|
@@ -15,8 +12,6 @@ export interface AgentPromptGuidanceDeps {
|
|
|
15
12
|
reportBrokerPromptDiagnostic?: (diagnostic: string) => void;
|
|
16
13
|
}
|
|
17
14
|
export interface AgentPromptGuidance {
|
|
18
|
-
|
|
19
|
-
systemPrompt: string;
|
|
20
|
-
}>;
|
|
15
|
+
buildPromptGuidelines: () => Promise<string[]>;
|
|
21
16
|
}
|
|
22
17
|
export declare function createAgentPromptGuidance(deps: AgentPromptGuidanceDeps): AgentPromptGuidance;
|
|
@@ -35,12 +35,7 @@ export function createAgentPromptGuidance(deps) {
|
|
|
35
35
|
}
|
|
36
36
|
return guidelines;
|
|
37
37
|
}
|
|
38
|
-
async function beforeAgentStart(event) {
|
|
39
|
-
return {
|
|
40
|
-
systemPrompt: event.systemPrompt + "\n\n" + (await buildPromptGuidelines()).join("\n"),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
38
|
return {
|
|
44
|
-
|
|
39
|
+
buildPromptGuidelines,
|
|
45
40
|
};
|
|
46
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ParsedAppHomeOpened, type ParsedSlashCommand, type ParsedThreadStarted } from "../../slack-access.js";
|
|
1
|
+
import { type SlackSocketErrorSource, type ParsedAppHomeOpened, type ParsedSlashCommand, type ParsedThreadStarted } from "../../slack-access.js";
|
|
2
2
|
import { type SlackIngressGuardSettings } from "../../helpers.js";
|
|
3
3
|
import { type ReactionCommandSettings } from "../../reaction-triggers.js";
|
|
4
4
|
import type { AdapterCapabilityRequest, AdapterCapabilityResult, InboundMessage, OutboundMessage, MessageAdapter } from "./types.js";
|
|
@@ -41,6 +41,9 @@ export interface SlackAdapterConfig {
|
|
|
41
41
|
onAppHomeOpened?: (event: ParsedAppHomeOpened) => Promise<void> | void;
|
|
42
42
|
/** Best-effort callback for Slack slash commands handled by the broker process. */
|
|
43
43
|
onSlashCommand?: (event: ParsedSlashCommand) => Promise<string | null> | string | null;
|
|
44
|
+
onSocketOpen?: () => void;
|
|
45
|
+
onSocketReconnectScheduled?: () => void;
|
|
46
|
+
onSocketError?: (message: string, source: SlackSocketErrorSource) => void;
|
|
44
47
|
}
|
|
45
48
|
export declare const SLACK_THREAD_CACHE_MAX_SIZE = 5000;
|
|
46
49
|
export declare const SLACK_THREAD_CACHE_TTL_MS: number;
|
|
@@ -69,6 +69,8 @@ export class SlackAdapter {
|
|
|
69
69
|
appToken: this.config.appToken,
|
|
70
70
|
dedup: this.processedSocketDeliveries,
|
|
71
71
|
abortAndWait: () => this.slackRequests.abortAndWait(),
|
|
72
|
+
onOpen: () => this.config.onSocketOpen?.(),
|
|
73
|
+
onReconnectScheduled: () => this.config.onSocketReconnectScheduled?.(),
|
|
72
74
|
onThreadStarted: (event) => this.onThreadStarted(event),
|
|
73
75
|
onThreadContextChanged: (event) => this.onContextChanged(event),
|
|
74
76
|
onMessage: (event) => this.onMessage(event),
|
|
@@ -77,9 +79,9 @@ export class SlackAdapter {
|
|
|
77
79
|
onAppHomeOpened: (event) => this.onAppHomeOpened(event),
|
|
78
80
|
onInteractive: (event) => this.emitInteractiveInbound(event),
|
|
79
81
|
onSlashCommand: (event) => this.onSlashCommand(event),
|
|
80
|
-
onError: (error) => {
|
|
82
|
+
onError: (error, source) => {
|
|
81
83
|
if (!isAbortError(error)) {
|
|
82
|
-
|
|
84
|
+
this.config.onSocketError?.(errorMsg(error), source);
|
|
83
85
|
}
|
|
84
86
|
},
|
|
85
87
|
});
|
|
@@ -88,14 +90,19 @@ export class SlackAdapter {
|
|
|
88
90
|
}
|
|
89
91
|
async disconnect() {
|
|
90
92
|
this.shuttingDown = true;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
try {
|
|
94
|
+
await this.threadStatuses.clearAll();
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
const socketMode = this.socketMode;
|
|
98
|
+
this.socketMode = null;
|
|
99
|
+
if (socketMode) {
|
|
100
|
+
await socketMode.disconnect();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await this.slackRequests.abortAndWait();
|
|
104
|
+
}
|
|
97
105
|
}
|
|
98
|
-
await this.slackRequests.abortAndWait();
|
|
99
106
|
}
|
|
100
107
|
onInbound(handler) {
|
|
101
108
|
this.inboundHandler = handler;
|
package/dist/broker/client.d.ts
CHANGED
|
@@ -34,13 +34,23 @@ export interface ScheduledWakeupInfo {
|
|
|
34
34
|
export type { PortLeaseAcquireInput, PortLeaseInfo, PortLeaseListOptions, PortLeaseReleaseInput, PortLeaseRenewInput, PinetLaneInfo, PinetLaneListOptions, PinetLaneParticipantInfo, PinetLaneParticipantUpsertInput, PinetLaneUpsertInput, };
|
|
35
35
|
export declare const DEFAULT_SOCKET_PATH: string;
|
|
36
36
|
export declare const REQUEST_TIMEOUT_MS = 5000;
|
|
37
|
-
export declare const RECONNECT_DELAY_MS = 3000;
|
|
38
37
|
export declare const INITIAL_RECONNECT_DELAY_MS = 1000;
|
|
39
38
|
export declare const MAX_RECONNECT_DELAY_MS = 30000;
|
|
40
39
|
export declare const HEARTBEAT_INTERVAL_MS = 5000;
|
|
41
40
|
export declare const HEARTBEAT_METADATA_PROVIDER_TIMEOUT_MS = 2500;
|
|
42
|
-
/** Compute reconnect delay with exponential backoff and jitter. */
|
|
41
|
+
/** Compute reconnect delay with exponential backoff and jitter (±25%). */
|
|
43
42
|
export declare function computeReconnectDelay(attempt: number, random?: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* A single-use, broker-issued wake fence presented by a runtime that the broker
|
|
45
|
+
* cold-launched to revive a hibernated identity. Consumed on the first
|
|
46
|
+
* successful register and never replayed on reconnect.
|
|
47
|
+
*/
|
|
48
|
+
interface WakeFence {
|
|
49
|
+
wakeLeaseId: string;
|
|
50
|
+
fenceToken: number;
|
|
51
|
+
runtimeGeneration: number;
|
|
52
|
+
reservationNonce: string;
|
|
53
|
+
}
|
|
44
54
|
export interface AgentBroadcastResult {
|
|
45
55
|
channel: string;
|
|
46
56
|
messageIds: number[];
|
|
@@ -59,11 +69,18 @@ export interface BrokerClientAuthOptions {
|
|
|
59
69
|
meshSecret?: string;
|
|
60
70
|
meshSecretPath?: string;
|
|
61
71
|
}
|
|
72
|
+
export interface BrokerClientTimingOptions {
|
|
73
|
+
/** @internal Allows tests to exercise reconnect behavior without waiting for production backoff. */
|
|
74
|
+
reconnectDelayMs?: (attempt: number) => number;
|
|
75
|
+
}
|
|
76
|
+
export type BrokerClientOptions = BrokerConnectOpts & BrokerClientAuthOptions & BrokerClientTimingOptions;
|
|
62
77
|
export declare class BrokerClient {
|
|
63
78
|
private readonly connectOpts;
|
|
64
79
|
private readonly meshSecret;
|
|
65
80
|
private readonly meshSecretPath;
|
|
81
|
+
private readonly reconnectDelayMs;
|
|
66
82
|
private socket;
|
|
83
|
+
private connectingSocket;
|
|
67
84
|
private connected;
|
|
68
85
|
private shuttingDown;
|
|
69
86
|
private reconnectTimer;
|
|
@@ -79,14 +96,14 @@ export declare class BrokerClient {
|
|
|
79
96
|
private nextId;
|
|
80
97
|
private readonly pending;
|
|
81
98
|
private buffer;
|
|
82
|
-
constructor(opts?: string |
|
|
99
|
+
constructor(opts?: string | BrokerClientOptions);
|
|
83
100
|
connect(): Promise<void>;
|
|
84
101
|
disconnect(): void;
|
|
85
102
|
disconnectGracefully(): Promise<void>;
|
|
86
103
|
isConnected(): boolean;
|
|
87
104
|
private resolveMeshSecret;
|
|
88
105
|
private authenticateIfNeeded;
|
|
89
|
-
register(name: string, emoji: string, metadata?: Record<string, unknown>, stableId?: string): Promise<{
|
|
106
|
+
register(name: string, emoji: string, metadata?: Record<string, unknown>, stableId?: string, wakeFence?: WakeFence): Promise<{
|
|
90
107
|
agentId: string;
|
|
91
108
|
name: string;
|
|
92
109
|
emoji: string;
|
package/dist/broker/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as net from "node:net";
|
|
2
|
+
import { computeBackoffDelay, withTimeout } from "@pinet/transport-core/async";
|
|
2
3
|
import { readMeshSecret } from "./auth.js";
|
|
3
4
|
import { DEFAULT_SOCKET_PATH as PINET_DEFAULT_SOCKET_PATH } from "./paths.js";
|
|
4
5
|
import { assertLoopbackTcpHost } from "./raw-tcp-loopback.js";
|
|
@@ -6,31 +7,17 @@ import { RPC_AGENT_NAME_CONFLICT, RPC_METHOD_NOT_FOUND } from "./types.js";
|
|
|
6
7
|
// ─── Constants (exported for testing) ────────────────────
|
|
7
8
|
export const DEFAULT_SOCKET_PATH = PINET_DEFAULT_SOCKET_PATH;
|
|
8
9
|
export const REQUEST_TIMEOUT_MS = 5000;
|
|
9
|
-
export const RECONNECT_DELAY_MS = 3000;
|
|
10
10
|
export const INITIAL_RECONNECT_DELAY_MS = 1000;
|
|
11
11
|
export const MAX_RECONNECT_DELAY_MS = 30000;
|
|
12
12
|
export const HEARTBEAT_INTERVAL_MS = 5000;
|
|
13
13
|
export const HEARTBEAT_METADATA_PROVIDER_TIMEOUT_MS = 2500;
|
|
14
|
-
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const timer = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
17
|
-
timer.unref?.();
|
|
18
|
-
promise.then((value) => {
|
|
19
|
-
clearTimeout(timer);
|
|
20
|
-
resolve(value);
|
|
21
|
-
}, (error) => {
|
|
22
|
-
clearTimeout(timer);
|
|
23
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
/** Compute reconnect delay with exponential backoff and jitter. */
|
|
14
|
+
/** Compute reconnect delay with exponential backoff and jitter (±25%). */
|
|
28
15
|
export function computeReconnectDelay(attempt, random = Math.random()) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
16
|
+
return computeBackoffDelay(attempt, {
|
|
17
|
+
initialMs: INITIAL_RECONNECT_DELAY_MS,
|
|
18
|
+
maxMs: MAX_RECONNECT_DELAY_MS,
|
|
19
|
+
random,
|
|
20
|
+
});
|
|
34
21
|
}
|
|
35
22
|
function createRpcRequestError(method, error) {
|
|
36
23
|
const err = new Error(error.message);
|
|
@@ -78,7 +65,9 @@ export class BrokerClient {
|
|
|
78
65
|
connectOpts;
|
|
79
66
|
meshSecret;
|
|
80
67
|
meshSecretPath;
|
|
68
|
+
reconnectDelayMs;
|
|
81
69
|
socket = null;
|
|
70
|
+
connectingSocket = null;
|
|
82
71
|
connected = false;
|
|
83
72
|
shuttingDown = false;
|
|
84
73
|
reconnectTimer = null;
|
|
@@ -95,6 +84,10 @@ export class BrokerClient {
|
|
|
95
84
|
pending = new Map();
|
|
96
85
|
buffer = "";
|
|
97
86
|
constructor(opts) {
|
|
87
|
+
this.reconnectDelayMs =
|
|
88
|
+
typeof opts === "object" && opts !== null
|
|
89
|
+
? (opts.reconnectDelayMs ?? computeReconnectDelay)
|
|
90
|
+
: computeReconnectDelay;
|
|
98
91
|
if (opts === undefined) {
|
|
99
92
|
this.connectOpts = { path: DEFAULT_SOCKET_PATH };
|
|
100
93
|
this.meshSecret = null;
|
|
@@ -139,7 +132,10 @@ export class BrokerClient {
|
|
|
139
132
|
}
|
|
140
133
|
this.stopHeartbeat();
|
|
141
134
|
this.rejectAllPending(new Error("Client disconnected"));
|
|
135
|
+
const connectingSocket = this.connectingSocket;
|
|
136
|
+
this.connectingSocket = null;
|
|
142
137
|
try {
|
|
138
|
+
connectingSocket?.destroy();
|
|
143
139
|
this.socket?.destroy();
|
|
144
140
|
}
|
|
145
141
|
catch {
|
|
@@ -198,13 +194,14 @@ export class BrokerClient {
|
|
|
198
194
|
}
|
|
199
195
|
}
|
|
200
196
|
// ─── Registration ────────────────────────────────────
|
|
201
|
-
async register(name, emoji, metadata, stableId) {
|
|
197
|
+
async register(name, emoji, metadata, stableId, wakeFence) {
|
|
202
198
|
this.registrationSnapshot = {
|
|
203
199
|
name,
|
|
204
200
|
emoji,
|
|
205
201
|
...(metadata ? { metadata } : {}),
|
|
206
202
|
...(stableId ? { stableId } : {}),
|
|
207
203
|
...(name.trim().length === 0 ? { brokerAssignedIdentity: true } : {}),
|
|
204
|
+
...(wakeFence ? { wakeFence } : {}),
|
|
208
205
|
};
|
|
209
206
|
return this.performRegister(this.registrationSnapshot);
|
|
210
207
|
}
|
|
@@ -368,7 +365,12 @@ export class BrokerClient {
|
|
|
368
365
|
}
|
|
369
366
|
async searchAgentSessions(options = {}) {
|
|
370
367
|
try {
|
|
371
|
-
|
|
368
|
+
const sessions = (await this.request("agent.sessions.search", options));
|
|
369
|
+
return sessions.map((session) => ({
|
|
370
|
+
...session,
|
|
371
|
+
runtimeKind: session.runtimeKind ?? (session.tmuxSession ? "tmux" : null),
|
|
372
|
+
runtimeLocator: session.runtimeLocator ?? (session.runtimeKind === "herdr" ? null : session.tmuxSession),
|
|
373
|
+
}));
|
|
372
374
|
}
|
|
373
375
|
catch (err) {
|
|
374
376
|
if (isRpcMethodNotFoundError(err, "agent.sessions.search")) {
|
|
@@ -480,7 +482,7 @@ export class BrokerClient {
|
|
|
480
482
|
scheduleReconnect() {
|
|
481
483
|
if (this.shuttingDown || this.reconnectTimer)
|
|
482
484
|
return;
|
|
483
|
-
const delay =
|
|
485
|
+
const delay = this.reconnectDelayMs(this.reconnectAttempt);
|
|
484
486
|
this.reconnectAttempt++;
|
|
485
487
|
this.reconnectTimer = setTimeout(() => {
|
|
486
488
|
this.reconnectTimer = null;
|
|
@@ -491,7 +493,27 @@ export class BrokerClient {
|
|
|
491
493
|
connectSocket() {
|
|
492
494
|
return new Promise((resolve, reject) => {
|
|
493
495
|
const sock = net.createConnection(this.connectOpts);
|
|
496
|
+
this.connectingSocket = sock;
|
|
497
|
+
let settled = false;
|
|
498
|
+
const rejectConnect = (error) => {
|
|
499
|
+
if (settled)
|
|
500
|
+
return;
|
|
501
|
+
settled = true;
|
|
502
|
+
if (this.connectingSocket === sock) {
|
|
503
|
+
this.connectingSocket = null;
|
|
504
|
+
}
|
|
505
|
+
reject(error);
|
|
506
|
+
};
|
|
494
507
|
sock.on("connect", () => {
|
|
508
|
+
if (this.shuttingDown) {
|
|
509
|
+
rejectConnect(new Error("Client disconnected while connecting"));
|
|
510
|
+
sock.destroy();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
settled = true;
|
|
514
|
+
if (this.connectingSocket === sock) {
|
|
515
|
+
this.connectingSocket = null;
|
|
516
|
+
}
|
|
495
517
|
this.socket = sock;
|
|
496
518
|
this.connected = true;
|
|
497
519
|
this.buffer = "";
|
|
@@ -501,6 +523,12 @@ export class BrokerClient {
|
|
|
501
523
|
this.onData(chunk.toString("utf-8"));
|
|
502
524
|
});
|
|
503
525
|
sock.on("close", () => {
|
|
526
|
+
if (this.connectingSocket === sock) {
|
|
527
|
+
this.connectingSocket = null;
|
|
528
|
+
}
|
|
529
|
+
if (!settled) {
|
|
530
|
+
rejectConnect(new Error("Socket closed before connection completed"));
|
|
531
|
+
}
|
|
504
532
|
const wasConnected = this.connected;
|
|
505
533
|
this.connected = false;
|
|
506
534
|
this.socket = null;
|
|
@@ -512,8 +540,8 @@ export class BrokerClient {
|
|
|
512
540
|
}
|
|
513
541
|
});
|
|
514
542
|
sock.on("error", (err) => {
|
|
515
|
-
if (!
|
|
516
|
-
|
|
543
|
+
if (!settled) {
|
|
544
|
+
rejectConnect(err);
|
|
517
545
|
}
|
|
518
546
|
// If already connected, the close event handles cleanup
|
|
519
547
|
});
|
|
@@ -526,14 +554,26 @@ export class BrokerClient {
|
|
|
526
554
|
pid: process.pid,
|
|
527
555
|
...(snapshot.metadata ? { metadata: snapshot.metadata } : {}),
|
|
528
556
|
...(snapshot.stableId ? { stableId: snapshot.stableId } : {}),
|
|
557
|
+
...(snapshot.wakeFence
|
|
558
|
+
? {
|
|
559
|
+
wakeLeaseId: snapshot.wakeFence.wakeLeaseId,
|
|
560
|
+
fenceToken: snapshot.wakeFence.fenceToken,
|
|
561
|
+
runtimeGeneration: snapshot.wakeFence.runtimeGeneration,
|
|
562
|
+
reservationNonce: snapshot.wakeFence.reservationNonce,
|
|
563
|
+
}
|
|
564
|
+
: {}),
|
|
529
565
|
}));
|
|
566
|
+
// Retain a reconnect snapshot but drop the single-use wake fence so a
|
|
567
|
+
// reconnect never replays a consumed generation reservation.
|
|
530
568
|
this.registrationSnapshot = snapshot.brokerAssignedIdentity
|
|
531
569
|
? {
|
|
532
570
|
...snapshot,
|
|
571
|
+
wakeFence: undefined,
|
|
533
572
|
...(result.metadata ? { metadata: result.metadata } : {}),
|
|
534
573
|
}
|
|
535
574
|
: {
|
|
536
575
|
...snapshot,
|
|
576
|
+
wakeFence: undefined,
|
|
537
577
|
name: result.name,
|
|
538
578
|
emoji: result.emoji,
|
|
539
579
|
...(result.metadata ? { metadata: result.metadata } : {}),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture the activation authority from the process-launch environment ONCE and
|
|
3
|
+
* freeze it for the process lifetime. Idempotent: the first call wins, and any
|
|
4
|
+
* later environment or settings mutation cannot change the frozen value. Returns
|
|
5
|
+
* the frozen authority. Call this explicitly at broker start so the freeze point
|
|
6
|
+
* is deterministic; reads via {@link hibernationActivationAuthorized} also
|
|
7
|
+
* freeze lazily on first use.
|
|
8
|
+
*
|
|
9
|
+
* Accepts `1`, `true`, `yes`, or `on` (case-insensitive, trimmed) as authorized;
|
|
10
|
+
* everything else — including unset/empty — is unauthorized (the production
|
|
11
|
+
* default, a strict no-op).
|
|
12
|
+
*/
|
|
13
|
+
export declare function freezeHibernationActivationAuthority(env?: NodeJS.ProcessEnv): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The frozen live-runtime activation authority. Lazily freezes on first read
|
|
16
|
+
* from the process-launch environment when {@link freezeHibernationActivationAuthority}
|
|
17
|
+
* has not run yet, so the gate is always a process-lifetime constant. Config
|
|
18
|
+
* reloads and settings edits can never flip it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hibernationActivationAuthorized(): boolean;
|
|
21
|
+
/** Test-only: clear the frozen value so a test can re-capture a chosen env. */
|
|
22
|
+
export declare function __resetHibernationActivationAuthorityForTest(): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Durable, non-reloadable hibernation runtime-activation authority.
|
|
2
|
+
//
|
|
3
|
+
// The Phase B live process/tmux adapters must be activated ONLY by a durable
|
|
4
|
+
// authority captured at broker start and FROZEN for the entire process
|
|
5
|
+
// lifetime. This deliberately does NOT read agent-editable `SlackBridgeSettings`
|
|
6
|
+
// (which pi re-reads on config reload): there must be NO settings, reload, or
|
|
7
|
+
// other in-process path that can elevate a running broker into live-runtime
|
|
8
|
+
// activation. The single source of truth is an external, process-launch
|
|
9
|
+
// environment variable, captured exactly once.
|
|
10
|
+
//
|
|
11
|
+
// Security rationale: an attacker (or a well-meaning agent) who can edit the
|
|
12
|
+
// broker's settings file or trigger a settings reload must never be able to turn
|
|
13
|
+
// on the live runtime. Requiring an external launch-environment variable moves
|
|
14
|
+
// the activation decision to whoever started the broker process and freezes it
|
|
15
|
+
// for the lifetime of that process.
|
|
16
|
+
/** External process-launch env var that authorizes live-runtime activation. */
|
|
17
|
+
const ACTIVATION_ENV_VAR = "PINET_HIBERNATION_RUNTIME_ACTIVATION";
|
|
18
|
+
/** Captured once at broker start; never re-read afterwards. */
|
|
19
|
+
let frozen;
|
|
20
|
+
/**
|
|
21
|
+
* Capture the activation authority from the process-launch environment ONCE and
|
|
22
|
+
* freeze it for the process lifetime. Idempotent: the first call wins, and any
|
|
23
|
+
* later environment or settings mutation cannot change the frozen value. Returns
|
|
24
|
+
* the frozen authority. Call this explicitly at broker start so the freeze point
|
|
25
|
+
* is deterministic; reads via {@link hibernationActivationAuthorized} also
|
|
26
|
+
* freeze lazily on first use.
|
|
27
|
+
*
|
|
28
|
+
* Accepts `1`, `true`, `yes`, or `on` (case-insensitive, trimmed) as authorized;
|
|
29
|
+
* everything else — including unset/empty — is unauthorized (the production
|
|
30
|
+
* default, a strict no-op).
|
|
31
|
+
*/
|
|
32
|
+
export function freezeHibernationActivationAuthority(env = process.env) {
|
|
33
|
+
if (frozen === undefined) {
|
|
34
|
+
const raw = env[ACTIVATION_ENV_VAR]?.trim().toLowerCase();
|
|
35
|
+
frozen = raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
36
|
+
}
|
|
37
|
+
return frozen;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The frozen live-runtime activation authority. Lazily freezes on first read
|
|
41
|
+
* from the process-launch environment when {@link freezeHibernationActivationAuthority}
|
|
42
|
+
* has not run yet, so the gate is always a process-lifetime constant. Config
|
|
43
|
+
* reloads and settings edits can never flip it.
|
|
44
|
+
*/
|
|
45
|
+
export function hibernationActivationAuthorized() {
|
|
46
|
+
return freezeHibernationActivationAuthority();
|
|
47
|
+
}
|
|
48
|
+
/** Test-only: clear the frozen value so a test can re-capture a chosen env. */
|
|
49
|
+
export function __resetHibernationActivationAuthorityForTest() {
|
|
50
|
+
frozen = undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { HibernationOrchestrator, type HibernationOrchestratorConfig, type RuntimeLaunchContext, type StrandedWakeRecovery } from "@pinet/broker-core";
|
|
2
|
+
import type { AgentRuntimeSpec } from "@pinet/broker-core/types";
|
|
3
|
+
import type { BrokerDB } from "./schema.js";
|
|
4
|
+
import { resolveVcsIdentity } from "./hibernation-runtime-adapters.js";
|
|
5
|
+
import { type SpawnAuthoredRuntimeFacts } from "./hibernation-runtime-helpers.js";
|
|
6
|
+
/** Optional command runner override (git remote resolution); defaults to real `execFile`. */
|
|
7
|
+
type VcsRunner = Parameters<typeof resolveVcsIdentity>[1];
|
|
8
|
+
/**
|
|
9
|
+
* The DEFAULT-OFF live-runtime gate. Live process/tmux composition happens ONLY
|
|
10
|
+
* when the durable, process-lifetime activation authority — captured at broker
|
|
11
|
+
* start from the external launch environment, never agent-editable settings — is
|
|
12
|
+
* set. Config reloads and settings edits can NEVER flip it, so the production
|
|
13
|
+
* default (authority unset) keeps the broker on the `activation_pending` stub
|
|
14
|
+
* with zero live-runtime side effects. Operational permission (enabled / mode /
|
|
15
|
+
* repo allowlist) is a SEPARATE settings policy enforced by the command layer;
|
|
16
|
+
* this gate only decides whether the real machinery is wired in at all.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hibernationRuntimeActive(): boolean;
|
|
19
|
+
export interface HibernationRuntimeDeps {
|
|
20
|
+
db: BrokerDB;
|
|
21
|
+
/** Broker owner id recorded as `brokerOwnerId`/`brokerInstanceId` on managed rows. */
|
|
22
|
+
brokerInstanceId: string;
|
|
23
|
+
/** slack-bridge extension entry a woken runtime loads (`pi -e <path>`). */
|
|
24
|
+
extensionEntryPath: string;
|
|
25
|
+
/** Base PINET_* env re-establishing the mesh connection for a woken worker. */
|
|
26
|
+
baseLaunchEnv: Record<string, string>;
|
|
27
|
+
/** Broker env var NAMES (never values) re-exported into a woken runtime when present. */
|
|
28
|
+
inheritedEnvKeys: string[];
|
|
29
|
+
config?: Partial<HibernationOrchestratorConfig>;
|
|
30
|
+
/**
|
|
31
|
+
* Confirm the woken runtime registered and its reserved generation was accepted.
|
|
32
|
+
* PRODUCTION leaves this undefined: the orchestrator's default polls the broker
|
|
33
|
+
* DB until the socket server's fenced-registration handler accepts the woken
|
|
34
|
+
* worker's generation. Injectable so an isolated E2E can key acceptance off the
|
|
35
|
+
* REAL respawned process coming back alive instead of a live socket handshake.
|
|
36
|
+
*/
|
|
37
|
+
awaitRuntimeRegistration?: (ctx: RuntimeLaunchContext) => Promise<boolean>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Seam 1 — compose the real orchestrator-backed executor.
|
|
41
|
+
*
|
|
42
|
+
* The process and tmux controllers are constructed INDEPENDENTLY: the immutable
|
|
43
|
+
* launch generation rides inside the attempt handle `respawnRuntime` returns, so
|
|
44
|
+
* attempt-scoped stop/liveness bind to the exact launched process with no shared
|
|
45
|
+
* registry. The returned {@link HibernationOrchestrator} structurally satisfies
|
|
46
|
+
* `HibernateCommandExecutor & WakeCommandExecutor`, so it drops directly into the
|
|
47
|
+
* command executor slot in place of the `activation_pending` stub — but only the
|
|
48
|
+
* caller's {@link hibernationRuntimeActive} gate decides when to use it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createHibernationOrchestrator(deps: HibernationRuntimeDeps): HibernationOrchestrator;
|
|
51
|
+
/** Broker-authored spawn facts minus the VCS identity (derived here from the git remote). */
|
|
52
|
+
type SpawnRuntimeSpecFactsWithoutVcs<T extends SpawnAuthoredRuntimeFacts> = T extends SpawnAuthoredRuntimeFacts ? Omit<T, "vcsIdentity"> : never;
|
|
53
|
+
export type SpawnRuntimeSpecFacts = SpawnRuntimeSpecFactsWithoutVcs<SpawnAuthoredRuntimeFacts>;
|
|
54
|
+
/**
|
|
55
|
+
* Seam 2 — persist a durable, broker-authored runtime spec at worker spawn.
|
|
56
|
+
*
|
|
57
|
+
* The canonical `owner/repo` VCS identity (the ONLY value the repo allowlist
|
|
58
|
+
* authorizes against) is resolved HERE from the runtime's actual git `origin`
|
|
59
|
+
* remote — never from filesystem directory names. Fails closed: an unresolvable
|
|
60
|
+
* remote records `vcsIdentity: null` (the fail-closed gate then refuses), and a
|
|
61
|
+
* spec that could not be safely hibernated/woken (missing session/tmux/repo
|
|
62
|
+
* locators) is never written. Returns the persisted spec, or null when the facts
|
|
63
|
+
* do not compose a durable spec.
|
|
64
|
+
*/
|
|
65
|
+
export declare function persistSpawnedRuntimeSpec(db: BrokerDB, facts: SpawnRuntimeSpecFacts, runner?: VcsRunner): Promise<AgentRuntimeSpec | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Seam 3 — reconcile crash-stranded wake state at broker startup.
|
|
68
|
+
*
|
|
69
|
+
* Must run BEFORE the broker socket accepts new registrations so a stranded
|
|
70
|
+
* `waking`/`dispatching` row is completed, quarantined, or requeued deterministically
|
|
71
|
+
* rather than racing an incoming (possibly duplicate) wake registration. This is a
|
|
72
|
+
* synchronous DB reconciliation; it launches nothing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function recoverStrandedWakesBeforeRegistrations(orchestrator: HibernationOrchestrator): StrandedWakeRecovery[];
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Phase B live-runtime activation composition.
|
|
2
|
+
//
|
|
3
|
+
// Wires the reviewed, approved hibernation RUNTIME ADAPTERS (real process/tmux
|
|
4
|
+
// controllers + spawn-authored runtime-spec builder + git-remote VCS identity)
|
|
5
|
+
// into the broker's three live seams, all behind the DEFAULT-OFF durable,
|
|
6
|
+
// non-reloadable activation authority (see hibernation-activation-authority.ts;
|
|
7
|
+
// `hibernationRuntimeActive()`):
|
|
8
|
+
//
|
|
9
|
+
// 1. createHibernationOrchestrator — the real orchestrator-backed executor
|
|
10
|
+
// that replaces the `activation_pending` stub ONLY under the gate.
|
|
11
|
+
// 2. persistSpawnedRuntimeSpec — records a durable, broker-authored
|
|
12
|
+
// runtime spec (canonical git-remote VCS identity) at worker spawn.
|
|
13
|
+
// 3. recoverStrandedWakesBeforeRegistrations — reconciles crash-stranded wake
|
|
14
|
+
// rows at broker startup, before the socket accepts new registrations.
|
|
15
|
+
//
|
|
16
|
+
// Keeping the composition here (rather than inline in index.ts / the subtree
|
|
17
|
+
// runtime) makes every seam unit- and E2E-testable against REAL dependencies
|
|
18
|
+
// (real BrokerDB, real git, real tmux/pi) with no production side effects.
|
|
19
|
+
import { HibernationOrchestrator, } from "@pinet/broker-core";
|
|
20
|
+
import { createHibernationProcessController, createHibernationTmuxController, resolveVcsIdentity, } from "./hibernation-runtime-adapters.js";
|
|
21
|
+
import { buildRuntimeSpecInput, } from "./hibernation-runtime-helpers.js";
|
|
22
|
+
import { hibernationActivationAuthorized } from "./hibernation-activation-authority.js";
|
|
23
|
+
/**
|
|
24
|
+
* The DEFAULT-OFF live-runtime gate. Live process/tmux composition happens ONLY
|
|
25
|
+
* when the durable, process-lifetime activation authority — captured at broker
|
|
26
|
+
* start from the external launch environment, never agent-editable settings — is
|
|
27
|
+
* set. Config reloads and settings edits can NEVER flip it, so the production
|
|
28
|
+
* default (authority unset) keeps the broker on the `activation_pending` stub
|
|
29
|
+
* with zero live-runtime side effects. Operational permission (enabled / mode /
|
|
30
|
+
* repo allowlist) is a SEPARATE settings policy enforced by the command layer;
|
|
31
|
+
* this gate only decides whether the real machinery is wired in at all.
|
|
32
|
+
*/
|
|
33
|
+
export function hibernationRuntimeActive() {
|
|
34
|
+
return hibernationActivationAuthorized();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Seam 1 — compose the real orchestrator-backed executor.
|
|
38
|
+
*
|
|
39
|
+
* The process and tmux controllers are constructed INDEPENDENTLY: the immutable
|
|
40
|
+
* launch generation rides inside the attempt handle `respawnRuntime` returns, so
|
|
41
|
+
* attempt-scoped stop/liveness bind to the exact launched process with no shared
|
|
42
|
+
* registry. The returned {@link HibernationOrchestrator} structurally satisfies
|
|
43
|
+
* `HibernateCommandExecutor & WakeCommandExecutor`, so it drops directly into the
|
|
44
|
+
* command executor slot in place of the `activation_pending` stub — but only the
|
|
45
|
+
* caller's {@link hibernationRuntimeActive} gate decides when to use it.
|
|
46
|
+
*/
|
|
47
|
+
export function createHibernationOrchestrator(deps) {
|
|
48
|
+
const process = createHibernationProcessController({
|
|
49
|
+
pendingInboxCount: (agentId) => deps.db.getUnreadInboxCount(agentId),
|
|
50
|
+
});
|
|
51
|
+
const tmux = createHibernationTmuxController({
|
|
52
|
+
extensionEntryPath: deps.extensionEntryPath,
|
|
53
|
+
baseLaunchEnv: deps.baseLaunchEnv,
|
|
54
|
+
inheritedEnvKeys: deps.inheritedEnvKeys,
|
|
55
|
+
});
|
|
56
|
+
return new HibernationOrchestrator({
|
|
57
|
+
db: deps.db,
|
|
58
|
+
process,
|
|
59
|
+
tmux,
|
|
60
|
+
brokerInstanceId: deps.brokerInstanceId,
|
|
61
|
+
...(deps.config ? { config: deps.config } : {}),
|
|
62
|
+
...(deps.awaitRuntimeRegistration
|
|
63
|
+
? { awaitRuntimeRegistration: deps.awaitRuntimeRegistration }
|
|
64
|
+
: {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Seam 2 — persist a durable, broker-authored runtime spec at worker spawn.
|
|
69
|
+
*
|
|
70
|
+
* The canonical `owner/repo` VCS identity (the ONLY value the repo allowlist
|
|
71
|
+
* authorizes against) is resolved HERE from the runtime's actual git `origin`
|
|
72
|
+
* remote — never from filesystem directory names. Fails closed: an unresolvable
|
|
73
|
+
* remote records `vcsIdentity: null` (the fail-closed gate then refuses), and a
|
|
74
|
+
* spec that could not be safely hibernated/woken (missing session/tmux/repo
|
|
75
|
+
* locators) is never written. Returns the persisted spec, or null when the facts
|
|
76
|
+
* do not compose a durable spec.
|
|
77
|
+
*/
|
|
78
|
+
export async function persistSpawnedRuntimeSpec(db, facts, runner) {
|
|
79
|
+
const vcsIdentity = await resolveVcsIdentity(facts.repoRoot, runner);
|
|
80
|
+
const input = buildRuntimeSpecInput({ ...facts, vcsIdentity });
|
|
81
|
+
if (!input)
|
|
82
|
+
return null;
|
|
83
|
+
return db.upsertAgentRuntimeSpec(input);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Seam 3 — reconcile crash-stranded wake state at broker startup.
|
|
87
|
+
*
|
|
88
|
+
* Must run BEFORE the broker socket accepts new registrations so a stranded
|
|
89
|
+
* `waking`/`dispatching` row is completed, quarantined, or requeued deterministically
|
|
90
|
+
* rather than racing an incoming (possibly duplicate) wake registration. This is a
|
|
91
|
+
* synchronous DB reconciliation; it launches nothing.
|
|
92
|
+
*/
|
|
93
|
+
export function recoverStrandedWakesBeforeRegistrations(orchestrator) {
|
|
94
|
+
return orchestrator.recoverStrandedWakes();
|
|
95
|
+
}
|