ework-daemon 0.4.5 → 0.4.6
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 +105 -0
- package/src/opencode.ts +3 -2
- package/src/schema-mysql.sql +6 -3
- package/src/schema-sqlite.sql +6 -3
- package/src/server.ts +8 -1
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -323,6 +323,111 @@ async function runMigrations(db: AsyncDatabase): Promise<void> {
|
|
|
323
323
|
await db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
+
// ── uid: surrogate AUTO_INCREMENT PK on issues/op_sessions/messages ──
|
|
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
|
+
if (sqlite) {
|
|
332
|
+
const UID_REBUILDS: { table: string; createSql: string; dataCols: string }[] = [
|
|
333
|
+
{
|
|
334
|
+
table: tIssues,
|
|
335
|
+
createSql: `CREATE TABLE ${tIssues} (
|
|
336
|
+
uid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
337
|
+
id TEXT NOT NULL UNIQUE,
|
|
338
|
+
tracker_type TEXT NOT NULL,
|
|
339
|
+
tracker_scope_key TEXT NOT NULL,
|
|
340
|
+
tracker_scope TEXT NOT NULL,
|
|
341
|
+
tracker_issue_id TEXT NOT NULL,
|
|
342
|
+
state TEXT NOT NULL DEFAULT 'created',
|
|
343
|
+
title TEXT NOT NULL DEFAULT '',
|
|
344
|
+
created_at TEXT NOT NULL,
|
|
345
|
+
updated_at TEXT NOT NULL,
|
|
346
|
+
UNIQUE(tracker_type, tracker_scope_key, tracker_issue_id)
|
|
347
|
+
)`,
|
|
348
|
+
dataCols:
|
|
349
|
+
"id, tracker_type, tracker_scope_key, tracker_scope, tracker_issue_id, state, title, created_at, updated_at",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
table: tSessions,
|
|
353
|
+
createSql: `CREATE TABLE ${tSessions} (
|
|
354
|
+
uid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
355
|
+
id TEXT NOT NULL UNIQUE,
|
|
356
|
+
issue_id TEXT NOT NULL REFERENCES ${tIssues}(id) ON DELETE CASCADE,
|
|
357
|
+
name TEXT NOT NULL,
|
|
358
|
+
state TEXT NOT NULL DEFAULT 'idle',
|
|
359
|
+
opencode_session_id TEXT,
|
|
360
|
+
opencode_pid INTEGER,
|
|
361
|
+
workdir TEXT,
|
|
362
|
+
created_at TEXT NOT NULL,
|
|
363
|
+
started_at INTEGER,
|
|
364
|
+
progress_comment_id TEXT,
|
|
365
|
+
reaction_comment_id TEXT,
|
|
366
|
+
current_prompt TEXT,
|
|
367
|
+
UNIQUE(issue_id, name)
|
|
368
|
+
)`,
|
|
369
|
+
dataCols:
|
|
370
|
+
"id, issue_id, name, state, opencode_session_id, opencode_pid, workdir, created_at, started_at, progress_comment_id, reaction_comment_id, current_prompt",
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
table: tMessages,
|
|
374
|
+
createSql: `CREATE TABLE ${tMessages} (
|
|
375
|
+
uid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
376
|
+
id TEXT NOT NULL UNIQUE,
|
|
377
|
+
session_id TEXT NOT NULL REFERENCES ${tSessions}(id) ON DELETE CASCADE,
|
|
378
|
+
content TEXT NOT NULL,
|
|
379
|
+
source_comment_id TEXT,
|
|
380
|
+
reaction_comment_id TEXT,
|
|
381
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
382
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
383
|
+
error TEXT,
|
|
384
|
+
created_at TEXT NOT NULL,
|
|
385
|
+
updated_at TEXT NOT NULL
|
|
386
|
+
)`,
|
|
387
|
+
dataCols:
|
|
388
|
+
"id, session_id, content, source_comment_id, reaction_comment_id, status, attempts, error, created_at, updated_at",
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
for (const { table, createSql, dataCols } of UID_REBUILDS) {
|
|
393
|
+
if (await hasColumn(table, "uid")) continue;
|
|
394
|
+
const exists = await db.get<{ cnt: number }>(
|
|
395
|
+
`SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type='table' AND name='${table}'`
|
|
396
|
+
);
|
|
397
|
+
if (Number(exists?.cnt ?? 0) === 0) continue;
|
|
398
|
+
|
|
399
|
+
await db.exec("PRAGMA foreign_keys = OFF");
|
|
400
|
+
try {
|
|
401
|
+
await db.transaction(async () => {
|
|
402
|
+
await db.exec(`ALTER TABLE ${table} RENAME TO ${table}__old_uid`);
|
|
403
|
+
await db.exec(createSql);
|
|
404
|
+
await db.exec(
|
|
405
|
+
`CREATE INDEX IF NOT EXISTS idx_${table}__id ON ${table}(id)`
|
|
406
|
+
);
|
|
407
|
+
await db.exec(`INSERT INTO ${table} (${dataCols}) SELECT ${dataCols} FROM ${table}__old_uid`);
|
|
408
|
+
await db.exec(`DROP TABLE ${table}__old_uid`);
|
|
409
|
+
});
|
|
410
|
+
} finally {
|
|
411
|
+
await db.exec("PRAGMA foreign_keys = ON");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
const uidMySqlTables = [tIssues, tSessions, tMessages];
|
|
416
|
+
for (const tbl of uidMySqlTables) {
|
|
417
|
+
if (await hasColumn(tbl, "uid")) continue;
|
|
418
|
+
try {
|
|
419
|
+
await db.exec(
|
|
420
|
+
`ALTER TABLE ${tbl} DROP PRIMARY KEY, ` +
|
|
421
|
+
`ADD COLUMN uid BIGINT AUTO_INCREMENT PRIMARY KEY FIRST, ` +
|
|
422
|
+
`ADD UNIQUE INDEX uk_${tbl}_id (id)`
|
|
423
|
+
);
|
|
424
|
+
} catch (e) {
|
|
425
|
+
if (e && typeof e === "object" && "errno" in e && (e as { errno: number }).errno === 1146) continue;
|
|
426
|
+
throw e;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
326
431
|
// issues.owner_daemon_id — points at the leasing daemon (nullable = unclaimed).
|
|
327
432
|
await ensureColumn(
|
|
328
433
|
tIssues,
|
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
|
-
|
|
11
|
+
uid BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
12
|
+
id 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
|
-
|
|
25
|
+
uid BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
26
|
+
id 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',
|
|
@@ -41,7 +43,8 @@ CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
|
41
43
|
-- idx_sessions_issue is needed on MySQL.
|
|
42
44
|
|
|
43
45
|
CREATE TABLE IF NOT EXISTS {{messages}} (
|
|
44
|
-
|
|
46
|
+
uid BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
47
|
+
id 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),
|
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
|
-
|
|
7
|
+
uid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
id 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,7 +18,8 @@ CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
|
17
18
|
);
|
|
18
19
|
|
|
19
20
|
CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
20
|
-
|
|
21
|
+
uid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
id TEXT NOT NULL UNIQUE,
|
|
21
23
|
issue_id TEXT NOT NULL REFERENCES {{issues}}(id) ON DELETE CASCADE,
|
|
22
24
|
name TEXT NOT NULL,
|
|
23
25
|
state TEXT NOT NULL DEFAULT 'idle',
|
|
@@ -33,7 +35,8 @@ CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
|
33
35
|
);
|
|
34
36
|
|
|
35
37
|
CREATE TABLE IF NOT EXISTS {{messages}} (
|
|
36
|
-
|
|
38
|
+
uid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
id TEXT NOT NULL UNIQUE,
|
|
37
40
|
session_id TEXT NOT NULL REFERENCES {{op_sessions}}(id) ON DELETE CASCADE,
|
|
38
41
|
content TEXT NOT NULL,
|
|
39
42
|
source_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;
|