ework-daemon 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 +262 -0
- package/bin/ework-daemon-server.js +2 -0
- package/package.json +65 -0
- package/scripts/ework-daemon.service +22 -0
- package/src/cli.ts +218 -0
- package/src/config.ts +134 -0
- package/src/gitea.ts +137 -0
- package/src/index.ts +48 -0
- package/src/logger.ts +64 -0
- package/src/op.ts +486 -0
- package/src/opencode.ts +1219 -0
- package/src/server.ts +160 -0
- package/src/trackers/gitea-tracker.ts +181 -0
- package/src/trackers/types.ts +155 -0
package/src/op.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { mkdirSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { log } from "./logger";
|
|
6
|
+
import type { TrackerRef, Issue, IssueState, OpSession, SessionState, Message } from "./trackers/types";
|
|
7
|
+
|
|
8
|
+
// ─── Row Types ───
|
|
9
|
+
|
|
10
|
+
interface IssueRow {
|
|
11
|
+
id: string;
|
|
12
|
+
tracker_type: string;
|
|
13
|
+
tracker_scope_key: string;
|
|
14
|
+
tracker_scope: string;
|
|
15
|
+
tracker_issue_id: string;
|
|
16
|
+
state: string;
|
|
17
|
+
title: string;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SessionRow {
|
|
23
|
+
id: string;
|
|
24
|
+
issue_id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
state: string;
|
|
27
|
+
opencode_session_id: string | null;
|
|
28
|
+
opencode_pid: number | null;
|
|
29
|
+
workdir: string | null;
|
|
30
|
+
created_at: string;
|
|
31
|
+
started_at: number | null;
|
|
32
|
+
progress_comment_id: string | null;
|
|
33
|
+
reaction_comment_id: string | null;
|
|
34
|
+
current_prompt: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MessageRow {
|
|
38
|
+
id: string;
|
|
39
|
+
session_id: string;
|
|
40
|
+
content: string;
|
|
41
|
+
source_comment_id: string | null;
|
|
42
|
+
reaction_comment_id: string | null;
|
|
43
|
+
status: string;
|
|
44
|
+
attempts: number;
|
|
45
|
+
error: string | null;
|
|
46
|
+
created_at: string;
|
|
47
|
+
updated_at: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Row → Domain Mappers ───
|
|
51
|
+
|
|
52
|
+
function rowToIssue(row: IssueRow): Issue {
|
|
53
|
+
let scope: Record<string, string>;
|
|
54
|
+
try { scope = JSON.parse(row.tracker_scope); } catch { scope = {}; }
|
|
55
|
+
return {
|
|
56
|
+
id: row.id,
|
|
57
|
+
trackerType: row.tracker_type,
|
|
58
|
+
trackerScope: scope,
|
|
59
|
+
trackerScopeKey: row.tracker_scope_key,
|
|
60
|
+
trackerIssueId: row.tracker_issue_id,
|
|
61
|
+
state: row.state as IssueState,
|
|
62
|
+
title: row.title,
|
|
63
|
+
createdAt: new Date(row.created_at),
|
|
64
|
+
updatedAt: new Date(row.updated_at),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rowToSession(row: SessionRow): OpSession {
|
|
69
|
+
return {
|
|
70
|
+
id: row.id,
|
|
71
|
+
issueId: row.issue_id,
|
|
72
|
+
name: row.name,
|
|
73
|
+
state: row.state as SessionState,
|
|
74
|
+
opencodeSessionId: row.opencode_session_id ?? undefined,
|
|
75
|
+
opencodePid: row.opencode_pid ?? undefined,
|
|
76
|
+
workdir: row.workdir ?? undefined,
|
|
77
|
+
createdAt: new Date(row.created_at),
|
|
78
|
+
startedAt: row.started_at ?? undefined,
|
|
79
|
+
progressCommentId: row.progress_comment_id ?? undefined,
|
|
80
|
+
reactionCommentId: row.reaction_comment_id ?? undefined,
|
|
81
|
+
currentPrompt: row.current_prompt ?? undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rowToMessage(row: MessageRow): Message {
|
|
86
|
+
return {
|
|
87
|
+
id: row.id,
|
|
88
|
+
sessionId: row.session_id,
|
|
89
|
+
content: row.content,
|
|
90
|
+
sourceCommentId: row.source_comment_id ?? undefined,
|
|
91
|
+
reactionCommentId: row.reaction_comment_id ?? undefined,
|
|
92
|
+
status: row.status as Message["status"],
|
|
93
|
+
attempts: row.attempts,
|
|
94
|
+
error: row.error ?? undefined,
|
|
95
|
+
createdAt: new Date(row.created_at),
|
|
96
|
+
updatedAt: new Date(row.updated_at),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function sessionToTrackerRef(session: OpSession, issue: Issue): TrackerRef {
|
|
101
|
+
return {
|
|
102
|
+
trackerType: issue.trackerType,
|
|
103
|
+
scope: issue.trackerScope,
|
|
104
|
+
issueId: issue.trackerIssueId,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Store (SQLite DAO) ───
|
|
109
|
+
|
|
110
|
+
export class Store {
|
|
111
|
+
private db: Database;
|
|
112
|
+
|
|
113
|
+
private issueStmts!: {
|
|
114
|
+
getById: ReturnType<Database["prepare"]>;
|
|
115
|
+
getByTrackerRef: ReturnType<Database["prepare"]>;
|
|
116
|
+
insert: ReturnType<Database["prepare"]>;
|
|
117
|
+
updateState: ReturnType<Database["prepare"]>;
|
|
118
|
+
updateTitle: ReturnType<Database["prepare"]>;
|
|
119
|
+
listActive: ReturnType<Database["prepare"]>;
|
|
120
|
+
listAll: ReturnType<Database["prepare"]>;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private sessionStmts!: {
|
|
124
|
+
getById: ReturnType<Database["prepare"]>;
|
|
125
|
+
getByName: ReturnType<Database["prepare"]>;
|
|
126
|
+
getByIssue: ReturnType<Database["prepare"]>;
|
|
127
|
+
insert: ReturnType<Database["prepare"]>;
|
|
128
|
+
update: ReturnType<Database["prepare"]>;
|
|
129
|
+
listAll: ReturnType<Database["prepare"]>;
|
|
130
|
+
listNonIdle: ReturnType<Database["prepare"]>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
private msgStmts!: {
|
|
134
|
+
getById: ReturnType<Database["prepare"]>;
|
|
135
|
+
insert: ReturnType<Database["prepare"]>;
|
|
136
|
+
getNextPending: ReturnType<Database["prepare"]>;
|
|
137
|
+
updateStatus: ReturnType<Database["prepare"]>;
|
|
138
|
+
updateStatusSelect: ReturnType<Database["prepare"]>;
|
|
139
|
+
getBySession: ReturnType<Database["prepare"]>;
|
|
140
|
+
getPendingOrRunning: ReturnType<Database["prepare"]>;
|
|
141
|
+
findByCommentId: ReturnType<Database["prepare"]>;
|
|
142
|
+
getRecentBySession: ReturnType<Database["prepare"]>;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
constructor(dbPath?: string) {
|
|
146
|
+
const resolved = dbPath ?? join(
|
|
147
|
+
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
|
|
148
|
+
"ework-daemon",
|
|
149
|
+
"ework-daemon.db"
|
|
150
|
+
);
|
|
151
|
+
const dir = join(resolved, "..");
|
|
152
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
153
|
+
|
|
154
|
+
this.db = new Database(resolved, { create: true });
|
|
155
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
156
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
157
|
+
|
|
158
|
+
this.initSchema();
|
|
159
|
+
this.migrateOldSchema();
|
|
160
|
+
this.prepareStatements();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private initSchema() {
|
|
164
|
+
// Drop old tables with incompatible schemas
|
|
165
|
+
const oldMessages = this.db.prepare(
|
|
166
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='messages'"
|
|
167
|
+
).get() as { sql: string } | null;
|
|
168
|
+
if (oldMessages && oldMessages.sql.includes('op_id')) {
|
|
169
|
+
log.info("store: dropping old messages table (incompatible schema)");
|
|
170
|
+
this.db.exec("DROP TABLE messages");
|
|
171
|
+
}
|
|
172
|
+
const oldSessions = this.db.prepare(
|
|
173
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name='sessions'"
|
|
174
|
+
).get() as { sql: string } | null;
|
|
175
|
+
if (oldSessions) {
|
|
176
|
+
log.info("store: dropping old sessions table");
|
|
177
|
+
this.db.exec("DROP TABLE IF EXISTS sessions");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.db.exec(`
|
|
181
|
+
CREATE TABLE IF NOT EXISTS issues (
|
|
182
|
+
id TEXT PRIMARY KEY,
|
|
183
|
+
tracker_type TEXT NOT NULL,
|
|
184
|
+
tracker_scope_key TEXT NOT NULL,
|
|
185
|
+
tracker_scope TEXT NOT NULL,
|
|
186
|
+
tracker_issue_id TEXT NOT NULL,
|
|
187
|
+
state TEXT NOT NULL DEFAULT 'created',
|
|
188
|
+
title TEXT NOT NULL DEFAULT '',
|
|
189
|
+
created_at TEXT NOT NULL,
|
|
190
|
+
updated_at TEXT NOT NULL,
|
|
191
|
+
UNIQUE(tracker_type, tracker_scope_key, tracker_issue_id)
|
|
192
|
+
)
|
|
193
|
+
`);
|
|
194
|
+
|
|
195
|
+
this.db.exec(`
|
|
196
|
+
CREATE TABLE IF NOT EXISTS op_sessions (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
issue_id TEXT NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
|
|
199
|
+
name TEXT NOT NULL,
|
|
200
|
+
state TEXT NOT NULL DEFAULT 'idle',
|
|
201
|
+
opencode_session_id TEXT,
|
|
202
|
+
opencode_pid INTEGER,
|
|
203
|
+
workdir TEXT,
|
|
204
|
+
created_at TEXT NOT NULL,
|
|
205
|
+
started_at INTEGER,
|
|
206
|
+
progress_comment_id TEXT,
|
|
207
|
+
reaction_comment_id TEXT,
|
|
208
|
+
current_prompt TEXT,
|
|
209
|
+
UNIQUE(issue_id, name)
|
|
210
|
+
)
|
|
211
|
+
`);
|
|
212
|
+
|
|
213
|
+
this.db.exec(`
|
|
214
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
215
|
+
id TEXT PRIMARY KEY,
|
|
216
|
+
session_id TEXT NOT NULL REFERENCES op_sessions(id) ON DELETE CASCADE,
|
|
217
|
+
content TEXT NOT NULL,
|
|
218
|
+
source_comment_id TEXT,
|
|
219
|
+
reaction_comment_id TEXT,
|
|
220
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
221
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
222
|
+
error TEXT,
|
|
223
|
+
created_at TEXT NOT NULL,
|
|
224
|
+
updated_at TEXT NOT NULL
|
|
225
|
+
)
|
|
226
|
+
`);
|
|
227
|
+
|
|
228
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_sessions_issue ON op_sessions(issue_id)");
|
|
229
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)");
|
|
230
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_messages_status ON messages(status)");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private migrateOldSchema() {
|
|
234
|
+
const hasOldOps = this.db.prepare(
|
|
235
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='ops'"
|
|
236
|
+
).get();
|
|
237
|
+
if (!hasOldOps) return;
|
|
238
|
+
|
|
239
|
+
log.info("store: migrating old ops table to issues + op_sessions");
|
|
240
|
+
|
|
241
|
+
this.db.exec(`
|
|
242
|
+
INSERT OR IGNORE INTO issues (id, tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id, state, title, created_at, updated_at)
|
|
243
|
+
SELECT
|
|
244
|
+
lower(hex(randomblob(4))),
|
|
245
|
+
tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id,
|
|
246
|
+
CASE WHEN status = 'closed' THEN 'closed' ELSE 'active' END,
|
|
247
|
+
'',
|
|
248
|
+
created_at, last_activity_at
|
|
249
|
+
FROM ops
|
|
250
|
+
WHERE tracker_type IS NOT NULL
|
|
251
|
+
`);
|
|
252
|
+
|
|
253
|
+
this.db.exec(`DROP TABLE IF EXISTS ops`);
|
|
254
|
+
this.db.exec(`DROP TABLE IF EXISTS sessions`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private prepareStatements() {
|
|
258
|
+
this.issueStmts = {
|
|
259
|
+
getById: this.db.prepare("SELECT * FROM issues WHERE id = ?"),
|
|
260
|
+
getByTrackerRef: this.db.prepare(
|
|
261
|
+
"SELECT * FROM issues WHERE tracker_type = ? AND tracker_scope_key = ? AND tracker_issue_id = ?"
|
|
262
|
+
),
|
|
263
|
+
insert: this.db.prepare(
|
|
264
|
+
"INSERT OR IGNORE INTO issues (id, tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id, state, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
265
|
+
),
|
|
266
|
+
updateState: this.db.prepare("UPDATE issues SET state = ?, updated_at = ? WHERE id = ?"),
|
|
267
|
+
updateTitle: this.db.prepare("UPDATE issues SET title = ?, updated_at = ? WHERE id = ?"),
|
|
268
|
+
listActive: this.db.prepare("SELECT * FROM issues WHERE state != 'closed'"),
|
|
269
|
+
listAll: this.db.prepare("SELECT * FROM issues"),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
this.sessionStmts = {
|
|
273
|
+
getById: this.db.prepare("SELECT * FROM op_sessions WHERE id = ?"),
|
|
274
|
+
getByName: this.db.prepare(
|
|
275
|
+
"SELECT * FROM op_sessions WHERE issue_id = ? AND name = ?"
|
|
276
|
+
),
|
|
277
|
+
getByIssue: this.db.prepare(
|
|
278
|
+
"SELECT * FROM op_sessions WHERE issue_id = ? ORDER BY created_at"
|
|
279
|
+
),
|
|
280
|
+
insert: this.db.prepare(
|
|
281
|
+
"INSERT OR IGNORE INTO op_sessions (id, issue_id, name, state, opencode_session_id, opencode_pid, workdir, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
282
|
+
),
|
|
283
|
+
update: this.db.prepare(
|
|
284
|
+
`UPDATE op_sessions SET state = ?, opencode_session_id = ?, opencode_pid = ?, workdir = ?,
|
|
285
|
+
started_at = ?, progress_comment_id = ?, reaction_comment_id = ?, current_prompt = ? WHERE id = ?`
|
|
286
|
+
),
|
|
287
|
+
listAll: this.db.prepare("SELECT * FROM op_sessions"),
|
|
288
|
+
listNonIdle: this.db.prepare("SELECT * FROM op_sessions WHERE state != 'idle'"),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
this.msgStmts = {
|
|
292
|
+
getById: this.db.prepare("SELECT * FROM messages WHERE id = ?"),
|
|
293
|
+
insert: this.db.prepare(
|
|
294
|
+
"INSERT OR IGNORE INTO messages (id, session_id, content, source_comment_id, reaction_comment_id, status, attempts, error, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
295
|
+
),
|
|
296
|
+
getNextPending: this.db.prepare(
|
|
297
|
+
"SELECT * FROM messages WHERE session_id = ? AND status = 'pending' ORDER BY created_at ASC LIMIT 1"
|
|
298
|
+
),
|
|
299
|
+
updateStatus: this.db.prepare(
|
|
300
|
+
"UPDATE messages SET status = ?, attempts = ?, error = ?, updated_at = ? WHERE id = ?"
|
|
301
|
+
),
|
|
302
|
+
updateStatusSelect: this.db.prepare("SELECT * FROM messages WHERE id = ?"),
|
|
303
|
+
getBySession: this.db.prepare(
|
|
304
|
+
"SELECT * FROM messages WHERE session_id = ? ORDER BY created_at DESC"
|
|
305
|
+
),
|
|
306
|
+
getPendingOrRunning: this.db.prepare(
|
|
307
|
+
"SELECT * FROM messages WHERE status IN ('pending', 'running') ORDER BY created_at ASC"
|
|
308
|
+
),
|
|
309
|
+
findByCommentId: this.db.prepare(
|
|
310
|
+
"SELECT * FROM messages WHERE source_comment_id = ?"
|
|
311
|
+
),
|
|
312
|
+
getRecentBySession: this.db.prepare(
|
|
313
|
+
"SELECT * FROM messages WHERE session_id = ? ORDER BY created_at DESC LIMIT ?"
|
|
314
|
+
),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ─── Issues ───
|
|
319
|
+
|
|
320
|
+
getIssue(id: string): Issue | undefined {
|
|
321
|
+
const row = this.issueStmts.getById.get(id) as IssueRow | null;
|
|
322
|
+
return row ? rowToIssue(row) : undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
findIssue(trackerType: string, scopeKey: string, issueId: string): Issue | undefined {
|
|
326
|
+
const row = this.issueStmts.getByTrackerRef.get(trackerType, scopeKey, issueId) as IssueRow | null;
|
|
327
|
+
return row ? rowToIssue(row) : undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
findOrCreateIssue(ref: TrackerRef, scopeKey: string, title: string): Issue {
|
|
331
|
+
const existing = this.findIssue(ref.trackerType, scopeKey, ref.issueId);
|
|
332
|
+
if (existing) {
|
|
333
|
+
if (title && existing.title !== title) {
|
|
334
|
+
this.issueStmts.updateTitle.run(title, new Date().toISOString(), existing.id);
|
|
335
|
+
existing.title = title;
|
|
336
|
+
}
|
|
337
|
+
return existing;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const now = new Date();
|
|
341
|
+
const id = crypto.randomUUID();
|
|
342
|
+
this.issueStmts.insert.run(
|
|
343
|
+
id, ref.trackerType, scopeKey,
|
|
344
|
+
JSON.stringify(ref.scope), ref.issueId,
|
|
345
|
+
"created", title, now.toISOString(), now.toISOString()
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Re-read in case INSERT OR IGNORE hit a concurrent insert
|
|
349
|
+
const inserted = this.findIssue(ref.trackerType, scopeKey, ref.issueId);
|
|
350
|
+
if (inserted) return inserted;
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
id, trackerType: ref.trackerType, trackerScope: ref.scope,
|
|
354
|
+
trackerScopeKey: scopeKey, trackerIssueId: ref.issueId,
|
|
355
|
+
state: "created", title, createdAt: now, updatedAt: now,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
updateIssueState(id: string, state: IssueState) {
|
|
360
|
+
this.issueStmts.updateState.run(state, new Date().toISOString(), id);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
listActiveIssues(): Issue[] {
|
|
364
|
+
return (this.issueStmts.listActive.all() as IssueRow[]).map(rowToIssue);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
listAllIssues(): Issue[] {
|
|
368
|
+
return (this.issueStmts.listAll.all() as IssueRow[]).map(rowToIssue);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── OpSessions ───
|
|
372
|
+
|
|
373
|
+
getSession(id: string): OpSession | undefined {
|
|
374
|
+
const row = this.sessionStmts.getById.get(id) as SessionRow | null;
|
|
375
|
+
return row ? rowToSession(row) : undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getSessionByName(issueId: string, name: string): OpSession | undefined {
|
|
379
|
+
const row = this.sessionStmts.getByName.get(issueId, name) as SessionRow | null;
|
|
380
|
+
return row ? rowToSession(row) : undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getSessionsForIssue(issueId: string): OpSession[] {
|
|
384
|
+
return (this.sessionStmts.getByIssue.all(issueId) as SessionRow[]).map(rowToSession);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
createSession(issueId: string, name: string): OpSession {
|
|
388
|
+
const existing = this.getSessionByName(issueId, name);
|
|
389
|
+
if (existing) return existing;
|
|
390
|
+
|
|
391
|
+
const session: OpSession = {
|
|
392
|
+
id: crypto.randomUUID(),
|
|
393
|
+
issueId,
|
|
394
|
+
name,
|
|
395
|
+
state: "idle",
|
|
396
|
+
createdAt: new Date(),
|
|
397
|
+
};
|
|
398
|
+
this.sessionStmts.insert.run(
|
|
399
|
+
session.id, session.issueId, session.name, session.state,
|
|
400
|
+
null, null, null, session.createdAt.toISOString()
|
|
401
|
+
);
|
|
402
|
+
return session;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
updateSession(id: string, patch: Partial<OpSession>): OpSession | undefined {
|
|
406
|
+
const row = this.sessionStmts.getById.get(id) as SessionRow | null;
|
|
407
|
+
if (!row) return undefined;
|
|
408
|
+
const existing = rowToSession(row);
|
|
409
|
+
const updated = { ...existing, ...patch };
|
|
410
|
+
|
|
411
|
+
this.sessionStmts.update.run(
|
|
412
|
+
updated.state,
|
|
413
|
+
updated.opencodeSessionId ?? null,
|
|
414
|
+
updated.opencodePid ?? null,
|
|
415
|
+
updated.workdir ?? null,
|
|
416
|
+
updated.startedAt ?? null,
|
|
417
|
+
updated.progressCommentId ?? null,
|
|
418
|
+
updated.reactionCommentId ?? null,
|
|
419
|
+
updated.currentPrompt ?? null,
|
|
420
|
+
id
|
|
421
|
+
);
|
|
422
|
+
return updated;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
listAllSessions(): OpSession[] {
|
|
426
|
+
return (this.sessionStmts.listAll.all() as SessionRow[]).map(rowToSession);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
listNonIdleSessions(): OpSession[] {
|
|
430
|
+
return (this.sessionStmts.listNonIdle.all() as SessionRow[]).map(rowToSession);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ─── Messages ───
|
|
434
|
+
|
|
435
|
+
createMessage(sessionId: string, content: string, sourceCommentId?: string, reactionCommentId?: string): Message {
|
|
436
|
+
const now = new Date().toISOString();
|
|
437
|
+
const id = crypto.randomUUID();
|
|
438
|
+
this.msgStmts.insert.run(
|
|
439
|
+
id, sessionId, content,
|
|
440
|
+
sourceCommentId ?? null, reactionCommentId ?? null,
|
|
441
|
+
"pending", 0, null, now, now
|
|
442
|
+
);
|
|
443
|
+
return {
|
|
444
|
+
id, sessionId, content, sourceCommentId, reactionCommentId,
|
|
445
|
+
status: "pending", attempts: 0,
|
|
446
|
+
createdAt: new Date(now), updatedAt: new Date(now),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
getMessage(id: string): Message | undefined {
|
|
451
|
+
const row = this.msgStmts.getById.get(id) as MessageRow | null;
|
|
452
|
+
return row ? rowToMessage(row) : undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
getNextPendingMessage(sessionId: string): Message | undefined {
|
|
456
|
+
const row = this.msgStmts.getNextPending.get(sessionId) as MessageRow | null;
|
|
457
|
+
return row ? rowToMessage(row) : undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
updateMessageStatus(id: string, status: Message["status"], error?: string) {
|
|
461
|
+
const row = this.msgStmts.updateStatusSelect.get(id) as MessageRow | null;
|
|
462
|
+
const attempts = row ? row.attempts + (status === "failed" ? 1 : 0) : 0;
|
|
463
|
+
this.msgStmts.updateStatus.run(status, attempts, error ?? null, new Date().toISOString(), id);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
getMessagesForSession(sessionId: string): Message[] {
|
|
467
|
+
return (this.msgStmts.getBySession.all(sessionId) as MessageRow[]).map(rowToMessage);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
getRecentMessages(sessionId: string, limit: number): Message[] {
|
|
471
|
+
return (this.msgStmts.getRecentBySession.all(sessionId, limit) as MessageRow[]).map(rowToMessage);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
getPendingOrRunningMessages(): Message[] {
|
|
475
|
+
return (this.msgStmts.getPendingOrRunning.all() as MessageRow[]).map(rowToMessage);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
findMessageByCommentId(commentId: string): Message | undefined {
|
|
479
|
+
const row = this.msgStmts.findByCommentId.get(commentId) as MessageRow | null;
|
|
480
|
+
return row ? rowToMessage(row) : undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
close() {
|
|
484
|
+
this.db.close();
|
|
485
|
+
}
|
|
486
|
+
}
|