telecodex 0.1.5 → 0.1.6
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/dist/bot/handlers/operationalHandlers.js +0 -2
- package/dist/bot/inputService.js +46 -23
- package/dist/bot/sessionFlow.js +1 -2
- package/dist/codex/sdkRuntime.js +6 -1
- package/dist/runtime/sessionRuntime.js +0 -4
- package/dist/store/sessions.js +0 -2
- package/dist/telegram/delivery.js +88 -11
- package/dist/telegram/messageBuffer.js +45 -8
- package/package.json +2 -2
|
@@ -94,7 +94,6 @@ export function registerOperationalHandlers(deps) {
|
|
|
94
94
|
textField("state", formatSessionRuntimeStatus(latestSession.runtimeStatus)),
|
|
95
95
|
textField("state detail", latestSession.runtimeStatusDetail ?? "none"),
|
|
96
96
|
textField("state updated", formatIsoTimestamp(latestSession.runtimeStatusUpdatedAt)),
|
|
97
|
-
codeField("active turn", latestSession.activeTurnId ?? "none"),
|
|
98
97
|
textField("active run", activeRun ? formatIsoTimestamp(activeRun.startedAt) : "none"),
|
|
99
98
|
codeField("active run thread", activeRun?.threadId ?? "none"),
|
|
100
99
|
textField("active run last event", activeRun?.lastEventType ?? "none"),
|
|
@@ -127,7 +126,6 @@ export function registerOperationalHandlers(deps) {
|
|
|
127
126
|
title: "Queue",
|
|
128
127
|
fields: [
|
|
129
128
|
textField("state", formatSessionRuntimeStatus(session.runtimeStatus)),
|
|
130
|
-
codeField("active turn", session.activeTurnId ?? "none"),
|
|
131
129
|
textField("queue", queueDepth),
|
|
132
130
|
],
|
|
133
131
|
sections: [
|
package/dist/bot/inputService.js
CHANGED
|
@@ -32,7 +32,7 @@ export async function handleUserInput(input) {
|
|
|
32
32
|
chatId: numericChatId(session),
|
|
33
33
|
messageThreadId: numericMessageThreadId(session),
|
|
34
34
|
}, [
|
|
35
|
-
`
|
|
35
|
+
`Codex is still ${describeBusyStatus(session.runtimeStatus)}. Your message was added to the queue.`,
|
|
36
36
|
`queue position: ${queueDepth}`,
|
|
37
37
|
`queued at: ${formatIsoTimestamp(queued.createdAt)}`,
|
|
38
38
|
"It will be processed automatically after the current run finishes.",
|
|
@@ -48,7 +48,7 @@ export async function handleUserInput(input) {
|
|
|
48
48
|
sessionKey: session.sessionKey,
|
|
49
49
|
event: {
|
|
50
50
|
type: "turn.preparing",
|
|
51
|
-
detail: "
|
|
51
|
+
detail: "waiting for first Codex SDK event",
|
|
52
52
|
},
|
|
53
53
|
logger,
|
|
54
54
|
});
|
|
@@ -78,17 +78,6 @@ export async function handleUserInput(input) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
store.setOutputMessage(session.sessionKey, outputMessageId);
|
|
81
|
-
const turnId = createLocalTurnId();
|
|
82
|
-
await applySessionRuntimeEvent({
|
|
83
|
-
bot,
|
|
84
|
-
store,
|
|
85
|
-
sessionKey: session.sessionKey,
|
|
86
|
-
event: {
|
|
87
|
-
type: "turn.started",
|
|
88
|
-
turnId,
|
|
89
|
-
},
|
|
90
|
-
logger,
|
|
91
|
-
});
|
|
92
81
|
void runSessionPrompt({
|
|
93
82
|
sessionKey: session.sessionKey,
|
|
94
83
|
prompt,
|
|
@@ -96,7 +85,6 @@ export async function handleUserInput(input) {
|
|
|
96
85
|
codex,
|
|
97
86
|
buffers,
|
|
98
87
|
bot,
|
|
99
|
-
turnId,
|
|
100
88
|
bufferKey,
|
|
101
89
|
...(logger ? { logger } : {}),
|
|
102
90
|
});
|
|
@@ -118,7 +106,6 @@ export async function refreshSessionIfActiveTurnIsStale(session, store, codex, b
|
|
|
118
106
|
sessionKey: latest.sessionKey,
|
|
119
107
|
event: {
|
|
120
108
|
type: "turn.failed",
|
|
121
|
-
turnId: latest.activeTurnId,
|
|
122
109
|
message: "The previous run was lost. Send the message again.",
|
|
123
110
|
},
|
|
124
111
|
logger,
|
|
@@ -179,10 +166,13 @@ export async function processNextQueuedInputForSession(sessionKey, store, codex,
|
|
|
179
166
|
}
|
|
180
167
|
}
|
|
181
168
|
async function runSessionPrompt(input) {
|
|
182
|
-
const { sessionKey, prompt, store, codex, buffers, bot,
|
|
169
|
+
const { sessionKey, prompt, store, codex, buffers, bot, bufferKey, logger } = input;
|
|
183
170
|
const session = store.get(sessionKey);
|
|
184
171
|
if (!session)
|
|
185
172
|
return;
|
|
173
|
+
logger?.info("starting codex sdk run", {
|
|
174
|
+
...sessionLogFields(session),
|
|
175
|
+
});
|
|
186
176
|
try {
|
|
187
177
|
const result = await codex.run({
|
|
188
178
|
profile: {
|
|
@@ -203,8 +193,19 @@ async function runSessionPrompt(input) {
|
|
|
203
193
|
callbacks: {
|
|
204
194
|
onThreadStarted: async (threadId) => {
|
|
205
195
|
store.bindThread(sessionKey, threadId);
|
|
196
|
+
logger?.info("codex sdk thread started", {
|
|
197
|
+
sessionKey,
|
|
198
|
+
threadId,
|
|
199
|
+
});
|
|
206
200
|
},
|
|
207
201
|
onEvent: async (event) => {
|
|
202
|
+
await applyRuntimeStateForSdkEvent({
|
|
203
|
+
event,
|
|
204
|
+
sessionKey,
|
|
205
|
+
store,
|
|
206
|
+
bot,
|
|
207
|
+
logger,
|
|
208
|
+
});
|
|
208
209
|
await projectEventToTelegramBuffer(buffers, bufferKey, event);
|
|
209
210
|
},
|
|
210
211
|
},
|
|
@@ -219,11 +220,14 @@ async function runSessionPrompt(input) {
|
|
|
219
220
|
sessionKey,
|
|
220
221
|
event: {
|
|
221
222
|
type: "turn.completed",
|
|
222
|
-
turnId,
|
|
223
223
|
},
|
|
224
224
|
logger,
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
|
+
logger?.info("codex sdk run completed", {
|
|
228
|
+
sessionKey,
|
|
229
|
+
threadId: result.threadId,
|
|
230
|
+
});
|
|
227
231
|
await buffers.complete(bufferKey, result.finalResponse || undefined);
|
|
228
232
|
}
|
|
229
233
|
catch (error) {
|
|
@@ -237,16 +241,18 @@ async function runSessionPrompt(input) {
|
|
|
237
241
|
event: isAbortError(error)
|
|
238
242
|
? {
|
|
239
243
|
type: "turn.interrupted",
|
|
240
|
-
turnId,
|
|
241
244
|
}
|
|
242
245
|
: {
|
|
243
246
|
type: "turn.failed",
|
|
244
|
-
turnId,
|
|
245
247
|
message: error instanceof Error ? error.message : String(error),
|
|
246
248
|
},
|
|
247
249
|
logger,
|
|
248
250
|
});
|
|
249
251
|
}
|
|
252
|
+
logger?.warn("codex sdk run failed", {
|
|
253
|
+
sessionKey,
|
|
254
|
+
error,
|
|
255
|
+
});
|
|
250
256
|
if (isAbortError(error)) {
|
|
251
257
|
await buffers.fail(bufferKey, "Current run interrupted.");
|
|
252
258
|
}
|
|
@@ -258,13 +264,33 @@ async function runSessionPrompt(input) {
|
|
|
258
264
|
await processNextQueuedInputForSession(sessionKey, store, codex, buffers, bot, logger);
|
|
259
265
|
}
|
|
260
266
|
}
|
|
267
|
+
async function applyRuntimeStateForSdkEvent(input) {
|
|
268
|
+
if (input.event.type !== "turn.started") {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const session = input.store.get(input.sessionKey);
|
|
272
|
+
if (!session)
|
|
273
|
+
return;
|
|
274
|
+
if (session.runtimeStatus === "running") {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
await applySessionRuntimeEvent({
|
|
278
|
+
bot: input.bot,
|
|
279
|
+
store: input.store,
|
|
280
|
+
sessionKey: input.sessionKey,
|
|
281
|
+
event: {
|
|
282
|
+
type: "turn.started",
|
|
283
|
+
},
|
|
284
|
+
logger: input.logger,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
261
287
|
async function projectEventToTelegramBuffer(buffers, key, event) {
|
|
262
288
|
switch (event.type) {
|
|
263
289
|
case "thread.started":
|
|
264
290
|
buffers.note(key, `thread started: ${event.thread_id}`);
|
|
265
291
|
return;
|
|
266
292
|
case "turn.started":
|
|
267
|
-
buffers.
|
|
293
|
+
buffers.markTurnStarted(key);
|
|
268
294
|
return;
|
|
269
295
|
case "turn.completed":
|
|
270
296
|
buffers.note(key, `token usage: in ${event.usage.input_tokens}, out ${event.usage.output_tokens}, cached ${event.usage.cached_input_tokens}`);
|
|
@@ -357,9 +383,6 @@ function projectTodoList(buffers, key, item) {
|
|
|
357
383
|
buffers.setPlan(key, lines.join("\n"));
|
|
358
384
|
}
|
|
359
385
|
}
|
|
360
|
-
function createLocalTurnId() {
|
|
361
|
-
return `sdk-turn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
362
|
-
}
|
|
363
386
|
function toSdkInput(input) {
|
|
364
387
|
return input;
|
|
365
388
|
}
|
package/dist/bot/sessionFlow.js
CHANGED
|
@@ -31,11 +31,10 @@ export function sessionLogFields(session) {
|
|
|
31
31
|
runtimeStatus: session.runtimeStatus,
|
|
32
32
|
runtimeStatusDetail: session.runtimeStatusDetail,
|
|
33
33
|
codexThreadId: session.codexThreadId,
|
|
34
|
-
activeTurnId: session.activeTurnId,
|
|
35
34
|
};
|
|
36
35
|
}
|
|
37
36
|
export function isSessionBusy(session) {
|
|
38
|
-
return session.runtimeStatus === "preparing" || session.runtimeStatus === "running"
|
|
37
|
+
return session.runtimeStatus === "preparing" || session.runtimeStatus === "running";
|
|
39
38
|
}
|
|
40
39
|
export function describeBusyStatus(status) {
|
|
41
40
|
switch (status) {
|
package/dist/codex/sdkRuntime.js
CHANGED
|
@@ -86,7 +86,12 @@ export class CodexSdkRuntime {
|
|
|
86
86
|
let finalResponse = "";
|
|
87
87
|
let usage = null;
|
|
88
88
|
let threadId = input.initialThreadId;
|
|
89
|
-
|
|
89
|
+
const iterator = streamed.events[Symbol.asyncIterator]();
|
|
90
|
+
while (true) {
|
|
91
|
+
const next = await iterator.next();
|
|
92
|
+
if (next.done)
|
|
93
|
+
break;
|
|
94
|
+
const event = next.value;
|
|
90
95
|
const activeRun = this.activeRuns.get(input.sessionKey);
|
|
91
96
|
if (activeRun) {
|
|
92
97
|
activeRun.lastEventAt = new Date().toISOString();
|
|
@@ -13,14 +13,12 @@ export function reduceSessionRuntimeState(session, event, updatedAt = new Date()
|
|
|
13
13
|
status: "preparing",
|
|
14
14
|
detail: event.detail ?? null,
|
|
15
15
|
updatedAt,
|
|
16
|
-
activeTurnId: null,
|
|
17
16
|
};
|
|
18
17
|
case "turn.started":
|
|
19
18
|
return {
|
|
20
19
|
status: "running",
|
|
21
20
|
detail: null,
|
|
22
21
|
updatedAt,
|
|
23
|
-
activeTurnId: event.turnId,
|
|
24
22
|
};
|
|
25
23
|
case "turn.completed":
|
|
26
24
|
case "turn.interrupted":
|
|
@@ -28,14 +26,12 @@ export function reduceSessionRuntimeState(session, event, updatedAt = new Date()
|
|
|
28
26
|
status: "idle",
|
|
29
27
|
detail: null,
|
|
30
28
|
updatedAt,
|
|
31
|
-
activeTurnId: null,
|
|
32
29
|
};
|
|
33
30
|
case "turn.failed":
|
|
34
31
|
return {
|
|
35
32
|
status: "failed",
|
|
36
33
|
detail: event.message?.trim() || null,
|
|
37
34
|
updatedAt,
|
|
38
|
-
activeTurnId: null,
|
|
39
35
|
};
|
|
40
36
|
}
|
|
41
37
|
}
|
package/dist/store/sessions.js
CHANGED
|
@@ -320,7 +320,6 @@ function mapStoredSession(stored, runtimeState, outputMessageId) {
|
|
|
320
320
|
status: "idle",
|
|
321
321
|
detail: null,
|
|
322
322
|
updatedAt: stored.updatedAt,
|
|
323
|
-
activeTurnId: null,
|
|
324
323
|
};
|
|
325
324
|
return {
|
|
326
325
|
...stored,
|
|
@@ -328,7 +327,6 @@ function mapStoredSession(stored, runtimeState, outputMessageId) {
|
|
|
328
327
|
runtimeStatus: runtime.status,
|
|
329
328
|
runtimeStatusDetail: runtime.detail,
|
|
330
329
|
runtimeStatusUpdatedAt: runtime.updatedAt,
|
|
331
|
-
activeTurnId: runtime.activeTurnId,
|
|
332
330
|
outputMessageId: outputMessageId ?? null,
|
|
333
331
|
};
|
|
334
332
|
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { GrammyError } from "grammy";
|
|
1
|
+
import { GrammyError, HttpError } from "grammy";
|
|
2
2
|
import { renderPlainChunksForTelegram } from "./renderer.js";
|
|
3
3
|
import { splitTelegramHtml } from "./splitMessage.js";
|
|
4
4
|
const telegramCooldownByClient = new WeakMap();
|
|
5
|
+
const MAX_TELEGRAM_RETRY_ATTEMPTS = 5;
|
|
6
|
+
const TELEGRAM_NETWORK_RETRY_BASE_MS = 100;
|
|
7
|
+
const TELEGRAM_NETWORK_RETRY_MAX_MS = 1_000;
|
|
8
|
+
const RETRYABLE_NETWORK_ERROR_CODES = new Set([
|
|
9
|
+
"ECONNRESET",
|
|
10
|
+
"ECONNREFUSED",
|
|
11
|
+
"EPIPE",
|
|
12
|
+
"ETIMEDOUT",
|
|
13
|
+
"ESOCKETTIMEDOUT",
|
|
14
|
+
"ENOTFOUND",
|
|
15
|
+
"EAI_AGAIN",
|
|
16
|
+
]);
|
|
5
17
|
export async function sendHtmlMessage(bot, input, logger) {
|
|
6
18
|
return retryTelegramCall(bot.api, () => bot.api.sendMessage(input.chatId, input.text, {
|
|
7
19
|
...(input.messageThreadId == null ? {} : { message_thread_id: input.messageThreadId }),
|
|
8
20
|
parse_mode: "HTML",
|
|
9
21
|
link_preview_options: { is_disabled: true },
|
|
10
|
-
}), logger, "telegram send
|
|
22
|
+
}), logger, "telegram send retry scheduled", {
|
|
11
23
|
chatId: input.chatId,
|
|
12
24
|
messageThreadId: input.messageThreadId,
|
|
13
25
|
});
|
|
@@ -29,9 +41,11 @@ export async function sendPlainChunks(bot, input, logger) {
|
|
|
29
41
|
export async function sendTypingAction(bot, input, logger) {
|
|
30
42
|
await retryTelegramCall(bot.api, () => bot.api.sendChatAction(input.chatId, "typing", {
|
|
31
43
|
...(input.messageThreadId == null ? {} : { message_thread_id: input.messageThreadId }),
|
|
32
|
-
}), logger, "telegram chat action
|
|
44
|
+
}), logger, "telegram chat action retry scheduled", {
|
|
33
45
|
chatId: input.chatId,
|
|
34
46
|
messageThreadId: input.messageThreadId,
|
|
47
|
+
}, {
|
|
48
|
+
allowNetworkRetry: true,
|
|
35
49
|
});
|
|
36
50
|
}
|
|
37
51
|
export async function replaceOrSendHtmlChunks(bot, input, logger) {
|
|
@@ -84,9 +98,11 @@ export async function editHtmlMessage(bot, input, logger) {
|
|
|
84
98
|
await retryTelegramCall(bot.api, () => bot.api.editMessageText(input.chatId, input.messageId, input.text, {
|
|
85
99
|
parse_mode: "HTML",
|
|
86
100
|
link_preview_options: { is_disabled: true },
|
|
87
|
-
}), logger, "telegram edit
|
|
101
|
+
}), logger, "telegram edit retry scheduled", {
|
|
88
102
|
chatId: input.chatId,
|
|
89
103
|
messageId: input.messageId,
|
|
104
|
+
}, {
|
|
105
|
+
allowNetworkRetry: true,
|
|
90
106
|
});
|
|
91
107
|
}
|
|
92
108
|
export function isMessageNotModifiedError(error) {
|
|
@@ -113,29 +129,90 @@ function retryAfterMs(error) {
|
|
|
113
129
|
}
|
|
114
130
|
return null;
|
|
115
131
|
}
|
|
132
|
+
function retryPlan(error, attempt) {
|
|
133
|
+
const rateLimitDelayMs = retryAfterMs(error);
|
|
134
|
+
if (rateLimitDelayMs != null) {
|
|
135
|
+
return {
|
|
136
|
+
kind: "rate-limit",
|
|
137
|
+
delayMs: rateLimitDelayMs + 250,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (isRetryableNetworkError(error)) {
|
|
141
|
+
return {
|
|
142
|
+
kind: "network",
|
|
143
|
+
delayMs: Math.min(TELEGRAM_NETWORK_RETRY_BASE_MS * 2 ** attempt, TELEGRAM_NETWORK_RETRY_MAX_MS),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
116
148
|
function descriptionOf(error) {
|
|
117
149
|
return typeof error.description === "string" ? error.description : null;
|
|
118
150
|
}
|
|
119
|
-
|
|
151
|
+
function isRetryableNetworkError(error) {
|
|
152
|
+
if (!(error instanceof HttpError))
|
|
153
|
+
return false;
|
|
154
|
+
return isRetryableNetworkCause(error.error);
|
|
155
|
+
}
|
|
156
|
+
function isRetryableNetworkCause(error) {
|
|
157
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const code = readErrorCode(error);
|
|
161
|
+
if (code && RETRYABLE_NETWORK_ERROR_CODES.has(code)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
const message = readErrorMessage(error);
|
|
165
|
+
if (!message)
|
|
166
|
+
return false;
|
|
167
|
+
return (message.includes("socket hang up") ||
|
|
168
|
+
message.includes("connection reset") ||
|
|
169
|
+
message.includes("network request failed") ||
|
|
170
|
+
message.includes("fetch failed") ||
|
|
171
|
+
message.includes("timed out") ||
|
|
172
|
+
message.includes("timeout"));
|
|
173
|
+
}
|
|
174
|
+
function readErrorCode(error) {
|
|
175
|
+
if (typeof error !== "object" || error == null || !("code" in error))
|
|
176
|
+
return null;
|
|
177
|
+
const code = error.code;
|
|
178
|
+
return typeof code === "string" && code ? code : null;
|
|
179
|
+
}
|
|
180
|
+
function readErrorMessage(error) {
|
|
181
|
+
if (error instanceof Error) {
|
|
182
|
+
return error.message.toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
if (typeof error === "string") {
|
|
185
|
+
return error.toLowerCase();
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
export async function retryTelegramCall(cooldownKey, operation, logger, message, context, options) {
|
|
120
190
|
for (let attempt = 0;; attempt += 1) {
|
|
121
191
|
await waitForTelegramCooldown(cooldownKey);
|
|
122
192
|
try {
|
|
123
193
|
return await operation();
|
|
124
194
|
}
|
|
125
195
|
catch (error) {
|
|
126
|
-
const
|
|
127
|
-
|
|
196
|
+
const rateLimitDelayMs = retryAfterMs(error);
|
|
197
|
+
const retry = options?.allowNetworkRetry === true
|
|
198
|
+
? retryPlan(error, attempt)
|
|
199
|
+
: rateLimitDelayMs == null
|
|
200
|
+
? null
|
|
201
|
+
: {
|
|
202
|
+
kind: "rate-limit",
|
|
203
|
+
delayMs: rateLimitDelayMs + 250,
|
|
204
|
+
};
|
|
205
|
+
if (retry == null || attempt >= MAX_TELEGRAM_RETRY_ATTEMPTS) {
|
|
128
206
|
throw error;
|
|
129
207
|
}
|
|
130
|
-
const cooldownMs = waitMs + 250;
|
|
131
208
|
logger?.warn(message, {
|
|
132
209
|
...context,
|
|
133
210
|
attempt: attempt + 1,
|
|
134
|
-
|
|
135
|
-
|
|
211
|
+
retryKind: retry.kind,
|
|
212
|
+
retryDelayMs: retry.delayMs,
|
|
136
213
|
error,
|
|
137
214
|
});
|
|
138
|
-
await applyTelegramCooldown(cooldownKey,
|
|
215
|
+
await applyTelegramCooldown(cooldownKey, retry.delayMs);
|
|
139
216
|
}
|
|
140
217
|
}
|
|
141
218
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { editHtmlMessage, isMessageNotModifiedError, replaceOrSendHtmlChunks, sendHtmlMessage, sendTypingAction, shouldFallbackToNewMessage, } from "./delivery.js";
|
|
2
2
|
import { renderMarkdownForTelegram, renderPlainChunksForTelegram, renderPlainForTelegram } from "./renderer.js";
|
|
3
|
-
const
|
|
3
|
+
const DEFAULT_ACTIVITY_PULSE_INTERVAL_MS = 4_000;
|
|
4
|
+
const DEFAULT_ACTIVITY_IDLE_MS = 60_000;
|
|
4
5
|
export class MessageBuffer {
|
|
5
6
|
bot;
|
|
6
7
|
updateIntervalMs;
|
|
7
8
|
logger;
|
|
8
9
|
states = new Map();
|
|
9
|
-
|
|
10
|
+
activityPulseIntervalMs;
|
|
11
|
+
activityIdleMs;
|
|
12
|
+
constructor(bot, updateIntervalMs, logger, input) {
|
|
10
13
|
this.bot = bot;
|
|
11
14
|
this.updateIntervalMs = updateIntervalMs;
|
|
12
15
|
this.logger = logger;
|
|
16
|
+
this.activityPulseIntervalMs = input?.activityPulseIntervalMs ?? DEFAULT_ACTIVITY_PULSE_INTERVAL_MS;
|
|
17
|
+
this.activityIdleMs = input?.activityIdleMs ?? DEFAULT_ACTIVITY_IDLE_MS;
|
|
13
18
|
}
|
|
14
19
|
async create(key, input) {
|
|
15
20
|
const previous = this.states.get(key);
|
|
@@ -22,12 +27,13 @@ export class MessageBuffer {
|
|
|
22
27
|
const message = await sendHtmlMessage(this.bot, {
|
|
23
28
|
chatId: input.chatId,
|
|
24
29
|
messageThreadId: input.messageThreadId,
|
|
25
|
-
text: "Codex
|
|
30
|
+
text: "Starting Codex...",
|
|
26
31
|
}, this.logger);
|
|
27
32
|
const state = {
|
|
28
33
|
chatId: input.chatId,
|
|
29
34
|
messageThreadId: input.messageThreadId,
|
|
30
35
|
messageId: message.message_id,
|
|
36
|
+
phase: "starting",
|
|
31
37
|
text: "",
|
|
32
38
|
progressLines: [],
|
|
33
39
|
planText: "",
|
|
@@ -36,6 +42,7 @@ export class MessageBuffer {
|
|
|
36
42
|
timer: null,
|
|
37
43
|
activityTimer: null,
|
|
38
44
|
activityInFlight: false,
|
|
45
|
+
lastActivityAt: Date.now(),
|
|
39
46
|
lastSentText: "",
|
|
40
47
|
queue: Promise.resolve(),
|
|
41
48
|
};
|
|
@@ -51,6 +58,7 @@ export class MessageBuffer {
|
|
|
51
58
|
if (!state)
|
|
52
59
|
return;
|
|
53
60
|
state.text = text;
|
|
61
|
+
this.touchActivity(state);
|
|
54
62
|
this.scheduleFlush(key, state);
|
|
55
63
|
}
|
|
56
64
|
note(key, line) {
|
|
@@ -68,6 +76,17 @@ export class MessageBuffer {
|
|
|
68
76
|
if (state.progressLines.length > 8) {
|
|
69
77
|
state.progressLines.splice(0, state.progressLines.length - 8);
|
|
70
78
|
}
|
|
79
|
+
this.touchActivity(state);
|
|
80
|
+
this.scheduleFlush(key, state);
|
|
81
|
+
}
|
|
82
|
+
markTurnStarted(key) {
|
|
83
|
+
const state = this.states.get(key);
|
|
84
|
+
if (!state)
|
|
85
|
+
return;
|
|
86
|
+
if (state.phase === "running")
|
|
87
|
+
return;
|
|
88
|
+
state.phase = "running";
|
|
89
|
+
this.touchActivity(state);
|
|
71
90
|
this.scheduleFlush(key, state);
|
|
72
91
|
}
|
|
73
92
|
setPlan(key, text) {
|
|
@@ -75,6 +94,7 @@ export class MessageBuffer {
|
|
|
75
94
|
if (!state)
|
|
76
95
|
return;
|
|
77
96
|
state.planText = text.trim();
|
|
97
|
+
this.touchActivity(state);
|
|
78
98
|
this.scheduleFlush(key, state);
|
|
79
99
|
}
|
|
80
100
|
setReasoningSummary(key, text) {
|
|
@@ -82,6 +102,7 @@ export class MessageBuffer {
|
|
|
82
102
|
if (!state)
|
|
83
103
|
return;
|
|
84
104
|
state.reasoningSummaryText = text.trim();
|
|
105
|
+
this.touchActivity(state);
|
|
85
106
|
this.scheduleFlush(key, state);
|
|
86
107
|
}
|
|
87
108
|
setToolOutput(key, text) {
|
|
@@ -89,6 +110,7 @@ export class MessageBuffer {
|
|
|
89
110
|
if (!state)
|
|
90
111
|
return;
|
|
91
112
|
state.toolOutputText = truncateTail(text.replace(/\r/g, "").trim(), 2000);
|
|
113
|
+
this.touchActivity(state);
|
|
92
114
|
this.scheduleFlush(key, state);
|
|
93
115
|
}
|
|
94
116
|
rename(from, to) {
|
|
@@ -144,7 +166,7 @@ export class MessageBuffer {
|
|
|
144
166
|
const latest = this.states.get(key);
|
|
145
167
|
if (!latest)
|
|
146
168
|
return;
|
|
147
|
-
const text = renderPlainForTelegram(truncateForEdit(composePendingText(latest)));
|
|
169
|
+
const text = renderPlainForTelegram(truncateForEdit(composePendingText(latest), latest.phase));
|
|
148
170
|
if (text === latest.lastSentText)
|
|
149
171
|
return;
|
|
150
172
|
await this.safeEdit(latest, text);
|
|
@@ -193,10 +215,12 @@ export class MessageBuffer {
|
|
|
193
215
|
}
|
|
194
216
|
}
|
|
195
217
|
startActivityPulse(state) {
|
|
218
|
+
if (state.activityTimer)
|
|
219
|
+
return;
|
|
196
220
|
void this.sendActivityPulse(state);
|
|
197
221
|
const timer = setInterval(() => {
|
|
198
222
|
void this.sendActivityPulse(state);
|
|
199
|
-
},
|
|
223
|
+
}, this.activityPulseIntervalMs);
|
|
200
224
|
timer.unref?.();
|
|
201
225
|
state.activityTimer = timer;
|
|
202
226
|
}
|
|
@@ -207,6 +231,10 @@ export class MessageBuffer {
|
|
|
207
231
|
state.activityTimer = null;
|
|
208
232
|
}
|
|
209
233
|
async sendActivityPulse(state) {
|
|
234
|
+
if (Date.now() - state.lastActivityAt >= this.activityIdleMs) {
|
|
235
|
+
this.stopActivityPulse(state);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
210
238
|
if (state.activityInFlight)
|
|
211
239
|
return;
|
|
212
240
|
state.activityInFlight = true;
|
|
@@ -227,6 +255,12 @@ export class MessageBuffer {
|
|
|
227
255
|
state.activityInFlight = false;
|
|
228
256
|
}
|
|
229
257
|
}
|
|
258
|
+
touchActivity(state) {
|
|
259
|
+
state.lastActivityAt = Date.now();
|
|
260
|
+
if (!state.activityTimer) {
|
|
261
|
+
this.startActivityPulse(state);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
230
264
|
async replaceWithChunks(state, chunks) {
|
|
231
265
|
const messageId = await replaceOrSendHtmlChunks(this.bot, {
|
|
232
266
|
chatId: state.chatId,
|
|
@@ -248,13 +282,13 @@ export class MessageBuffer {
|
|
|
248
282
|
await run;
|
|
249
283
|
}
|
|
250
284
|
}
|
|
251
|
-
function truncateForEdit(text) {
|
|
285
|
+
function truncateForEdit(text, phase) {
|
|
252
286
|
if (text.length <= 3800)
|
|
253
|
-
return text ||
|
|
287
|
+
return text || pendingBanner(phase);
|
|
254
288
|
return `${text.slice(0, 3800)}\n\n...`;
|
|
255
289
|
}
|
|
256
290
|
function composePendingText(state) {
|
|
257
|
-
const sections = [
|
|
291
|
+
const sections = [pendingBanner(state.phase)];
|
|
258
292
|
if (state.planText) {
|
|
259
293
|
sections.push(`[Plan]\n${state.planText}`);
|
|
260
294
|
}
|
|
@@ -275,6 +309,9 @@ function composePendingText(state) {
|
|
|
275
309
|
}
|
|
276
310
|
return sections.join("\n\n");
|
|
277
311
|
}
|
|
312
|
+
function pendingBanner(phase) {
|
|
313
|
+
return phase === "running" ? "Codex is working..." : "Starting Codex...";
|
|
314
|
+
}
|
|
278
315
|
function truncateTail(text, maxLength) {
|
|
279
316
|
if (text.length <= maxLength)
|
|
280
317
|
return text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telecodex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Telegram bridge for local Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@grammyjs/parse-mode": "^2.3.0",
|
|
53
53
|
"@grammyjs/runner": "^2.0.3",
|
|
54
54
|
"@napi-rs/keyring": "^1.2.0",
|
|
55
|
-
"@openai/codex-sdk": "^0.
|
|
55
|
+
"@openai/codex-sdk": "^0.121.0",
|
|
56
56
|
"clipboardy": "^4.0.0",
|
|
57
57
|
"grammy": "^1.42.0",
|
|
58
58
|
"markdown-it": "^14.1.0",
|