ework-daemon 0.4.5 → 0.4.7
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/package.json +1 -1
- package/src/db.ts +153 -0
- package/src/op.ts +23 -23
- package/src/opencode.ts +3 -2
- package/src/schema-mysql.sql +8 -5
- package/src/schema-sqlite.sql +8 -5
- package/src/server.ts +8 -1
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -323,6 +323,159 @@ async function runMigrations(db: AsyncDatabase): Promise<void> {
|
|
|
323
323
|
await db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
+
// ── id/uid swap: id becomes AUTO_INCREMENT PK, uid holds the UUID ──
|
|
327
|
+
// SQLite cannot ADD PRIMARY KEY via ALTER — must rebuild. Runs before
|
|
328
|
+
// owner_daemon_id etc. so rebuild only copies base columns; ephemeral
|
|
329
|
+
// coordination data (owner_daemon_id, nudge state) is re-added below.
|
|
330
|
+
const tMessages = `${prefix}messages`;
|
|
331
|
+
|
|
332
|
+
const UID_REBUILDS: {
|
|
333
|
+
table: string;
|
|
334
|
+
sqliteCreate: string;
|
|
335
|
+
mysqlCreate: string;
|
|
336
|
+
oldCols: string;
|
|
337
|
+
newCols: string;
|
|
338
|
+
}[] = [
|
|
339
|
+
{
|
|
340
|
+
table: tIssues,
|
|
341
|
+
sqliteCreate: `CREATE TABLE ${tIssues} (
|
|
342
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
343
|
+
uid TEXT NOT NULL UNIQUE,
|
|
344
|
+
tracker_type TEXT NOT NULL,
|
|
345
|
+
tracker_scope_key TEXT NOT NULL,
|
|
346
|
+
tracker_scope TEXT NOT NULL,
|
|
347
|
+
tracker_issue_id TEXT NOT NULL,
|
|
348
|
+
state TEXT NOT NULL DEFAULT 'created',
|
|
349
|
+
title TEXT NOT NULL DEFAULT '',
|
|
350
|
+
created_at TEXT NOT NULL,
|
|
351
|
+
updated_at TEXT NOT NULL,
|
|
352
|
+
UNIQUE(tracker_type, tracker_scope_key, tracker_issue_id)
|
|
353
|
+
)`,
|
|
354
|
+
mysqlCreate: `CREATE TABLE ${tIssues} (
|
|
355
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
356
|
+
uid VARCHAR(36) NOT NULL UNIQUE,
|
|
357
|
+
tracker_type VARCHAR(64) NOT NULL,
|
|
358
|
+
tracker_scope_key VARCHAR(255) NOT NULL,
|
|
359
|
+
tracker_scope TEXT NOT NULL,
|
|
360
|
+
tracker_issue_id VARCHAR(64) NOT NULL,
|
|
361
|
+
state VARCHAR(16) NOT NULL DEFAULT 'created',
|
|
362
|
+
title VARCHAR(512) NOT NULL DEFAULT '',
|
|
363
|
+
created_at VARCHAR(40) NOT NULL,
|
|
364
|
+
updated_at VARCHAR(40) NOT NULL,
|
|
365
|
+
UNIQUE (tracker_type, tracker_scope_key, tracker_issue_id)
|
|
366
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
|
367
|
+
oldCols:
|
|
368
|
+
"id, tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id, state, title, created_at, updated_at",
|
|
369
|
+
newCols:
|
|
370
|
+
"uid, tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id, state, title, created_at, updated_at",
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
table: tSessions,
|
|
374
|
+
sqliteCreate: `CREATE TABLE ${tSessions} (
|
|
375
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
376
|
+
uid TEXT NOT NULL UNIQUE,
|
|
377
|
+
issue_id TEXT NOT NULL REFERENCES ${tIssues}(uid) ON DELETE CASCADE,
|
|
378
|
+
name TEXT NOT NULL,
|
|
379
|
+
state TEXT NOT NULL DEFAULT 'idle',
|
|
380
|
+
opencode_session_id TEXT,
|
|
381
|
+
opencode_pid INTEGER,
|
|
382
|
+
workdir TEXT,
|
|
383
|
+
created_at TEXT NOT NULL,
|
|
384
|
+
started_at INTEGER,
|
|
385
|
+
progress_comment_id TEXT,
|
|
386
|
+
reaction_comment_id TEXT,
|
|
387
|
+
current_prompt TEXT,
|
|
388
|
+
UNIQUE(issue_id, name)
|
|
389
|
+
)`,
|
|
390
|
+
mysqlCreate: `CREATE TABLE ${tSessions} (
|
|
391
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
392
|
+
uid VARCHAR(36) NOT NULL UNIQUE,
|
|
393
|
+
issue_id VARCHAR(36) NOT NULL,
|
|
394
|
+
name VARCHAR(64) NOT NULL,
|
|
395
|
+
state VARCHAR(16) NOT NULL DEFAULT 'idle',
|
|
396
|
+
opencode_session_id VARCHAR(64),
|
|
397
|
+
opencode_pid BIGINT,
|
|
398
|
+
workdir VARCHAR(1024),
|
|
399
|
+
created_at VARCHAR(40) NOT NULL,
|
|
400
|
+
started_at BIGINT,
|
|
401
|
+
progress_comment_id VARCHAR(64),
|
|
402
|
+
reaction_comment_id VARCHAR(64),
|
|
403
|
+
current_prompt TEXT,
|
|
404
|
+
UNIQUE (issue_id, name)
|
|
405
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
|
406
|
+
oldCols:
|
|
407
|
+
"id, issue_id, name, state, opencode_session_id, opencode_pid, workdir, created_at, started_at, progress_comment_id, reaction_comment_id, current_prompt",
|
|
408
|
+
newCols:
|
|
409
|
+
"uid, issue_id, name, state, opencode_session_id, opencode_pid, workdir, created_at, started_at, progress_comment_id, reaction_comment_id, current_prompt",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
table: tMessages,
|
|
413
|
+
sqliteCreate: `CREATE TABLE ${tMessages} (
|
|
414
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
415
|
+
uid TEXT NOT NULL UNIQUE,
|
|
416
|
+
session_id TEXT NOT NULL REFERENCES ${tSessions}(uid) ON DELETE CASCADE,
|
|
417
|
+
content TEXT NOT NULL,
|
|
418
|
+
source_comment_id TEXT,
|
|
419
|
+
reaction_comment_id TEXT,
|
|
420
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
421
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
422
|
+
error TEXT,
|
|
423
|
+
created_at TEXT NOT NULL,
|
|
424
|
+
updated_at TEXT NOT NULL
|
|
425
|
+
)`,
|
|
426
|
+
mysqlCreate: `CREATE TABLE ${tMessages} (
|
|
427
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
428
|
+
uid VARCHAR(36) NOT NULL UNIQUE,
|
|
429
|
+
session_id VARCHAR(36) NOT NULL,
|
|
430
|
+
content LONGTEXT NOT NULL,
|
|
431
|
+
source_comment_id VARCHAR(64),
|
|
432
|
+
reaction_comment_id VARCHAR(64),
|
|
433
|
+
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
|
434
|
+
attempts INT NOT NULL DEFAULT 0,
|
|
435
|
+
error TEXT,
|
|
436
|
+
created_at VARCHAR(40) NOT NULL,
|
|
437
|
+
updated_at VARCHAR(40) NOT NULL
|
|
438
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
|
439
|
+
oldCols:
|
|
440
|
+
"id, session_id, content, source_comment_id, reaction_comment_id, status, attempts, error, created_at, updated_at",
|
|
441
|
+
newCols:
|
|
442
|
+
"uid, session_id, content, source_comment_id, reaction_comment_id, status, attempts, error, created_at, updated_at",
|
|
443
|
+
},
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
for (const { table, sqliteCreate, mysqlCreate, oldCols, newCols } of UID_REBUILDS) {
|
|
447
|
+
if (await hasColumn(table, "uid")) continue;
|
|
448
|
+
if (sqlite) {
|
|
449
|
+
const exists = await db.get<{ cnt: number }>(
|
|
450
|
+
`SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type='table' AND name='${table}'`
|
|
451
|
+
);
|
|
452
|
+
if (Number(exists?.cnt ?? 0) === 0) continue;
|
|
453
|
+
await db.exec("PRAGMA foreign_keys = OFF");
|
|
454
|
+
try {
|
|
455
|
+
await db.transaction(async () => {
|
|
456
|
+
await db.exec(`ALTER TABLE ${table} RENAME TO ${table}__old_id`);
|
|
457
|
+
await db.exec(sqliteCreate);
|
|
458
|
+
await db.exec(`INSERT INTO ${table} (${newCols}) SELECT ${oldCols} FROM ${table}__old_id`);
|
|
459
|
+
await db.exec(`DROP TABLE ${table}__old_id`);
|
|
460
|
+
});
|
|
461
|
+
} finally {
|
|
462
|
+
await db.exec("PRAGMA foreign_keys = ON");
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
try {
|
|
466
|
+
await db.exec("SET FOREIGN_KEY_CHECKS = 0");
|
|
467
|
+
await db.transaction(async () => {
|
|
468
|
+
await db.exec(`ALTER TABLE ${table} RENAME TO ${table}__old_id`);
|
|
469
|
+
await db.exec(mysqlCreate);
|
|
470
|
+
await db.exec(`INSERT INTO ${table} (${newCols}) SELECT ${oldCols} FROM ${table}__old_id`);
|
|
471
|
+
await db.exec(`DROP TABLE ${table}__old_id`);
|
|
472
|
+
});
|
|
473
|
+
} finally {
|
|
474
|
+
await db.exec("SET FOREIGN_KEY_CHECKS = 1");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
326
479
|
// issues.owner_daemon_id — points at the leasing daemon (nullable = unclaimed).
|
|
327
480
|
await ensureColumn(
|
|
328
481
|
tIssues,
|
package/src/op.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { TrackerRef, Issue, IssueState, OpSession, SessionState, Message }
|
|
|
5
5
|
// ─── Row Types ───
|
|
6
6
|
|
|
7
7
|
interface IssueRow {
|
|
8
|
-
|
|
8
|
+
uid: string;
|
|
9
9
|
tracker_type: string;
|
|
10
10
|
tracker_scope_key: string;
|
|
11
11
|
tracker_scope: string;
|
|
@@ -18,7 +18,7 @@ interface IssueRow {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface SessionRow {
|
|
21
|
-
|
|
21
|
+
uid: string;
|
|
22
22
|
issue_id: string;
|
|
23
23
|
name: string;
|
|
24
24
|
state: string;
|
|
@@ -37,7 +37,7 @@ interface SessionRow {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
interface MessageRow {
|
|
40
|
-
|
|
40
|
+
uid: string;
|
|
41
41
|
session_id: string;
|
|
42
42
|
content: string;
|
|
43
43
|
source_comment_id: string | null;
|
|
@@ -55,7 +55,7 @@ function rowToIssue(row: IssueRow): Issue {
|
|
|
55
55
|
let scope: Record<string, string>;
|
|
56
56
|
try { scope = JSON.parse(row.tracker_scope); } catch { scope = {}; }
|
|
57
57
|
return {
|
|
58
|
-
id: row.
|
|
58
|
+
id: row.uid,
|
|
59
59
|
trackerType: row.tracker_type,
|
|
60
60
|
trackerScope: scope,
|
|
61
61
|
trackerScopeKey: row.tracker_scope_key,
|
|
@@ -70,7 +70,7 @@ function rowToIssue(row: IssueRow): Issue {
|
|
|
70
70
|
|
|
71
71
|
function rowToSession(row: SessionRow): OpSession {
|
|
72
72
|
return {
|
|
73
|
-
id: row.
|
|
73
|
+
id: row.uid,
|
|
74
74
|
issueId: row.issue_id,
|
|
75
75
|
name: row.name,
|
|
76
76
|
state: row.state as SessionState,
|
|
@@ -91,7 +91,7 @@ function rowToSession(row: SessionRow): OpSession {
|
|
|
91
91
|
|
|
92
92
|
function rowToMessage(row: MessageRow): Message {
|
|
93
93
|
return {
|
|
94
|
-
id: row.
|
|
94
|
+
id: row.uid,
|
|
95
95
|
sessionId: row.session_id,
|
|
96
96
|
content: row.content,
|
|
97
97
|
sourceCommentId: row.source_comment_id ?? undefined,
|
|
@@ -121,7 +121,7 @@ export class Store {
|
|
|
121
121
|
// ─── Issues ───
|
|
122
122
|
|
|
123
123
|
async getIssue(id: string): Promise<Issue | undefined> {
|
|
124
|
-
const row = await getDB().get<IssueRow>("SELECT * FROM {{issues}} WHERE
|
|
124
|
+
const row = await getDB().get<IssueRow>("SELECT * FROM {{issues}} WHERE uid = ?", [id]);
|
|
125
125
|
return row ? rowToIssue(row) : undefined;
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -137,7 +137,7 @@ export class Store {
|
|
|
137
137
|
const existing = await this.findIssue(ref.trackerType, scopeKey, ref.issueId);
|
|
138
138
|
if (existing) {
|
|
139
139
|
if (title && existing.title !== title) {
|
|
140
|
-
await getDB().run("UPDATE {{issues}} SET title = ?, updated_at = ? WHERE
|
|
140
|
+
await getDB().run("UPDATE {{issues}} SET title = ?, updated_at = ? WHERE uid = ?", [
|
|
141
141
|
title,
|
|
142
142
|
new Date().toISOString(),
|
|
143
143
|
existing.id,
|
|
@@ -150,7 +150,7 @@ export class Store {
|
|
|
150
150
|
const now = new Date();
|
|
151
151
|
const id = crypto.randomUUID();
|
|
152
152
|
await getDB().run(
|
|
153
|
-
"INSERT OR IGNORE INTO {{issues}} (
|
|
153
|
+
"INSERT OR IGNORE INTO {{issues}} (uid, tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id, state, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
154
154
|
[id, ref.trackerType, scopeKey, JSON.stringify(ref.scope), ref.issueId, "created", title, now.toISOString(), now.toISOString()]
|
|
155
155
|
);
|
|
156
156
|
|
|
@@ -166,7 +166,7 @@ export class Store {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
async updateIssueState(id: string, state: IssueState): Promise<void> {
|
|
169
|
-
await getDB().run("UPDATE {{issues}} SET state = ?, updated_at = ? WHERE
|
|
169
|
+
await getDB().run("UPDATE {{issues}} SET state = ?, updated_at = ? WHERE uid = ?", [
|
|
170
170
|
state,
|
|
171
171
|
new Date().toISOString(),
|
|
172
172
|
id,
|
|
@@ -186,7 +186,7 @@ export class Store {
|
|
|
186
186
|
// ─── OpSessions ───
|
|
187
187
|
|
|
188
188
|
async getSession(id: string): Promise<OpSession | undefined> {
|
|
189
|
-
const row = await getDB().get<SessionRow>("SELECT * FROM {{op_sessions}} WHERE
|
|
189
|
+
const row = await getDB().get<SessionRow>("SELECT * FROM {{op_sessions}} WHERE uid = ?", [id]);
|
|
190
190
|
return row ? rowToSession(row) : undefined;
|
|
191
191
|
}
|
|
192
192
|
|
|
@@ -218,14 +218,14 @@ export class Store {
|
|
|
218
218
|
createdAt: new Date(),
|
|
219
219
|
};
|
|
220
220
|
await getDB().run(
|
|
221
|
-
"INSERT OR IGNORE INTO {{op_sessions}} (
|
|
221
|
+
"INSERT OR IGNORE INTO {{op_sessions}} (uid, issue_id, name, state, opencode_session_id, opencode_pid, workdir, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
222
222
|
[session.id, session.issueId, session.name, session.state, null, null, null, session.createdAt.toISOString()]
|
|
223
223
|
);
|
|
224
224
|
return session;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
async updateSession(id: string, patch: Partial<OpSession>): Promise<OpSession | undefined> {
|
|
228
|
-
const row = await getDB().get<SessionRow>("SELECT * FROM {{op_sessions}} WHERE
|
|
228
|
+
const row = await getDB().get<SessionRow>("SELECT * FROM {{op_sessions}} WHERE uid = ?", [id]);
|
|
229
229
|
if (!row) return undefined;
|
|
230
230
|
const existing = rowToSession(row);
|
|
231
231
|
const updated = { ...existing, ...patch };
|
|
@@ -233,7 +233,7 @@ export class Store {
|
|
|
233
233
|
await getDB().run(
|
|
234
234
|
`UPDATE {{op_sessions}} SET state = ?, opencode_session_id = ?, opencode_pid = ?, workdir = ?,
|
|
235
235
|
started_at = ?, progress_comment_id = ?, reaction_comment_id = ?, current_prompt = ?,
|
|
236
|
-
last_output_at = ?, nudge_rounds = ?, stuck_nudge_rounds = ?, generation = ? WHERE
|
|
236
|
+
last_output_at = ?, nudge_rounds = ?, stuck_nudge_rounds = ?, generation = ? WHERE uid = ?`,
|
|
237
237
|
[
|
|
238
238
|
updated.state,
|
|
239
239
|
updated.opencodeSessionId ?? null,
|
|
@@ -275,7 +275,7 @@ export class Store {
|
|
|
275
275
|
const now = new Date().toISOString();
|
|
276
276
|
const id = crypto.randomUUID();
|
|
277
277
|
await getDB().run(
|
|
278
|
-
"INSERT OR IGNORE INTO {{messages}} (
|
|
278
|
+
"INSERT OR IGNORE INTO {{messages}} (uid, session_id, content, source_comment_id, reaction_comment_id, status, attempts, error, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
279
279
|
[id, sessionId, content, sourceCommentId ?? null, reactionCommentId ?? null, "pending", 0, null, now, now]
|
|
280
280
|
);
|
|
281
281
|
return {
|
|
@@ -287,7 +287,7 @@ export class Store {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
async getMessage(id: string): Promise<Message | undefined> {
|
|
290
|
-
const row = await getDB().get<MessageRow>("SELECT * FROM {{messages}} WHERE
|
|
290
|
+
const row = await getDB().get<MessageRow>("SELECT * FROM {{messages}} WHERE uid = ?", [id]);
|
|
291
291
|
return row ? rowToMessage(row) : undefined;
|
|
292
292
|
}
|
|
293
293
|
|
|
@@ -300,10 +300,10 @@ export class Store {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
async updateMessageStatus(id: string, status: Message["status"], error?: string): Promise<void> {
|
|
303
|
-
const row = await getDB().get<MessageRow>("SELECT * FROM {{messages}} WHERE
|
|
303
|
+
const row = await getDB().get<MessageRow>("SELECT * FROM {{messages}} WHERE uid = ?", [id]);
|
|
304
304
|
const attempts = row ? row.attempts + (status === "failed" ? 1 : 0) : 0;
|
|
305
305
|
await getDB().run(
|
|
306
|
-
"UPDATE {{messages}} SET status = ?, attempts = ?, error = ?, updated_at = ? WHERE
|
|
306
|
+
"UPDATE {{messages}} SET status = ?, attempts = ?, error = ?, updated_at = ? WHERE uid = ?",
|
|
307
307
|
[status, attempts, error ?? null, new Date().toISOString(), id]
|
|
308
308
|
);
|
|
309
309
|
}
|
|
@@ -436,7 +436,7 @@ export class Store {
|
|
|
436
436
|
*/
|
|
437
437
|
async claimIssue(issueId: string, daemonId: number): Promise<boolean> {
|
|
438
438
|
const res = await getDB().run(
|
|
439
|
-
"UPDATE {{issues}} SET owner_daemon_id = ? WHERE
|
|
439
|
+
"UPDATE {{issues}} SET owner_daemon_id = ? WHERE uid = ? AND owner_daemon_id IS NULL",
|
|
440
440
|
[daemonId, issueId]
|
|
441
441
|
);
|
|
442
442
|
return res.changes === 1;
|
|
@@ -454,7 +454,7 @@ export class Store {
|
|
|
454
454
|
/** Atomic message claim: pending → running. False = lost or already done. */
|
|
455
455
|
async claimMessage(messageId: string): Promise<boolean> {
|
|
456
456
|
const res = await getDB().run(
|
|
457
|
-
"UPDATE {{messages}} SET status = 'running', updated_at = ? WHERE
|
|
457
|
+
"UPDATE {{messages}} SET status = 'running', updated_at = ? WHERE uid = ? AND status = 'pending'",
|
|
458
458
|
[new Date().toISOString(), messageId]
|
|
459
459
|
);
|
|
460
460
|
return res.changes === 1;
|
|
@@ -471,7 +471,7 @@ export class Store {
|
|
|
471
471
|
async listOwnedSessions(daemonId: number): Promise<OpSession[]> {
|
|
472
472
|
const rows = await getDB().all<SessionRow>(
|
|
473
473
|
`SELECT s.* FROM {{op_sessions}} s
|
|
474
|
-
INNER JOIN {{issues}} i ON i.
|
|
474
|
+
INNER JOIN {{issues}} i ON i.uid = s.issue_id
|
|
475
475
|
WHERE i.owner_daemon_id = ?
|
|
476
476
|
ORDER BY s.created_at`,
|
|
477
477
|
[daemonId]
|
|
@@ -483,8 +483,8 @@ export class Store {
|
|
|
483
483
|
async getOwnedPendingOrRunningMessages(daemonId: number): Promise<Message[]> {
|
|
484
484
|
const rows = await getDB().all<MessageRow>(
|
|
485
485
|
`SELECT m.* FROM {{messages}} m
|
|
486
|
-
INNER JOIN {{op_sessions}} s ON s.
|
|
487
|
-
INNER JOIN {{issues}} i ON i.
|
|
486
|
+
INNER JOIN {{op_sessions}} s ON s.uid = m.session_id
|
|
487
|
+
INNER JOIN {{issues}} i ON i.uid = s.issue_id
|
|
488
488
|
WHERE i.owner_daemon_id = ? AND m.status IN ('pending', 'running')
|
|
489
489
|
ORDER BY m.created_at ASC`,
|
|
490
490
|
[daemonId]
|
package/src/opencode.ts
CHANGED
|
@@ -63,8 +63,9 @@ export function resolveTemplatedWorkdir(
|
|
|
63
63
|
* hung scripts. Failures are logged and swallowed — never throws — so a broken
|
|
64
64
|
* init/destroy never blocks the opencode task flow. */
|
|
65
65
|
const HOOK_SCRIPT_TIMEOUT_MS = 60_000;
|
|
66
|
-
export async function runHookScript(script: string | undefined, workdir: string, label: string, env: Record<string, string> = {}): Promise<void> {
|
|
66
|
+
export async function runHookScript(script: string | undefined, workdir: string, label: string, env: Record<string, string> = {}, timeoutMs?: number): Promise<void> {
|
|
67
67
|
if (!script || !script.trim()) return;
|
|
68
|
+
const effectiveTimeout = timeoutMs ?? HOOK_SCRIPT_TIMEOUT_MS;
|
|
68
69
|
try {
|
|
69
70
|
mkdirSync(workdir, { recursive: true });
|
|
70
71
|
const proc = Bun.spawn({
|
|
@@ -74,7 +75,7 @@ export async function runHookScript(script: string | undefined, workdir: string,
|
|
|
74
75
|
stderr: "pipe",
|
|
75
76
|
env: { ...process.env, ...env },
|
|
76
77
|
});
|
|
77
|
-
const timer = setTimeout(() => { try { proc.kill("SIGKILL"); } catch { /* already dead */ } },
|
|
78
|
+
const timer = setTimeout(() => { try { proc.kill("SIGKILL"); } catch { /* already dead */ } }, effectiveTimeout);
|
|
78
79
|
try {
|
|
79
80
|
const exitCode = await proc.exited;
|
|
80
81
|
const stderr = await new Response(proc.stderr).text().catch(() => "");
|
package/src/schema-mysql.sql
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
-- tables InnoDB + utf8mb4 for FK CASCADE + full Unicode (emoji).
|
|
9
9
|
|
|
10
10
|
CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
11
|
-
id
|
|
11
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
12
|
+
uid VARCHAR(36) NOT NULL UNIQUE,
|
|
12
13
|
tracker_type VARCHAR(64) NOT NULL,
|
|
13
14
|
tracker_scope_key VARCHAR(255) NOT NULL,
|
|
14
15
|
tracker_scope TEXT NOT NULL,
|
|
@@ -21,7 +22,8 @@ CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
|
21
22
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
22
23
|
|
|
23
24
|
CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
24
|
-
id
|
|
25
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
26
|
+
uid VARCHAR(36) NOT NULL UNIQUE,
|
|
25
27
|
issue_id VARCHAR(36) NOT NULL,
|
|
26
28
|
name VARCHAR(64) NOT NULL,
|
|
27
29
|
state VARCHAR(16) NOT NULL DEFAULT 'idle',
|
|
@@ -34,14 +36,15 @@ CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
|
34
36
|
reaction_comment_id VARCHAR(64),
|
|
35
37
|
current_prompt TEXT,
|
|
36
38
|
UNIQUE (issue_id, name),
|
|
37
|
-
CONSTRAINT {{fk_sessions_issue}} FOREIGN KEY (issue_id) REFERENCES {{issues}}(
|
|
39
|
+
CONSTRAINT {{fk_sessions_issue}} FOREIGN KEY (issue_id) REFERENCES {{issues}}(uid) ON DELETE CASCADE
|
|
38
40
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
39
41
|
-- UNIQUE(issue_id, name) above creates a composite index whose leftmost
|
|
40
42
|
-- prefix (issue_id) satisfies the FK's index requirement, so no separate
|
|
41
43
|
-- idx_sessions_issue is needed on MySQL.
|
|
42
44
|
|
|
43
45
|
CREATE TABLE IF NOT EXISTS {{messages}} (
|
|
44
|
-
id
|
|
46
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
47
|
+
uid VARCHAR(36) NOT NULL UNIQUE,
|
|
45
48
|
session_id VARCHAR(36) NOT NULL,
|
|
46
49
|
content LONGTEXT NOT NULL,
|
|
47
50
|
source_comment_id VARCHAR(64),
|
|
@@ -51,7 +54,7 @@ CREATE TABLE IF NOT EXISTS {{messages}} (
|
|
|
51
54
|
error TEXT,
|
|
52
55
|
created_at VARCHAR(40) NOT NULL,
|
|
53
56
|
updated_at VARCHAR(40) NOT NULL,
|
|
54
|
-
CONSTRAINT {{fk_messages_session}} FOREIGN KEY (session_id) REFERENCES {{op_sessions}}(
|
|
57
|
+
CONSTRAINT {{fk_messages_session}} FOREIGN KEY (session_id) REFERENCES {{op_sessions}}(uid) ON DELETE CASCADE
|
|
55
58
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
56
59
|
CREATE INDEX idx_messages_session ON {{messages}} (session_id);
|
|
57
60
|
CREATE INDEX idx_messages_status ON {{messages}} (status);
|
package/src/schema-sqlite.sql
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
-- share one DB across multiple daemon instances.
|
|
5
5
|
|
|
6
6
|
CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
7
|
-
id
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
uid TEXT NOT NULL UNIQUE,
|
|
8
9
|
tracker_type TEXT NOT NULL,
|
|
9
10
|
tracker_scope_key TEXT NOT NULL,
|
|
10
11
|
tracker_scope TEXT NOT NULL,
|
|
@@ -17,8 +18,9 @@ CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
|
17
18
|
);
|
|
18
19
|
|
|
19
20
|
CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
20
|
-
id
|
|
21
|
-
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
uid TEXT NOT NULL UNIQUE,
|
|
23
|
+
issue_id TEXT NOT NULL REFERENCES {{issues}}(uid) ON DELETE CASCADE,
|
|
22
24
|
name TEXT NOT NULL,
|
|
23
25
|
state TEXT NOT NULL DEFAULT 'idle',
|
|
24
26
|
opencode_session_id TEXT,
|
|
@@ -33,8 +35,9 @@ CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
|
33
35
|
);
|
|
34
36
|
|
|
35
37
|
CREATE TABLE IF NOT EXISTS {{messages}} (
|
|
36
|
-
id
|
|
37
|
-
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
uid TEXT NOT NULL UNIQUE,
|
|
40
|
+
session_id TEXT NOT NULL REFERENCES {{op_sessions}}(uid) ON DELETE CASCADE,
|
|
38
41
|
content TEXT NOT NULL,
|
|
39
42
|
source_comment_id TEXT,
|
|
40
43
|
reaction_comment_id TEXT,
|
package/src/server.ts
CHANGED
|
@@ -11,10 +11,17 @@ type TrackerMap = Map<string, IssueTracker>;
|
|
|
11
11
|
|
|
12
12
|
export function parseGroupConfigHeader(raw: string | null): GroupConfig | undefined {
|
|
13
13
|
if (!raw) return undefined;
|
|
14
|
+
if (raw.length > 16_384) return undefined;
|
|
14
15
|
try {
|
|
15
16
|
const decoded = Buffer.from(raw, "base64").toString("utf8");
|
|
16
17
|
const parsed = JSON.parse(decoded);
|
|
17
|
-
if (typeof parsed
|
|
18
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
19
|
+
const gc = parsed as Record<string, unknown>;
|
|
20
|
+
if (gc.workdirTemplate !== undefined && typeof gc.workdirTemplate !== "string") return undefined;
|
|
21
|
+
if (gc.initScript !== undefined && typeof gc.initScript !== "string") return undefined;
|
|
22
|
+
if (gc.destroyScript !== undefined && typeof gc.destroyScript !== "string") return undefined;
|
|
23
|
+
if (gc.envInitScript !== undefined && typeof gc.envInitScript !== "string") return undefined;
|
|
24
|
+
return gc as GroupConfig;
|
|
18
25
|
} catch {
|
|
19
26
|
}
|
|
20
27
|
return undefined;
|