@wrongstack/webui-server 0.296.4 → 0.297.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/index.js +934 -429
  2. package/dist/index.js.map +4 -4
  3. package/dist/protocol/client-conversation.d.ts +1 -1
  4. package/dist/protocol/client-conversation.d.ts.map +1 -1
  5. package/dist/protocol/index.js +7 -1
  6. package/dist/protocol/index.js.map +2 -2
  7. package/dist/protocol/projections.d.ts +5 -1
  8. package/dist/protocol/projections.d.ts.map +1 -1
  9. package/dist/protocol/registry.d.ts +2 -2
  10. package/dist/protocol/registry.d.ts.map +1 -1
  11. package/dist/protocol/server-conversation.d.ts +1 -1
  12. package/dist/protocol/server-conversation.d.ts.map +1 -1
  13. package/dist/server/backend-services.d.ts +6 -2
  14. package/dist/server/backend-services.d.ts.map +1 -1
  15. package/dist/server/collaboration-ws-handler.d.ts +2 -2
  16. package/dist/server/collaboration-ws-handler.d.ts.map +1 -1
  17. package/dist/server/embedded-lifecycle.d.ts +2 -0
  18. package/dist/server/embedded-lifecycle.d.ts.map +1 -1
  19. package/dist/server/entry.js +856 -395
  20. package/dist/server/entry.js.map +4 -4
  21. package/dist/server/goal-ws-handler.d.ts +3 -1
  22. package/dist/server/goal-ws-handler.d.ts.map +1 -1
  23. package/dist/server/handlers.js.map +2 -2
  24. package/dist/server/http-server.d.ts.map +1 -1
  25. package/dist/server/kanban-supervisor.d.ts +6 -0
  26. package/dist/server/kanban-supervisor.d.ts.map +1 -1
  27. package/dist/server/mailbox-handlers.d.ts +2 -1
  28. package/dist/server/mailbox-handlers.d.ts.map +1 -1
  29. package/dist/server/mailbox-routes.d.ts +1 -0
  30. package/dist/server/mailbox-routes.d.ts.map +1 -1
  31. package/dist/server/memory-diagnostics.d.ts +69 -0
  32. package/dist/server/memory-diagnostics.d.ts.map +1 -0
  33. package/dist/server/sdd-board-ws-handler.d.ts +14 -8
  34. package/dist/server/sdd-board-ws-handler.d.ts.map +1 -1
  35. package/dist/server/sdd-wizard-wiring.d.ts +5 -6
  36. package/dist/server/sdd-wizard-wiring.d.ts.map +1 -1
  37. package/dist/server/sdd-wizard-ws-handler.d.ts +16 -6
  38. package/dist/server/sdd-wizard-ws-handler.d.ts.map +1 -1
  39. package/dist/server/start-webui.d.ts.map +1 -1
  40. package/dist/server/types.d.ts +2 -0
  41. package/dist/server/types.d.ts.map +1 -1
  42. package/dist/server/worktree-ws-handler.d.ts +30 -2
  43. package/dist/server/worktree-ws-handler.d.ts.map +1 -1
  44. package/dist/server/ws-payload-validation.d.ts +8 -0
  45. package/dist/server/ws-payload-validation.d.ts.map +1 -1
  46. package/package.json +10 -10
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
  }
@@ -5746,15 +5733,15 @@ async function handleGitChanges(ws, projectRoot) {
5746
5733
  if (!m) continue;
5747
5734
  const added = m[1] === "-" ? 0 : Number(m[1]);
5748
5735
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5749
- let path34 = m[3] ?? "";
5750
- if (path34 === "") {
5736
+ let path35 = m[3] ?? "";
5737
+ if (path35 === "") {
5751
5738
  i += 1;
5752
- path34 = parts[i + 1] ?? parts[i] ?? "";
5739
+ path35 = parts[i + 1] ?? parts[i] ?? "";
5753
5740
  i += 1;
5754
5741
  }
5755
- if (!path34) continue;
5756
- const prev = counts.get(path34) ?? { added: 0, deleted: 0 };
5757
- 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 });
5758
5745
  }
5759
5746
  };
5760
5747
  parseNumstat(unstagedNumstat);
@@ -5766,7 +5753,7 @@ async function handleGitChanges(ws, projectRoot) {
5766
5753
  if (!rec || rec.length < 3) continue;
5767
5754
  const x = rec[0] ?? " ";
5768
5755
  const y = rec[1] ?? " ";
5769
- const path34 = rec.slice(3);
5756
+ const path35 = rec.slice(3);
5770
5757
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5771
5758
  if (isRename) i += 1;
5772
5759
  let status;
@@ -5778,13 +5765,13 @@ async function handleGitChanges(ws, projectRoot) {
5778
5765
  else if (x === "D" || y === "D") status = "D";
5779
5766
  else status = "M";
5780
5767
  const staged = x !== " " && x !== "?";
5781
- let added = counts.get(path34)?.added ?? 0;
5782
- let deleted = counts.get(path34)?.deleted ?? 0;
5768
+ let added = counts.get(path35)?.added ?? 0;
5769
+ let deleted = counts.get(path35)?.deleted ?? 0;
5783
5770
  if (status === "?") {
5784
5771
  added = 0;
5785
5772
  deleted = 0;
5786
5773
  }
5787
- files.push({ path: path34, status, added, deleted, staged });
5774
+ files.push({ path: path35, status, added, deleted, staged });
5788
5775
  }
5789
5776
  send(ws, { type: "git.changes", payload: { files } });
5790
5777
  } catch (err) {
@@ -5795,21 +5782,21 @@ async function handleGitChanges(ws, projectRoot) {
5795
5782
  }
5796
5783
  }
5797
5784
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5798
- async function handleGitDiff(ws, projectRoot, path34) {
5785
+ async function handleGitDiff(ws, projectRoot, path35) {
5799
5786
  const cwd = projectRoot || void 0;
5800
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path34, ...extra } });
5801
- 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)) {
5802
5789
  reply2({ oldText: "", newText: "", error: "invalid path" });
5803
5790
  return;
5804
5791
  }
5805
5792
  try {
5806
5793
  const git = makeGit(cwd);
5807
5794
  const { readFile: readFile13 } = await import("node:fs/promises");
5808
- const { join: join18 } = await import("node:path");
5809
- const oldText = await git(["show", `HEAD:${path34}`]);
5795
+ const { join: join19 } = await import("node:path");
5796
+ const oldText = await git(["show", `HEAD:${path35}`]);
5810
5797
  let newText = "";
5811
5798
  try {
5812
- const abs = cwd ? join18(cwd, path34) : path34;
5799
+ const abs = cwd ? join19(cwd, path35) : path35;
5813
5800
  const buf = await readFile13(abs);
5814
5801
  if (buf.includes(0)) {
5815
5802
  reply2({ oldText: "", newText: "", binary: true });
@@ -5871,10 +5858,7 @@ async function handleGoalSnapshotRoute(ws, msg, handlers) {
5871
5858
  }
5872
5859
 
5873
5860
  // src/server/goal-ws-handler.ts
5874
- import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5875
- import {
5876
- assignNickname
5877
- } from "@wrongstack/core/coordination";
5861
+ import { assignNickname } from "@wrongstack/core/coordination";
5878
5862
  import {
5879
5863
  GoalAssessor,
5880
5864
  GoalPlanner,
@@ -5882,6 +5866,7 @@ import {
5882
5866
  PhaseOrchestrator,
5883
5867
  PhaseStore
5884
5868
  } from "@wrongstack/core/goal";
5869
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
5885
5870
  import { WorktreeManager } from "@wrongstack/core/worktree";
5886
5871
 
5887
5872
  // src/server/git-process.ts
@@ -5918,12 +5903,7 @@ function deriveTitle(goal) {
5918
5903
  return trimmed || "Goal";
5919
5904
  }
5920
5905
  async function commitsSince(cwd, baseSha, branch) {
5921
- const output = await gitStdout(cwd, [
5922
- "log",
5923
- "--reverse",
5924
- "--format=%H",
5925
- `${baseSha}..${branch}`
5926
- ]);
5906
+ const output = await gitStdout(cwd, ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`]);
5927
5907
  if (output === null) return [];
5928
5908
  return output.split("\n").map((s) => s.trim()).filter(Boolean);
5929
5909
  }
@@ -5974,6 +5954,17 @@ var GoalWebSocketHandler = class {
5974
5954
  ws.on("error", () => this.clients.delete(client));
5975
5955
  this.sendState(client);
5976
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
+ }
5977
5968
  async handleMessage(ws, msg) {
5978
5969
  switch (msg.type) {
5979
5970
  case "goal.assess":
@@ -6021,7 +6012,8 @@ var GoalWebSocketHandler = class {
6021
6012
  }
6022
6013
  case "goal.assignTask": {
6023
6014
  const { taskId, agentId, agentName } = msg.payload;
6024
- if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
6015
+ if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName))
6016
+ this.afterBoardMutation();
6025
6017
  break;
6026
6018
  }
6027
6019
  case "goal.addTask": {
@@ -6066,7 +6058,10 @@ var GoalWebSocketHandler = class {
6066
6058
  this.graph = graph;
6067
6059
  this.broadcast({ type: "goal.state", payload: this.buildState() });
6068
6060
  } else {
6069
- 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
+ });
6070
6065
  }
6071
6066
  }
6072
6067
  break;
@@ -6089,10 +6084,13 @@ var GoalWebSocketHandler = class {
6089
6084
  const mySeq = ++this.assessSeq;
6090
6085
  const sendResult7 = (result) => {
6091
6086
  if (mySeq !== this.assessSeq) return;
6092
- sendSerialized(ws, JSON.stringify({
6093
- type: "goal.assess.result",
6094
- payload: { ...result, reqSeq: seq }
6095
- }));
6087
+ sendSerialized(
6088
+ ws,
6089
+ JSON.stringify({
6090
+ type: "goal.assess.result",
6091
+ payload: { ...result, reqSeq: seq }
6092
+ })
6093
+ );
6096
6094
  };
6097
6095
  if (!goal.trim()) {
6098
6096
  sendResult7({
@@ -6138,15 +6136,24 @@ var GoalWebSocketHandler = class {
6138
6136
  const multiBoard = payload?.multiBoard ?? false;
6139
6137
  const verifyTasks = payload?.verifyTasks ?? false;
6140
6138
  const chimeraReview = payload?.chimeraReview ?? false;
6141
- this.abort = new AbortController();
6139
+ const runAbort = new AbortController();
6140
+ this.abort = runAbort;
6142
6141
  this.stopping = false;
6143
- const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, this.abort.signal);
6144
- 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) {
6145
6144
  this.broadcast({ type: "goal.stopped", payload: { title } });
6146
6145
  return;
6147
6146
  }
6148
6147
  this.logger.info(`[Goal] Starting: ${title}`);
6149
- 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();
6150
6157
  this.graph = graph;
6151
6158
  await this.store.save(graph);
6152
6159
  const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_GOAL_WORKTREES"] !== "0";
@@ -6165,9 +6172,10 @@ var GoalWebSocketHandler = class {
6165
6172
  maybeVerify.verifyPhase = (async (phase, env) => {
6166
6173
  const cwd = env?.cwd ?? this.projectRoot;
6167
6174
  try {
6168
- const { exec } = await import("node:child_process");
6175
+ const { execFile: execFile2 } = await import("node:child_process");
6169
6176
  const result = await new Promise((resolve16) => {
6170
- 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) => {
6171
6179
  if (err && err.code === "ENOENT") {
6172
6180
  resolve16("[verify] tsc not found \u2014 skipping");
6173
6181
  return;
@@ -6309,11 +6317,41 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
6309
6317
  /** Generic fallback phases when the LLM planner produces nothing usable. */
6310
6318
  defaultPhases() {
6311
6319
  return [
6312
- { name: "Discovery", description: "Requirements gathering", priority: "high", estimateHours: 2, parallelizable: false },
6313
- { name: "Design", description: "Architecture and design", priority: "critical", estimateHours: 4, parallelizable: false },
6314
- { name: "Implementation", description: "Core development", priority: "critical", estimateHours: 12, parallelizable: false },
6315
- { name: "Testing", description: "Unit and integration tests", priority: "high", estimateHours: 6, parallelizable: true },
6316
- { 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
+ }
6317
6355
  ];
6318
6356
  }
6319
6357
  /** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
@@ -6394,8 +6432,10 @@ Type: ${task.type}`;
6394
6432
  try {
6395
6433
  const result_ = await this.agent.run(reviewPrompt);
6396
6434
  if (result_.status === "done" && result_.finalText) {
6397
- this.logger.info(`[Goal] Chimera review for "${task.title}":
6398
- ${result_.finalText.slice(0, 2e3)}`);
6435
+ this.logger.info(
6436
+ `[Goal] Chimera review for "${task.title}":
6437
+ ${result_.finalText.slice(0, 2e3)}`
6438
+ );
6399
6439
  }
6400
6440
  } catch (err) {
6401
6441
  this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
@@ -6448,7 +6488,16 @@ ${result_.finalText.slice(0, 2e3)}`);
6448
6488
  }
6449
6489
  buildState(activePhaseId) {
6450
6490
  if (!this.graph) {
6451
- 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
+ };
6452
6501
  }
6453
6502
  const phases = Array.from(this.graph.phases.values());
6454
6503
  const currentActiveId = activePhaseId || phases.find((p) => p.status === "running")?.id || phases[0]?.id || "";
@@ -6954,9 +7003,9 @@ function getAnalyticsBuffer() {
6954
7003
  }
6955
7004
 
6956
7005
  // src/server/http-server.ts
6957
- import * as fs9 from "node:fs/promises";
7006
+ import * as fs10 from "node:fs/promises";
6958
7007
  import * as http from "node:http";
6959
- import * as path12 from "node:path";
7008
+ import * as path13 from "node:path";
6960
7009
  import * as v8 from "node:v8";
6961
7010
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6962
7011
 
@@ -7640,18 +7689,153 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7640
7689
  }
7641
7690
  }
7642
7691
 
7643
- // src/server/projects-manifest.ts
7692
+ // src/server/memory-diagnostics.ts
7644
7693
  import * as fs8 from "node:fs/promises";
7645
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";
7646
7830
  import { ConfigError } from "@wrongstack/core/types";
7647
7831
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7648
7832
  function projectsJsonPath(globalConfigPath) {
7649
- const base = path11.dirname(globalConfigPath);
7650
- return path11.join(base, "projects.json");
7833
+ const base = path12.dirname(globalConfigPath);
7834
+ return path12.join(base, "projects.json");
7651
7835
  }
7652
7836
  async function loadManifest(globalConfigPath) {
7653
7837
  try {
7654
- const raw = await fs8.readFile(projectsJsonPath(globalConfigPath), "utf8");
7838
+ const raw = await fs9.readFile(projectsJsonPath(globalConfigPath), "utf8");
7655
7839
  const parsed = JSON.parse(raw);
7656
7840
  return { projects: parsed.projects ?? [] };
7657
7841
  } catch {
@@ -7660,37 +7844,37 @@ async function loadManifest(globalConfigPath) {
7660
7844
  }
7661
7845
  async function saveManifest(manifest, globalConfigPath) {
7662
7846
  const file = projectsJsonPath(globalConfigPath);
7663
- await fs8.mkdir(path11.dirname(file), { recursive: true });
7664
- 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");
7665
7849
  }
7666
7850
  function generateProjectSlug(rootPath) {
7667
7851
  return projectSlug(rootPath);
7668
7852
  }
7669
7853
  async function ensureProjectDataDir(slug, globalConfigPath) {
7670
- const base = path11.dirname(globalConfigPath);
7671
- const dir = path11.join(base, "projects", slug);
7672
- 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 });
7673
7857
  return dir;
7674
7858
  }
7675
7859
  async function touchProjectInManifest(options, globalConfigPath) {
7676
- const root = path11.resolve(options.projectRoot);
7860
+ const root = path12.resolve(options.projectRoot);
7677
7861
  const file = projectsJsonPath(globalConfigPath);
7678
7862
  let entry;
7679
7863
  await withFileLock(file, async () => {
7680
7864
  const manifest = await loadManifest(globalConfigPath);
7681
7865
  const now = (/* @__PURE__ */ new Date()).toISOString();
7682
- entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7866
+ entry = manifest.projects.find((candidate) => path12.resolve(candidate.root) === root);
7683
7867
  if (entry) {
7684
7868
  entry.lastSeen = now;
7685
- if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7869
+ if (options.workingDir) entry.lastWorkingDir = path12.resolve(options.workingDir);
7686
7870
  } else {
7687
7871
  entry = {
7688
- name: options.name ?? path11.basename(root),
7872
+ name: options.name ?? path12.basename(root),
7689
7873
  root,
7690
7874
  slug: generateProjectSlug(root),
7691
7875
  createdAt: now,
7692
7876
  lastSeen: now,
7693
- lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7877
+ lastWorkingDir: options.workingDir ? path12.resolve(options.workingDir) : void 0
7694
7878
  };
7695
7879
  manifest.projects.push(entry);
7696
7880
  }
@@ -8101,9 +8285,9 @@ function buildCspHeader(publicWsUrl, host, port) {
8101
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'`;
8102
8286
  }
8103
8287
  function isInsideDist(candidate, distDir) {
8104
- const root = path12.resolve(distDir);
8105
- const resolved = path12.resolve(candidate);
8106
- 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);
8107
8291
  }
8108
8292
  function decodeSessionId(segment) {
8109
8293
  try {
@@ -8123,7 +8307,7 @@ function strictDecodeParam(segment, res) {
8123
8307
  }
8124
8308
  function createHttpServer(opts) {
8125
8309
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
8126
- const distDir = path12.resolve(opts.distDir);
8310
+ const distDir = path13.resolve(opts.distDir);
8127
8311
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
8128
8312
  let techStackRuntime = null;
8129
8313
  const getTechStackRuntime = async () => {
@@ -8451,11 +8635,7 @@ function createHttpServer(opts) {
8451
8635
  if (researchMatch && req.method === "POST") {
8452
8636
  const pkg = strictDecodeParam(researchMatch[1], res);
8453
8637
  if (pkg === null) return;
8454
- await handleTechStackDependencyResearch(
8455
- res,
8456
- deps2,
8457
- pkg
8458
- );
8638
+ await handleTechStackDependencyResearch(res, deps2, pkg);
8459
8639
  return;
8460
8640
  }
8461
8641
  } catch (error2) {
@@ -8491,6 +8671,7 @@ function createHttpServer(opts) {
8491
8671
  return;
8492
8672
  }
8493
8673
  if (url.pathname === "/debug/system" && req.method === "GET") {
8674
+ const processes = await readRecentProcessMemoryDiagnostics(opts.globalRoot);
8494
8675
  res.writeHead(200, {
8495
8676
  "Content-Type": "application/json",
8496
8677
  "Cache-Control": "no-store"
@@ -8503,6 +8684,7 @@ function createHttpServer(opts) {
8503
8684
  uptime: process.uptime(),
8504
8685
  cpuUsage: process.cpuUsage(),
8505
8686
  codebaseIndexServer: getIndexState2().server,
8687
+ processes,
8506
8688
  timestamp: Date.now()
8507
8689
  })
8508
8690
  );
@@ -8510,24 +8692,24 @@ function createHttpServer(opts) {
8510
8692
  }
8511
8693
  let filePath;
8512
8694
  if (url.pathname === "/" || url.pathname === "") {
8513
- filePath = path12.join(distDir, "index.html");
8695
+ filePath = path13.join(distDir, "index.html");
8514
8696
  } else {
8515
- filePath = path12.join(distDir, url.pathname);
8697
+ filePath = path13.join(distDir, url.pathname);
8516
8698
  }
8517
- const resolvedPath = path12.resolve(filePath);
8699
+ const resolvedPath = path13.resolve(filePath);
8518
8700
  if (!isInsideDist(resolvedPath, distDir)) {
8519
8701
  res.writeHead(403, { "Content-Type": "text/plain" });
8520
8702
  res.end("Forbidden");
8521
8703
  return;
8522
8704
  }
8523
- const ext = path12.extname(resolvedPath);
8705
+ const ext = path13.extname(resolvedPath);
8524
8706
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8525
8707
  res.setHeader("Content-Type", contentType);
8526
8708
  setStaticSecurityHeaders(res);
8527
8709
  if (ext === ".html") {
8528
8710
  if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
8529
8711
  res.setHeader("Content-Security-Policy", buildCspHeader(opts.publicWsUrl, opts.host, port));
8530
- const html = await fs9.readFile(resolvedPath, "utf8");
8712
+ const html = await fs10.readFile(resolvedPath, "utf8");
8531
8713
  res.writeHead(200);
8532
8714
  res.end(injectWsConfig(html, { publicWsUrl: opts.publicWsUrl }));
8533
8715
  return;
@@ -8538,13 +8720,13 @@ function createHttpServer(opts) {
8538
8720
  url.pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=3600"
8539
8721
  );
8540
8722
  }
8541
- const fileContent = await fs9.readFile(resolvedPath);
8723
+ const fileContent = await fs10.readFile(resolvedPath);
8542
8724
  res.writeHead(200);
8543
8725
  res.end(fileContent);
8544
8726
  } catch (err) {
8545
8727
  if (err.code === "ENOENT") {
8546
8728
  try {
8547
- const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8729
+ const html = await fs10.readFile(path13.join(distDir, "index.html"), "utf8");
8548
8730
  setStaticSecurityHeaders(res);
8549
8731
  res.writeHead(200, {
8550
8732
  "Content-Type": "text/html",
@@ -8574,14 +8756,14 @@ function createHttpServer(opts) {
8574
8756
 
8575
8757
  // src/server/instance-registry.ts
8576
8758
  import * as os from "node:os";
8577
- import * as path13 from "node:path";
8578
- import * as fs10 from "node:fs/promises";
8759
+ import * as path14 from "node:path";
8760
+ import * as fs11 from "node:fs/promises";
8579
8761
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8580
8762
  function defaultBaseDir() {
8581
- return path13.join(os.homedir(), ".wrongstack");
8763
+ return path14.join(os.homedir(), ".wrongstack");
8582
8764
  }
8583
8765
  function registryPath(baseDir = defaultBaseDir()) {
8584
- return path13.join(baseDir, "webui-instances.json");
8766
+ return path14.join(baseDir, "webui-instances.json");
8585
8767
  }
8586
8768
  function isPidAlive(pid) {
8587
8769
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -8594,7 +8776,7 @@ function isPidAlive(pid) {
8594
8776
  }
8595
8777
  async function load(file) {
8596
8778
  try {
8597
- const raw = await fs10.readFile(file, "utf8");
8779
+ const raw = await fs11.readFile(file, "utf8");
8598
8780
  const parsed = JSON.parse(raw);
8599
8781
  if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
8600
8782
  return parsed;
@@ -10451,14 +10633,14 @@ function registerShutdownHandlers(res) {
10451
10633
  }
10452
10634
 
10453
10635
  // src/server/config-doctor.ts
10454
- import * as fs11 from "node:fs/promises";
10636
+ import * as fs12 from "node:fs/promises";
10455
10637
  import {
10456
10638
  repairConfigDefaults
10457
10639
  } from "@wrongstack/core/storage";
10458
10640
  import { atomicWrite as atomicWrite5 } from "@wrongstack/core/utils";
10459
10641
  import { decryptConfigSecrets } from "@wrongstack/core/security";
10460
10642
  async function readConfig(file, vault) {
10461
- const raw = await fs11.readFile(file, "utf8");
10643
+ const raw = await fs12.readFile(file, "utf8");
10462
10644
  const parsed = JSON.parse(raw);
10463
10645
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10464
10646
  throw new Error("Active profile config must contain a JSON object.");
@@ -10522,6 +10704,7 @@ async function handleConfigDoctor(ws, apply, deps2) {
10522
10704
 
10523
10705
  // src/server/mailbox-handlers.ts
10524
10706
  import {
10707
+ actionToAckInput,
10525
10708
  getSharedProjectMailbox,
10526
10709
  isMailboxMessageVisibleTo,
10527
10710
  MAILBOX_TYPE_PROPERTIES,
@@ -10538,6 +10721,43 @@ function getMailboxForDeps(deps2) {
10538
10721
  const dir = resolveProjectDir(projectRoot, globalRoot);
10539
10722
  return getSharedProjectMailbox(dir, deps2.events);
10540
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
+ }
10541
10761
  async function handleMailboxSend(ws, deps2, payload) {
10542
10762
  const mb = getMailboxForDeps(deps2);
10543
10763
  if (!mb) {
@@ -10553,7 +10773,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10553
10773
  }
10554
10774
  try {
10555
10775
  const message = await mb.send({
10556
- from: "webui",
10776
+ from: payload.from ?? "webui",
10557
10777
  to: payload.to,
10558
10778
  type: payload.type,
10559
10779
  audience: payload.audience,
@@ -10568,6 +10788,7 @@ async function handleMailboxSend(ws, deps2, payload) {
10568
10788
  requestId: payload.requestId,
10569
10789
  success: true,
10570
10790
  messageId: message.id,
10791
+ from: message.from,
10571
10792
  to: message.to,
10572
10793
  audience: message.audience ?? "all"
10573
10794
  }
@@ -10612,6 +10833,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
10612
10833
  send(ws, {
10613
10834
  type: "mailbox.messages",
10614
10835
  payload: {
10836
+ ...payload?.unreadOnly === true ? { unreadOnly: true } : {},
10615
10837
  messages: visibleMessages.map((m) => {
10616
10838
  const readByMe = payload?.agentId !== void 0 ? payload.agentId in m.readBy : false;
10617
10839
  const completedByMe = payload?.agentId !== void 0 ? m.completedBy === payload.agentId : false;
@@ -10727,6 +10949,14 @@ function createMailboxRouteHandlers(ctx) {
10727
10949
  ...ctx.events ? { events: ctx.events } : {}
10728
10950
  };
10729
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
+ },
10730
10960
  send: (ws, msg) => {
10731
10961
  const parsed = validateMailboxSendPayload(msg.payload);
10732
10962
  if (!parsed.ok) {
@@ -10777,6 +11007,9 @@ function createMailboxRouteHandlers(ctx) {
10777
11007
  }
10778
11008
  async function handleMailboxRoute(ws, msg, handlers) {
10779
11009
  switch (msg.type) {
11010
+ case "mailbox.action":
11011
+ await handlers.action(ws, msg);
11012
+ return true;
10780
11013
  case "mailbox.send":
10781
11014
  await handlers.send(ws, msg);
10782
11015
  return true;
@@ -12090,10 +12323,10 @@ async function findFreePort(host, startPort, opts = {}) {
12090
12323
  import { spawn as spawn2 } from "node:child_process";
12091
12324
  import { existsSync } from "node:fs";
12092
12325
  import { findPackageJSON } from "node:module";
12093
- import * as path14 from "node:path";
12326
+ import * as path15 from "node:path";
12094
12327
  function resolveDistDir(input) {
12095
12328
  const options = typeof input === "string" ? { explicitDistDir: input } : input ?? {};
12096
- if (options.explicitDistDir) return path14.resolve(options.explicitDistDir);
12329
+ if (options.explicitDistDir) return path15.resolve(options.explicitDistDir);
12097
12330
  const exists = options.exists ?? existsSync;
12098
12331
  let packageTarget;
12099
12332
  try {
@@ -12105,15 +12338,15 @@ function resolveDistDir(input) {
12105
12338
  );
12106
12339
  }
12107
12340
  if (!packageTarget) return null;
12108
- 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);
12109
12342
  if (options.exists === void 0 && options.resolvePackageJson) return distDir;
12110
- return exists(path14.join(distDir, "index.html")) ? distDir : null;
12343
+ return exists(path15.join(distDir, "index.html")) ? distDir : null;
12111
12344
  }
12112
12345
  async function ensureDistDir(explicitDistDir, deps2 = {}) {
12113
12346
  const exists = deps2.exists ?? existsSync;
12114
12347
  if (explicitDistDir) {
12115
- const resolved2 = path14.resolve(explicitDistDir);
12116
- 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;
12117
12350
  }
12118
12351
  const resolveOptions = {
12119
12352
  resolvePackageJson: deps2.resolvePackageJson,
@@ -12125,7 +12358,7 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
12125
12358
  try {
12126
12359
  const packageJson = deps2.resolvePackageJson ? deps2.resolvePackageJson("@wrongstack/webui/package.json") : findPackageJSON("@wrongstack/webui", import.meta.url);
12127
12360
  if (!packageJson) throw new Error("not found");
12128
- packageDir = path14.dirname(packageJson);
12361
+ packageDir = path15.dirname(packageJson);
12129
12362
  } catch {
12130
12363
  throw new Error(
12131
12364
  "@wrongstack/webui package could not be resolved. Install workspace dependencies and rebuild the CLI."
@@ -12134,8 +12367,8 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
12134
12367
  const findRoot = deps2.findWorkspaceRoot ?? ((pkgDir) => {
12135
12368
  let dir = pkgDir;
12136
12369
  for (let i = 0; i < 10; i++) {
12137
- if (existsSync(path14.join(dir, "pnpm-workspace.yaml"))) return dir;
12138
- const parent = path14.dirname(dir);
12370
+ if (existsSync(path15.join(dir, "pnpm-workspace.yaml"))) return dir;
12371
+ const parent = path15.dirname(dir);
12139
12372
  if (parent === dir) return null;
12140
12373
  dir = parent;
12141
12374
  }
@@ -12219,7 +12452,7 @@ function runPnpmBuild(cwd, workspace, timeoutMs) {
12219
12452
  }
12220
12453
 
12221
12454
  // src/server/embedded-lifecycle.ts
12222
- import * as path15 from "node:path";
12455
+ import * as path16 from "node:path";
12223
12456
 
12224
12457
  // src/server/network-info.ts
12225
12458
  import * as os2 from "node:os";
@@ -12285,7 +12518,7 @@ function registerWebuiInstance(p, deps2 = {}) {
12285
12518
  httpPort: p.httpPort,
12286
12519
  host: p.host,
12287
12520
  projectRoot: p.projectRoot,
12288
- projectName: path15.basename(p.projectRoot) || p.projectRoot,
12521
+ projectName: path16.basename(p.projectRoot) || p.projectRoot,
12289
12522
  startedAt: p.startedAt,
12290
12523
  url: buildWebUIAccessUrl({
12291
12524
  host: p.host,
@@ -12336,6 +12569,7 @@ function createWebuiShutdown(res) {
12336
12569
  log("[WebUI] Shutting down...");
12337
12570
  res.abortInFlight();
12338
12571
  res.unsubscribeEvents();
12572
+ res.disposeResources?.();
12339
12573
  res.closeClients();
12340
12574
  const unregistered = unregister(res.pid, res.registryBaseDir).catch(
12341
12575
  (err) => debug(`[webui-server] unregister failed: ${err}`)
@@ -12505,7 +12739,7 @@ ${text2}` : text2;
12505
12739
 
12506
12740
  // src/server/client-presence.ts
12507
12741
  import * as crypto2 from "node:crypto";
12508
- import * as path16 from "node:path";
12742
+ import * as path17 from "node:path";
12509
12743
  import {
12510
12744
  getSharedProjectMailbox as getSharedProjectMailbox2,
12511
12745
  resolveProjectDir as resolveProjectDir2
@@ -12523,7 +12757,7 @@ function createWebuiClientPresence(deps2) {
12523
12757
  if (!deps2.projectRoot) return null;
12524
12758
  try {
12525
12759
  const projectRoot = deps2.projectRoot;
12526
- const projectName = path16.basename(projectRoot);
12760
+ const projectName = path17.basename(projectRoot);
12527
12761
  const nextMailbox = getSharedProjectMailbox2(
12528
12762
  resolveProjectDir2(projectRoot, wstackGlobalRoot()),
12529
12763
  deps2.events,
@@ -13142,6 +13376,26 @@ function createKanbanSupervisor(deps2) {
13142
13376
  const agentRunning = /* @__PURE__ */ new Set();
13143
13377
  let disposed = false;
13144
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
+ };
13145
13399
  const publish = (snapshot) => {
13146
13400
  snapshots.set(snapshot.boardId, snapshot);
13147
13401
  deps2.broadcast({
@@ -13241,6 +13495,7 @@ function createKanbanSupervisor(deps2) {
13241
13495
  onDone: async (result) => {
13242
13496
  clearTimeout(watchdog);
13243
13497
  agentRunning.delete(board.id);
13498
+ if (await getBoard3(deps2.projectRoot, board.id) === null) return;
13244
13499
  const current3 = snapshots.get(board.id) ?? snapshot;
13245
13500
  publish({
13246
13501
  ...current3,
@@ -13262,13 +13517,20 @@ function createKanbanSupervisor(deps2) {
13262
13517
  }
13263
13518
  };
13264
13519
  const auditNow = async (boardId) => {
13265
- const boards = boardId ? [await getBoard3(deps2.projectRoot, boardId)].filter(
13266
- (board) => Boolean(board)
13267
- ) : await Promise.all(
13268
- (await listBoards4(deps2.projectRoot)).map(
13269
- (summary) => getBoard3(deps2.projectRoot, summary.id)
13270
- )
13271
- ).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
+ }
13272
13534
  const results = [];
13273
13535
  for (const board of boards) results.push(await auditBoard(board));
13274
13536
  scheduleNext();
@@ -13303,6 +13565,7 @@ function createKanbanSupervisor(deps2) {
13303
13565
  try {
13304
13566
  const now = Date.now();
13305
13567
  const summaries = await listBoards4(deps2.projectRoot);
13568
+ pruneAbsentBoards(new Set(summaries.map((summary) => summary.id)));
13306
13569
  for (const summary of summaries) {
13307
13570
  if ((nextDue.get(summary.id) ?? 0) > now) continue;
13308
13571
  const board = await getBoard3(deps2.projectRoot, summary.id);
@@ -13318,12 +13581,22 @@ function createKanbanSupervisor(deps2) {
13318
13581
  return {
13319
13582
  getSnapshot: (boardId) => snapshots.get(boardId),
13320
13583
  auditNow,
13584
+ getStats: () => ({
13585
+ snapshots: snapshots.size,
13586
+ scheduledBoards: nextDue.size,
13587
+ agentCooldowns: agentLastRun.size,
13588
+ runningAgents: agentRunning.size
13589
+ }),
13321
13590
  dispose() {
13322
13591
  disposed = true;
13323
13592
  if (nextTimer !== void 0) {
13324
13593
  clearTimeout(nextTimer);
13325
13594
  nextTimer = void 0;
13326
13595
  }
13596
+ snapshots.clear();
13597
+ nextDue.clear();
13598
+ agentLastRun.clear();
13599
+ agentRunning.clear();
13327
13600
  }
13328
13601
  };
13329
13602
  }
@@ -13500,8 +13773,8 @@ function seedContextMeta(config, context) {
13500
13773
  }
13501
13774
 
13502
13775
  // src/server/pref-helpers.ts
13503
- import * as fs12 from "node:fs/promises";
13504
- import * as path17 from "node:path";
13776
+ import * as fs13 from "node:fs/promises";
13777
+ import * as path18 from "node:path";
13505
13778
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
13506
13779
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
13507
13780
  var PREF_KEYS = [
@@ -13597,11 +13870,11 @@ function prefSnapshot(contextMeta) {
13597
13870
  return snapshot;
13598
13871
  }
13599
13872
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
13600
- const globalRoot = path17.dirname(filePath);
13873
+ const globalRoot = path18.dirname(filePath);
13601
13874
  await backupConfigFile(filePath, { globalRoot });
13602
13875
  let raw;
13603
13876
  try {
13604
- raw = await fs12.readFile(filePath, "utf8");
13877
+ raw = await fs13.readFile(filePath, "utf8");
13605
13878
  } catch {
13606
13879
  raw = "{}";
13607
13880
  }
@@ -14076,16 +14349,16 @@ async function handleProcessRoute(ws, msg, handlers) {
14076
14349
  }
14077
14350
 
14078
14351
  // src/server/embedded-host-adapters.ts
14079
- import * as fs15 from "node:fs/promises";
14080
- import * as path19 from "node:path";
14352
+ import * as fs16 from "node:fs/promises";
14353
+ import * as path20 from "node:path";
14081
14354
  import { TOKENS } from "@wrongstack/core/kernel";
14082
14355
  import { DefaultSessionStore as DefaultSessionStore2 } from "@wrongstack/core/storage";
14083
14356
  import { toErrorMessage as toErrorMessage7, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
14084
14357
  import { makeProviderFromConfig } from "@wrongstack/providers";
14085
14358
 
14086
14359
  // src/server/project-handlers.ts
14087
- import * as fs13 from "node:fs/promises";
14088
- import * as path18 from "node:path";
14360
+ import * as fs14 from "node:fs/promises";
14361
+ import * as path19 from "node:path";
14089
14362
  import { DefaultSessionStore } from "@wrongstack/core/storage";
14090
14363
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
14091
14364
  function createProjectHandlers(ctx) {
@@ -14137,10 +14410,10 @@ function createProjectHandlers(ctx) {
14137
14410
  });
14138
14411
  return;
14139
14412
  }
14140
- const resolved = path18.resolve(parsed.value.root);
14141
- 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);
14142
14415
  try {
14143
- const stat3 = await fs13.stat(resolved).catch(() => null);
14416
+ const stat3 = await fs14.stat(resolved).catch(() => null);
14144
14417
  if (!stat3?.isDirectory()) {
14145
14418
  sendTo(ws, {
14146
14419
  type: "projects.added",
@@ -14149,7 +14422,7 @@ function createProjectHandlers(ctx) {
14149
14422
  return;
14150
14423
  }
14151
14424
  const before = await loadManifest(ctx.globalConfigPath);
14152
- const already = before.projects.some((project) => path18.resolve(project.root) === resolved);
14425
+ const already = before.projects.some((project) => path19.resolve(project.root) === resolved);
14153
14426
  const entry = await touchProjectInManifest(
14154
14427
  { projectRoot: resolved, workingDir: resolved, name: name2 },
14155
14428
  ctx.globalConfigPath
@@ -14179,8 +14452,8 @@ function createProjectHandlers(ctx) {
14179
14452
  });
14180
14453
  return;
14181
14454
  }
14182
- const resolved = path18.resolve(parsed.value.root);
14183
- 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);
14184
14457
  if (!ctx.allowProjectMutations) {
14185
14458
  sendTo(ws, {
14186
14459
  type: "projects.selected",
@@ -14193,7 +14466,7 @@ function createProjectHandlers(ctx) {
14193
14466
  return;
14194
14467
  }
14195
14468
  try {
14196
- const stat3 = await fs13.stat(resolved).catch(() => null);
14469
+ const stat3 = await fs14.stat(resolved).catch(() => null);
14197
14470
  if (!stat3?.isDirectory()) {
14198
14471
  sendTo(ws, {
14199
14472
  type: "projects.selected",
@@ -14223,7 +14496,7 @@ function createProjectHandlers(ctx) {
14223
14496
  const previousIdentityTarget = {
14224
14497
  projectSlug: previousPaths.projectSlug,
14225
14498
  projectRoot: previousProjectRoot,
14226
- projectName: path18.basename(previousProjectRoot),
14499
+ projectName: path19.basename(previousProjectRoot),
14227
14500
  workingDir: ctx.context.workingDir
14228
14501
  };
14229
14502
  const previousUsage = ctx.tokenCounter.total();
@@ -14386,14 +14659,14 @@ async function searchCatalogModels(registry, rawQuery, limit = 8) {
14386
14659
  }
14387
14660
 
14388
14661
  // src/server/provider-config-io.ts
14389
- import * as fs14 from "node:fs/promises";
14662
+ import * as fs15 from "node:fs/promises";
14390
14663
  import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
14391
14664
  import { atomicWrite as atomicWrite7 } from "@wrongstack/core/utils";
14392
14665
  import { decryptConfigSecrets as decryptConfigSecrets3, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
14393
14666
  async function loadSavedProviders(configPath, vault) {
14394
14667
  let raw;
14395
14668
  try {
14396
- raw = await fs14.readFile(configPath, "utf8");
14669
+ raw = await fs15.readFile(configPath, "utf8");
14397
14670
  } catch {
14398
14671
  return {};
14399
14672
  }
@@ -14411,7 +14684,7 @@ async function saveProviders(configPath, vault, providers, profileConfigPath) {
14411
14684
  let raw;
14412
14685
  let fileExists = true;
14413
14686
  try {
14414
- raw = await fs14.readFile(targetPath, "utf8");
14687
+ raw = await fs15.readFile(targetPath, "utf8");
14415
14688
  } catch (err) {
14416
14689
  if (err.code !== "ENOENT") {
14417
14690
  throw new ConfigError2({
@@ -15177,6 +15450,7 @@ var CLIENT_COLLABORATION_MESSAGE_TYPES = [
15177
15450
  "collab.resume",
15178
15451
  "collab.grant_control",
15179
15452
  "collab.inject_tool",
15453
+ "mailbox.action",
15180
15454
  "mailbox.agents",
15181
15455
  "mailbox.clear",
15182
15456
  "mailbox.compact",
@@ -15442,6 +15716,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
15442
15716
  "collab.pause.granted",
15443
15717
  "collab.pause.released",
15444
15718
  "collab.state",
15719
+ "mailbox.action_result",
15445
15720
  "mailbox.agent_registered",
15446
15721
  "mailbox.agents",
15447
15722
  "mailbox.cleared",
@@ -15665,13 +15940,13 @@ function isRegisteredMessageType(type, direction) {
15665
15940
  // src/protocol/decoder.ts
15666
15941
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
15667
15942
  var MAX_PAYLOAD_DEPTH = 32;
15668
- function inspectValue(value, path34, depth) {
15943
+ function inspectValue(value, path35, depth) {
15669
15944
  if (depth > MAX_PAYLOAD_DEPTH) {
15670
- 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 };
15671
15946
  }
15672
15947
  if (value === null || typeof value !== "object") return null;
15673
15948
  for (const key of Object.keys(value)) {
15674
- const childPath = `${path34}.${key}`;
15949
+ const childPath = `${path35}.${key}`;
15675
15950
  if (FORBIDDEN_KEYS.has(key)) {
15676
15951
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
15677
15952
  }
@@ -15768,7 +16043,11 @@ function projectChatMessage(message) {
15768
16043
  return value ? { kind: "text-delta", text: value, messageId: text(payload["messageId"]) } : null;
15769
16044
  }
15770
16045
  case "provider.response":
15771
- return { kind: "response", content: payload["content"] };
16046
+ return {
16047
+ kind: "response",
16048
+ content: payload["content"],
16049
+ stopReason: text(payload["stopReason"])
16050
+ };
15772
16051
  case "run.result":
15773
16052
  return {
15774
16053
  kind: "run-result",
@@ -16605,7 +16884,7 @@ function createEmbeddedConversationRoutes(ctx) {
16605
16884
  }
16606
16885
  function sessionStoreFor(opts) {
16607
16886
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot;
16608
- 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 });
16609
16888
  }
16610
16889
  function createEmbeddedSessionRoutes(ctx) {
16611
16890
  const { opts } = ctx;
@@ -16624,7 +16903,7 @@ function createEmbeddedSessionRoutes(ctx) {
16624
16903
  getSession: () => actx.session ?? opts.session,
16625
16904
  getSessionStore: () => sessionStoreFor(opts),
16626
16905
  canSwapSessions: () => opts.sessionStore !== void 0,
16627
- getSessionsDir: () => opts.sessionsDir ?? path19.join(getProjectRoot(), ".wrongstack", "sessions"),
16906
+ getSessionsDir: () => opts.sessionsDir ?? path20.join(getProjectRoot(), ".wrongstack", "sessions"),
16628
16907
  setSession: (next) => {
16629
16908
  actx.session = next;
16630
16909
  },
@@ -16641,7 +16920,7 @@ function createEmbeddedSessionRoutes(ctx) {
16641
16920
  async function broadcastEmbeddedGoalSnapshot(ctx) {
16642
16921
  const projectRoot = ctx.opts.projectRoot ?? ctx.opts.agent.ctx.projectRoot;
16643
16922
  try {
16644
- 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");
16645
16924
  ctx.broadcast({ type: "goal-state.updated", payload: JSON.parse(raw) });
16646
16925
  } catch {
16647
16926
  ctx.broadcast({ type: "goal-state.updated", payload: null });
@@ -16650,10 +16929,10 @@ async function broadcastEmbeddedGoalSnapshot(ctx) {
16650
16929
  function createEmbeddedProjectRoutes(ctx) {
16651
16930
  const { opts } = ctx;
16652
16931
  const actx = opts.agent.ctx;
16653
- const globalConfigPath = opts.globalConfigPath ?? path19.join(wstackGlobalRoot2(), "config.json");
16932
+ const globalConfigPath = opts.globalConfigPath ?? path20.join(wstackGlobalRoot2(), "config.json");
16654
16933
  return createProjectHandlers({
16655
16934
  globalConfigPath,
16656
- wpaths: { globalRoot: path19.dirname(globalConfigPath) },
16935
+ wpaths: { globalRoot: path20.dirname(globalConfigPath) },
16657
16936
  context: actx,
16658
16937
  tokenCounter: actx.tokenCounter,
16659
16938
  config: { model: actx.model, provider: actx.provider.id },
@@ -17701,8 +17980,8 @@ function createRouteFamilyDispatcher(options) {
17701
17980
  }
17702
17981
 
17703
17982
  // src/server/shell-open.ts
17704
- import * as fs16 from "node:fs/promises";
17705
- import * as path20 from "node:path";
17983
+ import * as fs17 from "node:fs/promises";
17984
+ import * as path21 from "node:path";
17706
17985
  import { spawn as spawn3 } from "node:child_process";
17707
17986
  function normalizeShellOpenTarget(target) {
17708
17987
  return target === "terminal" ? "terminal" : "file-manager";
@@ -17713,11 +17992,11 @@ function shellQuote(s) {
17713
17992
  }
17714
17993
  async function handleShellOpen(req, logger, options) {
17715
17994
  try {
17716
- const resolved = path20.resolve(req.path);
17995
+ const resolved = path21.resolve(req.path);
17717
17996
  if (options?.projectRoot) {
17718
- const root = path20.resolve(options.projectRoot);
17719
- const relative5 = path20.relative(root, resolved);
17720
- 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);
17721
18000
  if (escapes) {
17722
18001
  return {
17723
18002
  success: false,
@@ -17725,7 +18004,7 @@ async function handleShellOpen(req, logger, options) {
17725
18004
  };
17726
18005
  }
17727
18006
  }
17728
- await fs16.access(resolved);
18007
+ await fs17.access(resolved);
17729
18008
  if (METACHAR_REGEX.test(resolved)) {
17730
18009
  return { success: false, message: "Path contains unsupported characters." };
17731
18010
  }
@@ -18129,7 +18408,7 @@ function createEmbeddedMessageRouter(deps2) {
18129
18408
  }
18130
18409
 
18131
18410
  // src/server/provider-config-standalone.ts
18132
- import * as path21 from "node:path";
18411
+ import * as path22 from "node:path";
18133
18412
  import { DefaultSecretVault } from "@wrongstack/core/security";
18134
18413
  function createProviderConfigIO(configPath) {
18135
18414
  const keyFile = vaultKeyFileForConfigPath(configPath);
@@ -18140,15 +18419,15 @@ function createProviderConfigIO(configPath) {
18140
18419
  };
18141
18420
  }
18142
18421
  function vaultKeyFileForConfigPath(configPath) {
18143
- const configDir = path21.dirname(configPath);
18144
- const parentDir = path21.dirname(configDir);
18145
- const globalRoot = path21.basename(parentDir) === "profiles" ? path21.dirname(parentDir) : configDir;
18146
- 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");
18147
18426
  }
18148
18427
 
18149
18428
  // src/server/provider-store.ts
18150
18429
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
18151
- import * as fs17 from "node:fs/promises";
18430
+ import * as fs18 from "node:fs/promises";
18152
18431
  import { decryptConfigSecrets as decryptConfigSecrets4, encryptConfigSecrets as encryptConfigSecrets3 } from "@wrongstack/core/security";
18153
18432
  import { atomicWrite as atomicWrite8 } from "@wrongstack/core/utils";
18154
18433
  function createConfigWriteLock() {
@@ -18172,7 +18451,7 @@ function createProviderStore(deps2) {
18172
18451
  const configWriteLock = createConfigWriteLock();
18173
18452
  async function loadSavedProviders2() {
18174
18453
  try {
18175
- const raw = await fs17.readFile(profileConfigPath, "utf8");
18454
+ const raw = await fs18.readFile(profileConfigPath, "utf8");
18176
18455
  const parsed = JSON.parse(raw);
18177
18456
  if (!parsed.providers) return {};
18178
18457
  return decryptConfigSecrets4(parsed.providers, vault);
@@ -18186,7 +18465,7 @@ function createProviderStore(deps2) {
18186
18465
  await prev;
18187
18466
  let parsed;
18188
18467
  try {
18189
- const raw = await fs17.readFile(profileConfigPath, "utf8");
18468
+ const raw = await fs18.readFile(profileConfigPath, "utf8");
18190
18469
  parsed = JSON.parse(raw);
18191
18470
  } catch {
18192
18471
  parsed = {};
@@ -18236,7 +18515,12 @@ function createProviderStore(deps2) {
18236
18515
  }
18237
18516
 
18238
18517
  // src/server/sdd-board-ws-handler.ts
18239
- import { listBoards as listBoards5 } from "@wrongstack/kanban";
18518
+ import {
18519
+ enqueueKanbanWorkflowCommand,
18520
+ kanbanWorkflowId,
18521
+ listBoards as listBoards5,
18522
+ listKanbanWorkflowStates
18523
+ } from "@wrongstack/kanban";
18240
18524
  import {
18241
18525
  applySddLifecycle,
18242
18526
  extractVerificationCommand,
@@ -18263,7 +18547,7 @@ var SddBoardWebSocketHandler = class {
18263
18547
  clients = /* @__PURE__ */ new Set();
18264
18548
  lifecycle;
18265
18549
  security;
18266
- diskPollingEnabled;
18550
+ standalonePollingEnabled;
18267
18551
  latest = null;
18268
18552
  poll = null;
18269
18553
  pollInFlight = false;
@@ -18272,7 +18556,7 @@ var SddBoardWebSocketHandler = class {
18272
18556
  this.store = new SddBoardStore({ baseDir: boardsDir });
18273
18557
  this.lifecycle = lifecycle;
18274
18558
  this.security = security;
18275
- this.diskPollingEnabled = events === void 0;
18559
+ this.standalonePollingEnabled = events === void 0;
18276
18560
  if (events) {
18277
18561
  const handler = (e) => {
18278
18562
  this.latest = e.snapshot;
@@ -18307,7 +18591,7 @@ var SddBoardWebSocketHandler = class {
18307
18591
  return;
18308
18592
  }
18309
18593
  if (msg.type === "sdd.board.list") {
18310
- const boards = await this.store.list();
18594
+ const boards = await this.listBoardEntries();
18311
18595
  this.broadcast({ type: "sdd.board.list", payload: { boards } });
18312
18596
  return;
18313
18597
  }
@@ -18320,7 +18604,8 @@ var SddBoardWebSocketHandler = class {
18320
18604
  const verificationCommands = [];
18321
18605
  if (action === "set_task_verification") {
18322
18606
  const command = msg.payload?.verificationCommand;
18323
- if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
18607
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192))
18608
+ return;
18324
18609
  if (typeof command === "string" && command.trim()) {
18325
18610
  verificationCommands.push({ command, operation: "sdd.set_task_verification" });
18326
18611
  }
@@ -18354,18 +18639,26 @@ var SddBoardWebSocketHandler = class {
18354
18639
  );
18355
18640
  if (!authorization.allowed) return;
18356
18641
  }
18357
- 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;
18358
18643
  if (runId) {
18359
- await this.store.appendControl(runId, {
18360
- ts: Date.now(),
18361
- type: action,
18362
- payload: msg.payload
18363
- });
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
+ }
18364
18657
  }
18365
18658
  }
18366
18659
  }
18367
18660
  /**
18368
- * Apply a cleanup/rollback/destroy from disk and broadcast a structured
18661
+ * Apply a cleanup/rollback/destroy from durable state and broadcast a structured
18369
18662
  * `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
18370
18663
  * the user must stop it first; the UI gates the buttons on `!active` and the
18371
18664
  * Destroy flow auto-stops then waits before sending `destroy`.
@@ -18374,7 +18667,11 @@ var SddBoardWebSocketHandler = class {
18374
18667
  if (!this.lifecycle) {
18375
18668
  this.broadcast({
18376
18669
  type: "sdd.board.lifecycle_result",
18377
- 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
+ }
18378
18675
  });
18379
18676
  return;
18380
18677
  }
@@ -18390,6 +18687,7 @@ var SddBoardWebSocketHandler = class {
18390
18687
  projectRoot: this.lifecycle.projectRoot,
18391
18688
  paths: this.lifecycle.paths,
18392
18689
  runId,
18690
+ stateTransport: this.lifecycle.stateTransport ?? "kanban",
18393
18691
  revertMerged: payload?.revertMerged === true
18394
18692
  });
18395
18693
  this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
@@ -18418,22 +18716,27 @@ var SddBoardWebSocketHandler = class {
18418
18716
  if (this.pollInFlight) return;
18419
18717
  this.pollInFlight = true;
18420
18718
  try {
18421
- const entry = await this.store.latest();
18422
- if (!entry) return;
18423
- 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) {
18424
18722
  return;
18425
18723
  }
18426
- const snap = await this.store.load(entry.runId);
18427
- if (snap) {
18428
- this.latest = snap;
18429
- this.broadcast({ type: "sdd.board.snapshot", payload: snap });
18430
- }
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
+ );
18431
18734
  } finally {
18432
18735
  this.pollInFlight = false;
18433
18736
  }
18434
18737
  }
18435
18738
  startPolling() {
18436
- if (!this.diskPollingEnabled || this.poll !== null || this.clients.size === 0) return;
18739
+ if (!this.standalonePollingEnabled || this.poll !== null || this.clients.size === 0) return;
18437
18740
  this.poll = setInterval(() => void this.pollLatest(), 1e3);
18438
18741
  this.poll.unref?.();
18439
18742
  }
@@ -18443,17 +18746,44 @@ var SddBoardWebSocketHandler = class {
18443
18746
  this.poll = null;
18444
18747
  }
18445
18748
  async sendCurrent(client) {
18446
- const snap = this.latest ?? await this.loadLatestFromDisk();
18749
+ const snap = this.latest ?? await this.loadLatestSnapshot();
18447
18750
  if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
18448
18751
  }
18449
18752
  async broadcastCurrent() {
18450
- const snap = this.latest ?? await this.loadLatestFromDisk();
18753
+ const snap = this.latest ?? await this.loadLatestSnapshot();
18451
18754
  if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
18452
18755
  }
18453
- 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
+ }
18454
18762
  const entry = await this.store.latest();
18455
18763
  return entry ? this.store.load(entry.runId) : null;
18456
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
+ }
18457
18787
  broadcast(msg) {
18458
18788
  const data = JSON.stringify(msg);
18459
18789
  for (const client of this.clients) {
@@ -18464,17 +18794,20 @@ var SddBoardWebSocketHandler = class {
18464
18794
  sendSerialized(client.ws, JSON.stringify(msg));
18465
18795
  }
18466
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
+ }
18467
18802
 
18468
18803
  // src/server/sdd-wizard-wiring.ts
18469
- import * as path22 from "node:path";
18470
- import {
18471
- DefaultTaskStore,
18472
- TaskTracker
18473
- } from "@wrongstack/core/tasking";
18804
+ import * as path23 from "node:path";
18805
+ import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
18474
18806
  import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
18475
18807
  import { WorktreeManager as WorktreeManager2 } from "@wrongstack/core/worktree";
18476
18808
  import {
18477
18809
  cleanupStaleSddWorktrees,
18810
+ createKanbanSddSessionPersistence,
18478
18811
  decomposeNonAtomicTasks,
18479
18812
  gatherProjectContext,
18480
18813
  makeAcceptanceCriteriaVerifier,
@@ -18492,7 +18825,7 @@ import {
18492
18825
  } from "@wrongstack/sdd";
18493
18826
  async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
18494
18827
  const runTracker = tracker ?? (() => {
18495
- const t = new TaskTracker({ store: new DefaultTaskStore() });
18828
+ const t = new TaskTracker({ store: deps2.taskStore ?? new DefaultTaskStore() });
18496
18829
  t.setGraph(graph);
18497
18830
  return t;
18498
18831
  })();
@@ -18501,7 +18834,8 @@ async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
18501
18834
  if (worktreesEnabled && await isGitWorkTree(deps2.projectRoot)) {
18502
18835
  void cleanupStaleSddWorktrees({
18503
18836
  projectRoot: deps2.projectRoot,
18504
- boardsDir: deps2.projectSddBoards
18837
+ boardsDir: deps2.projectSddBoards,
18838
+ stateTransport: "kanban"
18505
18839
  }).catch(() => void 0);
18506
18840
  worktrees = new WorktreeManager2({
18507
18841
  projectRoot: deps2.projectRoot,
@@ -18580,7 +18914,8 @@ function buildSddWizardDeps(opts) {
18580
18914
  }).catch(() => {
18581
18915
  projectContext = "";
18582
18916
  });
18583
- 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);
18584
18919
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
18585
18920
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
18586
18921
  const runIsolatedTurn = async (prompt, name2) => {
@@ -18601,7 +18936,7 @@ function buildSddWizardDeps(opts) {
18601
18936
  const makeDriver = () => new SddInterviewDriver({
18602
18937
  specStore,
18603
18938
  graphStore,
18604
- sessionPath,
18939
+ sessionPersistence,
18605
18940
  projectContext
18606
18941
  });
18607
18942
  const launchFromGraph = async (graph, config, tracker) => {
@@ -18613,6 +18948,7 @@ function buildSddWizardDeps(opts) {
18613
18948
  projectRoot: opts.projectRoot,
18614
18949
  subagentFactory: opts.subagentFactory,
18615
18950
  projectSddBoards: opts.paths.projectSddBoards,
18951
+ taskStore: graphStore,
18616
18952
  registry,
18617
18953
  runIsolatedTurn,
18618
18954
  ...opts.brain ? { brain: opts.brain } : {}
@@ -18682,8 +19018,18 @@ var SddWizardWebSocketHandler = class {
18682
19018
  lastAgentText = "";
18683
19019
  /** Guards against overlapping interview turns (one in flight at a time). */
18684
19020
  busy = false;
18685
- /** 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. */
18686
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;
18687
19033
  async bootstrap() {
18688
19034
  try {
18689
19035
  await this.deps.ensureReady?.();
@@ -18691,7 +19037,7 @@ var SddWizardWebSocketHandler = class {
18691
19037
  }
18692
19038
  await this.tryResume();
18693
19039
  }
18694
- /** Rehydrate a persisted interview if one exists on disk. */
19040
+ /** Rehydrate a persisted interview if one exists. */
18695
19041
  async tryResume() {
18696
19042
  if (this.driver) return;
18697
19043
  try {
@@ -18700,9 +19046,11 @@ var SddWizardWebSocketHandler = class {
18700
19046
  this.driver = driver;
18701
19047
  this.lastAgentText = driver.getLastAgentText() ?? "";
18702
19048
  }
18703
- } catch {
19049
+ this.resumeError = null;
19050
+ } catch (error2) {
18704
19051
  this.driver = null;
18705
19052
  this.lastAgentText = "";
19053
+ this.resumeError = error2 instanceof Error ? error2.message : String(error2);
18706
19054
  }
18707
19055
  }
18708
19056
  addClient(ws) {
@@ -18711,6 +19059,10 @@ var SddWizardWebSocketHandler = class {
18711
19059
  ws.on("close", () => this.clients.delete(client));
18712
19060
  ws.on("error", () => this.clients.delete(client));
18713
19061
  void this.ready.then(() => {
19062
+ if (this.resumeError) {
19063
+ this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
19064
+ return;
19065
+ }
18714
19066
  if (this.driver) {
18715
19067
  this.send(client, this.snapshotMsg());
18716
19068
  if (this.lastAgentText) {
@@ -18722,6 +19074,22 @@ var SddWizardWebSocketHandler = class {
18722
19074
  async handleMessage(msg) {
18723
19075
  try {
18724
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
+ }
18725
19093
  switch (msg.type) {
18726
19094
  case "sdd.spec.start":
18727
19095
  await this.onStart(String(msg.payload?.goal ?? "").trim(), {
@@ -18738,7 +19106,10 @@ var SddWizardWebSocketHandler = class {
18738
19106
  if (this.driver) {
18739
19107
  this.broadcast(this.snapshotMsg());
18740
19108
  if (this.lastAgentText) {
18741
- 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
+ });
18742
19113
  }
18743
19114
  }
18744
19115
  break;
@@ -18952,10 +19323,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
18952
19323
 
18953
19324
  // src/server/setup-events-fleet-broadcaster.ts
18954
19325
  import { watch as fsWatch } from "node:fs";
18955
- import * as path23 from "node:path";
19326
+ import * as path24 from "node:path";
18956
19327
  function registerSetupEventsFleetBroadcaster(deps2) {
18957
19328
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
18958
- const globalRoot = globalConfigPath ? path23.dirname(globalConfigPath) : void 0;
19329
+ const globalRoot = globalConfigPath ? path24.dirname(globalConfigPath) : void 0;
18959
19330
  if (!globalRoot) return void 0;
18960
19331
  const disposers = [];
18961
19332
  const broadcastSessions = async () => {
@@ -18965,8 +19336,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
18965
19336
  const sessions = await registry.list();
18966
19337
  const ownEntry = sessions.find((s) => s.pid === process.pid);
18967
19338
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
18968
- const myRoot = path23.resolve(context.projectRoot);
18969
- 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) => ({
18970
19341
  sessionId: s.sessionId,
18971
19342
  projectName: s.projectName,
18972
19343
  projectSlug: s.projectSlug,
@@ -19156,14 +19527,14 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
19156
19527
 
19157
19528
  // src/server/setup-events-status-watcher.ts
19158
19529
  import { watch as fsWatch2 } from "node:fs";
19159
- import * as fs18 from "node:fs/promises";
19160
- import * as path25 from "node:path";
19530
+ import * as fs19 from "node:fs/promises";
19531
+ import * as path26 from "node:path";
19161
19532
 
19162
19533
  // src/server/setup-events-watcher.ts
19163
- import * as path24 from "node:path";
19534
+ import * as path25 from "node:path";
19164
19535
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
19165
19536
  const raw = String(filename);
19166
- const relative5 = path24.isAbsolute(raw) ? path24.relative(projectsDir, raw) : raw;
19537
+ const relative5 = path25.isAbsolute(raw) ? path25.relative(projectsDir, raw) : raw;
19167
19538
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
19168
19539
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
19169
19540
  return parts.at(-2) ?? null;
@@ -19198,7 +19569,7 @@ function logFileWatcherMetrics(metrics) {
19198
19569
  function registerSetupEventsStatusWatcher(deps2) {
19199
19570
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
19200
19571
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
19201
- const projectsDir = path25.join(wpaths.globalRoot, "projects");
19572
+ const projectsDir = path26.join(wpaths.globalRoot, "projects");
19202
19573
  const knownProjectHashes = /* @__PURE__ */ new Set();
19203
19574
  const debounceTimers = /* @__PURE__ */ new Map();
19204
19575
  const DEBOUNCE_MS2 = 150;
@@ -19243,7 +19614,7 @@ function registerSetupEventsStatusWatcher(deps2) {
19243
19614
  let watcher;
19244
19615
  const startWatcher = async () => {
19245
19616
  try {
19246
- await fs18.mkdir(projectsDir, { recursive: true });
19617
+ await fs19.mkdir(projectsDir, { recursive: true });
19247
19618
  if (isDisposed()) return;
19248
19619
  watcher = fsWatch2(
19249
19620
  projectsDir,
@@ -19257,8 +19628,8 @@ function registerSetupEventsStatusWatcher(deps2) {
19257
19628
  if (!knownProjectHashes.has(projectHash)) return;
19258
19629
  if (watcherMetrics) watcherMetrics.filesProcessed++;
19259
19630
  try {
19260
- const targetFile = path25.join(projectsDir, projectHash, "status.json");
19261
- 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");
19262
19633
  const statusData = JSON.parse(content);
19263
19634
  scheduleBroadcast(projectHash, statusData);
19264
19635
  } catch {
@@ -19315,8 +19686,8 @@ function registerSetupEventsStatusWatcher(deps2) {
19315
19686
  }
19316
19687
 
19317
19688
  // src/server/setup-events-core-watchers.ts
19318
- import * as fs19 from "node:fs/promises";
19319
- import * as path26 from "node:path";
19689
+ import * as fs20 from "node:fs/promises";
19690
+ import * as path27 from "node:path";
19320
19691
  function registerSetupEventsCoreWatchers(deps2) {
19321
19692
  const { broadcast: broadcast2, clients, context } = deps2;
19322
19693
  const disposers = [];
@@ -19352,9 +19723,9 @@ function registerSetupEventsClientStatusWriter(deps2) {
19352
19723
  if (wpaths?.projectStatus) {
19353
19724
  try {
19354
19725
  const statusFile = wpaths.projectStatus(e.projectHash);
19355
- const dir = path26.dirname(statusFile);
19356
- await fs19.mkdir(dir, { recursive: true });
19357
- 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");
19358
19729
  } catch (err) {
19359
19730
  console.error(
19360
19731
  JSON.stringify({
@@ -20379,7 +20750,7 @@ var SpecsWebSocketHandler = class {
20379
20750
  // src/server/start-webui.ts
20380
20751
  import { randomUUID as randomUUID6 } from "node:crypto";
20381
20752
  import * as http2 from "node:http";
20382
- import * as path33 from "node:path";
20753
+ import * as path34 from "node:path";
20383
20754
  import { createDefaultPipelines } from "@wrongstack/core/agent";
20384
20755
  import { getSharedProjectMailbox as getSharedProjectMailbox5, resolveProjectDir as resolveProjectDir4 } from "@wrongstack/core/coordination";
20385
20756
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
@@ -20393,7 +20764,7 @@ import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } fro
20393
20764
  import {
20394
20765
  expectDefined as expectDefined4,
20395
20766
  sessionScopedPath as sessionScopedPath3,
20396
- startHeapWatchdog,
20767
+ startSharedHeapWatchdog,
20397
20768
  toErrorMessage as toErrorMessage14,
20398
20769
  wstackGlobalRoot as wstackGlobalRoot4
20399
20770
  } from "@wrongstack/core/utils";
@@ -20402,10 +20773,8 @@ import { toLanguagePackageInput } from "@wrongstack/techstack";
20402
20773
  import { ensureSessionShell } from "@wrongstack/tools";
20403
20774
 
20404
20775
  // src/server/backend-services.ts
20405
- import { join as join15 } from "node:path";
20406
- import {
20407
- Agent
20408
- } from "@wrongstack/core/agent";
20776
+ import { join as join16 } from "node:path";
20777
+ import { Agent } from "@wrongstack/core/agent";
20409
20778
  import {
20410
20779
  BrainDecisionLedger,
20411
20780
  BrainMonitor,
@@ -20417,8 +20786,8 @@ import {
20417
20786
  mailboxSessionTag,
20418
20787
  ObservableBrainArbiter as ObservableBrainArbiterCtor
20419
20788
  } from "@wrongstack/core/coordination";
20420
- import { installDesignStudioMiddleware } from "@wrongstack/core/design";
20421
20789
  import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
20790
+ import { installDesignStudioMiddleware } from "@wrongstack/core/design";
20422
20791
  import {
20423
20792
  AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
20424
20793
  createBrainRuntime,
@@ -20428,7 +20797,9 @@ import {
20428
20797
  } from "@wrongstack/core/execution";
20429
20798
  import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
20430
20799
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
20431
- import { resolveContextWindowPolicy as resolveContextWindowPolicy2 } from "@wrongstack/core/types";
20800
+ import {
20801
+ resolveContextWindowPolicy as resolveContextWindowPolicy2
20802
+ } from "@wrongstack/core/types";
20432
20803
  import {
20433
20804
  estimateRequestTokensCalibrated,
20434
20805
  toErrorMessage as toErrorMessage10
@@ -20445,7 +20816,7 @@ import {
20445
20816
  import { spawn as spawn4 } from "node:child_process";
20446
20817
  import { createRequire } from "node:module";
20447
20818
  import { existsSync as existsSync2 } from "node:fs";
20448
- import { dirname as dirname10, join as join13 } from "node:path";
20819
+ import { dirname as dirname10, join as join14 } from "node:path";
20449
20820
  import { resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
20450
20821
  import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
20451
20822
  import { readLiveLock } from "@wrongstack/core/coordination";
@@ -20574,7 +20945,7 @@ function mailboxServeInvocation(projectRoot) {
20574
20945
  function findWorkspaceCliEntry(projectRoot) {
20575
20946
  let dir = projectRoot;
20576
20947
  for (let i = 0; i < 6; i++) {
20577
- const candidate = join13(dir, "packages", "cli", "dist", "index.js");
20948
+ const candidate = join14(dir, "packages", "cli", "dist", "index.js");
20578
20949
  if (existsSync2(candidate)) return candidate;
20579
20950
  const parent = dirname10(dir);
20580
20951
  if (parent === dir) return null;
@@ -20819,10 +21190,10 @@ function clampDim(value, fallback) {
20819
21190
  }
20820
21191
 
20821
21192
  // src/server/worktree-ws-handler.ts
20822
- 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";
20823
21195
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
20824
21196
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
20825
- import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
20826
21197
  var MAX_ACTIVITY = 6;
20827
21198
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
20828
21199
  var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
@@ -20841,6 +21212,20 @@ var WorktreeWebSocketHandler = class {
20841
21212
  baseBranch = "";
20842
21213
  broadcastInterval = null;
20843
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;
20844
21229
  addClient(ws) {
20845
21230
  this.clients.add(ws);
20846
21231
  ws.on("close", () => this.clients.delete(ws));
@@ -20859,7 +21244,10 @@ var WorktreeWebSocketHandler = class {
20859
21244
  return true;
20860
21245
  }
20861
21246
  if (msg.type === "worktree.remove") {
20862
- await this.removeOne(msg.payload?.["dir"], msg.payload?.["branch"]);
21247
+ await this.removeOne(
21248
+ msg.payload?.["dir"],
21249
+ msg.payload?.["branch"]
21250
+ );
20863
21251
  return true;
20864
21252
  }
20865
21253
  if (msg.type === "worktree.merge") {
@@ -20867,7 +21255,10 @@ var WorktreeWebSocketHandler = class {
20867
21255
  return true;
20868
21256
  }
20869
21257
  if (msg.type === "worktree.diff") {
20870
- await this.diffOne(msg.payload?.["dir"], msg.payload?.["baseBranch"]);
21258
+ await this.diffOne(
21259
+ msg.payload?.["dir"],
21260
+ msg.payload?.["baseBranch"]
21261
+ );
20871
21262
  return true;
20872
21263
  }
20873
21264
  return false;
@@ -20880,7 +21271,7 @@ var WorktreeWebSocketHandler = class {
20880
21271
  // ── orphan management ─────────────────────────────────────────────────────
20881
21272
  /** Absolute managed-worktrees root for this project. */
20882
21273
  worktreesRoot() {
20883
- return resolve13(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
21274
+ return resolve13(join15(this.management.projectRoot, ".wrongstack", "worktrees"));
20884
21275
  }
20885
21276
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
20886
21277
  underRoot(dir) {
@@ -20896,12 +21287,43 @@ var WorktreeWebSocketHandler = class {
20896
21287
  }
20897
21288
  return live;
20898
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
+ }
20899
21321
  /**
20900
21322
  * Scan the disk for managed worktrees/branches NOT owned by a live in-session
20901
21323
  * run and broadcast them as orphans, with whether it is safe to clean now.
20902
21324
  * No-op (empty inventory) when management deps were not wired.
20903
21325
  */
20904
- async scanAndBroadcast() {
21326
+ async runScanOnce() {
20905
21327
  if (!this.management) {
20906
21328
  this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
20907
21329
  return;
@@ -20957,7 +21379,8 @@ var WorktreeWebSocketHandler = class {
20957
21379
  }
20958
21380
  const res = await cleanupStaleSddWorktrees2({
20959
21381
  projectRoot: this.management.projectRoot,
20960
- boardsDir: this.management.boardsDir
21382
+ boardsDir: this.management.boardsDir,
21383
+ stateTransport: "kanban"
20961
21384
  });
20962
21385
  if (res.skippedReason) {
20963
21386
  this.broadcast({
@@ -20970,26 +21393,45 @@ var WorktreeWebSocketHandler = class {
20970
21393
  for (const [id, h] of [...this.handles]) {
20971
21394
  if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
20972
21395
  }
20973
- 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
+ });
20974
21400
  this.broadcastState();
20975
21401
  await this.scanAndBroadcast();
20976
21402
  }
20977
21403
  /** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
20978
21404
  async removeOne(dir, branch) {
20979
21405
  if (!this.management || !dir && !branch) {
20980
- 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
+ });
20981
21410
  return;
20982
21411
  }
20983
21412
  if (branch && !MANAGED_BRANCH_RE.test(branch)) {
20984
- 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
+ });
20985
21417
  return;
20986
21418
  }
20987
21419
  if (dir && !this.underRoot(dir)) {
20988
- 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
+ });
20989
21424
  return;
20990
21425
  }
20991
21426
  if (branch && this.liveActiveBranches().has(branch)) {
20992
- 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
+ });
20993
21435
  return;
20994
21436
  }
20995
21437
  let removed = false;
@@ -20998,31 +21440,54 @@ var WorktreeWebSocketHandler = class {
20998
21440
  ({ removed } = await wt.removeOne(dir, branch));
20999
21441
  }
21000
21442
  for (const [id, h] of [...this.handles]) {
21001
- 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);
21002
21445
  }
21003
- 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
+ });
21004
21454
  this.broadcastState();
21005
21455
  await this.scanAndBroadcast();
21006
21456
  }
21007
21457
  /** Squash-merge ONE branch into base. Refused while a live run owns it. */
21008
21458
  async mergeBranch(branch) {
21009
21459
  if (!this.management || !branch) {
21010
- 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
+ });
21011
21464
  return;
21012
21465
  }
21013
21466
  if (!MANAGED_BRANCH_RE.test(branch)) {
21014
- 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
+ });
21015
21471
  return;
21016
21472
  }
21017
21473
  if (this.liveActiveBranches().has(branch)) {
21018
- 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
+ });
21019
21478
  return;
21020
21479
  }
21021
21480
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
21022
21481
  const res = await wt.mergeBranch(branch);
21023
21482
  this.broadcast({
21024
21483
  type: "worktree.merge_result",
21025
- 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
+ }
21026
21491
  });
21027
21492
  await this.scanAndBroadcast();
21028
21493
  }
@@ -21064,8 +21529,14 @@ var WorktreeWebSocketHandler = class {
21064
21529
  }),
21065
21530
  on("worktree.committed", (p) => {
21066
21531
  const e = p;
21067
- this.patch(e.handleId, { status: "committing", insertions: e.insertions, deletions: e.deletions, files: e.files });
21068
- 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)`);
21069
21540
  this.broadcastState();
21070
21541
  }),
21071
21542
  on("worktree.merged", (p) => {
@@ -21106,10 +21577,15 @@ var WorktreeWebSocketHandler = class {
21106
21577
  activity(id, kind, text2) {
21107
21578
  const cur = this.handles.get(id);
21108
21579
  if (cur) {
21109
- 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
+ );
21110
21583
  this.handles.set(id, { ...cur, recentActivity });
21111
21584
  }
21112
- 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
+ });
21113
21589
  }
21114
21590
  stateMessage() {
21115
21591
  return {
@@ -21351,7 +21827,7 @@ async function createAgentServices(input) {
21351
21827
  const brainCfg = resolveBrainConfigDefaults(config.brain, {
21352
21828
  fallbackModels: config.fallbackModels
21353
21829
  });
21354
- const brainLedgerPath = join15(wpaths.projectDir, "brain-ledger.jsonl");
21830
+ const brainLedgerPath = join16(wpaths.projectDir, "brain-ledger.jsonl");
21355
21831
  let brainLedgerEnabled = brainCfg.ledger?.enabled !== false;
21356
21832
  let brainLedger;
21357
21833
  const startBrainLedger = () => {
@@ -21416,33 +21892,35 @@ async function createAgentServices(input) {
21416
21892
  brainLog.push(entry);
21417
21893
  if (brainLog.length > 20) brainLog.shift();
21418
21894
  };
21419
- events.on(
21420
- "brain.decision_answered",
21421
- (e) => pushBrainLog({
21422
- at: e.at,
21423
- kind: "answered",
21424
- question: e.request.question,
21425
- outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
21426
- })
21427
- );
21428
- events.on(
21429
- "brain.decision_ask_human",
21430
- (e) => pushBrainLog({
21431
- at: e.at,
21432
- kind: "ask_human",
21433
- question: e.request.question,
21434
- outcome: "needs human judgement"
21435
- })
21436
- );
21437
- events.on(
21438
- "brain.decision_denied",
21439
- (e) => pushBrainLog({
21440
- at: e.at,
21441
- kind: "denied",
21442
- question: e.request.question,
21443
- outcome: e.decision.type === "deny" ? e.decision.reason : ""
21444
- })
21445
- );
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
+ ];
21446
21924
  const brainMailbox = getSharedProjectMailbox3(wpaths.projectDir, events);
21447
21925
  brainMonitor = new BrainMonitor({
21448
21926
  events,
@@ -21554,6 +22032,17 @@ async function createAgentServices(input) {
21554
22032
  getActiveSessionId: () => context.session.id
21555
22033
  }
21556
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
+ };
21557
22046
  return {
21558
22047
  collabBus,
21559
22048
  compactor,
@@ -21579,6 +22068,7 @@ async function createAgentServices(input) {
21579
22068
  worktreeHandler,
21580
22069
  terminalHandler,
21581
22070
  collabHandler,
22071
+ disposeRealtimeHandlers,
21582
22072
  updateAutoCompactionMaxContext
21583
22073
  };
21584
22074
  }
@@ -21665,7 +22155,7 @@ function createConnectionHandler(options) {
21665
22155
  }
21666
22156
 
21667
22157
  // src/server/message-dispatcher.ts
21668
- import path27 from "node:path";
22158
+ import path28 from "node:path";
21669
22159
  function createMessageDispatcher(opts) {
21670
22160
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
21671
22161
  function makeWorklistContext() {
@@ -21686,7 +22176,7 @@ function createMessageDispatcher(opts) {
21686
22176
  skillLoader: deps2.skillLoader,
21687
22177
  skillInstaller: deps2.skillInstaller,
21688
22178
  projectRoot,
21689
- projectSkillsDir: path27.join(projectRoot, ".wrongstack", "skills"),
22179
+ projectSkillsDir: path28.join(projectRoot, ".wrongstack", "skills"),
21690
22180
  globalSkillsDir: deps2.wpaths.globalSkills
21691
22181
  };
21692
22182
  }
@@ -21938,7 +22428,7 @@ function createMessageDispatcher(opts) {
21938
22428
 
21939
22429
  // src/server/pre-context-services.ts
21940
22430
  import { createRequire as createRequire3 } from "node:module";
21941
- import * as path30 from "node:path";
22431
+ import * as path31 from "node:path";
21942
22432
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
21943
22433
  import {
21944
22434
  getSharedProjectMailbox as getSharedProjectMailbox4,
@@ -21992,8 +22482,8 @@ import { configureDangerBypass, configureExecPolicy } from "@wrongstack/tools";
21992
22482
  import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/tools/session-kanban";
21993
22483
 
21994
22484
  // src/server/model-auto-discovery.ts
21995
- import * as fs20 from "node:fs/promises";
21996
- import * as path28 from "node:path";
22485
+ import * as fs21 from "node:fs/promises";
22486
+ import * as path29 from "node:path";
21997
22487
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
21998
22488
  function isOverlayRegistry(value) {
21999
22489
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -22019,7 +22509,7 @@ function eligibleProviders(config) {
22019
22509
  }
22020
22510
  async function readCache(file) {
22021
22511
  try {
22022
- return JSON.parse(await fs20.readFile(file, "utf8"));
22512
+ return JSON.parse(await fs21.readFile(file, "utf8"));
22023
22513
  } catch {
22024
22514
  return {};
22025
22515
  }
@@ -22029,7 +22519,7 @@ async function discoverAndMergeWebuiProviders(opts) {
22029
22519
  if (!isOverlayRegistry(registry)) return;
22030
22520
  const targets = eligibleProviders(opts.config);
22031
22521
  if (targets.length === 0) return;
22032
- const cacheFile = path28.join(opts.cacheDir, "discovered-models-cache.json");
22522
+ const cacheFile = path29.join(opts.cacheDir, "discovered-models-cache.json");
22033
22523
  const cache2 = await readCache(cacheFile);
22034
22524
  let cacheDirty = false;
22035
22525
  await Promise.all(
@@ -22066,8 +22556,8 @@ async function discoverAndMergeWebuiProviders(opts) {
22066
22556
  );
22067
22557
  if (cacheDirty) {
22068
22558
  try {
22069
- await fs20.mkdir(path28.dirname(cacheFile), { recursive: true });
22070
- 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");
22071
22561
  } catch {
22072
22562
  opts.logger?.debug?.("provider auto-discovery cache write failed");
22073
22563
  }
@@ -22163,7 +22653,7 @@ function resolveSetupProvider(opts) {
22163
22653
  }
22164
22654
 
22165
22655
  // src/server/standalone-session-identity.ts
22166
- import * as path29 from "node:path";
22656
+ import * as path30 from "node:path";
22167
22657
  import {
22168
22658
  AgentStatusTracker,
22169
22659
  FleetNotifier,
@@ -22182,7 +22672,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
22182
22672
  let activeTarget = {
22183
22673
  projectSlug: paths.projectSlug,
22184
22674
  projectRoot: paths.projectRoot,
22185
- projectName: path29.basename(paths.projectRoot),
22675
+ projectName: path30.basename(paths.projectRoot),
22186
22676
  workingDir: opts.workingDir
22187
22677
  };
22188
22678
  let pendingClaim;
@@ -22413,7 +22903,7 @@ async function createPreContextServices(input) {
22413
22903
  await discoverAndMergeWebuiProviders({
22414
22904
  config,
22415
22905
  registry: modelsRegistry,
22416
- cacheDir: path30.dirname(wpaths.modelsCache),
22906
+ cacheDir: path31.dirname(wpaths.modelsCache),
22417
22907
  logger
22418
22908
  });
22419
22909
  } catch (err) {
@@ -22465,7 +22955,7 @@ async function createPreContextServices(input) {
22465
22955
  configureChildEnvGitIdentity(config.git?.identity ?? null);
22466
22956
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
22467
22957
  const mcpTokenStore = new MCPVaultTokenStore(
22468
- path30.join(wpaths.projectDir, "mcp-auth.json"),
22958
+ path31.join(wpaths.projectDir, "mcp-auth.json"),
22469
22959
  vault
22470
22960
  );
22471
22961
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -22571,7 +23061,7 @@ async function createPreContextServices(input) {
22571
23061
  };
22572
23062
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
22573
23063
  const skillInstaller = config.features.skills ? new SkillInstaller({
22574
- manifestPath: path30.join(wpaths.configDir, "installed-skills.json"),
23064
+ manifestPath: path31.join(wpaths.configDir, "installed-skills.json"),
22575
23065
  projectSkillsDir: wpaths.inProjectSkills,
22576
23066
  globalSkillsDir: wpaths.globalSkills,
22577
23067
  projectHash: wpaths.projectHash,
@@ -22581,8 +23071,8 @@ async function createPreContextServices(input) {
22581
23071
  const bundledPromptsDir = promptsEnabled ? (() => {
22582
23072
  try {
22583
23073
  const req = createRequire3(import.meta.url);
22584
- return path30.join(
22585
- path30.dirname(req.resolve("@wrongstack/core/package.json")),
23074
+ return path31.join(
23075
+ path31.dirname(req.resolve("@wrongstack/core/package.json")),
22586
23076
  "data",
22587
23077
  "prompts"
22588
23078
  );
@@ -22686,7 +23176,7 @@ async function createPreContextServices(input) {
22686
23176
  }
22687
23177
 
22688
23178
  // src/server/routes.ts
22689
- import path31 from "node:path";
23179
+ import path32 from "node:path";
22690
23180
  import { makeProviderFromConfig as makeProviderFromConfig4, withCatalogCapabilities } from "@wrongstack/providers";
22691
23181
 
22692
23182
  // src/server/mode-handlers.ts
@@ -22978,7 +23468,7 @@ function buildRoutes(state, deps2, cb) {
22978
23468
  };
22979
23469
  const mailboxRoutes = createMailboxRouteHandlers({
22980
23470
  getProjectRoot: state.getProjectRoot,
22981
- getGlobalRoot: () => path31.dirname(deps2.globalConfigPath),
23471
+ getGlobalRoot: () => path32.dirname(deps2.globalConfigPath),
22982
23472
  events: deps2.events
22983
23473
  });
22984
23474
  const mcpRoutes = {
@@ -23051,7 +23541,7 @@ function buildRoutes(state, deps2, cb) {
23051
23541
  }
23052
23542
 
23053
23543
  // src/server/server-runtime.ts
23054
- import * as path32 from "node:path";
23544
+ import * as path33 from "node:path";
23055
23545
  import { createRequire as createRequire4 } from "node:module";
23056
23546
  import { fileURLToPath } from "node:url";
23057
23547
  import { WebSocketServer } from "ws";
@@ -23112,7 +23602,7 @@ function createSessionStartPayload(g) {
23112
23602
  inputCost,
23113
23603
  outputCost,
23114
23604
  cacheReadCost,
23115
- projectName: path32.basename(projectRoot) || projectRoot,
23605
+ projectName: path33.basename(projectRoot) || projectRoot,
23116
23606
  projectRoot,
23117
23607
  cwd: g.getWorkingDir(),
23118
23608
  mode: g.getModeId(),
@@ -23200,13 +23690,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
23200
23690
  };
23201
23691
  }
23202
23692
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
23203
- if (explicitDistDir) return path32.resolve(explicitDistDir);
23693
+ if (explicitDistDir) return path33.resolve(explicitDistDir);
23204
23694
  try {
23205
23695
  const requireFromHere2 = createRequire4(fromUrl);
23206
23696
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
23207
- return path32.dirname(serverEntry);
23697
+ return path33.dirname(serverEntry);
23208
23698
  } catch {
23209
- return path32.resolve(path32.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
23699
+ return path33.resolve(path33.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
23210
23700
  }
23211
23701
  }
23212
23702
  function startHttpServer(opts) {
@@ -23420,6 +23910,7 @@ async function startWebUI(opts = {}) {
23420
23910
  worktreeHandler,
23421
23911
  terminalHandler,
23422
23912
  collabHandler,
23913
+ disposeRealtimeHandlers,
23423
23914
  updateAutoCompactionMaxContext
23424
23915
  } = agentServices;
23425
23916
  if (typeof context.meta["yolo"] === "boolean") {
@@ -23598,21 +24089,21 @@ async function startWebUI(opts = {}) {
23598
24089
  });
23599
24090
  }
23600
24091
  async function touchProjectEntry(root, workDir) {
23601
- const resolved = path33.resolve(root);
24092
+ const resolved = path34.resolve(root);
23602
24093
  const manifest = await loadManifest(globalConfigPath);
23603
24094
  const now = (/* @__PURE__ */ new Date()).toISOString();
23604
- const existing = manifest.projects.find((p) => path33.resolve(p.root) === resolved);
24095
+ const existing = manifest.projects.find((p) => path34.resolve(p.root) === resolved);
23605
24096
  if (existing) {
23606
24097
  existing.lastSeen = now;
23607
- if (workDir) existing.lastWorkingDir = path33.resolve(workDir);
24098
+ if (workDir) existing.lastWorkingDir = path34.resolve(workDir);
23608
24099
  } else {
23609
24100
  manifest.projects.push({
23610
- name: path33.basename(resolved),
24101
+ name: path34.basename(resolved),
23611
24102
  root: resolved,
23612
24103
  slug: generateProjectSlug(resolved),
23613
24104
  createdAt: now,
23614
24105
  lastSeen: now,
23615
- lastWorkingDir: workDir ? path33.resolve(workDir) : void 0
24106
+ lastWorkingDir: workDir ? path34.resolve(workDir) : void 0
23616
24107
  });
23617
24108
  }
23618
24109
  await saveManifest(manifest, globalConfigPath);
@@ -23789,7 +24280,20 @@ async function startWebUI(opts = {}) {
23789
24280
  );
23790
24281
  credentialWatcherClose = credentialWatcher.close;
23791
24282
  }
23792
- 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
+ });
23793
24297
  const routes = buildRoutes(state, deps2, cb);
23794
24298
  const handleMessage = createMessageDispatcher({
23795
24299
  state,
@@ -23867,6 +24371,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23867
24371
  await todosCheckpoint.detach();
23868
24372
  await stopHeapWatchdog();
23869
24373
  credentialWatcherClose?.();
24374
+ disposeRealtimeHandlers();
23870
24375
  brainMonitor.stop();
23871
24376
  await agentServices.brainLedger?.stop().catch(() => {
23872
24377
  });
@@ -23890,7 +24395,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23890
24395
  await memoryStore.dispose().catch(
23891
24396
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage14(err)}`)
23892
24397
  );
23893
- await unregisterInstance(process.pid, path33.dirname(globalConfigPath));
24398
+ await unregisterInstance(process.pid, path34.dirname(globalConfigPath));
23894
24399
  }
23895
24400
  });
23896
24401
  }