@grinev/opencode-telegram-bot 0.20.0 → 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.
|
@@ -292,7 +292,7 @@ async function executeCommand(ctx, deps, params) {
|
|
|
292
292
|
const model = storedModel.providerID && storedModel.modelID
|
|
293
293
|
? `${storedModel.providerID}/${storedModel.modelID}`
|
|
294
294
|
: undefined;
|
|
295
|
-
foregroundSessionState.markBusy(session.id);
|
|
295
|
+
foregroundSessionState.markBusy(session.id, session.directory);
|
|
296
296
|
await markAttachedSessionBusy(session.id);
|
|
297
297
|
assistantRunState.startRun(session.id, {
|
|
298
298
|
startedAt: Date.now(),
|
|
@@ -198,7 +198,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
198
198
|
fileCount: fileParts.length,
|
|
199
199
|
};
|
|
200
200
|
logger.info(`[Bot] Calling session.promptAsync (start-only) with agent=${currentAgent}, fileCount=${fileParts.length}...`);
|
|
201
|
-
foregroundSessionState.markBusy(currentSession.id);
|
|
201
|
+
foregroundSessionState.markBusy(currentSession.id, currentSession.directory);
|
|
202
202
|
await markAttachedSessionBusy(currentSession.id);
|
|
203
203
|
assistantRunState.startRun(currentSession.id, {
|
|
204
204
|
startedAt: Date.now(),
|
package/dist/bot/index.js
CHANGED
|
@@ -59,6 +59,7 @@ import { clearPromptResponseMode, processUserPrompt } from "./handlers/prompt.js
|
|
|
59
59
|
import { handleVoiceMessage } from "./handlers/voice.js";
|
|
60
60
|
import { handleDocumentMessage } from "./handlers/document.js";
|
|
61
61
|
import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
62
|
+
import { reconcileBusyState } from "./utils/busy-reconciliation.js";
|
|
62
63
|
import { finalizeAssistantResponse } from "./utils/finalize-assistant-response.js";
|
|
63
64
|
import { sendTtsResponseForSession } from "./utils/send-tts-response.js";
|
|
64
65
|
import { deliverThinkingMessage } from "./utils/thinking-message.js";
|
|
@@ -800,6 +801,9 @@ async function ensureEventSubscription(directory) {
|
|
|
800
801
|
});
|
|
801
802
|
logger.info(`[Bot] Subscribing to OpenCode events for project: ${directory}`);
|
|
802
803
|
subscribeToEvents(directory, (event) => {
|
|
804
|
+
if (event.type === "server.heartbeat") {
|
|
805
|
+
void reconcileBusyState(directory);
|
|
806
|
+
}
|
|
803
807
|
const attached = attachManager.getSnapshot();
|
|
804
808
|
const eventSessionId = getEventSessionId(event);
|
|
805
809
|
if (attached &&
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
2
|
+
import { foregroundSessionState, } from "../../scheduled-task/foreground-state.js";
|
|
3
|
+
import { scheduledTaskRuntime } from "../../scheduled-task/runtime.js";
|
|
4
|
+
import { attachManager } from "../../attach/manager.js";
|
|
5
|
+
import { markAttachedSessionBusy, markAttachedSessionIdle } from "../../attach/service.js";
|
|
6
|
+
import { assistantRunState } from "../assistant-run-state.js";
|
|
7
|
+
import { clearPromptResponseMode } from "../handlers/prompt.js";
|
|
8
|
+
import { logger } from "../../utils/logger.js";
|
|
9
|
+
const RECONCILE_MIN_INTERVAL_MS = 10_000;
|
|
10
|
+
const FOREGROUND_BUSY_RECONCILE_GRACE_MS = 2_000;
|
|
11
|
+
const inFlightDirectories = new Set();
|
|
12
|
+
const lastReconcileAtByDirectory = new Map();
|
|
13
|
+
function getReconciliationTargets(directory) {
|
|
14
|
+
const foregroundBusySessions = foregroundSessionState
|
|
15
|
+
.getBusySessions()
|
|
16
|
+
.filter((session) => session.directory === directory);
|
|
17
|
+
const attachedSession = attachManager.getSnapshot();
|
|
18
|
+
const attachedSessionForDirectory = attachedSession?.directory === directory ? attachedSession : null;
|
|
19
|
+
return { foregroundBusySessions, attachedSessionForDirectory };
|
|
20
|
+
}
|
|
21
|
+
function getSessionStatus(statuses, sessionId) {
|
|
22
|
+
if (!statuses || typeof statuses !== "object") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const status = statuses[sessionId];
|
|
26
|
+
return status ?? null;
|
|
27
|
+
}
|
|
28
|
+
function isTerminalStatus(status) {
|
|
29
|
+
return !status || status.type === "idle" || status.type === "error";
|
|
30
|
+
}
|
|
31
|
+
function isWithinForegroundBusyGracePeriod(session, now) {
|
|
32
|
+
return now - session.markedAt < FOREGROUND_BUSY_RECONCILE_GRACE_MS;
|
|
33
|
+
}
|
|
34
|
+
async function clearForegroundBusySession(sessionId, reason) {
|
|
35
|
+
foregroundSessionState.markIdle(sessionId);
|
|
36
|
+
assistantRunState.clearRun(sessionId, reason);
|
|
37
|
+
clearPromptResponseMode(sessionId);
|
|
38
|
+
}
|
|
39
|
+
export async function reconcileBusyStateNow(directory, now = Date.now()) {
|
|
40
|
+
if (!directory) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const { foregroundBusySessions, attachedSessionForDirectory } = getReconciliationTargets(directory);
|
|
44
|
+
if (foregroundBusySessions.length === 0 && !attachedSessionForDirectory) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const { data: statuses, error } = await opencodeClient.session.status({ directory });
|
|
48
|
+
if (error || !statuses) {
|
|
49
|
+
logger.warn("[BusyReconciliation] Failed to load session status", error);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const freshForegroundSessionIds = new Set(foregroundBusySessions
|
|
53
|
+
.filter((session) => isWithinForegroundBusyGracePeriod(session, now))
|
|
54
|
+
.map((session) => session.sessionId));
|
|
55
|
+
if (attachedSessionForDirectory) {
|
|
56
|
+
const attachedStatus = getSessionStatus(statuses, attachedSessionForDirectory.sessionId);
|
|
57
|
+
if (attachedStatus?.type === "busy") {
|
|
58
|
+
await markAttachedSessionBusy(attachedSessionForDirectory.sessionId);
|
|
59
|
+
}
|
|
60
|
+
else if (isTerminalStatus(attachedStatus) &&
|
|
61
|
+
!freshForegroundSessionIds.has(attachedSessionForDirectory.sessionId)) {
|
|
62
|
+
await markAttachedSessionIdle(attachedSessionForDirectory.sessionId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let clearedForegroundSession = false;
|
|
66
|
+
for (const session of foregroundBusySessions) {
|
|
67
|
+
const status = getSessionStatus(statuses, session.sessionId);
|
|
68
|
+
if (!isTerminalStatus(status)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (freshForegroundSessionIds.has(session.sessionId)) {
|
|
72
|
+
logger.debug(`[BusyReconciliation] Skipping fresh foreground busy state: session=${session.sessionId}, directory=${session.directory}, status=${status?.type ?? "not-found"}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
logger.info(`[BusyReconciliation] Clearing stale foreground busy state: session=${session.sessionId}, directory=${session.directory}, status=${status?.type ?? "not-found"}`);
|
|
76
|
+
if (attachedSessionForDirectory?.sessionId !== session.sessionId) {
|
|
77
|
+
await markAttachedSessionIdle(session.sessionId);
|
|
78
|
+
}
|
|
79
|
+
await clearForegroundBusySession(session.sessionId, "status_reconcile_idle");
|
|
80
|
+
clearedForegroundSession = true;
|
|
81
|
+
}
|
|
82
|
+
if (clearedForegroundSession) {
|
|
83
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function reconcileBusyState(directory, now = Date.now()) {
|
|
87
|
+
if (!directory || inFlightDirectories.has(directory)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const { foregroundBusySessions, attachedSessionForDirectory } = getReconciliationTargets(directory);
|
|
91
|
+
if (foregroundBusySessions.length === 0 && !attachedSessionForDirectory) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const lastReconcileAt = lastReconcileAtByDirectory.get(directory);
|
|
95
|
+
if (lastReconcileAt !== undefined && now - lastReconcileAt < RECONCILE_MIN_INTERVAL_MS) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
lastReconcileAtByDirectory.set(directory, now);
|
|
99
|
+
inFlightDirectories.add(directory);
|
|
100
|
+
try {
|
|
101
|
+
await reconcileBusyStateNow(directory, now);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
logger.warn("[BusyReconciliation] Failed to reconcile busy state", error);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
inFlightDirectories.delete(directory);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function __resetBusyReconciliationForTests() {
|
|
111
|
+
inFlightDirectories.clear();
|
|
112
|
+
lastReconcileAtByDirectory.clear();
|
|
113
|
+
}
|
|
@@ -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();
|
|
@@ -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));
|