@wrongstack/webui-server 0.308.1 → 0.308.2
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 +351 -24
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/registry.d.ts +1 -1
- package/dist/protocol/server-workspace.d.ts +1 -1
- package/dist/server/connections/auto-healer.d.ts +85 -0
- package/dist/server/embedded-message-router.d.ts +8 -0
- package/dist/server/entry.js +335 -24
- package/dist/server/start-webui-shutdown.d.ts +1 -1
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -16042,9 +16042,19 @@ async function handleBrainAsk(ctx, ws, question) {
|
|
|
16042
16042
|
risk: "medium",
|
|
16043
16043
|
fallback: "ask_human"
|
|
16044
16044
|
});
|
|
16045
|
+
const answerSessionId = ctx.getSessionId?.();
|
|
16045
16046
|
ctx.send(ws, {
|
|
16046
16047
|
type: "brain.answer",
|
|
16047
|
-
|
|
16048
|
+
// Omit sessionId when there is no session: this is a direct reply to
|
|
16049
|
+
// the asker, not a broadcast, but the client's session gate
|
|
16050
|
+
// (isActiveSessionMessage) is fail-closed on a present-but-empty
|
|
16051
|
+
// sessionId — stamping '' would hide the answer from its own asker
|
|
16052
|
+
// in an embedded host with an unbound agent context.
|
|
16053
|
+
payload: {
|
|
16054
|
+
...answerSessionId ? { sessionId: answerSessionId } : {},
|
|
16055
|
+
question: q,
|
|
16056
|
+
decision
|
|
16057
|
+
}
|
|
16048
16058
|
});
|
|
16049
16059
|
} catch (err) {
|
|
16050
16060
|
sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
|
|
@@ -18530,7 +18540,12 @@ function createProviderHandlers(deps2) {
|
|
|
18530
18540
|
|
|
18531
18541
|
// src/server/session-handlers.ts
|
|
18532
18542
|
import { loadTodosCheckpoint } from "@wrongstack/core/storage";
|
|
18533
|
-
import {
|
|
18543
|
+
import {
|
|
18544
|
+
CONTEXT_WINDOW_MODE_PINNED_META_KEY,
|
|
18545
|
+
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
18546
|
+
isContextWindowModeId,
|
|
18547
|
+
resolveContextWindowPolicy
|
|
18548
|
+
} from "@wrongstack/core/types";
|
|
18534
18549
|
import { repairToolUseAdjacency as repairToolUseAdjacency2, sessionScopedPath } from "@wrongstack/core/utils";
|
|
18535
18550
|
|
|
18536
18551
|
// src/protocol/connection-fsm.ts
|
|
@@ -19115,6 +19130,7 @@ var SERVER_WORKSPACE_MESSAGE_TYPES = [
|
|
|
19115
19130
|
var SERVER_CONFIGURATION_MESSAGE_TYPES = [
|
|
19116
19131
|
"auth.oauth.status",
|
|
19117
19132
|
"codebase.index.server.shutdown_result",
|
|
19133
|
+
"connections.auto_heal_status",
|
|
19118
19134
|
"connections.health_error",
|
|
19119
19135
|
"connections.health_result",
|
|
19120
19136
|
"connections.service_action_result",
|
|
@@ -20084,8 +20100,8 @@ function createSessionHandlers(ctx) {
|
|
|
20084
20100
|
return;
|
|
20085
20101
|
}
|
|
20086
20102
|
const { id } = parsed.value;
|
|
20087
|
-
let policy = resolveContextWindowPolicy({}, id);
|
|
20088
|
-
if (policy.id !== id) {
|
|
20103
|
+
let policy = resolveContextWindowPolicy({}, id, readSessionWindowTokens(ctx.context));
|
|
20104
|
+
if (!isContextWindowModeId(id) && policy.id !== id) {
|
|
20089
20105
|
const customModes = (await modeStore()).list().filter((m) => m.custom === true);
|
|
20090
20106
|
const custom = customModes.find((m) => m.id === id);
|
|
20091
20107
|
if (!custom) {
|
|
@@ -20096,6 +20112,7 @@ function createSessionHandlers(ctx) {
|
|
|
20096
20112
|
}
|
|
20097
20113
|
ctx.context.meta["contextWindowMode"] = policy.id;
|
|
20098
20114
|
ctx.context.meta["contextWindowPolicy"] = policy;
|
|
20115
|
+
ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY] = true;
|
|
20099
20116
|
result(ws, true, `Context mode switched to ${policy.id}`);
|
|
20100
20117
|
broadcastToAll({
|
|
20101
20118
|
type: "context.mode.changed",
|
|
@@ -20159,11 +20176,14 @@ function createSessionHandlers(ctx) {
|
|
|
20159
20176
|
}
|
|
20160
20177
|
const { id } = parsed.value;
|
|
20161
20178
|
if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
|
|
20162
|
-
|
|
20163
|
-
ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy(
|
|
20179
|
+
const policy = resolveContextWindowPolicy(
|
|
20164
20180
|
{},
|
|
20165
|
-
DEFAULT_CONTEXT_WINDOW_MODE_ID
|
|
20181
|
+
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
20182
|
+
readSessionWindowTokens(ctx.context)
|
|
20166
20183
|
);
|
|
20184
|
+
ctx.context.meta["contextWindowMode"] = policy.id;
|
|
20185
|
+
ctx.context.meta["contextWindowPolicy"] = policy;
|
|
20186
|
+
delete ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY];
|
|
20167
20187
|
}
|
|
20168
20188
|
const store = await modeStore();
|
|
20169
20189
|
const operation = store.remove(id);
|
|
@@ -20370,6 +20390,12 @@ function createSessionHandlers(ctx) {
|
|
|
20370
20390
|
}
|
|
20371
20391
|
};
|
|
20372
20392
|
}
|
|
20393
|
+
function readSessionWindowTokens(context) {
|
|
20394
|
+
const meta = context.meta?.["effectiveMaxContext"];
|
|
20395
|
+
if (typeof meta === "number" && Number.isFinite(meta) && meta > 0) return meta;
|
|
20396
|
+
const cap = context.provider?.capabilities?.maxContext;
|
|
20397
|
+
return typeof cap === "number" && Number.isFinite(cap) && cap > 0 ? cap : 0;
|
|
20398
|
+
}
|
|
20373
20399
|
|
|
20374
20400
|
// src/server/embedded-host-adapters.ts
|
|
20375
20401
|
async function applyEmbeddedModelSwitch(ctx, providerId, modelId) {
|
|
@@ -21032,6 +21058,259 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
21032
21058
|
return true;
|
|
21033
21059
|
}
|
|
21034
21060
|
|
|
21061
|
+
// src/server/connections/auto-healer.ts
|
|
21062
|
+
var AUTO_HEAL_ENV_FLAG = "WRONGSTACK_AUTO_HEAL_SERVICES";
|
|
21063
|
+
var AUTO_HEAL_DEFAULT_INTERVAL_MS = 3e4;
|
|
21064
|
+
var AUTO_HEAL_DEFAULT_COOLDOWN_MS = 5 * 6e4;
|
|
21065
|
+
var AUTO_HEAL_DEFAULT_MAX_ATTEMPTS = 3;
|
|
21066
|
+
var RESTARTABLE_SERVICE_IDS = /* @__PURE__ */ new Set([
|
|
21067
|
+
"kanban",
|
|
21068
|
+
"sage",
|
|
21069
|
+
"chronicle",
|
|
21070
|
+
"codebase-index",
|
|
21071
|
+
"mailbox"
|
|
21072
|
+
]);
|
|
21073
|
+
function isAutoHealEnabled() {
|
|
21074
|
+
return process.env[AUTO_HEAL_ENV_FLAG] === "1";
|
|
21075
|
+
}
|
|
21076
|
+
function createAutoHealer(options) {
|
|
21077
|
+
const enabled = options.enabled ?? isAutoHealEnabled();
|
|
21078
|
+
const intervalMs = options.intervalMs ?? AUTO_HEAL_DEFAULT_INTERVAL_MS;
|
|
21079
|
+
const cooldownMs = options.cooldownMs ?? AUTO_HEAL_DEFAULT_COOLDOWN_MS;
|
|
21080
|
+
const maxAttempts = options.maxAttempts ?? AUTO_HEAL_DEFAULT_MAX_ATTEMPTS;
|
|
21081
|
+
const collect = options.collect ?? (() => collectConnectionsHealth({
|
|
21082
|
+
projectRoot: options.projectRoot(),
|
|
21083
|
+
indexDir: options.indexDir(),
|
|
21084
|
+
backend: "standalone"
|
|
21085
|
+
}));
|
|
21086
|
+
const execute = options.execute ?? executeServiceAction;
|
|
21087
|
+
const services = /* @__PURE__ */ new Map();
|
|
21088
|
+
let timer = null;
|
|
21089
|
+
let running = false;
|
|
21090
|
+
let ticking = false;
|
|
21091
|
+
let disposed = false;
|
|
21092
|
+
let inFlightTick = null;
|
|
21093
|
+
let lastTickAt = null;
|
|
21094
|
+
let warnedNoBoundary = false;
|
|
21095
|
+
function stateFor(serviceId) {
|
|
21096
|
+
let state = services.get(serviceId);
|
|
21097
|
+
if (!state) {
|
|
21098
|
+
state = {
|
|
21099
|
+
lastAttemptAt: null,
|
|
21100
|
+
consecutiveFailures: 0,
|
|
21101
|
+
lastSuccess: null,
|
|
21102
|
+
lastMessage: null,
|
|
21103
|
+
inFlight: false,
|
|
21104
|
+
escalated: false
|
|
21105
|
+
};
|
|
21106
|
+
services.set(serviceId, state);
|
|
21107
|
+
}
|
|
21108
|
+
return state;
|
|
21109
|
+
}
|
|
21110
|
+
function snapshot() {
|
|
21111
|
+
return {
|
|
21112
|
+
enabled,
|
|
21113
|
+
running,
|
|
21114
|
+
lastTickAt,
|
|
21115
|
+
services: Object.fromEntries(services)
|
|
21116
|
+
};
|
|
21117
|
+
}
|
|
21118
|
+
function emitStatus(event) {
|
|
21119
|
+
try {
|
|
21120
|
+
options.onStatus?.({ ...event, at: Date.now() });
|
|
21121
|
+
} catch (error2) {
|
|
21122
|
+
options.logger?.warn?.(
|
|
21123
|
+
`[AutoHeal] onStatus hook threw: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
21124
|
+
);
|
|
21125
|
+
}
|
|
21126
|
+
}
|
|
21127
|
+
async function tick() {
|
|
21128
|
+
if (!enabled || disposed) return snapshot();
|
|
21129
|
+
if (!options.trustBoundary) {
|
|
21130
|
+
if (!warnedNoBoundary) {
|
|
21131
|
+
warnedNoBoundary = true;
|
|
21132
|
+
options.logger?.warn?.(
|
|
21133
|
+
"[AutoHeal] Disabled: no policy authority (trust boundary) is configured."
|
|
21134
|
+
);
|
|
21135
|
+
}
|
|
21136
|
+
return snapshot();
|
|
21137
|
+
}
|
|
21138
|
+
if (ticking) return snapshot();
|
|
21139
|
+
ticking = true;
|
|
21140
|
+
try {
|
|
21141
|
+
const report = await collect();
|
|
21142
|
+
const now = Date.now();
|
|
21143
|
+
const projectRoot = options.projectRoot();
|
|
21144
|
+
const indexDir = options.indexDir();
|
|
21145
|
+
for (const service of report.services) {
|
|
21146
|
+
if (disposed) break;
|
|
21147
|
+
const state = stateFor(service.id);
|
|
21148
|
+
if (service.status !== "error") {
|
|
21149
|
+
state.consecutiveFailures = 0;
|
|
21150
|
+
state.escalated = false;
|
|
21151
|
+
continue;
|
|
21152
|
+
}
|
|
21153
|
+
if (!RESTARTABLE_SERVICE_IDS.has(service.id) || service.control === "none") {
|
|
21154
|
+
continue;
|
|
21155
|
+
}
|
|
21156
|
+
if (state.lastAttemptAt !== null && now - state.lastAttemptAt < cooldownMs) continue;
|
|
21157
|
+
if (state.consecutiveFailures >= maxAttempts) {
|
|
21158
|
+
state.escalated = true;
|
|
21159
|
+
options.logger?.warn?.(
|
|
21160
|
+
`[AutoHeal] ${service.id} left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage ?? "unknown"}`
|
|
21161
|
+
);
|
|
21162
|
+
continue;
|
|
21163
|
+
}
|
|
21164
|
+
if (state.inFlight) continue;
|
|
21165
|
+
const authorization = await authorizeWebUIAction(
|
|
21166
|
+
options.trustBoundary,
|
|
21167
|
+
{
|
|
21168
|
+
capability: "connections.service.restart",
|
|
21169
|
+
subject: { kind: "process", id: `${service.id}@${projectRoot}` },
|
|
21170
|
+
risk: "elevated",
|
|
21171
|
+
cwd: projectRoot,
|
|
21172
|
+
metadata: { transport: "auto-heal", serviceId: service.id, action: "restart" }
|
|
21173
|
+
},
|
|
21174
|
+
options.logger
|
|
21175
|
+
);
|
|
21176
|
+
if (disposed) break;
|
|
21177
|
+
if (!authorization.allowed) {
|
|
21178
|
+
state.lastAttemptAt = now;
|
|
21179
|
+
state.lastMessage = `refused by policy: ${authorization.reason}`;
|
|
21180
|
+
emitStatus({
|
|
21181
|
+
serviceId: service.id,
|
|
21182
|
+
phase: "refused",
|
|
21183
|
+
message: `refused by policy: ${authorization.reason}`,
|
|
21184
|
+
attempt: state.consecutiveFailures + 1
|
|
21185
|
+
});
|
|
21186
|
+
options.logger?.warn?.(
|
|
21187
|
+
`[AutoHeal] ${service.id} restart refused by policy: ${authorization.reason}`
|
|
21188
|
+
);
|
|
21189
|
+
continue;
|
|
21190
|
+
}
|
|
21191
|
+
state.inFlight = true;
|
|
21192
|
+
const attempt = state.consecutiveFailures + 1;
|
|
21193
|
+
emitStatus({
|
|
21194
|
+
serviceId: service.id,
|
|
21195
|
+
phase: "restarting",
|
|
21196
|
+
message: `Auto-restarting ${service.id}`,
|
|
21197
|
+
attempt
|
|
21198
|
+
});
|
|
21199
|
+
try {
|
|
21200
|
+
const result = await execute(service.id, "restart", projectRoot, indexDir);
|
|
21201
|
+
state.consecutiveFailures = result.success ? 0 : state.consecutiveFailures + 1;
|
|
21202
|
+
state.lastSuccess = result.success;
|
|
21203
|
+
state.lastMessage = result.message;
|
|
21204
|
+
emitStatus({
|
|
21205
|
+
serviceId: service.id,
|
|
21206
|
+
phase: result.success ? "restarted" : "failed",
|
|
21207
|
+
message: result.message,
|
|
21208
|
+
attempt
|
|
21209
|
+
});
|
|
21210
|
+
if (!result.success && state.consecutiveFailures >= maxAttempts) {
|
|
21211
|
+
state.escalated = true;
|
|
21212
|
+
emitStatus({
|
|
21213
|
+
serviceId: service.id,
|
|
21214
|
+
phase: "escalated",
|
|
21215
|
+
message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`,
|
|
21216
|
+
attempt
|
|
21217
|
+
});
|
|
21218
|
+
options.logger?.warn?.(
|
|
21219
|
+
`[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`
|
|
21220
|
+
);
|
|
21221
|
+
}
|
|
21222
|
+
options.logger?.[result.success ? "info" : "warn"]?.(
|
|
21223
|
+
`[AutoHeal] ${service.id} auto-restart ${result.success ? "succeeded" : "failed"}: ${result.message}`
|
|
21224
|
+
);
|
|
21225
|
+
} catch (error2) {
|
|
21226
|
+
state.consecutiveFailures += 1;
|
|
21227
|
+
state.lastSuccess = false;
|
|
21228
|
+
state.lastMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
21229
|
+
emitStatus({
|
|
21230
|
+
serviceId: service.id,
|
|
21231
|
+
phase: "failed",
|
|
21232
|
+
message: state.lastMessage,
|
|
21233
|
+
attempt
|
|
21234
|
+
});
|
|
21235
|
+
if (state.consecutiveFailures >= maxAttempts) {
|
|
21236
|
+
state.escalated = true;
|
|
21237
|
+
emitStatus({
|
|
21238
|
+
serviceId: service.id,
|
|
21239
|
+
phase: "escalated",
|
|
21240
|
+
message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`,
|
|
21241
|
+
attempt
|
|
21242
|
+
});
|
|
21243
|
+
options.logger?.warn?.(
|
|
21244
|
+
`[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`
|
|
21245
|
+
);
|
|
21246
|
+
}
|
|
21247
|
+
options.logger?.warn?.(
|
|
21248
|
+
`[AutoHeal] ${service.id} auto-restart threw: ${state.lastMessage}`
|
|
21249
|
+
);
|
|
21250
|
+
} finally {
|
|
21251
|
+
state.lastAttemptAt = Date.now();
|
|
21252
|
+
state.inFlight = false;
|
|
21253
|
+
}
|
|
21254
|
+
}
|
|
21255
|
+
lastTickAt = Date.now();
|
|
21256
|
+
} catch (error2) {
|
|
21257
|
+
options.logger?.warn?.(
|
|
21258
|
+
`[AutoHeal] health collect failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
21259
|
+
);
|
|
21260
|
+
} finally {
|
|
21261
|
+
ticking = false;
|
|
21262
|
+
}
|
|
21263
|
+
return snapshot();
|
|
21264
|
+
}
|
|
21265
|
+
function runTick() {
|
|
21266
|
+
if (!enabled || disposed || running === false || ticking) return;
|
|
21267
|
+
const pending = tick();
|
|
21268
|
+
const tracked = pending.then(
|
|
21269
|
+
() => void 0,
|
|
21270
|
+
() => void 0
|
|
21271
|
+
);
|
|
21272
|
+
inFlightTick = tracked;
|
|
21273
|
+
void tracked.finally(() => {
|
|
21274
|
+
if (inFlightTick === tracked) inFlightTick = null;
|
|
21275
|
+
});
|
|
21276
|
+
}
|
|
21277
|
+
function stopInternal() {
|
|
21278
|
+
if (timer) {
|
|
21279
|
+
clearInterval(timer);
|
|
21280
|
+
timer = null;
|
|
21281
|
+
}
|
|
21282
|
+
running = false;
|
|
21283
|
+
}
|
|
21284
|
+
return {
|
|
21285
|
+
start() {
|
|
21286
|
+
if (!enabled || running || disposed) return;
|
|
21287
|
+
running = true;
|
|
21288
|
+
runTick();
|
|
21289
|
+
timer = setInterval(runTick, intervalMs);
|
|
21290
|
+
timer.unref?.();
|
|
21291
|
+
},
|
|
21292
|
+
stop: stopInternal,
|
|
21293
|
+
async dispose() {
|
|
21294
|
+
stopInternal();
|
|
21295
|
+
disposed = true;
|
|
21296
|
+
const pending = inFlightTick;
|
|
21297
|
+
if (pending) {
|
|
21298
|
+
await Promise.race([
|
|
21299
|
+
pending,
|
|
21300
|
+
new Promise((resolve20) => {
|
|
21301
|
+
const t = setTimeout(resolve20, 3e4);
|
|
21302
|
+
t.unref?.();
|
|
21303
|
+
})
|
|
21304
|
+
]);
|
|
21305
|
+
}
|
|
21306
|
+
disposed = true;
|
|
21307
|
+
},
|
|
21308
|
+
tick,
|
|
21309
|
+
getSnapshot: snapshot,
|
|
21310
|
+
isRunning: () => running
|
|
21311
|
+
};
|
|
21312
|
+
}
|
|
21313
|
+
|
|
21035
21314
|
// src/server/fallback-choice.ts
|
|
21036
21315
|
function emitFallbackChoice(events, msg) {
|
|
21037
21316
|
const parsed = validateModelFallbackChoicePayload(msg.payload);
|
|
@@ -21721,6 +22000,22 @@ async function handleShellOpen(req, logger, options) {
|
|
|
21721
22000
|
function createEmbeddedMessageRouter(deps2) {
|
|
21722
22001
|
const { opts, send: send2, sendResult: sendResult7 } = deps2;
|
|
21723
22002
|
const projectRoot = () => opts.projectRoot ?? opts.agent.ctx.projectRoot ?? "";
|
|
22003
|
+
const autoHealer = createAutoHealer({
|
|
22004
|
+
projectRoot,
|
|
22005
|
+
indexDir: () => typeof opts.agent.ctx.meta["codebaseIndexDir"] === "string" ? opts.agent.ctx.meta["codebaseIndexDir"] : void 0,
|
|
22006
|
+
trustBoundary: deps2.trustBoundary,
|
|
22007
|
+
logger: deps2.logger,
|
|
22008
|
+
onStatus: (event) => deps2.providerCtx.broadcast({
|
|
22009
|
+
type: "connections.auto_heal_status",
|
|
22010
|
+
payload: event
|
|
22011
|
+
})
|
|
22012
|
+
});
|
|
22013
|
+
autoHealer.start();
|
|
22014
|
+
if (deps2.onDispose) {
|
|
22015
|
+
deps2.onDispose(async () => {
|
|
22016
|
+
await autoHealer.dispose();
|
|
22017
|
+
});
|
|
22018
|
+
}
|
|
21724
22019
|
const terminal = async (ws, message) => {
|
|
21725
22020
|
await deps2.terminalHandler.handleMessage(ws, message).catch((error2) => {
|
|
21726
22021
|
const text2 = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -24704,6 +24999,7 @@ import {
|
|
|
24704
24999
|
import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
|
|
24705
25000
|
import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
|
|
24706
25001
|
import {
|
|
25002
|
+
CONTEXT_WINDOW_MODE_PINNED_META_KEY as CONTEXT_WINDOW_MODE_PINNED_META_KEY2,
|
|
24707
25003
|
DEFAULT_TOOLS_CONFIG,
|
|
24708
25004
|
resolveContextWindowPolicy as resolveContextWindowPolicy2
|
|
24709
25005
|
} from "@wrongstack/core/types";
|
|
@@ -25624,22 +25920,26 @@ async function createAgentServices(input) {
|
|
|
25624
25920
|
summarizerModel: config.context?.summarizerModel,
|
|
25625
25921
|
llmSelector: config.context?.llmSelector
|
|
25626
25922
|
});
|
|
25627
|
-
|
|
25923
|
+
let effectiveMaxContext = 0;
|
|
25924
|
+
try {
|
|
25925
|
+
const m = await resolveProviderModelMetadata(
|
|
25926
|
+
modelsRegistry,
|
|
25927
|
+
config.provider,
|
|
25928
|
+
context.model,
|
|
25929
|
+
config.providers?.[config.provider]
|
|
25930
|
+
);
|
|
25931
|
+
effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
|
|
25932
|
+
} catch {
|
|
25933
|
+
}
|
|
25934
|
+
if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
|
|
25935
|
+
if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
|
|
25936
|
+
const initialContextPolicy = resolveContextWindowPolicy2(
|
|
25937
|
+
config.context,
|
|
25938
|
+
void 0,
|
|
25939
|
+
effectiveMaxContext
|
|
25940
|
+
);
|
|
25628
25941
|
let autoCompactor;
|
|
25629
25942
|
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
25943
|
autoCompactor = new AutoCompactionMiddlewareCtor(
|
|
25644
25944
|
compactor,
|
|
25645
25945
|
effectiveMaxContext,
|
|
@@ -25694,6 +25994,15 @@ async function createAgentServices(input) {
|
|
|
25694
25994
|
context.meta["effectiveMaxContext"] = newMaxContext;
|
|
25695
25995
|
autoCompactor?.setMaxContext(newMaxContext);
|
|
25696
25996
|
autoCompactor?.setEnabled(config.context?.autoCompact !== false);
|
|
25997
|
+
if (context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY2] !== true) {
|
|
25998
|
+
const policy = resolveContextWindowPolicy2(
|
|
25999
|
+
currentConfig.context ?? {},
|
|
26000
|
+
void 0,
|
|
26001
|
+
newMaxContext
|
|
26002
|
+
);
|
|
26003
|
+
context.meta["contextWindowMode"] = policy.id;
|
|
26004
|
+
context.meta["contextWindowPolicy"] = policy;
|
|
26005
|
+
}
|
|
25697
26006
|
} else {
|
|
25698
26007
|
delete context.meta["effectiveMaxContext"];
|
|
25699
26008
|
autoCompactor?.setEnabled(false);
|
|
@@ -26325,8 +26634,22 @@ function createMessageDispatcher(opts) {
|
|
|
26325
26634
|
broadcast: (message) => broadcast(state.getClients(), message),
|
|
26326
26635
|
log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
|
|
26327
26636
|
});
|
|
26637
|
+
const autoHealer = createAutoHealer({
|
|
26638
|
+
projectRoot: () => state.getProjectRoot(),
|
|
26639
|
+
indexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
|
|
26640
|
+
trustBoundary: deps2.trustBoundary,
|
|
26641
|
+
logger: deps2.logger,
|
|
26642
|
+
onStatus: (event) => broadcast(state.getClients(), {
|
|
26643
|
+
type: "connections.auto_heal_status",
|
|
26644
|
+
payload: event
|
|
26645
|
+
})
|
|
26646
|
+
});
|
|
26647
|
+
autoHealer.start();
|
|
26328
26648
|
if (opts.onDispose) {
|
|
26329
|
-
const dispose = () =>
|
|
26649
|
+
const dispose = async () => {
|
|
26650
|
+
kanbanSupervisor.dispose();
|
|
26651
|
+
await autoHealer.dispose();
|
|
26652
|
+
};
|
|
26330
26653
|
opts.onDispose(dispose);
|
|
26331
26654
|
}
|
|
26332
26655
|
const kanbanContext = () => ({
|
|
@@ -27190,7 +27513,11 @@ async function createPreContextServices(input) {
|
|
|
27190
27513
|
model: config.model
|
|
27191
27514
|
});
|
|
27192
27515
|
context.meta["promptOnlineAgents"] = onlineAgents;
|
|
27193
|
-
const initialContextPolicy = resolveContextWindowPolicy3(
|
|
27516
|
+
const initialContextPolicy = resolveContextWindowPolicy3(
|
|
27517
|
+
config.context,
|
|
27518
|
+
void 0,
|
|
27519
|
+
provider.capabilities?.maxContext
|
|
27520
|
+
);
|
|
27194
27521
|
context.meta["contextWindowMode"] = initialContextPolicy.id;
|
|
27195
27522
|
context.meta["contextWindowPolicy"] = initialContextPolicy;
|
|
27196
27523
|
context.state.setMeta(
|
|
@@ -28035,7 +28362,7 @@ function setupWebuiShutdown(options) {
|
|
|
28035
28362
|
onPreShutdown: async () => {
|
|
28036
28363
|
await options.stopEmptySessionCleanup.dispose();
|
|
28037
28364
|
const disposeKanban = options.getKanbanSupervisorDispose();
|
|
28038
|
-
disposeKanban?.();
|
|
28365
|
+
await disposeKanban?.();
|
|
28039
28366
|
},
|
|
28040
28367
|
onShutdown: async () => {
|
|
28041
28368
|
unregister();
|
package/dist/protocol/index.js
CHANGED
|
@@ -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;
|
package/dist/server/entry.js
CHANGED
|
@@ -15217,9 +15217,19 @@ async function handleBrainAsk(ctx, ws, question) {
|
|
|
15217
15217
|
risk: "medium",
|
|
15218
15218
|
fallback: "ask_human"
|
|
15219
15219
|
});
|
|
15220
|
+
const answerSessionId = ctx.getSessionId?.();
|
|
15220
15221
|
ctx.send(ws, {
|
|
15221
15222
|
type: "brain.answer",
|
|
15222
|
-
|
|
15223
|
+
// Omit sessionId when there is no session: this is a direct reply to
|
|
15224
|
+
// the asker, not a broadcast, but the client's session gate
|
|
15225
|
+
// (isActiveSessionMessage) is fail-closed on a present-but-empty
|
|
15226
|
+
// sessionId — stamping '' would hide the answer from its own asker
|
|
15227
|
+
// in an embedded host with an unbound agent context.
|
|
15228
|
+
payload: {
|
|
15229
|
+
...answerSessionId ? { sessionId: answerSessionId } : {},
|
|
15230
|
+
question: q,
|
|
15231
|
+
decision
|
|
15232
|
+
}
|
|
15223
15233
|
});
|
|
15224
15234
|
} catch (err) {
|
|
15225
15235
|
sendResult6(ctx, ws, false, `Brain consultation failed: ${toErrorMessage6(err)}`);
|
|
@@ -17303,7 +17313,12 @@ function createProviderHandlers(deps2) {
|
|
|
17303
17313
|
|
|
17304
17314
|
// src/server/session-handlers.ts
|
|
17305
17315
|
import { loadTodosCheckpoint } from "@wrongstack/core/storage";
|
|
17306
|
-
import {
|
|
17316
|
+
import {
|
|
17317
|
+
CONTEXT_WINDOW_MODE_PINNED_META_KEY,
|
|
17318
|
+
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
17319
|
+
isContextWindowModeId,
|
|
17320
|
+
resolveContextWindowPolicy
|
|
17321
|
+
} from "@wrongstack/core/types";
|
|
17307
17322
|
import { repairToolUseAdjacency as repairToolUseAdjacency2, sessionScopedPath } from "@wrongstack/core/utils";
|
|
17308
17323
|
|
|
17309
17324
|
// src/protocol/client-conversation.ts
|
|
@@ -17825,6 +17840,7 @@ var SERVER_WORKSPACE_MESSAGE_TYPES = [
|
|
|
17825
17840
|
var SERVER_CONFIGURATION_MESSAGE_TYPES = [
|
|
17826
17841
|
"auth.oauth.status",
|
|
17827
17842
|
"codebase.index.server.shutdown_result",
|
|
17843
|
+
"connections.auto_heal_status",
|
|
17828
17844
|
"connections.health_error",
|
|
17829
17845
|
"connections.health_result",
|
|
17830
17846
|
"connections.service_action_result",
|
|
@@ -18603,8 +18619,8 @@ function createSessionHandlers(ctx) {
|
|
|
18603
18619
|
return;
|
|
18604
18620
|
}
|
|
18605
18621
|
const { id } = parsed.value;
|
|
18606
|
-
let policy = resolveContextWindowPolicy({}, id);
|
|
18607
|
-
if (policy.id !== id) {
|
|
18622
|
+
let policy = resolveContextWindowPolicy({}, id, readSessionWindowTokens(ctx.context));
|
|
18623
|
+
if (!isContextWindowModeId(id) && policy.id !== id) {
|
|
18608
18624
|
const customModes = (await modeStore()).list().filter((m) => m.custom === true);
|
|
18609
18625
|
const custom = customModes.find((m) => m.id === id);
|
|
18610
18626
|
if (!custom) {
|
|
@@ -18615,6 +18631,7 @@ function createSessionHandlers(ctx) {
|
|
|
18615
18631
|
}
|
|
18616
18632
|
ctx.context.meta["contextWindowMode"] = policy.id;
|
|
18617
18633
|
ctx.context.meta["contextWindowPolicy"] = policy;
|
|
18634
|
+
ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY] = true;
|
|
18618
18635
|
result(ws, true, `Context mode switched to ${policy.id}`);
|
|
18619
18636
|
broadcastToAll({
|
|
18620
18637
|
type: "context.mode.changed",
|
|
@@ -18678,11 +18695,14 @@ function createSessionHandlers(ctx) {
|
|
|
18678
18695
|
}
|
|
18679
18696
|
const { id } = parsed.value;
|
|
18680
18697
|
if (String(ctx.context.meta["contextWindowMode"] ?? "") === id) {
|
|
18681
|
-
|
|
18682
|
-
ctx.context.meta["contextWindowPolicy"] = resolveContextWindowPolicy(
|
|
18698
|
+
const policy = resolveContextWindowPolicy(
|
|
18683
18699
|
{},
|
|
18684
|
-
DEFAULT_CONTEXT_WINDOW_MODE_ID
|
|
18700
|
+
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
18701
|
+
readSessionWindowTokens(ctx.context)
|
|
18685
18702
|
);
|
|
18703
|
+
ctx.context.meta["contextWindowMode"] = policy.id;
|
|
18704
|
+
ctx.context.meta["contextWindowPolicy"] = policy;
|
|
18705
|
+
delete ctx.context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY];
|
|
18686
18706
|
}
|
|
18687
18707
|
const store = await modeStore();
|
|
18688
18708
|
const operation = store.remove(id);
|
|
@@ -18889,6 +18909,12 @@ function createSessionHandlers(ctx) {
|
|
|
18889
18909
|
}
|
|
18890
18910
|
};
|
|
18891
18911
|
}
|
|
18912
|
+
function readSessionWindowTokens(context) {
|
|
18913
|
+
const meta = context.meta?.["effectiveMaxContext"];
|
|
18914
|
+
if (typeof meta === "number" && Number.isFinite(meta) && meta > 0) return meta;
|
|
18915
|
+
const cap = context.provider?.capabilities?.maxContext;
|
|
18916
|
+
return typeof cap === "number" && Number.isFinite(cap) && cap > 0 ? cap : 0;
|
|
18917
|
+
}
|
|
18892
18918
|
|
|
18893
18919
|
// src/server/agent-roster-handlers.ts
|
|
18894
18920
|
import {
|
|
@@ -19387,6 +19413,259 @@ async function handleCodebaseIndexServerControl(ws, message, deps2) {
|
|
|
19387
19413
|
return true;
|
|
19388
19414
|
}
|
|
19389
19415
|
|
|
19416
|
+
// src/server/connections/auto-healer.ts
|
|
19417
|
+
var AUTO_HEAL_ENV_FLAG = "WRONGSTACK_AUTO_HEAL_SERVICES";
|
|
19418
|
+
var AUTO_HEAL_DEFAULT_INTERVAL_MS = 3e4;
|
|
19419
|
+
var AUTO_HEAL_DEFAULT_COOLDOWN_MS = 5 * 6e4;
|
|
19420
|
+
var AUTO_HEAL_DEFAULT_MAX_ATTEMPTS = 3;
|
|
19421
|
+
var RESTARTABLE_SERVICE_IDS = /* @__PURE__ */ new Set([
|
|
19422
|
+
"kanban",
|
|
19423
|
+
"sage",
|
|
19424
|
+
"chronicle",
|
|
19425
|
+
"codebase-index",
|
|
19426
|
+
"mailbox"
|
|
19427
|
+
]);
|
|
19428
|
+
function isAutoHealEnabled() {
|
|
19429
|
+
return process.env[AUTO_HEAL_ENV_FLAG] === "1";
|
|
19430
|
+
}
|
|
19431
|
+
function createAutoHealer(options) {
|
|
19432
|
+
const enabled = options.enabled ?? isAutoHealEnabled();
|
|
19433
|
+
const intervalMs = options.intervalMs ?? AUTO_HEAL_DEFAULT_INTERVAL_MS;
|
|
19434
|
+
const cooldownMs = options.cooldownMs ?? AUTO_HEAL_DEFAULT_COOLDOWN_MS;
|
|
19435
|
+
const maxAttempts = options.maxAttempts ?? AUTO_HEAL_DEFAULT_MAX_ATTEMPTS;
|
|
19436
|
+
const collect = options.collect ?? (() => collectConnectionsHealth({
|
|
19437
|
+
projectRoot: options.projectRoot(),
|
|
19438
|
+
indexDir: options.indexDir(),
|
|
19439
|
+
backend: "standalone"
|
|
19440
|
+
}));
|
|
19441
|
+
const execute = options.execute ?? executeServiceAction;
|
|
19442
|
+
const services = /* @__PURE__ */ new Map();
|
|
19443
|
+
let timer = null;
|
|
19444
|
+
let running = false;
|
|
19445
|
+
let ticking = false;
|
|
19446
|
+
let disposed = false;
|
|
19447
|
+
let inFlightTick = null;
|
|
19448
|
+
let lastTickAt = null;
|
|
19449
|
+
let warnedNoBoundary = false;
|
|
19450
|
+
function stateFor(serviceId) {
|
|
19451
|
+
let state = services.get(serviceId);
|
|
19452
|
+
if (!state) {
|
|
19453
|
+
state = {
|
|
19454
|
+
lastAttemptAt: null,
|
|
19455
|
+
consecutiveFailures: 0,
|
|
19456
|
+
lastSuccess: null,
|
|
19457
|
+
lastMessage: null,
|
|
19458
|
+
inFlight: false,
|
|
19459
|
+
escalated: false
|
|
19460
|
+
};
|
|
19461
|
+
services.set(serviceId, state);
|
|
19462
|
+
}
|
|
19463
|
+
return state;
|
|
19464
|
+
}
|
|
19465
|
+
function snapshot() {
|
|
19466
|
+
return {
|
|
19467
|
+
enabled,
|
|
19468
|
+
running,
|
|
19469
|
+
lastTickAt,
|
|
19470
|
+
services: Object.fromEntries(services)
|
|
19471
|
+
};
|
|
19472
|
+
}
|
|
19473
|
+
function emitStatus(event) {
|
|
19474
|
+
try {
|
|
19475
|
+
options.onStatus?.({ ...event, at: Date.now() });
|
|
19476
|
+
} catch (error2) {
|
|
19477
|
+
options.logger?.warn?.(
|
|
19478
|
+
`[AutoHeal] onStatus hook threw: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
19479
|
+
);
|
|
19480
|
+
}
|
|
19481
|
+
}
|
|
19482
|
+
async function tick() {
|
|
19483
|
+
if (!enabled || disposed) return snapshot();
|
|
19484
|
+
if (!options.trustBoundary) {
|
|
19485
|
+
if (!warnedNoBoundary) {
|
|
19486
|
+
warnedNoBoundary = true;
|
|
19487
|
+
options.logger?.warn?.(
|
|
19488
|
+
"[AutoHeal] Disabled: no policy authority (trust boundary) is configured."
|
|
19489
|
+
);
|
|
19490
|
+
}
|
|
19491
|
+
return snapshot();
|
|
19492
|
+
}
|
|
19493
|
+
if (ticking) return snapshot();
|
|
19494
|
+
ticking = true;
|
|
19495
|
+
try {
|
|
19496
|
+
const report = await collect();
|
|
19497
|
+
const now = Date.now();
|
|
19498
|
+
const projectRoot = options.projectRoot();
|
|
19499
|
+
const indexDir = options.indexDir();
|
|
19500
|
+
for (const service of report.services) {
|
|
19501
|
+
if (disposed) break;
|
|
19502
|
+
const state = stateFor(service.id);
|
|
19503
|
+
if (service.status !== "error") {
|
|
19504
|
+
state.consecutiveFailures = 0;
|
|
19505
|
+
state.escalated = false;
|
|
19506
|
+
continue;
|
|
19507
|
+
}
|
|
19508
|
+
if (!RESTARTABLE_SERVICE_IDS.has(service.id) || service.control === "none") {
|
|
19509
|
+
continue;
|
|
19510
|
+
}
|
|
19511
|
+
if (state.lastAttemptAt !== null && now - state.lastAttemptAt < cooldownMs) continue;
|
|
19512
|
+
if (state.consecutiveFailures >= maxAttempts) {
|
|
19513
|
+
state.escalated = true;
|
|
19514
|
+
options.logger?.warn?.(
|
|
19515
|
+
`[AutoHeal] ${service.id} left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage ?? "unknown"}`
|
|
19516
|
+
);
|
|
19517
|
+
continue;
|
|
19518
|
+
}
|
|
19519
|
+
if (state.inFlight) continue;
|
|
19520
|
+
const authorization = await authorizeWebUIAction(
|
|
19521
|
+
options.trustBoundary,
|
|
19522
|
+
{
|
|
19523
|
+
capability: "connections.service.restart",
|
|
19524
|
+
subject: { kind: "process", id: `${service.id}@${projectRoot}` },
|
|
19525
|
+
risk: "elevated",
|
|
19526
|
+
cwd: projectRoot,
|
|
19527
|
+
metadata: { transport: "auto-heal", serviceId: service.id, action: "restart" }
|
|
19528
|
+
},
|
|
19529
|
+
options.logger
|
|
19530
|
+
);
|
|
19531
|
+
if (disposed) break;
|
|
19532
|
+
if (!authorization.allowed) {
|
|
19533
|
+
state.lastAttemptAt = now;
|
|
19534
|
+
state.lastMessage = `refused by policy: ${authorization.reason}`;
|
|
19535
|
+
emitStatus({
|
|
19536
|
+
serviceId: service.id,
|
|
19537
|
+
phase: "refused",
|
|
19538
|
+
message: `refused by policy: ${authorization.reason}`,
|
|
19539
|
+
attempt: state.consecutiveFailures + 1
|
|
19540
|
+
});
|
|
19541
|
+
options.logger?.warn?.(
|
|
19542
|
+
`[AutoHeal] ${service.id} restart refused by policy: ${authorization.reason}`
|
|
19543
|
+
);
|
|
19544
|
+
continue;
|
|
19545
|
+
}
|
|
19546
|
+
state.inFlight = true;
|
|
19547
|
+
const attempt = state.consecutiveFailures + 1;
|
|
19548
|
+
emitStatus({
|
|
19549
|
+
serviceId: service.id,
|
|
19550
|
+
phase: "restarting",
|
|
19551
|
+
message: `Auto-restarting ${service.id}`,
|
|
19552
|
+
attempt
|
|
19553
|
+
});
|
|
19554
|
+
try {
|
|
19555
|
+
const result = await execute(service.id, "restart", projectRoot, indexDir);
|
|
19556
|
+
state.consecutiveFailures = result.success ? 0 : state.consecutiveFailures + 1;
|
|
19557
|
+
state.lastSuccess = result.success;
|
|
19558
|
+
state.lastMessage = result.message;
|
|
19559
|
+
emitStatus({
|
|
19560
|
+
serviceId: service.id,
|
|
19561
|
+
phase: result.success ? "restarted" : "failed",
|
|
19562
|
+
message: result.message,
|
|
19563
|
+
attempt
|
|
19564
|
+
});
|
|
19565
|
+
if (!result.success && state.consecutiveFailures >= maxAttempts) {
|
|
19566
|
+
state.escalated = true;
|
|
19567
|
+
emitStatus({
|
|
19568
|
+
serviceId: service.id,
|
|
19569
|
+
phase: "escalated",
|
|
19570
|
+
message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`,
|
|
19571
|
+
attempt
|
|
19572
|
+
});
|
|
19573
|
+
options.logger?.warn?.(
|
|
19574
|
+
`[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${result.message}`
|
|
19575
|
+
);
|
|
19576
|
+
}
|
|
19577
|
+
options.logger?.[result.success ? "info" : "warn"]?.(
|
|
19578
|
+
`[AutoHeal] ${service.id} auto-restart ${result.success ? "succeeded" : "failed"}: ${result.message}`
|
|
19579
|
+
);
|
|
19580
|
+
} catch (error2) {
|
|
19581
|
+
state.consecutiveFailures += 1;
|
|
19582
|
+
state.lastSuccess = false;
|
|
19583
|
+
state.lastMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
19584
|
+
emitStatus({
|
|
19585
|
+
serviceId: service.id,
|
|
19586
|
+
phase: "failed",
|
|
19587
|
+
message: state.lastMessage,
|
|
19588
|
+
attempt
|
|
19589
|
+
});
|
|
19590
|
+
if (state.consecutiveFailures >= maxAttempts) {
|
|
19591
|
+
state.escalated = true;
|
|
19592
|
+
emitStatus({
|
|
19593
|
+
serviceId: service.id,
|
|
19594
|
+
phase: "escalated",
|
|
19595
|
+
message: `left to manual intervention after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`,
|
|
19596
|
+
attempt
|
|
19597
|
+
});
|
|
19598
|
+
options.logger?.warn?.(
|
|
19599
|
+
`[AutoHeal] ${service.id} escalated after ${state.consecutiveFailures} failed auto-restart(s): ${state.lastMessage}`
|
|
19600
|
+
);
|
|
19601
|
+
}
|
|
19602
|
+
options.logger?.warn?.(
|
|
19603
|
+
`[AutoHeal] ${service.id} auto-restart threw: ${state.lastMessage}`
|
|
19604
|
+
);
|
|
19605
|
+
} finally {
|
|
19606
|
+
state.lastAttemptAt = Date.now();
|
|
19607
|
+
state.inFlight = false;
|
|
19608
|
+
}
|
|
19609
|
+
}
|
|
19610
|
+
lastTickAt = Date.now();
|
|
19611
|
+
} catch (error2) {
|
|
19612
|
+
options.logger?.warn?.(
|
|
19613
|
+
`[AutoHeal] health collect failed: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
19614
|
+
);
|
|
19615
|
+
} finally {
|
|
19616
|
+
ticking = false;
|
|
19617
|
+
}
|
|
19618
|
+
return snapshot();
|
|
19619
|
+
}
|
|
19620
|
+
function runTick() {
|
|
19621
|
+
if (!enabled || disposed || running === false || ticking) return;
|
|
19622
|
+
const pending = tick();
|
|
19623
|
+
const tracked = pending.then(
|
|
19624
|
+
() => void 0,
|
|
19625
|
+
() => void 0
|
|
19626
|
+
);
|
|
19627
|
+
inFlightTick = tracked;
|
|
19628
|
+
void tracked.finally(() => {
|
|
19629
|
+
if (inFlightTick === tracked) inFlightTick = null;
|
|
19630
|
+
});
|
|
19631
|
+
}
|
|
19632
|
+
function stopInternal() {
|
|
19633
|
+
if (timer) {
|
|
19634
|
+
clearInterval(timer);
|
|
19635
|
+
timer = null;
|
|
19636
|
+
}
|
|
19637
|
+
running = false;
|
|
19638
|
+
}
|
|
19639
|
+
return {
|
|
19640
|
+
start() {
|
|
19641
|
+
if (!enabled || running || disposed) return;
|
|
19642
|
+
running = true;
|
|
19643
|
+
runTick();
|
|
19644
|
+
timer = setInterval(runTick, intervalMs);
|
|
19645
|
+
timer.unref?.();
|
|
19646
|
+
},
|
|
19647
|
+
stop: stopInternal,
|
|
19648
|
+
async dispose() {
|
|
19649
|
+
stopInternal();
|
|
19650
|
+
disposed = true;
|
|
19651
|
+
const pending = inFlightTick;
|
|
19652
|
+
if (pending) {
|
|
19653
|
+
await Promise.race([
|
|
19654
|
+
pending,
|
|
19655
|
+
new Promise((resolve19) => {
|
|
19656
|
+
const t = setTimeout(resolve19, 3e4);
|
|
19657
|
+
t.unref?.();
|
|
19658
|
+
})
|
|
19659
|
+
]);
|
|
19660
|
+
}
|
|
19661
|
+
disposed = true;
|
|
19662
|
+
},
|
|
19663
|
+
tick,
|
|
19664
|
+
getSnapshot: snapshot,
|
|
19665
|
+
isRunning: () => running
|
|
19666
|
+
};
|
|
19667
|
+
}
|
|
19668
|
+
|
|
19390
19669
|
// src/server/fallback-choice.ts
|
|
19391
19670
|
function emitFallbackChoice(events, msg) {
|
|
19392
19671
|
const parsed = validateModelFallbackChoicePayload(msg.payload);
|
|
@@ -22572,6 +22851,7 @@ import {
|
|
|
22572
22851
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
22573
22852
|
import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
|
|
22574
22853
|
import {
|
|
22854
|
+
CONTEXT_WINDOW_MODE_PINNED_META_KEY as CONTEXT_WINDOW_MODE_PINNED_META_KEY2,
|
|
22575
22855
|
DEFAULT_TOOLS_CONFIG,
|
|
22576
22856
|
resolveContextWindowPolicy as resolveContextWindowPolicy2
|
|
22577
22857
|
} from "@wrongstack/core/types";
|
|
@@ -23492,22 +23772,26 @@ async function createAgentServices(input) {
|
|
|
23492
23772
|
summarizerModel: config.context?.summarizerModel,
|
|
23493
23773
|
llmSelector: config.context?.llmSelector
|
|
23494
23774
|
});
|
|
23495
|
-
|
|
23775
|
+
let effectiveMaxContext = 0;
|
|
23776
|
+
try {
|
|
23777
|
+
const m = await resolveProviderModelMetadata(
|
|
23778
|
+
modelsRegistry,
|
|
23779
|
+
config.provider,
|
|
23780
|
+
context.model,
|
|
23781
|
+
config.providers?.[config.provider]
|
|
23782
|
+
);
|
|
23783
|
+
effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
|
|
23784
|
+
} catch {
|
|
23785
|
+
}
|
|
23786
|
+
if (!effectiveMaxContext) effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
|
|
23787
|
+
if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
|
|
23788
|
+
const initialContextPolicy = resolveContextWindowPolicy2(
|
|
23789
|
+
config.context,
|
|
23790
|
+
void 0,
|
|
23791
|
+
effectiveMaxContext
|
|
23792
|
+
);
|
|
23496
23793
|
let autoCompactor;
|
|
23497
23794
|
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
23795
|
autoCompactor = new AutoCompactionMiddlewareCtor(
|
|
23512
23796
|
compactor,
|
|
23513
23797
|
effectiveMaxContext,
|
|
@@ -23562,6 +23846,15 @@ async function createAgentServices(input) {
|
|
|
23562
23846
|
context.meta["effectiveMaxContext"] = newMaxContext;
|
|
23563
23847
|
autoCompactor?.setMaxContext(newMaxContext);
|
|
23564
23848
|
autoCompactor?.setEnabled(config.context?.autoCompact !== false);
|
|
23849
|
+
if (context.meta[CONTEXT_WINDOW_MODE_PINNED_META_KEY2] !== true) {
|
|
23850
|
+
const policy = resolveContextWindowPolicy2(
|
|
23851
|
+
currentConfig.context ?? {},
|
|
23852
|
+
void 0,
|
|
23853
|
+
newMaxContext
|
|
23854
|
+
);
|
|
23855
|
+
context.meta["contextWindowMode"] = policy.id;
|
|
23856
|
+
context.meta["contextWindowPolicy"] = policy;
|
|
23857
|
+
}
|
|
23565
23858
|
} else {
|
|
23566
23859
|
delete context.meta["effectiveMaxContext"];
|
|
23567
23860
|
autoCompactor?.setEnabled(false);
|
|
@@ -24193,8 +24486,22 @@ function createMessageDispatcher(opts) {
|
|
|
24193
24486
|
broadcast: (message) => broadcast(state.getClients(), message),
|
|
24194
24487
|
log: (message) => deps2.logger.warn?.(`[KanbanSupervisor] ${message}`)
|
|
24195
24488
|
});
|
|
24489
|
+
const autoHealer = createAutoHealer({
|
|
24490
|
+
projectRoot: () => state.getProjectRoot(),
|
|
24491
|
+
indexDir: () => typeof deps2.context.meta["codebaseIndexDir"] === "string" ? deps2.context.meta["codebaseIndexDir"] : void 0,
|
|
24492
|
+
trustBoundary: deps2.trustBoundary,
|
|
24493
|
+
logger: deps2.logger,
|
|
24494
|
+
onStatus: (event) => broadcast(state.getClients(), {
|
|
24495
|
+
type: "connections.auto_heal_status",
|
|
24496
|
+
payload: event
|
|
24497
|
+
})
|
|
24498
|
+
});
|
|
24499
|
+
autoHealer.start();
|
|
24196
24500
|
if (opts.onDispose) {
|
|
24197
|
-
const dispose = () =>
|
|
24501
|
+
const dispose = async () => {
|
|
24502
|
+
kanbanSupervisor.dispose();
|
|
24503
|
+
await autoHealer.dispose();
|
|
24504
|
+
};
|
|
24198
24505
|
opts.onDispose(dispose);
|
|
24199
24506
|
}
|
|
24200
24507
|
const kanbanContext = () => ({
|
|
@@ -25058,7 +25365,11 @@ async function createPreContextServices(input) {
|
|
|
25058
25365
|
model: config.model
|
|
25059
25366
|
});
|
|
25060
25367
|
context.meta["promptOnlineAgents"] = onlineAgents;
|
|
25061
|
-
const initialContextPolicy = resolveContextWindowPolicy3(
|
|
25368
|
+
const initialContextPolicy = resolveContextWindowPolicy3(
|
|
25369
|
+
config.context,
|
|
25370
|
+
void 0,
|
|
25371
|
+
provider.capabilities?.maxContext
|
|
25372
|
+
);
|
|
25062
25373
|
context.meta["contextWindowMode"] = initialContextPolicy.id;
|
|
25063
25374
|
context.meta["contextWindowPolicy"] = initialContextPolicy;
|
|
25064
25375
|
context.state.setMeta(
|
|
@@ -25903,7 +26214,7 @@ function setupWebuiShutdown(options) {
|
|
|
25903
26214
|
onPreShutdown: async () => {
|
|
25904
26215
|
await options.stopEmptySessionCleanup.dispose();
|
|
25905
26216
|
const disposeKanban = options.getKanbanSupervisorDispose();
|
|
25906
|
-
disposeKanban?.();
|
|
26217
|
+
await disposeKanban?.();
|
|
25907
26218
|
},
|
|
25908
26219
|
onShutdown: async () => {
|
|
25909
26220
|
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.
|
|
3
|
+
"version": "0.308.2",
|
|
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/
|
|
44
|
-
"@wrongstack/requirement-intake": "0.308.
|
|
45
|
-
"@wrongstack/
|
|
46
|
-
"@wrongstack/
|
|
47
|
-
"@wrongstack/
|
|
48
|
-
"@wrongstack/
|
|
49
|
-
"@wrongstack/
|
|
50
|
-
"@wrongstack/
|
|
51
|
-
"@wrongstack/
|
|
52
|
-
"@wrongstack/
|
|
53
|
-
"@wrongstack/
|
|
43
|
+
"@wrongstack/sage": "0.308.2",
|
|
44
|
+
"@wrongstack/requirement-intake": "0.308.2",
|
|
45
|
+
"@wrongstack/core": "0.308.2",
|
|
46
|
+
"@wrongstack/providers": "0.308.2",
|
|
47
|
+
"@wrongstack/kanban": "0.308.2",
|
|
48
|
+
"@wrongstack/runtime": "0.308.2",
|
|
49
|
+
"@wrongstack/techstack": "0.308.2",
|
|
50
|
+
"@wrongstack/mcp": "0.308.2",
|
|
51
|
+
"@wrongstack/sdd": "0.308.2",
|
|
52
|
+
"@wrongstack/vector-memory": "0.308.2",
|
|
53
|
+
"@wrongstack/tools": "0.308.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^26.2.0",
|