ework-daemon 0.1.2 → 0.2.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/package.json +3 -1
- package/src/config.ts +41 -6
- package/src/db.ts +375 -0
- package/src/index.ts +43 -15
- package/src/op.ts +267 -278
- package/src/opencode.ts +369 -155
- package/src/schema-mysql.sql +74 -0
- package/src/schema-sqlite.sql +68 -0
- package/src/server.ts +14 -14
- package/src/trackers/types.ts +8 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- ework-daemon schema (MySQL 8.0+ / MariaDB 10.5+). Applied idempotently on boot:
|
|
2
|
+
-- CREATE TABLE IF NOT EXISTS + CREATE INDEX (no IF NOT EXISTS — MySQL lacks it;
|
|
3
|
+
-- re-runs tolerate ER_DUP_KEYNAME 1061). FK constraint names are {{tokenized}}
|
|
4
|
+
-- so prefixed instances don't collide on constraint-name uniqueness. Date
|
|
5
|
+
-- columns are VARCHAR(40) holding ISO-8601 strings — the app formats dates in
|
|
6
|
+
-- JS, never SQL date arithmetic, so strings avoid Date-vs-string friction
|
|
7
|
+
-- across drivers. FK columns carry their own indexes (MySQL requirement). All
|
|
8
|
+
-- tables InnoDB + utf8mb4 for FK CASCADE + full Unicode (emoji).
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
11
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
12
|
+
tracker_type VARCHAR(64) NOT NULL,
|
|
13
|
+
tracker_scope_key VARCHAR(255) NOT NULL,
|
|
14
|
+
tracker_scope TEXT NOT NULL,
|
|
15
|
+
tracker_issue_id VARCHAR(64) NOT NULL,
|
|
16
|
+
state VARCHAR(16) NOT NULL DEFAULT 'created',
|
|
17
|
+
title VARCHAR(512) NOT NULL DEFAULT '',
|
|
18
|
+
created_at VARCHAR(40) NOT NULL,
|
|
19
|
+
updated_at VARCHAR(40) NOT NULL,
|
|
20
|
+
UNIQUE (tracker_type, tracker_scope_key, tracker_issue_id)
|
|
21
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
24
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
25
|
+
issue_id VARCHAR(36) NOT NULL,
|
|
26
|
+
name VARCHAR(64) NOT NULL,
|
|
27
|
+
state VARCHAR(16) NOT NULL DEFAULT 'idle',
|
|
28
|
+
opencode_session_id VARCHAR(64),
|
|
29
|
+
opencode_pid BIGINT,
|
|
30
|
+
workdir VARCHAR(1024),
|
|
31
|
+
created_at VARCHAR(40) NOT NULL,
|
|
32
|
+
started_at BIGINT,
|
|
33
|
+
progress_comment_id VARCHAR(64),
|
|
34
|
+
reaction_comment_id VARCHAR(64),
|
|
35
|
+
current_prompt TEXT,
|
|
36
|
+
UNIQUE (issue_id, name),
|
|
37
|
+
CONSTRAINT {{fk_sessions_issue}} FOREIGN KEY (issue_id) REFERENCES {{issues}}(id) ON DELETE CASCADE
|
|
38
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
39
|
+
-- UNIQUE(issue_id, name) above creates a composite index whose leftmost
|
|
40
|
+
-- prefix (issue_id) satisfies the FK's index requirement, so no separate
|
|
41
|
+
-- idx_sessions_issue is needed on MySQL.
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS {{messages}} (
|
|
44
|
+
id VARCHAR(36) PRIMARY KEY,
|
|
45
|
+
session_id VARCHAR(36) NOT NULL,
|
|
46
|
+
content LONGTEXT NOT NULL,
|
|
47
|
+
source_comment_id VARCHAR(64),
|
|
48
|
+
reaction_comment_id VARCHAR(64),
|
|
49
|
+
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
|
50
|
+
attempts INT NOT NULL DEFAULT 0,
|
|
51
|
+
error TEXT,
|
|
52
|
+
created_at VARCHAR(40) NOT NULL,
|
|
53
|
+
updated_at VARCHAR(40) NOT NULL,
|
|
54
|
+
CONSTRAINT {{fk_messages_session}} FOREIGN KEY (session_id) REFERENCES {{op_sessions}}(id) ON DELETE CASCADE
|
|
55
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
56
|
+
CREATE INDEX idx_messages_session ON {{messages}} (session_id);
|
|
57
|
+
CREATE INDEX idx_messages_status ON {{messages}} (status);
|
|
58
|
+
|
|
59
|
+
-- Multi-machine coordination (Phase 1). daemons register, heartbeat, and
|
|
60
|
+
-- lease issues via issues.owner_daemon_id. id is a DB-allocated logical slot
|
|
61
|
+
-- (not IP-bound) so a restarted daemon re-adopts an orphan id. Dates are
|
|
62
|
+
-- VARCHAR(40) ISO-8601 to match the rest of the schema. The issues.owner_daemon_id
|
|
63
|
+
-- column + op_sessions runtime-state columns + their indexes are added by
|
|
64
|
+
-- idempotent ALTER in db.ts initDB() so existing DBs upgrade in-place.
|
|
65
|
+
CREATE TABLE IF NOT EXISTS {{daemons}} (
|
|
66
|
+
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
67
|
+
display_name VARCHAR(255) NOT NULL DEFAULT '',
|
|
68
|
+
internal_endpoint VARCHAR(255) NOT NULL DEFAULT '',
|
|
69
|
+
capacity INT NOT NULL DEFAULT 4,
|
|
70
|
+
last_heartbeat VARCHAR(40) NOT NULL,
|
|
71
|
+
registered_at VARCHAR(40) NOT NULL,
|
|
72
|
+
status VARCHAR(16) NOT NULL DEFAULT 'active'
|
|
73
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
74
|
+
CREATE INDEX idx_daemons_heartbeat ON {{daemons}} (last_heartbeat);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- ework-daemon schema (SQLite). Applied idempotently on boot via
|
|
2
|
+
-- CREATE TABLE/INDEX IF NOT EXISTS. See db.ts for PRAGMA setup
|
|
3
|
+
-- (WAL + foreign_keys = ON). {{tokenized}} names so WORK_DB_PREFIX can
|
|
4
|
+
-- share one DB across multiple daemon instances.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS {{issues}} (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
tracker_type TEXT NOT NULL,
|
|
9
|
+
tracker_scope_key TEXT NOT NULL,
|
|
10
|
+
tracker_scope TEXT NOT NULL,
|
|
11
|
+
tracker_issue_id TEXT NOT NULL,
|
|
12
|
+
state TEXT NOT NULL DEFAULT 'created',
|
|
13
|
+
title TEXT NOT NULL DEFAULT '',
|
|
14
|
+
created_at TEXT NOT NULL,
|
|
15
|
+
updated_at TEXT NOT NULL,
|
|
16
|
+
UNIQUE(tracker_type, tracker_scope_key, tracker_issue_id)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS {{op_sessions}} (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
issue_id TEXT NOT NULL REFERENCES {{issues}}(id) ON DELETE CASCADE,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
state TEXT NOT NULL DEFAULT 'idle',
|
|
24
|
+
opencode_session_id TEXT,
|
|
25
|
+
opencode_pid INTEGER,
|
|
26
|
+
workdir TEXT,
|
|
27
|
+
created_at TEXT NOT NULL,
|
|
28
|
+
started_at INTEGER,
|
|
29
|
+
progress_comment_id TEXT,
|
|
30
|
+
reaction_comment_id TEXT,
|
|
31
|
+
current_prompt TEXT,
|
|
32
|
+
UNIQUE(issue_id, name)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
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
|
+
content TEXT NOT NULL,
|
|
39
|
+
source_comment_id TEXT,
|
|
40
|
+
reaction_comment_id TEXT,
|
|
41
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
42
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
error TEXT,
|
|
44
|
+
created_at TEXT NOT NULL,
|
|
45
|
+
updated_at TEXT NOT NULL
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_issue ON {{op_sessions}}(issue_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON {{messages}}(session_id);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_messages_status ON {{messages}}(status);
|
|
51
|
+
|
|
52
|
+
-- Multi-machine coordination (Phase 1). daemons register, heartbeat, and
|
|
53
|
+
-- lease issues via issues.owner_daemon_id. id is a DB-allocated logical slot
|
|
54
|
+
-- (not IP-bound) so a restarted daemon re-adopts an orphan id. Dates are
|
|
55
|
+
-- TEXT ISO-8601 to match the rest of the schema. The issues.owner_daemon_id
|
|
56
|
+
-- column + op_sessions runtime-state columns + their indexes are added by
|
|
57
|
+
-- idempotent ALTER in db.ts initDB() so existing DBs upgrade in-place.
|
|
58
|
+
CREATE TABLE IF NOT EXISTS {{daemons}} (
|
|
59
|
+
id INTEGER PRIMARY KEY,
|
|
60
|
+
display_name TEXT NOT NULL DEFAULT '',
|
|
61
|
+
internal_endpoint TEXT NOT NULL DEFAULT '',
|
|
62
|
+
capacity INTEGER NOT NULL DEFAULT 4,
|
|
63
|
+
last_heartbeat TEXT NOT NULL,
|
|
64
|
+
registered_at TEXT NOT NULL,
|
|
65
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_daemons_heartbeat ON {{daemons}}(last_heartbeat);
|
package/src/server.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function createServer(
|
|
|
47
47
|
|
|
48
48
|
async function handleApi(req: Request, pathname: string): Promise<Response> {
|
|
49
49
|
if (pathname === "/api/status") {
|
|
50
|
-
const status = engine.getStatus();
|
|
50
|
+
const status = await engine.getStatus();
|
|
51
51
|
return json({
|
|
52
52
|
env: cfg.env,
|
|
53
53
|
daemon: { host: cfg.daemon.host, port: cfg.daemon.port },
|
|
@@ -56,36 +56,36 @@ export function createServer(
|
|
|
56
56
|
pending: status.pendingCount,
|
|
57
57
|
processes: status.processCount,
|
|
58
58
|
observedIssues: status.observedIssues,
|
|
59
|
-
issues: store.listAllIssues().length,
|
|
60
|
-
sessions: store.listAllSessions().length,
|
|
59
|
+
issues: (await store.listAllIssues()).length,
|
|
60
|
+
sessions: (await store.listAllSessions()).length,
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (pathname === "/api/issues") {
|
|
65
|
-
return json(store.listAllIssues());
|
|
65
|
+
return json(await store.listAllIssues());
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (pathname === "/api/sessions") {
|
|
69
|
-
return json(store.listAllSessions());
|
|
69
|
+
return json(await store.listAllSessions());
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const issueIdMatch = pathname.match(/^\/api\/issues\/([0-9a-f-]+)$/);
|
|
73
73
|
if (issueIdMatch) {
|
|
74
|
-
const issue = store.getIssue(issueIdMatch[1]!);
|
|
74
|
+
const issue = await store.getIssue(issueIdMatch[1]!);
|
|
75
75
|
if (!issue) return json({ error: "not found" }, 404);
|
|
76
|
-
const sessions = store.getSessionsForIssue(issue.id);
|
|
76
|
+
const sessions = await store.getSessionsForIssue(issue.id);
|
|
77
77
|
return json({ ...issue, sessions });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const sessionIdMatch = pathname.match(/^\/api\/sessions\/([0-9a-f-]+)$/);
|
|
81
81
|
if (sessionIdMatch) {
|
|
82
|
-
const session = store.getSession(sessionIdMatch[1]!);
|
|
82
|
+
const session = await store.getSession(sessionIdMatch[1]!);
|
|
83
83
|
if (!session) return json({ error: "not found" }, 404);
|
|
84
84
|
return json(session);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (pathname === "/api/queue") {
|
|
88
|
-
return json(engine.getQueue());
|
|
88
|
+
return json(await engine.getQueue());
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
if (pathname === "/api/processes") {
|
|
@@ -94,16 +94,16 @@ export function createServer(
|
|
|
94
94
|
|
|
95
95
|
const sessionMsgsMatch = pathname.match(/^\/api\/sessions\/([0-9a-f-]+)\/messages$/);
|
|
96
96
|
if (sessionMsgsMatch) {
|
|
97
|
-
const session = store.getSession(sessionMsgsMatch[1]!);
|
|
97
|
+
const session = await store.getSession(sessionMsgsMatch[1]!);
|
|
98
98
|
if (!session) return json({ error: "not found" }, 404);
|
|
99
|
-
return json(store.getMessagesForSession(session.id));
|
|
99
|
+
return json(await store.getMessagesForSession(session.id));
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const msgRetryMatch = pathname.match(/^\/api\/messages\/([0-9a-f-]+)\/retry$/);
|
|
103
103
|
if (msgRetryMatch && req.method === "PATCH") {
|
|
104
|
-
const result = engine.retryMessage(msgRetryMatch[1]!);
|
|
104
|
+
const result = await engine.retryMessage(msgRetryMatch[1]!);
|
|
105
105
|
if (!result) {
|
|
106
|
-
const msg = store.getMessage(msgRetryMatch[1]!);
|
|
106
|
+
const msg = await store.getMessage(msgRetryMatch[1]!);
|
|
107
107
|
if (!msg) return json({ error: "not found" }, 404);
|
|
108
108
|
if (msg.status !== "failed") return json({ error: "only failed messages can be retried" }, 400);
|
|
109
109
|
}
|
|
@@ -114,7 +114,7 @@ export function createServer(
|
|
|
114
114
|
if (forceStopMatch && req.method === "DELETE") {
|
|
115
115
|
const key = decodeURIComponent(forceStopMatch[1]!);
|
|
116
116
|
log.warn(`api: DELETE /api/processes/${key} (force-stop request)`);
|
|
117
|
-
const wasKilled = engine.forceStop(key);
|
|
117
|
+
const wasKilled = await engine.forceStop(key);
|
|
118
118
|
return json({ ok: true, stopped: wasKilled });
|
|
119
119
|
}
|
|
120
120
|
|
package/src/trackers/types.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface Issue {
|
|
|
65
65
|
title: string;
|
|
66
66
|
createdAt: Date;
|
|
67
67
|
updatedAt: Date;
|
|
68
|
+
/** Daemon id currently leasing this issue (null/undefined = unclaimed). */
|
|
69
|
+
ownerDaemonId?: number | null;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export type SessionState = "idle" | "running";
|
|
@@ -84,6 +86,12 @@ export interface OpSession {
|
|
|
84
86
|
progressCommentId?: string;
|
|
85
87
|
reactionCommentId?: string;
|
|
86
88
|
currentPrompt?: string;
|
|
89
|
+
// Multi-machine runtime state (Phase 1): persisted so a restarted daemon
|
|
90
|
+
// picks up nudge/generation counters rather than resetting to zero.
|
|
91
|
+
lastOutputAt?: number;
|
|
92
|
+
nudgeRounds?: number;
|
|
93
|
+
stuckNudgeRounds?: number;
|
|
94
|
+
generation?: number;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
/** Message = a prompt enqueued for a session */
|