telecodex 0.1.3 → 0.1.4
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 +6 -5
- package/dist/bot/handlers/operationalHandlers.js +84 -72
- package/dist/bot/handlers/projectHandlers.js +90 -76
- package/dist/bot/handlers/sessionConfigHandlers.js +141 -111
- package/dist/bot/inputService.js +30 -14
- 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,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
export function registerMessageHandlers(deps) {
|
|
5
6
|
const { bot, config, store, projects, codex, buffers, logger } = deps;
|
|
6
7
|
bot.on("message:text", async (ctx) => {
|
|
@@ -12,7 +13,7 @@ export function registerMessageHandlers(deps) {
|
|
|
12
13
|
});
|
|
13
14
|
if (text.startsWith("/"))
|
|
14
15
|
return;
|
|
15
|
-
const session =
|
|
16
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
16
17
|
if (!session) {
|
|
17
18
|
logger?.warn("ignored telegram text message because no scoped session was available", {
|
|
18
19
|
...contextLogFields(ctx),
|
|
@@ -31,7 +32,7 @@ export function registerMessageHandlers(deps) {
|
|
|
31
32
|
});
|
|
32
33
|
});
|
|
33
34
|
bot.on(["message:photo", "message:document"], async (ctx) => {
|
|
34
|
-
const session =
|
|
35
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
35
36
|
if (!session) {
|
|
36
37
|
logger?.warn("ignored telegram attachment because no scoped session was available", {
|
|
37
38
|
...contextLogFields(ctx),
|
|
@@ -47,7 +48,7 @@ export function registerMessageHandlers(deps) {
|
|
|
47
48
|
message: ctx.message,
|
|
48
49
|
});
|
|
49
50
|
if (!prompt) {
|
|
50
|
-
await ctx
|
|
51
|
+
await replyNotice(ctx, "Only image attachments are supported.");
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
await handleUserInput({
|
|
@@ -65,7 +66,7 @@ export function registerMessageHandlers(deps) {
|
|
|
65
66
|
...contextLogFields(ctx),
|
|
66
67
|
error,
|
|
67
68
|
});
|
|
68
|
-
await ctx
|
|
69
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
69
70
|
}
|
|
70
71
|
});
|
|
71
72
|
}
|
|
@@ -2,34 +2,37 @@ 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";
|
|
7
8
|
export function registerOperationalHandlers(deps) {
|
|
8
9
|
const { bot, config, store, projects, codex, logger } = deps;
|
|
9
10
|
bot.command(["start", "help"], async (ctx) => {
|
|
10
|
-
await ctx
|
|
11
|
+
await replyNotice(ctx, formatHelpText(ctx, projects));
|
|
11
12
|
});
|
|
12
13
|
bot.command("admin", async (ctx) => {
|
|
13
14
|
if (!isPrivateChat(ctx)) {
|
|
14
|
-
await ctx
|
|
15
|
+
await replyNotice(ctx, "Use /admin in the bot private chat.");
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
const authorizedUserId = store.getAuthorizedUserId();
|
|
18
19
|
if (authorizedUserId == null) {
|
|
19
|
-
await ctx
|
|
20
|
+
await replyNotice(ctx, "Admin binding is not completed yet.");
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
const { command } = parseSubcommand(ctx.match.trim());
|
|
23
24
|
const binding = store.getBindingCodeState();
|
|
24
25
|
if (!command || command === "status") {
|
|
25
|
-
await ctx
|
|
26
|
-
"Admin status",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
await replyDocument(ctx, {
|
|
27
|
+
title: "Admin status",
|
|
28
|
+
fields: [
|
|
29
|
+
textField("authorized telegram user id", authorizedUserId),
|
|
30
|
+
textField("pending handoff", binding?.mode === "rebind"
|
|
31
|
+
? `active until ${formatIsoTimestamp(binding.expiresAt)} (${binding.maxAttempts - binding.attempts} attempts remaining)`
|
|
32
|
+
: "none"),
|
|
33
|
+
],
|
|
34
|
+
footer: "Usage: /admin | /admin rebind | /admin cancel",
|
|
35
|
+
});
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
if (command === "rebind") {
|
|
@@ -38,124 +41,133 @@ export function registerOperationalHandlers(deps) {
|
|
|
38
41
|
mode: "rebind",
|
|
39
42
|
issuedByUserId: authorizedUserId,
|
|
40
43
|
});
|
|
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
|
-
|
|
44
|
+
await replyDocument(ctx, {
|
|
45
|
+
title: "Admin handoff code created.",
|
|
46
|
+
fields: [
|
|
47
|
+
textField("expires at", formatIsoTimestamp(next.expiresAt)),
|
|
48
|
+
textField("max failed attempts", next.maxAttempts),
|
|
49
|
+
codeField("code", next.code),
|
|
50
|
+
],
|
|
51
|
+
footer: "Send this code from the target Telegram account in this bot's private chat to transfer control.",
|
|
52
|
+
});
|
|
50
53
|
return;
|
|
51
54
|
}
|
|
52
55
|
if (command === "cancel") {
|
|
53
56
|
if (binding?.mode !== "rebind") {
|
|
54
|
-
await ctx
|
|
57
|
+
await replyNotice(ctx, "No pending admin handoff.");
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
60
|
store.clearBindingCode();
|
|
58
|
-
await ctx
|
|
61
|
+
await replyNotice(ctx, "Cancelled the pending admin handoff.");
|
|
59
62
|
return;
|
|
60
63
|
}
|
|
61
|
-
await ctx
|
|
64
|
+
await replyUsage(ctx, "/admin | /admin rebind | /admin cancel");
|
|
62
65
|
});
|
|
63
66
|
bot.command("status", async (ctx) => {
|
|
64
67
|
if (isPrivateChat(ctx)) {
|
|
65
|
-
await ctx
|
|
68
|
+
await replyNotice(ctx, formatPrivateStatus(store, projects));
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
const project = getProjectForContext(ctx, projects);
|
|
69
72
|
if (!project) {
|
|
70
|
-
await ctx
|
|
73
|
+
await replyNotice(ctx, "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.");
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
if (!hasTopicContext(ctx)) {
|
|
74
|
-
await ctx
|
|
77
|
+
await replyNotice(ctx, formatProjectStatus(project));
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
|
-
const session =
|
|
80
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
78
81
|
if (!session)
|
|
79
82
|
return;
|
|
80
83
|
const latestSession = await refreshSessionIfActiveTurnIsStale(session, store, codex, bot, logger);
|
|
81
84
|
const queueDepth = store.getQueuedInputCount(latestSession.sessionKey);
|
|
82
85
|
const queuedPreview = store.listQueuedInputs(latestSession.sessionKey, 3);
|
|
83
86
|
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
|
-
|
|
87
|
+
await replyDocument(ctx, {
|
|
88
|
+
title: "Status",
|
|
89
|
+
fields: [
|
|
90
|
+
codeField("project", project.name),
|
|
91
|
+
codeField("root", project.cwd),
|
|
92
|
+
codeField("thread", latestSession.codexThreadId ?? "not created"),
|
|
93
|
+
textField("state", formatSessionRuntimeStatus(latestSession.runtimeStatus)),
|
|
94
|
+
textField("state detail", latestSession.runtimeStatusDetail ?? "none"),
|
|
95
|
+
textField("state updated", formatIsoTimestamp(latestSession.runtimeStatusUpdatedAt)),
|
|
96
|
+
codeField("active turn", latestSession.activeTurnId ?? "none"),
|
|
97
|
+
textField("active run", activeRun ? formatIsoTimestamp(activeRun.startedAt) : "none"),
|
|
98
|
+
codeField("active run thread", activeRun?.threadId ?? "none"),
|
|
99
|
+
textField("active run last event", activeRun?.lastEventType ?? "none"),
|
|
100
|
+
textField("active run last update", activeRun ? formatIsoTimestamp(activeRun.lastEventAt) : "none"),
|
|
101
|
+
textField("queue", queueDepth),
|
|
102
|
+
textField("queue next", formatQueuedPreview(queuedPreview)),
|
|
103
|
+
codeField("cwd", latestSession.cwd),
|
|
104
|
+
textField("preset", presetFromProfile(latestSession)),
|
|
105
|
+
textField("sandbox", latestSession.sandboxMode),
|
|
106
|
+
textField("approval", latestSession.approvalPolicy),
|
|
107
|
+
textField("network", latestSession.networkAccessEnabled ? "on" : "off"),
|
|
108
|
+
textField("web", latestSession.webSearchMode ?? "codex-default"),
|
|
109
|
+
textField("git check", latestSession.skipGitRepoCheck ? "skip" : "enforce"),
|
|
110
|
+
textField("add dirs", latestSession.additionalDirectories.length),
|
|
111
|
+
textField("schema", latestSession.outputSchema ? "set" : "none"),
|
|
112
|
+
textField("model", latestSession.model),
|
|
113
|
+
textField("effort", formatReasoningEffort(latestSession.reasoningEffort)),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
111
116
|
});
|
|
112
117
|
bot.command("queue", async (ctx) => {
|
|
113
|
-
const session =
|
|
118
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
114
119
|
if (!session)
|
|
115
120
|
return;
|
|
116
121
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
117
122
|
if (!command) {
|
|
118
123
|
const queued = store.listQueuedInputs(session.sessionKey, 5);
|
|
119
124
|
const queueDepth = store.getQueuedInputCount(session.sessionKey);
|
|
120
|
-
await ctx
|
|
121
|
-
"Queue",
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
await replyDocument(ctx, {
|
|
126
|
+
title: "Queue",
|
|
127
|
+
fields: [
|
|
128
|
+
textField("state", formatSessionRuntimeStatus(session.runtimeStatus)),
|
|
129
|
+
codeField("active turn", session.activeTurnId ?? "none"),
|
|
130
|
+
textField("queue", queueDepth),
|
|
131
|
+
],
|
|
132
|
+
sections: [
|
|
133
|
+
{
|
|
134
|
+
title: "Items",
|
|
135
|
+
lines: queued.length > 0 ? [formatQueuedItems(queued)] : ["none"],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
footer: "Usage: /queue | /queue drop <id> | /queue clear",
|
|
139
|
+
});
|
|
128
140
|
return;
|
|
129
141
|
}
|
|
130
142
|
if (command === "clear") {
|
|
131
143
|
const removed = store.clearQueuedInputs(session.sessionKey);
|
|
132
|
-
await ctx
|
|
144
|
+
await replyNotice(ctx, `Cleared the queue and removed ${removed} pending message(s).`);
|
|
133
145
|
return;
|
|
134
146
|
}
|
|
135
147
|
if (command === "drop") {
|
|
136
148
|
const id = Number(args);
|
|
137
149
|
if (!Number.isInteger(id) || id <= 0) {
|
|
138
|
-
await ctx
|
|
150
|
+
await replyUsage(ctx, "/queue drop <id>");
|
|
139
151
|
return;
|
|
140
152
|
}
|
|
141
153
|
const removed = store.removeQueuedInputForSession(session.sessionKey, id);
|
|
142
|
-
await ctx
|
|
154
|
+
await replyNotice(ctx, removed ? `Removed queued item #${id}.` : `Queued item #${id} was not found.`);
|
|
143
155
|
return;
|
|
144
156
|
}
|
|
145
|
-
await ctx
|
|
157
|
+
await replyUsage(ctx, "/queue | /queue drop <id> | /queue clear");
|
|
146
158
|
});
|
|
147
159
|
bot.command("stop", async (ctx) => {
|
|
148
|
-
const session =
|
|
160
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
149
161
|
if (!session)
|
|
150
162
|
return;
|
|
151
163
|
const latest = store.get(session.sessionKey) ?? session;
|
|
152
164
|
if (!codex.isRunning(session.sessionKey)) {
|
|
153
|
-
await ctx
|
|
165
|
+
await replyNotice(ctx, "There is no active Codex SDK turn right now.");
|
|
154
166
|
return;
|
|
155
167
|
}
|
|
156
168
|
try {
|
|
157
169
|
codex.interrupt(session.sessionKey);
|
|
158
|
-
await ctx
|
|
170
|
+
await replyNotice(ctx, "Interrupt requested for the current run. Waiting for Codex SDK to stop.");
|
|
159
171
|
}
|
|
160
172
|
catch (error) {
|
|
161
173
|
logger?.warn("interrupt turn failed", {
|
|
@@ -163,7 +175,7 @@ export function registerOperationalHandlers(deps) {
|
|
|
163
175
|
...sessionLogFields(latest),
|
|
164
176
|
error,
|
|
165
177
|
});
|
|
166
|
-
await ctx
|
|
178
|
+
await replyError(ctx, `Interrupt failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
167
179
|
}
|
|
168
180
|
});
|
|
169
181
|
}
|