telecodex 0.1.4 → 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/messageHandlers.js +5 -4
- package/dist/bot/handlers/operationalHandlers.js +11 -12
- package/dist/bot/handlers/projectHandlers.js +5 -4
- package/dist/bot/handlers/sessionConfigHandlers.js +31 -30
- package/dist/bot/inputService.js +46 -23
- package/dist/bot/sessionFlow.js +1 -2
- package/dist/bot/userFacingErrors.js +59 -0
- 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
|
@@ -2,9 +2,10 @@ import { contextLogFields, requireScopedSession, } from "../commandSupport.js";
|
|
|
2
2
|
import { handleUserInput, handleUserText } from "../inputService.js";
|
|
3
3
|
import { telegramImageMessageToCodexInput } from "../../telegram/attachments.js";
|
|
4
4
|
import { replyError, replyNotice } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
5
6
|
export function registerMessageHandlers(deps) {
|
|
6
7
|
const { bot, config, store, projects, codex, buffers, logger } = deps;
|
|
7
|
-
bot.on("message:text", async (ctx) => {
|
|
8
|
+
bot.on("message:text", wrapUserFacingHandler("message:text", logger, async (ctx) => {
|
|
8
9
|
const text = ctx.message.text;
|
|
9
10
|
logger?.info("received telegram text message", {
|
|
10
11
|
...contextLogFields(ctx),
|
|
@@ -30,8 +31,8 @@ export function registerMessageHandlers(deps) {
|
|
|
30
31
|
bot,
|
|
31
32
|
...(logger ? { logger } : {}),
|
|
32
33
|
});
|
|
33
|
-
});
|
|
34
|
-
bot.on(["message:photo", "message:document"], async (ctx) => {
|
|
34
|
+
}));
|
|
35
|
+
bot.on(["message:photo", "message:document"], wrapUserFacingHandler("message:attachment", logger, async (ctx) => {
|
|
35
36
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
36
37
|
if (!session) {
|
|
37
38
|
logger?.warn("ignored telegram attachment because no scoped session was available", {
|
|
@@ -68,5 +69,5 @@ export function registerMessageHandlers(deps) {
|
|
|
68
69
|
});
|
|
69
70
|
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
70
71
|
}
|
|
71
|
-
});
|
|
72
|
+
}));
|
|
72
73
|
}
|
|
@@ -5,12 +5,13 @@ import { refreshSessionIfActiveTurnIsStale } from "../inputService.js";
|
|
|
5
5
|
import { contextLogFields, formatHelpText, formatPrivateStatus, formatProjectStatus, formatReasoningEffort, getProjectForContext, hasTopicContext, isPrivateChat, parseSubcommand, requireScopedSession, } from "../commandSupport.js";
|
|
6
6
|
import { formatIsoTimestamp, sessionLogFields } from "../sessionFlow.js";
|
|
7
7
|
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
8
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
8
9
|
export function registerOperationalHandlers(deps) {
|
|
9
10
|
const { bot, config, store, projects, codex, logger } = deps;
|
|
10
|
-
bot.command(["start", "help"], async (ctx) => {
|
|
11
|
+
bot.command(["start", "help"], wrapUserFacingHandler("help", logger, async (ctx) => {
|
|
11
12
|
await replyNotice(ctx, formatHelpText(ctx, projects));
|
|
12
|
-
});
|
|
13
|
-
bot.command("admin", async (ctx) => {
|
|
13
|
+
}));
|
|
14
|
+
bot.command("admin", wrapUserFacingHandler("admin", logger, async (ctx) => {
|
|
14
15
|
if (!isPrivateChat(ctx)) {
|
|
15
16
|
await replyNotice(ctx, "Use /admin in the bot private chat.");
|
|
16
17
|
return;
|
|
@@ -62,8 +63,8 @@ export function registerOperationalHandlers(deps) {
|
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
await replyUsage(ctx, "/admin | /admin rebind | /admin cancel");
|
|
65
|
-
});
|
|
66
|
-
bot.command("status", async (ctx) => {
|
|
66
|
+
}));
|
|
67
|
+
bot.command("status", wrapUserFacingHandler("status", logger, async (ctx) => {
|
|
67
68
|
if (isPrivateChat(ctx)) {
|
|
68
69
|
await replyNotice(ctx, formatPrivateStatus(store, projects));
|
|
69
70
|
return;
|
|
@@ -93,7 +94,6 @@ export function registerOperationalHandlers(deps) {
|
|
|
93
94
|
textField("state", formatSessionRuntimeStatus(latestSession.runtimeStatus)),
|
|
94
95
|
textField("state detail", latestSession.runtimeStatusDetail ?? "none"),
|
|
95
96
|
textField("state updated", formatIsoTimestamp(latestSession.runtimeStatusUpdatedAt)),
|
|
96
|
-
codeField("active turn", latestSession.activeTurnId ?? "none"),
|
|
97
97
|
textField("active run", activeRun ? formatIsoTimestamp(activeRun.startedAt) : "none"),
|
|
98
98
|
codeField("active run thread", activeRun?.threadId ?? "none"),
|
|
99
99
|
textField("active run last event", activeRun?.lastEventType ?? "none"),
|
|
@@ -113,8 +113,8 @@ export function registerOperationalHandlers(deps) {
|
|
|
113
113
|
textField("effort", formatReasoningEffort(latestSession.reasoningEffort)),
|
|
114
114
|
],
|
|
115
115
|
});
|
|
116
|
-
});
|
|
117
|
-
bot.command("queue", async (ctx) => {
|
|
116
|
+
}));
|
|
117
|
+
bot.command("queue", wrapUserFacingHandler("queue", logger, async (ctx) => {
|
|
118
118
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
119
119
|
if (!session)
|
|
120
120
|
return;
|
|
@@ -126,7 +126,6 @@ export function registerOperationalHandlers(deps) {
|
|
|
126
126
|
title: "Queue",
|
|
127
127
|
fields: [
|
|
128
128
|
textField("state", formatSessionRuntimeStatus(session.runtimeStatus)),
|
|
129
|
-
codeField("active turn", session.activeTurnId ?? "none"),
|
|
130
129
|
textField("queue", queueDepth),
|
|
131
130
|
],
|
|
132
131
|
sections: [
|
|
@@ -155,8 +154,8 @@ export function registerOperationalHandlers(deps) {
|
|
|
155
154
|
return;
|
|
156
155
|
}
|
|
157
156
|
await replyUsage(ctx, "/queue | /queue drop <id> | /queue clear");
|
|
158
|
-
});
|
|
159
|
-
bot.command("stop", async (ctx) => {
|
|
157
|
+
}));
|
|
158
|
+
bot.command("stop", wrapUserFacingHandler("stop", logger, async (ctx) => {
|
|
160
159
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
161
160
|
if (!session)
|
|
162
161
|
return;
|
|
@@ -177,7 +176,7 @@ export function registerOperationalHandlers(deps) {
|
|
|
177
176
|
});
|
|
178
177
|
await replyError(ctx, `Interrupt failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
179
178
|
}
|
|
180
|
-
});
|
|
179
|
+
}));
|
|
181
180
|
}
|
|
182
181
|
function formatQueuedPreview(items) {
|
|
183
182
|
if (items.length === 0)
|
|
@@ -2,10 +2,11 @@ import path from "node:path";
|
|
|
2
2
|
import { contextLogFields, ensureTopicSession, formatPrivateProjectList, formatProjectStatus, formatTopicName, getProjectForContext, hasTopicContext, isPrivateChat, isSupergroupChat, parseSubcommand, postTopicReadyMessage, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
|
|
3
3
|
import { formatSessionRuntimeStatus } from "../../runtime/sessionRuntime.js";
|
|
4
4
|
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
5
6
|
const PROJECT_REQUIRED_MESSAGE = "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.";
|
|
6
7
|
export function registerProjectHandlers(deps) {
|
|
7
8
|
const { bot, config, store, projects, logger } = deps;
|
|
8
|
-
bot.command("project", async (ctx) => {
|
|
9
|
+
bot.command("project", wrapUserFacingHandler("project", logger, async (ctx) => {
|
|
9
10
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
10
11
|
if (isPrivateChat(ctx)) {
|
|
11
12
|
if (!command || command === "list" || command === "status") {
|
|
@@ -67,8 +68,8 @@ export function registerProjectHandlers(deps) {
|
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
await replyUsage(ctx, ["/project", "/project bind <absolute-path>", "/project unbind"]);
|
|
70
|
-
});
|
|
71
|
-
bot.command("thread", async (ctx) => {
|
|
71
|
+
}));
|
|
72
|
+
bot.command("thread", wrapUserFacingHandler("thread", logger, async (ctx) => {
|
|
72
73
|
if (isPrivateChat(ctx)) {
|
|
73
74
|
await replyNotice(ctx, "The thread command is only available inside project supergroups.");
|
|
74
75
|
return;
|
|
@@ -112,7 +113,7 @@ export function registerProjectHandlers(deps) {
|
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
await replyUsage(ctx, ["/thread list", "/thread resume <threadId>", "/thread new <topic-name>"]);
|
|
115
|
-
});
|
|
116
|
+
}));
|
|
116
117
|
bot.on(["message:forum_topic_created", "message:forum_topic_edited"], async (ctx) => {
|
|
117
118
|
const threadId = ctx.message.message_thread_id;
|
|
118
119
|
if (threadId == null)
|
|
@@ -2,6 +2,7 @@ import { APPROVAL_POLICIES, MODE_PRESETS, REASONING_EFFORTS, SANDBOX_MODES, WEB_
|
|
|
2
2
|
import { parseCodexConfigOverrides } from "../../codex/configOverrides.js";
|
|
3
3
|
import { assertProjectScopedPath, formatProfileReply, formatReasoningEffort, getProjectForContext, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
|
|
4
4
|
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
5
6
|
export function registerSessionConfigHandlers(deps) {
|
|
6
7
|
registerDirectoryHandlers(deps);
|
|
7
8
|
registerProfileHandlers(deps);
|
|
@@ -9,8 +10,8 @@ export function registerSessionConfigHandlers(deps) {
|
|
|
9
10
|
registerAdvancedHandlers(deps);
|
|
10
11
|
}
|
|
11
12
|
function registerDirectoryHandlers(deps) {
|
|
12
|
-
const { bot, config, store, projects } = deps;
|
|
13
|
-
bot.command("cwd", async (ctx) => {
|
|
13
|
+
const { bot, config, store, projects, logger } = deps;
|
|
14
|
+
bot.command("cwd", wrapUserFacingHandler("cwd", logger, async (ctx) => {
|
|
14
15
|
const project = getProjectForContext(ctx, projects);
|
|
15
16
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
16
17
|
if (!project || !session)
|
|
@@ -34,11 +35,11 @@ function registerDirectoryHandlers(deps) {
|
|
|
34
35
|
catch (error) {
|
|
35
36
|
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
36
37
|
}
|
|
37
|
-
});
|
|
38
|
+
}));
|
|
38
39
|
}
|
|
39
40
|
function registerProfileHandlers(deps) {
|
|
40
|
-
const { bot, config, store, projects } = deps;
|
|
41
|
-
bot.command("mode", async (ctx) => {
|
|
41
|
+
const { bot, config, store, projects, logger } = deps;
|
|
42
|
+
bot.command("mode", wrapUserFacingHandler("mode", logger, async (ctx) => {
|
|
42
43
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
43
44
|
if (!session)
|
|
44
45
|
return;
|
|
@@ -59,8 +60,8 @@ function registerProfileHandlers(deps) {
|
|
|
59
60
|
store.setSandboxMode(session.sessionKey, profile.sandboxMode);
|
|
60
61
|
store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
|
|
61
62
|
await replyNotice(ctx, formatProfileReply("Preset updated.", profile.sandboxMode, profile.approvalPolicy));
|
|
62
|
-
});
|
|
63
|
-
bot.command("sandbox", async (ctx) => {
|
|
63
|
+
}));
|
|
64
|
+
bot.command("sandbox", wrapUserFacingHandler("sandbox", logger, async (ctx) => {
|
|
64
65
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
65
66
|
if (!session)
|
|
66
67
|
return;
|
|
@@ -75,8 +76,8 @@ function registerProfileHandlers(deps) {
|
|
|
75
76
|
}
|
|
76
77
|
store.setSandboxMode(session.sessionKey, sandboxMode);
|
|
77
78
|
await replyNotice(ctx, formatProfileReply("Sandbox updated.", sandboxMode, session.approvalPolicy));
|
|
78
|
-
});
|
|
79
|
-
bot.command("approval", async (ctx) => {
|
|
79
|
+
}));
|
|
80
|
+
bot.command("approval", wrapUserFacingHandler("approval", logger, async (ctx) => {
|
|
80
81
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
81
82
|
if (!session)
|
|
82
83
|
return;
|
|
@@ -91,8 +92,8 @@ function registerProfileHandlers(deps) {
|
|
|
91
92
|
}
|
|
92
93
|
store.setApprovalPolicy(session.sessionKey, approvalPolicy);
|
|
93
94
|
await replyNotice(ctx, formatProfileReply("Approval policy updated.", session.sandboxMode, approvalPolicy));
|
|
94
|
-
});
|
|
95
|
-
bot.command("yolo", async (ctx) => {
|
|
95
|
+
}));
|
|
96
|
+
bot.command("yolo", wrapUserFacingHandler("yolo", logger, async (ctx) => {
|
|
96
97
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
97
98
|
if (!session)
|
|
98
99
|
return;
|
|
@@ -110,11 +111,11 @@ function registerProfileHandlers(deps) {
|
|
|
110
111
|
store.setSandboxMode(session.sessionKey, profile.sandboxMode);
|
|
111
112
|
store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
|
|
112
113
|
await replyNotice(ctx, formatProfileReply(value === "on" ? "YOLO enabled." : "YOLO disabled. Restored the write preset.", profile.sandboxMode, profile.approvalPolicy));
|
|
113
|
-
});
|
|
114
|
+
}));
|
|
114
115
|
}
|
|
115
116
|
function registerExecutionHandlers(deps) {
|
|
116
|
-
const { bot, config, store, projects } = deps;
|
|
117
|
-
bot.command("model", async (ctx) => {
|
|
117
|
+
const { bot, config, store, projects, logger } = deps;
|
|
118
|
+
bot.command("model", wrapUserFacingHandler("model", logger, async (ctx) => {
|
|
118
119
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
119
120
|
if (!session)
|
|
120
121
|
return;
|
|
@@ -125,8 +126,8 @@ function registerExecutionHandlers(deps) {
|
|
|
125
126
|
}
|
|
126
127
|
store.setModel(session.sessionKey, model);
|
|
127
128
|
await replyNotice(ctx, `Set model: ${model}`);
|
|
128
|
-
});
|
|
129
|
-
bot.command("effort", async (ctx) => {
|
|
129
|
+
}));
|
|
130
|
+
bot.command("effort", wrapUserFacingHandler("effort", logger, async (ctx) => {
|
|
130
131
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
131
132
|
if (!session)
|
|
132
133
|
return;
|
|
@@ -141,8 +142,8 @@ function registerExecutionHandlers(deps) {
|
|
|
141
142
|
}
|
|
142
143
|
store.setReasoningEffort(session.sessionKey, value === "default" ? null : value);
|
|
143
144
|
await replyNotice(ctx, `Set reasoning effort: ${value === "default" ? "codex-default" : value}`);
|
|
144
|
-
});
|
|
145
|
-
bot.command("web", async (ctx) => {
|
|
145
|
+
}));
|
|
146
|
+
bot.command("web", wrapUserFacingHandler("web", logger, async (ctx) => {
|
|
146
147
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
147
148
|
if (!session)
|
|
148
149
|
return;
|
|
@@ -157,8 +158,8 @@ function registerExecutionHandlers(deps) {
|
|
|
157
158
|
}
|
|
158
159
|
store.setWebSearchMode(session.sessionKey, value === "default" ? null : value);
|
|
159
160
|
await replyNotice(ctx, `Set web search: ${value === "default" ? "codex-default" : value}`);
|
|
160
|
-
});
|
|
161
|
-
bot.command("network", async (ctx) => {
|
|
161
|
+
}));
|
|
162
|
+
bot.command("network", wrapUserFacingHandler("network", logger, async (ctx) => {
|
|
162
163
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
163
164
|
if (!session)
|
|
164
165
|
return;
|
|
@@ -173,8 +174,8 @@ function registerExecutionHandlers(deps) {
|
|
|
173
174
|
}
|
|
174
175
|
store.setNetworkAccessEnabled(session.sessionKey, value === "on");
|
|
175
176
|
await replyNotice(ctx, `Set network access: ${value}`);
|
|
176
|
-
});
|
|
177
|
-
bot.command("gitcheck", async (ctx) => {
|
|
177
|
+
}));
|
|
178
|
+
bot.command("gitcheck", wrapUserFacingHandler("gitcheck", logger, async (ctx) => {
|
|
178
179
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
179
180
|
if (!session)
|
|
180
181
|
return;
|
|
@@ -189,11 +190,11 @@ function registerExecutionHandlers(deps) {
|
|
|
189
190
|
}
|
|
190
191
|
store.setSkipGitRepoCheck(session.sessionKey, value === "skip");
|
|
191
192
|
await replyNotice(ctx, `Set git repo check: ${value}`);
|
|
192
|
-
});
|
|
193
|
+
}));
|
|
193
194
|
}
|
|
194
195
|
function registerAdvancedHandlers(deps) {
|
|
195
|
-
const { bot, config, store, projects, codex } = deps;
|
|
196
|
-
bot.command("adddir", async (ctx) => {
|
|
196
|
+
const { bot, config, store, projects, codex, logger } = deps;
|
|
197
|
+
bot.command("adddir", wrapUserFacingHandler("adddir", logger, async (ctx) => {
|
|
197
198
|
const project = getProjectForContext(ctx, projects);
|
|
198
199
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
199
200
|
if (!project || !session)
|
|
@@ -275,8 +276,8 @@ function registerAdvancedHandlers(deps) {
|
|
|
275
276
|
return;
|
|
276
277
|
}
|
|
277
278
|
await replyUsage(ctx, "/adddir list | /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear");
|
|
278
|
-
});
|
|
279
|
-
bot.command("schema", async (ctx) => {
|
|
279
|
+
}));
|
|
280
|
+
bot.command("schema", wrapUserFacingHandler("schema", logger, async (ctx) => {
|
|
280
281
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
281
282
|
if (!session)
|
|
282
283
|
return;
|
|
@@ -316,8 +317,8 @@ function registerAdvancedHandlers(deps) {
|
|
|
316
317
|
catch (error) {
|
|
317
318
|
await replyError(ctx, `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
319
|
}
|
|
319
|
-
});
|
|
320
|
-
bot.command("codexconfig", async (ctx) => {
|
|
320
|
+
}));
|
|
321
|
+
bot.command("codexconfig", wrapUserFacingHandler("codexconfig", logger, async (ctx) => {
|
|
321
322
|
const raw = ctx.match.trim();
|
|
322
323
|
if (!raw || raw === "show") {
|
|
323
324
|
const current = store.getAppState("codex_config_overrides");
|
|
@@ -353,7 +354,7 @@ function registerAdvancedHandlers(deps) {
|
|
|
353
354
|
catch (error) {
|
|
354
355
|
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
355
356
|
}
|
|
356
|
-
});
|
|
357
|
+
}));
|
|
357
358
|
}
|
|
358
359
|
function isPlainObject(value) {
|
|
359
360
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
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) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { GrammyError, HttpError } from "grammy";
|
|
2
|
+
import { replyError } from "../telegram/formatted.js";
|
|
3
|
+
import { contextLogFields } from "./commandSupport.js";
|
|
4
|
+
export function wrapUserFacingHandler(handlerName, logger, handler) {
|
|
5
|
+
return async (ctx) => {
|
|
6
|
+
try {
|
|
7
|
+
await handler(ctx);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
logger?.error("telegram handler failed", {
|
|
11
|
+
handler: handlerName,
|
|
12
|
+
...contextLogFields(ctx),
|
|
13
|
+
error,
|
|
14
|
+
});
|
|
15
|
+
await safeReplyError(ctx, describeUserFacingError(error), logger, handlerName, error);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function safeReplyError(ctx, message, logger, handlerName, originalError) {
|
|
20
|
+
try {
|
|
21
|
+
await replyError(ctx, message);
|
|
22
|
+
}
|
|
23
|
+
catch (replyFailure) {
|
|
24
|
+
logger?.error("failed to send telegram handler error reply", {
|
|
25
|
+
handler: handlerName,
|
|
26
|
+
...contextLogFields(ctx),
|
|
27
|
+
error: replyFailure,
|
|
28
|
+
originalError,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function describeUserFacingError(error) {
|
|
33
|
+
if (error instanceof GrammyError) {
|
|
34
|
+
return describeTelegramError(error);
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof HttpError) {
|
|
37
|
+
return "Telegram request failed before it reached the API. Check the local network and try again.";
|
|
38
|
+
}
|
|
39
|
+
if (error instanceof Error) {
|
|
40
|
+
return error.message;
|
|
41
|
+
}
|
|
42
|
+
return String(error);
|
|
43
|
+
}
|
|
44
|
+
function describeTelegramError(error) {
|
|
45
|
+
const description = error.description || error.message;
|
|
46
|
+
const normalized = description.toLowerCase();
|
|
47
|
+
if (error.method === "createForumTopic") {
|
|
48
|
+
if (normalized.includes("not enough rights")) {
|
|
49
|
+
return "Telegram rejected topic creation because the bot lacks permission to create topics. Promote the bot to admin and grant topic management, then try again.";
|
|
50
|
+
}
|
|
51
|
+
if (normalized.includes("forum is disabled") ||
|
|
52
|
+
normalized.includes("chat is not a forum") ||
|
|
53
|
+
normalized.includes("topics are not enabled")) {
|
|
54
|
+
return "This supergroup does not currently allow forum topics. Enable topics for the group and try again.";
|
|
55
|
+
}
|
|
56
|
+
return `Failed to create the Telegram topic: ${description}`;
|
|
57
|
+
}
|
|
58
|
+
return description;
|
|
59
|
+
}
|
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",
|