@wrongstack/webui 0.3.1 → 0.3.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.
@@ -1,24 +1,13 @@
1
1
  // src/server/index.ts
2
- import * as fs from "fs/promises";
3
- import * as os from "os";
2
+ import * as fs2 from "fs/promises";
4
3
  import * as path from "path";
5
4
  import {
6
5
  Agent,
7
6
  AutoCompactionMiddleware,
8
- Container,
9
7
  Context,
10
- DefaultConfigLoader,
11
- DefaultConfigStore,
12
- DefaultErrorHandler,
13
- DefaultLogger,
14
8
  DefaultMemoryStore,
15
9
  DefaultModeStore,
16
10
  DefaultModelsRegistry,
17
- DefaultPathResolver,
18
- DefaultPermissionPolicy,
19
- DefaultRetryPolicy,
20
- DefaultSecretScrubber,
21
- DefaultSecretVault,
22
11
  DefaultSessionStore,
23
12
  DefaultSkillLoader,
24
13
  DefaultSystemPromptBuilder,
@@ -30,13 +19,29 @@ import {
30
19
  ToolRegistry,
31
20
  atomicWrite,
32
21
  createDefaultPipelines,
33
- migratePlaintextSecrets,
34
- resolveWstackPaths
22
+ DEFAULT_CONTEXT_WINDOW_MODE_ID,
23
+ listContextWindowModes,
24
+ repairToolUseAdjacency,
25
+ resolveContextWindowPolicy
35
26
  } from "@wrongstack/core";
36
27
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
37
28
  import { forgetTool, rememberTool } from "@wrongstack/tools";
38
29
  import { builtinToolsPack } from "@wrongstack/tools/pack";
39
30
  import { WebSocket, WebSocketServer } from "ws";
31
+ import { randomBytes } from "crypto";
32
+ import { createDefaultContainer } from "@wrongstack/runtime";
33
+
34
+ // src/server/boot.ts
35
+ import * as fs from "fs/promises";
36
+ import * as os from "os";
37
+ import {
38
+ DefaultConfigLoader,
39
+ DefaultLogger,
40
+ DefaultPathResolver,
41
+ DefaultSecretVault,
42
+ migratePlaintextSecrets,
43
+ resolveWstackPaths
44
+ } from "@wrongstack/core";
40
45
  async function bootConfig() {
41
46
  const cwd = process.cwd();
42
47
  const pathResolver = new DefaultPathResolver(cwd);
@@ -63,18 +68,13 @@ async function bootConfig() {
63
68
  level: config.log?.level ?? "info",
64
69
  file: wpaths.logFile
65
70
  });
66
- return {
67
- config,
68
- vault,
69
- globalConfigPath: wpaths.globalConfig,
70
- projectRoot,
71
- wpaths,
72
- logger
73
- };
71
+ return { config, vault, globalConfigPath: wpaths.globalConfig, projectRoot, wpaths, logger };
74
72
  }
75
73
  function patchConfig(config, updates) {
76
74
  return Object.freeze({ ...config, ...updates });
77
75
  }
76
+
77
+ // src/server/index.ts
78
78
  async function startWebUI(opts = {}) {
79
79
  const wsPort = opts.wsPort ?? 3457;
80
80
  const wsHost = opts.wsHost ?? "127.0.0.1";
@@ -87,22 +87,8 @@ async function startWebUI(opts = {}) {
87
87
  cacheFile: wpaths.modelsCache,
88
88
  ttlSeconds: 24 * 3600
89
89
  });
90
- const container = new Container();
91
- const configStore = new DefaultConfigStore(config);
92
- container.bind(TOKENS.ConfigStore, () => configStore);
93
- container.bind(TOKENS.Logger, () => logger);
94
- container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
95
- container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
96
- container.bind(TOKENS.ErrorHandler, () => new DefaultErrorHandler());
97
- container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
98
- container.bind(
99
- TOKENS.PermissionPolicy,
100
- () => new DefaultPermissionPolicy({
101
- trustFile: wpaths.projectTrust,
102
- yolo: false,
103
- promptDelegate: void 0
104
- })
105
- );
90
+ const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
91
+ const configStore = container.resolve(TOKENS.ConfigStore);
106
92
  const providerRegistry = new ProviderRegistry();
107
93
  try {
108
94
  const factories = await buildProviderFactoriesFromRegistry({
@@ -191,24 +177,42 @@ async function startWebUI(opts = {}) {
191
177
  projectRoot,
192
178
  model: config.model
193
179
  });
180
+ const initialContextPolicy = resolveContextWindowPolicy(config.context);
181
+ context.meta["contextWindowMode"] = initialContextPolicy.id;
182
+ context.meta["contextWindowPolicy"] = initialContextPolicy;
194
183
  const pipelines = createDefaultPipelines();
195
184
  const compactor = new HybridCompactor({
196
185
  preserveK: config.context?.preserveK ?? 20,
197
186
  eliseThreshold: config.context?.eliseThreshold ?? 0.7
198
187
  });
199
188
  if (config.context?.autoCompact !== false) {
189
+ const effectiveMaxContext = config.context?.effectiveMaxContext ?? provider.capabilities.maxContext;
200
190
  const autoCompactor = new AutoCompactionMiddleware(
201
191
  compactor,
202
- 2e5,
192
+ effectiveMaxContext,
203
193
  (ctx) => {
204
194
  let total = 0;
205
195
  for (const m of ctx.messages) {
206
196
  if (typeof m.content === "string") total += Math.ceil(m.content.length / 4);
197
+ else if (Array.isArray(m.content)) {
198
+ for (const b of m.content) total += Math.ceil(JSON.stringify(b).length / 4);
199
+ }
207
200
  }
208
201
  return total;
209
202
  },
210
- { warn: 0.7, soft: 0.85, hard: 0.95 },
211
- { events }
203
+ {
204
+ warn: initialContextPolicy.thresholds.warn,
205
+ soft: initialContextPolicy.thresholds.soft,
206
+ hard: initialContextPolicy.thresholds.hard
207
+ },
208
+ {
209
+ events,
210
+ aggressiveOn: initialContextPolicy.aggressiveOn,
211
+ policyProvider: (ctx) => {
212
+ const policy = ctx.meta["contextWindowPolicy"];
213
+ return policy && typeof policy === "object" ? policy : initialContextPolicy;
214
+ }
215
+ }
212
216
  );
213
217
  pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
214
218
  }
@@ -250,15 +254,27 @@ async function startWebUI(opts = {}) {
250
254
  cacheReadCost,
251
255
  projectName: path.basename(projectRoot) || projectRoot,
252
256
  cwd: projectRoot,
253
- mode: modeId
257
+ mode: modeId,
258
+ contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
259
+ wsToken
254
260
  };
255
261
  }
262
+ const wsToken = randomBytes(16).toString("hex");
263
+ console.log(`[WebUI] WS auth token: ${wsToken}`);
264
+ const isLoopback = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
256
265
  const verifyClient = (info) => {
257
266
  const origin = info.origin;
258
- if (!origin) return true;
267
+ const url = info.req.url ?? "";
268
+ const tokenMatch = url.match(/[?&]token=([^&]+)/);
269
+ const providedToken = tokenMatch ? tokenMatch[1] : void 0;
270
+ const tokenOk = providedToken === wsToken;
271
+ if (!origin) {
272
+ return tokenOk || wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
273
+ }
259
274
  try {
260
275
  const { hostname } = new URL(origin);
261
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
276
+ if (isLoopback(hostname)) return true;
277
+ return tokenOk;
262
278
  } catch {
263
279
  return false;
264
280
  }
@@ -335,6 +351,16 @@ async function startWebUI(opts = {}) {
335
351
  }
336
352
  });
337
353
  });
354
+ events.on("context.repaired", (e) => {
355
+ broadcast({
356
+ type: "context.repaired",
357
+ payload: {
358
+ removedToolUses: e.removedToolUses,
359
+ removedToolResults: e.removedToolResults,
360
+ removedMessages: e.removedMessages
361
+ }
362
+ });
363
+ });
338
364
  events.on("error", (e) => {
339
365
  broadcast({
340
366
  type: "error",
@@ -535,6 +561,8 @@ async function startWebUI(opts = {}) {
535
561
  type: "context.debug",
536
562
  payload: {
537
563
  total,
564
+ mode: context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID,
565
+ policy: context.meta["contextWindowPolicy"],
538
566
  systemPrompt: sysTokens,
539
567
  tools: { total: toolTokens, count: tools.length, breakdown: toolBreakdown },
540
568
  messages: {
@@ -556,7 +584,8 @@ async function startWebUI(opts = {}) {
556
584
  before: report.before,
557
585
  after: report.after,
558
586
  saved: Math.max(0, report.before - report.after),
559
- reductions: report.reductions
587
+ reductions: report.reductions,
588
+ repaired: report.repaired
560
589
  }
561
590
  });
562
591
  sendResult(
@@ -569,6 +598,63 @@ async function startWebUI(opts = {}) {
569
598
  }
570
599
  break;
571
600
  }
601
+ case "context.repair": {
602
+ const beforeMessages = context.messages.length;
603
+ const repaired = repairToolUseAdjacency(context.messages);
604
+ if (repaired.report.changed) {
605
+ context.state.replaceMessages(repaired.messages);
606
+ }
607
+ const payload = {
608
+ removedToolUses: repaired.report.removedToolUses,
609
+ removedToolResults: repaired.report.removedToolResults,
610
+ removedMessages: repaired.report.removedMessages,
611
+ beforeMessages,
612
+ afterMessages: context.messages.length
613
+ };
614
+ broadcast({ type: "context.repaired", payload });
615
+ const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
616
+ sendResult(
617
+ ws,
618
+ true,
619
+ removed > 0 ? `Context repaired: removed ${removed} orphan protocol item(s)` : "Context repair found no orphan protocol blocks"
620
+ );
621
+ break;
622
+ }
623
+ case "context.modes.list": {
624
+ const active = String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID);
625
+ send(ws, {
626
+ type: "context.modes.list",
627
+ payload: {
628
+ activeId: active,
629
+ modes: listContextWindowModes().map((m) => ({
630
+ id: m.id,
631
+ name: m.name,
632
+ description: m.description,
633
+ isActive: m.id === active,
634
+ thresholds: m.thresholds,
635
+ preserveK: m.preserveK,
636
+ eliseThreshold: m.eliseThreshold
637
+ }))
638
+ }
639
+ });
640
+ break;
641
+ }
642
+ case "context.mode.switch": {
643
+ const { id } = msg.payload;
644
+ const policy = resolveContextWindowPolicy({}, id);
645
+ if (policy.id !== id) {
646
+ sendResult(ws, false, `Unknown context mode "${id}"`);
647
+ break;
648
+ }
649
+ context.meta["contextWindowMode"] = policy.id;
650
+ context.meta["contextWindowPolicy"] = policy;
651
+ sendResult(ws, true, `Context mode switched to ${policy.id}`);
652
+ broadcast({
653
+ type: "context.mode.changed",
654
+ payload: { id: policy.id, name: policy.name, policy }
655
+ });
656
+ break;
657
+ }
572
658
  case "providers.list": {
573
659
  const providers = await modelsRegistry.listProviders();
574
660
  const savedIds = new Set(Object.keys(config.providers ?? {}));
@@ -623,7 +709,7 @@ async function startWebUI(opts = {}) {
623
709
  const newProv = providerRegistry.has(newProvider) ? providerRegistry.create({ ...providerCfg, type: newProvider }) : makeProviderFromConfig(newProvider, providerCfg);
624
710
  context.provider = newProv;
625
711
  try {
626
- const raw = await fs.readFile(globalConfigPath, "utf8");
712
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
627
713
  const parsed = JSON.parse(raw);
628
714
  parsed.provider = newProvider;
629
715
  parsed.model = newModel;
@@ -925,7 +1011,7 @@ async function startWebUI(opts = {}) {
925
1011
  if (depth > 8 || results.length >= 600) return;
926
1012
  let entries = [];
927
1013
  try {
928
- entries = await fs.readdir(dir, { withFileTypes: true });
1014
+ entries = await fs2.readdir(dir, { withFileTypes: true });
929
1015
  } catch {
930
1016
  return;
931
1017
  }
@@ -1062,7 +1148,7 @@ async function startWebUI(opts = {}) {
1062
1148
  }
1063
1149
  async function loadSavedProviders() {
1064
1150
  try {
1065
- const raw = await fs.readFile(globalConfigPath, "utf8");
1151
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
1066
1152
  const parsed = JSON.parse(raw);
1067
1153
  return parsed.providers ?? {};
1068
1154
  } catch {
@@ -1072,7 +1158,7 @@ async function startWebUI(opts = {}) {
1072
1158
  async function saveProviders(providers) {
1073
1159
  let parsed;
1074
1160
  try {
1075
- const raw = await fs.readFile(globalConfigPath, "utf8");
1161
+ const raw = await fs2.readFile(globalConfigPath, "utf8");
1076
1162
  parsed = JSON.parse(raw);
1077
1163
  } catch {
1078
1164
  parsed = {};