@wrongstack/webui-server 0.296.3 → 0.297.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.js +948 -429
  2. package/dist/index.js.map +4 -4
  3. package/dist/protocol/client-conversation.d.ts +1 -1
  4. package/dist/protocol/client-conversation.d.ts.map +1 -1
  5. package/dist/protocol/index.js +7 -1
  6. package/dist/protocol/index.js.map +2 -2
  7. package/dist/protocol/projections.d.ts +5 -1
  8. package/dist/protocol/projections.d.ts.map +1 -1
  9. package/dist/protocol/registry.d.ts +2 -2
  10. package/dist/protocol/registry.d.ts.map +1 -1
  11. package/dist/protocol/server-conversation.d.ts +1 -1
  12. package/dist/protocol/server-conversation.d.ts.map +1 -1
  13. package/dist/server/backend-services.d.ts +6 -2
  14. package/dist/server/backend-services.d.ts.map +1 -1
  15. package/dist/server/collaboration-ws-handler.d.ts +2 -2
  16. package/dist/server/collaboration-ws-handler.d.ts.map +1 -1
  17. package/dist/server/connections-health-route.d.ts.map +1 -1
  18. package/dist/server/embedded-lifecycle.d.ts +2 -0
  19. package/dist/server/embedded-lifecycle.d.ts.map +1 -1
  20. package/dist/server/entry.js +870 -395
  21. package/dist/server/entry.js.map +4 -4
  22. package/dist/server/goal-ws-handler.d.ts +3 -1
  23. package/dist/server/goal-ws-handler.d.ts.map +1 -1
  24. package/dist/server/handlers.js.map +2 -2
  25. package/dist/server/http-server.d.ts.map +1 -1
  26. package/dist/server/kanban-supervisor.d.ts +6 -0
  27. package/dist/server/kanban-supervisor.d.ts.map +1 -1
  28. package/dist/server/mailbox-handlers.d.ts +2 -1
  29. package/dist/server/mailbox-handlers.d.ts.map +1 -1
  30. package/dist/server/mailbox-routes.d.ts +1 -0
  31. package/dist/server/mailbox-routes.d.ts.map +1 -1
  32. package/dist/server/memory-diagnostics.d.ts +69 -0
  33. package/dist/server/memory-diagnostics.d.ts.map +1 -0
  34. package/dist/server/sdd-board-ws-handler.d.ts +14 -8
  35. package/dist/server/sdd-board-ws-handler.d.ts.map +1 -1
  36. package/dist/server/sdd-wizard-wiring.d.ts +5 -6
  37. package/dist/server/sdd-wizard-wiring.d.ts.map +1 -1
  38. package/dist/server/sdd-wizard-ws-handler.d.ts +16 -6
  39. package/dist/server/sdd-wizard-ws-handler.d.ts.map +1 -1
  40. package/dist/server/start-webui.d.ts.map +1 -1
  41. package/dist/server/types.d.ts +2 -0
  42. package/dist/server/types.d.ts.map +1 -1
  43. package/dist/server/worktree-ws-handler.d.ts +30 -2
  44. package/dist/server/worktree-ws-handler.d.ts.map +1 -1
  45. package/dist/server/ws-payload-validation.d.ts +8 -0
  46. package/dist/server/ws-payload-validation.d.ts.map +1 -1
  47. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -134,85 +134,85 @@ var ENUM_PREF_KEYS = {
134
134
  autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
135
135
  fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"])
136
136
  };
137
- function validateModelRuntimeValue(modelRuntime, path34) {
137
+ function validateModelRuntimeValue(modelRuntime, path35) {
138
138
  const reasoning = modelRuntime["reasoning"];
139
139
  if (reasoning !== void 0) {
140
- if (!isRecord(reasoning)) return `${path34}.reasoning must be an object when provided`;
140
+ if (!isRecord(reasoning)) return `${path35}.reasoning must be an object when provided`;
141
141
  const mode = reasoning["mode"];
142
142
  const effort = reasoning["effort"];
143
143
  const preserve = reasoning["preserve"];
144
144
  if (mode !== void 0 && (typeof mode !== "string" || !REASONING_MODE_VALUES.has(mode))) {
145
- return `${path34}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
145
+ return `${path35}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
146
146
  }
147
147
  if (effort !== void 0 && (typeof effort !== "string" || !REASONING_EFFORT_VALUES.has(effort))) {
148
- return `${path34}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
148
+ return `${path35}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
149
149
  }
150
150
  if (preserve !== void 0 && typeof preserve !== "boolean") {
151
- return `${path34}.reasoning.preserve must be a boolean when provided`;
151
+ return `${path35}.reasoning.preserve must be a boolean when provided`;
152
152
  }
153
153
  }
154
154
  const cache2 = modelRuntime["cache"];
155
155
  if (cache2 !== void 0) {
156
- if (!isRecord(cache2)) return `${path34}.cache must be an object when provided`;
156
+ if (!isRecord(cache2)) return `${path35}.cache must be an object when provided`;
157
157
  const ttl = cache2["ttl"];
158
158
  if (ttl !== void 0 && (typeof ttl !== "string" || !CACHE_TTL_VALUES.has(ttl) || ttl === "default")) {
159
- return `${path34}.cache.ttl must be one of: 5m, 1h`;
159
+ return `${path35}.cache.ttl must be one of: 5m, 1h`;
160
160
  }
161
161
  }
162
162
  const parameters = modelRuntime["parameters"];
163
163
  if (parameters !== void 0 && !isRecord(parameters)) {
164
- return `${path34}.parameters must be an object when provided`;
164
+ return `${path35}.parameters must be an object when provided`;
165
165
  }
166
166
  return null;
167
167
  }
168
- function validateModelBlackoutRule(rule, path34) {
168
+ function validateModelBlackoutRule(rule, path35) {
169
169
  const id = rule["id"];
170
170
  if (typeof id !== "string" || id.trim().length === 0) {
171
- return `${path34}.id must be a non-empty string`;
171
+ return `${path35}.id must be a non-empty string`;
172
172
  }
173
173
  const start = rule["start"];
174
174
  if (typeof start !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(start)) {
175
- return `${path34}.start must be a string in HH:mm (00:00-23:59) format`;
175
+ return `${path35}.start must be a string in HH:mm (00:00-23:59) format`;
176
176
  }
177
177
  const end = rule["end"];
178
178
  if (typeof end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(end)) {
179
- return `${path34}.end must be a string in HH:mm (00:00-23:59) format`;
179
+ return `${path35}.end must be a string in HH:mm (00:00-23:59) format`;
180
180
  }
181
181
  if (rule["enabled"] !== void 0 && typeof rule["enabled"] !== "boolean") {
182
- return `${path34}.enabled must be a boolean when provided`;
182
+ return `${path35}.enabled must be a boolean when provided`;
183
183
  }
184
184
  if (rule["provider"] !== void 0 && typeof rule["provider"] !== "string") {
185
- return `${path34}.provider must be a string when provided`;
185
+ return `${path35}.provider must be a string when provided`;
186
186
  }
187
187
  if (rule["model"] !== void 0 && typeof rule["model"] !== "string") {
188
- return `${path34}.model must be a string when provided`;
188
+ return `${path35}.model must be a string when provided`;
189
189
  }
190
190
  if (rule["days"] !== void 0) {
191
- if (!Array.isArray(rule["days"])) return `${path34}.days must be an array when provided`;
191
+ if (!Array.isArray(rule["days"])) return `${path35}.days must be an array when provided`;
192
192
  const seen = /* @__PURE__ */ new Set();
193
193
  for (const d of rule["days"]) {
194
194
  if (typeof d !== "number" || !Number.isInteger(d) || d < 0 || d > 6) {
195
- return `${path34}.days elements must be integers 0-6 when provided`;
195
+ return `${path35}.days elements must be integers 0-6 when provided`;
196
196
  }
197
- if (seen.has(d)) return `${path34}.days contains duplicate day: ${d}`;
197
+ if (seen.has(d)) return `${path35}.days contains duplicate day: ${d}`;
198
198
  seen.add(d);
199
199
  }
200
200
  }
201
201
  if (rule["timezone"] !== void 0) {
202
202
  if (typeof rule["timezone"] !== "string") {
203
- return `${path34}.timezone must be a string when provided`;
203
+ return `${path35}.timezone must be a string when provided`;
204
204
  }
205
205
  try {
206
206
  Intl.DateTimeFormat(void 0, { timeZone: rule["timezone"] });
207
207
  } catch {
208
- return `${path34}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
208
+ return `${path35}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
209
209
  }
210
210
  }
211
211
  if (rule["label"] !== void 0 && typeof rule["label"] !== "string") {
212
- return `${path34}.label must be a string when provided`;
212
+ return `${path35}.label must be a string when provided`;
213
213
  }
214
214
  if (rule["mode"] !== void 0 && rule["mode"] !== "blackout" && rule["mode"] !== "allow_only") {
215
- return `${path34}.mode must be 'blackout' or 'allow_only' when provided`;
215
+ return `${path35}.mode must be 'blackout' or 'allow_only' when provided`;
216
216
  }
217
217
  return null;
218
218
  }
@@ -385,6 +385,37 @@ function validateMailboxMessagesPayload(payload) {
385
385
  }
386
386
  };
387
387
  }
388
+ var MAILBOX_ACTIONS = /* @__PURE__ */ new Set(["mark-read", "acknowledge", "reopen", "soft-delete"]);
389
+ function validateMailboxActionPayload(payload) {
390
+ if (!isRecord2(payload)) {
391
+ return { ok: false, message: "mailbox.action payload must be an object" };
392
+ }
393
+ const requestId = payload["requestId"];
394
+ const mailId = payload["mailId"];
395
+ const action = payload["action"];
396
+ const readerId = payload["readerId"];
397
+ if (typeof requestId !== "string" || requestId.trim().length === 0) {
398
+ return { ok: false, message: "mailbox.action payload.requestId must be a non-empty string" };
399
+ }
400
+ if (typeof mailId !== "string" || mailId.trim().length === 0) {
401
+ return { ok: false, message: "mailbox.action payload.mailId must be a non-empty string" };
402
+ }
403
+ if (typeof action !== "string" || !MAILBOX_ACTIONS.has(action)) {
404
+ return { ok: false, message: "mailbox.action payload.action must be a supported action" };
405
+ }
406
+ if (typeof readerId !== "string" || readerId.trim().length === 0) {
407
+ return { ok: false, message: "mailbox.action payload.readerId must be a non-empty string" };
408
+ }
409
+ return {
410
+ ok: true,
411
+ value: {
412
+ requestId: requestId.trim(),
413
+ mailId: mailId.trim(),
414
+ action,
415
+ readerId: readerId.trim()
416
+ }
417
+ };
418
+ }
388
419
  var MAILBOX_SEND_TYPES = /* @__PURE__ */ new Set([
389
420
  "note",
390
421
  "ask",
@@ -401,6 +432,7 @@ function validateMailboxSendPayload(payload) {
401
432
  return { ok: false, message: "mailbox.send payload must be an object" };
402
433
  }
403
434
  const requestId = payload["requestId"];
435
+ const rawFrom = payload["from"];
404
436
  const rawTo = payload["to"];
405
437
  const rawType = payload["type"];
406
438
  const rawAudience = payload["audience"];
@@ -411,6 +443,9 @@ function validateMailboxSendPayload(payload) {
411
443
  if (typeof requestId !== "string" || requestId.trim().length === 0) {
412
444
  return { ok: false, message: "mailbox.send payload.requestId must be a non-empty string" };
413
445
  }
446
+ if (rawFrom !== void 0 && (typeof rawFrom !== "string" || rawFrom.trim().length === 0)) {
447
+ return { ok: false, message: "mailbox.send payload.from must be a non-empty string when provided" };
448
+ }
414
449
  if (typeof rawTo !== "string" || rawTo.trim().length === 0) {
415
450
  return { ok: false, message: "mailbox.send payload.to must be a non-empty string" };
416
451
  }
@@ -447,6 +482,7 @@ function validateMailboxSendPayload(payload) {
447
482
  ok: true,
448
483
  value: {
449
484
  requestId: requestId.trim(),
485
+ ...rawFrom !== void 0 ? { from: rawFrom.trim() } : {},
450
486
  to,
451
487
  type,
452
488
  audience: rawAudience,
@@ -760,8 +796,8 @@ function validateShellOpenPayload(payload) {
760
796
  if (!isRecord2(payload)) {
761
797
  return { ok: false, message: "shell.open payload must be an object with string path" };
762
798
  }
763
- const path34 = payload["path"];
764
- if (typeof path34 !== "string" || path34.trim().length === 0) {
799
+ const path35 = payload["path"];
800
+ if (typeof path35 !== "string" || path35.trim().length === 0) {
765
801
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
766
802
  }
767
803
  const target = payload["target"];
@@ -774,7 +810,7 @@ function validateShellOpenPayload(payload) {
774
810
  return {
775
811
  ok: true,
776
812
  value: {
777
- path: path34,
813
+ path: path35,
778
814
  ...target !== void 0 ? { target } : {}
779
815
  }
780
816
  };
@@ -783,14 +819,14 @@ function validateGitDiffPayload(payload) {
783
819
  if (!isRecord2(payload)) {
784
820
  return { ok: false, message: "git.diff payload must be an object" };
785
821
  }
786
- const path34 = payload["path"];
787
- if (path34 === void 0 || path34 === null) {
822
+ const path35 = payload["path"];
823
+ if (path35 === void 0 || path35 === null) {
788
824
  return { ok: true, value: { path: "" } };
789
825
  }
790
- if (typeof path34 !== "string") {
826
+ if (typeof path35 !== "string") {
791
827
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
792
828
  }
793
- return { ok: true, value: { path: path34 } };
829
+ return { ok: true, value: { path: path35 } };
794
830
  }
795
831
  function validateProjectsAddPayload(payload) {
796
832
  if (!isRecord2(payload)) {
@@ -1575,6 +1611,7 @@ var CollaborationWebSocketHandler = class {
1575
1611
  for (const off of this.offs) off();
1576
1612
  this.offs.length = 0;
1577
1613
  this.stopBroadcast();
1614
+ this.clients.clear();
1578
1615
  }
1579
1616
  // ── Inbound client messages ────────────────────────────────────────────
1580
1617
  /**
@@ -1634,9 +1671,7 @@ var CollaborationWebSocketHandler = class {
1634
1671
  if (activeSessionId !== void 0 && sessionId !== activeSessionId) {
1635
1672
  this.send(
1636
1673
  ws,
1637
- this.errorMessage(
1638
- `collab.join sessionId mismatch (active: ${activeSessionId})`
1639
- )
1674
+ this.errorMessage(`collab.join sessionId mismatch (active: ${activeSessionId})`)
1640
1675
  );
1641
1676
  return;
1642
1677
  }
@@ -1647,26 +1682,19 @@ var CollaborationWebSocketHandler = class {
1647
1682
  if (role === "controller" && !this.bus) {
1648
1683
  this.send(
1649
1684
  ws,
1650
- this.errorMessage(
1651
- `role 'controller' is not available: server has no CollaborationBus`
1652
- )
1685
+ this.errorMessage(`role 'controller' is not available: server has no CollaborationBus`)
1653
1686
  );
1654
1687
  return;
1655
1688
  }
1656
1689
  if (role === "annotator" && !this.annotations) {
1657
1690
  this.send(
1658
1691
  ws,
1659
- this.errorMessage(
1660
- `role 'annotator' is not available: server has no annotations store`
1661
- )
1692
+ this.errorMessage(`role 'annotator' is not available: server has no annotations store`)
1662
1693
  );
1663
1694
  return;
1664
1695
  }
1665
1696
  if (role !== "observer" && this.options.authorizeRole?.({ ws, sessionId, requestedRole: role }) !== true) {
1666
- this.send(
1667
- ws,
1668
- this.errorMessage(`role '${role}' requires explicit server authorization`)
1669
- );
1697
+ this.send(ws, this.errorMessage(`role '${role}' requires explicit server authorization`));
1670
1698
  return;
1671
1699
  }
1672
1700
  const participant = {
@@ -1695,14 +1723,10 @@ var CollaborationWebSocketHandler = class {
1695
1723
  this.broadcast(sessionId, this.stateMessage(sessionId));
1696
1724
  if (this.reader) {
1697
1725
  this.replayHistory(ws, sessionId).catch((err) => {
1698
- this.logger.debug?.(
1699
- `collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`
1700
- );
1726
+ this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`);
1701
1727
  });
1702
1728
  }
1703
- this.logger.debug?.(
1704
- `collab: participant ${participant.participantId} joined ${sessionId}`
1705
- );
1729
+ this.logger.debug?.(`collab: participant ${participant.participantId} joined ${sessionId}`);
1706
1730
  }
1707
1731
  leave(ws) {
1708
1732
  this.handleDisconnect(ws);
@@ -1773,18 +1797,13 @@ var CollaborationWebSocketHandler = class {
1773
1797
  }
1774
1798
  const payload = raw;
1775
1799
  if (!payload?.sessionId || typeof payload.atEventIndex !== "number" || typeof payload.text !== "string") {
1776
- this.send(
1777
- ws,
1778
- this.errorMessage("annotate requires { sessionId, atEventIndex, text }")
1779
- );
1800
+ this.send(ws, this.errorMessage("annotate requires { sessionId, atEventIndex, text }"));
1780
1801
  return;
1781
1802
  }
1782
1803
  if (payload.sessionId !== participant.sessionId) {
1783
1804
  this.send(
1784
1805
  ws,
1785
- this.errorMessage(
1786
- `annotate sessionId mismatch (joined: ${participant.sessionId})`
1787
- )
1806
+ this.errorMessage(`annotate sessionId mismatch (joined: ${participant.sessionId})`)
1788
1807
  );
1789
1808
  return;
1790
1809
  }
@@ -1811,12 +1830,7 @@ var CollaborationWebSocketHandler = class {
1811
1830
  }
1812
1831
  });
1813
1832
  } catch (err) {
1814
- this.send(
1815
- ws,
1816
- this.errorMessage(
1817
- `annotation rejected: ${toErrorMessage(err)}`
1818
- )
1819
- );
1833
+ this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage(err)}`));
1820
1834
  }
1821
1835
  }
1822
1836
  async handleResolve(ws, raw) {
@@ -1832,26 +1846,19 @@ var CollaborationWebSocketHandler = class {
1832
1846
  if (participant.role !== "annotator") {
1833
1847
  this.send(
1834
1848
  ws,
1835
- this.errorMessage(
1836
- `resolve requires the 'annotator' role (current: '${participant.role}')`
1837
- )
1849
+ this.errorMessage(`resolve requires the 'annotator' role (current: '${participant.role}')`)
1838
1850
  );
1839
1851
  return;
1840
1852
  }
1841
1853
  const payload = raw;
1842
1854
  if (!payload?.sessionId || !payload.annotationId) {
1843
- this.send(
1844
- ws,
1845
- this.errorMessage("resolve requires { sessionId, annotationId }")
1846
- );
1855
+ this.send(ws, this.errorMessage("resolve requires { sessionId, annotationId }"));
1847
1856
  return;
1848
1857
  }
1849
1858
  if (payload.sessionId !== participant.sessionId) {
1850
1859
  this.send(
1851
1860
  ws,
1852
- this.errorMessage(
1853
- `resolve sessionId mismatch (joined: ${participant.sessionId})`
1854
- )
1861
+ this.errorMessage(`resolve sessionId mismatch (joined: ${participant.sessionId})`)
1855
1862
  );
1856
1863
  return;
1857
1864
  }
@@ -1862,10 +1869,7 @@ var CollaborationWebSocketHandler = class {
1862
1869
  resolvedBy: participant.participantId
1863
1870
  });
1864
1871
  if (!updated) {
1865
- this.send(
1866
- ws,
1867
- this.errorMessage(`annotation not found: ${payload.annotationId}`)
1868
- );
1872
+ this.send(ws, this.errorMessage(`annotation not found: ${payload.annotationId}`));
1869
1873
  return;
1870
1874
  }
1871
1875
  this.broadcast(payload.sessionId, {
@@ -1878,12 +1882,7 @@ var CollaborationWebSocketHandler = class {
1878
1882
  }
1879
1883
  });
1880
1884
  } catch (err) {
1881
- this.send(
1882
- ws,
1883
- this.errorMessage(
1884
- `resolve failed: ${toErrorMessage(err)}`
1885
- )
1886
- );
1885
+ this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage(err)}`));
1887
1886
  }
1888
1887
  }
1889
1888
  // ── Event subscription (live mirror) ───────────────────────────────────
@@ -1954,9 +1953,7 @@ var CollaborationWebSocketHandler = class {
1954
1953
  seen++;
1955
1954
  }
1956
1955
  } catch (err) {
1957
- this.logger.debug?.(
1958
- `collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`
1959
- );
1956
+ this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`);
1960
1957
  return;
1961
1958
  }
1962
1959
  const tail2 = seen <= REPLAY_LIMIT ? ring.slice(0, seen) : (
@@ -2038,9 +2035,7 @@ var CollaborationWebSocketHandler = class {
2038
2035
  try {
2039
2036
  sendSerialized(p.ws, data);
2040
2037
  } catch (err) {
2041
- this.logger.debug?.(
2042
- `collab broadcast failed: ${toErrorMessage(err)}`
2043
- );
2038
+ this.logger.debug?.(`collab broadcast failed: ${toErrorMessage(err)}`);
2044
2039
  }
2045
2040
  }
2046
2041
  }
@@ -2067,9 +2062,7 @@ var CollaborationWebSocketHandler = class {
2067
2062
  if (participant.role !== "controller") {
2068
2063
  this.send(
2069
2064
  ws,
2070
- this.errorMessage(
2071
- `pause requires the 'controller' role (current: '${participant.role}')`
2072
- )
2065
+ this.errorMessage(`pause requires the 'controller' role (current: '${participant.role}')`)
2073
2066
  );
2074
2067
  return;
2075
2068
  }
@@ -2114,9 +2107,7 @@ var CollaborationWebSocketHandler = class {
2114
2107
  if (participant.role !== "controller") {
2115
2108
  this.send(
2116
2109
  ws,
2117
- this.errorMessage(
2118
- `resume requires the 'controller' role (current: '${participant.role}')`
2119
- )
2110
+ this.errorMessage(`resume requires the 'controller' role (current: '${participant.role}')`)
2120
2111
  );
2121
2112
  return;
2122
2113
  }
@@ -2214,9 +2205,7 @@ var CollaborationWebSocketHandler = class {
2214
2205
  if (payload.sessionId !== participant.sessionId) {
2215
2206
  this.send(
2216
2207
  ws,
2217
- this.errorMessage(
2218
- `inject_tool sessionId mismatch (joined: ${participant.sessionId})`
2219
- )
2208
+ this.errorMessage(`inject_tool sessionId mismatch (joined: ${participant.sessionId})`)
2220
2209
  );
2221
2210
  return;
2222
2211
  }
@@ -2230,9 +2219,7 @@ var CollaborationWebSocketHandler = class {
2230
2219
  if (!queued) {
2231
2220
  this.send(
2232
2221
  ws,
2233
- this.errorMessage(
2234
- `an injection for toolUseId ${payload.toolUseId} is already queued`
2235
- )
2222
+ this.errorMessage(`an injection for toolUseId ${payload.toolUseId} is already queued`)
2236
2223
  );
2237
2224
  return;
2238
2225
  }
@@ -4376,8 +4363,8 @@ function jsonByteLength(value) {
4376
4363
  return MAX_PAYLOAD_BYTES + 1;
4377
4364
  }
4378
4365
  }
4379
- function error(errors, path34, code, message) {
4380
- errors.push({ path: path34, code, message });
4366
+ function error(errors, path35, code, message) {
4367
+ errors.push({ path: path35, code, message });
4381
4368
  }
4382
4369
  function isMessageRole(value) {
4383
4370
  return value === "user" || value === "assistant" || value === "system";
@@ -4385,25 +4372,25 @@ function isMessageRole(value) {
4385
4372
  function isPlainJsonObject(value) {
4386
4373
  return isRecord3(value);
4387
4374
  }
4388
- function validateCacheControl(value, path34, errors) {
4375
+ function validateCacheControl(value, path35, errors) {
4389
4376
  if (value === void 0) return void 0;
4390
4377
  if (!isRecord3(value) || value["type"] !== "ephemeral") {
4391
- error(errors, path34, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4378
+ error(errors, path35, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4392
4379
  return void 0;
4393
4380
  }
4394
4381
  return { type: "ephemeral" };
4395
4382
  }
4396
- function validateProviderMeta(value, path34, errors) {
4383
+ function validateProviderMeta(value, path35, errors) {
4397
4384
  if (value === void 0) return void 0;
4398
4385
  if (!isPlainJsonObject(value)) {
4399
- error(errors, path34, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4386
+ error(errors, path35, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4400
4387
  return void 0;
4401
4388
  }
4402
4389
  return value;
4403
4390
  }
4404
- function validateBlock(value, path34, errors) {
4391
+ function validateBlock(value, path35, errors) {
4405
4392
  if (!isRecord3(value)) {
4406
- error(errors, path34, "INVALID_BLOCK", "Content block must be an object.");
4393
+ error(errors, path35, "INVALID_BLOCK", "Content block must be an object.");
4407
4394
  return void 0;
4408
4395
  }
4409
4396
  const type = value["type"];
@@ -4411,14 +4398,14 @@ function validateBlock(value, path34, errors) {
4411
4398
  case "text": {
4412
4399
  const text2 = value["text"];
4413
4400
  if (typeof text2 !== "string") {
4414
- error(errors, `${path34}/text`, "INVALID_TEXT", "Text block text must be a string.");
4401
+ error(errors, `${path35}/text`, "INVALID_TEXT", "Text block text must be a string.");
4415
4402
  return void 0;
4416
4403
  }
4417
4404
  if (text2.length > MAX_STRING_LENGTH) {
4418
- error(errors, `${path34}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4405
+ error(errors, `${path35}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4419
4406
  return void 0;
4420
4407
  }
4421
- const cacheControl = validateCacheControl(value["cache_control"], `${path34}/cache_control`, errors);
4408
+ const cacheControl = validateCacheControl(value["cache_control"], `${path35}/cache_control`, errors);
4422
4409
  return cacheControl ? { type: "text", text: text2, cache_control: cacheControl } : { type: "text", text: text2 };
4423
4410
  }
4424
4411
  case "tool_use": {
@@ -4426,15 +4413,15 @@ function validateBlock(value, path34, errors) {
4426
4413
  const name2 = value["name"];
4427
4414
  const input = value["input"];
4428
4415
  if (typeof id !== "string" || id.length === 0) {
4429
- error(errors, `${path34}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4416
+ error(errors, `${path35}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4430
4417
  }
4431
4418
  if (typeof name2 !== "string" || name2.length === 0) {
4432
- error(errors, `${path34}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4419
+ error(errors, `${path35}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4433
4420
  }
4434
4421
  if (!isPlainJsonObject(input)) {
4435
- error(errors, `${path34}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4422
+ error(errors, `${path35}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4436
4423
  }
4437
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path34}/providerMeta`, errors);
4424
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path35}/providerMeta`, errors);
4438
4425
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4439
4426
  return void 0;
4440
4427
  }
@@ -4446,18 +4433,18 @@ function validateBlock(value, path34, errors) {
4446
4433
  const content = value["content"];
4447
4434
  const isError = value["is_error"];
4448
4435
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4449
- error(errors, `${path34}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4436
+ error(errors, `${path35}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4450
4437
  }
4451
4438
  if (name2 !== void 0 && typeof name2 !== "string") {
4452
- error(errors, `${path34}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4439
+ error(errors, `${path35}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4453
4440
  }
4454
4441
  if (typeof content !== "string") {
4455
- error(errors, `${path34}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4442
+ error(errors, `${path35}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4456
4443
  } else if (content.length > MAX_STRING_LENGTH) {
4457
- error(errors, `${path34}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4444
+ error(errors, `${path35}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4458
4445
  }
4459
4446
  if (isError !== void 0 && typeof isError !== "boolean") {
4460
- error(errors, `${path34}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4447
+ error(errors, `${path35}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4461
4448
  }
4462
4449
  if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4463
4450
  return {
@@ -4471,25 +4458,25 @@ function validateBlock(value, path34, errors) {
4471
4458
  case "image": {
4472
4459
  const source = value["source"];
4473
4460
  if (!isRecord3(source)) {
4474
- error(errors, `${path34}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4461
+ error(errors, `${path35}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4475
4462
  return void 0;
4476
4463
  }
4477
4464
  const sourceType = source["type"];
4478
4465
  if (sourceType !== "base64" && sourceType !== "url") {
4479
- error(errors, `${path34}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4466
+ error(errors, `${path35}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4480
4467
  return void 0;
4481
4468
  }
4482
4469
  const mediaType = source["media_type"];
4483
4470
  const data = source["data"];
4484
4471
  const url = source["url"];
4485
4472
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4486
- error(errors, `${path34}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4473
+ error(errors, `${path35}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4487
4474
  }
4488
4475
  if (data !== void 0 && typeof data !== "string") {
4489
- error(errors, `${path34}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4476
+ error(errors, `${path35}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4490
4477
  }
4491
4478
  if (url !== void 0 && typeof url !== "string") {
4492
- error(errors, `${path34}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4479
+ error(errors, `${path35}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4493
4480
  }
4494
4481
  return {
4495
4482
  type: "image",
@@ -4505,13 +4492,13 @@ function validateBlock(value, path34, errors) {
4505
4492
  const thinking = value["thinking"];
4506
4493
  const signature = value["signature"];
4507
4494
  if (typeof thinking !== "string") {
4508
- error(errors, `${path34}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4495
+ error(errors, `${path35}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4509
4496
  return void 0;
4510
4497
  }
4511
4498
  if (signature !== void 0 && typeof signature !== "string") {
4512
- error(errors, `${path34}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4499
+ error(errors, `${path35}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4513
4500
  }
4514
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path34}/providerMeta`, errors);
4501
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path35}/providerMeta`, errors);
4515
4502
  return {
4516
4503
  type: "thinking",
4517
4504
  thinking,
@@ -4520,7 +4507,7 @@ function validateBlock(value, path34, errors) {
4520
4507
  };
4521
4508
  }
4522
4509
  default:
4523
- error(errors, `${path34}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4510
+ error(errors, `${path35}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4524
4511
  return void 0;
4525
4512
  }
4526
4513
  }
@@ -4538,39 +4525,39 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4538
4525
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
4539
4526
  }
4540
4527
  value.forEach((item, index) => {
4541
- const path34 = `/messages/${index}`;
4528
+ const path35 = `/messages/${index}`;
4542
4529
  if (!isRecord3(item)) {
4543
- error(errors, path34, "INVALID_MESSAGE", "Message must be an object.");
4530
+ error(errors, path35, "INVALID_MESSAGE", "Message must be an object.");
4544
4531
  return;
4545
4532
  }
4546
4533
  const role = item["role"];
4547
4534
  if (!isMessageRole(role)) {
4548
- error(errors, `${path34}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4535
+ error(errors, `${path35}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4549
4536
  return;
4550
4537
  }
4551
4538
  const rawContent = item["content"];
4552
4539
  let content;
4553
4540
  if (typeof rawContent === "string") {
4554
4541
  if (rawContent.length > MAX_STRING_LENGTH) {
4555
- error(errors, `${path34}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4542
+ error(errors, `${path35}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4556
4543
  return;
4557
4544
  }
4558
4545
  content = rawContent;
4559
4546
  } else if (Array.isArray(rawContent)) {
4560
4547
  const blocks = [];
4561
4548
  rawContent.forEach((block, blockIndex) => {
4562
- const parsed = validateBlock(block, `${path34}/content/${blockIndex}`, errors);
4549
+ const parsed = validateBlock(block, `${path35}/content/${blockIndex}`, errors);
4563
4550
  if (parsed) blocks.push(parsed);
4564
4551
  });
4565
4552
  content = blocks;
4566
4553
  } else {
4567
- error(errors, `${path34}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4554
+ error(errors, `${path35}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4568
4555
  return;
4569
4556
  }
4570
4557
  const ts = item["ts"];
4571
4558
  if (ts !== void 0) {
4572
4559
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4573
- error(errors, `${path34}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4560
+ error(errors, `${path35}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4574
4561
  return;
4575
4562
  }
4576
4563
  }
@@ -4959,6 +4946,7 @@ import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wron
4959
4946
  import {
4960
4947
  checkCodebaseIndexServerHealth,
4961
4948
  getIndexState,
4949
+ resolveProjectIndexDaemonAvailability,
4962
4950
  shutdownCodebaseIndexServer
4963
4951
  } from "@wrongstack/tools";
4964
4952
  async function handleConnectionsHealthRoute(context, ws, message) {
@@ -5051,6 +5039,19 @@ async function chronicleHealth(projectRoot) {
5051
5039
  }
5052
5040
  async function codebaseIndexHealth(projectRoot, indexDir) {
5053
5041
  const startedAt = Date.now();
5042
+ const availability = resolveProjectIndexDaemonAvailability(projectRoot, indexDir);
5043
+ if (availability.kind === "endpoint-invalid") {
5044
+ return {
5045
+ id: "codebase-index",
5046
+ label: "Codebase index",
5047
+ status: "unavailable",
5048
+ required: false,
5049
+ mode: "endpoint-invalid",
5050
+ detail: `Socket path is ${availability.byteLength} bytes \u2014 over this platform's ${availability.maxBytes}-byte sun_path limit. Queries fall back to a process-local index. Set a shorter TMPDIR to restore the shared daemon.`,
5051
+ endpoint: availability.endpoint,
5052
+ latencyMs: Date.now() - startedAt
5053
+ };
5054
+ }
5054
5055
  try {
5055
5056
  const health = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
5056
5057
  timeoutMs: 2e3
@@ -5732,15 +5733,15 @@ async function handleGitChanges(ws, projectRoot) {
5732
5733
  if (!m) continue;
5733
5734
  const added = m[1] === "-" ? 0 : Number(m[1]);
5734
5735
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5735
- let path34 = m[3] ?? "";
5736
- if (path34 === "") {
5736
+ let path35 = m[3] ?? "";
5737
+ if (path35 === "") {
5737
5738
  i += 1;
5738
- path34 = parts[i + 1] ?? parts[i] ?? "";
5739
+ path35 = parts[i + 1] ?? parts[i] ?? "";
5739
5740
  i += 1;
5740
5741
  }
5741
- if (!path34) continue;
5742
- const prev = counts.get(path34) ?? { added: 0, deleted: 0 };
5743
- counts.set(path34, { added: prev.added + added, deleted: prev.deleted + deleted });
5742
+ if (!path35) continue;
5743
+ const prev = counts.get(path35) ?? { added: 0, deleted: 0 };
5744
+ counts.set(path35, { added: prev.added + added, deleted: prev.deleted + deleted });
5744
5745
  }
5745
5746
  };
5746
5747
  parseNumstat(unstagedNumstat);
@@ -5752,7 +5753,7 @@ async function handleGitChanges(ws, projectRoot) {
5752
5753
  if (!rec || rec.length < 3) continue;
5753
5754
  const x = rec[0] ?? " ";
5754
5755
  const y = rec[1] ?? " ";
5755
- const path34 = rec.slice(3);
5756
+ const path35 = rec.slice(3);
5756
5757
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5757
5758
  if (isRename) i += 1;
5758
5759
  let status;
@@ -5764,13 +5765,13 @@ async function handleGitChanges(ws, projectRoot) {
5764
5765
  else if (x === "D" || y === "D") status = "D";
5765
5766
  else status = "M";
5766
5767
  const staged = x !== " " && x !== "?";
5767
- let added = counts.get(path34)?.added ?? 0;
5768
- let deleted = counts.get(path34)?.deleted ?? 0;
5768
+ let added = counts.get(path35)?.added ?? 0;
5769
+ let deleted = counts.get(path35)?.deleted ?? 0;
5769
5770
  if (status === "?") {
5770
5771
  added = 0;
5771
5772
  deleted = 0;
5772
5773
  }
5773
- files.push({ path: path34, status, added, deleted, staged });
5774
+ files.push({ path: path35, status, added, deleted, staged });
5774
5775
  }
5775
5776
  send(ws, { type: "git.changes", payload: { files } });
5776
5777
  } catch (err) {
@@ -5781,21 +5782,21 @@ async function handleGitChanges(ws, projectRoot) {
5781
5782
  }
5782
5783
  }
5783
5784
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5784
- async function handleGitDiff(ws, projectRoot, path34) {
5785
+ async function handleGitDiff(ws, projectRoot, path35) {
5785
5786
  const cwd = projectRoot || void 0;
5786
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path34, ...extra } });
5787
- if (!path34 || path34.includes("\0") || path34.includes("..") || nodePath.isAbsolute(path34)) {
5787
+ const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path35, ...extra } });
5788
+ if (!path35 || path35.includes("\0") || path35.includes("..") || nodePath.isAbsolute(path35)) {
5788
5789
  reply2({ oldText: "", newText: "", error: "invalid path" });
5789
5790
  return;
5790
5791
  }
5791
5792
  try {
5792
5793
  const git = makeGit(cwd);
5793
5794
  const { readFile: readFile13 } = await import("node:fs/promises");
5794
- const { join: join18 } = await import("node:path");
5795
- const oldText = await git(["show", `HEAD:${path34}`]);
5795
+ const { join: join19 } = await import("node:path");
5796
+ const oldText = await git(["show", `HEAD:${path35}`]);
5796
5797
  let newText = "";
5797
5798
  try {
5798
- const abs = cwd ? join18(cwd, path34) : path34;
5799
+ const abs = cwd ? join19(cwd, path35) : path35;
5799
5800
  const buf = await readFile13(abs);
5800
5801
  if (buf.includes(0)) {
5801
5802
  reply2({ oldText: "", newText: "", binary: true });
@@ -5857,10 +5858,7 @@ async function handleGoalSnapshotRoute(ws, msg, handlers) {
5857
5858
  }
5858
5859
 
5859
5860
  // src/server/goal-ws-handler.ts
5860
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5861
- import {
5862
- assignNickname
5863
- } from "@wrongstack/core/coordination";
5861
+ import { assignNickname } from "@wrongstack/core/coordination";
5864
5862
  import {
5865
5863
  GoalAssessor,
5866
5864
  GoalPlanner,
@@ -5868,6 +5866,7 @@ import {
5868
5866
  PhaseOrchestrator,
5869
5867
  PhaseStore
5870
5868
  } from "@wrongstack/core/goal";
5869
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5871
5870
  import { WorktreeManager } from "@wrongstack/core/worktree";
5872
5871
 
5873
5872
  // src/server/git-process.ts
@@ -5904,12 +5903,7 @@ function deriveTitle(goal) {
5904
5903
  return trimmed || "Goal";
5905
5904
  }
5906
5905
  async function commitsSince(cwd, baseSha, branch) {
5907
- const output = await gitStdout(cwd, [
5908
- "log",
5909
- "--reverse",
5910
- "--format=%H",
5911
- `${baseSha}..${branch}`
5912
- ]);
5906
+ const output = await gitStdout(cwd, ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`]);
5913
5907
  if (output === null) return [];
5914
5908
  return output.split("\n").map((s) => s.trim()).filter(Boolean);
5915
5909
  }
@@ -5960,6 +5954,17 @@ var GoalWebSocketHandler = class {
5960
5954
  ws.on("error", () => this.clients.delete(client));
5961
5955
  this.sendState(client);
5962
5956
  }
5957
+ /** Release timers, in-flight work, and socket references owned by this host. */
5958
+ dispose() {
5959
+ this.stopping = true;
5960
+ this.abort?.abort();
5961
+ this.abort = null;
5962
+ this.assessAbort?.abort();
5963
+ this.assessAbort = null;
5964
+ this.orchestrator?.stop();
5965
+ this.stopBroadcast();
5966
+ this.clients.clear();
5967
+ }
5963
5968
  async handleMessage(ws, msg) {
5964
5969
  switch (msg.type) {
5965
5970
  case "goal.assess":
@@ -6007,7 +6012,8 @@ var GoalWebSocketHandler = class {
6007
6012
  }
6008
6013
  case "goal.assignTask": {
6009
6014
  const { taskId, agentId, agentName } = msg.payload;
6010
- if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
6015
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName))
6016
+ this.afterBoardMutation();
6011
6017
  break;
6012
6018
  }
6013
6019
  case "goal.addTask": {
@@ -6052,7 +6058,10 @@ var GoalWebSocketHandler = class {
6052
6058
  this.graph = graph;
6053
6059
  this.broadcast({ type: "goal.state", payload: this.buildState() });
6054
6060
  } else {
6055
- this.broadcast({ type: "goal.error", payload: { message: `Graph not found: ${graphId}` } });
6061
+ this.broadcast({
6062
+ type: "goal.error",
6063
+ payload: { message: `Graph not found: ${graphId}` }
6064
+ });
6056
6065
  }
6057
6066
  }
6058
6067
  break;
@@ -6075,10 +6084,13 @@ var GoalWebSocketHandler = class {
6075
6084
  const mySeq = ++this.assessSeq;
6076
6085
  const sendResult7 = (result) => {
6077
6086
  if (mySeq !== this.assessSeq) return;
6078
- sendSerialized(ws, JSON.stringify({
6079
- type: "goal.assess.result",
6080
- payload: { ...result, reqSeq: seq }
6081
- }));
6087
+ sendSerialized(
6088
+ ws,
6089
+ JSON.stringify({
6090
+ type: "goal.assess.result",
6091
+ payload: { ...result, reqSeq: seq }
6092
+ })
6093
+ );
6082
6094
  };
6083
6095
  if (!goal.trim()) {
6084
6096
  sendResult7({
@@ -6124,15 +6136,24 @@ var GoalWebSocketHandler = class {
6124
6136
  const multiBoard = payload?.multiBoard ?? false;
6125
6137
  const verifyTasks = payload?.verifyTasks ?? false;
6126
6138
  const chimeraReview = payload?.chimeraReview ?? false;
6127
- this.abort = new AbortController();
6139
+ const runAbort = new AbortController();
6140
+ this.abort = runAbort;
6128
6141
  this.stopping = false;
6129
- const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
6130
- if (this.stopping || this.abort.signal.aborted) {
6142
+ const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, runAbort.signal);
6143
+ if (this.stopping || runAbort.signal.aborted) {
6131
6144
  this.broadcast({ type: "goal.stopped", payload: { title } });
6132
6145
  return;
6133
6146
  }
6134
6147
  this.logger.info(`[Goal] Starting: ${title}`);
6135
- const graph = await new PhaseGraphBuilder({ title, description: goal, phases, autonomous, multiBoard, verifyTasks, chimeraReview }).build();
6148
+ const graph = await new PhaseGraphBuilder({
6149
+ title,
6150
+ description: goal,
6151
+ phases,
6152
+ autonomous,
6153
+ multiBoard,
6154
+ verifyTasks,
6155
+ chimeraReview
6156
+ }).build();
6136
6157
  this.graph = graph;
6137
6158
  await this.store.save(graph);
6138
6159
  const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_GOAL_WORKTREES"] !== "0";
@@ -6151,9 +6172,10 @@ var GoalWebSocketHandler = class {
6151
6172
  maybeVerify.verifyPhase = (async (phase, env) => {
6152
6173
  const cwd = env?.cwd ?? this.projectRoot;
6153
6174
  try {
6154
- const { exec } = await import("node:child_process");
6175
+ const { execFile: execFile2 } = await import("node:child_process");
6155
6176
  const result = await new Promise((resolve16) => {
6156
- exec("npx tsc --noEmit", { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
6177
+ const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
6178
+ execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
6157
6179
  if (err && err.code === "ENOENT") {
6158
6180
  resolve16("[verify] tsc not found \u2014 skipping");
6159
6181
  return;
@@ -6295,11 +6317,41 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
6295
6317
  /** Generic fallback phases when the LLM planner produces nothing usable. */
6296
6318
  defaultPhases() {
6297
6319
  return [
6298
- { name: "Discovery", description: "Requirements gathering", priority: "high", estimateHours: 2, parallelizable: false },
6299
- { name: "Design", description: "Architecture and design", priority: "critical", estimateHours: 4, parallelizable: false },
6300
- { name: "Implementation", description: "Core development", priority: "critical", estimateHours: 12, parallelizable: false },
6301
- { name: "Testing", description: "Unit and integration tests", priority: "high", estimateHours: 6, parallelizable: true },
6302
- { name: "Deployment", description: "Deploy to production", priority: "medium", estimateHours: 2, parallelizable: false }
6320
+ {
6321
+ name: "Discovery",
6322
+ description: "Requirements gathering",
6323
+ priority: "high",
6324
+ estimateHours: 2,
6325
+ parallelizable: false
6326
+ },
6327
+ {
6328
+ name: "Design",
6329
+ description: "Architecture and design",
6330
+ priority: "critical",
6331
+ estimateHours: 4,
6332
+ parallelizable: false
6333
+ },
6334
+ {
6335
+ name: "Implementation",
6336
+ description: "Core development",
6337
+ priority: "critical",
6338
+ estimateHours: 12,
6339
+ parallelizable: false
6340
+ },
6341
+ {
6342
+ name: "Testing",
6343
+ description: "Unit and integration tests",
6344
+ priority: "high",
6345
+ estimateHours: 6,
6346
+ parallelizable: true
6347
+ },
6348
+ {
6349
+ name: "Deployment",
6350
+ description: "Deploy to production",
6351
+ priority: "medium",
6352
+ estimateHours: 2,
6353
+ parallelizable: false
6354
+ }
6303
6355
  ];
6304
6356
  }
6305
6357
  /** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
@@ -6380,8 +6432,10 @@ Type: ${task.type}`;
6380
6432
  try {
6381
6433
  const result_ = await this.agent.run(reviewPrompt);
6382
6434
  if (result_.status === "done" && result_.finalText) {
6383
- this.logger.info(`[Goal] Chimera review for "${task.title}":
6384
- ${result_.finalText.slice(0, 2e3)}`);
6435
+ this.logger.info(
6436
+ `[Goal] Chimera review for "${task.title}":
6437
+ ${result_.finalText.slice(0, 2e3)}`
6438
+ );
6385
6439
  }
6386
6440
  } catch (err) {
6387
6441
  this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
@@ -6434,7 +6488,16 @@ ${result_.finalText.slice(0, 2e3)}`);
6434
6488
  }
6435
6489
  buildState(activePhaseId) {
6436
6490
  if (!this.graph) {
6437
- return { phases: [], tasks: [], overallPercent: 0, autonomous: true, title: "", multiBoard: false, verifyTasks: false, chimeraReview: false };
6491
+ return {
6492
+ phases: [],
6493
+ tasks: [],
6494
+ overallPercent: 0,
6495
+ autonomous: true,
6496
+ title: "",
6497
+ multiBoard: false,
6498
+ verifyTasks: false,
6499
+ chimeraReview: false
6500
+ };
6438
6501
  }
6439
6502
  const phases = Array.from(this.graph.phases.values());
6440
6503
  const currentActiveId = activePhaseId || phases.find((p) => p.status === "running")?.id || phases[0]?.id || "";
@@ -6940,9 +7003,9 @@ function getAnalyticsBuffer() {
6940
7003
  }
6941
7004
 
6942
7005
  // src/server/http-server.ts
6943
- import * as fs9 from "node:fs/promises";
7006
+ import * as fs10 from "node:fs/promises";
6944
7007
  import * as http from "node:http";
6945
- import * as path12 from "node:path";
7008
+ import * as path13 from "node:path";
6946
7009
  import * as v8 from "node:v8";
6947
7010
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6948
7011
 
@@ -7626,18 +7689,153 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7626
7689
  }
7627
7690
  }
7628
7691
 
7629
- // src/server/projects-manifest.ts
7692
+ // src/server/memory-diagnostics.ts
7630
7693
  import * as fs8 from "node:fs/promises";
7631
7694
  import * as path11 from "node:path";
7695
+ var DEFAULT_TAIL_BYTES = 1024 * 1024;
7696
+ var MAX_PROCESSES = 32;
7697
+ function finiteNumber(record2, key) {
7698
+ const value = record2[key];
7699
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
7700
+ }
7701
+ function stringValue(record2, key) {
7702
+ const value = record2[key];
7703
+ return typeof value === "string" && value.length > 0 ? value : void 0;
7704
+ }
7705
+ function toProcessDiagnostic(record2) {
7706
+ const pid = finiteNumber(record2, "pid");
7707
+ const ts = stringValue(record2, "ts");
7708
+ if (pid === void 0 || ts === void 0) return null;
7709
+ const queueEntries = finiteNumber(record2, "hqQueueEntries");
7710
+ const queueBytes = finiteNumber(record2, "hqQueueBytes");
7711
+ const queueMaxBytes = finiteNumber(record2, "hqQueueMaxBytes");
7712
+ const snapshotInFlight = finiteNumber(record2, "hqSnapshotInFlight");
7713
+ const snapshotPending = finiteNumber(record2, "hqSnapshotPending");
7714
+ const snapshotTimerScheduled = finiteNumber(record2, "hqSnapshotTimerScheduled");
7715
+ const eventInFlight = finiteNumber(record2, "hqEventInFlight");
7716
+ const eventPending = finiteNumber(record2, "hqEventPending");
7717
+ const eventCoalesced = finiteNumber(record2, "hqEventCoalesced");
7718
+ const eventDropped = finiteNumber(record2, "hqEventDropped");
7719
+ return {
7720
+ pid,
7721
+ surface: stringValue(record2, "surface") ?? "unknown",
7722
+ ...stringValue(record2, "sessionId") !== void 0 ? { sessionId: stringValue(record2, "sessionId") } : {},
7723
+ ts,
7724
+ memory: {
7725
+ rss: finiteNumber(record2, "rss") ?? 0,
7726
+ heapUsed: finiteNumber(record2, "heapUsed") ?? 0,
7727
+ heapTotal: finiteNumber(record2, "heapTotal") ?? 0,
7728
+ retainedHeapUsed: finiteNumber(record2, "retainedHeapUsed"),
7729
+ nativeResidual: finiteNumber(record2, "nativeResidual"),
7730
+ external: finiteNumber(record2, "external"),
7731
+ arrayBuffers: finiteNumber(record2, "arrayBuffers")
7732
+ },
7733
+ signal: stringValue(record2, "memorySignal"),
7734
+ heapGrowthBytesPerHour: finiteNumber(record2, "heapGrowthBytesPerHour"),
7735
+ rssGrowthBytesPerHour: finiteNumber(record2, "rssGrowthBytesPerHour"),
7736
+ workload: {
7737
+ messages: finiteNumber(record2, "messages"),
7738
+ messageEstimatedTokens: finiteNumber(record2, "messageEstimatedTokens"),
7739
+ historyEntries: finiteNumber(record2, "historyEntries"),
7740
+ historyMountedEntries: finiteNumber(record2, "historyMountedEntries"),
7741
+ historyCachedGroups: finiteNumber(record2, "historyCachedGroups"),
7742
+ appRenders: finiteNumber(record2, "appRenders"),
7743
+ metricsDroppedObservations: finiteNumber(record2, "metricsDroppedObservations"),
7744
+ kanbanSyncActive: finiteNumber(record2, "kanbanSyncActive") === 1,
7745
+ kanbanSyncPendingBoards: finiteNumber(record2, "kanbanSyncPendingBoards"),
7746
+ kanbanSyncFullRescanPending: finiteNumber(record2, "kanbanSyncFullRescanPending") === 1,
7747
+ kanbanSyncRemoteApplyQueued: finiteNumber(record2, "kanbanSyncRemoteApplyQueued") === 1,
7748
+ kanbanSyncPendingRemoteBoards: finiteNumber(record2, "kanbanSyncPendingRemoteBoards"),
7749
+ kanbanSyncPublishRuns: finiteNumber(record2, "kanbanSyncPublishRuns"),
7750
+ kanbanSyncCoalescedRefreshes: finiteNumber(record2, "kanbanSyncCoalescedRefreshes"),
7751
+ kanbanSupervisorSnapshots: finiteNumber(record2, "kanbanSupervisorSnapshots"),
7752
+ kanbanSupervisorScheduledBoards: finiteNumber(record2, "kanbanSupervisorScheduledBoards"),
7753
+ kanbanSupervisorAgentCooldowns: finiteNumber(record2, "kanbanSupervisorAgentCooldowns"),
7754
+ kanbanSupervisorRunningAgents: finiteNumber(record2, "kanbanSupervisorRunningAgents")
7755
+ },
7756
+ resources: {
7757
+ active: finiteNumber(record2, "activeResources"),
7758
+ types: stringValue(record2, "activeResourceTypes")
7759
+ },
7760
+ ...queueEntries !== void 0 && queueBytes !== void 0 && queueMaxBytes !== void 0 ? {
7761
+ hqQueue: {
7762
+ entries: queueEntries,
7763
+ bytes: queueBytes,
7764
+ maxBytes: queueMaxBytes,
7765
+ droppedFrames: finiteNumber(record2, "hqQueueDroppedFrames") ?? 0,
7766
+ droppedBytes: finiteNumber(record2, "hqQueueDroppedBytes") ?? 0,
7767
+ coalescedFrames: finiteNumber(record2, "hqQueueCoalescedFrames") ?? 0,
7768
+ coalescedBytes: finiteNumber(record2, "hqQueueCoalescedBytes") ?? 0
7769
+ }
7770
+ } : {},
7771
+ ...snapshotInFlight !== void 0 || snapshotPending !== void 0 || snapshotTimerScheduled !== void 0 ? {
7772
+ hqSnapshot: {
7773
+ inFlight: snapshotInFlight === 1,
7774
+ pending: snapshotPending === 1,
7775
+ timerScheduled: snapshotTimerScheduled === 1,
7776
+ eventInFlight: eventInFlight === 1,
7777
+ pendingEvents: eventPending ?? 0,
7778
+ coalescedEvents: eventCoalesced ?? 0,
7779
+ droppedEvents: eventDropped ?? 0
7780
+ }
7781
+ } : {},
7782
+ profileTopStack: stringValue(record2, "memoryProfileTopStack"),
7783
+ profileTopStackBytes: finiteNumber(record2, "memoryProfileTopStackBytes")
7784
+ };
7785
+ }
7786
+ async function readRecentProcessMemoryDiagnostics(globalRoot, tailBytes = DEFAULT_TAIL_BYTES) {
7787
+ if (!globalRoot) return [];
7788
+ const heapLog = path11.join(globalRoot, "logs", "heap.jsonl");
7789
+ let handle;
7790
+ try {
7791
+ handle = await fs8.open(heapLog, "r");
7792
+ const stat3 = await handle.stat();
7793
+ const length = Math.min(stat3.size, Math.max(1, tailBytes));
7794
+ if (length === 0) return [];
7795
+ const start = stat3.size - length;
7796
+ const buffer = Buffer.allocUnsafe(length);
7797
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
7798
+ let text2 = buffer.subarray(0, bytesRead).toString("utf8");
7799
+ if (start > 0) {
7800
+ const firstNewline = text2.indexOf("\n");
7801
+ if (firstNewline === -1) return [];
7802
+ text2 = text2.slice(firstNewline + 1);
7803
+ }
7804
+ const newestByPid = /* @__PURE__ */ new Map();
7805
+ const lines = text2.split(/\r?\n/u);
7806
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
7807
+ const line = lines[index]?.trim();
7808
+ if (!line) continue;
7809
+ try {
7810
+ const parsed = JSON.parse(line);
7811
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
7812
+ const diagnostic = toProcessDiagnostic(parsed);
7813
+ if (!diagnostic || newestByPid.has(diagnostic.pid)) continue;
7814
+ newestByPid.set(diagnostic.pid, diagnostic);
7815
+ if (newestByPid.size >= MAX_PROCESSES) break;
7816
+ } catch {
7817
+ }
7818
+ }
7819
+ return [...newestByPid.values()].sort((a, b) => b.ts.localeCompare(a.ts));
7820
+ } catch {
7821
+ return [];
7822
+ } finally {
7823
+ await handle?.close().catch(() => void 0);
7824
+ }
7825
+ }
7826
+
7827
+ // src/server/projects-manifest.ts
7828
+ import * as fs9 from "node:fs/promises";
7829
+ import * as path12 from "node:path";
7632
7830
  import { ConfigError } from "@wrongstack/core/types";
7633
7831
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7634
7832
  function projectsJsonPath(globalConfigPath) {
7635
- const base = path11.dirname(globalConfigPath);
7636
- return path11.join(base, "projects.json");
7833
+ const base = path12.dirname(globalConfigPath);
7834
+ return path12.join(base, "projects.json");
7637
7835
  }
7638
7836
  async function loadManifest(globalConfigPath) {
7639
7837
  try {
7640
- const raw = await fs8.readFile(projectsJsonPath(globalConfigPath), "utf8");
7838
+ const raw = await fs9.readFile(projectsJsonPath(globalConfigPath), "utf8");
7641
7839
  const parsed = JSON.parse(raw);
7642
7840
  return { projects: parsed.projects ?? [] };
7643
7841
  } catch {
@@ -7646,37 +7844,37 @@ async function loadManifest(globalConfigPath) {
7646
7844
  }
7647
7845
  async function saveManifest(manifest, globalConfigPath) {
7648
7846
  const file = projectsJsonPath(globalConfigPath);
7649
- await fs8.mkdir(path11.dirname(file), { recursive: true });
7650
- await fs8.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7847
+ await fs9.mkdir(path12.dirname(file), { recursive: true });
7848
+ await fs9.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7651
7849
  }
7652
7850
  function generateProjectSlug(rootPath) {
7653
7851
  return projectSlug(rootPath);
7654
7852
  }
7655
7853
  async function ensureProjectDataDir(slug, globalConfigPath) {
7656
- const base = path11.dirname(globalConfigPath);
7657
- const dir = path11.join(base, "projects", slug);
7658
- await fs8.mkdir(dir, { recursive: true });
7854
+ const base = path12.dirname(globalConfigPath);
7855
+ const dir = path12.join(base, "projects", slug);
7856
+ await fs9.mkdir(dir, { recursive: true });
7659
7857
  return dir;
7660
7858
  }
7661
7859
  async function touchProjectInManifest(options, globalConfigPath) {
7662
- const root = path11.resolve(options.projectRoot);
7860
+ const root = path12.resolve(options.projectRoot);
7663
7861
  const file = projectsJsonPath(globalConfigPath);
7664
7862
  let entry;
7665
7863
  await withFileLock(file, async () => {
7666
7864
  const manifest = await loadManifest(globalConfigPath);
7667
7865
  const now = (/* @__PURE__ */ new Date()).toISOString();
7668
- entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7866
+ entry = manifest.projects.find((candidate) => path12.resolve(candidate.root) === root);
7669
7867
  if (entry) {
7670
7868
  entry.lastSeen = now;
7671
- if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7869
+ if (options.workingDir) entry.lastWorkingDir = path12.resolve(options.workingDir);
7672
7870
  } else {
7673
7871
  entry = {
7674
- name: options.name ?? path11.basename(root),
7872
+ name: options.name ?? path12.basename(root),
7675
7873
  root,
7676
7874
  slug: generateProjectSlug(root),
7677
7875
  createdAt: now,
7678
7876
  lastSeen: now,
7679
- lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7877
+ lastWorkingDir: options.workingDir ? path12.resolve(options.workingDir) : void 0
7680
7878
  };
7681
7879
  manifest.projects.push(entry);
7682
7880
  }
@@ -8087,9 +8285,9 @@ function buildCspHeader(publicWsUrl, host, port) {
8087
8285
  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'`;
8088
8286
  }
8089
8287
  function isInsideDist(candidate, distDir) {
8090
- const root = path12.resolve(distDir);
8091
- const resolved = path12.resolve(candidate);
8092
- return resolved === root || resolved.startsWith(root + path12.sep);
8288
+ const root = path13.resolve(distDir);
8289
+ const resolved = path13.resolve(candidate);
8290
+ return resolved === root || resolved.startsWith(root + path13.sep);
8093
8291
  }
8094
8292
  function decodeSessionId(segment) {
8095
8293
  try {
@@ -8109,7 +8307,7 @@ function strictDecodeParam(segment, res) {
8109
8307
  }
8110
8308
  function createHttpServer(opts) {
8111
8309
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
8112
- const distDir = path12.resolve(opts.distDir);
8310
+ const distDir = path13.resolve(opts.distDir);
8113
8311
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
8114
8312
  let techStackRuntime = null;
8115
8313
  const getTechStackRuntime = async () => {
@@ -8437,11 +8635,7 @@ function createHttpServer(opts) {
8437
8635
  if (researchMatch && req.method === "POST") {
8438
8636
  const pkg = strictDecodeParam(researchMatch[1], res);
8439
8637
  if (pkg === null) return;
8440
- await handleTechStackDependencyResearch(
8441
- res,
8442
- deps2,
8443
- pkg
8444
- );
8638
+ await handleTechStackDependencyResearch(res, deps2, pkg);
8445
8639
  return;
8446
8640
  }
8447
8641
  } catch (error2) {
@@ -8477,6 +8671,7 @@ function createHttpServer(opts) {
8477
8671
  return;
8478
8672
  }
8479
8673
  if (url.pathname === "/debug/system" && req.method === "GET") {
8674
+ const processes = await readRecentProcessMemoryDiagnostics(opts.globalRoot);
8480
8675
  res.writeHead(200, {
8481
8676
  "Content-Type": "application/json",
8482
8677
  "Cache-Control": "no-store"
@@ -8489,6 +8684,7 @@ function createHttpServer(opts) {
8489
8684
  uptime: process.uptime(),
8490
8685
  cpuUsage: process.cpuUsage(),
8491
8686
  codebaseIndexServer: getIndexState2().server,
8687
+ processes,
8492
8688
  timestamp: Date.now()
8493
8689
  })
8494
8690
  );
@@ -8496,24 +8692,24 @@ function createHttpServer(opts) {
8496
8692
  }
8497
8693
  let filePath;
8498
8694
  if (url.pathname === "/" || url.pathname === "") {
8499
- filePath = path12.join(distDir, "index.html");
8695
+ filePath = path13.join(distDir, "index.html");
8500
8696
  } else {
8501
- filePath = path12.join(distDir, url.pathname);
8697
+ filePath = path13.join(distDir, url.pathname);
8502
8698
  }
8503
- const resolvedPath = path12.resolve(filePath);
8699
+ const resolvedPath = path13.resolve(filePath);
8504
8700
  if (!isInsideDist(resolvedPath, distDir)) {
8505
8701
  res.writeHead(403, { "Content-Type": "text/plain" });
8506
8702
  res.end("Forbidden");
8507
8703
  return;
8508
8704
  }
8509
- const ext = path12.extname(resolvedPath);
8705
+ const ext = path13.extname(resolvedPath);
8510
8706
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8511
8707
  res.setHeader("Content-Type", contentType);
8512
8708
  setStaticSecurityHeaders(res);
8513
8709
  if (ext === ".html") {
8514
8710
  if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
8515
8711
  res.setHeader("Content-Security-Policy", buildCspHeader(opts.publicWsUrl, opts.host, port));
8516
- const html = await fs9.readFile(resolvedPath, "utf8");
8712
+ const html = await fs10.readFile(resolvedPath, "utf8");
8517
8713
  res.writeHead(200);
8518
8714
  res.end(injectWsConfig(html, { publicWsUrl: opts.publicWsUrl }));
8519
8715
  return;
@@ -8524,13 +8720,13 @@ function createHttpServer(opts) {
8524
8720
  url.pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=3600"
8525
8721
  );
8526
8722
  }
8527
- const fileContent = await fs9.readFile(resolvedPath);
8723
+ const fileContent = await fs10.readFile(resolvedPath);
8528
8724
  res.writeHead(200);
8529
8725
  res.end(fileContent);
8530
8726
  } catch (err) {
8531
8727
  if (err.code === "ENOENT") {
8532
8728
  try {
8533
- const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8729
+ const html = await fs10.readFile(path13.join(distDir, "index.html"), "utf8");
8534
8730
  setStaticSecurityHeaders(res);
8535
8731
  res.writeHead(200, {
8536
8732
  "Content-Type": "text/html",
@@ -8560,14 +8756,14 @@ function createHttpServer(opts) {
8560
8756
 
8561
8757
  // src/server/instance-registry.ts
8562
8758
  import * as os from "node:os";
8563
- import * as path13 from "node:path";
8564
- import * as fs10 from "node:fs/promises";
8759
+ import * as path14 from "node:path";
8760
+ import * as fs11 from "node:fs/promises";
8565
8761
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8566
8762
  function defaultBaseDir() {
8567
- return path13.join(os.homedir(), ".wrongstack");
8763
+ return path14.join(os.homedir(), ".wrongstack");
8568
8764
  }
8569
8765
  function registryPath(baseDir = defaultBaseDir()) {
8570
- return path13.join(baseDir, "webui-instances.json");
8766
+ return path14.join(baseDir, "webui-instances.json");
8571
8767
  }
8572
8768
  function isPidAlive(pid) {
8573
8769
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -8580,7 +8776,7 @@ function isPidAlive(pid) {
8580
8776
  }
8581
8777
  async function load(file) {
8582
8778
  try {
8583
- const raw = await fs10.readFile(file, "utf8");
8779
+ const raw = await fs11.readFile(file, "utf8");
8584
8780
  const parsed = JSON.parse(raw);
8585
8781
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
8586
8782
  return parsed;
@@ -10437,14 +10633,14 @@ function registerShutdownHandlers(res) {
10437
10633
  }
10438
10634
 
10439
10635
  // src/server/config-doctor.ts
10440
- import * as fs11 from "node:fs/promises";
10636
+ import * as fs12 from "node:fs/promises";
10441
10637
  import {
10442
10638
  repairConfigDefaults
10443
10639
  } from "@wrongstack/core/storage";
10444
10640
  import { atomicWrite as atomicWrite5 } from "@wrongstack/core/utils";
10445
10641
  import { decryptConfigSecrets } from "@wrongstack/core/security";
10446
10642
  async function readConfig(file, vault) {
10447
- const raw = await fs11.readFile(file, "utf8");
10643
+ const raw = await fs12.readFile(file, "utf8");
10448
10644
  const parsed = JSON.parse(raw);
10449
10645
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10450
10646
  throw new Error("Active profile config must contain a JSON object.");
@@ -10508,6 +10704,7 @@ async function handleConfigDoctor(ws, apply, deps2) {
10508
10704
 
10509
10705
  // src/server/mailbox-handlers.ts
10510
10706
  import {
10707
+ actionToAckInput,
10511
10708
  getSharedProjectMailbox,
10512
10709
  isMailboxMessageVisibleTo,
10513
10710
  MAILBOX_TYPE_PROPERTIES,
@@ -10524,6 +10721,43 @@ function getMailboxForDeps(deps2) {
10524
10721
  const dir = resolveProjectDir(projectRoot, globalRoot);
10525
10722
  return getSharedProjectMailbox(dir, deps2.events);
10526
10723
  }
10724
+ async function handleMailboxAction(ws, deps2, payload) {
10725
+ const mb = getMailboxForDeps(deps2);
10726
+ if (!mb) {
10727
+ send(ws, {
10728
+ type: "mailbox.action_result",
10729
+ payload: {
10730
+ requestId: payload.requestId,
10731
+ success: false,
10732
+ error: "No project root available"
10733
+ }
10734
+ });
10735
+ return;
10736
+ }
10737
+ try {
10738
+ const message = payload.action === "soft-delete" ? await mb.softDelete(payload.mailId, payload.readerId) : await mb.ack(actionToAckInput(payload.action, payload));
10739
+ send(ws, {
10740
+ type: "mailbox.action_result",
10741
+ payload: {
10742
+ requestId: payload.requestId,
10743
+ success: message !== null,
10744
+ action: payload.action,
10745
+ mailId: payload.mailId
10746
+ }
10747
+ });
10748
+ } catch (err) {
10749
+ send(ws, {
10750
+ type: "mailbox.action_result",
10751
+ payload: {
10752
+ requestId: payload.requestId,
10753
+ success: false,
10754
+ action: payload.action,
10755
+ mailId: payload.mailId,
10756
+ error: errMessage(err)
10757
+ }
10758
+ });
10759
+ }
10760
+ }
10527
10761
  async function handleMailboxSend(ws, deps2, payload) {
10528
10762
  const mb = getMailboxForDeps(deps2);
10529
10763
  if (!mb) {
@@ -10539,7 +10773,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10539
10773
  }
10540
10774
  try {
10541
10775
  const message = await mb.send({
10542
- from: "webui",
10776
+ from: payload.from ?? "webui",
10543
10777
  to: payload.to,
10544
10778
  type: payload.type,
10545
10779
  audience: payload.audience,
@@ -10554,6 +10788,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10554
10788
  requestId: payload.requestId,
10555
10789
  success: true,
10556
10790
  messageId: message.id,
10791
+ from: message.from,
10557
10792
  to: message.to,
10558
10793
  audience: message.audience ?? "all"
10559
10794
  }
@@ -10598,6 +10833,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
10598
10833
  send(ws, {
10599
10834
  type: "mailbox.messages",
10600
10835
  payload: {
10836
+ ...payload?.unreadOnly === true ? { unreadOnly: true } : {},
10601
10837
  messages: visibleMessages.map((m) => {
10602
10838
  const readByMe = payload?.agentId !== void 0 ? payload.agentId in m.readBy : false;
10603
10839
  const completedByMe = payload?.agentId !== void 0 ? m.completedBy === payload.agentId : false;
@@ -10713,6 +10949,14 @@ function createMailboxRouteHandlers(ctx) {
10713
10949
  ...ctx.events ? { events: ctx.events } : {}
10714
10950
  };
10715
10951
  return {
10952
+ action: (ws, msg) => {
10953
+ const parsed = validateMailboxActionPayload(msg.payload);
10954
+ if (!parsed.ok) {
10955
+ sendResult2(ws, false, parsed.message);
10956
+ return;
10957
+ }
10958
+ return handleMailboxAction(ws, deps2, parsed.value);
10959
+ },
10716
10960
  send: (ws, msg) => {
10717
10961
  const parsed = validateMailboxSendPayload(msg.payload);
10718
10962
  if (!parsed.ok) {
@@ -10763,6 +11007,9 @@ function createMailboxRouteHandlers(ctx) {
10763
11007
  }
10764
11008
  async function handleMailboxRoute(ws, msg, handlers) {
10765
11009
  switch (msg.type) {
11010
+ case "mailbox.action":
11011
+ await handlers.action(ws, msg);
11012
+ return true;
10766
11013
  case "mailbox.send":
10767
11014
  await handlers.send(ws, msg);
10768
11015
  return true;
@@ -12076,10 +12323,10 @@ async function findFreePort(host, startPort, opts = {}) {
12076
12323
  import { spawn as spawn2 } from "node:child_process";
12077
12324
  import { existsSync } from "node:fs";
12078
12325
  import { findPackageJSON } from "node:module";
12079
- import * as path14 from "node:path";
12326
+ import * as path15 from "node:path";
12080
12327
  function resolveDistDir(input) {
12081
12328
  const options = typeof input === "string" ? { explicitDistDir: input } : input ?? {};
12082
- if (options.explicitDistDir) return path14.resolve(options.explicitDistDir);
12329
+ if (options.explicitDistDir) return path15.resolve(options.explicitDistDir);
12083
12330
  const exists = options.exists ?? existsSync;
12084
12331
  let packageTarget;
12085
12332
  try {
@@ -12091,15 +12338,15 @@ function resolveDistDir(input) {
12091
12338
  );
12092
12339
  }
12093
12340
  if (!packageTarget) return null;
12094
- const distDir = path14.basename(packageTarget) === "package.json" ? path14.join(path14.dirname(packageTarget), "dist") : path14.dirname(packageTarget);
12341
+ const distDir = path15.basename(packageTarget) === "package.json" ? path15.join(path15.dirname(packageTarget), "dist") : path15.dirname(packageTarget);
12095
12342
  if (options.exists === void 0 && options.resolvePackageJson) return distDir;
12096
- return exists(path14.join(distDir, "index.html")) ? distDir : null;
12343
+ return exists(path15.join(distDir, "index.html")) ? distDir : null;
12097
12344
  }
12098
12345
  async function ensureDistDir(explicitDistDir, deps2 = {}) {
12099
12346
  const exists = deps2.exists ?? existsSync;
12100
12347
  if (explicitDistDir) {
12101
- const resolved2 = path14.resolve(explicitDistDir);
12102
- return exists(path14.join(resolved2, "index.html")) ? resolved2 : null;
12348
+ const resolved2 = path15.resolve(explicitDistDir);
12349
+ return exists(path15.join(resolved2, "index.html")) ? resolved2 : null;
12103
12350
  }
12104
12351
  const resolveOptions = {
12105
12352
  resolvePackageJson: deps2.resolvePackageJson,
@@ -12111,7 +12358,7 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
12111
12358
  try {
12112
12359
  const packageJson = deps2.resolvePackageJson ? deps2.resolvePackageJson("@wrongstack/webui/package.json") : findPackageJSON("@wrongstack/webui", import.meta.url);
12113
12360
  if (!packageJson) throw new Error("not found");
12114
- packageDir = path14.dirname(packageJson);
12361
+ packageDir = path15.dirname(packageJson);
12115
12362
  } catch {
12116
12363
  throw new Error(
12117
12364
  "@wrongstack/webui package could not be resolved. Install workspace dependencies and rebuild the CLI."
@@ -12120,8 +12367,8 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
12120
12367
  const findRoot = deps2.findWorkspaceRoot ?? ((pkgDir) => {
12121
12368
  let dir = pkgDir;
12122
12369
  for (let i = 0; i < 10; i++) {
12123
- if (existsSync(path14.join(dir, "pnpm-workspace.yaml"))) return dir;
12124
- const parent = path14.dirname(dir);
12370
+ if (existsSync(path15.join(dir, "pnpm-workspace.yaml"))) return dir;
12371
+ const parent = path15.dirname(dir);
12125
12372
  if (parent === dir) return null;
12126
12373
  dir = parent;
12127
12374
  }
@@ -12205,7 +12452,7 @@ function runPnpmBuild(cwd, workspace, timeoutMs) {
12205
12452
  }
12206
12453
 
12207
12454
  // src/server/embedded-lifecycle.ts
12208
- import * as path15 from "node:path";
12455
+ import * as path16 from "node:path";
12209
12456
 
12210
12457
  // src/server/network-info.ts
12211
12458
  import * as os2 from "node:os";
@@ -12271,7 +12518,7 @@ function registerWebuiInstance(p, deps2 = {}) {
12271
12518
  httpPort: p.httpPort,
12272
12519
  host: p.host,
12273
12520
  projectRoot: p.projectRoot,
12274
- projectName: path15.basename(p.projectRoot) || p.projectRoot,
12521
+ projectName: path16.basename(p.projectRoot) || p.projectRoot,
12275
12522
  startedAt: p.startedAt,
12276
12523
  url: buildWebUIAccessUrl({
12277
12524
  host: p.host,
@@ -12322,6 +12569,7 @@ function createWebuiShutdown(res) {
12322
12569
  log("[WebUI] Shutting down...");
12323
12570
  res.abortInFlight();
12324
12571
  res.unsubscribeEvents();
12572
+ res.disposeResources?.();
12325
12573
  res.closeClients();
12326
12574
  const unregistered = unregister(res.pid, res.registryBaseDir).catch(
12327
12575
  (err) => debug(`[webui-server] unregister failed: ${err}`)
@@ -12491,7 +12739,7 @@ ${text2}` : text2;
12491
12739
 
12492
12740
  // src/server/client-presence.ts
12493
12741
  import * as crypto2 from "node:crypto";
12494
- import * as path16 from "node:path";
12742
+ import * as path17 from "node:path";
12495
12743
  import {
12496
12744
  getSharedProjectMailbox as getSharedProjectMailbox2,
12497
12745
  resolveProjectDir as resolveProjectDir2
@@ -12509,7 +12757,7 @@ function createWebuiClientPresence(deps2) {
12509
12757
  if (!deps2.projectRoot) return null;
12510
12758
  try {
12511
12759
  const projectRoot = deps2.projectRoot;
12512
- const projectName = path16.basename(projectRoot);
12760
+ const projectName = path17.basename(projectRoot);
12513
12761
  const nextMailbox = getSharedProjectMailbox2(
12514
12762
  resolveProjectDir2(projectRoot, wstackGlobalRoot()),
12515
12763
  deps2.events,
@@ -13128,6 +13376,26 @@ function createKanbanSupervisor(deps2) {
13128
13376
  const agentRunning = /* @__PURE__ */ new Set();
13129
13377
  let disposed = false;
13130
13378
  let nextTimer;
13379
+ const forgetBoard = (boardId) => {
13380
+ snapshots.delete(boardId);
13381
+ nextDue.delete(boardId);
13382
+ agentLastRun.delete(boardId);
13383
+ agentRunning.delete(boardId);
13384
+ };
13385
+ const pruneAbsentBoards = (presentBoardIds) => {
13386
+ for (const boardId of snapshots.keys()) {
13387
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13388
+ }
13389
+ for (const boardId of nextDue.keys()) {
13390
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13391
+ }
13392
+ for (const boardId of agentLastRun.keys()) {
13393
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13394
+ }
13395
+ for (const boardId of agentRunning) {
13396
+ if (!presentBoardIds.has(boardId)) forgetBoard(boardId);
13397
+ }
13398
+ };
13131
13399
  const publish = (snapshot) => {
13132
13400
  snapshots.set(snapshot.boardId, snapshot);
13133
13401
  deps2.broadcast({
@@ -13227,6 +13495,7 @@ function createKanbanSupervisor(deps2) {
13227
13495
  onDone: async (result) => {
13228
13496
  clearTimeout(watchdog);
13229
13497
  agentRunning.delete(board.id);
13498
+ if (await getBoard3(deps2.projectRoot, board.id) === null) return;
13230
13499
  const current3 = snapshots.get(board.id) ?? snapshot;
13231
13500
  publish({
13232
13501
  ...current3,
@@ -13248,13 +13517,20 @@ function createKanbanSupervisor(deps2) {
13248
13517
  }
13249
13518
  };
13250
13519
  const auditNow = async (boardId) => {
13251
- const boards = boardId ? [await getBoard3(deps2.projectRoot, boardId)].filter(
13252
- (board) => Boolean(board)
13253
- ) : await Promise.all(
13254
- (await listBoards4(deps2.projectRoot)).map(
13255
- (summary) => getBoard3(deps2.projectRoot, summary.id)
13256
- )
13257
- ).then((items) => items.filter((board) => Boolean(board)));
13520
+ let boards;
13521
+ if (boardId) {
13522
+ const board = await getBoard3(deps2.projectRoot, boardId);
13523
+ if (board === null) {
13524
+ forgetBoard(boardId);
13525
+ boards = [];
13526
+ } else {
13527
+ boards = [board];
13528
+ }
13529
+ } else {
13530
+ const summaries = await listBoards4(deps2.projectRoot);
13531
+ pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
13532
+ boards = (await Promise.all(summaries.map((summary) => getBoard3(deps2.projectRoot, summary.id)))).filter((board) => Boolean(board));
13533
+ }
13258
13534
  const results = [];
13259
13535
  for (const board of boards) results.push(await auditBoard(board));
13260
13536
  scheduleNext();
@@ -13289,6 +13565,7 @@ function createKanbanSupervisor(deps2) {
13289
13565
  try {
13290
13566
  const now = Date.now();
13291
13567
  const summaries = await listBoards4(deps2.projectRoot);
13568
+ pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
13292
13569
  for (const summary of summaries) {
13293
13570
  if ((nextDue.get(summary.id) ?? 0) > now) continue;
13294
13571
  const board = await getBoard3(deps2.projectRoot, summary.id);
@@ -13304,12 +13581,22 @@ function createKanbanSupervisor(deps2) {
13304
13581
  return {
13305
13582
  getSnapshot: (boardId) => snapshots.get(boardId),
13306
13583
  auditNow,
13584
+ getStats: () => ({
13585
+ snapshots: snapshots.size,
13586
+ scheduledBoards: nextDue.size,
13587
+ agentCooldowns: agentLastRun.size,
13588
+ runningAgents: agentRunning.size
13589
+ }),
13307
13590
  dispose() {
13308
13591
  disposed = true;
13309
13592
  if (nextTimer !== void 0) {
13310
13593
  clearTimeout(nextTimer);
13311
13594
  nextTimer = void 0;
13312
13595
  }
13596
+ snapshots.clear();
13597
+ nextDue.clear();
13598
+ agentLastRun.clear();
13599
+ agentRunning.clear();
13313
13600
  }
13314
13601
  };
13315
13602
  }
@@ -13486,8 +13773,8 @@ function seedContextMeta(config, context) {
13486
13773
  }
13487
13774
 
13488
13775
  // src/server/pref-helpers.ts
13489
- import * as fs12 from "node:fs/promises";
13490
- import * as path17 from "node:path";
13776
+ import * as fs13 from "node:fs/promises";
13777
+ import * as path18 from "node:path";
13491
13778
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
13492
13779
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
13493
13780
  var PREF_KEYS = [
@@ -13583,11 +13870,11 @@ function prefSnapshot(contextMeta) {
13583
13870
  return snapshot;
13584
13871
  }
13585
13872
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
13586
- const globalRoot = path17.dirname(filePath);
13873
+ const globalRoot = path18.dirname(filePath);
13587
13874
  await backupConfigFile(filePath, { globalRoot });
13588
13875
  let raw;
13589
13876
  try {
13590
- raw = await fs12.readFile(filePath, "utf8");
13877
+ raw = await fs13.readFile(filePath, "utf8");
13591
13878
  } catch {
13592
13879
  raw = "{}";
13593
13880
  }
@@ -14062,16 +14349,16 @@ async function handleProcessRoute(ws, msg, handlers) {
14062
14349
  }
14063
14350
 
14064
14351
  // src/server/embedded-host-adapters.ts
14065
- import * as fs15 from "node:fs/promises";
14066
- import * as path19 from "node:path";
14352
+ import * as fs16 from "node:fs/promises";
14353
+ import * as path20 from "node:path";
14067
14354
  import { TOKENS } from "@wrongstack/core/kernel";
14068
14355
  import { DefaultSessionStore as DefaultSessionStore2 } from "@wrongstack/core/storage";
14069
14356
  import { toErrorMessage as toErrorMessage7, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
14070
14357
  import { makeProviderFromConfig } from "@wrongstack/providers";
14071
14358
 
14072
14359
  // src/server/project-handlers.ts
14073
- import * as fs13 from "node:fs/promises";
14074
- import * as path18 from "node:path";
14360
+ import * as fs14 from "node:fs/promises";
14361
+ import * as path19 from "node:path";
14075
14362
  import { DefaultSessionStore } from "@wrongstack/core/storage";
14076
14363
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
14077
14364
  function createProjectHandlers(ctx) {
@@ -14123,10 +14410,10 @@ function createProjectHandlers(ctx) {
14123
14410
  });
14124
14411
  return;
14125
14412
  }
14126
- const resolved = path18.resolve(parsed.value.root);
14127
- const name2 = parsed.value.name?.trim() || path18.basename(resolved);
14413
+ const resolved = path19.resolve(parsed.value.root);
14414
+ const name2 = parsed.value.name?.trim() || path19.basename(resolved);
14128
14415
  try {
14129
- const stat3 = await fs13.stat(resolved).catch(() => null);
14416
+ const stat3 = await fs14.stat(resolved).catch(() => null);
14130
14417
  if (!stat3?.isDirectory()) {
14131
14418
  sendTo(ws, {
14132
14419
  type: "projects.added",
@@ -14135,7 +14422,7 @@ function createProjectHandlers(ctx) {
14135
14422
  return;
14136
14423
  }
14137
14424
  const before = await loadManifest(ctx.globalConfigPath);
14138
- const already = before.projects.some((project) => path18.resolve(project.root) === resolved);
14425
+ const already = before.projects.some((project) => path19.resolve(project.root) === resolved);
14139
14426
  const entry = await touchProjectInManifest(
14140
14427
  { projectRoot: resolved, workingDir: resolved, name: name2 },
14141
14428
  ctx.globalConfigPath
@@ -14165,8 +14452,8 @@ function createProjectHandlers(ctx) {
14165
14452
  });
14166
14453
  return;
14167
14454
  }
14168
- const resolved = path18.resolve(parsed.value.root);
14169
- const name2 = parsed.value.name?.trim() || path18.basename(resolved);
14455
+ const resolved = path19.resolve(parsed.value.root);
14456
+ const name2 = parsed.value.name?.trim() || path19.basename(resolved);
14170
14457
  if (!ctx.allowProjectMutations) {
14171
14458
  sendTo(ws, {
14172
14459
  type: "projects.selected",
@@ -14179,7 +14466,7 @@ function createProjectHandlers(ctx) {
14179
14466
  return;
14180
14467
  }
14181
14468
  try {
14182
- const stat3 = await fs13.stat(resolved).catch(() => null);
14469
+ const stat3 = await fs14.stat(resolved).catch(() => null);
14183
14470
  if (!stat3?.isDirectory()) {
14184
14471
  sendTo(ws, {
14185
14472
  type: "projects.selected",
@@ -14209,7 +14496,7 @@ function createProjectHandlers(ctx) {
14209
14496
  const previousIdentityTarget = {
14210
14497
  projectSlug: previousPaths.projectSlug,
14211
14498
  projectRoot: previousProjectRoot,
14212
- projectName: path18.basename(previousProjectRoot),
14499
+ projectName: path19.basename(previousProjectRoot),
14213
14500
  workingDir: ctx.context.workingDir
14214
14501
  };
14215
14502
  const previousUsage = ctx.tokenCounter.total();
@@ -14372,14 +14659,14 @@ async function searchCatalogModels(registry, rawQuery, limit = 8) {
14372
14659
  }
14373
14660
 
14374
14661
  // src/server/provider-config-io.ts
14375
- import * as fs14 from "node:fs/promises";
14662
+ import * as fs15 from "node:fs/promises";
14376
14663
  import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
14377
14664
  import { atomicWrite as atomicWrite7 } from "@wrongstack/core/utils";
14378
14665
  import { decryptConfigSecrets as decryptConfigSecrets3, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
14379
14666
  async function loadSavedProviders(configPath, vault) {
14380
14667
  let raw;
14381
14668
  try {
14382
- raw = await fs14.readFile(configPath, "utf8");
14669
+ raw = await fs15.readFile(configPath, "utf8");
14383
14670
  } catch {
14384
14671
  return {};
14385
14672
  }
@@ -14397,7 +14684,7 @@ async function saveProviders(configPath, vault, providers, profileConfigPath) {
14397
14684
  let raw;
14398
14685
  let fileExists = true;
14399
14686
  try {
14400
- raw = await fs14.readFile(targetPath, "utf8");
14687
+ raw = await fs15.readFile(targetPath, "utf8");
14401
14688
  } catch (err) {
14402
14689
  if (err.code !== "ENOENT") {
14403
14690
  throw new ConfigError2({
@@ -15163,6 +15450,7 @@ var CLIENT_COLLABORATION_MESSAGE_TYPES = [
15163
15450
  "collab.resume",
15164
15451
  "collab.grant_control",
15165
15452
  "collab.inject_tool",
15453
+ "mailbox.action",
15166
15454
  "mailbox.agents",
15167
15455
  "mailbox.clear",
15168
15456
  "mailbox.compact",
@@ -15428,6 +15716,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
15428
15716
  "collab.pause.granted",
15429
15717
  "collab.pause.released",
15430
15718
  "collab.state",
15719
+ "mailbox.action_result",
15431
15720
  "mailbox.agent_registered",
15432
15721
  "mailbox.agents",
15433
15722
  "mailbox.cleared",
@@ -15651,13 +15940,13 @@ function isRegisteredMessageType(type, direction) {
15651
15940
  // src/protocol/decoder.ts
15652
15941
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
15653
15942
  var MAX_PAYLOAD_DEPTH = 32;
15654
- function inspectValue(value, path34, depth) {
15943
+ function inspectValue(value, path35, depth) {
15655
15944
  if (depth > MAX_PAYLOAD_DEPTH) {
15656
- return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path34 };
15945
+ return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path35 };
15657
15946
  }
15658
15947
  if (value === null || typeof value !== "object") return null;
15659
15948
  for (const key of Object.keys(value)) {
15660
- const childPath = `${path34}.${key}`;
15949
+ const childPath = `${path35}.${key}`;
15661
15950
  if (FORBIDDEN_KEYS.has(key)) {
15662
15951
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
15663
15952
  }
@@ -15754,7 +16043,11 @@ function projectChatMessage(message) {
15754
16043
  return value ? { kind: "text-delta", text: value, messageId: text(payload["messageId"]) } : null;
15755
16044
  }
15756
16045
  case "provider.response":
15757
- return { kind: "response", content: payload["content"] };
16046
+ return {
16047
+ kind: "response",
16048
+ content: payload["content"],
16049
+ stopReason: text(payload["stopReason"])
16050
+ };
15758
16051
  case "run.result":
15759
16052
  return {
15760
16053
  kind: "run-result",
@@ -16591,7 +16884,7 @@ function createEmbeddedConversationRoutes(ctx) {
16591
16884
  }
16592
16885
  function sessionStoreFor(opts) {
16593
16886
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot;
16594
- return opts.sessionStore ?? new DefaultSessionStore2({ dir: path19.join(projectRoot, ".wrongstack", "sessions"), projectRoot });
16887
+ return opts.sessionStore ?? new DefaultSessionStore2({ dir: path20.join(projectRoot, ".wrongstack", "sessions"), projectRoot });
16595
16888
  }
16596
16889
  function createEmbeddedSessionRoutes(ctx) {
16597
16890
  const { opts } = ctx;
@@ -16610,7 +16903,7 @@ function createEmbeddedSessionRoutes(ctx) {
16610
16903
  getSession: () => actx.session ?? opts.session,
16611
16904
  getSessionStore: () => sessionStoreFor(opts),
16612
16905
  canSwapSessions: () => opts.sessionStore !== void 0,
16613
- getSessionsDir: () => opts.sessionsDir ?? path19.join(getProjectRoot(), ".wrongstack", "sessions"),
16906
+ getSessionsDir: () => opts.sessionsDir ?? path20.join(getProjectRoot(), ".wrongstack", "sessions"),
16614
16907
  setSession: (next) => {
16615
16908
  actx.session = next;
16616
16909
  },
@@ -16627,7 +16920,7 @@ function createEmbeddedSessionRoutes(ctx) {
16627
16920
  async function broadcastEmbeddedGoalSnapshot(ctx) {
16628
16921
  const projectRoot = ctx.opts.projectRoot ?? ctx.opts.agent.ctx.projectRoot;
16629
16922
  try {
16630
- const raw = await fs15.readFile(path19.join(projectRoot, ".wrongstack", "goal.json"), "utf8");
16923
+ const raw = await fs16.readFile(path20.join(projectRoot, ".wrongstack", "goal.json"), "utf8");
16631
16924
  ctx.broadcast({ type: "goal-state.updated", payload: JSON.parse(raw) });
16632
16925
  } catch {
16633
16926
  ctx.broadcast({ type: "goal-state.updated", payload: null });
@@ -16636,10 +16929,10 @@ async function broadcastEmbeddedGoalSnapshot(ctx) {
16636
16929
  function createEmbeddedProjectRoutes(ctx) {
16637
16930
  const { opts } = ctx;
16638
16931
  const actx = opts.agent.ctx;
16639
- const globalConfigPath = opts.globalConfigPath ?? path19.join(wstackGlobalRoot2(), "config.json");
16932
+ const globalConfigPath = opts.globalConfigPath ?? path20.join(wstackGlobalRoot2(), "config.json");
16640
16933
  return createProjectHandlers({
16641
16934
  globalConfigPath,
16642
- wpaths: { globalRoot: path19.dirname(globalConfigPath) },
16935
+ wpaths: { globalRoot: path20.dirname(globalConfigPath) },
16643
16936
  context: actx,
16644
16937
  tokenCounter: actx.tokenCounter,
16645
16938
  config: { model: actx.model, provider: actx.provider.id },
@@ -17687,8 +17980,8 @@ function createRouteFamilyDispatcher(options) {
17687
17980
  }
17688
17981
 
17689
17982
  // src/server/shell-open.ts
17690
- import * as fs16 from "node:fs/promises";
17691
- import * as path20 from "node:path";
17983
+ import * as fs17 from "node:fs/promises";
17984
+ import * as path21 from "node:path";
17692
17985
  import { spawn as spawn3 } from "node:child_process";
17693
17986
  function normalizeShellOpenTarget(target) {
17694
17987
  return target === "terminal" ? "terminal" : "file-manager";
@@ -17699,11 +17992,11 @@ function shellQuote(s) {
17699
17992
  }
17700
17993
  async function handleShellOpen(req, logger, options) {
17701
17994
  try {
17702
- const resolved = path20.resolve(req.path);
17995
+ const resolved = path21.resolve(req.path);
17703
17996
  if (options?.projectRoot) {
17704
- const root = path20.resolve(options.projectRoot);
17705
- const relative5 = path20.relative(root, resolved);
17706
- const escapes = relative5.startsWith("..") || path20.isAbsolute(relative5);
17997
+ const root = path21.resolve(options.projectRoot);
17998
+ const relative5 = path21.relative(root, resolved);
17999
+ const escapes = relative5.startsWith("..") || path21.isAbsolute(relative5);
17707
18000
  if (escapes) {
17708
18001
  return {
17709
18002
  success: false,
@@ -17711,7 +18004,7 @@ async function handleShellOpen(req, logger, options) {
17711
18004
  };
17712
18005
  }
17713
18006
  }
17714
- await fs16.access(resolved);
18007
+ await fs17.access(resolved);
17715
18008
  if (METACHAR_REGEX.test(resolved)) {
17716
18009
  return { success: false, message: "Path contains unsupported characters." };
17717
18010
  }
@@ -18115,7 +18408,7 @@ function createEmbeddedMessageRouter(deps2) {
18115
18408
  }
18116
18409
 
18117
18410
  // src/server/provider-config-standalone.ts
18118
- import * as path21 from "node:path";
18411
+ import * as path22 from "node:path";
18119
18412
  import { DefaultSecretVault } from "@wrongstack/core/security";
18120
18413
  function createProviderConfigIO(configPath) {
18121
18414
  const keyFile = vaultKeyFileForConfigPath(configPath);
@@ -18126,15 +18419,15 @@ function createProviderConfigIO(configPath) {
18126
18419
  };
18127
18420
  }
18128
18421
  function vaultKeyFileForConfigPath(configPath) {
18129
- const configDir = path21.dirname(configPath);
18130
- const parentDir = path21.dirname(configDir);
18131
- const globalRoot = path21.basename(parentDir) === "profiles" ? path21.dirname(parentDir) : configDir;
18132
- return path21.join(globalRoot, ".key");
18422
+ const configDir = path22.dirname(configPath);
18423
+ const parentDir = path22.dirname(configDir);
18424
+ const globalRoot = path22.basename(parentDir) === "profiles" ? path22.dirname(parentDir) : configDir;
18425
+ return path22.join(globalRoot, ".key");
18133
18426
  }
18134
18427
 
18135
18428
  // src/server/provider-store.ts
18136
18429
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
18137
- import * as fs17 from "node:fs/promises";
18430
+ import * as fs18 from "node:fs/promises";
18138
18431
  import { decryptConfigSecrets as decryptConfigSecrets4, encryptConfigSecrets as encryptConfigSecrets3 } from "@wrongstack/core/security";
18139
18432
  import { atomicWrite as atomicWrite8 } from "@wrongstack/core/utils";
18140
18433
  function createConfigWriteLock() {
@@ -18158,7 +18451,7 @@ function createProviderStore(deps2) {
18158
18451
  const configWriteLock = createConfigWriteLock();
18159
18452
  async function loadSavedProviders2() {
18160
18453
  try {
18161
- const raw = await fs17.readFile(profileConfigPath, "utf8");
18454
+ const raw = await fs18.readFile(profileConfigPath, "utf8");
18162
18455
  const parsed = JSON.parse(raw);
18163
18456
  if (!parsed.providers) return {};
18164
18457
  return decryptConfigSecrets4(parsed.providers, vault);
@@ -18172,7 +18465,7 @@ function createProviderStore(deps2) {
18172
18465
  await prev;
18173
18466
  let parsed;
18174
18467
  try {
18175
- const raw = await fs17.readFile(profileConfigPath, "utf8");
18468
+ const raw = await fs18.readFile(profileConfigPath, "utf8");
18176
18469
  parsed = JSON.parse(raw);
18177
18470
  } catch {
18178
18471
  parsed = {};
@@ -18222,7 +18515,12 @@ function createProviderStore(deps2) {
18222
18515
  }
18223
18516
 
18224
18517
  // src/server/sdd-board-ws-handler.ts
18225
- import { listBoards as listBoards5 } from "@wrongstack/kanban";
18518
+ import {
18519
+ enqueueKanbanWorkflowCommand,
18520
+ kanbanWorkflowId,
18521
+ listBoards as listBoards5,
18522
+ listKanbanWorkflowStates
18523
+ } from "@wrongstack/kanban";
18226
18524
  import {
18227
18525
  applySddLifecycle,
18228
18526
  extractVerificationCommand,
@@ -18249,7 +18547,7 @@ var SddBoardWebSocketHandler = class {
18249
18547
  clients = /* @__PURE__ */ new Set();
18250
18548
  lifecycle;
18251
18549
  security;
18252
- diskPollingEnabled;
18550
+ standalonePollingEnabled;
18253
18551
  latest = null;
18254
18552
  poll = null;
18255
18553
  pollInFlight = false;
@@ -18258,7 +18556,7 @@ var SddBoardWebSocketHandler = class {
18258
18556
  this.store = new SddBoardStore({ baseDir: boardsDir });
18259
18557
  this.lifecycle = lifecycle;
18260
18558
  this.security = security;
18261
- this.diskPollingEnabled = events === void 0;
18559
+ this.standalonePollingEnabled = events === void 0;
18262
18560
  if (events) {
18263
18561
  const handler = (e) => {
18264
18562
  this.latest = e.snapshot;
@@ -18293,7 +18591,7 @@ var SddBoardWebSocketHandler = class {
18293
18591
  return;
18294
18592
  }
18295
18593
  if (msg.type === "sdd.board.list") {
18296
- const boards = await this.store.list();
18594
+ const boards = await this.listBoardEntries();
18297
18595
  this.broadcast({ type: "sdd.board.list", payload: { boards } });
18298
18596
  return;
18299
18597
  }
@@ -18306,7 +18604,8 @@ var SddBoardWebSocketHandler = class {
18306
18604
  const verificationCommands = [];
18307
18605
  if (action === "set_task_verification") {
18308
18606
  const command = msg.payload?.verificationCommand;
18309
- if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
18607
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192))
18608
+ return;
18310
18609
  if (typeof command === "string" && command.trim()) {
18311
18610
  verificationCommands.push({ command, operation: "sdd.set_task_verification" });
18312
18611
  }
@@ -18340,18 +18639,26 @@ var SddBoardWebSocketHandler = class {
18340
18639
  );
18341
18640
  if (!authorization.allowed) return;
18342
18641
  }
18343
- const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
18642
+ const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.listBoardEntries())[0]?.runId;
18344
18643
  if (runId) {
18345
- await this.store.appendControl(runId, {
18346
- ts: Date.now(),
18347
- type: action,
18348
- payload: msg.payload
18349
- });
18644
+ if (this.lifecycle && this.lifecycle.controlTransport !== "legacy-file") {
18645
+ await enqueueKanbanWorkflowCommand(
18646
+ this.lifecycle.projectRoot,
18647
+ kanbanWorkflowId("sdd", runId),
18648
+ { type: action, payload: msg.payload }
18649
+ );
18650
+ } else {
18651
+ await this.store.appendControl(runId, {
18652
+ ts: Date.now(),
18653
+ type: action,
18654
+ payload: msg.payload
18655
+ });
18656
+ }
18350
18657
  }
18351
18658
  }
18352
18659
  }
18353
18660
  /**
18354
- * Apply a cleanup/rollback/destroy from disk and broadcast a structured
18661
+ * Apply a cleanup/rollback/destroy from durable state and broadcast a structured
18355
18662
  * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
18356
18663
  * the user must stop it first; the UI gates the buttons on `!active` and the
18357
18664
  * Destroy flow auto-stops then waits before sending `destroy`.
@@ -18360,7 +18667,11 @@ var SddBoardWebSocketHandler = class {
18360
18667
  if (!this.lifecycle) {
18361
18668
  this.broadcast({
18362
18669
  type: "sdd.board.lifecycle_result",
18363
- payload: { op, ok: false, reason: "Lifecycle operations are not available in this session." }
18670
+ payload: {
18671
+ op,
18672
+ ok: false,
18673
+ reason: "Lifecycle operations are not available in this session."
18674
+ }
18364
18675
  });
18365
18676
  return;
18366
18677
  }
@@ -18376,6 +18687,7 @@ var SddBoardWebSocketHandler = class {
18376
18687
  projectRoot: this.lifecycle.projectRoot,
18377
18688
  paths: this.lifecycle.paths,
18378
18689
  runId,
18690
+ stateTransport: this.lifecycle.stateTransport ?? "kanban",
18379
18691
  revertMerged: payload?.revertMerged === true
18380
18692
  });
18381
18693
  this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
@@ -18404,22 +18716,27 @@ var SddBoardWebSocketHandler = class {
18404
18716
  if (this.pollInFlight) return;
18405
18717
  this.pollInFlight = true;
18406
18718
  try {
18407
- const entry = await this.store.latest();
18408
- if (!entry) return;
18409
- if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
18719
+ const snap = await this.loadLatestSnapshot();
18720
+ if (!snap) return;
18721
+ if (this.latest && this.latest.updatedAt >= snap.updatedAt && this.latest.runId === snap.runId) {
18410
18722
  return;
18411
18723
  }
18412
- const snap = await this.store.load(entry.runId);
18413
- if (snap) {
18414
- this.latest = snap;
18415
- this.broadcast({ type: "sdd.board.snapshot", payload: snap });
18416
- }
18724
+ this.latest = snap;
18725
+ this.broadcast({ type: "sdd.board.snapshot", payload: snap });
18726
+ } catch (err) {
18727
+ console.warn(
18728
+ JSON.stringify({
18729
+ level: "warn",
18730
+ event: "sdd_board.poll_failed",
18731
+ message: err instanceof Error ? err.message : String(err)
18732
+ })
18733
+ );
18417
18734
  } finally {
18418
18735
  this.pollInFlight = false;
18419
18736
  }
18420
18737
  }
18421
18738
  startPolling() {
18422
- if (!this.diskPollingEnabled || this.poll !== null || this.clients.size === 0) return;
18739
+ if (!this.standalonePollingEnabled || this.poll !== null || this.clients.size === 0) return;
18423
18740
  this.poll = setInterval(() => void this.pollLatest(), 1e3);
18424
18741
  this.poll.unref?.();
18425
18742
  }
@@ -18429,17 +18746,44 @@ var SddBoardWebSocketHandler = class {
18429
18746
  this.poll = null;
18430
18747
  }
18431
18748
  async sendCurrent(client) {
18432
- const snap = this.latest ?? await this.loadLatestFromDisk();
18749
+ const snap = this.latest ?? await this.loadLatestSnapshot();
18433
18750
  if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
18434
18751
  }
18435
18752
  async broadcastCurrent() {
18436
- const snap = this.latest ?? await this.loadLatestFromDisk();
18753
+ const snap = this.latest ?? await this.loadLatestSnapshot();
18437
18754
  if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
18438
18755
  }
18439
- async loadLatestFromDisk() {
18756
+ async loadLatestSnapshot() {
18757
+ if (this.usesKanbanState()) {
18758
+ const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
18759
+ const snapshots = states.map((state) => state.value).filter(isSddBoardSnapshot).sort((a, b) => b.updatedAt - a.updatedAt);
18760
+ return snapshots[0] ?? null;
18761
+ }
18440
18762
  const entry = await this.store.latest();
18441
18763
  return entry ? this.store.load(entry.runId) : null;
18442
18764
  }
18765
+ async listBoardEntries() {
18766
+ if (!this.usesKanbanState()) return this.store.list();
18767
+ const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
18768
+ return states.flatMap((state) => {
18769
+ if (!isSddBoardSnapshot(state.value)) return [];
18770
+ const snapshot = state.value;
18771
+ return [
18772
+ {
18773
+ runId: snapshot.runId,
18774
+ ...snapshot.specId ? { specId: snapshot.specId } : {},
18775
+ title: snapshot.title,
18776
+ status: snapshot.status,
18777
+ total: snapshot.progress.total,
18778
+ completed: snapshot.progress.completed,
18779
+ updatedAt: snapshot.updatedAt
18780
+ }
18781
+ ];
18782
+ });
18783
+ }
18784
+ usesKanbanState() {
18785
+ return Boolean(this.lifecycle && this.lifecycle.stateTransport !== "legacy-file");
18786
+ }
18443
18787
  broadcast(msg) {
18444
18788
  const data = JSON.stringify(msg);
18445
18789
  for (const client of this.clients) {
@@ -18450,17 +18794,20 @@ var SddBoardWebSocketHandler = class {
18450
18794
  sendSerialized(client.ws, JSON.stringify(msg));
18451
18795
  }
18452
18796
  };
18797
+ function isSddBoardSnapshot(value) {
18798
+ if (!value || typeof value !== "object") return false;
18799
+ const snapshot = value;
18800
+ 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");
18801
+ }
18453
18802
 
18454
18803
  // src/server/sdd-wizard-wiring.ts
18455
- import * as path22 from "node:path";
18456
- import {
18457
- DefaultTaskStore,
18458
- TaskTracker
18459
- } from "@wrongstack/core/tasking";
18804
+ import * as path23 from "node:path";
18805
+ import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
18460
18806
  import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
18461
18807
  import { WorktreeManager as WorktreeManager2 } from "@wrongstack/core/worktree";
18462
18808
  import {
18463
18809
  cleanupStaleSddWorktrees,
18810
+ createKanbanSddSessionPersistence,
18464
18811
  decomposeNonAtomicTasks,
18465
18812
  gatherProjectContext,
18466
18813
  makeAcceptanceCriteriaVerifier,
@@ -18478,7 +18825,7 @@ import {
18478
18825
  } from "@wrongstack/sdd";
18479
18826
  async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
18480
18827
  const runTracker = tracker ?? (() => {
18481
- const t = new TaskTracker({ store: new DefaultTaskStore() });
18828
+ const t = new TaskTracker({ store: deps2.taskStore ?? new DefaultTaskStore() });
18482
18829
  t.setGraph(graph);
18483
18830
  return t;
18484
18831
  })();
@@ -18487,7 +18834,8 @@ async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
18487
18834
  if (worktreesEnabled && await isGitWorkTree(deps2.projectRoot)) {
18488
18835
  void cleanupStaleSddWorktrees({
18489
18836
  projectRoot: deps2.projectRoot,
18490
- boardsDir: deps2.projectSddBoards
18837
+ boardsDir: deps2.projectSddBoards,
18838
+ stateTransport: "kanban"
18491
18839
  }).catch(() => void 0);
18492
18840
  worktrees = new WorktreeManager2({
18493
18841
  projectRoot: deps2.projectRoot,
@@ -18566,7 +18914,8 @@ function buildSddWizardDeps(opts) {
18566
18914
  }).catch(() => {
18567
18915
  projectContext = "";
18568
18916
  });
18569
- const sessionPath = opts.paths.projectSddSession ?? path22.join(opts.paths.projectDir, "sdd-session.json");
18917
+ const legacySessionPath = opts.paths.projectSddSession ?? path23.join(opts.paths.projectDir, "sdd-session.json");
18918
+ const sessionPersistence = createKanbanSddSessionPersistence(opts.projectRoot, legacySessionPath);
18570
18919
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
18571
18920
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
18572
18921
  const runIsolatedTurn = async (prompt, name2) => {
@@ -18587,7 +18936,7 @@ function buildSddWizardDeps(opts) {
18587
18936
  const makeDriver = () => new SddInterviewDriver({
18588
18937
  specStore,
18589
18938
  graphStore,
18590
- sessionPath,
18939
+ sessionPersistence,
18591
18940
  projectContext
18592
18941
  });
18593
18942
  const launchFromGraph = async (graph, config, tracker) => {
@@ -18599,6 +18948,7 @@ function buildSddWizardDeps(opts) {
18599
18948
  projectRoot: opts.projectRoot,
18600
18949
  subagentFactory: opts.subagentFactory,
18601
18950
  projectSddBoards: opts.paths.projectSddBoards,
18951
+ taskStore: graphStore,
18602
18952
  registry,
18603
18953
  runIsolatedTurn,
18604
18954
  ...opts.brain ? { brain: opts.brain } : {}
@@ -18668,8 +19018,18 @@ var SddWizardWebSocketHandler = class {
18668
19018
  lastAgentText = "";
18669
19019
  /** Guards against overlapping interview turns (one in flight at a time). */
18670
19020
  busy = false;
18671
- /** Resolves once project-context gather + disk resume finish. */
19021
+ /** Set when authoritative session state could not be read during bootstrap. */
19022
+ resumeError = null;
19023
+ /** Resolves once project-context gather + durable resume finish. */
18672
19024
  ready;
19025
+ /**
19026
+ * Single-flight slot for the resume probe. `handleMessage` awaits this
19027
+ * when `resumeError` is set, so concurrent frames cannot race the retry
19028
+ * — one probe either clears the gate or re-latches it, then every
19029
+ * queued frame re-checks and proceeds. `null` when no probe is in
19030
+ * flight (or after the probe has settled).
19031
+ */
19032
+ resumeProbe = null;
18673
19033
  async bootstrap() {
18674
19034
  try {
18675
19035
  await this.deps.ensureReady?.();
@@ -18677,7 +19037,7 @@ var SddWizardWebSocketHandler = class {
18677
19037
  }
18678
19038
  await this.tryResume();
18679
19039
  }
18680
- /** Rehydrate a persisted interview if one exists on disk. */
19040
+ /** Rehydrate a persisted interview if one exists. */
18681
19041
  async tryResume() {
18682
19042
  if (this.driver) return;
18683
19043
  try {
@@ -18686,9 +19046,11 @@ var SddWizardWebSocketHandler = class {
18686
19046
  this.driver = driver;
18687
19047
  this.lastAgentText = driver.getLastAgentText() ?? "";
18688
19048
  }
18689
- } catch {
19049
+ this.resumeError = null;
19050
+ } catch (error2) {
18690
19051
  this.driver = null;
18691
19052
  this.lastAgentText = "";
19053
+ this.resumeError = error2 instanceof Error ? error2.message : String(error2);
18692
19054
  }
18693
19055
  }
18694
19056
  addClient(ws) {
@@ -18697,6 +19059,10 @@ var SddWizardWebSocketHandler = class {
18697
19059
  ws.on("close", () => this.clients.delete(client));
18698
19060
  ws.on("error", () => this.clients.delete(client));
18699
19061
  void this.ready.then(() => {
19062
+ if (this.resumeError) {
19063
+ this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
19064
+ return;
19065
+ }
18700
19066
  if (this.driver) {
18701
19067
  this.send(client, this.snapshotMsg());
18702
19068
  if (this.lastAgentText) {
@@ -18708,6 +19074,22 @@ var SddWizardWebSocketHandler = class {
18708
19074
  async handleMessage(msg) {
18709
19075
  try {
18710
19076
  await this.ready;
19077
+ if (this.resumeError) {
19078
+ if (this.resumeProbe === null) {
19079
+ const probe = Promise.resolve().then(() => this.tryResume());
19080
+ this.resumeProbe = probe;
19081
+ void probe.finally(() => {
19082
+ setTimeout(() => {
19083
+ if (this.resumeProbe === probe) this.resumeProbe = null;
19084
+ }, 0);
19085
+ });
19086
+ }
19087
+ await this.resumeProbe;
19088
+ if (this.resumeError) {
19089
+ this.broadcast({ type: "sdd.spec.error", payload: { message: this.resumeError } });
19090
+ return;
19091
+ }
19092
+ }
18711
19093
  switch (msg.type) {
18712
19094
  case "sdd.spec.start":
18713
19095
  await this.onStart(String(msg.payload?.goal ?? "").trim(), {
@@ -18724,7 +19106,10 @@ var SddWizardWebSocketHandler = class {
18724
19106
  if (this.driver) {
18725
19107
  this.broadcast(this.snapshotMsg());
18726
19108
  if (this.lastAgentText) {
18727
- this.broadcast({ type: "sdd.spec.agent_text", payload: { text: this.lastAgentText } });
19109
+ this.broadcast({
19110
+ type: "sdd.spec.agent_text",
19111
+ payload: { text: this.lastAgentText }
19112
+ });
18728
19113
  }
18729
19114
  }
18730
19115
  break;
@@ -18938,10 +19323,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
18938
19323
 
18939
19324
  // src/server/setup-events-fleet-broadcaster.ts
18940
19325
  import { watch as fsWatch } from "node:fs";
18941
- import * as path23 from "node:path";
19326
+ import * as path24 from "node:path";
18942
19327
  function registerSetupEventsFleetBroadcaster(deps2) {
18943
19328
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
18944
- const globalRoot = globalConfigPath ? path23.dirname(globalConfigPath) : void 0;
19329
+ const globalRoot = globalConfigPath ? path24.dirname(globalConfigPath) : void 0;
18945
19330
  if (!globalRoot) return void 0;
18946
19331
  const disposers = [];
18947
19332
  const broadcastSessions = async () => {
@@ -18951,8 +19336,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
18951
19336
  const sessions = await registry.list();
18952
19337
  const ownEntry = sessions.find((s) => s.pid === process.pid);
18953
19338
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
18954
- const myRoot = path23.resolve(context.projectRoot);
18955
- const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path23.resolve(s.projectRoot) === myRoot).map((s) => ({
19339
+ const myRoot = path24.resolve(context.projectRoot);
19340
+ const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path24.resolve(s.projectRoot) === myRoot).map((s) => ({
18956
19341
  sessionId: s.sessionId,
18957
19342
  projectName: s.projectName,
18958
19343
  projectSlug: s.projectSlug,
@@ -19142,14 +19527,14 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
19142
19527
 
19143
19528
  // src/server/setup-events-status-watcher.ts
19144
19529
  import { watch as fsWatch2 } from "node:fs";
19145
- import * as fs18 from "node:fs/promises";
19146
- import * as path25 from "node:path";
19530
+ import * as fs19 from "node:fs/promises";
19531
+ import * as path26 from "node:path";
19147
19532
 
19148
19533
  // src/server/setup-events-watcher.ts
19149
- import * as path24 from "node:path";
19534
+ import * as path25 from "node:path";
19150
19535
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
19151
19536
  const raw = String(filename);
19152
- const relative5 = path24.isAbsolute(raw) ? path24.relative(projectsDir, raw) : raw;
19537
+ const relative5 = path25.isAbsolute(raw) ? path25.relative(projectsDir, raw) : raw;
19153
19538
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
19154
19539
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
19155
19540
  return parts.at(-2) ?? null;
@@ -19184,7 +19569,7 @@ function logFileWatcherMetrics(metrics) {
19184
19569
  function registerSetupEventsStatusWatcher(deps2) {
19185
19570
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
19186
19571
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
19187
- const projectsDir = path25.join(wpaths.globalRoot, "projects");
19572
+ const projectsDir = path26.join(wpaths.globalRoot, "projects");
19188
19573
  const knownProjectHashes = /* @__PURE__ */ new Set();
19189
19574
  const debounceTimers = /* @__PURE__ */ new Map();
19190
19575
  const DEBOUNCE_MS2 = 150;
@@ -19229,7 +19614,7 @@ function registerSetupEventsStatusWatcher(deps2) {
19229
19614
  let watcher;
19230
19615
  const startWatcher = async () => {
19231
19616
  try {
19232
- await fs18.mkdir(projectsDir, { recursive: true });
19617
+ await fs19.mkdir(projectsDir, { recursive: true });
19233
19618
  if (isDisposed()) return;
19234
19619
  watcher = fsWatch2(
19235
19620
  projectsDir,
@@ -19243,8 +19628,8 @@ function registerSetupEventsStatusWatcher(deps2) {
19243
19628
  if (!knownProjectHashes.has(projectHash)) return;
19244
19629
  if (watcherMetrics) watcherMetrics.filesProcessed++;
19245
19630
  try {
19246
- const targetFile = path25.join(projectsDir, projectHash, "status.json");
19247
- const content = await fs18.readFile(targetFile, "utf-8");
19631
+ const targetFile = path26.join(projectsDir, projectHash, "status.json");
19632
+ const content = await fs19.readFile(targetFile, "utf-8");
19248
19633
  const statusData = JSON.parse(content);
19249
19634
  scheduleBroadcast(projectHash, statusData);
19250
19635
  } catch {
@@ -19301,8 +19686,8 @@ function registerSetupEventsStatusWatcher(deps2) {
19301
19686
  }
19302
19687
 
19303
19688
  // src/server/setup-events-core-watchers.ts
19304
- import * as fs19 from "node:fs/promises";
19305
- import * as path26 from "node:path";
19689
+ import * as fs20 from "node:fs/promises";
19690
+ import * as path27 from "node:path";
19306
19691
  function registerSetupEventsCoreWatchers(deps2) {
19307
19692
  const { broadcast: broadcast2, clients, context } = deps2;
19308
19693
  const disposers = [];
@@ -19338,9 +19723,9 @@ function registerSetupEventsClientStatusWriter(deps2) {
19338
19723
  if (wpaths?.projectStatus) {
19339
19724
  try {
19340
19725
  const statusFile = wpaths.projectStatus(e.projectHash);
19341
- const dir = path26.dirname(statusFile);
19342
- await fs19.mkdir(dir, { recursive: true });
19343
- await fs19.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
19726
+ const dir = path27.dirname(statusFile);
19727
+ await fs20.mkdir(dir, { recursive: true });
19728
+ await fs20.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
19344
19729
  } catch (err) {
19345
19730
  console.error(
19346
19731
  JSON.stringify({
@@ -20365,7 +20750,7 @@ var SpecsWebSocketHandler = class {
20365
20750
  // src/server/start-webui.ts
20366
20751
  import { randomUUID as randomUUID6 } from "node:crypto";
20367
20752
  import * as http2 from "node:http";
20368
- import * as path33 from "node:path";
20753
+ import * as path34 from "node:path";
20369
20754
  import { createDefaultPipelines } from "@wrongstack/core/agent";
20370
20755
  import { getSharedProjectMailbox as getSharedProjectMailbox5, resolveProjectDir as resolveProjectDir4 } from "@wrongstack/core/coordination";
20371
20756
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
@@ -20379,7 +20764,7 @@ import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } fro
20379
20764
  import {
20380
20765
  expectDefined as expectDefined4,
20381
20766
  sessionScopedPath as sessionScopedPath3,
20382
- startHeapWatchdog,
20767
+ startSharedHeapWatchdog,
20383
20768
  toErrorMessage as toErrorMessage14,
20384
20769
  wstackGlobalRoot as wstackGlobalRoot4
20385
20770
  } from "@wrongstack/core/utils";
@@ -20388,10 +20773,8 @@ import { toLanguagePackageInput } from "@wrongstack/techstack";
20388
20773
  import { ensureSessionShell } from "@wrongstack/tools";
20389
20774
 
20390
20775
  // src/server/backend-services.ts
20391
- import { join as join15 } from "node:path";
20392
- import {
20393
- Agent
20394
- } from "@wrongstack/core/agent";
20776
+ import { join as join16 } from "node:path";
20777
+ import { Agent } from "@wrongstack/core/agent";
20395
20778
  import {
20396
20779
  BrainDecisionLedger,
20397
20780
  BrainMonitor,
@@ -20403,8 +20786,8 @@ import {
20403
20786
  mailboxSessionTag,
20404
20787
  ObservableBrainArbiter as ObservableBrainArbiterCtor
20405
20788
  } from "@wrongstack/core/coordination";
20406
- import { installDesignStudioMiddleware } from "@wrongstack/core/design";
20407
20789
  import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
20790
+ import { installDesignStudioMiddleware } from "@wrongstack/core/design";
20408
20791
  import {
20409
20792
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
20410
20793
  createBrainRuntime,
@@ -20414,7 +20797,9 @@ import {
20414
20797
  } from "@wrongstack/core/execution";
20415
20798
  import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
20416
20799
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
20417
- import { resolveContextWindowPolicy as resolveContextWindowPolicy2 } from "@wrongstack/core/types";
20800
+ import {
20801
+ resolveContextWindowPolicy as resolveContextWindowPolicy2
20802
+ } from "@wrongstack/core/types";
20418
20803
  import {
20419
20804
  estimateRequestTokensCalibrated,
20420
20805
  toErrorMessage as toErrorMessage10
@@ -20431,7 +20816,7 @@ import {
20431
20816
  import { spawn as spawn4 } from "node:child_process";
20432
20817
  import { createRequire } from "node:module";
20433
20818
  import { existsSync as existsSync2 } from "node:fs";
20434
- import { dirname as dirname10, join as join13 } from "node:path";
20819
+ import { dirname as dirname10, join as join14 } from "node:path";
20435
20820
  import { resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
20436
20821
  import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
20437
20822
  import { readLiveLock } from "@wrongstack/core/coordination";
@@ -20560,7 +20945,7 @@ function mailboxServeInvocation(projectRoot) {
20560
20945
  function findWorkspaceCliEntry(projectRoot) {
20561
20946
  let dir = projectRoot;
20562
20947
  for (let i = 0; i < 6; i++) {
20563
- const candidate = join13(dir, "packages", "cli", "dist", "index.js");
20948
+ const candidate = join14(dir, "packages", "cli", "dist", "index.js");
20564
20949
  if (existsSync2(candidate)) return candidate;
20565
20950
  const parent = dirname10(dir);
20566
20951
  if (parent === dir) return null;
@@ -20805,10 +21190,10 @@ function clampDim(value, fallback) {
20805
21190
  }
20806
21191
 
20807
21192
  // src/server/worktree-ws-handler.ts
20808
- import { join as join14, resolve as resolve13, sep as sep5 } from "node:path";
21193
+ import { join as join15, resolve as resolve13, sep as sep5 } from "node:path";
21194
+ import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
20809
21195
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
20810
21196
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
20811
- import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
20812
21197
  var MAX_ACTIVITY = 6;
20813
21198
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
20814
21199
  var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
@@ -20827,6 +21212,20 @@ var WorktreeWebSocketHandler = class {
20827
21212
  baseBranch = "";
20828
21213
  broadcastInterval = null;
20829
21214
  offs = [];
21215
+ /**
21216
+ * Single-flight guard for orphan scans. Two concurrent `scanAndBroadcast()`
21217
+ * calls would race: the first call reads `wt.listManaged()`, then the
21218
+ * second call's mutation completes (e.g. `removeOne`), then the first call
21219
+ * broadcasts a stale orphan list — the user sees a row persist and clicks
21220
+ * Remove a second time, hitting "remove failed (not a managed worktree?)".
21221
+ *
21222
+ * We coalesce concurrent triggers: while a scan is in flight, additional
21223
+ * callers are remembered as "need a re-scan" and exactly one follow-up
21224
+ * scan runs after the in-flight one finishes. The re-scan picks up any
21225
+ * mutations that landed during the previous scan's `listManaged()` window.
21226
+ */
21227
+ scanInFlight = null;
21228
+ scanRescanNeeded = false;
20830
21229
  addClient(ws) {
20831
21230
  this.clients.add(ws);
20832
21231
  ws.on("close", () => this.clients.delete(ws));
@@ -20845,7 +21244,10 @@ var WorktreeWebSocketHandler = class {
20845
21244
  return true;
20846
21245
  }
20847
21246
  if (msg.type === "worktree.remove") {
20848
- await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
21247
+ await this.removeOne(
21248
+ msg.payload?.["dir"],
21249
+ msg.payload?.["branch"]
21250
+ );
20849
21251
  return true;
20850
21252
  }
20851
21253
  if (msg.type === "worktree.merge") {
@@ -20853,7 +21255,10 @@ var WorktreeWebSocketHandler = class {
20853
21255
  return true;
20854
21256
  }
20855
21257
  if (msg.type === "worktree.diff") {
20856
- await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
21258
+ await this.diffOne(
21259
+ msg.payload?.["dir"],
21260
+ msg.payload?.["baseBranch"]
21261
+ );
20857
21262
  return true;
20858
21263
  }
20859
21264
  return false;
@@ -20866,7 +21271,7 @@ var WorktreeWebSocketHandler = class {
20866
21271
  // ── orphan management ─────────────────────────────────────────────────────
20867
21272
  /** Absolute managed-worktrees root for this project. */
20868
21273
  worktreesRoot() {
20869
- return resolve13(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
21274
+ return resolve13(join15(this.management.projectRoot, ".wrongstack", "worktrees"));
20870
21275
  }
20871
21276
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
20872
21277
  underRoot(dir) {
@@ -20882,12 +21287,43 @@ var WorktreeWebSocketHandler = class {
20882
21287
  }
20883
21288
  return live;
20884
21289
  }
21290
+ /**
21291
+ * Coalesced orphan scan entry point. Multiple concurrent callers share one
21292
+ * in-flight scan; if a caller arrives during a scan, a single follow-up
21293
+ * scan is scheduled to pick up mutations that landed mid-scan. Returns the
21294
+ * shared promise so `await this.scanAndBroadcast()` still works for callers
21295
+ * that want to chain after the next broadcast.
21296
+ */
21297
+ scanAndBroadcast() {
21298
+ if (this.scanInFlight) {
21299
+ this.scanRescanNeeded = true;
21300
+ return this.scanInFlight;
21301
+ }
21302
+ this.scanInFlight = this.runScanAndMaybeRescan();
21303
+ return this.scanInFlight;
21304
+ }
21305
+ /**
21306
+ * Run the actual scan, then drain a pending re-scan request if one came in
21307
+ * during the scan. The re-scan is bounded (one follow-up) — additional
21308
+ * requests that arrive during the follow-up coalesce into the next one.
21309
+ */
21310
+ async runScanAndMaybeRescan() {
21311
+ try {
21312
+ await this.runScanOnce();
21313
+ while (this.scanRescanNeeded) {
21314
+ this.scanRescanNeeded = false;
21315
+ await this.runScanOnce();
21316
+ }
21317
+ } finally {
21318
+ this.scanInFlight = null;
21319
+ }
21320
+ }
20885
21321
  /**
20886
21322
  * Scan the disk for managed worktrees/branches NOT owned by a live in-session
20887
21323
  * run and broadcast them as orphans, with whether it is safe to clean now.
20888
21324
  * No-op (empty inventory) when management deps were not wired.
20889
21325
  */
20890
- async scanAndBroadcast() {
21326
+ async runScanOnce() {
20891
21327
  if (!this.management) {
20892
21328
  this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
20893
21329
  return;
@@ -20943,7 +21379,8 @@ var WorktreeWebSocketHandler = class {
20943
21379
  }
20944
21380
  const res = await cleanupStaleSddWorktrees2({
20945
21381
  projectRoot: this.management.projectRoot,
20946
- boardsDir: this.management.boardsDir
21382
+ boardsDir: this.management.boardsDir,
21383
+ stateTransport: "kanban"
20947
21384
  });
20948
21385
  if (res.skippedReason) {
20949
21386
  this.broadcast({
@@ -20956,26 +21393,45 @@ var WorktreeWebSocketHandler = class {
20956
21393
  for (const [id, h] of [...this.handles]) {
20957
21394
  if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
20958
21395
  }
20959
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: true, removed: res.removed } });
21396
+ this.broadcast({
21397
+ type: "worktree.cleanup_result",
21398
+ payload: { ok: true, removed: res.removed }
21399
+ });
20960
21400
  this.broadcastState();
20961
21401
  await this.scanAndBroadcast();
20962
21402
  }
20963
21403
  /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
20964
21404
  async removeOne(dir, branch) {
20965
21405
  if (!this.management || !dir && !branch) {
20966
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "nothing to remove" } });
21406
+ this.broadcast({
21407
+ type: "worktree.cleanup_result",
21408
+ payload: { ok: false, removed: 0, reason: "nothing to remove" }
21409
+ });
20967
21410
  return;
20968
21411
  }
20969
21412
  if (branch && !MANAGED_BRANCH_RE.test(branch)) {
20970
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "not a managed worktree branch" } });
21413
+ this.broadcast({
21414
+ type: "worktree.cleanup_result",
21415
+ payload: { ok: false, removed: 0, reason: "not a managed worktree branch" }
21416
+ });
20971
21417
  return;
20972
21418
  }
20973
21419
  if (dir && !this.underRoot(dir)) {
20974
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" } });
21420
+ this.broadcast({
21421
+ type: "worktree.cleanup_result",
21422
+ payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" }
21423
+ });
20975
21424
  return;
20976
21425
  }
20977
21426
  if (branch && this.liveActiveBranches().has(branch)) {
20978
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: false, removed: 0, reason: "a run is live on this worktree \u2014 stop it first" } });
21427
+ this.broadcast({
21428
+ type: "worktree.cleanup_result",
21429
+ payload: {
21430
+ ok: false,
21431
+ removed: 0,
21432
+ reason: "a run is live on this worktree \u2014 stop it first"
21433
+ }
21434
+ });
20979
21435
  return;
20980
21436
  }
20981
21437
  let removed = false;
@@ -20984,31 +21440,54 @@ var WorktreeWebSocketHandler = class {
20984
21440
  ({ removed } = await wt.removeOne(dir, branch));
20985
21441
  }
20986
21442
  for (const [id, h] of [...this.handles]) {
20987
- if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId)) this.handles.delete(id);
21443
+ if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId))
21444
+ this.handles.delete(id);
20988
21445
  }
20989
- this.broadcast({ type: "worktree.cleanup_result", payload: { ok: removed, removed: removed ? 1 : 0, reason: removed ? void 0 : "remove failed (not a managed worktree?)" } });
21446
+ this.broadcast({
21447
+ type: "worktree.cleanup_result",
21448
+ payload: {
21449
+ ok: removed,
21450
+ removed: removed ? 1 : 0,
21451
+ reason: removed ? void 0 : "remove failed (not a managed worktree?)"
21452
+ }
21453
+ });
20990
21454
  this.broadcastState();
20991
21455
  await this.scanAndBroadcast();
20992
21456
  }
20993
21457
  /** Squash-merge ONE branch into base. Refused while a live run owns it. */
20994
21458
  async mergeBranch(branch) {
20995
21459
  if (!this.management || !branch) {
20996
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch: branch ?? "", reason: "no branch" } });
21460
+ this.broadcast({
21461
+ type: "worktree.merge_result",
21462
+ payload: { ok: false, branch: branch ?? "", reason: "no branch" }
21463
+ });
20997
21464
  return;
20998
21465
  }
20999
21466
  if (!MANAGED_BRANCH_RE.test(branch)) {
21000
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "not a managed worktree branch" } });
21467
+ this.broadcast({
21468
+ type: "worktree.merge_result",
21469
+ payload: { ok: false, branch, reason: "not a managed worktree branch" }
21470
+ });
21001
21471
  return;
21002
21472
  }
21003
21473
  if (this.liveActiveBranches().has(branch)) {
21004
- this.broadcast({ type: "worktree.merge_result", payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" } });
21474
+ this.broadcast({
21475
+ type: "worktree.merge_result",
21476
+ payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" }
21477
+ });
21005
21478
  return;
21006
21479
  }
21007
21480
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
21008
21481
  const res = await wt.mergeBranch(branch);
21009
21482
  this.broadcast({
21010
21483
  type: "worktree.merge_result",
21011
- payload: { ok: res.ok, branch, conflict: res.conflict, conflictFiles: res.conflictFiles, reason: res.reason }
21484
+ payload: {
21485
+ ok: res.ok,
21486
+ branch,
21487
+ conflict: res.conflict,
21488
+ conflictFiles: res.conflictFiles,
21489
+ reason: res.reason
21490
+ }
21012
21491
  });
21013
21492
  await this.scanAndBroadcast();
21014
21493
  }
@@ -21050,8 +21529,14 @@ var WorktreeWebSocketHandler = class {
21050
21529
  }),
21051
21530
  on("worktree.committed", (p) => {
21052
21531
  const e = p;
21053
- this.patch(e.handleId, { status: "committing", insertions: e.insertions, deletions: e.deletions, files: e.files });
21054
- if (e.committed) this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
21532
+ this.patch(e.handleId, {
21533
+ status: "committing",
21534
+ insertions: e.insertions,
21535
+ deletions: e.deletions,
21536
+ files: e.files
21537
+ });
21538
+ if (e.committed)
21539
+ this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
21055
21540
  this.broadcastState();
21056
21541
  }),
21057
21542
  on("worktree.merged", (p) => {
@@ -21092,10 +21577,15 @@ var WorktreeWebSocketHandler = class {
21092
21577
  activity(id, kind, text2) {
21093
21578
  const cur = this.handles.get(id);
21094
21579
  if (cur) {
21095
- const recentActivity = [...cur.recentActivity, { kind, text: text2, at: Date.now() }].slice(-MAX_ACTIVITY);
21580
+ const recentActivity = [...cur.recentActivity, { kind, text: text2, at: Date.now() }].slice(
21581
+ -MAX_ACTIVITY
21582
+ );
21096
21583
  this.handles.set(id, { ...cur, recentActivity });
21097
21584
  }
21098
- this.broadcast({ type: "worktree.event", payload: { kind, handleId: id, text: text2, at: Date.now() } });
21585
+ this.broadcast({
21586
+ type: "worktree.event",
21587
+ payload: { kind, handleId: id, text: text2, at: Date.now() }
21588
+ });
21099
21589
  }
21100
21590
  stateMessage() {
21101
21591
  return {
@@ -21337,7 +21827,7 @@ async function createAgentServices(input) {
21337
21827
  const brainCfg = resolveBrainConfigDefaults(config.brain, {
21338
21828
  fallbackModels: config.fallbackModels
21339
21829
  });
21340
- const brainLedgerPath = join15(wpaths.projectDir, "brain-ledger.jsonl");
21830
+ const brainLedgerPath = join16(wpaths.projectDir, "brain-ledger.jsonl");
21341
21831
  let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
21342
21832
  let brainLedger;
21343
21833
  const startBrainLedger = () => {
@@ -21402,33 +21892,35 @@ async function createAgentServices(input) {
21402
21892
  brainLog.push(entry);
21403
21893
  if (brainLog.length > 20) brainLog.shift();
21404
21894
  };
21405
- events.on(
21406
- "brain.decision_answered",
21407
- (e) => pushBrainLog({
21408
- at: e.at,
21409
- kind: "answered",
21410
- question: e.request.question,
21411
- outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
21412
- })
21413
- );
21414
- events.on(
21415
- "brain.decision_ask_human",
21416
- (e) => pushBrainLog({
21417
- at: e.at,
21418
- kind: "ask_human",
21419
- question: e.request.question,
21420
- outcome: "needs human judgement"
21421
- })
21422
- );
21423
- events.on(
21424
- "brain.decision_denied",
21425
- (e) => pushBrainLog({
21426
- at: e.at,
21427
- kind: "denied",
21428
- question: e.request.question,
21429
- outcome: e.decision.type === "deny" ? e.decision.reason : ""
21430
- })
21431
- );
21895
+ const brainLogOffs = [
21896
+ events.on(
21897
+ "brain.decision_answered",
21898
+ (e) => pushBrainLog({
21899
+ at: e.at,
21900
+ kind: "answered",
21901
+ question: e.request.question,
21902
+ outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
21903
+ })
21904
+ ),
21905
+ events.on(
21906
+ "brain.decision_ask_human",
21907
+ (e) => pushBrainLog({
21908
+ at: e.at,
21909
+ kind: "ask_human",
21910
+ question: e.request.question,
21911
+ outcome: "needs human judgement"
21912
+ })
21913
+ ),
21914
+ events.on(
21915
+ "brain.decision_denied",
21916
+ (e) => pushBrainLog({
21917
+ at: e.at,
21918
+ kind: "denied",
21919
+ question: e.request.question,
21920
+ outcome: e.decision.type === "deny" ? e.decision.reason : ""
21921
+ })
21922
+ )
21923
+ ];
21432
21924
  const brainMailbox = getSharedProjectMailbox3(wpaths.projectDir, events);
21433
21925
  brainMonitor = new BrainMonitor({
21434
21926
  events,
@@ -21540,6 +22032,17 @@ async function createAgentServices(input) {
21540
22032
  getActiveSessionId: () => context.session.id
21541
22033
  }
21542
22034
  );
22035
+ let realtimeHandlersDisposed = false;
22036
+ const disposeRealtimeHandlers = () => {
22037
+ if (realtimeHandlersDisposed) return;
22038
+ realtimeHandlersDisposed = true;
22039
+ for (const off of brainLogOffs) off();
22040
+ goalHandler.dispose();
22041
+ sddBoardHandler.dispose();
22042
+ worktreeHandler.dispose();
22043
+ terminalHandler.dispose();
22044
+ collabHandler.dispose();
22045
+ };
21543
22046
  return {
21544
22047
  collabBus,
21545
22048
  compactor,
@@ -21565,6 +22068,7 @@ async function createAgentServices(input) {
21565
22068
  worktreeHandler,
21566
22069
  terminalHandler,
21567
22070
  collabHandler,
22071
+ disposeRealtimeHandlers,
21568
22072
  updateAutoCompactionMaxContext
21569
22073
  };
21570
22074
  }
@@ -21651,7 +22155,7 @@ function createConnectionHandler(options) {
21651
22155
  }
21652
22156
 
21653
22157
  // src/server/message-dispatcher.ts
21654
- import path27 from "node:path";
22158
+ import path28 from "node:path";
21655
22159
  function createMessageDispatcher(opts) {
21656
22160
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
21657
22161
  function makeWorklistContext() {
@@ -21672,7 +22176,7 @@ function createMessageDispatcher(opts) {
21672
22176
  skillLoader: deps2.skillLoader,
21673
22177
  skillInstaller: deps2.skillInstaller,
21674
22178
  projectRoot,
21675
- projectSkillsDir: path27.join(projectRoot, ".wrongstack", "skills"),
22179
+ projectSkillsDir: path28.join(projectRoot, ".wrongstack", "skills"),
21676
22180
  globalSkillsDir: deps2.wpaths.globalSkills
21677
22181
  };
21678
22182
  }
@@ -21924,7 +22428,7 @@ function createMessageDispatcher(opts) {
21924
22428
 
21925
22429
  // src/server/pre-context-services.ts
21926
22430
  import { createRequire as createRequire3 } from "node:module";
21927
- import * as path30 from "node:path";
22431
+ import * as path31 from "node:path";
21928
22432
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
21929
22433
  import {
21930
22434
  getSharedProjectMailbox as getSharedProjectMailbox4,
@@ -21978,8 +22482,8 @@ import { configureDangerBypass, configureExecPolicy } from "@wrongstack/tools";
21978
22482
  import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/tools/session-kanban";
21979
22483
 
21980
22484
  // src/server/model-auto-discovery.ts
21981
- import * as fs20 from "node:fs/promises";
21982
- import * as path28 from "node:path";
22485
+ import * as fs21 from "node:fs/promises";
22486
+ import * as path29 from "node:path";
21983
22487
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
21984
22488
  function isOverlayRegistry(value) {
21985
22489
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -22005,7 +22509,7 @@ function eligibleProviders(config) {
22005
22509
  }
22006
22510
  async function readCache(file) {
22007
22511
  try {
22008
- return JSON.parse(await fs20.readFile(file, "utf8"));
22512
+ return JSON.parse(await fs21.readFile(file, "utf8"));
22009
22513
  } catch {
22010
22514
  return {};
22011
22515
  }
@@ -22015,7 +22519,7 @@ async function discoverAndMergeWebuiProviders(opts) {
22015
22519
  if (!isOverlayRegistry(registry)) return;
22016
22520
  const targets = eligibleProviders(opts.config);
22017
22521
  if (targets.length === 0) return;
22018
- const cacheFile = path28.join(opts.cacheDir, "discovered-models-cache.json");
22522
+ const cacheFile = path29.join(opts.cacheDir, "discovered-models-cache.json");
22019
22523
  const cache2 = await readCache(cacheFile);
22020
22524
  let cacheDirty = false;
22021
22525
  await Promise.all(
@@ -22052,8 +22556,8 @@ async function discoverAndMergeWebuiProviders(opts) {
22052
22556
  );
22053
22557
  if (cacheDirty) {
22054
22558
  try {
22055
- await fs20.mkdir(path28.dirname(cacheFile), { recursive: true });
22056
- await fs20.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
22559
+ await fs21.mkdir(path29.dirname(cacheFile), { recursive: true });
22560
+ await fs21.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
22057
22561
  } catch {
22058
22562
  opts.logger?.debug?.("provider auto-discovery cache write failed");
22059
22563
  }
@@ -22149,7 +22653,7 @@ function resolveSetupProvider(opts) {
22149
22653
  }
22150
22654
 
22151
22655
  // src/server/standalone-session-identity.ts
22152
- import * as path29 from "node:path";
22656
+ import * as path30 from "node:path";
22153
22657
  import {
22154
22658
  AgentStatusTracker,
22155
22659
  FleetNotifier,
@@ -22168,7 +22672,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
22168
22672
  let activeTarget = {
22169
22673
  projectSlug: paths.projectSlug,
22170
22674
  projectRoot: paths.projectRoot,
22171
- projectName: path29.basename(paths.projectRoot),
22675
+ projectName: path30.basename(paths.projectRoot),
22172
22676
  workingDir: opts.workingDir
22173
22677
  };
22174
22678
  let pendingClaim;
@@ -22399,7 +22903,7 @@ async function createPreContextServices(input) {
22399
22903
  await discoverAndMergeWebuiProviders({
22400
22904
  config,
22401
22905
  registry: modelsRegistry,
22402
- cacheDir: path30.dirname(wpaths.modelsCache),
22906
+ cacheDir: path31.dirname(wpaths.modelsCache),
22403
22907
  logger
22404
22908
  });
22405
22909
  } catch (err) {
@@ -22451,7 +22955,7 @@ async function createPreContextServices(input) {
22451
22955
  configureChildEnvGitIdentity(config.git?.identity ?? null);
22452
22956
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
22453
22957
  const mcpTokenStore = new MCPVaultTokenStore(
22454
- path30.join(wpaths.projectDir, "mcp-auth.json"),
22958
+ path31.join(wpaths.projectDir, "mcp-auth.json"),
22455
22959
  vault
22456
22960
  );
22457
22961
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -22557,7 +23061,7 @@ async function createPreContextServices(input) {
22557
23061
  };
22558
23062
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
22559
23063
  const skillInstaller = config.features.skills ? new SkillInstaller({
22560
- manifestPath: path30.join(wpaths.configDir, "installed-skills.json"),
23064
+ manifestPath: path31.join(wpaths.configDir, "installed-skills.json"),
22561
23065
  projectSkillsDir: wpaths.inProjectSkills,
22562
23066
  globalSkillsDir: wpaths.globalSkills,
22563
23067
  projectHash: wpaths.projectHash,
@@ -22567,8 +23071,8 @@ async function createPreContextServices(input) {
22567
23071
  const bundledPromptsDir = promptsEnabled ? (() => {
22568
23072
  try {
22569
23073
  const req = createRequire3(import.meta.url);
22570
- return path30.join(
22571
- path30.dirname(req.resolve("@wrongstack/core/package.json")),
23074
+ return path31.join(
23075
+ path31.dirname(req.resolve("@wrongstack/core/package.json")),
22572
23076
  "data",
22573
23077
  "prompts"
22574
23078
  );
@@ -22672,7 +23176,7 @@ async function createPreContextServices(input) {
22672
23176
  }
22673
23177
 
22674
23178
  // src/server/routes.ts
22675
- import path31 from "node:path";
23179
+ import path32 from "node:path";
22676
23180
  import { makeProviderFromConfig as makeProviderFromConfig4, withCatalogCapabilities } from "@wrongstack/providers";
22677
23181
 
22678
23182
  // src/server/mode-handlers.ts
@@ -22964,7 +23468,7 @@ function buildRoutes(state, deps2, cb) {
22964
23468
  };
22965
23469
  const mailboxRoutes = createMailboxRouteHandlers({
22966
23470
  getProjectRoot: state.getProjectRoot,
22967
- getGlobalRoot: () => path31.dirname(deps2.globalConfigPath),
23471
+ getGlobalRoot: () => path32.dirname(deps2.globalConfigPath),
22968
23472
  events: deps2.events
22969
23473
  });
22970
23474
  const mcpRoutes = {
@@ -23037,7 +23541,7 @@ function buildRoutes(state, deps2, cb) {
23037
23541
  }
23038
23542
 
23039
23543
  // src/server/server-runtime.ts
23040
- import * as path32 from "node:path";
23544
+ import * as path33 from "node:path";
23041
23545
  import { createRequire as createRequire4 } from "node:module";
23042
23546
  import { fileURLToPath } from "node:url";
23043
23547
  import { WebSocketServer } from "ws";
@@ -23098,7 +23602,7 @@ function createSessionStartPayload(g) {
23098
23602
  inputCost,
23099
23603
  outputCost,
23100
23604
  cacheReadCost,
23101
- projectName: path32.basename(projectRoot) || projectRoot,
23605
+ projectName: path33.basename(projectRoot) || projectRoot,
23102
23606
  projectRoot,
23103
23607
  cwd: g.getWorkingDir(),
23104
23608
  mode: g.getModeId(),
@@ -23186,13 +23690,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
23186
23690
  };
23187
23691
  }
23188
23692
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
23189
- if (explicitDistDir) return path32.resolve(explicitDistDir);
23693
+ if (explicitDistDir) return path33.resolve(explicitDistDir);
23190
23694
  try {
23191
23695
  const requireFromHere2 = createRequire4(fromUrl);
23192
23696
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
23193
- return path32.dirname(serverEntry);
23697
+ return path33.dirname(serverEntry);
23194
23698
  } catch {
23195
- return path32.resolve(path32.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
23699
+ return path33.resolve(path33.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
23196
23700
  }
23197
23701
  }
23198
23702
  function startHttpServer(opts) {
@@ -23406,6 +23910,7 @@ async function startWebUI(opts = {}) {
23406
23910
  worktreeHandler,
23407
23911
  terminalHandler,
23408
23912
  collabHandler,
23913
+ disposeRealtimeHandlers,
23409
23914
  updateAutoCompactionMaxContext
23410
23915
  } = agentServices;
23411
23916
  if (typeof context.meta["yolo"] === "boolean") {
@@ -23584,21 +24089,21 @@ async function startWebUI(opts = {}) {
23584
24089
  });
23585
24090
  }
23586
24091
  async function touchProjectEntry(root, workDir) {
23587
- const resolved = path33.resolve(root);
24092
+ const resolved = path34.resolve(root);
23588
24093
  const manifest = await loadManifest(globalConfigPath);
23589
24094
  const now = (/* @__PURE__ */ new Date()).toISOString();
23590
- const existing = manifest.projects.find((p) => path33.resolve(p.root) === resolved);
24095
+ const existing = manifest.projects.find((p) => path34.resolve(p.root) === resolved);
23591
24096
  if (existing) {
23592
24097
  existing.lastSeen = now;
23593
- if (workDir) existing.lastWorkingDir = path33.resolve(workDir);
24098
+ if (workDir) existing.lastWorkingDir = path34.resolve(workDir);
23594
24099
  } else {
23595
24100
  manifest.projects.push({
23596
- name: path33.basename(resolved),
24101
+ name: path34.basename(resolved),
23597
24102
  root: resolved,
23598
24103
  slug: generateProjectSlug(resolved),
23599
24104
  createdAt: now,
23600
24105
  lastSeen: now,
23601
- lastWorkingDir: workDir ? path33.resolve(workDir) : void 0
24106
+ lastWorkingDir: workDir ? path34.resolve(workDir) : void 0
23602
24107
  });
23603
24108
  }
23604
24109
  await saveManifest(manifest, globalConfigPath);
@@ -23775,7 +24280,20 @@ async function startWebUI(opts = {}) {
23775
24280
  );
23776
24281
  credentialWatcherClose = credentialWatcher.close;
23777
24282
  }
23778
- const stopHeapWatchdog = startHeapWatchdog();
24283
+ const stopHeapWatchdog = startSharedHeapWatchdog({
24284
+ collectStats: () => ({
24285
+ surface: opts.surface ?? "webui",
24286
+ sessionId: context.session.id,
24287
+ messages: context.state.messages.length,
24288
+ messageEstimatedTokens: context.state.messages.reduce(
24289
+ (sum, message) => sum + (message._estTokens ?? 0),
24290
+ 0
24291
+ ),
24292
+ webClients: clients.size,
24293
+ pendingConfirms: pendingConfirms.size,
24294
+ runActive: runLockControl.get() !== null
24295
+ })
24296
+ });
23779
24297
  const routes = buildRoutes(state, deps2, cb);
23780
24298
  const handleMessage = createMessageDispatcher({
23781
24299
  state,
@@ -23853,6 +24371,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23853
24371
  await todosCheckpoint.detach();
23854
24372
  await stopHeapWatchdog();
23855
24373
  credentialWatcherClose?.();
24374
+ disposeRealtimeHandlers();
23856
24375
  brainMonitor.stop();
23857
24376
  await agentServices.brainLedger?.stop().catch(() => {
23858
24377
  });
@@ -23876,7 +24395,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23876
24395
  await memoryStore.dispose().catch(
23877
24396
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage14(err)}`)
23878
24397
  );
23879
- await unregisterInstance(process.pid, path33.dirname(globalConfigPath));
24398
+ await unregisterInstance(process.pid, path34.dirname(globalConfigPath));
23880
24399
  }
23881
24400
  });
23882
24401
  }