tono 0.2.1 → 0.3.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.
Files changed (60) hide show
  1. package/README.md +125 -20
  2. package/dist/cli/checks.js +504 -0
  3. package/dist/cli/checks.js.map +1 -0
  4. package/dist/cli/commands/config.js +20 -3
  5. package/dist/cli/commands/config.js.map +1 -1
  6. package/dist/cli/commands/configure.js +1 -1
  7. package/dist/cli/commands/configure.js.map +1 -1
  8. package/dist/cli/commands/doctor.js +22 -0
  9. package/dist/cli/commands/doctor.js.map +1 -0
  10. package/dist/cli/commands/gateway.js +39 -14
  11. package/dist/cli/commands/gateway.js.map +1 -1
  12. package/dist/cli/commands/init.js +22 -3
  13. package/dist/cli/commands/init.js.map +1 -1
  14. package/dist/cli/commands/start.js +69 -19
  15. package/dist/cli/commands/start.js.map +1 -1
  16. package/dist/cli/commands/worker.js +500 -0
  17. package/dist/cli/commands/worker.js.map +1 -0
  18. package/dist/cli/index.js +28 -9
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/launchd.js +5 -5
  21. package/dist/cli/launchd.js.map +1 -1
  22. package/dist/server/app.js +130 -35
  23. package/dist/server/app.js.map +1 -1
  24. package/dist/server/config/load.js +19 -1
  25. package/dist/server/config/load.js.map +1 -1
  26. package/dist/server/config/schema.json +19 -0
  27. package/dist/server/db/client.js +73 -2
  28. package/dist/server/db/client.js.map +1 -1
  29. package/dist/server/db/queries.js +126 -3
  30. package/dist/server/db/queries.js.map +1 -1
  31. package/dist/server/db/schema.sql +36 -21
  32. package/dist/server/events.js.map +1 -1
  33. package/dist/server/server.js +28 -11
  34. package/dist/server/server.js.map +1 -1
  35. package/dist/server/worker/agent.js +441 -0
  36. package/dist/server/worker/agent.js.map +1 -0
  37. package/dist/server/worker/pty-bridge.js +27 -0
  38. package/dist/server/worker/pty-bridge.js.map +1 -0
  39. package/dist/server/workers/github-poller.js +26 -8
  40. package/dist/server/workers/github-poller.js.map +1 -1
  41. package/dist/server/workers/reaper.js +44 -0
  42. package/dist/server/workers/reaper.js.map +1 -0
  43. package/dist/server/workers/registry.js +174 -0
  44. package/dist/server/workers/registry.js.map +1 -0
  45. package/dist/server/workers/scheduler.js +79 -130
  46. package/dist/server/workers/scheduler.js.map +1 -1
  47. package/dist/server/ws/pty.js +71 -54
  48. package/dist/server/ws/pty.js.map +1 -1
  49. package/dist/server/ws/workers.js +241 -0
  50. package/dist/server/ws/workers.js.map +1 -0
  51. package/dist/shared/types.js +4 -1
  52. package/dist/shared/types.js.map +1 -1
  53. package/dist/shared/worker-protocol.js +28 -0
  54. package/dist/shared/worker-protocol.js.map +1 -0
  55. package/dist/web/assets/index-BsiC8WMV.js +129 -0
  56. package/dist/web/assets/index-CjzMqxyT.css +1 -0
  57. package/dist/web/index.html +2 -2
  58. package/package.json +18 -26
  59. package/dist/web/assets/index-5VFn-lxF.js +0 -129
  60. package/dist/web/assets/index-CZHd5NaX.css +0 -1
@@ -0,0 +1,27 @@
1
+ import { encodePtyFrame } from "../../shared/worker-protocol.js";
2
+ /**
3
+ * Bridge a single PTY session to the worker→gateway WebSocket. Outbound PTY
4
+ * data goes as binary frames (`<36 bytes sessionId><payload>`); the worker's
5
+ * caller emits the JSON `session.exit` itself when the PTY ends.
6
+ *
7
+ * Returns a teardown function that detaches the listeners. The caller is
8
+ * responsible for keeping the Session alive — this bridge does not own it.
9
+ */
10
+ export function bridgeSessionToWs(args) {
11
+ const { ws, session } = args;
12
+ const onData = (chunk) => {
13
+ if (ws.readyState !== ws.OPEN)
14
+ return;
15
+ try {
16
+ ws.send(encodePtyFrame(session.id, chunk), { binary: true });
17
+ }
18
+ catch {
19
+ /* connection went away mid-flush; ignore — caller will tear down */
20
+ }
21
+ };
22
+ session.on("data", onData);
23
+ return () => {
24
+ session.off("data", onData);
25
+ };
26
+ }
27
+ //# sourceMappingURL=pty-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty-bridge.js","sourceRoot":"","sources":["../../../src/server/worker/pty-bridge.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAyC;IACzE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAE7B,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE;QAC/B,IAAI,EAAE,CAAC,UAAU,KAAK,EAAE,CAAC,IAAI;YAAE,OAAO;QACtC,IAAI,CAAC;YACH,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,oEAAoE;QACtE,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE3B,OAAO,GAAG,EAAE;QACV,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC,CAAC;AACJ,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import * as gh from "../github/gh.js";
2
- import { labelForAgentKind } from "../../shared/types.js";
3
- import { resolveRepoAgents } from "../config/load.js";
2
+ import { DEFAULT_IMPLEMENT_LABEL, DEFAULT_REVIEW_LABEL, labelForAgentKind, } from "../../shared/types.js";
3
+ import { resolveDefaultAgent, resolveRepoAgents } from "../config/load.js";
4
4
  const MIN_BACKOFF_MS = 30_000;
5
5
  const MAX_BACKOFF_MS = 5 * 60_000;
6
6
  function backoffMs(failures) {
@@ -80,12 +80,13 @@ export class GithubPoller {
80
80
  async pollRepo(repo) {
81
81
  const s = this.state.get(repo.slug);
82
82
  const enabledAgents = resolveRepoAgents(this.cfg, repo.agents);
83
+ const defaultAgent = resolveDefaultAgent(this.cfg, repo);
83
84
  let queuedTotal = 0;
84
85
  let firstError = null;
85
86
  for (const agent of enabledAgents) {
86
87
  // Implement loop: poll issues with `tono-<short>` label.
87
88
  try {
88
- queuedTotal += await this.pollIssuesForAgent(repo, agent);
89
+ queuedTotal += await this.pollIssuesForAgent(repo, agent, labelForAgentKind(agent, "implement"));
89
90
  }
90
91
  catch (err) {
91
92
  firstError = firstError ?? err.message;
@@ -93,13 +94,32 @@ export class GithubPoller {
93
94
  }
94
95
  // Review loop: poll PRs with `tono-<short>-review` label.
95
96
  try {
96
- queuedTotal += await this.pollPrsForAgent(repo, agent);
97
+ queuedTotal += await this.pollPrsForAgent(repo, agent, labelForAgentKind(agent, "review"));
97
98
  }
98
99
  catch (err) {
99
100
  firstError = firstError ?? err.message;
100
101
  this.log(`[poller] ${repo.slug} ${agent}/review failed: ${err.message}`);
101
102
  }
102
103
  }
104
+ // Default-agent fall-through: bare `tono` / `tono-review` labels route to
105
+ // whichever agent the repo nominates as default. Skipped silently when no
106
+ // default is resolvable (multi-agent repo without an explicit choice).
107
+ if (defaultAgent) {
108
+ try {
109
+ queuedTotal += await this.pollIssuesForAgent(repo, defaultAgent, DEFAULT_IMPLEMENT_LABEL);
110
+ }
111
+ catch (err) {
112
+ firstError = firstError ?? err.message;
113
+ this.log(`[poller] ${repo.slug} default(${defaultAgent})/implement failed: ${err.message}`);
114
+ }
115
+ try {
116
+ queuedTotal += await this.pollPrsForAgent(repo, defaultAgent, DEFAULT_REVIEW_LABEL);
117
+ }
118
+ catch (err) {
119
+ firstError = firstError ?? err.message;
120
+ this.log(`[poller] ${repo.slug} default(${defaultAgent})/review failed: ${err.message}`);
121
+ }
122
+ }
103
123
  if (firstError) {
104
124
  s.consecutiveFailures += 1;
105
125
  s.lastError = firstError;
@@ -114,9 +134,8 @@ export class GithubPoller {
114
134
  this.bus.emit("poll:success", { repoSlug: repo.slug, count: queuedTotal, at: s.lastSuccessAt });
115
135
  }
116
136
  }
117
- async pollIssuesForAgent(repo, agent) {
137
+ async pollIssuesForAgent(repo, agent, label) {
118
138
  const kind = "implement";
119
- const label = labelForAgentKind(agent, kind);
120
139
  const issues = await gh.issueList(repo.slug, label);
121
140
  let queued = 0;
122
141
  for (const issue of issues) {
@@ -143,9 +162,8 @@ export class GithubPoller {
143
162
  }
144
163
  return queued;
145
164
  }
146
- async pollPrsForAgent(repo, agent) {
165
+ async pollPrsForAgent(repo, agent, label) {
147
166
  const kind = "review";
148
- const label = labelForAgentKind(agent, kind);
149
167
  const prs = await gh.prList(repo.slug, label);
150
168
  let queued = 0;
151
169
  for (const pr of prs) {
@@ -1 +1 @@
1
- {"version":3,"file":"github-poller.js","sourceRoot":"","sources":["../../../src/server/workers/github-poller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"github-poller.js","sourceRoot":"","sources":["../../../src/server/workers/github-poller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Marks tasks failed when their owning worker stays disconnected past graceMs.
3
+ * Runs alongside the scheduler. Cheap to over-run; idempotent.
4
+ */
5
+ export class Reaper {
6
+ opts;
7
+ timer = null;
8
+ log;
9
+ constructor(opts) {
10
+ this.opts = opts;
11
+ this.log = opts.log ?? console.log;
12
+ }
13
+ start() {
14
+ if (this.timer)
15
+ return;
16
+ const tickMs = this.opts.tickMs ?? Math.max(5_000, Math.floor(this.opts.graceMs / 2));
17
+ this.timer = setInterval(() => this.tick(), tickMs);
18
+ }
19
+ stop() {
20
+ if (this.timer)
21
+ clearInterval(this.timer);
22
+ this.timer = null;
23
+ }
24
+ tick() {
25
+ const cutoff = Date.now() - this.opts.graceMs;
26
+ const stale = this.opts.q.listDisconnectedSessions();
27
+ for (const s of stale) {
28
+ if (!s.disconnectedAt)
29
+ continue;
30
+ const at = Date.parse(s.disconnectedAt);
31
+ if (Number.isNaN(at) || at > cutoff)
32
+ continue;
33
+ this.log(`[reaper] session ${s.id} (task #${s.taskId}) past grace; failing task`);
34
+ this.opts.q.endSession(s.id);
35
+ this.opts.q.setTaskStatus(s.taskId, "failed", { exitCode: -3 });
36
+ this.opts.q.clearTaskAssignment(s.taskId);
37
+ this.opts.registry.forgetSession(s.id);
38
+ const updated = this.opts.q.getTask(s.taskId);
39
+ if (updated)
40
+ this.opts.bus.emit("task:updated", { task: updated });
41
+ }
42
+ }
43
+ }
44
+ //# sourceMappingURL=reaper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reaper.js","sourceRoot":"","sources":["../../../src/server/workers/reaper.ts"],"names":[],"mappings":"AAeA;;;GAGG;AACH,MAAM,OAAO,MAAM;IAIY;IAHrB,KAAK,GAA0B,IAAI,CAAC;IAC3B,GAAG,CAAwB;IAE5C,YAA6B,IAAmB;QAAnB,SAAI,GAAJ,IAAI,CAAe;QAC9C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACrC,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QACtF,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;IACtD,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,KAAK;YAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAEO,IAAI;QACV,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,wBAAwB,EAAE,CAAC;QACrD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC,CAAC,cAAc;gBAAE,SAAS;YAChC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;YACxC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,MAAM;gBAAE,SAAS;YAC9C,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,MAAM,4BAA4B,CAAC,CAAC;YAClF,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;YAChE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC9C,IAAI,OAAO;gBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,174 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { encodePtyFrame, } from "../../shared/worker-protocol.js";
3
+ import { RingBuffer } from "../pty/ring-buffer.js";
4
+ const SCROLLBACK_BYTES = 4 * 1024 * 1024;
5
+ /**
6
+ * In-memory registry of connected workers. The DB has the persistent worker
7
+ * row; this object holds the live WS connection and per-session fan-out.
8
+ *
9
+ * Scrollback for distributed sessions lives here on the gateway so multiple
10
+ * browser viewers can attach without round-tripping the worker for replay.
11
+ */
12
+ export class WorkerRegistry extends EventEmitter {
13
+ conns = new Map();
14
+ sessions = new Map();
15
+ /** Sticky round-robin cursor per (agent|kind) to spread tasks evenly. */
16
+ rrCursor = 0;
17
+ register(conn) {
18
+ // If a worker reconnects with the same id, replace the old connection.
19
+ const existing = this.conns.get(conn.workerId);
20
+ if (existing && existing.ws !== conn.ws) {
21
+ try {
22
+ existing.ws.close(4000, "reconnected");
23
+ }
24
+ catch {
25
+ /* ignore */
26
+ }
27
+ }
28
+ this.conns.set(conn.workerId, conn);
29
+ for (const ref of conn.runningSessions.values()) {
30
+ this.ensureSession(ref.sessionId, conn.workerId, ref.taskId);
31
+ }
32
+ }
33
+ unregister(workerId) {
34
+ this.conns.delete(workerId);
35
+ // Sessions remain in the map (with their buffers) so an in-flight browser
36
+ // viewer can still see scrollback. The reaper / scheduler decides when to
37
+ // drop them based on disconnect grace.
38
+ }
39
+ get(workerId) {
40
+ return this.conns.get(workerId);
41
+ }
42
+ list() {
43
+ return [...this.conns.values()];
44
+ }
45
+ /** Update capacity + running sessions from a heartbeat. */
46
+ updateHeartbeat(workerId, capacity, running) {
47
+ const conn = this.conns.get(workerId);
48
+ if (!conn)
49
+ return;
50
+ conn.capacity = capacity;
51
+ conn.lastHeartbeatAt = new Date();
52
+ conn.runningSessions = new Map(running.map((r) => [r.sessionId, r]));
53
+ for (const ref of running) {
54
+ this.ensureSession(ref.sessionId, workerId, ref.taskId);
55
+ }
56
+ }
57
+ /**
58
+ * Choose a worker that advertises the given agent and has free capacity for
59
+ * `kind`. Round-robins across eligible workers so no single box gets all
60
+ * the tasks when several are equally qualified.
61
+ */
62
+ findEligible(agent, kind) {
63
+ const candidates = [];
64
+ for (const conn of this.conns.values()) {
65
+ if (!conn.capabilities.agents[agent])
66
+ continue;
67
+ const slot = conn.capacity.byAgent[agent];
68
+ if (!slot)
69
+ continue;
70
+ if (slot[kind] <= 0)
71
+ continue;
72
+ candidates.push(conn);
73
+ }
74
+ if (candidates.length === 0)
75
+ return null;
76
+ const pick = candidates[this.rrCursor % candidates.length];
77
+ this.rrCursor = (this.rrCursor + 1) % Math.max(1, candidates.length);
78
+ return pick;
79
+ }
80
+ // ---- per-session state ----
81
+ ensureSession(sessionId, workerId, taskId, opts = {}) {
82
+ let s = this.sessions.get(sessionId);
83
+ if (!s) {
84
+ s = {
85
+ workerId,
86
+ taskId,
87
+ cols: 120,
88
+ rows: 32,
89
+ buffer: new RingBuffer(SCROLLBACK_BYTES),
90
+ kind: opts.kind ?? (taskId === null ? "shell" : "agent"),
91
+ cwd: opts.cwd,
92
+ };
93
+ this.sessions.set(sessionId, s);
94
+ }
95
+ else {
96
+ // Reattach: the worker reclaimed a session it already owns. Refresh ids.
97
+ s.workerId = workerId;
98
+ s.taskId = taskId;
99
+ if (opts.cwd)
100
+ s.cwd = opts.cwd;
101
+ }
102
+ return s;
103
+ }
104
+ /** All sessions tracked in the registry, in insertion order. */
105
+ listSessions() {
106
+ return [...this.sessions.entries()].map(([sessionId, state]) => ({ sessionId, state }));
107
+ }
108
+ getSession(sessionId) {
109
+ return this.sessions.get(sessionId);
110
+ }
111
+ forgetSession(sessionId) {
112
+ this.sessions.delete(sessionId);
113
+ }
114
+ /** Append PTY output bytes from the worker into the session ring + fan out. */
115
+ appendSessionData(sessionId, payload) {
116
+ let s = this.sessions.get(sessionId);
117
+ if (!s) {
118
+ // Late-arriving frame for a session we don't know about yet — start a
119
+ // placeholder so scrollback gets captured. The taskId is fixed up later
120
+ // when session.started arrives.
121
+ return;
122
+ }
123
+ s.buffer.push(payload);
124
+ this.emit(`session:${sessionId}:data`, payload);
125
+ }
126
+ setSessionSize(sessionId, cols, rows) {
127
+ const s = this.sessions.get(sessionId);
128
+ if (!s)
129
+ return;
130
+ if (cols > 0)
131
+ s.cols = cols;
132
+ if (rows > 0)
133
+ s.rows = rows;
134
+ }
135
+ scrollbackFor(sessionId) {
136
+ const s = this.sessions.get(sessionId);
137
+ return s ? s.buffer.snapshot() : Buffer.alloc(0);
138
+ }
139
+ // ---- send helpers ----
140
+ sendControl(workerId, msg) {
141
+ const conn = this.conns.get(workerId);
142
+ if (!conn || conn.ws.readyState !== conn.ws.OPEN)
143
+ return false;
144
+ try {
145
+ conn.ws.send(JSON.stringify(msg));
146
+ return true;
147
+ }
148
+ catch {
149
+ return false;
150
+ }
151
+ }
152
+ sendInput(workerId, sessionId, data) {
153
+ const conn = this.conns.get(workerId);
154
+ if (!conn || conn.ws.readyState !== conn.ws.OPEN)
155
+ return false;
156
+ try {
157
+ conn.ws.send(encodePtyFrame(sessionId, data), { binary: true });
158
+ return true;
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ /**
165
+ * Subscribe to PTY data frames for a session. Returns an unsubscribe fn.
166
+ * Listeners receive the raw payload bytes (already stripped of the frame header).
167
+ */
168
+ subscribe(sessionId, listener) {
169
+ const evt = `session:${sessionId}:data`;
170
+ this.on(evt, listener);
171
+ return () => this.off(evt, listener);
172
+ }
173
+ }
174
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/server/workers/registry.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,15 @@
1
- import { randomUUID } from "node:crypto";
2
- import { join } from "node:path";
3
- import { createReviewWorktree, createWorktree } from "../git/worktrees.js";
4
- import { getAdapter, resolveAgentConfig, } from "../agents/registry.js";
5
- import { parsePrUrl } from "../github/gh.js";
6
- import { tonoHome } from "../config/load.js";
1
+ import { resolveAgentConfig } from "../agents/registry.js";
2
+ /**
3
+ * Dispatches queued tasks to connected workers.
4
+ *
5
+ * Decisions:
6
+ * - Concurrency is enforced PER (agent, kind) globally — sums across workers.
7
+ * - Worker selection is round-robin across workers that advertise the agent
8
+ * AND have free capacity for the kind.
9
+ * - The scheduler does NOT spawn agents directly; everything goes through a
10
+ * `task.assign` over the registry's worker WS. The session.started reply
11
+ * flips the task to 'running'; the gateway-side WS handler does that.
12
+ */
7
13
  export class Scheduler {
8
14
  opts;
9
15
  running = false;
@@ -15,6 +21,7 @@ export class Scheduler {
15
21
  }
16
22
  start() {
17
23
  this.opts.bus.on("task:queued", () => this.wake());
24
+ this.opts.bus.on("worker:connected", () => this.wake());
18
25
  this.recoverInterruptedTasks();
19
26
  this.wake();
20
27
  }
@@ -55,155 +62,97 @@ export class Scheduler {
55
62
  const cap = agentCfg.concurrency[kind];
56
63
  if (cap <= 0)
57
64
  continue;
58
- const running = this.opts.q.countRunningFor(agent, kind);
59
- if (running >= cap)
65
+ const inFlight = this.opts.q.countByStatus("assigning", "running");
66
+ // Cheap: per-(agent, kind) running gate. Keeps current single-machine
67
+ // semantics — global cap holds across whichever workers picked it up.
68
+ const runningForThis = countMatching(this.opts.q.listByStatus("assigning", "running"), agent, kind);
69
+ if (runningForThis >= cap)
60
70
  continue;
61
71
  const queued = this.opts.q.listQueuedFor(agent, kind);
62
- const slots = cap - running;
72
+ const slots = cap - runningForThis;
63
73
  for (const task of queued.slice(0, slots)) {
64
74
  try {
65
- await this.dispatch(task);
75
+ const ok = this.dispatch(task);
76
+ if (!ok) {
77
+ // Couldn't place — no eligible worker right now. Stop pushing
78
+ // this partition for this tick; we'll try again on next worker
79
+ // connect or task:queued.
80
+ break;
81
+ }
66
82
  }
67
83
  catch (err) {
68
84
  this.log(`[scheduler] dispatch failed for task #${task.id}: ${err.message}`);
69
85
  this.opts.q.setTaskStatus(task.id, "failed", { exitCode: -1 });
70
86
  }
71
87
  }
88
+ // Touch inFlight to silence unused warning; keep for future budget check.
89
+ void inFlight;
72
90
  }
73
91
  }
74
92
  }
93
+ /**
94
+ * On gateway start: any task we left in 'running' or 'assigning' has lost
95
+ * its worker (process restart). Mark them failed so the user can retry.
96
+ * Workers that reconnect within the disconnect grace window will reattach
97
+ * via the WS handshake — those don't go through this path.
98
+ */
75
99
  recoverInterruptedTasks() {
76
- const stuck = this.opts.q.listByStatus("running");
77
- for (const t of stuck) {
78
- this.log(`[scheduler] recovering task #${t.id} that was running at shutdown — marking failed`);
100
+ for (const t of this.opts.q.listByStatus("running", "assigning")) {
101
+ this.log(`[scheduler] recovering task #${t.id} (was ${t.status} at startup) — marking failed`);
79
102
  this.opts.q.setTaskStatus(t.id, "failed", { exitCode: -1 });
103
+ this.opts.q.clearTaskAssignment(t.id);
80
104
  }
81
105
  }
82
- async dispatch(task) {
106
+ /**
107
+ * Send `task.assign` to an eligible worker. Returns true when placed.
108
+ * Returns false when no worker is available — caller should stop and wait
109
+ * for a worker:connected or task:queued wakeup.
110
+ */
111
+ dispatch(task) {
83
112
  const cfg = this.opts.configManager.cfg;
84
113
  const repo = cfg.repos.find((r) => r.slug === task.repoSlug);
85
114
  if (!repo) {
86
115
  throw new Error(`task #${task.id}: repo ${task.repoSlug} no longer in config`);
87
116
  }
88
- const isReview = task.kind === "review";
89
- const wt = isReview
90
- ? await createReviewWorktree({
91
- workspacesRoot: cfg.workspaces.root,
92
- slug: task.repoSlug,
93
- prNumber: task.issueNumber,
94
- sourcePath: repo.path,
95
- })
96
- : await createWorktree({
97
- workspacesRoot: cfg.workspaces.root,
98
- slug: task.repoSlug,
99
- issueNumber: task.issueNumber,
100
- baseBranch: repo.baseBranch,
101
- sourcePath: repo.path,
102
- });
103
- const adapter = getAdapter(task.agent);
104
- const specArgs = {
105
- agentConfig: resolveAgentConfig(cfg, task.agent),
117
+ const conn = this.opts.registry.findEligible(task.agent, task.kind);
118
+ if (!conn)
119
+ return false;
120
+ const agentConfig = resolveAgentConfig(cfg, task.agent);
121
+ const branch = task.kind === "implement" ? `tono/issue-${task.issueNumber}` : `tono/pr-${task.issueNumber}`;
122
+ const assign = {
123
+ type: "task.assign",
124
+ taskId: task.id,
106
125
  kind: task.kind,
107
- ctx: {
108
- issueNumber: task.issueNumber,
109
- issueTitle: task.issueTitle,
110
- issueBody: task.issueBody,
111
- repoSlug: task.repoSlug,
112
- baseBranch: repo.baseBranch,
113
- branch: wt.branch,
114
- ...(isReview ? { prUrl: task.prUrl ?? "", prHeadRef: wt.branch } : {}),
115
- },
116
- worktreePath: wt.path,
126
+ repoSlug: task.repoSlug,
127
+ repoSourcePath: repo.path ?? null,
128
+ baseBranch: repo.baseBranch,
129
+ issueNumber: task.issueNumber,
130
+ issueTitle: task.issueTitle,
131
+ issueBody: task.issueBody,
132
+ prUrl: task.prUrl,
133
+ branch,
134
+ agent: task.agent,
135
+ resumeAgentSessionId: task.agentSessionId,
136
+ agentConfig,
117
137
  };
118
- let resumeId = task.agentSessionId;
119
- if (!resumeId) {
120
- resumeId = adapter.findExistingSession({ cwd: wt.path });
121
- if (resumeId) {
122
- this.opts.q.setAgentSessionId(task.id, resumeId);
123
- this.log(`[scheduler] task #${task.id} discovered prior session ${resumeId}`);
124
- }
138
+ const sent = this.opts.registry.sendControl(conn.workerId, assign);
139
+ if (!sent) {
140
+ this.log(`[scheduler] failed to send task.assign for #${task.id} to ${conn.workerId}`);
141
+ return false;
125
142
  }
126
- const spec = resumeId
127
- ? adapter.buildResumeSpec({ ...specArgs, sessionId: resumeId })
128
- : adapter.buildFreshSpec(specArgs);
129
- const sessionId = randomUUID();
130
- const logFile = join(tonoHome(), "logs", `task-${task.id}.log`);
131
- const session = this.opts.pty.spawn({ sessionId, taskId: task.id, spec, logFile });
132
- this.opts.q.setTaskStatus(task.id, "running");
133
- this.opts.q.insertSession({
134
- id: sessionId,
135
- taskId: task.id,
136
- pid: session.pid,
137
- worktreePath: wt.path,
138
- });
139
- this.log(`[scheduler] task #${task.id} ${task.agent}/${task.kind} ${task.repoSlug}#${task.issueNumber} -> session ${sessionId} (pid ${session.pid}) in ${wt.path}${spec.resumed ? ` [resumed ${resumeId}]` : ""}`);
140
- this.opts.bus.emit("task:updated", { task: this.opts.q.getTask(task.id) });
141
- if (!resumeId) {
142
- void adapter
143
- .captureSessionId({ cwd: wt.path })
144
- .then((agentSessionId) => {
145
- if (!agentSessionId) {
146
- this.log(`[scheduler] task #${task.id}: could not capture agent session id (timed out)`);
147
- return;
148
- }
149
- const cur = this.opts.q.getTask(task.id);
150
- if (!cur || cur.agentSessionId === agentSessionId)
151
- return;
152
- this.opts.q.setAgentSessionId(task.id, agentSessionId);
153
- this.log(`[scheduler] task #${task.id} agent session = ${agentSessionId}`);
154
- const updated = this.opts.q.getTask(task.id);
155
- if (updated)
156
- this.opts.bus.emit("task:updated", { task: updated });
157
- })
158
- .catch((err) => {
159
- this.log(`[scheduler] task #${task.id} captureSessionId error: ${err.message}`);
160
- });
161
- }
162
- // PR-URL detection only applies to implement tasks. Review tasks already
163
- // know their PR URL (set at insert time), and the agent isn't expected to
164
- // create a new PR — if it does, we ignore that.
165
- let detectedPrUrl = null;
166
- if (!isReview) {
167
- let tail = "";
168
- const onChunk = (chunk) => {
169
- if (detectedPrUrl)
170
- return;
171
- tail = (tail + chunk.toString("utf8")).slice(-4096);
172
- const url = parsePrUrl(tail);
173
- if (!url)
174
- return;
175
- detectedPrUrl = url;
176
- this.opts.q.setTaskStatus(task.id, "pr_open", { prUrl: url, exitCode: 0 });
177
- const updated = this.opts.q.getTask(task.id);
178
- if (updated)
179
- this.opts.bus.emit("task:updated", { task: updated });
180
- this.log(`[scheduler] task #${task.id} -> pr_open (${url}); session ${sessionId} still attached`);
181
- session.off("data", onChunk);
182
- this.wake();
183
- };
184
- session.on("data", onChunk);
185
- }
186
- session.once("exit", ({ exitCode }) => {
187
- this.opts.q.endSession(sessionId);
188
- const current = this.opts.q.getTask(task.id);
189
- const preserveStatuses = new Set(["completed", "pr_open", "pr_closed", "merged", "cleaned"]);
190
- if (current && preserveStatuses.has(current.status)) {
191
- this.log(`[scheduler] task #${task.id} session ended (exit ${exitCode}); status ${current.status} preserved`);
192
- this.opts.bus.emit("task:updated", { task: current });
193
- this.wake();
194
- return;
195
- }
196
- const prUrl = isReview
197
- ? task.prUrl ?? null
198
- : detectedPrUrl ?? parsePrUrl(session.scrollback().toString("utf8"));
199
- const status = exitCode === 0 ? "completed" : "failed";
200
- this.opts.q.setTaskStatus(task.id, status, { exitCode, prUrl });
201
- this.log(`[scheduler] task #${task.id} ended with code ${exitCode}${prUrl ? ` (PR: ${prUrl})` : ""}`);
202
- const updated = this.opts.q.getTask(task.id);
203
- if (updated)
204
- this.opts.bus.emit("task:updated", { task: updated });
205
- this.wake();
206
- });
143
+ this.opts.q.setTaskAssigned(task.id, conn.workerId);
144
+ this.log(`[scheduler] task #${task.id} ${task.agent}/${task.kind} ${task.repoSlug}#${task.issueNumber} -> worker ${conn.hostname} (${conn.workerId})`);
145
+ const updated = this.opts.q.getTask(task.id);
146
+ if (updated)
147
+ this.opts.bus.emit("task:updated", { task: updated });
148
+ return true;
207
149
  }
208
150
  }
151
+ function countMatching(rows, agent, kind) {
152
+ let n = 0;
153
+ for (const r of rows)
154
+ if (r.agent === agent && r.kind === kind)
155
+ n += 1;
156
+ return n;
157
+ }
209
158
  //# sourceMappingURL=scheduler.js.map