@oxgeneral/orch 1.0.1 → 1.0.3

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 (132) hide show
  1. package/dist/App-A5TZETZ2.js +21 -0
  2. package/dist/agent-Q34L27AY.js +9 -0
  3. package/dist/agent-shop-D2RS4BZK.js +2 -0
  4. package/dist/chunk-4MMHVHA6.js +112 -0
  5. package/dist/chunk-64WUDYEM.js +6 -0
  6. package/dist/chunk-6MJ7V6VY.js +2 -0
  7. package/dist/{chunk-DAVHOWGD.js → chunk-BCPUTULS.js} +16 -188
  8. package/dist/{chunk-UIJYU3J7.js → chunk-BSJVYRI3.js} +5 -2
  9. package/dist/chunk-BSJVYRI3.js.map +1 -0
  10. package/dist/chunk-CDFA4IIQ.js +2 -0
  11. package/dist/chunk-CHRW4CLD.js +2 -0
  12. package/dist/{chunk-O2OQCSBL.js → chunk-FDSFFFMT.js} +46 -29
  13. package/dist/chunk-FDSFFFMT.js.map +1 -0
  14. package/dist/chunk-HMMPM7MF.js +3 -0
  15. package/dist/chunk-HXOMNULD.js +2 -0
  16. package/dist/chunk-IKNBPOQL.js +2 -0
  17. package/dist/chunk-KFQTWMZI.js +3 -0
  18. package/dist/chunk-LXNRCJ22.js +2 -0
  19. package/dist/{chunk-C6XZ3FJT.js → chunk-MOWC2CHX.js} +7 -4
  20. package/dist/chunk-MOWC2CHX.js.map +1 -0
  21. package/dist/chunk-P4JTJBWO.js +2 -0
  22. package/dist/chunk-PJ5DKXGR.js +2 -0
  23. package/dist/chunk-QNZ6D63E.js +4 -0
  24. package/dist/chunk-RHFRHCN5.js +16 -0
  25. package/dist/chunk-RHFRHCN5.js.map +1 -0
  26. package/dist/chunk-TPTHLFOB.js +2 -0
  27. package/dist/chunk-UMZEA3JT.js +5 -0
  28. package/dist/chunk-X54D5JZG.js +11 -0
  29. package/dist/{claude-WUJU5KIE.js → claude-4R6L6BWY.js} +4 -4
  30. package/dist/{claude-WUJU5KIE.js.map → claude-4R6L6BWY.js.map} +1 -1
  31. package/dist/claude-ZLVOLRUG.js +2 -0
  32. package/dist/cli.js +1 -199
  33. package/dist/clipboard-service-WVON5ZN4.js +25 -0
  34. package/dist/{codex-NYJWEPRQ.js → codex-IMOW5ZUZ.js} +4 -4
  35. package/dist/{codex-NYJWEPRQ.js.map → codex-IMOW5ZUZ.js.map} +1 -1
  36. package/dist/codex-YRSODLH4.js +2 -0
  37. package/dist/config-OOB7QEKM.js +2 -0
  38. package/dist/container-CFMUT5KS.js +4 -0
  39. package/dist/context-FXRERFSP.js +7 -0
  40. package/dist/cursor-2HILMNM3.js +2 -0
  41. package/dist/{cursor-3YHVD4NP.js → cursor-NHV7X3WG.js} +4 -4
  42. package/dist/{cursor-3YHVD4NP.js.map → cursor-NHV7X3WG.js.map} +1 -1
  43. package/dist/doctor-GKTV4YYD.js +2 -0
  44. package/dist/doctor-service-PB7YBH3F.js +2 -0
  45. package/dist/editor-7IFRWVTL.js +8 -0
  46. package/dist/goal-RNNZYMNR.js +8 -0
  47. package/dist/index.d.ts +16 -14
  48. package/dist/index.js +247 -75
  49. package/dist/index.js.map +1 -1
  50. package/dist/init-56QC5QVX.js +53 -0
  51. package/dist/logs-4ISTWUWV.js +12 -0
  52. package/dist/msg-4ELI7Q52.js +9 -0
  53. package/dist/opencode-3S4VDXRG.js +2 -0
  54. package/dist/{opencode-FAMPSA6X.js → opencode-ULT6DYCT.js} +6 -5
  55. package/dist/opencode-ULT6DYCT.js.map +1 -0
  56. package/dist/orchestrator-6XH2TY3T.js +6 -0
  57. package/dist/{orchestrator-X2CWGFCL.js.map → orchestrator-6XH2TY3T.js.map} +1 -1
  58. package/dist/orchestrator-DRITPUWT.js +13 -0
  59. package/dist/org-WAK3CDPG.js +3 -0
  60. package/dist/process-manager-33H27MQF.js +2 -0
  61. package/dist/registry-BO2PPRNG.js +2 -0
  62. package/dist/run-NE5E4JPW.js +3 -0
  63. package/dist/shell-OBHIVC2H.js +2 -0
  64. package/dist/{shell-NJNW3O6K.js → shell-UXEJRK3D.js} +4 -4
  65. package/dist/{shell-NJNW3O6K.js.map → shell-UXEJRK3D.js.map} +1 -1
  66. package/dist/shop-picker-LE3SKFOX.js +5 -0
  67. package/dist/status-NYHZ7Q5G.js +2 -0
  68. package/dist/task-JUS5OZJR.js +20 -0
  69. package/dist/team-VCJSUDWX.js +4 -0
  70. package/dist/template-engine-42PKL5KD.js +2 -0
  71. package/dist/tui-R6LXKKSH.js +2 -0
  72. package/dist/update-FFKCOV63.js +2 -0
  73. package/dist/update-check-HGMBDYHL.js +2 -0
  74. package/dist/{workspace-manager-EVD67GCG.js → workspace-manager-ABXFBL2A.js} +3 -3
  75. package/dist/{workspace-manager-EVD67GCG.js.map → workspace-manager-ABXFBL2A.js.map} +1 -1
  76. package/dist/workspace-manager-Q6WWXSTR.js +3 -0
  77. package/package.json +1 -1
  78. package/readme.md +5 -5
  79. package/dist/App-GJVTVGRU.js +0 -6717
  80. package/dist/agent-7ZJ3ZDJ7.js +0 -183
  81. package/dist/agent-shop-YN2BSLHM.js +0 -2
  82. package/dist/chunk-2C2TFQ7K.js +0 -136
  83. package/dist/chunk-45K2XID7.js +0 -29
  84. package/dist/chunk-4IFIOMCW.js +0 -86
  85. package/dist/chunk-7X2GI5OV.js +0 -181
  86. package/dist/chunk-C6XZ3FJT.js.map +0 -1
  87. package/dist/chunk-CHIP7O6V.js +0 -83
  88. package/dist/chunk-FRTKB575.js +0 -87
  89. package/dist/chunk-HXYAZGLP.js +0 -15
  90. package/dist/chunk-I3SMISEF.js +0 -29
  91. package/dist/chunk-K6DMQERQ.js +0 -89
  92. package/dist/chunk-LV6GDBBI.js +0 -297
  93. package/dist/chunk-MGGSRXWJ.js +0 -69
  94. package/dist/chunk-O2OQCSBL.js.map +0 -1
  95. package/dist/chunk-P6ATSXGL.js +0 -107
  96. package/dist/chunk-PNE6LQRF.js +0 -5
  97. package/dist/chunk-U2VDNUZL.js +0 -52
  98. package/dist/chunk-UIJYU3J7.js.map +0 -1
  99. package/dist/chunk-VXS2CJFH.js +0 -273
  100. package/dist/chunk-XDVMX2FO.js +0 -8
  101. package/dist/chunk-XDVMX2FO.js.map +0 -1
  102. package/dist/chunk-XJTJ2TJV.js +0 -221
  103. package/dist/claude-ZUEKJJ4X.js +0 -5
  104. package/dist/clipboard-service-RTDUUQQU.js +0 -200
  105. package/dist/codex-7IXXXG5U.js +0 -123
  106. package/dist/config-OTAVSMOD.js +0 -75
  107. package/dist/container-RY54L3XC.js +0 -1596
  108. package/dist/context-OL4BVUV5.js +0 -83
  109. package/dist/cursor-622RBRHH.js +0 -97
  110. package/dist/doctor-XSGQSD57.js +0 -67
  111. package/dist/doctor-service-TPOMFAIG.js +0 -2
  112. package/dist/goal-FMYYN2FR.js +0 -138
  113. package/dist/init-45BEMVL6.js +0 -199
  114. package/dist/logs-PHPYWQ6I.js +0 -207
  115. package/dist/msg-FUWWLEKM.js +0 -95
  116. package/dist/opencode-FAMPSA6X.js.map +0 -1
  117. package/dist/opencode-WOR53TSC.js +0 -98
  118. package/dist/orchestrator-O6MFMATT.js +0 -1448
  119. package/dist/orchestrator-X2CWGFCL.js +0 -5
  120. package/dist/process-manager-HUVNAPQV.js +0 -2
  121. package/dist/registry-PQWRVNF2.js +0 -2
  122. package/dist/run-N72G5V2H.js +0 -95
  123. package/dist/shell-DVFHHYAZ.js +0 -5
  124. package/dist/shop-picker-2HY67UWP.js +0 -79
  125. package/dist/status-RZWN2C6C.js +0 -56
  126. package/dist/task-5EL2RNGW.js +0 -221
  127. package/dist/team-PFLP4PPL.js +0 -97
  128. package/dist/template-engine-AWIS56BL.js +0 -3
  129. package/dist/tui-LN5XHSQY.js +0 -245
  130. package/dist/update-YLP7FPNY.js +0 -64
  131. package/dist/update-check-4YKLGBFB.js +0 -2
  132. package/dist/workspace-manager-JM6U7JOH.js +0 -215
@@ -1,1448 +0,0 @@
1
- #!/usr/bin/env node
2
- import { DEFAULT_SYSTEM_TEMPLATE, DEFAULT_USER_TEMPLATE, buildPromptContext } from './chunk-VXS2CJFH.js';
3
- import { resolveFailureStatus, isDispatchable, isTerminal, isBlocked, resolveCompletionStatus, calculateRetryDelay } from './chunk-U2VDNUZL.js';
4
- import { AUTONOMOUS_LABEL } from './chunk-PNE6LQRF.js';
5
- import { LockConflictError, WorkspaceError, TaskAlreadyRunningError, NoAgentsError, classifyAdapterError } from './chunk-2C2TFQ7K.js';
6
- import { dirname } from 'path';
7
- import fs from 'fs/promises';
8
- import { execFile } from 'child_process';
9
-
10
- function scopesOverlap(a, b) {
11
- if (!a?.length || !b?.length) return false;
12
- for (const pa of a) {
13
- for (const pb of b) {
14
- if (patternsOverlap(pa, pb)) return true;
15
- }
16
- }
17
- return false;
18
- }
19
- function computePatternInfo(pattern) {
20
- const base = pattern.split("*")[0];
21
- const isFile = !base.endsWith("/");
22
- const dir = isFile ? dirname(base) : "";
23
- return { raw: pattern, base, isFile, dir };
24
- }
25
- var ScopeIndex = class {
26
- entries;
27
- constructor(scopes) {
28
- this.entries = [];
29
- for (const scope of scopes) {
30
- if (scope?.length) {
31
- for (const p of scope) {
32
- this.entries.push(computePatternInfo(p));
33
- }
34
- }
35
- }
36
- }
37
- /** Returns true if the given scope overlaps with any pattern in the index. */
38
- overlapsAny(scope) {
39
- if (!scope?.length || this.entries.length === 0) return false;
40
- for (const raw of scope) {
41
- const info = computePatternInfo(raw);
42
- for (const entry of this.entries) {
43
- if (patternsOverlapInfo(info, entry)) return true;
44
- }
45
- }
46
- return false;
47
- }
48
- /** Add patterns to the index (e.g. from an approved candidate). */
49
- add(scope) {
50
- if (!scope?.length) return;
51
- for (const p of scope) {
52
- this.entries.push(computePatternInfo(p));
53
- }
54
- }
55
- get size() {
56
- return this.entries.length;
57
- }
58
- };
59
- function patternsOverlapInfo(a, b) {
60
- if (a.raw === b.raw) return true;
61
- if (a.base.startsWith(b.base) || b.base.startsWith(a.base)) return true;
62
- if (a.isFile && b.isFile) {
63
- return a.dir === b.dir && a.dir !== ".";
64
- }
65
- return false;
66
- }
67
- function patternsOverlap(a, b) {
68
- if (a === b) return true;
69
- const aBase = a.split("*")[0];
70
- const bBase = b.split("*")[0];
71
- if (aBase.startsWith(bBase) || bBase.startsWith(aBase)) return true;
72
- if (!aBase.endsWith("/") && !bBase.endsWith("/")) {
73
- const aDir = dirname(aBase);
74
- const bDir = dirname(bBase);
75
- return aDir === bDir && aDir !== ".";
76
- }
77
- return false;
78
- }
79
- var acquireMutex = Promise.resolve();
80
- async function acquireLock(lockPath) {
81
- let release;
82
- const gate = new Promise((r) => {
83
- release = r;
84
- });
85
- const prev = acquireMutex;
86
- acquireMutex = gate;
87
- await prev;
88
- try {
89
- return await doAcquire(lockPath);
90
- } finally {
91
- release();
92
- }
93
- }
94
- async function doAcquire(lockPath) {
95
- const existing = await readLockPid(lockPath);
96
- if (existing !== null) {
97
- if (isProcessAlive(existing)) {
98
- return { acquired: false, pid: existing };
99
- }
100
- await fs.unlink(lockPath).catch(() => {
101
- });
102
- }
103
- try {
104
- const fd = await fs.open(lockPath, "wx");
105
- await fd.writeFile(String(process.pid), "utf-8");
106
- await fd.close();
107
- return { acquired: true, pid: process.pid };
108
- } catch (err) {
109
- if (err.code === "EEXIST") {
110
- const pid = await readLockPid(lockPath);
111
- return { acquired: false, pid: pid ?? void 0 };
112
- }
113
- throw err;
114
- }
115
- }
116
- async function releaseLock(lockPath) {
117
- await fs.unlink(lockPath).catch(() => {
118
- });
119
- }
120
- async function readLockPid(lockPath) {
121
- try {
122
- const content = await fs.readFile(lockPath, "utf-8");
123
- const pid = parseInt(content.trim(), 10);
124
- return isNaN(pid) ? null : pid;
125
- } catch {
126
- return null;
127
- }
128
- }
129
- function isProcessAlive(pid) {
130
- try {
131
- process.kill(pid, 0);
132
- return true;
133
- } catch (err) {
134
- if (err.code === "EPERM") return true;
135
- return false;
136
- }
137
- }
138
-
139
- // src/infrastructure/storage/cached-stores.ts
140
- var CachedTaskStore = class {
141
- constructor(inner) {
142
- this.inner = inner;
143
- }
144
- cache = /* @__PURE__ */ new Map();
145
- async list(filter) {
146
- const key = filter ? `${filter.status ?? ""}:${filter.goalId ?? ""}` : "__all__";
147
- if (this.cache.has(key)) {
148
- return this.cache.get(key);
149
- }
150
- const result = await this.inner.list(filter);
151
- this.cache.set(key, result);
152
- return result;
153
- }
154
- async get(id) {
155
- return this.inner.get(id);
156
- }
157
- async save(task) {
158
- await this.inner.save(task);
159
- this.cache.clear();
160
- }
161
- async delete(id) {
162
- await this.inner.delete(id);
163
- this.cache.clear();
164
- }
165
- invalidate() {
166
- this.cache.clear();
167
- }
168
- };
169
- var CachedAgentStore = class {
170
- constructor(inner) {
171
- this.inner = inner;
172
- }
173
- listCache = null;
174
- nameCache = /* @__PURE__ */ new Map();
175
- async list() {
176
- if (this.listCache) {
177
- return this.listCache;
178
- }
179
- const result = await this.inner.list();
180
- this.listCache = result;
181
- return result;
182
- }
183
- async get(id) {
184
- return this.inner.get(id);
185
- }
186
- async getByName(name) {
187
- if (this.nameCache.has(name)) {
188
- return this.nameCache.get(name) ?? null;
189
- }
190
- const result = await this.inner.getByName(name);
191
- this.nameCache.set(name, result);
192
- return result;
193
- }
194
- async save(agent) {
195
- await this.inner.save(agent);
196
- this.listCache = null;
197
- this.nameCache.clear();
198
- }
199
- async delete(id) {
200
- await this.inner.delete(id);
201
- this.listCache = null;
202
- this.nameCache.clear();
203
- }
204
- invalidate() {
205
- this.listCache = null;
206
- this.nameCache.clear();
207
- }
208
- };
209
- var CachedGoalStore = class {
210
- constructor(inner) {
211
- this.inner = inner;
212
- }
213
- cache = /* @__PURE__ */ new Map();
214
- async list(filter) {
215
- const key = filter?.status ?? "__all__";
216
- if (this.cache.has(key)) return this.cache.get(key);
217
- const result = await this.inner.list(filter);
218
- this.cache.set(key, result);
219
- return result;
220
- }
221
- async get(id) {
222
- return this.inner.get(id);
223
- }
224
- async save(goal) {
225
- await this.inner.save(goal);
226
- this.cache.clear();
227
- }
228
- async delete(id) {
229
- await this.inner.delete(id);
230
- this.cache.clear();
231
- }
232
- invalidate() {
233
- this.cache.clear();
234
- }
235
- };
236
- var CRITERION_COMMANDS = {
237
- test_pass: { cmd: "npm", args: ["test"] },
238
- typecheck: { cmd: "npx", args: ["tsc", "--noEmit"] },
239
- lint: { cmd: "npm", args: ["run", "lint"] }
240
- };
241
- var ReviewRunner = class {
242
- cwd;
243
- timeoutMs;
244
- constructor(options) {
245
- this.cwd = options.cwd;
246
- this.timeoutMs = options.timeout_ms ?? 12e4;
247
- }
248
- /**
249
- * Run all criteria and return results.
250
- * Continues running even if one criterion fails.
251
- */
252
- async runAll(criteria) {
253
- const results = [];
254
- for (const criterion of criteria) {
255
- const result = await this.runCriterion(criterion);
256
- results.push(result);
257
- }
258
- return results;
259
- }
260
- /**
261
- * Check if all results passed.
262
- */
263
- static allPassed(results) {
264
- return results.length > 0 && results.every((r) => r.passed);
265
- }
266
- /**
267
- * Format results into a human-readable report.
268
- */
269
- static formatReport(results) {
270
- const lines = results.map((r) => {
271
- const icon = r.passed ? "\u2713" : "\u2717";
272
- const truncated = r.output.slice(0, 500);
273
- return `${icon} ${r.criterion}: ${r.passed ? "PASSED" : "FAILED"}
274
- ${truncated}`;
275
- });
276
- return lines.join("\n\n");
277
- }
278
- runCriterion(criterion) {
279
- const { cmd, args } = CRITERION_COMMANDS[criterion];
280
- return new Promise((resolve) => {
281
- execFile(
282
- cmd,
283
- args,
284
- { cwd: this.cwd, timeout: this.timeoutMs, maxBuffer: 1024 * 1024 },
285
- (error, stdout, stderr) => {
286
- const output = (stdout + "\n" + stderr).trim();
287
- resolve({
288
- criterion,
289
- passed: !error,
290
- output: output.slice(0, 2e3)
291
- });
292
- }
293
- );
294
- });
295
- }
296
- };
297
-
298
- // src/application/orchestrator.ts
299
- var MAX_EVENT_DATA_LEN = 8192;
300
- var MAX_BUS_DATA_LEN = 4096;
301
- var Orchestrator = class {
302
- constructor(deps) {
303
- this.deps = deps;
304
- this.cachedTaskStore = new CachedTaskStore(deps.taskStore);
305
- this.cachedAgentStore = new CachedAgentStore(deps.agentStore);
306
- this.cachedGoalStore = deps.goalStore ? new CachedGoalStore(deps.goalStore) : null;
307
- }
308
- intervalId = null;
309
- shuttingDown = false;
310
- state = null;
311
- abortControllers = /* @__PURE__ */ new Map();
312
- cachedTaskStore;
313
- cachedAgentStore;
314
- cachedGoalStore;
315
- saveStateTimer = null;
316
- saveStateDirty = false;
317
- lockAcquired = false;
318
- consecutiveTickFailures = 0;
319
- maxConsecutiveTickFailures = 5;
320
- maxRetryQueueSize = 100;
321
- signalHandlers = [];
322
- immediateDispatchTimer = null;
323
- taskCreatedUnsub = null;
324
- tickInProgress = false;
325
- /** Promise-chain mutex to serialize critical state mutations. */
326
- stateMutex = Promise.resolve();
327
- /**
328
- * Check if this instance owns the lock (can mutate state).
329
- */
330
- get isOwner() {
331
- return this.lockAcquired;
332
- }
333
- /**
334
- * Serialize access to state mutations via a Promise-chain mutex.
335
- * Prevents concurrent tick/stop/reconcile from reading stale state.
336
- */
337
- withStateLock(fn) {
338
- let release;
339
- const next = new Promise((resolve) => {
340
- release = resolve;
341
- });
342
- const prev = this.stateMutex;
343
- this.stateMutex = next;
344
- return prev.then(async () => {
345
- try {
346
- return await fn();
347
- } finally {
348
- release();
349
- }
350
- });
351
- }
352
- /**
353
- * Run a single task by ID.
354
- * If watch mode is active (lock already held), dispatches inline via stateMutex.
355
- * Otherwise acquires a temporary lock for the duration of the run.
356
- */
357
- async runTask(taskId) {
358
- if (this.lockAcquired) {
359
- await this.freshDispatch(() => this.dispatchTask(taskId));
360
- return;
361
- }
362
- await this.withTemporaryLock(() => this.freshDispatch(() => this.dispatchTask(taskId)));
363
- }
364
- /**
365
- * Run all dispatchable tasks.
366
- * If watch mode is active (lock already held), dispatches inline via stateMutex.
367
- * Otherwise acquires a temporary lock for the duration of the run.
368
- */
369
- async runAll() {
370
- if (this.lockAcquired) {
371
- await this.freshDispatch(() => this.dispatchAll());
372
- return;
373
- }
374
- await this.withTemporaryLock(() => this.freshDispatch(() => this.dispatchAll()));
375
- }
376
- /**
377
- * Invalidate caches → loadState → run dispatch fn → saveState.
378
- * Shared by runTask, runAll, and immediateDispatch.
379
- */
380
- async freshDispatch(fn) {
381
- await this.withStateLock(async () => {
382
- this.cachedTaskStore.invalidate();
383
- this.cachedAgentStore.invalidate();
384
- await this.loadState();
385
- await fn();
386
- await this.saveState();
387
- });
388
- }
389
- /**
390
- * Acquire lock, run fn, then release lock.
391
- * Used by single-shot commands (runTask, runAll) that don't go through startWatch.
392
- */
393
- async withTemporaryLock(fn) {
394
- const lockResult = await acquireLock(this.deps.lockPath);
395
- if (!lockResult.acquired) {
396
- throw new LockConflictError(lockResult.pid);
397
- }
398
- this.lockAcquired = true;
399
- try {
400
- await fn();
401
- } finally {
402
- this.lockAcquired = false;
403
- await releaseLock(this.deps.lockPath);
404
- }
405
- }
406
- /**
407
- * Start watch mode — continuous tick loop.
408
- * Acquires a PID lock to prevent multiple orchestrators.
409
- */
410
- async startWatch() {
411
- const lockResult = await acquireLock(this.deps.lockPath);
412
- if (!lockResult.acquired) {
413
- throw new LockConflictError(lockResult.pid);
414
- }
415
- this.lockAcquired = true;
416
- await this.loadState();
417
- this.state.pid = process.pid;
418
- this.state.started_at = (/* @__PURE__ */ new Date()).toISOString();
419
- await this.saveState();
420
- this.registerSignalHandlers();
421
- this.taskCreatedUnsub = this.deps.eventBus.on("task:created", () => {
422
- this.scheduleImmediateDispatch();
423
- });
424
- await this.tick();
425
- this.intervalId = setInterval(
426
- () => this.tick().then(
427
- () => {
428
- this.consecutiveTickFailures = 0;
429
- },
430
- (err) => {
431
- this.consecutiveTickFailures++;
432
- const error = err instanceof Error ? err.message : String(err);
433
- this.deps.eventBus.emit({
434
- type: "orchestrator:error",
435
- error,
436
- context: "tick",
437
- fatal: this.consecutiveTickFailures >= this.maxConsecutiveTickFailures
438
- });
439
- if (this.consecutiveTickFailures >= this.maxConsecutiveTickFailures) {
440
- this.deps.eventBus.emit({
441
- type: "orchestrator:shutdown",
442
- reason: `${this.consecutiveTickFailures} consecutive tick failures`
443
- });
444
- this.stop().catch((err2) => {
445
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err2 instanceof Error ? err2.message : String(err2), context: "stop after consecutive tick failures", fatal: false });
446
- });
447
- }
448
- }
449
- ),
450
- this.deps.config.scheduling.poll_interval_ms
451
- );
452
- }
453
- /**
454
- * Register SIGINT/SIGTERM handlers for graceful shutdown.
455
- */
456
- registerSignalHandlers() {
457
- const handler = (signal) => {
458
- this.deps.eventBus.emit({
459
- type: "orchestrator:shutdown",
460
- reason: `Received ${signal}`
461
- });
462
- this.stop().catch((err) => {
463
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `stop after ${signal} signal`, fatal: false });
464
- });
465
- };
466
- for (const sig of ["SIGINT", "SIGTERM"]) {
467
- const bound = () => handler(sig);
468
- this.signalHandlers.push([sig, bound]);
469
- process.on(sig, bound);
470
- }
471
- }
472
- /**
473
- * Remove signal handlers to avoid listener leaks.
474
- */
475
- removeSignalHandlers() {
476
- for (const [sig, handler] of this.signalHandlers) {
477
- process.removeListener(sig, handler);
478
- }
479
- this.signalHandlers = [];
480
- }
481
- /**
482
- * Stop the watch loop and clean up.
483
- */
484
- async stop() {
485
- if (this.shuttingDown) return;
486
- this.shuttingDown = true;
487
- if (this.intervalId) {
488
- clearInterval(this.intervalId);
489
- this.intervalId = null;
490
- }
491
- if (this.taskCreatedUnsub) {
492
- this.taskCreatedUnsub();
493
- this.taskCreatedUnsub = null;
494
- }
495
- if (this.immediateDispatchTimer) {
496
- clearTimeout(this.immediateDispatchTimer);
497
- this.immediateDispatchTimer = null;
498
- }
499
- await this.flushStateLazy();
500
- await this.withStateLock(async () => {
501
- if (this.state) {
502
- for (const [taskId, entry] of Object.entries(this.state.running)) {
503
- this.abortControllers.get(taskId)?.abort();
504
- this.abortControllers.delete(taskId);
505
- await this.deps.processManager.killWithGrace(entry.pid);
506
- await this.deps.runService.finish(entry.run_id, "cancelled");
507
- const task = await this.deps.taskStore.get(taskId);
508
- if (task) {
509
- await this.deps.taskService.updateStatus(taskId, resolveFailureStatus(task));
510
- }
511
- await this.deps.agentService.setStatus(entry.agent_id, "idle");
512
- }
513
- this.state.running = {};
514
- this.state.claimed = /* @__PURE__ */ new Set();
515
- this.state.pid = void 0;
516
- this.state.started_at = void 0;
517
- await this.saveState();
518
- }
519
- });
520
- if (this.lockAcquired) {
521
- await releaseLock(this.deps.lockPath);
522
- this.lockAcquired = false;
523
- }
524
- this.removeSignalHandlers();
525
- }
526
- /**
527
- * Cancel a running task: kill agent process, clean state, mark cancelled.
528
- * Acquires lock if not already owned (standalone CLI invocation).
529
- */
530
- async cancelTask(taskId) {
531
- if (!this.lockAcquired) {
532
- return this.withTemporaryLock(() => this.cancelTask(taskId));
533
- }
534
- await this.withStateLock(async () => {
535
- await this.loadState();
536
- const state = this.state;
537
- const entry = state.running[taskId];
538
- if (entry) {
539
- this.abortControllers.get(taskId)?.abort();
540
- this.abortControllers.delete(taskId);
541
- await this.deps.processManager.killWithGrace(entry.pid, 3e3).catch((err) => {
542
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `cancelTask kill process ${entry.pid} for task ${taskId}`, fatal: false });
543
- });
544
- await this.deps.runService.finish(entry.run_id, "cancelled").catch((err) => {
545
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `cancelTask finish run ${entry.run_id}`, fatal: false });
546
- });
547
- await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
548
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `cancelTask setStatus idle for agent ${entry.agent_id}`, fatal: false });
549
- });
550
- delete state.running[taskId];
551
- await this.saveState();
552
- }
553
- state.retry_queue = state.retry_queue.filter((r) => r.task_id !== taskId);
554
- try {
555
- await this.deps.taskService.cancel(taskId);
556
- } catch {
557
- try {
558
- await this.deps.taskService.updateStatus(taskId, "cancelled");
559
- } catch {
560
- }
561
- }
562
- await this.saveState();
563
- });
564
- }
565
- /**
566
- * Force-stop a specific agent: kill process, clean state, release agent.
567
- * Acquires lock if not already owned (standalone CLI invocation).
568
- */
569
- async forceStopAgent(agentId) {
570
- if (!this.lockAcquired) {
571
- return this.withTemporaryLock(() => this.forceStopAgent(agentId));
572
- }
573
- await this.withStateLock(async () => {
574
- await this.loadState();
575
- const state = this.state;
576
- for (const [taskId, entry] of Object.entries(state.running)) {
577
- if (entry.agent_id === agentId) {
578
- this.abortControllers.get(taskId)?.abort();
579
- this.abortControllers.delete(taskId);
580
- await this.deps.processManager.killWithGrace(entry.pid, 3e3);
581
- await this.deps.runService.finish(entry.run_id, "cancelled");
582
- try {
583
- await this.deps.taskService.updateStatus(taskId, "failed");
584
- } catch {
585
- }
586
- delete state.running[taskId];
587
- }
588
- }
589
- await this.deps.agentService.setStatus(agentId, "idle");
590
- await this.saveState();
591
- });
592
- }
593
- /**
594
- * Single tick: Reconcile → Dispatch → Collect
595
- * Serialized via mutex to prevent concurrent ticks from racing on state.
596
- */
597
- async tick() {
598
- if (this.shuttingDown) return;
599
- this.tickInProgress = true;
600
- try {
601
- await this.withStateLock(async () => {
602
- if (this.shuttingDown) return;
603
- this.cachedTaskStore.invalidate();
604
- this.cachedAgentStore.invalidate();
605
- this.cachedGoalStore?.invalidate();
606
- await this.loadState();
607
- await this.reconcile();
608
- await this.seedAutonomousTasks();
609
- await this.dispatchAll();
610
- const tasks = await this.cachedTaskStore.list();
611
- const running = Object.keys(this.state.running).length;
612
- const queued = tasks.filter((t) => isDispatchable(t.status)).length;
613
- this.deps.eventBus.emit({
614
- type: "orchestrator:tick",
615
- running,
616
- queued
617
- });
618
- });
619
- } finally {
620
- this.tickInProgress = false;
621
- }
622
- }
623
- /**
624
- * Schedule an immediate dispatch with 500ms debounce.
625
- * Called on task:created to avoid waiting for the next 30s tick.
626
- * Retries up to 10 times (5s) if a tick is in progress.
627
- */
628
- scheduleImmediateDispatch(retries = 0) {
629
- if (this.shuttingDown) return;
630
- if (this.immediateDispatchTimer) return;
631
- this.immediateDispatchTimer = setTimeout(() => {
632
- this.immediateDispatchTimer = null;
633
- if (this.shuttingDown) return;
634
- if (this.tickInProgress) {
635
- if (retries < 10) this.scheduleImmediateDispatch(retries + 1);
636
- return;
637
- }
638
- this.immediateDispatch().catch((err) => {
639
- this.deps.eventBus.emit({
640
- type: "orchestrator:error",
641
- error: err instanceof Error ? err.message : String(err),
642
- context: "immediate dispatch on task:created",
643
- fatal: false
644
- });
645
- });
646
- }, 500);
647
- }
648
- /**
649
- * Mini-tick: invalidate caches → loadState → dispatchAll → saveState.
650
- * Skips reconcile/collect — only dispatches new tasks immediately.
651
- */
652
- async immediateDispatch() {
653
- if (this.shuttingDown) return;
654
- await this.freshDispatch(() => this.shuttingDown ? Promise.resolve() : this.dispatchAll());
655
- }
656
- /**
657
- * Reconcile: check PID liveness, detect stalls, process retry queue.
658
- */
659
- async reconcile() {
660
- const state = this.state;
661
- const now = Date.now();
662
- const runningEntries = Object.entries(state.running);
663
- const runningTaskData = await Promise.all(
664
- runningEntries.map(([taskId]) => this.deps.taskStore.get(taskId))
665
- );
666
- for (let i = 0; i < runningEntries.length; i++) {
667
- const [taskId, entry] = runningEntries[i];
668
- const taskData = runningTaskData[i];
669
- if (!taskData || isTerminal(taskData.status)) {
670
- this.abortControllers.delete(taskId);
671
- delete state.running[taskId];
672
- await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
673
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `reconcile setStatus idle for stale agent ${entry.agent_id} (task ${taskId})`, fatal: false });
674
- });
675
- continue;
676
- }
677
- if (!this.deps.processManager.isAlive(entry.pid)) {
678
- try {
679
- await this._handleRunFailure(taskId, entry, "Process crashed unexpectedly");
680
- } catch {
681
- delete state.running[taskId];
682
- await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
683
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `reconcile crash fallback setStatus idle for agent ${entry.agent_id} (task ${taskId})`, fatal: false });
684
- });
685
- }
686
- continue;
687
- }
688
- const lastEventAt = new Date(entry.last_event_at).getTime();
689
- const stallTimeout = this.deps.config.defaults.agent.stall_timeout_ms;
690
- if (now - lastEventAt > stallTimeout) {
691
- this.deps.eventBus.emit({
692
- type: "orchestrator:stall_detected",
693
- runId: entry.run_id
694
- });
695
- this.abortControllers.get(taskId)?.abort();
696
- await this.deps.processManager.killWithGrace(entry.pid, 5e3);
697
- try {
698
- await this._handleRunFailure(taskId, entry, "Agent stalled (no events)");
699
- } catch {
700
- delete state.running[taskId];
701
- await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
702
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `reconcile stall fallback setStatus idle for agent ${entry.agent_id} (task ${taskId})`, fatal: false });
703
- });
704
- }
705
- }
706
- }
707
- const runningAgentIds = new Set(Object.values(state.running).map((e) => e.agent_id));
708
- const allAgents = await this.cachedAgentStore.list();
709
- for (const agent of allAgents) {
710
- if (agent.status === "running" && !runningAgentIds.has(agent.id)) {
711
- await this.deps.agentService.setStatus(agent.id, "idle");
712
- }
713
- }
714
- const allTasks = await this.cachedTaskStore.list();
715
- for (const task of allTasks) {
716
- if (task.status === "in_progress" && !state.running[task.id]) {
717
- try {
718
- await this.deps.taskService.updateStatus(task.id, "failed");
719
- } catch {
720
- task.status = "failed";
721
- task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
722
- await this.deps.taskStore.save(task).catch((err) => {
723
- this.deps.eventBus.emit({
724
- type: "orchestrator:error",
725
- error: err instanceof Error ? err.message : String(err),
726
- context: `force-write orphaned task ${task.id}`,
727
- fatal: false
728
- });
729
- });
730
- }
731
- this.deps.eventBus.emit({
732
- type: "task:orphaned",
733
- taskId: task.id
734
- });
735
- }
736
- }
737
- const dueRetries = [];
738
- state.retry_queue = state.retry_queue.filter((retry) => {
739
- if (now >= new Date(retry.due_at).getTime()) {
740
- dueRetries.push(retry.task_id);
741
- return false;
742
- }
743
- return true;
744
- });
745
- for (const taskId of dueRetries) {
746
- await this.dispatchTask(taskId);
747
- }
748
- await this.saveState();
749
- }
750
- /**
751
- * Create tasks for autonomous agents that have no active work.
752
- *
753
- * Priority: active Goals assigned to the agent come first.
754
- * If no goals, falls back to role-based autonomous work.
755
- */
756
- async seedAutonomousTasks() {
757
- const agents = await this.cachedAgentStore.list();
758
- const autonomousAgents = agents.filter(
759
- (a) => a.autonomous && a.status === "idle"
760
- );
761
- if (autonomousAgents.length === 0) return;
762
- const allTasks = await this.cachedTaskStore.list();
763
- const activeGoals = this.cachedGoalStore ? await this.cachedGoalStore.list({ status: "active" }) : [];
764
- let anyCreated = false;
765
- const claimedGoalIds = /* @__PURE__ */ new Set();
766
- for (const agent of autonomousAgents) {
767
- const hasActiveTask = allTasks.some(
768
- (t) => t.assignee === agent.id && !isTerminal(t.status)
769
- );
770
- if (hasActiveTask) continue;
771
- const goal = activeGoals.find(
772
- (g) => g.assignee === agent.id && !claimedGoalIds.has(g.id)
773
- ) ?? activeGoals.find(
774
- (g) => !g.assignee && !claimedGoalIds.has(g.id)
775
- );
776
- if (goal) claimedGoalIds.add(goal.id);
777
- const role = agent.role ?? "general assistant";
778
- const title = goal ? `[auto] ${agent.name}: ${goal.title.slice(0, 60)}` : `[auto] ${agent.name}: ${role.slice(0, 60)}`;
779
- const description = goal ? `## GOAL (highest priority)
780
-
781
- ${goal.description || goal.title}
782
-
783
- ---
784
- Agent role: ${role}` : `Autonomous work cycle. Agent role: ${role}`;
785
- try {
786
- await this.deps.taskService.create({
787
- title,
788
- description,
789
- assignee: agent.id,
790
- labels: [AUTONOMOUS_LABEL],
791
- priority: 3,
792
- goalId: goal?.id
793
- });
794
- anyCreated = true;
795
- } catch (err) {
796
- this.deps.eventBus.emit({
797
- type: "orchestrator:error",
798
- error: err instanceof Error ? err.message : String(err),
799
- context: `autonomous task for agent ${agent.id}`,
800
- fatal: false
801
- });
802
- }
803
- }
804
- if (anyCreated) this.cachedTaskStore.invalidate();
805
- }
806
- /**
807
- * Dispatch all dispatchable tasks up to max_concurrent_agents.
808
- */
809
- async dispatchAll() {
810
- const state = this.state;
811
- const maxConcurrent = this.deps.config.scheduling.max_concurrent_agents;
812
- const currentRunning = Object.keys(state.running).length;
813
- const availableSlots = maxConcurrent - currentRunning;
814
- if (availableSlots <= 0) return;
815
- const allTasks = await this.cachedTaskStore.list();
816
- const taskMap = new Map(allTasks.map((t) => [t.id, t]));
817
- const candidates = allTasks.filter(
818
- (t) => isDispatchable(t.status) && !isBlocked(t, taskMap) && !state.running[t.id] && !state.claimed.has(t.id)
819
- ).sort((a, b) => {
820
- const priDiff = (a.priority ?? 3) - (b.priority ?? 3);
821
- if (priDiff !== 0) return priDiff;
822
- const goalDiff = (a.goalId ? 0 : 1) - (b.goalId ? 0 : 1);
823
- if (goalDiff !== 0) return goalDiff;
824
- const bTime = b.updated_at ?? "";
825
- const aTime = a.updated_at ?? "";
826
- return bTime < aTime ? -1 : bTime > aTime ? 1 : 0;
827
- }).slice(0, availableSlots);
828
- const blockedIds = /* @__PURE__ */ new Set();
829
- const inProgressScoped = allTasks.filter((t) => t.status === "in_progress" && t.scope?.length);
830
- const scopeIndex = new ScopeIndex(inProgressScoped.map((t) => t.scope));
831
- for (const candidate of candidates) {
832
- if (!candidate.scope?.length) continue;
833
- if (scopeIndex.overlapsAny(candidate.scope)) {
834
- const overlapper = inProgressScoped.find((t) => scopesOverlap(candidate.scope, t.scope));
835
- this.deps.eventBus.emit({
836
- type: "task:scope_overlap",
837
- taskId: candidate.id,
838
- overlappingTaskId: overlapper?.id ?? candidate.id,
839
- patterns: candidate.scope
840
- });
841
- blockedIds.add(candidate.id);
842
- } else {
843
- scopeIndex.add(candidate.scope);
844
- }
845
- }
846
- for (const task of candidates) {
847
- if (blockedIds.has(task.id)) continue;
848
- try {
849
- await this.dispatchTask(task.id);
850
- } catch (err) {
851
- if (err instanceof WorkspaceError) {
852
- try {
853
- const t = await this.deps.taskStore.get(task.id);
854
- if (t && !isTerminal(t.status)) {
855
- t.status = "failed";
856
- t.updated_at = (/* @__PURE__ */ new Date()).toISOString();
857
- await this.deps.taskStore.save(t);
858
- }
859
- } catch {
860
- }
861
- }
862
- this.deps.eventBus.emit({
863
- type: "orchestrator:error",
864
- error: err instanceof Error ? err.message : String(err),
865
- context: `dispatch task ${task.id}`,
866
- fatal: false
867
- });
868
- }
869
- }
870
- }
871
- /**
872
- * Dispatch a single task: claim → assign → execute.
873
- */
874
- async dispatchTask(taskId) {
875
- const state = this.state;
876
- if (state.running[taskId]) {
877
- const entry = state.running[taskId];
878
- throw new TaskAlreadyRunningError(taskId, entry.run_id, entry.agent_id);
879
- }
880
- const task = await this.deps.taskService.get(taskId);
881
- state.claimed.add(taskId);
882
- await this.saveState();
883
- try {
884
- const allAgents = await this.cachedAgentStore.list();
885
- const agent = await this.deps.agentService.findBestAgent(task);
886
- if (!agent) {
887
- if (allAgents.length === 0) {
888
- throw new NoAgentsError();
889
- }
890
- this.unclaim(taskId);
891
- await this.saveState();
892
- return;
893
- }
894
- const { path: workspacePath, branch: worktreeBranch } = await this.deps.workspaceManager.prepare(
895
- task,
896
- agent,
897
- this.deps.config
898
- );
899
- const systemTemplate = this.deps.config.prompt?.system_template ?? DEFAULT_SYSTEM_TEMPLATE;
900
- const userTemplate = this.deps.config.prompt?.user_template ?? DEFAULT_USER_TEMPLATE;
901
- const legacyTemplate = this.deps.config.prompt?.template;
902
- const attempt = task.attempts + 1;
903
- let retryContext;
904
- if (attempt > 1) {
905
- const failedData = await this.deps.runService.getLastFailedRunContext(task.id);
906
- if (failedData) {
907
- retryContext = {
908
- previous_error: failedData.error,
909
- previous_output: failedData.output
910
- };
911
- }
912
- }
913
- const goalId = task.goalId;
914
- const [sharedContext, pendingMessages, goalRaw] = await Promise.all([
915
- this.deps.contextStore?.getAll(),
916
- this.deps.messageService ? this.deps.messageService.drainMailbox(agent.id, task.id) : [],
917
- goalId && this.cachedGoalStore ? this.cachedGoalStore.get(goalId).catch(() => null) : null
918
- ]);
919
- let goalContext;
920
- if (goalRaw) {
921
- const allTasks = await this.cachedTaskStore.list();
922
- const goalTasks = allTasks.filter((t) => t.goalId === goalId);
923
- const progressEntry = await this.deps.contextStore?.get(`${goalId}-progress`);
924
- const taskNames = goalTasks.map((t) => `[${t.status}] ${t.title}`);
925
- goalContext = {
926
- id: goalRaw.id,
927
- title: goalRaw.title,
928
- description: goalRaw.description,
929
- status: goalRaw.status,
930
- task_names: taskNames,
931
- progress: progressEntry?.value
932
- };
933
- }
934
- const context = buildPromptContext(
935
- task,
936
- agent,
937
- attempt,
938
- workspacePath,
939
- this.deps.config,
940
- { allAgents, retryContext, sharedContext, feedback: task.feedback, messages: pendingMessages.length ? pendingMessages : void 0, goal: goalContext }
941
- );
942
- let prompt;
943
- let systemPrompt;
944
- if (legacyTemplate) {
945
- prompt = await this.deps.templateEngine.render(legacyTemplate, context);
946
- } else {
947
- systemPrompt = await this.deps.templateEngine.render(systemTemplate, context);
948
- prompt = await this.deps.templateEngine.render(userTemplate, context);
949
- }
950
- const run = await this.deps.runService.create({
951
- taskId: task.id,
952
- agentId: agent.id,
953
- attempt,
954
- prompt,
955
- workspacePath
956
- });
957
- if (task.status === "failed" || task.status === "cancelled") {
958
- await this.deps.taskService.retry(taskId);
959
- task.status = "todo";
960
- task.attempts = 0;
961
- }
962
- await this.deps.taskService.updateStatus(taskId, "in_progress");
963
- await this.deps.taskService.assign(taskId, agent.id);
964
- await this.deps.taskService.incrementAttempts(taskId);
965
- if (worktreeBranch) {
966
- const freshTask = await this.deps.taskStore.get(taskId);
967
- if (freshTask) {
968
- freshTask.proof = { ...freshTask.proof ?? { files_changed: [] }, branch: worktreeBranch };
969
- freshTask.workspace = workspacePath;
970
- await this.deps.taskStore.save(freshTask);
971
- }
972
- }
973
- await this.deps.agentService.setStatus(agent.id, "running");
974
- const agentData = await this.deps.agentService.get(agent.id);
975
- agentData.current_task = taskId;
976
- agentData.last_error = void 0;
977
- await this.deps.agentStore.save(agentData);
978
- const adapter = this.deps.adapterRegistry.require(agent.adapter);
979
- const abortController = new AbortController();
980
- this.abortControllers.set(taskId, abortController);
981
- const handle = adapter.execute({
982
- prompt,
983
- systemPrompt,
984
- workspace: workspacePath,
985
- env: {
986
- ...agent.config.env,
987
- ORCH_AGENT_ID: agent.id,
988
- ORCH_AGENT_NAME: agent.name,
989
- ORCH_TASK_ID: task.id
990
- },
991
- config: agentData.config,
992
- signal: abortController.signal
993
- });
994
- const agentPid = handle.pid;
995
- const now = (/* @__PURE__ */ new Date()).toISOString();
996
- await this.deps.runService.start(run.id, agentPid);
997
- this.unclaim(taskId);
998
- state.running[taskId] = {
999
- run_id: run.id,
1000
- agent_id: agent.id,
1001
- task_id: taskId,
1002
- pid: agentPid,
1003
- started_at: now,
1004
- last_event_at: now
1005
- };
1006
- await this.saveState();
1007
- this.collectEvents(
1008
- handle.events,
1009
- run.id,
1010
- taskId,
1011
- agent.id
1012
- ).catch((err) => {
1013
- this.deps.eventBus.emit({
1014
- type: "orchestrator:error",
1015
- error: err instanceof Error ? err.message : String(err),
1016
- context: `adapter execution for ${taskId}`,
1017
- fatal: false
1018
- });
1019
- });
1020
- } catch (err) {
1021
- this.unclaim(taskId);
1022
- await this.saveState();
1023
- throw err;
1024
- }
1025
- }
1026
- /**
1027
- * Collect events from an adapter's async generator.
1028
- */
1029
- async collectEvents(generator, runId, taskId, agentId) {
1030
- let collectedTokens;
1031
- let resultText;
1032
- let lastAgentMessage;
1033
- let lastErrorKind;
1034
- const filesChangedSet = /* @__PURE__ */ new Set();
1035
- try {
1036
- for await (const event of generator) {
1037
- if (this.shuttingDown) break;
1038
- if (event.type === "done") {
1039
- if (event.tokens) collectedTokens = event.tokens;
1040
- const data = event.data;
1041
- if (data && typeof data.result === "string") {
1042
- resultText = data.result;
1043
- }
1044
- }
1045
- if (event.type === "output") {
1046
- const data = event.data;
1047
- if (data) {
1048
- const text = typeof data.text === "string" ? data.text : typeof data.message === "string" ? data.message : void 0;
1049
- if (text?.trim()) lastAgentMessage = text;
1050
- }
1051
- }
1052
- if (event.type === "file_change") {
1053
- const data = event.data;
1054
- if (data && Array.isArray(data.paths)) {
1055
- for (const p of data.paths) {
1056
- if (typeof p === "string") filesChangedSet.add(p);
1057
- }
1058
- } else {
1059
- const filePath2 = data && typeof data.path === "string" ? data.path : typeof event.data === "string" ? event.data : String(event.data);
1060
- filesChangedSet.add(filePath2);
1061
- }
1062
- }
1063
- const eventTimestamp = isValidISOTimestamp(event.timestamp) ? event.timestamp : (/* @__PURE__ */ new Date()).toISOString();
1064
- const filePath = event.type === "file_change" ? (() => {
1065
- const d = event.data;
1066
- return d && typeof d.path === "string" ? d.path : typeof event.data === "string" ? event.data : String(event.data);
1067
- })() : null;
1068
- const serialized = serializeEventData(event.data, MAX_EVENT_DATA_LEN);
1069
- event.data = void 0;
1070
- const runEvent = {
1071
- timestamp: eventTimestamp,
1072
- type: event.type === "output" ? "agent_output" : event.type === "file_change" ? "file_changed" : event.type === "command" ? "command_run" : event.type === "tool_call" ? "tool_call" : event.type === "error" ? "error" : "done",
1073
- data: serialized
1074
- };
1075
- await this.deps.runService.appendEvent(runId, runEvent);
1076
- if (this.state?.running[taskId]) {
1077
- this.state.running[taskId].last_event_at = eventTimestamp;
1078
- this.saveStateLazy();
1079
- }
1080
- const busData = serializeEventData(serialized, MAX_BUS_DATA_LEN);
1081
- if (event.type === "output" || event.type === "tool_call") {
1082
- this.deps.eventBus.emit({
1083
- type: "agent:output",
1084
- runId,
1085
- agentId,
1086
- data: busData
1087
- });
1088
- } else if (event.type === "file_change") {
1089
- this.deps.eventBus.emit({
1090
- type: "agent:file_changed",
1091
- runId,
1092
- agentId,
1093
- path: filePath
1094
- });
1095
- } else if (event.type === "error") {
1096
- if (event.errorKind) lastErrorKind = event.errorKind;
1097
- this.deps.eventBus.emit({
1098
- type: "agent:error",
1099
- runId,
1100
- agentId,
1101
- error: busData,
1102
- ...event.errorKind ? { errorKind: event.errorKind } : {}
1103
- });
1104
- }
1105
- }
1106
- const finalResult = resultText ?? lastAgentMessage;
1107
- await this.handleRunSuccess(taskId, runId, agentId, collectedTokens, finalResult, [...filesChangedSet]);
1108
- } catch (err) {
1109
- const error = err instanceof Error ? err.message : String(err);
1110
- const errorKind = lastErrorKind ?? (err instanceof Error ? err.errorKind : void 0);
1111
- const entry = this.state?.running[taskId];
1112
- if (entry) {
1113
- await this.handleRunFailure(taskId, entry, error, errorKind);
1114
- }
1115
- }
1116
- }
1117
- async handleRunSuccess(taskId, runId, agentId, tokens, resultText, filesChanged) {
1118
- return this.withStateLock(() => this._handleRunSuccess(taskId, runId, agentId, tokens, resultText, filesChanged));
1119
- }
1120
- async _handleRunSuccess(taskId, runId, agentId, tokens, resultText, filesChanged) {
1121
- await this.flushStateLazy();
1122
- this.abortControllers.delete(taskId);
1123
- const state = this.state;
1124
- if (!state.running[taskId]) return;
1125
- const task = await this.deps.taskStore.get(taskId);
1126
- if (!task) return;
1127
- task.proof = {
1128
- ...task.proof,
1129
- agent_summary: resultText?.slice(0, 2e3) ?? task.proof?.agent_summary,
1130
- files_changed: filesChanged?.length ? filesChanged : task.proof?.files_changed ?? []
1131
- };
1132
- delete task.feedback;
1133
- await this.deps.taskStore.save(task);
1134
- const agent = await this.deps.agentStore.get(agentId);
1135
- const isAutonomousTask = task.labels?.includes(AUTONOMOUS_LABEL);
1136
- const autoApprove = isAutonomousTask || agent?.config.approval_policy === "auto";
1137
- const newStatus = resolveCompletionStatus(task, true, autoApprove);
1138
- await this.deps.runService.finish(runId, "succeeded", tokens);
1139
- const runningEntry = state.running[taskId];
1140
- const successRuntimeMs = runningEntry ? Date.now() - new Date(runningEntry.started_at).getTime() : 0;
1141
- if (runningEntry) {
1142
- state.stats.total_runtime_ms += successRuntimeMs;
1143
- }
1144
- delete state.running[taskId];
1145
- const statsUpdate = {
1146
- tasks_completed: (agent?.stats.tasks_completed ?? 0) + 1,
1147
- total_runs: (agent?.stats.total_runs ?? 0) + 1,
1148
- total_runtime_ms: (agent?.stats.total_runtime_ms ?? 0) + successRuntimeMs
1149
- };
1150
- if (tokens) {
1151
- statsUpdate.tokens_used = (agent?.stats.tokens_used ?? 0) + tokens.total;
1152
- }
1153
- await this.deps.agentService.updateStats(agentId, statsUpdate).catch((err) => {
1154
- this.deps.eventBus.emit({
1155
- type: "orchestrator:error",
1156
- error: err instanceof Error ? err.message : String(err),
1157
- context: `agent stats update for ${agentId}`,
1158
- fatal: false
1159
- });
1160
- });
1161
- state.stats.total_tasks_completed++;
1162
- state.stats.total_runs++;
1163
- if (tokens) {
1164
- state.stats.total_tokens.input += tokens.input;
1165
- state.stats.total_tokens.output += tokens.output;
1166
- state.stats.total_tokens.total = state.stats.total_tokens.input + state.stats.total_tokens.output;
1167
- }
1168
- if (task.proof?.branch) {
1169
- try {
1170
- const mergeResult = await this.deps.workspaceManager.mergeBack(task.proof.branch);
1171
- if (mergeResult.success) {
1172
- this.deps.eventBus.emit({
1173
- type: "workspace:merge_succeeded",
1174
- taskId,
1175
- branch: task.proof.branch
1176
- });
1177
- await this.deps.workspaceManager.cleanup(taskId).catch((err) => {
1178
- this.deps.eventBus.emit({
1179
- type: "orchestrator:error",
1180
- error: err instanceof Error ? err.message : String(err),
1181
- context: `workspace cleanup for ${taskId}`,
1182
- fatal: false
1183
- });
1184
- });
1185
- } else {
1186
- this.deps.eventBus.emit({
1187
- type: "workspace:merge_conflict",
1188
- taskId,
1189
- branch: task.proof.branch,
1190
- conflictInfo: mergeResult.conflictInfo
1191
- });
1192
- await this.forceTaskToReview(task, agentId, `MERGE CONFLICT: ${mergeResult.conflictInfo}`);
1193
- return;
1194
- }
1195
- } catch (err) {
1196
- const error = err instanceof Error ? err.message : String(err);
1197
- await this.forceTaskToReview(task, agentId, `MERGE ERROR: ${error}`);
1198
- return;
1199
- }
1200
- }
1201
- try {
1202
- await this.deps.taskService.updateStatus(taskId, newStatus);
1203
- } catch (validationErr) {
1204
- const error = validationErr instanceof Error ? validationErr.message : String(validationErr);
1205
- this.deps.eventBus.emit({
1206
- type: "orchestrator:error",
1207
- error,
1208
- context: `state machine validation failed for task ${taskId} -> ${newStatus}, force-writing`,
1209
- fatal: false
1210
- });
1211
- task.status = newStatus;
1212
- task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1213
- await this.deps.taskStore.save(task).catch((saveErr) => {
1214
- this.deps.eventBus.emit({
1215
- type: "orchestrator:error",
1216
- error: saveErr instanceof Error ? saveErr.message : String(saveErr),
1217
- context: `force-write task ${taskId} to store failed`,
1218
- fatal: false
1219
- });
1220
- });
1221
- }
1222
- await this.deps.agentService.setStatus(agentId, "idle").catch((err) => {
1223
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `_handleRunSuccess setStatus idle for agent ${agentId}`, fatal: false });
1224
- });
1225
- const agentAfter = await this.deps.agentStore.get(agentId);
1226
- if (agentAfter) {
1227
- agentAfter.current_task = void 0;
1228
- await this.deps.agentStore.save(agentAfter);
1229
- }
1230
- if (newStatus === "review" && task.review_criteria?.length) {
1231
- await this.runAutoReview(taskId, task.review_criteria, task.workspace ?? this.deps.projectRoot, autoApprove);
1232
- } else if (newStatus === "review" && autoApprove) {
1233
- await this.deps.taskService.updateStatus(taskId, "done");
1234
- }
1235
- await this.saveState();
1236
- this.scheduleImmediateDispatch();
1237
- }
1238
- async handleRunFailure(taskId, entry, error, errorKind) {
1239
- return this.withStateLock(() => this._handleRunFailure(taskId, entry, error, errorKind));
1240
- }
1241
- async _handleRunFailure(taskId, entry, error, errorKind) {
1242
- await this.flushStateLazy();
1243
- this.abortControllers.delete(taskId);
1244
- const state = this.state;
1245
- const task = await this.deps.taskStore.get(taskId);
1246
- if (!task) return;
1247
- await this.deps.runService.finish(entry.run_id, "failed", void 0, error);
1248
- await this.deps.agentService.setStatus(entry.agent_id, "idle");
1249
- const agentAfterIdle = await this.deps.agentStore.get(entry.agent_id);
1250
- if (agentAfterIdle) {
1251
- agentAfterIdle.current_task = void 0;
1252
- agentAfterIdle.last_error = {
1253
- message: error.slice(0, 500),
1254
- kind: errorKind ?? classifyAdapterError(error),
1255
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1256
- };
1257
- await this.deps.agentStore.save(agentAfterIdle);
1258
- }
1259
- const agent = await this.deps.agentStore.get(entry.agent_id);
1260
- const runtimeMs = Date.now() - new Date(entry.started_at).getTime();
1261
- await this.deps.agentService.updateStats(entry.agent_id, {
1262
- tasks_failed: (agent?.stats.tasks_failed ?? 0) + 1,
1263
- total_runs: (agent?.stats.total_runs ?? 0) + 1,
1264
- total_runtime_ms: (agent?.stats.total_runtime_ms ?? 0) + runtimeMs
1265
- });
1266
- const failureStatus = resolveFailureStatus(task);
1267
- await this.deps.taskService.updateStatus(taskId, failureStatus);
1268
- if (failureStatus === "retrying") {
1269
- const delay = calculateRetryDelay(
1270
- task.attempts - 1,
1271
- this.deps.config.scheduling.retry_base_delay_ms,
1272
- this.deps.config.scheduling.retry_max_delay_ms
1273
- );
1274
- const alreadyQueued = state.retry_queue.some((r) => r.task_id === taskId);
1275
- if (!alreadyQueued) {
1276
- if (state.retry_queue.length >= this.maxRetryQueueSize) {
1277
- state.retry_queue.shift();
1278
- }
1279
- state.retry_queue.push({
1280
- task_id: taskId,
1281
- attempt: task.attempts + 1,
1282
- due_at: new Date(Date.now() + delay).toISOString(),
1283
- error
1284
- });
1285
- }
1286
- this.deps.eventBus.emit({
1287
- type: "run:retry",
1288
- runId: entry.run_id,
1289
- attempt: task.attempts + 1,
1290
- delay_ms: delay
1291
- });
1292
- } else {
1293
- state.stats.total_tasks_failed++;
1294
- }
1295
- state.stats.total_runtime_ms += runtimeMs;
1296
- delete state.running[taskId];
1297
- state.stats.total_runs++;
1298
- await this.saveState();
1299
- this.scheduleImmediateDispatch();
1300
- }
1301
- /**
1302
- * Run automatic review criteria on a task in 'review' status.
1303
- * If all criteria pass, transition review → done.
1304
- * If any fail, stay in review with results attached.
1305
- */
1306
- async runAutoReview(taskId, criteria, cwd, autoApprove = false) {
1307
- const runner = new ReviewRunner({ cwd });
1308
- const results = await runner.runAll(criteria);
1309
- const allPassed = ReviewRunner.allPassed(results);
1310
- const task = await this.deps.taskStore.get(taskId);
1311
- if (!task) return;
1312
- task.review_results = results;
1313
- task.proof = {
1314
- ...task.proof,
1315
- test_results: ReviewRunner.formatReport(results),
1316
- files_changed: task.proof?.files_changed ?? []
1317
- };
1318
- await this.deps.taskStore.save(task);
1319
- this.deps.eventBus.emit({
1320
- type: "task:auto_reviewed",
1321
- taskId,
1322
- passed: allPassed,
1323
- results
1324
- });
1325
- if (allPassed || autoApprove) {
1326
- if (!allPassed) {
1327
- this.deps.eventBus.emit({
1328
- type: "orchestrator:error",
1329
- error: `Review criteria failed for task ${taskId} but autoApprove is set \u2014 force-approving`,
1330
- context: "auto-review-with-auto-approve",
1331
- fatal: false
1332
- });
1333
- }
1334
- try {
1335
- await this.deps.taskService.updateStatus(taskId, "done");
1336
- } catch (validationErr) {
1337
- const error = validationErr instanceof Error ? validationErr.message : String(validationErr);
1338
- this.deps.eventBus.emit({
1339
- type: "orchestrator:error",
1340
- error,
1341
- context: `auto-review transition failed for task ${taskId} -> done, force-writing`,
1342
- fatal: false
1343
- });
1344
- task.status = "done";
1345
- task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1346
- await this.deps.taskStore.save(task).catch((saveErr) => {
1347
- this.deps.eventBus.emit({
1348
- type: "orchestrator:error",
1349
- error: saveErr instanceof Error ? saveErr.message : String(saveErr),
1350
- context: `force-write task ${taskId} to store failed (auto-review)`,
1351
- fatal: false
1352
- });
1353
- });
1354
- }
1355
- }
1356
- }
1357
- /**
1358
- * Force a task to 'review' status with a summary prefix.
1359
- * Used when merge-back fails (conflict or infrastructure error).
1360
- */
1361
- async forceTaskToReview(task, agentId, summaryPrefix) {
1362
- task.proof = {
1363
- ...task.proof,
1364
- agent_summary: `${summaryPrefix}
1365
-
1366
- ${task.proof?.agent_summary ?? ""}`.slice(0, 2e3),
1367
- files_changed: task.proof?.files_changed ?? []
1368
- };
1369
- task.status = "review";
1370
- task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1371
- await this.deps.taskStore.save(task);
1372
- await this.deps.agentService.setStatus(agentId, "idle").catch((err) => {
1373
- this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `forceTaskToReview setStatus idle for agent ${agentId}`, fatal: false });
1374
- });
1375
- const agentAfter = await this.deps.agentStore.get(agentId);
1376
- if (agentAfter) {
1377
- agentAfter.current_task = void 0;
1378
- await this.deps.agentStore.save(agentAfter);
1379
- }
1380
- await this.saveState();
1381
- }
1382
- unclaim(taskId) {
1383
- this.state.claimed.delete(taskId);
1384
- }
1385
- /**
1386
- * Throw if this instance doesn't own the lock (read-only session).
1387
- */
1388
- requireOwnership() {
1389
- if (!this.lockAcquired) {
1390
- throw new LockConflictError(0);
1391
- }
1392
- }
1393
- async loadState() {
1394
- this.state = await this.deps.stateStore.read();
1395
- }
1396
- async saveState() {
1397
- if (this.state) {
1398
- await this.deps.stateStore.write(this.state);
1399
- }
1400
- }
1401
- /**
1402
- * Debounced saveState — batches rapid writes within 500ms window.
1403
- * Used for non-critical updates like last_event_at in collectEvents.
1404
- */
1405
- saveStateLazy() {
1406
- this.saveStateDirty = true;
1407
- if (this.saveStateTimer) return;
1408
- this.saveStateTimer = setTimeout(() => {
1409
- this.saveStateTimer = null;
1410
- if (this.saveStateDirty) {
1411
- this.saveStateDirty = false;
1412
- this.saveState().catch((err) => {
1413
- this.deps.eventBus.emit({
1414
- type: "orchestrator:error",
1415
- error: err instanceof Error ? err.message : String(err),
1416
- context: "debounced state save",
1417
- fatal: false
1418
- });
1419
- });
1420
- }
1421
- }, 500);
1422
- }
1423
- /**
1424
- * Flush any pending debounced saveState immediately.
1425
- * Call before critical transitions to ensure state is persisted.
1426
- */
1427
- async flushStateLazy() {
1428
- if (this.saveStateTimer) {
1429
- clearTimeout(this.saveStateTimer);
1430
- this.saveStateTimer = null;
1431
- }
1432
- if (this.saveStateDirty) {
1433
- this.saveStateDirty = false;
1434
- await this.saveState();
1435
- }
1436
- }
1437
- };
1438
- function isValidISOTimestamp(value) {
1439
- if (typeof value !== "string") return false;
1440
- const d = new Date(value);
1441
- return !isNaN(d.getTime()) && d.toISOString() === value;
1442
- }
1443
- function serializeEventData(data, maxLen) {
1444
- const str = typeof data === "string" ? data : JSON.stringify(data);
1445
- return str.length > maxLen ? str.slice(0, maxLen) + "\u2026" : str;
1446
- }
1447
-
1448
- export { Orchestrator };