@grinev/opencode-telegram-bot 0.20.1 → 0.20.2

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.
@@ -32,6 +32,65 @@ function waitWithAbort(ms, signal) {
32
32
  signal.addEventListener("abort", onAbort, { once: true });
33
33
  });
34
34
  }
35
+ function isRecord(value) {
36
+ return typeof value === "object" && value !== null;
37
+ }
38
+ function isEventLike(value) {
39
+ return isRecord(value) && typeof value.type === "string" && isRecord(value.properties);
40
+ }
41
+ function normalizeDirectoryForComparison(directory) {
42
+ const normalized = directory.replace(/\\/g, "/").replace(/\/+$/, "");
43
+ return /^[a-z]:/i.test(normalized) ? normalized.toLowerCase() : normalized;
44
+ }
45
+ function isSameDirectory(left, right) {
46
+ return normalizeDirectoryForComparison(left) === normalizeDirectoryForComparison(right);
47
+ }
48
+ function normalizeGlobalEvent(rawEvent, directory) {
49
+ if (isEventLike(rawEvent)) {
50
+ return rawEvent;
51
+ }
52
+ if (!isRecord(rawEvent) || !("payload" in rawEvent)) {
53
+ logger.debug("[Events] Ignoring global event with unknown shape");
54
+ return null;
55
+ }
56
+ const eventDirectory = typeof rawEvent.directory === "string" ? rawEvent.directory : null;
57
+ if (eventDirectory && !isSameDirectory(eventDirectory, directory)) {
58
+ return null;
59
+ }
60
+ if (!isEventLike(rawEvent.payload)) {
61
+ logger.debug("[Events] Ignoring global event with unknown payload shape");
62
+ return null;
63
+ }
64
+ return rawEvent.payload;
65
+ }
66
+ function normalizeEvent(rawEvent, source, directory) {
67
+ if (source === "global") {
68
+ return normalizeGlobalEvent(rawEvent, directory);
69
+ }
70
+ if (!isEventLike(rawEvent)) {
71
+ logger.debug("[Events] Ignoring legacy event with unknown shape");
72
+ return null;
73
+ }
74
+ return rawEvent;
75
+ }
76
+ async function subscribeToGlobalEventStream(signal) {
77
+ const globalEvents = opencodeClient.global;
78
+ if (!globalEvents?.event) {
79
+ throw new Error("Global event subscription is not available");
80
+ }
81
+ const result = await globalEvents.event({ signal });
82
+ if (!result.stream) {
83
+ throw new Error(FATAL_NO_STREAM_ERROR);
84
+ }
85
+ return { source: "global", stream: result.stream };
86
+ }
87
+ async function subscribeToLegacyEventStream(directory, signal) {
88
+ const result = await opencodeClient.event.subscribe({ directory }, { signal });
89
+ if (!result.stream) {
90
+ throw new Error(FATAL_NO_STREAM_ERROR);
91
+ }
92
+ return { source: "legacy", stream: result.stream };
93
+ }
35
94
  export async function subscribeToEvents(directory, callback) {
36
95
  if (isListening && activeDirectory === directory) {
37
96
  eventCallback = callback;
@@ -53,14 +112,33 @@ export async function subscribeToEvents(directory, callback) {
53
112
  streamAbortController = controller;
54
113
  try {
55
114
  let reconnectAttempt = 0;
115
+ let useLegacyEventsOnce = false;
56
116
  while (isListening && activeDirectory === directory && !controller.signal.aborted) {
57
117
  try {
58
- const result = await opencodeClient.event.subscribe({ directory }, { signal: controller.signal });
59
- if (!result.stream) {
60
- throw new Error(FATAL_NO_STREAM_ERROR);
118
+ let subscription;
119
+ if (useLegacyEventsOnce) {
120
+ useLegacyEventsOnce = false;
121
+ subscription = await subscribeToLegacyEventStream(directory, controller.signal);
122
+ }
123
+ else {
124
+ try {
125
+ subscription = await subscribeToGlobalEventStream(controller.signal);
126
+ logger.debug(`Using global OpenCode event stream for ${directory}`);
127
+ }
128
+ catch (error) {
129
+ if (controller.signal.aborted || !isListening || activeDirectory !== directory) {
130
+ throw error;
131
+ }
132
+ if (isExpectedOpencodeUnavailableError(error)) {
133
+ throw error;
134
+ }
135
+ logger.warn(`Global event stream unavailable for ${directory}, falling back to project event stream`, error);
136
+ subscription = await subscribeToLegacyEventStream(directory, controller.signal);
137
+ }
61
138
  }
62
139
  reconnectAttempt = 0;
63
- eventStream = result.stream;
140
+ eventStream = subscription.stream;
141
+ let usefulEventCount = 0;
64
142
  for await (const event of eventStream) {
65
143
  if (!isListening || activeDirectory !== directory || controller.signal.aborted) {
66
144
  logger.debug(`Event listener stopped or changed directory, breaking loop`);
@@ -69,6 +147,13 @@ export async function subscribeToEvents(directory, callback) {
69
147
  // CRITICAL: Explicitly yield to the event loop BEFORE processing the event
70
148
  // This allows grammY to handle getUpdates between SSE events
71
149
  await new Promise((resolve) => setImmediate(resolve));
150
+ const normalizedEvent = normalizeEvent(event, subscription.source, directory);
151
+ if (!normalizedEvent) {
152
+ continue;
153
+ }
154
+ if (normalizedEvent.type !== "server.connected") {
155
+ usefulEventCount++;
156
+ }
72
157
  if (eventCallback) {
73
158
  // Use setImmediate to avoid blocking the event loop
74
159
  // and let grammY process incoming Telegram updates
@@ -81,7 +166,7 @@ export async function subscribeToEvents(directory, callback) {
81
166
  listenerGeneration !== generation) {
82
167
  return;
83
168
  }
84
- callbackSnapshot(event);
169
+ callbackSnapshot(normalizedEvent);
85
170
  });
86
171
  }
87
172
  }
@@ -89,6 +174,11 @@ export async function subscribeToEvents(directory, callback) {
89
174
  if (!isListening || activeDirectory !== directory || controller.signal.aborted) {
90
175
  break;
91
176
  }
177
+ if (subscription.source === "global" && usefulEventCount === 0) {
178
+ useLegacyEventsOnce = true;
179
+ logger.warn(`Global event stream ended without project events for ${directory}, falling back to project event stream`);
180
+ continue;
181
+ }
92
182
  reconnectAttempt++;
93
183
  const reconnectDelay = getReconnectDelayMs(reconnectAttempt);
94
184
  logger.warn(`Event stream ended for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
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",