@grinev/opencode-telegram-bot 0.19.3 → 0.20.1

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.
@@ -2,6 +2,7 @@ import { config } from "../config.js";
2
2
  import { t } from "../i18n/index.js";
3
3
  import { opencodeClient } from "../opencode/client.js";
4
4
  import { logger } from "../utils/logger.js";
5
+ import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./session-ignore.js";
5
6
  export const SCHEDULED_TASK_AGENT = "build";
6
7
  const SCHEDULED_TASK_SESSION_TITLE = "Scheduled task run";
7
8
  const EXECUTION_POLL_INTERVAL_MS = 2000;
@@ -298,6 +299,7 @@ export async function executeScheduledTask(task) {
298
299
  let sessionId = null;
299
300
  let deleteTemporarySession = true;
300
301
  try {
302
+ await cleanupScheduledTaskSessionIgnores();
301
303
  const { data: session, error: createError } = await opencodeClient.session.create({
302
304
  directory: task.projectWorktree,
303
305
  title: SCHEDULED_TASK_SESSION_TITLE,
@@ -306,6 +308,7 @@ export async function executeScheduledTask(task) {
306
308
  throw createError || new Error("Failed to create temporary scheduled task session");
307
309
  }
308
310
  sessionId = session.id;
311
+ await registerScheduledTaskSessionIgnore(session.id);
309
312
  const promptOptions = {
310
313
  sessionID: session.id,
311
314
  directory: session.directory,
@@ -1,32 +1,42 @@
1
1
  import { logger } from "../utils/logger.js";
2
2
  class ForegroundSessionState {
3
- activeSessionIds = new Set();
4
- markBusy(sessionId) {
5
- if (!sessionId) {
3
+ activeSessions = new Map();
4
+ markBusy(sessionId, directory) {
5
+ if (!sessionId || !directory) {
6
6
  return;
7
7
  }
8
- this.activeSessionIds.add(sessionId);
9
- logger.debug(`[ScheduledTaskForeground] Marked session busy: session=${sessionId}, count=${this.activeSessionIds.size}`);
8
+ this.activeSessions.set(sessionId, { sessionId, directory, markedAt: Date.now() });
9
+ logger.debug(`[ScheduledTaskForeground] Marked session busy: session=${sessionId}, directory=${directory}, count=${this.activeSessions.size}`);
10
10
  }
11
11
  markIdle(sessionId) {
12
12
  if (!sessionId) {
13
13
  return;
14
14
  }
15
- this.activeSessionIds.delete(sessionId);
16
- logger.debug(`[ScheduledTaskForeground] Marked session idle: session=${sessionId}, count=${this.activeSessionIds.size}`);
15
+ this.activeSessions.delete(sessionId);
16
+ logger.debug(`[ScheduledTaskForeground] Marked session idle: session=${sessionId}, count=${this.activeSessions.size}`);
17
+ }
18
+ getBusySessions() {
19
+ return Array.from(this.activeSessions.values(), (session) => ({ ...session }));
17
20
  }
18
21
  isBusy() {
19
- return this.activeSessionIds.size > 0;
22
+ return this.activeSessions.size > 0;
20
23
  }
21
24
  clearAll(reason) {
22
- if (this.activeSessionIds.size === 0) {
25
+ if (this.activeSessions.size === 0) {
23
26
  return;
24
27
  }
25
- logger.info(`[ScheduledTaskForeground] Cleared foreground busy state: reason=${reason}, count=${this.activeSessionIds.size}`);
26
- this.activeSessionIds.clear();
28
+ logger.info(`[ScheduledTaskForeground] Cleared foreground busy state: reason=${reason}, count=${this.activeSessions.size}`);
29
+ this.activeSessions.clear();
27
30
  }
28
31
  __resetForTests() {
29
- this.activeSessionIds.clear();
32
+ this.activeSessions.clear();
33
+ }
34
+ __setMarkedAtForTests(sessionId, markedAt) {
35
+ const session = this.activeSessions.get(sessionId);
36
+ if (!session) {
37
+ return;
38
+ }
39
+ this.activeSessions.set(sessionId, { ...session, markedAt });
30
40
  }
31
41
  }
32
42
  export const foregroundSessionState = new ForegroundSessionState();
@@ -7,6 +7,7 @@ import { sendBotText } from "../bot/utils/telegram-text.js";
7
7
  import { formatAssistantRunFooter } from "../bot/utils/assistant-run-footer.js";
8
8
  import { executeScheduledTask, SCHEDULED_TASK_AGENT } from "./executor.js";
9
9
  import { foregroundSessionState } from "./foreground-state.js";
10
+ import { cleanupScheduledTaskSessionIgnores } from "./session-ignore.js";
10
11
  import { computeNextRunAt, isTaskDue } from "./next-run.js";
11
12
  import { getScheduledTask, listScheduledTasks, removeScheduledTask, replaceScheduledTasks, updateScheduledTask, } from "./store.js";
12
13
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
@@ -96,6 +97,7 @@ export class ScheduledTaskRuntime {
96
97
  return;
97
98
  }
98
99
  this.initialized = true;
100
+ await cleanupScheduledTaskSessionIgnores();
99
101
  await this.recoverTasksOnStartup();
100
102
  await this.flushDeferredDeliveries();
101
103
  }
@@ -1,5 +1,6 @@
1
1
  import { opencodeClient } from "../opencode/client.js";
2
2
  import { logger } from "../utils/logger.js";
3
+ import { cleanupScheduledTaskSessionIgnores, registerScheduledTaskSessionIgnore, } from "./session-ignore.js";
3
4
  const SCHEDULE_PARSE_SESSION_TITLE = "Scheduled task schedule parser";
4
5
  function getLocalTimezone() {
5
6
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -133,6 +134,8 @@ export async function parseTaskSchedule(scheduleText, directory) {
133
134
  const timezone = getLocalTimezone();
134
135
  let sessionId = null;
135
136
  try {
137
+ logger.debug(`[ScheduledTaskScheduleParser] Parsing schedule: directory=${trimmedDirectory}, textLength=${trimmedScheduleText.length}`);
138
+ await cleanupScheduledTaskSessionIgnores();
136
139
  const { data: session, error: createError } = await opencodeClient.session.create({
137
140
  directory: trimmedDirectory,
138
141
  title: SCHEDULE_PARSE_SESSION_TITLE,
@@ -141,6 +144,8 @@ export async function parseTaskSchedule(scheduleText, directory) {
141
144
  throw createError || new Error("Failed to create temporary schedule parser session");
142
145
  }
143
146
  sessionId = session.id;
147
+ await registerScheduledTaskSessionIgnore(session.id);
148
+ logger.debug(`[ScheduledTaskScheduleParser] Created temporary session: sessionId=${session.id}`);
144
149
  const { data: response, error: promptError } = await opencodeClient.session.prompt({
145
150
  sessionID: session.id,
146
151
  directory: session.directory,
@@ -151,6 +156,7 @@ export async function parseTaskSchedule(scheduleText, directory) {
151
156
  throw promptError || new Error("Failed to parse schedule");
152
157
  }
153
158
  const responseText = collectResponseText(response.parts);
159
+ logger.debug(`[ScheduledTaskScheduleParser] Received parser response: sessionId=${session.id}, textLength=${responseText.length}`);
154
160
  if (!responseText) {
155
161
  throw new Error("Schedule parser returned an empty response");
156
162
  }
@@ -0,0 +1,53 @@
1
+ import { getScheduledTaskSessionIgnores, setScheduledTaskSessionIgnores, } from "../settings/manager.js";
2
+ const SCHEDULED_TASK_SESSION_IGNORE_TTL_MS = 24 * 60 * 60 * 1000;
3
+ let mutationQueue = Promise.resolve();
4
+ function isFreshIgnore(ignore, nowMs) {
5
+ const createdAtMs = Date.parse(ignore.createdAt);
6
+ return !Number.isNaN(createdAtMs) && nowMs - createdAtMs < SCHEDULED_TASK_SESSION_IGNORE_TTL_MS;
7
+ }
8
+ function pruneExpiredIgnores(ignores, nowMs) {
9
+ return ignores.filter((ignore) => isFreshIgnore(ignore, nowMs));
10
+ }
11
+ async function mutateIgnores(mutator) {
12
+ const runMutation = async () => {
13
+ const currentIgnores = getScheduledTaskSessionIgnores();
14
+ const { ignores, result } = mutator(currentIgnores);
15
+ await setScheduledTaskSessionIgnores(ignores);
16
+ return result;
17
+ };
18
+ const mutationPromise = mutationQueue.then(runMutation, runMutation);
19
+ mutationQueue = mutationPromise.catch(() => undefined);
20
+ return mutationPromise;
21
+ }
22
+ export function isScheduledTaskSessionIgnored(sessionId, now = new Date()) {
23
+ const nowMs = now.getTime();
24
+ return getScheduledTaskSessionIgnores().some((ignore) => ignore.sessionId === sessionId && isFreshIgnore(ignore, nowMs));
25
+ }
26
+ export async function registerScheduledTaskSessionIgnore(sessionId, createdAt = new Date()) {
27
+ await mutateIgnores((ignores) => {
28
+ const nowMs = createdAt.getTime();
29
+ const nextIgnores = pruneExpiredIgnores(ignores, nowMs).filter((ignore) => ignore.sessionId !== sessionId);
30
+ return {
31
+ ignores: [...nextIgnores, { sessionId, createdAt: createdAt.toISOString() }],
32
+ result: undefined,
33
+ };
34
+ });
35
+ }
36
+ export async function removeScheduledTaskSessionIgnore(sessionId) {
37
+ await mutateIgnores((ignores) => ({
38
+ ignores: ignores.filter((ignore) => ignore.sessionId !== sessionId),
39
+ result: undefined,
40
+ }));
41
+ }
42
+ export async function cleanupScheduledTaskSessionIgnores(now = new Date()) {
43
+ return mutateIgnores((ignores) => {
44
+ const nextIgnores = pruneExpiredIgnores(ignores, now.getTime());
45
+ return {
46
+ ignores: nextIgnores,
47
+ result: ignores.length - nextIgnores.length,
48
+ };
49
+ });
50
+ }
51
+ export function __resetScheduledTaskSessionIgnoreForTests() {
52
+ mutationQueue = Promise.resolve();
53
+ }
@@ -5,6 +5,9 @@ import { logger } from "../utils/logger.js";
5
5
  function cloneScheduledTasks(tasks) {
6
6
  return tasks?.map((task) => cloneScheduledTask(task));
7
7
  }
8
+ function cloneScheduledTaskSessionIgnores(ignores) {
9
+ return ignores?.map((ignore) => ({ ...ignore }));
10
+ }
8
11
  function getSettingsFilePath() {
9
12
  return getRuntimePaths().settingsFilePath;
10
13
  }
@@ -121,6 +124,13 @@ export function setScheduledTasks(tasks) {
121
124
  currentSettings.scheduledTasks = cloneScheduledTasks(tasks);
122
125
  return writeSettingsFile(currentSettings);
123
126
  }
127
+ export function getScheduledTaskSessionIgnores() {
128
+ return cloneScheduledTaskSessionIgnores(currentSettings.scheduledTaskSessionIgnores) ?? [];
129
+ }
130
+ export function setScheduledTaskSessionIgnores(ignores) {
131
+ currentSettings.scheduledTaskSessionIgnores = cloneScheduledTaskSessionIgnores(ignores);
132
+ return writeSettingsFile(currentSettings);
133
+ }
124
134
  export function __resetSettingsForTests() {
125
135
  currentSettings = {};
126
136
  settingsWriteQueue = Promise.resolve();
@@ -138,6 +148,8 @@ export async function loadSettings() {
138
148
  }
139
149
  currentSettings = loadedSettings;
140
150
  currentSettings.scheduledTasks = cloneScheduledTasks(loadedSettings.scheduledTasks) ?? [];
151
+ currentSettings.scheduledTaskSessionIgnores =
152
+ cloneScheduledTaskSessionIgnores(loadedSettings.scheduledTaskSessionIgnores) ?? [];
141
153
  if (requiresRewrite) {
142
154
  void writeSettingsFile(currentSettings);
143
155
  }
@@ -173,6 +173,10 @@ class SummaryAggregator {
173
173
  this.handleMessagePartDelta(event);
174
174
  return;
175
175
  }
176
+ if (eventType === "server.heartbeat") {
177
+ logger.debug("[Aggregator] Heartbeat received");
178
+ return;
179
+ }
176
180
  // Log all question-related events for debugging
177
181
  if (event.type.startsWith("question.")) {
178
182
  logger.info(`[Aggregator] Question event: ${event.type}`, JSON.stringify(event.properties, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.19.3",
3
+ "version": "0.20.1",
4
4
  "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",