telecodex 0.1.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/bot/auth.js +64 -0
- package/dist/bot/commandSupport.js +239 -0
- package/dist/bot/createBot.js +51 -0
- package/dist/bot/handlerDeps.js +1 -0
- package/dist/bot/handlers/messageHandlers.js +71 -0
- package/dist/bot/handlers/operationalHandlers.js +131 -0
- package/dist/bot/handlers/projectHandlers.js +192 -0
- package/dist/bot/handlers/sessionConfigHandlers.js +319 -0
- package/dist/bot/inputService.js +372 -0
- package/dist/bot/registerHandlers.js +10 -0
- package/dist/bot/session.js +22 -0
- package/dist/bot/sessionFlow.js +51 -0
- package/dist/cli.js +14 -0
- package/dist/codex/sdkRuntime.js +165 -0
- package/dist/config.js +69 -0
- package/dist/runtime/appPaths.js +14 -0
- package/dist/runtime/bootstrap.js +213 -0
- package/dist/runtime/instanceLock.js +89 -0
- package/dist/runtime/logger.js +75 -0
- package/dist/runtime/secrets.js +45 -0
- package/dist/runtime/sessionRuntime.js +53 -0
- package/dist/runtime/startTelecodex.js +118 -0
- package/dist/store/db.js +267 -0
- package/dist/store/projects.js +47 -0
- package/dist/store/sessions.js +328 -0
- package/dist/telegram/attachments.js +67 -0
- package/dist/telegram/delivery.js +140 -0
- package/dist/telegram/messageBuffer.js +272 -0
- package/dist/telegram/renderer.js +146 -0
- package/dist/telegram/splitMessage.js +141 -0
- package/package.json +66 -0
package/dist/store/db.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
export const LATEST_DB_SCHEMA_VERSION = 12;
|
|
5
|
+
export function openDatabase(dbPath) {
|
|
6
|
+
mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
7
|
+
const db = new DatabaseSync(dbPath);
|
|
8
|
+
runMigrations(db);
|
|
9
|
+
return db;
|
|
10
|
+
}
|
|
11
|
+
const MIGRATIONS = [
|
|
12
|
+
{
|
|
13
|
+
version: 1,
|
|
14
|
+
apply(db) {
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
17
|
+
key TEXT PRIMARY KEY,
|
|
18
|
+
value TEXT NOT NULL,
|
|
19
|
+
updated_at TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
23
|
+
chat_id TEXT PRIMARY KEY,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
cwd TEXT NOT NULL,
|
|
26
|
+
created_at TEXT NOT NULL,
|
|
27
|
+
updated_at TEXT NOT NULL
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
31
|
+
session_key TEXT PRIMARY KEY,
|
|
32
|
+
chat_id TEXT NOT NULL,
|
|
33
|
+
message_thread_id TEXT,
|
|
34
|
+
codex_thread_id TEXT,
|
|
35
|
+
cwd TEXT NOT NULL,
|
|
36
|
+
model TEXT NOT NULL,
|
|
37
|
+
active_turn_id TEXT,
|
|
38
|
+
output_message_id INTEGER,
|
|
39
|
+
sandbox_mode TEXT NOT NULL DEFAULT 'read-only',
|
|
40
|
+
approval_policy TEXT NOT NULL DEFAULT 'on-request',
|
|
41
|
+
telegram_topic_name TEXT,
|
|
42
|
+
reasoning_effort TEXT,
|
|
43
|
+
web_search_mode TEXT,
|
|
44
|
+
network_access_enabled INTEGER NOT NULL DEFAULT 1,
|
|
45
|
+
skip_git_repo_check INTEGER NOT NULL DEFAULT 1,
|
|
46
|
+
additional_directories TEXT,
|
|
47
|
+
output_schema TEXT,
|
|
48
|
+
runtime_status TEXT NOT NULL DEFAULT 'idle',
|
|
49
|
+
runtime_status_detail TEXT,
|
|
50
|
+
runtime_status_updated_at TEXT,
|
|
51
|
+
created_at TEXT NOT NULL,
|
|
52
|
+
updated_at TEXT NOT NULL
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS sessions_codex_thread_id_idx
|
|
56
|
+
ON sessions (codex_thread_id);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS queued_inputs (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
session_key TEXT NOT NULL,
|
|
61
|
+
text TEXT NOT NULL,
|
|
62
|
+
input_json TEXT,
|
|
63
|
+
created_at TEXT NOT NULL,
|
|
64
|
+
updated_at TEXT NOT NULL
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS queued_inputs_session_key_idx
|
|
68
|
+
ON queued_inputs (session_key, id);
|
|
69
|
+
`);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
version: 2,
|
|
74
|
+
apply(db) {
|
|
75
|
+
const addedSandboxMode = ensureTableColumn(db, "sessions", "sandbox_mode", "ALTER TABLE sessions ADD COLUMN sandbox_mode TEXT NOT NULL DEFAULT 'read-only'");
|
|
76
|
+
const addedApprovalPolicy = ensureTableColumn(db, "sessions", "approval_policy", "ALTER TABLE sessions ADD COLUMN approval_policy TEXT NOT NULL DEFAULT 'on-request'");
|
|
77
|
+
ensureTableColumn(db, "sessions", "telegram_topic_name", "ALTER TABLE sessions ADD COLUMN telegram_topic_name TEXT");
|
|
78
|
+
ensureTableColumn(db, "sessions", "reasoning_effort", "ALTER TABLE sessions ADD COLUMN reasoning_effort TEXT");
|
|
79
|
+
if (addedSandboxMode) {
|
|
80
|
+
db.exec(`
|
|
81
|
+
UPDATE sessions
|
|
82
|
+
SET sandbox_mode = CASE mode
|
|
83
|
+
WHEN 'write' THEN 'workspace-write'
|
|
84
|
+
ELSE 'read-only'
|
|
85
|
+
END
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
if (addedApprovalPolicy) {
|
|
89
|
+
db.exec(`
|
|
90
|
+
UPDATE sessions
|
|
91
|
+
SET approval_policy = 'on-request'
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
version: 5,
|
|
98
|
+
apply(db) {
|
|
99
|
+
ensureTableColumn(db, "sessions", "runtime_status", "ALTER TABLE sessions ADD COLUMN runtime_status TEXT NOT NULL DEFAULT 'idle'");
|
|
100
|
+
ensureTableColumn(db, "sessions", "runtime_status_detail", "ALTER TABLE sessions ADD COLUMN runtime_status_detail TEXT");
|
|
101
|
+
const addedRuntimeStatusUpdatedAt = ensureTableColumn(db, "sessions", "runtime_status_updated_at", "ALTER TABLE sessions ADD COLUMN runtime_status_updated_at TEXT");
|
|
102
|
+
db.exec(`
|
|
103
|
+
UPDATE sessions
|
|
104
|
+
SET runtime_status = CASE
|
|
105
|
+
WHEN active_turn_id IS NOT NULL THEN 'running'
|
|
106
|
+
ELSE 'idle'
|
|
107
|
+
END
|
|
108
|
+
WHERE runtime_status IS NULL
|
|
109
|
+
OR runtime_status NOT IN ('idle', 'preparing', 'running', 'failed')
|
|
110
|
+
`);
|
|
111
|
+
if (addedRuntimeStatusUpdatedAt) {
|
|
112
|
+
db.exec(`
|
|
113
|
+
UPDATE sessions
|
|
114
|
+
SET runtime_status_updated_at = updated_at
|
|
115
|
+
WHERE runtime_status_updated_at IS NULL
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
version: 6,
|
|
122
|
+
apply(db) {
|
|
123
|
+
db.exec(`
|
|
124
|
+
CREATE TABLE IF NOT EXISTS queued_inputs (
|
|
125
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
126
|
+
session_key TEXT NOT NULL,
|
|
127
|
+
text TEXT NOT NULL,
|
|
128
|
+
created_at TEXT NOT NULL,
|
|
129
|
+
updated_at TEXT NOT NULL
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
CREATE INDEX IF NOT EXISTS queued_inputs_session_key_idx
|
|
133
|
+
ON queued_inputs (session_key, id)
|
|
134
|
+
`);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
version: 10,
|
|
139
|
+
apply(db) {
|
|
140
|
+
db.exec(`
|
|
141
|
+
DROP INDEX IF EXISTS turn_deliveries_thread_id_idx;
|
|
142
|
+
DROP INDEX IF EXISTS turn_deliveries_session_key_idx;
|
|
143
|
+
DROP INDEX IF EXISTS turn_deliveries_status_idx;
|
|
144
|
+
DROP INDEX IF EXISTS pending_interactions_session_key_idx;
|
|
145
|
+
DROP TABLE IF EXISTS turn_deliveries;
|
|
146
|
+
DROP TABLE IF EXISTS pending_interactions;
|
|
147
|
+
`);
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
version: 11,
|
|
152
|
+
apply(db) {
|
|
153
|
+
db.exec(`
|
|
154
|
+
CREATE TABLE sessions_v11 (
|
|
155
|
+
session_key TEXT PRIMARY KEY,
|
|
156
|
+
chat_id TEXT NOT NULL,
|
|
157
|
+
message_thread_id TEXT,
|
|
158
|
+
codex_thread_id TEXT,
|
|
159
|
+
cwd TEXT NOT NULL,
|
|
160
|
+
model TEXT NOT NULL,
|
|
161
|
+
active_turn_id TEXT,
|
|
162
|
+
output_message_id INTEGER,
|
|
163
|
+
sandbox_mode TEXT NOT NULL DEFAULT 'read-only',
|
|
164
|
+
approval_policy TEXT NOT NULL DEFAULT 'on-request',
|
|
165
|
+
telegram_topic_name TEXT,
|
|
166
|
+
reasoning_effort TEXT,
|
|
167
|
+
runtime_status TEXT NOT NULL DEFAULT 'idle',
|
|
168
|
+
runtime_status_detail TEXT,
|
|
169
|
+
runtime_status_updated_at TEXT,
|
|
170
|
+
created_at TEXT NOT NULL,
|
|
171
|
+
updated_at TEXT NOT NULL
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
INSERT INTO sessions_v11 (
|
|
175
|
+
session_key,
|
|
176
|
+
chat_id,
|
|
177
|
+
message_thread_id,
|
|
178
|
+
codex_thread_id,
|
|
179
|
+
cwd,
|
|
180
|
+
model,
|
|
181
|
+
active_turn_id,
|
|
182
|
+
output_message_id,
|
|
183
|
+
sandbox_mode,
|
|
184
|
+
approval_policy,
|
|
185
|
+
telegram_topic_name,
|
|
186
|
+
reasoning_effort,
|
|
187
|
+
runtime_status,
|
|
188
|
+
runtime_status_detail,
|
|
189
|
+
runtime_status_updated_at,
|
|
190
|
+
created_at,
|
|
191
|
+
updated_at
|
|
192
|
+
)
|
|
193
|
+
SELECT
|
|
194
|
+
session_key,
|
|
195
|
+
chat_id,
|
|
196
|
+
message_thread_id,
|
|
197
|
+
codex_thread_id,
|
|
198
|
+
cwd,
|
|
199
|
+
model,
|
|
200
|
+
active_turn_id,
|
|
201
|
+
output_message_id,
|
|
202
|
+
sandbox_mode,
|
|
203
|
+
approval_policy,
|
|
204
|
+
telegram_topic_name,
|
|
205
|
+
reasoning_effort,
|
|
206
|
+
runtime_status,
|
|
207
|
+
runtime_status_detail,
|
|
208
|
+
runtime_status_updated_at,
|
|
209
|
+
created_at,
|
|
210
|
+
updated_at
|
|
211
|
+
FROM sessions;
|
|
212
|
+
|
|
213
|
+
DROP TABLE sessions;
|
|
214
|
+
ALTER TABLE sessions_v11 RENAME TO sessions;
|
|
215
|
+
|
|
216
|
+
CREATE INDEX IF NOT EXISTS sessions_codex_thread_id_idx
|
|
217
|
+
ON sessions (codex_thread_id);
|
|
218
|
+
`);
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
version: 12,
|
|
223
|
+
apply(db) {
|
|
224
|
+
ensureTableColumn(db, "sessions", "web_search_mode", "ALTER TABLE sessions ADD COLUMN web_search_mode TEXT");
|
|
225
|
+
ensureTableColumn(db, "sessions", "network_access_enabled", "ALTER TABLE sessions ADD COLUMN network_access_enabled INTEGER NOT NULL DEFAULT 1");
|
|
226
|
+
ensureTableColumn(db, "sessions", "skip_git_repo_check", "ALTER TABLE sessions ADD COLUMN skip_git_repo_check INTEGER NOT NULL DEFAULT 1");
|
|
227
|
+
ensureTableColumn(db, "sessions", "additional_directories", "ALTER TABLE sessions ADD COLUMN additional_directories TEXT");
|
|
228
|
+
ensureTableColumn(db, "sessions", "output_schema", "ALTER TABLE sessions ADD COLUMN output_schema TEXT");
|
|
229
|
+
ensureTableColumn(db, "queued_inputs", "input_json", "ALTER TABLE queued_inputs ADD COLUMN input_json TEXT");
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
function runMigrations(db) {
|
|
234
|
+
const currentVersion = getSchemaVersion(db);
|
|
235
|
+
if (currentVersion >= LATEST_DB_SCHEMA_VERSION) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
for (const migration of MIGRATIONS) {
|
|
239
|
+
if (migration.version <= currentVersion)
|
|
240
|
+
continue;
|
|
241
|
+
db.exec("BEGIN");
|
|
242
|
+
try {
|
|
243
|
+
migration.apply(db);
|
|
244
|
+
setSchemaVersion(db, migration.version);
|
|
245
|
+
db.exec("COMMIT");
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
db.exec("ROLLBACK");
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function ensureTableColumn(db, tableName, columnName, sql) {
|
|
254
|
+
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
255
|
+
if (columns.some((column) => column.name === columnName)) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
db.exec(sql);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
function getSchemaVersion(db) {
|
|
262
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
263
|
+
return row?.user_version ?? 0;
|
|
264
|
+
}
|
|
265
|
+
function setSchemaVersion(db, version) {
|
|
266
|
+
db.exec(`PRAGMA user_version = ${version}`);
|
|
267
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export class ProjectStore {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
get(chatId) {
|
|
8
|
+
const row = this.db.prepare("SELECT * FROM projects WHERE chat_id = ?").get(chatId);
|
|
9
|
+
return row ? mapRow(row) : null;
|
|
10
|
+
}
|
|
11
|
+
upsert(input) {
|
|
12
|
+
const now = new Date().toISOString();
|
|
13
|
+
const cwd = path.resolve(input.cwd);
|
|
14
|
+
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;
|
|
28
|
+
}
|
|
29
|
+
remove(chatId) {
|
|
30
|
+
this.db.prepare("DELETE FROM projects WHERE chat_id = ?").run(chatId);
|
|
31
|
+
}
|
|
32
|
+
list() {
|
|
33
|
+
const rows = this.db
|
|
34
|
+
.prepare("SELECT * FROM projects ORDER BY updated_at DESC")
|
|
35
|
+
.all();
|
|
36
|
+
return rows.map(mapRow);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { DEFAULT_SESSION_PROFILE, isSessionApprovalPolicy, isSessionReasoningEffort, isSessionSandboxMode, isSessionWebSearchMode, } from "../config.js";
|
|
2
|
+
export class SessionStore {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
getAppState(key) {
|
|
8
|
+
const row = this.db.prepare("SELECT value FROM app_state WHERE key = ?").get(key);
|
|
9
|
+
return row?.value ?? null;
|
|
10
|
+
}
|
|
11
|
+
setAppState(key, value) {
|
|
12
|
+
this.db
|
|
13
|
+
.prepare(`INSERT INTO app_state (key, value, updated_at)
|
|
14
|
+
VALUES (?, ?, ?)
|
|
15
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
16
|
+
.run(key, value, new Date().toISOString());
|
|
17
|
+
}
|
|
18
|
+
deleteAppState(key) {
|
|
19
|
+
this.db.prepare("DELETE FROM app_state WHERE key = ?").run(key);
|
|
20
|
+
}
|
|
21
|
+
getAuthorizedUserId() {
|
|
22
|
+
const value = this.getAppState("authorized_user_id");
|
|
23
|
+
if (value == null)
|
|
24
|
+
return null;
|
|
25
|
+
const userId = Number(value);
|
|
26
|
+
return Number.isSafeInteger(userId) ? userId : null;
|
|
27
|
+
}
|
|
28
|
+
getBootstrapCode() {
|
|
29
|
+
return this.getAppState("bootstrap_code");
|
|
30
|
+
}
|
|
31
|
+
setBootstrapCode(code) {
|
|
32
|
+
this.setAppState("bootstrap_code", code);
|
|
33
|
+
}
|
|
34
|
+
clearBootstrapCode() {
|
|
35
|
+
this.deleteAppState("bootstrap_code");
|
|
36
|
+
}
|
|
37
|
+
claimAuthorizedUserId(userId) {
|
|
38
|
+
const existing = this.getAuthorizedUserId();
|
|
39
|
+
if (existing != null)
|
|
40
|
+
return existing;
|
|
41
|
+
this.db.prepare("INSERT OR IGNORE INTO app_state (key, value, updated_at) VALUES ('authorized_user_id', ?, ?)").run(String(userId), new Date().toISOString());
|
|
42
|
+
const current = this.getAuthorizedUserId();
|
|
43
|
+
if (current == null)
|
|
44
|
+
throw new Error("Failed to persist authorized Telegram user id");
|
|
45
|
+
this.clearBootstrapCode();
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
clearAuthorizedUserId() {
|
|
49
|
+
this.deleteAppState("authorized_user_id");
|
|
50
|
+
}
|
|
51
|
+
getOrCreate(input) {
|
|
52
|
+
const existing = this.get(input.sessionKey);
|
|
53
|
+
if (existing)
|
|
54
|
+
return existing;
|
|
55
|
+
const now = new Date().toISOString();
|
|
56
|
+
this.db
|
|
57
|
+
.prepare(`INSERT INTO sessions (
|
|
58
|
+
session_key, chat_id, message_thread_id, telegram_topic_name, cwd, model, sandbox_mode, approval_policy, created_at, updated_at
|
|
59
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
60
|
+
.run(input.sessionKey, input.chatId, input.messageThreadId, input.telegramTopicName ?? null, input.defaultCwd, input.defaultModel, DEFAULT_SESSION_PROFILE.sandboxMode, DEFAULT_SESSION_PROFILE.approvalPolicy, now, now);
|
|
61
|
+
const created = this.get(input.sessionKey);
|
|
62
|
+
if (!created)
|
|
63
|
+
throw new Error("Session insert failed");
|
|
64
|
+
return created;
|
|
65
|
+
}
|
|
66
|
+
get(sessionKey) {
|
|
67
|
+
const row = this.db
|
|
68
|
+
.prepare("SELECT * FROM sessions WHERE session_key = ?")
|
|
69
|
+
.get(sessionKey);
|
|
70
|
+
return row ? mapSessionRow(row) : null;
|
|
71
|
+
}
|
|
72
|
+
getByThreadId(threadId) {
|
|
73
|
+
const row = this.db
|
|
74
|
+
.prepare("SELECT * FROM sessions WHERE codex_thread_id = ? LIMIT 1")
|
|
75
|
+
.get(threadId);
|
|
76
|
+
return row ? mapSessionRow(row) : null;
|
|
77
|
+
}
|
|
78
|
+
listTopicSessions() {
|
|
79
|
+
const rows = this.db
|
|
80
|
+
.prepare("SELECT * FROM sessions WHERE message_thread_id IS NOT NULL ORDER BY session_key ASC")
|
|
81
|
+
.all();
|
|
82
|
+
return rows.map(mapSessionRow);
|
|
83
|
+
}
|
|
84
|
+
remove(sessionKey) {
|
|
85
|
+
this.db.prepare("DELETE FROM queued_inputs WHERE session_key = ?").run(sessionKey);
|
|
86
|
+
this.db.prepare("DELETE FROM sessions WHERE session_key = ?").run(sessionKey);
|
|
87
|
+
}
|
|
88
|
+
enqueueInput(sessionKey, input) {
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
const text = formatCodexInputPreview(input);
|
|
91
|
+
const result = this.db
|
|
92
|
+
.prepare(`INSERT INTO queued_inputs (session_key, text, input_json, created_at, updated_at)
|
|
93
|
+
VALUES (?, ?, ?, ?, ?)`)
|
|
94
|
+
.run(sessionKey, text, JSON.stringify(input), now, now);
|
|
95
|
+
const id = Number(result.lastInsertRowid);
|
|
96
|
+
const queued = this.getQueuedInput(id);
|
|
97
|
+
if (!queued)
|
|
98
|
+
throw new Error("Queued input insert failed");
|
|
99
|
+
return queued;
|
|
100
|
+
}
|
|
101
|
+
getQueuedInput(id) {
|
|
102
|
+
const row = this.db.prepare("SELECT * FROM queued_inputs WHERE id = ?").get(id);
|
|
103
|
+
return row ? mapQueuedInputRow(row) : null;
|
|
104
|
+
}
|
|
105
|
+
getQueuedInputCount(sessionKey) {
|
|
106
|
+
const row = this.db
|
|
107
|
+
.prepare("SELECT COUNT(*) AS count FROM queued_inputs WHERE session_key = ?")
|
|
108
|
+
.get(sessionKey);
|
|
109
|
+
return row?.count ?? 0;
|
|
110
|
+
}
|
|
111
|
+
peekNextQueuedInput(sessionKey) {
|
|
112
|
+
const row = this.db
|
|
113
|
+
.prepare("SELECT * FROM queued_inputs WHERE session_key = ? ORDER BY id ASC LIMIT 1")
|
|
114
|
+
.get(sessionKey);
|
|
115
|
+
return row ? mapQueuedInputRow(row) : null;
|
|
116
|
+
}
|
|
117
|
+
listQueuedInputs(sessionKey, limit = 5) {
|
|
118
|
+
const rows = this.db
|
|
119
|
+
.prepare("SELECT * FROM queued_inputs WHERE session_key = ? ORDER BY id ASC LIMIT ?")
|
|
120
|
+
.all(sessionKey, limit);
|
|
121
|
+
return rows.map(mapQueuedInputRow);
|
|
122
|
+
}
|
|
123
|
+
removeQueuedInput(id) {
|
|
124
|
+
this.db.prepare("DELETE FROM queued_inputs WHERE id = ?").run(id);
|
|
125
|
+
}
|
|
126
|
+
removeQueuedInputForSession(sessionKey, id) {
|
|
127
|
+
const result = this.db
|
|
128
|
+
.prepare("DELETE FROM queued_inputs WHERE session_key = ? AND id = ?")
|
|
129
|
+
.run(sessionKey, id);
|
|
130
|
+
return (result.changes ?? 0) > 0;
|
|
131
|
+
}
|
|
132
|
+
clearQueuedInputs(sessionKey) {
|
|
133
|
+
const result = this.db.prepare("DELETE FROM queued_inputs WHERE session_key = ?").run(sessionKey);
|
|
134
|
+
return result.changes ?? 0;
|
|
135
|
+
}
|
|
136
|
+
bindThread(sessionKey, threadId) {
|
|
137
|
+
this.patch(sessionKey, {
|
|
138
|
+
codex_thread_id: threadId,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
setTelegramTopicName(sessionKey, topicName) {
|
|
142
|
+
this.patch(sessionKey, { telegram_topic_name: topicName });
|
|
143
|
+
}
|
|
144
|
+
setRuntimeState(sessionKey, state) {
|
|
145
|
+
this.patch(sessionKey, {
|
|
146
|
+
runtime_status: state.status,
|
|
147
|
+
runtime_status_detail: state.detail,
|
|
148
|
+
runtime_status_updated_at: state.updatedAt,
|
|
149
|
+
active_turn_id: state.activeTurnId,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
setOutputMessage(sessionKey, messageId) {
|
|
153
|
+
this.patch(sessionKey, { output_message_id: messageId });
|
|
154
|
+
}
|
|
155
|
+
setCwd(sessionKey, cwd) {
|
|
156
|
+
this.patch(sessionKey, { cwd });
|
|
157
|
+
}
|
|
158
|
+
setModel(sessionKey, model) {
|
|
159
|
+
this.patch(sessionKey, { model });
|
|
160
|
+
}
|
|
161
|
+
setSandboxMode(sessionKey, sandboxMode) {
|
|
162
|
+
this.patch(sessionKey, {
|
|
163
|
+
sandbox_mode: sandboxMode,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
setApprovalPolicy(sessionKey, approvalPolicy) {
|
|
167
|
+
this.patch(sessionKey, { approval_policy: approvalPolicy });
|
|
168
|
+
}
|
|
169
|
+
setReasoningEffort(sessionKey, reasoningEffort) {
|
|
170
|
+
this.patch(sessionKey, { reasoning_effort: reasoningEffort });
|
|
171
|
+
}
|
|
172
|
+
setWebSearchMode(sessionKey, webSearchMode) {
|
|
173
|
+
this.patch(sessionKey, { web_search_mode: webSearchMode });
|
|
174
|
+
}
|
|
175
|
+
setNetworkAccessEnabled(sessionKey, enabled) {
|
|
176
|
+
this.patch(sessionKey, { network_access_enabled: enabled ? 1 : 0 });
|
|
177
|
+
}
|
|
178
|
+
setSkipGitRepoCheck(sessionKey, skip) {
|
|
179
|
+
this.patch(sessionKey, { skip_git_repo_check: skip ? 1 : 0 });
|
|
180
|
+
}
|
|
181
|
+
setAdditionalDirectories(sessionKey, directories) {
|
|
182
|
+
this.patch(sessionKey, { additional_directories: JSON.stringify(directories) });
|
|
183
|
+
}
|
|
184
|
+
setOutputSchema(sessionKey, outputSchema) {
|
|
185
|
+
this.patch(sessionKey, { output_schema: outputSchema });
|
|
186
|
+
}
|
|
187
|
+
patch(sessionKey, fields) {
|
|
188
|
+
const entries = Object.entries(fields);
|
|
189
|
+
if (entries.length === 0)
|
|
190
|
+
return;
|
|
191
|
+
const setSql = entries.map(([key]) => `${key} = ?`).join(", ");
|
|
192
|
+
const values = entries.map(([, value]) => value);
|
|
193
|
+
values.push(new Date().toISOString(), sessionKey);
|
|
194
|
+
this.db.prepare(`UPDATE sessions SET ${setSql}, updated_at = ? WHERE session_key = ?`).run(...values);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export function makeSessionKey(chatId, messageThreadId) {
|
|
198
|
+
return messageThreadId == null ? String(chatId) : `${chatId}:${messageThreadId}`;
|
|
199
|
+
}
|
|
200
|
+
function mapSessionRow(row) {
|
|
201
|
+
return {
|
|
202
|
+
sessionKey: row.session_key,
|
|
203
|
+
chatId: row.chat_id,
|
|
204
|
+
messageThreadId: row.message_thread_id,
|
|
205
|
+
telegramTopicName: row.telegram_topic_name ?? null,
|
|
206
|
+
codexThreadId: row.codex_thread_id,
|
|
207
|
+
cwd: row.cwd,
|
|
208
|
+
model: row.model,
|
|
209
|
+
sandboxMode: normalizeSandboxMode(row.sandbox_mode),
|
|
210
|
+
approvalPolicy: normalizeApprovalPolicy(row.approval_policy),
|
|
211
|
+
reasoningEffort: normalizeReasoningEffort(row.reasoning_effort),
|
|
212
|
+
webSearchMode: normalizeWebSearchMode(row.web_search_mode),
|
|
213
|
+
networkAccessEnabled: normalizeBoolean(row.network_access_enabled, true),
|
|
214
|
+
skipGitRepoCheck: normalizeBoolean(row.skip_git_repo_check, true),
|
|
215
|
+
additionalDirectories: normalizeStringArray(row.additional_directories),
|
|
216
|
+
outputSchema: normalizeOutputSchema(row.output_schema),
|
|
217
|
+
runtimeStatus: normalizeRuntimeStatus(row.runtime_status, row.active_turn_id),
|
|
218
|
+
runtimeStatusDetail: row.runtime_status_detail ?? null,
|
|
219
|
+
runtimeStatusUpdatedAt: row.runtime_status_updated_at ?? row.updated_at,
|
|
220
|
+
activeTurnId: row.active_turn_id,
|
|
221
|
+
outputMessageId: row.output_message_id,
|
|
222
|
+
createdAt: row.created_at,
|
|
223
|
+
updatedAt: row.updated_at,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function mapQueuedInputRow(row) {
|
|
227
|
+
return {
|
|
228
|
+
id: row.id,
|
|
229
|
+
sessionKey: row.session_key,
|
|
230
|
+
text: row.text,
|
|
231
|
+
input: parseStoredCodexInput(row.input_json, row.text),
|
|
232
|
+
createdAt: row.created_at,
|
|
233
|
+
updatedAt: row.updated_at,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function normalizeSandboxMode(value) {
|
|
237
|
+
return value && isSessionSandboxMode(value) ? value : DEFAULT_SESSION_PROFILE.sandboxMode;
|
|
238
|
+
}
|
|
239
|
+
function normalizeApprovalPolicy(value) {
|
|
240
|
+
return value && isSessionApprovalPolicy(value) ? value : DEFAULT_SESSION_PROFILE.approvalPolicy;
|
|
241
|
+
}
|
|
242
|
+
function normalizeReasoningEffort(value) {
|
|
243
|
+
return value && isSessionReasoningEffort(value) ? value : null;
|
|
244
|
+
}
|
|
245
|
+
function normalizeWebSearchMode(value) {
|
|
246
|
+
return value && isSessionWebSearchMode(value) ? value : null;
|
|
247
|
+
}
|
|
248
|
+
function normalizeBoolean(value, fallback) {
|
|
249
|
+
if (value == null)
|
|
250
|
+
return fallback;
|
|
251
|
+
return Number(value) !== 0;
|
|
252
|
+
}
|
|
253
|
+
function normalizeStringArray(value) {
|
|
254
|
+
if (!value)
|
|
255
|
+
return [];
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(value);
|
|
258
|
+
if (!Array.isArray(parsed))
|
|
259
|
+
return [];
|
|
260
|
+
return parsed.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function normalizeOutputSchema(value) {
|
|
267
|
+
if (!value?.trim())
|
|
268
|
+
return null;
|
|
269
|
+
try {
|
|
270
|
+
const parsed = JSON.parse(value);
|
|
271
|
+
return isPlainObject(parsed) ? JSON.stringify(parsed) : null;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function normalizeRuntimeStatus(value, activeTurnId) {
|
|
278
|
+
switch (value) {
|
|
279
|
+
case "idle":
|
|
280
|
+
case "preparing":
|
|
281
|
+
case "running":
|
|
282
|
+
case "failed":
|
|
283
|
+
return value;
|
|
284
|
+
default:
|
|
285
|
+
return activeTurnId ? "running" : "idle";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function parseStoredCodexInput(inputJson, fallbackText) {
|
|
289
|
+
if (!inputJson)
|
|
290
|
+
return fallbackText;
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(inputJson);
|
|
293
|
+
return normalizeStoredCodexInput(parsed) ?? fallbackText;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return fallbackText;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function normalizeStoredCodexInput(value) {
|
|
300
|
+
if (typeof value === "string")
|
|
301
|
+
return value;
|
|
302
|
+
if (!Array.isArray(value))
|
|
303
|
+
return null;
|
|
304
|
+
const items = [];
|
|
305
|
+
for (const item of value) {
|
|
306
|
+
if (!isPlainObject(item))
|
|
307
|
+
return null;
|
|
308
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
309
|
+
items.push({ type: "text", text: item.text });
|
|
310
|
+
}
|
|
311
|
+
else if (item.type === "local_image" && typeof item.path === "string") {
|
|
312
|
+
items.push({ type: "local_image", path: item.path });
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return items;
|
|
319
|
+
}
|
|
320
|
+
export function formatCodexInputPreview(input) {
|
|
321
|
+
if (typeof input === "string")
|
|
322
|
+
return input;
|
|
323
|
+
const parts = input.map((item) => (item.type === "text" ? item.text : `[image: ${item.path}]`));
|
|
324
|
+
return parts.join(" ").trim() || "[image]";
|
|
325
|
+
}
|
|
326
|
+
function isPlainObject(value) {
|
|
327
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
328
|
+
}
|