telecodex 0.1.3 → 0.1.5
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/auth.js +9 -8
- package/dist/bot/commandSupport.js +27 -14
- package/dist/bot/handlers/messageHandlers.js +11 -9
- package/dist/bot/handlers/operationalHandlers.js +95 -82
- package/dist/bot/handlers/projectHandlers.js +95 -80
- package/dist/bot/handlers/sessionConfigHandlers.js +168 -137
- package/dist/bot/inputService.js +30 -14
- package/dist/bot/userFacingErrors.js +59 -0
- package/dist/codex/configOverrides.js +50 -0
- package/dist/codex/sessionCatalog.js +66 -12
- package/dist/runtime/bootstrap.js +60 -37
- package/dist/runtime/startTelecodex.js +28 -21
- package/dist/store/fileState.js +100 -8
- package/dist/store/projects.js +3 -0
- package/dist/store/sessions.js +3 -0
- package/dist/telegram/delivery.js +41 -5
- package/dist/telegram/formatted.js +183 -0
- package/dist/telegram/messageBuffer.js +10 -0
- package/dist/telegram/splitMessage.js +1 -1
- package/package.json +5 -3
package/dist/bot/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { replyError, replyNotice } from "../telegram/formatted.js";
|
|
1
2
|
export function authMiddleware(input) {
|
|
2
3
|
return async (ctx, next) => {
|
|
3
4
|
const userId = ctx.from?.id;
|
|
@@ -10,7 +11,7 @@ export function authMiddleware(input) {
|
|
|
10
11
|
hasTextMessage: Boolean(ctx.message?.text),
|
|
11
12
|
});
|
|
12
13
|
if (ctx.message?.text && ctx.chat?.type !== "private") {
|
|
13
|
-
await ctx
|
|
14
|
+
await replyError(ctx, "This message was sent as the group identity or as an anonymous admin. telecodex cannot verify the operator. Send it from your personal account instead.");
|
|
14
15
|
}
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
@@ -31,7 +32,7 @@ export function authMiddleware(input) {
|
|
|
31
32
|
store: input.store,
|
|
32
33
|
success: async () => {
|
|
33
34
|
input.store.rebindAuthorizedUserId(userId);
|
|
34
|
-
await ctx
|
|
35
|
+
await replyNotice(ctx, "Admin handoff succeeded. This Telegram account is now authorized to use telecodex.");
|
|
35
36
|
},
|
|
36
37
|
mismatchLabel: "Admin handoff code did not match.",
|
|
37
38
|
exhaustedLabel: "Admin handoff code exhausted its attempt limit and was invalidated. Issue a new one from the currently authorized account.",
|
|
@@ -68,7 +69,7 @@ export function authMiddleware(input) {
|
|
|
68
69
|
const claimedUserId = input.store.claimAuthorizedUserId(userId);
|
|
69
70
|
if (claimedUserId === userId) {
|
|
70
71
|
input.onAdminBound?.(userId);
|
|
71
|
-
await ctx
|
|
72
|
+
await replyNotice(ctx, "Admin binding succeeded. Only this Telegram account can use this bot from now on.");
|
|
72
73
|
return;
|
|
73
74
|
}
|
|
74
75
|
await deny(ctx, "An admin account has already claimed this bot.");
|
|
@@ -79,7 +80,7 @@ export function authMiddleware(input) {
|
|
|
79
80
|
if (handled)
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
|
-
await ctx
|
|
83
|
+
await replyNotice(ctx, "This bot is not initialized yet. Send the binding code shown in the startup logs to complete the one-time admin binding.");
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
async function handleBindingCodeMessage(input) {
|
|
@@ -92,14 +93,14 @@ async function handleBindingCodeMessage(input) {
|
|
|
92
93
|
}
|
|
93
94
|
const attempt = input.store.recordBindingCodeFailure();
|
|
94
95
|
if (!attempt) {
|
|
95
|
-
await input.ctx
|
|
96
|
+
await replyError(input.ctx, "The binding code is no longer active. Issue a new one and try again.");
|
|
96
97
|
return true;
|
|
97
98
|
}
|
|
98
99
|
if (attempt.exhausted) {
|
|
99
|
-
await input.ctx
|
|
100
|
+
await replyError(input.ctx, input.exhaustedLabel);
|
|
100
101
|
return true;
|
|
101
102
|
}
|
|
102
|
-
await input.ctx
|
|
103
|
+
await replyError(input.ctx, input.mismatchLabel, `Remaining attempts: ${attempt.remaining}`);
|
|
103
104
|
return true;
|
|
104
105
|
}
|
|
105
106
|
async function deny(ctx, text) {
|
|
@@ -108,6 +109,6 @@ async function deny(ctx, text) {
|
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
if (ctx.chat?.type === "private") {
|
|
111
|
-
await ctx
|
|
112
|
+
await replyError(ctx, text);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
@@ -2,7 +2,7 @@ import { realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { APPROVAL_POLICIES, MODE_PRESETS, REASONING_EFFORTS, SANDBOX_MODES, } from "../config.js";
|
|
4
4
|
import { makeSessionKey } from "../store/sessions.js";
|
|
5
|
-
import {
|
|
5
|
+
import { replyNotice, sendReplyNotice } from "../telegram/formatted.js";
|
|
6
6
|
import { numericChatId, numericMessageThreadId, sessionFromContext } from "./session.js";
|
|
7
7
|
import { truncateSingleLine } from "./sessionFlow.js";
|
|
8
8
|
export function getProjectForContext(ctx, projects) {
|
|
@@ -12,20 +12,12 @@ export function getProjectForContext(ctx, projects) {
|
|
|
12
12
|
return projects.get(String(chatId));
|
|
13
13
|
}
|
|
14
14
|
export function getScopedSession(ctx, store, projects, config, options) {
|
|
15
|
-
if (isPrivateChat(ctx)) {
|
|
16
|
-
void ctx.reply("Private chat is only for admin binding and project overview. Do actual work inside project supergroup topics.");
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
15
|
const project = getProjectForContext(ctx, projects);
|
|
20
|
-
if (!project)
|
|
21
|
-
void ctx.reply("This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.");
|
|
16
|
+
if (!project || isPrivateChat(ctx))
|
|
22
17
|
return null;
|
|
23
|
-
}
|
|
24
18
|
const requireTopic = options?.requireTopic ?? true;
|
|
25
|
-
if (requireTopic && !hasTopicContext(ctx))
|
|
26
|
-
void ctx.reply("Use this inside a forum topic. The root chat is only for project-level commands; work happens inside topics.");
|
|
19
|
+
if (requireTopic && !hasTopicContext(ctx))
|
|
27
20
|
return null;
|
|
28
|
-
}
|
|
29
21
|
const session = sessionFromContext(ctx, store, config);
|
|
30
22
|
if (!isPathWithinRoot(session.cwd, project.cwd)) {
|
|
31
23
|
store.setCwd(session.sessionKey, project.cwd);
|
|
@@ -33,6 +25,13 @@ export function getScopedSession(ctx, store, projects, config, options) {
|
|
|
33
25
|
}
|
|
34
26
|
return session;
|
|
35
27
|
}
|
|
28
|
+
export async function requireScopedSession(ctx, store, projects, config, options) {
|
|
29
|
+
const session = getScopedSession(ctx, store, projects, config, options);
|
|
30
|
+
if (session)
|
|
31
|
+
return session;
|
|
32
|
+
await replyScopedSessionRequirement(ctx, projects, options);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
36
35
|
export function formatHelpText(ctx, projects) {
|
|
37
36
|
if (isPrivateChat(ctx)) {
|
|
38
37
|
return [
|
|
@@ -163,11 +162,10 @@ export function ensureTopicSession(input) {
|
|
|
163
162
|
return input.store.get(session.sessionKey) ?? session;
|
|
164
163
|
}
|
|
165
164
|
export async function postTopicReadyMessage(bot, session, text, logger) {
|
|
166
|
-
await
|
|
165
|
+
await sendReplyNotice(bot, {
|
|
167
166
|
chatId: numericChatId(session),
|
|
168
167
|
messageThreadId: numericMessageThreadId(session),
|
|
169
|
-
|
|
170
|
-
}, logger);
|
|
168
|
+
}, text, logger);
|
|
171
169
|
}
|
|
172
170
|
export function parseSubcommand(input) {
|
|
173
171
|
const trimmed = input.trim();
|
|
@@ -252,3 +250,18 @@ function canonicalizeBoundaryPath(input) {
|
|
|
252
250
|
return resolved;
|
|
253
251
|
}
|
|
254
252
|
}
|
|
253
|
+
async function replyScopedSessionRequirement(ctx, projects, options) {
|
|
254
|
+
if (isPrivateChat(ctx)) {
|
|
255
|
+
await replyNotice(ctx, "Private chat is only for admin binding and project overview. Do actual work inside project supergroup topics.");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const project = getProjectForContext(ctx, projects);
|
|
259
|
+
if (!project) {
|
|
260
|
+
await replyNotice(ctx, "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const requireTopic = options?.requireTopic ?? true;
|
|
264
|
+
if (requireTopic && !hasTopicContext(ctx)) {
|
|
265
|
+
await replyNotice(ctx, "Use this inside a forum topic. The root chat is only for project-level commands; work happens inside topics.");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { contextLogFields,
|
|
1
|
+
import { contextLogFields, requireScopedSession, } from "../commandSupport.js";
|
|
2
2
|
import { handleUserInput, handleUserText } from "../inputService.js";
|
|
3
3
|
import { telegramImageMessageToCodexInput } from "../../telegram/attachments.js";
|
|
4
|
+
import { replyError, replyNotice } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
4
6
|
export function registerMessageHandlers(deps) {
|
|
5
7
|
const { bot, config, store, projects, codex, buffers, logger } = deps;
|
|
6
|
-
bot.on("message:text", async (ctx) => {
|
|
8
|
+
bot.on("message:text", wrapUserFacingHandler("message:text", logger, async (ctx) => {
|
|
7
9
|
const text = ctx.message.text;
|
|
8
10
|
logger?.info("received telegram text message", {
|
|
9
11
|
...contextLogFields(ctx),
|
|
@@ -12,7 +14,7 @@ export function registerMessageHandlers(deps) {
|
|
|
12
14
|
});
|
|
13
15
|
if (text.startsWith("/"))
|
|
14
16
|
return;
|
|
15
|
-
const session =
|
|
17
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
16
18
|
if (!session) {
|
|
17
19
|
logger?.warn("ignored telegram text message because no scoped session was available", {
|
|
18
20
|
...contextLogFields(ctx),
|
|
@@ -29,9 +31,9 @@ export function registerMessageHandlers(deps) {
|
|
|
29
31
|
bot,
|
|
30
32
|
...(logger ? { logger } : {}),
|
|
31
33
|
});
|
|
32
|
-
});
|
|
33
|
-
bot.on(["message:photo", "message:document"], async (ctx) => {
|
|
34
|
-
const session =
|
|
34
|
+
}));
|
|
35
|
+
bot.on(["message:photo", "message:document"], wrapUserFacingHandler("message:attachment", logger, async (ctx) => {
|
|
36
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
35
37
|
if (!session) {
|
|
36
38
|
logger?.warn("ignored telegram attachment because no scoped session was available", {
|
|
37
39
|
...contextLogFields(ctx),
|
|
@@ -47,7 +49,7 @@ export function registerMessageHandlers(deps) {
|
|
|
47
49
|
message: ctx.message,
|
|
48
50
|
});
|
|
49
51
|
if (!prompt) {
|
|
50
|
-
await ctx
|
|
52
|
+
await replyNotice(ctx, "Only image attachments are supported.");
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
55
|
await handleUserInput({
|
|
@@ -65,7 +67,7 @@ export function registerMessageHandlers(deps) {
|
|
|
65
67
|
...contextLogFields(ctx),
|
|
66
68
|
error,
|
|
67
69
|
});
|
|
68
|
-
await ctx
|
|
70
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
69
71
|
}
|
|
70
|
-
});
|
|
72
|
+
}));
|
|
71
73
|
}
|
|
@@ -2,34 +2,38 @@ import { presetFromProfile } from "../../config.js";
|
|
|
2
2
|
import { generateBindingCode } from "../../runtime/bindingCodes.js";
|
|
3
3
|
import { formatSessionRuntimeStatus } from "../../runtime/sessionRuntime.js";
|
|
4
4
|
import { refreshSessionIfActiveTurnIsStale } from "../inputService.js";
|
|
5
|
-
import { contextLogFields, formatHelpText, formatPrivateStatus, formatProjectStatus, formatReasoningEffort, getProjectForContext,
|
|
5
|
+
import { contextLogFields, formatHelpText, formatPrivateStatus, formatProjectStatus, formatReasoningEffort, getProjectForContext, hasTopicContext, isPrivateChat, parseSubcommand, requireScopedSession, } from "../commandSupport.js";
|
|
6
6
|
import { formatIsoTimestamp, sessionLogFields } from "../sessionFlow.js";
|
|
7
|
+
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
8
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
7
9
|
export function registerOperationalHandlers(deps) {
|
|
8
10
|
const { bot, config, store, projects, codex, logger } = deps;
|
|
9
|
-
bot.command(["start", "help"], async (ctx) => {
|
|
10
|
-
await ctx
|
|
11
|
-
});
|
|
12
|
-
bot.command("admin", async (ctx) => {
|
|
11
|
+
bot.command(["start", "help"], wrapUserFacingHandler("help", logger, async (ctx) => {
|
|
12
|
+
await replyNotice(ctx, formatHelpText(ctx, projects));
|
|
13
|
+
}));
|
|
14
|
+
bot.command("admin", wrapUserFacingHandler("admin", logger, async (ctx) => {
|
|
13
15
|
if (!isPrivateChat(ctx)) {
|
|
14
|
-
await ctx
|
|
16
|
+
await replyNotice(ctx, "Use /admin in the bot private chat.");
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
19
|
const authorizedUserId = store.getAuthorizedUserId();
|
|
18
20
|
if (authorizedUserId == null) {
|
|
19
|
-
await ctx
|
|
21
|
+
await replyNotice(ctx, "Admin binding is not completed yet.");
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
22
24
|
const { command } = parseSubcommand(ctx.match.trim());
|
|
23
25
|
const binding = store.getBindingCodeState();
|
|
24
26
|
if (!command || command === "status") {
|
|
25
|
-
await ctx
|
|
26
|
-
"Admin status",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
await replyDocument(ctx, {
|
|
28
|
+
title: "Admin status",
|
|
29
|
+
fields: [
|
|
30
|
+
textField("authorized telegram user id", authorizedUserId),
|
|
31
|
+
textField("pending handoff", binding?.mode === "rebind"
|
|
32
|
+
? `active until ${formatIsoTimestamp(binding.expiresAt)} (${binding.maxAttempts - binding.attempts} attempts remaining)`
|
|
33
|
+
: "none"),
|
|
34
|
+
],
|
|
35
|
+
footer: "Usage: /admin | /admin rebind | /admin cancel",
|
|
36
|
+
});
|
|
33
37
|
return;
|
|
34
38
|
}
|
|
35
39
|
if (command === "rebind") {
|
|
@@ -38,124 +42,133 @@ export function registerOperationalHandlers(deps) {
|
|
|
38
42
|
mode: "rebind",
|
|
39
43
|
issuedByUserId: authorizedUserId,
|
|
40
44
|
});
|
|
41
|
-
await ctx
|
|
42
|
-
"Admin handoff code created.",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"Send this code from the target Telegram account in this bot's private chat to transfer control.",
|
|
49
|
-
|
|
45
|
+
await replyDocument(ctx, {
|
|
46
|
+
title: "Admin handoff code created.",
|
|
47
|
+
fields: [
|
|
48
|
+
textField("expires at", formatIsoTimestamp(next.expiresAt)),
|
|
49
|
+
textField("max failed attempts", next.maxAttempts),
|
|
50
|
+
codeField("code", next.code),
|
|
51
|
+
],
|
|
52
|
+
footer: "Send this code from the target Telegram account in this bot's private chat to transfer control.",
|
|
53
|
+
});
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
if (command === "cancel") {
|
|
53
57
|
if (binding?.mode !== "rebind") {
|
|
54
|
-
await ctx
|
|
58
|
+
await replyNotice(ctx, "No pending admin handoff.");
|
|
55
59
|
return;
|
|
56
60
|
}
|
|
57
61
|
store.clearBindingCode();
|
|
58
|
-
await ctx
|
|
62
|
+
await replyNotice(ctx, "Cancelled the pending admin handoff.");
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
|
-
await ctx
|
|
62
|
-
});
|
|
63
|
-
bot.command("status", async (ctx) => {
|
|
65
|
+
await replyUsage(ctx, "/admin | /admin rebind | /admin cancel");
|
|
66
|
+
}));
|
|
67
|
+
bot.command("status", wrapUserFacingHandler("status", logger, async (ctx) => {
|
|
64
68
|
if (isPrivateChat(ctx)) {
|
|
65
|
-
await ctx
|
|
69
|
+
await replyNotice(ctx, formatPrivateStatus(store, projects));
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
const project = getProjectForContext(ctx, projects);
|
|
69
73
|
if (!project) {
|
|
70
|
-
await ctx
|
|
74
|
+
await replyNotice(ctx, "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.");
|
|
71
75
|
return;
|
|
72
76
|
}
|
|
73
77
|
if (!hasTopicContext(ctx)) {
|
|
74
|
-
await ctx
|
|
78
|
+
await replyNotice(ctx, formatProjectStatus(project));
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
|
-
const session =
|
|
81
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
78
82
|
if (!session)
|
|
79
83
|
return;
|
|
80
84
|
const latestSession = await refreshSessionIfActiveTurnIsStale(session, store, codex, bot, logger);
|
|
81
85
|
const queueDepth = store.getQueuedInputCount(latestSession.sessionKey);
|
|
82
86
|
const queuedPreview = store.listQueuedInputs(latestSession.sessionKey, 3);
|
|
83
87
|
const activeRun = codex.getActiveRun(latestSession.sessionKey);
|
|
84
|
-
await ctx
|
|
85
|
-
"Status",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
88
|
+
await replyDocument(ctx, {
|
|
89
|
+
title: "Status",
|
|
90
|
+
fields: [
|
|
91
|
+
codeField("project", project.name),
|
|
92
|
+
codeField("root", project.cwd),
|
|
93
|
+
codeField("thread", latestSession.codexThreadId ?? "not created"),
|
|
94
|
+
textField("state", formatSessionRuntimeStatus(latestSession.runtimeStatus)),
|
|
95
|
+
textField("state detail", latestSession.runtimeStatusDetail ?? "none"),
|
|
96
|
+
textField("state updated", formatIsoTimestamp(latestSession.runtimeStatusUpdatedAt)),
|
|
97
|
+
codeField("active turn", latestSession.activeTurnId ?? "none"),
|
|
98
|
+
textField("active run", activeRun ? formatIsoTimestamp(activeRun.startedAt) : "none"),
|
|
99
|
+
codeField("active run thread", activeRun?.threadId ?? "none"),
|
|
100
|
+
textField("active run last event", activeRun?.lastEventType ?? "none"),
|
|
101
|
+
textField("active run last update", activeRun ? formatIsoTimestamp(activeRun.lastEventAt) : "none"),
|
|
102
|
+
textField("queue", queueDepth),
|
|
103
|
+
textField("queue next", formatQueuedPreview(queuedPreview)),
|
|
104
|
+
codeField("cwd", latestSession.cwd),
|
|
105
|
+
textField("preset", presetFromProfile(latestSession)),
|
|
106
|
+
textField("sandbox", latestSession.sandboxMode),
|
|
107
|
+
textField("approval", latestSession.approvalPolicy),
|
|
108
|
+
textField("network", latestSession.networkAccessEnabled ? "on" : "off"),
|
|
109
|
+
textField("web", latestSession.webSearchMode ?? "codex-default"),
|
|
110
|
+
textField("git check", latestSession.skipGitRepoCheck ? "skip" : "enforce"),
|
|
111
|
+
textField("add dirs", latestSession.additionalDirectories.length),
|
|
112
|
+
textField("schema", latestSession.outputSchema ? "set" : "none"),
|
|
113
|
+
textField("model", latestSession.model),
|
|
114
|
+
textField("effort", formatReasoningEffort(latestSession.reasoningEffort)),
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
}));
|
|
118
|
+
bot.command("queue", wrapUserFacingHandler("queue", logger, async (ctx) => {
|
|
119
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
114
120
|
if (!session)
|
|
115
121
|
return;
|
|
116
122
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
117
123
|
if (!command) {
|
|
118
124
|
const queued = store.listQueuedInputs(session.sessionKey, 5);
|
|
119
125
|
const queueDepth = store.getQueuedInputCount(session.sessionKey);
|
|
120
|
-
await ctx
|
|
121
|
-
"Queue",
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
await replyDocument(ctx, {
|
|
127
|
+
title: "Queue",
|
|
128
|
+
fields: [
|
|
129
|
+
textField("state", formatSessionRuntimeStatus(session.runtimeStatus)),
|
|
130
|
+
codeField("active turn", session.activeTurnId ?? "none"),
|
|
131
|
+
textField("queue", queueDepth),
|
|
132
|
+
],
|
|
133
|
+
sections: [
|
|
134
|
+
{
|
|
135
|
+
title: "Items",
|
|
136
|
+
lines: queued.length > 0 ? [formatQueuedItems(queued)] : ["none"],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
footer: "Usage: /queue | /queue drop <id> | /queue clear",
|
|
140
|
+
});
|
|
128
141
|
return;
|
|
129
142
|
}
|
|
130
143
|
if (command === "clear") {
|
|
131
144
|
const removed = store.clearQueuedInputs(session.sessionKey);
|
|
132
|
-
await ctx
|
|
145
|
+
await replyNotice(ctx, `Cleared the queue and removed ${removed} pending message(s).`);
|
|
133
146
|
return;
|
|
134
147
|
}
|
|
135
148
|
if (command === "drop") {
|
|
136
149
|
const id = Number(args);
|
|
137
150
|
if (!Number.isInteger(id) || id <= 0) {
|
|
138
|
-
await ctx
|
|
151
|
+
await replyUsage(ctx, "/queue drop <id>");
|
|
139
152
|
return;
|
|
140
153
|
}
|
|
141
154
|
const removed = store.removeQueuedInputForSession(session.sessionKey, id);
|
|
142
|
-
await ctx
|
|
155
|
+
await replyNotice(ctx, removed ? `Removed queued item #${id}.` : `Queued item #${id} was not found.`);
|
|
143
156
|
return;
|
|
144
157
|
}
|
|
145
|
-
await ctx
|
|
146
|
-
});
|
|
147
|
-
bot.command("stop", async (ctx) => {
|
|
148
|
-
const session =
|
|
158
|
+
await replyUsage(ctx, "/queue | /queue drop <id> | /queue clear");
|
|
159
|
+
}));
|
|
160
|
+
bot.command("stop", wrapUserFacingHandler("stop", logger, async (ctx) => {
|
|
161
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
149
162
|
if (!session)
|
|
150
163
|
return;
|
|
151
164
|
const latest = store.get(session.sessionKey) ?? session;
|
|
152
165
|
if (!codex.isRunning(session.sessionKey)) {
|
|
153
|
-
await ctx
|
|
166
|
+
await replyNotice(ctx, "There is no active Codex SDK turn right now.");
|
|
154
167
|
return;
|
|
155
168
|
}
|
|
156
169
|
try {
|
|
157
170
|
codex.interrupt(session.sessionKey);
|
|
158
|
-
await ctx
|
|
171
|
+
await replyNotice(ctx, "Interrupt requested for the current run. Waiting for Codex SDK to stop.");
|
|
159
172
|
}
|
|
160
173
|
catch (error) {
|
|
161
174
|
logger?.warn("interrupt turn failed", {
|
|
@@ -163,9 +176,9 @@ export function registerOperationalHandlers(deps) {
|
|
|
163
176
|
...sessionLogFields(latest),
|
|
164
177
|
error,
|
|
165
178
|
});
|
|
166
|
-
await ctx
|
|
179
|
+
await replyError(ctx, `Interrupt failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
167
180
|
}
|
|
168
|
-
});
|
|
181
|
+
}));
|
|
169
182
|
}
|
|
170
183
|
function formatQueuedPreview(items) {
|
|
171
184
|
if (items.length === 0)
|