@coinseeker/opencode-telegram-plugin 1.1.3 → 1.1.5
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 +4 -4
- package/dist/telegram-remote.js +183 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,15 +15,15 @@ Configure the npm package in `~/.config/opencode/opencode.json`:
|
|
|
15
15
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
|
-
"plugin": ["@coinseeker/opencode-telegram-plugin@1.1.
|
|
18
|
+
"plugin": ["@coinseeker/opencode-telegram-plugin@1.1.4"]
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Current stable version: `@coinseeker/opencode-telegram-plugin@1.1.
|
|
22
|
+
Current stable version: `@coinseeker/opencode-telegram-plugin@1.1.4`.
|
|
23
23
|
|
|
24
24
|
Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
|
|
25
25
|
|
|
26
|
-
To update an existing install, replace the previous pinned package entry with `@coinseeker/opencode-telegram-plugin@1.1.
|
|
26
|
+
To update an existing install, replace the previous pinned package entry with `@coinseeker/opencode-telegram-plugin@1.1.4`, keep the rest of the `plugin` array unchanged, and restart OpenCode.
|
|
27
27
|
|
|
28
28
|
## Configure Telegram
|
|
29
29
|
|
|
@@ -62,7 +62,7 @@ Keep this file private. Never commit or share your Telegram bot token.
|
|
|
62
62
|
- Multi-session-safe Telegram polling through a file-lock leader model.
|
|
63
63
|
- Log file output instead of stdout terminal spam.
|
|
64
64
|
- Cross-process remote session listing via `/sessions`, `/status N`, `/start_work N`, `/help` slash commands.
|
|
65
|
-
- Safety-gated remote `/start-work` execution: verifies agent
|
|
65
|
+
- Safety-gated remote `/start-work` execution: verifies a raw `plan` agent or Prometheus Plan Builder label, idle status, incomplete plan, and no active boulder before dispatching.
|
|
66
66
|
|
|
67
67
|
## Logs
|
|
68
68
|
|
package/dist/telegram-remote.js
CHANGED
|
@@ -269,11 +269,24 @@ This chat is now active for OpenCode notifications.`
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
// src/config.ts
|
|
272
|
+
function parseInteger(value) {
|
|
273
|
+
if (!/^-?\d+$/.test(value)) return void 0;
|
|
274
|
+
const parsed = Number(value);
|
|
275
|
+
return Number.isSafeInteger(parsed) ? parsed : void 0;
|
|
276
|
+
}
|
|
272
277
|
function parseAllowedUserIds(value) {
|
|
273
278
|
if (!value || value.trim() === "") {
|
|
274
|
-
return
|
|
279
|
+
return void 0;
|
|
280
|
+
}
|
|
281
|
+
const tokens = value.split(",").map((id2) => id2.trim()).filter((id2) => id2 !== "");
|
|
282
|
+
if (tokens.length === 0) return void 0;
|
|
283
|
+
const parsed = [];
|
|
284
|
+
for (const token of tokens) {
|
|
285
|
+
const id2 = parseInteger(token);
|
|
286
|
+
if (id2 === void 0) return void 0;
|
|
287
|
+
parsed.push(id2);
|
|
275
288
|
}
|
|
276
|
-
return
|
|
289
|
+
return parsed;
|
|
277
290
|
}
|
|
278
291
|
function loadConfig(opts) {
|
|
279
292
|
const { logger, env } = opts;
|
|
@@ -285,18 +298,23 @@ function loadConfig(opts) {
|
|
|
285
298
|
throw new Error("Missing required environment variable: TELEGRAM_BOT_TOKEN");
|
|
286
299
|
}
|
|
287
300
|
const allowedUserIds = parseAllowedUserIds(allowedUserIdsStr);
|
|
288
|
-
if (allowedUserIds
|
|
301
|
+
if (allowedUserIds === void 0) {
|
|
289
302
|
logger.error("missing or invalid TELEGRAM_ALLOWED_USER_IDS");
|
|
290
303
|
throw new Error("Missing or invalid TELEGRAM_ALLOWED_USER_IDS");
|
|
291
304
|
}
|
|
292
305
|
let chatId;
|
|
293
306
|
if (chatIdStr && chatIdStr.trim() !== "") {
|
|
294
|
-
const parsed =
|
|
295
|
-
if (
|
|
296
|
-
|
|
307
|
+
const parsed = parseInteger(chatIdStr.trim());
|
|
308
|
+
if (parsed === void 0) {
|
|
309
|
+
logger.error("invalid TELEGRAM_CHAT_ID");
|
|
310
|
+
throw new Error("Invalid TELEGRAM_CHAT_ID");
|
|
297
311
|
}
|
|
312
|
+
chatId = parsed;
|
|
298
313
|
}
|
|
299
|
-
logger.info("config loaded", {
|
|
314
|
+
logger.info("config loaded", {
|
|
315
|
+
allowedUserCount: allowedUserIds.length,
|
|
316
|
+
hasChatId: chatId !== void 0
|
|
317
|
+
});
|
|
300
318
|
return {
|
|
301
319
|
botToken,
|
|
302
320
|
allowedUserIds,
|
|
@@ -305,9 +323,9 @@ function loadConfig(opts) {
|
|
|
305
323
|
}
|
|
306
324
|
|
|
307
325
|
// src/lib/claim.ts
|
|
326
|
+
import { createHash } from "crypto";
|
|
308
327
|
import { mkdir, open, readdir, stat, unlink } from "fs/promises";
|
|
309
328
|
import { join } from "path";
|
|
310
|
-
import { createHash } from "crypto";
|
|
311
329
|
var DEFAULT_TTL_MS = 6e4;
|
|
312
330
|
var sweptDirs = /* @__PURE__ */ new Set();
|
|
313
331
|
function hasCode(err, code) {
|
|
@@ -322,16 +340,18 @@ async function sweep(claimsDir, ttlMs) {
|
|
|
322
340
|
sweptDirs.add(claimsDir);
|
|
323
341
|
try {
|
|
324
342
|
const entries = await readdir(claimsDir, { withFileTypes: true });
|
|
325
|
-
await Promise.all(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
343
|
+
await Promise.all(
|
|
344
|
+
entries.filter((entry) => entry.isFile() && entry.name.endsWith(".claim")).map(async (entry) => {
|
|
345
|
+
const filePath = join(claimsDir, entry.name);
|
|
346
|
+
try {
|
|
347
|
+
const fileStat = await stat(filePath);
|
|
348
|
+
if (Date.now() - fileStat.mtimeMs > ttlMs * 2) {
|
|
349
|
+
await unlink(filePath);
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
331
352
|
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
}));
|
|
353
|
+
})
|
|
354
|
+
);
|
|
335
355
|
} catch {
|
|
336
356
|
}
|
|
337
357
|
}
|
|
@@ -366,6 +386,13 @@ async function claimOnce(opts) {
|
|
|
366
386
|
}
|
|
367
387
|
return false;
|
|
368
388
|
}
|
|
389
|
+
async function releaseClaim(opts) {
|
|
390
|
+
try {
|
|
391
|
+
await unlink(claimPath(opts.claimsDir, opts.key));
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (!(err instanceof Error) || !hasCode(err, "ENOENT")) throw err;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
369
396
|
|
|
370
397
|
// src/lib/pending-permissions.ts
|
|
371
398
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -562,7 +589,8 @@ async function upgradeLegacyPendingPermission(permission, ctx) {
|
|
|
562
589
|
}
|
|
563
590
|
async function handleNormalizedPermission(permission, ctx) {
|
|
564
591
|
const permissionKey = `${ctx.serverUrl.href}:${permission.sessionID}:${permission.requestID}`;
|
|
565
|
-
const
|
|
592
|
+
const claimKey = `permission:${permissionKey}`;
|
|
593
|
+
const claimed = await claimOnce({ claimsDir: ctx.claimsDir, key: claimKey });
|
|
566
594
|
if (!claimed) {
|
|
567
595
|
if (permission.endpoint === "request") await upgradeLegacyPendingPermission(permission, ctx);
|
|
568
596
|
return;
|
|
@@ -596,6 +624,7 @@ async function handleNormalizedPermission(permission, ctx) {
|
|
|
596
624
|
};
|
|
597
625
|
await ctx.pendingPermissions.savePending(shortHash, pending);
|
|
598
626
|
} catch (err) {
|
|
627
|
+
await releaseClaim({ claimsDir: ctx.claimsDir, key: claimKey });
|
|
599
628
|
ctx.logger.error("failed to send permission notification", { error: String(err) });
|
|
600
629
|
}
|
|
601
630
|
}
|
|
@@ -662,6 +691,11 @@ function createPermissionDispatcher(ctx) {
|
|
|
662
691
|
await ctx.bot.editMessageRemoveKeyboard(messageId, "This permission request has expired.");
|
|
663
692
|
return;
|
|
664
693
|
}
|
|
694
|
+
if (pending.expiresAt < Date.now()) {
|
|
695
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "This permission request has expired.");
|
|
696
|
+
await ctx.pendingPermissions.deletePending(shortHash);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
665
699
|
try {
|
|
666
700
|
await ctx.replyToPermission(
|
|
667
701
|
pending.requestID,
|
|
@@ -887,7 +921,7 @@ async function editPromptForQuestion(ctx, pending, shortHash, questionIndex) {
|
|
|
887
921
|
});
|
|
888
922
|
}
|
|
889
923
|
async function completeIfReady(ctx, pending, shortHash) {
|
|
890
|
-
const nextIndex = pending.answersInProgress.
|
|
924
|
+
const nextIndex = pending.answersInProgress.indexOf(null);
|
|
891
925
|
if (nextIndex >= 0) {
|
|
892
926
|
pending.currentQuestionIndex = nextIndex;
|
|
893
927
|
await ctx.pendingQuestions.savePending(shortHash, pending);
|
|
@@ -920,11 +954,8 @@ ${answerSummary(pending.questions, answers)}`
|
|
|
920
954
|
async function handleQuestionAsked(event, ctx) {
|
|
921
955
|
const request = event.properties;
|
|
922
956
|
if (request.questions.length === 0) return;
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
key: `question:${ctx.serverUrl.href}:${request.sessionID}:${request.id}`,
|
|
926
|
-
ttlMs: 5e3
|
|
927
|
-
});
|
|
957
|
+
const claimKey = `question:${ctx.serverUrl.href}:${request.sessionID}:${request.id}`;
|
|
958
|
+
const claimed = await claimOnce({ claimsDir: ctx.claimsDir, key: claimKey, ttlMs: 5e3 });
|
|
928
959
|
if (!claimed) return;
|
|
929
960
|
const shortHash = createQuestionShortHash(request.id, request.sessionID, ctx.serverUrl.href);
|
|
930
961
|
const firstQuestion = request.questions[0];
|
|
@@ -957,6 +988,7 @@ async function handleQuestionAsked(event, ctx) {
|
|
|
957
988
|
count: request.questions.length
|
|
958
989
|
});
|
|
959
990
|
} catch (err) {
|
|
991
|
+
await releaseClaim({ claimsDir: ctx.claimsDir, key: claimKey });
|
|
960
992
|
ctx.logger.error("failed to send question prompt", {
|
|
961
993
|
error: String(err),
|
|
962
994
|
requestID: request.id
|
|
@@ -976,6 +1008,11 @@ function createQuestionDispatcher(ctx) {
|
|
|
976
1008
|
await ctx.bot.editMessageRemoveKeyboard(messageId, "This question has expired.");
|
|
977
1009
|
return;
|
|
978
1010
|
}
|
|
1011
|
+
if (pending.expiresAt < Date.now()) {
|
|
1012
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "This question has expired.");
|
|
1013
|
+
await ctx.pendingQuestions.deletePending(shortHash);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
979
1016
|
pending.expiresAt = Date.now() + QUESTION_EXPIRY_MS;
|
|
980
1017
|
const question = pending.questions[questionIndex];
|
|
981
1018
|
if (!question) return;
|
|
@@ -1030,6 +1067,11 @@ function createQuestionDispatcher(ctx) {
|
|
|
1030
1067
|
if (!match) return;
|
|
1031
1068
|
const awaiting = match.data.awaitingCustomFor;
|
|
1032
1069
|
if (!awaiting || awaiting.promptMessageId !== replyToMessageId) return;
|
|
1070
|
+
if (match.data.expiresAt < Date.now()) {
|
|
1071
|
+
await ctx.bot.sendMessage("This question has expired.");
|
|
1072
|
+
await ctx.pendingQuestions.deletePending(match.shortHash);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1033
1075
|
match.data.expiresAt = Date.now() + QUESTION_EXPIRY_MS;
|
|
1034
1076
|
const question = match.data.questions[awaiting.questionIndex];
|
|
1035
1077
|
if (question?.multiple === true) {
|
|
@@ -1084,7 +1126,7 @@ async function handleQuestionReplied(event, ctx) {
|
|
|
1084
1126
|
}
|
|
1085
1127
|
|
|
1086
1128
|
// src/lib/session-registry.ts
|
|
1087
|
-
import { chmod, mkdir as mkdir4,
|
|
1129
|
+
import { chmod, mkdir as mkdir4, readdir as readdir4, readFile as readFile3, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
1088
1130
|
import { join as join4 } from "path";
|
|
1089
1131
|
|
|
1090
1132
|
// src/lib/opencode-http.ts
|
|
@@ -1221,7 +1263,7 @@ async function getRemoteMessages(serverUrl, sessionId, limit, fetcher = fetch) {
|
|
|
1221
1263
|
|
|
1222
1264
|
// src/lib/session-registry.ts
|
|
1223
1265
|
function filenameForSession(sessionId) {
|
|
1224
|
-
return Buffer.from(sessionId).toString("base64url")
|
|
1266
|
+
return `${Buffer.from(sessionId).toString("base64url")}.json`;
|
|
1225
1267
|
}
|
|
1226
1268
|
function hasCode4(err, code) {
|
|
1227
1269
|
return err instanceof Error && "code" in err && err.code === code;
|
|
@@ -1341,7 +1383,7 @@ function createSessionRegistryStore(opts) {
|
|
|
1341
1383
|
...patch,
|
|
1342
1384
|
sessionId,
|
|
1343
1385
|
title: patch.title ?? existing.title,
|
|
1344
|
-
parentID: patch.parentID
|
|
1386
|
+
parentID: patch.parentID === void 0 ? existing.parentID : patch.parentID,
|
|
1345
1387
|
serverUrl: patch.serverUrl ?? existing.serverUrl,
|
|
1346
1388
|
updatedAt: patch.updatedAt ?? Date.now()
|
|
1347
1389
|
});
|
|
@@ -1429,6 +1471,13 @@ async function handleSessionError(event, ctx) {
|
|
|
1429
1471
|
ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
|
|
1430
1472
|
}
|
|
1431
1473
|
|
|
1474
|
+
// src/lib/plan-agent.ts
|
|
1475
|
+
function isPlanSessionAgent(agent) {
|
|
1476
|
+
if (!agent) return false;
|
|
1477
|
+
const normalized = agent.trim().replace(/[–—]/g, "-").replace(/\s+/g, " ").toLowerCase();
|
|
1478
|
+
return normalized === "plan" || normalized === "prometheus" || normalized === "prometheus - plan builder" || normalized === "prometheus (plan builder)";
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1432
1481
|
// src/lib/pending-start-work.ts
|
|
1433
1482
|
import { createHash as createHash4 } from "crypto";
|
|
1434
1483
|
import { mkdir as mkdir5, readdir as readdir5, readFile as readFile4, rename as rename4, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -1453,6 +1502,14 @@ function parsePending3(text) {
|
|
|
1453
1502
|
throw new Error("Invalid pending start-work: expiresAt");
|
|
1454
1503
|
if (typeof parsed.telegramMessageId !== "number")
|
|
1455
1504
|
throw new Error("Invalid pending start-work: telegramMessageId");
|
|
1505
|
+
if (parsed.telegramMessageIds !== void 0 && (!Array.isArray(parsed.telegramMessageIds) || !parsed.telegramMessageIds.every((messageId) => typeof messageId === "number")))
|
|
1506
|
+
throw new Error("Invalid pending start-work: telegramMessageIds");
|
|
1507
|
+
if (parsed.status !== void 0 && parsed.status !== "pending" && parsed.status !== "consumed")
|
|
1508
|
+
throw new Error("Invalid pending start-work: status");
|
|
1509
|
+
if (parsed.handledAt !== void 0 && typeof parsed.handledAt !== "number")
|
|
1510
|
+
throw new Error("Invalid pending start-work: handledAt");
|
|
1511
|
+
parsed.telegramMessageIds = parsed.telegramMessageIds ?? [parsed.telegramMessageId];
|
|
1512
|
+
parsed.status = parsed.status ?? "pending";
|
|
1456
1513
|
return parsed;
|
|
1457
1514
|
}
|
|
1458
1515
|
async function listPendingFiles3(dir) {
|
|
@@ -1513,14 +1570,7 @@ function createStartWorkShortHash(sessionID) {
|
|
|
1513
1570
|
|
|
1514
1571
|
// src/events/start-work.ts
|
|
1515
1572
|
var CALLBACK_RE3 = /^sw:([^:]+)$/;
|
|
1516
|
-
var START_WORK_COMMAND = "start-work";
|
|
1517
1573
|
var START_WORK_EXPIRY_MS = 24 * 60 * 6e4;
|
|
1518
|
-
function startWorkKeyboard(shortHash) {
|
|
1519
|
-
const callbackData = `sw:${shortHash}`;
|
|
1520
|
-
if (Buffer.byteLength(callbackData, "utf8") > 64)
|
|
1521
|
-
throw new Error("Telegram callback_data exceeds 64 bytes");
|
|
1522
|
-
return [[{ text: "\u25B6\uFE0F Run /start-work", callback_data: callbackData }]];
|
|
1523
|
-
}
|
|
1524
1574
|
function planCompleteMessage(title) {
|
|
1525
1575
|
return title ? `plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.
|
|
1526
1576
|
|
|
@@ -1534,7 +1584,9 @@ function createPendingStartWork(sessionID, title, serverUrl, telegramMessageId)
|
|
|
1534
1584
|
title: title ?? void 0,
|
|
1535
1585
|
sentAt,
|
|
1536
1586
|
expiresAt: sentAt + START_WORK_EXPIRY_MS,
|
|
1537
|
-
telegramMessageId
|
|
1587
|
+
telegramMessageId,
|
|
1588
|
+
telegramMessageIds: [telegramMessageId],
|
|
1589
|
+
status: "pending"
|
|
1538
1590
|
};
|
|
1539
1591
|
}
|
|
1540
1592
|
function startWorkShortHash(sessionID) {
|
|
@@ -1545,6 +1597,34 @@ async function expirePending(ctx, shortHash, pending, messageId) {
|
|
|
1545
1597
|
await ctx.pendingStartWorks.deletePending(shortHash);
|
|
1546
1598
|
ctx.logger.info("pending start-work expired", { sessionID: pending.sessionID });
|
|
1547
1599
|
}
|
|
1600
|
+
var START_WORK_BUTTON_DISABLED_MESSAGE = "This /start-work button is no longer used. Use /sessions and /start_work <number> instead.";
|
|
1601
|
+
function messageIdsFor(pending, currentMessageId) {
|
|
1602
|
+
return [
|
|
1603
|
+
.../* @__PURE__ */ new Set([...pending.telegramMessageIds ?? [pending.telegramMessageId], currentMessageId])
|
|
1604
|
+
];
|
|
1605
|
+
}
|
|
1606
|
+
async function editDuplicateMessages(ctx, pending, currentMessageId) {
|
|
1607
|
+
for (const messageId of messageIdsFor(pending, currentMessageId)) {
|
|
1608
|
+
if (messageId === currentMessageId) continue;
|
|
1609
|
+
try {
|
|
1610
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, START_WORK_BUTTON_DISABLED_MESSAGE);
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
ctx.logger.warn("failed to clear duplicate start-work keyboard", {
|
|
1613
|
+
messageId,
|
|
1614
|
+
error: String(err)
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
async function consumePending(ctx, shortHash, pending, messageId) {
|
|
1620
|
+
await ctx.pendingStartWorks.savePending(shortHash, {
|
|
1621
|
+
...pending,
|
|
1622
|
+
telegramMessageId: messageId,
|
|
1623
|
+
telegramMessageIds: messageIdsFor(pending, messageId),
|
|
1624
|
+
status: "consumed",
|
|
1625
|
+
handledAt: Date.now()
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1548
1628
|
function createStartWorkDispatcher(ctx) {
|
|
1549
1629
|
return {
|
|
1550
1630
|
async handleCallbackQuery(data, messageId) {
|
|
@@ -1556,32 +1636,18 @@ function createStartWorkDispatcher(ctx) {
|
|
|
1556
1636
|
await ctx.bot.editMessageRemoveKeyboard(messageId, "This /start-work request has expired.");
|
|
1557
1637
|
return;
|
|
1558
1638
|
}
|
|
1639
|
+
if (pending.status === "consumed") {
|
|
1640
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, START_WORK_BUTTON_DISABLED_MESSAGE);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1559
1643
|
if (pending.expiresAt < Date.now()) {
|
|
1560
1644
|
await expirePending(ctx, shortHash, pending, messageId);
|
|
1561
1645
|
return;
|
|
1562
1646
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
messageId,
|
|
1568
|
-
`\u25B6\uFE0F Sent /start-work to opencode.
|
|
1569
|
-
|
|
1570
|
-
Session: ${label}`
|
|
1571
|
-
);
|
|
1572
|
-
ctx.logger.info("start-work command sent", { sessionID: pending.sessionID });
|
|
1573
|
-
} catch (err) {
|
|
1574
|
-
await ctx.bot.editMessageRemoveKeyboard(
|
|
1575
|
-
messageId,
|
|
1576
|
-
"\u26A0\uFE0F Failed to send /start-work to opencode"
|
|
1577
|
-
);
|
|
1578
|
-
ctx.logger.error("failed to send start-work command", {
|
|
1579
|
-
sessionID: pending.sessionID,
|
|
1580
|
-
error: String(err)
|
|
1581
|
-
});
|
|
1582
|
-
} finally {
|
|
1583
|
-
await ctx.pendingStartWorks.deletePending(shortHash);
|
|
1584
|
-
}
|
|
1647
|
+
await consumePending(ctx, shortHash, pending, messageId);
|
|
1648
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, START_WORK_BUTTON_DISABLED_MESSAGE);
|
|
1649
|
+
await editDuplicateMessages(ctx, pending, messageId);
|
|
1650
|
+
ctx.logger.info("legacy start-work button disabled", { sessionID: pending.sessionID });
|
|
1585
1651
|
}
|
|
1586
1652
|
};
|
|
1587
1653
|
}
|
|
@@ -1634,7 +1700,11 @@ async function hydrateDescendants(sessionId, ctx, seen = /* @__PURE__ */ new Set
|
|
|
1634
1700
|
for (const child of result.data ?? []) {
|
|
1635
1701
|
ctx.sessionTitleService.setSessionInfo(child);
|
|
1636
1702
|
await ctx.sessionRegistry.upsertSession(
|
|
1637
|
-
registryEntryFromSession(
|
|
1703
|
+
registryEntryFromSession(
|
|
1704
|
+
child,
|
|
1705
|
+
ctx.serverUrl.href,
|
|
1706
|
+
ctx.sessionTitleService.getSessionStatus(child.id)
|
|
1707
|
+
)
|
|
1638
1708
|
);
|
|
1639
1709
|
await hydrateDescendants(child.id, ctx, seen);
|
|
1640
1710
|
}
|
|
@@ -1655,18 +1725,24 @@ async function sendIdleNotification(sessionId, ctx) {
|
|
|
1655
1725
|
if (!claimed) return;
|
|
1656
1726
|
const title = ctx.sessionTitleService.getSessionTitle(sessionId);
|
|
1657
1727
|
const agent = ctx.sessionTitleService.getSessionAgent(sessionId);
|
|
1658
|
-
const isPlanSession = agent
|
|
1728
|
+
const isPlanSession = isPlanSessionAgent(agent);
|
|
1659
1729
|
const text = isPlanSession ? planCompleteMessage(title) : agentFinishedMessage(title, agent);
|
|
1660
1730
|
try {
|
|
1661
1731
|
if (isPlanSession) {
|
|
1662
1732
|
const shortHash = startWorkShortHash(sessionId);
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1733
|
+
const pending = await ctx.pendingStartWorks.loadPending(shortHash);
|
|
1734
|
+
if (pending && pending.expiresAt >= Date.now()) {
|
|
1735
|
+
ctx.logger.info("plan completion notice already sent - skipping duplicate", { sessionId });
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
if (pending) await ctx.pendingStartWorks.deletePending(shortHash);
|
|
1739
|
+
const message = await ctx.bot.sendMessage(text);
|
|
1740
|
+
const sentAt = Date.now();
|
|
1741
|
+
await ctx.pendingStartWorks.savePending(shortHash, {
|
|
1742
|
+
...createPendingStartWork(sessionId, title, ctx.serverUrl.href, message.message_id),
|
|
1743
|
+
status: "consumed",
|
|
1744
|
+
handledAt: sentAt
|
|
1665
1745
|
});
|
|
1666
|
-
await ctx.pendingStartWorks.savePending(
|
|
1667
|
-
shortHash,
|
|
1668
|
-
createPendingStartWork(sessionId, title, ctx.serverUrl.href, message.message_id)
|
|
1669
|
-
);
|
|
1670
1746
|
} else {
|
|
1671
1747
|
await ctx.bot.sendMessage(text);
|
|
1672
1748
|
}
|
|
@@ -1771,7 +1847,10 @@ async function handleSessionStatus(event, ctx) {
|
|
|
1771
1847
|
return;
|
|
1772
1848
|
}
|
|
1773
1849
|
if (previousStatus !== statusType) {
|
|
1774
|
-
await ctx.sessionRegistry.updateSession(sessionId, {
|
|
1850
|
+
await ctx.sessionRegistry.updateSession(sessionId, {
|
|
1851
|
+
status: statusType,
|
|
1852
|
+
updatedAt: Date.now()
|
|
1853
|
+
});
|
|
1775
1854
|
}
|
|
1776
1855
|
}
|
|
1777
1856
|
|
|
@@ -2409,7 +2488,7 @@ function createStatusDispatcher(deps) {
|
|
|
2409
2488
|
projectRoot,
|
|
2410
2489
|
sessionId: entry.sessionId,
|
|
2411
2490
|
planHint: rawTitle,
|
|
2412
|
-
allowLatestFallback: rawAgent
|
|
2491
|
+
allowLatestFallback: isPlanSessionAgent(rawAgent)
|
|
2413
2492
|
});
|
|
2414
2493
|
const userSnippet = buildSnippet(findLastByRole(messages, "user"));
|
|
2415
2494
|
const assistantSnippet = buildSnippet(findLastByRole(messages, "assistant"));
|
|
@@ -2470,6 +2549,38 @@ async function sendHtml(bot, text) {
|
|
|
2470
2549
|
async function sendPlain(bot, text) {
|
|
2471
2550
|
await bot.sendMessage(text);
|
|
2472
2551
|
}
|
|
2552
|
+
function pendingMessageIds(pending) {
|
|
2553
|
+
return [.../* @__PURE__ */ new Set([...pending.telegramMessageIds ?? [pending.telegramMessageId]])];
|
|
2554
|
+
}
|
|
2555
|
+
async function consumeInlineStartWorkButtons(bot, pendingStartWorks, sessionId, logger) {
|
|
2556
|
+
if (!pendingStartWorks) return;
|
|
2557
|
+
const shortHash = createStartWorkShortHash(sessionId);
|
|
2558
|
+
const pending = await pendingStartWorks.loadPending(shortHash);
|
|
2559
|
+
if (!pending) return;
|
|
2560
|
+
if (pending.expiresAt < Date.now()) {
|
|
2561
|
+
await pendingStartWorks.deletePending(shortHash);
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
await pendingStartWorks.savePending(shortHash, {
|
|
2565
|
+
...pending,
|
|
2566
|
+
status: "consumed",
|
|
2567
|
+
handledAt: Date.now()
|
|
2568
|
+
});
|
|
2569
|
+
for (const messageId of pendingMessageIds(pending)) {
|
|
2570
|
+
try {
|
|
2571
|
+
await bot.editMessageRemoveKeyboard(
|
|
2572
|
+
messageId,
|
|
2573
|
+
"This /start-work request was already handled. Use /start_work <number> from /sessions."
|
|
2574
|
+
);
|
|
2575
|
+
} catch (err) {
|
|
2576
|
+
logger.error("failed to clear start-work keyboard after command dispatch", {
|
|
2577
|
+
sessionId,
|
|
2578
|
+
messageId,
|
|
2579
|
+
error: String(err)
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2473
2584
|
function createStartWorkCommandDispatcher(deps) {
|
|
2474
2585
|
return async ({ chatId, bot, args }) => {
|
|
2475
2586
|
const rawIndex = args[0]?.trim();
|
|
@@ -2528,10 +2639,10 @@ function createStartWorkCommandDispatcher(deps) {
|
|
|
2528
2639
|
return;
|
|
2529
2640
|
}
|
|
2530
2641
|
const agent = deps.sessionTitleService.getSessionAgent(sessionId) ?? agentFromSession3(session) ?? entry.agent;
|
|
2531
|
-
if (agent
|
|
2642
|
+
if (!isPlanSessionAgent(agent)) {
|
|
2532
2643
|
await sendPlain(
|
|
2533
2644
|
bot,
|
|
2534
|
-
`${index}\uBC88 \uC138\uC158\uC758 \uC5D0\uC774\uC804\uD2B8\uB294
|
|
2645
|
+
`${index}\uBC88 \uC138\uC158\uC758 \uC5D0\uC774\uC804\uD2B8\uB294 plan builder \uAC00 \uC544\uB2D9\uB2C8\uB2E4 (\uD604\uC7AC: ${agent ?? "unknown"}). /start_work \uB294 plan \uC138\uC158\uC5D0\uC11C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4`
|
|
2535
2646
|
);
|
|
2536
2647
|
return;
|
|
2537
2648
|
}
|
|
@@ -2554,6 +2665,7 @@ function createStartWorkCommandDispatcher(deps) {
|
|
|
2554
2665
|
}
|
|
2555
2666
|
try {
|
|
2556
2667
|
await deps.runSessionCommand(sessionId, "start-work", sourceServerUrl);
|
|
2668
|
+
await consumeInlineStartWorkButtons(bot, deps.pendingStartWorks, sessionId, deps.logger);
|
|
2557
2669
|
await sendHtml(
|
|
2558
2670
|
bot,
|
|
2559
2671
|
`${index}\uBC88 \uC138\uC158\uC5D0 opencode /start-work \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC804\uC1A1 \uC644\uB8CC. (${escapeHtml(entry.title)})`
|
|
@@ -2577,7 +2689,7 @@ var HELP_TEXT = `<b>OpenCode Telegram Plugin \u2014 \uBA85\uB839 \uB3C4\uC6C0\uB
|
|
|
2577
2689
|
|
|
2578
2690
|
<b>/start_work <\uBC88\uD638></b>
|
|
2579
2691
|
\uD574\uB2F9 \uC138\uC158\uC5D0 opencode <code>/start-work</code> \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC804\uC1A1.
|
|
2580
|
-
\uC548\uC804 \uAC8C\uC774\uD2B8:
|
|
2692
|
+
\uC548\uC804 \uAC8C\uC774\uD2B8: raw plan agent \uB610\uB294 Prometheus Plan Builder \uB77C\uBCA8 AND status=idle AND .omo/plans \uC5D0 \uBBF8\uC644\uB8CC plan \uC874\uC7AC AND .omo/boulder.json \uBD80\uC7AC.
|
|
2581
2693
|
\uC870\uAC74 \uBBF8\uCDA9\uC871\uC2DC \uAD6C\uCCB4\uC801 \uC0AC\uC720 \uC548\uB0B4.
|
|
2582
2694
|
(Telegram \uBD07 \uBA85\uB839\uC740 <code>/start_work</code>, \uB0B4\uBD80 \uD2B8\uB9AC\uAC70 \uB300\uC0C1\uC740 opencode \uC758 <code>/start-work</code>)
|
|
2583
2695
|
|
|
@@ -3367,6 +3479,7 @@ var TelegramRemote = async (input) => {
|
|
|
3367
3479
|
sessionTitleService,
|
|
3368
3480
|
client: input.client,
|
|
3369
3481
|
serverUrl: input.serverUrl.href,
|
|
3482
|
+
pendingStartWorks,
|
|
3370
3483
|
runSessionCommand,
|
|
3371
3484
|
logger
|
|
3372
3485
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinseeker/opencode-telegram-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Control and monitor OpenCode from Telegram with notifications, question replies, and subagent-aware completion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/telegram-remote.js",
|