@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.
- package/.env.example +5 -0
- package/README.md +5 -0
- package/dist/attach/service.js +0 -2
- package/dist/background-session/tracker.js +137 -0
- package/dist/bot/commands/commands.js +1 -1
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/detach.js +54 -0
- package/dist/bot/commands/ls.js +445 -0
- package/dist/bot/commands/open.js +9 -4
- package/dist/bot/commands/projects.js +6 -2
- package/dist/bot/commands/sessions.js +233 -82
- package/dist/bot/commands/worktree.js +7 -2
- package/dist/bot/handlers/inline-menu.js +1 -0
- package/dist/bot/handlers/prompt.js +1 -1
- package/dist/bot/index.js +74 -8
- package/dist/bot/utils/busy-reconciliation.js +113 -0
- package/dist/bot/utils/send-downloaded-file.js +57 -0
- package/dist/bot/utils/switch-project.js +9 -14
- package/dist/config.js +1 -0
- package/dist/i18n/de.js +26 -0
- package/dist/i18n/en.js +26 -0
- package/dist/i18n/es.js +26 -0
- package/dist/i18n/fr.js +26 -0
- package/dist/i18n/ru.js +26 -0
- package/dist/i18n/zh.js +26 -0
- package/dist/interaction/busy.js +1 -1
- package/dist/interaction/manager.js +1 -1
- package/dist/scheduled-task/executor.js +3 -0
- package/dist/scheduled-task/foreground-state.js +22 -12
- package/dist/scheduled-task/runtime.js +2 -0
- package/dist/scheduled-task/schedule-parser.js +6 -0
- package/dist/scheduled-task/session-ignore.js +53 -0
- package/dist/settings/manager.js +12 -0
- package/dist/summary/aggregator.js +4 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.
|
|
9
|
-
logger.debug(`[ScheduledTaskForeground] Marked session busy: session=${sessionId}, count=${this.
|
|
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.
|
|
16
|
-
logger.debug(`[ScheduledTaskForeground] Marked session idle: session=${sessionId}, count=${this.
|
|
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.
|
|
22
|
+
return this.activeSessions.size > 0;
|
|
20
23
|
}
|
|
21
24
|
clearAll(reason) {
|
|
22
|
-
if (this.
|
|
25
|
+
if (this.activeSessions.size === 0) {
|
|
23
26
|
return;
|
|
24
27
|
}
|
|
25
|
-
logger.info(`[ScheduledTaskForeground] Cleared foreground busy state: reason=${reason}, count=${this.
|
|
26
|
-
this.
|
|
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.
|
|
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
|
+
}
|
package/dist/settings/manager.js
CHANGED
|
@@ -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));
|