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.
@@ -1,299 +1,344 @@
1
1
  import { APPROVAL_POLICIES, MODE_PRESETS, REASONING_EFFORTS, SANDBOX_MODES, WEB_SEARCH_MODES, isSessionApprovalPolicy, isSessionModePreset, isSessionReasoningEffort, isSessionSandboxMode, isSessionWebSearchMode, presetFromProfile, profileFromPreset, } from "../../config.js";
2
- import { assertProjectScopedPath, formatProfileReply, formatReasoningEffort, getProjectForContext, getScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
2
+ import { parseCodexConfigOverrides } from "../../codex/configOverrides.js";
3
+ import { assertProjectScopedPath, formatProfileReply, formatReasoningEffort, getProjectForContext, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
4
+ import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
5
+ import { wrapUserFacingHandler } from "../userFacingErrors.js";
3
6
  export function registerSessionConfigHandlers(deps) {
4
- const { bot, config, store, projects, codex } = deps;
5
- bot.command("cwd", async (ctx) => {
7
+ registerDirectoryHandlers(deps);
8
+ registerProfileHandlers(deps);
9
+ registerExecutionHandlers(deps);
10
+ registerAdvancedHandlers(deps);
11
+ }
12
+ function registerDirectoryHandlers(deps) {
13
+ const { bot, config, store, projects, logger } = deps;
14
+ bot.command("cwd", wrapUserFacingHandler("cwd", logger, async (ctx) => {
6
15
  const project = getProjectForContext(ctx, projects);
7
- const session = getScopedSession(ctx, store, projects, config);
16
+ const session = await requireScopedSession(ctx, store, projects, config);
8
17
  if (!project || !session)
9
18
  return;
10
19
  const cwd = ctx.match.trim();
11
20
  if (!cwd) {
12
- await ctx.reply(`Current directory: ${session.cwd}\nProject root: ${project.cwd}`);
21
+ await replyCurrentSetting(ctx, "Current directory", [
22
+ codeField("cwd", session.cwd),
23
+ codeField("project root", project.cwd),
24
+ ]);
13
25
  return;
14
26
  }
15
27
  try {
16
28
  const allowed = assertProjectScopedPath(cwd, project.cwd);
17
29
  store.setCwd(session.sessionKey, allowed);
18
- await ctx.reply(`Set cwd:\n${allowed}`);
30
+ await replyDocument(ctx, {
31
+ title: "Set cwd",
32
+ fields: [codeField("cwd", allowed)],
33
+ });
19
34
  }
20
35
  catch (error) {
21
- await ctx.reply(error instanceof Error ? error.message : String(error));
36
+ await replyError(ctx, error instanceof Error ? error.message : String(error));
22
37
  }
23
- });
24
- bot.command("mode", async (ctx) => {
25
- const session = getScopedSession(ctx, store, projects, config);
38
+ }));
39
+ }
40
+ function registerProfileHandlers(deps) {
41
+ const { bot, config, store, projects, logger } = deps;
42
+ bot.command("mode", wrapUserFacingHandler("mode", logger, async (ctx) => {
43
+ const session = await requireScopedSession(ctx, store, projects, config);
26
44
  if (!session)
27
45
  return;
28
46
  const preset = ctx.match.trim();
29
47
  if (!preset) {
30
- await ctx.reply([
31
- `Current preset: ${presetFromProfile(session)}`,
32
- `sandbox: ${session.sandboxMode}`,
33
- `approval: ${session.approvalPolicy}`,
34
- `Usage: /mode ${MODE_PRESETS.join("|")}`,
35
- ].join("\n"));
48
+ await replyCurrentSetting(ctx, "Current preset", [
49
+ textField("preset", presetFromProfile(session)),
50
+ textField("sandbox", session.sandboxMode),
51
+ textField("approval", session.approvalPolicy),
52
+ ], `Usage: /mode ${MODE_PRESETS.join("|")}`);
36
53
  return;
37
54
  }
38
55
  if (!isSessionModePreset(preset)) {
39
- await ctx.reply(`Invalid preset.\nUsage: /mode ${MODE_PRESETS.join("|")}`);
56
+ await replyInvalidValue(ctx, "Invalid preset.", `Usage: /mode ${MODE_PRESETS.join("|")}`);
40
57
  return;
41
58
  }
42
59
  const profile = profileFromPreset(preset);
43
60
  store.setSandboxMode(session.sessionKey, profile.sandboxMode);
44
61
  store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
45
- await ctx.reply(formatProfileReply("Preset updated.", profile.sandboxMode, profile.approvalPolicy));
46
- });
47
- bot.command("sandbox", async (ctx) => {
48
- const session = getScopedSession(ctx, store, projects, config);
62
+ await replyNotice(ctx, formatProfileReply("Preset updated.", profile.sandboxMode, profile.approvalPolicy));
63
+ }));
64
+ bot.command("sandbox", wrapUserFacingHandler("sandbox", logger, async (ctx) => {
65
+ const session = await requireScopedSession(ctx, store, projects, config);
49
66
  if (!session)
50
67
  return;
51
68
  const sandboxMode = ctx.match.trim();
52
69
  if (!sandboxMode) {
53
- await ctx.reply(`Current sandbox: ${session.sandboxMode}\nUsage: /sandbox ${SANDBOX_MODES.join("|")}`);
70
+ await replyCurrentSetting(ctx, "Current sandbox", [textField("sandbox", session.sandboxMode)], `Usage: /sandbox ${SANDBOX_MODES.join("|")}`);
54
71
  return;
55
72
  }
56
73
  if (!isSessionSandboxMode(sandboxMode)) {
57
- await ctx.reply(`Invalid sandbox.\nUsage: /sandbox ${SANDBOX_MODES.join("|")}`);
74
+ await replyInvalidValue(ctx, "Invalid sandbox.", `Usage: /sandbox ${SANDBOX_MODES.join("|")}`);
58
75
  return;
59
76
  }
60
77
  store.setSandboxMode(session.sessionKey, sandboxMode);
61
- await ctx.reply(formatProfileReply("Sandbox updated.", sandboxMode, session.approvalPolicy));
62
- });
63
- bot.command("approval", async (ctx) => {
64
- const session = getScopedSession(ctx, store, projects, config);
78
+ await replyNotice(ctx, formatProfileReply("Sandbox updated.", sandboxMode, session.approvalPolicy));
79
+ }));
80
+ bot.command("approval", wrapUserFacingHandler("approval", logger, async (ctx) => {
81
+ const session = await requireScopedSession(ctx, store, projects, config);
65
82
  if (!session)
66
83
  return;
67
84
  const approvalPolicy = ctx.match.trim();
68
85
  if (!approvalPolicy) {
69
- await ctx.reply(`Current approval: ${session.approvalPolicy}\nUsage: /approval ${APPROVAL_POLICIES.join("|")}`);
86
+ await replyCurrentSetting(ctx, "Current approval", [textField("approval", session.approvalPolicy)], `Usage: /approval ${APPROVAL_POLICIES.join("|")}`);
70
87
  return;
71
88
  }
72
89
  if (!isSessionApprovalPolicy(approvalPolicy)) {
73
- await ctx.reply(`Invalid approval policy.\nUsage: /approval ${APPROVAL_POLICIES.join("|")}`);
90
+ await replyInvalidValue(ctx, "Invalid approval policy.", `Usage: /approval ${APPROVAL_POLICIES.join("|")}`);
74
91
  return;
75
92
  }
76
93
  store.setApprovalPolicy(session.sessionKey, approvalPolicy);
77
- await ctx.reply(formatProfileReply("Approval policy updated.", session.sandboxMode, approvalPolicy));
78
- });
79
- bot.command("yolo", async (ctx) => {
80
- const session = getScopedSession(ctx, store, projects, config);
94
+ await replyNotice(ctx, formatProfileReply("Approval policy updated.", session.sandboxMode, approvalPolicy));
95
+ }));
96
+ bot.command("yolo", wrapUserFacingHandler("yolo", logger, async (ctx) => {
97
+ const session = await requireScopedSession(ctx, store, projects, config);
81
98
  if (!session)
82
99
  return;
83
100
  const value = ctx.match.trim().toLowerCase();
84
101
  if (!value) {
85
102
  const enabled = session.sandboxMode === "danger-full-access" && session.approvalPolicy === "never";
86
- await ctx.reply(`Current yolo: ${enabled ? "on" : "off"}\nUsage: /yolo on|off`);
103
+ await replyCurrentSetting(ctx, "Current yolo", [textField("yolo", enabled ? "on" : "off")], "Usage: /yolo on|off");
87
104
  return;
88
105
  }
89
106
  if (value !== "on" && value !== "off") {
90
- await ctx.reply("Usage: /yolo on|off");
107
+ await replyUsage(ctx, "/yolo on|off");
91
108
  return;
92
109
  }
93
110
  const profile = profileFromPreset(value === "on" ? "yolo" : "write");
94
111
  store.setSandboxMode(session.sessionKey, profile.sandboxMode);
95
112
  store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
96
- await ctx.reply(formatProfileReply(value === "on" ? "YOLO enabled." : "YOLO disabled. Restored the write preset.", profile.sandboxMode, profile.approvalPolicy));
97
- });
98
- bot.command("model", async (ctx) => {
99
- const session = getScopedSession(ctx, store, projects, config);
113
+ await replyNotice(ctx, formatProfileReply(value === "on" ? "YOLO enabled." : "YOLO disabled. Restored the write preset.", profile.sandboxMode, profile.approvalPolicy));
114
+ }));
115
+ }
116
+ function registerExecutionHandlers(deps) {
117
+ const { bot, config, store, projects, logger } = deps;
118
+ bot.command("model", wrapUserFacingHandler("model", logger, async (ctx) => {
119
+ const session = await requireScopedSession(ctx, store, projects, config);
100
120
  if (!session)
101
121
  return;
102
122
  const model = ctx.match.trim();
103
123
  if (!model) {
104
- await ctx.reply(`Current model: ${session.model}`);
124
+ await replyCurrentSetting(ctx, "Current model", [textField("model", session.model)]);
105
125
  return;
106
126
  }
107
127
  store.setModel(session.sessionKey, model);
108
- await ctx.reply(`Set model: ${model}`);
109
- });
110
- bot.command("effort", async (ctx) => {
111
- const session = getScopedSession(ctx, store, projects, config);
128
+ await replyNotice(ctx, `Set model: ${model}`);
129
+ }));
130
+ bot.command("effort", wrapUserFacingHandler("effort", logger, async (ctx) => {
131
+ const session = await requireScopedSession(ctx, store, projects, config);
112
132
  if (!session)
113
133
  return;
114
134
  const value = ctx.match.trim().toLowerCase();
115
135
  if (!value) {
116
- await ctx.reply(`Current reasoning effort: ${formatReasoningEffort(session.reasoningEffort)}\nUsage: /effort default|${REASONING_EFFORTS.join("|")}`);
136
+ await replyCurrentSetting(ctx, "Current reasoning effort", [textField("effort", formatReasoningEffort(session.reasoningEffort))], `Usage: /effort default|${REASONING_EFFORTS.join("|")}`);
117
137
  return;
118
138
  }
119
139
  if (value !== "default" && !isSessionReasoningEffort(value)) {
120
- await ctx.reply(`Invalid reasoning effort.\nUsage: /effort default|${REASONING_EFFORTS.join("|")}`);
140
+ await replyInvalidValue(ctx, "Invalid reasoning effort.", `Usage: /effort default|${REASONING_EFFORTS.join("|")}`);
121
141
  return;
122
142
  }
123
- if (value === "default") {
124
- store.setReasoningEffort(session.sessionKey, null);
125
- }
126
- else {
127
- store.setReasoningEffort(session.sessionKey, value);
128
- }
129
- await ctx.reply(`Set reasoning effort: ${value === "default" ? "codex-default" : value}`);
130
- });
131
- bot.command("web", async (ctx) => {
132
- const session = getScopedSession(ctx, store, projects, config);
143
+ store.setReasoningEffort(session.sessionKey, value === "default" ? null : value);
144
+ await replyNotice(ctx, `Set reasoning effort: ${value === "default" ? "codex-default" : value}`);
145
+ }));
146
+ bot.command("web", wrapUserFacingHandler("web", logger, async (ctx) => {
147
+ const session = await requireScopedSession(ctx, store, projects, config);
133
148
  if (!session)
134
149
  return;
135
150
  const value = ctx.match.trim().toLowerCase();
136
151
  if (!value) {
137
- await ctx.reply(`Current web search: ${session.webSearchMode ?? "codex-default"}\nUsage: /web default|${WEB_SEARCH_MODES.join("|")}`);
152
+ await replyCurrentSetting(ctx, "Current web search", [textField("web", session.webSearchMode ?? "codex-default")], `Usage: /web default|${WEB_SEARCH_MODES.join("|")}`);
138
153
  return;
139
154
  }
140
155
  if (value !== "default" && !isSessionWebSearchMode(value)) {
141
- await ctx.reply(`Invalid web search mode.\nUsage: /web default|${WEB_SEARCH_MODES.join("|")}`);
156
+ await replyInvalidValue(ctx, "Invalid web search mode.", `Usage: /web default|${WEB_SEARCH_MODES.join("|")}`);
142
157
  return;
143
158
  }
144
159
  store.setWebSearchMode(session.sessionKey, value === "default" ? null : value);
145
- await ctx.reply(`Set web search: ${value === "default" ? "codex-default" : value}`);
146
- });
147
- bot.command("network", async (ctx) => {
148
- const session = getScopedSession(ctx, store, projects, config);
160
+ await replyNotice(ctx, `Set web search: ${value === "default" ? "codex-default" : value}`);
161
+ }));
162
+ bot.command("network", wrapUserFacingHandler("network", logger, async (ctx) => {
163
+ const session = await requireScopedSession(ctx, store, projects, config);
149
164
  if (!session)
150
165
  return;
151
166
  const value = ctx.match.trim().toLowerCase();
152
167
  if (!value) {
153
- await ctx.reply(`Current network access: ${session.networkAccessEnabled ? "on" : "off"}\nUsage: /network on|off`);
168
+ await replyCurrentSetting(ctx, "Current network access", [textField("network", session.networkAccessEnabled ? "on" : "off")], "Usage: /network on|off");
154
169
  return;
155
170
  }
156
171
  if (value !== "on" && value !== "off") {
157
- await ctx.reply("Usage: /network on|off");
172
+ await replyUsage(ctx, "/network on|off");
158
173
  return;
159
174
  }
160
175
  store.setNetworkAccessEnabled(session.sessionKey, value === "on");
161
- await ctx.reply(`Set network access: ${value}`);
162
- });
163
- bot.command("gitcheck", async (ctx) => {
164
- const session = getScopedSession(ctx, store, projects, config);
176
+ await replyNotice(ctx, `Set network access: ${value}`);
177
+ }));
178
+ bot.command("gitcheck", wrapUserFacingHandler("gitcheck", logger, async (ctx) => {
179
+ const session = await requireScopedSession(ctx, store, projects, config);
165
180
  if (!session)
166
181
  return;
167
182
  const value = ctx.match.trim().toLowerCase();
168
183
  if (!value) {
169
- await ctx.reply(`Current git repo check: ${session.skipGitRepoCheck ? "skip" : "enforce"}\nUsage: /gitcheck skip|enforce`);
184
+ await replyCurrentSetting(ctx, "Current git repo check", [textField("git check", session.skipGitRepoCheck ? "skip" : "enforce")], "Usage: /gitcheck skip|enforce");
170
185
  return;
171
186
  }
172
187
  if (value !== "skip" && value !== "enforce") {
173
- await ctx.reply("Usage: /gitcheck skip|enforce");
188
+ await replyUsage(ctx, "/gitcheck skip|enforce");
174
189
  return;
175
190
  }
176
191
  store.setSkipGitRepoCheck(session.sessionKey, value === "skip");
177
- await ctx.reply(`Set git repo check: ${value}`);
178
- });
179
- bot.command("adddir", async (ctx) => {
192
+ await replyNotice(ctx, `Set git repo check: ${value}`);
193
+ }));
194
+ }
195
+ function registerAdvancedHandlers(deps) {
196
+ const { bot, config, store, projects, codex, logger } = deps;
197
+ bot.command("adddir", wrapUserFacingHandler("adddir", logger, async (ctx) => {
180
198
  const project = getProjectForContext(ctx, projects);
181
- const session = getScopedSession(ctx, store, projects, config);
199
+ const session = await requireScopedSession(ctx, store, projects, config);
182
200
  if (!project || !session)
183
201
  return;
184
202
  const [command, ...rest] = ctx.match.trim().split(/\s+/).filter(Boolean);
185
203
  const args = rest.join(" ");
186
204
  if (!command || command === "list") {
187
- await ctx.reply(session.additionalDirectories.length === 0
188
- ? "additional directories: none\nUsage: /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear"
189
- : [
190
- "additional directories:",
191
- ...session.additionalDirectories.map((directory, index) => `${index + 1}. ${directory}`),
192
- "Usage: /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear",
193
- ].join("\n"));
205
+ await replyDocument(ctx, {
206
+ title: "Additional directories",
207
+ ...(session.additionalDirectories.length > 0
208
+ ? {
209
+ sections: [
210
+ {
211
+ title: "Directories",
212
+ fields: session.additionalDirectories.map((directory, index) => codeField(String(index + 1), directory)),
213
+ },
214
+ ],
215
+ }
216
+ : {
217
+ fields: [textField("directories", "none")],
218
+ }),
219
+ footer: "Usage: /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear",
220
+ });
194
221
  return;
195
222
  }
196
223
  if (command === "add") {
197
224
  if (!args) {
198
- await ctx.reply("Usage: /adddir add <path-inside-project>");
225
+ await replyUsage(ctx, "/adddir add <path-inside-project>");
199
226
  return;
200
227
  }
201
228
  try {
202
229
  const directory = assertProjectScopedPath(args, project.cwd);
203
230
  const next = [...session.additionalDirectories.filter((entry) => entry !== directory), directory];
204
231
  store.setAdditionalDirectories(session.sessionKey, next);
205
- await ctx.reply(`Added additional directory:\n${directory}`);
232
+ await replyDocument(ctx, {
233
+ title: "Added additional directory",
234
+ fields: [codeField("directory", directory)],
235
+ });
206
236
  }
207
237
  catch (error) {
208
- await ctx.reply(error instanceof Error ? error.message : String(error));
238
+ await replyError(ctx, error instanceof Error ? error.message : String(error));
209
239
  }
210
240
  return;
211
241
  }
212
242
  if (command === "add-external") {
213
243
  if (!args) {
214
- await ctx.reply("Usage: /adddir add-external <absolute-path>");
244
+ await replyUsage(ctx, "/adddir add-external <absolute-path>");
215
245
  return;
216
246
  }
217
247
  try {
218
248
  const directory = resolveExistingDirectory(args);
219
249
  const next = [...session.additionalDirectories.filter((entry) => entry !== directory), directory];
220
250
  store.setAdditionalDirectories(session.sessionKey, next);
221
- await ctx.reply([
222
- "Added external additional directory outside the project root.",
223
- directory,
224
- "Codex can now read files there during future runs.",
225
- ].join("\n"));
251
+ await replyDocument(ctx, {
252
+ title: "Added external additional directory outside the project root.",
253
+ fields: [codeField("directory", directory)],
254
+ footer: "Codex can now read files there during future runs.",
255
+ });
226
256
  }
227
257
  catch (error) {
228
- await ctx.reply(error instanceof Error ? error.message : String(error));
258
+ await replyError(ctx, error instanceof Error ? error.message : String(error));
229
259
  }
230
260
  return;
231
261
  }
232
262
  if (command === "drop") {
233
263
  const index = Number(args);
234
264
  if (!Number.isInteger(index) || index <= 0 || index > session.additionalDirectories.length) {
235
- await ctx.reply("Usage: /adddir drop <index>");
265
+ await replyUsage(ctx, "/adddir drop <index>");
236
266
  return;
237
267
  }
238
268
  const next = session.additionalDirectories.filter((_entry, entryIndex) => entryIndex !== index - 1);
239
269
  store.setAdditionalDirectories(session.sessionKey, next);
240
- await ctx.reply(`Removed additional directory #${index}.`);
270
+ await replyNotice(ctx, `Removed additional directory #${index}.`);
241
271
  return;
242
272
  }
243
273
  if (command === "clear") {
244
274
  store.setAdditionalDirectories(session.sessionKey, []);
245
- await ctx.reply("Cleared additional directories.");
275
+ await replyNotice(ctx, "Cleared additional directories.");
246
276
  return;
247
277
  }
248
- await ctx.reply("Usage: /adddir list | /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear");
249
- });
250
- bot.command("schema", async (ctx) => {
251
- const session = getScopedSession(ctx, store, projects, config);
278
+ await replyUsage(ctx, "/adddir list | /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear");
279
+ }));
280
+ bot.command("schema", wrapUserFacingHandler("schema", logger, async (ctx) => {
281
+ const session = await requireScopedSession(ctx, store, projects, config);
252
282
  if (!session)
253
283
  return;
254
284
  const raw = ctx.match.trim();
255
285
  if (!raw || raw === "show") {
256
- await ctx.reply(session.outputSchema ? `Current output schema:\n${session.outputSchema}` : "Current output schema: none\nUsage: /schema set <JSON object> | /schema clear");
286
+ await replyDocument(ctx, {
287
+ title: "Current output schema",
288
+ fields: session.outputSchema
289
+ ? [codeField("schema", session.outputSchema)]
290
+ : [textField("schema", "none")],
291
+ footer: "Usage: /schema set <JSON object> | /schema clear",
292
+ });
257
293
  return;
258
294
  }
259
295
  if (raw === "clear") {
260
296
  store.setOutputSchema(session.sessionKey, null);
261
- await ctx.reply("Cleared output schema.");
297
+ await replyNotice(ctx, "Cleared output schema.");
262
298
  return;
263
299
  }
264
300
  if (!raw.startsWith("set ")) {
265
- await ctx.reply("Usage: /schema show | /schema set <JSON object> | /schema clear");
301
+ await replyUsage(ctx, "/schema show | /schema set <JSON object> | /schema clear");
266
302
  return;
267
303
  }
268
304
  try {
269
305
  const parsed = JSON.parse(raw.slice(4).trim());
270
306
  if (!isPlainObject(parsed)) {
271
- await ctx.reply("Output schema must be a JSON object.");
307
+ await replyError(ctx, "Output schema must be a JSON object.");
272
308
  return;
273
309
  }
274
310
  const normalized = JSON.stringify(parsed);
275
311
  store.setOutputSchema(session.sessionKey, normalized);
276
- await ctx.reply(`Set output schema:\n${normalized}`);
312
+ await replyDocument(ctx, {
313
+ title: "Set output schema",
314
+ fields: [codeField("schema", normalized)],
315
+ });
277
316
  }
278
317
  catch (error) {
279
- await ctx.reply(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`);
318
+ await replyError(ctx, `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`);
280
319
  }
281
- });
282
- bot.command("codexconfig", async (ctx) => {
320
+ }));
321
+ bot.command("codexconfig", wrapUserFacingHandler("codexconfig", logger, async (ctx) => {
283
322
  const raw = ctx.match.trim();
284
323
  if (!raw || raw === "show") {
285
324
  const current = store.getAppState("codex_config_overrides");
286
- await ctx.reply(current ? `Current Codex config overrides:\n${current}` : "Current Codex config overrides: none\nUsage: /codexconfig set <JSON object> | /codexconfig clear");
325
+ await replyDocument(ctx, {
326
+ title: "Current Codex config overrides",
327
+ fields: current
328
+ ? [codeField("config", current)]
329
+ : [textField("config", "none")],
330
+ footer: "Usage: /codexconfig set <JSON object> | /codexconfig clear",
331
+ });
287
332
  return;
288
333
  }
289
334
  if (raw === "clear") {
290
335
  store.deleteAppState("codex_config_overrides");
291
336
  codex.setConfigOverrides(undefined);
292
- await ctx.reply("Cleared Codex config overrides. They will apply to future runs.");
337
+ await replyNotice(ctx, "Cleared Codex config overrides. They will apply to future runs.");
293
338
  return;
294
339
  }
295
340
  if (!raw.startsWith("set ")) {
296
- await ctx.reply("Usage: /codexconfig show | /codexconfig set <JSON object> | /codexconfig clear");
341
+ await replyUsage(ctx, "/codexconfig show | /codexconfig set <JSON object> | /codexconfig clear");
297
342
  return;
298
343
  }
299
344
  try {
@@ -301,40 +346,26 @@ export function registerSessionConfigHandlers(deps) {
301
346
  const serialized = JSON.stringify(configOverrides);
302
347
  store.setAppState("codex_config_overrides", serialized);
303
348
  codex.setConfigOverrides(configOverrides);
304
- await ctx.reply(`Set Codex config overrides. They will apply to future runs.\n${serialized}`);
349
+ await replyDocument(ctx, {
350
+ title: "Set Codex config overrides. They will apply to future runs.",
351
+ fields: [codeField("config", serialized)],
352
+ });
305
353
  }
306
354
  catch (error) {
307
- await ctx.reply(error instanceof Error ? error.message : String(error));
355
+ await replyError(ctx, error instanceof Error ? error.message : String(error));
308
356
  }
309
- });
357
+ }));
310
358
  }
311
359
  function isPlainObject(value) {
312
360
  return typeof value === "object" && value !== null && !Array.isArray(value);
313
361
  }
314
- function parseCodexConfigOverrides(raw) {
315
- const parsed = JSON.parse(raw);
316
- if (!isPlainObject(parsed)) {
317
- throw new Error("Codex config overrides must be a JSON object.");
318
- }
319
- assertCodexConfigValue(parsed, "config");
320
- return parsed;
362
+ async function replyCurrentSetting(ctx, title, fields, footer) {
363
+ await replyDocument(ctx, {
364
+ title,
365
+ fields,
366
+ ...(footer ? { footer } : {}),
367
+ });
321
368
  }
322
- function assertCodexConfigValue(value, path) {
323
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
324
- return;
325
- if (Array.isArray(value)) {
326
- for (let index = 0; index < value.length; index += 1) {
327
- assertCodexConfigValue(value[index], `${path}[${index}]`);
328
- }
329
- return;
330
- }
331
- if (isPlainObject(value)) {
332
- for (const [key, child] of Object.entries(value)) {
333
- if (!key)
334
- throw new Error("Codex config override key cannot be empty.");
335
- assertCodexConfigValue(child, `${path}.${key}`);
336
- }
337
- return;
338
- }
339
- throw new Error(`${path} may only contain string, number, boolean, array, or object values.`);
369
+ async function replyInvalidValue(ctx, message, usage) {
370
+ await replyError(ctx, message, usage);
340
371
  }
@@ -1,5 +1,5 @@
1
1
  import { applySessionRuntimeEvent } from "../runtime/sessionRuntime.js";
2
- import { sendPlainChunks } from "../telegram/delivery.js";
2
+ import { sendReplyNotice } from "../telegram/formatted.js";
3
3
  import { isAbortError } from "../codex/sdkRuntime.js";
4
4
  import { numericChatId, numericMessageThreadId } from "./session.js";
5
5
  import { describeBusyStatus, formatIsoTimestamp, isSessionBusy, sessionBufferKey, sessionLogFields, truncateSingleLine, } from "./sessionFlow.js";
@@ -28,16 +28,15 @@ export async function handleUserInput(input) {
28
28
  }
29
29
  const queued = store.enqueueInput(session.sessionKey, prompt);
30
30
  const queueDepth = store.getQueuedInputCount(session.sessionKey);
31
- await sendPlainChunks(bot, {
31
+ await sendReplyNotice(bot, {
32
32
  chatId: numericChatId(session),
33
33
  messageThreadId: numericMessageThreadId(session),
34
- text: [
35
- `The current Codex task is still ${describeBusyStatus(session.runtimeStatus)}. Your message was added to the queue.`,
36
- `queue position: ${queueDepth}`,
37
- `queued at: ${formatIsoTimestamp(queued.createdAt)}`,
38
- "It will be processed automatically after the current run finishes.",
39
- ].join("\n"),
40
- }, logger);
34
+ }, [
35
+ `The current Codex task is still ${describeBusyStatus(session.runtimeStatus)}. Your message was added to the queue.`,
36
+ `queue position: ${queueDepth}`,
37
+ `queued at: ${formatIsoTimestamp(queued.createdAt)}`,
38
+ "It will be processed automatically after the current run finishes.",
39
+ ], logger);
41
40
  return {
42
41
  status: "queued",
43
42
  consumed: true,
@@ -138,11 +137,10 @@ export async function recoverActiveTopicSessions(store, codex, _buffers, bot, lo
138
137
  });
139
138
  for (const session of sessions) {
140
139
  const refreshed = await refreshSessionIfActiveTurnIsStale(session, store, codex, bot, logger);
141
- await sendPlainChunks(bot, {
140
+ await sendReplyNotice(bot, {
142
141
  chatId: numericChatId(refreshed),
143
142
  messageThreadId: numericMessageThreadId(refreshed),
144
- text: "telecodex restarted and cannot resume the previous streamed run state. Send the message again if you want to continue.",
145
- }, logger).catch((error) => {
143
+ }, "telecodex restarted and cannot resume the previous streamed run state. Send the message again if you want to continue.", logger).catch((error) => {
146
144
  logger?.warn("failed to notify session about stale sdk recovery", {
147
145
  ...sessionLogFields(refreshed),
148
146
  error,
@@ -199,7 +197,7 @@ async function runSessionPrompt(input) {
199
197
  networkAccessEnabled: session.networkAccessEnabled,
200
198
  skipGitRepoCheck: session.skipGitRepoCheck,
201
199
  additionalDirectories: session.additionalDirectories,
202
- outputSchema: parseOutputSchema(session.outputSchema),
200
+ outputSchema: readOutputSchema(session, store, logger),
203
201
  },
204
202
  prompt: toSdkInput(prompt),
205
203
  callbacks: {
@@ -368,5 +366,23 @@ function toSdkInput(input) {
368
366
  function parseOutputSchema(value) {
369
367
  if (!value)
370
368
  return undefined;
371
- return JSON.parse(value);
369
+ try {
370
+ return JSON.parse(value);
371
+ }
372
+ catch (error) {
373
+ throw new Error(`Invalid stored output schema: ${error instanceof Error ? error.message : String(error)}`);
374
+ }
375
+ }
376
+ function readOutputSchema(session, store, logger) {
377
+ try {
378
+ return parseOutputSchema(session.outputSchema);
379
+ }
380
+ catch (error) {
381
+ store.setOutputSchema(session.sessionKey, null);
382
+ logger?.warn("cleared invalid stored output schema", {
383
+ ...sessionLogFields(session),
384
+ error,
385
+ });
386
+ return undefined;
387
+ }
372
388
  }