ework-daemon 0.1.3 → 0.2.1

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.
@@ -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
 
@@ -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 */