@pinet/slack-bridge 0.1.0
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/LICENSE +21 -0
- package/README.md +570 -0
- package/dist/activity-log.d.ts +62 -0
- package/dist/activity-log.js +293 -0
- package/dist/agent-completion-runtime.d.ts +17 -0
- package/dist/agent-completion-runtime.js +26 -0
- package/dist/agent-event-runtime.d.ts +13 -0
- package/dist/agent-event-runtime.js +27 -0
- package/dist/agent-prompt-guidance.d.ts +22 -0
- package/dist/agent-prompt-guidance.js +46 -0
- package/dist/broker/adapters/slack.d.ts +74 -0
- package/dist/broker/adapters/slack.js +563 -0
- package/dist/broker/adapters/types.d.ts +1 -0
- package/dist/broker/adapters/types.js +1 -0
- package/dist/broker/agent-messaging.d.ts +1 -0
- package/dist/broker/agent-messaging.js +1 -0
- package/dist/broker/auth.d.ts +1 -0
- package/dist/broker/auth.js +1 -0
- package/dist/broker/client.d.ts +167 -0
- package/dist/broker/client.js +624 -0
- package/dist/broker/control-plane-dashboard.d.ts +98 -0
- package/dist/broker/control-plane-dashboard.js +213 -0
- package/dist/broker/ghost-reaper.d.ts +49 -0
- package/dist/broker/ghost-reaper.js +209 -0
- package/dist/broker/index.d.ts +36 -0
- package/dist/broker/index.js +90 -0
- package/dist/broker/leader.d.ts +1 -0
- package/dist/broker/leader.js +1 -0
- package/dist/broker/maintenance.d.ts +1 -0
- package/dist/broker/maintenance.js +1 -0
- package/dist/broker/message-send.d.ts +1 -0
- package/dist/broker/message-send.js +1 -0
- package/dist/broker/paths.d.ts +1 -0
- package/dist/broker/paths.js +1 -0
- package/dist/broker/raw-tcp-loopback.d.ts +1 -0
- package/dist/broker/raw-tcp-loopback.js +1 -0
- package/dist/broker/router.d.ts +1 -0
- package/dist/broker/router.js +1 -0
- package/dist/broker/schema.d.ts +9 -0
- package/dist/broker/schema.js +63 -0
- package/dist/broker/socket-server.d.ts +120 -0
- package/dist/broker/socket-server.js +1087 -0
- package/dist/broker/types.d.ts +1 -0
- package/dist/broker/types.js +1 -0
- package/dist/broker-delivery.d.ts +10 -0
- package/dist/broker-delivery.js +26 -0
- package/dist/broker-inbound-persistence.d.ts +11 -0
- package/dist/broker-inbound-persistence.js +22 -0
- package/dist/broker-prompt-loader.d.ts +34 -0
- package/dist/broker-prompt-loader.js +185 -0
- package/dist/broker-runtime-access.d.ts +14 -0
- package/dist/broker-runtime-access.js +20 -0
- package/dist/broker-runtime.d.ts +100 -0
- package/dist/broker-runtime.js +533 -0
- package/dist/broker-thread-owner-hints.d.ts +10 -0
- package/dist/broker-thread-owner-hints.js +14 -0
- package/dist/canvases.d.ts +80 -0
- package/dist/canvases.js +221 -0
- package/dist/command-registration-runtime.d.ts +9 -0
- package/dist/command-registration-runtime.js +9 -0
- package/dist/core-tool-guardrails.d.ts +24 -0
- package/dist/core-tool-guardrails.js +66 -0
- package/dist/deploy-manifest.d.ts +27 -0
- package/dist/deploy-manifest.js +197 -0
- package/dist/follower-delivery.d.ts +16 -0
- package/dist/follower-delivery.js +70 -0
- package/dist/follower-runtime.d.ts +61 -0
- package/dist/follower-runtime.js +350 -0
- package/dist/git-metadata.d.ts +35 -0
- package/dist/git-metadata.js +88 -0
- package/dist/guardrails.d.ts +69 -0
- package/dist/guardrails.js +283 -0
- package/dist/helpers.d.ts +550 -0
- package/dist/helpers.js +2711 -0
- package/dist/home-tab.d.ts +27 -0
- package/dist/home-tab.js +242 -0
- package/dist/imessage-tools.d.ts +33 -0
- package/dist/imessage-tools.js +96 -0
- package/dist/inbox-drain-runtime.d.ts +30 -0
- package/dist/inbox-drain-runtime.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1330 -0
- package/dist/persisted-runtime-state.d.ts +52 -0
- package/dist/persisted-runtime-state.js +98 -0
- package/dist/pinet-activity-formatting.d.ts +20 -0
- package/dist/pinet-activity-formatting.js +43 -0
- package/dist/pinet-agent-status.d.ts +36 -0
- package/dist/pinet-agent-status.js +48 -0
- package/dist/pinet-commands.d.ts +70 -0
- package/dist/pinet-commands.js +402 -0
- package/dist/pinet-confirmation-replies.d.ts +7 -0
- package/dist/pinet-confirmation-replies.js +30 -0
- package/dist/pinet-control-plane-dashboard.d.ts +34 -0
- package/dist/pinet-control-plane-dashboard.js +86 -0
- package/dist/pinet-home-tabs.d.ts +36 -0
- package/dist/pinet-home-tabs.js +101 -0
- package/dist/pinet-maintenance-delivery.d.ts +22 -0
- package/dist/pinet-maintenance-delivery.js +45 -0
- package/dist/pinet-mesh-ops.d.ts +97 -0
- package/dist/pinet-mesh-ops.js +266 -0
- package/dist/pinet-registration-gate.d.ts +14 -0
- package/dist/pinet-registration-gate.js +42 -0
- package/dist/pinet-remote-control-acks.d.ts +16 -0
- package/dist/pinet-remote-control-acks.js +46 -0
- package/dist/pinet-remote-control.d.ts +17 -0
- package/dist/pinet-remote-control.js +74 -0
- package/dist/pinet-runtime-composition.d.ts +27 -0
- package/dist/pinet-runtime-composition.js +20 -0
- package/dist/pinet-tools.d.ts +67 -0
- package/dist/pinet-tools.js +1211 -0
- package/dist/prompts/broker/default.md +41 -0
- package/dist/prompts/broker/tmux.md +49 -0
- package/dist/ralph-loop.d.ts +76 -0
- package/dist/ralph-loop.js +524 -0
- package/dist/reaction-triggers.d.ts +25 -0
- package/dist/reaction-triggers.js +171 -0
- package/dist/repo-tool-guardrails.d.ts +20 -0
- package/dist/repo-tool-guardrails.js +81 -0
- package/dist/runtime-agent-context.d.ts +93 -0
- package/dist/runtime-agent-context.js +302 -0
- package/dist/runtime-mode.d.ts +10 -0
- package/dist/runtime-mode.js +39 -0
- package/dist/scheduled-wakeups.d.ts +1 -0
- package/dist/scheduled-wakeups.js +1 -0
- package/dist/session-ui-runtime.d.ts +20 -0
- package/dist/session-ui-runtime.js +163 -0
- package/dist/single-player-runtime.d.ts +71 -0
- package/dist/single-player-runtime.js +436 -0
- package/dist/skins/cosmere.json +1571 -0
- package/dist/skins/foundation.json +1304 -0
- package/dist/slack-access.d.ts +186 -0
- package/dist/slack-access.js +462 -0
- package/dist/slack-api.d.ts +2 -0
- package/dist/slack-api.js +1 -0
- package/dist/slack-block-kit.d.ts +36 -0
- package/dist/slack-block-kit.js +242 -0
- package/dist/slack-export.d.ts +31 -0
- package/dist/slack-export.js +206 -0
- package/dist/slack-message-context.d.ts +15 -0
- package/dist/slack-message-context.js +207 -0
- package/dist/slack-modals.d.ts +13 -0
- package/dist/slack-modals.js +79 -0
- package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
- package/dist/slack-pinet-runtime-adapter.js +72 -0
- package/dist/slack-presence.d.ts +36 -0
- package/dist/slack-presence.js +89 -0
- package/dist/slack-request-runtime.d.ts +8 -0
- package/dist/slack-request-runtime.js +19 -0
- package/dist/slack-runtime-access.d.ts +28 -0
- package/dist/slack-runtime-access.js +107 -0
- package/dist/slack-scope-diagnostics.d.ts +31 -0
- package/dist/slack-scope-diagnostics.js +235 -0
- package/dist/slack-socket-dedup.d.ts +7 -0
- package/dist/slack-socket-dedup.js +135 -0
- package/dist/slack-thread-status.d.ts +41 -0
- package/dist/slack-thread-status.js +156 -0
- package/dist/slack-tool-policy-runtime.d.ts +36 -0
- package/dist/slack-tool-policy-runtime.js +96 -0
- package/dist/slack-tools.d.ts +52 -0
- package/dist/slack-tools.js +2688 -0
- package/dist/slack-turn-guardrails.d.ts +19 -0
- package/dist/slack-turn-guardrails.js +36 -0
- package/dist/slack-upload.d.ts +49 -0
- package/dist/slack-upload.js +224 -0
- package/dist/task-assignments.d.ts +45 -0
- package/dist/task-assignments.js +527 -0
- package/dist/thread-confirmations.d.ts +22 -0
- package/dist/thread-confirmations.js +139 -0
- package/dist/tool-registration-runtime.d.ts +13 -0
- package/dist/tool-registration-runtime.js +13 -0
- package/dist/ttl-cache.d.ts +47 -0
- package/dist/ttl-cache.js +105 -0
- package/manifest.yaml +57 -0
- package/package.json +60 -0
- package/skills/pinet-skin-creator/SKILL.md +109 -0
- package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
- package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
- package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
- package/skills/slack-bridge/SKILL.md +360 -0
|
@@ -0,0 +1,1087 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as crypto from "node:crypto";
|
|
5
|
+
import { DEFAULT_SOCKET_PATH } from "./paths.js";
|
|
6
|
+
import { MessageRouter } from "./router.js";
|
|
7
|
+
import { dispatchDirectAgentMessage } from "./agent-messaging.js";
|
|
8
|
+
import { sendBrokerMessage } from "./message-send.js";
|
|
9
|
+
import { assertLoopbackTcpHost } from "./raw-tcp-loopback.js";
|
|
10
|
+
import { RPC_PARSE_ERROR, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_INVALID_PARAMS, RPC_INTERNAL_ERROR, RPC_AUTH_REQUIRED, RPC_AGENT_NAME_CONFLICT, RPC_AGENT_STABLE_ID_CONFLICT, } from "./types.js";
|
|
11
|
+
export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 15_000;
|
|
12
|
+
export const DEFAULT_PRUNE_INTERVAL_MS = 5_000;
|
|
13
|
+
export const DEFAULT_AUTH_TIMEOUT_MS = 2_000;
|
|
14
|
+
function toClientAgentInfo(agent) {
|
|
15
|
+
const { stableId, ...clientAgent } = agent;
|
|
16
|
+
void stableId;
|
|
17
|
+
return clientAgent;
|
|
18
|
+
}
|
|
19
|
+
// ─── RPC helpers ─────────────────────────────────────────
|
|
20
|
+
function rpcOk(id, result) {
|
|
21
|
+
return { jsonrpc: "2.0", id, result };
|
|
22
|
+
}
|
|
23
|
+
function rpcError(id, code, message, data) {
|
|
24
|
+
const error = { code, message };
|
|
25
|
+
if (data !== undefined)
|
|
26
|
+
error.data = data;
|
|
27
|
+
return { jsonrpc: "2.0", id, error };
|
|
28
|
+
}
|
|
29
|
+
function isJsonRpcRequestId(value) {
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
return Number.isFinite(value) && value !== 0;
|
|
32
|
+
}
|
|
33
|
+
return typeof value === "string" && value.length > 0;
|
|
34
|
+
}
|
|
35
|
+
function isJsonRpcRequestPayload(value) {
|
|
36
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const request = value;
|
|
40
|
+
if (request.jsonrpc !== "2.0") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (typeof request.method !== "string" || request.method.length === 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (!isJsonRpcRequestId(request.id)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (request.params !== undefined &&
|
|
50
|
+
(typeof request.params !== "object" || request.params === null || Array.isArray(request.params))) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
function extractJsonRpcRequestId(value) {
|
|
56
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const id = value.id;
|
|
60
|
+
return isJsonRpcRequestId(id) ? id : null;
|
|
61
|
+
}
|
|
62
|
+
// ─── Socket server ───────────────────────────────────────
|
|
63
|
+
export class BrokerSocketServer {
|
|
64
|
+
server = null;
|
|
65
|
+
target;
|
|
66
|
+
db;
|
|
67
|
+
router;
|
|
68
|
+
connections = new Map();
|
|
69
|
+
heartbeatTimeoutMs;
|
|
70
|
+
pruneIntervalMs;
|
|
71
|
+
authTimeoutMs;
|
|
72
|
+
meshSecret;
|
|
73
|
+
pruneTimer = null;
|
|
74
|
+
assignedPort = null;
|
|
75
|
+
agentMessageCallback = null;
|
|
76
|
+
agentStatusChangeCallback = null;
|
|
77
|
+
outboundMessageAdapters = [];
|
|
78
|
+
agentRegistrationResolver = null;
|
|
79
|
+
constructor(db, target, options = {}) {
|
|
80
|
+
this.db = db;
|
|
81
|
+
this.router = new MessageRouter(db);
|
|
82
|
+
this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
83
|
+
this.pruneIntervalMs = options.pruneIntervalMs ?? DEFAULT_PRUNE_INTERVAL_MS;
|
|
84
|
+
this.authTimeoutMs = options.authTimeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
|
|
85
|
+
const meshSecret = options.meshSecret?.trim();
|
|
86
|
+
this.meshSecret = meshSecret && meshSecret.length > 0 ? meshSecret : null;
|
|
87
|
+
if (typeof target === "string") {
|
|
88
|
+
this.target = { type: "unix", path: target };
|
|
89
|
+
}
|
|
90
|
+
else if (target) {
|
|
91
|
+
this.target = target;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.target = { type: "unix", path: DEFAULT_SOCKET_PATH };
|
|
95
|
+
}
|
|
96
|
+
if (this.target.type === "tcp") {
|
|
97
|
+
assertLoopbackTcpHost(this.target.host, "broker listen target");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async start() {
|
|
101
|
+
// Clean up stale socket file for Unix mode
|
|
102
|
+
if (this.target.type === "unix") {
|
|
103
|
+
if (fs.existsSync(this.target.path)) {
|
|
104
|
+
fs.unlinkSync(this.target.path);
|
|
105
|
+
}
|
|
106
|
+
fs.mkdirSync(path.dirname(this.target.path), { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
110
|
+
this.server.on("error", (err) => {
|
|
111
|
+
reject(err);
|
|
112
|
+
});
|
|
113
|
+
if (this.target.type === "unix") {
|
|
114
|
+
this.server.listen(this.target.path, () => {
|
|
115
|
+
this.startPruning();
|
|
116
|
+
resolve();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.server.listen(this.target.port, this.target.host, () => {
|
|
121
|
+
const addr = this.server.address();
|
|
122
|
+
if (addr && typeof addr === "object") {
|
|
123
|
+
this.assignedPort = addr.port;
|
|
124
|
+
}
|
|
125
|
+
this.startPruning();
|
|
126
|
+
resolve();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async stop() {
|
|
132
|
+
this.stopPruning();
|
|
133
|
+
// Mark all connected agents as resumably disconnected. Clear agentId so the
|
|
134
|
+
// async close handler won't mark them a second time after db shutdown.
|
|
135
|
+
for (const [socket, state] of this.connections) {
|
|
136
|
+
if (state.agentId) {
|
|
137
|
+
this.db.disconnectAgent(state.agentId, this.heartbeatTimeoutMs);
|
|
138
|
+
state.agentId = null;
|
|
139
|
+
}
|
|
140
|
+
socket.destroy();
|
|
141
|
+
}
|
|
142
|
+
this.connections.clear();
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
if (!this.server) {
|
|
145
|
+
resolve();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.server.close(() => {
|
|
149
|
+
// Clean up socket file for Unix mode
|
|
150
|
+
if (this.target.type === "unix") {
|
|
151
|
+
try {
|
|
152
|
+
if (fs.existsSync(this.target.path)) {
|
|
153
|
+
fs.unlinkSync(this.target.path);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// best effort
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this.server = null;
|
|
161
|
+
this.assignedPort = null;
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get connection info for clients. Returns the socket path (Unix)
|
|
168
|
+
* or { host, port } (TCP).
|
|
169
|
+
*/
|
|
170
|
+
getConnectInfo() {
|
|
171
|
+
if (this.target.type === "unix") {
|
|
172
|
+
return { type: "unix", path: this.target.path };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
type: "tcp",
|
|
176
|
+
host: this.target.host,
|
|
177
|
+
port: this.assignedPort ?? this.target.port,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Register a callback invoked whenever a worker sends an agent-to-agent
|
|
182
|
+
* message via the socket server. The broker uses this to push messages
|
|
183
|
+
* targeting itself into its in-memory inbox.
|
|
184
|
+
*/
|
|
185
|
+
onAgentMessage(cb) {
|
|
186
|
+
this.agentMessageCallback = cb;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Register a callback invoked whenever a connected agent explicitly updates
|
|
190
|
+
* its broker status. The broker uses idle transitions to kick maintenance so
|
|
191
|
+
* backlog can be reassigned immediately.
|
|
192
|
+
*/
|
|
193
|
+
onAgentStatusChange(cb) {
|
|
194
|
+
this.agentStatusChangeCallback = cb;
|
|
195
|
+
}
|
|
196
|
+
setAgentRegistrationResolver(resolver) {
|
|
197
|
+
this.agentRegistrationResolver = resolver;
|
|
198
|
+
}
|
|
199
|
+
setOutboundMessageAdapters(adapters) {
|
|
200
|
+
this.outboundMessageAdapters = adapters;
|
|
201
|
+
}
|
|
202
|
+
startPruning() {
|
|
203
|
+
this.stopPruning();
|
|
204
|
+
this.pruneTimer = setInterval(() => {
|
|
205
|
+
try {
|
|
206
|
+
this.db.pruneStaleAgents(this.heartbeatTimeoutMs);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
/* best effort */
|
|
210
|
+
}
|
|
211
|
+
}, this.pruneIntervalMs);
|
|
212
|
+
this.pruneTimer.unref?.();
|
|
213
|
+
}
|
|
214
|
+
stopPruning() {
|
|
215
|
+
if (!this.pruneTimer)
|
|
216
|
+
return;
|
|
217
|
+
clearInterval(this.pruneTimer);
|
|
218
|
+
this.pruneTimer = null;
|
|
219
|
+
}
|
|
220
|
+
clearAuthTimer(state) {
|
|
221
|
+
if (!state.authTimer)
|
|
222
|
+
return;
|
|
223
|
+
clearTimeout(state.authTimer);
|
|
224
|
+
state.authTimer = null;
|
|
225
|
+
}
|
|
226
|
+
startAuthTimer(socket, state) {
|
|
227
|
+
this.clearAuthTimer(state);
|
|
228
|
+
if (state.authenticated || !this.meshSecret) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
state.authTimer = setTimeout(() => {
|
|
232
|
+
this.clearAuthTimer(state);
|
|
233
|
+
socket.destroy();
|
|
234
|
+
}, this.authTimeoutMs);
|
|
235
|
+
state.authTimer.unref?.();
|
|
236
|
+
}
|
|
237
|
+
disconnectDuplicateConnections(agentId, currentSocket) {
|
|
238
|
+
for (const [socket, state] of this.connections) {
|
|
239
|
+
if (socket === currentSocket || state.agentId !== agentId) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
state.agentId = null;
|
|
243
|
+
socket.destroy();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
findLiveStableIdConflict(stableId, currentSocket) {
|
|
247
|
+
const existing = this.db.getAgentByStableId(stableId);
|
|
248
|
+
if (!existing) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
for (const [socket, state] of this.connections) {
|
|
252
|
+
if (socket === currentSocket || state.agentId !== existing.id) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
return { ownerAgentId: existing.id };
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
// ─── Connection handling ─────────────────────────────
|
|
260
|
+
onConnection(socket) {
|
|
261
|
+
const state = {
|
|
262
|
+
agentId: null,
|
|
263
|
+
buffer: "",
|
|
264
|
+
authenticated: this.meshSecret == null,
|
|
265
|
+
authTimer: null,
|
|
266
|
+
};
|
|
267
|
+
this.connections.set(socket, state);
|
|
268
|
+
this.startAuthTimer(socket, state);
|
|
269
|
+
socket.on("data", (chunk) => {
|
|
270
|
+
state.buffer += chunk.toString("utf-8");
|
|
271
|
+
void this.processBuffer(socket, state);
|
|
272
|
+
});
|
|
273
|
+
socket.on("close", () => {
|
|
274
|
+
this.clearAuthTimer(state);
|
|
275
|
+
if (state.agentId) {
|
|
276
|
+
this.db.disconnectAgent(state.agentId, this.heartbeatTimeoutMs);
|
|
277
|
+
}
|
|
278
|
+
this.connections.delete(socket);
|
|
279
|
+
});
|
|
280
|
+
socket.on("error", () => {
|
|
281
|
+
// close event will handle cleanup
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
processBuffer(socket, state) {
|
|
285
|
+
let newlineIdx;
|
|
286
|
+
while ((newlineIdx = state.buffer.indexOf("\n")) !== -1) {
|
|
287
|
+
const line = state.buffer.slice(0, newlineIdx).trim();
|
|
288
|
+
state.buffer = state.buffer.slice(newlineIdx + 1);
|
|
289
|
+
if (!line)
|
|
290
|
+
continue;
|
|
291
|
+
let parsed;
|
|
292
|
+
try {
|
|
293
|
+
parsed = JSON.parse(line);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
this.send(socket, rpcError(null, RPC_PARSE_ERROR, "Parse error"));
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!isJsonRpcRequestPayload(parsed)) {
|
|
300
|
+
this.send(socket, rpcError(extractJsonRpcRequestId(parsed), RPC_INVALID_REQUEST, "Invalid request"));
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
void this.dispatchRequest(parsed, state, socket);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async dispatchRequest(req, state, socket) {
|
|
307
|
+
try {
|
|
308
|
+
const response = await this.handleRequest(req, state, socket);
|
|
309
|
+
this.send(socket, response);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
313
|
+
this.send(socket, rpcError(req.id, RPC_INTERNAL_ERROR, message));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
send(socket, response) {
|
|
317
|
+
try {
|
|
318
|
+
socket.write(JSON.stringify(response) + "\n");
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// connection may have closed
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// ─── Request dispatch ────────────────────────────────
|
|
325
|
+
async handleRequest(req, state, socket) {
|
|
326
|
+
try {
|
|
327
|
+
if (!state.authenticated && req.method !== "auth") {
|
|
328
|
+
setImmediate(() => socket.destroy());
|
|
329
|
+
return rpcError(req.id, RPC_AUTH_REQUIRED, "Authentication required before calling broker methods.");
|
|
330
|
+
}
|
|
331
|
+
switch (req.method) {
|
|
332
|
+
case "auth":
|
|
333
|
+
return this.handleAuth(req, state, socket);
|
|
334
|
+
case "register":
|
|
335
|
+
return this.handleRegister(req, state, socket);
|
|
336
|
+
case "unregister":
|
|
337
|
+
return this.handleUnregister(req, state);
|
|
338
|
+
case "heartbeat":
|
|
339
|
+
return this.handleHeartbeat(req, state);
|
|
340
|
+
case "inbox.poll":
|
|
341
|
+
return this.handleInboxPoll(req, state);
|
|
342
|
+
case "inbox.read":
|
|
343
|
+
return this.handleInboxRead(req, state);
|
|
344
|
+
case "inbox.ack":
|
|
345
|
+
return this.handleInboxAck(req, state);
|
|
346
|
+
case "send":
|
|
347
|
+
return this.handleSend(req, state);
|
|
348
|
+
case "message.send":
|
|
349
|
+
return await this.handleMessageSend(req, state);
|
|
350
|
+
case "threads.list":
|
|
351
|
+
return this.handleThreadsList(req, state);
|
|
352
|
+
case "agents.list":
|
|
353
|
+
return this.handleAgentsList(req);
|
|
354
|
+
case "thread.claim":
|
|
355
|
+
return this.handleThreadClaim(req, state);
|
|
356
|
+
case "resolveThread":
|
|
357
|
+
return this.handleResolveThread(req, state);
|
|
358
|
+
case "agent.message":
|
|
359
|
+
return this.handleAgentMessage(req, state);
|
|
360
|
+
case "agent.broadcast":
|
|
361
|
+
return this.handleAgentBroadcast(req, state);
|
|
362
|
+
case "lane.list":
|
|
363
|
+
return this.handleLaneList(req, state);
|
|
364
|
+
case "lane.upsert":
|
|
365
|
+
return this.handleLaneUpsert(req, state);
|
|
366
|
+
case "lane.participant":
|
|
367
|
+
return this.handleLaneParticipant(req, state);
|
|
368
|
+
case "portLease.acquire":
|
|
369
|
+
return this.handlePortLeaseAcquire(req, state);
|
|
370
|
+
case "portLease.renew":
|
|
371
|
+
return this.handlePortLeaseRenew(req, state);
|
|
372
|
+
case "portLease.release":
|
|
373
|
+
return this.handlePortLeaseRelease(req, state);
|
|
374
|
+
case "portLease.status":
|
|
375
|
+
return this.handlePortLeaseStatus(req, state);
|
|
376
|
+
case "portLease.list":
|
|
377
|
+
return this.handlePortLeaseList(req, state);
|
|
378
|
+
case "portLease.expire":
|
|
379
|
+
return this.handlePortLeaseExpire(req, state);
|
|
380
|
+
case "schedule.create":
|
|
381
|
+
return this.handleScheduleCreate(req, state);
|
|
382
|
+
case "status.update":
|
|
383
|
+
return this.handleStatusUpdate(req, state);
|
|
384
|
+
case "adapter.capability":
|
|
385
|
+
return await this.handleAdapterCapability(req, state);
|
|
386
|
+
case "slack.proxy":
|
|
387
|
+
return await this.handleLegacySlackProxy(req, state);
|
|
388
|
+
default:
|
|
389
|
+
return rpcError(req.id, RPC_METHOD_NOT_FOUND, `Unknown method: ${req.method}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
394
|
+
return rpcError(req.id, RPC_INTERNAL_ERROR, message);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// ─── Method handlers ─────────────────────────────────
|
|
398
|
+
handleAuth(req, state, socket) {
|
|
399
|
+
if (!this.meshSecret) {
|
|
400
|
+
state.authenticated = true;
|
|
401
|
+
this.clearAuthTimer(state);
|
|
402
|
+
return rpcOk(req.id, { ok: true });
|
|
403
|
+
}
|
|
404
|
+
const params = req.params ?? {};
|
|
405
|
+
const providedSecret = typeof params.secret === "string" ? params.secret.trim() : "";
|
|
406
|
+
if (!providedSecret) {
|
|
407
|
+
setImmediate(() => socket.destroy());
|
|
408
|
+
return rpcError(req.id, RPC_AUTH_REQUIRED, "Mesh secret is required.");
|
|
409
|
+
}
|
|
410
|
+
if (providedSecret !== this.meshSecret) {
|
|
411
|
+
setImmediate(() => socket.destroy());
|
|
412
|
+
return rpcError(req.id, RPC_AUTH_REQUIRED, "Invalid mesh secret.");
|
|
413
|
+
}
|
|
414
|
+
state.authenticated = true;
|
|
415
|
+
this.clearAuthTimer(state);
|
|
416
|
+
return rpcOk(req.id, { ok: true });
|
|
417
|
+
}
|
|
418
|
+
handleRegister(req, state, socket) {
|
|
419
|
+
const params = req.params ?? {};
|
|
420
|
+
const requestedName = typeof params.name === "string" ? params.name.trim() : "";
|
|
421
|
+
const requestedEmoji = typeof params.emoji === "string" ? params.emoji : "";
|
|
422
|
+
const pid = typeof params.pid === "number" ? params.pid : 0;
|
|
423
|
+
const stableId = typeof params.stableId === "string" ? params.stableId : undefined;
|
|
424
|
+
const metadata = params.metadata && typeof params.metadata === "object"
|
|
425
|
+
? params.metadata
|
|
426
|
+
: undefined;
|
|
427
|
+
if (stableId) {
|
|
428
|
+
const liveStableIdConflict = this.findLiveStableIdConflict(stableId, socket);
|
|
429
|
+
if (liveStableIdConflict) {
|
|
430
|
+
return rpcError(req.id, RPC_AGENT_STABLE_ID_CONFLICT, `Agent stableId "${stableId}" is already active on another live connection. Wait for that agent to disconnect before retrying.`, {
|
|
431
|
+
code: "AGENT_STABLE_ID_CONFLICT",
|
|
432
|
+
stableId,
|
|
433
|
+
ownerAgentId: liveStableIdConflict.ownerAgentId,
|
|
434
|
+
retryable: true,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const candidateId = state.agentId ?? crypto.randomUUID();
|
|
439
|
+
const resolved = this.agentRegistrationResolver?.({
|
|
440
|
+
agentId: candidateId,
|
|
441
|
+
name: requestedName,
|
|
442
|
+
emoji: requestedEmoji,
|
|
443
|
+
pid,
|
|
444
|
+
stableId,
|
|
445
|
+
metadata,
|
|
446
|
+
});
|
|
447
|
+
const explicitNameRequest = requestedName.length > 0;
|
|
448
|
+
const finalName = explicitNameRequest
|
|
449
|
+
? requestedName
|
|
450
|
+
: (resolved?.name ?? requestedName) || "anonymous";
|
|
451
|
+
const finalEmoji = explicitNameRequest
|
|
452
|
+
? requestedEmoji.trim() || resolved?.emoji?.trim() || ""
|
|
453
|
+
: (resolved?.emoji ?? requestedEmoji).trim();
|
|
454
|
+
const finalMetadata = resolved?.metadata ?? metadata;
|
|
455
|
+
if (explicitNameRequest) {
|
|
456
|
+
const conflict = this.db.findAgentNameConflict(finalName, candidateId, stableId);
|
|
457
|
+
if (conflict) {
|
|
458
|
+
// Do not expose raw ownerStableId on this client-visible payload (#495).
|
|
459
|
+
// The message + code + requestedName are enough to drive the retry path.
|
|
460
|
+
return rpcError(req.id, RPC_AGENT_NAME_CONFLICT, `Agent name "${finalName}" is already reserved. Retry with a different name or leave the name empty so the broker can assign one.`, {
|
|
461
|
+
code: "AGENT_NAME_CONFLICT",
|
|
462
|
+
requestedName: finalName,
|
|
463
|
+
retryable: true,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const agent = this.db.registerAgent(candidateId, finalName, finalEmoji, pid, finalMetadata, stableId);
|
|
468
|
+
this.disconnectDuplicateConnections(agent.id, socket);
|
|
469
|
+
state.agentId = agent.id;
|
|
470
|
+
return rpcOk(req.id, {
|
|
471
|
+
agentId: agent.id,
|
|
472
|
+
name: agent.name,
|
|
473
|
+
emoji: agent.emoji,
|
|
474
|
+
metadata: agent.metadata,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
handleUnregister(req, state) {
|
|
478
|
+
if (!state.agentId) {
|
|
479
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
480
|
+
}
|
|
481
|
+
this.db.unregisterAgent(state.agentId);
|
|
482
|
+
state.agentId = null;
|
|
483
|
+
return rpcOk(req.id, { ok: true });
|
|
484
|
+
}
|
|
485
|
+
handleHeartbeat(req, state) {
|
|
486
|
+
if (!state.agentId) {
|
|
487
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
488
|
+
}
|
|
489
|
+
const params = req.params ?? {};
|
|
490
|
+
const metadata = params.metadata;
|
|
491
|
+
if (metadata !== undefined && (typeof metadata !== "object" || Array.isArray(metadata))) {
|
|
492
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "metadata must be an object or null");
|
|
493
|
+
}
|
|
494
|
+
this.db.heartbeatAgent(state.agentId);
|
|
495
|
+
if (metadata !== undefined) {
|
|
496
|
+
this.db.updateAgentMetadata(state.agentId, metadata);
|
|
497
|
+
}
|
|
498
|
+
return rpcOk(req.id, { ok: true });
|
|
499
|
+
}
|
|
500
|
+
handleInboxPoll(req, state) {
|
|
501
|
+
if (!state.agentId) {
|
|
502
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
503
|
+
}
|
|
504
|
+
this.db.touchAgent(state.agentId);
|
|
505
|
+
const params = req.params ?? {};
|
|
506
|
+
const limit = typeof params.limit === "number" ? params.limit : 50;
|
|
507
|
+
const items = this.db.getInbox(state.agentId, limit);
|
|
508
|
+
return rpcOk(req.id, items.map((item) => ({
|
|
509
|
+
inboxId: item.entry.id,
|
|
510
|
+
message: item.message,
|
|
511
|
+
})));
|
|
512
|
+
}
|
|
513
|
+
handleInboxRead(req, state) {
|
|
514
|
+
if (!state.agentId) {
|
|
515
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
516
|
+
}
|
|
517
|
+
this.db.touchAgent(state.agentId);
|
|
518
|
+
const params = req.params ?? {};
|
|
519
|
+
const threadId = typeof params.threadId === "string" ? params.threadId.trim() : undefined;
|
|
520
|
+
if (params.threadId !== undefined && !threadId) {
|
|
521
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "threadId must be a non-empty string");
|
|
522
|
+
}
|
|
523
|
+
const limit = params.limit === undefined ? undefined : params.limit;
|
|
524
|
+
if (limit !== undefined && (typeof limit !== "number" || !Number.isFinite(limit))) {
|
|
525
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "limit must be a finite number");
|
|
526
|
+
}
|
|
527
|
+
const unreadOnly = params.unreadOnly === undefined ? undefined : params.unreadOnly;
|
|
528
|
+
if (unreadOnly !== undefined && typeof unreadOnly !== "boolean") {
|
|
529
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "unreadOnly must be a boolean");
|
|
530
|
+
}
|
|
531
|
+
const markRead = params.markRead === undefined ? undefined : params.markRead;
|
|
532
|
+
if (markRead !== undefined && typeof markRead !== "boolean") {
|
|
533
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "markRead must be a boolean");
|
|
534
|
+
}
|
|
535
|
+
return rpcOk(req.id, this.db.readInbox(state.agentId, {
|
|
536
|
+
...(threadId ? { threadId } : {}),
|
|
537
|
+
...(typeof limit === "number" ? { limit } : {}),
|
|
538
|
+
...(typeof unreadOnly === "boolean" ? { unreadOnly } : {}),
|
|
539
|
+
...(typeof markRead === "boolean" ? { markRead } : {}),
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
handleInboxAck(req, state) {
|
|
543
|
+
if (!state.agentId) {
|
|
544
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
545
|
+
}
|
|
546
|
+
const params = req.params ?? {};
|
|
547
|
+
const ids = params.ids;
|
|
548
|
+
if (!Array.isArray(ids) || !ids.every((id) => typeof id === "number")) {
|
|
549
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "ids must be an array of numbers");
|
|
550
|
+
}
|
|
551
|
+
this.db.markDelivered(ids, state.agentId);
|
|
552
|
+
// Activity tracking: acknowledging messages proves the agent processed them
|
|
553
|
+
if (state.agentId) {
|
|
554
|
+
this.db.touchAgentActivity(state.agentId);
|
|
555
|
+
}
|
|
556
|
+
return rpcOk(req.id, { ok: true });
|
|
557
|
+
}
|
|
558
|
+
handleSend(req, state) {
|
|
559
|
+
if (!state.agentId) {
|
|
560
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
561
|
+
}
|
|
562
|
+
const params = req.params ?? {};
|
|
563
|
+
const threadId = typeof params.threadId === "string" ? params.threadId : null;
|
|
564
|
+
const body = typeof params.body === "string" ? params.body : null;
|
|
565
|
+
if (!threadId || !body) {
|
|
566
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "threadId and body are required");
|
|
567
|
+
}
|
|
568
|
+
const source = typeof params.source === "string" ? params.source : "agent";
|
|
569
|
+
const direction = params.direction === "inbound" || params.direction === "outbound"
|
|
570
|
+
? params.direction
|
|
571
|
+
: "outbound";
|
|
572
|
+
const metadata = params.metadata && typeof params.metadata === "object"
|
|
573
|
+
? params.metadata
|
|
574
|
+
: undefined;
|
|
575
|
+
// Ensure thread exists
|
|
576
|
+
let thread = this.db.getThread(threadId);
|
|
577
|
+
if (!thread) {
|
|
578
|
+
const channel = typeof params.channel === "string" ? params.channel : "";
|
|
579
|
+
thread = this.db.createThread(threadId, source, channel, state.agentId);
|
|
580
|
+
}
|
|
581
|
+
// Route to all OTHER connected agents
|
|
582
|
+
const allAgents = this.db.getAgents();
|
|
583
|
+
const targetIds = allAgents.filter((a) => a.id !== state.agentId).map((a) => a.id);
|
|
584
|
+
const msg = this.db.insertMessage(threadId, source, direction, state.agentId, body, targetIds, metadata);
|
|
585
|
+
// Activity tracking: sending a message proves the agent is working
|
|
586
|
+
this.db.touchAgentActivity(state.agentId);
|
|
587
|
+
return rpcOk(req.id, { messageId: msg.id });
|
|
588
|
+
}
|
|
589
|
+
async handleMessageSend(req, state) {
|
|
590
|
+
if (!state.agentId) {
|
|
591
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
592
|
+
}
|
|
593
|
+
const params = req.params ?? {};
|
|
594
|
+
const threadId = typeof params.threadId === "string" ? params.threadId : null;
|
|
595
|
+
const body = typeof params.body === "string" ? params.body : null;
|
|
596
|
+
const source = typeof params.source === "string" ? params.source : undefined;
|
|
597
|
+
const channel = typeof params.channel === "string" ? params.channel : undefined;
|
|
598
|
+
const agentName = typeof params.agentName === "string" ? params.agentName : undefined;
|
|
599
|
+
const agentEmoji = typeof params.agentEmoji === "string" ? params.agentEmoji : undefined;
|
|
600
|
+
const agentOwnerToken = typeof params.agentOwnerToken === "string" ? params.agentOwnerToken : undefined;
|
|
601
|
+
const blocks = Array.isArray(params.blocks)
|
|
602
|
+
? params.blocks.filter((entry) => !!entry && typeof entry === "object")
|
|
603
|
+
: undefined;
|
|
604
|
+
let content;
|
|
605
|
+
if (params.content !== undefined) {
|
|
606
|
+
if (!params.content || typeof params.content !== "object" || Array.isArray(params.content)) {
|
|
607
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "content must be an object");
|
|
608
|
+
}
|
|
609
|
+
const raw = params.content;
|
|
610
|
+
const text = typeof raw.text === "string" ? raw.text.trim() : "";
|
|
611
|
+
if (!text) {
|
|
612
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "content.text is required when content is provided");
|
|
613
|
+
}
|
|
614
|
+
const markdown = typeof raw.markdown === "string" ? raw.markdown.trim() : undefined;
|
|
615
|
+
const slackBlocks = Array.isArray(raw.slackBlocks)
|
|
616
|
+
? raw.slackBlocks.filter((entry) => !!entry && typeof entry === "object")
|
|
617
|
+
: undefined;
|
|
618
|
+
content = {
|
|
619
|
+
text,
|
|
620
|
+
...(markdown ? { markdown } : {}),
|
|
621
|
+
...(slackBlocks && slackBlocks.length > 0 ? { slackBlocks } : {}),
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const metadata = params.metadata && typeof params.metadata === "object"
|
|
625
|
+
? params.metadata
|
|
626
|
+
: undefined;
|
|
627
|
+
if (!threadId || !body) {
|
|
628
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "threadId and body are required");
|
|
629
|
+
}
|
|
630
|
+
const result = await sendBrokerMessage({
|
|
631
|
+
db: this.db,
|
|
632
|
+
adapters: this.outboundMessageAdapters,
|
|
633
|
+
}, {
|
|
634
|
+
threadId,
|
|
635
|
+
body,
|
|
636
|
+
senderAgentId: state.agentId,
|
|
637
|
+
...(source ? { source } : {}),
|
|
638
|
+
...(channel ? { channel } : {}),
|
|
639
|
+
...(content ? { content } : {}),
|
|
640
|
+
...(blocks && blocks.length > 0 ? { blocks } : {}),
|
|
641
|
+
...(agentName ? { agentName } : {}),
|
|
642
|
+
...(agentEmoji ? { agentEmoji } : {}),
|
|
643
|
+
...(agentOwnerToken ? { agentOwnerToken } : {}),
|
|
644
|
+
...(metadata ? { metadata } : {}),
|
|
645
|
+
});
|
|
646
|
+
this.db.touchAgentActivity(state.agentId);
|
|
647
|
+
return rpcOk(req.id, {
|
|
648
|
+
adapter: result.adapter,
|
|
649
|
+
messageId: result.message.id,
|
|
650
|
+
threadId: result.thread.threadId,
|
|
651
|
+
channel: result.thread.channel,
|
|
652
|
+
source: result.thread.source,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
handleThreadsList(req, state) {
|
|
656
|
+
if (!state.agentId) {
|
|
657
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
658
|
+
}
|
|
659
|
+
const threads = this.db.getThreads(state.agentId);
|
|
660
|
+
return rpcOk(req.id, threads);
|
|
661
|
+
}
|
|
662
|
+
handleAgentsList(req) {
|
|
663
|
+
const params = req.params ?? {};
|
|
664
|
+
const includeDisconnected = params.includeDisconnected === true;
|
|
665
|
+
const agents = (includeDisconnected ? this.db.getAllAgents() : this.db.getAgents()).map((agent) => ({
|
|
666
|
+
...toClientAgentInfo(agent),
|
|
667
|
+
pendingInboxCount: this.db.getPendingInboxCount(agent.id),
|
|
668
|
+
}));
|
|
669
|
+
return rpcOk(req.id, agents);
|
|
670
|
+
}
|
|
671
|
+
// ─── Thread claim handler ─────────────────────────────
|
|
672
|
+
handleThreadClaim(req, state) {
|
|
673
|
+
if (!state.agentId) {
|
|
674
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
675
|
+
}
|
|
676
|
+
const params = req.params ?? {};
|
|
677
|
+
const threadId = typeof params.threadId === "string" ? params.threadId : null;
|
|
678
|
+
if (!threadId) {
|
|
679
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "threadId is required");
|
|
680
|
+
}
|
|
681
|
+
const channel = typeof params.channel === "string" ? params.channel : undefined;
|
|
682
|
+
let source = typeof params.source === "string" && params.source.trim().length > 0
|
|
683
|
+
? params.source.trim()
|
|
684
|
+
: undefined;
|
|
685
|
+
// Backward compatibility: legacy Slack callers used channel-only thread.claim
|
|
686
|
+
// requests. Keep those claims Slack-sendable while allowing truly source-less
|
|
687
|
+
// claims to fall through to broker-core's neutral default.
|
|
688
|
+
const legacyChannelOnlyClaim = !source && !!channel;
|
|
689
|
+
if (legacyChannelOnlyClaim) {
|
|
690
|
+
source = "slack";
|
|
691
|
+
}
|
|
692
|
+
const claimed = this.router.claimThread(threadId, state.agentId, channel, source);
|
|
693
|
+
if (claimed && legacyChannelOnlyClaim) {
|
|
694
|
+
this.db.updateThread(threadId, { source, channel });
|
|
695
|
+
}
|
|
696
|
+
return rpcOk(req.id, { claimed });
|
|
697
|
+
}
|
|
698
|
+
handleResolveThread(req, state) {
|
|
699
|
+
if (!state.agentId) {
|
|
700
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
701
|
+
}
|
|
702
|
+
const params = req.params ?? {};
|
|
703
|
+
const threadTs = typeof params.threadTs === "string" ? params.threadTs : null;
|
|
704
|
+
if (!threadTs) {
|
|
705
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "threadTs is required");
|
|
706
|
+
}
|
|
707
|
+
const channelId = this.db.getThread(threadTs)?.channel || null;
|
|
708
|
+
return rpcOk(req.id, { channelId });
|
|
709
|
+
}
|
|
710
|
+
// ─── Agent-to-agent messaging ─────────────────────────
|
|
711
|
+
handleAgentMessage(req, state) {
|
|
712
|
+
if (!state.agentId) {
|
|
713
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
714
|
+
}
|
|
715
|
+
const params = req.params ?? {};
|
|
716
|
+
const targetAgent = typeof params.targetAgent === "string" ? params.targetAgent : null;
|
|
717
|
+
const body = typeof params.body === "string" ? params.body : null;
|
|
718
|
+
if (!targetAgent || !body) {
|
|
719
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "targetAgent and body are required");
|
|
720
|
+
}
|
|
721
|
+
const metadata = params.metadata && typeof params.metadata === "object"
|
|
722
|
+
? params.metadata
|
|
723
|
+
: undefined;
|
|
724
|
+
const sender = this.db.getAgents().find((agent) => agent.id === state.agentId);
|
|
725
|
+
const senderName = sender?.name ?? state.agentId;
|
|
726
|
+
try {
|
|
727
|
+
const result = dispatchDirectAgentMessage(this.db, {
|
|
728
|
+
senderAgentId: state.agentId,
|
|
729
|
+
senderAgentName: senderName,
|
|
730
|
+
target: targetAgent,
|
|
731
|
+
body,
|
|
732
|
+
metadata,
|
|
733
|
+
}, (target, msg, enrichedMeta) => {
|
|
734
|
+
this.agentMessageCallback?.(target.id, msg, enrichedMeta);
|
|
735
|
+
});
|
|
736
|
+
// Activity tracking: sending agent-to-agent messages proves the agent is working
|
|
737
|
+
this.db.touchAgentActivity(state.agentId);
|
|
738
|
+
return rpcOk(req.id, { ok: true, messageId: result.messageId });
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
742
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
handleAgentBroadcast(req, state) {
|
|
746
|
+
if (!state.agentId) {
|
|
747
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
748
|
+
}
|
|
749
|
+
const params = req.params ?? {};
|
|
750
|
+
const channel = typeof params.channel === "string" ? params.channel : null;
|
|
751
|
+
const body = typeof params.body === "string" ? params.body : null;
|
|
752
|
+
if (!channel || !body) {
|
|
753
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "channel and body are required");
|
|
754
|
+
}
|
|
755
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Broadcast channels are broker-only and cannot be sent by connected clients.");
|
|
756
|
+
}
|
|
757
|
+
handleLaneList(req, state) {
|
|
758
|
+
if (!state.agentId) {
|
|
759
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
760
|
+
}
|
|
761
|
+
const params = req.params ?? {};
|
|
762
|
+
const options = {
|
|
763
|
+
...(typeof params.state === "string" ? { state: params.state } : {}),
|
|
764
|
+
...(typeof params.ownerAgentId === "string" ? { ownerAgentId: params.ownerAgentId } : {}),
|
|
765
|
+
...(typeof params.includeDone === "boolean" ? { includeDone: params.includeDone } : {}),
|
|
766
|
+
};
|
|
767
|
+
try {
|
|
768
|
+
return rpcOk(req.id, this.db.listPinetLanes(options));
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
772
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
handleLaneUpsert(req, state) {
|
|
776
|
+
if (!state.agentId) {
|
|
777
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
778
|
+
}
|
|
779
|
+
const params = req.params ?? {};
|
|
780
|
+
if (typeof params.laneId !== "string" || params.laneId.trim().length === 0) {
|
|
781
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "laneId is required");
|
|
782
|
+
}
|
|
783
|
+
const input = {
|
|
784
|
+
laneId: params.laneId,
|
|
785
|
+
...(typeof params.name === "string" || params.name === null
|
|
786
|
+
? { name: params.name }
|
|
787
|
+
: {}),
|
|
788
|
+
...(typeof params.task === "string" || params.task === null
|
|
789
|
+
? { task: params.task }
|
|
790
|
+
: {}),
|
|
791
|
+
...(typeof params.issueNumber === "number" || params.issueNumber === null
|
|
792
|
+
? { issueNumber: params.issueNumber }
|
|
793
|
+
: {}),
|
|
794
|
+
...(typeof params.prNumber === "number" || params.prNumber === null
|
|
795
|
+
? { prNumber: params.prNumber }
|
|
796
|
+
: {}),
|
|
797
|
+
...(typeof params.threadId === "string" || params.threadId === null
|
|
798
|
+
? { threadId: params.threadId }
|
|
799
|
+
: {}),
|
|
800
|
+
...(typeof params.ownerAgentId === "string" || params.ownerAgentId === null
|
|
801
|
+
? { ownerAgentId: params.ownerAgentId }
|
|
802
|
+
: {}),
|
|
803
|
+
...(typeof params.implementationLeadAgentId === "string" ||
|
|
804
|
+
params.implementationLeadAgentId === null
|
|
805
|
+
? { implementationLeadAgentId: params.implementationLeadAgentId }
|
|
806
|
+
: {}),
|
|
807
|
+
...(typeof params.pmMode === "boolean" ? { pmMode: params.pmMode } : {}),
|
|
808
|
+
...(typeof params.state === "string" ? { state: params.state } : {}),
|
|
809
|
+
...(typeof params.summary === "string" || params.summary === null
|
|
810
|
+
? { summary: params.summary }
|
|
811
|
+
: {}),
|
|
812
|
+
...(params.metadata && typeof params.metadata === "object" && !Array.isArray(params.metadata)
|
|
813
|
+
? { metadata: params.metadata }
|
|
814
|
+
: params.metadata === null
|
|
815
|
+
? { metadata: null }
|
|
816
|
+
: {}),
|
|
817
|
+
};
|
|
818
|
+
try {
|
|
819
|
+
const lane = this.db.upsertPinetLane(input);
|
|
820
|
+
this.db.touchAgentActivity(state.agentId);
|
|
821
|
+
return rpcOk(req.id, lane);
|
|
822
|
+
}
|
|
823
|
+
catch (err) {
|
|
824
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
825
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
handleLaneParticipant(req, state) {
|
|
829
|
+
if (!state.agentId) {
|
|
830
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
831
|
+
}
|
|
832
|
+
const params = req.params ?? {};
|
|
833
|
+
if (typeof params.laneId !== "string" || params.laneId.trim().length === 0) {
|
|
834
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "laneId is required");
|
|
835
|
+
}
|
|
836
|
+
const agentId = typeof params.agentId === "string" && params.agentId.trim().length > 0
|
|
837
|
+
? params.agentId
|
|
838
|
+
: state.agentId;
|
|
839
|
+
const input = {
|
|
840
|
+
laneId: params.laneId,
|
|
841
|
+
agentId,
|
|
842
|
+
role: (typeof params.role === "string" ? params.role : "observer"),
|
|
843
|
+
...(typeof params.status === "string" || params.status === null
|
|
844
|
+
? { status: params.status }
|
|
845
|
+
: {}),
|
|
846
|
+
...(typeof params.summary === "string" || params.summary === null
|
|
847
|
+
? { summary: params.summary }
|
|
848
|
+
: {}),
|
|
849
|
+
...(params.metadata && typeof params.metadata === "object" && !Array.isArray(params.metadata)
|
|
850
|
+
? { metadata: params.metadata }
|
|
851
|
+
: params.metadata === null
|
|
852
|
+
? { metadata: null }
|
|
853
|
+
: {}),
|
|
854
|
+
};
|
|
855
|
+
try {
|
|
856
|
+
const participant = this.db.setPinetLaneParticipant(input);
|
|
857
|
+
this.db.touchAgentActivity(state.agentId);
|
|
858
|
+
return rpcOk(req.id, participant);
|
|
859
|
+
}
|
|
860
|
+
catch (err) {
|
|
861
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
862
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
handlePortLeaseAcquire(req, state) {
|
|
866
|
+
if (!state.agentId) {
|
|
867
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
868
|
+
}
|
|
869
|
+
const params = req.params ?? {};
|
|
870
|
+
const input = {
|
|
871
|
+
purpose: typeof params.purpose === "string" ? params.purpose : "",
|
|
872
|
+
ttlMs: typeof params.ttlMs === "number" ? params.ttlMs : Number(params.ttlMs),
|
|
873
|
+
ownerAgentId: state.agentId,
|
|
874
|
+
...(typeof params.host === "string" ? { host: params.host } : {}),
|
|
875
|
+
...(typeof params.port === "number" ? { port: params.port } : {}),
|
|
876
|
+
...(typeof params.minPort === "number" ? { minPort: params.minPort } : {}),
|
|
877
|
+
...(typeof params.maxPort === "number" ? { maxPort: params.maxPort } : {}),
|
|
878
|
+
...(typeof params.pid === "number" || params.pid === null
|
|
879
|
+
? { pid: params.pid }
|
|
880
|
+
: {}),
|
|
881
|
+
...(params.metadata && typeof params.metadata === "object" && !Array.isArray(params.metadata)
|
|
882
|
+
? { metadata: params.metadata }
|
|
883
|
+
: params.metadata === null
|
|
884
|
+
? { metadata: null }
|
|
885
|
+
: {}),
|
|
886
|
+
};
|
|
887
|
+
try {
|
|
888
|
+
const lease = this.db.acquirePortLease(input);
|
|
889
|
+
this.db.touchAgentActivity(state.agentId);
|
|
890
|
+
return rpcOk(req.id, lease);
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
894
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
handlePortLeaseRenew(req, state) {
|
|
898
|
+
if (!state.agentId) {
|
|
899
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
900
|
+
}
|
|
901
|
+
const params = req.params ?? {};
|
|
902
|
+
const input = {
|
|
903
|
+
leaseId: typeof params.leaseId === "string" ? params.leaseId : "",
|
|
904
|
+
ttlMs: typeof params.ttlMs === "number" ? params.ttlMs : Number(params.ttlMs),
|
|
905
|
+
ownerAgentId: state.agentId,
|
|
906
|
+
};
|
|
907
|
+
try {
|
|
908
|
+
const lease = this.db.renewPortLease(input);
|
|
909
|
+
this.db.touchAgentActivity(state.agentId);
|
|
910
|
+
return rpcOk(req.id, lease);
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
914
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
handlePortLeaseRelease(req, state) {
|
|
918
|
+
if (!state.agentId) {
|
|
919
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
920
|
+
}
|
|
921
|
+
const params = req.params ?? {};
|
|
922
|
+
const input = {
|
|
923
|
+
leaseId: typeof params.leaseId === "string" ? params.leaseId : "",
|
|
924
|
+
ownerAgentId: state.agentId,
|
|
925
|
+
};
|
|
926
|
+
try {
|
|
927
|
+
const lease = this.db.releasePortLease(input);
|
|
928
|
+
this.db.touchAgentActivity(state.agentId);
|
|
929
|
+
return rpcOk(req.id, lease);
|
|
930
|
+
}
|
|
931
|
+
catch (err) {
|
|
932
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
933
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
handlePortLeaseStatus(req, state) {
|
|
937
|
+
if (!state.agentId) {
|
|
938
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
939
|
+
}
|
|
940
|
+
const params = req.params ?? {};
|
|
941
|
+
const leaseId = typeof params.leaseId === "string" ? params.leaseId : "";
|
|
942
|
+
try {
|
|
943
|
+
const lease = this.db.getPortLease(leaseId);
|
|
944
|
+
return rpcOk(req.id, lease?.ownerAgentId === state.agentId ? lease : null);
|
|
945
|
+
}
|
|
946
|
+
catch (err) {
|
|
947
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
948
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
handlePortLeaseList(req, state) {
|
|
952
|
+
if (!state.agentId) {
|
|
953
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
954
|
+
}
|
|
955
|
+
const params = req.params ?? {};
|
|
956
|
+
const options = {
|
|
957
|
+
...(typeof params.includeInactive === "boolean"
|
|
958
|
+
? { includeInactive: params.includeInactive }
|
|
959
|
+
: {}),
|
|
960
|
+
...(typeof params.expiredOnly === "boolean" ? { expiredOnly: params.expiredOnly } : {}),
|
|
961
|
+
ownerAgentId: state.agentId,
|
|
962
|
+
...(typeof params.purpose === "string" ? { purpose: params.purpose } : {}),
|
|
963
|
+
...(typeof params.host === "string" ? { host: params.host } : {}),
|
|
964
|
+
};
|
|
965
|
+
try {
|
|
966
|
+
return rpcOk(req.id, this.db.listPortLeases(options));
|
|
967
|
+
}
|
|
968
|
+
catch (err) {
|
|
969
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
970
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
handlePortLeaseExpire(req, state) {
|
|
974
|
+
if (!state.agentId) {
|
|
975
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
const leases = this.db
|
|
979
|
+
.expirePortLeases()
|
|
980
|
+
.filter((lease) => lease.ownerAgentId === state.agentId);
|
|
981
|
+
if (leases.length > 0) {
|
|
982
|
+
this.db.touchAgentActivity(state.agentId);
|
|
983
|
+
}
|
|
984
|
+
return rpcOk(req.id, leases);
|
|
985
|
+
}
|
|
986
|
+
catch (err) {
|
|
987
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
988
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, message);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
handleScheduleCreate(req, state) {
|
|
992
|
+
if (!state.agentId) {
|
|
993
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
994
|
+
}
|
|
995
|
+
const params = req.params ?? {};
|
|
996
|
+
const fireAt = typeof params.fireAt === "string" ? params.fireAt : null;
|
|
997
|
+
const body = typeof params.body === "string" ? params.body.trim() : null;
|
|
998
|
+
if (!fireAt || !body) {
|
|
999
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "fireAt and body are required");
|
|
1000
|
+
}
|
|
1001
|
+
if (Number.isNaN(Date.parse(fireAt))) {
|
|
1002
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "fireAt must be a valid ISO timestamp");
|
|
1003
|
+
}
|
|
1004
|
+
const wakeup = this.db.scheduleWakeup(state.agentId, body, fireAt);
|
|
1005
|
+
return rpcOk(req.id, {
|
|
1006
|
+
id: wakeup.id,
|
|
1007
|
+
threadId: wakeup.threadId,
|
|
1008
|
+
fireAt: wakeup.fireAt,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
// ─── Status update handler ─────────────────────────────
|
|
1012
|
+
handleStatusUpdate(req, state) {
|
|
1013
|
+
if (!state.agentId) {
|
|
1014
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "Not registered");
|
|
1015
|
+
}
|
|
1016
|
+
const params = req.params ?? {};
|
|
1017
|
+
const status = params.status === "working" ? "working" : "idle";
|
|
1018
|
+
this.db.updateAgentStatus(state.agentId, status);
|
|
1019
|
+
try {
|
|
1020
|
+
this.agentStatusChangeCallback?.(state.agentId, status);
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
/* best effort */
|
|
1024
|
+
}
|
|
1025
|
+
return rpcOk(req.id, { ok: true });
|
|
1026
|
+
}
|
|
1027
|
+
// ─── Adapter capability handler ───────────────────────
|
|
1028
|
+
async handleAdapterCapability(req, state) {
|
|
1029
|
+
const params = req.params ?? {};
|
|
1030
|
+
const adapterName = typeof params.adapter === "string"
|
|
1031
|
+
? params.adapter.trim()
|
|
1032
|
+
: typeof params.source === "string"
|
|
1033
|
+
? params.source.trim()
|
|
1034
|
+
: "";
|
|
1035
|
+
const capability = typeof params.capability === "string" ? params.capability.trim() : "";
|
|
1036
|
+
if (!adapterName) {
|
|
1037
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "adapter is required for adapter.capability");
|
|
1038
|
+
}
|
|
1039
|
+
if (!capability) {
|
|
1040
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "capability is required for adapter.capability");
|
|
1041
|
+
}
|
|
1042
|
+
const capabilityParams = params.params && typeof params.params === "object" && !Array.isArray(params.params)
|
|
1043
|
+
? params.params
|
|
1044
|
+
: {};
|
|
1045
|
+
return await this.dispatchAdapterCapability(req.id, adapterName, capability, capabilityParams, state);
|
|
1046
|
+
}
|
|
1047
|
+
async handleLegacySlackProxy(req, state) {
|
|
1048
|
+
const params = req.params ?? {};
|
|
1049
|
+
const method = typeof params.method === "string" ? params.method.trim() : "";
|
|
1050
|
+
if (!method) {
|
|
1051
|
+
return rpcError(req.id, RPC_INVALID_PARAMS, "method is required for slack.proxy");
|
|
1052
|
+
}
|
|
1053
|
+
const apiParams = params.params && typeof params.params === "object" && !Array.isArray(params.params)
|
|
1054
|
+
? params.params
|
|
1055
|
+
: {};
|
|
1056
|
+
return await this.dispatchAdapterCapability(req.id, "slack", "api.call", { method, params: apiParams }, state, "slack.proxy");
|
|
1057
|
+
}
|
|
1058
|
+
async dispatchAdapterCapability(id, adapterName, capability, capabilityParams, state, errorPrefix = `${adapterName}.${capability}`) {
|
|
1059
|
+
const adapter = this.outboundMessageAdapters.find((candidate) => candidate.name === adapterName);
|
|
1060
|
+
if (!adapter?.invokeCapability) {
|
|
1061
|
+
return rpcError(id, RPC_METHOD_NOT_FOUND, `Adapter ${adapterName} does not implement capability ${capability}`);
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
const response = await adapter.invokeCapability({ capability, params: capabilityParams });
|
|
1065
|
+
this.applyAdapterCapabilityEffects(adapterName, response, state);
|
|
1066
|
+
return rpcOk(id, response.result);
|
|
1067
|
+
}
|
|
1068
|
+
catch (err) {
|
|
1069
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1070
|
+
return rpcError(id, RPC_INTERNAL_ERROR, `${errorPrefix}: ${message}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
applyAdapterCapabilityEffects(adapterName, response, state) {
|
|
1074
|
+
if (!state.agentId)
|
|
1075
|
+
return;
|
|
1076
|
+
const claimThread = response.effects?.claimThread;
|
|
1077
|
+
const claims = Array.isArray(claimThread) ? claimThread : claimThread ? [claimThread] : [];
|
|
1078
|
+
for (const claim of claims) {
|
|
1079
|
+
if (!claim.threadId)
|
|
1080
|
+
continue;
|
|
1081
|
+
const claimed = this.router.claimThread(claim.threadId, state.agentId, claim.channel, adapterName);
|
|
1082
|
+
if (claimed && claim.channel) {
|
|
1083
|
+
this.db.updateThread(claim.threadId, { source: adapterName, channel: claim.channel });
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|