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.
- package/README.md +104 -132
- package/dist/bot/commandSupport.js +3 -1
- package/dist/bot/createBot.js +59 -2
- package/dist/bot/handlers/projectHandlers.js +67 -8
- package/dist/bot/topicCleanup.js +80 -0
- package/dist/codex/sessionCatalog.js +215 -0
- package/dist/config.js +0 -1
- package/dist/runtime/appPaths.js +4 -1
- package/dist/runtime/bootstrap.js +11 -7
- package/dist/runtime/startTelecodex.js +5 -0
- package/dist/store/fileState.js +370 -0
- package/dist/store/legacyMigration.js +160 -0
- package/dist/store/projects.js +11 -33
- package/dist/store/sessions.js +138 -210
- package/package.json +2 -2
- package/dist/store/db.js +0 -267
|
@@ -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
|
+
}
|
package/dist/store/projects.js
CHANGED
|
@@ -1,47 +1,25 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
export class ProjectStore {
|
|
3
|
-
|
|
4
|
-
constructor(
|
|
5
|
-
this.
|
|
3
|
+
storage;
|
|
4
|
+
constructor(storage) {
|
|
5
|
+
this.storage = storage;
|
|
6
6
|
}
|
|
7
7
|
get(chatId) {
|
|
8
|
-
|
|
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.
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
20
|
+
this.storage.removeProject(chatId);
|
|
31
21
|
}
|
|
32
22
|
list() {
|
|
33
|
-
|
|
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
|
-
}
|
package/dist/store/sessions.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import { DEFAULT_SESSION_PROFILE,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
return row?.value ?? null;
|
|
14
|
+
return this.storage.getAppState(key);
|
|
12
15
|
}
|
|
13
16
|
setAppState(key, value) {
|
|
14
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
147
|
-
.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
158
|
-
|
|
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
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
.
|
|
171
|
-
.
|
|
172
|
-
|
|
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.
|
|
176
|
-
this.
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
193
|
-
|
|
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
|
-
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
209
|
-
|
|
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
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
224
|
-
|
|
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.
|
|
228
|
-
|
|
257
|
+
this.patchDurableSession(sessionKey, {
|
|
258
|
+
codexThreadId: threadId,
|
|
229
259
|
});
|
|
230
260
|
}
|
|
231
261
|
setTelegramTopicName(sessionKey, topicName) {
|
|
232
|
-
this.
|
|
262
|
+
this.patchDurableSession(sessionKey, {
|
|
263
|
+
telegramTopicName: topicName,
|
|
264
|
+
});
|
|
233
265
|
}
|
|
234
266
|
setRuntimeState(sessionKey, state) {
|
|
235
|
-
this.
|
|
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
|
-
|
|
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.
|
|
277
|
+
this.patchDurableSession(sessionKey, { cwd });
|
|
247
278
|
}
|
|
248
279
|
setModel(sessionKey, model) {
|
|
249
|
-
this.
|
|
280
|
+
this.patchDurableSession(sessionKey, { model });
|
|
250
281
|
}
|
|
251
282
|
setSandboxMode(sessionKey, sandboxMode) {
|
|
252
|
-
this.
|
|
253
|
-
sandbox_mode: sandboxMode,
|
|
254
|
-
});
|
|
283
|
+
this.patchDurableSession(sessionKey, { sandboxMode });
|
|
255
284
|
}
|
|
256
285
|
setApprovalPolicy(sessionKey, approvalPolicy) {
|
|
257
|
-
this.
|
|
286
|
+
this.patchDurableSession(sessionKey, { approvalPolicy });
|
|
258
287
|
}
|
|
259
288
|
setReasoningEffort(sessionKey, reasoningEffort) {
|
|
260
|
-
this.
|
|
289
|
+
this.patchDurableSession(sessionKey, { reasoningEffort });
|
|
261
290
|
}
|
|
262
291
|
setWebSearchMode(sessionKey, webSearchMode) {
|
|
263
|
-
this.
|
|
292
|
+
this.patchDurableSession(sessionKey, { webSearchMode });
|
|
264
293
|
}
|
|
265
294
|
setNetworkAccessEnabled(sessionKey, enabled) {
|
|
266
|
-
this.
|
|
295
|
+
this.patchDurableSession(sessionKey, { networkAccessEnabled: enabled });
|
|
267
296
|
}
|
|
268
297
|
setSkipGitRepoCheck(sessionKey, skip) {
|
|
269
|
-
this.
|
|
298
|
+
this.patchDurableSession(sessionKey, { skipGitRepoCheck: skip });
|
|
270
299
|
}
|
|
271
300
|
setAdditionalDirectories(sessionKey, directories) {
|
|
272
|
-
this.
|
|
301
|
+
this.patchDurableSession(sessionKey, { additionalDirectories: [...directories] });
|
|
273
302
|
}
|
|
274
303
|
setOutputSchema(sessionKey, outputSchema) {
|
|
275
|
-
this.
|
|
304
|
+
this.patchDurableSession(sessionKey, { outputSchema });
|
|
276
305
|
}
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
332
|
+
function cloneQueuedInput(input) {
|
|
317
333
|
return {
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
327
|
-
return
|
|
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
|
-
}
|