@wrongstack/webui-server 0.296.4 → 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 (46) hide show
  1. package/dist/index.js +934 -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/embedded-lifecycle.d.ts +2 -0
  18. package/dist/server/embedded-lifecycle.d.ts.map +1 -1
  19. package/dist/server/entry.js +856 -395
  20. package/dist/server/entry.js.map +4 -4
  21. package/dist/server/goal-ws-handler.d.ts +3 -1
  22. package/dist/server/goal-ws-handler.d.ts.map +1 -1
  23. package/dist/server/handlers.js.map +2 -2
  24. package/dist/server/http-server.d.ts.map +1 -1
  25. package/dist/server/kanban-supervisor.d.ts +6 -0
  26. package/dist/server/kanban-supervisor.d.ts.map +1 -1
  27. package/dist/server/mailbox-handlers.d.ts +2 -1
  28. package/dist/server/mailbox-handlers.d.ts.map +1 -1
  29. package/dist/server/mailbox-routes.d.ts +1 -0
  30. package/dist/server/mailbox-routes.d.ts.map +1 -1
  31. package/dist/server/memory-diagnostics.d.ts +69 -0
  32. package/dist/server/memory-diagnostics.d.ts.map +1 -0
  33. package/dist/server/sdd-board-ws-handler.d.ts +14 -8
  34. package/dist/server/sdd-board-ws-handler.d.ts.map +1 -1
  35. package/dist/server/sdd-wizard-wiring.d.ts +5 -6
  36. package/dist/server/sdd-wizard-wiring.d.ts.map +1 -1
  37. package/dist/server/sdd-wizard-ws-handler.d.ts +16 -6
  38. package/dist/server/sdd-wizard-ws-handler.d.ts.map +1 -1
  39. package/dist/server/start-webui.d.ts.map +1 -1
  40. package/dist/server/types.d.ts +2 -0
  41. package/dist/server/types.d.ts.map +1 -1
  42. package/dist/server/worktree-ws-handler.d.ts +30 -2
  43. package/dist/server/worktree-ws-handler.d.ts.map +1 -1
  44. package/dist/server/ws-payload-validation.d.ts +8 -0
  45. package/dist/server/ws-payload-validation.d.ts.map +1 -1
  46. 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
  }
@@ -5420,15 +5407,15 @@ async function handleGitChanges(ws, projectRoot) {
5420
5407
  if (!m) continue;
5421
5408
  const added = m[1] === "-" ? 0 : Number(m[1]);
5422
5409
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5423
- let path29 = m[3] ?? "";
5424
- if (path29 === "") {
5410
+ let path30 = m[3] ?? "";
5411
+ if (path30 === "") {
5425
5412
  i += 1;
5426
- path29 = parts[i + 1] ?? parts[i] ?? "";
5413
+ path30 = parts[i + 1] ?? parts[i] ?? "";
5427
5414
  i += 1;
5428
5415
  }
5429
- if (!path29) continue;
5430
- const prev = counts.get(path29) ?? { added: 0, deleted: 0 };
5431
- 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 });
5432
5419
  }
5433
5420
  };
5434
5421
  parseNumstat(unstagedNumstat);
@@ -5440,7 +5427,7 @@ async function handleGitChanges(ws, projectRoot) {
5440
5427
  if (!rec || rec.length < 3) continue;
5441
5428
  const x = rec[0] ?? " ";
5442
5429
  const y = rec[1] ?? " ";
5443
- const path29 = rec.slice(3);
5430
+ const path30 = rec.slice(3);
5444
5431
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5445
5432
  if (isRename) i += 1;
5446
5433
  let status;
@@ -5452,13 +5439,13 @@ async function handleGitChanges(ws, projectRoot) {
5452
5439
  else if (x === "D" || y === "D") status = "D";
5453
5440
  else status = "M";
5454
5441
  const staged = x !== " " && x !== "?";
5455
- let added = counts.get(path29)?.added ?? 0;
5456
- let deleted = counts.get(path29)?.deleted ?? 0;
5442
+ let added = counts.get(path30)?.added ?? 0;
5443
+ let deleted = counts.get(path30)?.deleted ?? 0;
5457
5444
  if (status === "?") {
5458
5445
  added = 0;
5459
5446
  deleted = 0;
5460
5447
  }
5461
- files.push({ path: path29, status, added, deleted, staged });
5448
+ files.push({ path: path30, status, added, deleted, staged });
5462
5449
  }
5463
5450
  send(ws, { type: "git.changes", payload: { files } });
5464
5451
  } catch (err) {
@@ -5469,21 +5456,21 @@ async function handleGitChanges(ws, projectRoot) {
5469
5456
  }
5470
5457
  }
5471
5458
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5472
- async function handleGitDiff(ws, projectRoot, path29) {
5459
+ async function handleGitDiff(ws, projectRoot, path30) {
5473
5460
  const cwd = projectRoot || void 0;
5474
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path29, ...extra } });
5475
- 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)) {
5476
5463
  reply2({ oldText: "", newText: "", error: "invalid path" });
5477
5464
  return;
5478
5465
  }
5479
5466
  try {
5480
5467
  const git = makeGit(cwd);
5481
5468
  const { readFile: readFile11 } = await import("node:fs/promises");
5482
- const { join: join15 } = await import("node:path");
5483
- const oldText = await git(["show", `HEAD:${path29}`]);
5469
+ const { join: join16 } = await import("node:path");
5470
+ const oldText = await git(["show", `HEAD:${path30}`]);
5484
5471
  let newText = "";
5485
5472
  try {
5486
- const abs = cwd ? join15(cwd, path29) : path29;
5473
+ const abs = cwd ? join16(cwd, path30) : path30;
5487
5474
  const buf = await readFile11(abs);
5488
5475
  if (buf.includes(0)) {
5489
5476
  reply2({ oldText: "", newText: "", binary: true });
@@ -5545,10 +5532,7 @@ async function handleGoalSnapshotRoute(ws, msg, handlers) {
5545
5532
  }
5546
5533
 
5547
5534
  // src/server/goal-ws-handler.ts
5548
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5549
- import {
5550
- assignNickname
5551
- } from "@wrongstack/core/coordination";
5535
+ import { assignNickname } from "@wrongstack/core/coordination";
5552
5536
  import {
5553
5537
  GoalAssessor,
5554
5538
  GoalPlanner,
@@ -5556,6 +5540,7 @@ import {
5556
5540
  PhaseOrchestrator,
5557
5541
  PhaseStore
5558
5542
  } from "@wrongstack/core/goal";
5543
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5559
5544
  import { WorktreeManager } from "@wrongstack/core/worktree";
5560
5545
 
5561
5546
  // src/server/git-process.ts
@@ -5592,12 +5577,7 @@ function deriveTitle(goal) {
5592
5577
  return trimmed || "Goal";
5593
5578
  }
5594
5579
  async function commitsSince(cwd, baseSha, branch) {
5595
- const output = await gitStdout(cwd, [
5596
- "log",
5597
- "--reverse",
5598
- "--format=%H",
5599
- `${baseSha}..${branch}`
5600
- ]);
5580
+ const output = await gitStdout(cwd, ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`]);
5601
5581
  if (output === null) return [];
5602
5582
  return output.split("\n").map((s) => s.trim()).filter(Boolean);
5603
5583
  }
@@ -5648,6 +5628,17 @@ var GoalWebSocketHandler = class {
5648
5628
  ws.on("error", () => this.clients.delete(client));
5649
5629
  this.sendState(client);
5650
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
+ }
5651
5642
  async handleMessage(ws, msg) {
5652
5643
  switch (msg.type) {
5653
5644
  case "goal.assess":
@@ -5695,7 +5686,8 @@ var GoalWebSocketHandler = class {
5695
5686
  }
5696
5687
  case "goal.assignTask": {
5697
5688
  const { taskId, agentId, agentName } = msg.payload;
5698
- if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
5689
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName))
5690
+ this.afterBoardMutation();
5699
5691
  break;
5700
5692
  }
5701
5693
  case "goal.addTask": {
@@ -5740,7 +5732,10 @@ var GoalWebSocketHandler = class {
5740
5732
  this.graph = graph;
5741
5733
  this.broadcast({ type: "goal.state", payload: this.buildState() });
5742
5734
  } else {
5743
- 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
+ });
5744
5739
  }
5745
5740
  }
5746
5741
  break;
@@ -5763,10 +5758,13 @@ var GoalWebSocketHandler = class {
5763
5758
  const mySeq = ++this.assessSeq;
5764
5759
  const sendResult7 = (result) => {
5765
5760
  if (mySeq !== this.assessSeq) return;
5766
- sendSerialized(ws, JSON.stringify({
5767
- type: "goal.assess.result",
5768
- payload: { ...result, reqSeq: seq }
5769
- }));
5761
+ sendSerialized(
5762
+ ws,
5763
+ JSON.stringify({
5764
+ type: "goal.assess.result",
5765
+ payload: { ...result, reqSeq: seq }
5766
+ })
5767
+ );
5770
5768
  };
5771
5769
  if (!goal.trim()) {
5772
5770
  sendResult7({
@@ -5812,15 +5810,24 @@ var GoalWebSocketHandler = class {
5812
5810
  const multiBoard = payload?.multiBoard ?? false;
5813
5811
  const verifyTasks = payload?.verifyTasks ?? false;
5814
5812
  const chimeraReview = payload?.chimeraReview ?? false;
5815
- this.abort = new AbortController();
5813
+ const runAbort = new AbortController();
5814
+ this.abort = runAbort;
5816
5815
  this.stopping = false;
5817
- const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
5818
- 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) {
5819
5818
  this.broadcast({ type: "goal.stopped", payload: { title } });
5820
5819
  return;
5821
5820
  }
5822
5821
  this.logger.info(`[Goal] Starting: ${title}`);
5823
- 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();
5824
5831
  this.graph = graph;
5825
5832
  await this.store.save(graph);
5826
5833
  const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_GOAL_WORKTREES"] !== "0";
@@ -5839,9 +5846,10 @@ var GoalWebSocketHandler = class {
5839
5846
  maybeVerify.verifyPhase = (async (phase, env) => {
5840
5847
  const cwd = env?.cwd ?? this.projectRoot;
5841
5848
  try {
5842
- const { exec } = await import("node:child_process");
5849
+ const { execFile: execFile2 } = await import("node:child_process");
5843
5850
  const result = await new Promise((resolve15) => {
5844
- 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) => {
5845
5853
  if (err && err.code === "ENOENT") {
5846
5854
  resolve15("[verify] tsc not found \u2014 skipping");
5847
5855
  return;
@@ -5983,11 +5991,41 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
5983
5991
  /** Generic fallback phases when the LLM planner produces nothing usable. */
5984
5992
  defaultPhases() {
5985
5993
  return [
5986
- { name: "Discovery", description: "Requirements gathering", priority: "high", estimateHours: 2, parallelizable: false },
5987
- { name: "Design", description: "Architecture and design", priority: "critical", estimateHours: 4, parallelizable: false },
5988
- { name: "Implementation", description: "Core development", priority: "critical", estimateHours: 12, parallelizable: false },
5989
- { name: "Testing", description: "Unit and integration tests", priority: "high", estimateHours: 6, parallelizable: true },
5990
- { 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
+ }
5991
6029
  ];
5992
6030
  }
5993
6031
  /** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
@@ -6068,8 +6106,10 @@ Type: ${task.type}`;
6068
6106
  try {
6069
6107
  const result_ = await this.agent.run(reviewPrompt);
6070
6108
  if (result_.status === "done" && result_.finalText) {
6071
- this.logger.info(`[Goal] Chimera review for "${task.title}":
6072
- ${result_.finalText.slice(0, 2e3)}`);
6109
+ this.logger.info(
6110
+ `[Goal] Chimera review for "${task.title}":
6111
+ ${result_.finalText.slice(0, 2e3)}`
6112
+ );
6073
6113
  }
6074
6114
  } catch (err) {
6075
6115
  this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
@@ -6122,7 +6162,16 @@ ${result_.finalText.slice(0, 2e3)}`);
6122
6162
  }
6123
6163
  buildState(activePhaseId) {
6124
6164
  if (!this.graph) {
6125
- 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
+ };
6126
6175
  }
6127
6176
  const phases = Array.from(this.graph.phases.values());
6128
6177
  const currentActiveId = activePhaseId || phases.find((p) => p.status === "running")?.id || phases[0]?.id || "";
@@ -6620,9 +6669,9 @@ async function handleApiAnalyticsSummary(res) {
6620
6669
  }
6621
6670
 
6622
6671
  // src/server/http-server.ts
6623
- import * as fs9 from "node:fs/promises";
6672
+ import * as fs10 from "node:fs/promises";
6624
6673
  import * as http from "node:http";
6625
- import * as path12 from "node:path";
6674
+ import * as path13 from "node:path";
6626
6675
  import * as v8 from "node:v8";
6627
6676
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6628
6677
 
@@ -7306,18 +7355,153 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7306
7355
  }
7307
7356
  }
7308
7357
 
7309
- // src/server/projects-manifest.ts
7358
+ // src/server/memory-diagnostics.ts
7310
7359
  import * as fs8 from "node:fs/promises";
7311
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";
7312
7496
  import { ConfigError } from "@wrongstack/core/types";
7313
7497
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7314
7498
  function projectsJsonPath(globalConfigPath) {
7315
- const base = path11.dirname(globalConfigPath);
7316
- return path11.join(base, "projects.json");
7499
+ const base = path12.dirname(globalConfigPath);
7500
+ return path12.join(base, "projects.json");
7317
7501
  }
7318
7502
  async function loadManifest(globalConfigPath) {
7319
7503
  try {
7320
- const raw = await fs8.readFile(projectsJsonPath(globalConfigPath), "utf8");
7504
+ const raw = await fs9.readFile(projectsJsonPath(globalConfigPath), "utf8");
7321
7505
  const parsed = JSON.parse(raw);
7322
7506
  return { projects: parsed.projects ?? [] };
7323
7507
  } catch {
@@ -7326,37 +7510,37 @@ async function loadManifest(globalConfigPath) {
7326
7510
  }
7327
7511
  async function saveManifest(manifest, globalConfigPath) {
7328
7512
  const file = projectsJsonPath(globalConfigPath);
7329
- await fs8.mkdir(path11.dirname(file), { recursive: true });
7330
- 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");
7331
7515
  }
7332
7516
  function generateProjectSlug(rootPath) {
7333
7517
  return projectSlug(rootPath);
7334
7518
  }
7335
7519
  async function ensureProjectDataDir(slug, globalConfigPath) {
7336
- const base = path11.dirname(globalConfigPath);
7337
- const dir = path11.join(base, "projects", slug);
7338
- 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 });
7339
7523
  return dir;
7340
7524
  }
7341
7525
  async function touchProjectInManifest(options, globalConfigPath) {
7342
- const root = path11.resolve(options.projectRoot);
7526
+ const root = path12.resolve(options.projectRoot);
7343
7527
  const file = projectsJsonPath(globalConfigPath);
7344
7528
  let entry;
7345
7529
  await withFileLock(file, async () => {
7346
7530
  const manifest = await loadManifest(globalConfigPath);
7347
7531
  const now = (/* @__PURE__ */ new Date()).toISOString();
7348
- entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7532
+ entry = manifest.projects.find((candidate) => path12.resolve(candidate.root) === root);
7349
7533
  if (entry) {
7350
7534
  entry.lastSeen = now;
7351
- if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7535
+ if (options.workingDir) entry.lastWorkingDir = path12.resolve(options.workingDir);
7352
7536
  } else {
7353
7537
  entry = {
7354
- name: options.name ?? path11.basename(root),
7538
+ name: options.name ?? path12.basename(root),
7355
7539
  root,
7356
7540
  slug: generateProjectSlug(root),
7357
7541
  createdAt: now,
7358
7542
  lastSeen: now,
7359
- lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7543
+ lastWorkingDir: options.workingDir ? path12.resolve(options.workingDir) : void 0
7360
7544
  };
7361
7545
  manifest.projects.push(entry);
7362
7546
  }
@@ -7767,9 +7951,9 @@ function buildCspHeader(publicWsUrl, host, port) {
7767
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'`;
7768
7952
  }
7769
7953
  function isInsideDist(candidate, distDir) {
7770
- const root = path12.resolve(distDir);
7771
- const resolved = path12.resolve(candidate);
7772
- 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);
7773
7957
  }
7774
7958
  function decodeSessionId(segment) {
7775
7959
  try {
@@ -7789,7 +7973,7 @@ function strictDecodeParam(segment, res) {
7789
7973
  }
7790
7974
  function createHttpServer(opts) {
7791
7975
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
7792
- const distDir = path12.resolve(opts.distDir);
7976
+ const distDir = path13.resolve(opts.distDir);
7793
7977
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
7794
7978
  let techStackRuntime = null;
7795
7979
  const getTechStackRuntime = async () => {
@@ -8117,11 +8301,7 @@ function createHttpServer(opts) {
8117
8301
  if (researchMatch && req.method === "POST") {
8118
8302
  const pkg = strictDecodeParam(researchMatch[1], res);
8119
8303
  if (pkg === null) return;
8120
- await handleTechStackDependencyResearch(
8121
- res,
8122
- deps2,
8123
- pkg
8124
- );
8304
+ await handleTechStackDependencyResearch(res, deps2, pkg);
8125
8305
  return;
8126
8306
  }
8127
8307
  } catch (error2) {
@@ -8157,6 +8337,7 @@ function createHttpServer(opts) {
8157
8337
  return;
8158
8338
  }
8159
8339
  if (url.pathname === "/debug/system" && req.method === "GET") {
8340
+ const processes = await readRecentProcessMemoryDiagnostics(opts.globalRoot);
8160
8341
  res.writeHead(200, {
8161
8342
  "Content-Type": "application/json",
8162
8343
  "Cache-Control": "no-store"
@@ -8169,6 +8350,7 @@ function createHttpServer(opts) {
8169
8350
  uptime: process.uptime(),
8170
8351
  cpuUsage: process.cpuUsage(),
8171
8352
  codebaseIndexServer: getIndexState2().server,
8353
+ processes,
8172
8354
  timestamp: Date.now()
8173
8355
  })
8174
8356
  );
@@ -8176,24 +8358,24 @@ function createHttpServer(opts) {
8176
8358
  }
8177
8359
  let filePath;
8178
8360
  if (url.pathname === "/" || url.pathname === "") {
8179
- filePath = path12.join(distDir, "index.html");
8361
+ filePath = path13.join(distDir, "index.html");
8180
8362
  } else {
8181
- filePath = path12.join(distDir, url.pathname);
8363
+ filePath = path13.join(distDir, url.pathname);
8182
8364
  }
8183
- const resolvedPath = path12.resolve(filePath);
8365
+ const resolvedPath = path13.resolve(filePath);
8184
8366
  if (!isInsideDist(resolvedPath, distDir)) {
8185
8367
  res.writeHead(403, { "Content-Type": "text/plain" });
8186
8368
  res.end("Forbidden");
8187
8369
  return;
8188
8370
  }
8189
- const ext = path12.extname(resolvedPath);
8371
+ const ext = path13.extname(resolvedPath);
8190
8372
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8191
8373
  res.setHeader("Content-Type", contentType);
8192
8374
  setStaticSecurityHeaders(res);
8193
8375
  if (ext === ".html") {
8194
8376
  if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
8195
8377
  res.setHeader("Content-Security-Policy", buildCspHeader(opts.publicWsUrl, opts.host, port));
8196
- const html = await fs9.readFile(resolvedPath, "utf8");
8378
+ const html = await fs10.readFile(resolvedPath, "utf8");
8197
8379
  res.writeHead(200);
8198
8380
  res.end(injectWsConfig(html, { publicWsUrl: opts.publicWsUrl }));
8199
8381
  return;
@@ -8204,13 +8386,13 @@ function createHttpServer(opts) {
8204
8386
  url.pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=3600"
8205
8387
  );
8206
8388
  }
8207
- const fileContent = await fs9.readFile(resolvedPath);
8389
+ const fileContent = await fs10.readFile(resolvedPath);
8208
8390
  res.writeHead(200);
8209
8391
  res.end(fileContent);
8210
8392
  } catch (err) {
8211
8393
  if (err.code === "ENOENT") {
8212
8394
  try {
8213
- const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8395
+ const html = await fs10.readFile(path13.join(distDir, "index.html"), "utf8");
8214
8396
  setStaticSecurityHeaders(res);
8215
8397
  res.writeHead(200, {
8216
8398
  "Content-Type": "text/html",
@@ -8240,14 +8422,14 @@ function createHttpServer(opts) {
8240
8422
 
8241
8423
  // src/server/instance-registry.ts
8242
8424
  import * as os from "node:os";
8243
- import * as path13 from "node:path";
8244
- import * as fs10 from "node:fs/promises";
8425
+ import * as path14 from "node:path";
8426
+ import * as fs11 from "node:fs/promises";
8245
8427
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8246
8428
  function defaultBaseDir() {
8247
- return path13.join(os.homedir(), ".wrongstack");
8429
+ return path14.join(os.homedir(), ".wrongstack");
8248
8430
  }
8249
8431
  function registryPath(baseDir = defaultBaseDir()) {
8250
- return path13.join(baseDir, "webui-instances.json");
8432
+ return path14.join(baseDir, "webui-instances.json");
8251
8433
  }
8252
8434
  function isPidAlive(pid) {
8253
8435
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -8260,7 +8442,7 @@ function isPidAlive(pid) {
8260
8442
  }
8261
8443
  async function load(file) {
8262
8444
  try {
8263
- const raw = await fs10.readFile(file, "utf8");
8445
+ const raw = await fs11.readFile(file, "utf8");
8264
8446
  const parsed = JSON.parse(raw);
8265
8447
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
8266
8448
  return parsed;
@@ -10058,14 +10240,14 @@ function registerShutdownHandlers(res) {
10058
10240
  }
10059
10241
 
10060
10242
  // src/server/config-doctor.ts
10061
- import * as fs11 from "node:fs/promises";
10243
+ import * as fs12 from "node:fs/promises";
10062
10244
  import {
10063
10245
  repairConfigDefaults
10064
10246
  } from "@wrongstack/core/storage";
10065
10247
  import { atomicWrite as atomicWrite5 } from "@wrongstack/core/utils";
10066
10248
  import { decryptConfigSecrets } from "@wrongstack/core/security";
10067
10249
  async function readConfig(file, vault) {
10068
- const raw = await fs11.readFile(file, "utf8");
10250
+ const raw = await fs12.readFile(file, "utf8");
10069
10251
  const parsed = JSON.parse(raw);
10070
10252
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10071
10253
  throw new Error("Active profile config must contain a JSON object.");
@@ -10129,6 +10311,7 @@ async function handleConfigDoctor(ws, apply, deps2) {
10129
10311
 
10130
10312
  // src/server/mailbox-handlers.ts
10131
10313
  import {
10314
+ actionToAckInput,
10132
10315
  getSharedProjectMailbox,
10133
10316
  isMailboxMessageVisibleTo,
10134
10317
  MAILBOX_TYPE_PROPERTIES,
@@ -10145,6 +10328,43 @@ function getMailboxForDeps(deps2) {
10145
10328
  const dir = resolveProjectDir(projectRoot, globalRoot);
10146
10329
  return getSharedProjectMailbox(dir, deps2.events);
10147
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
+ }
10148
10368
  async function handleMailboxSend(ws, deps2, payload) {
10149
10369
  const mb = getMailboxForDeps(deps2);
10150
10370
  if (!mb) {
@@ -10160,7 +10380,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10160
10380
  }
10161
10381
  try {
10162
10382
  const message = await mb.send({
10163
- from: "webui",
10383
+ from: payload.from ?? "webui",
10164
10384
  to: payload.to,
10165
10385
  type: payload.type,
10166
10386
  audience: payload.audience,
@@ -10175,6 +10395,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10175
10395
  requestId: payload.requestId,
10176
10396
  success: true,
10177
10397
  messageId: message.id,
10398
+ from: message.from,
10178
10399
  to: message.to,
10179
10400
  audience: message.audience ?? "all"
10180
10401
  }
@@ -10219,6 +10440,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
10219
10440
  send(ws, {
10220
10441
  type: "mailbox.messages",
10221
10442
  payload: {
10443
+ ...payload?.unreadOnly === true ? { unreadOnly: true } : {},
10222
10444
  messages: visibleMessages.map((m) => {
10223
10445
  const readByMe = payload?.agentId !== void 0 ? payload.agentId in m.readBy : false;
10224
10446
  const completedByMe = payload?.agentId !== void 0 ? m.completedBy === payload.agentId : false;
@@ -10334,6 +10556,14 @@ function createMailboxRouteHandlers(ctx) {
10334
10556
  ...ctx.events ? { events: ctx.events } : {}
10335
10557
  };
10336
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
+ },
10337
10567
  send: (ws, msg) => {
10338
10568
  const parsed = validateMailboxSendPayload(msg.payload);
10339
10569
  if (!parsed.ok) {
@@ -10384,6 +10614,9 @@ function createMailboxRouteHandlers(ctx) {
10384
10614
  }
10385
10615
  async function handleMailboxRoute(ws, msg, handlers) {
10386
10616
  switch (msg.type) {
10617
+ case "mailbox.action":
10618
+ await handlers.action(ws, msg);
10619
+ return true;
10387
10620
  case "mailbox.send":
10388
10621
  await handlers.send(ws, msg);
10389
10622
  return true;
@@ -11930,8 +12163,8 @@ function seedContextMeta(config, context) {
11930
12163
  }
11931
12164
 
11932
12165
  // src/server/pref-helpers.ts
11933
- import * as fs12 from "node:fs/promises";
11934
- import * as path14 from "node:path";
12166
+ import * as fs13 from "node:fs/promises";
12167
+ import * as path15 from "node:path";
11935
12168
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
11936
12169
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
11937
12170
  var PREF_KEYS = [
@@ -12027,11 +12260,11 @@ function prefSnapshot(contextMeta) {
12027
12260
  return snapshot;
12028
12261
  }
12029
12262
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
12030
- const globalRoot = path14.dirname(filePath);
12263
+ const globalRoot = path15.dirname(filePath);
12031
12264
  await backupConfigFile(filePath, { globalRoot });
12032
12265
  let raw;
12033
12266
  try {
12034
- raw = await fs12.readFile(filePath, "utf8");
12267
+ raw = await fs13.readFile(filePath, "utf8");
12035
12268
  } catch {
12036
12269
  raw = "{}";
12037
12270
  }
@@ -12506,8 +12739,8 @@ async function handleProcessRoute(ws, msg, handlers) {
12506
12739
  }
12507
12740
 
12508
12741
  // src/server/project-handlers.ts
12509
- import * as fs13 from "node:fs/promises";
12510
- import * as path15 from "node:path";
12742
+ import * as fs14 from "node:fs/promises";
12743
+ import * as path16 from "node:path";
12511
12744
  import { DefaultSessionStore } from "@wrongstack/core/storage";
12512
12745
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
12513
12746
  function createProjectHandlers(ctx) {
@@ -12559,10 +12792,10 @@ function createProjectHandlers(ctx) {
12559
12792
  });
12560
12793
  return;
12561
12794
  }
12562
- const resolved = path15.resolve(parsed.value.root);
12563
- 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);
12564
12797
  try {
12565
- const stat3 = await fs13.stat(resolved).catch(() => null);
12798
+ const stat3 = await fs14.stat(resolved).catch(() => null);
12566
12799
  if (!stat3?.isDirectory()) {
12567
12800
  sendTo(ws, {
12568
12801
  type: "projects.added",
@@ -12571,7 +12804,7 @@ function createProjectHandlers(ctx) {
12571
12804
  return;
12572
12805
  }
12573
12806
  const before = await loadManifest(ctx.globalConfigPath);
12574
- const already = before.projects.some((project) => path15.resolve(project.root) === resolved);
12807
+ const already = before.projects.some((project) => path16.resolve(project.root) === resolved);
12575
12808
  const entry = await touchProjectInManifest(
12576
12809
  { projectRoot: resolved, workingDir: resolved, name: name2 },
12577
12810
  ctx.globalConfigPath
@@ -12601,8 +12834,8 @@ function createProjectHandlers(ctx) {
12601
12834
  });
12602
12835
  return;
12603
12836
  }
12604
- const resolved = path15.resolve(parsed.value.root);
12605
- 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);
12606
12839
  if (!ctx.allowProjectMutations) {
12607
12840
  sendTo(ws, {
12608
12841
  type: "projects.selected",
@@ -12615,7 +12848,7 @@ function createProjectHandlers(ctx) {
12615
12848
  return;
12616
12849
  }
12617
12850
  try {
12618
- const stat3 = await fs13.stat(resolved).catch(() => null);
12851
+ const stat3 = await fs14.stat(resolved).catch(() => null);
12619
12852
  if (!stat3?.isDirectory()) {
12620
12853
  sendTo(ws, {
12621
12854
  type: "projects.selected",
@@ -12645,7 +12878,7 @@ function createProjectHandlers(ctx) {
12645
12878
  const previousIdentityTarget = {
12646
12879
  projectSlug: previousPaths.projectSlug,
12647
12880
  projectRoot: previousProjectRoot,
12648
- projectName: path15.basename(previousProjectRoot),
12881
+ projectName: path16.basename(previousProjectRoot),
12649
12882
  workingDir: ctx.context.workingDir
12650
12883
  };
12651
12884
  const previousUsage = ctx.tokenCounter.total();
@@ -12808,14 +13041,14 @@ async function searchCatalogModels(registry, rawQuery, limit = 8) {
12808
13041
  }
12809
13042
 
12810
13043
  // src/server/provider-config-io.ts
12811
- import * as fs14 from "node:fs/promises";
13044
+ import * as fs15 from "node:fs/promises";
12812
13045
  import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
12813
13046
  import { atomicWrite as atomicWrite7 } from "@wrongstack/core/utils";
12814
13047
  import { decryptConfigSecrets as decryptConfigSecrets3, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
12815
13048
  async function loadSavedProviders(configPath, vault) {
12816
13049
  let raw;
12817
13050
  try {
12818
- raw = await fs14.readFile(configPath, "utf8");
13051
+ raw = await fs15.readFile(configPath, "utf8");
12819
13052
  } catch {
12820
13053
  return {};
12821
13054
  }
@@ -12833,7 +13066,7 @@ async function saveProviders(configPath, vault, providers, profileConfigPath) {
12833
13066
  let raw;
12834
13067
  let fileExists = true;
12835
13068
  try {
12836
- raw = await fs14.readFile(targetPath, "utf8");
13069
+ raw = await fs15.readFile(targetPath, "utf8");
12837
13070
  } catch (err) {
12838
13071
  if (err.code !== "ENOENT") {
12839
13072
  throw new ConfigError2({
@@ -13536,6 +13769,7 @@ var CLIENT_COLLABORATION_MESSAGE_TYPES = [
13536
13769
  "collab.resume",
13537
13770
  "collab.grant_control",
13538
13771
  "collab.inject_tool",
13772
+ "mailbox.action",
13539
13773
  "mailbox.agents",
13540
13774
  "mailbox.clear",
13541
13775
  "mailbox.compact",
@@ -13801,6 +14035,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
13801
14035
  "collab.pause.granted",
13802
14036
  "collab.pause.released",
13803
14037
  "collab.state",
14038
+ "mailbox.action_result",
13804
14039
  "mailbox.agent_registered",
13805
14040
  "mailbox.agents",
13806
14041
  "mailbox.cleared",
@@ -14024,13 +14259,13 @@ function isRegisteredMessageType(type, direction) {
14024
14259
  // src/protocol/decoder.ts
14025
14260
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
14026
14261
  var MAX_PAYLOAD_DEPTH = 32;
14027
- function inspectValue(value, path29, depth) {
14262
+ function inspectValue(value, path30, depth) {
14028
14263
  if (depth > MAX_PAYLOAD_DEPTH) {
14029
- 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 };
14030
14265
  }
14031
14266
  if (value === null || typeof value !== "object") return null;
14032
14267
  for (const key of Object.keys(value)) {
14033
- const childPath = `${path29}.${key}`;
14268
+ const childPath = `${path30}.${key}`;
14034
14269
  if (FORBIDDEN_KEYS.has(key)) {
14035
14270
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
14036
14271
  }
@@ -15726,8 +15961,8 @@ function createRouteFamilyDispatcher(options) {
15726
15961
  }
15727
15962
 
15728
15963
  // src/server/shell-open.ts
15729
- import * as fs15 from "node:fs/promises";
15730
- import * as path16 from "node:path";
15964
+ import * as fs16 from "node:fs/promises";
15965
+ import * as path17 from "node:path";
15731
15966
  import { spawn } from "node:child_process";
15732
15967
  function normalizeShellOpenTarget(target) {
15733
15968
  return target === "terminal" ? "terminal" : "file-manager";
@@ -15738,11 +15973,11 @@ function shellQuote(s) {
15738
15973
  }
15739
15974
  async function handleShellOpen(req, logger, options) {
15740
15975
  try {
15741
- const resolved = path16.resolve(req.path);
15976
+ const resolved = path17.resolve(req.path);
15742
15977
  if (options?.projectRoot) {
15743
- const root = path16.resolve(options.projectRoot);
15744
- const relative5 = path16.relative(root, resolved);
15745
- 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);
15746
15981
  if (escapes) {
15747
15982
  return {
15748
15983
  success: false,
@@ -15750,7 +15985,7 @@ async function handleShellOpen(req, logger, options) {
15750
15985
  };
15751
15986
  }
15752
15987
  }
15753
- await fs15.access(resolved);
15988
+ await fs16.access(resolved);
15754
15989
  if (METACHAR_REGEX.test(resolved)) {
15755
15990
  return { success: false, message: "Path contains unsupported characters." };
15756
15991
  }
@@ -15801,7 +16036,12 @@ async function handleShellOpen(req, logger, options) {
15801
16036
  }
15802
16037
 
15803
16038
  // src/server/sdd-board-ws-handler.ts
15804
- import { listBoards as listBoards3 } from "@wrongstack/kanban";
16039
+ import {
16040
+ enqueueKanbanWorkflowCommand,
16041
+ kanbanWorkflowId,
16042
+ listBoards as listBoards3,
16043
+ listKanbanWorkflowStates
16044
+ } from "@wrongstack/kanban";
15805
16045
  import {
15806
16046
  applySddLifecycle,
15807
16047
  extractVerificationCommand,
@@ -15828,7 +16068,7 @@ var SddBoardWebSocketHandler = class {
15828
16068
  clients = /* @__PURE__ */ new Set();
15829
16069
  lifecycle;
15830
16070
  security;
15831
- diskPollingEnabled;
16071
+ standalonePollingEnabled;
15832
16072
  latest = null;
15833
16073
  poll = null;
15834
16074
  pollInFlight = false;
@@ -15837,7 +16077,7 @@ var SddBoardWebSocketHandler = class {
15837
16077
  this.store = new SddBoardStore({ baseDir: boardsDir });
15838
16078
  this.lifecycle = lifecycle;
15839
16079
  this.security = security;
15840
- this.diskPollingEnabled = events === void 0;
16080
+ this.standalonePollingEnabled = events === void 0;
15841
16081
  if (events) {
15842
16082
  const handler = (e) => {
15843
16083
  this.latest = e.snapshot;
@@ -15872,7 +16112,7 @@ var SddBoardWebSocketHandler = class {
15872
16112
  return;
15873
16113
  }
15874
16114
  if (msg.type === "sdd.board.list") {
15875
- const boards = await this.store.list();
16115
+ const boards = await this.listBoardEntries();
15876
16116
  this.broadcast({ type: "sdd.board.list", payload: { boards } });
15877
16117
  return;
15878
16118
  }
@@ -15885,7 +16125,8 @@ var SddBoardWebSocketHandler = class {
15885
16125
  const verificationCommands = [];
15886
16126
  if (action === "set_task_verification") {
15887
16127
  const command = msg.payload?.verificationCommand;
15888
- if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
16128
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192))
16129
+ return;
15889
16130
  if (typeof command === "string" && command.trim()) {
15890
16131
  verificationCommands.push({ command, operation: "sdd.set_task_verification" });
15891
16132
  }
@@ -15919,18 +16160,26 @@ var SddBoardWebSocketHandler = class {
15919
16160
  );
15920
16161
  if (!authorization.allowed) return;
15921
16162
  }
15922
- 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;
15923
16164
  if (runId) {
15924
- await this.store.appendControl(runId, {
15925
- ts: Date.now(),
15926
- type: action,
15927
- payload: msg.payload
15928
- });
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
+ }
15929
16178
  }
15930
16179
  }
15931
16180
  }
15932
16181
  /**
15933
- * Apply a cleanup/rollback/destroy from disk and broadcast a structured
16182
+ * Apply a cleanup/rollback/destroy from durable state and broadcast a structured
15934
16183
  * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
15935
16184
  * the user must stop it first; the UI gates the buttons on `!active` and the
15936
16185
  * Destroy flow auto-stops then waits before sending `destroy`.
@@ -15939,7 +16188,11 @@ var SddBoardWebSocketHandler = class {
15939
16188
  if (!this.lifecycle) {
15940
16189
  this.broadcast({
15941
16190
  type: "sdd.board.lifecycle_result",
15942
- 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
+ }
15943
16196
  });
15944
16197
  return;
15945
16198
  }
@@ -15955,6 +16208,7 @@ var SddBoardWebSocketHandler = class {
15955
16208
  projectRoot: this.lifecycle.projectRoot,
15956
16209
  paths: this.lifecycle.paths,
15957
16210
  runId,
16211
+ stateTransport: this.lifecycle.stateTransport ?? "kanban",
15958
16212
  revertMerged: payload?.revertMerged === true
15959
16213
  });
15960
16214
  this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
@@ -15983,22 +16237,27 @@ var SddBoardWebSocketHandler = class {
15983
16237
  if (this.pollInFlight) return;
15984
16238
  this.pollInFlight = true;
15985
16239
  try {
15986
- const entry = await this.store.latest();
15987
- if (!entry) return;
15988
- 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) {
15989
16243
  return;
15990
16244
  }
15991
- const snap = await this.store.load(entry.runId);
15992
- if (snap) {
15993
- this.latest = snap;
15994
- this.broadcast({ type: "sdd.board.snapshot", payload: snap });
15995
- }
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
+ );
15996
16255
  } finally {
15997
16256
  this.pollInFlight = false;
15998
16257
  }
15999
16258
  }
16000
16259
  startPolling() {
16001
- if (!this.diskPollingEnabled || this.poll !== null || this.clients.size === 0) return;
16260
+ if (!this.standalonePollingEnabled || this.poll !== null || this.clients.size === 0) return;
16002
16261
  this.poll = setInterval(() => void this.pollLatest(), 1e3);
16003
16262
  this.poll.unref?.();
16004
16263
  }
@@ -16008,17 +16267,44 @@ var SddBoardWebSocketHandler = class {
16008
16267
  this.poll = null;
16009
16268
  }
16010
16269
  async sendCurrent(client) {
16011
- const snap = this.latest ?? await this.loadLatestFromDisk();
16270
+ const snap = this.latest ?? await this.loadLatestSnapshot();
16012
16271
  if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
16013
16272
  }
16014
16273
  async broadcastCurrent() {
16015
- const snap = this.latest ?? await this.loadLatestFromDisk();
16274
+ const snap = this.latest ?? await this.loadLatestSnapshot();
16016
16275
  if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
16017
16276
  }
16018
- 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
+ }
16019
16283
  const entry = await this.store.latest();
16020
16284
  return entry ? this.store.load(entry.runId) : null;
16021
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
+ }
16022
16308
  broadcast(msg) {
16023
16309
  const data = JSON.stringify(msg);
16024
16310
  for (const client of this.clients) {
@@ -16029,17 +16315,20 @@ var SddBoardWebSocketHandler = class {
16029
16315
  sendSerialized(client.ws, JSON.stringify(msg));
16030
16316
  }
16031
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
+ }
16032
16323
 
16033
16324
  // src/server/sdd-wizard-wiring.ts
16034
- import * as path17 from "node:path";
16035
- import {
16036
- DefaultTaskStore,
16037
- TaskTracker
16038
- } from "@wrongstack/core/tasking";
16325
+ import * as path18 from "node:path";
16326
+ import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
16039
16327
  import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
16040
16328
  import { WorktreeManager as WorktreeManager2 } from "@wrongstack/core/worktree";
16041
16329
  import {
16042
16330
  cleanupStaleSddWorktrees,
16331
+ createKanbanSddSessionPersistence,
16043
16332
  decomposeNonAtomicTasks,
16044
16333
  gatherProjectContext,
16045
16334
  makeAcceptanceCriteriaVerifier,
@@ -16057,7 +16346,7 @@ import {
16057
16346
  } from "@wrongstack/sdd";
16058
16347
  async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
16059
16348
  const runTracker = tracker ?? (() => {
16060
- const t = new TaskTracker({ store: new DefaultTaskStore() });
16349
+ const t = new TaskTracker({ store: deps2.taskStore ?? new DefaultTaskStore() });
16061
16350
  t.setGraph(graph);
16062
16351
  return t;
16063
16352
  })();
@@ -16066,7 +16355,8 @@ async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
16066
16355
  if (worktreesEnabled && await isGitWorkTree(deps2.projectRoot)) {
16067
16356
  void cleanupStaleSddWorktrees({
16068
16357
  projectRoot: deps2.projectRoot,
16069
- boardsDir: deps2.projectSddBoards
16358
+ boardsDir: deps2.projectSddBoards,
16359
+ stateTransport: "kanban"
16070
16360
  }).catch(() => void 0);
16071
16361
  worktrees = new WorktreeManager2({
16072
16362
  projectRoot: deps2.projectRoot,
@@ -16145,7 +16435,8 @@ function buildSddWizardDeps(opts) {
16145
16435
  }).catch(() => {
16146
16436
  projectContext = "";
16147
16437
  });
16148
- 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);
16149
16440
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
16150
16441
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
16151
16442
  const runIsolatedTurn = async (prompt, name2) => {
@@ -16166,7 +16457,7 @@ function buildSddWizardDeps(opts) {
16166
16457
  const makeDriver = () => new SddInterviewDriver({
16167
16458
  specStore,
16168
16459
  graphStore,
16169
- sessionPath,
16460
+ sessionPersistence,
16170
16461
  projectContext
16171
16462
  });
16172
16463
  const launchFromGraph = async (graph, config, tracker) => {
@@ -16178,6 +16469,7 @@ function buildSddWizardDeps(opts) {
16178
16469
  projectRoot: opts.projectRoot,
16179
16470
  subagentFactory: opts.subagentFactory,
16180
16471
  projectSddBoards: opts.paths.projectSddBoards,
16472
+ taskStore: graphStore,
16181
16473
  registry,
16182
16474
  runIsolatedTurn,
16183
16475
  ...opts.brain ? { brain: opts.brain } : {}
@@ -16247,8 +16539,18 @@ var SddWizardWebSocketHandler = class {
16247
16539
  lastAgentText = "";
16248
16540
  /** Guards against overlapping interview turns (one in flight at a time). */
16249
16541
  busy = false;
16250
- /** 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. */
16251
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;
16252
16554
  async bootstrap() {
16253
16555
  try {
16254
16556
  await this.deps.ensureReady?.();
@@ -16256,7 +16558,7 @@ var SddWizardWebSocketHandler = class {
16256
16558
  }
16257
16559
  await this.tryResume();
16258
16560
  }
16259
- /** Rehydrate a persisted interview if one exists on disk. */
16561
+ /** Rehydrate a persisted interview if one exists. */
16260
16562
  async tryResume() {
16261
16563
  if (this.driver) return;
16262
16564
  try {
@@ -16265,9 +16567,11 @@ var SddWizardWebSocketHandler = class {
16265
16567
  this.driver = driver;
16266
16568
  this.lastAgentText = driver.getLastAgentText() ?? "";
16267
16569
  }
16268
- } catch {
16570
+ this.resumeError = null;
16571
+ } catch (error2) {
16269
16572
  this.driver = null;
16270
16573
  this.lastAgentText = "";
16574
+ this.resumeError = error2 instanceof Error ? error2.message : String(error2);
16271
16575
  }
16272
16576
  }
16273
16577
  addClient(ws) {
@@ -16276,6 +16580,10 @@ var SddWizardWebSocketHandler = class {
16276
16580
  ws.on("close", () => this.clients.delete(client));
16277
16581
  ws.on("error", () => this.clients.delete(client));
16278
16582
  void this.ready.then(() => {
16583
+ if (this.resumeError) {
16584
+ this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
16585
+ return;
16586
+ }
16279
16587
  if (this.driver) {
16280
16588
  this.send(client, this.snapshotMsg());
16281
16589
  if (this.lastAgentText) {
@@ -16287,6 +16595,22 @@ var SddWizardWebSocketHandler = class {
16287
16595
  async handleMessage(msg) {
16288
16596
  try {
16289
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
+ }
16290
16614
  switch (msg.type) {
16291
16615
  case "sdd.spec.start":
16292
16616
  await this.onStart(String(msg.payload?.goal ?? "").trim(), {
@@ -16303,7 +16627,10 @@ var SddWizardWebSocketHandler = class {
16303
16627
  if (this.driver) {
16304
16628
  this.broadcast(this.snapshotMsg());
16305
16629
  if (this.lastAgentText) {
16306
- 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
+ });
16307
16634
  }
16308
16635
  }
16309
16636
  break;
@@ -16517,10 +16844,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
16517
16844
 
16518
16845
  // src/server/setup-events-fleet-broadcaster.ts
16519
16846
  import { watch as fsWatch } from "node:fs";
16520
- import * as path18 from "node:path";
16847
+ import * as path19 from "node:path";
16521
16848
  function registerSetupEventsFleetBroadcaster(deps2) {
16522
16849
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
16523
- const globalRoot = globalConfigPath ? path18.dirname(globalConfigPath) : void 0;
16850
+ const globalRoot = globalConfigPath ? path19.dirname(globalConfigPath) : void 0;
16524
16851
  if (!globalRoot) return void 0;
16525
16852
  const disposers = [];
16526
16853
  const broadcastSessions = async () => {
@@ -16530,8 +16857,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
16530
16857
  const sessions = await registry.list();
16531
16858
  const ownEntry = sessions.find((s) => s.pid === process.pid);
16532
16859
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
16533
- const myRoot = path18.resolve(context.projectRoot);
16534
- 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) => ({
16535
16862
  sessionId: s.sessionId,
16536
16863
  projectName: s.projectName,
16537
16864
  projectSlug: s.projectSlug,
@@ -16721,14 +17048,14 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
16721
17048
 
16722
17049
  // src/server/setup-events-status-watcher.ts
16723
17050
  import { watch as fsWatch2 } from "node:fs";
16724
- import * as fs16 from "node:fs/promises";
16725
- import * as path20 from "node:path";
17051
+ import * as fs17 from "node:fs/promises";
17052
+ import * as path21 from "node:path";
16726
17053
 
16727
17054
  // src/server/setup-events-watcher.ts
16728
- import * as path19 from "node:path";
17055
+ import * as path20 from "node:path";
16729
17056
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
16730
17057
  const raw = String(filename);
16731
- const relative5 = path19.isAbsolute(raw) ? path19.relative(projectsDir, raw) : raw;
17058
+ const relative5 = path20.isAbsolute(raw) ? path20.relative(projectsDir, raw) : raw;
16732
17059
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
16733
17060
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
16734
17061
  return parts.at(-2) ?? null;
@@ -16763,7 +17090,7 @@ function logFileWatcherMetrics(metrics) {
16763
17090
  function registerSetupEventsStatusWatcher(deps2) {
16764
17091
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
16765
17092
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
16766
- const projectsDir = path20.join(wpaths.globalRoot, "projects");
17093
+ const projectsDir = path21.join(wpaths.globalRoot, "projects");
16767
17094
  const knownProjectHashes = /* @__PURE__ */ new Set();
16768
17095
  const debounceTimers = /* @__PURE__ */ new Map();
16769
17096
  const DEBOUNCE_MS = 150;
@@ -16808,7 +17135,7 @@ function registerSetupEventsStatusWatcher(deps2) {
16808
17135
  let watcher;
16809
17136
  const startWatcher = async () => {
16810
17137
  try {
16811
- await fs16.mkdir(projectsDir, { recursive: true });
17138
+ await fs17.mkdir(projectsDir, { recursive: true });
16812
17139
  if (isDisposed()) return;
16813
17140
  watcher = fsWatch2(
16814
17141
  projectsDir,
@@ -16822,8 +17149,8 @@ function registerSetupEventsStatusWatcher(deps2) {
16822
17149
  if (!knownProjectHashes.has(projectHash)) return;
16823
17150
  if (watcherMetrics) watcherMetrics.filesProcessed++;
16824
17151
  try {
16825
- const targetFile = path20.join(projectsDir, projectHash, "status.json");
16826
- 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");
16827
17154
  const statusData = JSON.parse(content);
16828
17155
  scheduleBroadcast(projectHash, statusData);
16829
17156
  } catch {
@@ -16880,8 +17207,8 @@ function registerSetupEventsStatusWatcher(deps2) {
16880
17207
  }
16881
17208
 
16882
17209
  // src/server/setup-events-core-watchers.ts
16883
- import * as fs17 from "node:fs/promises";
16884
- import * as path21 from "node:path";
17210
+ import * as fs18 from "node:fs/promises";
17211
+ import * as path22 from "node:path";
16885
17212
  function registerSetupEventsCoreWatchers(deps2) {
16886
17213
  const { broadcast: broadcast2, clients, context } = deps2;
16887
17214
  const disposers = [];
@@ -16917,9 +17244,9 @@ function registerSetupEventsClientStatusWriter(deps2) {
16917
17244
  if (wpaths?.projectStatus) {
16918
17245
  try {
16919
17246
  const statusFile = wpaths.projectStatus(e.projectHash);
16920
- const dir = path21.dirname(statusFile);
16921
- await fs17.mkdir(dir, { recursive: true });
16922
- 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");
16923
17250
  } catch (err) {
16924
17251
  console.error(
16925
17252
  JSON.stringify({
@@ -17944,7 +18271,7 @@ var SpecsWebSocketHandler = class {
17944
18271
  // src/server/start-webui.ts
17945
18272
  import { randomUUID as randomUUID5 } from "node:crypto";
17946
18273
  import * as http2 from "node:http";
17947
- import * as path28 from "node:path";
18274
+ import * as path29 from "node:path";
17948
18275
  import { createDefaultPipelines } from "@wrongstack/core/agent";
17949
18276
  import { getSharedProjectMailbox as getSharedProjectMailbox4, resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
17950
18277
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
@@ -17958,7 +18285,7 @@ import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } fro
17958
18285
  import {
17959
18286
  expectDefined as expectDefined3,
17960
18287
  sessionScopedPath as sessionScopedPath3,
17961
- startHeapWatchdog,
18288
+ startSharedHeapWatchdog,
17962
18289
  toErrorMessage as toErrorMessage13,
17963
18290
  wstackGlobalRoot as wstackGlobalRoot2
17964
18291
  } from "@wrongstack/core/utils";
@@ -17967,10 +18294,8 @@ import { toLanguagePackageInput } from "@wrongstack/techstack";
17967
18294
  import { ensureSessionShell } from "@wrongstack/tools";
17968
18295
 
17969
18296
  // src/server/backend-services.ts
17970
- import { join as join12 } from "node:path";
17971
- import {
17972
- Agent
17973
- } from "@wrongstack/core/agent";
18297
+ import { join as join13 } from "node:path";
18298
+ import { Agent } from "@wrongstack/core/agent";
17974
18299
  import {
17975
18300
  BrainDecisionLedger,
17976
18301
  BrainMonitor,
@@ -17982,8 +18307,8 @@ import {
17982
18307
  mailboxSessionTag,
17983
18308
  ObservableBrainArbiter as ObservableBrainArbiterCtor
17984
18309
  } from "@wrongstack/core/coordination";
17985
- import { installDesignStudioMiddleware } from "@wrongstack/core/design";
17986
18310
  import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
18311
+ import { installDesignStudioMiddleware } from "@wrongstack/core/design";
17987
18312
  import {
17988
18313
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
17989
18314
  createBrainRuntime,
@@ -17993,7 +18318,9 @@ import {
17993
18318
  } from "@wrongstack/core/execution";
17994
18319
  import { TOKENS } from "@wrongstack/core/kernel";
17995
18320
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
17996
- import { resolveContextWindowPolicy as resolveContextWindowPolicy2 } from "@wrongstack/core/types";
18321
+ import {
18322
+ resolveContextWindowPolicy as resolveContextWindowPolicy2
18323
+ } from "@wrongstack/core/types";
17997
18324
  import {
17998
18325
  estimateRequestTokensCalibrated,
17999
18326
  toErrorMessage as toErrorMessage9
@@ -18010,7 +18337,7 @@ import {
18010
18337
  import { spawn as spawn2 } from "node:child_process";
18011
18338
  import { createRequire } from "node:module";
18012
18339
  import { existsSync } from "node:fs";
18013
- import { dirname as dirname7, join as join10 } from "node:path";
18340
+ import { dirname as dirname7, join as join11 } from "node:path";
18014
18341
  import { resolveProjectDir as resolveProjectDir2 } from "@wrongstack/core/coordination";
18015
18342
  import { wstackGlobalRoot } from "@wrongstack/core/utils";
18016
18343
  import { readLiveLock } from "@wrongstack/core/coordination";
@@ -18139,7 +18466,7 @@ function mailboxServeInvocation(projectRoot) {
18139
18466
  function findWorkspaceCliEntry(projectRoot) {
18140
18467
  let dir = projectRoot;
18141
18468
  for (let i = 0; i < 6; i++) {
18142
- const candidate = join10(dir, "packages", "cli", "dist", "index.js");
18469
+ const candidate = join11(dir, "packages", "cli", "dist", "index.js");
18143
18470
  if (existsSync(candidate)) return candidate;
18144
18471
  const parent = dirname7(dir);
18145
18472
  if (parent === dir) return null;
@@ -18384,10 +18711,10 @@ function clampDim(value, fallback) {
18384
18711
  }
18385
18712
 
18386
18713
  // src/server/worktree-ws-handler.ts
18387
- 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";
18388
18716
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
18389
18717
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
18390
- import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
18391
18718
  var MAX_ACTIVITY = 6;
18392
18719
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
18393
18720
  var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
@@ -18406,6 +18733,20 @@ var WorktreeWebSocketHandler = class {
18406
18733
  baseBranch = "";
18407
18734
  broadcastInterval = null;
18408
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;
18409
18750
  addClient(ws) {
18410
18751
  this.clients.add(ws);
18411
18752
  ws.on("close", () => this.clients.delete(ws));
@@ -18424,7 +18765,10 @@ var WorktreeWebSocketHandler = class {
18424
18765
  return true;
18425
18766
  }
18426
18767
  if (msg.type === "worktree.remove") {
18427
- await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
18768
+ await this.removeOne(
18769
+ msg.payload?.["dir"],
18770
+ msg.payload?.["branch"]
18771
+ );
18428
18772
  return true;
18429
18773
  }
18430
18774
  if (msg.type === "worktree.merge") {
@@ -18432,7 +18776,10 @@ var WorktreeWebSocketHandler = class {
18432
18776
  return true;
18433
18777
  }
18434
18778
  if (msg.type === "worktree.diff") {
18435
- await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
18779
+ await this.diffOne(
18780
+ msg.payload?.["dir"],
18781
+ msg.payload?.["baseBranch"]
18782
+ );
18436
18783
  return true;
18437
18784
  }
18438
18785
  return false;
@@ -18445,7 +18792,7 @@ var WorktreeWebSocketHandler = class {
18445
18792
  // ── orphan management ─────────────────────────────────────────────────────
18446
18793
  /** Absolute managed-worktrees root for this project. */
18447
18794
  worktreesRoot() {
18448
- return resolve12(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
18795
+ return resolve12(join12(this.management.projectRoot, ".wrongstack", "worktrees"));
18449
18796
  }
18450
18797
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
18451
18798
  underRoot(dir) {
@@ -18461,12 +18808,43 @@ var WorktreeWebSocketHandler = class {
18461
18808
  }
18462
18809
  return live;
18463
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
+ }
18464
18842
  /**
18465
18843
  * Scan the disk for managed worktrees/branches NOT owned by a live in-session
18466
18844
  * run and broadcast them as orphans, with whether it is safe to clean now.
18467
18845
  * No-op (empty inventory) when management deps were not wired.
18468
18846
  */
18469
- async scanAndBroadcast() {
18847
+ async runScanOnce() {
18470
18848
  if (!this.management) {
18471
18849
  this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
18472
18850
  return;
@@ -18522,7 +18900,8 @@ var WorktreeWebSocketHandler = class {
18522
18900
  }
18523
18901
  const res = await cleanupStaleSddWorktrees2({
18524
18902
  projectRoot: this.management.projectRoot,
18525
- boardsDir: this.management.boardsDir
18903
+ boardsDir: this.management.boardsDir,
18904
+ stateTransport: "kanban"
18526
18905
  });
18527
18906
  if (res.skippedReason) {
18528
18907
  this.broadcast({
@@ -18535,26 +18914,45 @@ var WorktreeWebSocketHandler = class {
18535
18914
  for (const [id, h] of [...this.handles]) {
18536
18915
  if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
18537
18916
  }
18538
- 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
+ });
18539
18921
  this.broadcastState();
18540
18922
  await this.scanAndBroadcast();
18541
18923
  }
18542
18924
  /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
18543
18925
  async removeOne(dir, branch) {
18544
18926
  if (!this.management || !dir && !branch) {
18545
- 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
+ });
18546
18931
  return;
18547
18932
  }
18548
18933
  if (branch && !MANAGED_BRANCH_RE.test(branch)) {
18549
- 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
+ });
18550
18938
  return;
18551
18939
  }
18552
18940
  if (dir && !this.underRoot(dir)) {
18553
- 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
+ });
18554
18945
  return;
18555
18946
  }
18556
18947
  if (branch && this.liveActiveBranches().has(branch)) {
18557
- 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
+ });
18558
18956
  return;
18559
18957
  }
18560
18958
  let removed = false;
@@ -18563,31 +18961,54 @@ var WorktreeWebSocketHandler = class {
18563
18961
  ({ removed } = await wt.removeOne(dir, branch));
18564
18962
  }
18565
18963
  for (const [id, h] of [...this.handles]) {
18566
- 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);
18567
18966
  }
18568
- 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
+ });
18569
18975
  this.broadcastState();
18570
18976
  await this.scanAndBroadcast();
18571
18977
  }
18572
18978
  /** Squash-merge ONE branch into base. Refused while a live run owns it. */
18573
18979
  async mergeBranch(branch) {
18574
18980
  if (!this.management || !branch) {
18575
- 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
+ });
18576
18985
  return;
18577
18986
  }
18578
18987
  if (!MANAGED_BRANCH_RE.test(branch)) {
18579
- 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
+ });
18580
18992
  return;
18581
18993
  }
18582
18994
  if (this.liveActiveBranches().has(branch)) {
18583
- 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
+ });
18584
18999
  return;
18585
19000
  }
18586
19001
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
18587
19002
  const res = await wt.mergeBranch(branch);
18588
19003
  this.broadcast({
18589
19004
  type: "worktree.merge_result",
18590
- 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
+ }
18591
19012
  });
18592
19013
  await this.scanAndBroadcast();
18593
19014
  }
@@ -18629,8 +19050,14 @@ var WorktreeWebSocketHandler = class {
18629
19050
  }),
18630
19051
  on("worktree.committed", (p) => {
18631
19052
  const e = p;
18632
- this.patch(e.handleId, { status: "committing", insertions: e.insertions, deletions: e.deletions, files: e.files });
18633
- 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)`);
18634
19061
  this.broadcastState();
18635
19062
  }),
18636
19063
  on("worktree.merged", (p) => {
@@ -18671,10 +19098,15 @@ var WorktreeWebSocketHandler = class {
18671
19098
  activity(id, kind, text) {
18672
19099
  const cur = this.handles.get(id);
18673
19100
  if (cur) {
18674
- 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
+ );
18675
19104
  this.handles.set(id, { ...cur, recentActivity });
18676
19105
  }
18677
- 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
+ });
18678
19110
  }
18679
19111
  stateMessage() {
18680
19112
  return {
@@ -18916,7 +19348,7 @@ async function createAgentServices(input) {
18916
19348
  const brainCfg = resolveBrainConfigDefaults(config.brain, {
18917
19349
  fallbackModels: config.fallbackModels
18918
19350
  });
18919
- const brainLedgerPath = join12(wpaths.projectDir, "brain-ledger.jsonl");
19351
+ const brainLedgerPath = join13(wpaths.projectDir, "brain-ledger.jsonl");
18920
19352
  let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
18921
19353
  let brainLedger;
18922
19354
  const startBrainLedger = () => {
@@ -18981,33 +19413,35 @@ async function createAgentServices(input) {
18981
19413
  brainLog.push(entry);
18982
19414
  if (brainLog.length > 20) brainLog.shift();
18983
19415
  };
18984
- events.on(
18985
- "brain.decision_answered",
18986
- (e) => pushBrainLog({
18987
- at: e.at,
18988
- kind: "answered",
18989
- question: e.request.question,
18990
- outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
18991
- })
18992
- );
18993
- events.on(
18994
- "brain.decision_ask_human",
18995
- (e) => pushBrainLog({
18996
- at: e.at,
18997
- kind: "ask_human",
18998
- question: e.request.question,
18999
- outcome: "needs human judgement"
19000
- })
19001
- );
19002
- events.on(
19003
- "brain.decision_denied",
19004
- (e) => pushBrainLog({
19005
- at: e.at,
19006
- kind: "denied",
19007
- question: e.request.question,
19008
- outcome: e.decision.type === "deny" ? e.decision.reason : ""
19009
- })
19010
- );
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
+ ];
19011
19445
  const brainMailbox = getSharedProjectMailbox2(wpaths.projectDir, events);
19012
19446
  brainMonitor = new BrainMonitor({
19013
19447
  events,
@@ -19119,6 +19553,17 @@ async function createAgentServices(input) {
19119
19553
  getActiveSessionId: () => context.session.id
19120
19554
  }
19121
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
+ };
19122
19567
  return {
19123
19568
  collabBus,
19124
19569
  compactor,
@@ -19144,6 +19589,7 @@ async function createAgentServices(input) {
19144
19589
  worktreeHandler,
19145
19590
  terminalHandler,
19146
19591
  collabHandler,
19592
+ disposeRealtimeHandlers,
19147
19593
  updateAutoCompactionMaxContext
19148
19594
  };
19149
19595
  }
@@ -19230,7 +19676,7 @@ function createConnectionHandler(options) {
19230
19676
  }
19231
19677
 
19232
19678
  // src/server/message-dispatcher.ts
19233
- import path22 from "node:path";
19679
+ import path23 from "node:path";
19234
19680
  function createMessageDispatcher(opts) {
19235
19681
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
19236
19682
  function makeWorklistContext() {
@@ -19251,7 +19697,7 @@ function createMessageDispatcher(opts) {
19251
19697
  skillLoader: deps2.skillLoader,
19252
19698
  skillInstaller: deps2.skillInstaller,
19253
19699
  projectRoot,
19254
- projectSkillsDir: path22.join(projectRoot, ".wrongstack", "skills"),
19700
+ projectSkillsDir: path23.join(projectRoot, ".wrongstack", "skills"),
19255
19701
  globalSkillsDir: deps2.wpaths.globalSkills
19256
19702
  };
19257
19703
  }
@@ -19503,7 +19949,7 @@ function createMessageDispatcher(opts) {
19503
19949
 
19504
19950
  // src/server/pre-context-services.ts
19505
19951
  import { createRequire as createRequire3 } from "node:module";
19506
- import * as path25 from "node:path";
19952
+ import * as path26 from "node:path";
19507
19953
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
19508
19954
  import {
19509
19955
  getSharedProjectMailbox as getSharedProjectMailbox3,
@@ -19557,8 +20003,8 @@ import { configureDangerBypass, configureExecPolicy } from "@wrongstack/tools";
19557
20003
  import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/tools/session-kanban";
19558
20004
 
19559
20005
  // src/server/model-auto-discovery.ts
19560
- import * as fs18 from "node:fs/promises";
19561
- import * as path23 from "node:path";
20006
+ import * as fs19 from "node:fs/promises";
20007
+ import * as path24 from "node:path";
19562
20008
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
19563
20009
  function isOverlayRegistry(value) {
19564
20010
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -19584,7 +20030,7 @@ function eligibleProviders(config) {
19584
20030
  }
19585
20031
  async function readCache(file) {
19586
20032
  try {
19587
- return JSON.parse(await fs18.readFile(file, "utf8"));
20033
+ return JSON.parse(await fs19.readFile(file, "utf8"));
19588
20034
  } catch {
19589
20035
  return {};
19590
20036
  }
@@ -19594,7 +20040,7 @@ async function discoverAndMergeWebuiProviders(opts) {
19594
20040
  if (!isOverlayRegistry(registry)) return;
19595
20041
  const targets = eligibleProviders(opts.config);
19596
20042
  if (targets.length === 0) return;
19597
- const cacheFile = path23.join(opts.cacheDir, "discovered-models-cache.json");
20043
+ const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
19598
20044
  const cache2 = await readCache(cacheFile);
19599
20045
  let cacheDirty = false;
19600
20046
  await Promise.all(
@@ -19631,8 +20077,8 @@ async function discoverAndMergeWebuiProviders(opts) {
19631
20077
  );
19632
20078
  if (cacheDirty) {
19633
20079
  try {
19634
- await fs18.mkdir(path23.dirname(cacheFile), { recursive: true });
19635
- 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");
19636
20082
  } catch {
19637
20083
  opts.logger?.debug?.("provider auto-discovery cache write failed");
19638
20084
  }
@@ -19728,7 +20174,7 @@ function resolveSetupProvider(opts) {
19728
20174
  }
19729
20175
 
19730
20176
  // src/server/standalone-session-identity.ts
19731
- import * as path24 from "node:path";
20177
+ import * as path25 from "node:path";
19732
20178
  import {
19733
20179
  AgentStatusTracker,
19734
20180
  FleetNotifier,
@@ -19747,7 +20193,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
19747
20193
  let activeTarget = {
19748
20194
  projectSlug: paths.projectSlug,
19749
20195
  projectRoot: paths.projectRoot,
19750
- projectName: path24.basename(paths.projectRoot),
20196
+ projectName: path25.basename(paths.projectRoot),
19751
20197
  workingDir: opts.workingDir
19752
20198
  };
19753
20199
  let pendingClaim;
@@ -19978,7 +20424,7 @@ async function createPreContextServices(input) {
19978
20424
  await discoverAndMergeWebuiProviders({
19979
20425
  config,
19980
20426
  registry: modelsRegistry,
19981
- cacheDir: path25.dirname(wpaths.modelsCache),
20427
+ cacheDir: path26.dirname(wpaths.modelsCache),
19982
20428
  logger
19983
20429
  });
19984
20430
  } catch (err) {
@@ -20030,7 +20476,7 @@ async function createPreContextServices(input) {
20030
20476
  configureChildEnvGitIdentity(config.git?.identity ?? null);
20031
20477
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
20032
20478
  const mcpTokenStore = new MCPVaultTokenStore(
20033
- path25.join(wpaths.projectDir, "mcp-auth.json"),
20479
+ path26.join(wpaths.projectDir, "mcp-auth.json"),
20034
20480
  vault
20035
20481
  );
20036
20482
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -20136,7 +20582,7 @@ async function createPreContextServices(input) {
20136
20582
  };
20137
20583
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
20138
20584
  const skillInstaller = config.features.skills ? new SkillInstaller({
20139
- manifestPath: path25.join(wpaths.configDir, "installed-skills.json"),
20585
+ manifestPath: path26.join(wpaths.configDir, "installed-skills.json"),
20140
20586
  projectSkillsDir: wpaths.inProjectSkills,
20141
20587
  globalSkillsDir: wpaths.globalSkills,
20142
20588
  projectHash: wpaths.projectHash,
@@ -20146,8 +20592,8 @@ async function createPreContextServices(input) {
20146
20592
  const bundledPromptsDir = promptsEnabled ? (() => {
20147
20593
  try {
20148
20594
  const req = createRequire3(import.meta.url);
20149
- return path25.join(
20150
- path25.dirname(req.resolve("@wrongstack/core/package.json")),
20595
+ return path26.join(
20596
+ path26.dirname(req.resolve("@wrongstack/core/package.json")),
20151
20597
  "data",
20152
20598
  "prompts"
20153
20599
  );
@@ -20251,7 +20697,7 @@ async function createPreContextServices(input) {
20251
20697
  }
20252
20698
 
20253
20699
  // src/server/routes.ts
20254
- import path26 from "node:path";
20700
+ import path27 from "node:path";
20255
20701
  import { makeProviderFromConfig as makeProviderFromConfig2, withCatalogCapabilities } from "@wrongstack/providers";
20256
20702
 
20257
20703
  // src/server/mode-handlers.ts
@@ -20543,7 +20989,7 @@ function buildRoutes(state, deps2, cb) {
20543
20989
  };
20544
20990
  const mailboxRoutes = createMailboxRouteHandlers({
20545
20991
  getProjectRoot: state.getProjectRoot,
20546
- getGlobalRoot: () => path26.dirname(deps2.globalConfigPath),
20992
+ getGlobalRoot: () => path27.dirname(deps2.globalConfigPath),
20547
20993
  events: deps2.events
20548
20994
  });
20549
20995
  const mcpRoutes = {
@@ -20616,7 +21062,7 @@ function buildRoutes(state, deps2, cb) {
20616
21062
  }
20617
21063
 
20618
21064
  // src/server/server-runtime.ts
20619
- import * as path27 from "node:path";
21065
+ import * as path28 from "node:path";
20620
21066
  import { createRequire as createRequire4 } from "node:module";
20621
21067
  import { fileURLToPath } from "node:url";
20622
21068
  import { WebSocketServer } from "ws";
@@ -20677,7 +21123,7 @@ function createSessionStartPayload(g) {
20677
21123
  inputCost,
20678
21124
  outputCost,
20679
21125
  cacheReadCost,
20680
- projectName: path27.basename(projectRoot) || projectRoot,
21126
+ projectName: path28.basename(projectRoot) || projectRoot,
20681
21127
  projectRoot,
20682
21128
  cwd: g.getWorkingDir(),
20683
21129
  mode: g.getModeId(),
@@ -20765,13 +21211,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
20765
21211
  };
20766
21212
  }
20767
21213
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
20768
- if (explicitDistDir) return path27.resolve(explicitDistDir);
21214
+ if (explicitDistDir) return path28.resolve(explicitDistDir);
20769
21215
  try {
20770
21216
  const requireFromHere2 = createRequire4(fromUrl);
20771
21217
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
20772
- return path27.dirname(serverEntry);
21218
+ return path28.dirname(serverEntry);
20773
21219
  } catch {
20774
- return path27.resolve(path27.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
21220
+ return path28.resolve(path28.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
20775
21221
  }
20776
21222
  }
20777
21223
  function startHttpServer(opts) {
@@ -20985,6 +21431,7 @@ async function startWebUI(opts = {}) {
20985
21431
  worktreeHandler,
20986
21432
  terminalHandler,
20987
21433
  collabHandler,
21434
+ disposeRealtimeHandlers,
20988
21435
  updateAutoCompactionMaxContext
20989
21436
  } = agentServices;
20990
21437
  if (typeof context.meta["yolo"] === "boolean") {
@@ -21163,21 +21610,21 @@ async function startWebUI(opts = {}) {
21163
21610
  });
21164
21611
  }
21165
21612
  async function touchProjectEntry(root, workDir) {
21166
- const resolved = path28.resolve(root);
21613
+ const resolved = path29.resolve(root);
21167
21614
  const manifest = await loadManifest(globalConfigPath);
21168
21615
  const now = (/* @__PURE__ */ new Date()).toISOString();
21169
- const existing = manifest.projects.find((p) => path28.resolve(p.root) === resolved);
21616
+ const existing = manifest.projects.find((p) => path29.resolve(p.root) === resolved);
21170
21617
  if (existing) {
21171
21618
  existing.lastSeen = now;
21172
- if (workDir) existing.lastWorkingDir = path28.resolve(workDir);
21619
+ if (workDir) existing.lastWorkingDir = path29.resolve(workDir);
21173
21620
  } else {
21174
21621
  manifest.projects.push({
21175
- name: path28.basename(resolved),
21622
+ name: path29.basename(resolved),
21176
21623
  root: resolved,
21177
21624
  slug: generateProjectSlug(resolved),
21178
21625
  createdAt: now,
21179
21626
  lastSeen: now,
21180
- lastWorkingDir: workDir ? path28.resolve(workDir) : void 0
21627
+ lastWorkingDir: workDir ? path29.resolve(workDir) : void 0
21181
21628
  });
21182
21629
  }
21183
21630
  await saveManifest(manifest, globalConfigPath);
@@ -21354,7 +21801,20 @@ async function startWebUI(opts = {}) {
21354
21801
  );
21355
21802
  credentialWatcherClose = credentialWatcher.close;
21356
21803
  }
21357
- 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
+ });
21358
21818
  const routes = buildRoutes(state, deps2, cb);
21359
21819
  const handleMessage = createMessageDispatcher({
21360
21820
  state,
@@ -21432,6 +21892,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21432
21892
  await todosCheckpoint.detach();
21433
21893
  await stopHeapWatchdog();
21434
21894
  credentialWatcherClose?.();
21895
+ disposeRealtimeHandlers();
21435
21896
  brainMonitor.stop();
21436
21897
  await agentServices.brainLedger?.stop().catch(() => {
21437
21898
  });
@@ -21455,7 +21916,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21455
21916
  await memoryStore.dispose().catch(
21456
21917
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
21457
21918
  );
21458
- await unregisterInstance(process.pid, path28.dirname(globalConfigPath));
21919
+ await unregisterInstance(process.pid, path29.dirname(globalConfigPath));
21459
21920
  }
21460
21921
  });
21461
21922
  }
@@ -21548,7 +22009,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
21548
22009
  console.error(err instanceof Error ? err.message : String(err));
21549
22010
  process.exit(1);
21550
22011
  }
21551
- 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";
21552
22013
  const requireToken = argv.includes("--require-token") || envFlag2("WEBUI_REQUIRE_TOKEN");
21553
22014
  console.log(`[WebUI] Starting standalone server on ${wsHost} (http:${httpPort})...`);
21554
22015
  startWebUI({
@@ -21558,7 +22019,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
21558
22019
  publicUrl,
21559
22020
  publicWsUrl,
21560
22021
  requireToken,
21561
- open,
22022
+ open: open2,
21562
22023
  distDir
21563
22024
  }).catch((err) => {
21564
22025
  console.error(JSON.stringify({