telecodex 0.1.1 → 0.1.3

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.
@@ -0,0 +1,160 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { DEFAULT_SESSION_PROFILE, isSessionApprovalPolicy, isSessionReasoningEffort, isSessionSandboxMode, isSessionWebSearchMode, } from "../config.js";
4
+ const LEGACY_IMPORT_MARKER_KEY = "__telecodex_legacy_sqlite_import_completed_at";
5
+ const LEGACY_SQLITE_ARTIFACT_SUFFIXES = ["", "-shm", "-wal", "-journal"];
6
+ export function migrateLegacySqliteState(input) {
7
+ if (input.storage.getAppState(LEGACY_IMPORT_MARKER_KEY) != null) {
8
+ cleanupLegacySqliteArtifacts(input.legacyDbPath);
9
+ return { imported: false };
10
+ }
11
+ if (!existsSync(input.legacyDbPath)) {
12
+ return { imported: false };
13
+ }
14
+ let imported = false;
15
+ const db = new DatabaseSync(input.legacyDbPath);
16
+ try {
17
+ const appState = hasTable(db, "app_state") ? readAppState(db) : {};
18
+ const projects = hasTable(db, "projects") ? readProjects(db) : [];
19
+ const sessions = hasTable(db, "sessions") ? readSessions(db) : [];
20
+ input.storage.mergeImportedAppState(appState);
21
+ input.storage.mergeImportedProjects(projects);
22
+ input.storage.mergeImportedSessions(sessions);
23
+ input.storage.setAppState(LEGACY_IMPORT_MARKER_KEY, new Date().toISOString());
24
+ imported = true;
25
+ }
26
+ finally {
27
+ db.close();
28
+ }
29
+ cleanupLegacySqliteArtifacts(input.legacyDbPath);
30
+ return { imported };
31
+ }
32
+ function readAppState(db) {
33
+ const rows = db.prepare("SELECT key, value FROM app_state").all();
34
+ const values = {};
35
+ for (const row of rows) {
36
+ if (typeof row.key !== "string" || typeof row.value !== "string")
37
+ continue;
38
+ values[row.key] = row.value;
39
+ }
40
+ return values;
41
+ }
42
+ function readProjects(db) {
43
+ const rows = db.prepare("SELECT * FROM projects").all();
44
+ const projects = [];
45
+ for (const row of rows) {
46
+ if (typeof row.chat_id !== "string" || typeof row.cwd !== "string")
47
+ continue;
48
+ const cwd = row.cwd.trim();
49
+ if (!cwd)
50
+ continue;
51
+ const now = new Date().toISOString();
52
+ projects.push({
53
+ chatId: row.chat_id,
54
+ name: typeof row.name === "string" && row.name.trim() ? row.name : cwd,
55
+ cwd,
56
+ createdAt: typeof row.created_at === "string" ? row.created_at : now,
57
+ updatedAt: typeof row.updated_at === "string" ? row.updated_at : now,
58
+ });
59
+ }
60
+ return projects;
61
+ }
62
+ function readSessions(db) {
63
+ const rows = db.prepare("SELECT * FROM sessions").all();
64
+ const sessions = [];
65
+ for (const row of rows) {
66
+ if (typeof row.session_key !== "string" ||
67
+ typeof row.chat_id !== "string" ||
68
+ typeof row.cwd !== "string" ||
69
+ typeof row.model !== "string") {
70
+ continue;
71
+ }
72
+ const now = new Date().toISOString();
73
+ sessions.push({
74
+ sessionKey: row.session_key,
75
+ chatId: row.chat_id,
76
+ messageThreadId: typeof row.message_thread_id === "string" ? row.message_thread_id : null,
77
+ telegramTopicName: typeof row.telegram_topic_name === "string" ? row.telegram_topic_name : null,
78
+ codexThreadId: typeof row.codex_thread_id === "string" ? row.codex_thread_id : null,
79
+ cwd: row.cwd,
80
+ model: row.model,
81
+ sandboxMode: normalizeSandboxMode(row.sandbox_mode, row.mode),
82
+ approvalPolicy: normalizeApprovalPolicy(row.approval_policy),
83
+ reasoningEffort: normalizeReasoningEffort(row.reasoning_effort),
84
+ webSearchMode: normalizeWebSearchMode(row.web_search_mode),
85
+ networkAccessEnabled: normalizeBoolean(row.network_access_enabled, true),
86
+ skipGitRepoCheck: normalizeBoolean(row.skip_git_repo_check, true),
87
+ additionalDirectories: normalizeStringArray(row.additional_directories),
88
+ outputSchema: normalizeOutputSchema(row.output_schema),
89
+ createdAt: typeof row.created_at === "string" ? row.created_at : now,
90
+ updatedAt: typeof row.updated_at === "string" ? row.updated_at : now,
91
+ });
92
+ }
93
+ return sessions;
94
+ }
95
+ function hasTable(db, tableName) {
96
+ const row = db
97
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1")
98
+ .get(tableName);
99
+ return typeof row?.name === "string";
100
+ }
101
+ function normalizeSandboxMode(current, legacyMode) {
102
+ if (typeof current === "string" && isSessionSandboxMode(current))
103
+ return current;
104
+ if (legacyMode === "write")
105
+ return "workspace-write";
106
+ return DEFAULT_SESSION_PROFILE.sandboxMode;
107
+ }
108
+ function normalizeApprovalPolicy(value) {
109
+ return typeof value === "string" && isSessionApprovalPolicy(value) ? value : DEFAULT_SESSION_PROFILE.approvalPolicy;
110
+ }
111
+ function normalizeReasoningEffort(value) {
112
+ return typeof value === "string" && isSessionReasoningEffort(value) ? value : null;
113
+ }
114
+ function normalizeWebSearchMode(value) {
115
+ return typeof value === "string" && isSessionWebSearchMode(value) ? value : null;
116
+ }
117
+ function normalizeBoolean(value, fallback) {
118
+ if (typeof value === "boolean")
119
+ return value;
120
+ if (typeof value === "number" || typeof value === "bigint")
121
+ return Number(value) !== 0;
122
+ return fallback;
123
+ }
124
+ function normalizeStringArray(value) {
125
+ if (typeof value !== "string" || !value.trim())
126
+ return [];
127
+ try {
128
+ const parsed = JSON.parse(value);
129
+ return Array.isArray(parsed)
130
+ ? parsed.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
131
+ : [];
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ }
137
+ function normalizeOutputSchema(value) {
138
+ if (typeof value !== "string" || !value.trim())
139
+ return null;
140
+ try {
141
+ const parsed = JSON.parse(value);
142
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? JSON.stringify(parsed) : null;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ function cleanupLegacySqliteArtifacts(legacyDbPath) {
149
+ for (const suffix of LEGACY_SQLITE_ARTIFACT_SUFFIXES) {
150
+ const filePath = `${legacyDbPath}${suffix}`;
151
+ if (!existsSync(filePath))
152
+ continue;
153
+ try {
154
+ rmSync(filePath, { force: true });
155
+ }
156
+ catch {
157
+ // Retry on the next startup; legacy files are no longer a source of truth.
158
+ }
159
+ }
160
+ }
@@ -1,47 +1,25 @@
1
1
  import path from "node:path";
2
2
  export class ProjectStore {
3
- db;
4
- constructor(db) {
5
- this.db = db;
3
+ storage;
4
+ constructor(storage) {
5
+ this.storage = storage;
6
6
  }
7
7
  get(chatId) {
8
- const row = this.db.prepare("SELECT * FROM projects WHERE chat_id = ?").get(chatId);
9
- return row ? mapRow(row) : null;
8
+ return this.storage.getProject(chatId);
10
9
  }
11
10
  upsert(input) {
12
- const now = new Date().toISOString();
13
11
  const cwd = path.resolve(input.cwd);
14
12
  const name = input.name?.trim() || path.basename(cwd) || cwd;
15
- this.db
16
- .prepare(`INSERT INTO projects (chat_id, name, cwd, created_at, updated_at)
17
- VALUES (?, ?, ?, ?, ?)
18
- ON CONFLICT(chat_id) DO UPDATE SET
19
- name = excluded.name,
20
- cwd = excluded.cwd,
21
- updated_at = excluded.updated_at`)
22
- .run(input.chatId, name, cwd, now, now);
23
- const project = this.get(input.chatId);
24
- if (!project) {
25
- throw new Error("Project upsert failed");
26
- }
27
- return project;
13
+ return this.storage.upsertProject({
14
+ chatId: input.chatId,
15
+ cwd,
16
+ name,
17
+ });
28
18
  }
29
19
  remove(chatId) {
30
- this.db.prepare("DELETE FROM projects WHERE chat_id = ?").run(chatId);
20
+ this.storage.removeProject(chatId);
31
21
  }
32
22
  list() {
33
- const rows = this.db
34
- .prepare("SELECT * FROM projects ORDER BY updated_at DESC")
35
- .all();
36
- return rows.map(mapRow);
23
+ return this.storage.listProjects();
37
24
  }
38
25
  }
39
- function mapRow(row) {
40
- return {
41
- chatId: row.chat_id,
42
- name: row.name,
43
- cwd: row.cwd,
44
- createdAt: row.created_at,
45
- updatedAt: row.updated_at,
46
- };
47
- }
@@ -1,24 +1,23 @@
1
- import { DEFAULT_SESSION_PROFILE, isSessionApprovalPolicy, isSessionReasoningEffort, isSessionSandboxMode, isSessionWebSearchMode, } from "../config.js";
1
+ import { DEFAULT_SESSION_PROFILE, } from "../config.js";
2
2
  export const BINDING_CODE_TTL_MS = 15 * 60 * 1000;
3
3
  export const BINDING_CODE_MAX_ATTEMPTS = 5;
4
4
  export class SessionStore {
5
- db;
6
- constructor(db) {
7
- this.db = db;
5
+ storage;
6
+ runtimeStateBySession = new Map();
7
+ outputMessageBySession = new Map();
8
+ queueBySession = new Map();
9
+ nextQueuedInputId = 1;
10
+ constructor(storage) {
11
+ this.storage = storage;
8
12
  }
9
13
  getAppState(key) {
10
- const row = this.db.prepare("SELECT value FROM app_state WHERE key = ?").get(key);
11
- return row?.value ?? null;
14
+ return this.storage.getAppState(key);
12
15
  }
13
16
  setAppState(key, value) {
14
- this.db
15
- .prepare(`INSERT INTO app_state (key, value, updated_at)
16
- VALUES (?, ?, ?)
17
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
18
- .run(key, value, new Date().toISOString());
17
+ this.storage.setAppState(key, value);
19
18
  }
20
19
  deleteAppState(key) {
21
- this.db.prepare("DELETE FROM app_state WHERE key = ?").run(key);
20
+ this.storage.deleteAppState(key);
22
21
  }
23
22
  getAuthorizedUserId() {
24
23
  const value = this.getAppState("authorized_user_id");
@@ -123,11 +122,11 @@ export class SessionStore {
123
122
  const existing = this.getAuthorizedUserId();
124
123
  if (existing != null)
125
124
  return existing;
126
- this.db.prepare("INSERT OR IGNORE INTO app_state (key, value, updated_at) VALUES ('authorized_user_id', ?, ?)").run(String(userId), new Date().toISOString());
125
+ this.setAppState("authorized_user_id", String(userId));
126
+ this.clearBindingCode();
127
127
  const current = this.getAuthorizedUserId();
128
128
  if (current == null)
129
129
  throw new Error("Failed to persist authorized Telegram user id");
130
- this.clearBindingCode();
131
130
  return current;
132
131
  }
133
132
  rebindAuthorizedUserId(userId) {
@@ -143,237 +142,201 @@ export class SessionStore {
143
142
  if (existing)
144
143
  return existing;
145
144
  const now = new Date().toISOString();
146
- this.db
147
- .prepare(`INSERT INTO sessions (
148
- session_key, chat_id, message_thread_id, telegram_topic_name, cwd, model, sandbox_mode, approval_policy, created_at, updated_at
149
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
150
- .run(input.sessionKey, input.chatId, input.messageThreadId, input.telegramTopicName ?? null, input.defaultCwd, input.defaultModel, DEFAULT_SESSION_PROFILE.sandboxMode, DEFAULT_SESSION_PROFILE.approvalPolicy, now, now);
145
+ this.storage.putSession({
146
+ sessionKey: input.sessionKey,
147
+ chatId: input.chatId,
148
+ messageThreadId: input.messageThreadId,
149
+ telegramTopicName: input.telegramTopicName ?? null,
150
+ codexThreadId: null,
151
+ cwd: input.defaultCwd,
152
+ model: input.defaultModel,
153
+ sandboxMode: DEFAULT_SESSION_PROFILE.sandboxMode,
154
+ approvalPolicy: DEFAULT_SESSION_PROFILE.approvalPolicy,
155
+ reasoningEffort: null,
156
+ webSearchMode: null,
157
+ networkAccessEnabled: true,
158
+ skipGitRepoCheck: true,
159
+ additionalDirectories: [],
160
+ outputSchema: null,
161
+ createdAt: now,
162
+ updatedAt: now,
163
+ });
151
164
  const created = this.get(input.sessionKey);
152
165
  if (!created)
153
166
  throw new Error("Session insert failed");
154
167
  return created;
155
168
  }
156
169
  get(sessionKey) {
157
- const row = this.db
158
- .prepare("SELECT * FROM sessions WHERE session_key = ?")
159
- .get(sessionKey);
160
- return row ? mapSessionRow(row) : null;
170
+ const stored = this.storage.getSession(sessionKey);
171
+ return mapStoredSession(stored, this.runtimeStateBySession.get(sessionKey), this.outputMessageBySession.get(sessionKey));
161
172
  }
162
173
  getByThreadId(threadId) {
163
- const row = this.db
164
- .prepare("SELECT * FROM sessions WHERE codex_thread_id = ? LIMIT 1")
165
- .get(threadId);
166
- return row ? mapSessionRow(row) : null;
174
+ const stored = this.storage.getSessionByThreadId(threadId);
175
+ return stored ? mapStoredSession(stored, this.runtimeStateBySession.get(stored.sessionKey), this.outputMessageBySession.get(stored.sessionKey)) : null;
167
176
  }
168
177
  listTopicSessions() {
169
- const rows = this.db
170
- .prepare("SELECT * FROM sessions WHERE message_thread_id IS NOT NULL ORDER BY session_key ASC")
171
- .all();
172
- return rows.map(mapSessionRow);
178
+ return this.storage
179
+ .listSessions()
180
+ .filter((session) => session.messageThreadId != null)
181
+ .map((session) => mapStoredSession(session, this.runtimeStateBySession.get(session.sessionKey), this.outputMessageBySession.get(session.sessionKey)))
182
+ .filter((session) => session != null);
173
183
  }
174
184
  remove(sessionKey) {
175
- this.db.prepare("DELETE FROM queued_inputs WHERE session_key = ?").run(sessionKey);
176
- this.db.prepare("DELETE FROM sessions WHERE session_key = ?").run(sessionKey);
185
+ this.queueBySession.delete(sessionKey);
186
+ this.runtimeStateBySession.delete(sessionKey);
187
+ this.outputMessageBySession.delete(sessionKey);
188
+ this.storage.removeSession(sessionKey);
177
189
  }
178
190
  enqueueInput(sessionKey, input) {
179
191
  const now = new Date().toISOString();
180
- const text = formatCodexInputPreview(input);
181
- const result = this.db
182
- .prepare(`INSERT INTO queued_inputs (session_key, text, input_json, created_at, updated_at)
183
- VALUES (?, ?, ?, ?, ?)`)
184
- .run(sessionKey, text, JSON.stringify(input), now, now);
185
- const id = Number(result.lastInsertRowid);
186
- const queued = this.getQueuedInput(id);
187
- if (!queued)
188
- throw new Error("Queued input insert failed");
189
- return queued;
192
+ const queued = {
193
+ id: this.nextQueuedInputId,
194
+ sessionKey,
195
+ text: formatCodexInputPreview(input),
196
+ input: cloneStoredCodexInput(input),
197
+ createdAt: now,
198
+ updatedAt: now,
199
+ };
200
+ this.nextQueuedInputId += 1;
201
+ const queue = this.queueBySession.get(sessionKey) ?? [];
202
+ queue.push(queued);
203
+ this.queueBySession.set(sessionKey, queue);
204
+ return cloneQueuedInput(queued);
190
205
  }
191
206
  getQueuedInput(id) {
192
- const row = this.db.prepare("SELECT * FROM queued_inputs WHERE id = ?").get(id);
193
- return row ? mapQueuedInputRow(row) : null;
207
+ for (const queue of this.queueBySession.values()) {
208
+ const match = queue.find((item) => item.id === id);
209
+ if (match)
210
+ return cloneQueuedInput(match);
211
+ }
212
+ return null;
194
213
  }
195
214
  getQueuedInputCount(sessionKey) {
196
- const row = this.db
197
- .prepare("SELECT COUNT(*) AS count FROM queued_inputs WHERE session_key = ?")
198
- .get(sessionKey);
199
- return row?.count ?? 0;
215
+ return this.queueBySession.get(sessionKey)?.length ?? 0;
200
216
  }
201
217
  peekNextQueuedInput(sessionKey) {
202
- const row = this.db
203
- .prepare("SELECT * FROM queued_inputs WHERE session_key = ? ORDER BY id ASC LIMIT 1")
204
- .get(sessionKey);
205
- return row ? mapQueuedInputRow(row) : null;
218
+ const queue = this.queueBySession.get(sessionKey);
219
+ const [next] = queue ?? [];
220
+ return next ? cloneQueuedInput(next) : null;
206
221
  }
207
222
  listQueuedInputs(sessionKey, limit = 5) {
208
- const rows = this.db
209
- .prepare("SELECT * FROM queued_inputs WHERE session_key = ? ORDER BY id ASC LIMIT ?")
210
- .all(sessionKey, limit);
211
- return rows.map(mapQueuedInputRow);
223
+ const queue = this.queueBySession.get(sessionKey) ?? [];
224
+ return queue.slice(0, limit).map(cloneQueuedInput);
212
225
  }
213
226
  removeQueuedInput(id) {
214
- this.db.prepare("DELETE FROM queued_inputs WHERE id = ?").run(id);
227
+ for (const [sessionKey, queue] of this.queueBySession.entries()) {
228
+ const index = queue.findIndex((item) => item.id === id);
229
+ if (index < 0)
230
+ continue;
231
+ queue.splice(index, 1);
232
+ if (queue.length === 0) {
233
+ this.queueBySession.delete(sessionKey);
234
+ }
235
+ return;
236
+ }
215
237
  }
216
238
  removeQueuedInputForSession(sessionKey, id) {
217
- const result = this.db
218
- .prepare("DELETE FROM queued_inputs WHERE session_key = ? AND id = ?")
219
- .run(sessionKey, id);
220
- return (result.changes ?? 0) > 0;
239
+ const queue = this.queueBySession.get(sessionKey);
240
+ if (!queue)
241
+ return false;
242
+ const index = queue.findIndex((item) => item.id === id);
243
+ if (index < 0)
244
+ return false;
245
+ queue.splice(index, 1);
246
+ if (queue.length === 0) {
247
+ this.queueBySession.delete(sessionKey);
248
+ }
249
+ return true;
221
250
  }
222
251
  clearQueuedInputs(sessionKey) {
223
- const result = this.db.prepare("DELETE FROM queued_inputs WHERE session_key = ?").run(sessionKey);
224
- return result.changes ?? 0;
252
+ const queue = this.queueBySession.get(sessionKey) ?? [];
253
+ this.queueBySession.delete(sessionKey);
254
+ return queue.length;
225
255
  }
226
256
  bindThread(sessionKey, threadId) {
227
- this.patch(sessionKey, {
228
- codex_thread_id: threadId,
257
+ this.patchDurableSession(sessionKey, {
258
+ codexThreadId: threadId,
229
259
  });
230
260
  }
231
261
  setTelegramTopicName(sessionKey, topicName) {
232
- this.patch(sessionKey, { telegram_topic_name: topicName });
262
+ this.patchDurableSession(sessionKey, {
263
+ telegramTopicName: topicName,
264
+ });
233
265
  }
234
266
  setRuntimeState(sessionKey, state) {
235
- this.patch(sessionKey, {
236
- runtime_status: state.status,
237
- runtime_status_detail: state.detail,
238
- runtime_status_updated_at: state.updatedAt,
239
- active_turn_id: state.activeTurnId,
240
- });
267
+ this.runtimeStateBySession.set(sessionKey, state);
241
268
  }
242
269
  setOutputMessage(sessionKey, messageId) {
243
- this.patch(sessionKey, { output_message_id: messageId });
270
+ if (messageId == null) {
271
+ this.outputMessageBySession.delete(sessionKey);
272
+ return;
273
+ }
274
+ this.outputMessageBySession.set(sessionKey, messageId);
244
275
  }
245
276
  setCwd(sessionKey, cwd) {
246
- this.patch(sessionKey, { cwd });
277
+ this.patchDurableSession(sessionKey, { cwd });
247
278
  }
248
279
  setModel(sessionKey, model) {
249
- this.patch(sessionKey, { model });
280
+ this.patchDurableSession(sessionKey, { model });
250
281
  }
251
282
  setSandboxMode(sessionKey, sandboxMode) {
252
- this.patch(sessionKey, {
253
- sandbox_mode: sandboxMode,
254
- });
283
+ this.patchDurableSession(sessionKey, { sandboxMode });
255
284
  }
256
285
  setApprovalPolicy(sessionKey, approvalPolicy) {
257
- this.patch(sessionKey, { approval_policy: approvalPolicy });
286
+ this.patchDurableSession(sessionKey, { approvalPolicy });
258
287
  }
259
288
  setReasoningEffort(sessionKey, reasoningEffort) {
260
- this.patch(sessionKey, { reasoning_effort: reasoningEffort });
289
+ this.patchDurableSession(sessionKey, { reasoningEffort });
261
290
  }
262
291
  setWebSearchMode(sessionKey, webSearchMode) {
263
- this.patch(sessionKey, { web_search_mode: webSearchMode });
292
+ this.patchDurableSession(sessionKey, { webSearchMode });
264
293
  }
265
294
  setNetworkAccessEnabled(sessionKey, enabled) {
266
- this.patch(sessionKey, { network_access_enabled: enabled ? 1 : 0 });
295
+ this.patchDurableSession(sessionKey, { networkAccessEnabled: enabled });
267
296
  }
268
297
  setSkipGitRepoCheck(sessionKey, skip) {
269
- this.patch(sessionKey, { skip_git_repo_check: skip ? 1 : 0 });
298
+ this.patchDurableSession(sessionKey, { skipGitRepoCheck: skip });
270
299
  }
271
300
  setAdditionalDirectories(sessionKey, directories) {
272
- this.patch(sessionKey, { additional_directories: JSON.stringify(directories) });
301
+ this.patchDurableSession(sessionKey, { additionalDirectories: [...directories] });
273
302
  }
274
303
  setOutputSchema(sessionKey, outputSchema) {
275
- this.patch(sessionKey, { output_schema: outputSchema });
304
+ this.patchDurableSession(sessionKey, { outputSchema });
276
305
  }
277
- patch(sessionKey, fields) {
278
- const entries = Object.entries(fields);
279
- if (entries.length === 0)
280
- return;
281
- const setSql = entries.map(([key]) => `${key} = ?`).join(", ");
282
- const values = entries.map(([, value]) => value);
283
- values.push(new Date().toISOString(), sessionKey);
284
- this.db.prepare(`UPDATE sessions SET ${setSql}, updated_at = ? WHERE session_key = ?`).run(...values);
306
+ patchDurableSession(sessionKey, patch) {
307
+ this.storage.patchSession(sessionKey, patch);
285
308
  }
286
309
  }
287
310
  export function makeSessionKey(chatId, messageThreadId) {
288
311
  return messageThreadId == null ? String(chatId) : `${chatId}:${messageThreadId}`;
289
312
  }
290
- function mapSessionRow(row) {
313
+ function mapStoredSession(stored, runtimeState, outputMessageId) {
314
+ if (!stored)
315
+ return null;
316
+ const runtime = runtimeState ?? {
317
+ status: "idle",
318
+ detail: null,
319
+ updatedAt: stored.updatedAt,
320
+ activeTurnId: null,
321
+ };
291
322
  return {
292
- sessionKey: row.session_key,
293
- chatId: row.chat_id,
294
- messageThreadId: row.message_thread_id,
295
- telegramTopicName: row.telegram_topic_name ?? null,
296
- codexThreadId: row.codex_thread_id,
297
- cwd: row.cwd,
298
- model: row.model,
299
- sandboxMode: normalizeSandboxMode(row.sandbox_mode),
300
- approvalPolicy: normalizeApprovalPolicy(row.approval_policy),
301
- reasoningEffort: normalizeReasoningEffort(row.reasoning_effort),
302
- webSearchMode: normalizeWebSearchMode(row.web_search_mode),
303
- networkAccessEnabled: normalizeBoolean(row.network_access_enabled, true),
304
- skipGitRepoCheck: normalizeBoolean(row.skip_git_repo_check, true),
305
- additionalDirectories: normalizeStringArray(row.additional_directories),
306
- outputSchema: normalizeOutputSchema(row.output_schema),
307
- runtimeStatus: normalizeRuntimeStatus(row.runtime_status, row.active_turn_id),
308
- runtimeStatusDetail: row.runtime_status_detail ?? null,
309
- runtimeStatusUpdatedAt: row.runtime_status_updated_at ?? row.updated_at,
310
- activeTurnId: row.active_turn_id,
311
- outputMessageId: row.output_message_id,
312
- createdAt: row.created_at,
313
- updatedAt: row.updated_at,
323
+ ...stored,
324
+ additionalDirectories: [...stored.additionalDirectories],
325
+ runtimeStatus: runtime.status,
326
+ runtimeStatusDetail: runtime.detail,
327
+ runtimeStatusUpdatedAt: runtime.updatedAt,
328
+ activeTurnId: runtime.activeTurnId,
329
+ outputMessageId: outputMessageId ?? null,
314
330
  };
315
331
  }
316
- function mapQueuedInputRow(row) {
332
+ function cloneQueuedInput(input) {
317
333
  return {
318
- id: row.id,
319
- sessionKey: row.session_key,
320
- text: row.text,
321
- input: parseStoredCodexInput(row.input_json, row.text),
322
- createdAt: row.created_at,
323
- updatedAt: row.updated_at,
334
+ ...input,
335
+ input: cloneStoredCodexInput(input.input),
324
336
  };
325
337
  }
326
- function normalizeSandboxMode(value) {
327
- return value && isSessionSandboxMode(value) ? value : DEFAULT_SESSION_PROFILE.sandboxMode;
328
- }
329
- function normalizeApprovalPolicy(value) {
330
- return value && isSessionApprovalPolicy(value) ? value : DEFAULT_SESSION_PROFILE.approvalPolicy;
331
- }
332
- function normalizeReasoningEffort(value) {
333
- return value && isSessionReasoningEffort(value) ? value : null;
334
- }
335
- function normalizeWebSearchMode(value) {
336
- return value && isSessionWebSearchMode(value) ? value : null;
337
- }
338
- function normalizeBoolean(value, fallback) {
339
- if (value == null)
340
- return fallback;
341
- return Number(value) !== 0;
342
- }
343
- function normalizeStringArray(value) {
344
- if (!value)
345
- return [];
346
- try {
347
- const parsed = JSON.parse(value);
348
- if (!Array.isArray(parsed))
349
- return [];
350
- return parsed.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
351
- }
352
- catch {
353
- return [];
354
- }
355
- }
356
- function normalizeOutputSchema(value) {
357
- if (!value?.trim())
358
- return null;
359
- try {
360
- const parsed = JSON.parse(value);
361
- return isPlainObject(parsed) ? JSON.stringify(parsed) : null;
362
- }
363
- catch {
364
- return null;
365
- }
366
- }
367
- function normalizeRuntimeStatus(value, activeTurnId) {
368
- switch (value) {
369
- case "idle":
370
- case "preparing":
371
- case "running":
372
- case "failed":
373
- return value;
374
- default:
375
- return activeTurnId ? "running" : "idle";
376
- }
338
+ function cloneStoredCodexInput(input) {
339
+ return typeof input === "string" ? input : input.map((item) => ({ ...item }));
377
340
  }
378
341
  function normalizeBindingCodeMode(value) {
379
342
  return value === "rebind" ? "rebind" : "bootstrap";
@@ -390,44 +353,9 @@ function normalizeOptionalUserId(value) {
390
353
  const parsed = Number(value);
391
354
  return Number.isSafeInteger(parsed) ? parsed : null;
392
355
  }
393
- function parseStoredCodexInput(inputJson, fallbackText) {
394
- if (!inputJson)
395
- return fallbackText;
396
- try {
397
- const parsed = JSON.parse(inputJson);
398
- return normalizeStoredCodexInput(parsed) ?? fallbackText;
399
- }
400
- catch {
401
- return fallbackText;
402
- }
403
- }
404
- function normalizeStoredCodexInput(value) {
405
- if (typeof value === "string")
406
- return value;
407
- if (!Array.isArray(value))
408
- return null;
409
- const items = [];
410
- for (const item of value) {
411
- if (!isPlainObject(item))
412
- return null;
413
- if (item.type === "text" && typeof item.text === "string") {
414
- items.push({ type: "text", text: item.text });
415
- }
416
- else if (item.type === "local_image" && typeof item.path === "string") {
417
- items.push({ type: "local_image", path: item.path });
418
- }
419
- else {
420
- return null;
421
- }
422
- }
423
- return items;
424
- }
425
356
  export function formatCodexInputPreview(input) {
426
357
  if (typeof input === "string")
427
358
  return input;
428
359
  const parts = input.map((item) => (item.type === "text" ? item.text : `[image: ${item.path}]`));
429
360
  return parts.join(" ").trim() || "[image]";
430
361
  }
431
- function isPlainObject(value) {
432
- return typeof value === "object" && value !== null && !Array.isArray(value);
433
- }