@coinseeker/opencode-telegram-plugin 1.0.13 → 1.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/README.md CHANGED
@@ -15,11 +15,11 @@ Configure the npm package in `~/.config/opencode/opencode.json`:
15
15
 
16
16
  ```json
17
17
  {
18
- "plugin": ["@coinseeker/opencode-telegram-plugin@1.0.12"]
18
+ "plugin": ["@coinseeker/opencode-telegram-plugin@1.1.0"]
19
19
  }
20
20
  ```
21
21
 
22
- Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.12`.
22
+ Current stable version: `@coinseeker/opencode-telegram-plugin@1.1.0`.
23
23
 
24
24
  Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
25
25
 
@@ -59,7 +59,7 @@ Keep this file private. Never commit or share your Telegram bot token.
59
59
  - Permission approve/reject buttons from Telegram.
60
60
  - Multi-session-safe Telegram polling through a file-lock leader model.
61
61
  - Log file output instead of stdout terminal spam.
62
- - Remote session listing via `/sessions`, `/status N`, `/start_work N`, `/help` slash commands.
62
+ - Cross-process remote session listing via `/sessions`, `/status N`, `/start_work N`, `/help` slash commands.
63
63
  - Safety-gated remote `/start-work` execution: verifies agent=plan, idle status, incomplete plan, and no active boulder before dispatching.
64
64
 
65
65
  ## Logs
@@ -6,7 +6,7 @@
6
6
  // src/telegram-remote.ts
7
7
  import { createHash as createHash5 } from "crypto";
8
8
  import { homedir as homedir3, tmpdir as tmpdir5 } from "os";
9
- import { dirname as dirname6, join as join9 } from "path";
9
+ import { dirname as dirname6, join as join10 } from "path";
10
10
  import { fileURLToPath } from "url";
11
11
 
12
12
  // src/bot.ts
@@ -923,6 +923,7 @@ function createQuestionDispatcher(ctx) {
923
923
  await expirePending2(ctx, shortHash, pending, messageId);
924
924
  return;
925
925
  }
926
+ pending.expiresAt = Date.now() + QUESTION_EXPIRY_MS;
926
927
  const question = pending.questions[questionIndex];
927
928
  if (!question) return;
928
929
  if (selection === "c") {
@@ -980,6 +981,7 @@ function createQuestionDispatcher(ctx) {
980
981
  await expirePending2(ctx, match.shortHash, match.data, match.data.telegramMessageIds[0]);
981
982
  return;
982
983
  }
984
+ match.data.expiresAt = Date.now() + QUESTION_EXPIRY_MS;
983
985
  const question = match.data.questions[awaiting.questionIndex];
984
986
  if (question?.multiple === true) {
985
987
  const current = selectedAnswers(match.data, awaiting.questionIndex);
@@ -1017,9 +1019,313 @@ async function handleQuestionReplied(event, ctx) {
1017
1019
  }
1018
1020
  }
1019
1021
 
1022
+ // src/lib/session-registry.ts
1023
+ import { chmod, mkdir as mkdir4, readFile as readFile3, readdir as readdir4, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
1024
+ import { join as join4 } from "path";
1025
+
1026
+ // src/lib/opencode-http.ts
1027
+ var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
1028
+ function asRecord(value) {
1029
+ if (!value || typeof value !== "object") return void 0;
1030
+ return value;
1031
+ }
1032
+ function isStatusType(value) {
1033
+ return value === "idle" || value === "busy" || value === "retry";
1034
+ }
1035
+ function normalizeOpenCodeServerUrl(value) {
1036
+ if (!value) return void 0;
1037
+ try {
1038
+ const url = new URL(value);
1039
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
1040
+ if (url.username || url.password) return void 0;
1041
+ if (url.search || url.hash) return void 0;
1042
+ if (url.pathname !== "/" && url.pathname !== "") return void 0;
1043
+ if (!ALLOWED_HOSTNAMES.has(url.hostname)) return void 0;
1044
+ return url.href;
1045
+ } catch {
1046
+ return void 0;
1047
+ }
1048
+ }
1049
+ function requireOpenCodeServerUrl(serverUrl) {
1050
+ const normalized = normalizeOpenCodeServerUrl(serverUrl);
1051
+ if (!normalized) throw new Error("Invalid OpenCode server URL");
1052
+ return normalized;
1053
+ }
1054
+ function endpoint(serverUrl, path) {
1055
+ return new URL(path, requireOpenCodeServerUrl(serverUrl));
1056
+ }
1057
+ function isDifferentServerUrl(sourceServerUrl, currentServerUrl) {
1058
+ const source = normalizeOpenCodeServerUrl(sourceServerUrl);
1059
+ const current = normalizeOpenCodeServerUrl(currentServerUrl);
1060
+ if (!source || !current) return false;
1061
+ return source !== current;
1062
+ }
1063
+ function normalizeSession(value) {
1064
+ const record = asRecord(value);
1065
+ if (!record || typeof record.directory !== "string") return void 0;
1066
+ const session = { directory: record.directory };
1067
+ if (typeof record.id === "string") session.id = record.id;
1068
+ if (typeof record.title === "string") session.title = record.title;
1069
+ if (typeof record.parentID === "string" || record.parentID === null) {
1070
+ session.parentID = record.parentID;
1071
+ }
1072
+ if (typeof record.agent === "string") session.agent = record.agent;
1073
+ return session;
1074
+ }
1075
+ function normalizeSessionList(value) {
1076
+ if (!Array.isArray(value)) return [];
1077
+ const sessions = [];
1078
+ for (const raw of value) {
1079
+ const record = asRecord(raw);
1080
+ const time = asRecord(record?.time);
1081
+ if (!record) continue;
1082
+ if (typeof record.id !== "string") continue;
1083
+ if (typeof record.title !== "string") continue;
1084
+ if (!time || typeof time.updated !== "number") continue;
1085
+ const session = {
1086
+ id: record.id,
1087
+ title: record.title,
1088
+ time: { updated: time.updated }
1089
+ };
1090
+ if (typeof record.parentID === "string" || record.parentID === null) {
1091
+ session.parentID = record.parentID;
1092
+ }
1093
+ if (typeof record.agent === "string") session.agent = record.agent;
1094
+ sessions.push(session);
1095
+ }
1096
+ return sessions;
1097
+ }
1098
+ function normalizeStatusMap(value) {
1099
+ const record = asRecord(value);
1100
+ if (!record) return {};
1101
+ const out = {};
1102
+ for (const [sessionId, rawStatus] of Object.entries(record)) {
1103
+ const status = asRecord(rawStatus);
1104
+ if (status && isStatusType(status.type)) out[sessionId] = { type: status.type };
1105
+ }
1106
+ return out;
1107
+ }
1108
+ function normalizeMessages(value) {
1109
+ if (!Array.isArray(value)) return [];
1110
+ const messages = [];
1111
+ for (const rawMessage of value) {
1112
+ const message = asRecord(rawMessage);
1113
+ const info = asRecord(message?.info);
1114
+ if (!message || !info || typeof info.role !== "string" || !Array.isArray(message.parts)) continue;
1115
+ const parts = [];
1116
+ for (const rawPart of message.parts) {
1117
+ const part = asRecord(rawPart);
1118
+ if (!part || typeof part.type !== "string") continue;
1119
+ const normalized = { type: part.type };
1120
+ if (typeof part.text === "string") normalized.text = part.text;
1121
+ parts.push(normalized);
1122
+ }
1123
+ messages.push({ info: { role: info.role }, parts });
1124
+ }
1125
+ return messages;
1126
+ }
1127
+ async function fetchJson(serverUrl, path, fetcher) {
1128
+ const response = await fetcher(endpoint(serverUrl, path), { redirect: "error" });
1129
+ if (response.status === 404) return { data: void 0, response: { status: response.status } };
1130
+ if (!response.ok) {
1131
+ throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}`);
1132
+ }
1133
+ return { data: await response.json(), response: { status: response.status } };
1134
+ }
1135
+ async function getRemoteSession(serverUrl, sessionId, fetcher = fetch) {
1136
+ const result = await fetchJson(serverUrl, `/session/${encodeURIComponent(sessionId)}`, fetcher);
1137
+ return { data: normalizeSession(result.data), response: result.response };
1138
+ }
1139
+ async function getRemoteStatusMap(serverUrl, fetcher = fetch) {
1140
+ const result = await fetchJson(serverUrl, "/session/status", fetcher);
1141
+ return normalizeStatusMap(result.data);
1142
+ }
1143
+ async function getRemoteSessions(serverUrl, fetcher = fetch) {
1144
+ const result = await fetchJson(serverUrl, "/session", fetcher);
1145
+ return normalizeSessionList(result.data);
1146
+ }
1147
+ async function getRemoteMessages(serverUrl, sessionId, limit, fetcher = fetch) {
1148
+ const url = endpoint(serverUrl, `/session/${encodeURIComponent(sessionId)}/message`);
1149
+ url.searchParams.set("limit", String(limit));
1150
+ const response = await fetcher(url, { redirect: "error" });
1151
+ if (response.status === 404) return [];
1152
+ if (!response.ok) {
1153
+ throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}`);
1154
+ }
1155
+ return normalizeMessages(await response.json());
1156
+ }
1157
+
1158
+ // src/lib/session-registry.ts
1159
+ function filenameForSession(sessionId) {
1160
+ return Buffer.from(sessionId).toString("base64url") + ".json";
1161
+ }
1162
+ function hasCode4(err, code) {
1163
+ return err instanceof Error && "code" in err && err.code === code;
1164
+ }
1165
+ function normalizeEntry(entry) {
1166
+ const serverUrl = normalizeOpenCodeServerUrl(entry.serverUrl);
1167
+ if (!serverUrl) throw new Error("invalid registry serverUrl");
1168
+ const out = {
1169
+ sessionId: entry.sessionId,
1170
+ title: entry.title,
1171
+ parentID: entry.parentID,
1172
+ serverUrl,
1173
+ updatedAt: entry.updatedAt
1174
+ };
1175
+ if (entry.agent !== void 0) out.agent = entry.agent;
1176
+ if (entry.status !== void 0) out.status = entry.status;
1177
+ return out;
1178
+ }
1179
+ function isRegistryFile(value) {
1180
+ if (!value || typeof value !== "object") return false;
1181
+ const file = value;
1182
+ if (file.version !== 1) return false;
1183
+ const entry = file.entry;
1184
+ if (!entry || typeof entry !== "object") return false;
1185
+ const e = entry;
1186
+ if (typeof e.sessionId !== "string") return false;
1187
+ if (typeof e.title !== "string") return false;
1188
+ if (e.parentID !== null && typeof e.parentID !== "string") return false;
1189
+ if (e.agent !== void 0 && typeof e.agent !== "string") return false;
1190
+ if (e.status !== void 0 && e.status !== "idle" && e.status !== "busy" && e.status !== "retry") {
1191
+ return false;
1192
+ }
1193
+ if (typeof e.serverUrl !== "string") return false;
1194
+ if (!normalizeOpenCodeServerUrl(e.serverUrl)) return false;
1195
+ if (typeof e.updatedAt !== "number") return false;
1196
+ return true;
1197
+ }
1198
+ function agentFromSession(session) {
1199
+ const candidate = session;
1200
+ return typeof candidate.agent === "string" ? candidate.agent : void 0;
1201
+ }
1202
+ function registryEntryFromSession(session, serverUrl, status) {
1203
+ const entry = {
1204
+ sessionId: session.id,
1205
+ title: session.title,
1206
+ parentID: session.parentID ?? null,
1207
+ serverUrl,
1208
+ updatedAt: session.time.updated
1209
+ };
1210
+ const agent = agentFromSession(session);
1211
+ if (agent !== void 0) entry.agent = agent;
1212
+ if (status !== void 0) entry.status = status;
1213
+ return entry;
1214
+ }
1215
+ function createSessionRegistryStore(opts) {
1216
+ const registryDir = join4(opts.configDir, "session-registry", opts.tokenHash);
1217
+ function filePath(sessionId) {
1218
+ return join4(registryDir, filenameForSession(sessionId));
1219
+ }
1220
+ async function readEntry(sessionId) {
1221
+ let text;
1222
+ try {
1223
+ text = await readFile3(filePath(sessionId), "utf8");
1224
+ } catch (err) {
1225
+ if (hasCode4(err, "ENOENT")) return null;
1226
+ opts.logger.error("session-registry: failed to read file", {
1227
+ sessionId,
1228
+ error: String(err)
1229
+ });
1230
+ return null;
1231
+ }
1232
+ try {
1233
+ const parsed = JSON.parse(text);
1234
+ if (!isRegistryFile(parsed)) return null;
1235
+ return normalizeEntry(parsed.entry);
1236
+ } catch (err) {
1237
+ opts.logger.error("session-registry: corrupted JSON", { sessionId, error: String(err) });
1238
+ return null;
1239
+ }
1240
+ }
1241
+ async function writeEntry(entry) {
1242
+ await mkdir4(registryDir, { recursive: true });
1243
+ try {
1244
+ await chmod(registryDir, 448);
1245
+ } catch (err) {
1246
+ opts.logger.error("session-registry: failed to chmod dir", { error: String(err) });
1247
+ }
1248
+ const payload = { version: 1, entry: normalizeEntry(entry) };
1249
+ const target = filePath(entry.sessionId);
1250
+ const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
1251
+ await writeFile3(tmp, JSON.stringify(payload, null, 2), "utf8");
1252
+ try {
1253
+ await rename3(tmp, target);
1254
+ } catch (err) {
1255
+ try {
1256
+ await unlink4(tmp);
1257
+ } catch {
1258
+ }
1259
+ throw err;
1260
+ }
1261
+ await chmod(target, 384);
1262
+ }
1263
+ async function upsertSession(entry) {
1264
+ const existing = await readEntry(entry.sessionId);
1265
+ await writeEntry({
1266
+ ...existing,
1267
+ ...entry,
1268
+ agent: entry.agent ?? existing?.agent,
1269
+ status: entry.status ?? existing?.status
1270
+ });
1271
+ }
1272
+ async function updateSession(sessionId, patch) {
1273
+ const existing = await readEntry(sessionId);
1274
+ if (!existing) return;
1275
+ await writeEntry({
1276
+ ...existing,
1277
+ ...patch,
1278
+ sessionId,
1279
+ title: patch.title ?? existing.title,
1280
+ parentID: patch.parentID ?? existing.parentID,
1281
+ serverUrl: patch.serverUrl ?? existing.serverUrl,
1282
+ updatedAt: patch.updatedAt ?? Date.now()
1283
+ });
1284
+ }
1285
+ async function listSessions() {
1286
+ let names;
1287
+ try {
1288
+ names = await readdir4(registryDir);
1289
+ } catch (err) {
1290
+ if (hasCode4(err, "ENOENT")) return [];
1291
+ opts.logger.error("session-registry: failed to list dir", { error: String(err) });
1292
+ return [];
1293
+ }
1294
+ const entries = [];
1295
+ for (const name of names) {
1296
+ if (!name.endsWith(".json")) continue;
1297
+ let text;
1298
+ try {
1299
+ text = await readFile3(join4(registryDir, name), "utf8");
1300
+ } catch (err) {
1301
+ opts.logger.error("session-registry: failed to read listed file", {
1302
+ file: name,
1303
+ error: String(err)
1304
+ });
1305
+ continue;
1306
+ }
1307
+ try {
1308
+ const parsed = JSON.parse(text);
1309
+ if (isRegistryFile(parsed)) entries.push(normalizeEntry(parsed.entry));
1310
+ } catch (err) {
1311
+ opts.logger.error("session-registry: corrupted listed file", {
1312
+ file: name,
1313
+ error: String(err)
1314
+ });
1315
+ }
1316
+ }
1317
+ return entries;
1318
+ }
1319
+ return { upsertSession, updateSession, listSessions };
1320
+ }
1321
+
1020
1322
  // src/events/session-created.ts
1021
1323
  async function handleSessionCreated(event, ctx) {
1022
- ctx.sessionTitleService.setSessionInfo(event.properties.info);
1324
+ const info = event.properties.info;
1325
+ ctx.sessionTitleService.setSessionInfo(info);
1326
+ await ctx.sessionRegistry.upsertSession(
1327
+ registryEntryFromSession(info, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(info.id))
1328
+ );
1023
1329
  }
1024
1330
 
1025
1331
  // src/lib/abort-tracker.ts
@@ -1061,14 +1367,14 @@ async function handleSessionError(event, ctx) {
1061
1367
 
1062
1368
  // src/lib/pending-start-work.ts
1063
1369
  import { createHash as createHash4 } from "crypto";
1064
- import { mkdir as mkdir4, readdir as readdir4, readFile as readFile3, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
1370
+ import { mkdir as mkdir5, readdir as readdir5, readFile as readFile4, rename as rename4, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
1065
1371
  import { tmpdir as tmpdir3 } from "os";
1066
- import { dirname as dirname3, join as join4 } from "path";
1067
- function hasCode4(err, code) {
1372
+ import { dirname as dirname3, join as join5 } from "path";
1373
+ function hasCode5(err, code) {
1068
1374
  return "code" in err && err.code === code;
1069
1375
  }
1070
1376
  function pendingFilePath3(dir, shortHash) {
1071
- return join4(dir, `${shortHash}.json`);
1377
+ return join5(dir, `${shortHash}.json`);
1072
1378
  }
1073
1379
  function parsePending3(text) {
1074
1380
  const parsed = JSON.parse(text);
@@ -1087,10 +1393,10 @@ function parsePending3(text) {
1087
1393
  }
1088
1394
  async function listPendingFiles3(dir) {
1089
1395
  try {
1090
- const entries = await readdir4(dir, { withFileTypes: true });
1396
+ const entries = await readdir5(dir, { withFileTypes: true });
1091
1397
  return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
1092
1398
  } catch (err) {
1093
- if (err instanceof Error && hasCode4(err, "ENOENT")) return [];
1399
+ if (err instanceof Error && hasCode5(err, "ENOENT")) return [];
1094
1400
  throw err;
1095
1401
  }
1096
1402
  }
@@ -1098,29 +1404,29 @@ function shortHashFromFileName3(fileName) {
1098
1404
  return fileName.slice(0, -".json".length);
1099
1405
  }
1100
1406
  function createPendingStartWorkStore(opts) {
1101
- const dir = opts.baseDir ?? join4(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
1407
+ const dir = opts.baseDir ?? join5(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
1102
1408
  return {
1103
1409
  dir,
1104
1410
  async savePending(shortHash, data) {
1105
1411
  const filePath = pendingFilePath3(dir, shortHash);
1106
- await mkdir4(dirname3(filePath), { recursive: true });
1412
+ await mkdir5(dirname3(filePath), { recursive: true });
1107
1413
  const tmpPath = `${filePath}.tmp.${process.pid}`;
1108
- await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf8");
1109
- await rename3(tmpPath, filePath);
1414
+ await writeFile4(tmpPath, JSON.stringify(data, null, 2), "utf8");
1415
+ await rename4(tmpPath, filePath);
1110
1416
  },
1111
1417
  async loadPending(shortHash) {
1112
1418
  try {
1113
- return parsePending3(await readFile3(pendingFilePath3(dir, shortHash), "utf8"));
1419
+ return parsePending3(await readFile4(pendingFilePath3(dir, shortHash), "utf8"));
1114
1420
  } catch (err) {
1115
- if (err instanceof Error && hasCode4(err, "ENOENT")) return void 0;
1421
+ if (err instanceof Error && hasCode5(err, "ENOENT")) return void 0;
1116
1422
  throw err;
1117
1423
  }
1118
1424
  },
1119
1425
  async deletePending(shortHash) {
1120
1426
  try {
1121
- await unlink4(pendingFilePath3(dir, shortHash));
1427
+ await unlink5(pendingFilePath3(dir, shortHash));
1122
1428
  } catch (err) {
1123
- if (!(err instanceof Error) || !hasCode4(err, "ENOENT")) throw err;
1429
+ if (!(err instanceof Error) || !hasCode5(err, "ENOENT")) throw err;
1124
1430
  }
1125
1431
  },
1126
1432
  async sweepExpired() {
@@ -1218,9 +1524,21 @@ Session: ${label}`
1218
1524
 
1219
1525
  // src/events/session-idle.ts
1220
1526
  var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
1527
+ var DEFERRED_PARENT_CONFIRM_DELAY_MS = 2500;
1528
+ var deferredConfirmTimers = /* @__PURE__ */ new Map();
1221
1529
  function sleep(ms) {
1222
1530
  return new Promise((resolve) => setTimeout(resolve, ms));
1223
1531
  }
1532
+ function agentFinishedMessage(title, agent) {
1533
+ const base = title ? `Agent has finished: ${title}` : "Agent has finished.";
1534
+ return agent ? `${base} (${agent})` : base;
1535
+ }
1536
+ function cancelDeferredParentConfirm(sessionId) {
1537
+ const timer = deferredConfirmTimers.get(sessionId);
1538
+ if (timer === void 0) return;
1539
+ clearTimeout(timer);
1540
+ deferredConfirmTimers.delete(sessionId);
1541
+ }
1224
1542
  async function resolveParentID(sessionId, ctx) {
1225
1543
  const cachedParentID = ctx.sessionTitleService.getParentID(sessionId);
1226
1544
  if (cachedParentID !== void 0) return cachedParentID;
@@ -1228,6 +1546,13 @@ async function resolveParentID(sessionId, ctx) {
1228
1546
  const result = await ctx.client.session.get({ path: { id: sessionId } });
1229
1547
  if (result.data) {
1230
1548
  ctx.sessionTitleService.setSessionInfo(result.data);
1549
+ await ctx.sessionRegistry.upsertSession(
1550
+ registryEntryFromSession(
1551
+ result.data,
1552
+ ctx.serverUrl.href,
1553
+ ctx.sessionTitleService.getSessionStatus(sessionId)
1554
+ )
1555
+ );
1231
1556
  return ctx.sessionTitleService.getParentID(sessionId);
1232
1557
  }
1233
1558
  ctx.logger.warn("session parentID cache miss fetch returned no data", { sessionId });
@@ -1244,6 +1569,9 @@ async function hydrateDescendants(sessionId, ctx, seen = /* @__PURE__ */ new Set
1244
1569
  const result = await ctx.client.session.children({ path: { id: sessionId } });
1245
1570
  for (const child of result.data ?? []) {
1246
1571
  ctx.sessionTitleService.setSessionInfo(child);
1572
+ await ctx.sessionRegistry.upsertSession(
1573
+ registryEntryFromSession(child, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(child.id))
1574
+ );
1247
1575
  await hydrateDescendants(child.id, ctx, seen);
1248
1576
  }
1249
1577
  } catch (err) {
@@ -1262,8 +1590,9 @@ async function sendIdleNotification(sessionId, ctx) {
1262
1590
  });
1263
1591
  if (!claimed) return;
1264
1592
  const title = ctx.sessionTitleService.getSessionTitle(sessionId);
1265
- const isPlanSession = ctx.sessionTitleService.getSessionAgent(sessionId) === "plan";
1266
- const text = isPlanSession ? planCompleteMessage(title) : title ? `Agent has finished: ${title}` : "Agent has finished.";
1593
+ const agent = ctx.sessionTitleService.getSessionAgent(sessionId);
1594
+ const isPlanSession = agent === "plan";
1595
+ const text = isPlanSession ? planCompleteMessage(title) : agentFinishedMessage(title, agent);
1267
1596
  try {
1268
1597
  if (isPlanSession) {
1269
1598
  const shortHash = startWorkShortHash(sessionId);
@@ -1287,18 +1616,49 @@ async function flushDeferredParentIfReady(parentID, ctx) {
1287
1616
  if (!ctx.sessionTitleService.hasDeferredIdleNotification(parentID)) return;
1288
1617
  if (ctx.sessionTitleService.hasUnfinishedDescendants(parentID)) return;
1289
1618
  const parentStatus = ctx.sessionTitleService.getSessionStatus(parentID);
1290
- if (parentStatus === "idle") {
1291
- ctx.logger.info("keeping deferred parent idle notification - waiting for parent to resume", {
1619
+ if (parentStatus !== "idle") {
1620
+ if (parentStatus !== void 0) {
1621
+ cancelDeferredParentConfirm(parentID);
1622
+ ctx.sessionTitleService.clearDeferredIdleNotification(parentID);
1623
+ ctx.logger.info("clearing deferred parent idle notification - parent resumed", {
1624
+ sessionId: parentID
1625
+ });
1626
+ }
1627
+ return;
1628
+ }
1629
+ scheduleDeferredParentConfirm(parentID, ctx);
1630
+ }
1631
+ function scheduleDeferredParentConfirm(parentID, ctx) {
1632
+ if (deferredConfirmTimers.has(parentID)) return;
1633
+ const delay = ctx.deferredConfirmDelayMs ?? DEFERRED_PARENT_CONFIRM_DELAY_MS;
1634
+ const timer = setTimeout(() => {
1635
+ deferredConfirmTimers.delete(parentID);
1636
+ void confirmDeferredParentIdle(parentID, ctx);
1637
+ }, delay);
1638
+ timer.unref?.();
1639
+ deferredConfirmTimers.set(parentID, timer);
1640
+ ctx.logger.info("parent idle and descendants finished - confirming deferred notification", {
1641
+ sessionId: parentID
1642
+ });
1643
+ }
1644
+ async function confirmDeferredParentIdle(parentID, ctx) {
1645
+ if (!ctx.sessionTitleService.hasDeferredIdleNotification(parentID)) return;
1646
+ if (ctx.sessionTitleService.getSessionStatus(parentID) !== "idle") {
1647
+ ctx.sessionTitleService.clearDeferredIdleNotification(parentID);
1648
+ ctx.logger.info("clearing deferred parent idle notification - parent resumed during confirm", {
1292
1649
  sessionId: parentID
1293
1650
  });
1294
1651
  return;
1295
1652
  }
1296
- if (parentStatus !== void 0) {
1297
- ctx.sessionTitleService.clearDeferredIdleNotification(parentID);
1298
- ctx.logger.info("clearing deferred parent idle notification - parent resumed", {
1653
+ await hydrateDescendants(parentID, ctx);
1654
+ if (ctx.sessionTitleService.hasUnfinishedDescendants(parentID)) {
1655
+ ctx.logger.info("keeping deferred parent idle notification - descendants active again", {
1299
1656
  sessionId: parentID
1300
1657
  });
1658
+ return;
1301
1659
  }
1660
+ ctx.logger.info("sending deferred parent idle notification", { sessionId: parentID });
1661
+ await sendIdleNotification(parentID, ctx);
1302
1662
  }
1303
1663
  async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
1304
1664
  await hydrateDescendants(sessionId, ctx);
@@ -1313,6 +1673,7 @@ async function handleSessionIdle(event, ctx) {
1313
1673
  const sessionId = event.properties.sessionID;
1314
1674
  const parentID = await resolveParentID(sessionId, ctx);
1315
1675
  ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
1676
+ await ctx.sessionRegistry.updateSession(sessionId, { status: "idle", updatedAt: Date.now() });
1316
1677
  if (typeof parentID === "string") {
1317
1678
  ctx.logger.info("suppressing child session idle notification", { sessionId, parentID });
1318
1679
  await flushDeferredParentIfReady(parentID, ctx);
@@ -1339,9 +1700,14 @@ async function handleSessionIdle(event, ctx) {
1339
1700
  async function handleSessionStatus(event, ctx) {
1340
1701
  const sessionId = event.properties.sessionID;
1341
1702
  const statusType = event.properties.status.type;
1703
+ const previousStatus = ctx.sessionTitleService.getSessionStatus(sessionId);
1342
1704
  ctx.sessionTitleService.setSessionStatus(sessionId, statusType);
1343
1705
  if (statusType === "idle") {
1344
1706
  await handleSessionIdle(event, ctx);
1707
+ return;
1708
+ }
1709
+ if (previousStatus !== statusType) {
1710
+ await ctx.sessionRegistry.updateSession(sessionId, { status: statusType, updatedAt: Date.now() });
1345
1711
  }
1346
1712
  }
1347
1713
 
@@ -1349,6 +1715,9 @@ async function handleSessionStatus(event, ctx) {
1349
1715
  async function handleSessionUpdated(event, ctx) {
1350
1716
  const info = event.properties.info;
1351
1717
  ctx.sessionTitleService.setSessionInfo(info);
1718
+ await ctx.sessionRegistry.upsertSession(
1719
+ registryEntryFromSession(info, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(info.id))
1720
+ );
1352
1721
  }
1353
1722
 
1354
1723
  // src/lib/html-escape.ts
@@ -1370,11 +1739,110 @@ function stripCodeFences(input) {
1370
1739
  var MAX_BODY_CHARS = 3900;
1371
1740
  var MAX_TITLE_CHARS = 55;
1372
1741
  var MAX_SESSIONS = 20;
1742
+ function agentFromSession2(session) {
1743
+ const candidate = session;
1744
+ return typeof candidate.agent === "string" ? candidate.agent : void 0;
1745
+ }
1746
+ function isRootSession(session) {
1747
+ return session.parentID === void 0 || session.parentID === null;
1748
+ }
1749
+ function isRootRegistryEntry(entry) {
1750
+ return entry.parentID === null;
1751
+ }
1752
+ function isRootRemoteSession(session) {
1753
+ return session.parentID === void 0 || session.parentID === null;
1754
+ }
1755
+ function addRegistryRecord(combined, entry, status) {
1756
+ combined.set(entry.sessionId, {
1757
+ sessionId: entry.sessionId,
1758
+ title: entry.title,
1759
+ agent: entry.agent,
1760
+ status: status ?? entry.status ?? "idle",
1761
+ serverUrl: entry.serverUrl,
1762
+ updatedAt: entry.updatedAt
1763
+ });
1764
+ }
1765
+ async function addRemoteServerRecords(combined, serverUrl, deps) {
1766
+ const [remoteSessions, remoteStatusMap] = await Promise.all([
1767
+ getRemoteSessions(serverUrl, deps.opencodeFetch),
1768
+ getRemoteStatusMap(serverUrl, deps.opencodeFetch)
1769
+ ]);
1770
+ for (const session of remoteSessions.filter(isRootRemoteSession)) {
1771
+ const status = remoteStatusMap[session.id]?.type ?? "idle";
1772
+ combined.set(session.id, {
1773
+ sessionId: session.id,
1774
+ title: session.title,
1775
+ agent: session.agent,
1776
+ status,
1777
+ serverUrl,
1778
+ updatedAt: session.time.updated
1779
+ });
1780
+ await deps.sessionRegistry.upsertSession({
1781
+ sessionId: session.id,
1782
+ title: session.title,
1783
+ parentID: session.parentID ?? null,
1784
+ agent: session.agent,
1785
+ status,
1786
+ serverUrl,
1787
+ updatedAt: session.time.updated
1788
+ });
1789
+ }
1790
+ }
1373
1791
  function createSessionsDispatcher(deps) {
1374
1792
  return async ({ chatId, bot }) => {
1375
- const sessions = deps.sessionTitleService.getRootSessionsByRecency(MAX_SESSIONS);
1793
+ let sessions;
1794
+ try {
1795
+ const [listResult, statusResult] = await Promise.all([
1796
+ deps.client.session.list(),
1797
+ deps.client.session.status()
1798
+ ]);
1799
+ const statusMap = statusResult.data ?? {};
1800
+ for (const session of listResult.data ?? []) {
1801
+ deps.sessionTitleService.setSessionInfo(session);
1802
+ deps.sessionTitleService.setServerUrl(session.id, deps.serverUrl);
1803
+ const status = statusMap[session.id]?.type ?? "idle";
1804
+ await deps.sessionRegistry.upsertSession(
1805
+ registryEntryFromSession(session, deps.serverUrl, status)
1806
+ );
1807
+ if (status !== void 0) deps.sessionTitleService.setSessionStatus(session.id, status);
1808
+ }
1809
+ const registrySessions = await deps.sessionRegistry.listSessions();
1810
+ const combined = /* @__PURE__ */ new Map();
1811
+ const remoteServerUrls = /* @__PURE__ */ new Set();
1812
+ for (const entry of registrySessions.filter(isRootRegistryEntry)) {
1813
+ const serverUrl = normalizeOpenCodeServerUrl(entry.serverUrl);
1814
+ if (!serverUrl) continue;
1815
+ if (isDifferentServerUrl(serverUrl, deps.serverUrl)) {
1816
+ remoteServerUrls.add(serverUrl);
1817
+ continue;
1818
+ }
1819
+ addRegistryRecord(combined, { ...entry, serverUrl }, statusMap[entry.sessionId]?.type);
1820
+ }
1821
+ for (const serverUrl of remoteServerUrls) {
1822
+ try {
1823
+ await addRemoteServerRecords(combined, serverUrl, deps);
1824
+ } catch (err) {
1825
+ deps.logger.error("sessions remote server refresh failed", { serverUrl, error: String(err) });
1826
+ }
1827
+ }
1828
+ for (const session of (listResult.data ?? []).filter(isRootSession)) {
1829
+ combined.set(session.id, {
1830
+ sessionId: session.id,
1831
+ title: session.title,
1832
+ agent: agentFromSession2(session),
1833
+ status: statusMap[session.id]?.type ?? "idle",
1834
+ serverUrl: deps.serverUrl,
1835
+ updatedAt: session.time.updated
1836
+ });
1837
+ }
1838
+ sessions = [...combined.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS);
1839
+ } catch (err) {
1840
+ await bot.sendMessage("\uC138\uC158 \uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", { parse_mode: "HTML" });
1841
+ deps.logger.error("sessions list failed", { chatId, error: String(err) });
1842
+ return;
1843
+ }
1376
1844
  if (sessions.length === 0) {
1377
- await bot.sendMessage("\uD65C\uC131 \uC138\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", { parse_mode: "HTML" });
1845
+ await bot.sendMessage("\uC138\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", { parse_mode: "HTML" });
1378
1846
  return;
1379
1847
  }
1380
1848
  const capturedAt = Date.now();
@@ -1382,11 +1850,11 @@ function createSessionsDispatcher(deps) {
1382
1850
  const entry = {
1383
1851
  index: i + 1,
1384
1852
  sessionId: session.sessionId,
1385
- title: session.title ?? "",
1853
+ title: session.title,
1386
1854
  capturedAt
1387
1855
  };
1388
1856
  if (session.agent !== void 0) entry.agent = session.agent;
1389
- if (session.status !== void 0) entry.status = session.status;
1857
+ entry.status = session.status;
1390
1858
  if (session.serverUrl !== void 0) entry.serverUrl = session.serverUrl;
1391
1859
  return entry;
1392
1860
  });
@@ -1394,14 +1862,14 @@ function createSessionsDispatcher(deps) {
1394
1862
  const lines = entries.map((entry) => {
1395
1863
  const agent = entry.agent ? escapeHtml(entry.agent) : "?";
1396
1864
  const title = truncateForTelegram(escapeHtml(entry.title), MAX_TITLE_CHARS);
1397
- const status = entry.status ?? "unknown";
1398
- return `${entry.index}. [${agent}] ${title} \u2014 ${status}`;
1865
+ const status = ` \u2014 ${escapeHtml(entry.status ?? "idle")}`;
1866
+ return `${entry.index}. [${agent}] ${title}${status}`;
1399
1867
  });
1400
1868
  let body = lines.join("\n");
1401
1869
  if (body.length > MAX_BODY_CHARS) {
1402
1870
  body = body.slice(0, MAX_BODY_CHARS) + "\u2026";
1403
1871
  }
1404
- const text = `<b>\uD65C\uC131 \uC138\uC158 (top ${entries.length})</b>
1872
+ const text = `<b>\uCD5C\uADFC \uC138\uC158 (top ${entries.length})</b>
1405
1873
  ${body}
1406
1874
 
1407
1875
  <i>/status N \uB610\uB294 /start_work N \uC73C\uB85C \uC870\uC791</i>`;
@@ -1411,13 +1879,13 @@ ${body}
1411
1879
  }
1412
1880
 
1413
1881
  // src/lib/plan-readiness.ts
1414
- import { access, readFile as readFile4, readdir as readdir5, stat as stat2 } from "fs/promises";
1415
- import { join as join5 } from "path";
1882
+ import { access, readFile as readFile5, readdir as readdir6, stat as stat2 } from "fs/promises";
1883
+ import { join as join6 } from "path";
1416
1884
  async function checkPlanReadiness(args) {
1417
1885
  const { projectRoot } = args;
1418
- const omoDir = join5(projectRoot, ".omo");
1419
- const plansDir = join5(omoDir, "plans");
1420
- const boulderPath = join5(omoDir, "boulder.json");
1886
+ const omoDir = join6(projectRoot, ".omo");
1887
+ const plansDir = join6(omoDir, "plans");
1888
+ const boulderPath = join6(omoDir, "boulder.json");
1421
1889
  try {
1422
1890
  await access(omoDir);
1423
1891
  } catch {
@@ -1438,7 +1906,7 @@ async function checkPlanReadiness(args) {
1438
1906
  }
1439
1907
  let planFiles = [];
1440
1908
  try {
1441
- const entries = await readdir5(plansDir);
1909
+ const entries = await readdir6(plansDir);
1442
1910
  planFiles = entries.filter((e) => e.endsWith(".md"));
1443
1911
  } catch {
1444
1912
  return {
@@ -1456,14 +1924,14 @@ async function checkPlanReadiness(args) {
1456
1924
  }
1457
1925
  const stats = await Promise.all(
1458
1926
  planFiles.map(async (f) => {
1459
- const full = join5(plansDir, f);
1927
+ const full = join6(plansDir, f);
1460
1928
  const s = await stat2(full);
1461
1929
  return { path: full, name: f, mtime: s.mtime.getTime() };
1462
1930
  })
1463
1931
  );
1464
1932
  stats.sort((a, b) => b.mtime - a.mtime);
1465
1933
  const latest = stats[0];
1466
- const content = await readFile4(latest.path, "utf8");
1934
+ const content = await readFile5(latest.path, "utf8");
1467
1935
  const totalMatches = content.match(/^- \[[ xX]\]/gm) ?? [];
1468
1936
  const completedMatches = content.match(/^- \[[xX]\]/gm) ?? [];
1469
1937
  const total = totalMatches.length;
@@ -1494,7 +1962,7 @@ async function recheckSessionIdle(client, sessionId) {
1494
1962
  const result = await client.session.status();
1495
1963
  const statuses = result.data ?? {};
1496
1964
  const sessionStatus = statuses[sessionId];
1497
- return sessionStatus?.type === "idle";
1965
+ return (sessionStatus?.type ?? "idle") === "idle";
1498
1966
  }
1499
1967
 
1500
1968
  // src/events/status-command.ts
@@ -1593,25 +2061,65 @@ function createStatusDispatcher(deps) {
1593
2061
  });
1594
2062
  return;
1595
2063
  }
1596
- const [getResult, statusResult, messagesResult] = await Promise.all([
1597
- deps.client.session.get({ path: { id: entry.sessionId } }),
1598
- deps.client.session.status(),
1599
- deps.client.session.messages({
1600
- path: { id: entry.sessionId },
1601
- query: { limit: MESSAGES_LIMIT }
1602
- })
1603
- ]);
1604
- const session = getResult.data;
1605
- const responseStatus = getResult.response?.status;
2064
+ const rawSourceServerUrl = entry.serverUrl ?? deps.sessionTitleService.getServerUrl(entry.sessionId);
2065
+ const sourceServerUrl = normalizeOpenCodeServerUrl(rawSourceServerUrl);
2066
+ if (rawSourceServerUrl && !sourceServerUrl) {
2067
+ await bot.sendMessage("\uC138\uC158 \uC11C\uBC84 \uC815\uBCF4\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
2068
+ parse_mode: "HTML"
2069
+ });
2070
+ deps.logger.error("status invalid server url", { chatId, sessionId: entry.sessionId });
2071
+ return;
2072
+ }
2073
+ const useRemoteServer = isDifferentServerUrl(sourceServerUrl, deps.serverUrl);
2074
+ let session;
2075
+ let responseStatus;
2076
+ let sessionStatus = "idle";
2077
+ let messages = [];
2078
+ if (sourceServerUrl && useRemoteServer) {
2079
+ try {
2080
+ const getResult = await getRemoteSession(sourceServerUrl, entry.sessionId, deps.opencodeFetch);
2081
+ session = getResult.data;
2082
+ responseStatus = getResult.response.status;
2083
+ if (!session || responseStatus === 404) {
2084
+ await bot.sendMessage("\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
2085
+ parse_mode: "HTML"
2086
+ });
2087
+ return;
2088
+ }
2089
+ const [statusMap, remoteMessages] = await Promise.all([
2090
+ getRemoteStatusMap(sourceServerUrl, deps.opencodeFetch),
2091
+ getRemoteMessages(sourceServerUrl, entry.sessionId, MESSAGES_LIMIT, deps.opencodeFetch)
2092
+ ]);
2093
+ sessionStatus = statusMap[entry.sessionId]?.type ?? "idle";
2094
+ messages = remoteMessages;
2095
+ } catch (err) {
2096
+ await bot.sendMessage("\uC138\uC158 \uC0C1\uD0DC\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
2097
+ parse_mode: "HTML"
2098
+ });
2099
+ deps.logger.error("status remote lookup failed", { chatId, sessionId: entry.sessionId, error: String(err) });
2100
+ return;
2101
+ }
2102
+ } else {
2103
+ const [getResult, statusResult, messagesResult] = await Promise.all([
2104
+ deps.client.session.get({ path: { id: entry.sessionId } }),
2105
+ deps.client.session.status(),
2106
+ deps.client.session.messages({
2107
+ path: { id: entry.sessionId },
2108
+ query: { limit: MESSAGES_LIMIT }
2109
+ })
2110
+ ]);
2111
+ session = normalizeSession(getResult.data);
2112
+ responseStatus = getResult.response?.status;
2113
+ const statusMap = normalizeStatusMap(statusResult.data);
2114
+ sessionStatus = statusMap[entry.sessionId]?.type ?? "idle";
2115
+ messages = normalizeMessages(messagesResult.data);
2116
+ }
1606
2117
  if (!session || responseStatus === 404) {
1607
2118
  await bot.sendMessage("\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
1608
2119
  parse_mode: "HTML"
1609
2120
  });
1610
2121
  return;
1611
2122
  }
1612
- const statusMap = statusResult.data ?? {};
1613
- const sessionStatus = statusMap[entry.sessionId]?.type ?? "unknown";
1614
- const messages = messagesResult.data ?? [];
1615
2123
  const projectRoot = resolveProjectRoot(session);
1616
2124
  const planReady = await checkPlanReadiness({ projectRoot });
1617
2125
  const userSnippet = buildSnippet(findLastByRole(messages, "user"));
@@ -1641,9 +2149,8 @@ function createStatusDispatcher(deps) {
1641
2149
  }
1642
2150
 
1643
2151
  // src/events/start-work-command.ts
1644
- function agentFromSession(session) {
1645
- const candidate = session;
1646
- return typeof candidate.agent === "string" ? candidate.agent : void 0;
2152
+ function agentFromSession3(session) {
2153
+ return session.agent;
1647
2154
  }
1648
2155
  function resolveProjectRoot2(session) {
1649
2156
  return session.directory;
@@ -1695,24 +2202,41 @@ function createStartWorkCommandDispatcher(deps) {
1695
2202
  return;
1696
2203
  }
1697
2204
  const sessionId = entry.sessionId;
2205
+ const rawSourceServerUrl = entry.serverUrl ?? deps.sessionTitleService.getServerUrl(sessionId);
2206
+ const sourceServerUrl = normalizeOpenCodeServerUrl(rawSourceServerUrl);
2207
+ if (rawSourceServerUrl && !sourceServerUrl) {
2208
+ await sendPlain(bot, "\uC138\uC158 \uC11C\uBC84 \uC815\uBCF4\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694");
2209
+ deps.logger.error("start-work invalid server url", { sessionId });
2210
+ return;
2211
+ }
2212
+ const useRemoteServer = isDifferentServerUrl(sourceServerUrl, deps.serverUrl);
1698
2213
  let session;
1699
2214
  try {
1700
- const result = await deps.client.session.get({ path: { id: sessionId } });
1701
- if (!result.data) {
1702
- await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
1703
- return;
2215
+ if (sourceServerUrl && useRemoteServer) {
2216
+ const result = await getRemoteSession(sourceServerUrl, sessionId, deps.opencodeFetch);
2217
+ if (!result.data || result.response.status === 404) {
2218
+ await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
2219
+ return;
2220
+ }
2221
+ session = result.data;
2222
+ } else {
2223
+ const result = await deps.client.session.get({ path: { id: sessionId } });
2224
+ if (!result.data) {
2225
+ await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
2226
+ return;
2227
+ }
2228
+ session = result.data;
1704
2229
  }
1705
- session = result.data;
1706
2230
  } catch (err) {
1707
2231
  if (err instanceof Error && isSessionNotFoundError(err)) {
1708
2232
  await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
1709
2233
  return;
1710
2234
  }
1711
- await sendPlain(bot, `\uC138\uC158 \uD655\uC778 \uC2E4\uD328: ${String(err)}`);
2235
+ await sendPlain(bot, "\uC138\uC158 \uD655\uC778 \uC2E4\uD328. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694");
1712
2236
  deps.logger.error("start-work session lookup failed", { sessionId, error: String(err) });
1713
2237
  return;
1714
2238
  }
1715
- const agent = deps.sessionTitleService.getSessionAgent(sessionId) ?? agentFromSession(session);
2239
+ const agent = deps.sessionTitleService.getSessionAgent(sessionId) ?? agentFromSession3(session) ?? entry.agent;
1716
2240
  if (agent !== "plan") {
1717
2241
  await sendPlain(
1718
2242
  bot,
@@ -1720,7 +2244,14 @@ function createStartWorkCommandDispatcher(deps) {
1720
2244
  );
1721
2245
  return;
1722
2246
  }
1723
- const idle = await recheckSessionIdle(deps.client, sessionId);
2247
+ let idle;
2248
+ try {
2249
+ idle = sourceServerUrl && useRemoteServer ? ((await getRemoteStatusMap(sourceServerUrl, deps.opencodeFetch))[sessionId]?.type ?? "idle") === "idle" : await recheckSessionIdle(deps.client, sessionId);
2250
+ } catch (err) {
2251
+ await sendPlain(bot, "\uC138\uC158 \uC0C1\uD0DC \uD655\uC778 \uC2E4\uD328. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694");
2252
+ deps.logger.error("start-work idle recheck failed", { sessionId, error: String(err) });
2253
+ return;
2254
+ }
1724
2255
  if (!idle) {
1725
2256
  await sendPlain(bot, `${index}\uBC88 \uC138\uC158\uC774 idle \uC0C1\uD0DC\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uC791\uC5C5 \uC644\uB8CC\uB97C \uAE30\uB2E4\uB9AC\uC138\uC694`);
1726
2257
  return;
@@ -1731,15 +2262,14 @@ function createStartWorkCommandDispatcher(deps) {
1731
2262
  return;
1732
2263
  }
1733
2264
  try {
1734
- const serverUrl = deps.sessionTitleService.getServerUrl(sessionId);
1735
- await deps.runSessionCommand(sessionId, "start-work", serverUrl);
2265
+ await deps.runSessionCommand(sessionId, "start-work", sourceServerUrl);
1736
2266
  await sendHtml(
1737
2267
  bot,
1738
2268
  `${index}\uBC88 \uC138\uC158\uC5D0 opencode /start-work \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC804\uC1A1 \uC644\uB8CC. (${escapeHtml(entry.title)})`
1739
2269
  );
1740
2270
  deps.logger.info("start-work dispatched", { chatId, sessionId, index });
1741
2271
  } catch (err) {
1742
- await sendHtml(bot, "opencode /start-work \uC804\uC1A1 \uC2E4\uD328: " + String(err));
2272
+ await sendHtml(bot, "opencode /start-work \uC804\uC1A1 \uC2E4\uD328");
1743
2273
  deps.logger.error("start-work dispatch failed", { sessionId, error: String(err) });
1744
2274
  }
1745
2275
  };
@@ -1776,14 +2306,14 @@ function createHelpDispatcher(deps) {
1776
2306
  // src/lib/env-loader.ts
1777
2307
  import { existsSync } from "fs";
1778
2308
  import { homedir } from "os";
1779
- import { join as join6 } from "path";
2309
+ import { join as join7 } from "path";
1780
2310
  import dotenv from "dotenv";
1781
2311
  function loadPluginEnv(opts) {
1782
2312
  const paths = [
1783
- join6(opts.pluginDir, "../../.env"),
1784
- join6(opts.pluginDir, "..", ".env"),
1785
- join6(opts.pluginDir, ".env"),
1786
- join6(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
2313
+ join7(opts.pluginDir, "../../.env"),
2314
+ join7(opts.pluginDir, "..", ".env"),
2315
+ join7(opts.pluginDir, ".env"),
2316
+ join7(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
1787
2317
  ];
1788
2318
  const loadedFrom = [];
1789
2319
  const values = {};
@@ -1801,10 +2331,10 @@ function loadPluginEnv(opts) {
1801
2331
  }
1802
2332
 
1803
2333
  // src/lib/lock.ts
1804
- import { open as open2, readFile as readFile5, stat as stat3, unlink as unlink5 } from "fs/promises";
2334
+ import { open as open2, readFile as readFile6, stat as stat3, unlink as unlink6 } from "fs/promises";
1805
2335
  import { hostname } from "os";
1806
2336
  var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
1807
- function hasCode5(err, code) {
2337
+ function hasCode6(err, code) {
1808
2338
  return "code" in err && err.code === code;
1809
2339
  }
1810
2340
  function parseLockData(text) {
@@ -1823,7 +2353,7 @@ function isPidAlive(pid) {
1823
2353
  process.kill(pid, 0);
1824
2354
  return true;
1825
2355
  } catch (err) {
1826
- if (err instanceof Error && hasCode5(err, "ESRCH")) return false;
2356
+ if (err instanceof Error && hasCode6(err, "ESRCH")) return false;
1827
2357
  return true;
1828
2358
  }
1829
2359
  }
@@ -1844,7 +2374,7 @@ async function createLock(lockPath, pid) {
1844
2374
  if (released) return;
1845
2375
  released = true;
1846
2376
  try {
1847
- await unlink5(lockPath);
2377
+ await unlink6(lockPath);
1848
2378
  } catch {
1849
2379
  }
1850
2380
  }
@@ -1854,7 +2384,7 @@ async function inspectExisting(lockPath, ttlMs) {
1854
2384
  let ownerPid;
1855
2385
  let dead = false;
1856
2386
  try {
1857
- const text = await readFile5(lockPath, "utf8");
2387
+ const text = await readFile6(lockPath, "utf8");
1858
2388
  const data = parseLockData(text);
1859
2389
  if (data) {
1860
2390
  ownerPid = data.pid;
@@ -1880,7 +2410,7 @@ async function acquireLock(opts) {
1880
2410
  try {
1881
2411
  return { acquired: true, handle: await createLock(opts.lockPath, pid) };
1882
2412
  } catch (err) {
1883
- if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) {
2413
+ if (!(err instanceof Error) || !hasCode6(err, "EEXIST")) {
1884
2414
  return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
1885
2415
  }
1886
2416
  const existing = await inspectExisting(opts.lockPath, ttlMs);
@@ -1888,7 +2418,7 @@ async function acquireLock(opts) {
1888
2418
  return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
1889
2419
  }
1890
2420
  try {
1891
- await unlink5(opts.lockPath);
2421
+ await unlink6(opts.lockPath);
1892
2422
  } catch {
1893
2423
  return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
1894
2424
  }
@@ -1968,10 +2498,10 @@ function createLogger(opts = {}) {
1968
2498
  }
1969
2499
 
1970
2500
  // src/lib/session-snapshot.ts
1971
- import { chmod, mkdir as mkdir5, readFile as readFile6, rename as rename4, unlink as unlink6, writeFile as writeFile4 } from "fs/promises";
1972
- import { dirname as dirname4, join as join7 } from "path";
2501
+ import { chmod as chmod2, mkdir as mkdir6, readFile as readFile7, rename as rename5, unlink as unlink7, writeFile as writeFile5 } from "fs/promises";
2502
+ import { dirname as dirname4, join as join8 } from "path";
1973
2503
  var TTL_MS = 60 * 60 * 1e3;
1974
- function hasCode6(err, code) {
2504
+ function hasCode7(err, code) {
1975
2505
  return err instanceof Error && "code" in err && err.code === code;
1976
2506
  }
1977
2507
  function isSnapshotFile(value) {
@@ -1990,11 +2520,14 @@ function isSnapshotFile(value) {
1990
2520
  if (typeof e.capturedAt !== "number") return false;
1991
2521
  if (e.agent !== void 0 && typeof e.agent !== "string") return false;
1992
2522
  if (e.status !== void 0 && typeof e.status !== "string") return false;
1993
- if (e.serverUrl !== void 0 && typeof e.serverUrl !== "string") return false;
2523
+ if (e.serverUrl !== void 0) {
2524
+ if (typeof e.serverUrl !== "string") return false;
2525
+ if (!normalizeOpenCodeServerUrl(e.serverUrl)) return false;
2526
+ }
1994
2527
  }
1995
2528
  return true;
1996
2529
  }
1997
- function normalizeEntry(entry) {
2530
+ function normalizeEntry2(entry) {
1998
2531
  const out = {
1999
2532
  index: entry.index,
2000
2533
  sessionId: entry.sessionId,
@@ -2003,22 +2536,25 @@ function normalizeEntry(entry) {
2003
2536
  };
2004
2537
  if (entry.agent !== void 0) out.agent = entry.agent;
2005
2538
  if (entry.status !== void 0) out.status = entry.status;
2006
- if (entry.serverUrl !== void 0) out.serverUrl = entry.serverUrl;
2539
+ if (entry.serverUrl !== void 0) {
2540
+ const serverUrl = normalizeOpenCodeServerUrl(entry.serverUrl);
2541
+ if (serverUrl !== void 0) out.serverUrl = serverUrl;
2542
+ }
2007
2543
  return out;
2008
2544
  }
2009
2545
  function createSnapshotStore(opts) {
2010
2546
  const { configDir, tokenHash, logger } = opts;
2011
- const snapshotsDir = join7(configDir, "snapshots");
2547
+ const snapshotsDir = join8(configDir, "snapshots");
2012
2548
  const writeLocks = /* @__PURE__ */ new Map();
2013
2549
  function snapshotFilePath(chatId) {
2014
- return join7(snapshotsDir, `${tokenHash}-${chatId}.json`);
2550
+ return join8(snapshotsDir, `${tokenHash}-${chatId}.json`);
2015
2551
  }
2016
2552
  async function performSave(chatId, entries) {
2017
2553
  const filePath = snapshotFilePath(chatId);
2018
2554
  const parent = dirname4(filePath);
2019
- await mkdir5(parent, { recursive: true });
2555
+ await mkdir6(parent, { recursive: true });
2020
2556
  try {
2021
- await chmod(parent, 448);
2557
+ await chmod2(parent, 448);
2022
2558
  } catch (err) {
2023
2559
  logger.error("snapshot: failed to chmod parent dir", {
2024
2560
  path: parent,
@@ -2029,20 +2565,20 @@ function createSnapshotStore(opts) {
2029
2565
  version: 1,
2030
2566
  chatId,
2031
2567
  createdAt: Date.now(),
2032
- entries: entries.map(normalizeEntry)
2568
+ entries: entries.map(normalizeEntry2)
2033
2569
  };
2034
2570
  const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
2035
- await writeFile4(tmpPath, JSON.stringify(payload, null, 2), "utf8");
2571
+ await writeFile5(tmpPath, JSON.stringify(payload, null, 2), "utf8");
2036
2572
  try {
2037
- await rename4(tmpPath, filePath);
2573
+ await rename5(tmpPath, filePath);
2038
2574
  } catch (err) {
2039
2575
  try {
2040
- await unlink6(tmpPath);
2576
+ await unlink7(tmpPath);
2041
2577
  } catch {
2042
2578
  }
2043
2579
  throw err;
2044
2580
  }
2045
- await chmod(filePath, 384);
2581
+ await chmod2(filePath, 384);
2046
2582
  }
2047
2583
  async function saveSnapshot(chatId, entries) {
2048
2584
  const prev = writeLocks.get(chatId) ?? Promise.resolve();
@@ -2061,9 +2597,9 @@ function createSnapshotStore(opts) {
2061
2597
  const filePath = snapshotFilePath(chatId);
2062
2598
  let text;
2063
2599
  try {
2064
- text = await readFile6(filePath, "utf8");
2600
+ text = await readFile7(filePath, "utf8");
2065
2601
  } catch (err) {
2066
- if (hasCode6(err, "ENOENT")) return null;
2602
+ if (hasCode7(err, "ENOENT")) return null;
2067
2603
  logger.error("snapshot: failed to read file", {
2068
2604
  path: filePath,
2069
2605
  error: err instanceof Error ? err.message : String(err)
@@ -2086,9 +2622,9 @@ function createSnapshotStore(opts) {
2086
2622
  }
2087
2623
  if (parsed.createdAt + TTL_MS < Date.now()) {
2088
2624
  try {
2089
- await unlink6(filePath);
2625
+ await unlink7(filePath);
2090
2626
  } catch (err) {
2091
- if (!hasCode6(err, "ENOENT")) {
2627
+ if (!hasCode7(err, "ENOENT")) {
2092
2628
  logger.error("snapshot: failed to unlink expired file", {
2093
2629
  path: filePath,
2094
2630
  error: err instanceof Error ? err.message : String(err)
@@ -2097,16 +2633,16 @@ function createSnapshotStore(opts) {
2097
2633
  }
2098
2634
  return null;
2099
2635
  }
2100
- return parsed.entries.map(normalizeEntry);
2636
+ return parsed.entries.map(normalizeEntry2);
2101
2637
  }
2102
2638
  return { saveSnapshot, loadSnapshot, snapshotFilePath };
2103
2639
  }
2104
2640
 
2105
2641
  // src/lib/state-store.ts
2106
- import { mkdir as mkdir6, readFile as readFile7, rename as rename5, writeFile as writeFile5 } from "fs/promises";
2642
+ import { mkdir as mkdir7, readFile as readFile8, rename as rename6, writeFile as writeFile6 } from "fs/promises";
2107
2643
  import { homedir as homedir2 } from "os";
2108
- import { dirname as dirname5, join as join8 } from "path";
2109
- function hasCode7(err, code) {
2644
+ import { dirname as dirname5, join as join9 } from "path";
2645
+ function hasCode8(err, code) {
2110
2646
  return "code" in err && err.code === code;
2111
2647
  }
2112
2648
  function parseState(text) {
@@ -2118,28 +2654,28 @@ function parseState(text) {
2118
2654
  return state;
2119
2655
  }
2120
2656
  function createStateStore(opts = {}) {
2121
- const filePath = opts.filePath ?? join8(homedir2(), ".config/opencode/telegram-remote/state.json");
2657
+ const filePath = opts.filePath ?? join9(homedir2(), ".config/opencode/telegram-remote/state.json");
2122
2658
  return {
2123
2659
  async read() {
2124
2660
  try {
2125
- return parseState(await readFile7(filePath, "utf8"));
2661
+ return parseState(await readFile8(filePath, "utf8"));
2126
2662
  } catch (err) {
2127
- if (err instanceof Error && hasCode7(err, "ENOENT")) return {};
2663
+ if (err instanceof Error && hasCode8(err, "ENOENT")) return {};
2128
2664
  throw err;
2129
2665
  }
2130
2666
  },
2131
2667
  async write(patch) {
2132
2668
  const existing = await this.read();
2133
2669
  const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2134
- await mkdir6(dirname5(filePath), { recursive: true });
2670
+ await mkdir7(dirname5(filePath), { recursive: true });
2135
2671
  const tmpPath = `${filePath}.tmp.${process.pid}`;
2136
- await writeFile5(tmpPath, JSON.stringify(next, null, 2), "utf8");
2672
+ await writeFile6(tmpPath, JSON.stringify(next, null, 2), "utf8");
2137
2673
  try {
2138
- await rename5(tmpPath, filePath);
2674
+ await rename6(tmpPath, filePath);
2139
2675
  } catch (err) {
2140
- if (!(err instanceof Error) || !hasCode7(err, "ENOENT")) throw err;
2141
- await writeFile5(tmpPath, JSON.stringify(next, null, 2), "utf8");
2142
- await rename5(tmpPath, filePath);
2676
+ if (!(err instanceof Error) || !hasCode8(err, "ENOENT")) throw err;
2677
+ await writeFile6(tmpPath, JSON.stringify(next, null, 2), "utf8");
2678
+ await rename6(tmpPath, filePath);
2143
2679
  }
2144
2680
  return next;
2145
2681
  }
@@ -2147,7 +2683,7 @@ function createStateStore(opts = {}) {
2147
2683
  }
2148
2684
 
2149
2685
  // src/services/session-title-service.ts
2150
- function agentFromSession2(info) {
2686
+ function agentFromSession4(info) {
2151
2687
  const candidate = info;
2152
2688
  return typeof candidate.agent === "string" ? candidate.agent : void 0;
2153
2689
  }
@@ -2158,7 +2694,7 @@ var SessionTitleService = class {
2158
2694
  this.sessions.set(info.id, {
2159
2695
  title: info.title || null,
2160
2696
  parentID: info.parentID ?? null,
2161
- agent: agentFromSession2(info) ?? existing?.agent,
2697
+ agent: agentFromSession4(info) ?? existing?.agent,
2162
2698
  status: existing?.status,
2163
2699
  idleNotificationPending: existing?.idleNotificationPending ?? false,
2164
2700
  lastSeenAt: Date.now(),
@@ -2286,17 +2822,17 @@ var SessionTitleService = class {
2286
2822
  // src/telegram-remote.ts
2287
2823
  var pluginDir = dirname6(fileURLToPath(import.meta.url));
2288
2824
  async function postToServer(serverUrl, path, body) {
2289
- const url = new URL(path, serverUrl);
2825
+ const safeServerUrl = normalizeOpenCodeServerUrl(serverUrl);
2826
+ if (!safeServerUrl) throw new Error("Invalid OpenCode server URL");
2827
+ const url = new URL(path, safeServerUrl);
2290
2828
  const response = await fetch(url, {
2291
2829
  method: "POST",
2292
2830
  headers: { "Content-Type": "application/json" },
2293
- body: JSON.stringify(body)
2831
+ body: JSON.stringify(body),
2832
+ redirect: "error"
2294
2833
  });
2295
2834
  if (response.ok) return;
2296
- const text = await response.text();
2297
- throw new Error(
2298
- `OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
2299
- );
2835
+ throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}`);
2300
2836
  }
2301
2837
  function getSessionAgentFromMessage(event) {
2302
2838
  const info = event.properties.info;
@@ -2320,10 +2856,11 @@ var TelegramRemote = async (input) => {
2320
2856
  const stateStore = createStateStore();
2321
2857
  const initialState = await stateStore.read();
2322
2858
  const tokenHash = createHash5("sha256").update(config.botToken).digest("hex").slice(0, 16);
2323
- const configDir = join9(homedir3(), ".config/opencode/telegram-remote");
2859
+ const configDir = join10(homedir3(), ".config/opencode/telegram-remote");
2324
2860
  const snapshotStore = createSnapshotStore({ configDir, tokenHash, logger });
2325
- const lockPath = join9(tmpdir5(), `opencoder-telegram-${tokenHash}.lock`);
2326
- const claimsDir = join9(tmpdir5(), `opencoder-telegram-claims-${tokenHash}`);
2861
+ const sessionRegistry = createSessionRegistryStore({ configDir, tokenHash, logger });
2862
+ const lockPath = join10(tmpdir5(), `opencoder-telegram-${tokenHash}.lock`);
2863
+ const claimsDir = join10(tmpdir5(), `opencoder-telegram-claims-${tokenHash}`);
2327
2864
  const pendingQuestions = createPendingQuestionStore({ tokenHash });
2328
2865
  const pendingPermissions = createPendingPermissionStore({ tokenHash });
2329
2866
  const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
@@ -2353,8 +2890,8 @@ var TelegramRemote = async (input) => {
2353
2890
  throwOnError: true
2354
2891
  });
2355
2892
  };
2356
- const replyToPermission = async (requestID, sessionID, reply, endpoint, serverUrl = input.serverUrl.href) => {
2357
- if (endpoint === "request") {
2893
+ const replyToPermission = async (requestID, sessionID, reply, endpoint2, serverUrl = input.serverUrl.href) => {
2894
+ if (endpoint2 === "request") {
2358
2895
  const path2 = `/permission/${encodeURIComponent(requestID)}/reply`;
2359
2896
  if (serverUrl !== input.serverUrl.href) {
2360
2897
  await postToServer(serverUrl, path2, { reply });
@@ -2438,6 +2975,7 @@ var TelegramRemote = async (input) => {
2438
2975
  pendingQuestions,
2439
2976
  pendingPermissions,
2440
2977
  pendingStartWorks,
2978
+ sessionRegistry,
2441
2979
  replyToQuestion,
2442
2980
  replyToPermission,
2443
2981
  runSessionCommand
@@ -2446,26 +2984,30 @@ var TelegramRemote = async (input) => {
2446
2984
  bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
2447
2985
  bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
2448
2986
  bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
2449
- bot.setSessionsDispatcher(createSessionsDispatcher({ sessionTitleService, snapshotStore, logger }));
2450
- bot.setStatusDispatcher(createStatusDispatcher({ snapshotStore, sessionTitleService, client: input.client, logger }));
2987
+ bot.setSessionsDispatcher(createSessionsDispatcher({
2988
+ client: input.client,
2989
+ sessionTitleService,
2990
+ sessionRegistry,
2991
+ snapshotStore,
2992
+ serverUrl: input.serverUrl.href,
2993
+ logger
2994
+ }));
2995
+ bot.setStatusDispatcher(createStatusDispatcher({
2996
+ snapshotStore,
2997
+ sessionTitleService,
2998
+ client: input.client,
2999
+ logger,
3000
+ serverUrl: input.serverUrl.href
3001
+ }));
2451
3002
  bot.setStartWorkCommandDispatcher(createStartWorkCommandDispatcher({
2452
3003
  snapshotStore,
2453
3004
  sessionTitleService,
2454
3005
  client: input.client,
3006
+ serverUrl: input.serverUrl.href,
2455
3007
  runSessionCommand,
2456
3008
  logger
2457
3009
  }));
2458
3010
  bot.setHelpDispatcher(createHelpDispatcher({ logger }));
2459
- try {
2460
- const sessions = await input.client.session.list();
2461
- for (const s of sessions.data ?? []) {
2462
- sessionTitleService.setSessionInfo(s);
2463
- sessionTitleService.setServerUrl(s.id, input.serverUrl.href);
2464
- }
2465
- logger.info("cold-start cache primed", { count: (sessions.data ?? []).length });
2466
- } catch (err) {
2467
- logger.error("cold-start priming failed", { error: String(err) });
2468
- }
2469
3011
  }
2470
3012
  return {
2471
3013
  event: async ({ event }) => {
@@ -2485,8 +3027,16 @@ var TelegramRemote = async (input) => {
2485
3027
  case "message.updated": {
2486
3028
  const messageAgent = getSessionAgentFromMessage(event);
2487
3029
  if (messageAgent) {
3030
+ const previousAgent = ctx.sessionTitleService.getSessionAgent(messageAgent.sessionID);
2488
3031
  ctx.sessionTitleService.setSessionAgent(messageAgent.sessionID, messageAgent.agent);
2489
3032
  ctx.sessionTitleService.setServerUrl(messageAgent.sessionID, input.serverUrl.href);
3033
+ if (previousAgent !== messageAgent.agent) {
3034
+ await ctx.sessionRegistry.updateSession(messageAgent.sessionID, {
3035
+ agent: messageAgent.agent,
3036
+ serverUrl: input.serverUrl.href,
3037
+ updatedAt: Date.now()
3038
+ });
3039
+ }
2490
3040
  }
2491
3041
  return;
2492
3042
  }
@@ -2495,7 +3045,16 @@ var TelegramRemote = async (input) => {
2495
3045
  default: {
2496
3046
  const stepAgent = getSessionAgentFromNextStep(extEvent);
2497
3047
  if (stepAgent) {
3048
+ const previousAgent = ctx.sessionTitleService.getSessionAgent(stepAgent.sessionID);
2498
3049
  ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
3050
+ ctx.sessionTitleService.setServerUrl(stepAgent.sessionID, input.serverUrl.href);
3051
+ if (previousAgent !== stepAgent.agent) {
3052
+ await ctx.sessionRegistry.updateSession(stepAgent.sessionID, {
3053
+ agent: stepAgent.agent,
3054
+ serverUrl: input.serverUrl.href,
3055
+ updatedAt: Date.now()
3056
+ });
3057
+ }
2499
3058
  return;
2500
3059
  }
2501
3060
  if (isEventPermissionAsked(extEvent)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinseeker/opencode-telegram-plugin",
3
- "version": "1.0.13",
3
+ "version": "1.1.0",
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",