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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-daemon",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Issue-driven AI development daemon. Spawns opencode subprocesses to resolve Gitea issues.",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
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
- id: string;
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
- id: string;
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
- id: string;
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.id,
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.id,
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.id,
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 id = ?", [id]);
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 id = ?", [
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}} (id, tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id, state, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
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 id = ?", [
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 id = ?", [id]);
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}} (id, issue_id, name, state, opencode_session_id, opencode_pid, workdir, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
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 id = ?", [id]);
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 id = ?`,
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}} (id, session_id, content, source_comment_id, reaction_comment_id, status, attempts, error, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
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 id = ?", [id]);
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 id = ?", [id]);
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 id = ?",
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 id = ? AND owner_daemon_id IS NULL",
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 id = ? AND status = 'pending'",
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.id = s.issue_id
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.id = m.session_id
487
- INNER JOIN {{issues}} i ON i.id = s.issue_id
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 */ } }, HOOK_SCRIPT_TIMEOUT_MS);
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(() => "");
@@ -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 VARCHAR(36) PRIMARY KEY,
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 VARCHAR(36) PRIMARY KEY,
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}}(id) ON DELETE CASCADE
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 VARCHAR(36) PRIMARY KEY,
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}}(id) ON DELETE CASCADE
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);
@@ -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 TEXT PRIMARY KEY,
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 TEXT PRIMARY KEY,
21
- issue_id TEXT NOT NULL REFERENCES {{issues}}(id) ON DELETE CASCADE,
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 TEXT PRIMARY KEY,
37
- session_id TEXT NOT NULL REFERENCES {{op_sessions}}(id) ON DELETE CASCADE,
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 === "object" && parsed !== null) return parsed as GroupConfig;
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;