@wrongstack/webui-server 0.308.1 → 0.308.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.
package/dist/index.js CHANGED
@@ -12010,6 +12010,7 @@ ${metrics}` : "",
12010
12010
  task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
12011
12011
  "",
12012
12012
  "Work the task end-to-end. Use the kanban tool, not direct file edits, to update this task.",
12013
+ "Stay inside this task: its description and success criteria define the boundary. If you notice problems outside this task while working, do not fix them \u2014 note them in lastResult instead.",
12013
12014
  ...leaseId ? [
12014
12015
  "LEASE CONTRACT:",
12015
12016
  `- Your leaseId is "${leaseId}". Include expectedLeaseId "${leaseId}" in every mark_assignment and heartbeat_assignment call.`,
@@ -12018,7 +12019,7 @@ ${metrics}` : "",
12018
12019
  "On failure the host may call recover_stale; respect its decisions and do not duplicate work."
12019
12020
  ] : [],
12020
12021
  `When you start or finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", and assignmentStatus "running", "completed", or "failed"${leaseId ? `, and expectedLeaseId "${leaseId}"` : ""}. Include lastResult or error when you finish. On managed lifecycle boards, mark_assignment(completed) will automatically advance your card from Running to Review \u2014 a separate verification step is required before the card reaches Done.`,
12021
- "When finished, report what changed, what you verified, and any remaining blockers."
12022
+ "When finished, report what changed, what you verified, any remaining blockers, and any issues you noticed outside this task's scope."
12022
12023
  ].filter(Boolean).join("\n");
12023
12024
  }
12024
12025
  function findTask(tasks, taskId) {
@@ -16042,9 +16043,19 @@ async function handleBrainAsk(ctx, ws, question) {
16042
16043
  risk: "medium",
16043
16044
  fallback: "ask_human"
16044
16045
  });
16046
+ const answerSessionId = ctx.getSessionId?.();
16045
16047
  ctx.send(ws, {
16046
16048
  type: "brain.answer",
16047
- payload: { sessionId: ctx.getSessionId?.(), question: q, decision }
16049
+ // Omit sessionId when there is no session: this is a direct reply to
16050
+ // the asker, not a broadcast, but the client's session gate
16051
+ // (isActiveSessionMessage) is fail-closed on a present-but-empty
16052
+ // sessionId — stamping '' would hide the answer from its own asker
16053
+ // in an embedded host with an unbound agent context.
16054
+ payload: {
16055
+ ...answerSessionId ? { sessionId: answerSessionId } : {},
16056
+ question: q,
16057
+ decision
16058
+ }
16048
16059
  });
16049
16060
  } catch (err) {
16050
16061
  sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
@@ -18530,7 +18541,12 @@ function createProviderHandlers(deps2) {
18530
18541
 
18531
18542
  // src/server/session-handlers.ts
18532
18543
  import { loadTodosCheckpoint } from "@wrongstack/core/storage";
18533
- import { DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveContextWindowPolicy } from "@wrongstack/core/types";
18544
+ import {
18545
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY,
18546
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
18547
+ isContextWindowModeId,
18548
+ resolveContextWindowPolicy
18549
+ } from "@wrongstack/core/types";
18534
18550
  import { repairToolUseAdjacency as repairToolUseAdjacency2, sessionScopedPath } from "@wrongstack/core/utils";
18535
18551
 
18536
18552
  // src/protocol/connection-fsm.ts
@@ -19115,6 +19131,7 @@ var SERVER_WORKSPACE_MESSAGE_TYPES = [
19115
19131
  var SERVER_CONFIGURATION_MESSAGE_TYPES = [
19116
19132
  "auth.oauth.status",
19117
19133
  "codebase.index.server.shutdown_result",
19134
+ "connections.auto_heal_status",
19118
19135
  "connections.health_error",
19119
19136
  "connections.health_result",
19120
19137
  "connections.service_action_result",
@@ -20084,8 +20101,8 @@ function createSessionHandlers(ctx) {
20084
20101
  return;
20085
20102
  }
20086
20103
  const { id } = parsed.value;
20087
- let policy = resolveContextWindowPolicy({}, id);
20088
- if (policy.id !== id) {
20104
+ let policy = resolveContextWindowPolicy({}, id, readSessionWindowTokens(ctx.context));
20105
+ if (!isContextWindowModeId(id) && policy.id !== id) {
20089
20106
  const customModes = (await modeStore()).list().filter((m) => m.custom === true);
20090
20107
  const custom = customModes.find((m) => m.id === id);
20091
20108
  if (!custom) {
@@ -20096,6 +20113,7 @@ function createSessionHandlers(ctx) {
20096
20113
  }
20097
20114
  ctx.context.meta["contextWindowMode"] = policy.id;
20098
20115
  ctx.context.meta["contextWindowPolicy"] = policy;
20116
+ ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY] = true;
20099
20117
  result(ws, true, `Context mode switched to ${policy.id}`);
20100
20118
  broadcastToAll({
20101
20119
  type: "context.mode.changed",
@@ -20159,11 +20177,14 @@ function createSessionHandlers(ctx) {
20159
20177
  }
20160
20178
  const { id } = parsed.value;
20161
20179
  if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
20162
- ctx.context.meta["contextWindowMode"] = DEFAULT_CONTEXT_WINDOW_MODE_ID;
20163
- ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy(
20180
+ const policy = resolveContextWindowPolicy(
20164
20181
  {},
20165
- DEFAULT_CONTEXT_WINDOW_MODE_ID
20182
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
20183
+ readSessionWindowTokens(ctx.context)
20166
20184
  );
20185
+ ctx.context.meta["contextWindowMode"] = policy.id;
20186
+ ctx.context.meta["contextWindowPolicy"] = policy;
20187
+ delete ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY];
20167
20188
  }
20168
20189
  const store = await modeStore();
20169
20190
  const operation = store.remove(id);
@@ -20370,6 +20391,12 @@ function createSessionHandlers(ctx) {
20370
20391
  }
20371
20392
  };
20372
20393
  }
20394
+ function readSessionWindowTokens(context) {
20395
+ const meta = context.meta?.["effectiveMaxContext"];
20396
+ if (typeof meta === "number" && Number.isFinite(meta) && meta > 0) return meta;
20397
+ const cap = context.provider?.capabilities?.maxContext;
20398
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0 ? cap : 0;
20399
+ }
20373
20400
 
20374
20401
  // src/server/embedded-host-adapters.ts
20375
20402
  async function applyEmbeddedModelSwitch(ctx, providerId, modelId) {
@@ -21032,6 +21059,259 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
21032
21059
  return true;
21033
21060
  }
21034
21061
 
21062
+ // src/server/connections/auto-healer.ts
21063
+ var AUTO_HEAL_ENV_FLAG = "WRONGSTACK_AUTO_HEAL_SERVICES";
21064
+ var AUTO_HEAL_DEFAULT_INTERVAL_MS = 3e4;
21065
+ var AUTO_HEAL_DEFAULT_COOLDOWN_MS = 5 * 6e4;
21066
+ var AUTO_HEAL_DEFAULT_MAX_ATTEMPTS = 3;
21067
+ var RESTARTABLE_SERVICE_IDS = /* @__PURE__ */ new Set([
21068
+ "kanban",
21069
+ "sage",
21070
+ "chronicle",
21071
+ "codebase-index",
21072
+ "mailbox"
21073
+ ]);
21074
+ function isAutoHealEnabled() {
21075
+ return process.env[AUTO_HEAL_ENV_FLAG] === "1";
21076
+ }
21077
+ function createAutoHealer(options) {
21078
+ const enabled = options.enabled ?? isAutoHealEnabled();
21079
+ const intervalMs = options.intervalMs ?? AUTO_HEAL_DEFAULT_INTERVAL_MS;
21080
+ const cooldownMs = options.cooldownMs ?? AUTO_HEAL_DEFAULT_COOLDOWN_MS;
21081
+ const maxAttempts = options.maxAttempts ?? AUTO_HEAL_DEFAULT_MAX_ATTEMPTS;
21082
+ const collect = options.collect ?? (() => collectConnectionsHealth({
21083
+ projectRoot: options.projectRoot(),
21084
+ indexDir: options.indexDir(),
21085
+ backend: "standalone"
21086
+ }));
21087
+ const execute = options.execute ?? executeServiceAction;
21088
+ const services = /* @__PURE__ */ new Map();
21089
+ let timer = null;
21090
+ let running = false;
21091
+ let ticking = false;
21092
+ let disposed = false;
21093
+ let inFlightTick = null;
21094
+ let lastTickAt = null;
21095
+ let warnedNoBoundary = false;
21096
+ function stateFor(serviceId) {
21097
+ let state = services.get(serviceId);
21098
+ if (!state) {
21099
+ state = {
21100
+ lastAttemptAt: null,
21101
+ consecutiveFailures: 0,
21102
+ lastSuccess: null,
21103
+ lastMessage: null,
21104
+ inFlight: false,
21105
+ escalated: false
21106
+ };
21107
+ services.set(serviceId, state);
21108
+ }
21109
+ return state;
21110
+ }
21111
+ function snapshot() {
21112
+ return {
21113
+ enabled,
21114
+ running,
21115
+ lastTickAt,
21116
+ services: Object.fromEntries(services)
21117
+ };
21118
+ }
21119
+ function emitStatus(event) {
21120
+ try {
21121
+ options.onStatus?.({ ...event, at: Date.now() });
21122
+ } catch (error2) {
21123
+ options.logger?.warn?.(
21124
+ `[AutoHeal] onStatus hook threw: ${error2 instanceof Error ? error2.message : String(error2)}`
21125
+ );
21126
+ }
21127
+ }
21128
+ async function tick() {
21129
+ if (!enabled || disposed) return snapshot();
21130
+ if (!options.trustBoundary) {
21131
+ if (!warnedNoBoundary) {
21132
+ warnedNoBoundary = true;
21133
+ options.logger?.warn?.(
21134
+ "[AutoHeal] Disabled: no policy authority (trust boundary) is configured."
21135
+ );
21136
+ }
21137
+ return snapshot();
21138
+ }
21139
+ if (ticking) return snapshot();
21140
+ ticking = true;
21141
+ try {
21142
+ const report = await collect();
21143
+ const now = Date.now();
21144
+ const projectRoot = options.projectRoot();
21145
+ const indexDir = options.indexDir();
21146
+ for (const service of report.services) {
21147
+ if (disposed) break;
21148
+ const state = stateFor(service.id);
21149
+ if (service.status !== "error") {
21150
+ state.consecutiveFailures = 0;
21151
+ state.escalated = false;
21152
+ continue;
21153
+ }
21154
+ if (!RESTARTABLE_SERVICE_IDS.has(service.id) || service.control === "none") {
21155
+ continue;
21156
+ }
21157
+ if (state.lastAttemptAt !== null && now - state.lastAttemptAt < cooldownMs) continue;
21158
+ if (state.consecutiveFailures >= maxAttempts) {
21159
+ state.escalated = true;
21160
+ options.logger?.warn?.(
21161
+ `[AutoHeal] ${service.id} left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage ?? "unknown"}`
21162
+ );
21163
+ continue;
21164
+ }
21165
+ if (state.inFlight) continue;
21166
+ const authorization = await authorizeWebUIAction(
21167
+ options.trustBoundary,
21168
+ {
21169
+ capability: "connections.service.restart",
21170
+ subject: { kind: "process", id: `${service.id}@${projectRoot}` },
21171
+ risk: "elevated",
21172
+ cwd: projectRoot,
21173
+ metadata: { transport: "auto-heal", serviceId: service.id, action: "restart" }
21174
+ },
21175
+ options.logger
21176
+ );
21177
+ if (disposed) break;
21178
+ if (!authorization.allowed) {
21179
+ state.lastAttemptAt = now;
21180
+ state.lastMessage = `refused by policy: ${authorization.reason}`;
21181
+ emitStatus({
21182
+ serviceId: service.id,
21183
+ phase: "refused",
21184
+ message: `refused by policy: ${authorization.reason}`,
21185
+ attempt: state.consecutiveFailures + 1
21186
+ });
21187
+ options.logger?.warn?.(
21188
+ `[AutoHeal] ${service.id} restart refused by policy: ${authorization.reason}`
21189
+ );
21190
+ continue;
21191
+ }
21192
+ state.inFlight = true;
21193
+ const attempt = state.consecutiveFailures + 1;
21194
+ emitStatus({
21195
+ serviceId: service.id,
21196
+ phase: "restarting",
21197
+ message: `Auto-restarting ${service.id}`,
21198
+ attempt
21199
+ });
21200
+ try {
21201
+ const result = await execute(service.id, "restart", projectRoot, indexDir);
21202
+ state.consecutiveFailures = result.success ? 0 : state.consecutiveFailures + 1;
21203
+ state.lastSuccess = result.success;
21204
+ state.lastMessage = result.message;
21205
+ emitStatus({
21206
+ serviceId: service.id,
21207
+ phase: result.success ? "restarted" : "failed",
21208
+ message: result.message,
21209
+ attempt
21210
+ });
21211
+ if (!result.success && state.consecutiveFailures >= maxAttempts) {
21212
+ state.escalated = true;
21213
+ emitStatus({
21214
+ serviceId: service.id,
21215
+ phase: "escalated",
21216
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`,
21217
+ attempt
21218
+ });
21219
+ options.logger?.warn?.(
21220
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`
21221
+ );
21222
+ }
21223
+ options.logger?.[result.success ? "info" : "warn"]?.(
21224
+ `[AutoHeal] ${service.id} auto-restart ${result.success ? "succeeded" : "failed"}: ${result.message}`
21225
+ );
21226
+ } catch (error2) {
21227
+ state.consecutiveFailures += 1;
21228
+ state.lastSuccess = false;
21229
+ state.lastMessage = error2 instanceof Error ? error2.message : String(error2);
21230
+ emitStatus({
21231
+ serviceId: service.id,
21232
+ phase: "failed",
21233
+ message: state.lastMessage,
21234
+ attempt
21235
+ });
21236
+ if (state.consecutiveFailures >= maxAttempts) {
21237
+ state.escalated = true;
21238
+ emitStatus({
21239
+ serviceId: service.id,
21240
+ phase: "escalated",
21241
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`,
21242
+ attempt
21243
+ });
21244
+ options.logger?.warn?.(
21245
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`
21246
+ );
21247
+ }
21248
+ options.logger?.warn?.(
21249
+ `[AutoHeal] ${service.id} auto-restart threw: ${state.lastMessage}`
21250
+ );
21251
+ } finally {
21252
+ state.lastAttemptAt = Date.now();
21253
+ state.inFlight = false;
21254
+ }
21255
+ }
21256
+ lastTickAt = Date.now();
21257
+ } catch (error2) {
21258
+ options.logger?.warn?.(
21259
+ `[AutoHeal] health collect failed: ${error2 instanceof Error ? error2.message : String(error2)}`
21260
+ );
21261
+ } finally {
21262
+ ticking = false;
21263
+ }
21264
+ return snapshot();
21265
+ }
21266
+ function runTick() {
21267
+ if (!enabled || disposed || running === false || ticking) return;
21268
+ const pending = tick();
21269
+ const tracked = pending.then(
21270
+ () => void 0,
21271
+ () => void 0
21272
+ );
21273
+ inFlightTick = tracked;
21274
+ void tracked.finally(() => {
21275
+ if (inFlightTick === tracked) inFlightTick = null;
21276
+ });
21277
+ }
21278
+ function stopInternal() {
21279
+ if (timer) {
21280
+ clearInterval(timer);
21281
+ timer = null;
21282
+ }
21283
+ running = false;
21284
+ }
21285
+ return {
21286
+ start() {
21287
+ if (!enabled || running || disposed) return;
21288
+ running = true;
21289
+ runTick();
21290
+ timer = setInterval(runTick, intervalMs);
21291
+ timer.unref?.();
21292
+ },
21293
+ stop: stopInternal,
21294
+ async dispose() {
21295
+ stopInternal();
21296
+ disposed = true;
21297
+ const pending = inFlightTick;
21298
+ if (pending) {
21299
+ await Promise.race([
21300
+ pending,
21301
+ new Promise((resolve20) => {
21302
+ const t = setTimeout(resolve20, 3e4);
21303
+ t.unref?.();
21304
+ })
21305
+ ]);
21306
+ }
21307
+ disposed = true;
21308
+ },
21309
+ tick,
21310
+ getSnapshot: snapshot,
21311
+ isRunning: () => running
21312
+ };
21313
+ }
21314
+
21035
21315
  // src/server/fallback-choice.ts
21036
21316
  function emitFallbackChoice(events, msg) {
21037
21317
  const parsed = validateModelFallbackChoicePayload(msg.payload);
@@ -21721,6 +22001,22 @@ async function handleShellOpen(req, logger, options) {
21721
22001
  function createEmbeddedMessageRouter(deps2) {
21722
22002
  const { opts, send: send2, sendResult: sendResult7 } = deps2;
21723
22003
  const projectRoot = () => opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
22004
+ const autoHealer = createAutoHealer({
22005
+ projectRoot,
22006
+ indexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
22007
+ trustBoundary: deps2.trustBoundary,
22008
+ logger: deps2.logger,
22009
+ onStatus: (event) => deps2.providerCtx.broadcast({
22010
+ type: "connections.auto_heal_status",
22011
+ payload: event
22012
+ })
22013
+ });
22014
+ autoHealer.start();
22015
+ if (deps2.onDispose) {
22016
+ deps2.onDispose(async () => {
22017
+ await autoHealer.dispose();
22018
+ });
22019
+ }
21724
22020
  const terminal = async (ws, message) => {
21725
22021
  await deps2.terminalHandler.handleMessage(ws, message).catch((error2) => {
21726
22022
  const text2 = error2 instanceof Error ? error2.message : String(error2);
@@ -24704,6 +25000,7 @@ import {
24704
25000
  import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
24705
25001
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
24706
25002
  import {
25003
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY as CONTEXT_WINDOW_MODE_PINNED_META_KEY2,
24707
25004
  DEFAULT_TOOLS_CONFIG,
24708
25005
  resolveContextWindowPolicy as resolveContextWindowPolicy2
24709
25006
  } from "@wrongstack/core/types";
@@ -25624,22 +25921,26 @@ async function createAgentServices(input) {
25624
25921
  summarizerModel: config.context?.summarizerModel,
25625
25922
  llmSelector: config.context?.llmSelector
25626
25923
  });
25627
- const initialContextPolicy = resolveContextWindowPolicy2(config.context);
25924
+ let effectiveMaxContext = 0;
25925
+ try {
25926
+ const m = await resolveProviderModelMetadata(
25927
+ modelsRegistry,
25928
+ config.provider,
25929
+ context.model,
25930
+ config.providers?.[config.provider]
25931
+ );
25932
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
25933
+ } catch {
25934
+ }
25935
+ if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
25936
+ if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
25937
+ const initialContextPolicy = resolveContextWindowPolicy2(
25938
+ config.context,
25939
+ void 0,
25940
+ effectiveMaxContext
25941
+ );
25628
25942
  let autoCompactor;
25629
25943
  if (config.context?.autoCompact !== false) {
25630
- let effectiveMaxContext = 0;
25631
- try {
25632
- const m = await resolveProviderModelMetadata(
25633
- modelsRegistry,
25634
- config.provider,
25635
- context.model,
25636
- config.providers?.[config.provider]
25637
- );
25638
- effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
25639
- } catch {
25640
- }
25641
- if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
25642
- if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
25643
25944
  autoCompactor = new AutoCompactionMiddlewareCtor(
25644
25945
  compactor,
25645
25946
  effectiveMaxContext,
@@ -25694,6 +25995,15 @@ async function createAgentServices(input) {
25694
25995
  context.meta["effectiveMaxContext"] = newMaxContext;
25695
25996
  autoCompactor?.setMaxContext(newMaxContext);
25696
25997
  autoCompactor?.setEnabled(config.context?.autoCompact !== false);
25998
+ if (context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY2] !== true) {
25999
+ const policy = resolveContextWindowPolicy2(
26000
+ currentConfig.context ?? {},
26001
+ void 0,
26002
+ newMaxContext
26003
+ );
26004
+ context.meta["contextWindowMode"] = policy.id;
26005
+ context.meta["contextWindowPolicy"] = policy;
26006
+ }
25697
26007
  } else {
25698
26008
  delete context.meta["effectiveMaxContext"];
25699
26009
  autoCompactor?.setEnabled(false);
@@ -26325,8 +26635,22 @@ function createMessageDispatcher(opts) {
26325
26635
  broadcast: (message) => broadcast(state.getClients(), message),
26326
26636
  log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
26327
26637
  });
26638
+ const autoHealer = createAutoHealer({
26639
+ projectRoot: () => state.getProjectRoot(),
26640
+ indexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
26641
+ trustBoundary: deps2.trustBoundary,
26642
+ logger: deps2.logger,
26643
+ onStatus: (event) => broadcast(state.getClients(), {
26644
+ type: "connections.auto_heal_status",
26645
+ payload: event
26646
+ })
26647
+ });
26648
+ autoHealer.start();
26328
26649
  if (opts.onDispose) {
26329
- const dispose = () => kanbanSupervisor.dispose();
26650
+ const dispose = async () => {
26651
+ kanbanSupervisor.dispose();
26652
+ await autoHealer.dispose();
26653
+ };
26330
26654
  opts.onDispose(dispose);
26331
26655
  }
26332
26656
  const kanbanContext = () => ({
@@ -27190,7 +27514,11 @@ async function createPreContextServices(input) {
27190
27514
  model: config.model
27191
27515
  });
27192
27516
  context.meta["promptOnlineAgents"] = onlineAgents;
27193
- const initialContextPolicy = resolveContextWindowPolicy3(config.context);
27517
+ const initialContextPolicy = resolveContextWindowPolicy3(
27518
+ config.context,
27519
+ void 0,
27520
+ provider.capabilities?.maxContext
27521
+ );
27194
27522
  context.meta["contextWindowMode"] = initialContextPolicy.id;
27195
27523
  context.meta["contextWindowPolicy"] = initialContextPolicy;
27196
27524
  context.state.setMeta(
@@ -28035,7 +28363,7 @@ function setupWebuiShutdown(options) {
28035
28363
  onPreShutdown: async () => {
28036
28364
  await options.stopEmptySessionCleanup.dispose();
28037
28365
  const disposeKanban = options.getKanbanSupervisorDispose();
28038
- disposeKanban?.();
28366
+ await disposeKanban?.();
28039
28367
  },
28040
28368
  onShutdown: async () => {
28041
28369
  unregister();
@@ -580,6 +580,7 @@ var SERVER_WORKSPACE_MESSAGE_TYPES = [
580
580
  var SERVER_CONFIGURATION_MESSAGE_TYPES = [
581
581
  "auth.oauth.status",
582
582
  "codebase.index.server.shutdown_result",
583
+ "connections.auto_heal_status",
583
584
  "connections.health_error",
584
585
  "connections.health_result",
585
586
  "connections.service_action_result",
@@ -1,5 +1,5 @@
1
1
  export declare const CLIENT_MESSAGE_TYPES: readonly ["abort", "ping", "user_message", "tool.confirm_result", "topic.advice", "completion.request", "model.switch", "model.refine", "model.fallback_choice", "autonomy.switch", "context.clear", "context.compact", "context.debug", "context.editor.open", "context.editor.validate", "context.editor.apply", "context.mode.create", "context.mode.delete", "context.mode.switch", "context.mode.update", "context.modes.list", "context.repair", "mode.switch", "modes.list", "session.checkpoints", "session.delete", "session.inspect", "session.new", "session.rename", "session.resume", "session.rewind", "session.save", "sessions.list", "side_effects.list", "stats.get", "todo.update", "todos.clear", "todos.get", "todos.remove", "collab.join", "collab.leave", "collab.annotate", "collab.resolve", "collab.request_pause", "collab.resume", "collab.grant_control", "collab.inject_tool", "mailbox.action", "mailbox.agents", "mailbox.clear", "mailbox.compact", "mailbox.messages", "mailbox.purge", "mailbox.send", "files.create", "files.delete", "files.list", "files.move", "files.read", "files.rename", "files.skeleton", "files.tree", "files.write", "git.changes", "git.diff", "git.info", "projects.add", "projects.list", "projects.select", "working_dir.set", "worktree.cleanup", "worktree.diff", "worktree.merge", "worktree.remove", "worktree.scan", "shell.open", "process.kill", "process.killAll", "process.list", "terminal.close", "terminal.create", "terminal.input", "terminal.resize", "codebase.index.server.shutdown", "connections.health", "connections.service_action", "diag.get", "key.add", "key.delete", "key.set_active", "key.update", "prefs.get", "prefs.update", "provider.add", "provider.clear_models", "provider.custom_models.remove", "provider.custom_models.set", "provider.models", "provider.models.search", "provider.probe", "provider.remove", "provider.status.clear", "provider.status.get", "provider.status.retry", "provider.undo_clear", "provider.update", "providers.list", "providers.saved", "tool.disable", "tool.enable", "tools.list", "webui.shutdown", "goal-state.get", "goal.addTask", "goal.assess", "goal.assignTask", "goal.clear", "goal.get", "goal.list", "goal.load", "goal.moveTask", "goal.pause", "goal.resume", "goal.retryTask", "goal.revert", "goal.runTask", "goal.save", "goal.selectPhase", "goal.start", "goal.state", "goal.status", "goal.stop", "goal.taskStatus", "goal.toggleAutonomous", "plan.get", "plan.item.update", "plan.template_use", "task.update", "tasks.get", "sdd.board.cancel_task", "sdd.board.cleanup_worktrees", "sdd.board.delete_task", "sdd.board.destroy", "sdd.board.get", "sdd.board.list", "sdd.board.pause", "sdd.board.reassign", "sdd.board.resume", "sdd.board.retry", "sdd.board.retry_all_failed", "sdd.board.rollback", "sdd.board.set_task_fallbacks", "sdd.board.set_task_model", "sdd.board.set_task_verification", "sdd.board.split_task", "sdd.board.stop", "sdd.run.from_graph", "sdd.run.from_spec", "sdd.run.start", "sdd.spec.approve", "sdd.spec.discard", "sdd.spec.get", "sdd.spec.message", "sdd.spec.rewind", "sdd.spec.start", "specs.get", "specs.list", "specs.taskStatus", "brain.ask", "brain.config.get", "brain.config.set", "brain.risk", "brain.status", "chronicle.facet", "chronicle.facets", "chronicle.graph", "chronicle.metrics", "chronicle.query", "chronicle.status", "config.doctor", "design.list", "design.materialize", "design.set", "design.state", "design.swap", "design.tune", "design.use", "design.verify", "memory.list", "memory.sage.backfillRecoverable", "memory.sage.candidateResolve", "memory.sage.delete", "memory.sage.forFile", "memory.sage.get", "memory.sage.graph", "memory.sage.list", "memory.sage.listCandidates", "memory.sage.listPage", "memory.sage.recover", "memory.sage.remember", "memory.sage.searchBreakdown", "memory.sage.update", "auth.oauth.cancel", "auth.oauth.code", "auth.oauth.start", "mcp.add", "mcp.disable", "mcp.discover", "mcp.enable", "mcp.list", "mcp.prompt.get", "mcp.prompts", "mcp.remove", "mcp.resource.read", "mcp.resources", "mcp.restart", "mcp.sleep", "mcp.update", "mcp.wake", "prompts.content", "prompts.create", "prompts.favorite", "prompts.journal", "prompts.list", "prompts.recent", "prompts.search", "prompts.used", "skills.content", "skills.create", "skills.edit", "skills.export", "skills.install", "skills.list", "skills.uninstall", "skills.update"];
2
- export declare const SERVER_MESSAGE_TYPES: readonly ["error", "log", "pong", "side_effects", "agent.status_changed", "agent.timeline.message", "client.status_update", "chimera.report_available", "compaction.failed", "completion.result", "context.compacted", "context.debug", "context.editor.snapshot", "context.editor.validation", "context.editor.applied", "context.mode.changed", "context.modes.list", "context.repaired", "ctx.max_context", "ctx.pct", "delegate.completed", "delegate.started", "iteration.completed", "iteration.limit_reached", "iteration.started", "model.refine_result", "modes.list", "provider.active_blocked", "provider.error", "provider.fallback", "provider.fallback_pending", "provider.response", "provider.retry", "provider.status_changed", "provider.stream_error", "provider.text_delta", "provider.thinking_delta", "run.result", "session.checkpoints", "session.damaged", "session.end", "session.inspect", "session.rewound", "session.start", "session.stats", "sessions.list", "sessions.status_update", "stats.get", "token.cost_estimate_unavailable", "token.threshold", "tool.confirm_needed", "tool.disabled", "tool.enabled", "tool.executed", "tool.loop_detected", "tool.progress", "tool.started", "topic.advice_result", "tools.list", "trust.persisted", "collab.annotation.added", "collab.annotation.resolved", "collab.event", "collab.injection.granted", "collab.participant.joined", "collab.participant.left", "collab.pause.granted", "collab.pause.released", "collab.state", "mailbox.action_result", "mailbox.agent_registered", "mailbox.agent_deregistered", "mailbox.agents", "mailbox.cleared", "mailbox.compacted", "mailbox.event", "mailbox.messages", "mailbox.sent", "mailbox.purged", "mailbox.received", "subagent.budget_extended", "subagent.event", "checkpoint.written", "codemap.file_event", "codemap.index_updated", "codemap.tool_executed", "codemap.tool_started", "file.saved", "files.created", "files.deleted", "files.list", "files.moved", "files.read", "files.renamed", "files.skeleton_result", "files.tree", "files.tree.changed", "files.written", "git.changes", "git.diff", "git.info", "process.list", "projects.added", "projects.list", "projects.selected", "terminal.exit", "terminal.output", "working_dir.changed", "worktree.cleanup_result", "worktree.diff_result", "worktree.event", "worktree.merge_result", "worktree.orphans", "worktree.state", "auth.oauth.status", "codebase.index.server.shutdown_result", "connections.health_error", "connections.health_result", "connections.service_action_result", "diag.get", "key.operation_result", "model.switch_result", "prefs.updated", "provider.catalog", "provider.models", "provider.models.search_result", "provider.probe", "provider.status.snapshot", "providers.saved", "budget.decision", "budget.threshold_reached", "coordinator.stats", "coordinator.status", "eternal.iteration", "fleet.concurrency_update", "goal-state.updated", "goal.assess.result", "goal.list", "goal.paused", "goal.resumed", "goal.saved", "goal.error", "goal.stopped", "goal.failed", "goal.completed", "goal.cleared", "goal.reverted", "goal.progress", "goal.state", "in_flight.ended", "in_flight.started", "plan.updated", "task.completed", "task.failed", "task.pending", "task.started", "tasks.updated", "todos.cleared", "todos.updated", "kanban.task.activity", "sdd.board.lifecycle_result", "sdd.board.list", "sdd.board.snapshot", "sdd.run.started", "sdd.spec.agent_text", "sdd.spec.error", "sdd.spec.snapshot", "specs.detail", "specs.list", "consensus.vote_cast", "consensus.vote_initiated", "consensus.vote_resolved", "cron.job_fired", "cron.snapshot", "techstack.job.cancelled", "techstack.job.failed", "techstack.job.progress", "techstack.job.started", "techstack.report.delivered", "techstack.report.ready", "techstack.snapshot.updated", "techstack.workspace.completed", "brain.answer", "brain.config", "brain.event", "brain.status", "chronicle.error", "chronicle.facet_result", "chronicle.facets_result", "chronicle.graph_result", "chronicle.metrics_result", "chronicle.query_result", "chronicle.status_result", "config.doctor.result", "design.list", "design.materialize", "design.set", "design.state", "design.swap", "design.tune", "design.use", "design.verify", "memory.event", "memory.list", "memory.sage.backfillRecoverable", "memory.sage.candidateResolve", "memory.sage.delete", "memory.sage.forFile", "memory.sage.get", "memory.sage.graph", "memory.sage.list", "memory.sage.listCandidates", "memory.sage.listPage", "memory.sage.recover", "memory.sage.remember", "memory.sage.searchBreakdown", "memory.sage.update", "mcp.content.error", "mcp.content.selected", "mcp.list", "mcp.operation_result", "mcp.prompts", "mcp.resources", "mcp.server.added", "mcp.server.connected", "mcp.server.disconnected", "mcp.server.discovered", "mcp.server.error", "mcp.server.reconnected", "mcp.server.removed", "mcp.server.sleeping", "mcp.server.updated", "mcp.server.waking", "prompts.content", "prompts.created", "prompts.favorite", "prompts.journal", "prompts.list", "prompts.recent", "prompts.search", "prompts.used", "skills.content", "skills.created", "skills.edited", "skills.exported", "skills.installed", "skills.list", "skills.uninstalled", "skills.updated"];
2
+ export declare const SERVER_MESSAGE_TYPES: readonly ["error", "log", "pong", "side_effects", "agent.status_changed", "agent.timeline.message", "client.status_update", "chimera.report_available", "compaction.failed", "completion.result", "context.compacted", "context.debug", "context.editor.snapshot", "context.editor.validation", "context.editor.applied", "context.mode.changed", "context.modes.list", "context.repaired", "ctx.max_context", "ctx.pct", "delegate.completed", "delegate.started", "iteration.completed", "iteration.limit_reached", "iteration.started", "model.refine_result", "modes.list", "provider.active_blocked", "provider.error", "provider.fallback", "provider.fallback_pending", "provider.response", "provider.retry", "provider.status_changed", "provider.stream_error", "provider.text_delta", "provider.thinking_delta", "run.result", "session.checkpoints", "session.damaged", "session.end", "session.inspect", "session.rewound", "session.start", "session.stats", "sessions.list", "sessions.status_update", "stats.get", "token.cost_estimate_unavailable", "token.threshold", "tool.confirm_needed", "tool.disabled", "tool.enabled", "tool.executed", "tool.loop_detected", "tool.progress", "tool.started", "topic.advice_result", "tools.list", "trust.persisted", "collab.annotation.added", "collab.annotation.resolved", "collab.event", "collab.injection.granted", "collab.participant.joined", "collab.participant.left", "collab.pause.granted", "collab.pause.released", "collab.state", "mailbox.action_result", "mailbox.agent_registered", "mailbox.agent_deregistered", "mailbox.agents", "mailbox.cleared", "mailbox.compacted", "mailbox.event", "mailbox.messages", "mailbox.sent", "mailbox.purged", "mailbox.received", "subagent.budget_extended", "subagent.event", "checkpoint.written", "codemap.file_event", "codemap.index_updated", "codemap.tool_executed", "codemap.tool_started", "file.saved", "files.created", "files.deleted", "files.list", "files.moved", "files.read", "files.renamed", "files.skeleton_result", "files.tree", "files.tree.changed", "files.written", "git.changes", "git.diff", "git.info", "process.list", "projects.added", "projects.list", "projects.selected", "terminal.exit", "terminal.output", "working_dir.changed", "worktree.cleanup_result", "worktree.diff_result", "worktree.event", "worktree.merge_result", "worktree.orphans", "worktree.state", "auth.oauth.status", "codebase.index.server.shutdown_result", "connections.auto_heal_status", "connections.health_error", "connections.health_result", "connections.service_action_result", "diag.get", "key.operation_result", "model.switch_result", "prefs.updated", "provider.catalog", "provider.models", "provider.models.search_result", "provider.probe", "provider.status.snapshot", "providers.saved", "budget.decision", "budget.threshold_reached", "coordinator.stats", "coordinator.status", "eternal.iteration", "fleet.concurrency_update", "goal-state.updated", "goal.assess.result", "goal.list", "goal.paused", "goal.resumed", "goal.saved", "goal.error", "goal.stopped", "goal.failed", "goal.completed", "goal.cleared", "goal.reverted", "goal.progress", "goal.state", "in_flight.ended", "in_flight.started", "plan.updated", "task.completed", "task.failed", "task.pending", "task.started", "tasks.updated", "todos.cleared", "todos.updated", "kanban.task.activity", "sdd.board.lifecycle_result", "sdd.board.list", "sdd.board.snapshot", "sdd.run.started", "sdd.spec.agent_text", "sdd.spec.error", "sdd.spec.snapshot", "specs.detail", "specs.list", "consensus.vote_cast", "consensus.vote_initiated", "consensus.vote_resolved", "cron.job_fired", "cron.snapshot", "techstack.job.cancelled", "techstack.job.failed", "techstack.job.progress", "techstack.job.started", "techstack.report.delivered", "techstack.report.ready", "techstack.snapshot.updated", "techstack.workspace.completed", "brain.answer", "brain.config", "brain.event", "brain.status", "chronicle.error", "chronicle.facet_result", "chronicle.facets_result", "chronicle.graph_result", "chronicle.metrics_result", "chronicle.query_result", "chronicle.status_result", "config.doctor.result", "design.list", "design.materialize", "design.set", "design.state", "design.swap", "design.tune", "design.use", "design.verify", "memory.event", "memory.list", "memory.sage.backfillRecoverable", "memory.sage.candidateResolve", "memory.sage.delete", "memory.sage.forFile", "memory.sage.get", "memory.sage.graph", "memory.sage.list", "memory.sage.listCandidates", "memory.sage.listPage", "memory.sage.recover", "memory.sage.remember", "memory.sage.searchBreakdown", "memory.sage.update", "mcp.content.error", "mcp.content.selected", "mcp.list", "mcp.operation_result", "mcp.prompts", "mcp.resources", "mcp.server.added", "mcp.server.connected", "mcp.server.disconnected", "mcp.server.discovered", "mcp.server.error", "mcp.server.reconnected", "mcp.server.removed", "mcp.server.sleeping", "mcp.server.updated", "mcp.server.waking", "prompts.content", "prompts.created", "prompts.favorite", "prompts.journal", "prompts.list", "prompts.recent", "prompts.search", "prompts.used", "skills.content", "skills.created", "skills.edited", "skills.exported", "skills.installed", "skills.list", "skills.uninstalled", "skills.updated"];
3
3
  export type ExactClientMessageType = (typeof CLIENT_MESSAGE_TYPES)[number];
4
4
  export type ExactServerMessageType = (typeof SERVER_MESSAGE_TYPES)[number];
5
5
  export declare function isRegisteredMessageType(type: string, direction: 'client' | 'server'): boolean;
@@ -1,3 +1,3 @@
1
1
  export declare const SERVER_WORKSPACE_MESSAGE_TYPES: readonly ['checkpoint.written', 'codemap.file_event', 'codemap.index_updated', 'codemap.tool_executed', 'codemap.tool_started', 'file.saved', 'files.created', 'files.deleted', 'files.list', 'files.moved', 'files.read', 'files.renamed', 'files.skeleton_result', 'files.tree', 'files.tree.changed', 'files.written', 'git.changes', 'git.diff', 'git.info', 'process.list', 'projects.added', 'projects.list', 'projects.selected', 'terminal.exit', 'terminal.output', 'working_dir.changed', 'worktree.cleanup_result', 'worktree.diff_result', 'worktree.event', 'worktree.merge_result', 'worktree.orphans', 'worktree.state'];
2
- export declare const SERVER_CONFIGURATION_MESSAGE_TYPES: readonly ['auth.oauth.status', 'codebase.index.server.shutdown_result', 'connections.health_error', 'connections.health_result', 'connections.service_action_result', 'diag.get', 'key.operation_result', 'model.switch_result', 'prefs.updated', 'provider.catalog', 'provider.models', 'provider.models.search_result', 'provider.probe', 'provider.status.snapshot', 'providers.saved'];
2
+ export declare const SERVER_CONFIGURATION_MESSAGE_TYPES: readonly ['auth.oauth.status', 'codebase.index.server.shutdown_result', 'connections.auto_heal_status', 'connections.health_error', 'connections.health_result', 'connections.service_action_result', 'diag.get', 'key.operation_result', 'model.switch_result', 'prefs.updated', 'provider.catalog', 'provider.models', 'provider.models.search_result', 'provider.probe', 'provider.status.snapshot', 'providers.saved'];
3
3
  //# sourceMappingURL=server-workspace.d.ts.map
@@ -0,0 +1,85 @@
1
+ import type { Logger } from '@wrongstack/core/types';
2
+ import type { TrustBoundary } from '@wrongstack/core/security';
3
+ import type { ConnectionHealthService, ConnectionsHealthReport, ServiceActionResult } from './types.js';
4
+ /**
5
+ * Auto-heal watchdog for /connections IPC services.
6
+ *
7
+ * There is deliberately no retry loop in the /connections layer: the health
8
+ * report is a passive, read-only probe and the RotateCcw button drives the
9
+ * only restart path (`connections.service_action` → `executeServiceAction`).
10
+ * This module turns that same machinery into an opt-in server-side watchdog:
11
+ * on an interval it collects the health report and, for any restartable
12
+ * service stuck in `error`, re-runs the exact restart used by the button
13
+ * (shutdown → wait-for-dead → respawn → ping verification).
14
+ *
15
+ * Trigger contract — status `error` only. `offline` / `unavailable` are the
16
+ * normal sleeping states of on-demand daemons and must never be restarted;
17
+ * `degraded` means the service is serving (inline fallback, quarantine,
18
+ * damaged rows) and should also never trigger a restart.
19
+ */
20
+ export declare const AUTO_HEAL_ENV_FLAG = "WRONGSTACK_AUTO_HEAL_SERVICES";
21
+ export declare const AUTO_HEAL_DEFAULT_INTERVAL_MS = 30000;
22
+ export declare const AUTO_HEAL_DEFAULT_COOLDOWN_MS: number;
23
+ export declare const AUTO_HEAL_DEFAULT_MAX_ATTEMPTS = 3;
24
+ export interface AutoHealServiceState {
25
+ lastAttemptAt: number | null;
26
+ consecutiveFailures: number;
27
+ lastSuccess: boolean | null;
28
+ lastMessage: string | null;
29
+ inFlight: boolean;
30
+ escalated: boolean;
31
+ }
32
+ /** UI-facing event emitted at each healer decision point. */
33
+ export type AutoHealStatusPhase = 'restarting' | 'restarted' | 'failed' | 'escalated' | 'refused';
34
+ export interface AutoHealStatusEvent {
35
+ serviceId: ConnectionHealthService['id'] | null;
36
+ phase: AutoHealStatusPhase;
37
+ message: string;
38
+ at: number;
39
+ /** 1-based attempt number within the current failure streak. */
40
+ attempt: number;
41
+ }
42
+ export interface AutoHealSnapshot {
43
+ enabled: boolean;
44
+ running: boolean;
45
+ lastTickAt: number | null;
46
+ services: Record<string, AutoHealServiceState>;
47
+ }
48
+ export interface AutoHealerOptions {
49
+ projectRoot: () => string;
50
+ indexDir: () => string | undefined;
51
+ /** Policy boundary. When absent, auto-heal refuses to act (same rule as the manual WS path). */
52
+ trustBoundary?: TrustBoundary | undefined;
53
+ logger?: Logger | undefined;
54
+ /**
55
+ * UI visibility hook. The wiring layer forwards these to the WebSocket
56
+ * broadcast as `connections.auto_heal_status` so clients can render an
57
+ * "auto-restarting…" state instead of silently flipping statuses.
58
+ */
59
+ onStatus?: ((event: AutoHealStatusEvent) => void) | undefined;
60
+ /** Health collector override (tests). Defaults to the real per-project collector. */
61
+ collect?: () => Promise<ConnectionsHealthReport>;
62
+ /** Restart executor override (tests). Defaults to the button's `executeServiceAction`. */
63
+ execute?: (serviceId: ConnectionHealthService['id'], action: 'restart', projectRoot: string, indexDir: string | undefined) => Promise<ServiceActionResult>;
64
+ /** Enable override. Defaults to the `WRONGSTACK_AUTO_HEAL_SERVICES` env flag. */
65
+ enabled?: boolean;
66
+ intervalMs?: number;
67
+ cooldownMs?: number;
68
+ maxAttempts?: number;
69
+ }
70
+ export interface AutoHealer {
71
+ start(): void;
72
+ stop(): void;
73
+ /**
74
+ * Idempotent teardown: stops the interval and waits for any in-flight tick
75
+ * (including a restart in progress) to finish. No new restart is started
76
+ * after disposal begins. Safe to call multiple times and when never started.
77
+ */
78
+ dispose(): Promise<void>;
79
+ tick(): Promise<AutoHealSnapshot>;
80
+ getSnapshot(): AutoHealSnapshot;
81
+ isRunning(): boolean;
82
+ }
83
+ export declare function isAutoHealEnabled(): boolean;
84
+ export declare function createAutoHealer(options: AutoHealerOptions): AutoHealer;
85
+ //# sourceMappingURL=auto-healer.d.ts.map
@@ -40,6 +40,14 @@ export interface EmbeddedMessageRouterDeps {
40
40
  };
41
41
  currentSessionId: () => string;
42
42
  shutdown: () => void;
43
+ /**
44
+ * Caller-supplied register hook for long-lived disposables created inside
45
+ * the router (the auto-heal watchdog). The router hands its disposer to the
46
+ * caller once during construction; the caller is responsible for invoking
47
+ * it during its own shutdown — the router itself never invokes it. Mirrors
48
+ * `MessageDispatcherOptions.onDispose` in the standalone dispatcher.
49
+ */
50
+ onDispose?: ((disposer: () => void | Promise<void>) => void) | undefined;
43
51
  providerCtx: EmbeddedProviderContext;
44
52
  brainCtx: BrainHandlerContext;
45
53
  introspectionCtx: IntrospectionRouteContext;
@@ -11827,6 +11827,7 @@ ${metrics}` : "",
11827
11827
  task.labels?.length ? `Labels: ${task.labels.join(", ")}` : "",
11828
11828
  "",
11829
11829
  "Work the task end-to-end. Use the kanban tool, not direct file edits, to update this task.",
11830
+ "Stay inside this task: its description and success criteria define the boundary. If you notice problems outside this task while working, do not fix them \u2014 note them in lastResult instead.",
11830
11831
  ...leaseId ? [
11831
11832
  "LEASE CONTRACT:",
11832
11833
  `- Your leaseId is "${leaseId}". Include expectedLeaseId "${leaseId}" in every mark_assignment and heartbeat_assignment call.`,
@@ -11835,7 +11836,7 @@ ${metrics}` : "",
11835
11836
  "On failure the host may call recover_stale; respect its decisions and do not duplicate work."
11836
11837
  ] : [],
11837
11838
  `When you start or finish, call kanban with action "mark_assignment", boardId "${board.id}", taskId "${task.id}", and assignmentStatus "running", "completed", or "failed"${leaseId ? `, and expectedLeaseId "${leaseId}"` : ""}. Include lastResult or error when you finish. On managed lifecycle boards, mark_assignment(completed) will automatically advance your card from Running to Review \u2014 a separate verification step is required before the card reaches Done.`,
11838
- "When finished, report what changed, what you verified, and any remaining blockers."
11839
+ "When finished, report what changed, what you verified, any remaining blockers, and any issues you noticed outside this task's scope."
11839
11840
  ].filter(Boolean).join("\n");
11840
11841
  }
11841
11842
  function findTask(tasks, taskId) {
@@ -15217,9 +15218,19 @@ async function handleBrainAsk(ctx, ws, question) {
15217
15218
  risk: "medium",
15218
15219
  fallback: "ask_human"
15219
15220
  });
15221
+ const answerSessionId = ctx.getSessionId?.();
15220
15222
  ctx.send(ws, {
15221
15223
  type: "brain.answer",
15222
- payload: { sessionId: ctx.getSessionId?.(), question: q, decision }
15224
+ // Omit sessionId when there is no session: this is a direct reply to
15225
+ // the asker, not a broadcast, but the client's session gate
15226
+ // (isActiveSessionMessage) is fail-closed on a present-but-empty
15227
+ // sessionId — stamping '' would hide the answer from its own asker
15228
+ // in an embedded host with an unbound agent context.
15229
+ payload: {
15230
+ ...answerSessionId ? { sessionId: answerSessionId } : {},
15231
+ question: q,
15232
+ decision
15233
+ }
15223
15234
  });
15224
15235
  } catch (err) {
15225
15236
  sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
@@ -17303,7 +17314,12 @@ function createProviderHandlers(deps2) {
17303
17314
 
17304
17315
  // src/server/session-handlers.ts
17305
17316
  import { loadTodosCheckpoint } from "@wrongstack/core/storage";
17306
- import { DEFAULT_CONTEXT_WINDOW_MODE_ID, resolveContextWindowPolicy } from "@wrongstack/core/types";
17317
+ import {
17318
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY,
17319
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
17320
+ isContextWindowModeId,
17321
+ resolveContextWindowPolicy
17322
+ } from "@wrongstack/core/types";
17307
17323
  import { repairToolUseAdjacency as repairToolUseAdjacency2, sessionScopedPath } from "@wrongstack/core/utils";
17308
17324
 
17309
17325
  // src/protocol/client-conversation.ts
@@ -17825,6 +17841,7 @@ var SERVER_WORKSPACE_MESSAGE_TYPES = [
17825
17841
  var SERVER_CONFIGURATION_MESSAGE_TYPES = [
17826
17842
  "auth.oauth.status",
17827
17843
  "codebase.index.server.shutdown_result",
17844
+ "connections.auto_heal_status",
17828
17845
  "connections.health_error",
17829
17846
  "connections.health_result",
17830
17847
  "connections.service_action_result",
@@ -18603,8 +18620,8 @@ function createSessionHandlers(ctx) {
18603
18620
  return;
18604
18621
  }
18605
18622
  const { id } = parsed.value;
18606
- let policy = resolveContextWindowPolicy({}, id);
18607
- if (policy.id !== id) {
18623
+ let policy = resolveContextWindowPolicy({}, id, readSessionWindowTokens(ctx.context));
18624
+ if (!isContextWindowModeId(id) && policy.id !== id) {
18608
18625
  const customModes = (await modeStore()).list().filter((m) => m.custom === true);
18609
18626
  const custom = customModes.find((m) => m.id === id);
18610
18627
  if (!custom) {
@@ -18615,6 +18632,7 @@ function createSessionHandlers(ctx) {
18615
18632
  }
18616
18633
  ctx.context.meta["contextWindowMode"] = policy.id;
18617
18634
  ctx.context.meta["contextWindowPolicy"] = policy;
18635
+ ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY] = true;
18618
18636
  result(ws, true, `Context mode switched to ${policy.id}`);
18619
18637
  broadcastToAll({
18620
18638
  type: "context.mode.changed",
@@ -18678,11 +18696,14 @@ function createSessionHandlers(ctx) {
18678
18696
  }
18679
18697
  const { id } = parsed.value;
18680
18698
  if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
18681
- ctx.context.meta["contextWindowMode"] = DEFAULT_CONTEXT_WINDOW_MODE_ID;
18682
- ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy(
18699
+ const policy = resolveContextWindowPolicy(
18683
18700
  {},
18684
- DEFAULT_CONTEXT_WINDOW_MODE_ID
18701
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
18702
+ readSessionWindowTokens(ctx.context)
18685
18703
  );
18704
+ ctx.context.meta["contextWindowMode"] = policy.id;
18705
+ ctx.context.meta["contextWindowPolicy"] = policy;
18706
+ delete ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY];
18686
18707
  }
18687
18708
  const store = await modeStore();
18688
18709
  const operation = store.remove(id);
@@ -18889,6 +18910,12 @@ function createSessionHandlers(ctx) {
18889
18910
  }
18890
18911
  };
18891
18912
  }
18913
+ function readSessionWindowTokens(context) {
18914
+ const meta = context.meta?.["effectiveMaxContext"];
18915
+ if (typeof meta === "number" && Number.isFinite(meta) && meta > 0) return meta;
18916
+ const cap = context.provider?.capabilities?.maxContext;
18917
+ return typeof cap === "number" && Number.isFinite(cap) && cap > 0 ? cap : 0;
18918
+ }
18892
18919
 
18893
18920
  // src/server/agent-roster-handlers.ts
18894
18921
  import {
@@ -19387,6 +19414,259 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
19387
19414
  return true;
19388
19415
  }
19389
19416
 
19417
+ // src/server/connections/auto-healer.ts
19418
+ var AUTO_HEAL_ENV_FLAG = "WRONGSTACK_AUTO_HEAL_SERVICES";
19419
+ var AUTO_HEAL_DEFAULT_INTERVAL_MS = 3e4;
19420
+ var AUTO_HEAL_DEFAULT_COOLDOWN_MS = 5 * 6e4;
19421
+ var AUTO_HEAL_DEFAULT_MAX_ATTEMPTS = 3;
19422
+ var RESTARTABLE_SERVICE_IDS = /* @__PURE__ */ new Set([
19423
+ "kanban",
19424
+ "sage",
19425
+ "chronicle",
19426
+ "codebase-index",
19427
+ "mailbox"
19428
+ ]);
19429
+ function isAutoHealEnabled() {
19430
+ return process.env[AUTO_HEAL_ENV_FLAG] === "1";
19431
+ }
19432
+ function createAutoHealer(options) {
19433
+ const enabled = options.enabled ?? isAutoHealEnabled();
19434
+ const intervalMs = options.intervalMs ?? AUTO_HEAL_DEFAULT_INTERVAL_MS;
19435
+ const cooldownMs = options.cooldownMs ?? AUTO_HEAL_DEFAULT_COOLDOWN_MS;
19436
+ const maxAttempts = options.maxAttempts ?? AUTO_HEAL_DEFAULT_MAX_ATTEMPTS;
19437
+ const collect = options.collect ?? (() => collectConnectionsHealth({
19438
+ projectRoot: options.projectRoot(),
19439
+ indexDir: options.indexDir(),
19440
+ backend: "standalone"
19441
+ }));
19442
+ const execute = options.execute ?? executeServiceAction;
19443
+ const services = /* @__PURE__ */ new Map();
19444
+ let timer = null;
19445
+ let running = false;
19446
+ let ticking = false;
19447
+ let disposed = false;
19448
+ let inFlightTick = null;
19449
+ let lastTickAt = null;
19450
+ let warnedNoBoundary = false;
19451
+ function stateFor(serviceId) {
19452
+ let state = services.get(serviceId);
19453
+ if (!state) {
19454
+ state = {
19455
+ lastAttemptAt: null,
19456
+ consecutiveFailures: 0,
19457
+ lastSuccess: null,
19458
+ lastMessage: null,
19459
+ inFlight: false,
19460
+ escalated: false
19461
+ };
19462
+ services.set(serviceId, state);
19463
+ }
19464
+ return state;
19465
+ }
19466
+ function snapshot() {
19467
+ return {
19468
+ enabled,
19469
+ running,
19470
+ lastTickAt,
19471
+ services: Object.fromEntries(services)
19472
+ };
19473
+ }
19474
+ function emitStatus(event) {
19475
+ try {
19476
+ options.onStatus?.({ ...event, at: Date.now() });
19477
+ } catch (error2) {
19478
+ options.logger?.warn?.(
19479
+ `[AutoHeal] onStatus hook threw: ${error2 instanceof Error ? error2.message : String(error2)}`
19480
+ );
19481
+ }
19482
+ }
19483
+ async function tick() {
19484
+ if (!enabled || disposed) return snapshot();
19485
+ if (!options.trustBoundary) {
19486
+ if (!warnedNoBoundary) {
19487
+ warnedNoBoundary = true;
19488
+ options.logger?.warn?.(
19489
+ "[AutoHeal] Disabled: no policy authority (trust boundary) is configured."
19490
+ );
19491
+ }
19492
+ return snapshot();
19493
+ }
19494
+ if (ticking) return snapshot();
19495
+ ticking = true;
19496
+ try {
19497
+ const report = await collect();
19498
+ const now = Date.now();
19499
+ const projectRoot = options.projectRoot();
19500
+ const indexDir = options.indexDir();
19501
+ for (const service of report.services) {
19502
+ if (disposed) break;
19503
+ const state = stateFor(service.id);
19504
+ if (service.status !== "error") {
19505
+ state.consecutiveFailures = 0;
19506
+ state.escalated = false;
19507
+ continue;
19508
+ }
19509
+ if (!RESTARTABLE_SERVICE_IDS.has(service.id) || service.control === "none") {
19510
+ continue;
19511
+ }
19512
+ if (state.lastAttemptAt !== null && now - state.lastAttemptAt < cooldownMs) continue;
19513
+ if (state.consecutiveFailures >= maxAttempts) {
19514
+ state.escalated = true;
19515
+ options.logger?.warn?.(
19516
+ `[AutoHeal] ${service.id} left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage ?? "unknown"}`
19517
+ );
19518
+ continue;
19519
+ }
19520
+ if (state.inFlight) continue;
19521
+ const authorization = await authorizeWebUIAction(
19522
+ options.trustBoundary,
19523
+ {
19524
+ capability: "connections.service.restart",
19525
+ subject: { kind: "process", id: `${service.id}@${projectRoot}` },
19526
+ risk: "elevated",
19527
+ cwd: projectRoot,
19528
+ metadata: { transport: "auto-heal", serviceId: service.id, action: "restart" }
19529
+ },
19530
+ options.logger
19531
+ );
19532
+ if (disposed) break;
19533
+ if (!authorization.allowed) {
19534
+ state.lastAttemptAt = now;
19535
+ state.lastMessage = `refused by policy: ${authorization.reason}`;
19536
+ emitStatus({
19537
+ serviceId: service.id,
19538
+ phase: "refused",
19539
+ message: `refused by policy: ${authorization.reason}`,
19540
+ attempt: state.consecutiveFailures + 1
19541
+ });
19542
+ options.logger?.warn?.(
19543
+ `[AutoHeal] ${service.id} restart refused by policy: ${authorization.reason}`
19544
+ );
19545
+ continue;
19546
+ }
19547
+ state.inFlight = true;
19548
+ const attempt = state.consecutiveFailures + 1;
19549
+ emitStatus({
19550
+ serviceId: service.id,
19551
+ phase: "restarting",
19552
+ message: `Auto-restarting ${service.id}`,
19553
+ attempt
19554
+ });
19555
+ try {
19556
+ const result = await execute(service.id, "restart", projectRoot, indexDir);
19557
+ state.consecutiveFailures = result.success ? 0 : state.consecutiveFailures + 1;
19558
+ state.lastSuccess = result.success;
19559
+ state.lastMessage = result.message;
19560
+ emitStatus({
19561
+ serviceId: service.id,
19562
+ phase: result.success ? "restarted" : "failed",
19563
+ message: result.message,
19564
+ attempt
19565
+ });
19566
+ if (!result.success && state.consecutiveFailures >= maxAttempts) {
19567
+ state.escalated = true;
19568
+ emitStatus({
19569
+ serviceId: service.id,
19570
+ phase: "escalated",
19571
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`,
19572
+ attempt
19573
+ });
19574
+ options.logger?.warn?.(
19575
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`
19576
+ );
19577
+ }
19578
+ options.logger?.[result.success ? "info" : "warn"]?.(
19579
+ `[AutoHeal] ${service.id} auto-restart ${result.success ? "succeeded" : "failed"}: ${result.message}`
19580
+ );
19581
+ } catch (error2) {
19582
+ state.consecutiveFailures += 1;
19583
+ state.lastSuccess = false;
19584
+ state.lastMessage = error2 instanceof Error ? error2.message : String(error2);
19585
+ emitStatus({
19586
+ serviceId: service.id,
19587
+ phase: "failed",
19588
+ message: state.lastMessage,
19589
+ attempt
19590
+ });
19591
+ if (state.consecutiveFailures >= maxAttempts) {
19592
+ state.escalated = true;
19593
+ emitStatus({
19594
+ serviceId: service.id,
19595
+ phase: "escalated",
19596
+ message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`,
19597
+ attempt
19598
+ });
19599
+ options.logger?.warn?.(
19600
+ `[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`
19601
+ );
19602
+ }
19603
+ options.logger?.warn?.(
19604
+ `[AutoHeal] ${service.id} auto-restart threw: ${state.lastMessage}`
19605
+ );
19606
+ } finally {
19607
+ state.lastAttemptAt = Date.now();
19608
+ state.inFlight = false;
19609
+ }
19610
+ }
19611
+ lastTickAt = Date.now();
19612
+ } catch (error2) {
19613
+ options.logger?.warn?.(
19614
+ `[AutoHeal] health collect failed: ${error2 instanceof Error ? error2.message : String(error2)}`
19615
+ );
19616
+ } finally {
19617
+ ticking = false;
19618
+ }
19619
+ return snapshot();
19620
+ }
19621
+ function runTick() {
19622
+ if (!enabled || disposed || running === false || ticking) return;
19623
+ const pending = tick();
19624
+ const tracked = pending.then(
19625
+ () => void 0,
19626
+ () => void 0
19627
+ );
19628
+ inFlightTick = tracked;
19629
+ void tracked.finally(() => {
19630
+ if (inFlightTick === tracked) inFlightTick = null;
19631
+ });
19632
+ }
19633
+ function stopInternal() {
19634
+ if (timer) {
19635
+ clearInterval(timer);
19636
+ timer = null;
19637
+ }
19638
+ running = false;
19639
+ }
19640
+ return {
19641
+ start() {
19642
+ if (!enabled || running || disposed) return;
19643
+ running = true;
19644
+ runTick();
19645
+ timer = setInterval(runTick, intervalMs);
19646
+ timer.unref?.();
19647
+ },
19648
+ stop: stopInternal,
19649
+ async dispose() {
19650
+ stopInternal();
19651
+ disposed = true;
19652
+ const pending = inFlightTick;
19653
+ if (pending) {
19654
+ await Promise.race([
19655
+ pending,
19656
+ new Promise((resolve19) => {
19657
+ const t = setTimeout(resolve19, 3e4);
19658
+ t.unref?.();
19659
+ })
19660
+ ]);
19661
+ }
19662
+ disposed = true;
19663
+ },
19664
+ tick,
19665
+ getSnapshot: snapshot,
19666
+ isRunning: () => running
19667
+ };
19668
+ }
19669
+
19390
19670
  // src/server/fallback-choice.ts
19391
19671
  function emitFallbackChoice(events, msg) {
19392
19672
  const parsed = validateModelFallbackChoicePayload(msg.payload);
@@ -22572,6 +22852,7 @@ import {
22572
22852
  import { TOKENS } from "@wrongstack/core/kernel";
22573
22853
  import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
22574
22854
  import {
22855
+ CONTEXT_WINDOW_MODE_PINNED_META_KEY as CONTEXT_WINDOW_MODE_PINNED_META_KEY2,
22575
22856
  DEFAULT_TOOLS_CONFIG,
22576
22857
  resolveContextWindowPolicy as resolveContextWindowPolicy2
22577
22858
  } from "@wrongstack/core/types";
@@ -23492,22 +23773,26 @@ async function createAgentServices(input) {
23492
23773
  summarizerModel: config.context?.summarizerModel,
23493
23774
  llmSelector: config.context?.llmSelector
23494
23775
  });
23495
- const initialContextPolicy = resolveContextWindowPolicy2(config.context);
23776
+ let effectiveMaxContext = 0;
23777
+ try {
23778
+ const m = await resolveProviderModelMetadata(
23779
+ modelsRegistry,
23780
+ config.provider,
23781
+ context.model,
23782
+ config.providers?.[config.provider]
23783
+ );
23784
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
23785
+ } catch {
23786
+ }
23787
+ if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23788
+ if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
23789
+ const initialContextPolicy = resolveContextWindowPolicy2(
23790
+ config.context,
23791
+ void 0,
23792
+ effectiveMaxContext
23793
+ );
23496
23794
  let autoCompactor;
23497
23795
  if (config.context?.autoCompact !== false) {
23498
- let effectiveMaxContext = 0;
23499
- try {
23500
- const m = await resolveProviderModelMetadata(
23501
- modelsRegistry,
23502
- config.provider,
23503
- context.model,
23504
- config.providers?.[config.provider]
23505
- );
23506
- effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
23507
- } catch {
23508
- }
23509
- if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
23510
- if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
23511
23796
  autoCompactor = new AutoCompactionMiddlewareCtor(
23512
23797
  compactor,
23513
23798
  effectiveMaxContext,
@@ -23562,6 +23847,15 @@ async function createAgentServices(input) {
23562
23847
  context.meta["effectiveMaxContext"] = newMaxContext;
23563
23848
  autoCompactor?.setMaxContext(newMaxContext);
23564
23849
  autoCompactor?.setEnabled(config.context?.autoCompact !== false);
23850
+ if (context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY2] !== true) {
23851
+ const policy = resolveContextWindowPolicy2(
23852
+ currentConfig.context ?? {},
23853
+ void 0,
23854
+ newMaxContext
23855
+ );
23856
+ context.meta["contextWindowMode"] = policy.id;
23857
+ context.meta["contextWindowPolicy"] = policy;
23858
+ }
23565
23859
  } else {
23566
23860
  delete context.meta["effectiveMaxContext"];
23567
23861
  autoCompactor?.setEnabled(false);
@@ -24193,8 +24487,22 @@ function createMessageDispatcher(opts) {
24193
24487
  broadcast: (message) => broadcast(state.getClients(), message),
24194
24488
  log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
24195
24489
  });
24490
+ const autoHealer = createAutoHealer({
24491
+ projectRoot: () => state.getProjectRoot(),
24492
+ indexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
24493
+ trustBoundary: deps2.trustBoundary,
24494
+ logger: deps2.logger,
24495
+ onStatus: (event) => broadcast(state.getClients(), {
24496
+ type: "connections.auto_heal_status",
24497
+ payload: event
24498
+ })
24499
+ });
24500
+ autoHealer.start();
24196
24501
  if (opts.onDispose) {
24197
- const dispose = () => kanbanSupervisor.dispose();
24502
+ const dispose = async () => {
24503
+ kanbanSupervisor.dispose();
24504
+ await autoHealer.dispose();
24505
+ };
24198
24506
  opts.onDispose(dispose);
24199
24507
  }
24200
24508
  const kanbanContext = () => ({
@@ -25058,7 +25366,11 @@ async function createPreContextServices(input) {
25058
25366
  model: config.model
25059
25367
  });
25060
25368
  context.meta["promptOnlineAgents"] = onlineAgents;
25061
- const initialContextPolicy = resolveContextWindowPolicy3(config.context);
25369
+ const initialContextPolicy = resolveContextWindowPolicy3(
25370
+ config.context,
25371
+ void 0,
25372
+ provider.capabilities?.maxContext
25373
+ );
25062
25374
  context.meta["contextWindowMode"] = initialContextPolicy.id;
25063
25375
  context.meta["contextWindowPolicy"] = initialContextPolicy;
25064
25376
  context.state.setMeta(
@@ -25903,7 +26215,7 @@ function setupWebuiShutdown(options) {
25903
26215
  onPreShutdown: async () => {
25904
26216
  await options.stopEmptySessionCleanup.dispose();
25905
26217
  const disposeKanban = options.getKanbanSupervisorDispose();
25906
- disposeKanban?.();
26218
+ await disposeKanban?.();
25907
26219
  },
25908
26220
  onShutdown: async () => {
25909
26221
  unregister();
@@ -12,7 +12,7 @@ export declare function setupWebuiShutdown(options: {
12
12
  stopEmptySessionCleanup: {
13
13
  dispose: () => Promise<void>;
14
14
  };
15
- getKanbanSupervisorDispose: () => (() => void) | null;
15
+ getKanbanSupervisorDispose: () => (() => void | Promise<void>) | null;
16
16
  todosCheckpoint: {
17
17
  detach: () => Promise<void>;
18
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/webui-server",
3
- "version": "0.308.1",
3
+ "version": "0.308.4",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack WebUI HTTP/WebSocket server module — extracted from @wrongstack/webui in PR #243/244 to remove the CLI -> @wrongstack/webui/server cross-package edge (audit §3.1.1). Pure backend: HTTP routes, WebSocket handlers, MCP tool wrappers, HTML serving. The web frontend lives in @wrongstack/webui; this package is the standalone server it can run on.",
6
6
  "keywords": [
@@ -40,17 +40,17 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "ws": "^8.21.3",
43
- "@wrongstack/core": "0.308.1",
44
- "@wrongstack/requirement-intake": "0.308.1",
45
- "@wrongstack/kanban": "0.308.1",
46
- "@wrongstack/sdd": "0.308.1",
47
- "@wrongstack/mcp": "0.308.1",
48
- "@wrongstack/providers": "0.308.1",
49
- "@wrongstack/runtime": "0.308.1",
50
- "@wrongstack/techstack": "0.308.1",
51
- "@wrongstack/sage": "0.308.1",
52
- "@wrongstack/tools": "0.308.1",
53
- "@wrongstack/vector-memory": "0.308.1"
43
+ "@wrongstack/requirement-intake": "0.308.4",
44
+ "@wrongstack/core": "0.308.4",
45
+ "@wrongstack/mcp": "0.308.4",
46
+ "@wrongstack/kanban": "0.308.4",
47
+ "@wrongstack/techstack": "0.308.4",
48
+ "@wrongstack/sage": "0.308.4",
49
+ "@wrongstack/sdd": "0.308.4",
50
+ "@wrongstack/runtime": "0.308.4",
51
+ "@wrongstack/vector-memory": "0.308.4",
52
+ "@wrongstack/tools": "0.308.4",
53
+ "@wrongstack/providers": "0.308.4"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "^26.2.0",