jeopi-cli 16.6.14 → 16.6.18
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/CHANGELOG.md +26 -0
- package/README.md +2 -0
- package/dist/cli.js +4106 -4053
- package/dist/types/cli/messenger-cli.d.ts +10 -2
- package/dist/types/commands/messenger.d.ts +21 -1
- package/dist/types/messenger/discord-setup.d.ts +48 -0
- package/dist/types/messenger/discord.d.ts +3 -0
- package/dist/types/messenger/service.d.ts +19 -0
- package/dist/types/messenger/types.d.ts +4 -0
- package/package.json +12 -12
- package/src/cli/messenger-cli.ts +77 -2
- package/src/commands/messenger.ts +26 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/messenger/discord-setup.ts +563 -0
- package/src/messenger/discord.ts +213 -54
- package/src/messenger/gateway.ts +48 -22
- package/src/messenger/service.ts +122 -0
- package/src/messenger/session-pool.ts +55 -21
- package/src/messenger/session.ts +3 -0
- package/src/messenger/slack.ts +62 -1
- package/src/messenger/telegram.ts +54 -1
- package/src/messenger/types.ts +4 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { logger } from "jeopi-utils";
|
|
2
3
|
import {
|
|
3
4
|
createWorkerHandle,
|
|
4
5
|
createWorkerSubprocess,
|
|
@@ -106,6 +107,7 @@ interface Lane {
|
|
|
106
107
|
/** Turns queued or running; idle retirement waits for zero. */
|
|
107
108
|
turns: number;
|
|
108
109
|
live?: LiveSession;
|
|
110
|
+
retiring?: Promise<void>;
|
|
109
111
|
idle?: NodeJS.Timeout;
|
|
110
112
|
/** Already chosen to free a slot, so concurrent starts pick different victims. */
|
|
111
113
|
evicting?: boolean;
|
|
@@ -152,27 +154,52 @@ export function createMessengerSessionPool(
|
|
|
152
154
|
};
|
|
153
155
|
|
|
154
156
|
/** Let the process flush and dispose its session, then kill it; the session file stays for resumption. */
|
|
155
|
-
const retire =
|
|
157
|
+
const retire = (lane: Lane): Promise<void> => {
|
|
158
|
+
if (lane.retiring) return lane.retiring;
|
|
156
159
|
const live = lane.live;
|
|
157
|
-
if (!live) return;
|
|
160
|
+
if (!live) return Promise.resolve();
|
|
158
161
|
lane.live = undefined;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
162
|
+
const retirement = (async () => {
|
|
163
|
+
const id = String(++nextId);
|
|
164
|
+
const acknowledged = Promise.withResolvers<void>();
|
|
165
|
+
const unsubscribe = live.process.onMessage(event => {
|
|
166
|
+
if (event.type === "closed" && event.id === id) acknowledged.resolve();
|
|
167
|
+
});
|
|
168
|
+
const timer = setTimeout(acknowledged.resolve, SESSION_CLOSE_TIMEOUT_MS);
|
|
169
|
+
try {
|
|
170
|
+
live.process.send({ type: "close", id });
|
|
171
|
+
await Promise.race([acknowledged.promise, live.process.exited]);
|
|
172
|
+
} catch {
|
|
173
|
+
// Still try to terminate a process with a broken IPC channel.
|
|
174
|
+
} finally {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
unsubscribe();
|
|
177
|
+
}
|
|
173
178
|
live.process.kill();
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
const timeout = Promise.withResolvers<void>();
|
|
180
|
+
const exitTimer = setTimeout(
|
|
181
|
+
() => timeout.reject(new Error("Messenger session process did not exit")),
|
|
182
|
+
SESSION_CLOSE_TIMEOUT_MS,
|
|
183
|
+
);
|
|
184
|
+
try {
|
|
185
|
+
await Promise.race([
|
|
186
|
+
live.process.exited.then(
|
|
187
|
+
() => {},
|
|
188
|
+
() => {},
|
|
189
|
+
),
|
|
190
|
+
timeout.promise,
|
|
191
|
+
]);
|
|
192
|
+
} finally {
|
|
193
|
+
clearTimeout(exitTimer);
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
lane.retiring = retirement;
|
|
197
|
+
const clearRetirement = (): void => {
|
|
198
|
+
if (lane.retiring === retirement) lane.retiring = undefined;
|
|
199
|
+
};
|
|
200
|
+
void live.process.exited.then(clearRetirement, clearRetirement);
|
|
201
|
+
void retirement.catch(() => {});
|
|
202
|
+
return retirement;
|
|
176
203
|
};
|
|
177
204
|
|
|
178
205
|
const retireIfIdle = async (lane: Lane): Promise<void> => {
|
|
@@ -261,6 +288,7 @@ export function createMessengerSessionPool(
|
|
|
261
288
|
result.reject(new Error("Messenger request aborted"));
|
|
262
289
|
};
|
|
263
290
|
try {
|
|
291
|
+
if (lane.retiring) await lane.retiring;
|
|
264
292
|
live ??= await start(lane, signal);
|
|
265
293
|
const current = live;
|
|
266
294
|
current.failTurn = result.reject;
|
|
@@ -271,7 +299,7 @@ export function createMessengerSessionPool(
|
|
|
271
299
|
else if (event.type === "failed") result.reject(new Error("Messenger session turn failed"));
|
|
272
300
|
});
|
|
273
301
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
274
|
-
if (signal.aborted)
|
|
302
|
+
if (signal.aborted) throw new Error("Messenger request aborted");
|
|
275
303
|
try {
|
|
276
304
|
current.process.send({ type: "turn", id, message });
|
|
277
305
|
} catch (error) {
|
|
@@ -309,7 +337,11 @@ export function createMessengerSessionPool(
|
|
|
309
337
|
} finally {
|
|
310
338
|
current.turns--;
|
|
311
339
|
if (current.turns === 0 && !closed) {
|
|
312
|
-
current.idle = setTimeout(() =>
|
|
340
|
+
current.idle = setTimeout(() => {
|
|
341
|
+
void enqueue(current, () => retireIfIdle(current)).catch(() =>
|
|
342
|
+
logger.warn("Messenger session retirement failed"),
|
|
343
|
+
);
|
|
344
|
+
}, idleMs);
|
|
313
345
|
current.idle.unref?.();
|
|
314
346
|
capacityChanged();
|
|
315
347
|
}
|
|
@@ -323,7 +355,9 @@ export function createMessengerSessionPool(
|
|
|
323
355
|
const all = [...lanes.values()];
|
|
324
356
|
lanes.clear();
|
|
325
357
|
for (const lane of all) clearTimeout(lane.idle);
|
|
326
|
-
await Promise.allSettled(all.map(retire));
|
|
358
|
+
const results = await Promise.allSettled(all.map(retire));
|
|
359
|
+
const failure = results.find(result => result.status === "rejected");
|
|
360
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
327
361
|
})();
|
|
328
362
|
return closing;
|
|
329
363
|
};
|
package/src/messenger/session.ts
CHANGED
|
@@ -83,6 +83,9 @@ export function createMessengerSessionBridge(
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
async function initialize(route: string, file: string): Promise<void> {
|
|
86
|
+
const userSettings = await Settings.loadReadOnly({ cwd, agentDir: dependencies?.agentDir });
|
|
87
|
+
const defaultModel = userSettings.getModelRole("default");
|
|
88
|
+
if (defaultModel) settings.override("modelRoles", { default: defaultModel });
|
|
86
89
|
const sessionManager = await SessionManager.open(file, sessionDir, undefined, {
|
|
87
90
|
initialCwd: cwd,
|
|
88
91
|
suppressBreadcrumb: true,
|
package/src/messenger/slack.ts
CHANGED
|
@@ -40,7 +40,7 @@ function requiredToken(value: string | undefined, prefix: string): string {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async function slackRequest(
|
|
43
|
-
method: "auth.test" | "apps.connections.open" | "chat.postMessage",
|
|
43
|
+
method: "auth.test" | "apps.connections.open" | "chat.postMessage" | "chat.update" | "chat.delete",
|
|
44
44
|
token: string,
|
|
45
45
|
body: Record<string, unknown>,
|
|
46
46
|
signal?: AbortSignal,
|
|
@@ -247,6 +247,7 @@ export function createSlackAdapter(
|
|
|
247
247
|
): MessengerAdapter {
|
|
248
248
|
let teamId: string | undefined;
|
|
249
249
|
let running = false;
|
|
250
|
+
const activityMessages = new Map<string, string>();
|
|
250
251
|
return {
|
|
251
252
|
platform: "slack",
|
|
252
253
|
async start(onMessage, signal) {
|
|
@@ -329,5 +330,65 @@ export function createSlackAdapter(
|
|
|
329
330
|
);
|
|
330
331
|
}
|
|
331
332
|
},
|
|
333
|
+
releaseActivity(message) {
|
|
334
|
+
activityMessages.delete(
|
|
335
|
+
JSON.stringify([message.accountId, message.channelId, message.threadId ?? "", message.messageId]),
|
|
336
|
+
);
|
|
337
|
+
},
|
|
338
|
+
async activity(message, text, signal) {
|
|
339
|
+
if (message.platform !== "slack" || (teamId !== undefined && message.accountId !== teamId)) {
|
|
340
|
+
throw new SlackConfigurationError("Slack reply account does not match");
|
|
341
|
+
}
|
|
342
|
+
const token = requiredToken(env[config.tokenEnv], "xoxb-");
|
|
343
|
+
const key = JSON.stringify([message.accountId, message.channelId, message.threadId ?? "", message.messageId]);
|
|
344
|
+
const statusId = activityMessages.get(key);
|
|
345
|
+
if (text === undefined) {
|
|
346
|
+
if (!statusId) return;
|
|
347
|
+
await slackRequest("chat.delete", token, { channel: message.channelId, ts: statusId }, signal, deps.fetch);
|
|
348
|
+
activityMessages.delete(key);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const body = {
|
|
352
|
+
channel: message.channelId,
|
|
353
|
+
text: `⏳ ${text}`,
|
|
354
|
+
mrkdwn: false,
|
|
355
|
+
parse: "none",
|
|
356
|
+
link_names: false,
|
|
357
|
+
unfurl_links: false,
|
|
358
|
+
unfurl_media: false,
|
|
359
|
+
};
|
|
360
|
+
if (statusId) {
|
|
361
|
+
await slackRequest("chat.update", token, { ...body, ts: statusId }, signal, deps.fetch);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (signal?.aborted) return;
|
|
365
|
+
// Keep creation alive after cancellation so a late ID can be deleted; HTTP has its own timeout.
|
|
366
|
+
const created = await slackRequest(
|
|
367
|
+
"chat.postMessage",
|
|
368
|
+
token,
|
|
369
|
+
{
|
|
370
|
+
...body,
|
|
371
|
+
thread_ts: message.threadId ?? (message.direct ? undefined : message.messageId),
|
|
372
|
+
},
|
|
373
|
+
undefined,
|
|
374
|
+
deps.fetch,
|
|
375
|
+
);
|
|
376
|
+
if (typeof created.ts !== "string" || !created.ts) throw new Error("Slack activity message returned no ID");
|
|
377
|
+
if (signal?.aborted) {
|
|
378
|
+
try {
|
|
379
|
+
await slackRequest(
|
|
380
|
+
"chat.delete",
|
|
381
|
+
token,
|
|
382
|
+
{ channel: message.channelId, ts: created.ts },
|
|
383
|
+
AbortSignal.timeout(3_000),
|
|
384
|
+
deps.fetch,
|
|
385
|
+
);
|
|
386
|
+
} catch {
|
|
387
|
+
logger.warn("Slack late activity removal failed");
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
activityMessages.set(key, created.ts);
|
|
392
|
+
},
|
|
332
393
|
};
|
|
333
394
|
}
|
|
@@ -144,6 +144,7 @@ export function createTelegramAdapter(
|
|
|
144
144
|
const baseUrl = `https://api.telegram.org/bot${encodeURIComponent(token).replaceAll("%3A", ":")}/`;
|
|
145
145
|
let running = false;
|
|
146
146
|
let offset = 0;
|
|
147
|
+
const activityMessages = new Map<string, number>();
|
|
147
148
|
|
|
148
149
|
async function request(method: string, parameters: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
|
|
149
150
|
const response = await messengerFetchJson(
|
|
@@ -156,7 +157,9 @@ export function createTelegramAdapter(
|
|
|
156
157
|
if (!response.ok) {
|
|
157
158
|
const status = isInteger(response.error_code) ? response.error_code : 400;
|
|
158
159
|
const retryAfter = isRecord(response.parameters) ? response.parameters.retry_after : undefined;
|
|
159
|
-
|
|
160
|
+
const retryAfterMs = isPositiveId(retryAfter) ? Math.min(retryAfter, 300) * 1_000 : 0;
|
|
161
|
+
if (status === 429) throw new MessengerHttpError(status, retryAfterMs || undefined);
|
|
162
|
+
throw new TelegramApiError(status, retryAfterMs);
|
|
160
163
|
}
|
|
161
164
|
if (!("result" in response)) throw new TelegramApiError(400);
|
|
162
165
|
return response.result;
|
|
@@ -240,5 +243,55 @@ export function createTelegramAdapter(
|
|
|
240
243
|
if (!isRecord(result) || !isPositiveId(result.message_id)) throw new TelegramApiError(400);
|
|
241
244
|
}
|
|
242
245
|
},
|
|
246
|
+
releaseActivity(message) {
|
|
247
|
+
activityMessages.delete(
|
|
248
|
+
JSON.stringify([message.accountId, message.channelId, message.threadId ?? "", message.messageId]),
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
async activity(message, text, signal) {
|
|
252
|
+
const key = JSON.stringify([message.accountId, message.channelId, message.threadId ?? "", message.messageId]);
|
|
253
|
+
const statusId = activityMessages.get(key);
|
|
254
|
+
if (text === undefined) {
|
|
255
|
+
if (!statusId) return;
|
|
256
|
+
await request("deleteMessage", { chat_id: message.channelId, message_id: statusId }, signal);
|
|
257
|
+
activityMessages.delete(key);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (statusId) {
|
|
261
|
+
await request(
|
|
262
|
+
"editMessageText",
|
|
263
|
+
{ chat_id: message.channelId, message_id: statusId, text: `⏳ ${text}` },
|
|
264
|
+
signal,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const threadId = message.threadId === undefined ? undefined : Number(message.threadId);
|
|
269
|
+
if (threadId !== undefined && !isPositiveId(threadId)) throw new Error("Telegram topic identifier is invalid");
|
|
270
|
+
if (signal?.aborted) return;
|
|
271
|
+
// Keep creation alive after cancellation so a late ID can be deleted; HTTP has its own timeout.
|
|
272
|
+
const created = await request(
|
|
273
|
+
"sendMessage",
|
|
274
|
+
{
|
|
275
|
+
chat_id: message.channelId,
|
|
276
|
+
text: `⏳ ${text}`,
|
|
277
|
+
...(threadId === undefined ? {} : { message_thread_id: threadId }),
|
|
278
|
+
},
|
|
279
|
+
undefined,
|
|
280
|
+
);
|
|
281
|
+
if (!isRecord(created) || !isPositiveId(created.message_id)) throw new TelegramApiError(400);
|
|
282
|
+
if (signal?.aborted) {
|
|
283
|
+
try {
|
|
284
|
+
await request(
|
|
285
|
+
"deleteMessage",
|
|
286
|
+
{ chat_id: message.channelId, message_id: created.message_id },
|
|
287
|
+
AbortSignal.timeout(3_000),
|
|
288
|
+
);
|
|
289
|
+
} catch {
|
|
290
|
+
logger.warn("Telegram late activity removal failed");
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
activityMessages.set(key, created.message_id);
|
|
295
|
+
},
|
|
243
296
|
};
|
|
244
297
|
}
|
package/src/messenger/types.ts
CHANGED
|
@@ -27,11 +27,15 @@ export interface MessengerAdapter {
|
|
|
27
27
|
status: "processing" | "success" | "failure",
|
|
28
28
|
signal?: AbortSignal,
|
|
29
29
|
): Promise<void>;
|
|
30
|
+
/** Remove an in-progress status after cancellation without adding a terminal status. */
|
|
31
|
+
clearStatus?(message: MessengerMessage, signal?: AbortSignal): Promise<void>;
|
|
30
32
|
/**
|
|
31
33
|
* Show `text` (one sanitized line) as the request's single in-progress status, editing it in place on
|
|
32
34
|
* later calls; `undefined` removes it. The gateway serializes and throttles calls per request.
|
|
33
35
|
*/
|
|
34
36
|
activity?(message: MessengerMessage, text: string | undefined, signal?: AbortSignal): Promise<void>;
|
|
37
|
+
/** Forget request-local cosmetic activity state after the gateway finishes cleanup retries. */
|
|
38
|
+
releaseActivity?(message: MessengerMessage): void;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export interface MessengerPlatformConfig {
|