@wrongstack/webui-server 0.296.2 → 0.296.3

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 +837 -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 +559 -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
@@ -135,85 +135,85 @@ var ENUM_PREF_KEYS = {
135
135
  autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
136
136
  fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"])
137
137
  };
138
- function validateModelRuntimeValue(modelRuntime, path28) {
138
+ function validateModelRuntimeValue(modelRuntime, path29) {
139
139
  const reasoning = modelRuntime["reasoning"];
140
140
  if (reasoning !== void 0) {
141
- if (!isRecord(reasoning)) return `${path28}.reasoning must be an object when provided`;
141
+ if (!isRecord(reasoning)) return `${path29}.reasoning must be an object when provided`;
142
142
  const mode = reasoning["mode"];
143
143
  const effort = reasoning["effort"];
144
144
  const preserve = reasoning["preserve"];
145
145
  if (mode !== void 0 && (typeof mode !== "string" || !REASONING_MODE_VALUES.has(mode))) {
146
- return `${path28}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
146
+ return `${path29}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
147
147
  }
148
148
  if (effort !== void 0 && (typeof effort !== "string" || !REASONING_EFFORT_VALUES.has(effort))) {
149
- return `${path28}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
149
+ return `${path29}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
150
150
  }
151
151
  if (preserve !== void 0 && typeof preserve !== "boolean") {
152
- return `${path28}.reasoning.preserve must be a boolean when provided`;
152
+ return `${path29}.reasoning.preserve must be a boolean when provided`;
153
153
  }
154
154
  }
155
155
  const cache2 = modelRuntime["cache"];
156
156
  if (cache2 !== void 0) {
157
- if (!isRecord(cache2)) return `${path28}.cache must be an object when provided`;
157
+ if (!isRecord(cache2)) return `${path29}.cache must be an object when provided`;
158
158
  const ttl = cache2["ttl"];
159
159
  if (ttl !== void 0 && (typeof ttl !== "string" || !CACHE_TTL_VALUES.has(ttl) || ttl === "default")) {
160
- return `${path28}.cache.ttl must be one of: 5m, 1h`;
160
+ return `${path29}.cache.ttl must be one of: 5m, 1h`;
161
161
  }
162
162
  }
163
163
  const parameters = modelRuntime["parameters"];
164
164
  if (parameters !== void 0 && !isRecord(parameters)) {
165
- return `${path28}.parameters must be an object when provided`;
165
+ return `${path29}.parameters must be an object when provided`;
166
166
  }
167
167
  return null;
168
168
  }
169
- function validateModelBlackoutRule(rule, path28) {
169
+ function validateModelBlackoutRule(rule, path29) {
170
170
  const id = rule["id"];
171
171
  if (typeof id !== "string" || id.trim().length === 0) {
172
- return `${path28}.id must be a non-empty string`;
172
+ return `${path29}.id must be a non-empty string`;
173
173
  }
174
174
  const start = rule["start"];
175
175
  if (typeof start !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(start)) {
176
- return `${path28}.start must be a string in HH:mm (00:00-23:59) format`;
176
+ return `${path29}.start must be a string in HH:mm (00:00-23:59) format`;
177
177
  }
178
178
  const end = rule["end"];
179
179
  if (typeof end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(end)) {
180
- return `${path28}.end must be a string in HH:mm (00:00-23:59) format`;
180
+ return `${path29}.end must be a string in HH:mm (00:00-23:59) format`;
181
181
  }
182
182
  if (rule["enabled"] !== void 0 && typeof rule["enabled"] !== "boolean") {
183
- return `${path28}.enabled must be a boolean when provided`;
183
+ return `${path29}.enabled must be a boolean when provided`;
184
184
  }
185
185
  if (rule["provider"] !== void 0 && typeof rule["provider"] !== "string") {
186
- return `${path28}.provider must be a string when provided`;
186
+ return `${path29}.provider must be a string when provided`;
187
187
  }
188
188
  if (rule["model"] !== void 0 && typeof rule["model"] !== "string") {
189
- return `${path28}.model must be a string when provided`;
189
+ return `${path29}.model must be a string when provided`;
190
190
  }
191
191
  if (rule["days"] !== void 0) {
192
- if (!Array.isArray(rule["days"])) return `${path28}.days must be an array when provided`;
192
+ if (!Array.isArray(rule["days"])) return `${path29}.days must be an array when provided`;
193
193
  const seen = /* @__PURE__ */ new Set();
194
194
  for (const d of rule["days"]) {
195
195
  if (typeof d !== "number" || !Number.isInteger(d) || d < 0 || d > 6) {
196
- return `${path28}.days elements must be integers 0-6 when provided`;
196
+ return `${path29}.days elements must be integers 0-6 when provided`;
197
197
  }
198
- if (seen.has(d)) return `${path28}.days contains duplicate day: ${d}`;
198
+ if (seen.has(d)) return `${path29}.days contains duplicate day: ${d}`;
199
199
  seen.add(d);
200
200
  }
201
201
  }
202
202
  if (rule["timezone"] !== void 0) {
203
203
  if (typeof rule["timezone"] !== "string") {
204
- return `${path28}.timezone must be a string when provided`;
204
+ return `${path29}.timezone must be a string when provided`;
205
205
  }
206
206
  try {
207
207
  Intl.DateTimeFormat(void 0, { timeZone: rule["timezone"] });
208
208
  } catch {
209
- return `${path28}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
209
+ return `${path29}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
210
210
  }
211
211
  }
212
212
  if (rule["label"] !== void 0 && typeof rule["label"] !== "string") {
213
- return `${path28}.label must be a string when provided`;
213
+ return `${path29}.label must be a string when provided`;
214
214
  }
215
215
  if (rule["mode"] !== void 0 && rule["mode"] !== "blackout" && rule["mode"] !== "allow_only") {
216
- return `${path28}.mode must be 'blackout' or 'allow_only' when provided`;
216
+ return `${path29}.mode must be 'blackout' or 'allow_only' when provided`;
217
217
  }
218
218
  return null;
219
219
  }
@@ -737,8 +737,8 @@ function validateShellOpenPayload(payload) {
737
737
  if (!isRecord2(payload)) {
738
738
  return { ok: false, message: "shell.open payload must be an object with string path" };
739
739
  }
740
- const path28 = payload["path"];
741
- if (typeof path28 !== "string" || path28.trim().length === 0) {
740
+ const path29 = payload["path"];
741
+ if (typeof path29 !== "string" || path29.trim().length === 0) {
742
742
  return { ok: false, message: "shell.open payload.path must be a non-empty string" };
743
743
  }
744
744
  const target = payload["target"];
@@ -751,7 +751,7 @@ function validateShellOpenPayload(payload) {
751
751
  return {
752
752
  ok: true,
753
753
  value: {
754
- path: path28,
754
+ path: path29,
755
755
  ...target !== void 0 ? { target } : {}
756
756
  }
757
757
  };
@@ -760,14 +760,14 @@ function validateGitDiffPayload(payload) {
760
760
  if (!isRecord2(payload)) {
761
761
  return { ok: false, message: "git.diff payload must be an object" };
762
762
  }
763
- const path28 = payload["path"];
764
- if (path28 === void 0 || path28 === null) {
763
+ const path29 = payload["path"];
764
+ if (path29 === void 0 || path29 === null) {
765
765
  return { ok: true, value: { path: "" } };
766
766
  }
767
- if (typeof path28 !== "string") {
767
+ if (typeof path29 !== "string") {
768
768
  return { ok: false, message: "git.diff payload.path must be a string when provided" };
769
769
  }
770
- return { ok: true, value: { path: path28 } };
770
+ return { ok: true, value: { path: path29 } };
771
771
  }
772
772
  function validateProjectsAddPayload(payload) {
773
773
  if (!isRecord2(payload)) {
@@ -4288,8 +4288,8 @@ function jsonByteLength(value) {
4288
4288
  return MAX_PAYLOAD_BYTES + 1;
4289
4289
  }
4290
4290
  }
4291
- function error(errors, path28, code, message) {
4292
- errors.push({ path: path28, code, message });
4291
+ function error(errors, path29, code, message) {
4292
+ errors.push({ path: path29, code, message });
4293
4293
  }
4294
4294
  function isMessageRole(value) {
4295
4295
  return value === "user" || value === "assistant" || value === "system";
@@ -4297,25 +4297,25 @@ function isMessageRole(value) {
4297
4297
  function isPlainJsonObject(value) {
4298
4298
  return isRecord3(value);
4299
4299
  }
4300
- function validateCacheControl(value, path28, errors) {
4300
+ function validateCacheControl(value, path29, errors) {
4301
4301
  if (value === void 0) return void 0;
4302
4302
  if (!isRecord3(value) || value["type"] !== "ephemeral") {
4303
- error(errors, path28, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4303
+ error(errors, path29, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
4304
4304
  return void 0;
4305
4305
  }
4306
4306
  return { type: "ephemeral" };
4307
4307
  }
4308
- function validateProviderMeta(value, path28, errors) {
4308
+ function validateProviderMeta(value, path29, errors) {
4309
4309
  if (value === void 0) return void 0;
4310
4310
  if (!isPlainJsonObject(value)) {
4311
- error(errors, path28, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4311
+ error(errors, path29, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
4312
4312
  return void 0;
4313
4313
  }
4314
4314
  return value;
4315
4315
  }
4316
- function validateBlock(value, path28, errors) {
4316
+ function validateBlock(value, path29, errors) {
4317
4317
  if (!isRecord3(value)) {
4318
- error(errors, path28, "INVALID_BLOCK", "Content block must be an object.");
4318
+ error(errors, path29, "INVALID_BLOCK", "Content block must be an object.");
4319
4319
  return void 0;
4320
4320
  }
4321
4321
  const type = value["type"];
@@ -4323,14 +4323,14 @@ function validateBlock(value, path28, errors) {
4323
4323
  case "text": {
4324
4324
  const text = value["text"];
4325
4325
  if (typeof text !== "string") {
4326
- error(errors, `${path28}/text`, "INVALID_TEXT", "Text block text must be a string.");
4326
+ error(errors, `${path29}/text`, "INVALID_TEXT", "Text block text must be a string.");
4327
4327
  return void 0;
4328
4328
  }
4329
4329
  if (text.length > MAX_STRING_LENGTH) {
4330
- error(errors, `${path28}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4330
+ error(errors, `${path29}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
4331
4331
  return void 0;
4332
4332
  }
4333
- const cacheControl = validateCacheControl(value["cache_control"], `${path28}/cache_control`, errors);
4333
+ const cacheControl = validateCacheControl(value["cache_control"], `${path29}/cache_control`, errors);
4334
4334
  return cacheControl ? { type: "text", text, cache_control: cacheControl } : { type: "text", text };
4335
4335
  }
4336
4336
  case "tool_use": {
@@ -4338,15 +4338,15 @@ function validateBlock(value, path28, errors) {
4338
4338
  const name2 = value["name"];
4339
4339
  const input = value["input"];
4340
4340
  if (typeof id !== "string" || id.length === 0) {
4341
- error(errors, `${path28}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4341
+ error(errors, `${path29}/id`, "INVALID_TOOL_USE_ID", "tool_use.id must be a non-empty string.");
4342
4342
  }
4343
4343
  if (typeof name2 !== "string" || name2.length === 0) {
4344
- error(errors, `${path28}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4344
+ error(errors, `${path29}/name`, "INVALID_TOOL_NAME", "tool_use.name must be a non-empty string.");
4345
4345
  }
4346
4346
  if (!isPlainJsonObject(input)) {
4347
- error(errors, `${path28}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4347
+ error(errors, `${path29}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
4348
4348
  }
4349
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path28}/providerMeta`, errors);
4349
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path29}/providerMeta`, errors);
4350
4350
  if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
4351
4351
  return void 0;
4352
4352
  }
@@ -4358,18 +4358,18 @@ function validateBlock(value, path28, errors) {
4358
4358
  const content = value["content"];
4359
4359
  const isError = value["is_error"];
4360
4360
  if (typeof toolUseId !== "string" || toolUseId.length === 0) {
4361
- error(errors, `${path28}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4361
+ error(errors, `${path29}/tool_use_id`, "INVALID_TOOL_RESULT_ID", "tool_result.tool_use_id must be a non-empty string.");
4362
4362
  }
4363
4363
  if (name2 !== void 0 && typeof name2 !== "string") {
4364
- error(errors, `${path28}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4364
+ error(errors, `${path29}/name`, "INVALID_TOOL_RESULT_NAME", "tool_result.name must be a string.");
4365
4365
  }
4366
4366
  if (typeof content !== "string") {
4367
- error(errors, `${path28}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4367
+ error(errors, `${path29}/content`, "INVALID_TOOL_RESULT_CONTENT", "tool_result.content must be a string.");
4368
4368
  } else if (content.length > MAX_STRING_LENGTH) {
4369
- error(errors, `${path28}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4369
+ error(errors, `${path29}/content`, "TOOL_RESULT_TOO_LARGE", "tool_result.content is too large.");
4370
4370
  }
4371
4371
  if (isError !== void 0 && typeof isError !== "boolean") {
4372
- error(errors, `${path28}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4372
+ error(errors, `${path29}/is_error`, "INVALID_TOOL_RESULT_ERROR", "tool_result.is_error must be boolean.");
4373
4373
  }
4374
4374
  if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string") return void 0;
4375
4375
  return {
@@ -4383,25 +4383,25 @@ function validateBlock(value, path28, errors) {
4383
4383
  case "image": {
4384
4384
  const source = value["source"];
4385
4385
  if (!isRecord3(source)) {
4386
- error(errors, `${path28}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4386
+ error(errors, `${path29}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
4387
4387
  return void 0;
4388
4388
  }
4389
4389
  const sourceType = source["type"];
4390
4390
  if (sourceType !== "base64" && sourceType !== "url") {
4391
- error(errors, `${path28}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4391
+ error(errors, `${path29}/source/type`, "INVALID_IMAGE_SOURCE_TYPE", "image.source.type must be base64 or url.");
4392
4392
  return void 0;
4393
4393
  }
4394
4394
  const mediaType = source["media_type"];
4395
4395
  const data = source["data"];
4396
4396
  const url = source["url"];
4397
4397
  if (mediaType !== void 0 && typeof mediaType !== "string") {
4398
- error(errors, `${path28}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4398
+ error(errors, `${path29}/source/media_type`, "INVALID_IMAGE_MEDIA_TYPE", "image.source.media_type must be a string.");
4399
4399
  }
4400
4400
  if (data !== void 0 && typeof data !== "string") {
4401
- error(errors, `${path28}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4401
+ error(errors, `${path29}/source/data`, "INVALID_IMAGE_DATA", "image.source.data must be a string.");
4402
4402
  }
4403
4403
  if (url !== void 0 && typeof url !== "string") {
4404
- error(errors, `${path28}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4404
+ error(errors, `${path29}/source/url`, "INVALID_IMAGE_URL", "image.source.url must be a string.");
4405
4405
  }
4406
4406
  return {
4407
4407
  type: "image",
@@ -4417,13 +4417,13 @@ function validateBlock(value, path28, errors) {
4417
4417
  const thinking = value["thinking"];
4418
4418
  const signature = value["signature"];
4419
4419
  if (typeof thinking !== "string") {
4420
- error(errors, `${path28}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4420
+ error(errors, `${path29}/thinking`, "INVALID_THINKING", "thinking.thinking must be a string.");
4421
4421
  return void 0;
4422
4422
  }
4423
4423
  if (signature !== void 0 && typeof signature !== "string") {
4424
- error(errors, `${path28}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4424
+ error(errors, `${path29}/signature`, "INVALID_THINKING_SIGNATURE", "thinking.signature must be a string.");
4425
4425
  }
4426
- const providerMeta = validateProviderMeta(value["providerMeta"], `${path28}/providerMeta`, errors);
4426
+ const providerMeta = validateProviderMeta(value["providerMeta"], `${path29}/providerMeta`, errors);
4427
4427
  return {
4428
4428
  type: "thinking",
4429
4429
  thinking,
@@ -4432,7 +4432,7 @@ function validateBlock(value, path28, errors) {
4432
4432
  };
4433
4433
  }
4434
4434
  default:
4435
- error(errors, `${path28}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4435
+ error(errors, `${path29}/type`, "UNKNOWN_BLOCK_TYPE", `Unknown content block type: ${String(type)}`);
4436
4436
  return void 0;
4437
4437
  }
4438
4438
  }
@@ -4450,39 +4450,39 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
4450
4450
  error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
4451
4451
  }
4452
4452
  value.forEach((item, index) => {
4453
- const path28 = `/messages/${index}`;
4453
+ const path29 = `/messages/${index}`;
4454
4454
  if (!isRecord3(item)) {
4455
- error(errors, path28, "INVALID_MESSAGE", "Message must be an object.");
4455
+ error(errors, path29, "INVALID_MESSAGE", "Message must be an object.");
4456
4456
  return;
4457
4457
  }
4458
4458
  const role = item["role"];
4459
4459
  if (!isMessageRole(role)) {
4460
- error(errors, `${path28}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4460
+ error(errors, `${path29}/role`, "INVALID_ROLE", "Message role must be user, assistant, or system.");
4461
4461
  return;
4462
4462
  }
4463
4463
  const rawContent = item["content"];
4464
4464
  let content;
4465
4465
  if (typeof rawContent === "string") {
4466
4466
  if (rawContent.length > MAX_STRING_LENGTH) {
4467
- error(errors, `${path28}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4467
+ error(errors, `${path29}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
4468
4468
  return;
4469
4469
  }
4470
4470
  content = rawContent;
4471
4471
  } else if (Array.isArray(rawContent)) {
4472
4472
  const blocks = [];
4473
4473
  rawContent.forEach((block, blockIndex) => {
4474
- const parsed = validateBlock(block, `${path28}/content/${blockIndex}`, errors);
4474
+ const parsed = validateBlock(block, `${path29}/content/${blockIndex}`, errors);
4475
4475
  if (parsed) blocks.push(parsed);
4476
4476
  });
4477
4477
  content = blocks;
4478
4478
  } else {
4479
- error(errors, `${path28}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4479
+ error(errors, `${path29}/content`, "INVALID_CONTENT", "Message content must be a string or content block array.");
4480
4480
  return;
4481
4481
  }
4482
4482
  const ts = item["ts"];
4483
4483
  if (ts !== void 0) {
4484
4484
  if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
4485
- error(errors, `${path28}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4485
+ error(errors, `${path29}/ts`, "INVALID_TIMESTAMP", "Message ts must be an ISO-like timestamp string.");
4486
4486
  return;
4487
4487
  }
4488
4488
  }
@@ -4856,7 +4856,11 @@ function createConnectionLifecycle(options) {
4856
4856
  }
4857
4857
 
4858
4858
  // src/server/connections-health-route.ts
4859
- import { createChronicleProjectAccess as createChronicleProjectAccess2 } from "@wrongstack/core/chronicle";
4859
+ import {
4860
+ ChronicleProjectServerClient,
4861
+ createChronicleProjectAccess as createChronicleProjectAccess2,
4862
+ resolveChronicleProjectServerOptions
4863
+ } from "@wrongstack/core/chronicle";
4860
4864
  import {
4861
4865
  isMailboxProjectServerAvailable,
4862
4866
  MailboxProjectServerConnection
@@ -4864,7 +4868,11 @@ import {
4864
4868
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
4865
4869
  import { getKanbanServerConnection } from "@wrongstack/kanban";
4866
4870
  import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
4867
- import { checkCodebaseIndexServerHealth, getIndexState } from "@wrongstack/tools";
4871
+ import {
4872
+ checkCodebaseIndexServerHealth,
4873
+ getIndexState,
4874
+ shutdownCodebaseIndexServer
4875
+ } from "@wrongstack/tools";
4868
4876
  async function handleConnectionsHealthRoute(context, ws, message) {
4869
4877
  if (message.type !== "connections.health") return false;
4870
4878
  try {
@@ -5341,9 +5349,9 @@ async function handleGitInfo(ws, projectRoot) {
5341
5349
  const cwd = projectRoot || void 0;
5342
5350
  try {
5343
5351
  const { execFile: ef } = await import("node:child_process");
5344
- const git = (args) => new Promise((resolve14) => {
5352
+ const git = (args) => new Promise((resolve15) => {
5345
5353
  ef("git", args, { cwd, timeout: 3e3 }, (err, stdout) => {
5346
- resolve14(err ? "" : stdout.trim());
5354
+ resolve15(err ? "" : stdout.trim());
5347
5355
  });
5348
5356
  });
5349
5357
  const [branchRaw, diffRaw, statusRaw, upstreamRaw] = await Promise.all([
@@ -5369,12 +5377,12 @@ async function handleGitInfo(ws, projectRoot) {
5369
5377
  function makeGit(cwd) {
5370
5378
  return async (args) => {
5371
5379
  const { execFile: ef } = await import("node:child_process");
5372
- return new Promise((resolve14) => {
5380
+ return new Promise((resolve15) => {
5373
5381
  ef(
5374
5382
  "git",
5375
5383
  args,
5376
5384
  { cwd, timeout: 5e3, maxBuffer: 1024 * 1024 * 16 },
5377
- (err, stdout) => resolve14(err ? "" : stdout)
5385
+ (err, stdout) => resolve15(err ? "" : stdout)
5378
5386
  );
5379
5387
  });
5380
5388
  };
@@ -5398,15 +5406,15 @@ async function handleGitChanges(ws, projectRoot) {
5398
5406
  if (!m) continue;
5399
5407
  const added = m[1] === "-" ? 0 : Number(m[1]);
5400
5408
  const deleted = m[2] === "-" ? 0 : Number(m[2]);
5401
- let path28 = m[3] ?? "";
5402
- if (path28 === "") {
5409
+ let path29 = m[3] ?? "";
5410
+ if (path29 === "") {
5403
5411
  i += 1;
5404
- path28 = parts[i + 1] ?? parts[i] ?? "";
5412
+ path29 = parts[i + 1] ?? parts[i] ?? "";
5405
5413
  i += 1;
5406
5414
  }
5407
- if (!path28) continue;
5408
- const prev = counts.get(path28) ?? { added: 0, deleted: 0 };
5409
- counts.set(path28, { added: prev.added + added, deleted: prev.deleted + deleted });
5415
+ if (!path29) continue;
5416
+ const prev = counts.get(path29) ?? { added: 0, deleted: 0 };
5417
+ counts.set(path29, { added: prev.added + added, deleted: prev.deleted + deleted });
5410
5418
  }
5411
5419
  };
5412
5420
  parseNumstat(unstagedNumstat);
@@ -5418,7 +5426,7 @@ async function handleGitChanges(ws, projectRoot) {
5418
5426
  if (!rec || rec.length < 3) continue;
5419
5427
  const x = rec[0] ?? " ";
5420
5428
  const y = rec[1] ?? " ";
5421
- const path28 = rec.slice(3);
5429
+ const path29 = rec.slice(3);
5422
5430
  const isRename = x === "R" || x === "C" || y === "R" || y === "C";
5423
5431
  if (isRename) i += 1;
5424
5432
  let status;
@@ -5430,13 +5438,13 @@ async function handleGitChanges(ws, projectRoot) {
5430
5438
  else if (x === "D" || y === "D") status = "D";
5431
5439
  else status = "M";
5432
5440
  const staged = x !== " " && x !== "?";
5433
- let added = counts.get(path28)?.added ?? 0;
5434
- let deleted = counts.get(path28)?.deleted ?? 0;
5441
+ let added = counts.get(path29)?.added ?? 0;
5442
+ let deleted = counts.get(path29)?.deleted ?? 0;
5435
5443
  if (status === "?") {
5436
5444
  added = 0;
5437
5445
  deleted = 0;
5438
5446
  }
5439
- files.push({ path: path28, status, added, deleted, staged });
5447
+ files.push({ path: path29, status, added, deleted, staged });
5440
5448
  }
5441
5449
  send(ws, { type: "git.changes", payload: { files } });
5442
5450
  } catch (err) {
@@ -5447,10 +5455,10 @@ async function handleGitChanges(ws, projectRoot) {
5447
5455
  }
5448
5456
  }
5449
5457
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
5450
- async function handleGitDiff(ws, projectRoot, path28) {
5458
+ async function handleGitDiff(ws, projectRoot, path29) {
5451
5459
  const cwd = projectRoot || void 0;
5452
- const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path28, ...extra } });
5453
- if (!path28 || path28.includes("\0") || path28.includes("..") || nodePath.isAbsolute(path28)) {
5460
+ const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path29, ...extra } });
5461
+ if (!path29 || path29.includes("\0") || path29.includes("..") || nodePath.isAbsolute(path29)) {
5454
5462
  reply2({ oldText: "", newText: "", error: "invalid path" });
5455
5463
  return;
5456
5464
  }
@@ -5458,10 +5466,10 @@ async function handleGitDiff(ws, projectRoot, path28) {
5458
5466
  const git = makeGit(cwd);
5459
5467
  const { readFile: readFile11 } = await import("node:fs/promises");
5460
5468
  const { join: join15 } = await import("node:path");
5461
- const oldText = await git(["show", `HEAD:${path28}`]);
5469
+ const oldText = await git(["show", `HEAD:${path29}`]);
5462
5470
  let newText = "";
5463
5471
  try {
5464
- const abs = cwd ? join15(cwd, path28) : path28;
5472
+ const abs = cwd ? join15(cwd, path29) : path29;
5465
5473
  const buf = await readFile11(abs);
5466
5474
  if (buf.includes(0)) {
5467
5475
  reply2({ oldText: "", newText: "", binary: true });
@@ -5541,7 +5549,7 @@ import { execFile } from "node:child_process";
5541
5549
  var GIT_TIMEOUT_MS = 1e4;
5542
5550
  var GIT_MAX_OUTPUT_BYTES = 1024 * 1024;
5543
5551
  function gitStdout(cwd, args) {
5544
- return new Promise((resolve14) => {
5552
+ return new Promise((resolve15) => {
5545
5553
  execFile(
5546
5554
  "git",
5547
5555
  [...args],
@@ -5552,7 +5560,7 @@ function gitStdout(cwd, args) {
5552
5560
  timeout: GIT_TIMEOUT_MS,
5553
5561
  maxBuffer: GIT_MAX_OUTPUT_BYTES
5554
5562
  },
5555
- (error2, stdout) => resolve14(error2 ? null : stdout)
5563
+ (error2, stdout) => resolve15(error2 ? null : stdout)
5556
5564
  );
5557
5565
  });
5558
5566
  }
@@ -5818,13 +5826,13 @@ var GoalWebSocketHandler = class {
5818
5826
  const cwd = env?.cwd ?? this.projectRoot;
5819
5827
  try {
5820
5828
  const { exec } = await import("node:child_process");
5821
- const result = await new Promise((resolve14) => {
5829
+ const result = await new Promise((resolve15) => {
5822
5830
  exec("npx tsc --noEmit", { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
5823
5831
  if (err && err.code === "ENOENT") {
5824
- resolve14("[verify] tsc not found \u2014 skipping");
5832
+ resolve15("[verify] tsc not found \u2014 skipping");
5825
5833
  return;
5826
5834
  }
5827
- resolve14(stdout + stderr);
5835
+ resolve15(stdout + stderr);
5828
5836
  });
5829
5837
  });
5830
5838
  if (result.includes("[verify]") || result.trim().length === 0) {
@@ -6503,7 +6511,7 @@ function pushEvent(event) {
6503
6511
  }
6504
6512
  }
6505
6513
  function parseBody(req) {
6506
- return new Promise((resolve14, reject) => {
6514
+ return new Promise((resolve15, reject) => {
6507
6515
  let body = "";
6508
6516
  let bodyBytes = 0;
6509
6517
  let tooLarge = false;
@@ -6523,7 +6531,7 @@ function parseBody(req) {
6523
6531
  return;
6524
6532
  }
6525
6533
  try {
6526
- resolve14(JSON.parse(body));
6534
+ resolve15(JSON.parse(body));
6527
6535
  } catch {
6528
6536
  reject(new Error("Invalid JSON"));
6529
6537
  }
@@ -6600,7 +6608,7 @@ async function handleApiAnalyticsSummary(res) {
6600
6608
  // src/server/http-server.ts
6601
6609
  import * as fs9 from "node:fs/promises";
6602
6610
  import * as http from "node:http";
6603
- import * as path11 from "node:path";
6611
+ import * as path12 from "node:path";
6604
6612
  import * as v8 from "node:v8";
6605
6613
  import { getIndexState as getIndexState2 } from "@wrongstack/tools";
6606
6614
 
@@ -6680,6 +6688,156 @@ async function handleCodemapSymbols(res, deps2, file) {
6680
6688
  );
6681
6689
  }
6682
6690
 
6691
+ // src/server/deadcode-handlers.ts
6692
+ import * as path10 from "node:path";
6693
+ import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
6694
+ var MAX_BODY_BYTES = 10 * 1024 * 1024;
6695
+ function readJsonBody(req) {
6696
+ return new Promise((resolve15, reject) => {
6697
+ const chunks = [];
6698
+ let total = 0;
6699
+ req.on("data", (chunk) => {
6700
+ total += chunk.length;
6701
+ if (total > MAX_BODY_BYTES) {
6702
+ req.destroy(new Error("Request body too large"));
6703
+ reject(new Error("Request body exceeds 10 MiB limit"));
6704
+ return;
6705
+ }
6706
+ chunks.push(chunk);
6707
+ });
6708
+ req.on("end", () => resolve15(Buffer.concat(chunks).toString("utf8")));
6709
+ req.on("error", (err) => reject(err));
6710
+ });
6711
+ }
6712
+ async function handleDeadCodeScan(res, deps2, req) {
6713
+ try {
6714
+ let body = {};
6715
+ const raw = await readJsonBody(req);
6716
+ if (raw) {
6717
+ try {
6718
+ body = JSON.parse(raw);
6719
+ } catch {
6720
+ res.writeHead(400, { "Content-Type": "application/json" });
6721
+ res.end(JSON.stringify({ error: "Invalid JSON body" }));
6722
+ return;
6723
+ }
6724
+ }
6725
+ const scanIndexDir = body.indexDir ?? deps2.indexDir;
6726
+ if (scanIndexDir) {
6727
+ const resolvedRoot = path10.resolve(deps2.projectRoot);
6728
+ const resolvedIndex = path10.resolve(deps2.projectRoot, scanIndexDir);
6729
+ if (resolvedIndex !== resolvedRoot && !resolvedIndex.startsWith(resolvedRoot + path10.sep)) {
6730
+ res.writeHead(400, { "Content-Type": "application/json" });
6731
+ res.end(JSON.stringify({ error: "Invalid indexDir: must be within project root" }));
6732
+ return;
6733
+ }
6734
+ }
6735
+ const result = runDeadCodeScan(deps2.projectRoot, {
6736
+ indexDir: scanIndexDir,
6737
+ userEntryPoints: body.entryPoints
6738
+ });
6739
+ res.writeHead(200, { "Content-Type": "application/json" });
6740
+ res.end(JSON.stringify(result));
6741
+ } catch (err) {
6742
+ res.writeHead(500, { "Content-Type": "application/json" });
6743
+ res.end(
6744
+ JSON.stringify({
6745
+ error: "Dead-code scan failed",
6746
+ detail: err instanceof Error ? err.message : String(err)
6747
+ })
6748
+ );
6749
+ }
6750
+ }
6751
+ function handleDeadCodeActionPlan(res, _deps, req) {
6752
+ return readJsonBody(req).then((raw) => {
6753
+ let parsed;
6754
+ try {
6755
+ parsed = JSON.parse(raw);
6756
+ } catch {
6757
+ res.writeHead(400, { "Content-Type": "application/json" });
6758
+ res.end(JSON.stringify({ error: "Invalid scan result JSON" }));
6759
+ return;
6760
+ }
6761
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.deadPackages) || !Array.isArray(parsed.deadFiles) || !Array.isArray(parsed.deadSymbols)) {
6762
+ res.writeHead(400, { "Content-Type": "application/json" });
6763
+ res.end(
6764
+ JSON.stringify({
6765
+ error: "Invalid scan result: missing or malformed required fields (deadPackages, deadFiles, deadSymbols)"
6766
+ })
6767
+ );
6768
+ return;
6769
+ }
6770
+ const result = parsed;
6771
+ const plan = buildActionPlan(result);
6772
+ res.writeHead(200, { "Content-Type": "application/json" });
6773
+ res.end(JSON.stringify(plan));
6774
+ }).catch((err) => {
6775
+ res.writeHead(500, { "Content-Type": "application/json" });
6776
+ res.end(
6777
+ JSON.stringify({
6778
+ error: "Failed to read request body",
6779
+ detail: err instanceof Error ? err.message : String(err)
6780
+ })
6781
+ );
6782
+ });
6783
+ }
6784
+ function buildActionPlan(result) {
6785
+ const files = /* @__PURE__ */ new Map();
6786
+ for (const dp of result.deadPackages) {
6787
+ const pseudoFile = {
6788
+ file: `${dp.package}/ (package)`,
6789
+ symbolCount: dp.fileCount,
6790
+ symbols: [`remove package ${dp.package} (${dp.fileCount} files, path: ${dp.path})`],
6791
+ priority: 0
6792
+ };
6793
+ files.set(pseudoFile.file, pseudoFile);
6794
+ }
6795
+ for (const df of result.deadFiles) {
6796
+ const existing = files.get(df.file);
6797
+ if (existing) {
6798
+ if (existing.priority > 1) existing.priority = 1;
6799
+ existing.symbolCount += df.symbolCount;
6800
+ continue;
6801
+ }
6802
+ files.set(df.file, {
6803
+ file: df.file,
6804
+ symbolCount: df.symbolCount,
6805
+ symbols: [`entire file (${df.symbolCount} symbols) is dead`],
6806
+ priority: 1
6807
+ });
6808
+ }
6809
+ const deadInAliveFiles = /* @__PURE__ */ new Map();
6810
+ const deadFileSet = new Set(result.deadFiles.map((df) => df.file));
6811
+ for (const ds of result.deadSymbols) {
6812
+ if (deadFileSet.has(ds.file)) continue;
6813
+ const list = deadInAliveFiles.get(ds.file) ?? [];
6814
+ list.push(`${ds.kind} ${ds.name} (line ${ds.line})`);
6815
+ deadInAliveFiles.set(ds.file, list);
6816
+ }
6817
+ for (const [file, symbols] of deadInAliveFiles) {
6818
+ const existing = files.get(file);
6819
+ if (existing) {
6820
+ existing.symbols.push(...symbols);
6821
+ existing.symbolCount += symbols.length;
6822
+ continue;
6823
+ }
6824
+ files.set(file, {
6825
+ file,
6826
+ symbolCount: symbols.length,
6827
+ symbols,
6828
+ priority: 2
6829
+ });
6830
+ }
6831
+ const sorted = [...files.values()].sort((a, b) => a.priority - b.priority || a.file.localeCompare(b.file));
6832
+ return {
6833
+ 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.`,
6834
+ files: sorted,
6835
+ totalDeadSymbols: result.stats.dead,
6836
+ totalDeadFiles: result.deadFiles.length,
6837
+ totalDeadPackages: result.deadPackages.length
6838
+ };
6839
+ }
6840
+
6683
6841
  // src/server/http-server/api-handlers.ts
6684
6842
  async function handleApiSessions(res, globalRoot) {
6685
6843
  if (!globalRoot) {
@@ -6901,8 +7059,8 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
6901
7059
  res.end(JSON.stringify({ error: String(err) }));
6902
7060
  }
6903
7061
  }
6904
- function readJsonBody(req) {
6905
- return new Promise((resolve14, reject) => {
7062
+ function readJsonBody2(req) {
7063
+ return new Promise((resolve15, reject) => {
6906
7064
  let data = "";
6907
7065
  req.on("data", (chunk) => {
6908
7066
  data += chunk;
@@ -6913,7 +7071,7 @@ function readJsonBody(req) {
6913
7071
  });
6914
7072
  req.on("end", () => {
6915
7073
  try {
6916
- resolve14(data ? JSON.parse(data) : {});
7074
+ resolve15(data ? JSON.parse(data) : {});
6917
7075
  } catch (err) {
6918
7076
  reject(err instanceof Error ? err : new Error(String(err)));
6919
7077
  }
@@ -6929,7 +7087,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
6929
7087
  }
6930
7088
  let body;
6931
7089
  try {
6932
- body = await readJsonBody(req);
7090
+ body = await readJsonBody2(req);
6933
7091
  } catch {
6934
7092
  res.writeHead(400, { "Content-Type": "application/json" });
6935
7093
  res.end(JSON.stringify({ error: "Invalid request body" }));
@@ -7031,7 +7189,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
7031
7189
  }
7032
7190
  let body = {};
7033
7191
  try {
7034
- body = await readJsonBody(req);
7192
+ body = await readJsonBody2(req);
7035
7193
  } catch {
7036
7194
  }
7037
7195
  const reason = typeof body["reason"] === "string" && body["reason"].trim() ? body["reason"].trim() : "Operator requested stop from Fleet HQ";
@@ -7072,7 +7230,7 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7072
7230
  }
7073
7231
  let body;
7074
7232
  try {
7075
- body = await readJsonBody(req);
7233
+ body = await readJsonBody2(req);
7076
7234
  } catch {
7077
7235
  res.writeHead(400, { "Content-Type": "application/json" });
7078
7236
  res.end(JSON.stringify({ error: "Invalid request body" }));
@@ -7136,12 +7294,12 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
7136
7294
 
7137
7295
  // src/server/projects-manifest.ts
7138
7296
  import * as fs8 from "node:fs/promises";
7139
- import * as path10 from "node:path";
7297
+ import * as path11 from "node:path";
7140
7298
  import { ConfigError } from "@wrongstack/core/types";
7141
7299
  import { projectSlug, withFileLock } from "@wrongstack/core/utils";
7142
7300
  function projectsJsonPath(globalConfigPath) {
7143
- const base = path10.dirname(globalConfigPath);
7144
- return path10.join(base, "projects.json");
7301
+ const base = path11.dirname(globalConfigPath);
7302
+ return path11.join(base, "projects.json");
7145
7303
  }
7146
7304
  async function loadManifest(globalConfigPath) {
7147
7305
  try {
@@ -7154,37 +7312,37 @@ async function loadManifest(globalConfigPath) {
7154
7312
  }
7155
7313
  async function saveManifest(manifest, globalConfigPath) {
7156
7314
  const file = projectsJsonPath(globalConfigPath);
7157
- await fs8.mkdir(path10.dirname(file), { recursive: true });
7315
+ await fs8.mkdir(path11.dirname(file), { recursive: true });
7158
7316
  await fs8.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
7159
7317
  }
7160
7318
  function generateProjectSlug(rootPath) {
7161
7319
  return projectSlug(rootPath);
7162
7320
  }
7163
7321
  async function ensureProjectDataDir(slug, globalConfigPath) {
7164
- const base = path10.dirname(globalConfigPath);
7165
- const dir = path10.join(base, "projects", slug);
7322
+ const base = path11.dirname(globalConfigPath);
7323
+ const dir = path11.join(base, "projects", slug);
7166
7324
  await fs8.mkdir(dir, { recursive: true });
7167
7325
  return dir;
7168
7326
  }
7169
7327
  async function touchProjectInManifest(options, globalConfigPath) {
7170
- const root = path10.resolve(options.projectRoot);
7328
+ const root = path11.resolve(options.projectRoot);
7171
7329
  const file = projectsJsonPath(globalConfigPath);
7172
7330
  let entry;
7173
7331
  await withFileLock(file, async () => {
7174
7332
  const manifest = await loadManifest(globalConfigPath);
7175
7333
  const now = (/* @__PURE__ */ new Date()).toISOString();
7176
- entry = manifest.projects.find((candidate) => path10.resolve(candidate.root) === root);
7334
+ entry = manifest.projects.find((candidate) => path11.resolve(candidate.root) === root);
7177
7335
  if (entry) {
7178
7336
  entry.lastSeen = now;
7179
- if (options.workingDir) entry.lastWorkingDir = path10.resolve(options.workingDir);
7337
+ if (options.workingDir) entry.lastWorkingDir = path11.resolve(options.workingDir);
7180
7338
  } else {
7181
7339
  entry = {
7182
- name: options.name ?? path10.basename(root),
7340
+ name: options.name ?? path11.basename(root),
7183
7341
  root,
7184
7342
  slug: generateProjectSlug(root),
7185
7343
  createdAt: now,
7186
7344
  lastSeen: now,
7187
- lastWorkingDir: options.workingDir ? path10.resolve(options.workingDir) : void 0
7345
+ lastWorkingDir: options.workingDir ? path11.resolve(options.workingDir) : void 0
7188
7346
  };
7189
7347
  manifest.projects.push(entry);
7190
7348
  }
@@ -7595,9 +7753,9 @@ function buildCspHeader(publicWsUrl, host, port) {
7595
7753
  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'`;
7596
7754
  }
7597
7755
  function isInsideDist(candidate, distDir) {
7598
- const root = path11.resolve(distDir);
7599
- const resolved = path11.resolve(candidate);
7600
- return resolved === root || resolved.startsWith(root + path11.sep);
7756
+ const root = path12.resolve(distDir);
7757
+ const resolved = path12.resolve(candidate);
7758
+ return resolved === root || resolved.startsWith(root + path12.sep);
7601
7759
  }
7602
7760
  function decodeSessionId(segment) {
7603
7761
  try {
@@ -7617,7 +7775,7 @@ function strictDecodeParam(segment, res) {
7617
7775
  }
7618
7776
  function createHttpServer(opts) {
7619
7777
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
7620
- const distDir = path11.resolve(opts.distDir);
7778
+ const distDir = path12.resolve(opts.distDir);
7621
7779
  const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
7622
7780
  let techStackRuntime = null;
7623
7781
  const getTechStackRuntime = async () => {
@@ -7836,6 +7994,42 @@ function createHttpServer(opts) {
7836
7994
  );
7837
7995
  return;
7838
7996
  }
7997
+ if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
7998
+ if (requireAccessToken && !accessTokenOk) {
7999
+ res.writeHead(401, { "Content-Type": "application/json" });
8000
+ res.end(JSON.stringify({ error: "Unauthorized" }));
8001
+ return;
8002
+ }
8003
+ if (!opts.projectRoot) {
8004
+ res.writeHead(503, { "Content-Type": "application/json" });
8005
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8006
+ return;
8007
+ }
8008
+ const deadCodeDeps = {
8009
+ projectRoot: opts.projectRoot,
8010
+ ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8011
+ };
8012
+ await handleDeadCodeScan(res, deadCodeDeps, req);
8013
+ return;
8014
+ }
8015
+ if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
8016
+ if (requireAccessToken && !accessTokenOk) {
8017
+ res.writeHead(401, { "Content-Type": "application/json" });
8018
+ res.end(JSON.stringify({ error: "Unauthorized" }));
8019
+ return;
8020
+ }
8021
+ if (!opts.projectRoot) {
8022
+ res.writeHead(503, { "Content-Type": "application/json" });
8023
+ res.end(JSON.stringify({ error: "Project root not configured" }));
8024
+ return;
8025
+ }
8026
+ const deadCodeDeps = {
8027
+ projectRoot: opts.projectRoot,
8028
+ ...opts.indexDir ? { indexDir: opts.indexDir } : {}
8029
+ };
8030
+ await handleDeadCodeActionPlan(res, deadCodeDeps, req);
8031
+ return;
8032
+ }
7839
8033
  if (url.pathname.startsWith("/api/techstack/")) {
7840
8034
  if (requireAccessToken && !accessTokenOk) {
7841
8035
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -7968,17 +8162,17 @@ function createHttpServer(opts) {
7968
8162
  }
7969
8163
  let filePath;
7970
8164
  if (url.pathname === "/" || url.pathname === "") {
7971
- filePath = path11.join(distDir, "index.html");
8165
+ filePath = path12.join(distDir, "index.html");
7972
8166
  } else {
7973
- filePath = path11.join(distDir, url.pathname);
8167
+ filePath = path12.join(distDir, url.pathname);
7974
8168
  }
7975
- const resolvedPath = path11.resolve(filePath);
8169
+ const resolvedPath = path12.resolve(filePath);
7976
8170
  if (!isInsideDist(resolvedPath, distDir)) {
7977
8171
  res.writeHead(403, { "Content-Type": "text/plain" });
7978
8172
  res.end("Forbidden");
7979
8173
  return;
7980
8174
  }
7981
- const ext = path11.extname(resolvedPath);
8175
+ const ext = path12.extname(resolvedPath);
7982
8176
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
7983
8177
  res.setHeader("Content-Type", contentType);
7984
8178
  setStaticSecurityHeaders(res);
@@ -8002,7 +8196,7 @@ function createHttpServer(opts) {
8002
8196
  } catch (err) {
8003
8197
  if (err.code === "ENOENT") {
8004
8198
  try {
8005
- const html = await fs9.readFile(path11.join(distDir, "index.html"), "utf8");
8199
+ const html = await fs9.readFile(path12.join(distDir, "index.html"), "utf8");
8006
8200
  setStaticSecurityHeaders(res);
8007
8201
  res.writeHead(200, {
8008
8202
  "Content-Type": "text/html",
@@ -8032,14 +8226,14 @@ function createHttpServer(opts) {
8032
8226
 
8033
8227
  // src/server/instance-registry.ts
8034
8228
  import * as os from "node:os";
8035
- import * as path12 from "node:path";
8229
+ import * as path13 from "node:path";
8036
8230
  import * as fs10 from "node:fs/promises";
8037
8231
  import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
8038
8232
  function defaultBaseDir() {
8039
- return path12.join(os.homedir(), ".wrongstack");
8233
+ return path13.join(os.homedir(), ".wrongstack");
8040
8234
  }
8041
8235
  function registryPath(baseDir = defaultBaseDir()) {
8042
- return path12.join(baseDir, "webui-instances.json");
8236
+ return path13.join(baseDir, "webui-instances.json");
8043
8237
  }
8044
8238
  function isPidAlive(pid) {
8045
8239
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -11402,16 +11596,16 @@ function createModelOperations(context) {
11402
11596
  import * as net from "node:net";
11403
11597
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
11404
11598
  function isPortFree(host, port) {
11405
- return new Promise((resolve14) => {
11599
+ return new Promise((resolve15) => {
11406
11600
  const srv = net.createServer();
11407
- srv.once("error", () => resolve14(false));
11601
+ srv.once("error", () => resolve15(false));
11408
11602
  srv.once("listening", () => {
11409
- srv.close(() => resolve14(true));
11603
+ srv.close(() => resolve15(true));
11410
11604
  });
11411
11605
  try {
11412
11606
  srv.listen(port, host);
11413
11607
  } catch {
11414
- resolve14(false);
11608
+ resolve15(false);
11415
11609
  }
11416
11610
  });
11417
11611
  }
@@ -11703,7 +11897,7 @@ function seedContextMeta(config, context) {
11703
11897
  meta["autoReviewModel"] = autoReviewExt?.["model"] ?? "";
11704
11898
  meta["autoReviewFallbackProfile"] = autoReviewExt?.["fallbackProfile"] ?? "";
11705
11899
  meta["autoReviewFallbackModels"] = Array.isArray(autoReviewExt?.["fallbackModels"]) ? autoReviewExt?.["fallbackModels"] : [];
11706
- meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 5e3;
11900
+ meta["autoReviewDebounceMs"] = typeof autoReviewExt?.["debounceMs"] === "number" && autoReviewExt["debounceMs"] >= 0 ? autoReviewExt["debounceMs"] : 15e3;
11707
11901
  meta["autoReviewMaxFilesPerBatch"] = typeof autoReviewExt?.["maxFilesPerBatch"] === "number" && autoReviewExt["maxFilesPerBatch"] >= 1 ? autoReviewExt["maxFilesPerBatch"] : 15;
11708
11902
  meta["autoReviewMaxConcurrentReviews"] = typeof autoReviewExt?.["maxConcurrentReviews"] === "number" && autoReviewExt["maxConcurrentReviews"] >= 1 ? autoReviewExt["maxConcurrentReviews"] : 2;
11709
11903
  const cascade = autoReviewExt?.["cascadeOn"];
@@ -11723,7 +11917,7 @@ function seedContextMeta(config, context) {
11723
11917
 
11724
11918
  // src/server/pref-helpers.ts
11725
11919
  import * as fs12 from "node:fs/promises";
11726
- import * as path13 from "node:path";
11920
+ import * as path14 from "node:path";
11727
11921
  import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
11728
11922
  import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
11729
11923
  var PREF_KEYS = [
@@ -11819,7 +12013,7 @@ function prefSnapshot(contextMeta) {
11819
12013
  return snapshot;
11820
12014
  }
11821
12015
  async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
11822
- const globalRoot = path13.dirname(filePath);
12016
+ const globalRoot = path14.dirname(filePath);
11823
12017
  await backupConfigFile(filePath, { globalRoot });
11824
12018
  let raw;
11825
12019
  try {
@@ -12299,7 +12493,7 @@ async function handleProcessRoute(ws, msg, handlers) {
12299
12493
 
12300
12494
  // src/server/project-handlers.ts
12301
12495
  import * as fs13 from "node:fs/promises";
12302
- import * as path14 from "node:path";
12496
+ import * as path15 from "node:path";
12303
12497
  import { DefaultSessionStore } from "@wrongstack/core/storage";
12304
12498
  import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
12305
12499
  function createProjectHandlers(ctx) {
@@ -12351,8 +12545,8 @@ function createProjectHandlers(ctx) {
12351
12545
  });
12352
12546
  return;
12353
12547
  }
12354
- const resolved = path14.resolve(parsed.value.root);
12355
- const name2 = parsed.value.name?.trim() || path14.basename(resolved);
12548
+ const resolved = path15.resolve(parsed.value.root);
12549
+ const name2 = parsed.value.name?.trim() || path15.basename(resolved);
12356
12550
  try {
12357
12551
  const stat3 = await fs13.stat(resolved).catch(() => null);
12358
12552
  if (!stat3?.isDirectory()) {
@@ -12363,7 +12557,7 @@ function createProjectHandlers(ctx) {
12363
12557
  return;
12364
12558
  }
12365
12559
  const before = await loadManifest(ctx.globalConfigPath);
12366
- const already = before.projects.some((project) => path14.resolve(project.root) === resolved);
12560
+ const already = before.projects.some((project) => path15.resolve(project.root) === resolved);
12367
12561
  const entry = await touchProjectInManifest(
12368
12562
  { projectRoot: resolved, workingDir: resolved, name: name2 },
12369
12563
  ctx.globalConfigPath
@@ -12393,8 +12587,8 @@ function createProjectHandlers(ctx) {
12393
12587
  });
12394
12588
  return;
12395
12589
  }
12396
- const resolved = path14.resolve(parsed.value.root);
12397
- const name2 = parsed.value.name?.trim() || path14.basename(resolved);
12590
+ const resolved = path15.resolve(parsed.value.root);
12591
+ const name2 = parsed.value.name?.trim() || path15.basename(resolved);
12398
12592
  if (!ctx.allowProjectMutations) {
12399
12593
  sendTo(ws, {
12400
12594
  type: "projects.selected",
@@ -12429,6 +12623,17 @@ function createProjectHandlers(ctx) {
12429
12623
  });
12430
12624
  const previous = ctx.getSession();
12431
12625
  const previousId = previous.id;
12626
+ const previousProjectRoot = ctx.getProjectRoot();
12627
+ const previousPaths = resolveWstackPaths4({
12628
+ projectRoot: previousProjectRoot,
12629
+ globalRoot: ctx.wpaths.globalRoot
12630
+ });
12631
+ const previousIdentityTarget = {
12632
+ projectSlug: previousPaths.projectSlug,
12633
+ projectRoot: previousProjectRoot,
12634
+ projectName: path15.basename(previousProjectRoot),
12635
+ workingDir: ctx.context.workingDir
12636
+ };
12432
12637
  const previousUsage = ctx.tokenCounter.total();
12433
12638
  const config = ctx.getConfig?.() ?? ctx.config;
12434
12639
  const next = await store.create({
@@ -12454,7 +12659,16 @@ function createProjectHandlers(ctx) {
12454
12659
  };
12455
12660
  try {
12456
12661
  await ctx.onSessionSwapped?.(next.id, identityTarget);
12662
+ await ctx.onBeforeSessionTodosReplaced?.(next.id, paths.projectSessions);
12457
12663
  } catch (err) {
12664
+ try {
12665
+ await ctx.onBeforeSessionTodosReplaced?.(previous.id, previousPaths.projectSessions);
12666
+ } catch {
12667
+ }
12668
+ try {
12669
+ await ctx.onSessionSwapped?.(previous.id, previousIdentityTarget);
12670
+ } catch {
12671
+ }
12458
12672
  await next.close().catch(() => void 0);
12459
12673
  await store.delete(next.id).catch(() => void 0);
12460
12674
  throw err;
@@ -13477,6 +13691,7 @@ var CLIENT_WORKSPACE_MESSAGE_TYPES = [
13477
13691
  var CLIENT_CONFIGURATION_MESSAGE_TYPES = [
13478
13692
  "codebase.index.server.shutdown",
13479
13693
  "connections.health",
13694
+ "connections.service_action",
13480
13695
  "diag.get",
13481
13696
  "key.add",
13482
13697
  "key.delete",
@@ -13750,6 +13965,7 @@ var SERVER_CONFIGURATION_MESSAGE_TYPES = [
13750
13965
  "codebase.index.server.shutdown_result",
13751
13966
  "connections.health_error",
13752
13967
  "connections.health_result",
13968
+ "connections.service_action_result",
13753
13969
  "diag.get",
13754
13970
  "key.operation_result",
13755
13971
  "model.switch_result",
@@ -13794,13 +14010,13 @@ function isRegisteredMessageType(type, direction) {
13794
14010
  // src/protocol/decoder.ts
13795
14011
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
13796
14012
  var MAX_PAYLOAD_DEPTH = 32;
13797
- function inspectValue(value, path28, depth) {
14013
+ function inspectValue(value, path29, depth) {
13798
14014
  if (depth > MAX_PAYLOAD_DEPTH) {
13799
- return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path28 };
14015
+ return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path29 };
13800
14016
  }
13801
14017
  if (value === null || typeof value !== "object") return null;
13802
14018
  for (const key of Object.keys(value)) {
13803
- const childPath = `${path28}.${key}`;
14019
+ const childPath = `${path29}.${key}`;
13804
14020
  if (FORBIDDEN_KEYS.has(key)) {
13805
14021
  return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
13806
14022
  }
@@ -14010,6 +14226,7 @@ function createSessionHandlers(ctx) {
14010
14226
  ctx.context.session = next;
14011
14227
  ctx.context.state.replaceMessages(messages);
14012
14228
  await ctx.context.flushConversationJournal?.();
14229
+ await ctx.onBeforeSessionTodosReplaced?.(next.id, sessionsDirectory());
14013
14230
  ctx.context.state.replaceTodos(todos);
14014
14231
  resetContextAccounting();
14015
14232
  ctx.context.readFiles.clear();
@@ -14406,7 +14623,10 @@ function createSessionHandlers(ctx) {
14406
14623
  rollbackClaim = await ctx.claimSession?.(canonicalId);
14407
14624
  const resumed = await store.resume(canonicalId);
14408
14625
  const restoredTodos = await loadTodosCheckpoint(
14409
- sessionScopedPath(sessionsDirectory(), resumed.writer.id, ".todos.json")
14626
+ sessionScopedPath(sessionsDirectory(), resumed.writer.id, ".todos.json"),
14627
+ ctx.events,
14628
+ ctx.context.traceId,
14629
+ resumed.writer.id
14410
14630
  ).catch(() => null) ?? [];
14411
14631
  activated = true;
14412
14632
  await activateSession(
@@ -14925,7 +15145,7 @@ ${String(p.content ?? "")}`;
14925
15145
  };
14926
15146
 
14927
15147
  // src/server/codebase-index-server-control.ts
14928
- import { shutdownCodebaseIndexServer } from "@wrongstack/tools";
15148
+ import { shutdownCodebaseIndexServer as shutdownCodebaseIndexServer2 } from "@wrongstack/tools";
14929
15149
  async function handleCodebaseIndexServerControl(ws, message, deps2) {
14930
15150
  if (message.type !== "codebase.index.server.shutdown") return false;
14931
15151
  const requestId = message.payload && typeof message.payload === "object" && typeof message.payload.requestId === "string" ? message.payload.requestId : "";
@@ -14952,7 +15172,7 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
14952
15172
  });
14953
15173
  return true;
14954
15174
  }
14955
- const result = await shutdownCodebaseIndexServer(
15175
+ const result = await shutdownCodebaseIndexServer2(
14956
15176
  projectRoot,
14957
15177
  deps2.getIndexDir(),
14958
15178
  "websocket-request"
@@ -15493,7 +15713,7 @@ function createRouteFamilyDispatcher(options) {
15493
15713
 
15494
15714
  // src/server/shell-open.ts
15495
15715
  import * as fs15 from "node:fs/promises";
15496
- import * as path15 from "node:path";
15716
+ import * as path16 from "node:path";
15497
15717
  import { spawn } from "node:child_process";
15498
15718
  function normalizeShellOpenTarget(target) {
15499
15719
  return target === "terminal" ? "terminal" : "file-manager";
@@ -15504,11 +15724,11 @@ function shellQuote(s) {
15504
15724
  }
15505
15725
  async function handleShellOpen(req, logger, options) {
15506
15726
  try {
15507
- const resolved = path15.resolve(req.path);
15727
+ const resolved = path16.resolve(req.path);
15508
15728
  if (options?.projectRoot) {
15509
- const root = path15.resolve(options.projectRoot);
15510
- const relative5 = path15.relative(root, resolved);
15511
- const escapes = relative5.startsWith("..") || path15.isAbsolute(relative5);
15729
+ const root = path16.resolve(options.projectRoot);
15730
+ const relative5 = path16.relative(root, resolved);
15731
+ const escapes = relative5.startsWith("..") || path16.isAbsolute(relative5);
15512
15732
  if (escapes) {
15513
15733
  return {
15514
15734
  success: false,
@@ -15570,6 +15790,7 @@ async function handleShellOpen(req, logger, options) {
15570
15790
  import { listBoards as listBoards3 } from "@wrongstack/kanban";
15571
15791
  import {
15572
15792
  applySddLifecycle,
15793
+ extractVerificationCommand,
15573
15794
  SddBoardStore
15574
15795
  } from "@wrongstack/sdd";
15575
15796
  var CONTROL_TYPES = /* @__PURE__ */ new Set([
@@ -15592,14 +15813,16 @@ var SddBoardWebSocketHandler = class {
15592
15813
  store;
15593
15814
  clients = /* @__PURE__ */ new Set();
15594
15815
  lifecycle;
15816
+ security;
15595
15817
  diskPollingEnabled;
15596
15818
  latest = null;
15597
15819
  poll = null;
15598
15820
  pollInFlight = false;
15599
15821
  unsub = null;
15600
- constructor(boardsDir, events, lifecycle) {
15822
+ constructor(boardsDir, events, lifecycle, security) {
15601
15823
  this.store = new SddBoardStore({ baseDir: boardsDir });
15602
15824
  this.lifecycle = lifecycle;
15825
+ this.security = security;
15603
15826
  this.diskPollingEnabled = events === void 0;
15604
15827
  if (events) {
15605
15828
  const handler = (e) => {
@@ -15645,6 +15868,43 @@ var SddBoardWebSocketHandler = class {
15645
15868
  return;
15646
15869
  }
15647
15870
  if (CONTROL_TYPES.has(action)) {
15871
+ const verificationCommands = [];
15872
+ if (action === "set_task_verification") {
15873
+ const command = msg.payload?.verificationCommand;
15874
+ if (command !== void 0 && (typeof command !== "string" || command.length > 8192)) return;
15875
+ if (typeof command === "string" && command.trim()) {
15876
+ verificationCommands.push({ command, operation: "sdd.set_task_verification" });
15877
+ }
15878
+ } else if (action === "split_task") {
15879
+ const subtasks = msg.payload?.subtasks;
15880
+ if (Array.isArray(subtasks)) {
15881
+ for (const subtask of subtasks) {
15882
+ if (!subtask || typeof subtask !== "object") continue;
15883
+ const criterion = subtask.successCriterion;
15884
+ if (criterion === void 0) continue;
15885
+ if (typeof criterion !== "string") return;
15886
+ const command = extractVerificationCommand([criterion]);
15887
+ if (!command) continue;
15888
+ if (command.length > 8192) return;
15889
+ verificationCommands.push({ command, operation: "sdd.split_task_verification" });
15890
+ }
15891
+ }
15892
+ }
15893
+ for (const { command, operation } of verificationCommands) {
15894
+ if (!this.security) return;
15895
+ const authorization = await authorizeWebUIAction(
15896
+ this.security.trustBoundary,
15897
+ {
15898
+ capability: "process.spawn",
15899
+ subject: { kind: "command", id: command },
15900
+ risk: "high",
15901
+ cwd: this.lifecycle?.projectRoot,
15902
+ metadata: { operation }
15903
+ },
15904
+ this.security.logger
15905
+ );
15906
+ if (!authorization.allowed) return;
15907
+ }
15648
15908
  const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
15649
15909
  if (runId) {
15650
15910
  await this.store.appendControl(runId, {
@@ -15757,7 +16017,7 @@ var SddBoardWebSocketHandler = class {
15757
16017
  };
15758
16018
 
15759
16019
  // src/server/sdd-wizard-wiring.ts
15760
- import * as path16 from "node:path";
16020
+ import * as path17 from "node:path";
15761
16021
  import {
15762
16022
  DefaultTaskStore,
15763
16023
  TaskTracker
@@ -15871,7 +16131,7 @@ function buildSddWizardDeps(opts) {
15871
16131
  }).catch(() => {
15872
16132
  projectContext = "";
15873
16133
  });
15874
- const sessionPath = opts.paths.projectSddSession ?? path16.join(opts.paths.projectDir, "sdd-session.json");
16134
+ const sessionPath = opts.paths.projectSddSession ?? path17.join(opts.paths.projectDir, "sdd-session.json");
15875
16135
  const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
15876
16136
  const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
15877
16137
  const runIsolatedTurn = async (prompt, name2) => {
@@ -16153,7 +16413,7 @@ var SddWizardWebSocketHandler = class {
16153
16413
  return;
16154
16414
  }
16155
16415
  const { runId } = await this.deps.startRun(this.driver, opts);
16156
- this.driver.setLastRunId(runId);
16416
+ await this.driver.setLastRunId(runId);
16157
16417
  if (this.driver.phase() !== "executing" && this.driver.phase() !== "done") {
16158
16418
  try {
16159
16419
  if (this.driver.phase() === "task_review") await this.driver.approve();
@@ -16212,7 +16472,7 @@ var SddWizardWebSocketHandler = class {
16212
16472
  this.lastAgentText = text;
16213
16473
  if (this.driver) {
16214
16474
  await this.driver.ingestAgentOutput(text);
16215
- this.driver.setLastAgentText(text);
16475
+ await this.driver.setLastAgentText(text);
16216
16476
  }
16217
16477
  this.broadcast({ type: "sdd.spec.agent_text", payload: { text } });
16218
16478
  } finally {
@@ -16243,10 +16503,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
16243
16503
 
16244
16504
  // src/server/setup-events-fleet-broadcaster.ts
16245
16505
  import { watch as fsWatch } from "node:fs";
16246
- import * as path17 from "node:path";
16506
+ import * as path18 from "node:path";
16247
16507
  function registerSetupEventsFleetBroadcaster(deps2) {
16248
16508
  const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
16249
- const globalRoot = globalConfigPath ? path17.dirname(globalConfigPath) : void 0;
16509
+ const globalRoot = globalConfigPath ? path18.dirname(globalConfigPath) : void 0;
16250
16510
  if (!globalRoot) return void 0;
16251
16511
  const disposers = [];
16252
16512
  const broadcastSessions = async () => {
@@ -16256,8 +16516,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
16256
16516
  const sessions = await registry.list();
16257
16517
  const ownEntry = sessions.find((s) => s.pid === process.pid);
16258
16518
  const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
16259
- const myRoot = path17.resolve(context.projectRoot);
16260
- const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path17.resolve(s.projectRoot) === myRoot).map((s) => ({
16519
+ const myRoot = path18.resolve(context.projectRoot);
16520
+ const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path18.resolve(s.projectRoot) === myRoot).map((s) => ({
16261
16521
  sessionId: s.sessionId,
16262
16522
  projectName: s.projectName,
16263
16523
  projectSlug: s.projectSlug,
@@ -16448,13 +16708,13 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
16448
16708
  // src/server/setup-events-status-watcher.ts
16449
16709
  import { watch as fsWatch2 } from "node:fs";
16450
16710
  import * as fs16 from "node:fs/promises";
16451
- import * as path19 from "node:path";
16711
+ import * as path20 from "node:path";
16452
16712
 
16453
16713
  // src/server/setup-events-watcher.ts
16454
- import * as path18 from "node:path";
16714
+ import * as path19 from "node:path";
16455
16715
  function statusProjectHashFromWatchFilename(projectsDir, filename) {
16456
16716
  const raw = String(filename);
16457
- const relative5 = path18.isAbsolute(raw) ? path18.relative(projectsDir, raw) : raw;
16717
+ const relative5 = path19.isAbsolute(raw) ? path19.relative(projectsDir, raw) : raw;
16458
16718
  const parts = relative5.split(/[\\/]+/).filter(Boolean);
16459
16719
  if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
16460
16720
  return parts.at(-2) ?? null;
@@ -16489,7 +16749,7 @@ function logFileWatcherMetrics(metrics) {
16489
16749
  function registerSetupEventsStatusWatcher(deps2) {
16490
16750
  const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
16491
16751
  if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
16492
- const projectsDir = path19.join(wpaths.globalRoot, "projects");
16752
+ const projectsDir = path20.join(wpaths.globalRoot, "projects");
16493
16753
  const knownProjectHashes = /* @__PURE__ */ new Set();
16494
16754
  const debounceTimers = /* @__PURE__ */ new Map();
16495
16755
  const DEBOUNCE_MS = 150;
@@ -16548,7 +16808,7 @@ function registerSetupEventsStatusWatcher(deps2) {
16548
16808
  if (!knownProjectHashes.has(projectHash)) return;
16549
16809
  if (watcherMetrics) watcherMetrics.filesProcessed++;
16550
16810
  try {
16551
- const targetFile = path19.join(projectsDir, projectHash, "status.json");
16811
+ const targetFile = path20.join(projectsDir, projectHash, "status.json");
16552
16812
  const content = await fs16.readFile(targetFile, "utf-8");
16553
16813
  const statusData = JSON.parse(content);
16554
16814
  scheduleBroadcast(projectHash, statusData);
@@ -16607,7 +16867,7 @@ function registerSetupEventsStatusWatcher(deps2) {
16607
16867
 
16608
16868
  // src/server/setup-events-core-watchers.ts
16609
16869
  import * as fs17 from "node:fs/promises";
16610
- import * as path20 from "node:path";
16870
+ import * as path21 from "node:path";
16611
16871
  function registerSetupEventsCoreWatchers(deps2) {
16612
16872
  const { broadcast: broadcast2, clients, context } = deps2;
16613
16873
  const disposers = [];
@@ -16643,7 +16903,7 @@ function registerSetupEventsClientStatusWriter(deps2) {
16643
16903
  if (wpaths?.projectStatus) {
16644
16904
  try {
16645
16905
  const statusFile = wpaths.projectStatus(e.projectHash);
16646
- const dir = path20.dirname(statusFile);
16906
+ const dir = path21.dirname(statusFile);
16647
16907
  await fs17.mkdir(dir, { recursive: true });
16648
16908
  await fs17.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
16649
16909
  } catch (err) {
@@ -16833,6 +17093,9 @@ function setupEvents(deps2) {
16833
17093
  input: scrub(e.input),
16834
17094
  fileTargets: extractCodeMapFileTargets(projectRoot || ".", e.name, e.input),
16835
17095
  output: scrub(e.output),
17096
+ // SAGE-injected memory rides beside the tool text so the client renders
17097
+ // it as a memory card. Never folded back into `output`.
17098
+ ...e.sage && e.sage.length > 0 ? { sage: e.sage.map((line) => scrub(line)) } : {},
16836
17099
  outputBytes: e.outputBytes,
16837
17100
  outputTokens: e.outputTokens,
16838
17101
  outputLines: e.outputLines,
@@ -17667,17 +17930,24 @@ var SpecsWebSocketHandler = class {
17667
17930
  // src/server/start-webui.ts
17668
17931
  import { randomUUID as randomUUID5 } from "node:crypto";
17669
17932
  import * as http2 from "node:http";
17670
- import * as path27 from "node:path";
17933
+ import * as path28 from "node:path";
17671
17934
  import { createDefaultPipelines } from "@wrongstack/core/agent";
17672
17935
  import { getSharedProjectMailbox as getSharedProjectMailbox4, resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
17673
17936
  import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
17674
17937
  import {
17938
+ attachTodosCheckpoint,
17675
17939
  createSessionEventBridge,
17676
17940
  resolveSessionLoggingConfig,
17677
17941
  watchProviderConfig
17678
17942
  } from "@wrongstack/core/storage";
17679
17943
  import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } from "@wrongstack/core/types";
17680
- import { expectDefined as expectDefined3, startHeapWatchdog, toErrorMessage as toErrorMessage13, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
17944
+ import {
17945
+ expectDefined as expectDefined3,
17946
+ sessionScopedPath as sessionScopedPath3,
17947
+ startHeapWatchdog,
17948
+ toErrorMessage as toErrorMessage13,
17949
+ wstackGlobalRoot as wstackGlobalRoot2
17950
+ } from "@wrongstack/core/utils";
17681
17951
  import { makeProviderFromConfig as makeProviderFromConfig3 } from "@wrongstack/providers";
17682
17952
  import { toLanguagePackageInput } from "@wrongstack/techstack";
17683
17953
  import { ensureSessionShell } from "@wrongstack/tools";
@@ -17864,7 +18134,7 @@ function findWorkspaceCliEntry(projectRoot) {
17864
18134
  return null;
17865
18135
  }
17866
18136
  function sleep(ms) {
17867
- return new Promise((resolve14) => setTimeout(resolve14, ms));
18137
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
17868
18138
  }
17869
18139
 
17870
18140
  // src/server/terminal-ws-handler.ts
@@ -18100,7 +18370,7 @@ function clampDim(value, fallback) {
18100
18370
  }
18101
18371
 
18102
18372
  // src/server/worktree-ws-handler.ts
18103
- import { join as join11, resolve as resolve11, sep as sep4 } from "node:path";
18373
+ import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
18104
18374
  import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
18105
18375
  import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
18106
18376
  import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
@@ -18161,13 +18431,13 @@ var WorktreeWebSocketHandler = class {
18161
18431
  // ── orphan management ─────────────────────────────────────────────────────
18162
18432
  /** Absolute managed-worktrees root for this project. */
18163
18433
  worktreesRoot() {
18164
- return resolve11(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
18434
+ return resolve12(join11(this.management.projectRoot, ".wrongstack", "worktrees"));
18165
18435
  }
18166
18436
  /** True iff `dir` resolves strictly inside the managed worktrees root. */
18167
18437
  underRoot(dir) {
18168
- const abs = resolve11(dir);
18438
+ const abs = resolve12(dir);
18169
18439
  const root = this.worktreesRoot();
18170
- return abs !== root && abs.startsWith(root + sep4);
18440
+ return abs !== root && abs.startsWith(root + sep5);
18171
18441
  }
18172
18442
  /** Branches of worktrees a live in-session run currently owns. */
18173
18443
  liveActiveBranches() {
@@ -18315,7 +18585,7 @@ var WorktreeWebSocketHandler = class {
18315
18585
  }
18316
18586
  const base = baseBranch && MANAGED_BRANCH_RE.test(baseBranch) ? baseBranch : void 0;
18317
18587
  const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
18318
- const summary = await wt.diffSummary(resolve11(dir), base);
18588
+ const summary = await wt.diffSummary(resolve12(dir), base);
18319
18589
  this.broadcast({ type: "worktree.diff_result", payload: { dir, summary } });
18320
18590
  }
18321
18591
  // ── internals ───────────────────────────────────────────────────────────
@@ -18464,7 +18734,9 @@ async function createAgentServices(input) {
18464
18734
  memory: memoryRetrieval,
18465
18735
  maxHintsPerTool: config.Sage?.inject?.maxHintsPerTool,
18466
18736
  maxCharsPerTool: config.Sage?.inject?.maxCharsPerTool,
18737
+ taskAware: config.Sage?.inject?.taskAware,
18467
18738
  minScore: config.Sage?.inject?.minScore,
18739
+ minImportance: config.Sage?.inject?.minImportance,
18468
18740
  repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
18469
18741
  verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
18470
18742
  triggers: config.Sage?.inject?.triggers
@@ -18767,15 +19039,20 @@ async function createAgentServices(input) {
18767
19039
  projectRoot
18768
19040
  );
18769
19041
  const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
18770
- const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards, void 0, {
18771
- projectRoot,
18772
- paths: {
18773
- projectSpecs: wpaths.projectSpecs,
18774
- projectTaskGraphs: wpaths.projectTaskGraphs,
18775
- projectSddSession: wpaths.projectSddSession,
18776
- projectSddBoards: wpaths.projectSddBoards
18777
- }
18778
- });
19042
+ const sddBoardHandler = new SddBoardWebSocketHandler(
19043
+ wpaths.projectSddBoards,
19044
+ void 0,
19045
+ {
19046
+ projectRoot,
19047
+ paths: {
19048
+ projectSpecs: wpaths.projectSpecs,
19049
+ projectTaskGraphs: wpaths.projectTaskGraphs,
19050
+ projectSddSession: wpaths.projectSddSession,
19051
+ projectSddBoards: wpaths.projectSddBoards
19052
+ }
19053
+ },
19054
+ { trustBoundary: input.trustBoundary, logger }
19055
+ );
18779
19056
  const sddWizardHandler = new SddWizardWebSocketHandler(
18780
19057
  buildSddWizardDeps({
18781
19058
  agent,
@@ -18787,7 +19064,16 @@ async function createAgentServices(input) {
18787
19064
  providerRegistry,
18788
19065
  toolRegistry,
18789
19066
  session: input.sessionGetter(),
18790
- projectRoot
19067
+ projectRoot,
19068
+ // Thread the container-provided ProviderModelStatusTracker so a 429
19069
+ // from this subagent's first call transitions the (provider, model)
19070
+ // pair to `state: 'blocked'` instead of silently no-op'ing. The
19071
+ // runtime container binds a default `ProviderModelStatusTracker`
19072
+ // (see packages/runtime/src/container.ts); without this dep, the
19073
+ // subagent's fallback extension's tracker hooks are undefined and
19074
+ // round-robin keeps reassigning the doomed model. Mirrors the CLI
19075
+ // factory wiring at host-subagent-factory.ts:337.
19076
+ statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker)
18791
19077
  }),
18792
19078
  paths: {
18793
19079
  projectSpecs: wpaths.projectSpecs,
@@ -18930,7 +19216,7 @@ function createConnectionHandler(options) {
18930
19216
  }
18931
19217
 
18932
19218
  // src/server/message-dispatcher.ts
18933
- import path21 from "node:path";
19219
+ import path22 from "node:path";
18934
19220
  function createMessageDispatcher(opts) {
18935
19221
  const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
18936
19222
  function makeWorklistContext() {
@@ -18951,7 +19237,7 @@ function createMessageDispatcher(opts) {
18951
19237
  skillLoader: deps2.skillLoader,
18952
19238
  skillInstaller: deps2.skillInstaller,
18953
19239
  projectRoot,
18954
- projectSkillsDir: path21.join(projectRoot, ".wrongstack", "skills"),
19240
+ projectSkillsDir: path22.join(projectRoot, ".wrongstack", "skills"),
18955
19241
  globalSkillsDir: deps2.wpaths.globalSkills
18956
19242
  };
18957
19243
  }
@@ -19203,7 +19489,7 @@ function createMessageDispatcher(opts) {
19203
19489
 
19204
19490
  // src/server/pre-context-services.ts
19205
19491
  import { createRequire as createRequire3 } from "node:module";
19206
- import * as path24 from "node:path";
19492
+ import * as path25 from "node:path";
19207
19493
  import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
19208
19494
  import {
19209
19495
  getSharedProjectMailbox as getSharedProjectMailbox3,
@@ -19258,7 +19544,7 @@ import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/too
19258
19544
 
19259
19545
  // src/server/model-auto-discovery.ts
19260
19546
  import * as fs18 from "node:fs/promises";
19261
- import * as path22 from "node:path";
19547
+ import * as path23 from "node:path";
19262
19548
  import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
19263
19549
  function isOverlayRegistry(value) {
19264
19550
  return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
@@ -19294,7 +19580,7 @@ async function discoverAndMergeWebuiProviders(opts) {
19294
19580
  if (!isOverlayRegistry(registry)) return;
19295
19581
  const targets = eligibleProviders(opts.config);
19296
19582
  if (targets.length === 0) return;
19297
- const cacheFile = path22.join(opts.cacheDir, "discovered-models-cache.json");
19583
+ const cacheFile = path23.join(opts.cacheDir, "discovered-models-cache.json");
19298
19584
  const cache2 = await readCache(cacheFile);
19299
19585
  let cacheDirty = false;
19300
19586
  await Promise.all(
@@ -19331,7 +19617,7 @@ async function discoverAndMergeWebuiProviders(opts) {
19331
19617
  );
19332
19618
  if (cacheDirty) {
19333
19619
  try {
19334
- await fs18.mkdir(path22.dirname(cacheFile), { recursive: true });
19620
+ await fs18.mkdir(path23.dirname(cacheFile), { recursive: true });
19335
19621
  await fs18.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
19336
19622
  } catch {
19337
19623
  opts.logger?.debug?.("provider auto-discovery cache write failed");
@@ -19428,7 +19714,7 @@ function resolveSetupProvider(opts) {
19428
19714
  }
19429
19715
 
19430
19716
  // src/server/standalone-session-identity.ts
19431
- import * as path23 from "node:path";
19717
+ import * as path24 from "node:path";
19432
19718
  import {
19433
19719
  AgentStatusTracker,
19434
19720
  FleetNotifier,
@@ -19447,7 +19733,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
19447
19733
  let activeTarget = {
19448
19734
  projectSlug: paths.projectSlug,
19449
19735
  projectRoot: paths.projectRoot,
19450
- projectName: path23.basename(paths.projectRoot),
19736
+ projectName: path24.basename(paths.projectRoot),
19451
19737
  workingDir: opts.workingDir
19452
19738
  };
19453
19739
  let pendingClaim;
@@ -19678,7 +19964,7 @@ async function createPreContextServices(input) {
19678
19964
  await discoverAndMergeWebuiProviders({
19679
19965
  config,
19680
19966
  registry: modelsRegistry,
19681
- cacheDir: path24.dirname(wpaths.modelsCache),
19967
+ cacheDir: path25.dirname(wpaths.modelsCache),
19682
19968
  logger
19683
19969
  });
19684
19970
  } catch (err) {
@@ -19730,7 +20016,7 @@ async function createPreContextServices(input) {
19730
20016
  configureChildEnvGitIdentity(config.git?.identity ?? null);
19731
20017
  console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
19732
20018
  const mcpTokenStore = new MCPVaultTokenStore(
19733
- path24.join(wpaths.projectDir, "mcp-auth.json"),
20019
+ path25.join(wpaths.projectDir, "mcp-auth.json"),
19734
20020
  vault
19735
20021
  );
19736
20022
  const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
@@ -19836,7 +20122,7 @@ async function createPreContextServices(input) {
19836
20122
  };
19837
20123
  const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
19838
20124
  const skillInstaller = config.features.skills ? new SkillInstaller({
19839
- manifestPath: path24.join(wpaths.configDir, "installed-skills.json"),
20125
+ manifestPath: path25.join(wpaths.configDir, "installed-skills.json"),
19840
20126
  projectSkillsDir: wpaths.inProjectSkills,
19841
20127
  globalSkillsDir: wpaths.globalSkills,
19842
20128
  projectHash: wpaths.projectHash,
@@ -19846,8 +20132,8 @@ async function createPreContextServices(input) {
19846
20132
  const bundledPromptsDir = promptsEnabled ? (() => {
19847
20133
  try {
19848
20134
  const req = createRequire3(import.meta.url);
19849
- return path24.join(
19850
- path24.dirname(req.resolve("@wrongstack/core/package.json")),
20135
+ return path25.join(
20136
+ path25.dirname(req.resolve("@wrongstack/core/package.json")),
19851
20137
  "data",
19852
20138
  "prompts"
19853
20139
  );
@@ -19951,7 +20237,7 @@ async function createPreContextServices(input) {
19951
20237
  }
19952
20238
 
19953
20239
  // src/server/routes.ts
19954
- import path25 from "node:path";
20240
+ import path26 from "node:path";
19955
20241
  import { makeProviderFromConfig as makeProviderFromConfig2, withCatalogCapabilities } from "@wrongstack/providers";
19956
20242
 
19957
20243
  // src/server/mode-handlers.ts
@@ -20076,6 +20362,7 @@ function buildRoutes(state, deps2, cb) {
20076
20362
  config: state.getConfig(),
20077
20363
  clients: state.getClients(),
20078
20364
  context: deps2.context,
20365
+ events: deps2.events,
20079
20366
  toolRegistry: deps2.toolRegistry,
20080
20367
  compactor: deps2.compactor,
20081
20368
  customModeStore: deps2.customModeStore,
@@ -20087,6 +20374,7 @@ function buildRoutes(state, deps2, cb) {
20087
20374
  setSession: state.setSession,
20088
20375
  setSessionStartedAt: state.setSessionStartedAt,
20089
20376
  claimSession: cb.claimSession,
20377
+ onBeforeSessionTodosReplaced: cb.onBeforeSessionTodosReplaced,
20090
20378
  onSessionSwapped: cb.onSessionSwapped,
20091
20379
  abortActiveRun: state.abortRunLock,
20092
20380
  isRunActive: state.isRunActive,
@@ -20108,6 +20396,8 @@ function buildRoutes(state, deps2, cb) {
20108
20396
  setSessionStore: state.setSessionStore,
20109
20397
  setSessionStartedAt: state.setSessionStartedAt,
20110
20398
  abortRunLock: state.abortRunLock,
20399
+ onBeforeSessionTodosReplaced: cb.onBeforeSessionTodosReplaced,
20400
+ onSessionSwapped: cb.onSessionSwapped,
20111
20401
  sessionStartPayload: cb.sessionStartPayload
20112
20402
  });
20113
20403
  const modeRoutes = createModeHandlers({
@@ -20239,7 +20529,7 @@ function buildRoutes(state, deps2, cb) {
20239
20529
  };
20240
20530
  const mailboxRoutes = createMailboxRouteHandlers({
20241
20531
  getProjectRoot: state.getProjectRoot,
20242
- getGlobalRoot: () => path25.dirname(deps2.globalConfigPath),
20532
+ getGlobalRoot: () => path26.dirname(deps2.globalConfigPath),
20243
20533
  events: deps2.events
20244
20534
  });
20245
20535
  const mcpRoutes = {
@@ -20312,7 +20602,7 @@ function buildRoutes(state, deps2, cb) {
20312
20602
  }
20313
20603
 
20314
20604
  // src/server/server-runtime.ts
20315
- import * as path26 from "node:path";
20605
+ import * as path27 from "node:path";
20316
20606
  import { createRequire as createRequire4 } from "node:module";
20317
20607
  import { fileURLToPath } from "node:url";
20318
20608
  import { WebSocketServer } from "ws";
@@ -20373,7 +20663,7 @@ function createSessionStartPayload(g) {
20373
20663
  inputCost,
20374
20664
  outputCost,
20375
20665
  cacheReadCost,
20376
- projectName: path26.basename(projectRoot) || projectRoot,
20666
+ projectName: path27.basename(projectRoot) || projectRoot,
20377
20667
  projectRoot,
20378
20668
  cwd: g.getWorkingDir(),
20379
20669
  mode: g.getModeId(),
@@ -20461,13 +20751,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
20461
20751
  };
20462
20752
  }
20463
20753
  function resolveWebuiDistDir(fromUrl, explicitDistDir) {
20464
- if (explicitDistDir) return path26.resolve(explicitDistDir);
20754
+ if (explicitDistDir) return path27.resolve(explicitDistDir);
20465
20755
  try {
20466
20756
  const requireFromHere2 = createRequire4(fromUrl);
20467
20757
  const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
20468
- return path26.dirname(serverEntry);
20758
+ return path27.dirname(serverEntry);
20469
20759
  } catch {
20470
- return path26.resolve(path26.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
20760
+ return path27.resolve(path27.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
20471
20761
  }
20472
20762
  }
20473
20763
  function startHttpServer(opts) {
@@ -20498,6 +20788,56 @@ function registerShutdown(deps2) {
20498
20788
  }
20499
20789
 
20500
20790
  // src/server/start-webui.ts
20791
+ function createStandaloneTodosCheckpointLifecycle(input) {
20792
+ let checkpointSessionId = input.sessionId;
20793
+ let checkpointSessionsDir = input.sessionsDir;
20794
+ const attachCheckpoint = (sessionId, sessionsDir) => attachTodosCheckpoint(
20795
+ input.state,
20796
+ sessionScopedPath3(sessionsDir, sessionId, ".todos.json"),
20797
+ sessionId,
20798
+ input.events,
20799
+ input.traceId,
20800
+ input.warn
20801
+ );
20802
+ let detachCurrent = attachCheckpoint(input.sessionId, input.sessionsDir);
20803
+ let checkpointAttached = true;
20804
+ const detachCurrentCheckpoint = async () => {
20805
+ if (!checkpointAttached) return;
20806
+ checkpointAttached = false;
20807
+ await detachCurrent();
20808
+ };
20809
+ let transitionTail = Promise.resolve();
20810
+ const rebind = (nextSessionId, sessionsDir) => {
20811
+ const transition = transitionTail.then(async () => {
20812
+ if (checkpointAttached && nextSessionId === checkpointSessionId && sessionsDir === checkpointSessionsDir) {
20813
+ return;
20814
+ }
20815
+ let detachFailed = false;
20816
+ let detachError;
20817
+ try {
20818
+ await detachCurrentCheckpoint();
20819
+ } catch (error2) {
20820
+ detachFailed = true;
20821
+ detachError = error2;
20822
+ }
20823
+ const nextDetach = attachCheckpoint(nextSessionId, sessionsDir);
20824
+ checkpointSessionId = nextSessionId;
20825
+ checkpointSessionsDir = sessionsDir;
20826
+ detachCurrent = nextDetach;
20827
+ checkpointAttached = true;
20828
+ if (detachFailed) throw detachError;
20829
+ });
20830
+ transitionTail = transition.catch(() => void 0);
20831
+ return transition;
20832
+ };
20833
+ return {
20834
+ rebind,
20835
+ detach: async () => {
20836
+ await transitionTail;
20837
+ await detachCurrentCheckpoint();
20838
+ }
20839
+ };
20840
+ }
20501
20841
  async function startWebUI(opts = {}) {
20502
20842
  ensureSessionShell();
20503
20843
  const ports = await resolvePorts(opts);
@@ -20565,6 +20905,14 @@ async function startWebUI(opts = {}) {
20565
20905
  } = preContext;
20566
20906
  let sessionStore = preContext.sessionStore;
20567
20907
  let session = preContext.session;
20908
+ const todosCheckpoint = createStandaloneTodosCheckpointLifecycle({
20909
+ state: context.state,
20910
+ sessionsDir: wpaths.projectSessions,
20911
+ sessionId: session.id,
20912
+ events,
20913
+ traceId: context.traceId,
20914
+ warn: (message) => logger.warn(message)
20915
+ });
20568
20916
  let sessionStartedAt = preContext.sessionStartedAt;
20569
20917
  let modeId = preContext.modeId;
20570
20918
  const needsSetup = preContext.needsSetup;
@@ -20691,7 +21039,7 @@ async function startWebUI(opts = {}) {
20691
21039
  if (events.listenerCount("tool.confirm_needed") === 0) {
20692
21040
  throw new Error("No permission confirmation surface is connected");
20693
21041
  }
20694
- const decision = await new Promise((resolve14) => {
21042
+ const decision = await new Promise((resolve15) => {
20695
21043
  events.emit("tool.confirm_needed", {
20696
21044
  sessionId: context.session.id,
20697
21045
  tool: confirmTool,
@@ -20701,7 +21049,7 @@ async function startWebUI(opts = {}) {
20701
21049
  decisionSource: pending.decisionSource,
20702
21050
  riskTier: pending.riskTier,
20703
21051
  boundaryReason: pending.boundaryReason,
20704
- resolve: resolve14
21052
+ resolve: resolve15
20705
21053
  });
20706
21054
  });
20707
21055
  const rule = { tool: "language_package", pattern: pending.suggestedPattern };
@@ -20801,21 +21149,21 @@ async function startWebUI(opts = {}) {
20801
21149
  });
20802
21150
  }
20803
21151
  async function touchProjectEntry(root, workDir) {
20804
- const resolved = path27.resolve(root);
21152
+ const resolved = path28.resolve(root);
20805
21153
  const manifest = await loadManifest(globalConfigPath);
20806
21154
  const now = (/* @__PURE__ */ new Date()).toISOString();
20807
- const existing = manifest.projects.find((p) => path27.resolve(p.root) === resolved);
21155
+ const existing = manifest.projects.find((p) => path28.resolve(p.root) === resolved);
20808
21156
  if (existing) {
20809
21157
  existing.lastSeen = now;
20810
- if (workDir) existing.lastWorkingDir = path27.resolve(workDir);
21158
+ if (workDir) existing.lastWorkingDir = path28.resolve(workDir);
20811
21159
  } else {
20812
21160
  manifest.projects.push({
20813
- name: path27.basename(resolved),
21161
+ name: path28.basename(resolved),
20814
21162
  root: resolved,
20815
21163
  slug: generateProjectSlug(resolved),
20816
21164
  createdAt: now,
20817
21165
  lastSeen: now,
20818
- lastWorkingDir: workDir ? path27.resolve(workDir) : void 0
21166
+ lastWorkingDir: workDir ? path28.resolve(workDir) : void 0
20819
21167
  });
20820
21168
  }
20821
21169
  await saveManifest(manifest, globalConfigPath);
@@ -20916,6 +21264,7 @@ async function startWebUI(opts = {}) {
20916
21264
  const cb = {
20917
21265
  sessionStartPayload,
20918
21266
  claimSession: (sessionId, target) => sessionIdentity.claim(sessionId, target),
21267
+ onBeforeSessionTodosReplaced: todosCheckpoint.rebind,
20919
21268
  onSessionSwapped: async (sessionId, target) => {
20920
21269
  await sessionIdentity.activate(sessionId, target);
20921
21270
  const { hydrateSessionKanban: hydrateSessionKanban2 } = await import("@wrongstack/tools/session-kanban");
@@ -21066,6 +21415,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21066
21415
  ...wssSecondary ? [wssSecondary] : []
21067
21416
  ],
21068
21417
  onShutdown: async () => {
21418
+ await todosCheckpoint.detach();
21069
21419
  await stopHeapWatchdog();
21070
21420
  credentialWatcherClose?.();
21071
21421
  brainMonitor.stop();
@@ -21091,7 +21441,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
21091
21441
  await memoryStore.dispose().catch(
21092
21442
  (err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
21093
21443
  );
21094
- await unregisterInstance(process.pid, path27.dirname(globalConfigPath));
21444
+ await unregisterInstance(process.pid, path28.dirname(globalConfigPath));
21095
21445
  }
21096
21446
  });
21097
21447
  }