@wrongstack/webui-server 0.296.2 → 0.296.4

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 (33) hide show
  1. package/dist/index.js +851 -238
  2. package/dist/index.js.map +4 -4
  3. package/dist/protocol/client-workspace.d.ts +1 -1
  4. package/dist/protocol/client-workspace.d.ts.map +1 -1
  5. package/dist/protocol/index.js +4 -1
  6. package/dist/protocol/index.js.map +2 -2
  7. package/dist/protocol/projections.d.ts +6 -0
  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-workspace.d.ts +1 -1
  12. package/dist/protocol/server-workspace.d.ts.map +1 -1
  13. package/dist/server/backend-services.d.ts.map +1 -1
  14. package/dist/server/connections-health-route.d.ts +8 -0
  15. package/dist/server/connections-health-route.d.ts.map +1 -1
  16. package/dist/server/embedded-host-adapters.d.ts +3 -0
  17. package/dist/server/embedded-host-adapters.d.ts.map +1 -1
  18. package/dist/server/embedded-message-router.d.ts.map +1 -1
  19. package/dist/server/entry.js +573 -209
  20. package/dist/server/entry.js.map +4 -4
  21. package/dist/server/http-server.d.ts.map +1 -1
  22. package/dist/server/project-handlers.d.ts +1 -0
  23. package/dist/server/project-handlers.d.ts.map +1 -1
  24. package/dist/server/routes.d.ts +2 -0
  25. package/dist/server/routes.d.ts.map +1 -1
  26. package/dist/server/sdd-board-ws-handler.d.ts +8 -1
  27. package/dist/server/sdd-board-ws-handler.d.ts.map +1 -1
  28. package/dist/server/session-handlers.d.ts +3 -0
  29. package/dist/server/session-handlers.d.ts.map +1 -1
  30. package/dist/server/setup-events.d.ts.map +1 -1
  31. package/dist/server/start-webui.d.ts +12 -0
  32. package/dist/server/start-webui.d.ts.map +1 -1
  33. 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, path33) {
137
+ function validateModelRuntimeValue(modelRuntime, path34) {
138
138
  const reasoning = modelRuntime["reasoning"];
139
139
  if (reasoning !== void 0) {
140
- if (!isRecord(reasoning)) return `${path33}.reasoning must be an object when provided`;
140
+ if (!isRecord(reasoning)) return `${path34}.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 `${path33}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
145
+ return `${path34}.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 `${path33}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
148
+ return `${path34}.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 `${path33}.reasoning.preserve must be a boolean when provided`;
151
+ return `${path34}.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 `${path33}.cache must be an object when provided`;
156
+ if (!isRecord(cache2)) return `${path34}.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 `${path33}.cache.ttl must be one of: 5m, 1h`;
159
+ return `${path34}.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 `${path33}.parameters must be an object when provided`;
164
+ return `${path34}.parameters must be an object when provided`;
165
165
  }
166
166
  return null;
167
167
  }
168
- function validateModelBlackoutRule(rule, path33) {
168
+ function validateModelBlackoutRule(rule, path34) {
169
169
  const id = rule["id"];
170
170
  if (typeof id !== "string" || id.trim().length === 0) {
171
- return `${path33}.id must be a non-empty string`;
171
+ return `${path34}.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 `${path33}.start must be a string in HH:mm (00:00-23:59) format`;
175
+ return `${path34}.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 `${path33}.end must be a string in HH:mm (00:00-23:59) format`;
179
+ return `${path34}.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 `${path33}.enabled must be a boolean when provided`;
182
+ return `${path34}.enabled must be a boolean when provided`;
183
183
  }
184
184
  if (rule["provider"] !== void 0 && typeof rule["provider"] !== "string") {
185
- return `${path33}.provider must be a string when provided`;
185
+ return `${path34}.provider must be a string when provided`;
186
186
  }
187
187
  if (rule["model"] !== void 0 && typeof rule["model"] !== "string") {
188
- return `${path33}.model must be a string when provided`;
188
+ return `${path34}.model must be a string when provided`;
189
189
  }
190
190
  if (rule["days"] !== void 0) {
191
- if (!Array.isArray(rule["days"])) return `${path33}.days must be an array when provided`;
191
+ if (!Array.isArray(rule["days"])) return `${path34}.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 `${path33}.days elements must be integers 0-6 when provided`;
195
+ return `${path34}.days elements must be integers 0-6 when provided`;
196
196
  }
197
- if (seen.has(d)) return `${path33}.days contains duplicate day: ${d}`;
197
+ if (seen.has(d)) return `${path34}.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 `${path33}.timezone must be a string when provided`;
203
+ return `${path34}.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 `${path33}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
208
+ return `${path34}.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 `${path33}.label must be a string when provided`;
212
+ return `${path34}.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 `${path33}.mode must be 'blackout' or 'allow_only' when provided`;
215
+ return `${path34}.mode must be 'blackout' or 'allow_only' when provided`;
216
216
  }
217
217
  return null;
218
218
  }
@@ -760,8 +760,8 @@ function validateShellOpenPayload(payload) {
760
760
  if (!isRecord2(payload)) {
761
761
  return { ok: false, message: "shell.open payload must be an object with string path" };
762
762
  }
763
- const path33 = payload["path"];
764
- if (typeof path33 !== "string" || path33.trim().length === 0) {
763
+ const path34 = payload["path"];
764
+ if (typeof path34 !== "string" || path34.trim().length === 0) {
765
765
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
766
766
  }
767
767
  const target = payload["target"];
@@ -774,7 +774,7 @@ function validateShellOpenPayload(payload) {
774
774
  return {
775
775
  ok: true,
776
776
  value: {
777
- path: path33,
777
+ path: path34,
778
778
  ...target !== void 0 ? { target } : {}
779
779
  }
780
780
  };
@@ -783,14 +783,14 @@ function validateGitDiffPayload(payload) {
783
783
  if (!isRecord2(payload)) {
784
784
  return { ok: false, message: "git.diff payload must be an object" };
785
785
  }
786
- const path33 = payload["path"];
787
- if (path33 === void 0 || path33 === null) {
786
+ const path34 = payload["path"];
787
+ if (path34 === void 0 || path34 === null) {
788
788
  return { ok: true, value: { path: "" } };
789
789
  }
790
- if (typeof path33 !== "string") {
790
+ if (typeof path34 !== "string") {
791
791
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
792
792
  }
793
- return { ok: true, value: { path: path33 } };
793
+ return { ok: true, value: { path: path34 } };
794
794
  }
795
795
  function validateProjectsAddPayload(payload) {
796
796
  if (!isRecord2(payload)) {
@@ -4376,8 +4376,8 @@ function jsonByteLength(value) {
4376
4376
  return MAX_PAYLOAD_BYTES + 1;
4377
4377
  }
4378
4378
  }
4379
- function error(errors, path33, code, message) {
4380
- errors.push({ path: path33, code, message });
4379
+ function error(errors, path34, code, message) {
4380
+ errors.push({ path: path34, code, message });
4381
4381
  }
4382
4382
  function isMessageRole(value) {
4383
4383
  return value === "user" || value === "assistant" || value === "system";
@@ -4385,25 +4385,25 @@ function isMessageRole(value) {
4385
4385
  function isPlainJsonObject(value) {
4386
4386
  return isRecord3(value);
4387
4387
  }
4388
- function validateCacheControl(value, path33, errors) {
4388
+ function validateCacheControl(value, path34, errors) {
4389
4389
  if (value === void 0) return void 0;
4390
4390
  if (!isRecord3(value) || value["type"] !== "ephemeral") {
4391
- error(errors, path33, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4391
+ error(errors, path34, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4392
4392
  return void 0;
4393
4393
  }
4394
4394
  return { type: "ephemeral" };
4395
4395
  }
4396
- function validateProviderMeta(value, path33, errors) {
4396
+ function validateProviderMeta(value, path34, errors) {
4397
4397
  if (value === void 0) return void 0;
4398
4398
  if (!isPlainJsonObject(value)) {
4399
- error(errors, path33, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4399
+ error(errors, path34, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4400
4400
  return void 0;
4401
4401
  }
4402
4402
  return value;
4403
4403
  }
4404
- function validateBlock(value, path33, errors) {
4404
+ function validateBlock(value, path34, errors) {
4405
4405
  if (!isRecord3(value)) {
4406
- error(errors, path33, "INVALID_BLOCK", "Content block must be an object.");
4406
+ error(errors, path34, "INVALID_BLOCK", "Content block must be an object.");
4407
4407
  return void 0;
4408
4408
  }
4409
4409
  const type = value["type"];
@@ -4411,14 +4411,14 @@ function validateBlock(value, path33, errors) {
4411
4411
  case "text": {
4412
4412
  const text2 = value["text"];
4413
4413
  if (typeof text2 !== "string") {
4414
- error(errors, `${path33}/text`, "INVALID_TEXT", "Text block text must be a string.");
4414
+ error(errors, `${path34}/text`, "INVALID_TEXT", "Text block text must be a string.");
4415
4415
  return void 0;
4416
4416
  }
4417
4417
  if (text2.length > MAX_STRING_LENGTH) {
4418
- error(errors, `${path33}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4418
+ error(errors, `${path34}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4419
4419
  return void 0;
4420
4420
  }
4421
- const cacheControl = validateCacheControl(value["cache_control"], `${path33}/cache_control`, errors);
4421
+ const cacheControl = validateCacheControl(value["cache_control"], `${path34}/cache_control`, errors);
4422
4422
  return cacheControl ? { type: "text", text: text2, cache_control: cacheControl } : { type: "text", text: text2 };
4423
4423
  }
4424
4424
  case "tool_use": {
@@ -4426,15 +4426,15 @@ function validateBlock(value, path33, errors) {
4426
4426
  const name2 = value["name"];
4427
4427
  const input = value["input"];
4428
4428
  if (typeof id !== "string" || id.length === 0) {
4429
- error(errors, `${path33}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4429
+ error(errors, `${path34}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4430
4430
  }
4431
4431
  if (typeof name2 !== "string" || name2.length === 0) {
4432
- error(errors, `${path33}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4432
+ error(errors, `${path34}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4433
4433
  }
4434
4434
  if (!isPlainJsonObject(input)) {
4435
- error(errors, `${path33}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4435
+ error(errors, `${path34}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4436
4436
  }
4437
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path33}/providerMeta`, errors);
4437
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path34}/providerMeta`, errors);
4438
4438
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4439
4439
  return void 0;
4440
4440
  }
@@ -4446,18 +4446,18 @@ function validateBlock(value, path33, errors) {
4446
4446
  const content = value["content"];
4447
4447
  const isError = value["is_error"];
4448
4448
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4449
- error(errors, `${path33}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4449
+ error(errors, `${path34}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4450
4450
  }
4451
4451
  if (name2 !== void 0 && typeof name2 !== "string") {
4452
- error(errors, `${path33}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4452
+ error(errors, `${path34}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4453
4453
  }
4454
4454
  if (typeof content !== "string") {
4455
- error(errors, `${path33}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4455
+ error(errors, `${path34}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4456
4456
  } else if (content.length > MAX_STRING_LENGTH) {
4457
- error(errors, `${path33}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4457
+ error(errors, `${path34}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4458
4458
  }
4459
4459
  if (isError !== void 0 && typeof isError !== "boolean") {
4460
- error(errors, `${path33}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4460
+ error(errors, `${path34}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4461
4461
  }
4462
4462
  if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4463
4463
  return {
@@ -4471,25 +4471,25 @@ function validateBlock(value, path33, errors) {
4471
4471
  case "image": {
4472
4472
  const source = value["source"];
4473
4473
  if (!isRecord3(source)) {
4474
- error(errors, `${path33}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4474
+ error(errors, `${path34}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4475
4475
  return void 0;
4476
4476
  }
4477
4477
  const sourceType = source["type"];
4478
4478
  if (sourceType !== "base64" && sourceType !== "url") {
4479
- error(errors, `${path33}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4479
+ error(errors, `${path34}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4480
4480
  return void 0;
4481
4481
  }
4482
4482
  const mediaType = source["media_type"];
4483
4483
  const data = source["data"];
4484
4484
  const url = source["url"];
4485
4485
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4486
- error(errors, `${path33}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4486
+ error(errors, `${path34}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4487
4487
  }
4488
4488
  if (data !== void 0 && typeof data !== "string") {
4489
- error(errors, `${path33}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4489
+ error(errors, `${path34}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4490
4490
  }
4491
4491
  if (url !== void 0 && typeof url !== "string") {
4492
- error(errors, `${path33}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4492
+ error(errors, `${path34}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4493
4493
  }
4494
4494
  return {
4495
4495
  type: "image",
@@ -4505,13 +4505,13 @@ function validateBlock(value, path33, errors) {
4505
4505
  const thinking = value["thinking"];
4506
4506
  const signature = value["signature"];
4507
4507
  if (typeof thinking !== "string") {
4508
- error(errors, `${path33}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4508
+ error(errors, `${path34}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4509
4509
  return void 0;
4510
4510
  }
4511
4511
  if (signature !== void 0 && typeof signature !== "string") {
4512
- error(errors, `${path33}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4512
+ error(errors, `${path34}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4513
4513
  }
4514
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path33}/providerMeta`, errors);
4514
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path34}/providerMeta`, errors);
4515
4515
  return {
4516
4516
  type: "thinking",
4517
4517
  thinking,
@@ -4520,7 +4520,7 @@ function validateBlock(value, path33, errors) {
4520
4520
  };
4521
4521
  }
4522
4522
  default:
4523
- error(errors, `${path33}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4523
+ error(errors, `${path34}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4524
4524
  return void 0;
4525
4525
  }
4526
4526
  }
@@ -4538,39 +4538,39 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4538
4538
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
4539
4539
  }
4540
4540
  value.forEach((item, index) => {
4541
- const path33 = `/messages/${index}`;
4541
+ const path34 = `/messages/${index}`;
4542
4542
  if (!isRecord3(item)) {
4543
- error(errors, path33, "INVALID_MESSAGE", "Message must be an object.");
4543
+ error(errors, path34, "INVALID_MESSAGE", "Message must be an object.");
4544
4544
  return;
4545
4545
  }
4546
4546
  const role = item["role"];
4547
4547
  if (!isMessageRole(role)) {
4548
- error(errors, `${path33}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4548
+ error(errors, `${path34}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4549
4549
  return;
4550
4550
  }
4551
4551
  const rawContent = item["content"];
4552
4552
  let content;
4553
4553
  if (typeof rawContent === "string") {
4554
4554
  if (rawContent.length > MAX_STRING_LENGTH) {
4555
- error(errors, `${path33}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4555
+ error(errors, `${path34}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4556
4556
  return;
4557
4557
  }
4558
4558
  content = rawContent;
4559
4559
  } else if (Array.isArray(rawContent)) {
4560
4560
  const blocks = [];
4561
4561
  rawContent.forEach((block, blockIndex) => {
4562
- const parsed = validateBlock(block, `${path33}/content/${blockIndex}`, errors);
4562
+ const parsed = validateBlock(block, `${path34}/content/${blockIndex}`, errors);
4563
4563
  if (parsed) blocks.push(parsed);
4564
4564
  });
4565
4565
  content = blocks;
4566
4566
  } else {
4567
- error(errors, `${path33}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4567
+ error(errors, `${path34}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4568
4568
  return;
4569
4569
  }
4570
4570
  const ts = item["ts"];
4571
4571
  if (ts !== void 0) {
4572
4572
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4573
- error(errors, `${path33}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4573
+ error(errors, `${path34}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4574
4574
  return;
4575
4575
  }
4576
4576
  }
@@ -4944,7 +4944,11 @@ function createConnectionLifecycle(options) {
4944
4944
  }
4945
4945
 
4946
4946
  // src/server/connections-health-route.ts
4947
- import { createChronicleProjectAccess as createChronicleProjectAccess2 } from "@wrongstack/core/chronicle";
4947
+ import {
4948
+ ChronicleProjectServerClient,
4949
+ createChronicleProjectAccess as createChronicleProjectAccess2,
4950
+ resolveChronicleProjectServerOptions
4951
+ } from "@wrongstack/core/chronicle";
4948
4952
  import {
4949
4953
  isMailboxProjectServerAvailable,
4950
4954
  MailboxProjectServerConnection
@@ -4952,7 +4956,12 @@ import {
4952
4956
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
4953
4957
  import { getKanbanServerConnection } from "@wrongstack/kanban";
4954
4958
  import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
4955
- import { checkCodebaseIndexServerHealth, getIndexState } from "@wrongstack/tools";
4959
+ import {
4960
+ checkCodebaseIndexServerHealth,
4961
+ getIndexState,
4962
+ resolveProjectIndexDaemonAvailability,
4963
+ shutdownCodebaseIndexServer
4964
+ } from "@wrongstack/tools";
4956
4965
  async function handleConnectionsHealthRoute(context, ws, message) {
4957
4966
  if (message.type !== "connections.health") return false;
4958
4967
  try {
@@ -5043,6 +5052,19 @@ async function chronicleHealth(projectRoot) {
5043
5052
  }
5044
5053
  async function codebaseIndexHealth(projectRoot, indexDir) {
5045
5054
  const startedAt = Date.now();
5055
+ const availability = resolveProjectIndexDaemonAvailability(projectRoot, indexDir);
5056
+ if (availability.kind === "endpoint-invalid") {
5057
+ return {
5058
+ id: "codebase-index",
5059
+ label: "Codebase index",
5060
+ status: "unavailable",
5061
+ required: false,
5062
+ mode: "endpoint-invalid",
5063
+ detail: `Socket path is ${availability.byteLength} bytes \u2014 over this platform's ${availability.maxBytes}-byte sun_path limit. Queries fall back to a process-local index. Set a shorter TMPDIR to restore the shared daemon.`,
5064
+ endpoint: availability.endpoint,
5065
+ latencyMs: Date.now() - startedAt
5066
+ };
5067
+ }
5046
5068
  try {
5047
5069
  const health = await checkCodebaseIndexServerHealth(projectRoot, indexDir, {
5048
5070
  timeoutMs: 2e3
@@ -5269,6 +5291,244 @@ async function mailboxHealth(projectRoot) {
5269
5291
  connection.close();
5270
5292
  }
5271
5293
  }
5294
+ async function handleConnectionsServiceAction(ws, message, context) {
5295
+ if (message.type !== "connections.service_action") return false;
5296
+ const payload = message.payload;
5297
+ const serviceId = payload?.serviceId;
5298
+ const rawAction = payload?.action ?? "shutdown";
5299
+ if (!serviceId) {
5300
+ context.send(ws, {
5301
+ type: "connections.service_action_result",
5302
+ payload: {
5303
+ serviceId: null,
5304
+ action: rawAction,
5305
+ success: false,
5306
+ message: "Missing serviceId in payload"
5307
+ }
5308
+ });
5309
+ return true;
5310
+ }
5311
+ if (rawAction !== "shutdown") {
5312
+ context.send(ws, {
5313
+ type: "connections.service_action_result",
5314
+ payload: {
5315
+ serviceId,
5316
+ action: rawAction,
5317
+ success: false,
5318
+ message: `Unsupported action "${rawAction}" \u2014 only "shutdown" is currently supported`
5319
+ }
5320
+ });
5321
+ return true;
5322
+ }
5323
+ const action = rawAction;
5324
+ if (serviceId === "webui") {
5325
+ context.send(ws, {
5326
+ type: "connections.service_action_result",
5327
+ payload: {
5328
+ serviceId: "webui",
5329
+ action,
5330
+ success: false,
5331
+ message: "Cannot shut down the WebUI transport itself"
5332
+ }
5333
+ });
5334
+ return true;
5335
+ }
5336
+ try {
5337
+ const result = await executeServiceAction(
5338
+ serviceId,
5339
+ action,
5340
+ context.getProjectRoot(),
5341
+ context.getIndexDir()
5342
+ );
5343
+ context.send(ws, {
5344
+ type: "connections.service_action_result",
5345
+ payload: result
5346
+ });
5347
+ } catch (error2) {
5348
+ context.send(ws, {
5349
+ type: "connections.service_action_result",
5350
+ payload: {
5351
+ serviceId,
5352
+ action,
5353
+ success: false,
5354
+ message: error2 instanceof Error ? error2.message : String(error2)
5355
+ }
5356
+ });
5357
+ }
5358
+ return true;
5359
+ }
5360
+ async function executeServiceAction(serviceId, action, projectRoot, indexDir) {
5361
+ switch (serviceId) {
5362
+ case "kanban":
5363
+ return killKanbanServer(projectRoot, action);
5364
+ case "sage":
5365
+ return killSageServer(projectRoot, action);
5366
+ case "chronicle":
5367
+ return killChronicleServer(projectRoot, action);
5368
+ case "codebase-index":
5369
+ return killCodebaseIndexServer(projectRoot, indexDir, action);
5370
+ case "mailbox":
5371
+ return killMailboxServer(projectRoot, action);
5372
+ default:
5373
+ return {
5374
+ serviceId,
5375
+ action,
5376
+ success: false,
5377
+ message: `Unknown service: ${serviceId}`
5378
+ };
5379
+ }
5380
+ }
5381
+ async function killKanbanServer(projectRoot, action) {
5382
+ if (process.env["WRONGSTACK_KANBAN_SERVER"] === "0") {
5383
+ return {
5384
+ serviceId: "kanban",
5385
+ action,
5386
+ success: false,
5387
+ message: "Kanban IPC daemon is disabled via WRONGSTACK_KANBAN_SERVER=0"
5388
+ };
5389
+ }
5390
+ let connection;
5391
+ try {
5392
+ connection = await getKanbanServerConnection(projectRoot);
5393
+ } catch (error2) {
5394
+ return {
5395
+ serviceId: "kanban",
5396
+ action,
5397
+ success: false,
5398
+ message: error2 instanceof Error ? error2.message : String(error2)
5399
+ };
5400
+ }
5401
+ if (!connection) {
5402
+ return {
5403
+ serviceId: "kanban",
5404
+ action,
5405
+ success: false,
5406
+ message: "Kanban IPC daemon is not running"
5407
+ };
5408
+ }
5409
+ try {
5410
+ const result = await connection.request("shutdown", {
5411
+ reason: `WebUI request: ${action}`
5412
+ });
5413
+ return {
5414
+ serviceId: "kanban",
5415
+ action,
5416
+ success: result.stopping,
5417
+ message: result.stopping ? `Kanban IPC daemon ${action} requested` : `Kanban IPC daemon ${action} failed (shutdown not confirmed)`
5418
+ };
5419
+ } catch (error2) {
5420
+ return {
5421
+ serviceId: "kanban",
5422
+ action,
5423
+ success: false,
5424
+ message: error2 instanceof Error ? error2.message : String(error2)
5425
+ };
5426
+ }
5427
+ }
5428
+ async function killSageServer(projectRoot, action) {
5429
+ if (!isSageProjectServerAvailable()) {
5430
+ return {
5431
+ serviceId: "sage",
5432
+ action,
5433
+ success: false,
5434
+ message: "SAGE project server is unavailable in this runtime"
5435
+ };
5436
+ }
5437
+ const connection = new SageProjectServerConnection(projectRoot);
5438
+ try {
5439
+ const result = await connection.shutdown(`WebUI request: ${action}`);
5440
+ return {
5441
+ serviceId: "sage",
5442
+ action,
5443
+ success: result.stopped,
5444
+ message: result.stopped ? `SAGE memory server ${action} requested` : `SAGE memory server ${action} failed: ${result.reason ?? "unknown"}`
5445
+ };
5446
+ } catch (error2) {
5447
+ return {
5448
+ serviceId: "sage",
5449
+ action,
5450
+ success: false,
5451
+ message: error2 instanceof Error ? error2.message : String(error2)
5452
+ };
5453
+ } finally {
5454
+ connection.close();
5455
+ }
5456
+ }
5457
+ async function killChronicleServer(projectRoot, action) {
5458
+ const options = resolveChronicleProjectServerOptions({ projectRoot });
5459
+ const client = new ChronicleProjectServerClient(options);
5460
+ try {
5461
+ const result = await client.shutdown(`WebUI request: ${action}`);
5462
+ return {
5463
+ serviceId: "chronicle",
5464
+ action,
5465
+ success: result.stopped,
5466
+ message: result.stopped ? `Chronicle telemetry server ${action} requested` : `Chronicle telemetry server ${action} failed: ${result.reason ?? "unknown"}`
5467
+ };
5468
+ } catch (error2) {
5469
+ return {
5470
+ serviceId: "chronicle",
5471
+ action,
5472
+ success: false,
5473
+ message: error2 instanceof Error ? error2.message : String(error2)
5474
+ };
5475
+ } finally {
5476
+ client.close();
5477
+ }
5478
+ }
5479
+ async function killCodebaseIndexServer(projectRoot, indexDir, action) {
5480
+ try {
5481
+ const result = await shutdownCodebaseIndexServer(
5482
+ projectRoot,
5483
+ indexDir,
5484
+ `websocket-request:${action}`
5485
+ );
5486
+ return {
5487
+ serviceId: "codebase-index",
5488
+ action,
5489
+ success: result.stopped,
5490
+ message: result.stopped ? `Codebase index server ${action} requested` : `Codebase index server ${action} failed: ${result.reason ?? "unknown"}`
5491
+ };
5492
+ } catch (error2) {
5493
+ return {
5494
+ serviceId: "codebase-index",
5495
+ action,
5496
+ success: false,
5497
+ message: error2 instanceof Error ? error2.message : String(error2)
5498
+ };
5499
+ }
5500
+ }
5501
+ async function killMailboxServer(projectRoot, action) {
5502
+ if (!isMailboxProjectServerAvailable()) {
5503
+ return {
5504
+ serviceId: "mailbox",
5505
+ action,
5506
+ success: false,
5507
+ message: "Mailbox project server is unavailable in this runtime"
5508
+ };
5509
+ }
5510
+ const connection = new MailboxProjectServerConnection(
5511
+ resolveWstackPaths2({ projectRoot }).projectDir
5512
+ );
5513
+ try {
5514
+ const result = await connection.shutdown(`WebUI request: ${action}`);
5515
+ return {
5516
+ serviceId: "mailbox",
5517
+ action,
5518
+ success: result.stopped,
5519
+ message: result.stopped ? `Mailbox IPC server ${action} requested` : `Mailbox IPC server ${action} failed: ${result.reason ?? "unknown"}`
5520
+ };
5521
+ } catch (error2) {
5522
+ return {
5523
+ serviceId: "mailbox",
5524
+ action,
5525
+ success: false,
5526
+ message: error2 instanceof Error ? error2.message : String(error2)
5527
+ };
5528
+ } finally {
5529
+ connection.close();
5530
+ }
5531
+ }
5272
5532
  function failureService(id, label, required, mode, error2, latencyMs) {
5273
5533
  const message = error2 instanceof Error ? error2.message : String(error2);
5274
5534
  return {
@@ -5429,9 +5689,9 @@ async function handleGitInfo(ws, projectRoot) {
5429
5689
  const cwd = projectRoot || void 0;
5430
5690
  try {
5431
5691
  const { execFile: ef } = await import("node:child_process");
5432
- const git = (args) => new Promise((resolve15) => {
5692
+ const git = (args) => new Promise((resolve16) => {
5433
5693
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
5434
- resolve15(err ? "" : stdout.trim());
5694
+ resolve16(err ? "" : stdout.trim());
5435
5695
  });
5436
5696
  });
5437
5697
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -5457,12 +5717,12 @@ async function handleGitInfo(ws, projectRoot) {
5457
5717
  function makeGit(cwd) {
5458
5718
  return async (args) => {
5459
5719
  const { execFile: ef } = await import("node:child_process");
5460
- return new Promise((resolve15) => {
5720
+ return new Promise((resolve16) => {
5461
5721
  ef(
5462
5722
  "git",
5463
5723
  args,
5464
5724
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
5465
- (err, stdout) => resolve15(err ? "" : stdout)
5725
+ (err, stdout) => resolve16(err ? "" : stdout)
5466
5726
  );
5467
5727
  });
5468
5728
  };
@@ -5486,15 +5746,15 @@ async function handleGitChanges(ws, projectRoot) {
5486
5746
  if (!m) continue;
5487
5747
  const added = m[1] === "-" ? 0 : Number(m[1]);
5488
5748
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5489
- let path33 = m[3] ?? "";
5490
- if (path33 === "") {
5749
+ let path34 = m[3] ?? "";
5750
+ if (path34 === "") {
5491
5751
  i += 1;
5492
- path33 = parts[i + 1] ?? parts[i] ?? "";
5752
+ path34 = parts[i + 1] ?? parts[i] ?? "";
5493
5753
  i += 1;
5494
5754
  }
5495
- if (!path33) continue;
5496
- const prev = counts.get(path33) ?? { added: 0, deleted: 0 };
5497
- counts.set(path33, { added: prev.added + added, deleted: prev.deleted + deleted });
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 });
5498
5758
  }
5499
5759
  };
5500
5760
  parseNumstat(unstagedNumstat);
@@ -5506,7 +5766,7 @@ async function handleGitChanges(ws, projectRoot) {
5506
5766
  if (!rec || rec.length < 3) continue;
5507
5767
  const x = rec[0] ?? " ";
5508
5768
  const y = rec[1] ?? " ";
5509
- const path33 = rec.slice(3);
5769
+ const path34 = rec.slice(3);
5510
5770
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5511
5771
  if (isRename) i += 1;
5512
5772
  let status;
@@ -5518,13 +5778,13 @@ async function handleGitChanges(ws, projectRoot) {
5518
5778
  else if (x === "D" || y === "D") status = "D";
5519
5779
  else status = "M";
5520
5780
  const staged = x !== " " && x !== "?";
5521
- let added = counts.get(path33)?.added ?? 0;
5522
- let deleted = counts.get(path33)?.deleted ?? 0;
5781
+ let added = counts.get(path34)?.added ?? 0;
5782
+ let deleted = counts.get(path34)?.deleted ?? 0;
5523
5783
  if (status === "?") {
5524
5784
  added = 0;
5525
5785
  deleted = 0;
5526
5786
  }
5527
- files.push({ path: path33, status, added, deleted, staged });
5787
+ files.push({ path: path34, status, added, deleted, staged });
5528
5788
  }
5529
5789
  send(ws, { type: "git.changes", payload: { files } });
5530
5790
  } catch (err) {
@@ -5535,10 +5795,10 @@ async function handleGitChanges(ws, projectRoot) {
5535
5795
  }
5536
5796
  }
5537
5797
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5538
- async function handleGitDiff(ws, projectRoot, path33) {
5798
+ async function handleGitDiff(ws, projectRoot, path34) {
5539
5799
  const cwd = projectRoot || void 0;
5540
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path33, ...extra } });
5541
- if (!path33 || path33.includes("\0") || path33.includes("..") || nodePath.isAbsolute(path33)) {
5800
+ const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path34, ...extra } });
5801
+ if (!path34 || path34.includes("\0") || path34.includes("..") || nodePath.isAbsolute(path34)) {
5542
5802
  reply2({ oldText: "", newText: "", error: "invalid path" });
5543
5803
  return;
5544
5804
  }
@@ -5546,10 +5806,10 @@ async function handleGitDiff(ws, projectRoot, path33) {
5546
5806
  const git = makeGit(cwd);
5547
5807
  const { readFile: readFile13 } = await import("node:fs/promises");
5548
5808
  const { join: join18 } = await import("node:path");
5549
- const oldText = await git(["show", `HEAD:${path33}`]);
5809
+ const oldText = await git(["show", `HEAD:${path34}`]);
5550
5810
  let newText = "";
5551
5811
  try {
5552
- const abs = cwd ? join18(cwd, path33) : path33;
5812
+ const abs = cwd ? join18(cwd, path34) : path34;
5553
5813
  const buf = await readFile13(abs);
5554
5814
  if (buf.includes(0)) {
5555
5815
  reply2({ oldText: "", newText: "", binary: true });
@@ -5629,7 +5889,7 @@ import { execFile } from "node:child_process";
5629
5889
  var GIT_TIMEOUT_MS = 1e4;
5630
5890
  var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
5631
5891
  function gitStdout(cwd, args) {
5632
- return new Promise((resolve15) => {
5892
+ return new Promise((resolve16) => {
5633
5893
  execFile(
5634
5894
  "git",
5635
5895
  [...args],
@@ -5640,7 +5900,7 @@ function gitStdout(cwd, args) {
5640
5900
  timeout: GIT_TIMEOUT_MS,
5641
5901
  maxBuffer: GIT_MAX_OUTPUT_BYTES
5642
5902
  },
5643
- (error2, stdout) => resolve15(error2 ? null : stdout)
5903
+ (error2, stdout) => resolve16(error2 ? null : stdout)
5644
5904
  );
5645
5905
  });
5646
5906
  }
@@ -5906,13 +6166,13 @@ var GoalWebSocketHandler = class {
5906
6166
  const cwd = env?.cwd ?? this.projectRoot;
5907
6167
  try {
5908
6168
  const { exec } = await import("node:child_process");
5909
- const result = await new Promise((resolve15) => {
6169
+ const result = await new Promise((resolve16) => {
5910
6170
  exec("npx tsc --noEmit", { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
5911
6171
  if (err && err.code === "ENOENT") {
5912
- resolve15("[verify] tsc not found \u2014 skipping");
6172
+ resolve16("[verify] tsc not found \u2014 skipping");
5913
6173
  return;
5914
6174
  }
5915
- resolve15(stdout + stderr);
6175
+ resolve16(stdout + stderr);
5916
6176
  });
5917
6177
  });
5918
6178
  if (result.includes("[verify]") || result.trim().length === 0) {
@@ -6591,7 +6851,7 @@ function pushEvent(event) {
6591
6851
  }
6592
6852
  }
6593
6853
  function parseBody(req) {
6594
- return new Promise((resolve15, reject) => {
6854
+ return new Promise((resolve16, reject) => {
6595
6855
  let body = "";
6596
6856
  let bodyBytes = 0;
6597
6857
  let tooLarge = false;
@@ -6611,7 +6871,7 @@ function parseBody(req) {
6611
6871
  return;
6612
6872
  }
6613
6873
  try {
6614
- resolve15(JSON.parse(body));
6874
+ resolve16(JSON.parse(body));
6615
6875
  } catch {
6616
6876
  reject(new Error("Invalid JSON"));
6617
6877
  }
@@ -6696,7 +6956,7 @@ function getAnalyticsBuffer() {
6696
6956
  // src/server/http-server.ts
6697
6957
  import * as fs9 from "node:fs/promises";
6698
6958
  import * as http from "node:http";
6699
- import * as path11 from "node:path";
6959
+ import * as path12 from "node:path";
6700
6960
  import * as v8 from "node:v8";
6701
6961
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6702
6962
 
@@ -6776,6 +7036,156 @@ async function handleCodemapSymbols(res, deps2, file) {
6776
7036
  );
6777
7037
  }
6778
7038
 
7039
+ // src/server/deadcode-handlers.ts
7040
+ import * as path10 from "node:path";
7041
+ import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
7042
+ var MAX_BODY_BYTES = 10 * 1024 * 1024;
7043
+ function readJsonBody(req) {
7044
+ return new Promise((resolve16, reject) => {
7045
+ const chunks = [];
7046
+ let total = 0;
7047
+ req.on("data", (chunk) => {
7048
+ total += chunk.length;
7049
+ if (total > MAX_BODY_BYTES) {
7050
+ req.destroy(new Error("Request body too large"));
7051
+ reject(new Error("Request body exceeds 10 MiB limit"));
7052
+ return;
7053
+ }
7054
+ chunks.push(chunk);
7055
+ });
7056
+ req.on("end", () => resolve16(Buffer.concat(chunks).toString("utf8")));
7057
+ req.on("error", (err) => reject(err));
7058
+ });
7059
+ }
7060
+ async function handleDeadCodeScan(res, deps2, req) {
7061
+ try {
7062
+ let body = {};
7063
+ const raw = await readJsonBody(req);
7064
+ if (raw) {
7065
+ try {
7066
+ body = JSON.parse(raw);
7067
+ } catch {
7068
+ res.writeHead(400, { "Content-Type": "application/json" });
7069
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
7070
+ return;
7071
+ }
7072
+ }
7073
+ const scanIndexDir = body.indexDir ?? deps2.indexDir;
7074
+ if (scanIndexDir) {
7075
+ const resolvedRoot = path10.resolve(deps2.projectRoot);
7076
+ const resolvedIndex = path10.resolve(deps2.projectRoot, scanIndexDir);
7077
+ if (resolvedIndex !== resolvedRoot && !resolvedIndex.startsWith(resolvedRoot + path10.sep)) {
7078
+ res.writeHead(400, { "Content-Type": "application/json" });
7079
+ res.end(JSON.stringify({ error: "Invalid indexDir: must be within project root" }));
7080
+ return;
7081
+ }
7082
+ }
7083
+ const result = runDeadCodeScan(deps2.projectRoot, {
7084
+ indexDir: scanIndexDir,
7085
+ userEntryPoints: body.entryPoints
7086
+ });
7087
+ res.writeHead(200, { "Content-Type": "application/json" });
7088
+ res.end(JSON.stringify(result));
7089
+ } catch (err) {
7090
+ res.writeHead(500, { "Content-Type": "application/json" });
7091
+ res.end(
7092
+ JSON.stringify({
7093
+ error: "Dead-code scan failed",
7094
+ detail: err instanceof Error ? err.message : String(err)
7095
+ })
7096
+ );
7097
+ }
7098
+ }
7099
+ function handleDeadCodeActionPlan(res, _deps, req) {
7100
+ return readJsonBody(req).then((raw) => {
7101
+ let parsed;
7102
+ try {
7103
+ parsed = JSON.parse(raw);
7104
+ } catch {
7105
+ res.writeHead(400, { "Content-Type": "application/json" });
7106
+ res.end(JSON.stringify({ error: "Invalid scan result JSON" }));
7107
+ return;
7108
+ }
7109
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.deadPackages) || !Array.isArray(parsed.deadFiles) || !Array.isArray(parsed.deadSymbols)) {
7110
+ res.writeHead(400, { "Content-Type": "application/json" });
7111
+ res.end(
7112
+ JSON.stringify({
7113
+ error: "Invalid scan result: missing or malformed required fields (deadPackages, deadFiles, deadSymbols)"
7114
+ })
7115
+ );
7116
+ return;
7117
+ }
7118
+ const result = parsed;
7119
+ const plan = buildActionPlan(result);
7120
+ res.writeHead(200, { "Content-Type": "application/json" });
7121
+ res.end(JSON.stringify(plan));
7122
+ }).catch((err) => {
7123
+ res.writeHead(500, { "Content-Type": "application/json" });
7124
+ res.end(
7125
+ JSON.stringify({
7126
+ error: "Failed to read request body",
7127
+ detail: err instanceof Error ? err.message : String(err)
7128
+ })
7129
+ );
7130
+ });
7131
+ }
7132
+ function buildActionPlan(result) {
7133
+ const files = /* @__PURE__ */ new Map();
7134
+ for (const dp of result.deadPackages) {
7135
+ const pseudoFile = {
7136
+ file: `${dp.package}/ (package)`,
7137
+ symbolCount: dp.fileCount,
7138
+ symbols: [`remove package ${dp.package} (${dp.fileCount} files, path: ${dp.path})`],
7139
+ priority: 0
7140
+ };
7141
+ files.set(pseudoFile.file, pseudoFile);
7142
+ }
7143
+ for (const df of result.deadFiles) {
7144
+ const existing = files.get(df.file);
7145
+ if (existing) {
7146
+ if (existing.priority > 1) existing.priority = 1;
7147
+ existing.symbolCount += df.symbolCount;
7148
+ continue;
7149
+ }
7150
+ files.set(df.file, {
7151
+ file: df.file,
7152
+ symbolCount: df.symbolCount,
7153
+ symbols: [`entire file (${df.symbolCount} symbols) is dead`],
7154
+ priority: 1
7155
+ });
7156
+ }
7157
+ const deadInAliveFiles = /* @__PURE__ */ new Map();
7158
+ const deadFileSet = new Set(result.deadFiles.map((df) => df.file));
7159
+ for (const ds of result.deadSymbols) {
7160
+ if (deadFileSet.has(ds.file)) continue;
7161
+ const list = deadInAliveFiles.get(ds.file) ?? [];
7162
+ list.push(`${ds.kind} ${ds.name} (line ${ds.line})`);
7163
+ deadInAliveFiles.set(ds.file, list);
7164
+ }
7165
+ for (const [file, symbols] of deadInAliveFiles) {
7166
+ const existing = files.get(file);
7167
+ if (existing) {
7168
+ existing.symbols.push(...symbols);
7169
+ existing.symbolCount += symbols.length;
7170
+ continue;
7171
+ }
7172
+ files.set(file, {
7173
+ file,
7174
+ symbolCount: symbols.length,
7175
+ symbols,
7176
+ priority: 2
7177
+ });
7178
+ }
7179
+ const sorted = [...files.values()].sort((a, b) => a.priority - b.priority || a.file.localeCompare(b.file));
7180
+ return {
7181
+ summary: `Dead-code scan found ${result.stats.dead} dead symbols across ${result.deadFiles.length} dead files and ${result.deadPackages.length} dead packages. Action plan has ${sorted.length} file group(s) to address.`,
7182
+ files: sorted,
7183
+ totalDeadSymbols: result.stats.dead,
7184
+ totalDeadFiles: result.deadFiles.length,
7185
+ totalDeadPackages: result.deadPackages.length
7186
+ };
7187
+ }
7188
+
6779
7189
  // src/server/http-server/api-handlers.ts
6780
7190
  async function handleApiSessions(res, globalRoot) {
6781
7191
  if (!globalRoot) {
@@ -6997,8 +7407,8 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
6997
7407
  res.end(JSON.stringify({ error: String(err) }));
6998
7408
  }
6999
7409
  }
7000
- function readJsonBody(req) {
7001
- return new Promise((resolve15, reject) => {
7410
+ function readJsonBody2(req) {
7411
+ return new Promise((resolve16, reject) => {
7002
7412
  let data = "";
7003
7413
  req.on("data", (chunk) => {
7004
7414
  data += chunk;
@@ -7009,7 +7419,7 @@ function readJsonBody(req) {
7009
7419
  });
7010
7420
  req.on("end", () => {
7011
7421
  try {
7012
- resolve15(data ? JSON.parse(data) : {});
7422
+ resolve16(data ? JSON.parse(data) : {});
7013
7423
  } catch (err) {
7014
7424
  reject(err instanceof Error ? err : new Error(String(err)));
7015
7425
  }
@@ -7025,7 +7435,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
7025
7435
  }
7026
7436
  let body;
7027
7437
  try {
7028
- body = await readJsonBody(req);
7438
+ body = await readJsonBody2(req);
7029
7439
  } catch {
7030
7440
  res.writeHead(400, { "Content-Type": "application/json" });
7031
7441
  res.end(JSON.stringify({ error: "Invalid request body" }));
@@ -7127,7 +7537,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7127
7537
  }
7128
7538
  let body = {};
7129
7539
  try {
7130
- body = await readJsonBody(req);
7540
+ body = await readJsonBody2(req);
7131
7541
  } catch {
7132
7542
  }
7133
7543
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
@@ -7168,7 +7578,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7168
7578
  }
7169
7579
  let body;
7170
7580
  try {
7171
- body = await readJsonBody(req);
7581
+ body = await readJsonBody2(req);
7172
7582
  } catch {
7173
7583
  res.writeHead(400, { "Content-Type": "application/json" });
7174
7584
  res.end(JSON.stringify({ error: "Invalid request body" }));
@@ -7232,12 +7642,12 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7232
7642
 
7233
7643
  // src/server/projects-manifest.ts
7234
7644
  import * as fs8 from "node:fs/promises";
7235
- import * as path10 from "node:path";
7645
+ import * as path11 from "node:path";
7236
7646
  import { ConfigError } from "@wrongstack/core/types";
7237
7647
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7238
7648
  function projectsJsonPath(globalConfigPath) {
7239
- const base = path10.dirname(globalConfigPath);
7240
- return path10.join(base, "projects.json");
7649
+ const base = path11.dirname(globalConfigPath);
7650
+ return path11.join(base, "projects.json");
7241
7651
  }
7242
7652
  async function loadManifest(globalConfigPath) {
7243
7653
  try {
@@ -7250,37 +7660,37 @@ async function loadManifest(globalConfigPath) {
7250
7660
  }
7251
7661
  async function saveManifest(manifest, globalConfigPath) {
7252
7662
  const file = projectsJsonPath(globalConfigPath);
7253
- await fs8.mkdir(path10.dirname(file), { recursive: true });
7663
+ await fs8.mkdir(path11.dirname(file), { recursive: true });
7254
7664
  await fs8.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7255
7665
  }
7256
7666
  function generateProjectSlug(rootPath) {
7257
7667
  return projectSlug(rootPath);
7258
7668
  }
7259
7669
  async function ensureProjectDataDir(slug, globalConfigPath) {
7260
- const base = path10.dirname(globalConfigPath);
7261
- const dir = path10.join(base, "projects", slug);
7670
+ const base = path11.dirname(globalConfigPath);
7671
+ const dir = path11.join(base, "projects", slug);
7262
7672
  await fs8.mkdir(dir, { recursive: true });
7263
7673
  return dir;
7264
7674
  }
7265
7675
  async function touchProjectInManifest(options, globalConfigPath) {
7266
- const root = path10.resolve(options.projectRoot);
7676
+ const root = path11.resolve(options.projectRoot);
7267
7677
  const file = projectsJsonPath(globalConfigPath);
7268
7678
  let entry;
7269
7679
  await withFileLock(file, async () => {
7270
7680
  const manifest = await loadManifest(globalConfigPath);
7271
7681
  const now = (/* @__PURE__ */ new Date()).toISOString();
7272
- entry = manifest.projects.find((candidate) => path10.resolve(candidate.root) === root);
7682
+ entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7273
7683
  if (entry) {
7274
7684
  entry.lastSeen = now;
7275
- if (options.workingDir) entry.lastWorkingDir = path10.resolve(options.workingDir);
7685
+ if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7276
7686
  } else {
7277
7687
  entry = {
7278
- name: options.name ?? path10.basename(root),
7688
+ name: options.name ?? path11.basename(root),
7279
7689
  root,
7280
7690
  slug: generateProjectSlug(root),
7281
7691
  createdAt: now,
7282
7692
  lastSeen: now,
7283
- lastWorkingDir: options.workingDir ? path10.resolve(options.workingDir) : void 0
7693
+ lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7284
7694
  };
7285
7695
  manifest.projects.push(entry);
7286
7696
  }
@@ -7691,9 +8101,9 @@ function buildCspHeader(publicWsUrl, host, port) {
7691
8101
  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'`;
7692
8102
  }
7693
8103
  function isInsideDist(candidate, distDir) {
7694
- const root = path11.resolve(distDir);
7695
- const resolved = path11.resolve(candidate);
7696
- return resolved === root || resolved.startsWith(root + path11.sep);
8104
+ const root = path12.resolve(distDir);
8105
+ const resolved = path12.resolve(candidate);
8106
+ return resolved === root || resolved.startsWith(root + path12.sep);
7697
8107
  }
7698
8108
  function decodeSessionId(segment) {
7699
8109
  try {
@@ -7713,7 +8123,7 @@ function strictDecodeParam(segment, res) {
7713
8123
  }
7714
8124
  function createHttpServer(opts) {
7715
8125
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
7716
- const distDir = path11.resolve(opts.distDir);
8126
+ const distDir = path12.resolve(opts.distDir);
7717
8127
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
7718
8128
  let techStackRuntime = null;
7719
8129
  const getTechStackRuntime = async () => {
@@ -7932,6 +8342,42 @@ function createHttpServer(opts) {
7932
8342
  );
7933
8343
  return;
7934
8344
  }
8345
+ if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
8346
+ if (requireAccessToken && !accessTokenOk) {
8347
+ res.writeHead(401, { "Content-Type": "application/json" });
8348
+ res.end(JSON.stringify({ error: "Unauthorized" }));
8349
+ return;
8350
+ }
8351
+ if (!opts.projectRoot) {
8352
+ res.writeHead(503, { "Content-Type": "application/json" });
8353
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8354
+ return;
8355
+ }
8356
+ const deadCodeDeps = {
8357
+ projectRoot: opts.projectRoot,
8358
+ ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8359
+ };
8360
+ await handleDeadCodeScan(res, deadCodeDeps, req);
8361
+ return;
8362
+ }
8363
+ if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8364
+ if (requireAccessToken && !accessTokenOk) {
8365
+ res.writeHead(401, { "Content-Type": "application/json" });
8366
+ res.end(JSON.stringify({ error: "Unauthorized" }));
8367
+ return;
8368
+ }
8369
+ if (!opts.projectRoot) {
8370
+ res.writeHead(503, { "Content-Type": "application/json" });
8371
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8372
+ return;
8373
+ }
8374
+ const deadCodeDeps = {
8375
+ projectRoot: opts.projectRoot,
8376
+ ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8377
+ };
8378
+ await handleDeadCodeActionPlan(res, deadCodeDeps, req);
8379
+ return;
8380
+ }
7935
8381
  if (url.pathname.startsWith("/api/techstack/")) {
7936
8382
  if (requireAccessToken && !accessTokenOk) {
7937
8383
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -8064,17 +8510,17 @@ function createHttpServer(opts) {
8064
8510
  }
8065
8511
  let filePath;
8066
8512
  if (url.pathname === "/" || url.pathname === "") {
8067
- filePath = path11.join(distDir, "index.html");
8513
+ filePath = path12.join(distDir, "index.html");
8068
8514
  } else {
8069
- filePath = path11.join(distDir, url.pathname);
8515
+ filePath = path12.join(distDir, url.pathname);
8070
8516
  }
8071
- const resolvedPath = path11.resolve(filePath);
8517
+ const resolvedPath = path12.resolve(filePath);
8072
8518
  if (!isInsideDist(resolvedPath, distDir)) {
8073
8519
  res.writeHead(403, { "Content-Type": "text/plain" });
8074
8520
  res.end("Forbidden");
8075
8521
  return;
8076
8522
  }
8077
- const ext = path11.extname(resolvedPath);
8523
+ const ext = path12.extname(resolvedPath);
8078
8524
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8079
8525
  res.setHeader("Content-Type", contentType);
8080
8526
  setStaticSecurityHeaders(res);
@@ -8098,7 +8544,7 @@ function createHttpServer(opts) {
8098
8544
  } catch (err) {
8099
8545
  if (err.code === "ENOENT") {
8100
8546
  try {
8101
- const html = await fs9.readFile(path11.join(distDir, "index.html"), "utf8");
8547
+ const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8102
8548
  setStaticSecurityHeaders(res);
8103
8549
  res.writeHead(200, {
8104
8550
  "Content-Type": "text/html",
@@ -8128,14 +8574,14 @@ function createHttpServer(opts) {
8128
8574
 
8129
8575
  // src/server/instance-registry.ts
8130
8576
  import * as os from "node:os";
8131
- import * as path12 from "node:path";
8577
+ import * as path13 from "node:path";
8132
8578
  import * as fs10 from "node:fs/promises";
8133
8579
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8134
8580
  function defaultBaseDir() {
8135
- return path12.join(os.homedir(), ".wrongstack");
8581
+ return path13.join(os.homedir(), ".wrongstack");
8136
8582
  }
8137
8583
  function registryPath(baseDir = defaultBaseDir()) {
8138
- return path12.join(baseDir, "webui-instances.json");
8584
+ return path13.join(baseDir, "webui-instances.json");
8139
8585
  }
8140
8586
  function isPidAlive(pid) {
8141
8587
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -11610,16 +12056,16 @@ function getSurfaceDefaultPorts(surface) {
11610
12056
  return { http: SURFACE_DEFAULT_PORTS[surface].http };
11611
12057
  }
11612
12058
  function isPortFree(host, port) {
11613
- return new Promise((resolve15) => {
12059
+ return new Promise((resolve16) => {
11614
12060
  const srv = net.createServer();
11615
- srv.once("error", () => resolve15(false));
12061
+ srv.once("error", () => resolve16(false));
11616
12062
  srv.once("listening", () => {
11617
- srv.close(() => resolve15(true));
12063
+ srv.close(() => resolve16(true));
11618
12064
  });
11619
12065
  try {
11620
12066
  srv.listen(port, host);
11621
12067
  } catch {
11622
- resolve15(false);
12068
+ resolve16(false);
11623
12069
  }
11624
12070
  });
11625
12071
  }
@@ -11644,10 +12090,10 @@ async function findFreePort(host, startPort, opts = {}) {
11644
12090
  import { spawn as spawn2 } from "node:child_process";
11645
12091
  import { existsSync } from "node:fs";
11646
12092
  import { findPackageJSON } from "node:module";
11647
- import * as path13 from "node:path";
12093
+ import * as path14 from "node:path";
11648
12094
  function resolveDistDir(input) {
11649
12095
  const options = typeof input === "string" ? { explicitDistDir: input } : input ?? {};
11650
- if (options.explicitDistDir) return path13.resolve(options.explicitDistDir);
12096
+ if (options.explicitDistDir) return path14.resolve(options.explicitDistDir);
11651
12097
  const exists = options.exists ?? existsSync;
11652
12098
  let packageTarget;
11653
12099
  try {
@@ -11659,15 +12105,15 @@ function resolveDistDir(input) {
11659
12105
  );
11660
12106
  }
11661
12107
  if (!packageTarget) return null;
11662
- const distDir = path13.basename(packageTarget) === "package.json" ? path13.join(path13.dirname(packageTarget), "dist") : path13.dirname(packageTarget);
12108
+ const distDir = path14.basename(packageTarget) === "package.json" ? path14.join(path14.dirname(packageTarget), "dist") : path14.dirname(packageTarget);
11663
12109
  if (options.exists === void 0 && options.resolvePackageJson) return distDir;
11664
- return exists(path13.join(distDir, "index.html")) ? distDir : null;
12110
+ return exists(path14.join(distDir, "index.html")) ? distDir : null;
11665
12111
  }
11666
12112
  async function ensureDistDir(explicitDistDir, deps2 = {}) {
11667
12113
  const exists = deps2.exists ?? existsSync;
11668
12114
  if (explicitDistDir) {
11669
- const resolved2 = path13.resolve(explicitDistDir);
11670
- return exists(path13.join(resolved2, "index.html")) ? resolved2 : null;
12115
+ const resolved2 = path14.resolve(explicitDistDir);
12116
+ return exists(path14.join(resolved2, "index.html")) ? resolved2 : null;
11671
12117
  }
11672
12118
  const resolveOptions = {
11673
12119
  resolvePackageJson: deps2.resolvePackageJson,
@@ -11679,7 +12125,7 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
11679
12125
  try {
11680
12126
  const packageJson = deps2.resolvePackageJson ? deps2.resolvePackageJson("@wrongstack/webui/package.json") : findPackageJSON("@wrongstack/webui", import.meta.url);
11681
12127
  if (!packageJson) throw new Error("not found");
11682
- packageDir = path13.dirname(packageJson);
12128
+ packageDir = path14.dirname(packageJson);
11683
12129
  } catch {
11684
12130
  throw new Error(
11685
12131
  "@wrongstack/webui package could not be resolved. Install workspace dependencies and rebuild the CLI."
@@ -11688,8 +12134,8 @@ async function ensureDistDir(explicitDistDir, deps2 = {}) {
11688
12134
  const findRoot = deps2.findWorkspaceRoot ?? ((pkgDir) => {
11689
12135
  let dir = pkgDir;
11690
12136
  for (let i = 0; i < 10; i++) {
11691
- if (existsSync(path13.join(dir, "pnpm-workspace.yaml"))) return dir;
11692
- const parent = path13.dirname(dir);
12137
+ if (existsSync(path14.join(dir, "pnpm-workspace.yaml"))) return dir;
12138
+ const parent = path14.dirname(dir);
11693
12139
  if (parent === dir) return null;
11694
12140
  dir = parent;
11695
12141
  }
@@ -11748,7 +12194,7 @@ async function startStaticServe(opts, deps2 = {}) {
11748
12194
  return { server, port: opts.httpPort };
11749
12195
  }
11750
12196
  function runPnpmBuild(cwd, workspace, timeoutMs) {
11751
- return new Promise((resolve15, reject) => {
12197
+ return new Promise((resolve16, reject) => {
11752
12198
  const child = spawn2("pnpm", ["--filter", workspace, "build"], {
11753
12199
  cwd,
11754
12200
  shell: process.platform === "win32",
@@ -11766,14 +12212,14 @@ function runPnpmBuild(cwd, workspace, timeoutMs) {
11766
12212
  });
11767
12213
  child.once("close", (code) => {
11768
12214
  clearTimeout(timer);
11769
- if (code === 0) resolve15();
12215
+ if (code === 0) resolve16();
11770
12216
  else reject(new Error(`pnpm build exited with code ${String(code)}`));
11771
12217
  });
11772
12218
  });
11773
12219
  }
11774
12220
 
11775
12221
  // src/server/embedded-lifecycle.ts
11776
- import * as path14 from "node:path";
12222
+ import * as path15 from "node:path";
11777
12223
 
11778
12224
  // src/server/network-info.ts
11779
12225
  import * as os2 from "node:os";
@@ -11839,7 +12285,7 @@ function registerWebuiInstance(p, deps2 = {}) {
11839
12285
  httpPort: p.httpPort,
11840
12286
  host: p.host,
11841
12287
  projectRoot: p.projectRoot,
11842
- projectName: path14.basename(p.projectRoot) || p.projectRoot,
12288
+ projectName: path15.basename(p.projectRoot) || p.projectRoot,
11843
12289
  startedAt: p.startedAt,
11844
12290
  url: buildWebUIAccessUrl({
11845
12291
  host: p.host,
@@ -12059,7 +12505,7 @@ ${text2}` : text2;
12059
12505
 
12060
12506
  // src/server/client-presence.ts
12061
12507
  import * as crypto2 from "node:crypto";
12062
- import * as path15 from "node:path";
12508
+ import * as path16 from "node:path";
12063
12509
  import {
12064
12510
  getSharedProjectMailbox as getSharedProjectMailbox2,
12065
12511
  resolveProjectDir as resolveProjectDir2
@@ -12077,7 +12523,7 @@ function createWebuiClientPresence(deps2) {
12077
12523
  if (!deps2.projectRoot) return null;
12078
12524
  try {
12079
12525
  const projectRoot = deps2.projectRoot;
12080
- const projectName = path15.basename(projectRoot);
12526
+ const projectName = path16.basename(projectRoot);
12081
12527
  const nextMailbox = getSharedProjectMailbox2(
12082
12528
  resolveProjectDir2(projectRoot, wstackGlobalRoot()),
12083
12529
  deps2.events,
@@ -13035,7 +13481,7 @@ function seedContextMeta(config, context) {
13035
13481
  meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
13036
13482
  meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
13037
13483
  meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
13038
- meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 5e3;
13484
+ meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
13039
13485
  meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
13040
13486
  meta["autoReviewMaxConcurrentReviews"] = typeof autoReviewExt?.["maxConcurrentReviews"] === "number" && autoReviewExt["maxConcurrentReviews"] >= 1 ? autoReviewExt["maxConcurrentReviews"] : 2;
13041
13487
  const cascade = autoReviewExt?.["cascadeOn"];
@@ -13055,7 +13501,7 @@ function seedContextMeta(config, context) {
13055
13501
 
13056
13502
  // src/server/pref-helpers.ts
13057
13503
  import * as fs12 from "node:fs/promises";
13058
- import * as path16 from "node:path";
13504
+ import * as path17 from "node:path";
13059
13505
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
13060
13506
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
13061
13507
  var PREF_KEYS = [
@@ -13151,7 +13597,7 @@ function prefSnapshot(contextMeta) {
13151
13597
  return snapshot;
13152
13598
  }
13153
13599
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
13154
- const globalRoot = path16.dirname(filePath);
13600
+ const globalRoot = path17.dirname(filePath);
13155
13601
  await backupConfigFile(filePath, { globalRoot });
13156
13602
  let raw;
13157
13603
  try {
@@ -13631,7 +14077,7 @@ async function handleProcessRoute(ws, msg, handlers) {
13631
14077
 
13632
14078
  // src/server/embedded-host-adapters.ts
13633
14079
  import * as fs15 from "node:fs/promises";
13634
- import * as path18 from "node:path";
14080
+ import * as path19 from "node:path";
13635
14081
  import { TOKENS } from "@wrongstack/core/kernel";
13636
14082
  import { DefaultSessionStore as DefaultSessionStore2 } from "@wrongstack/core/storage";
13637
14083
  import { toErrorMessage as toErrorMessage7, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
@@ -13639,7 +14085,7 @@ import { makeProviderFromConfig } from "@wrongstack/providers";
13639
14085
 
13640
14086
  // src/server/project-handlers.ts
13641
14087
  import * as fs13 from "node:fs/promises";
13642
- import * as path17 from "node:path";
14088
+ import * as path18 from "node:path";
13643
14089
  import { DefaultSessionStore } from "@wrongstack/core/storage";
13644
14090
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
13645
14091
  function createProjectHandlers(ctx) {
@@ -13691,8 +14137,8 @@ function createProjectHandlers(ctx) {
13691
14137
  });
13692
14138
  return;
13693
14139
  }
13694
- const resolved = path17.resolve(parsed.value.root);
13695
- const name2 = parsed.value.name?.trim() || path17.basename(resolved);
14140
+ const resolved = path18.resolve(parsed.value.root);
14141
+ const name2 = parsed.value.name?.trim() || path18.basename(resolved);
13696
14142
  try {
13697
14143
  const stat3 = await fs13.stat(resolved).catch(() => null);
13698
14144
  if (!stat3?.isDirectory()) {
@@ -13703,7 +14149,7 @@ function createProjectHandlers(ctx) {
13703
14149
  return;
13704
14150
  }
13705
14151
  const before = await loadManifest(ctx.globalConfigPath);
13706
- const already = before.projects.some((project) => path17.resolve(project.root) === resolved);
14152
+ const already = before.projects.some((project) => path18.resolve(project.root) === resolved);
13707
14153
  const entry = await touchProjectInManifest(
13708
14154
  { projectRoot: resolved, workingDir: resolved, name: name2 },
13709
14155
  ctx.globalConfigPath
@@ -13733,8 +14179,8 @@ function createProjectHandlers(ctx) {
13733
14179
  });
13734
14180
  return;
13735
14181
  }
13736
- const resolved = path17.resolve(parsed.value.root);
13737
- const name2 = parsed.value.name?.trim() || path17.basename(resolved);
14182
+ const resolved = path18.resolve(parsed.value.root);
14183
+ const name2 = parsed.value.name?.trim() || path18.basename(resolved);
13738
14184
  if (!ctx.allowProjectMutations) {
13739
14185
  sendTo(ws, {
13740
14186
  type: "projects.selected",
@@ -13769,6 +14215,17 @@ function createProjectHandlers(ctx) {
13769
14215
  });
13770
14216
  const previous = ctx.getSession();
13771
14217
  const previousId = previous.id;
14218
+ const previousProjectRoot = ctx.getProjectRoot();
14219
+ const previousPaths = resolveWstackPaths4({
14220
+ projectRoot: previousProjectRoot,
14221
+ globalRoot: ctx.wpaths.globalRoot
14222
+ });
14223
+ const previousIdentityTarget = {
14224
+ projectSlug: previousPaths.projectSlug,
14225
+ projectRoot: previousProjectRoot,
14226
+ projectName: path18.basename(previousProjectRoot),
14227
+ workingDir: ctx.context.workingDir
14228
+ };
13772
14229
  const previousUsage = ctx.tokenCounter.total();
13773
14230
  const config = ctx.getConfig?.() ?? ctx.config;
13774
14231
  const next = await store.create({
@@ -13794,7 +14251,16 @@ function createProjectHandlers(ctx) {
13794
14251
  };
13795
14252
  try {
13796
14253
  await ctx.onSessionSwapped?.(next.id, identityTarget);
14254
+ await ctx.onBeforeSessionTodosReplaced?.(next.id, paths.projectSessions);
13797
14255
  } catch (err) {
14256
+ try {
14257
+ await ctx.onBeforeSessionTodosReplaced?.(previous.id, previousPaths.projectSessions);
14258
+ } catch {
14259
+ }
14260
+ try {
14261
+ await ctx.onSessionSwapped?.(previous.id, previousIdentityTarget);
14262
+ } catch {
14263
+ }
13798
14264
  await next.close().catch(() => void 0);
13799
14265
  await store.delete(next.id).catch(() => void 0);
13800
14266
  throw err;
@@ -14880,6 +15346,7 @@ var CLIENT_WORKSPACE_MESSAGE_TYPES = [
14880
15346
  var CLIENT_CONFIGURATION_MESSAGE_TYPES = [
14881
15347
  "codebase.index.server.shutdown",
14882
15348
  "connections.health",
15349
+ "connections.service_action",
14883
15350
  "diag.get",
14884
15351
  "key.add",
14885
15352
  "key.delete",
@@ -15153,6 +15620,7 @@ var SERVER_CONFIGURATION_MESSAGE_TYPES = [
15153
15620
  "codebase.index.server.shutdown_result",
15154
15621
  "connections.health_error",
15155
15622
  "connections.health_result",
15623
+ "connections.service_action_result",
15156
15624
  "diag.get",
15157
15625
  "key.operation_result",
15158
15626
  "model.switch_result",
@@ -15197,13 +15665,13 @@ function isRegisteredMessageType(type, direction) {
15197
15665
  // src/protocol/decoder.ts
15198
15666
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
15199
15667
  var MAX_PAYLOAD_DEPTH = 32;
15200
- function inspectValue(value, path33, depth) {
15668
+ function inspectValue(value, path34, depth) {
15201
15669
  if (depth > MAX_PAYLOAD_DEPTH) {
15202
- return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path33 };
15670
+ return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path34 };
15203
15671
  }
15204
15672
  if (value === null || typeof value !== "object") return null;
15205
15673
  for (const key of Object.keys(value)) {
15206
- const childPath = `${path33}.${key}`;
15674
+ const childPath = `${path34}.${key}`;
15207
15675
  if (FORBIDDEN_KEYS.has(key)) {
15208
15676
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
15209
15677
  }
@@ -15344,7 +15812,8 @@ function projectToolMessage(message) {
15344
15812
  name: text(payload["name"]),
15345
15813
  ok: payload["ok"] !== false,
15346
15814
  durationMs: finite(payload["durationMs"]),
15347
- ...typeof payload["output"] === "string" ? { output: payload["output"] } : {}
15815
+ ...typeof payload["output"] === "string" ? { output: payload["output"] } : {},
15816
+ ...Array.isArray(payload["sage"]) ? { sage: payload["sage"].filter((line) => typeof line === "string") } : {}
15348
15817
  };
15349
15818
  }
15350
15819
  return null;
@@ -15584,6 +16053,7 @@ function createSessionHandlers(ctx) {
15584
16053
  ctx.context.session = next;
15585
16054
  ctx.context.state.replaceMessages(messages);
15586
16055
  await ctx.context.flushConversationJournal?.();
16056
+ await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
15587
16057
  ctx.context.state.replaceTodos(todos);
15588
16058
  resetContextAccounting();
15589
16059
  ctx.context.readFiles.clear();
@@ -15980,7 +16450,10 @@ function createSessionHandlers(ctx) {
15980
16450
  rollbackClaim = await ctx.claimSession?.(canonicalId);
15981
16451
  const resumed = await store.resume(canonicalId);
15982
16452
  const restoredTodos = await loadTodosCheckpoint(
15983
- sessionScopedPath(sessionsDirectory(), resumed.writer.id, ".todos.json")
16453
+ sessionScopedPath(sessionsDirectory(), resumed.writer.id, ".todos.json"),
16454
+ ctx.events,
16455
+ ctx.context.traceId,
16456
+ resumed.writer.id
15984
16457
  ).catch(() => null) ?? [];
15985
16458
  activated = true;
15986
16459
  await activateSession(
@@ -16132,7 +16605,7 @@ function createEmbeddedConversationRoutes(ctx) {
16132
16605
  }
16133
16606
  function sessionStoreFor(opts) {
16134
16607
  const projectRoot = opts.projectRoot ?? opts.agent.ctx.projectRoot;
16135
- return opts.sessionStore ?? new DefaultSessionStore2({ dir: path18.join(projectRoot, ".wrongstack", "sessions"), projectRoot });
16608
+ return opts.sessionStore ?? new DefaultSessionStore2({ dir: path19.join(projectRoot, ".wrongstack", "sessions"), projectRoot });
16136
16609
  }
16137
16610
  function createEmbeddedSessionRoutes(ctx) {
16138
16611
  const { opts } = ctx;
@@ -16142,6 +16615,7 @@ function createEmbeddedSessionRoutes(ctx) {
16142
16615
  config: { model: actx.model ?? "", provider: actx.provider?.id ?? "" },
16143
16616
  getConfig: () => ({ model: actx.model ?? "", provider: actx.provider?.id ?? "" }),
16144
16617
  context: actx,
16618
+ events: opts.events,
16145
16619
  listTools: () => opts.agent.tools.list(),
16146
16620
  getCompactor: () => opts.agent.container.resolve(TOKENS.Compactor),
16147
16621
  getCustomModeStore: ctx.getCustomModeStore,
@@ -16150,11 +16624,12 @@ function createEmbeddedSessionRoutes(ctx) {
16150
16624
  getSession: () => actx.session ?? opts.session,
16151
16625
  getSessionStore: () => sessionStoreFor(opts),
16152
16626
  canSwapSessions: () => opts.sessionStore !== void 0,
16153
- getSessionsDir: () => opts.sessionsDir ?? path18.join(getProjectRoot(), ".wrongstack", "sessions"),
16627
+ getSessionsDir: () => opts.sessionsDir ?? path19.join(getProjectRoot(), ".wrongstack", "sessions"),
16154
16628
  setSession: (next) => {
16155
16629
  actx.session = next;
16156
16630
  },
16157
16631
  claimSession: opts.claimSession,
16632
+ onBeforeSessionTodosReplaced: async (sessionId, sessionsDir) => opts.onBeforeSessionTodosReplaced?.(sessionId, sessionsDir),
16158
16633
  onSessionSwapped: async (sessionId, target) => opts.onSessionSwapped?.(sessionId, target),
16159
16634
  abortActiveRun: ctx.abortActiveRun,
16160
16635
  isRunActive: ctx.isRunActive,
@@ -16166,7 +16641,7 @@ function createEmbeddedSessionRoutes(ctx) {
16166
16641
  async function broadcastEmbeddedGoalSnapshot(ctx) {
16167
16642
  const projectRoot = ctx.opts.projectRoot ?? ctx.opts.agent.ctx.projectRoot;
16168
16643
  try {
16169
- const raw = await fs15.readFile(path18.join(projectRoot, ".wrongstack", "goal.json"), "utf8");
16644
+ const raw = await fs15.readFile(path19.join(projectRoot, ".wrongstack", "goal.json"), "utf8");
16170
16645
  ctx.broadcast({ type: "goal-state.updated", payload: JSON.parse(raw) });
16171
16646
  } catch {
16172
16647
  ctx.broadcast({ type: "goal-state.updated", payload: null });
@@ -16175,10 +16650,10 @@ async function broadcastEmbeddedGoalSnapshot(ctx) {
16175
16650
  function createEmbeddedProjectRoutes(ctx) {
16176
16651
  const { opts } = ctx;
16177
16652
  const actx = opts.agent.ctx;
16178
- const globalConfigPath = opts.globalConfigPath ?? path18.join(wstackGlobalRoot2(), "config.json");
16653
+ const globalConfigPath = opts.globalConfigPath ?? path19.join(wstackGlobalRoot2(), "config.json");
16179
16654
  return createProjectHandlers({
16180
16655
  globalConfigPath,
16181
- wpaths: { globalRoot: path18.dirname(globalConfigPath) },
16656
+ wpaths: { globalRoot: path19.dirname(globalConfigPath) },
16182
16657
  context: actx,
16183
16658
  tokenCounter: actx.tokenCounter,
16184
16659
  config: { model: actx.model, provider: actx.provider.id },
@@ -16203,6 +16678,7 @@ function createEmbeddedProjectRoutes(ctx) {
16203
16678
  for (const controller of ctx.abortControllers.values()) controller.abort();
16204
16679
  ctx.abortControllers.clear();
16205
16680
  },
16681
+ onBeforeSessionTodosReplaced: async (sessionId, sessionsDir) => opts.onBeforeSessionTodosReplaced?.(sessionId, sessionsDir),
16206
16682
  onSessionSwapped: async (sessionId, target) => opts.onSessionSwapped?.(sessionId, target),
16207
16683
  allowProjectMutations: true,
16208
16684
  sessionStartPayload: ctx.buildSessionStart,
@@ -16658,7 +17134,7 @@ ${String(p.content ?? "")}`;
16658
17134
  };
16659
17135
 
16660
17136
  // src/server/codebase-index-server-control.ts
16661
- import { shutdownCodebaseIndexServer } from "@wrongstack/tools";
17137
+ import { shutdownCodebaseIndexServer as shutdownCodebaseIndexServer2 } from "@wrongstack/tools";
16662
17138
  async function handleCodebaseIndexServerControl(ws, message, deps2) {
16663
17139
  if (message.type !== "codebase.index.server.shutdown") return false;
16664
17140
  const requestId = message.payload && typeof message.payload === "object" && typeof message.payload.requestId === "string" ? message.payload.requestId : "";
@@ -16685,7 +17161,7 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
16685
17161
  });
16686
17162
  return true;
16687
17163
  }
16688
- const result = await shutdownCodebaseIndexServer(
17164
+ const result = await shutdownCodebaseIndexServer2(
16689
17165
  projectRoot,
16690
17166
  deps2.getIndexDir(),
16691
17167
  "websocket-request"
@@ -17226,7 +17702,7 @@ function createRouteFamilyDispatcher(options) {
17226
17702
 
17227
17703
  // src/server/shell-open.ts
17228
17704
  import * as fs16 from "node:fs/promises";
17229
- import * as path19 from "node:path";
17705
+ import * as path20 from "node:path";
17230
17706
  import { spawn as spawn3 } from "node:child_process";
17231
17707
  function normalizeShellOpenTarget(target) {
17232
17708
  return target === "terminal" ? "terminal" : "file-manager";
@@ -17237,11 +17713,11 @@ function shellQuote(s) {
17237
17713
  }
17238
17714
  async function handleShellOpen(req, logger, options) {
17239
17715
  try {
17240
- const resolved = path19.resolve(req.path);
17716
+ const resolved = path20.resolve(req.path);
17241
17717
  if (options?.projectRoot) {
17242
- const root = path19.resolve(options.projectRoot);
17243
- const relative5 = path19.relative(root, resolved);
17244
- const escapes = relative5.startsWith("..") || path19.isAbsolute(relative5);
17718
+ const root = path20.resolve(options.projectRoot);
17719
+ const relative5 = path20.relative(root, resolved);
17720
+ const escapes = relative5.startsWith("..") || path20.isAbsolute(relative5);
17245
17721
  if (escapes) {
17246
17722
  return {
17247
17723
  success: false,
@@ -17632,6 +18108,13 @@ function createEmbeddedMessageRouter(deps2) {
17632
18108
  message
17633
18109
  ))
17634
18110
  return;
18111
+ if (await handleConnectionsServiceAction(ws, message, {
18112
+ getProjectRoot: projectRoot,
18113
+ getIndexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
18114
+ send: send2,
18115
+ backend: "cli-embedded"
18116
+ }))
18117
+ return;
17635
18118
  if (await handleCodebaseIndexServerControl(ws, message, {
17636
18119
  trustBoundary: deps2.trustBoundary,
17637
18120
  logger: deps2.logger,
@@ -17646,7 +18129,7 @@ function createEmbeddedMessageRouter(deps2) {
17646
18129
  }
17647
18130
 
17648
18131
  // src/server/provider-config-standalone.ts
17649
- import * as path20 from "node:path";
18132
+ import * as path21 from "node:path";
17650
18133
  import { DefaultSecretVault } from "@wrongstack/core/security";
17651
18134
  function createProviderConfigIO(configPath) {
17652
18135
  const keyFile = vaultKeyFileForConfigPath(configPath);
@@ -17657,10 +18140,10 @@ function createProviderConfigIO(configPath) {
17657
18140
  };
17658
18141
  }
17659
18142
  function vaultKeyFileForConfigPath(configPath) {
17660
- const configDir = path20.dirname(configPath);
17661
- const parentDir = path20.dirname(configDir);
17662
- const globalRoot = path20.basename(parentDir) === "profiles" ? path20.dirname(parentDir) : configDir;
17663
- return path20.join(globalRoot, ".key");
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");
17664
18147
  }
17665
18148
 
17666
18149
  // src/server/provider-store.ts
@@ -17677,8 +18160,8 @@ function createConfigWriteLock() {
17677
18160
  acquire() {
17678
18161
  const prev = lock;
17679
18162
  let release = () => void 0;
17680
- lock = new Promise((resolve15) => {
17681
- release = resolve15;
18163
+ lock = new Promise((resolve16) => {
18164
+ release = resolve16;
17682
18165
  });
17683
18166
  return { prev, release };
17684
18167
  }
@@ -17756,6 +18239,7 @@ function createProviderStore(deps2) {
17756
18239
  import { listBoards as listBoards5 } from "@wrongstack/kanban";
17757
18240
  import {
17758
18241
  applySddLifecycle,
18242
+ extractVerificationCommand,
17759
18243
  SddBoardStore
17760
18244
  } from "@wrongstack/sdd";
17761
18245
  var CONTROL_TYPES = /* @__PURE__ */ new Set([
@@ -17778,14 +18262,16 @@ var SddBoardWebSocketHandler = class {
17778
18262
  store;
17779
18263
  clients = /* @__PURE__ */ new Set();
17780
18264
  lifecycle;
18265
+ security;
17781
18266
  diskPollingEnabled;
17782
18267
  latest = null;
17783
18268
  poll = null;
17784
18269
  pollInFlight = false;
17785
18270
  unsub = null;
17786
- constructor(boardsDir, events, lifecycle) {
18271
+ constructor(boardsDir, events, lifecycle, security) {
17787
18272
  this.store = new SddBoardStore({ baseDir: boardsDir });
17788
18273
  this.lifecycle = lifecycle;
18274
+ this.security = security;
17789
18275
  this.diskPollingEnabled = events === void 0;
17790
18276
  if (events) {
17791
18277
  const handler = (e) => {
@@ -17831,6 +18317,43 @@ var SddBoardWebSocketHandler = class {
17831
18317
  return;
17832
18318
  }
17833
18319
  if (CONTROL_TYPES.has(action)) {
18320
+ const verificationCommands = [];
18321
+ if (action === "set_task_verification") {
18322
+ const command = msg.payload?.verificationCommand;
18323
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
18324
+ if (typeof command === "string" && command.trim()) {
18325
+ verificationCommands.push({ command, operation: "sdd.set_task_verification" });
18326
+ }
18327
+ } else if (action === "split_task") {
18328
+ const subtasks = msg.payload?.subtasks;
18329
+ if (Array.isArray(subtasks)) {
18330
+ for (const subtask of subtasks) {
18331
+ if (!subtask || typeof subtask !== "object") continue;
18332
+ const criterion = subtask.successCriterion;
18333
+ if (criterion === void 0) continue;
18334
+ if (typeof criterion !== "string") return;
18335
+ const command = extractVerificationCommand([criterion]);
18336
+ if (!command) continue;
18337
+ if (command.length > 8192) return;
18338
+ verificationCommands.push({ command, operation: "sdd.split_task_verification" });
18339
+ }
18340
+ }
18341
+ }
18342
+ for (const { command, operation } of verificationCommands) {
18343
+ if (!this.security) return;
18344
+ const authorization = await authorizeWebUIAction(
18345
+ this.security.trustBoundary,
18346
+ {
18347
+ capability: "process.spawn",
18348
+ subject: { kind: "command", id: command },
18349
+ risk: "high",
18350
+ cwd: this.lifecycle?.projectRoot,
18351
+ metadata: { operation }
18352
+ },
18353
+ this.security.logger
18354
+ );
18355
+ if (!authorization.allowed) return;
18356
+ }
17834
18357
  const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
17835
18358
  if (runId) {
17836
18359
  await this.store.appendControl(runId, {
@@ -17943,7 +18466,7 @@ var SddBoardWebSocketHandler = class {
17943
18466
  };
17944
18467
 
17945
18468
  // src/server/sdd-wizard-wiring.ts
17946
- import * as path21 from "node:path";
18469
+ import * as path22 from "node:path";
17947
18470
  import {
17948
18471
  DefaultTaskStore,
17949
18472
  TaskTracker
@@ -18057,7 +18580,7 @@ function buildSddWizardDeps(opts) {
18057
18580
  }).catch(() => {
18058
18581
  projectContext = "";
18059
18582
  });
18060
- const sessionPath = opts.paths.projectSddSession ?? path21.join(opts.paths.projectDir, "sdd-session.json");
18583
+ const sessionPath = opts.paths.projectSddSession ?? path22.join(opts.paths.projectDir, "sdd-session.json");
18061
18584
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
18062
18585
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
18063
18586
  const runIsolatedTurn = async (prompt, name2) => {
@@ -18339,7 +18862,7 @@ var SddWizardWebSocketHandler = class {
18339
18862
  return;
18340
18863
  }
18341
18864
  const { runId } = await this.deps.startRun(this.driver, opts);
18342
- this.driver.setLastRunId(runId);
18865
+ await this.driver.setLastRunId(runId);
18343
18866
  if (this.driver.phase() !== "executing" && this.driver.phase() !== "done") {
18344
18867
  try {
18345
18868
  if (this.driver.phase() === "task_review") await this.driver.approve();
@@ -18398,7 +18921,7 @@ var SddWizardWebSocketHandler = class {
18398
18921
  this.lastAgentText = text2;
18399
18922
  if (this.driver) {
18400
18923
  await this.driver.ingestAgentOutput(text2);
18401
- this.driver.setLastAgentText(text2);
18924
+ await this.driver.setLastAgentText(text2);
18402
18925
  }
18403
18926
  this.broadcast({ type: "sdd.spec.agent_text", payload: { text: text2 } });
18404
18927
  } finally {
@@ -18429,10 +18952,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
18429
18952
 
18430
18953
  // src/server/setup-events-fleet-broadcaster.ts
18431
18954
  import { watch as fsWatch } from "node:fs";
18432
- import * as path22 from "node:path";
18955
+ import * as path23 from "node:path";
18433
18956
  function registerSetupEventsFleetBroadcaster(deps2) {
18434
18957
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
18435
- const globalRoot = globalConfigPath ? path22.dirname(globalConfigPath) : void 0;
18958
+ const globalRoot = globalConfigPath ? path23.dirname(globalConfigPath) : void 0;
18436
18959
  if (!globalRoot) return void 0;
18437
18960
  const disposers = [];
18438
18961
  const broadcastSessions = async () => {
@@ -18442,8 +18965,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
18442
18965
  const sessions = await registry.list();
18443
18966
  const ownEntry = sessions.find((s) => s.pid === process.pid);
18444
18967
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
18445
- const myRoot = path22.resolve(context.projectRoot);
18446
- const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path22.resolve(s.projectRoot) === myRoot).map((s) => ({
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) => ({
18447
18970
  sessionId: s.sessionId,
18448
18971
  projectName: s.projectName,
18449
18972
  projectSlug: s.projectSlug,
@@ -18634,13 +19157,13 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
18634
19157
  // src/server/setup-events-status-watcher.ts
18635
19158
  import { watch as fsWatch2 } from "node:fs";
18636
19159
  import * as fs18 from "node:fs/promises";
18637
- import * as path24 from "node:path";
19160
+ import * as path25 from "node:path";
18638
19161
 
18639
19162
  // src/server/setup-events-watcher.ts
18640
- import * as path23 from "node:path";
19163
+ import * as path24 from "node:path";
18641
19164
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
18642
19165
  const raw = String(filename);
18643
- const relative5 = path23.isAbsolute(raw) ? path23.relative(projectsDir, raw) : raw;
19166
+ const relative5 = path24.isAbsolute(raw) ? path24.relative(projectsDir, raw) : raw;
18644
19167
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
18645
19168
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
18646
19169
  return parts.at(-2) ?? null;
@@ -18675,7 +19198,7 @@ function logFileWatcherMetrics(metrics) {
18675
19198
  function registerSetupEventsStatusWatcher(deps2) {
18676
19199
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
18677
19200
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
18678
- const projectsDir = path24.join(wpaths.globalRoot, "projects");
19201
+ const projectsDir = path25.join(wpaths.globalRoot, "projects");
18679
19202
  const knownProjectHashes = /* @__PURE__ */ new Set();
18680
19203
  const debounceTimers = /* @__PURE__ */ new Map();
18681
19204
  const DEBOUNCE_MS2 = 150;
@@ -18734,7 +19257,7 @@ function registerSetupEventsStatusWatcher(deps2) {
18734
19257
  if (!knownProjectHashes.has(projectHash)) return;
18735
19258
  if (watcherMetrics) watcherMetrics.filesProcessed++;
18736
19259
  try {
18737
- const targetFile = path24.join(projectsDir, projectHash, "status.json");
19260
+ const targetFile = path25.join(projectsDir, projectHash, "status.json");
18738
19261
  const content = await fs18.readFile(targetFile, "utf-8");
18739
19262
  const statusData = JSON.parse(content);
18740
19263
  scheduleBroadcast(projectHash, statusData);
@@ -18793,7 +19316,7 @@ function registerSetupEventsStatusWatcher(deps2) {
18793
19316
 
18794
19317
  // src/server/setup-events-core-watchers.ts
18795
19318
  import * as fs19 from "node:fs/promises";
18796
- import * as path25 from "node:path";
19319
+ import * as path26 from "node:path";
18797
19320
  function registerSetupEventsCoreWatchers(deps2) {
18798
19321
  const { broadcast: broadcast2, clients, context } = deps2;
18799
19322
  const disposers = [];
@@ -18829,7 +19352,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
18829
19352
  if (wpaths?.projectStatus) {
18830
19353
  try {
18831
19354
  const statusFile = wpaths.projectStatus(e.projectHash);
18832
- const dir = path25.dirname(statusFile);
19355
+ const dir = path26.dirname(statusFile);
18833
19356
  await fs19.mkdir(dir, { recursive: true });
18834
19357
  await fs19.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
18835
19358
  } catch (err) {
@@ -19019,6 +19542,9 @@ function setupEvents(deps2) {
19019
19542
  input: scrub(e.input),
19020
19543
  fileTargets: extractCodeMapFileTargets(projectRoot || ".", e.name, e.input),
19021
19544
  output: scrub(e.output),
19545
+ // SAGE-injected memory rides beside the tool text so the client renders
19546
+ // it as a memory card. Never folded back into `output`.
19547
+ ...e.sage && e.sage.length > 0 ? { sage: e.sage.map((line) => scrub(line)) } : {},
19022
19548
  outputBytes: e.outputBytes,
19023
19549
  outputTokens: e.outputTokens,
19024
19550
  outputLines: e.outputLines,
@@ -19853,17 +20379,24 @@ var SpecsWebSocketHandler = class {
19853
20379
  // src/server/start-webui.ts
19854
20380
  import { randomUUID as randomUUID6 } from "node:crypto";
19855
20381
  import * as http2 from "node:http";
19856
- import * as path32 from "node:path";
20382
+ import * as path33 from "node:path";
19857
20383
  import { createDefaultPipelines } from "@wrongstack/core/agent";
19858
20384
  import { getSharedProjectMailbox as getSharedProjectMailbox5, resolveProjectDir as resolveProjectDir4 } from "@wrongstack/core/coordination";
19859
20385
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
19860
20386
  import {
20387
+ attachTodosCheckpoint,
19861
20388
  createSessionEventBridge,
19862
20389
  resolveSessionLoggingConfig,
19863
20390
  watchProviderConfig
19864
20391
  } from "@wrongstack/core/storage";
19865
20392
  import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } from "@wrongstack/core/types";
19866
- import { expectDefined as expectDefined4, startHeapWatchdog, toErrorMessage as toErrorMessage14, wstackGlobalRoot as wstackGlobalRoot4 } from "@wrongstack/core/utils";
20393
+ import {
20394
+ expectDefined as expectDefined4,
20395
+ sessionScopedPath as sessionScopedPath3,
20396
+ startHeapWatchdog,
20397
+ toErrorMessage as toErrorMessage14,
20398
+ wstackGlobalRoot as wstackGlobalRoot4
20399
+ } from "@wrongstack/core/utils";
19867
20400
  import { makeProviderFromConfig as makeProviderFromConfig5 } from "@wrongstack/providers";
19868
20401
  import { toLanguagePackageInput } from "@wrongstack/techstack";
19869
20402
  import { ensureSessionShell } from "@wrongstack/tools";
@@ -20050,7 +20583,7 @@ function findWorkspaceCliEntry(projectRoot) {
20050
20583
  return null;
20051
20584
  }
20052
20585
  function sleep(ms) {
20053
- return new Promise((resolve15) => setTimeout(resolve15, ms));
20586
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
20054
20587
  }
20055
20588
 
20056
20589
  // src/server/terminal-ws-handler.ts
@@ -20286,7 +20819,7 @@ function clampDim(value, fallback) {
20286
20819
  }
20287
20820
 
20288
20821
  // src/server/worktree-ws-handler.ts
20289
- import { join as join14, resolve as resolve12, sep as sep4 } from "node:path";
20822
+ import { join as join14, resolve as resolve13, sep as sep5 } from "node:path";
20290
20823
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
20291
20824
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
20292
20825
  import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
@@ -20347,13 +20880,13 @@ var WorktreeWebSocketHandler = class {
20347
20880
  // ── orphan management ─────────────────────────────────────────────────────
20348
20881
  /** Absolute managed-worktrees root for this project. */
20349
20882
  worktreesRoot() {
20350
- return resolve12(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
20883
+ return resolve13(join14(this.management.projectRoot, ".wrongstack", "worktrees"));
20351
20884
  }
20352
20885
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
20353
20886
  underRoot(dir) {
20354
- const abs = resolve12(dir);
20887
+ const abs = resolve13(dir);
20355
20888
  const root = this.worktreesRoot();
20356
- return abs !== root && abs.startsWith(root + sep4);
20889
+ return abs !== root && abs.startsWith(root + sep5);
20357
20890
  }
20358
20891
  /** Branches of worktrees a live in-session run currently owns. */
20359
20892
  liveActiveBranches() {
@@ -20501,7 +21034,7 @@ var WorktreeWebSocketHandler = class {
20501
21034
  }
20502
21035
  const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
20503
21036
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
20504
- const summary = await wt.diffSummary(resolve12(dir), base);
21037
+ const summary = await wt.diffSummary(resolve13(dir), base);
20505
21038
  this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
20506
21039
  }
20507
21040
  // ── internals ───────────────────────────────────────────────────────────
@@ -20650,7 +21183,9 @@ async function createAgentServices(input) {
20650
21183
  memory: memoryRetrieval,
20651
21184
  maxHintsPerTool: config.Sage?.inject?.maxHintsPerTool,
20652
21185
  maxCharsPerTool: config.Sage?.inject?.maxCharsPerTool,
21186
+ taskAware: config.Sage?.inject?.taskAware,
20653
21187
  minScore: config.Sage?.inject?.minScore,
21188
+ minImportance: config.Sage?.inject?.minImportance,
20654
21189
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
20655
21190
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
20656
21191
  triggers: config.Sage?.inject?.triggers
@@ -20953,15 +21488,20 @@ async function createAgentServices(input) {
20953
21488
  projectRoot
20954
21489
  );
20955
21490
  const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
20956
- const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards, void 0, {
20957
- projectRoot,
20958
- paths: {
20959
- projectSpecs: wpaths.projectSpecs,
20960
- projectTaskGraphs: wpaths.projectTaskGraphs,
20961
- projectSddSession: wpaths.projectSddSession,
20962
- projectSddBoards: wpaths.projectSddBoards
20963
- }
20964
- });
21491
+ const sddBoardHandler = new SddBoardWebSocketHandler(
21492
+ wpaths.projectSddBoards,
21493
+ void 0,
21494
+ {
21495
+ projectRoot,
21496
+ paths: {
21497
+ projectSpecs: wpaths.projectSpecs,
21498
+ projectTaskGraphs: wpaths.projectTaskGraphs,
21499
+ projectSddSession: wpaths.projectSddSession,
21500
+ projectSddBoards: wpaths.projectSddBoards
21501
+ }
21502
+ },
21503
+ { trustBoundary: input.trustBoundary, logger }
21504
+ );
20965
21505
  const sddWizardHandler = new SddWizardWebSocketHandler(
20966
21506
  buildSddWizardDeps({
20967
21507
  agent,
@@ -20973,7 +21513,16 @@ async function createAgentServices(input) {
20973
21513
  providerRegistry,
20974
21514
  toolRegistry,
20975
21515
  session: input.sessionGetter(),
20976
- projectRoot
21516
+ projectRoot,
21517
+ // Thread the container-provided ProviderModelStatusTracker so a 429
21518
+ // from this subagent's first call transitions the (provider, model)
21519
+ // pair to `state: 'blocked'` instead of silently no-op'ing. The
21520
+ // runtime container binds a default `ProviderModelStatusTracker`
21521
+ // (see packages/runtime/src/container.ts); without this dep, the
21522
+ // subagent's fallback extension's tracker hooks are undefined and
21523
+ // round-robin keeps reassigning the doomed model. Mirrors the CLI
21524
+ // factory wiring at host-subagent-factory.ts:337.
21525
+ statusTracker: container.safeResolve(TOKENS2.ProviderModelStatusTracker)
20977
21526
  }),
20978
21527
  paths: {
20979
21528
  projectSpecs: wpaths.projectSpecs,
@@ -21116,7 +21665,7 @@ function createConnectionHandler(options) {
21116
21665
  }
21117
21666
 
21118
21667
  // src/server/message-dispatcher.ts
21119
- import path26 from "node:path";
21668
+ import path27 from "node:path";
21120
21669
  function createMessageDispatcher(opts) {
21121
21670
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
21122
21671
  function makeWorklistContext() {
@@ -21137,7 +21686,7 @@ function createMessageDispatcher(opts) {
21137
21686
  skillLoader: deps2.skillLoader,
21138
21687
  skillInstaller: deps2.skillInstaller,
21139
21688
  projectRoot,
21140
- projectSkillsDir: path26.join(projectRoot, ".wrongstack", "skills"),
21689
+ projectSkillsDir: path27.join(projectRoot, ".wrongstack", "skills"),
21141
21690
  globalSkillsDir: deps2.wpaths.globalSkills
21142
21691
  };
21143
21692
  }
@@ -21389,7 +21938,7 @@ function createMessageDispatcher(opts) {
21389
21938
 
21390
21939
  // src/server/pre-context-services.ts
21391
21940
  import { createRequire as createRequire3 } from "node:module";
21392
- import * as path29 from "node:path";
21941
+ import * as path30 from "node:path";
21393
21942
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
21394
21943
  import {
21395
21944
  getSharedProjectMailbox as getSharedProjectMailbox4,
@@ -21444,7 +21993,7 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
21444
21993
 
21445
21994
  // src/server/model-auto-discovery.ts
21446
21995
  import * as fs20 from "node:fs/promises";
21447
- import * as path27 from "node:path";
21996
+ import * as path28 from "node:path";
21448
21997
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
21449
21998
  function isOverlayRegistry(value) {
21450
21999
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -21480,7 +22029,7 @@ async function discoverAndMergeWebuiProviders(opts) {
21480
22029
  if (!isOverlayRegistry(registry)) return;
21481
22030
  const targets = eligibleProviders(opts.config);
21482
22031
  if (targets.length === 0) return;
21483
- const cacheFile = path27.join(opts.cacheDir, "discovered-models-cache.json");
22032
+ const cacheFile = path28.join(opts.cacheDir, "discovered-models-cache.json");
21484
22033
  const cache2 = await readCache(cacheFile);
21485
22034
  let cacheDirty = false;
21486
22035
  await Promise.all(
@@ -21517,7 +22066,7 @@ async function discoverAndMergeWebuiProviders(opts) {
21517
22066
  );
21518
22067
  if (cacheDirty) {
21519
22068
  try {
21520
- await fs20.mkdir(path27.dirname(cacheFile), { recursive: true });
22069
+ await fs20.mkdir(path28.dirname(cacheFile), { recursive: true });
21521
22070
  await fs20.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
21522
22071
  } catch {
21523
22072
  opts.logger?.debug?.("provider auto-discovery cache write failed");
@@ -21614,7 +22163,7 @@ function resolveSetupProvider(opts) {
21614
22163
  }
21615
22164
 
21616
22165
  // src/server/standalone-session-identity.ts
21617
- import * as path28 from "node:path";
22166
+ import * as path29 from "node:path";
21618
22167
  import {
21619
22168
  AgentStatusTracker,
21620
22169
  FleetNotifier,
@@ -21633,7 +22182,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
21633
22182
  let activeTarget = {
21634
22183
  projectSlug: paths.projectSlug,
21635
22184
  projectRoot: paths.projectRoot,
21636
- projectName: path28.basename(paths.projectRoot),
22185
+ projectName: path29.basename(paths.projectRoot),
21637
22186
  workingDir: opts.workingDir
21638
22187
  };
21639
22188
  let pendingClaim;
@@ -21864,7 +22413,7 @@ async function createPreContextServices(input) {
21864
22413
  await discoverAndMergeWebuiProviders({
21865
22414
  config,
21866
22415
  registry: modelsRegistry,
21867
- cacheDir: path29.dirname(wpaths.modelsCache),
22416
+ cacheDir: path30.dirname(wpaths.modelsCache),
21868
22417
  logger
21869
22418
  });
21870
22419
  } catch (err) {
@@ -21916,7 +22465,7 @@ async function createPreContextServices(input) {
21916
22465
  configureChildEnvGitIdentity(config.git?.identity ?? null);
21917
22466
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
21918
22467
  const mcpTokenStore = new MCPVaultTokenStore(
21919
- path29.join(wpaths.projectDir, "mcp-auth.json"),
22468
+ path30.join(wpaths.projectDir, "mcp-auth.json"),
21920
22469
  vault
21921
22470
  );
21922
22471
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -22022,7 +22571,7 @@ async function createPreContextServices(input) {
22022
22571
  };
22023
22572
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
22024
22573
  const skillInstaller = config.features.skills ? new SkillInstaller({
22025
- manifestPath: path29.join(wpaths.configDir, "installed-skills.json"),
22574
+ manifestPath: path30.join(wpaths.configDir, "installed-skills.json"),
22026
22575
  projectSkillsDir: wpaths.inProjectSkills,
22027
22576
  globalSkillsDir: wpaths.globalSkills,
22028
22577
  projectHash: wpaths.projectHash,
@@ -22032,8 +22581,8 @@ async function createPreContextServices(input) {
22032
22581
  const bundledPromptsDir = promptsEnabled ? (() => {
22033
22582
  try {
22034
22583
  const req = createRequire3(import.meta.url);
22035
- return path29.join(
22036
- path29.dirname(req.resolve("@wrongstack/core/package.json")),
22584
+ return path30.join(
22585
+ path30.dirname(req.resolve("@wrongstack/core/package.json")),
22037
22586
  "data",
22038
22587
  "prompts"
22039
22588
  );
@@ -22137,7 +22686,7 @@ async function createPreContextServices(input) {
22137
22686
  }
22138
22687
 
22139
22688
  // src/server/routes.ts
22140
- import path30 from "node:path";
22689
+ import path31 from "node:path";
22141
22690
  import { makeProviderFromConfig as makeProviderFromConfig4, withCatalogCapabilities } from "@wrongstack/providers";
22142
22691
 
22143
22692
  // src/server/mode-handlers.ts
@@ -22262,6 +22811,7 @@ function buildRoutes(state, deps2, cb) {
22262
22811
  config: state.getConfig(),
22263
22812
  clients: state.getClients(),
22264
22813
  context: deps2.context,
22814
+ events: deps2.events,
22265
22815
  toolRegistry: deps2.toolRegistry,
22266
22816
  compactor: deps2.compactor,
22267
22817
  customModeStore: deps2.customModeStore,
@@ -22273,6 +22823,7 @@ function buildRoutes(state, deps2, cb) {
22273
22823
  setSession: state.setSession,
22274
22824
  setSessionStartedAt: state.setSessionStartedAt,
22275
22825
  claimSession: cb.claimSession,
22826
+ onBeforeSessionTodosReplaced: cb.onBeforeSessionTodosReplaced,
22276
22827
  onSessionSwapped: cb.onSessionSwapped,
22277
22828
  abortActiveRun: state.abortRunLock,
22278
22829
  isRunActive: state.isRunActive,
@@ -22294,6 +22845,8 @@ function buildRoutes(state, deps2, cb) {
22294
22845
  setSessionStore: state.setSessionStore,
22295
22846
  setSessionStartedAt: state.setSessionStartedAt,
22296
22847
  abortRunLock: state.abortRunLock,
22848
+ onBeforeSessionTodosReplaced: cb.onBeforeSessionTodosReplaced,
22849
+ onSessionSwapped: cb.onSessionSwapped,
22297
22850
  sessionStartPayload: cb.sessionStartPayload
22298
22851
  });
22299
22852
  const modeRoutes = createModeHandlers({
@@ -22425,7 +22978,7 @@ function buildRoutes(state, deps2, cb) {
22425
22978
  };
22426
22979
  const mailboxRoutes = createMailboxRouteHandlers({
22427
22980
  getProjectRoot: state.getProjectRoot,
22428
- getGlobalRoot: () => path30.dirname(deps2.globalConfigPath),
22981
+ getGlobalRoot: () => path31.dirname(deps2.globalConfigPath),
22429
22982
  events: deps2.events
22430
22983
  });
22431
22984
  const mcpRoutes = {
@@ -22498,7 +23051,7 @@ function buildRoutes(state, deps2, cb) {
22498
23051
  }
22499
23052
 
22500
23053
  // src/server/server-runtime.ts
22501
- import * as path31 from "node:path";
23054
+ import * as path32 from "node:path";
22502
23055
  import { createRequire as createRequire4 } from "node:module";
22503
23056
  import { fileURLToPath } from "node:url";
22504
23057
  import { WebSocketServer } from "ws";
@@ -22559,7 +23112,7 @@ function createSessionStartPayload(g) {
22559
23112
  inputCost,
22560
23113
  outputCost,
22561
23114
  cacheReadCost,
22562
- projectName: path31.basename(projectRoot) || projectRoot,
23115
+ projectName: path32.basename(projectRoot) || projectRoot,
22563
23116
  projectRoot,
22564
23117
  cwd: g.getWorkingDir(),
22565
23118
  mode: g.getModeId(),
@@ -22647,13 +23200,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
22647
23200
  };
22648
23201
  }
22649
23202
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
22650
- if (explicitDistDir) return path31.resolve(explicitDistDir);
23203
+ if (explicitDistDir) return path32.resolve(explicitDistDir);
22651
23204
  try {
22652
23205
  const requireFromHere2 = createRequire4(fromUrl);
22653
23206
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
22654
- return path31.dirname(serverEntry);
23207
+ return path32.dirname(serverEntry);
22655
23208
  } catch {
22656
- return path31.resolve(path31.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
23209
+ return path32.resolve(path32.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
22657
23210
  }
22658
23211
  }
22659
23212
  function startHttpServer(opts) {
@@ -22684,6 +23237,56 @@ function registerShutdown(deps2) {
22684
23237
  }
22685
23238
 
22686
23239
  // src/server/start-webui.ts
23240
+ function createStandaloneTodosCheckpointLifecycle(input) {
23241
+ let checkpointSessionId = input.sessionId;
23242
+ let checkpointSessionsDir = input.sessionsDir;
23243
+ const attachCheckpoint = (sessionId, sessionsDir) => attachTodosCheckpoint(
23244
+ input.state,
23245
+ sessionScopedPath3(sessionsDir, sessionId, ".todos.json"),
23246
+ sessionId,
23247
+ input.events,
23248
+ input.traceId,
23249
+ input.warn
23250
+ );
23251
+ let detachCurrent = attachCheckpoint(input.sessionId, input.sessionsDir);
23252
+ let checkpointAttached = true;
23253
+ const detachCurrentCheckpoint = async () => {
23254
+ if (!checkpointAttached) return;
23255
+ checkpointAttached = false;
23256
+ await detachCurrent();
23257
+ };
23258
+ let transitionTail = Promise.resolve();
23259
+ const rebind = (nextSessionId, sessionsDir) => {
23260
+ const transition = transitionTail.then(async () => {
23261
+ if (checkpointAttached && nextSessionId === checkpointSessionId && sessionsDir === checkpointSessionsDir) {
23262
+ return;
23263
+ }
23264
+ let detachFailed = false;
23265
+ let detachError;
23266
+ try {
23267
+ await detachCurrentCheckpoint();
23268
+ } catch (error2) {
23269
+ detachFailed = true;
23270
+ detachError = error2;
23271
+ }
23272
+ const nextDetach = attachCheckpoint(nextSessionId, sessionsDir);
23273
+ checkpointSessionId = nextSessionId;
23274
+ checkpointSessionsDir = sessionsDir;
23275
+ detachCurrent = nextDetach;
23276
+ checkpointAttached = true;
23277
+ if (detachFailed) throw detachError;
23278
+ });
23279
+ transitionTail = transition.catch(() => void 0);
23280
+ return transition;
23281
+ };
23282
+ return {
23283
+ rebind,
23284
+ detach: async () => {
23285
+ await transitionTail;
23286
+ await detachCurrentCheckpoint();
23287
+ }
23288
+ };
23289
+ }
22687
23290
  async function startWebUI(opts = {}) {
22688
23291
  ensureSessionShell();
22689
23292
  const ports = await resolvePorts(opts);
@@ -22751,6 +23354,14 @@ async function startWebUI(opts = {}) {
22751
23354
  } = preContext;
22752
23355
  let sessionStore = preContext.sessionStore;
22753
23356
  let session = preContext.session;
23357
+ const todosCheckpoint = createStandaloneTodosCheckpointLifecycle({
23358
+ state: context.state,
23359
+ sessionsDir: wpaths.projectSessions,
23360
+ sessionId: session.id,
23361
+ events,
23362
+ traceId: context.traceId,
23363
+ warn: (message) => logger.warn(message)
23364
+ });
22754
23365
  let sessionStartedAt = preContext.sessionStartedAt;
22755
23366
  let modeId = preContext.modeId;
22756
23367
  const needsSetup = preContext.needsSetup;
@@ -22877,7 +23488,7 @@ async function startWebUI(opts = {}) {
22877
23488
  if (events.listenerCount("tool.confirm_needed") === 0) {
22878
23489
  throw new Error("No permission confirmation surface is connected");
22879
23490
  }
22880
- const decision = await new Promise((resolve15) => {
23491
+ const decision = await new Promise((resolve16) => {
22881
23492
  events.emit("tool.confirm_needed", {
22882
23493
  sessionId: context.session.id,
22883
23494
  tool: confirmTool,
@@ -22887,7 +23498,7 @@ async function startWebUI(opts = {}) {
22887
23498
  decisionSource: pending.decisionSource,
22888
23499
  riskTier: pending.riskTier,
22889
23500
  boundaryReason: pending.boundaryReason,
22890
- resolve: resolve15
23501
+ resolve: resolve16
22891
23502
  });
22892
23503
  });
22893
23504
  const rule = { tool: "language_package", pattern: pending.suggestedPattern };
@@ -22987,21 +23598,21 @@ async function startWebUI(opts = {}) {
22987
23598
  });
22988
23599
  }
22989
23600
  async function touchProjectEntry(root, workDir) {
22990
- const resolved = path32.resolve(root);
23601
+ const resolved = path33.resolve(root);
22991
23602
  const manifest = await loadManifest(globalConfigPath);
22992
23603
  const now = (/* @__PURE__ */ new Date()).toISOString();
22993
- const existing = manifest.projects.find((p) => path32.resolve(p.root) === resolved);
23604
+ const existing = manifest.projects.find((p) => path33.resolve(p.root) === resolved);
22994
23605
  if (existing) {
22995
23606
  existing.lastSeen = now;
22996
- if (workDir) existing.lastWorkingDir = path32.resolve(workDir);
23607
+ if (workDir) existing.lastWorkingDir = path33.resolve(workDir);
22997
23608
  } else {
22998
23609
  manifest.projects.push({
22999
- name: path32.basename(resolved),
23610
+ name: path33.basename(resolved),
23000
23611
  root: resolved,
23001
23612
  slug: generateProjectSlug(resolved),
23002
23613
  createdAt: now,
23003
23614
  lastSeen: now,
23004
- lastWorkingDir: workDir ? path32.resolve(workDir) : void 0
23615
+ lastWorkingDir: workDir ? path33.resolve(workDir) : void 0
23005
23616
  });
23006
23617
  }
23007
23618
  await saveManifest(manifest, globalConfigPath);
@@ -23102,6 +23713,7 @@ async function startWebUI(opts = {}) {
23102
23713
  const cb = {
23103
23714
  sessionStartPayload,
23104
23715
  claimSession: (sessionId, target) => sessionIdentity.claim(sessionId, target),
23716
+ onBeforeSessionTodosReplaced: todosCheckpoint.rebind,
23105
23717
  onSessionSwapped: async (sessionId, target) => {
23106
23718
  await sessionIdentity.activate(sessionId, target);
23107
23719
  const { hydrateSessionKanban: hydrateSessionKanban2 } = await import("@wrongstack/tools/session-kanban");
@@ -23252,6 +23864,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23252
23864
  ...wssSecondary ? [wssSecondary] : []
23253
23865
  ],
23254
23866
  onShutdown: async () => {
23867
+ await todosCheckpoint.detach();
23255
23868
  await stopHeapWatchdog();
23256
23869
  credentialWatcherClose?.();
23257
23870
  brainMonitor.stop();
@@ -23277,7 +23890,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
23277
23890
  await memoryStore.dispose().catch(
23278
23891
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage14(err)}`)
23279
23892
  );
23280
- await unregisterInstance(process.pid, path32.dirname(globalConfigPath));
23893
+ await unregisterInstance(process.pid, path33.dirname(globalConfigPath));
23281
23894
  }
23282
23895
  });
23283
23896
  }