@wrongstack/webui-server 0.296.3 → 0.297.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.js +948 -429
  2. package/dist/index.js.map +4 -4
  3. package/dist/protocol/client-conversation.d.ts +1 -1
  4. package/dist/protocol/client-conversation.d.ts.map +1 -1
  5. package/dist/protocol/index.js +7 -1
  6. package/dist/protocol/index.js.map +2 -2
  7. package/dist/protocol/projections.d.ts +5 -1
  8. package/dist/protocol/projections.d.ts.map +1 -1
  9. package/dist/protocol/registry.d.ts +2 -2
  10. package/dist/protocol/registry.d.ts.map +1 -1
  11. package/dist/protocol/server-conversation.d.ts +1 -1
  12. package/dist/protocol/server-conversation.d.ts.map +1 -1
  13. package/dist/server/backend-services.d.ts +6 -2
  14. package/dist/server/backend-services.d.ts.map +1 -1
  15. package/dist/server/collaboration-ws-handler.d.ts +2 -2
  16. package/dist/server/collaboration-ws-handler.d.ts.map +1 -1
  17. package/dist/server/connections-health-route.d.ts.map +1 -1
  18. package/dist/server/embedded-lifecycle.d.ts +2 -0
  19. package/dist/server/embedded-lifecycle.d.ts.map +1 -1
  20. package/dist/server/entry.js +870 -395
  21. package/dist/server/entry.js.map +4 -4
  22. package/dist/server/goal-ws-handler.d.ts +3 -1
  23. package/dist/server/goal-ws-handler.d.ts.map +1 -1
  24. package/dist/server/handlers.js.map +2 -2
  25. package/dist/server/http-server.d.ts.map +1 -1
  26. package/dist/server/kanban-supervisor.d.ts +6 -0
  27. package/dist/server/kanban-supervisor.d.ts.map +1 -1
  28. package/dist/server/mailbox-handlers.d.ts +2 -1
  29. package/dist/server/mailbox-handlers.d.ts.map +1 -1
  30. package/dist/server/mailbox-routes.d.ts +1 -0
  31. package/dist/server/mailbox-routes.d.ts.map +1 -1
  32. package/dist/server/memory-diagnostics.d.ts +69 -0
  33. package/dist/server/memory-diagnostics.d.ts.map +1 -0
  34. package/dist/server/sdd-board-ws-handler.d.ts +14 -8
  35. package/dist/server/sdd-board-ws-handler.d.ts.map +1 -1
  36. package/dist/server/sdd-wizard-wiring.d.ts +5 -6
  37. package/dist/server/sdd-wizard-wiring.d.ts.map +1 -1
  38. package/dist/server/sdd-wizard-ws-handler.d.ts +16 -6
  39. package/dist/server/sdd-wizard-ws-handler.d.ts.map +1 -1
  40. package/dist/server/start-webui.d.ts.map +1 -1
  41. package/dist/server/types.d.ts +2 -0
  42. package/dist/server/types.d.ts.map +1 -1
  43. package/dist/server/worktree-ws-handler.d.ts +30 -2
  44. package/dist/server/worktree-ws-handler.d.ts.map +1 -1
  45. package/dist/server/ws-payload-validation.d.ts +8 -0
  46. package/dist/server/ws-payload-validation.d.ts.map +1 -1
  47. package/package.json +10 -10
@@ -135,85 +135,85 @@ var ENUM_PREF_KEYS = {
135
135
  autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
136
136
  fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"])
137
137
  };
138
- function validateModelRuntimeValue(modelRuntime, path29) {
138
+ function validateModelRuntimeValue(modelRuntime, path30) {
139
139
  const reasoning = modelRuntime["reasoning"];
140
140
  if (reasoning !== void 0) {
141
- if (!isRecord(reasoning)) return `${path29}.reasoning must be an object when provided`;
141
+ if (!isRecord(reasoning)) return `${path30}.reasoning must be an object when provided`;
142
142
  const mode = reasoning["mode"];
143
143
  const effort = reasoning["effort"];
144
144
  const preserve = reasoning["preserve"];
145
145
  if (mode !== void 0 && (typeof mode !== "string" || !REASONING_MODE_VALUES.has(mode))) {
146
- return `${path29}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
146
+ return `${path30}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
147
147
  }
148
148
  if (effort !== void 0 && (typeof effort !== "string" || !REASONING_EFFORT_VALUES.has(effort))) {
149
- return `${path29}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
149
+ return `${path30}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
150
150
  }
151
151
  if (preserve !== void 0 && typeof preserve !== "boolean") {
152
- return `${path29}.reasoning.preserve must be a boolean when provided`;
152
+ return `${path30}.reasoning.preserve must be a boolean when provided`;
153
153
  }
154
154
  }
155
155
  const cache2 = modelRuntime["cache"];
156
156
  if (cache2 !== void 0) {
157
- if (!isRecord(cache2)) return `${path29}.cache must be an object when provided`;
157
+ if (!isRecord(cache2)) return `${path30}.cache must be an object when provided`;
158
158
  const ttl = cache2["ttl"];
159
159
  if (ttl !== void 0 && (typeof ttl !== "string" || !CACHE_TTL_VALUES.has(ttl) || ttl === "default")) {
160
- return `${path29}.cache.ttl must be one of: 5m, 1h`;
160
+ return `${path30}.cache.ttl must be one of: 5m, 1h`;
161
161
  }
162
162
  }
163
163
  const parameters = modelRuntime["parameters"];
164
164
  if (parameters !== void 0 && !isRecord(parameters)) {
165
- return `${path29}.parameters must be an object when provided`;
165
+ return `${path30}.parameters must be an object when provided`;
166
166
  }
167
167
  return null;
168
168
  }
169
- function validateModelBlackoutRule(rule, path29) {
169
+ function validateModelBlackoutRule(rule, path30) {
170
170
  const id = rule["id"];
171
171
  if (typeof id !== "string" || id.trim().length === 0) {
172
- return `${path29}.id must be a non-empty string`;
172
+ return `${path30}.id must be a non-empty string`;
173
173
  }
174
174
  const start = rule["start"];
175
175
  if (typeof start !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(start)) {
176
- return `${path29}.start must be a string in HH:mm (00:00-23:59) format`;
176
+ return `${path30}.start must be a string in HH:mm (00:00-23:59) format`;
177
177
  }
178
178
  const end = rule["end"];
179
179
  if (typeof end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(end)) {
180
- return `${path29}.end must be a string in HH:mm (00:00-23:59) format`;
180
+ return `${path30}.end must be a string in HH:mm (00:00-23:59) format`;
181
181
  }
182
182
  if (rule["enabled"] !== void 0 && typeof rule["enabled"] !== "boolean") {
183
- return `${path29}.enabled must be a boolean when provided`;
183
+ return `${path30}.enabled must be a boolean when provided`;
184
184
  }
185
185
  if (rule["provider"] !== void 0 && typeof rule["provider"] !== "string") {
186
- return `${path29}.provider must be a string when provided`;
186
+ return `${path30}.provider must be a string when provided`;
187
187
  }
188
188
  if (rule["model"] !== void 0 && typeof rule["model"] !== "string") {
189
- return `${path29}.model must be a string when provided`;
189
+ return `${path30}.model must be a string when provided`;
190
190
  }
191
191
  if (rule["days"] !== void 0) {
192
- if (!Array.isArray(rule["days"])) return `${path29}.days must be an array when provided`;
192
+ if (!Array.isArray(rule["days"])) return `${path30}.days must be an array when provided`;
193
193
  const seen = /* @__PURE__ */ new Set();
194
194
  for (const d of rule["days"]) {
195
195
  if (typeof d !== "number" || !Number.isInteger(d) || d < 0 || d > 6) {
196
- return `${path29}.days elements must be integers 0-6 when provided`;
196
+ return `${path30}.days elements must be integers 0-6 when provided`;
197
197
  }
198
- if (seen.has(d)) return `${path29}.days contains duplicate day: ${d}`;
198
+ if (seen.has(d)) return `${path30}.days contains duplicate day: ${d}`;
199
199
  seen.add(d);
200
200
  }
201
201
  }
202
202
  if (rule["timezone"] !== void 0) {
203
203
  if (typeof rule["timezone"] !== "string") {
204
- return `${path29}.timezone must be a string when provided`;
204
+ return `${path30}.timezone must be a string when provided`;
205
205
  }
206
206
  try {
207
207
  Intl.DateTimeFormat(void 0, { timeZone: rule["timezone"] });
208
208
  } catch {
209
- return `${path29}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
209
+ return `${path30}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
210
210
  }
211
211
  }
212
212
  if (rule["label"] !== void 0 && typeof rule["label"] !== "string") {
213
- return `${path29}.label must be a string when provided`;
213
+ return `${path30}.label must be a string when provided`;
214
214
  }
215
215
  if (rule["mode"] !== void 0 && rule["mode"] !== "blackout" && rule["mode"] !== "allow_only") {
216
- return `${path29}.mode must be 'blackout' or 'allow_only' when provided`;
216
+ return `${path30}.mode must be 'blackout' or 'allow_only' when provided`;
217
217
  }
218
218
  return null;
219
219
  }
@@ -386,6 +386,37 @@ function validateMailboxMessagesPayload(payload) {
386
386
  }
387
387
  };
388
388
  }
389
+ var MAILBOX_ACTIONS = /* @__PURE__ */ new Set(["mark-read", "acknowledge", "reopen", "soft-delete"]);
390
+ function validateMailboxActionPayload(payload) {
391
+ if (!isRecord2(payload)) {
392
+ return { ok: false, message: "mailbox.action payload must be an object" };
393
+ }
394
+ const requestId = payload["requestId"];
395
+ const mailId = payload["mailId"];
396
+ const action = payload["action"];
397
+ const readerId = payload["readerId"];
398
+ if (typeof requestId !== "string" || requestId.trim().length === 0) {
399
+ return { ok: false, message: "mailbox.action payload.requestId must be a non-empty string" };
400
+ }
401
+ if (typeof mailId !== "string" || mailId.trim().length === 0) {
402
+ return { ok: false, message: "mailbox.action payload.mailId must be a non-empty string" };
403
+ }
404
+ if (typeof action !== "string" || !MAILBOX_ACTIONS.has(action)) {
405
+ return { ok: false, message: "mailbox.action payload.action must be a supported action" };
406
+ }
407
+ if (typeof readerId !== "string" || readerId.trim().length === 0) {
408
+ return { ok: false, message: "mailbox.action payload.readerId must be a non-empty string" };
409
+ }
410
+ return {
411
+ ok: true,
412
+ value: {
413
+ requestId: requestId.trim(),
414
+ mailId: mailId.trim(),
415
+ action,
416
+ readerId: readerId.trim()
417
+ }
418
+ };
419
+ }
389
420
  var MAILBOX_SEND_TYPES = /* @__PURE__ */ new Set([
390
421
  "note",
391
422
  "ask",
@@ -402,6 +433,7 @@ function validateMailboxSendPayload(payload) {
402
433
  return { ok: false, message: "mailbox.send payload must be an object" };
403
434
  }
404
435
  const requestId = payload["requestId"];
436
+ const rawFrom = payload["from"];
405
437
  const rawTo = payload["to"];
406
438
  const rawType = payload["type"];
407
439
  const rawAudience = payload["audience"];
@@ -412,6 +444,9 @@ function validateMailboxSendPayload(payload) {
412
444
  if (typeof requestId !== "string" || requestId.trim().length === 0) {
413
445
  return { ok: false, message: "mailbox.send payload.requestId must be a non-empty string" };
414
446
  }
447
+ if (rawFrom !== void 0 && (typeof rawFrom !== "string" || rawFrom.trim().length === 0)) {
448
+ return { ok: false, message: "mailbox.send payload.from must be a non-empty string when provided" };
449
+ }
415
450
  if (typeof rawTo !== "string" || rawTo.trim().length === 0) {
416
451
  return { ok: false, message: "mailbox.send payload.to must be a non-empty string" };
417
452
  }
@@ -448,6 +483,7 @@ function validateMailboxSendPayload(payload) {
448
483
  ok: true,
449
484
  value: {
450
485
  requestId: requestId.trim(),
486
+ ...rawFrom !== void 0 ? { from: rawFrom.trim() } : {},
451
487
  to,
452
488
  type,
453
489
  audience: rawAudience,
@@ -737,8 +773,8 @@ function validateShellOpenPayload(payload) {
737
773
  if (!isRecord2(payload)) {
738
774
  return { ok: false, message: "shell.open payload must be an object with string path" };
739
775
  }
740
- const path29 = payload["path"];
741
- if (typeof path29 !== "string" || path29.trim().length === 0) {
776
+ const path30 = payload["path"];
777
+ if (typeof path30 !== "string" || path30.trim().length === 0) {
742
778
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
743
779
  }
744
780
  const target = payload["target"];
@@ -751,7 +787,7 @@ function validateShellOpenPayload(payload) {
751
787
  return {
752
788
  ok: true,
753
789
  value: {
754
- path: path29,
790
+ path: path30,
755
791
  ...target !== void 0 ? { target } : {}
756
792
  }
757
793
  };
@@ -760,14 +796,14 @@ function validateGitDiffPayload(payload) {
760
796
  if (!isRecord2(payload)) {
761
797
  return { ok: false, message: "git.diff payload must be an object" };
762
798
  }
763
- const path29 = payload["path"];
764
- if (path29 === void 0 || path29 === null) {
799
+ const path30 = payload["path"];
800
+ if (path30 === void 0 || path30 === null) {
765
801
  return { ok: true, value: { path: "" } };
766
802
  }
767
- if (typeof path29 !== "string") {
803
+ if (typeof path30 !== "string") {
768
804
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
769
805
  }
770
- return { ok: true, value: { path: path29 } };
806
+ return { ok: true, value: { path: path30 } };
771
807
  }
772
808
  function validateProjectsAddPayload(payload) {
773
809
  if (!isRecord2(payload)) {
@@ -1529,6 +1565,7 @@ var CollaborationWebSocketHandler = class {
1529
1565
  for (const off of this.offs) off();
1530
1566
  this.offs.length = 0;
1531
1567
  this.stopBroadcast();
1568
+ this.clients.clear();
1532
1569
  }
1533
1570
  // ── Inbound client messages ────────────────────────────────────────────
1534
1571
  /**
@@ -1588,9 +1625,7 @@ var CollaborationWebSocketHandler = class {
1588
1625
  if (activeSessionId !== void 0 && sessionId !== activeSessionId) {
1589
1626
  this.send(
1590
1627
  ws,
1591
- this.errorMessage(
1592
- `collab.join sessionId mismatch (active: ${activeSessionId})`
1593
- )
1628
+ this.errorMessage(`collab.join sessionId mismatch (active: ${activeSessionId})`)
1594
1629
  );
1595
1630
  return;
1596
1631
  }
@@ -1601,26 +1636,19 @@ var CollaborationWebSocketHandler = class {
1601
1636
  if (role === "controller" && !this.bus) {
1602
1637
  this.send(
1603
1638
  ws,
1604
- this.errorMessage(
1605
- `role 'controller' is not available: server has no CollaborationBus`
1606
- )
1639
+ this.errorMessage(`role 'controller' is not available: server has no CollaborationBus`)
1607
1640
  );
1608
1641
  return;
1609
1642
  }
1610
1643
  if (role === "annotator" && !this.annotations) {
1611
1644
  this.send(
1612
1645
  ws,
1613
- this.errorMessage(
1614
- `role 'annotator' is not available: server has no annotations store`
1615
- )
1646
+ this.errorMessage(`role 'annotator' is not available: server has no annotations store`)
1616
1647
  );
1617
1648
  return;
1618
1649
  }
1619
1650
  if (role !== "observer" && this.options.authorizeRole?.({ ws, sessionId, requestedRole: role }) !== true) {
1620
- this.send(
1621
- ws,
1622
- this.errorMessage(`role '${role}' requires explicit server authorization`)
1623
- );
1651
+ this.send(ws, this.errorMessage(`role '${role}' requires explicit server authorization`));
1624
1652
  return;
1625
1653
  }
1626
1654
  const participant = {
@@ -1649,14 +1677,10 @@ var CollaborationWebSocketHandler = class {
1649
1677
  this.broadcast(sessionId, this.stateMessage(sessionId));
1650
1678
  if (this.reader) {
1651
1679
  this.replayHistory(ws, sessionId).catch((err) => {
1652
- this.logger.debug?.(
1653
- `collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`
1654
- );
1680
+ this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`);
1655
1681
  });
1656
1682
  }
1657
- this.logger.debug?.(
1658
- `collab: participant ${participant.participantId} joined ${sessionId}`
1659
- );
1683
+ this.logger.debug?.(`collab: participant ${participant.participantId} joined ${sessionId}`);
1660
1684
  }
1661
1685
  leave(ws) {
1662
1686
  this.handleDisconnect(ws);
@@ -1727,18 +1751,13 @@ var CollaborationWebSocketHandler = class {
1727
1751
  }
1728
1752
  const payload = raw;
1729
1753
  if (!payload?.sessionId || typeof payload.atEventIndex !== "number" || typeof payload.text !== "string") {
1730
- this.send(
1731
- ws,
1732
- this.errorMessage("annotate requires { sessionId, atEventIndex, text }")
1733
- );
1754
+ this.send(ws, this.errorMessage("annotate requires { sessionId, atEventIndex, text }"));
1734
1755
  return;
1735
1756
  }
1736
1757
  if (payload.sessionId !== participant.sessionId) {
1737
1758
  this.send(
1738
1759
  ws,
1739
- this.errorMessage(
1740
- `annotate sessionId mismatch (joined: ${participant.sessionId})`
1741
- )
1760
+ this.errorMessage(`annotate sessionId mismatch (joined: ${participant.sessionId})`)
1742
1761
  );
1743
1762
  return;
1744
1763
  }
@@ -1765,12 +1784,7 @@ var CollaborationWebSocketHandler = class {
1765
1784
  }
1766
1785
  });
1767
1786
  } catch (err) {
1768
- this.send(
1769
- ws,
1770
- this.errorMessage(
1771
- `annotation rejected: ${toErrorMessage(err)}`
1772
- )
1773
- );
1787
+ this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage(err)}`));
1774
1788
  }
1775
1789
  }
1776
1790
  async handleResolve(ws, raw) {
@@ -1786,26 +1800,19 @@ var CollaborationWebSocketHandler = class {
1786
1800
  if (participant.role !== "annotator") {
1787
1801
  this.send(
1788
1802
  ws,
1789
- this.errorMessage(
1790
- `resolve requires the 'annotator' role (current: '${participant.role}')`
1791
- )
1803
+ this.errorMessage(`resolve requires the 'annotator' role (current: '${participant.role}')`)
1792
1804
  );
1793
1805
  return;
1794
1806
  }
1795
1807
  const payload = raw;
1796
1808
  if (!payload?.sessionId || !payload.annotationId) {
1797
- this.send(
1798
- ws,
1799
- this.errorMessage("resolve requires { sessionId, annotationId }")
1800
- );
1809
+ this.send(ws, this.errorMessage("resolve requires { sessionId, annotationId }"));
1801
1810
  return;
1802
1811
  }
1803
1812
  if (payload.sessionId !== participant.sessionId) {
1804
1813
  this.send(
1805
1814
  ws,
1806
- this.errorMessage(
1807
- `resolve sessionId mismatch (joined: ${participant.sessionId})`
1808
- )
1815
+ this.errorMessage(`resolve sessionId mismatch (joined: ${participant.sessionId})`)
1809
1816
  );
1810
1817
  return;
1811
1818
  }
@@ -1816,10 +1823,7 @@ var CollaborationWebSocketHandler = class {
1816
1823
  resolvedBy: participant.participantId
1817
1824
  });
1818
1825
  if (!updated) {
1819
- this.send(
1820
- ws,
1821
- this.errorMessage(`annotation not found: ${payload.annotationId}`)
1822
- );
1826
+ this.send(ws, this.errorMessage(`annotation not found: ${payload.annotationId}`));
1823
1827
  return;
1824
1828
  }
1825
1829
  this.broadcast(payload.sessionId, {
@@ -1832,12 +1836,7 @@ var CollaborationWebSocketHandler = class {
1832
1836
  }
1833
1837
  });
1834
1838
  } catch (err) {
1835
- this.send(
1836
- ws,
1837
- this.errorMessage(
1838
- `resolve failed: ${toErrorMessage(err)}`
1839
- )
1840
- );
1839
+ this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage(err)}`));
1841
1840
  }
1842
1841
  }
1843
1842
  // ── Event subscription (live mirror) ───────────────────────────────────
@@ -1908,9 +1907,7 @@ var CollaborationWebSocketHandler = class {
1908
1907
  seen++;
1909
1908
  }
1910
1909
  } catch (err) {
1911
- this.logger.debug?.(
1912
- `collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`
1913
- );
1910
+ this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`);
1914
1911
  return;
1915
1912
  }
1916
1913
  const tail2 = seen <= REPLAY_LIMIT ? ring.slice(0, seen) : (
@@ -1992,9 +1989,7 @@ var CollaborationWebSocketHandler = class {
1992
1989
  try {
1993
1990
  sendSerialized(p.ws, data);
1994
1991
  } catch (err) {
1995
- this.logger.debug?.(
1996
- `collab broadcast failed: ${toErrorMessage(err)}`
1997
- );
1992
+ this.logger.debug?.(`collab broadcast failed: ${toErrorMessage(err)}`);
1998
1993
  }
1999
1994
  }
2000
1995
  }
@@ -2021,9 +2016,7 @@ var CollaborationWebSocketHandler = class {
2021
2016
  if (participant.role !== "controller") {
2022
2017
  this.send(
2023
2018
  ws,
2024
- this.errorMessage(
2025
- `pause requires the 'controller' role (current: '${participant.role}')`
2026
- )
2019
+ this.errorMessage(`pause requires the 'controller' role (current: '${participant.role}')`)
2027
2020
  );
2028
2021
  return;
2029
2022
  }
@@ -2068,9 +2061,7 @@ var CollaborationWebSocketHandler = class {
2068
2061
  if (participant.role !== "controller") {
2069
2062
  this.send(
2070
2063
  ws,
2071
- this.errorMessage(
2072
- `resume requires the 'controller' role (current: '${participant.role}')`
2073
- )
2064
+ this.errorMessage(`resume requires the 'controller' role (current: '${participant.role}')`)
2074
2065
  );
2075
2066
  return;
2076
2067
  }
@@ -2168,9 +2159,7 @@ var CollaborationWebSocketHandler = class {
2168
2159
  if (payload.sessionId !== participant.sessionId) {
2169
2160
  this.send(
2170
2161
  ws,
2171
- this.errorMessage(
2172
- `inject_tool sessionId mismatch (joined: ${participant.sessionId})`
2173
- )
2162
+ this.errorMessage(`inject_tool sessionId mismatch (joined: ${participant.sessionId})`)
2174
2163
  );
2175
2164
  return;
2176
2165
  }
@@ -2184,9 +2173,7 @@ var CollaborationWebSocketHandler = class {
2184
2173
  if (!queued) {
2185
2174
  this.send(
2186
2175
  ws,
2187
- this.errorMessage(
2188
- `an injection for toolUseId ${payload.toolUseId} is already queued`
2189
- )
2176
+ this.errorMessage(`an injection for toolUseId ${payload.toolUseId} is already queued`)
2190
2177
  );
2191
2178
  return;
2192
2179
  }
@@ -4288,8 +4275,8 @@ function jsonByteLength(value) {
4288
4275
  return MAX_PAYLOAD_BYTES + 1;
4289
4276
  }
4290
4277
  }
4291
- function error(errors, path29, code, message) {
4292
- errors.push({ path: path29, code, message });
4278
+ function error(errors, path30, code, message) {
4279
+ errors.push({ path: path30, code, message });
4293
4280
  }
4294
4281
  function isMessageRole(value) {
4295
4282
  return value === "user" || value === "assistant" || value === "system";
@@ -4297,25 +4284,25 @@ function isMessageRole(value) {
4297
4284
  function isPlainJsonObject(value) {
4298
4285
  return isRecord3(value);
4299
4286
  }
4300
- function validateCacheControl(value, path29, errors) {
4287
+ function validateCacheControl(value, path30, errors) {
4301
4288
  if (value === void 0) return void 0;
4302
4289
  if (!isRecord3(value) || value["type"] !== "ephemeral") {
4303
- error(errors, path29, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4290
+ error(errors, path30, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4304
4291
  return void 0;
4305
4292
  }
4306
4293
  return { type: "ephemeral" };
4307
4294
  }
4308
- function validateProviderMeta(value, path29, errors) {
4295
+ function validateProviderMeta(value, path30, errors) {
4309
4296
  if (value === void 0) return void 0;
4310
4297
  if (!isPlainJsonObject(value)) {
4311
- error(errors, path29, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4298
+ error(errors, path30, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4312
4299
  return void 0;
4313
4300
  }
4314
4301
  return value;
4315
4302
  }
4316
- function validateBlock(value, path29, errors) {
4303
+ function validateBlock(value, path30, errors) {
4317
4304
  if (!isRecord3(value)) {
4318
- error(errors, path29, "INVALID_BLOCK", "Content block must be an object.");
4305
+ error(errors, path30, "INVALID_BLOCK", "Content block must be an object.");
4319
4306
  return void 0;
4320
4307
  }
4321
4308
  const type = value["type"];
@@ -4323,14 +4310,14 @@ function validateBlock(value, path29, errors) {
4323
4310
  case "text": {
4324
4311
  const text = value["text"];
4325
4312
  if (typeof text !== "string") {
4326
- error(errors, `${path29}/text`, "INVALID_TEXT", "Text block text must be a string.");
4313
+ error(errors, `${path30}/text`, "INVALID_TEXT", "Text block text must be a string.");
4327
4314
  return void 0;
4328
4315
  }
4329
4316
  if (text.length > MAX_STRING_LENGTH) {
4330
- error(errors, `${path29}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4317
+ error(errors, `${path30}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4331
4318
  return void 0;
4332
4319
  }
4333
- const cacheControl = validateCacheControl(value["cache_control"], `${path29}/cache_control`, errors);
4320
+ const cacheControl = validateCacheControl(value["cache_control"], `${path30}/cache_control`, errors);
4334
4321
  return cacheControl ? { type: "text", text, cache_control: cacheControl } : { type: "text", text };
4335
4322
  }
4336
4323
  case "tool_use": {
@@ -4338,15 +4325,15 @@ function validateBlock(value, path29, errors) {
4338
4325
  const name2 = value["name"];
4339
4326
  const input = value["input"];
4340
4327
  if (typeof id !== "string" || id.length === 0) {
4341
- error(errors, `${path29}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4328
+ error(errors, `${path30}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4342
4329
  }
4343
4330
  if (typeof name2 !== "string" || name2.length === 0) {
4344
- error(errors, `${path29}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4331
+ error(errors, `${path30}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4345
4332
  }
4346
4333
  if (!isPlainJsonObject(input)) {
4347
- error(errors, `${path29}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4334
+ error(errors, `${path30}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4348
4335
  }
4349
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path29}/providerMeta`, errors);
4336
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path30}/providerMeta`, errors);
4350
4337
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4351
4338
  return void 0;
4352
4339
  }
@@ -4358,18 +4345,18 @@ function validateBlock(value, path29, errors) {
4358
4345
  const content = value["content"];
4359
4346
  const isError = value["is_error"];
4360
4347
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4361
- error(errors, `${path29}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4348
+ error(errors, `${path30}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4362
4349
  }
4363
4350
  if (name2 !== void 0 && typeof name2 !== "string") {
4364
- error(errors, `${path29}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4351
+ error(errors, `${path30}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4365
4352
  }
4366
4353
  if (typeof content !== "string") {
4367
- error(errors, `${path29}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4354
+ error(errors, `${path30}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4368
4355
  } else if (content.length > MAX_STRING_LENGTH) {
4369
- error(errors, `${path29}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4356
+ error(errors, `${path30}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4370
4357
  }
4371
4358
  if (isError !== void 0 && typeof isError !== "boolean") {
4372
- error(errors, `${path29}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4359
+ error(errors, `${path30}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4373
4360
  }
4374
4361
  if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4375
4362
  return {
@@ -4383,25 +4370,25 @@ function validateBlock(value, path29, errors) {
4383
4370
  case "image": {
4384
4371
  const source = value["source"];
4385
4372
  if (!isRecord3(source)) {
4386
- error(errors, `${path29}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4373
+ error(errors, `${path30}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4387
4374
  return void 0;
4388
4375
  }
4389
4376
  const sourceType = source["type"];
4390
4377
  if (sourceType !== "base64" && sourceType !== "url") {
4391
- error(errors, `${path29}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4378
+ error(errors, `${path30}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4392
4379
  return void 0;
4393
4380
  }
4394
4381
  const mediaType = source["media_type"];
4395
4382
  const data = source["data"];
4396
4383
  const url = source["url"];
4397
4384
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4398
- error(errors, `${path29}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4385
+ error(errors, `${path30}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4399
4386
  }
4400
4387
  if (data !== void 0 && typeof data !== "string") {
4401
- error(errors, `${path29}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4388
+ error(errors, `${path30}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4402
4389
  }
4403
4390
  if (url !== void 0 && typeof url !== "string") {
4404
- error(errors, `${path29}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4391
+ error(errors, `${path30}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4405
4392
  }
4406
4393
  return {
4407
4394
  type: "image",
@@ -4417,13 +4404,13 @@ function validateBlock(value, path29, errors) {
4417
4404
  const thinking = value["thinking"];
4418
4405
  const signature = value["signature"];
4419
4406
  if (typeof thinking !== "string") {
4420
- error(errors, `${path29}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4407
+ error(errors, `${path30}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4421
4408
  return void 0;
4422
4409
  }
4423
4410
  if (signature !== void 0 && typeof signature !== "string") {
4424
- error(errors, `${path29}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4411
+ error(errors, `${path30}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4425
4412
  }
4426
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path29}/providerMeta`, errors);
4413
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path30}/providerMeta`, errors);
4427
4414
  return {
4428
4415
  type: "thinking",
4429
4416
  thinking,
@@ -4432,7 +4419,7 @@ function validateBlock(value, path29, errors) {
4432
4419
  };
4433
4420
  }
4434
4421
  default:
4435
- error(errors, `${path29}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4422
+ error(errors, `${path30}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4436
4423
  return void 0;
4437
4424
  }
4438
4425
  }
@@ -4450,39 +4437,39 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4450
4437
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
4451
4438
  }
4452
4439
  value.forEach((item, index) => {
4453
- const path29 = `/messages/${index}`;
4440
+ const path30 = `/messages/${index}`;
4454
4441
  if (!isRecord3(item)) {
4455
- error(errors, path29, "INVALID_MESSAGE", "Message must be an object.");
4442
+ error(errors, path30, "INVALID_MESSAGE", "Message must be an object.");
4456
4443
  return;
4457
4444
  }
4458
4445
  const role = item["role"];
4459
4446
  if (!isMessageRole(role)) {
4460
- error(errors, `${path29}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4447
+ error(errors, `${path30}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4461
4448
  return;
4462
4449
  }
4463
4450
  const rawContent = item["content"];
4464
4451
  let content;
4465
4452
  if (typeof rawContent === "string") {
4466
4453
  if (rawContent.length > MAX_STRING_LENGTH) {
4467
- error(errors, `${path29}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4454
+ error(errors, `${path30}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4468
4455
  return;
4469
4456
  }
4470
4457
  content = rawContent;
4471
4458
  } else if (Array.isArray(rawContent)) {
4472
4459
  const blocks = [];
4473
4460
  rawContent.forEach((block, blockIndex) => {
4474
- const parsed = validateBlock(block, `${path29}/content/${blockIndex}`, errors);
4461
+ const parsed = validateBlock(block, `${path30}/content/${blockIndex}`, errors);
4475
4462
  if (parsed) blocks.push(parsed);
4476
4463
  });
4477
4464
  content = blocks;
4478
4465
  } else {
4479
- error(errors, `${path29}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4466
+ error(errors, `${path30}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4480
4467
  return;
4481
4468
  }
4482
4469
  const ts = item["ts"];
4483
4470
  if (ts !== void 0) {
4484
4471
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4485
- error(errors, `${path29}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4472
+ error(errors, `${path30}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4486
4473
  return;
4487
4474
  }
4488
4475
  }
@@ -4871,6 +4858,7 @@ import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wron
4871
4858
  import {
4872
4859
  checkCodebaseIndexServerHealth,
4873
4860
  getIndexState,
4861
+ resolveProjectIndexDaemonAvailability,
4874
4862
  shutdownCodebaseIndexServer
4875
4863
  } from "@wrongstack/tools";
4876
4864
  async function handleConnectionsHealthRoute(context, ws, message) {
@@ -4963,6 +4951,19 @@ async function chronicleHealth(projectRoot) {
4963
4951
  }
4964
4952
  async function codebaseIndexHealth(projectRoot, indexDir) {
4965
4953
  const startedAt = Date.now();
4954
+ const availability = resolveProjectIndexDaemonAvailability(projectRoot, indexDir);
4955
+ if (availability.kind === "endpoint-invalid") {
4956
+ return {
4957
+ id: "codebase-index",
4958
+ label: "Codebase index",
4959
+ status: "unavailable",
4960
+ required: false,
4961
+ mode: "endpoint-invalid",
4962
+ detail: `Socket path is ${availability.byteLength} bytes \u2014 over this platform's ${availability.maxBytes}-byte sun_path limit. Queries fall back to a process-local index. Set a shorter TMPDIR to restore the shared daemon.`,
4963
+ endpoint: availability.endpoint,
4964
+ latencyMs: Date.now() - startedAt
4965
+ };
4966
+ }
4966
4967
  try {
4967
4968
  const health = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
4968
4969
  timeoutMs: 2e3
@@ -5406,15 +5407,15 @@ async function handleGitChanges(ws, projectRoot) {
5406
5407
  if (!m) continue;
5407
5408
  const added = m[1] === "-" ? 0 : Number(m[1]);
5408
5409
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5409
- let path29 = m[3] ?? "";
5410
- if (path29 === "") {
5410
+ let path30 = m[3] ?? "";
5411
+ if (path30 === "") {
5411
5412
  i += 1;
5412
- path29 = parts[i + 1] ?? parts[i] ?? "";
5413
+ path30 = parts[i + 1] ?? parts[i] ?? "";
5413
5414
  i += 1;
5414
5415
  }
5415
- if (!path29) continue;
5416
- const prev = counts.get(path29) ?? { added: 0, deleted: 0 };
5417
- counts.set(path29, { added: prev.added + added, deleted: prev.deleted + deleted });
5416
+ if (!path30) continue;
5417
+ const prev = counts.get(path30) ?? { added: 0, deleted: 0 };
5418
+ counts.set(path30, { added: prev.added + added, deleted: prev.deleted + deleted });
5418
5419
  }
5419
5420
  };
5420
5421
  parseNumstat(unstagedNumstat);
@@ -5426,7 +5427,7 @@ async function handleGitChanges(ws, projectRoot) {
5426
5427
  if (!rec || rec.length < 3) continue;
5427
5428
  const x = rec[0] ?? " ";
5428
5429
  const y = rec[1] ?? " ";
5429
- const path29 = rec.slice(3);
5430
+ const path30 = rec.slice(3);
5430
5431
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5431
5432
  if (isRename) i += 1;
5432
5433
  let status;
@@ -5438,13 +5439,13 @@ async function handleGitChanges(ws, projectRoot) {
5438
5439
  else if (x === "D" || y === "D") status = "D";
5439
5440
  else status = "M";
5440
5441
  const staged = x !== " " && x !== "?";
5441
- let added = counts.get(path29)?.added ?? 0;
5442
- let deleted = counts.get(path29)?.deleted ?? 0;
5442
+ let added = counts.get(path30)?.added ?? 0;
5443
+ let deleted = counts.get(path30)?.deleted ?? 0;
5443
5444
  if (status === "?") {
5444
5445
  added = 0;
5445
5446
  deleted = 0;
5446
5447
  }
5447
- files.push({ path: path29, status, added, deleted, staged });
5448
+ files.push({ path: path30, status, added, deleted, staged });
5448
5449
  }
5449
5450
  send(ws, { type: "git.changes", payload: { files } });
5450
5451
  } catch (err) {
@@ -5455,21 +5456,21 @@ async function handleGitChanges(ws, projectRoot) {
5455
5456
  }
5456
5457
  }
5457
5458
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5458
- async function handleGitDiff(ws, projectRoot, path29) {
5459
+ async function handleGitDiff(ws, projectRoot, path30) {
5459
5460
  const cwd = projectRoot || void 0;
5460
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path29, ...extra } });
5461
- if (!path29 || path29.includes("\0") || path29.includes("..") || nodePath.isAbsolute(path29)) {
5461
+ const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path30, ...extra } });
5462
+ if (!path30 || path30.includes("\0") || path30.includes("..") || nodePath.isAbsolute(path30)) {
5462
5463
  reply2({ oldText: "", newText: "", error: "invalid path" });
5463
5464
  return;
5464
5465
  }
5465
5466
  try {
5466
5467
  const git = makeGit(cwd);
5467
5468
  const { readFile: readFile11 } = await import("node:fs/promises");
5468
- const { join: join15 } = await import("node:path");
5469
- const oldText = await git(["show", `HEAD:${path29}`]);
5469
+ const { join: join16 } = await import("node:path");
5470
+ const oldText = await git(["show", `HEAD:${path30}`]);
5470
5471
  let newText = "";
5471
5472
  try {
5472
- const abs = cwd ? join15(cwd, path29) : path29;
5473
+ const abs = cwd ? join16(cwd, path30) : path30;
5473
5474
  const buf = await readFile11(abs);
5474
5475
  if (buf.includes(0)) {
5475
5476
  reply2({ oldText: "", newText: "", binary: true });
@@ -5531,10 +5532,7 @@ async function handleGoalSnapshotRoute(ws, msg, handlers) {
5531
5532
  }
5532
5533
 
5533
5534
  // src/server/goal-ws-handler.ts
5534
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5535
- import {
5536
- assignNickname
5537
- } from "@wrongstack/core/coordination";
5535
+ import { assignNickname } from "@wrongstack/core/coordination";
5538
5536
  import {
5539
5537
  GoalAssessor,
5540
5538
  GoalPlanner,
@@ -5542,6 +5540,7 @@ import {
5542
5540
  PhaseOrchestrator,
5543
5541
  PhaseStore
5544
5542
  } from "@wrongstack/core/goal";
5543
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5545
5544
  import { WorktreeManager } from "@wrongstack/core/worktree";
5546
5545
 
5547
5546
  // src/server/git-process.ts
@@ -5578,12 +5577,7 @@ function deriveTitle(goal) {
5578
5577
  return trimmed || "Goal";
5579
5578
  }
5580
5579
  async function commitsSince(cwd, baseSha, branch) {
5581
- const output = await gitStdout(cwd, [
5582
- "log",
5583
- "--reverse",
5584
- "--format=%H",
5585
- `${baseSha}..${branch}`
5586
- ]);
5580
+ const output = await gitStdout(cwd, ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`]);
5587
5581
  if (output === null) return [];
5588
5582
  return output.split("\n").map((s) => s.trim()).filter(Boolean);
5589
5583
  }
@@ -5634,6 +5628,17 @@ var GoalWebSocketHandler = class {
5634
5628
  ws.on("error", () => this.clients.delete(client));
5635
5629
  this.sendState(client);
5636
5630
  }
5631
+ /** Release timers, in-flight work, and socket references owned by this host. */
5632
+ dispose() {
5633
+ this.stopping = true;
5634
+ this.abort?.abort();
5635
+ this.abort = null;
5636
+ this.assessAbort?.abort();
5637
+ this.assessAbort = null;
5638
+ this.orchestrator?.stop();
5639
+ this.stopBroadcast();
5640
+ this.clients.clear();
5641
+ }
5637
5642
  async handleMessage(ws, msg) {
5638
5643
  switch (msg.type) {
5639
5644
  case "goal.assess":
@@ -5681,7 +5686,8 @@ var GoalWebSocketHandler = class {
5681
5686
  }
5682
5687
  case "goal.assignTask": {
5683
5688
  const { taskId, agentId, agentName } = msg.payload;
5684
- if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
5689
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName))
5690
+ this.afterBoardMutation();
5685
5691
  break;
5686
5692
  }
5687
5693
  case "goal.addTask": {
@@ -5726,7 +5732,10 @@ var GoalWebSocketHandler = class {
5726
5732
  this.graph = graph;
5727
5733
  this.broadcast({ type: "goal.state", payload: this.buildState() });
5728
5734
  } else {
5729
- this.broadcast({ type: "goal.error", payload: { message: `Graph not found: ${graphId}` } });
5735
+ this.broadcast({
5736
+ type: "goal.error",
5737
+ payload: { message: `Graph not found: ${graphId}` }
5738
+ });
5730
5739
  }
5731
5740
  }
5732
5741
  break;
@@ -5749,10 +5758,13 @@ var GoalWebSocketHandler = class {
5749
5758
  const mySeq = ++this.assessSeq;
5750
5759
  const sendResult7 = (result) => {
5751
5760
  if (mySeq !== this.assessSeq) return;
5752
- sendSerialized(ws, JSON.stringify({
5753
- type: "goal.assess.result",
5754
- payload: { ...result, reqSeq: seq }
5755
- }));
5761
+ sendSerialized(
5762
+ ws,
5763
+ JSON.stringify({
5764
+ type: "goal.assess.result",
5765
+ payload: { ...result, reqSeq: seq }
5766
+ })
5767
+ );
5756
5768
  };
5757
5769
  if (!goal.trim()) {
5758
5770
  sendResult7({
@@ -5798,15 +5810,24 @@ var GoalWebSocketHandler = class {
5798
5810
  const multiBoard = payload?.multiBoard ?? false;
5799
5811
  const verifyTasks = payload?.verifyTasks ?? false;
5800
5812
  const chimeraReview = payload?.chimeraReview ?? false;
5801
- this.abort = new AbortController();
5813
+ const runAbort = new AbortController();
5814
+ this.abort = runAbort;
5802
5815
  this.stopping = false;
5803
- const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
5804
- if (this.stopping || this.abort.signal.aborted) {
5816
+ const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, runAbort.signal);
5817
+ if (this.stopping || runAbort.signal.aborted) {
5805
5818
  this.broadcast({ type: "goal.stopped", payload: { title } });
5806
5819
  return;
5807
5820
  }
5808
5821
  this.logger.info(`[Goal] Starting: ${title}`);
5809
- const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous, multiBoard, verifyTasks, chimeraReview }).build();
5822
+ const graph = await new PhaseGraphBuilder({
5823
+ title,
5824
+ description: goal,
5825
+ phases,
5826
+ autonomous,
5827
+ multiBoard,
5828
+ verifyTasks,
5829
+ chimeraReview
5830
+ }).build();
5810
5831
  this.graph = graph;
5811
5832
  await this.store.save(graph);
5812
5833
  const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_GOAL_WORKTREES"] !== "0";
@@ -5825,9 +5846,10 @@ var GoalWebSocketHandler = class {
5825
5846
  maybeVerify.verifyPhase = (async (phase, env) => {
5826
5847
  const cwd = env?.cwd ?? this.projectRoot;
5827
5848
  try {
5828
- const { exec } = await import("node:child_process");
5849
+ const { execFile: execFile2 } = await import("node:child_process");
5829
5850
  const result = await new Promise((resolve15) => {
5830
- exec("npx tsc --noEmit", { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
5851
+ const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
5852
+ execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
5831
5853
  if (err && err.code === "ENOENT") {
5832
5854
  resolve15("[verify] tsc not found \u2014 skipping");
5833
5855
  return;
@@ -5969,11 +5991,41 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
5969
5991
  /** Generic fallback phases when the LLM planner produces nothing usable. */
5970
5992
  defaultPhases() {
5971
5993
  return [
5972
- { name: "Discovery", description: "Requirements gathering", priority: "high", estimateHours: 2, parallelizable: false },
5973
- { name: "Design", description: "Architecture and design", priority: "critical", estimateHours: 4, parallelizable: false },
5974
- { name: "Implementation", description: "Core development", priority: "critical", estimateHours: 12, parallelizable: false },
5975
- { name: "Testing", description: "Unit and integration tests", priority: "high", estimateHours: 6, parallelizable: true },
5976
- { name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
5994
+ {
5995
+ name: "Discovery",
5996
+ description: "Requirements gathering",
5997
+ priority: "high",
5998
+ estimateHours: 2,
5999
+ parallelizable: false
6000
+ },
6001
+ {
6002
+ name: "Design",
6003
+ description: "Architecture and design",
6004
+ priority: "critical",
6005
+ estimateHours: 4,
6006
+ parallelizable: false
6007
+ },
6008
+ {
6009
+ name: "Implementation",
6010
+ description: "Core development",
6011
+ priority: "critical",
6012
+ estimateHours: 12,
6013
+ parallelizable: false
6014
+ },
6015
+ {
6016
+ name: "Testing",
6017
+ description: "Unit and integration tests",
6018
+ priority: "high",
6019
+ estimateHours: 6,
6020
+ parallelizable: true
6021
+ },
6022
+ {
6023
+ name: "Deployment",
6024
+ description: "Deploy to production",
6025
+ priority: "medium",
6026
+ estimateHours: 2,
6027
+ parallelizable: false
6028
+ }
5977
6029
  ];
5978
6030
  }
5979
6031
  /** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
@@ -6054,8 +6106,10 @@ Type: ${task.type}`;
6054
6106
  try {
6055
6107
  const result_ = await this.agent.run(reviewPrompt);
6056
6108
  if (result_.status === "done" && result_.finalText) {
6057
- this.logger.info(`[Goal] Chimera review for "${task.title}":
6058
- ${result_.finalText.slice(0, 2e3)}`);
6109
+ this.logger.info(
6110
+ `[Goal] Chimera review for "${task.title}":
6111
+ ${result_.finalText.slice(0, 2e3)}`
6112
+ );
6059
6113
  }
6060
6114
  } catch (err) {
6061
6115
  this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
@@ -6108,7 +6162,16 @@ ${result_.finalText.slice(0, 2e3)}`);
6108
6162
  }
6109
6163
  buildState(activePhaseId) {
6110
6164
  if (!this.graph) {
6111
- return { phases: [], tasks: [], overallPercent: 0, autonomous: true, title: "", multiBoard: false, verifyTasks: false, chimeraReview: false };
6165
+ return {
6166
+ phases: [],
6167
+ tasks: [],
6168
+ overallPercent: 0,
6169
+ autonomous: true,
6170
+ title: "",
6171
+ multiBoard: false,
6172
+ verifyTasks: false,
6173
+ chimeraReview: false
6174
+ };
6112
6175
  }
6113
6176
  const phases = Array.from(this.graph.phases.values());
6114
6177
  const currentActiveId = activePhaseId || phases.find((p) => p.status === "running")?.id || phases[0]?.id || "";
@@ -6606,9 +6669,9 @@ async function handleApiAnalyticsSummary(res) {
6606
6669
  }
6607
6670
 
6608
6671
  // src/server/http-server.ts
6609
- import * as fs9 from "node:fs/promises";
6672
+ import * as fs10 from "node:fs/promises";
6610
6673
  import * as http from "node:http";
6611
- import * as path12 from "node:path";
6674
+ import * as path13 from "node:path";
6612
6675
  import * as v8 from "node:v8";
6613
6676
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6614
6677
 
@@ -7292,18 +7355,153 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7292
7355
  }
7293
7356
  }
7294
7357
 
7295
- // src/server/projects-manifest.ts
7358
+ // src/server/memory-diagnostics.ts
7296
7359
  import * as fs8 from "node:fs/promises";
7297
7360
  import * as path11 from "node:path";
7361
+ var DEFAULT_TAIL_BYTES = 1024 * 1024;
7362
+ var MAX_PROCESSES = 32;
7363
+ function finiteNumber(record, key) {
7364
+ const value = record[key];
7365
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7366
+ }
7367
+ function stringValue(record, key) {
7368
+ const value = record[key];
7369
+ return typeof value === "string" && value.length > 0 ? value : void 0;
7370
+ }
7371
+ function toProcessDiagnostic(record) {
7372
+ const pid = finiteNumber(record, "pid");
7373
+ const ts = stringValue(record, "ts");
7374
+ if (pid === void 0 || ts === void 0) return null;
7375
+ const queueEntries = finiteNumber(record, "hqQueueEntries");
7376
+ const queueBytes = finiteNumber(record, "hqQueueBytes");
7377
+ const queueMaxBytes = finiteNumber(record, "hqQueueMaxBytes");
7378
+ const snapshotInFlight = finiteNumber(record, "hqSnapshotInFlight");
7379
+ const snapshotPending = finiteNumber(record, "hqSnapshotPending");
7380
+ const snapshotTimerScheduled = finiteNumber(record, "hqSnapshotTimerScheduled");
7381
+ const eventInFlight = finiteNumber(record, "hqEventInFlight");
7382
+ const eventPending = finiteNumber(record, "hqEventPending");
7383
+ const eventCoalesced = finiteNumber(record, "hqEventCoalesced");
7384
+ const eventDropped = finiteNumber(record, "hqEventDropped");
7385
+ return {
7386
+ pid,
7387
+ surface: stringValue(record, "surface") ?? "unknown",
7388
+ ...stringValue(record, "sessionId") !== void 0 ? { sessionId: stringValue(record, "sessionId") } : {},
7389
+ ts,
7390
+ memory: {
7391
+ rss: finiteNumber(record, "rss") ?? 0,
7392
+ heapUsed: finiteNumber(record, "heapUsed") ?? 0,
7393
+ heapTotal: finiteNumber(record, "heapTotal") ?? 0,
7394
+ retainedHeapUsed: finiteNumber(record, "retainedHeapUsed"),
7395
+ nativeResidual: finiteNumber(record, "nativeResidual"),
7396
+ external: finiteNumber(record, "external"),
7397
+ arrayBuffers: finiteNumber(record, "arrayBuffers")
7398
+ },
7399
+ signal: stringValue(record, "memorySignal"),
7400
+ heapGrowthBytesPerHour: finiteNumber(record, "heapGrowthBytesPerHour"),
7401
+ rssGrowthBytesPerHour: finiteNumber(record, "rssGrowthBytesPerHour"),
7402
+ workload: {
7403
+ messages: finiteNumber(record, "messages"),
7404
+ messageEstimatedTokens: finiteNumber(record, "messageEstimatedTokens"),
7405
+ historyEntries: finiteNumber(record, "historyEntries"),
7406
+ historyMountedEntries: finiteNumber(record, "historyMountedEntries"),
7407
+ historyCachedGroups: finiteNumber(record, "historyCachedGroups"),
7408
+ appRenders: finiteNumber(record, "appRenders"),
7409
+ metricsDroppedObservations: finiteNumber(record, "metricsDroppedObservations"),
7410
+ kanbanSyncActive: finiteNumber(record, "kanbanSyncActive") === 1,
7411
+ kanbanSyncPendingBoards: finiteNumber(record, "kanbanSyncPendingBoards"),
7412
+ kanbanSyncFullRescanPending: finiteNumber(record, "kanbanSyncFullRescanPending") === 1,
7413
+ kanbanSyncRemoteApplyQueued: finiteNumber(record, "kanbanSyncRemoteApplyQueued") === 1,
7414
+ kanbanSyncPendingRemoteBoards: finiteNumber(record, "kanbanSyncPendingRemoteBoards"),
7415
+ kanbanSyncPublishRuns: finiteNumber(record, "kanbanSyncPublishRuns"),
7416
+ kanbanSyncCoalescedRefreshes: finiteNumber(record, "kanbanSyncCoalescedRefreshes"),
7417
+ kanbanSupervisorSnapshots: finiteNumber(record, "kanbanSupervisorSnapshots"),
7418
+ kanbanSupervisorScheduledBoards: finiteNumber(record, "kanbanSupervisorScheduledBoards"),
7419
+ kanbanSupervisorAgentCooldowns: finiteNumber(record, "kanbanSupervisorAgentCooldowns"),
7420
+ kanbanSupervisorRunningAgents: finiteNumber(record, "kanbanSupervisorRunningAgents")
7421
+ },
7422
+ resources: {
7423
+ active: finiteNumber(record, "activeResources"),
7424
+ types: stringValue(record, "activeResourceTypes")
7425
+ },
7426
+ ...queueEntries !== void 0 && queueBytes !== void 0 && queueMaxBytes !== void 0 ? {
7427
+ hqQueue: {
7428
+ entries: queueEntries,
7429
+ bytes: queueBytes,
7430
+ maxBytes: queueMaxBytes,
7431
+ droppedFrames: finiteNumber(record, "hqQueueDroppedFrames") ?? 0,
7432
+ droppedBytes: finiteNumber(record, "hqQueueDroppedBytes") ?? 0,
7433
+ coalescedFrames: finiteNumber(record, "hqQueueCoalescedFrames") ?? 0,
7434
+ coalescedBytes: finiteNumber(record, "hqQueueCoalescedBytes") ?? 0
7435
+ }
7436
+ } : {},
7437
+ ...snapshotInFlight !== void 0 || snapshotPending !== void 0 || snapshotTimerScheduled !== void 0 ? {
7438
+ hqSnapshot: {
7439
+ inFlight: snapshotInFlight === 1,
7440
+ pending: snapshotPending === 1,
7441
+ timerScheduled: snapshotTimerScheduled === 1,
7442
+ eventInFlight: eventInFlight === 1,
7443
+ pendingEvents: eventPending ?? 0,
7444
+ coalescedEvents: eventCoalesced ?? 0,
7445
+ droppedEvents: eventDropped ?? 0
7446
+ }
7447
+ } : {},
7448
+ profileTopStack: stringValue(record, "memoryProfileTopStack"),
7449
+ profileTopStackBytes: finiteNumber(record, "memoryProfileTopStackBytes")
7450
+ };
7451
+ }
7452
+ async function readRecentProcessMemoryDiagnostics(globalRoot, tailBytes = DEFAULT_TAIL_BYTES) {
7453
+ if (!globalRoot) return [];
7454
+ const heapLog = path11.join(globalRoot, "logs", "heap.jsonl");
7455
+ let handle;
7456
+ try {
7457
+ handle = await fs8.open(heapLog, "r");
7458
+ const stat3 = await handle.stat();
7459
+ const length = Math.min(stat3.size, Math.max(1, tailBytes));
7460
+ if (length === 0) return [];
7461
+ const start = stat3.size - length;
7462
+ const buffer = Buffer.allocUnsafe(length);
7463
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
7464
+ let text = buffer.subarray(0, bytesRead).toString("utf8");
7465
+ if (start > 0) {
7466
+ const firstNewline = text.indexOf("\n");
7467
+ if (firstNewline === -1) return [];
7468
+ text = text.slice(firstNewline + 1);
7469
+ }
7470
+ const newestByPid = /* @__PURE__ */ new Map();
7471
+ const lines = text.split(/\r?\n/u);
7472
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
7473
+ const line = lines[index]?.trim();
7474
+ if (!line) continue;
7475
+ try {
7476
+ const parsed = JSON.parse(line);
7477
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
7478
+ const diagnostic = toProcessDiagnostic(parsed);
7479
+ if (!diagnostic || newestByPid.has(diagnostic.pid)) continue;
7480
+ newestByPid.set(diagnostic.pid, diagnostic);
7481
+ if (newestByPid.size >= MAX_PROCESSES) break;
7482
+ } catch {
7483
+ }
7484
+ }
7485
+ return [...newestByPid.values()].sort((a, b) => b.ts.localeCompare(a.ts));
7486
+ } catch {
7487
+ return [];
7488
+ } finally {
7489
+ await handle?.close().catch(() => void 0);
7490
+ }
7491
+ }
7492
+
7493
+ // src/server/projects-manifest.ts
7494
+ import * as fs9 from "node:fs/promises";
7495
+ import * as path12 from "node:path";
7298
7496
  import { ConfigError } from "@wrongstack/core/types";
7299
7497
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7300
7498
  function projectsJsonPath(globalConfigPath) {
7301
- const base = path11.dirname(globalConfigPath);
7302
- return path11.join(base, "projects.json");
7499
+ const base = path12.dirname(globalConfigPath);
7500
+ return path12.join(base, "projects.json");
7303
7501
  }
7304
7502
  async function loadManifest(globalConfigPath) {
7305
7503
  try {
7306
- const raw = await fs8.readFile(projectsJsonPath(globalConfigPath), "utf8");
7504
+ const raw = await fs9.readFile(projectsJsonPath(globalConfigPath), "utf8");
7307
7505
  const parsed = JSON.parse(raw);
7308
7506
  return { projects: parsed.projects ?? [] };
7309
7507
  } catch {
@@ -7312,37 +7510,37 @@ async function loadManifest(globalConfigPath) {
7312
7510
  }
7313
7511
  async function saveManifest(manifest, globalConfigPath) {
7314
7512
  const file = projectsJsonPath(globalConfigPath);
7315
- await fs8.mkdir(path11.dirname(file), { recursive: true });
7316
- await fs8.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7513
+ await fs9.mkdir(path12.dirname(file), { recursive: true });
7514
+ await fs9.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7317
7515
  }
7318
7516
  function generateProjectSlug(rootPath) {
7319
7517
  return projectSlug(rootPath);
7320
7518
  }
7321
7519
  async function ensureProjectDataDir(slug, globalConfigPath) {
7322
- const base = path11.dirname(globalConfigPath);
7323
- const dir = path11.join(base, "projects", slug);
7324
- await fs8.mkdir(dir, { recursive: true });
7520
+ const base = path12.dirname(globalConfigPath);
7521
+ const dir = path12.join(base, "projects", slug);
7522
+ await fs9.mkdir(dir, { recursive: true });
7325
7523
  return dir;
7326
7524
  }
7327
7525
  async function touchProjectInManifest(options, globalConfigPath) {
7328
- const root = path11.resolve(options.projectRoot);
7526
+ const root = path12.resolve(options.projectRoot);
7329
7527
  const file = projectsJsonPath(globalConfigPath);
7330
7528
  let entry;
7331
7529
  await withFileLock(file, async () => {
7332
7530
  const manifest = await loadManifest(globalConfigPath);
7333
7531
  const now = (/* @__PURE__ */ new Date()).toISOString();
7334
- entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7532
+ entry = manifest.projects.find((candidate) => path12.resolve(candidate.root) === root);
7335
7533
  if (entry) {
7336
7534
  entry.lastSeen = now;
7337
- if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7535
+ if (options.workingDir) entry.lastWorkingDir = path12.resolve(options.workingDir);
7338
7536
  } else {
7339
7537
  entry = {
7340
- name: options.name ?? path11.basename(root),
7538
+ name: options.name ?? path12.basename(root),
7341
7539
  root,
7342
7540
  slug: generateProjectSlug(root),
7343
7541
  createdAt: now,
7344
7542
  lastSeen: now,
7345
- lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7543
+ lastWorkingDir: options.workingDir ? path12.resolve(options.workingDir) : void 0
7346
7544
  };
7347
7545
  manifest.projects.push(entry);
7348
7546
  }
@@ -7753,9 +7951,9 @@ function buildCspHeader(publicWsUrl, host, port) {
7753
7951
  return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; connect-src ${Array.from(connect).join(" ")}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
7754
7952
  }
7755
7953
  function isInsideDist(candidate, distDir) {
7756
- const root = path12.resolve(distDir);
7757
- const resolved = path12.resolve(candidate);
7758
- return resolved === root || resolved.startsWith(root + path12.sep);
7954
+ const root = path13.resolve(distDir);
7955
+ const resolved = path13.resolve(candidate);
7956
+ return resolved === root || resolved.startsWith(root + path13.sep);
7759
7957
  }
7760
7958
  function decodeSessionId(segment) {
7761
7959
  try {
@@ -7775,7 +7973,7 @@ function strictDecodeParam(segment, res) {
7775
7973
  }
7776
7974
  function createHttpServer(opts) {
7777
7975
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
7778
- const distDir = path12.resolve(opts.distDir);
7976
+ const distDir = path13.resolve(opts.distDir);
7779
7977
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
7780
7978
  let techStackRuntime = null;
7781
7979
  const getTechStackRuntime = async () => {
@@ -8103,11 +8301,7 @@ function createHttpServer(opts) {
8103
8301
  if (researchMatch && req.method === "POST") {
8104
8302
  const pkg = strictDecodeParam(researchMatch[1], res);
8105
8303
  if (pkg === null) return;
8106
- await handleTechStackDependencyResearch(
8107
- res,
8108
- deps2,
8109
- pkg
8110
- );
8304
+ await handleTechStackDependencyResearch(res, deps2, pkg);
8111
8305
  return;
8112
8306
  }
8113
8307
  } catch (error2) {
@@ -8143,6 +8337,7 @@ function createHttpServer(opts) {
8143
8337
  return;
8144
8338
  }
8145
8339
  if (url.pathname === "/debug/system" && req.method === "GET") {
8340
+ const processes = await readRecentProcessMemoryDiagnostics(opts.globalRoot);
8146
8341
  res.writeHead(200, {
8147
8342
  "Content-Type": "application/json",
8148
8343
  "Cache-Control": "no-store"
@@ -8155,6 +8350,7 @@ function createHttpServer(opts) {
8155
8350
  uptime: process.uptime(),
8156
8351
  cpuUsage: process.cpuUsage(),
8157
8352
  codebaseIndexServer: getIndexState2().server,
8353
+ processes,
8158
8354
  timestamp: Date.now()
8159
8355
  })
8160
8356
  );
@@ -8162,24 +8358,24 @@ function createHttpServer(opts) {
8162
8358
  }
8163
8359
  let filePath;
8164
8360
  if (url.pathname === "/" || url.pathname === "") {
8165
- filePath = path12.join(distDir, "index.html");
8361
+ filePath = path13.join(distDir, "index.html");
8166
8362
  } else {
8167
- filePath = path12.join(distDir, url.pathname);
8363
+ filePath = path13.join(distDir, url.pathname);
8168
8364
  }
8169
- const resolvedPath = path12.resolve(filePath);
8365
+ const resolvedPath = path13.resolve(filePath);
8170
8366
  if (!isInsideDist(resolvedPath, distDir)) {
8171
8367
  res.writeHead(403, { "Content-Type": "text/plain" });
8172
8368
  res.end("Forbidden");
8173
8369
  return;
8174
8370
  }
8175
- const ext = path12.extname(resolvedPath);
8371
+ const ext = path13.extname(resolvedPath);
8176
8372
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8177
8373
  res.setHeader("Content-Type", contentType);
8178
8374
  setStaticSecurityHeaders(res);
8179
8375
  if (ext === ".html") {
8180
8376
  if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
8181
8377
  res.setHeader("Content-Security-Policy", buildCspHeader(opts.publicWsUrl, opts.host, port));
8182
- const html = await fs9.readFile(resolvedPath, "utf8");
8378
+ const html = await fs10.readFile(resolvedPath, "utf8");
8183
8379
  res.writeHead(200);
8184
8380
  res.end(injectWsConfig(html, { publicWsUrl: opts.publicWsUrl }));
8185
8381
  return;
@@ -8190,13 +8386,13 @@ function createHttpServer(opts) {
8190
8386
  url.pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=3600"
8191
8387
  );
8192
8388
  }
8193
- const fileContent = await fs9.readFile(resolvedPath);
8389
+ const fileContent = await fs10.readFile(resolvedPath);
8194
8390
  res.writeHead(200);
8195
8391
  res.end(fileContent);
8196
8392
  } catch (err) {
8197
8393
  if (err.code === "ENOENT") {
8198
8394
  try {
8199
- const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8395
+ const html = await fs10.readFile(path13.join(distDir, "index.html"), "utf8");
8200
8396
  setStaticSecurityHeaders(res);
8201
8397
  res.writeHead(200, {
8202
8398
  "Content-Type": "text/html",
@@ -8226,14 +8422,14 @@ function createHttpServer(opts) {
8226
8422
 
8227
8423
  // src/server/instance-registry.ts
8228
8424
  import * as os from "node:os";
8229
- import * as path13 from "node:path";
8230
- import * as fs10 from "node:fs/promises";
8425
+ import * as path14 from "node:path";
8426
+ import * as fs11 from "node:fs/promises";
8231
8427
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8232
8428
  function defaultBaseDir() {
8233
- return path13.join(os.homedir(), ".wrongstack");
8429
+ return path14.join(os.homedir(), ".wrongstack");
8234
8430
  }
8235
8431
  function registryPath(baseDir = defaultBaseDir()) {
8236
- return path13.join(baseDir, "webui-instances.json");
8432
+ return path14.join(baseDir, "webui-instances.json");
8237
8433
  }
8238
8434
  function isPidAlive(pid) {
8239
8435
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -8246,7 +8442,7 @@ function isPidAlive(pid) {
8246
8442
  }
8247
8443
  async function load(file) {
8248
8444
  try {
8249
- const raw = await fs10.readFile(file, "utf8");
8445
+ const raw = await fs11.readFile(file, "utf8");
8250
8446
  const parsed = JSON.parse(raw);
8251
8447
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
8252
8448
  return parsed;
@@ -10044,14 +10240,14 @@ function registerShutdownHandlers(res) {
10044
10240
  }
10045
10241
 
10046
10242
  // src/server/config-doctor.ts
10047
- import * as fs11 from "node:fs/promises";
10243
+ import * as fs12 from "node:fs/promises";
10048
10244
  import {
10049
10245
  repairConfigDefaults
10050
10246
  } from "@wrongstack/core/storage";
10051
10247
  import { atomicWrite as atomicWrite5 } from "@wrongstack/core/utils";
10052
10248
  import { decryptConfigSecrets } from "@wrongstack/core/security";
10053
10249
  async function readConfig(file, vault) {
10054
- const raw = await fs11.readFile(file, "utf8");
10250
+ const raw = await fs12.readFile(file, "utf8");
10055
10251
  const parsed = JSON.parse(raw);
10056
10252
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10057
10253
  throw new Error("Active profile config must contain a JSON object.");
@@ -10115,6 +10311,7 @@ async function handleConfigDoctor(ws, apply, deps2) {
10115
10311
 
10116
10312
  // src/server/mailbox-handlers.ts
10117
10313
  import {
10314
+ actionToAckInput,
10118
10315
  getSharedProjectMailbox,
10119
10316
  isMailboxMessageVisibleTo,
10120
10317
  MAILBOX_TYPE_PROPERTIES,
@@ -10131,6 +10328,43 @@ function getMailboxForDeps(deps2) {
10131
10328
  const dir = resolveProjectDir(projectRoot, globalRoot);
10132
10329
  return getSharedProjectMailbox(dir, deps2.events);
10133
10330
  }
10331
+ async function handleMailboxAction(ws, deps2, payload) {
10332
+ const mb = getMailboxForDeps(deps2);
10333
+ if (!mb) {
10334
+ send(ws, {
10335
+ type: "mailbox.action_result",
10336
+ payload: {
10337
+ requestId: payload.requestId,
10338
+ success: false,
10339
+ error: "No project root available"
10340
+ }
10341
+ });
10342
+ return;
10343
+ }
10344
+ try {
10345
+ const message = payload.action === "soft-delete" ? await mb.softDelete(payload.mailId, payload.readerId) : await mb.ack(actionToAckInput(payload.action, payload));
10346
+ send(ws, {
10347
+ type: "mailbox.action_result",
10348
+ payload: {
10349
+ requestId: payload.requestId,
10350
+ success: message !== null,
10351
+ action: payload.action,
10352
+ mailId: payload.mailId
10353
+ }
10354
+ });
10355
+ } catch (err) {
10356
+ send(ws, {
10357
+ type: "mailbox.action_result",
10358
+ payload: {
10359
+ requestId: payload.requestId,
10360
+ success: false,
10361
+ action: payload.action,
10362
+ mailId: payload.mailId,
10363
+ error: errMessage(err)
10364
+ }
10365
+ });
10366
+ }
10367
+ }
10134
10368
  async function handleMailboxSend(ws, deps2, payload) {
10135
10369
  const mb = getMailboxForDeps(deps2);
10136
10370
  if (!mb) {
@@ -10146,7 +10380,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10146
10380
  }
10147
10381
  try {
10148
10382
  const message = await mb.send({
10149
- from: "webui",
10383
+ from: payload.from ?? "webui",
10150
10384
  to: payload.to,
10151
10385
  type: payload.type,
10152
10386
  audience: payload.audience,
@@ -10161,6 +10395,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10161
10395
  requestId: payload.requestId,
10162
10396
  success: true,
10163
10397
  messageId: message.id,
10398
+ from: message.from,
10164
10399
  to: message.to,
10165
10400
  audience: message.audience ?? "all"
10166
10401
  }
@@ -10205,6 +10440,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
10205
10440
  send(ws, {
10206
10441
  type: "mailbox.messages",
10207
10442
  payload: {
10443
+ ...payload?.unreadOnly === true ? { unreadOnly: true } : {},
10208
10444
  messages: visibleMessages.map((m) => {
10209
10445
  const readByMe = payload?.agentId !== void 0 ? payload.agentId in m.readBy : false;
10210
10446
  const completedByMe = payload?.agentId !== void 0 ? m.completedBy === payload.agentId : false;
@@ -10320,6 +10556,14 @@ function createMailboxRouteHandlers(ctx) {
10320
10556
  ...ctx.events ? { events: ctx.events } : {}
10321
10557
  };
10322
10558
  return {
10559
+ action: (ws, msg) => {
10560
+ const parsed = validateMailboxActionPayload(msg.payload);
10561
+ if (!parsed.ok) {
10562
+ sendResult2(ws, false, parsed.message);
10563
+ return;
10564
+ }
10565
+ return handleMailboxAction(ws, deps2, parsed.value);
10566
+ },
10323
10567
  send: (ws, msg) => {
10324
10568
  const parsed = validateMailboxSendPayload(msg.payload);
10325
10569
  if (!parsed.ok) {
@@ -10370,6 +10614,9 @@ function createMailboxRouteHandlers(ctx) {
10370
10614
  }
10371
10615
  async function handleMailboxRoute(ws, msg, handlers) {
10372
10616
  switch (msg.type) {
10617
+ case "mailbox.action":
10618
+ await handlers.action(ws, msg);
10619
+ return true;
10373
10620
  case "mailbox.send":
10374
10621
  await handlers.send(ws, msg);
10375
10622
  return true;
@@ -11916,8 +12163,8 @@ function seedContextMeta(config, context) {
11916
12163
  }
11917
12164
 
11918
12165
  // src/server/pref-helpers.ts
11919
- import * as fs12 from "node:fs/promises";
11920
- import * as path14 from "node:path";
12166
+ import * as fs13 from "node:fs/promises";
12167
+ import * as path15 from "node:path";
11921
12168
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
11922
12169
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
11923
12170
  var PREF_KEYS = [
@@ -12013,11 +12260,11 @@ function prefSnapshot(contextMeta) {
12013
12260
  return snapshot;
12014
12261
  }
12015
12262
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
12016
- const globalRoot = path14.dirname(filePath);
12263
+ const globalRoot = path15.dirname(filePath);
12017
12264
  await backupConfigFile(filePath, { globalRoot });
12018
12265
  let raw;
12019
12266
  try {
12020
- raw = await fs12.readFile(filePath, "utf8");
12267
+ raw = await fs13.readFile(filePath, "utf8");
12021
12268
  } catch {
12022
12269
  raw = "{}";
12023
12270
  }
@@ -12492,8 +12739,8 @@ async function handleProcessRoute(ws, msg, handlers) {
12492
12739
  }
12493
12740
 
12494
12741
  // src/server/project-handlers.ts
12495
- import * as fs13 from "node:fs/promises";
12496
- import * as path15 from "node:path";
12742
+ import * as fs14 from "node:fs/promises";
12743
+ import * as path16 from "node:path";
12497
12744
  import { DefaultSessionStore } from "@wrongstack/core/storage";
12498
12745
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
12499
12746
  function createProjectHandlers(ctx) {
@@ -12545,10 +12792,10 @@ function createProjectHandlers(ctx) {
12545
12792
  });
12546
12793
  return;
12547
12794
  }
12548
- const resolved = path15.resolve(parsed.value.root);
12549
- const name2 = parsed.value.name?.trim() || path15.basename(resolved);
12795
+ const resolved = path16.resolve(parsed.value.root);
12796
+ const name2 = parsed.value.name?.trim() || path16.basename(resolved);
12550
12797
  try {
12551
- const stat3 = await fs13.stat(resolved).catch(() => null);
12798
+ const stat3 = await fs14.stat(resolved).catch(() => null);
12552
12799
  if (!stat3?.isDirectory()) {
12553
12800
  sendTo(ws, {
12554
12801
  type: "projects.added",
@@ -12557,7 +12804,7 @@ function createProjectHandlers(ctx) {
12557
12804
  return;
12558
12805
  }
12559
12806
  const before = await loadManifest(ctx.globalConfigPath);
12560
- const already = before.projects.some((project) => path15.resolve(project.root) === resolved);
12807
+ const already = before.projects.some((project) => path16.resolve(project.root) === resolved);
12561
12808
  const entry = await touchProjectInManifest(
12562
12809
  { projectRoot: resolved, workingDir: resolved, name: name2 },
12563
12810
  ctx.globalConfigPath
@@ -12587,8 +12834,8 @@ function createProjectHandlers(ctx) {
12587
12834
  });
12588
12835
  return;
12589
12836
  }
12590
- const resolved = path15.resolve(parsed.value.root);
12591
- const name2 = parsed.value.name?.trim() || path15.basename(resolved);
12837
+ const resolved = path16.resolve(parsed.value.root);
12838
+ const name2 = parsed.value.name?.trim() || path16.basename(resolved);
12592
12839
  if (!ctx.allowProjectMutations) {
12593
12840
  sendTo(ws, {
12594
12841
  type: "projects.selected",
@@ -12601,7 +12848,7 @@ function createProjectHandlers(ctx) {
12601
12848
  return;
12602
12849
  }
12603
12850
  try {
12604
- const stat3 = await fs13.stat(resolved).catch(() => null);
12851
+ const stat3 = await fs14.stat(resolved).catch(() => null);
12605
12852
  if (!stat3?.isDirectory()) {
12606
12853
  sendTo(ws, {
12607
12854
  type: "projects.selected",
@@ -12631,7 +12878,7 @@ function createProjectHandlers(ctx) {
12631
12878
  const previousIdentityTarget = {
12632
12879
  projectSlug: previousPaths.projectSlug,
12633
12880
  projectRoot: previousProjectRoot,
12634
- projectName: path15.basename(previousProjectRoot),
12881
+ projectName: path16.basename(previousProjectRoot),
12635
12882
  workingDir: ctx.context.workingDir
12636
12883
  };
12637
12884
  const previousUsage = ctx.tokenCounter.total();
@@ -12794,14 +13041,14 @@ async function searchCatalogModels(registry, rawQuery, limit = 8) {
12794
13041
  }
12795
13042
 
12796
13043
  // src/server/provider-config-io.ts
12797
- import * as fs14 from "node:fs/promises";
13044
+ import * as fs15 from "node:fs/promises";
12798
13045
  import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
12799
13046
  import { atomicWrite as atomicWrite7 } from "@wrongstack/core/utils";
12800
13047
  import { decryptConfigSecrets as decryptConfigSecrets3, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
12801
13048
  async function loadSavedProviders(configPath, vault) {
12802
13049
  let raw;
12803
13050
  try {
12804
- raw = await fs14.readFile(configPath, "utf8");
13051
+ raw = await fs15.readFile(configPath, "utf8");
12805
13052
  } catch {
12806
13053
  return {};
12807
13054
  }
@@ -12819,7 +13066,7 @@ async function saveProviders(configPath, vault, providers, profileConfigPath) {
12819
13066
  let raw;
12820
13067
  let fileExists = true;
12821
13068
  try {
12822
- raw = await fs14.readFile(targetPath, "utf8");
13069
+ raw = await fs15.readFile(targetPath, "utf8");
12823
13070
  } catch (err) {
12824
13071
  if (err.code !== "ENOENT") {
12825
13072
  throw new ConfigError2({
@@ -13522,6 +13769,7 @@ var CLIENT_COLLABORATION_MESSAGE_TYPES = [
13522
13769
  "collab.resume",
13523
13770
  "collab.grant_control",
13524
13771
  "collab.inject_tool",
13772
+ "mailbox.action",
13525
13773
  "mailbox.agents",
13526
13774
  "mailbox.clear",
13527
13775
  "mailbox.compact",
@@ -13787,6 +14035,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
13787
14035
  "collab.pause.granted",
13788
14036
  "collab.pause.released",
13789
14037
  "collab.state",
14038
+ "mailbox.action_result",
13790
14039
  "mailbox.agent_registered",
13791
14040
  "mailbox.agents",
13792
14041
  "mailbox.cleared",
@@ -14010,13 +14259,13 @@ function isRegisteredMessageType(type, direction) {
14010
14259
  // src/protocol/decoder.ts
14011
14260
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
14012
14261
  var MAX_PAYLOAD_DEPTH = 32;
14013
- function inspectValue(value, path29, depth) {
14262
+ function inspectValue(value, path30, depth) {
14014
14263
  if (depth > MAX_PAYLOAD_DEPTH) {
14015
- return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path29 };
14264
+ return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path30 };
14016
14265
  }
14017
14266
  if (value === null || typeof value !== "object") return null;
14018
14267
  for (const key of Object.keys(value)) {
14019
- const childPath = `${path29}.${key}`;
14268
+ const childPath = `${path30}.${key}`;
14020
14269
  if (FORBIDDEN_KEYS.has(key)) {
14021
14270
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
14022
14271
  }
@@ -15712,8 +15961,8 @@ function createRouteFamilyDispatcher(options) {
15712
15961
  }
15713
15962
 
15714
15963
  // src/server/shell-open.ts
15715
- import * as fs15 from "node:fs/promises";
15716
- import * as path16 from "node:path";
15964
+ import * as fs16 from "node:fs/promises";
15965
+ import * as path17 from "node:path";
15717
15966
  import { spawn } from "node:child_process";
15718
15967
  function normalizeShellOpenTarget(target) {
15719
15968
  return target === "terminal" ? "terminal" : "file-manager";
@@ -15724,11 +15973,11 @@ function shellQuote(s) {
15724
15973
  }
15725
15974
  async function handleShellOpen(req, logger, options) {
15726
15975
  try {
15727
- const resolved = path16.resolve(req.path);
15976
+ const resolved = path17.resolve(req.path);
15728
15977
  if (options?.projectRoot) {
15729
- const root = path16.resolve(options.projectRoot);
15730
- const relative5 = path16.relative(root, resolved);
15731
- const escapes = relative5.startsWith("..") || path16.isAbsolute(relative5);
15978
+ const root = path17.resolve(options.projectRoot);
15979
+ const relative5 = path17.relative(root, resolved);
15980
+ const escapes = relative5.startsWith("..") || path17.isAbsolute(relative5);
15732
15981
  if (escapes) {
15733
15982
  return {
15734
15983
  success: false,
@@ -15736,7 +15985,7 @@ async function handleShellOpen(req, logger, options) {
15736
15985
  };
15737
15986
  }
15738
15987
  }
15739
- await fs15.access(resolved);
15988
+ await fs16.access(resolved);
15740
15989
  if (METACHAR_REGEX.test(resolved)) {
15741
15990
  return { success: false, message: "Path contains unsupported characters." };
15742
15991
  }
@@ -15787,7 +16036,12 @@ async function handleShellOpen(req, logger, options) {
15787
16036
  }
15788
16037
 
15789
16038
  // src/server/sdd-board-ws-handler.ts
15790
- import { listBoards as listBoards3 } from "@wrongstack/kanban";
16039
+ import {
16040
+ enqueueKanbanWorkflowCommand,
16041
+ kanbanWorkflowId,
16042
+ listBoards as listBoards3,
16043
+ listKanbanWorkflowStates
16044
+ } from "@wrongstack/kanban";
15791
16045
  import {
15792
16046
  applySddLifecycle,
15793
16047
  extractVerificationCommand,
@@ -15814,7 +16068,7 @@ var SddBoardWebSocketHandler = class {
15814
16068
  clients = /* @__PURE__ */ new Set();
15815
16069
  lifecycle;
15816
16070
  security;
15817
- diskPollingEnabled;
16071
+ standalonePollingEnabled;
15818
16072
  latest = null;
15819
16073
  poll = null;
15820
16074
  pollInFlight = false;
@@ -15823,7 +16077,7 @@ var SddBoardWebSocketHandler = class {
15823
16077
  this.store = new SddBoardStore({ baseDir: boardsDir });
15824
16078
  this.lifecycle = lifecycle;
15825
16079
  this.security = security;
15826
- this.diskPollingEnabled = events === void 0;
16080
+ this.standalonePollingEnabled = events === void 0;
15827
16081
  if (events) {
15828
16082
  const handler = (e) => {
15829
16083
  this.latest = e.snapshot;
@@ -15858,7 +16112,7 @@ var SddBoardWebSocketHandler = class {
15858
16112
  return;
15859
16113
  }
15860
16114
  if (msg.type === "sdd.board.list") {
15861
- const boards = await this.store.list();
16115
+ const boards = await this.listBoardEntries();
15862
16116
  this.broadcast({ type: "sdd.board.list", payload: { boards } });
15863
16117
  return;
15864
16118
  }
@@ -15871,7 +16125,8 @@ var SddBoardWebSocketHandler = class {
15871
16125
  const verificationCommands = [];
15872
16126
  if (action === "set_task_verification") {
15873
16127
  const command = msg.payload?.verificationCommand;
15874
- if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
16128
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192))
16129
+ return;
15875
16130
  if (typeof command === "string" && command.trim()) {
15876
16131
  verificationCommands.push({ command, operation: "sdd.set_task_verification" });
15877
16132
  }
@@ -15905,18 +16160,26 @@ var SddBoardWebSocketHandler = class {
15905
16160
  );
15906
16161
  if (!authorization.allowed) return;
15907
16162
  }
15908
- const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
16163
+ const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.listBoardEntries())[0]?.runId;
15909
16164
  if (runId) {
15910
- await this.store.appendControl(runId, {
15911
- ts: Date.now(),
15912
- type: action,
15913
- payload: msg.payload
15914
- });
16165
+ if (this.lifecycle && this.lifecycle.controlTransport !== "legacy-file") {
16166
+ await enqueueKanbanWorkflowCommand(
16167
+ this.lifecycle.projectRoot,
16168
+ kanbanWorkflowId("sdd", runId),
16169
+ { type: action, payload: msg.payload }
16170
+ );
16171
+ } else {
16172
+ await this.store.appendControl(runId, {
16173
+ ts: Date.now(),
16174
+ type: action,
16175
+ payload: msg.payload
16176
+ });
16177
+ }
15915
16178
  }
15916
16179
  }
15917
16180
  }
15918
16181
  /**
15919
- * Apply a cleanup/rollback/destroy from disk and broadcast a structured
16182
+ * Apply a cleanup/rollback/destroy from durable state and broadcast a structured
15920
16183
  * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
15921
16184
  * the user must stop it first; the UI gates the buttons on `!active` and the
15922
16185
  * Destroy flow auto-stops then waits before sending `destroy`.
@@ -15925,7 +16188,11 @@ var SddBoardWebSocketHandler = class {
15925
16188
  if (!this.lifecycle) {
15926
16189
  this.broadcast({
15927
16190
  type: "sdd.board.lifecycle_result",
15928
- payload: { op, ok: false, reason: "Lifecycle operations are not available in this session." }
16191
+ payload: {
16192
+ op,
16193
+ ok: false,
16194
+ reason: "Lifecycle operations are not available in this session."
16195
+ }
15929
16196
  });
15930
16197
  return;
15931
16198
  }
@@ -15941,6 +16208,7 @@ var SddBoardWebSocketHandler = class {
15941
16208
  projectRoot: this.lifecycle.projectRoot,
15942
16209
  paths: this.lifecycle.paths,
15943
16210
  runId,
16211
+ stateTransport: this.lifecycle.stateTransport ?? "kanban",
15944
16212
  revertMerged: payload?.revertMerged === true
15945
16213
  });
15946
16214
  this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
@@ -15969,22 +16237,27 @@ var SddBoardWebSocketHandler = class {
15969
16237
  if (this.pollInFlight) return;
15970
16238
  this.pollInFlight = true;
15971
16239
  try {
15972
- const entry = await this.store.latest();
15973
- if (!entry) return;
15974
- if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
16240
+ const snap = await this.loadLatestSnapshot();
16241
+ if (!snap) return;
16242
+ if (this.latest && this.latest.updatedAt >= snap.updatedAt && this.latest.runId === snap.runId) {
15975
16243
  return;
15976
16244
  }
15977
- const snap = await this.store.load(entry.runId);
15978
- if (snap) {
15979
- this.latest = snap;
15980
- this.broadcast({ type: "sdd.board.snapshot", payload: snap });
15981
- }
16245
+ this.latest = snap;
16246
+ this.broadcast({ type: "sdd.board.snapshot", payload: snap });
16247
+ } catch (err) {
16248
+ console.warn(
16249
+ JSON.stringify({
16250
+ level: "warn",
16251
+ event: "sdd_board.poll_failed",
16252
+ message: err instanceof Error ? err.message : String(err)
16253
+ })
16254
+ );
15982
16255
  } finally {
15983
16256
  this.pollInFlight = false;
15984
16257
  }
15985
16258
  }
15986
16259
  startPolling() {
15987
- if (!this.diskPollingEnabled || this.poll !== null || this.clients.size === 0) return;
16260
+ if (!this.standalonePollingEnabled || this.poll !== null || this.clients.size === 0) return;
15988
16261
  this.poll = setInterval(() => void this.pollLatest(), 1e3);
15989
16262
  this.poll.unref?.();
15990
16263
  }
@@ -15994,17 +16267,44 @@ var SddBoardWebSocketHandler = class {
15994
16267
  this.poll = null;
15995
16268
  }
15996
16269
  async sendCurrent(client) {
15997
- const snap = this.latest ?? await this.loadLatestFromDisk();
16270
+ const snap = this.latest ?? await this.loadLatestSnapshot();
15998
16271
  if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
15999
16272
  }
16000
16273
  async broadcastCurrent() {
16001
- const snap = this.latest ?? await this.loadLatestFromDisk();
16274
+ const snap = this.latest ?? await this.loadLatestSnapshot();
16002
16275
  if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
16003
16276
  }
16004
- async loadLatestFromDisk() {
16277
+ async loadLatestSnapshot() {
16278
+ if (this.usesKanbanState()) {
16279
+ const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
16280
+ const snapshots = states.map((state) => state.value).filter(isSddBoardSnapshot).sort((a, b) => b.updatedAt - a.updatedAt);
16281
+ return snapshots[0] ?? null;
16282
+ }
16005
16283
  const entry = await this.store.latest();
16006
16284
  return entry ? this.store.load(entry.runId) : null;
16007
16285
  }
16286
+ async listBoardEntries() {
16287
+ if (!this.usesKanbanState()) return this.store.list();
16288
+ const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
16289
+ return states.flatMap((state) => {
16290
+ if (!isSddBoardSnapshot(state.value)) return [];
16291
+ const snapshot = state.value;
16292
+ return [
16293
+ {
16294
+ runId: snapshot.runId,
16295
+ ...snapshot.specId ? { specId: snapshot.specId } : {},
16296
+ title: snapshot.title,
16297
+ status: snapshot.status,
16298
+ total: snapshot.progress.total,
16299
+ completed: snapshot.progress.completed,
16300
+ updatedAt: snapshot.updatedAt
16301
+ }
16302
+ ];
16303
+ });
16304
+ }
16305
+ usesKanbanState() {
16306
+ return Boolean(this.lifecycle && this.lifecycle.stateTransport !== "legacy-file");
16307
+ }
16008
16308
  broadcast(msg) {
16009
16309
  const data = JSON.stringify(msg);
16010
16310
  for (const client of this.clients) {
@@ -16015,17 +16315,20 @@ var SddBoardWebSocketHandler = class {
16015
16315
  sendSerialized(client.ws, JSON.stringify(msg));
16016
16316
  }
16017
16317
  };
16318
+ function isSddBoardSnapshot(value) {
16319
+ if (!value || typeof value !== "object") return false;
16320
+ const snapshot = value;
16321
+ return typeof snapshot.runId === "string" && typeof snapshot.title === "string" && typeof snapshot.status === "string" && typeof snapshot.updatedAt === "number" && Array.isArray(snapshot.tasks) && Boolean(snapshot.progress && typeof snapshot.progress.total === "number");
16322
+ }
16018
16323
 
16019
16324
  // src/server/sdd-wizard-wiring.ts
16020
- import * as path17 from "node:path";
16021
- import {
16022
- DefaultTaskStore,
16023
- TaskTracker
16024
- } from "@wrongstack/core/tasking";
16325
+ import * as path18 from "node:path";
16326
+ import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
16025
16327
  import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
16026
16328
  import { WorktreeManager as WorktreeManager2 } from "@wrongstack/core/worktree";
16027
16329
  import {
16028
16330
  cleanupStaleSddWorktrees,
16331
+ createKanbanSddSessionPersistence,
16029
16332
  decomposeNonAtomicTasks,
16030
16333
  gatherProjectContext,
16031
16334
  makeAcceptanceCriteriaVerifier,
@@ -16043,7 +16346,7 @@ import {
16043
16346
  } from "@wrongstack/sdd";
16044
16347
  async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
16045
16348
  const runTracker = tracker ?? (() => {
16046
- const t = new TaskTracker({ store: new DefaultTaskStore() });
16349
+ const t = new TaskTracker({ store: deps2.taskStore ?? new DefaultTaskStore() });
16047
16350
  t.setGraph(graph);
16048
16351
  return t;
16049
16352
  })();
@@ -16052,7 +16355,8 @@ async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
16052
16355
  if (worktreesEnabled && await isGitWorkTree(deps2.projectRoot)) {
16053
16356
  void cleanupStaleSddWorktrees({
16054
16357
  projectRoot: deps2.projectRoot,
16055
- boardsDir: deps2.projectSddBoards
16358
+ boardsDir: deps2.projectSddBoards,
16359
+ stateTransport: "kanban"
16056
16360
  }).catch(() => void 0);
16057
16361
  worktrees = new WorktreeManager2({
16058
16362
  projectRoot: deps2.projectRoot,
@@ -16131,7 +16435,8 @@ function buildSddWizardDeps(opts) {
16131
16435
  }).catch(() => {
16132
16436
  projectContext = "";
16133
16437
  });
16134
- const sessionPath = opts.paths.projectSddSession ?? path17.join(opts.paths.projectDir, "sdd-session.json");
16438
+ const legacySessionPath = opts.paths.projectSddSession ?? path18.join(opts.paths.projectDir, "sdd-session.json");
16439
+ const sessionPersistence = createKanbanSddSessionPersistence(opts.projectRoot, legacySessionPath);
16135
16440
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
16136
16441
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
16137
16442
  const runIsolatedTurn = async (prompt, name2) => {
@@ -16152,7 +16457,7 @@ function buildSddWizardDeps(opts) {
16152
16457
  const makeDriver = () => new SddInterviewDriver({
16153
16458
  specStore,
16154
16459
  graphStore,
16155
- sessionPath,
16460
+ sessionPersistence,
16156
16461
  projectContext
16157
16462
  });
16158
16463
  const launchFromGraph = async (graph, config, tracker) => {
@@ -16164,6 +16469,7 @@ function buildSddWizardDeps(opts) {
16164
16469
  projectRoot: opts.projectRoot,
16165
16470
  subagentFactory: opts.subagentFactory,
16166
16471
  projectSddBoards: opts.paths.projectSddBoards,
16472
+ taskStore: graphStore,
16167
16473
  registry,
16168
16474
  runIsolatedTurn,
16169
16475
  ...opts.brain ? { brain: opts.brain } : {}
@@ -16233,8 +16539,18 @@ var SddWizardWebSocketHandler = class {
16233
16539
  lastAgentText = "";
16234
16540
  /** Guards against overlapping interview turns (one in flight at a time). */
16235
16541
  busy = false;
16236
- /** Resolves once project-context gather + disk resume finish. */
16542
+ /** Set when authoritative session state could not be read during bootstrap. */
16543
+ resumeError = null;
16544
+ /** Resolves once project-context gather + durable resume finish. */
16237
16545
  ready;
16546
+ /**
16547
+ * Single-flight slot for the resume probe. `handleMessage` awaits this
16548
+ * when `resumeError` is set, so concurrent frames cannot race the retry
16549
+ * — one probe either clears the gate or re-latches it, then every
16550
+ * queued frame re-checks and proceeds. `null` when no probe is in
16551
+ * flight (or after the probe has settled).
16552
+ */
16553
+ resumeProbe = null;
16238
16554
  async bootstrap() {
16239
16555
  try {
16240
16556
  await this.deps.ensureReady?.();
@@ -16242,7 +16558,7 @@ var SddWizardWebSocketHandler = class {
16242
16558
  }
16243
16559
  await this.tryResume();
16244
16560
  }
16245
- /** Rehydrate a persisted interview if one exists on disk. */
16561
+ /** Rehydrate a persisted interview if one exists. */
16246
16562
  async tryResume() {
16247
16563
  if (this.driver) return;
16248
16564
  try {
@@ -16251,9 +16567,11 @@ var SddWizardWebSocketHandler = class {
16251
16567
  this.driver = driver;
16252
16568
  this.lastAgentText = driver.getLastAgentText() ?? "";
16253
16569
  }
16254
- } catch {
16570
+ this.resumeError = null;
16571
+ } catch (error2) {
16255
16572
  this.driver = null;
16256
16573
  this.lastAgentText = "";
16574
+ this.resumeError = error2 instanceof Error ? error2.message : String(error2);
16257
16575
  }
16258
16576
  }
16259
16577
  addClient(ws) {
@@ -16262,6 +16580,10 @@ var SddWizardWebSocketHandler = class {
16262
16580
  ws.on("close", () => this.clients.delete(client));
16263
16581
  ws.on("error", () => this.clients.delete(client));
16264
16582
  void this.ready.then(() => {
16583
+ if (this.resumeError) {
16584
+ this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
16585
+ return;
16586
+ }
16265
16587
  if (this.driver) {
16266
16588
  this.send(client, this.snapshotMsg());
16267
16589
  if (this.lastAgentText) {
@@ -16273,6 +16595,22 @@ var SddWizardWebSocketHandler = class {
16273
16595
  async handleMessage(msg) {
16274
16596
  try {
16275
16597
  await this.ready;
16598
+ if (this.resumeError) {
16599
+ if (this.resumeProbe === null) {
16600
+ const probe = Promise.resolve().then(() => this.tryResume());
16601
+ this.resumeProbe = probe;
16602
+ void probe.finally(() => {
16603
+ setTimeout(() => {
16604
+ if (this.resumeProbe === probe) this.resumeProbe = null;
16605
+ }, 0);
16606
+ });
16607
+ }
16608
+ await this.resumeProbe;
16609
+ if (this.resumeError) {
16610
+ this.broadcast({ type: "sdd.spec.error", payload: { message: this.resumeError } });
16611
+ return;
16612
+ }
16613
+ }
16276
16614
  switch (msg.type) {
16277
16615
  case "sdd.spec.start":
16278
16616
  await this.onStart(String(msg.payload?.goal ?? "").trim(), {
@@ -16289,7 +16627,10 @@ var SddWizardWebSocketHandler = class {
16289
16627
  if (this.driver) {
16290
16628
  this.broadcast(this.snapshotMsg());
16291
16629
  if (this.lastAgentText) {
16292
- this.broadcast({ type: "sdd.spec.agent_text", payload: { text: this.lastAgentText } });
16630
+ this.broadcast({
16631
+ type: "sdd.spec.agent_text",
16632
+ payload: { text: this.lastAgentText }
16633
+ });
16293
16634
  }
16294
16635
  }
16295
16636
  break;
@@ -16503,10 +16844,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
16503
16844
 
16504
16845
  // src/server/setup-events-fleet-broadcaster.ts
16505
16846
  import { watch as fsWatch } from "node:fs";
16506
- import * as path18 from "node:path";
16847
+ import * as path19 from "node:path";
16507
16848
  function registerSetupEventsFleetBroadcaster(deps2) {
16508
16849
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
16509
- const globalRoot = globalConfigPath ? path18.dirname(globalConfigPath) : void 0;
16850
+ const globalRoot = globalConfigPath ? path19.dirname(globalConfigPath) : void 0;
16510
16851
  if (!globalRoot) return void 0;
16511
16852
  const disposers = [];
16512
16853
  const broadcastSessions = async () => {
@@ -16516,8 +16857,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
16516
16857
  const sessions = await registry.list();
16517
16858
  const ownEntry = sessions.find((s) => s.pid === process.pid);
16518
16859
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
16519
- const myRoot = path18.resolve(context.projectRoot);
16520
- const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path18.resolve(s.projectRoot) === myRoot).map((s) => ({
16860
+ const myRoot = path19.resolve(context.projectRoot);
16861
+ const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path19.resolve(s.projectRoot) === myRoot).map((s) => ({
16521
16862
  sessionId: s.sessionId,
16522
16863
  projectName: s.projectName,
16523
16864
  projectSlug: s.projectSlug,
@@ -16707,14 +17048,14 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
16707
17048
 
16708
17049
  // src/server/setup-events-status-watcher.ts
16709
17050
  import { watch as fsWatch2 } from "node:fs";
16710
- import * as fs16 from "node:fs/promises";
16711
- import * as path20 from "node:path";
17051
+ import * as fs17 from "node:fs/promises";
17052
+ import * as path21 from "node:path";
16712
17053
 
16713
17054
  // src/server/setup-events-watcher.ts
16714
- import * as path19 from "node:path";
17055
+ import * as path20 from "node:path";
16715
17056
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
16716
17057
  const raw = String(filename);
16717
- const relative5 = path19.isAbsolute(raw) ? path19.relative(projectsDir, raw) : raw;
17058
+ const relative5 = path20.isAbsolute(raw) ? path20.relative(projectsDir, raw) : raw;
16718
17059
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
16719
17060
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
16720
17061
  return parts.at(-2) ?? null;
@@ -16749,7 +17090,7 @@ function logFileWatcherMetrics(metrics) {
16749
17090
  function registerSetupEventsStatusWatcher(deps2) {
16750
17091
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
16751
17092
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
16752
- const projectsDir = path20.join(wpaths.globalRoot, "projects");
17093
+ const projectsDir = path21.join(wpaths.globalRoot, "projects");
16753
17094
  const knownProjectHashes = /* @__PURE__ */ new Set();
16754
17095
  const debounceTimers = /* @__PURE__ */ new Map();
16755
17096
  const DEBOUNCE_MS = 150;
@@ -16794,7 +17135,7 @@ function registerSetupEventsStatusWatcher(deps2) {
16794
17135
  let watcher;
16795
17136
  const startWatcher = async () => {
16796
17137
  try {
16797
- await fs16.mkdir(projectsDir, { recursive: true });
17138
+ await fs17.mkdir(projectsDir, { recursive: true });
16798
17139
  if (isDisposed()) return;
16799
17140
  watcher = fsWatch2(
16800
17141
  projectsDir,
@@ -16808,8 +17149,8 @@ function registerSetupEventsStatusWatcher(deps2) {
16808
17149
  if (!knownProjectHashes.has(projectHash)) return;
16809
17150
  if (watcherMetrics) watcherMetrics.filesProcessed++;
16810
17151
  try {
16811
- const targetFile = path20.join(projectsDir, projectHash, "status.json");
16812
- const content = await fs16.readFile(targetFile, "utf-8");
17152
+ const targetFile = path21.join(projectsDir, projectHash, "status.json");
17153
+ const content = await fs17.readFile(targetFile, "utf-8");
16813
17154
  const statusData = JSON.parse(content);
16814
17155
  scheduleBroadcast(projectHash, statusData);
16815
17156
  } catch {
@@ -16866,8 +17207,8 @@ function registerSetupEventsStatusWatcher(deps2) {
16866
17207
  }
16867
17208
 
16868
17209
  // src/server/setup-events-core-watchers.ts
16869
- import * as fs17 from "node:fs/promises";
16870
- import * as path21 from "node:path";
17210
+ import * as fs18 from "node:fs/promises";
17211
+ import * as path22 from "node:path";
16871
17212
  function registerSetupEventsCoreWatchers(deps2) {
16872
17213
  const { broadcast: broadcast2, clients, context } = deps2;
16873
17214
  const disposers = [];
@@ -16903,9 +17244,9 @@ function registerSetupEventsClientStatusWriter(deps2) {
16903
17244
  if (wpaths?.projectStatus) {
16904
17245
  try {
16905
17246
  const statusFile = wpaths.projectStatus(e.projectHash);
16906
- const dir = path21.dirname(statusFile);
16907
- await fs17.mkdir(dir, { recursive: true });
16908
- await fs17.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
17247
+ const dir = path22.dirname(statusFile);
17248
+ await fs18.mkdir(dir, { recursive: true });
17249
+ await fs18.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
16909
17250
  } catch (err) {
16910
17251
  console.error(
16911
17252
  JSON.stringify({
@@ -17930,7 +18271,7 @@ var SpecsWebSocketHandler = class {
17930
18271
  // src/server/start-webui.ts
17931
18272
  import { randomUUID as randomUUID5 } from "node:crypto";
17932
18273
  import * as http2 from "node:http";
17933
- import * as path28 from "node:path";
18274
+ import * as path29 from "node:path";
17934
18275
  import { createDefaultPipelines } from "@wrongstack/core/agent";
17935
18276
  import { getSharedProjectMailbox as getSharedProjectMailbox4, resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
17936
18277
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
@@ -17944,7 +18285,7 @@ import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } fro
17944
18285
  import {
17945
18286
  expectDefined as expectDefined3,
17946
18287
  sessionScopedPath as sessionScopedPath3,
17947
- startHeapWatchdog,
18288
+ startSharedHeapWatchdog,
17948
18289
  toErrorMessage as toErrorMessage13,
17949
18290
  wstackGlobalRoot as wstackGlobalRoot2
17950
18291
  } from "@wrongstack/core/utils";
@@ -17953,10 +18294,8 @@ import { toLanguagePackageInput } from "@wrongstack/techstack";
17953
18294
  import { ensureSessionShell } from "@wrongstack/tools";
17954
18295
 
17955
18296
  // src/server/backend-services.ts
17956
- import { join as join12 } from "node:path";
17957
- import {
17958
- Agent
17959
- } from "@wrongstack/core/agent";
18297
+ import { join as join13 } from "node:path";
18298
+ import { Agent } from "@wrongstack/core/agent";
17960
18299
  import {
17961
18300
  BrainDecisionLedger,
17962
18301
  BrainMonitor,
@@ -17968,8 +18307,8 @@ import {
17968
18307
  mailboxSessionTag,
17969
18308
  ObservableBrainArbiter as ObservableBrainArbiterCtor
17970
18309
  } from "@wrongstack/core/coordination";
17971
- import { installDesignStudioMiddleware } from "@wrongstack/core/design";
17972
18310
  import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
18311
+ import { installDesignStudioMiddleware } from "@wrongstack/core/design";
17973
18312
  import {
17974
18313
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
17975
18314
  createBrainRuntime,
@@ -17979,7 +18318,9 @@ import {
17979
18318
  } from "@wrongstack/core/execution";
17980
18319
  import { TOKENS } from "@wrongstack/core/kernel";
17981
18320
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
17982
- import { resolveContextWindowPolicy as resolveContextWindowPolicy2 } from "@wrongstack/core/types";
18321
+ import {
18322
+ resolveContextWindowPolicy as resolveContextWindowPolicy2
18323
+ } from "@wrongstack/core/types";
17983
18324
  import {
17984
18325
  estimateRequestTokensCalibrated,
17985
18326
  toErrorMessage as toErrorMessage9
@@ -17996,7 +18337,7 @@ import {
17996
18337
  import { spawn as spawn2 } from "node:child_process";
17997
18338
  import { createRequire } from "node:module";
17998
18339
  import { existsSync } from "node:fs";
17999
- import { dirname as dirname7, join as join10 } from "node:path";
18340
+ import { dirname as dirname7, join as join11 } from "node:path";
18000
18341
  import { resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core/coordination";
18001
18342
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
18002
18343
  import { readLiveLock } from "@wrongstack/core/coordination";
@@ -18125,7 +18466,7 @@ function mailboxServeInvocation(projectRoot) {
18125
18466
  function findWorkspaceCliEntry(projectRoot) {
18126
18467
  let dir = projectRoot;
18127
18468
  for (let i = 0; i < 6; i++) {
18128
- const candidate = join10(dir, "packages", "cli", "dist", "index.js");
18469
+ const candidate = join11(dir, "packages", "cli", "dist", "index.js");
18129
18470
  if (existsSync(candidate)) return candidate;
18130
18471
  const parent = dirname7(dir);
18131
18472
  if (parent === dir) return null;
@@ -18370,10 +18711,10 @@ function clampDim(value, fallback) {
18370
18711
  }
18371
18712
 
18372
18713
  // src/server/worktree-ws-handler.ts
18373
- import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
18714
+ import { join as join12, resolve as resolve12, sep as sep5 } from "node:path";
18715
+ import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
18374
18716
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
18375
18717
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
18376
- import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
18377
18718
  var MAX_ACTIVITY = 6;
18378
18719
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
18379
18720
  var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
@@ -18392,6 +18733,20 @@ var WorktreeWebSocketHandler = class {
18392
18733
  baseBranch = "";
18393
18734
  broadcastInterval = null;
18394
18735
  offs = [];
18736
+ /**
18737
+ * Single-flight guard for orphan scans. Two concurrent `scanAndBroadcast()`
18738
+ * calls would race: the first call reads `wt.listManaged()`, then the
18739
+ * second call's mutation completes (e.g. `removeOne`), then the first call
18740
+ * broadcasts a stale orphan list — the user sees a row persist and clicks
18741
+ * Remove a second time, hitting "remove failed (not a managed worktree?)".
18742
+ *
18743
+ * We coalesce concurrent triggers: while a scan is in flight, additional
18744
+ * callers are remembered as "need a re-scan" and exactly one follow-up
18745
+ * scan runs after the in-flight one finishes. The re-scan picks up any
18746
+ * mutations that landed during the previous scan's `listManaged()` window.
18747
+ */
18748
+ scanInFlight = null;
18749
+ scanRescanNeeded = false;
18395
18750
  addClient(ws) {
18396
18751
  this.clients.add(ws);
18397
18752
  ws.on("close", () => this.clients.delete(ws));
@@ -18410,7 +18765,10 @@ var WorktreeWebSocketHandler = class {
18410
18765
  return true;
18411
18766
  }
18412
18767
  if (msg.type === "worktree.remove") {
18413
- await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
18768
+ await this.removeOne(
18769
+ msg.payload?.["dir"],
18770
+ msg.payload?.["branch"]
18771
+ );
18414
18772
  return true;
18415
18773
  }
18416
18774
  if (msg.type === "worktree.merge") {
@@ -18418,7 +18776,10 @@ var WorktreeWebSocketHandler = class {
18418
18776
  return true;
18419
18777
  }
18420
18778
  if (msg.type === "worktree.diff") {
18421
- await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
18779
+ await this.diffOne(
18780
+ msg.payload?.["dir"],
18781
+ msg.payload?.["baseBranch"]
18782
+ );
18422
18783
  return true;
18423
18784
  }
18424
18785
  return false;
@@ -18431,7 +18792,7 @@ var WorktreeWebSocketHandler = class {
18431
18792
  // ── orphan management ─────────────────────────────────────────────────────
18432
18793
  /** Absolute managed-worktrees root for this project. */
18433
18794
  worktreesRoot() {
18434
- return resolve12(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
18795
+ return resolve12(join12(this.management.projectRoot, ".wrongstack", "worktrees"));
18435
18796
  }
18436
18797
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
18437
18798
  underRoot(dir) {
@@ -18447,12 +18808,43 @@ var WorktreeWebSocketHandler = class {
18447
18808
  }
18448
18809
  return live;
18449
18810
  }
18811
+ /**
18812
+ * Coalesced orphan scan entry point. Multiple concurrent callers share one
18813
+ * in-flight scan; if a caller arrives during a scan, a single follow-up
18814
+ * scan is scheduled to pick up mutations that landed mid-scan. Returns the
18815
+ * shared promise so `await this.scanAndBroadcast()` still works for callers
18816
+ * that want to chain after the next broadcast.
18817
+ */
18818
+ scanAndBroadcast() {
18819
+ if (this.scanInFlight) {
18820
+ this.scanRescanNeeded = true;
18821
+ return this.scanInFlight;
18822
+ }
18823
+ this.scanInFlight = this.runScanAndMaybeRescan();
18824
+ return this.scanInFlight;
18825
+ }
18826
+ /**
18827
+ * Run the actual scan, then drain a pending re-scan request if one came in
18828
+ * during the scan. The re-scan is bounded (one follow-up) — additional
18829
+ * requests that arrive during the follow-up coalesce into the next one.
18830
+ */
18831
+ async runScanAndMaybeRescan() {
18832
+ try {
18833
+ await this.runScanOnce();
18834
+ while (this.scanRescanNeeded) {
18835
+ this.scanRescanNeeded = false;
18836
+ await this.runScanOnce();
18837
+ }
18838
+ } finally {
18839
+ this.scanInFlight = null;
18840
+ }
18841
+ }
18450
18842
  /**
18451
18843
  * Scan the disk for managed worktrees/branches NOT owned by a live in-session
18452
18844
  * run and broadcast them as orphans, with whether it is safe to clean now.
18453
18845
  * No-op (empty inventory) when management deps were not wired.
18454
18846
  */
18455
- async scanAndBroadcast() {
18847
+ async runScanOnce() {
18456
18848
  if (!this.management) {
18457
18849
  this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
18458
18850
  return;
@@ -18508,7 +18900,8 @@ var WorktreeWebSocketHandler = class {
18508
18900
  }
18509
18901
  const res = await cleanupStaleSddWorktrees2({
18510
18902
  projectRoot: this.management.projectRoot,
18511
- boardsDir: this.management.boardsDir
18903
+ boardsDir: this.management.boardsDir,
18904
+ stateTransport: "kanban"
18512
18905
  });
18513
18906
  if (res.skippedReason) {
18514
18907
  this.broadcast({
@@ -18521,26 +18914,45 @@ var WorktreeWebSocketHandler = class {
18521
18914
  for (const [id, h] of [...this.handles]) {
18522
18915
  if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
18523
18916
  }
18524
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: true, removed: res.removed } });
18917
+ this.broadcast({
18918
+ type: "worktree.cleanup_result",
18919
+ payload: { ok: true, removed: res.removed }
18920
+ });
18525
18921
  this.broadcastState();
18526
18922
  await this.scanAndBroadcast();
18527
18923
  }
18528
18924
  /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
18529
18925
  async removeOne(dir, branch) {
18530
18926
  if (!this.management || !dir && !branch) {
18531
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "nothing to remove" } });
18927
+ this.broadcast({
18928
+ type: "worktree.cleanup_result",
18929
+ payload: { ok: false, removed: 0, reason: "nothing to remove" }
18930
+ });
18532
18931
  return;
18533
18932
  }
18534
18933
  if (branch && !MANAGED_BRANCH_RE.test(branch)) {
18535
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "not a managed worktree branch" } });
18934
+ this.broadcast({
18935
+ type: "worktree.cleanup_result",
18936
+ payload: { ok: false, removed: 0, reason: "not a managed worktree branch" }
18937
+ });
18536
18938
  return;
18537
18939
  }
18538
18940
  if (dir && !this.underRoot(dir)) {
18539
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" } });
18941
+ this.broadcast({
18942
+ type: "worktree.cleanup_result",
18943
+ payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" }
18944
+ });
18540
18945
  return;
18541
18946
  }
18542
18947
  if (branch && this.liveActiveBranches().has(branch)) {
18543
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "a run is live on this worktree \u2014 stop it first" } });
18948
+ this.broadcast({
18949
+ type: "worktree.cleanup_result",
18950
+ payload: {
18951
+ ok: false,
18952
+ removed: 0,
18953
+ reason: "a run is live on this worktree \u2014 stop it first"
18954
+ }
18955
+ });
18544
18956
  return;
18545
18957
  }
18546
18958
  let removed = false;
@@ -18549,31 +18961,54 @@ var WorktreeWebSocketHandler = class {
18549
18961
  ({ removed } = await wt.removeOne(dir, branch));
18550
18962
  }
18551
18963
  for (const [id, h] of [...this.handles]) {
18552
- if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId)) this.handles.delete(id);
18964
+ if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId))
18965
+ this.handles.delete(id);
18553
18966
  }
18554
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: removed, removed: removed ? 1 : 0, reason: removed ? void 0 : "remove failed (not a managed worktree?)" } });
18967
+ this.broadcast({
18968
+ type: "worktree.cleanup_result",
18969
+ payload: {
18970
+ ok: removed,
18971
+ removed: removed ? 1 : 0,
18972
+ reason: removed ? void 0 : "remove failed (not a managed worktree?)"
18973
+ }
18974
+ });
18555
18975
  this.broadcastState();
18556
18976
  await this.scanAndBroadcast();
18557
18977
  }
18558
18978
  /** Squash-merge ONE branch into base. Refused while a live run owns it. */
18559
18979
  async mergeBranch(branch) {
18560
18980
  if (!this.management || !branch) {
18561
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch: branch ?? "", reason: "no branch" } });
18981
+ this.broadcast({
18982
+ type: "worktree.merge_result",
18983
+ payload: { ok: false, branch: branch ?? "", reason: "no branch" }
18984
+ });
18562
18985
  return;
18563
18986
  }
18564
18987
  if (!MANAGED_BRANCH_RE.test(branch)) {
18565
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "not a managed worktree branch" } });
18988
+ this.broadcast({
18989
+ type: "worktree.merge_result",
18990
+ payload: { ok: false, branch, reason: "not a managed worktree branch" }
18991
+ });
18566
18992
  return;
18567
18993
  }
18568
18994
  if (this.liveActiveBranches().has(branch)) {
18569
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" } });
18995
+ this.broadcast({
18996
+ type: "worktree.merge_result",
18997
+ payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" }
18998
+ });
18570
18999
  return;
18571
19000
  }
18572
19001
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
18573
19002
  const res = await wt.mergeBranch(branch);
18574
19003
  this.broadcast({
18575
19004
  type: "worktree.merge_result",
18576
- payload: { ok: res.ok, branch, conflict: res.conflict, conflictFiles: res.conflictFiles, reason: res.reason }
19005
+ payload: {
19006
+ ok: res.ok,
19007
+ branch,
19008
+ conflict: res.conflict,
19009
+ conflictFiles: res.conflictFiles,
19010
+ reason: res.reason
19011
+ }
18577
19012
  });
18578
19013
  await this.scanAndBroadcast();
18579
19014
  }
@@ -18615,8 +19050,14 @@ var WorktreeWebSocketHandler = class {
18615
19050
  }),
18616
19051
  on("worktree.committed", (p) => {
18617
19052
  const e = p;
18618
- this.patch(e.handleId, { status: "committing", insertions: e.insertions, deletions: e.deletions, files: e.files });
18619
- if (e.committed) this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
19053
+ this.patch(e.handleId, {
19054
+ status: "committing",
19055
+ insertions: e.insertions,
19056
+ deletions: e.deletions,
19057
+ files: e.files
19058
+ });
19059
+ if (e.committed)
19060
+ this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
18620
19061
  this.broadcastState();
18621
19062
  }),
18622
19063
  on("worktree.merged", (p) => {
@@ -18657,10 +19098,15 @@ var WorktreeWebSocketHandler = class {
18657
19098
  activity(id, kind, text) {
18658
19099
  const cur = this.handles.get(id);
18659
19100
  if (cur) {
18660
- const recentActivity = [...cur.recentActivity, { kind, text, at: Date.now() }].slice(-MAX_ACTIVITY);
19101
+ const recentActivity = [...cur.recentActivity, { kind, text, at: Date.now() }].slice(
19102
+ -MAX_ACTIVITY
19103
+ );
18661
19104
  this.handles.set(id, { ...cur, recentActivity });
18662
19105
  }
18663
- this.broadcast({ type: "worktree.event", payload: { kind, handleId: id, text, at: Date.now() } });
19106
+ this.broadcast({
19107
+ type: "worktree.event",
19108
+ payload: { kind, handleId: id, text, at: Date.now() }
19109
+ });
18664
19110
  }
18665
19111
  stateMessage() {
18666
19112
  return {
@@ -18902,7 +19348,7 @@ async function createAgentServices(input) {
18902
19348
  const brainCfg = resolveBrainConfigDefaults(config.brain, {
18903
19349
  fallbackModels: config.fallbackModels
18904
19350
  });
18905
- const brainLedgerPath = join12(wpaths.projectDir, "brain-ledger.jsonl");
19351
+ const brainLedgerPath = join13(wpaths.projectDir, "brain-ledger.jsonl");
18906
19352
  let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
18907
19353
  let brainLedger;
18908
19354
  const startBrainLedger = () => {
@@ -18967,33 +19413,35 @@ async function createAgentServices(input) {
18967
19413
  brainLog.push(entry);
18968
19414
  if (brainLog.length > 20) brainLog.shift();
18969
19415
  };
18970
- events.on(
18971
- "brain.decision_answered",
18972
- (e) => pushBrainLog({
18973
- at: e.at,
18974
- kind: "answered",
18975
- question: e.request.question,
18976
- outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
18977
- })
18978
- );
18979
- events.on(
18980
- "brain.decision_ask_human",
18981
- (e) => pushBrainLog({
18982
- at: e.at,
18983
- kind: "ask_human",
18984
- question: e.request.question,
18985
- outcome: "needs human judgement"
18986
- })
18987
- );
18988
- events.on(
18989
- "brain.decision_denied",
18990
- (e) => pushBrainLog({
18991
- at: e.at,
18992
- kind: "denied",
18993
- question: e.request.question,
18994
- outcome: e.decision.type === "deny" ? e.decision.reason : ""
18995
- })
18996
- );
19416
+ const brainLogOffs = [
19417
+ events.on(
19418
+ "brain.decision_answered",
19419
+ (e) => pushBrainLog({
19420
+ at: e.at,
19421
+ kind: "answered",
19422
+ question: e.request.question,
19423
+ outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
19424
+ })
19425
+ ),
19426
+ events.on(
19427
+ "brain.decision_ask_human",
19428
+ (e) => pushBrainLog({
19429
+ at: e.at,
19430
+ kind: "ask_human",
19431
+ question: e.request.question,
19432
+ outcome: "needs human judgement"
19433
+ })
19434
+ ),
19435
+ events.on(
19436
+ "brain.decision_denied",
19437
+ (e) => pushBrainLog({
19438
+ at: e.at,
19439
+ kind: "denied",
19440
+ question: e.request.question,
19441
+ outcome: e.decision.type === "deny" ? e.decision.reason : ""
19442
+ })
19443
+ )
19444
+ ];
18997
19445
  const brainMailbox = getSharedProjectMailbox2(wpaths.projectDir, events);
18998
19446
  brainMonitor = new BrainMonitor({
18999
19447
  events,
@@ -19105,6 +19553,17 @@ async function createAgentServices(input) {
19105
19553
  getActiveSessionId: () => context.session.id
19106
19554
  }
19107
19555
  );
19556
+ let realtimeHandlersDisposed = false;
19557
+ const disposeRealtimeHandlers = () => {
19558
+ if (realtimeHandlersDisposed) return;
19559
+ realtimeHandlersDisposed = true;
19560
+ for (const off of brainLogOffs) off();
19561
+ goalHandler.dispose();
19562
+ sddBoardHandler.dispose();
19563
+ worktreeHandler.dispose();
19564
+ terminalHandler.dispose();
19565
+ collabHandler.dispose();
19566
+ };
19108
19567
  return {
19109
19568
  collabBus,
19110
19569
  compactor,
@@ -19130,6 +19589,7 @@ async function createAgentServices(input) {
19130
19589
  worktreeHandler,
19131
19590
  terminalHandler,
19132
19591
  collabHandler,
19592
+ disposeRealtimeHandlers,
19133
19593
  updateAutoCompactionMaxContext
19134
19594
  };
19135
19595
  }
@@ -19216,7 +19676,7 @@ function createConnectionHandler(options) {
19216
19676
  }
19217
19677
 
19218
19678
  // src/server/message-dispatcher.ts
19219
- import path22 from "node:path";
19679
+ import path23 from "node:path";
19220
19680
  function createMessageDispatcher(opts) {
19221
19681
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
19222
19682
  function makeWorklistContext() {
@@ -19237,7 +19697,7 @@ function createMessageDispatcher(opts) {
19237
19697
  skillLoader: deps2.skillLoader,
19238
19698
  skillInstaller: deps2.skillInstaller,
19239
19699
  projectRoot,
19240
- projectSkillsDir: path22.join(projectRoot, ".wrongstack", "skills"),
19700
+ projectSkillsDir: path23.join(projectRoot, ".wrongstack", "skills"),
19241
19701
  globalSkillsDir: deps2.wpaths.globalSkills
19242
19702
  };
19243
19703
  }
@@ -19489,7 +19949,7 @@ function createMessageDispatcher(opts) {
19489
19949
 
19490
19950
  // src/server/pre-context-services.ts
19491
19951
  import { createRequire as createRequire3 } from "node:module";
19492
- import * as path25 from "node:path";
19952
+ import * as path26 from "node:path";
19493
19953
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
19494
19954
  import {
19495
19955
  getSharedProjectMailbox as getSharedProjectMailbox3,
@@ -19543,8 +20003,8 @@ import { configureDangerBypass, configureExecPolicy } from "@wrongstack/tools";
19543
20003
  import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/tools/session-kanban";
19544
20004
 
19545
20005
  // src/server/model-auto-discovery.ts
19546
- import * as fs18 from "node:fs/promises";
19547
- import * as path23 from "node:path";
20006
+ import * as fs19 from "node:fs/promises";
20007
+ import * as path24 from "node:path";
19548
20008
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
19549
20009
  function isOverlayRegistry(value) {
19550
20010
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -19570,7 +20030,7 @@ function eligibleProviders(config) {
19570
20030
  }
19571
20031
  async function readCache(file) {
19572
20032
  try {
19573
- return JSON.parse(await fs18.readFile(file, "utf8"));
20033
+ return JSON.parse(await fs19.readFile(file, "utf8"));
19574
20034
  } catch {
19575
20035
  return {};
19576
20036
  }
@@ -19580,7 +20040,7 @@ async function discoverAndMergeWebuiProviders(opts) {
19580
20040
  if (!isOverlayRegistry(registry)) return;
19581
20041
  const targets = eligibleProviders(opts.config);
19582
20042
  if (targets.length === 0) return;
19583
- const cacheFile = path23.join(opts.cacheDir, "discovered-models-cache.json");
20043
+ const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
19584
20044
  const cache2 = await readCache(cacheFile);
19585
20045
  let cacheDirty = false;
19586
20046
  await Promise.all(
@@ -19617,8 +20077,8 @@ async function discoverAndMergeWebuiProviders(opts) {
19617
20077
  );
19618
20078
  if (cacheDirty) {
19619
20079
  try {
19620
- await fs18.mkdir(path23.dirname(cacheFile), { recursive: true });
19621
- await fs18.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
20080
+ await fs19.mkdir(path24.dirname(cacheFile), { recursive: true });
20081
+ await fs19.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
19622
20082
  } catch {
19623
20083
  opts.logger?.debug?.("provider auto-discovery cache write failed");
19624
20084
  }
@@ -19714,7 +20174,7 @@ function resolveSetupProvider(opts) {
19714
20174
  }
19715
20175
 
19716
20176
  // src/server/standalone-session-identity.ts
19717
- import * as path24 from "node:path";
20177
+ import * as path25 from "node:path";
19718
20178
  import {
19719
20179
  AgentStatusTracker,
19720
20180
  FleetNotifier,
@@ -19733,7 +20193,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
19733
20193
  let activeTarget = {
19734
20194
  projectSlug: paths.projectSlug,
19735
20195
  projectRoot: paths.projectRoot,
19736
- projectName: path24.basename(paths.projectRoot),
20196
+ projectName: path25.basename(paths.projectRoot),
19737
20197
  workingDir: opts.workingDir
19738
20198
  };
19739
20199
  let pendingClaim;
@@ -19964,7 +20424,7 @@ async function createPreContextServices(input) {
19964
20424
  await discoverAndMergeWebuiProviders({
19965
20425
  config,
19966
20426
  registry: modelsRegistry,
19967
- cacheDir: path25.dirname(wpaths.modelsCache),
20427
+ cacheDir: path26.dirname(wpaths.modelsCache),
19968
20428
  logger
19969
20429
  });
19970
20430
  } catch (err) {
@@ -20016,7 +20476,7 @@ async function createPreContextServices(input) {
20016
20476
  configureChildEnvGitIdentity(config.git?.identity ?? null);
20017
20477
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
20018
20478
  const mcpTokenStore = new MCPVaultTokenStore(
20019
- path25.join(wpaths.projectDir, "mcp-auth.json"),
20479
+ path26.join(wpaths.projectDir, "mcp-auth.json"),
20020
20480
  vault
20021
20481
  );
20022
20482
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -20122,7 +20582,7 @@ async function createPreContextServices(input) {
20122
20582
  };
20123
20583
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
20124
20584
  const skillInstaller = config.features.skills ? new SkillInstaller({
20125
- manifestPath: path25.join(wpaths.configDir, "installed-skills.json"),
20585
+ manifestPath: path26.join(wpaths.configDir, "installed-skills.json"),
20126
20586
  projectSkillsDir: wpaths.inProjectSkills,
20127
20587
  globalSkillsDir: wpaths.globalSkills,
20128
20588
  projectHash: wpaths.projectHash,
@@ -20132,8 +20592,8 @@ async function createPreContextServices(input) {
20132
20592
  const bundledPromptsDir = promptsEnabled ? (() => {
20133
20593
  try {
20134
20594
  const req = createRequire3(import.meta.url);
20135
- return path25.join(
20136
- path25.dirname(req.resolve("@wrongstack/core/package.json")),
20595
+ return path26.join(
20596
+ path26.dirname(req.resolve("@wrongstack/core/package.json")),
20137
20597
  "data",
20138
20598
  "prompts"
20139
20599
  );
@@ -20237,7 +20697,7 @@ async function createPreContextServices(input) {
20237
20697
  }
20238
20698
 
20239
20699
  // src/server/routes.ts
20240
- import path26 from "node:path";
20700
+ import path27 from "node:path";
20241
20701
  import { makeProviderFromConfig as makeProviderFromConfig2, withCatalogCapabilities } from "@wrongstack/providers";
20242
20702
 
20243
20703
  // src/server/mode-handlers.ts
@@ -20529,7 +20989,7 @@ function buildRoutes(state, deps2, cb) {
20529
20989
  };
20530
20990
  const mailboxRoutes = createMailboxRouteHandlers({
20531
20991
  getProjectRoot: state.getProjectRoot,
20532
- getGlobalRoot: () => path26.dirname(deps2.globalConfigPath),
20992
+ getGlobalRoot: () => path27.dirname(deps2.globalConfigPath),
20533
20993
  events: deps2.events
20534
20994
  });
20535
20995
  const mcpRoutes = {
@@ -20602,7 +21062,7 @@ function buildRoutes(state, deps2, cb) {
20602
21062
  }
20603
21063
 
20604
21064
  // src/server/server-runtime.ts
20605
- import * as path27 from "node:path";
21065
+ import * as path28 from "node:path";
20606
21066
  import { createRequire as createRequire4 } from "node:module";
20607
21067
  import { fileURLToPath } from "node:url";
20608
21068
  import { WebSocketServer } from "ws";
@@ -20663,7 +21123,7 @@ function createSessionStartPayload(g) {
20663
21123
  inputCost,
20664
21124
  outputCost,
20665
21125
  cacheReadCost,
20666
- projectName: path27.basename(projectRoot) || projectRoot,
21126
+ projectName: path28.basename(projectRoot) || projectRoot,
20667
21127
  projectRoot,
20668
21128
  cwd: g.getWorkingDir(),
20669
21129
  mode: g.getModeId(),
@@ -20751,13 +21211,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
20751
21211
  };
20752
21212
  }
20753
21213
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
20754
- if (explicitDistDir) return path27.resolve(explicitDistDir);
21214
+ if (explicitDistDir) return path28.resolve(explicitDistDir);
20755
21215
  try {
20756
21216
  const requireFromHere2 = createRequire4(fromUrl);
20757
21217
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
20758
- return path27.dirname(serverEntry);
21218
+ return path28.dirname(serverEntry);
20759
21219
  } catch {
20760
- return path27.resolve(path27.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
21220
+ return path28.resolve(path28.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
20761
21221
  }
20762
21222
  }
20763
21223
  function startHttpServer(opts) {
@@ -20971,6 +21431,7 @@ async function startWebUI(opts = {}) {
20971
21431
  worktreeHandler,
20972
21432
  terminalHandler,
20973
21433
  collabHandler,
21434
+ disposeRealtimeHandlers,
20974
21435
  updateAutoCompactionMaxContext
20975
21436
  } = agentServices;
20976
21437
  if (typeof context.meta["yolo"] === "boolean") {
@@ -21149,21 +21610,21 @@ async function startWebUI(opts = {}) {
21149
21610
  });
21150
21611
  }
21151
21612
  async function touchProjectEntry(root, workDir) {
21152
- const resolved = path28.resolve(root);
21613
+ const resolved = path29.resolve(root);
21153
21614
  const manifest = await loadManifest(globalConfigPath);
21154
21615
  const now = (/* @__PURE__ */ new Date()).toISOString();
21155
- const existing = manifest.projects.find((p) => path28.resolve(p.root) === resolved);
21616
+ const existing = manifest.projects.find((p) => path29.resolve(p.root) === resolved);
21156
21617
  if (existing) {
21157
21618
  existing.lastSeen = now;
21158
- if (workDir) existing.lastWorkingDir = path28.resolve(workDir);
21619
+ if (workDir) existing.lastWorkingDir = path29.resolve(workDir);
21159
21620
  } else {
21160
21621
  manifest.projects.push({
21161
- name: path28.basename(resolved),
21622
+ name: path29.basename(resolved),
21162
21623
  root: resolved,
21163
21624
  slug: generateProjectSlug(resolved),
21164
21625
  createdAt: now,
21165
21626
  lastSeen: now,
21166
- lastWorkingDir: workDir ? path28.resolve(workDir) : void 0
21627
+ lastWorkingDir: workDir ? path29.resolve(workDir) : void 0
21167
21628
  });
21168
21629
  }
21169
21630
  await saveManifest(manifest, globalConfigPath);
@@ -21340,7 +21801,20 @@ async function startWebUI(opts = {}) {
21340
21801
  );
21341
21802
  credentialWatcherClose = credentialWatcher.close;
21342
21803
  }
21343
- const stopHeapWatchdog = startHeapWatchdog();
21804
+ const stopHeapWatchdog = startSharedHeapWatchdog({
21805
+ collectStats: () => ({
21806
+ surface: opts.surface ?? "webui",
21807
+ sessionId: context.session.id,
21808
+ messages: context.state.messages.length,
21809
+ messageEstimatedTokens: context.state.messages.reduce(
21810
+ (sum, message) => sum + (message._estTokens ?? 0),
21811
+ 0
21812
+ ),
21813
+ webClients: clients.size,
21814
+ pendingConfirms: pendingConfirms.size,
21815
+ runActive: runLockControl.get() !== null
21816
+ })
21817
+ });
21344
21818
  const routes = buildRoutes(state, deps2, cb);
21345
21819
  const handleMessage = createMessageDispatcher({
21346
21820
  state,
@@ -21418,6 +21892,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21418
21892
  await todosCheckpoint.detach();
21419
21893
  await stopHeapWatchdog();
21420
21894
  credentialWatcherClose?.();
21895
+ disposeRealtimeHandlers();
21421
21896
  brainMonitor.stop();
21422
21897
  await agentServices.brainLedger?.stop().catch(() => {
21423
21898
  });
@@ -21441,7 +21916,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21441
21916
  await memoryStore.dispose().catch(
21442
21917
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
21443
21918
  );
21444
- await unregisterInstance(process.pid, path28.dirname(globalConfigPath));
21919
+ await unregisterInstance(process.pid, path29.dirname(globalConfigPath));
21445
21920
  }
21446
21921
  });
21447
21922
  }
@@ -21534,7 +22009,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
21534
22009
  console.error(err instanceof Error ? err.message : String(err));
21535
22010
  process.exit(1);
21536
22011
  }
21537
- const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
22012
+ const open2 = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
21538
22013
  const requireToken = argv.includes("--require-token") || envFlag2("WEBUI_REQUIRE_TOKEN");
21539
22014
  console.log(`[WebUI] Starting standalone server on ${wsHost} (http:${httpPort})...`);
21540
22015
  startWebUI({
@@ -21544,7 +22019,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
21544
22019
  publicUrl,
21545
22020
  publicWsUrl,
21546
22021
  requireToken,
21547
- open,
22022
+ open: open2,
21548
22023
  distDir
21549
22024
  }).catch((err) => {
21550
22025
  console.error(JSON.stringify({