@wrongstack/webui-server 0.296.4 → 0.298.0
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 +2138 -793
- package/dist/protocol/client-conversation.d.ts +1 -1
- package/dist/protocol/index.js +7 -1
- package/dist/protocol/projections.d.ts +5 -1
- package/dist/protocol/registry.d.ts +2 -2
- package/dist/protocol/server-conversation.d.ts +1 -1
- package/dist/server/backend-services.d.ts +8 -2
- package/dist/server/codemap-cache.d.ts +1 -0
- package/dist/server/collaboration-ws-handler.d.ts +2 -2
- package/dist/server/connection-lifecycle.d.ts +13 -0
- package/dist/server/connections-health-route.d.ts +7 -1
- package/dist/server/context-editor.d.ts +1 -2
- package/dist/server/embedded-lifecycle.d.ts +2 -0
- package/dist/server/entry.js +1760 -744
- package/dist/server/frontend-static-serve.d.ts +7 -0
- package/dist/server/goal-ws-handler.d.ts +3 -1
- package/dist/server/governance-runtime.d.ts +30 -0
- package/dist/server/http-server.d.ts +7 -0
- package/dist/server/kanban-supervisor.d.ts +6 -0
- package/dist/server/mailbox-handlers.d.ts +2 -1
- package/dist/server/mailbox-routes.d.ts +1 -0
- package/dist/server/mcp-handlers.d.ts +3 -1
- package/dist/server/memory-diagnostics.d.ts +69 -0
- package/dist/server/memory-handlers.d.ts +3 -3
- package/dist/server/pending-confirms.d.ts +9 -0
- package/dist/server/sdd-board-ws-handler.d.ts +15 -8
- package/dist/server/sdd-wizard-wiring.d.ts +5 -6
- package/dist/server/sdd-wizard-ws-handler.d.ts +18 -6
- package/dist/server/specs-ws-handler.d.ts +1 -0
- package/dist/server/types.d.ts +5 -1
- package/dist/server/worktree-ws-handler.d.ts +30 -2
- package/dist/server/ws-auth.d.ts +24 -0
- package/dist/server/ws-payload-validation.d.ts +26 -0
- package/dist/server/ws-utils.d.ts +12 -1
- package/package.json +12 -11
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -7
- package/dist/protocol/client-conversation.d.ts.map +0 -1
- package/dist/protocol/client-integrations.d.ts.map +0 -1
- package/dist/protocol/client-operations.d.ts.map +0 -1
- package/dist/protocol/client-workspace.d.ts.map +0 -1
- package/dist/protocol/connection-fsm.d.ts.map +0 -1
- package/dist/protocol/decoder.d.ts.map +0 -1
- package/dist/protocol/index.d.ts.map +0 -1
- package/dist/protocol/index.js.map +0 -7
- package/dist/protocol/projections.d.ts.map +0 -1
- package/dist/protocol/registry.d.ts.map +0 -1
- package/dist/protocol/replay-payload.d.ts.map +0 -1
- package/dist/protocol/server-conversation.d.ts.map +0 -1
- package/dist/protocol/server-integrations.d.ts.map +0 -1
- package/dist/protocol/server-operations.d.ts.map +0 -1
- package/dist/protocol/server-workspace.d.ts.map +0 -1
- package/dist/protocol/types.d.ts.map +0 -1
- package/dist/protocol/version.d.ts.map +0 -1
- package/dist/server/agent-roster-handlers.d.ts.map +0 -1
- package/dist/server/agent-roster-routes.d.ts.map +0 -1
- package/dist/server/autonomy-routes.d.ts.map +0 -1
- package/dist/server/backend-services.d.ts.map +0 -1
- package/dist/server/boot.d.ts.map +0 -1
- package/dist/server/brain-handlers.d.ts.map +0 -1
- package/dist/server/brain-routes.d.ts.map +0 -1
- package/dist/server/chronicle-routes.d.ts.map +0 -1
- package/dist/server/client-presence.d.ts.map +0 -1
- package/dist/server/client-transport-routes.d.ts.map +0 -1
- package/dist/server/codebase-index-server-control.d.ts.map +0 -1
- package/dist/server/codebase-indexing.d.ts.map +0 -1
- package/dist/server/codemap-cache.d.ts.map +0 -1
- package/dist/server/codemap-handlers.d.ts.map +0 -1
- package/dist/server/codemap-telemetry.d.ts.map +0 -1
- package/dist/server/collaboration-ws-handler.d.ts.map +0 -1
- package/dist/server/completion-handlers.d.ts.map +0 -1
- package/dist/server/completion-routes.d.ts.map +0 -1
- package/dist/server/config-doctor.d.ts.map +0 -1
- package/dist/server/connection-handler.d.ts.map +0 -1
- package/dist/server/connection-lifecycle.d.ts.map +0 -1
- package/dist/server/connections-health-route.d.ts.map +0 -1
- package/dist/server/content-routes.d.ts.map +0 -1
- package/dist/server/context-editor.d.ts.map +0 -1
- package/dist/server/context-meta.d.ts.map +0 -1
- package/dist/server/conversation-operations.d.ts.map +0 -1
- package/dist/server/conversation-routes.d.ts.map +0 -1
- package/dist/server/custom-context-modes.d.ts.map +0 -1
- package/dist/server/deadcode-handlers.d.ts.map +0 -1
- package/dist/server/design-handlers.d.ts.map +0 -1
- package/dist/server/discover-mailbox-bridge.d.ts.map +0 -1
- package/dist/server/embedded-host-adapters.d.ts.map +0 -1
- package/dist/server/embedded-lifecycle.d.ts.map +0 -1
- package/dist/server/embedded-message-router.d.ts.map +0 -1
- package/dist/server/entry.d.ts.map +0 -1
- package/dist/server/entry.js.map +0 -7
- package/dist/server/eternal-iteration-broadcast.d.ts.map +0 -1
- package/dist/server/file-handlers.d.ts.map +0 -1
- package/dist/server/file-picker.d.ts.map +0 -1
- package/dist/server/frontend-static-serve.d.ts.map +0 -1
- package/dist/server/git-handlers.d.ts.map +0 -1
- package/dist/server/git-process.d.ts.map +0 -1
- package/dist/server/goal-handlers.d.ts.map +0 -1
- package/dist/server/goal-routes.d.ts.map +0 -1
- package/dist/server/goal-snapshot-routes.d.ts.map +0 -1
- package/dist/server/goal-ws-handler.d.ts.map +0 -1
- package/dist/server/handlers/index.d.ts.map +0 -1
- package/dist/server/handlers/worklist-handlers.d.ts.map +0 -1
- package/dist/server/handlers.js.map +0 -7
- package/dist/server/host-routes.d.ts.map +0 -1
- package/dist/server/http-server/analytics-handler.d.ts.map +0 -1
- package/dist/server/http-server/api-handlers.d.ts.map +0 -1
- package/dist/server/http-server.d.ts.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/instance-registry.d.ts.map +0 -1
- package/dist/server/introspection-routes.d.ts.map +0 -1
- package/dist/server/kanban-board-watcher.d.ts.map +0 -1
- package/dist/server/kanban-daemon-subscriber.d.ts.map +0 -1
- package/dist/server/kanban-decomposition-routes.d.ts.map +0 -1
- package/dist/server/kanban-dispatch.d.ts.map +0 -1
- package/dist/server/kanban-host-routes.d.ts.map +0 -1
- package/dist/server/kanban-route-helpers.d.ts.map +0 -1
- package/dist/server/kanban-route-pagination.d.ts.map +0 -1
- package/dist/server/kanban-route-protocol.d.ts.map +0 -1
- package/dist/server/kanban-routes.d.ts.map +0 -1
- package/dist/server/kanban-run-mirror.d.ts.map +0 -1
- package/dist/server/kanban-supervisor.d.ts.map +0 -1
- package/dist/server/kanban-task-routes.d.ts.map +0 -1
- package/dist/server/lifecycle.d.ts.map +0 -1
- package/dist/server/mailbox-handlers.d.ts.map +0 -1
- package/dist/server/mailbox-routes.d.ts.map +0 -1
- package/dist/server/mcp-handlers.d.ts.map +0 -1
- package/dist/server/mcp-routes.d.ts.map +0 -1
- package/dist/server/memory-handlers.d.ts.map +0 -1
- package/dist/server/memory-routes.d.ts.map +0 -1
- package/dist/server/message-dispatcher.d.ts.map +0 -1
- package/dist/server/mode-handlers.d.ts.map +0 -1
- package/dist/server/mode-operations.d.ts.map +0 -1
- package/dist/server/mode-routes.d.ts.map +0 -1
- package/dist/server/model-auto-discovery.d.ts.map +0 -1
- package/dist/server/model-catalog-search.d.ts.map +0 -1
- package/dist/server/model-catalog.d.ts.map +0 -1
- package/dist/server/model-operations.d.ts.map +0 -1
- package/dist/server/network-info.d.ts.map +0 -1
- package/dist/server/open-browser.d.ts.map +0 -1
- package/dist/server/path-containment.d.ts.map +0 -1
- package/dist/server/pending-confirms.d.ts.map +0 -1
- package/dist/server/port-utils.d.ts.map +0 -1
- package/dist/server/pre-context-services.d.ts.map +0 -1
- package/dist/server/pref-helpers.d.ts.map +0 -1
- package/dist/server/prefs-handlers.d.ts.map +0 -1
- package/dist/server/prefs-routes.d.ts.map +0 -1
- package/dist/server/privileged-actions.d.ts.map +0 -1
- package/dist/server/process-handlers.d.ts.map +0 -1
- package/dist/server/process-routes.d.ts.map +0 -1
- package/dist/server/project-handlers.d.ts.map +0 -1
- package/dist/server/project-routes.d.ts.map +0 -1
- package/dist/server/projects-manifest.d.ts.map +0 -1
- package/dist/server/prompts-handlers.d.ts.map +0 -1
- package/dist/server/provider-config-io.d.ts.map +0 -1
- package/dist/server/provider-config-standalone.d.ts.map +0 -1
- package/dist/server/provider-handlers.d.ts.map +0 -1
- package/dist/server/provider-keys.d.ts.map +0 -1
- package/dist/server/provider-routes.d.ts.map +0 -1
- package/dist/server/provider-store.d.ts.map +0 -1
- package/dist/server/route-family-dispatcher.d.ts.map +0 -1
- package/dist/server/routes.d.ts.map +0 -1
- package/dist/server/sdd-board-routes.d.ts.map +0 -1
- package/dist/server/sdd-board-ws-handler.d.ts.map +0 -1
- package/dist/server/sdd-wizard-routes.d.ts.map +0 -1
- package/dist/server/sdd-wizard-wiring.d.ts.map +0 -1
- package/dist/server/sdd-wizard-ws-handler.d.ts.map +0 -1
- package/dist/server/server-runtime.d.ts.map +0 -1
- package/dist/server/session-handlers.d.ts.map +0 -1
- package/dist/server/session-history.d.ts.map +0 -1
- package/dist/server/session-routes.d.ts.map +0 -1
- package/dist/server/setup-event-projection.d.ts.map +0 -1
- package/dist/server/setup-events-core-watchers.d.ts.map +0 -1
- package/dist/server/setup-events-fleet-broadcaster.d.ts.map +0 -1
- package/dist/server/setup-events-provider-handlers.d.ts.map +0 -1
- package/dist/server/setup-events-session-helpers.d.ts.map +0 -1
- package/dist/server/setup-events-status-watcher.d.ts.map +0 -1
- package/dist/server/setup-events-watcher.d.ts.map +0 -1
- package/dist/server/setup-events.d.ts.map +0 -1
- package/dist/server/setup-screen.d.ts.map +0 -1
- package/dist/server/shell-git-routes.d.ts.map +0 -1
- package/dist/server/shell-open.d.ts.map +0 -1
- package/dist/server/skills-handlers.d.ts.map +0 -1
- package/dist/server/specs-routes.d.ts.map +0 -1
- package/dist/server/specs-ws-handler.d.ts.map +0 -1
- package/dist/server/standalone-session-identity.d.ts.map +0 -1
- package/dist/server/start-webui.d.ts.map +0 -1
- package/dist/server/stream-coalescer.d.ts.map +0 -1
- package/dist/server/techstack-handlers.d.ts.map +0 -1
- package/dist/server/terminal-ws-handler.d.ts.map +0 -1
- package/dist/server/token-estimator.d.ts.map +0 -1
- package/dist/server/types.d.ts.map +0 -1
- package/dist/server/usage-cost.d.ts.map +0 -1
- package/dist/server/worklist-routes.d.ts.map +0 -1
- package/dist/server/worktree-routes.d.ts.map +0 -1
- package/dist/server/worktree-ws-handler.d.ts.map +0 -1
- package/dist/server/ws-auth.d.ts.map +0 -1
- package/dist/server/ws-payload-preferences.d.ts.map +0 -1
- package/dist/server/ws-payload-validation.d.ts.map +0 -1
- package/dist/server/ws-utils.d.ts.map +0 -1
- package/dist/server/zip.d.ts.map +0 -1
package/dist/server/entry.js
CHANGED
|
@@ -6,9 +6,13 @@ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/ty
|
|
|
6
6
|
import { getProcessRegistry } from "@wrongstack/tools";
|
|
7
7
|
|
|
8
8
|
// src/server/pending-confirms.ts
|
|
9
|
+
function isDestructivePendingConfirm(confirm) {
|
|
10
|
+
return confirm.riskTier === "destructive" || confirm.decisionSource === "yolo_destructive";
|
|
11
|
+
}
|
|
9
12
|
function resolveYoloEligiblePendingConfirms(pendingConfirms) {
|
|
10
13
|
for (const [id, confirm] of pendingConfirms) {
|
|
11
14
|
if (confirm.boundaryReason) continue;
|
|
15
|
+
if (isDestructivePendingConfirm(confirm)) continue;
|
|
12
16
|
pendingConfirms.delete(id);
|
|
13
17
|
confirm.resolve("yes");
|
|
14
18
|
}
|
|
@@ -72,7 +76,18 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
72
76
|
"showModelReasoning",
|
|
73
77
|
// Display-only toggles (purely visual, persisted in localStorage via Zustand).
|
|
74
78
|
"groupToolCalls",
|
|
75
|
-
"showThinkingLogs"
|
|
79
|
+
"showThinkingLogs",
|
|
80
|
+
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
81
|
+
"showAgentSwarmPanel",
|
|
82
|
+
"allowOutsideProjectRoot",
|
|
83
|
+
// v13 Display parity (TUI SettingsPicker fields 42 & 43): the read tool
|
|
84
|
+
// includes codebase-index symbols, and SAGE memory-inject blocks are
|
|
85
|
+
// surfaced in tool results. Both are pure WebUI-display toggles mirrored
|
|
86
|
+
// against the TUI's settings picker; the server mirrors them through
|
|
87
|
+
// `prefs.update` exactly so the panel does not error with
|
|
88
|
+
// "unknown preference key".
|
|
89
|
+
"readSymbols",
|
|
90
|
+
"showSageMemoryInject"
|
|
76
91
|
]);
|
|
77
92
|
var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
78
93
|
"fallbackModels",
|
|
@@ -93,14 +108,39 @@ var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
93
108
|
"maxIterations",
|
|
94
109
|
"maxConcurrent",
|
|
95
110
|
"enhanceDelayMs",
|
|
111
|
+
"enhanceCountdownMs",
|
|
96
112
|
"tgLongToolMs",
|
|
97
113
|
"breakerAutoKillResetMs",
|
|
98
114
|
// Chimera + auto-review numeric knobs
|
|
99
115
|
"chimeraMaxFiles",
|
|
100
116
|
"autoReviewDebounceMs",
|
|
101
117
|
"autoReviewMaxFilesPerBatch",
|
|
102
|
-
"autoReviewMaxConcurrentReviews"
|
|
118
|
+
"autoReviewMaxConcurrentReviews",
|
|
119
|
+
// v13 Display parity (TUI SettingsPicker fields 41, 44, 21): the
|
|
120
|
+
// pre-refine grace countdown (seconds), the SAGE memory-inject relation
|
|
121
|
+
// floor (0..1), and the minimum file count for the multi-diff summary
|
|
122
|
+
// footer. All three are driven by the WebUI sliders in DisplaySection
|
|
123
|
+
// and previously tripped the "unknown preference key" rejection.
|
|
124
|
+
"preRefineSeconds",
|
|
125
|
+
"sageMemoryInjectThreshold",
|
|
126
|
+
"multiDiffSummaryThreshold"
|
|
103
127
|
]);
|
|
128
|
+
var NUMBER_PREF_BOUNDS = {
|
|
129
|
+
// Iteration / concurrency — must be at least 1 to make progress.
|
|
130
|
+
maxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
|
|
131
|
+
maxConcurrent: { min: 1, max: Number.POSITIVE_INFINITY },
|
|
132
|
+
autoProceedMaxIterations: { min: 1, max: Number.POSITIVE_INFINITY },
|
|
133
|
+
chimeraMaxFiles: { min: 1, max: Number.POSITIVE_INFINITY },
|
|
134
|
+
autoReviewMaxFilesPerBatch: { min: 1, max: Number.POSITIVE_INFINITY },
|
|
135
|
+
autoReviewMaxConcurrentReviews: { min: 1, max: Number.POSITIVE_INFINITY },
|
|
136
|
+
// Counts (must be >= 1 to render a footer at all).
|
|
137
|
+
multiDiffSummaryThreshold: { min: 1, max: Number.POSITIVE_INFINITY },
|
|
138
|
+
preRefineSeconds: { min: 0, max: Number.POSITIVE_INFINITY },
|
|
139
|
+
// Probability — must lie in [0, 1].
|
|
140
|
+
sageMemoryInjectThreshold: { min: 0, max: 1 },
|
|
141
|
+
// Debounce / delay — non-negative ms.
|
|
142
|
+
autoReviewDebounceMs: { min: 0, max: Number.POSITIVE_INFINITY }
|
|
143
|
+
};
|
|
104
144
|
var STRING_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
105
145
|
"hqUrl",
|
|
106
146
|
"hqToken",
|
|
@@ -135,85 +175,85 @@ var ENUM_PREF_KEYS = {
|
|
|
135
175
|
autoReviewCascadeOn: /* @__PURE__ */ new Set(["off", "critical", "high"]),
|
|
136
176
|
fleetChatVerbosity: /* @__PURE__ */ new Set(["off", "full"])
|
|
137
177
|
};
|
|
138
|
-
function validateModelRuntimeValue(modelRuntime,
|
|
178
|
+
function validateModelRuntimeValue(modelRuntime, path30) {
|
|
139
179
|
const reasoning = modelRuntime["reasoning"];
|
|
140
180
|
if (reasoning !== void 0) {
|
|
141
|
-
if (!isRecord(reasoning)) return `${
|
|
181
|
+
if (!isRecord(reasoning)) return `${path30}.reasoning must be an object when provided`;
|
|
142
182
|
const mode = reasoning["mode"];
|
|
143
183
|
const effort = reasoning["effort"];
|
|
144
184
|
const preserve = reasoning["preserve"];
|
|
145
185
|
if (mode !== void 0 && (typeof mode !== "string" || !REASONING_MODE_VALUES.has(mode))) {
|
|
146
|
-
return `${
|
|
186
|
+
return `${path30}.reasoning.mode must be one of: ${Array.from(REASONING_MODE_VALUES).join(", ")}`;
|
|
147
187
|
}
|
|
148
188
|
if (effort !== void 0 && (typeof effort !== "string" || !REASONING_EFFORT_VALUES.has(effort))) {
|
|
149
|
-
return `${
|
|
189
|
+
return `${path30}.reasoning.effort must be one of: ${Array.from(REASONING_EFFORT_VALUES).join(", ")}`;
|
|
150
190
|
}
|
|
151
191
|
if (preserve !== void 0 && typeof preserve !== "boolean") {
|
|
152
|
-
return `${
|
|
192
|
+
return `${path30}.reasoning.preserve must be a boolean when provided`;
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
195
|
const cache2 = modelRuntime["cache"];
|
|
156
196
|
if (cache2 !== void 0) {
|
|
157
|
-
if (!isRecord(cache2)) return `${
|
|
197
|
+
if (!isRecord(cache2)) return `${path30}.cache must be an object when provided`;
|
|
158
198
|
const ttl = cache2["ttl"];
|
|
159
199
|
if (ttl !== void 0 && (typeof ttl !== "string" || !CACHE_TTL_VALUES.has(ttl) || ttl === "default")) {
|
|
160
|
-
return `${
|
|
200
|
+
return `${path30}.cache.ttl must be one of: 5m, 1h`;
|
|
161
201
|
}
|
|
162
202
|
}
|
|
163
203
|
const parameters = modelRuntime["parameters"];
|
|
164
204
|
if (parameters !== void 0 && !isRecord(parameters)) {
|
|
165
|
-
return `${
|
|
205
|
+
return `${path30}.parameters must be an object when provided`;
|
|
166
206
|
}
|
|
167
207
|
return null;
|
|
168
208
|
}
|
|
169
|
-
function validateModelBlackoutRule(rule,
|
|
209
|
+
function validateModelBlackoutRule(rule, path30) {
|
|
170
210
|
const id = rule["id"];
|
|
171
211
|
if (typeof id !== "string" || id.trim().length === 0) {
|
|
172
|
-
return `${
|
|
212
|
+
return `${path30}.id must be a non-empty string`;
|
|
173
213
|
}
|
|
174
214
|
const start = rule["start"];
|
|
175
215
|
if (typeof start !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(start)) {
|
|
176
|
-
return `${
|
|
216
|
+
return `${path30}.start must be a string in HH:mm (00:00-23:59) format`;
|
|
177
217
|
}
|
|
178
218
|
const end = rule["end"];
|
|
179
219
|
if (typeof end !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(end)) {
|
|
180
|
-
return `${
|
|
220
|
+
return `${path30}.end must be a string in HH:mm (00:00-23:59) format`;
|
|
181
221
|
}
|
|
182
222
|
if (rule["enabled"] !== void 0 && typeof rule["enabled"] !== "boolean") {
|
|
183
|
-
return `${
|
|
223
|
+
return `${path30}.enabled must be a boolean when provided`;
|
|
184
224
|
}
|
|
185
225
|
if (rule["provider"] !== void 0 && typeof rule["provider"] !== "string") {
|
|
186
|
-
return `${
|
|
226
|
+
return `${path30}.provider must be a string when provided`;
|
|
187
227
|
}
|
|
188
228
|
if (rule["model"] !== void 0 && typeof rule["model"] !== "string") {
|
|
189
|
-
return `${
|
|
229
|
+
return `${path30}.model must be a string when provided`;
|
|
190
230
|
}
|
|
191
231
|
if (rule["days"] !== void 0) {
|
|
192
|
-
if (!Array.isArray(rule["days"])) return `${
|
|
232
|
+
if (!Array.isArray(rule["days"])) return `${path30}.days must be an array when provided`;
|
|
193
233
|
const seen = /* @__PURE__ */ new Set();
|
|
194
234
|
for (const d of rule["days"]) {
|
|
195
235
|
if (typeof d !== "number" || !Number.isInteger(d) || d < 0 || d > 6) {
|
|
196
|
-
return `${
|
|
236
|
+
return `${path30}.days elements must be integers 0-6 when provided`;
|
|
197
237
|
}
|
|
198
|
-
if (seen.has(d)) return `${
|
|
238
|
+
if (seen.has(d)) return `${path30}.days contains duplicate day: ${d}`;
|
|
199
239
|
seen.add(d);
|
|
200
240
|
}
|
|
201
241
|
}
|
|
202
242
|
if (rule["timezone"] !== void 0) {
|
|
203
243
|
if (typeof rule["timezone"] !== "string") {
|
|
204
|
-
return `${
|
|
244
|
+
return `${path30}.timezone must be a string when provided`;
|
|
205
245
|
}
|
|
206
246
|
try {
|
|
207
247
|
Intl.DateTimeFormat(void 0, { timeZone: rule["timezone"] });
|
|
208
248
|
} catch {
|
|
209
|
-
return `${
|
|
249
|
+
return `${path30}.timezone is not a valid IANA timezone (e.g. "America/New_York")`;
|
|
210
250
|
}
|
|
211
251
|
}
|
|
212
252
|
if (rule["label"] !== void 0 && typeof rule["label"] !== "string") {
|
|
213
|
-
return `${
|
|
253
|
+
return `${path30}.label must be a string when provided`;
|
|
214
254
|
}
|
|
215
255
|
if (rule["mode"] !== void 0 && rule["mode"] !== "blackout" && rule["mode"] !== "allow_only") {
|
|
216
|
-
return `${
|
|
256
|
+
return `${path30}.mode must be 'blackout' or 'allow_only' when provided`;
|
|
217
257
|
}
|
|
218
258
|
return null;
|
|
219
259
|
}
|
|
@@ -222,7 +262,15 @@ function validatePreferenceValue(key, value) {
|
|
|
222
262
|
return typeof value === "boolean" ? null : `prefs.update payload.${key} must be a boolean`;
|
|
223
263
|
}
|
|
224
264
|
if (NUMBER_PREF_KEYS.has(key)) {
|
|
225
|
-
|
|
265
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
266
|
+
return `prefs.update payload.${key} must be a finite number`;
|
|
267
|
+
}
|
|
268
|
+
const bounds = NUMBER_PREF_BOUNDS[key];
|
|
269
|
+
if (bounds && (value < bounds.min || value > bounds.max)) {
|
|
270
|
+
const maxStr = bounds.max === Number.POSITIVE_INFINITY ? "\u221E" : String(bounds.max);
|
|
271
|
+
return `prefs.update payload.${key} must be in [${bounds.min}, ${maxStr}]`;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
226
274
|
}
|
|
227
275
|
if (STRING_PREF_KEYS.has(key)) {
|
|
228
276
|
return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
|
|
@@ -246,9 +294,16 @@ function validatePreferenceValue(key, value) {
|
|
|
246
294
|
return null;
|
|
247
295
|
}
|
|
248
296
|
if (STRING_ARRAY_RECORD_PREF_KEYS.has(key)) {
|
|
249
|
-
|
|
297
|
+
if (!isRecord(value) || !Object.values(value).every(
|
|
250
298
|
(v) => Array.isArray(v) && v.every((item) => typeof item === "string")
|
|
251
|
-
)
|
|
299
|
+
)) {
|
|
300
|
+
return `prefs.update payload.${key} must be an object of string arrays`;
|
|
301
|
+
}
|
|
302
|
+
const badKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
|
|
303
|
+
if (badKey) {
|
|
304
|
+
return `prefs.update payload.${key} contains a forbidden key: ${badKey}`;
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
252
307
|
}
|
|
253
308
|
if (BOOLEAN_RECORD_PREF_KEYS.has(key)) {
|
|
254
309
|
if (!isRecord(value) || !Object.values(value).every((v) => typeof v === "boolean")) {
|
|
@@ -262,6 +317,10 @@ function validatePreferenceValue(key, value) {
|
|
|
262
317
|
}
|
|
263
318
|
if (MODEL_MATRIX_PREF_KEYS.has(key)) {
|
|
264
319
|
if (!isRecord(value)) return `prefs.update payload.${key} must be an object`;
|
|
320
|
+
const badMatrixKey = Object.keys(value).find((k) => FORBIDDEN_PROTO_KEYS.has(k));
|
|
321
|
+
if (badMatrixKey) {
|
|
322
|
+
return `prefs.update payload.${key} contains a forbidden key: ${badMatrixKey}`;
|
|
323
|
+
}
|
|
265
324
|
for (const entry of Object.values(value)) {
|
|
266
325
|
if (!isRecord(entry)) return `prefs.update payload.${key} entries must be objects`;
|
|
267
326
|
const provider = entry["provider"];
|
|
@@ -386,6 +445,37 @@ function validateMailboxMessagesPayload(payload) {
|
|
|
386
445
|
}
|
|
387
446
|
};
|
|
388
447
|
}
|
|
448
|
+
var MAILBOX_ACTIONS = /* @__PURE__ */ new Set(["mark-read", "acknowledge", "reopen", "soft-delete"]);
|
|
449
|
+
function validateMailboxActionPayload(payload) {
|
|
450
|
+
if (!isRecord2(payload)) {
|
|
451
|
+
return { ok: false, message: "mailbox.action payload must be an object" };
|
|
452
|
+
}
|
|
453
|
+
const requestId = payload["requestId"];
|
|
454
|
+
const mailId = payload["mailId"];
|
|
455
|
+
const action = payload["action"];
|
|
456
|
+
const readerId = payload["readerId"];
|
|
457
|
+
if (typeof requestId !== "string" || requestId.trim().length === 0) {
|
|
458
|
+
return { ok: false, message: "mailbox.action payload.requestId must be a non-empty string" };
|
|
459
|
+
}
|
|
460
|
+
if (typeof mailId !== "string" || mailId.trim().length === 0) {
|
|
461
|
+
return { ok: false, message: "mailbox.action payload.mailId must be a non-empty string" };
|
|
462
|
+
}
|
|
463
|
+
if (typeof action !== "string" || !MAILBOX_ACTIONS.has(action)) {
|
|
464
|
+
return { ok: false, message: "mailbox.action payload.action must be a supported action" };
|
|
465
|
+
}
|
|
466
|
+
if (typeof readerId !== "string" || readerId.trim().length === 0) {
|
|
467
|
+
return { ok: false, message: "mailbox.action payload.readerId must be a non-empty string" };
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
ok: true,
|
|
471
|
+
value: {
|
|
472
|
+
requestId: requestId.trim(),
|
|
473
|
+
mailId: mailId.trim(),
|
|
474
|
+
action,
|
|
475
|
+
readerId: readerId.trim()
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
389
479
|
var MAILBOX_SEND_TYPES = /* @__PURE__ */ new Set([
|
|
390
480
|
"note",
|
|
391
481
|
"ask",
|
|
@@ -402,6 +492,7 @@ function validateMailboxSendPayload(payload) {
|
|
|
402
492
|
return { ok: false, message: "mailbox.send payload must be an object" };
|
|
403
493
|
}
|
|
404
494
|
const requestId = payload["requestId"];
|
|
495
|
+
const rawFrom = payload["from"];
|
|
405
496
|
const rawTo = payload["to"];
|
|
406
497
|
const rawType = payload["type"];
|
|
407
498
|
const rawAudience = payload["audience"];
|
|
@@ -412,6 +503,9 @@ function validateMailboxSendPayload(payload) {
|
|
|
412
503
|
if (typeof requestId !== "string" || requestId.trim().length === 0) {
|
|
413
504
|
return { ok: false, message: "mailbox.send payload.requestId must be a non-empty string" };
|
|
414
505
|
}
|
|
506
|
+
if (rawFrom !== void 0 && (typeof rawFrom !== "string" || rawFrom.trim().length === 0)) {
|
|
507
|
+
return { ok: false, message: "mailbox.send payload.from must be a non-empty string when provided" };
|
|
508
|
+
}
|
|
415
509
|
if (typeof rawTo !== "string" || rawTo.trim().length === 0) {
|
|
416
510
|
return { ok: false, message: "mailbox.send payload.to must be a non-empty string" };
|
|
417
511
|
}
|
|
@@ -448,6 +542,7 @@ function validateMailboxSendPayload(payload) {
|
|
|
448
542
|
ok: true,
|
|
449
543
|
value: {
|
|
450
544
|
requestId: requestId.trim(),
|
|
545
|
+
...rawFrom !== void 0 ? { from: rawFrom.trim() } : {},
|
|
451
546
|
to,
|
|
452
547
|
type,
|
|
453
548
|
audience: rawAudience,
|
|
@@ -737,8 +832,8 @@ function validateShellOpenPayload(payload) {
|
|
|
737
832
|
if (!isRecord2(payload)) {
|
|
738
833
|
return { ok: false, message: "shell.open payload must be an object with string path" };
|
|
739
834
|
}
|
|
740
|
-
const
|
|
741
|
-
if (typeof
|
|
835
|
+
const path30 = payload["path"];
|
|
836
|
+
if (typeof path30 !== "string" || path30.trim().length === 0) {
|
|
742
837
|
return { ok: false, message: "shell.open payload.path must be a non-empty string" };
|
|
743
838
|
}
|
|
744
839
|
const target = payload["target"];
|
|
@@ -751,7 +846,7 @@ function validateShellOpenPayload(payload) {
|
|
|
751
846
|
return {
|
|
752
847
|
ok: true,
|
|
753
848
|
value: {
|
|
754
|
-
path:
|
|
849
|
+
path: path30,
|
|
755
850
|
...target !== void 0 ? { target } : {}
|
|
756
851
|
}
|
|
757
852
|
};
|
|
@@ -760,14 +855,14 @@ function validateGitDiffPayload(payload) {
|
|
|
760
855
|
if (!isRecord2(payload)) {
|
|
761
856
|
return { ok: false, message: "git.diff payload must be an object" };
|
|
762
857
|
}
|
|
763
|
-
const
|
|
764
|
-
if (
|
|
858
|
+
const path30 = payload["path"];
|
|
859
|
+
if (path30 === void 0 || path30 === null) {
|
|
765
860
|
return { ok: true, value: { path: "" } };
|
|
766
861
|
}
|
|
767
|
-
if (typeof
|
|
862
|
+
if (typeof path30 !== "string") {
|
|
768
863
|
return { ok: false, message: "git.diff payload.path must be a string when provided" };
|
|
769
864
|
}
|
|
770
|
-
return { ok: true, value: { path:
|
|
865
|
+
return { ok: true, value: { path: path30 } };
|
|
771
866
|
}
|
|
772
867
|
function validateProjectsAddPayload(payload) {
|
|
773
868
|
if (!isRecord2(payload)) {
|
|
@@ -803,6 +898,58 @@ function validateProjectsSelectPayload(payload) {
|
|
|
803
898
|
value: { root, ...typeof name2 === "string" ? { name: name2 } : {} }
|
|
804
899
|
};
|
|
805
900
|
}
|
|
901
|
+
var MCP_MAX_STRING = 4096;
|
|
902
|
+
var MCP_MAX_ARRAY = 256;
|
|
903
|
+
function isStringArray(value, max = MCP_MAX_ARRAY) {
|
|
904
|
+
return Array.isArray(value) && value.length <= max && value.every((item) => typeof item === "string" && item.length <= MCP_MAX_STRING);
|
|
905
|
+
}
|
|
906
|
+
function isStringRecord(value) {
|
|
907
|
+
if (!isRecord2(value)) return false;
|
|
908
|
+
const entries = Object.entries(value);
|
|
909
|
+
if (entries.length > MCP_MAX_ARRAY) return false;
|
|
910
|
+
return entries.every(
|
|
911
|
+
([key, item]) => typeof key === "string" && key.length <= MCP_MAX_STRING && typeof item === "string" && item.length <= MCP_MAX_STRING
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
function validateMcpServerPayload(payload, label) {
|
|
915
|
+
if (!isRecord2(payload)) {
|
|
916
|
+
return { ok: false, message: `${label} payload must be an object` };
|
|
917
|
+
}
|
|
918
|
+
const name2 = payload["name"];
|
|
919
|
+
if (typeof name2 !== "string" || name2.trim().length === 0 || name2.length > MCP_MAX_STRING) {
|
|
920
|
+
return { ok: false, message: `${label} payload.name must be a non-empty string` };
|
|
921
|
+
}
|
|
922
|
+
for (const key of ["description", "command", "url", "transport", "permission"]) {
|
|
923
|
+
const value = payload[key];
|
|
924
|
+
if (value !== void 0 && (typeof value !== "string" || value.length > MCP_MAX_STRING)) {
|
|
925
|
+
if (!(key === "transport" && isRecord2(value))) {
|
|
926
|
+
return { ok: false, message: `${label} payload.${key} must be a string` };
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const key of ["enabled", "lazy"]) {
|
|
931
|
+
const value = payload[key];
|
|
932
|
+
if (value !== void 0 && typeof value !== "boolean") {
|
|
933
|
+
return { ok: false, message: `${label} payload.${key} must be a boolean` };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
for (const key of ["args", "allowedTools", "passthroughEnv"]) {
|
|
937
|
+
const value = payload[key];
|
|
938
|
+
if (value !== void 0 && !isStringArray(value)) {
|
|
939
|
+
return { ok: false, message: `${label} payload.${key} must be an array of strings` };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
for (const key of ["env", "headers"]) {
|
|
943
|
+
const value = payload[key];
|
|
944
|
+
if (value !== void 0 && !isStringRecord(value)) {
|
|
945
|
+
return {
|
|
946
|
+
ok: false,
|
|
947
|
+
message: `${label} payload.${key} must be an object of string values`
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return { ok: true, value: payload };
|
|
952
|
+
}
|
|
806
953
|
|
|
807
954
|
// src/server/prefs-handlers.ts
|
|
808
955
|
function sendResult(ctx, ws, success, message) {
|
|
@@ -839,6 +986,9 @@ function handlePrefsUpdate(ctx, ws, input) {
|
|
|
839
986
|
const payload = parsed.value.prefs;
|
|
840
987
|
for (const [key, value] of Object.entries(payload)) ctx.meta[key] = value;
|
|
841
988
|
void ctx.persist(payload);
|
|
989
|
+
if (typeof payload["autonomy"] === "string") {
|
|
990
|
+
ctx.setAutonomy?.(payload["autonomy"]);
|
|
991
|
+
}
|
|
842
992
|
if (typeof payload["yolo"] === "boolean") {
|
|
843
993
|
ctx.setYolo?.(payload["yolo"]);
|
|
844
994
|
if (payload["yolo"]) resolveYoloEligiblePendingConfirms(ctx.pendingConfirms);
|
|
@@ -875,6 +1025,7 @@ function handleAutonomySwitch(ctx, ws, mode) {
|
|
|
875
1025
|
|
|
876
1026
|
// src/server/ws-utils.ts
|
|
877
1027
|
import { randomBytes } from "node:crypto";
|
|
1028
|
+
import { scrubErrorDetail } from "@wrongstack/core/security";
|
|
878
1029
|
import { WebSocket } from "ws";
|
|
879
1030
|
var WEBUI_WS_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
880
1031
|
function sendSerialized(ws, data) {
|
|
@@ -912,7 +1063,7 @@ function sendResult2(ws, success, message) {
|
|
|
912
1063
|
send(ws, { type: "key.operation_result", payload: { success, message } });
|
|
913
1064
|
}
|
|
914
1065
|
function errMessage(err) {
|
|
915
|
-
return
|
|
1066
|
+
return scrubErrorDetail(err);
|
|
916
1067
|
}
|
|
917
1068
|
function generateAuthToken() {
|
|
918
1069
|
return randomBytes(16).toString("hex");
|
|
@@ -1205,7 +1356,16 @@ import * as path from "node:path";
|
|
|
1205
1356
|
import { resolveIndexDir } from "@wrongstack/tools/codebase-index/index";
|
|
1206
1357
|
var DB_FILE = "index.db";
|
|
1207
1358
|
var MAX_CACHE_ENTRIES = 128;
|
|
1359
|
+
var MAX_CACHE_BODY_CHARS = 4 * 1024 * 1024;
|
|
1360
|
+
var MAX_CACHE_TOTAL_CHARS = 16 * 1024 * 1024;
|
|
1208
1361
|
var cache = /* @__PURE__ */ new Map();
|
|
1362
|
+
var cacheChars = 0;
|
|
1363
|
+
function deleteCachedCodemapBody(key) {
|
|
1364
|
+
const entry = cache.get(key);
|
|
1365
|
+
if (!entry) return false;
|
|
1366
|
+
cacheChars = Math.max(0, cacheChars - entry.body.length);
|
|
1367
|
+
return cache.delete(key);
|
|
1368
|
+
}
|
|
1209
1369
|
function indexDbVersion(projectRoot, indexDir) {
|
|
1210
1370
|
try {
|
|
1211
1371
|
const dir = resolveIndexDir(projectRoot, indexDir);
|
|
@@ -1222,7 +1382,7 @@ function getCachedCodemapBody(key, version) {
|
|
|
1222
1382
|
const entry = cache.get(key);
|
|
1223
1383
|
if (!entry) return void 0;
|
|
1224
1384
|
if (entry.version !== version) {
|
|
1225
|
-
|
|
1385
|
+
deleteCachedCodemapBody(key);
|
|
1226
1386
|
return void 0;
|
|
1227
1387
|
}
|
|
1228
1388
|
cache.delete(key);
|
|
@@ -1230,16 +1390,19 @@ function getCachedCodemapBody(key, version) {
|
|
|
1230
1390
|
return entry.body;
|
|
1231
1391
|
}
|
|
1232
1392
|
function setCachedCodemapBody(key, version, body) {
|
|
1233
|
-
if (cache.has(key))
|
|
1393
|
+
if (cache.has(key)) deleteCachedCodemapBody(key);
|
|
1394
|
+
if (body.length > MAX_CACHE_BODY_CHARS) return;
|
|
1234
1395
|
cache.set(key, { version, body });
|
|
1235
|
-
|
|
1396
|
+
cacheChars += body.length;
|
|
1397
|
+
while (cache.size > MAX_CACHE_ENTRIES || cacheChars > MAX_CACHE_TOTAL_CHARS) {
|
|
1236
1398
|
const oldest = cache.keys().next().value;
|
|
1237
1399
|
if (oldest === void 0) break;
|
|
1238
|
-
|
|
1400
|
+
deleteCachedCodemapBody(oldest);
|
|
1239
1401
|
}
|
|
1240
1402
|
}
|
|
1241
1403
|
function clearCodemapGraphCache() {
|
|
1242
1404
|
cache.clear();
|
|
1405
|
+
cacheChars = 0;
|
|
1243
1406
|
}
|
|
1244
1407
|
|
|
1245
1408
|
// src/server/codebase-indexing.ts
|
|
@@ -1529,6 +1692,8 @@ var CollaborationWebSocketHandler = class {
|
|
|
1529
1692
|
for (const off of this.offs) off();
|
|
1530
1693
|
this.offs.length = 0;
|
|
1531
1694
|
this.stopBroadcast();
|
|
1695
|
+
this.clients.clear();
|
|
1696
|
+
this.bySession.clear();
|
|
1532
1697
|
}
|
|
1533
1698
|
// ── Inbound client messages ────────────────────────────────────────────
|
|
1534
1699
|
/**
|
|
@@ -1588,9 +1753,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1588
1753
|
if (activeSessionId !== void 0 && sessionId !== activeSessionId) {
|
|
1589
1754
|
this.send(
|
|
1590
1755
|
ws,
|
|
1591
|
-
this.errorMessage(
|
|
1592
|
-
`collab.join sessionId mismatch (active: ${activeSessionId})`
|
|
1593
|
-
)
|
|
1756
|
+
this.errorMessage(`collab.join sessionId mismatch (active: ${activeSessionId})`)
|
|
1594
1757
|
);
|
|
1595
1758
|
return;
|
|
1596
1759
|
}
|
|
@@ -1601,26 +1764,19 @@ var CollaborationWebSocketHandler = class {
|
|
|
1601
1764
|
if (role === "controller" && !this.bus) {
|
|
1602
1765
|
this.send(
|
|
1603
1766
|
ws,
|
|
1604
|
-
this.errorMessage(
|
|
1605
|
-
`role 'controller' is not available: server has no CollaborationBus`
|
|
1606
|
-
)
|
|
1767
|
+
this.errorMessage(`role 'controller' is not available: server has no CollaborationBus`)
|
|
1607
1768
|
);
|
|
1608
1769
|
return;
|
|
1609
1770
|
}
|
|
1610
1771
|
if (role === "annotator" && !this.annotations) {
|
|
1611
1772
|
this.send(
|
|
1612
1773
|
ws,
|
|
1613
|
-
this.errorMessage(
|
|
1614
|
-
`role 'annotator' is not available: server has no annotations store`
|
|
1615
|
-
)
|
|
1774
|
+
this.errorMessage(`role 'annotator' is not available: server has no annotations store`)
|
|
1616
1775
|
);
|
|
1617
1776
|
return;
|
|
1618
1777
|
}
|
|
1619
1778
|
if (role !== "observer" && this.options.authorizeRole?.({ ws, sessionId, requestedRole: role }) !== true) {
|
|
1620
|
-
this.send(
|
|
1621
|
-
ws,
|
|
1622
|
-
this.errorMessage(`role '${role}' requires explicit server authorization`)
|
|
1623
|
-
);
|
|
1779
|
+
this.send(ws, this.errorMessage(`role '${role}' requires explicit server authorization`));
|
|
1624
1780
|
return;
|
|
1625
1781
|
}
|
|
1626
1782
|
const participant = {
|
|
@@ -1649,14 +1805,10 @@ var CollaborationWebSocketHandler = class {
|
|
|
1649
1805
|
this.broadcast(sessionId, this.stateMessage(sessionId));
|
|
1650
1806
|
if (this.reader) {
|
|
1651
1807
|
this.replayHistory(ws, sessionId).catch((err) => {
|
|
1652
|
-
this.logger.debug?.(
|
|
1653
|
-
`collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`
|
|
1654
|
-
);
|
|
1808
|
+
this.logger.debug?.(`collab: replay failed for ${sessionId}: ${toErrorMessage(err)}`);
|
|
1655
1809
|
});
|
|
1656
1810
|
}
|
|
1657
|
-
this.logger.debug?.(
|
|
1658
|
-
`collab: participant ${participant.participantId} joined ${sessionId}`
|
|
1659
|
-
);
|
|
1811
|
+
this.logger.debug?.(`collab: participant ${participant.participantId} joined ${sessionId}`);
|
|
1660
1812
|
}
|
|
1661
1813
|
leave(ws) {
|
|
1662
1814
|
this.handleDisconnect(ws);
|
|
@@ -1727,18 +1879,13 @@ var CollaborationWebSocketHandler = class {
|
|
|
1727
1879
|
}
|
|
1728
1880
|
const payload = raw;
|
|
1729
1881
|
if (!payload?.sessionId || typeof payload.atEventIndex !== "number" || typeof payload.text !== "string") {
|
|
1730
|
-
this.send(
|
|
1731
|
-
ws,
|
|
1732
|
-
this.errorMessage("annotate requires { sessionId, atEventIndex, text }")
|
|
1733
|
-
);
|
|
1882
|
+
this.send(ws, this.errorMessage("annotate requires { sessionId, atEventIndex, text }"));
|
|
1734
1883
|
return;
|
|
1735
1884
|
}
|
|
1736
1885
|
if (payload.sessionId !== participant.sessionId) {
|
|
1737
1886
|
this.send(
|
|
1738
1887
|
ws,
|
|
1739
|
-
this.errorMessage(
|
|
1740
|
-
`annotate sessionId mismatch (joined: ${participant.sessionId})`
|
|
1741
|
-
)
|
|
1888
|
+
this.errorMessage(`annotate sessionId mismatch (joined: ${participant.sessionId})`)
|
|
1742
1889
|
);
|
|
1743
1890
|
return;
|
|
1744
1891
|
}
|
|
@@ -1765,12 +1912,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1765
1912
|
}
|
|
1766
1913
|
});
|
|
1767
1914
|
} catch (err) {
|
|
1768
|
-
this.send(
|
|
1769
|
-
ws,
|
|
1770
|
-
this.errorMessage(
|
|
1771
|
-
`annotation rejected: ${toErrorMessage(err)}`
|
|
1772
|
-
)
|
|
1773
|
-
);
|
|
1915
|
+
this.send(ws, this.errorMessage(`annotation rejected: ${toErrorMessage(err)}`));
|
|
1774
1916
|
}
|
|
1775
1917
|
}
|
|
1776
1918
|
async handleResolve(ws, raw) {
|
|
@@ -1786,26 +1928,19 @@ var CollaborationWebSocketHandler = class {
|
|
|
1786
1928
|
if (participant.role !== "annotator") {
|
|
1787
1929
|
this.send(
|
|
1788
1930
|
ws,
|
|
1789
|
-
this.errorMessage(
|
|
1790
|
-
`resolve requires the 'annotator' role (current: '${participant.role}')`
|
|
1791
|
-
)
|
|
1931
|
+
this.errorMessage(`resolve requires the 'annotator' role (current: '${participant.role}')`)
|
|
1792
1932
|
);
|
|
1793
1933
|
return;
|
|
1794
1934
|
}
|
|
1795
1935
|
const payload = raw;
|
|
1796
1936
|
if (!payload?.sessionId || !payload.annotationId) {
|
|
1797
|
-
this.send(
|
|
1798
|
-
ws,
|
|
1799
|
-
this.errorMessage("resolve requires { sessionId, annotationId }")
|
|
1800
|
-
);
|
|
1937
|
+
this.send(ws, this.errorMessage("resolve requires { sessionId, annotationId }"));
|
|
1801
1938
|
return;
|
|
1802
1939
|
}
|
|
1803
1940
|
if (payload.sessionId !== participant.sessionId) {
|
|
1804
1941
|
this.send(
|
|
1805
1942
|
ws,
|
|
1806
|
-
this.errorMessage(
|
|
1807
|
-
`resolve sessionId mismatch (joined: ${participant.sessionId})`
|
|
1808
|
-
)
|
|
1943
|
+
this.errorMessage(`resolve sessionId mismatch (joined: ${participant.sessionId})`)
|
|
1809
1944
|
);
|
|
1810
1945
|
return;
|
|
1811
1946
|
}
|
|
@@ -1816,10 +1951,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1816
1951
|
resolvedBy: participant.participantId
|
|
1817
1952
|
});
|
|
1818
1953
|
if (!updated) {
|
|
1819
|
-
this.send(
|
|
1820
|
-
ws,
|
|
1821
|
-
this.errorMessage(`annotation not found: ${payload.annotationId}`)
|
|
1822
|
-
);
|
|
1954
|
+
this.send(ws, this.errorMessage(`annotation not found: ${payload.annotationId}`));
|
|
1823
1955
|
return;
|
|
1824
1956
|
}
|
|
1825
1957
|
this.broadcast(payload.sessionId, {
|
|
@@ -1832,12 +1964,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1832
1964
|
}
|
|
1833
1965
|
});
|
|
1834
1966
|
} catch (err) {
|
|
1835
|
-
this.send(
|
|
1836
|
-
ws,
|
|
1837
|
-
this.errorMessage(
|
|
1838
|
-
`resolve failed: ${toErrorMessage(err)}`
|
|
1839
|
-
)
|
|
1840
|
-
);
|
|
1967
|
+
this.send(ws, this.errorMessage(`resolve failed: ${toErrorMessage(err)}`));
|
|
1841
1968
|
}
|
|
1842
1969
|
}
|
|
1843
1970
|
// ── Event subscription (live mirror) ───────────────────────────────────
|
|
@@ -1908,9 +2035,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1908
2035
|
seen++;
|
|
1909
2036
|
}
|
|
1910
2037
|
} catch (err) {
|
|
1911
|
-
this.logger.debug?.(
|
|
1912
|
-
`collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`
|
|
1913
|
-
);
|
|
2038
|
+
this.logger.debug?.(`collab: session reader rejected ${sessionId}: ${toErrorMessage(err)}`);
|
|
1914
2039
|
return;
|
|
1915
2040
|
}
|
|
1916
2041
|
const tail2 = seen <= REPLAY_LIMIT ? ring.slice(0, seen) : (
|
|
@@ -1992,9 +2117,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
1992
2117
|
try {
|
|
1993
2118
|
sendSerialized(p.ws, data);
|
|
1994
2119
|
} catch (err) {
|
|
1995
|
-
this.logger.debug?.(
|
|
1996
|
-
`collab broadcast failed: ${toErrorMessage(err)}`
|
|
1997
|
-
);
|
|
2120
|
+
this.logger.debug?.(`collab broadcast failed: ${toErrorMessage(err)}`);
|
|
1998
2121
|
}
|
|
1999
2122
|
}
|
|
2000
2123
|
}
|
|
@@ -2021,9 +2144,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
2021
2144
|
if (participant.role !== "controller") {
|
|
2022
2145
|
this.send(
|
|
2023
2146
|
ws,
|
|
2024
|
-
this.errorMessage(
|
|
2025
|
-
`pause requires the 'controller' role (current: '${participant.role}')`
|
|
2026
|
-
)
|
|
2147
|
+
this.errorMessage(`pause requires the 'controller' role (current: '${participant.role}')`)
|
|
2027
2148
|
);
|
|
2028
2149
|
return;
|
|
2029
2150
|
}
|
|
@@ -2068,9 +2189,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
2068
2189
|
if (participant.role !== "controller") {
|
|
2069
2190
|
this.send(
|
|
2070
2191
|
ws,
|
|
2071
|
-
this.errorMessage(
|
|
2072
|
-
`resume requires the 'controller' role (current: '${participant.role}')`
|
|
2073
|
-
)
|
|
2192
|
+
this.errorMessage(`resume requires the 'controller' role (current: '${participant.role}')`)
|
|
2074
2193
|
);
|
|
2075
2194
|
return;
|
|
2076
2195
|
}
|
|
@@ -2168,9 +2287,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
2168
2287
|
if (payload.sessionId !== participant.sessionId) {
|
|
2169
2288
|
this.send(
|
|
2170
2289
|
ws,
|
|
2171
|
-
this.errorMessage(
|
|
2172
|
-
`inject_tool sessionId mismatch (joined: ${participant.sessionId})`
|
|
2173
|
-
)
|
|
2290
|
+
this.errorMessage(`inject_tool sessionId mismatch (joined: ${participant.sessionId})`)
|
|
2174
2291
|
);
|
|
2175
2292
|
return;
|
|
2176
2293
|
}
|
|
@@ -2184,9 +2301,7 @@ var CollaborationWebSocketHandler = class {
|
|
|
2184
2301
|
if (!queued) {
|
|
2185
2302
|
this.send(
|
|
2186
2303
|
ws,
|
|
2187
|
-
this.errorMessage(
|
|
2188
|
-
`an injection for toolUseId ${payload.toolUseId} is already queued`
|
|
2189
|
-
)
|
|
2304
|
+
this.errorMessage(`an injection for toolUseId ${payload.toolUseId} is already queued`)
|
|
2190
2305
|
);
|
|
2191
2306
|
return;
|
|
2192
2307
|
}
|
|
@@ -2782,6 +2897,7 @@ import {
|
|
|
2782
2897
|
materializeTokens,
|
|
2783
2898
|
recordKitChoice,
|
|
2784
2899
|
recordOverrides,
|
|
2900
|
+
resolveMaterializeTarget,
|
|
2785
2901
|
resolveSemanticTune,
|
|
2786
2902
|
runDesignVerify,
|
|
2787
2903
|
setActiveKit,
|
|
@@ -2993,7 +3109,7 @@ async function handleDesignMaterialize(ws, ctx, msg) {
|
|
|
2993
3109
|
kitId: active.kit,
|
|
2994
3110
|
outPath: typeof payload.out === "string" ? payload.out : void 0
|
|
2995
3111
|
});
|
|
2996
|
-
const abs =
|
|
3112
|
+
const abs = await resolveMaterializeTarget(result.path, ctx.projectRoot);
|
|
2997
3113
|
await fs3.mkdir(path5.dirname(abs), { recursive: true });
|
|
2998
3114
|
await fs3.writeFile(abs, result.content);
|
|
2999
3115
|
send(ws, {
|
|
@@ -3146,7 +3262,29 @@ async function resolveFileInsideProject(projectRoot, filePath) {
|
|
|
3146
3262
|
if (!isPathInside(realProjectRoot, realFull)) {
|
|
3147
3263
|
throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
|
|
3148
3264
|
}
|
|
3149
|
-
|
|
3265
|
+
const realFinal = await canonicalizeFinalComponent(realFull);
|
|
3266
|
+
if (!isPathInside(realProjectRoot, realFinal)) {
|
|
3267
|
+
throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
|
|
3268
|
+
}
|
|
3269
|
+
return realFinal;
|
|
3270
|
+
}
|
|
3271
|
+
async function canonicalizeFinalComponent(p) {
|
|
3272
|
+
let stat3;
|
|
3273
|
+
try {
|
|
3274
|
+
stat3 = await fs5.lstat(p);
|
|
3275
|
+
} catch (err) {
|
|
3276
|
+
if (err.code === "ENOENT") return p;
|
|
3277
|
+
throw err;
|
|
3278
|
+
}
|
|
3279
|
+
if (!stat3.isSymbolicLink()) return p;
|
|
3280
|
+
try {
|
|
3281
|
+
return await fs5.realpath(p);
|
|
3282
|
+
} catch (err) {
|
|
3283
|
+
if (err.code === "ENOENT") {
|
|
3284
|
+
throw new ToolValidationError2({ message: "Path outside project root", field: "path" });
|
|
3285
|
+
}
|
|
3286
|
+
throw err;
|
|
3287
|
+
}
|
|
3150
3288
|
}
|
|
3151
3289
|
function splitParentAndBase(p) {
|
|
3152
3290
|
const base = path7.basename(p);
|
|
@@ -4179,13 +4317,12 @@ function createConversationOperations(ctx) {
|
|
|
4179
4317
|
},
|
|
4180
4318
|
ping: (ws) => ctx.send(ws, { type: "pong", payload: {} }),
|
|
4181
4319
|
confirmTool: (_ws, msg) => {
|
|
4182
|
-
const requested = requestedSessionId(msg);
|
|
4183
|
-
const current2 = ctx.getSessionId();
|
|
4184
|
-
if (requested && current2 && requested !== current2) return;
|
|
4185
4320
|
const { id, decision } = msg.payload ?? {};
|
|
4186
4321
|
if (typeof id !== "string") return;
|
|
4187
4322
|
const confirm = ctx.pendingConfirms.get(id);
|
|
4188
4323
|
if (!confirm) return;
|
|
4324
|
+
const claimedSession = requestedSessionId(msg) ?? ctx.getSessionId();
|
|
4325
|
+
if (confirm.sessionId !== void 0 && confirm.sessionId !== claimedSession) return;
|
|
4189
4326
|
ctx.pendingConfirms.delete(id);
|
|
4190
4327
|
confirm.resolve(decision);
|
|
4191
4328
|
}
|
|
@@ -4194,7 +4331,17 @@ function createConversationOperations(ctx) {
|
|
|
4194
4331
|
|
|
4195
4332
|
// src/server/context-editor.ts
|
|
4196
4333
|
import { createHash } from "node:crypto";
|
|
4197
|
-
import
|
|
4334
|
+
import net from "node:net";
|
|
4335
|
+
import {
|
|
4336
|
+
ALLOWED_IMAGE_MEDIA_TYPES,
|
|
4337
|
+
base64DecodedBytes,
|
|
4338
|
+
isAllowedImageMediaType,
|
|
4339
|
+
isPrivateIPv4,
|
|
4340
|
+
isPrivateIPv6,
|
|
4341
|
+
isValidImageBase64,
|
|
4342
|
+
MAX_INCOMING_IMAGE_BYTES,
|
|
4343
|
+
repairToolUseAdjacency
|
|
4344
|
+
} from "@wrongstack/core/utils";
|
|
4198
4345
|
|
|
4199
4346
|
// src/server/token-estimator.ts
|
|
4200
4347
|
function estimateTokens(s) {
|
|
@@ -4258,6 +4405,40 @@ var REVISION_PREFIX = "wrongstack-context-editor-v1\0";
|
|
|
4258
4405
|
var MAX_MESSAGE_COUNT_GROWTH = 10;
|
|
4259
4406
|
var MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
|
|
4260
4407
|
var MAX_STRING_LENGTH = 8 * 1024 * 1024;
|
|
4408
|
+
var MAX_IMAGE_URL_LENGTH = 2048;
|
|
4409
|
+
function imageUrlRejectionReason(url) {
|
|
4410
|
+
if (url.length > MAX_IMAGE_URL_LENGTH) {
|
|
4411
|
+
return `image.source.url exceeds ${MAX_IMAGE_URL_LENGTH} characters.`;
|
|
4412
|
+
}
|
|
4413
|
+
let parsed;
|
|
4414
|
+
try {
|
|
4415
|
+
parsed = new URL(url);
|
|
4416
|
+
} catch {
|
|
4417
|
+
return "image.source.url must be an absolute URL.";
|
|
4418
|
+
}
|
|
4419
|
+
if (parsed.protocol !== "https:") {
|
|
4420
|
+
return `image.source.url must use https (got "${parsed.protocol}").`;
|
|
4421
|
+
}
|
|
4422
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
4423
|
+
return "image.source.url must not embed credentials.";
|
|
4424
|
+
}
|
|
4425
|
+
const host = parsed.hostname.startsWith("[") && parsed.hostname.endsWith("]") ? parsed.hostname.slice(1, -1) : parsed.hostname;
|
|
4426
|
+
const bareHost = host.endsWith(".") ? host.slice(0, -1) : host;
|
|
4427
|
+
if (bareHost === "") {
|
|
4428
|
+
return "image.source.url must include a hostname.";
|
|
4429
|
+
}
|
|
4430
|
+
if (bareHost === "localhost" || bareHost.endsWith(".localhost")) {
|
|
4431
|
+
return "image.source.url must not target localhost.";
|
|
4432
|
+
}
|
|
4433
|
+
const family = net.isIP(bareHost);
|
|
4434
|
+
if (family === 4 && isPrivateIPv4(bareHost)) {
|
|
4435
|
+
return `image.source.url must not target a private or loopback address ("${bareHost}").`;
|
|
4436
|
+
}
|
|
4437
|
+
if (family === 6 && isPrivateIPv6(bareHost)) {
|
|
4438
|
+
return `image.source.url must not target a private or loopback address ("${bareHost}").`;
|
|
4439
|
+
}
|
|
4440
|
+
return void 0;
|
|
4441
|
+
}
|
|
4261
4442
|
function isRecord3(value) {
|
|
4262
4443
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4263
4444
|
}
|
|
@@ -4288,8 +4469,8 @@ function jsonByteLength(value) {
|
|
|
4288
4469
|
return MAX_PAYLOAD_BYTES + 1;
|
|
4289
4470
|
}
|
|
4290
4471
|
}
|
|
4291
|
-
function error(errors,
|
|
4292
|
-
errors.push({ path:
|
|
4472
|
+
function error(errors, path30, code, message) {
|
|
4473
|
+
errors.push({ path: path30, code, message });
|
|
4293
4474
|
}
|
|
4294
4475
|
function isMessageRole(value) {
|
|
4295
4476
|
return value === "user" || value === "assistant" || value === "system";
|
|
@@ -4297,25 +4478,25 @@ function isMessageRole(value) {
|
|
|
4297
4478
|
function isPlainJsonObject(value) {
|
|
4298
4479
|
return isRecord3(value);
|
|
4299
4480
|
}
|
|
4300
|
-
function validateCacheControl(value,
|
|
4481
|
+
function validateCacheControl(value, path30, errors) {
|
|
4301
4482
|
if (value === void 0) return void 0;
|
|
4302
4483
|
if (!isRecord3(value) || value["type"] !== "ephemeral") {
|
|
4303
|
-
error(errors,
|
|
4484
|
+
error(errors, path30, "INVALID_CACHE_CONTROL", 'cache_control must be { type: "ephemeral" }.');
|
|
4304
4485
|
return void 0;
|
|
4305
4486
|
}
|
|
4306
4487
|
return { type: "ephemeral" };
|
|
4307
4488
|
}
|
|
4308
|
-
function validateProviderMeta(value,
|
|
4489
|
+
function validateProviderMeta(value, path30, errors) {
|
|
4309
4490
|
if (value === void 0) return void 0;
|
|
4310
4491
|
if (!isPlainJsonObject(value)) {
|
|
4311
|
-
error(errors,
|
|
4492
|
+
error(errors, path30, "INVALID_PROVIDER_META", "providerMeta must be a JSON object.");
|
|
4312
4493
|
return void 0;
|
|
4313
4494
|
}
|
|
4314
4495
|
return value;
|
|
4315
4496
|
}
|
|
4316
|
-
function validateBlock(value,
|
|
4497
|
+
function validateBlock(value, path30, errors) {
|
|
4317
4498
|
if (!isRecord3(value)) {
|
|
4318
|
-
error(errors,
|
|
4499
|
+
error(errors, path30, "INVALID_BLOCK", "Content block must be an object.");
|
|
4319
4500
|
return void 0;
|
|
4320
4501
|
}
|
|
4321
4502
|
const type = value["type"];
|
|
@@ -4323,14 +4504,18 @@ function validateBlock(value, path29, errors) {
|
|
|
4323
4504
|
case "text": {
|
|
4324
4505
|
const text = value["text"];
|
|
4325
4506
|
if (typeof text !== "string") {
|
|
4326
|
-
error(errors, `${
|
|
4507
|
+
error(errors, `${path30}/text`, "INVALID_TEXT", "Text block text must be a string.");
|
|
4327
4508
|
return void 0;
|
|
4328
4509
|
}
|
|
4329
4510
|
if (text.length > MAX_STRING_LENGTH) {
|
|
4330
|
-
error(errors, `${
|
|
4511
|
+
error(errors, `${path30}/text`, "TEXT_TOO_LARGE", "Text block is too large.");
|
|
4331
4512
|
return void 0;
|
|
4332
4513
|
}
|
|
4333
|
-
const cacheControl = validateCacheControl(
|
|
4514
|
+
const cacheControl = validateCacheControl(
|
|
4515
|
+
value["cache_control"],
|
|
4516
|
+
`${path30}/cache_control`,
|
|
4517
|
+
errors
|
|
4518
|
+
);
|
|
4334
4519
|
return cacheControl ? { type: "text", text, cache_control: cacheControl } : { type: "text", text };
|
|
4335
4520
|
}
|
|
4336
4521
|
case "tool_use": {
|
|
@@ -4338,15 +4523,29 @@ function validateBlock(value, path29, errors) {
|
|
|
4338
4523
|
const name2 = value["name"];
|
|
4339
4524
|
const input = value["input"];
|
|
4340
4525
|
if (typeof id !== "string" || id.length === 0) {
|
|
4341
|
-
error(
|
|
4526
|
+
error(
|
|
4527
|
+
errors,
|
|
4528
|
+
`${path30}/id`,
|
|
4529
|
+
"INVALID_TOOL_USE_ID",
|
|
4530
|
+
"tool_use.id must be a non-empty string."
|
|
4531
|
+
);
|
|
4342
4532
|
}
|
|
4343
4533
|
if (typeof name2 !== "string" || name2.length === 0) {
|
|
4344
|
-
error(
|
|
4534
|
+
error(
|
|
4535
|
+
errors,
|
|
4536
|
+
`${path30}/name`,
|
|
4537
|
+
"INVALID_TOOL_NAME",
|
|
4538
|
+
"tool_use.name must be a non-empty string."
|
|
4539
|
+
);
|
|
4345
4540
|
}
|
|
4346
4541
|
if (!isPlainJsonObject(input)) {
|
|
4347
|
-
error(errors, `${
|
|
4542
|
+
error(errors, `${path30}/input`, "INVALID_TOOL_INPUT", "tool_use.input must be an object.");
|
|
4348
4543
|
}
|
|
4349
|
-
const providerMeta = validateProviderMeta(
|
|
4544
|
+
const providerMeta = validateProviderMeta(
|
|
4545
|
+
value["providerMeta"],
|
|
4546
|
+
`${path30}/providerMeta`,
|
|
4547
|
+
errors
|
|
4548
|
+
);
|
|
4350
4549
|
if (typeof id !== "string" || id.length === 0 || typeof name2 !== "string" || name2.length === 0 || !isPlainJsonObject(input)) {
|
|
4351
4550
|
return void 0;
|
|
4352
4551
|
}
|
|
@@ -4358,20 +4557,46 @@ function validateBlock(value, path29, errors) {
|
|
|
4358
4557
|
const content = value["content"];
|
|
4359
4558
|
const isError = value["is_error"];
|
|
4360
4559
|
if (typeof toolUseId !== "string" || toolUseId.length === 0) {
|
|
4361
|
-
error(
|
|
4560
|
+
error(
|
|
4561
|
+
errors,
|
|
4562
|
+
`${path30}/tool_use_id`,
|
|
4563
|
+
"INVALID_TOOL_RESULT_ID",
|
|
4564
|
+
"tool_result.tool_use_id must be a non-empty string."
|
|
4565
|
+
);
|
|
4362
4566
|
}
|
|
4363
4567
|
if (name2 !== void 0 && typeof name2 !== "string") {
|
|
4364
|
-
error(
|
|
4568
|
+
error(
|
|
4569
|
+
errors,
|
|
4570
|
+
`${path30}/name`,
|
|
4571
|
+
"INVALID_TOOL_RESULT_NAME",
|
|
4572
|
+
"tool_result.name must be a string."
|
|
4573
|
+
);
|
|
4365
4574
|
}
|
|
4366
4575
|
if (typeof content !== "string") {
|
|
4367
|
-
error(
|
|
4576
|
+
error(
|
|
4577
|
+
errors,
|
|
4578
|
+
`${path30}/content`,
|
|
4579
|
+
"INVALID_TOOL_RESULT_CONTENT",
|
|
4580
|
+
"tool_result.content must be a string."
|
|
4581
|
+
);
|
|
4368
4582
|
} else if (content.length > MAX_STRING_LENGTH) {
|
|
4369
|
-
error(
|
|
4583
|
+
error(
|
|
4584
|
+
errors,
|
|
4585
|
+
`${path30}/content`,
|
|
4586
|
+
"TOOL_RESULT_TOO_LARGE",
|
|
4587
|
+
"tool_result.content is too large."
|
|
4588
|
+
);
|
|
4370
4589
|
}
|
|
4371
4590
|
if (isError !== void 0 && typeof isError !== "boolean") {
|
|
4372
|
-
error(
|
|
4591
|
+
error(
|
|
4592
|
+
errors,
|
|
4593
|
+
`${path30}/is_error`,
|
|
4594
|
+
"INVALID_TOOL_RESULT_ERROR",
|
|
4595
|
+
"tool_result.is_error must be boolean."
|
|
4596
|
+
);
|
|
4373
4597
|
}
|
|
4374
|
-
if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string")
|
|
4598
|
+
if (typeof toolUseId !== "string" || toolUseId.length === 0 || typeof content !== "string")
|
|
4599
|
+
return void 0;
|
|
4375
4600
|
return {
|
|
4376
4601
|
type: "tool_result",
|
|
4377
4602
|
tool_use_id: toolUseId,
|
|
@@ -4383,33 +4608,97 @@ function validateBlock(value, path29, errors) {
|
|
|
4383
4608
|
case "image": {
|
|
4384
4609
|
const source = value["source"];
|
|
4385
4610
|
if (!isRecord3(source)) {
|
|
4386
|
-
error(errors, `${
|
|
4611
|
+
error(errors, `${path30}/source`, "INVALID_IMAGE_SOURCE", "image.source must be an object.");
|
|
4387
4612
|
return void 0;
|
|
4388
4613
|
}
|
|
4389
4614
|
const sourceType = source["type"];
|
|
4390
4615
|
if (sourceType !== "base64" && sourceType !== "url") {
|
|
4391
|
-
error(
|
|
4616
|
+
error(
|
|
4617
|
+
errors,
|
|
4618
|
+
`${path30}/source/type`,
|
|
4619
|
+
"INVALID_IMAGE_SOURCE_TYPE",
|
|
4620
|
+
"image.source.type must be base64 or url."
|
|
4621
|
+
);
|
|
4392
4622
|
return void 0;
|
|
4393
4623
|
}
|
|
4394
4624
|
const mediaType = source["media_type"];
|
|
4395
4625
|
const data = source["data"];
|
|
4396
4626
|
const url = source["url"];
|
|
4397
4627
|
if (mediaType !== void 0 && typeof mediaType !== "string") {
|
|
4398
|
-
error(
|
|
4628
|
+
error(
|
|
4629
|
+
errors,
|
|
4630
|
+
`${path30}/source/media_type`,
|
|
4631
|
+
"INVALID_IMAGE_MEDIA_TYPE",
|
|
4632
|
+
"image.source.media_type must be a string."
|
|
4633
|
+
);
|
|
4634
|
+
return void 0;
|
|
4399
4635
|
}
|
|
4400
|
-
if (
|
|
4401
|
-
error(
|
|
4636
|
+
if (typeof mediaType === "string" && !isAllowedImageMediaType(mediaType)) {
|
|
4637
|
+
error(
|
|
4638
|
+
errors,
|
|
4639
|
+
`${path30}/source/media_type`,
|
|
4640
|
+
"UNSUPPORTED_IMAGE_MEDIA_TYPE",
|
|
4641
|
+
`image.source.media_type must be one of: ${[...ALLOWED_IMAGE_MEDIA_TYPES].join(", ")}.`
|
|
4642
|
+
);
|
|
4643
|
+
return void 0;
|
|
4644
|
+
}
|
|
4645
|
+
if (sourceType === "base64") {
|
|
4646
|
+
if (typeof data !== "string" || data.length === 0) {
|
|
4647
|
+
error(
|
|
4648
|
+
errors,
|
|
4649
|
+
`${path30}/source/data`,
|
|
4650
|
+
"INVALID_IMAGE_DATA",
|
|
4651
|
+
"image.source.data must be a non-empty string for a base64 source."
|
|
4652
|
+
);
|
|
4653
|
+
return void 0;
|
|
4654
|
+
}
|
|
4655
|
+
if (!isValidImageBase64(data)) {
|
|
4656
|
+
error(
|
|
4657
|
+
errors,
|
|
4658
|
+
`${path30}/source/data`,
|
|
4659
|
+
"INVALID_IMAGE_DATA",
|
|
4660
|
+
"image.source.data is not valid base64."
|
|
4661
|
+
);
|
|
4662
|
+
return void 0;
|
|
4663
|
+
}
|
|
4664
|
+
if (base64DecodedBytes(data) > MAX_INCOMING_IMAGE_BYTES) {
|
|
4665
|
+
error(
|
|
4666
|
+
errors,
|
|
4667
|
+
`${path30}/source/data`,
|
|
4668
|
+
"IMAGE_TOO_LARGE",
|
|
4669
|
+
`image.source.data exceeds the ${MAX_INCOMING_IMAGE_BYTES / (1024 * 1024)} MB limit.`
|
|
4670
|
+
);
|
|
4671
|
+
return void 0;
|
|
4672
|
+
}
|
|
4673
|
+
return {
|
|
4674
|
+
type: "image",
|
|
4675
|
+
source: {
|
|
4676
|
+
type: "base64",
|
|
4677
|
+
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4678
|
+
data
|
|
4679
|
+
}
|
|
4680
|
+
};
|
|
4402
4681
|
}
|
|
4403
|
-
if (url !==
|
|
4404
|
-
error(
|
|
4682
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
4683
|
+
error(
|
|
4684
|
+
errors,
|
|
4685
|
+
`${path30}/source/url`,
|
|
4686
|
+
"INVALID_IMAGE_URL",
|
|
4687
|
+
"image.source.url must be a non-empty string for a url source."
|
|
4688
|
+
);
|
|
4689
|
+
return void 0;
|
|
4690
|
+
}
|
|
4691
|
+
const urlError = imageUrlRejectionReason(url);
|
|
4692
|
+
if (urlError !== void 0) {
|
|
4693
|
+
error(errors, `${path30}/source/url`, "UNSAFE_IMAGE_URL", urlError);
|
|
4694
|
+
return void 0;
|
|
4405
4695
|
}
|
|
4406
4696
|
return {
|
|
4407
4697
|
type: "image",
|
|
4408
4698
|
source: {
|
|
4409
|
-
type:
|
|
4699
|
+
type: "url",
|
|
4410
4700
|
...typeof mediaType === "string" ? { media_type: mediaType } : {},
|
|
4411
|
-
|
|
4412
|
-
...typeof url === "string" ? { url } : {}
|
|
4701
|
+
url
|
|
4413
4702
|
}
|
|
4414
4703
|
};
|
|
4415
4704
|
}
|
|
@@ -4417,13 +4706,27 @@ function validateBlock(value, path29, errors) {
|
|
|
4417
4706
|
const thinking = value["thinking"];
|
|
4418
4707
|
const signature = value["signature"];
|
|
4419
4708
|
if (typeof thinking !== "string") {
|
|
4420
|
-
error(
|
|
4709
|
+
error(
|
|
4710
|
+
errors,
|
|
4711
|
+
`${path30}/thinking`,
|
|
4712
|
+
"INVALID_THINKING",
|
|
4713
|
+
"thinking.thinking must be a string."
|
|
4714
|
+
);
|
|
4421
4715
|
return void 0;
|
|
4422
4716
|
}
|
|
4423
4717
|
if (signature !== void 0 && typeof signature !== "string") {
|
|
4424
|
-
error(
|
|
4718
|
+
error(
|
|
4719
|
+
errors,
|
|
4720
|
+
`${path30}/signature`,
|
|
4721
|
+
"INVALID_THINKING_SIGNATURE",
|
|
4722
|
+
"thinking.signature must be a string."
|
|
4723
|
+
);
|
|
4425
4724
|
}
|
|
4426
|
-
const providerMeta = validateProviderMeta(
|
|
4725
|
+
const providerMeta = validateProviderMeta(
|
|
4726
|
+
value["providerMeta"],
|
|
4727
|
+
`${path30}/providerMeta`,
|
|
4728
|
+
errors
|
|
4729
|
+
);
|
|
4427
4730
|
return {
|
|
4428
4731
|
type: "thinking",
|
|
4429
4732
|
thinking,
|
|
@@ -4432,7 +4735,12 @@ function validateBlock(value, path29, errors) {
|
|
|
4432
4735
|
};
|
|
4433
4736
|
}
|
|
4434
4737
|
default:
|
|
4435
|
-
error(
|
|
4738
|
+
error(
|
|
4739
|
+
errors,
|
|
4740
|
+
`${path30}/type`,
|
|
4741
|
+
"UNKNOWN_BLOCK_TYPE",
|
|
4742
|
+
`Unknown content block type: ${String(type)}`
|
|
4743
|
+
);
|
|
4436
4744
|
return void 0;
|
|
4437
4745
|
}
|
|
4438
4746
|
}
|
|
@@ -4444,45 +4752,65 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
|
|
|
4444
4752
|
return { messages, errors };
|
|
4445
4753
|
}
|
|
4446
4754
|
if (value.length > currentMessageCount + MAX_MESSAGE_COUNT_GROWTH) {
|
|
4447
|
-
error(
|
|
4755
|
+
error(
|
|
4756
|
+
errors,
|
|
4757
|
+
"/messages",
|
|
4758
|
+
"TOO_MANY_MESSAGES",
|
|
4759
|
+
"Context editor is deletion-oriented; proposed message count grew too much."
|
|
4760
|
+
);
|
|
4448
4761
|
}
|
|
4449
4762
|
if (jsonByteLength(value) > MAX_PAYLOAD_BYTES) {
|
|
4450
4763
|
error(errors, "/messages", "PAYLOAD_TOO_LARGE", "Context editor payload is too large.");
|
|
4451
4764
|
}
|
|
4452
4765
|
value.forEach((item, index) => {
|
|
4453
|
-
const
|
|
4766
|
+
const path30 = `/messages/${index}`;
|
|
4454
4767
|
if (!isRecord3(item)) {
|
|
4455
|
-
error(errors,
|
|
4768
|
+
error(errors, path30, "INVALID_MESSAGE", "Message must be an object.");
|
|
4456
4769
|
return;
|
|
4457
4770
|
}
|
|
4458
4771
|
const role = item["role"];
|
|
4459
4772
|
if (!isMessageRole(role)) {
|
|
4460
|
-
error(
|
|
4773
|
+
error(
|
|
4774
|
+
errors,
|
|
4775
|
+
`${path30}/role`,
|
|
4776
|
+
"INVALID_ROLE",
|
|
4777
|
+
"Message role must be user, assistant, or system."
|
|
4778
|
+
);
|
|
4461
4779
|
return;
|
|
4462
4780
|
}
|
|
4463
4781
|
const rawContent = item["content"];
|
|
4464
4782
|
let content;
|
|
4465
4783
|
if (typeof rawContent === "string") {
|
|
4466
4784
|
if (rawContent.length > MAX_STRING_LENGTH) {
|
|
4467
|
-
error(errors, `${
|
|
4785
|
+
error(errors, `${path30}/content`, "CONTENT_TOO_LARGE", "Message content is too large.");
|
|
4468
4786
|
return;
|
|
4469
4787
|
}
|
|
4470
4788
|
content = rawContent;
|
|
4471
4789
|
} else if (Array.isArray(rawContent)) {
|
|
4472
4790
|
const blocks = [];
|
|
4473
4791
|
rawContent.forEach((block, blockIndex) => {
|
|
4474
|
-
const parsed = validateBlock(block, `${
|
|
4792
|
+
const parsed = validateBlock(block, `${path30}/content/${blockIndex}`, errors);
|
|
4475
4793
|
if (parsed) blocks.push(parsed);
|
|
4476
4794
|
});
|
|
4477
4795
|
content = blocks;
|
|
4478
4796
|
} else {
|
|
4479
|
-
error(
|
|
4797
|
+
error(
|
|
4798
|
+
errors,
|
|
4799
|
+
`${path30}/content`,
|
|
4800
|
+
"INVALID_CONTENT",
|
|
4801
|
+
"Message content must be a string or content block array."
|
|
4802
|
+
);
|
|
4480
4803
|
return;
|
|
4481
4804
|
}
|
|
4482
4805
|
const ts = item["ts"];
|
|
4483
4806
|
if (ts !== void 0) {
|
|
4484
4807
|
if (typeof ts !== "string" || Number.isNaN(Date.parse(ts))) {
|
|
4485
|
-
error(
|
|
4808
|
+
error(
|
|
4809
|
+
errors,
|
|
4810
|
+
`${path30}/ts`,
|
|
4811
|
+
"INVALID_TIMESTAMP",
|
|
4812
|
+
"Message ts must be an ISO-like timestamp string."
|
|
4813
|
+
);
|
|
4486
4814
|
return;
|
|
4487
4815
|
}
|
|
4488
4816
|
}
|
|
@@ -4492,7 +4820,8 @@ function validateContextEditorMessages(value, currentMessageCount = 0) {
|
|
|
4492
4820
|
}
|
|
4493
4821
|
function countBlocks(messages) {
|
|
4494
4822
|
let total = 0;
|
|
4495
|
-
for (const message of messages)
|
|
4823
|
+
for (const message of messages)
|
|
4824
|
+
total += Array.isArray(message.content) ? message.content.length : 1;
|
|
4496
4825
|
return total;
|
|
4497
4826
|
}
|
|
4498
4827
|
function toolDiagnostics(messages) {
|
|
@@ -4556,11 +4885,13 @@ function metricFor(ctx, messages, tools) {
|
|
|
4556
4885
|
};
|
|
4557
4886
|
}
|
|
4558
4887
|
function buildContextEditorSnapshot(ctx, tools) {
|
|
4559
|
-
const messages = ctx.messages.map(
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4888
|
+
const messages = ctx.messages.map(
|
|
4889
|
+
(message) => ({
|
|
4890
|
+
role: message.role,
|
|
4891
|
+
content: message.content,
|
|
4892
|
+
...message.ts ? { ts: message.ts } : {}
|
|
4893
|
+
})
|
|
4894
|
+
);
|
|
4564
4895
|
const breakdown = estimateContextBreakdown({
|
|
4565
4896
|
systemPrompt: ctx.systemPrompt,
|
|
4566
4897
|
tools: tools ?? ctx.tools ?? [],
|
|
@@ -4605,7 +4936,10 @@ function validateContextEditorProposal(input) {
|
|
|
4605
4936
|
validationErrors: [],
|
|
4606
4937
|
warnings: [],
|
|
4607
4938
|
repair: emptyRepair,
|
|
4608
|
-
conflict: {
|
|
4939
|
+
conflict: {
|
|
4940
|
+
code: "RUN_ACTIVE",
|
|
4941
|
+
message: "The agent is currently running. Abort or wait before applying context edits."
|
|
4942
|
+
}
|
|
4609
4943
|
};
|
|
4610
4944
|
}
|
|
4611
4945
|
if (input.baseRevision !== currentRevision) {
|
|
@@ -4617,7 +4951,10 @@ function validateContextEditorProposal(input) {
|
|
|
4617
4951
|
validationErrors: [],
|
|
4618
4952
|
warnings: [],
|
|
4619
4953
|
repair: emptyRepair,
|
|
4620
|
-
conflict: {
|
|
4954
|
+
conflict: {
|
|
4955
|
+
code: "CONTEXT_REVISION_CONFLICT",
|
|
4956
|
+
message: "Context changed while the editor was open. Reload before applying."
|
|
4957
|
+
}
|
|
4621
4958
|
};
|
|
4622
4959
|
}
|
|
4623
4960
|
const parsed = validateContextEditorMessages(input.messages, input.ctx.messages.length);
|
|
@@ -4700,6 +5037,7 @@ async function applyContextEditorProposal(input) {
|
|
|
4700
5037
|
}
|
|
4701
5038
|
|
|
4702
5039
|
// src/server/connection-lifecycle.ts
|
|
5040
|
+
var RATE_LIMIT_EXEMPT_TYPES = /* @__PURE__ */ new Set(["ping", "pong"]);
|
|
4703
5041
|
function createConnectionLifecycle(options) {
|
|
4704
5042
|
const rateLimitMax = options.rateLimitMax ?? 0;
|
|
4705
5043
|
const rateLimitWindowMs = options.rateLimitWindowMs ?? 6e4;
|
|
@@ -4770,25 +5108,31 @@ function createConnectionLifecycle(options) {
|
|
|
4770
5108
|
}
|
|
4771
5109
|
let messageCount = 0;
|
|
4772
5110
|
let rateWindowResetAt = Date.now() + rateLimitWindowMs;
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
5111
|
+
let rateLimitNotified = false;
|
|
5112
|
+
const chargeRateLimit = () => {
|
|
5113
|
+
if (!Number.isFinite(rateLimitMax) || rateLimitMax <= 0) return true;
|
|
5114
|
+
const now = Date.now();
|
|
5115
|
+
if (now > rateWindowResetAt) {
|
|
5116
|
+
messageCount = 0;
|
|
5117
|
+
rateWindowResetAt = now + rateLimitWindowMs;
|
|
5118
|
+
rateLimitNotified = false;
|
|
5119
|
+
}
|
|
5120
|
+
if (++messageCount <= rateLimitMax) return true;
|
|
5121
|
+
if (!rateLimitNotified) {
|
|
5122
|
+
rateLimitNotified = true;
|
|
5123
|
+
options.send(ws, {
|
|
5124
|
+
type: "error",
|
|
5125
|
+
payload: options.sessionPayload({
|
|
5126
|
+
phase: "rate_limit",
|
|
5127
|
+
message: options.rateLimitMessage ?? "Too many messages. Please wait."
|
|
5128
|
+
})
|
|
5129
|
+
});
|
|
4790
5130
|
}
|
|
5131
|
+
return false;
|
|
5132
|
+
};
|
|
5133
|
+
ws.on("message", async (data) => {
|
|
4791
5134
|
const decoded = options.decode(data.toString());
|
|
5135
|
+
if (!decoded.ok && !chargeRateLimit()) return;
|
|
4792
5136
|
if (!decoded.ok) {
|
|
4793
5137
|
const eventName = decoded.issue.message === "Protocol frame is not valid JSON" ? "webui_server.message_parse_failed" : "webui_server.message_rejected";
|
|
4794
5138
|
const level = decoded.issue.code === "unsafe_key" || decoded.issue.code === "too_deep" ? "error" : "warn";
|
|
@@ -4826,6 +5170,10 @@ function createConnectionLifecycle(options) {
|
|
|
4826
5170
|
});
|
|
4827
5171
|
return;
|
|
4828
5172
|
}
|
|
5173
|
+
const messageType = decoded.message.type;
|
|
5174
|
+
if ((typeof messageType !== "string" || !RATE_LIMIT_EXEMPT_TYPES.has(messageType)) && !chargeRateLimit()) {
|
|
5175
|
+
return;
|
|
5176
|
+
}
|
|
4829
5177
|
try {
|
|
4830
5178
|
await options.dispatch(ws, client, decoded.message);
|
|
4831
5179
|
} catch (error2) {
|
|
@@ -4866,10 +5214,16 @@ import {
|
|
|
4866
5214
|
MailboxProjectServerConnection
|
|
4867
5215
|
} from "@wrongstack/core/coordination";
|
|
4868
5216
|
import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core/utils";
|
|
4869
|
-
import {
|
|
5217
|
+
import {
|
|
5218
|
+
closeKanbanServerConnections,
|
|
5219
|
+
getKanbanServerConnection,
|
|
5220
|
+
isKanbanServerAvailable
|
|
5221
|
+
} from "@wrongstack/kanban";
|
|
5222
|
+
import { readGovernanceDaemonOperatorStatus } from "@wrongstack/runtime/governance-bootstrap";
|
|
4870
5223
|
import { isSageProjectServerAvailable, SageProjectServerConnection } from "@wrongstack/sage";
|
|
4871
5224
|
import {
|
|
4872
5225
|
checkCodebaseIndexServerHealth,
|
|
5226
|
+
ensureCodebaseIndexServer as ensureCodebaseIndexServer2,
|
|
4873
5227
|
getIndexState,
|
|
4874
5228
|
resolveProjectIndexDaemonAvailability,
|
|
4875
5229
|
shutdownCodebaseIndexServer
|
|
@@ -4898,7 +5252,8 @@ async function collectConnectionsHealth(options) {
|
|
|
4898
5252
|
codebaseIndexHealth(options.projectRoot, options.indexDir),
|
|
4899
5253
|
sageHealth(options.projectRoot),
|
|
4900
5254
|
kanbanHealth(options.projectRoot),
|
|
4901
|
-
mailboxHealth(options.projectRoot)
|
|
5255
|
+
mailboxHealth(options.projectRoot),
|
|
5256
|
+
governanceHealth(options.projectRoot)
|
|
4902
5257
|
]);
|
|
4903
5258
|
const required = services.filter((service) => service.required);
|
|
4904
5259
|
const overall = required.some((service) => service.status === "error") ? "error" : required.some((service) => service.status !== "healthy") ? "degraded" : "healthy";
|
|
@@ -5203,6 +5558,42 @@ async function mailboxHealth(projectRoot) {
|
|
|
5203
5558
|
connection.close();
|
|
5204
5559
|
}
|
|
5205
5560
|
}
|
|
5561
|
+
async function governanceHealth(projectRoot) {
|
|
5562
|
+
const startedAt = Date.now();
|
|
5563
|
+
const result = await readGovernanceDaemonOperatorStatus(projectRoot);
|
|
5564
|
+
if (!result.available) {
|
|
5565
|
+
const missing = result.code === "broker_missing";
|
|
5566
|
+
return {
|
|
5567
|
+
id: "governance",
|
|
5568
|
+
label: "Governance control plane",
|
|
5569
|
+
status: missing ? "offline" : "error",
|
|
5570
|
+
required: false,
|
|
5571
|
+
mode: missing ? "compatibility-default-off" : "project-daemon",
|
|
5572
|
+
detail: missing ? "Not active for this project. Existing agent and model execution remains unchanged." : `Read-only governance status is unavailable: ${result.message}`,
|
|
5573
|
+
latencyMs: Date.now() - startedAt,
|
|
5574
|
+
control: "none",
|
|
5575
|
+
...missing ? {} : { lastError: result.message }
|
|
5576
|
+
};
|
|
5577
|
+
}
|
|
5578
|
+
const { status } = result;
|
|
5579
|
+
return {
|
|
5580
|
+
id: "governance",
|
|
5581
|
+
label: "Governance control plane",
|
|
5582
|
+
status: status.signal.level === "healthy" ? "healthy" : "degraded",
|
|
5583
|
+
required: false,
|
|
5584
|
+
mode: "project-daemon-advisory",
|
|
5585
|
+
detail: `${status.signal.message} Execution continues; no automatic task or model stop.`,
|
|
5586
|
+
ownerPid: status.pid,
|
|
5587
|
+
uptimeMs: Math.max(0, Date.now() - Date.parse(status.startedAt)),
|
|
5588
|
+
latencyMs: Date.now() - startedAt,
|
|
5589
|
+
control: "none",
|
|
5590
|
+
advisory: {
|
|
5591
|
+
code: status.signal.code,
|
|
5592
|
+
operatorAction: status.signal.operatorAction,
|
|
5593
|
+
executionDisposition: status.signal.executionDisposition
|
|
5594
|
+
}
|
|
5595
|
+
};
|
|
5596
|
+
}
|
|
5206
5597
|
function failureService(id, label, required, mode, error2, latencyMs) {
|
|
5207
5598
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
5208
5599
|
return {
|
|
@@ -5420,15 +5811,15 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
5420
5811
|
if (!m) continue;
|
|
5421
5812
|
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
5422
5813
|
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
5423
|
-
let
|
|
5424
|
-
if (
|
|
5814
|
+
let path30 = m[3] ?? "";
|
|
5815
|
+
if (path30 === "") {
|
|
5425
5816
|
i += 1;
|
|
5426
|
-
|
|
5817
|
+
path30 = parts[i + 1] ?? parts[i] ?? "";
|
|
5427
5818
|
i += 1;
|
|
5428
5819
|
}
|
|
5429
|
-
if (!
|
|
5430
|
-
const prev = counts.get(
|
|
5431
|
-
counts.set(
|
|
5820
|
+
if (!path30) continue;
|
|
5821
|
+
const prev = counts.get(path30) ?? { added: 0, deleted: 0 };
|
|
5822
|
+
counts.set(path30, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
5432
5823
|
}
|
|
5433
5824
|
};
|
|
5434
5825
|
parseNumstat(unstagedNumstat);
|
|
@@ -5440,7 +5831,7 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
5440
5831
|
if (!rec || rec.length < 3) continue;
|
|
5441
5832
|
const x = rec[0] ?? " ";
|
|
5442
5833
|
const y = rec[1] ?? " ";
|
|
5443
|
-
const
|
|
5834
|
+
const path30 = rec.slice(3);
|
|
5444
5835
|
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
5445
5836
|
if (isRename) i += 1;
|
|
5446
5837
|
let status;
|
|
@@ -5452,13 +5843,13 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
5452
5843
|
else if (x === "D" || y === "D") status = "D";
|
|
5453
5844
|
else status = "M";
|
|
5454
5845
|
const staged = x !== " " && x !== "?";
|
|
5455
|
-
let added = counts.get(
|
|
5456
|
-
let deleted = counts.get(
|
|
5846
|
+
let added = counts.get(path30)?.added ?? 0;
|
|
5847
|
+
let deleted = counts.get(path30)?.deleted ?? 0;
|
|
5457
5848
|
if (status === "?") {
|
|
5458
5849
|
added = 0;
|
|
5459
5850
|
deleted = 0;
|
|
5460
5851
|
}
|
|
5461
|
-
files.push({ path:
|
|
5852
|
+
files.push({ path: path30, status, added, deleted, staged });
|
|
5462
5853
|
}
|
|
5463
5854
|
send(ws, { type: "git.changes", payload: { files } });
|
|
5464
5855
|
} catch (err) {
|
|
@@ -5469,10 +5860,10 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
5469
5860
|
}
|
|
5470
5861
|
}
|
|
5471
5862
|
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
5472
|
-
async function handleGitDiff(ws, projectRoot,
|
|
5863
|
+
async function handleGitDiff(ws, projectRoot, path30) {
|
|
5473
5864
|
const cwd = projectRoot || void 0;
|
|
5474
|
-
const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path:
|
|
5475
|
-
if (!
|
|
5865
|
+
const reply2 = (extra) => send(ws, { type: "git.diff", payload: { path: path30, ...extra } });
|
|
5866
|
+
if (!path30 || path30.includes("\0") || path30.includes("..") || nodePath.isAbsolute(path30)) {
|
|
5476
5867
|
reply2({ oldText: "", newText: "", error: "invalid path" });
|
|
5477
5868
|
return;
|
|
5478
5869
|
}
|
|
@@ -5480,10 +5871,10 @@ async function handleGitDiff(ws, projectRoot, path29) {
|
|
|
5480
5871
|
const git = makeGit(cwd);
|
|
5481
5872
|
const { readFile: readFile11 } = await import("node:fs/promises");
|
|
5482
5873
|
const { join: join15 } = await import("node:path");
|
|
5483
|
-
const oldText = await git(["show", `HEAD:${
|
|
5874
|
+
const oldText = await git(["show", `HEAD:${path30}`]);
|
|
5484
5875
|
let newText = "";
|
|
5485
5876
|
try {
|
|
5486
|
-
const abs = cwd ? join15(cwd,
|
|
5877
|
+
const abs = cwd ? join15(cwd, path30) : path30;
|
|
5487
5878
|
const buf = await readFile11(abs);
|
|
5488
5879
|
if (buf.includes(0)) {
|
|
5489
5880
|
reply2({ oldText: "", newText: "", binary: true });
|
|
@@ -5545,10 +5936,7 @@ async function handleGoalSnapshotRoute(ws, msg, handlers) {
|
|
|
5545
5936
|
}
|
|
5546
5937
|
|
|
5547
5938
|
// src/server/goal-ws-handler.ts
|
|
5548
|
-
import {
|
|
5549
|
-
import {
|
|
5550
|
-
assignNickname
|
|
5551
|
-
} from "@wrongstack/core/coordination";
|
|
5939
|
+
import { assignNickname } from "@wrongstack/core/coordination";
|
|
5552
5940
|
import {
|
|
5553
5941
|
GoalAssessor,
|
|
5554
5942
|
GoalPlanner,
|
|
@@ -5556,6 +5944,7 @@ import {
|
|
|
5556
5944
|
PhaseOrchestrator,
|
|
5557
5945
|
PhaseStore
|
|
5558
5946
|
} from "@wrongstack/core/goal";
|
|
5947
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
5559
5948
|
import { WorktreeManager } from "@wrongstack/core/worktree";
|
|
5560
5949
|
|
|
5561
5950
|
// src/server/git-process.ts
|
|
@@ -5592,12 +5981,7 @@ function deriveTitle(goal) {
|
|
|
5592
5981
|
return trimmed || "Goal";
|
|
5593
5982
|
}
|
|
5594
5983
|
async function commitsSince(cwd, baseSha, branch) {
|
|
5595
|
-
const output = await gitStdout(cwd, [
|
|
5596
|
-
"log",
|
|
5597
|
-
"--reverse",
|
|
5598
|
-
"--format=%H",
|
|
5599
|
-
`${baseSha}..${branch}`
|
|
5600
|
-
]);
|
|
5984
|
+
const output = await gitStdout(cwd, ["log", "--reverse", "--format=%H", `${baseSha}..${branch}`]);
|
|
5601
5985
|
if (output === null) return [];
|
|
5602
5986
|
return output.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
5603
5987
|
}
|
|
@@ -5648,6 +6032,20 @@ var GoalWebSocketHandler = class {
|
|
|
5648
6032
|
ws.on("error", () => this.clients.delete(client));
|
|
5649
6033
|
this.sendState(client);
|
|
5650
6034
|
}
|
|
6035
|
+
/** Release timers, in-flight work, and socket references owned by this host. */
|
|
6036
|
+
dispose() {
|
|
6037
|
+
this.stopping = true;
|
|
6038
|
+
this.abort?.abort();
|
|
6039
|
+
this.abort = null;
|
|
6040
|
+
this.assessAbort?.abort();
|
|
6041
|
+
this.assessAbort = null;
|
|
6042
|
+
this.orchestrator?.stop();
|
|
6043
|
+
this.orchestrator = null;
|
|
6044
|
+
this.stopBroadcast();
|
|
6045
|
+
this.clients.clear();
|
|
6046
|
+
this.usedNicknames.clear();
|
|
6047
|
+
this.worktrees = null;
|
|
6048
|
+
}
|
|
5651
6049
|
async handleMessage(ws, msg) {
|
|
5652
6050
|
switch (msg.type) {
|
|
5653
6051
|
case "goal.assess":
|
|
@@ -5695,7 +6093,8 @@ var GoalWebSocketHandler = class {
|
|
|
5695
6093
|
}
|
|
5696
6094
|
case "goal.assignTask": {
|
|
5697
6095
|
const { taskId, agentId, agentName } = msg.payload;
|
|
5698
|
-
if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName))
|
|
6096
|
+
if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName))
|
|
6097
|
+
this.afterBoardMutation();
|
|
5699
6098
|
break;
|
|
5700
6099
|
}
|
|
5701
6100
|
case "goal.addTask": {
|
|
@@ -5740,7 +6139,10 @@ var GoalWebSocketHandler = class {
|
|
|
5740
6139
|
this.graph = graph;
|
|
5741
6140
|
this.broadcast({ type: "goal.state", payload: this.buildState() });
|
|
5742
6141
|
} else {
|
|
5743
|
-
this.broadcast({
|
|
6142
|
+
this.broadcast({
|
|
6143
|
+
type: "goal.error",
|
|
6144
|
+
payload: { message: `Graph not found: ${graphId}` }
|
|
6145
|
+
});
|
|
5744
6146
|
}
|
|
5745
6147
|
}
|
|
5746
6148
|
break;
|
|
@@ -5763,10 +6165,13 @@ var GoalWebSocketHandler = class {
|
|
|
5763
6165
|
const mySeq = ++this.assessSeq;
|
|
5764
6166
|
const sendResult7 = (result) => {
|
|
5765
6167
|
if (mySeq !== this.assessSeq) return;
|
|
5766
|
-
sendSerialized(
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
6168
|
+
sendSerialized(
|
|
6169
|
+
ws,
|
|
6170
|
+
JSON.stringify({
|
|
6171
|
+
type: "goal.assess.result",
|
|
6172
|
+
payload: { ...result, reqSeq: seq }
|
|
6173
|
+
})
|
|
6174
|
+
);
|
|
5770
6175
|
};
|
|
5771
6176
|
if (!goal.trim()) {
|
|
5772
6177
|
sendResult7({
|
|
@@ -5812,15 +6217,24 @@ var GoalWebSocketHandler = class {
|
|
|
5812
6217
|
const multiBoard = payload?.multiBoard ?? false;
|
|
5813
6218
|
const verifyTasks = payload?.verifyTasks ?? false;
|
|
5814
6219
|
const chimeraReview = payload?.chimeraReview ?? false;
|
|
5815
|
-
|
|
6220
|
+
const runAbort = new AbortController();
|
|
6221
|
+
this.abort = runAbort;
|
|
5816
6222
|
this.stopping = false;
|
|
5817
|
-
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal,
|
|
5818
|
-
if (this.stopping ||
|
|
6223
|
+
const phases = Array.isArray(payload?.phases) ? payload.phases : await this.planPhases(goal, runAbort.signal);
|
|
6224
|
+
if (this.stopping || runAbort.signal.aborted) {
|
|
5819
6225
|
this.broadcast({ type: "goal.stopped", payload: { title } });
|
|
5820
6226
|
return;
|
|
5821
6227
|
}
|
|
5822
6228
|
this.logger.info(`[Goal] Starting: ${title}`);
|
|
5823
|
-
const graph = await new PhaseGraphBuilder({
|
|
6229
|
+
const graph = await new PhaseGraphBuilder({
|
|
6230
|
+
title,
|
|
6231
|
+
description: goal,
|
|
6232
|
+
phases,
|
|
6233
|
+
autonomous,
|
|
6234
|
+
multiBoard,
|
|
6235
|
+
verifyTasks,
|
|
6236
|
+
chimeraReview
|
|
6237
|
+
}).build();
|
|
5824
6238
|
this.graph = graph;
|
|
5825
6239
|
await this.store.save(graph);
|
|
5826
6240
|
const useWorktrees = payload?.worktrees ?? process.env["WRONGSTACK_GOAL_WORKTREES"] !== "0";
|
|
@@ -5839,9 +6253,10 @@ var GoalWebSocketHandler = class {
|
|
|
5839
6253
|
maybeVerify.verifyPhase = (async (phase, env) => {
|
|
5840
6254
|
const cwd = env?.cwd ?? this.projectRoot;
|
|
5841
6255
|
try {
|
|
5842
|
-
const {
|
|
6256
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
5843
6257
|
const result = await new Promise((resolve15) => {
|
|
5844
|
-
|
|
6258
|
+
const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
6259
|
+
execFile2(npxCommand, ["tsc", "--noEmit"], { cwd, timeout: 6e4 }, (err, stdout, stderr) => {
|
|
5845
6260
|
if (err && err.code === "ENOENT") {
|
|
5846
6261
|
resolve15("[verify] tsc not found \u2014 skipping");
|
|
5847
6262
|
return;
|
|
@@ -5983,11 +6398,41 @@ Run npx tsc --noEmit to verify the fix. Output the fixed file paths.`;
|
|
|
5983
6398
|
/** Generic fallback phases when the LLM planner produces nothing usable. */
|
|
5984
6399
|
defaultPhases() {
|
|
5985
6400
|
return [
|
|
5986
|
-
{
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
6401
|
+
{
|
|
6402
|
+
name: "Discovery",
|
|
6403
|
+
description: "Requirements gathering",
|
|
6404
|
+
priority: "high",
|
|
6405
|
+
estimateHours: 2,
|
|
6406
|
+
parallelizable: false
|
|
6407
|
+
},
|
|
6408
|
+
{
|
|
6409
|
+
name: "Design",
|
|
6410
|
+
description: "Architecture and design",
|
|
6411
|
+
priority: "critical",
|
|
6412
|
+
estimateHours: 4,
|
|
6413
|
+
parallelizable: false
|
|
6414
|
+
},
|
|
6415
|
+
{
|
|
6416
|
+
name: "Implementation",
|
|
6417
|
+
description: "Core development",
|
|
6418
|
+
priority: "critical",
|
|
6419
|
+
estimateHours: 12,
|
|
6420
|
+
parallelizable: false
|
|
6421
|
+
},
|
|
6422
|
+
{
|
|
6423
|
+
name: "Testing",
|
|
6424
|
+
description: "Unit and integration tests",
|
|
6425
|
+
priority: "high",
|
|
6426
|
+
estimateHours: 6,
|
|
6427
|
+
parallelizable: true
|
|
6428
|
+
},
|
|
6429
|
+
{
|
|
6430
|
+
name: "Deployment",
|
|
6431
|
+
description: "Deploy to production",
|
|
6432
|
+
priority: "medium",
|
|
6433
|
+
estimateHours: 2,
|
|
6434
|
+
parallelizable: false
|
|
6435
|
+
}
|
|
5991
6436
|
];
|
|
5992
6437
|
}
|
|
5993
6438
|
/** Plan phases+todos for the goal via the LLM; fall back to defaults on failure.
|
|
@@ -6068,8 +6513,10 @@ Type: ${task.type}`;
|
|
|
6068
6513
|
try {
|
|
6069
6514
|
const result_ = await this.agent.run(reviewPrompt);
|
|
6070
6515
|
if (result_.status === "done" && result_.finalText) {
|
|
6071
|
-
this.logger.info(
|
|
6072
|
-
${
|
|
6516
|
+
this.logger.info(
|
|
6517
|
+
`[Goal] Chimera review for "${task.title}":
|
|
6518
|
+
${result_.finalText.slice(0, 2e3)}`
|
|
6519
|
+
);
|
|
6073
6520
|
}
|
|
6074
6521
|
} catch (err) {
|
|
6075
6522
|
this.logger.warn(`[Goal] Chimera review failed for "${task.title}": ${toErrorMessage2(err)}`);
|
|
@@ -6122,7 +6569,16 @@ ${result_.finalText.slice(0, 2e3)}`);
|
|
|
6122
6569
|
}
|
|
6123
6570
|
buildState(activePhaseId) {
|
|
6124
6571
|
if (!this.graph) {
|
|
6125
|
-
return {
|
|
6572
|
+
return {
|
|
6573
|
+
phases: [],
|
|
6574
|
+
tasks: [],
|
|
6575
|
+
overallPercent: 0,
|
|
6576
|
+
autonomous: true,
|
|
6577
|
+
title: "",
|
|
6578
|
+
multiBoard: false,
|
|
6579
|
+
verifyTasks: false,
|
|
6580
|
+
chimeraReview: false
|
|
6581
|
+
};
|
|
6126
6582
|
}
|
|
6127
6583
|
const phases = Array.from(this.graph.phases.values());
|
|
6128
6584
|
const currentActiveId = activePhaseId || phases.find((p) => p.status === "running")?.id || phases[0]?.id || "";
|
|
@@ -6620,88 +7076,13 @@ async function handleApiAnalyticsSummary(res) {
|
|
|
6620
7076
|
}
|
|
6621
7077
|
|
|
6622
7078
|
// src/server/http-server.ts
|
|
6623
|
-
import * as
|
|
7079
|
+
import * as fs10 from "node:fs/promises";
|
|
6624
7080
|
import * as http from "node:http";
|
|
6625
|
-
import * as
|
|
7081
|
+
import * as path13 from "node:path";
|
|
6626
7082
|
import * as v8 from "node:v8";
|
|
7083
|
+
import { sanitizeApiError as sanitizeApiError2 } from "@wrongstack/core/security";
|
|
6627
7084
|
import { getIndexState as getIndexState2 } from "@wrongstack/tools";
|
|
6628
7085
|
|
|
6629
|
-
// src/server/codemap-handlers.ts
|
|
6630
|
-
import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
|
|
6631
|
-
function sendJsonBody(res, status, body, cacheStatus) {
|
|
6632
|
-
res.writeHead(status, {
|
|
6633
|
-
"Content-Type": "application/json",
|
|
6634
|
-
"X-Codemap-Cache": cacheStatus
|
|
6635
|
-
});
|
|
6636
|
-
res.end(body);
|
|
6637
|
-
}
|
|
6638
|
-
function sendJson(res, status, data, cacheStatus = "BYPASS") {
|
|
6639
|
-
sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
|
|
6640
|
-
}
|
|
6641
|
-
async function serveCachedGraph(res, deps2, scope, compute) {
|
|
6642
|
-
try {
|
|
6643
|
-
const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
|
|
6644
|
-
const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
|
|
6645
|
-
const hit = getCachedCodemapBody(key, version);
|
|
6646
|
-
if (hit !== void 0) {
|
|
6647
|
-
sendJsonBody(res, 200, hit, "HIT");
|
|
6648
|
-
return;
|
|
6649
|
-
}
|
|
6650
|
-
const graph = await compute();
|
|
6651
|
-
const body = JSON.stringify(graph);
|
|
6652
|
-
if (version !== "missing") {
|
|
6653
|
-
setCachedCodemapBody(key, version, body);
|
|
6654
|
-
}
|
|
6655
|
-
sendJsonBody(res, 200, body, "MISS");
|
|
6656
|
-
} catch (err) {
|
|
6657
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6658
|
-
sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
|
|
6659
|
-
}
|
|
6660
|
-
}
|
|
6661
|
-
async function handleCodemapPackages(res, deps2) {
|
|
6662
|
-
await serveCachedGraph(
|
|
6663
|
-
res,
|
|
6664
|
-
deps2,
|
|
6665
|
-
"packages",
|
|
6666
|
-
() => packageGraphService({
|
|
6667
|
-
projectRoot: deps2.projectRoot,
|
|
6668
|
-
...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
|
|
6669
|
-
})
|
|
6670
|
-
);
|
|
6671
|
-
}
|
|
6672
|
-
async function handleCodemapFiles(res, deps2, pkg) {
|
|
6673
|
-
if (!pkg) {
|
|
6674
|
-
sendJson(res, 400, { error: 'Missing "package" query parameter' });
|
|
6675
|
-
return;
|
|
6676
|
-
}
|
|
6677
|
-
await serveCachedGraph(
|
|
6678
|
-
res,
|
|
6679
|
-
deps2,
|
|
6680
|
-
`files:${pkg}`,
|
|
6681
|
-
() => fileGraphService({
|
|
6682
|
-
projectRoot: deps2.projectRoot,
|
|
6683
|
-
packageFilter: pkg,
|
|
6684
|
-
...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
|
|
6685
|
-
})
|
|
6686
|
-
);
|
|
6687
|
-
}
|
|
6688
|
-
async function handleCodemapSymbols(res, deps2, file) {
|
|
6689
|
-
if (!file) {
|
|
6690
|
-
sendJson(res, 400, { error: 'Missing "file" query parameter' });
|
|
6691
|
-
return;
|
|
6692
|
-
}
|
|
6693
|
-
await serveCachedGraph(
|
|
6694
|
-
res,
|
|
6695
|
-
deps2,
|
|
6696
|
-
`symbols:${file}`,
|
|
6697
|
-
() => symbolGraphService({
|
|
6698
|
-
projectRoot: deps2.projectRoot,
|
|
6699
|
-
fileFilter: file,
|
|
6700
|
-
...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
|
|
6701
|
-
})
|
|
6702
|
-
);
|
|
6703
|
-
}
|
|
6704
|
-
|
|
6705
7086
|
// src/server/deadcode-handlers.ts
|
|
6706
7087
|
import * as path10 from "node:path";
|
|
6707
7088
|
import { runDeadCodeScan } from "@wrongstack/tools/codebase-index";
|
|
@@ -6852,7 +7233,84 @@ function buildActionPlan(result) {
|
|
|
6852
7233
|
};
|
|
6853
7234
|
}
|
|
6854
7235
|
|
|
7236
|
+
// src/server/codemap-handlers.ts
|
|
7237
|
+
import { fileGraphService, packageGraphService, symbolGraphService } from "@wrongstack/tools";
|
|
7238
|
+
function sendJsonBody(res, status, body, cacheStatus) {
|
|
7239
|
+
res.writeHead(status, {
|
|
7240
|
+
"Content-Type": "application/json",
|
|
7241
|
+
"X-Codemap-Cache": cacheStatus
|
|
7242
|
+
});
|
|
7243
|
+
res.end(body);
|
|
7244
|
+
}
|
|
7245
|
+
function sendJson(res, status, data, cacheStatus = "BYPASS") {
|
|
7246
|
+
sendJsonBody(res, status, JSON.stringify(data), cacheStatus);
|
|
7247
|
+
}
|
|
7248
|
+
async function serveCachedGraph(res, deps2, scope, compute) {
|
|
7249
|
+
try {
|
|
7250
|
+
const version = indexDbVersion(deps2.projectRoot, deps2.indexDir);
|
|
7251
|
+
const key = codemapCacheKey(deps2.projectRoot, deps2.indexDir, scope);
|
|
7252
|
+
const hit = getCachedCodemapBody(key, version);
|
|
7253
|
+
if (hit !== void 0) {
|
|
7254
|
+
sendJsonBody(res, 200, hit, "HIT");
|
|
7255
|
+
return;
|
|
7256
|
+
}
|
|
7257
|
+
const graph = await compute();
|
|
7258
|
+
const body = JSON.stringify(graph);
|
|
7259
|
+
if (version !== "missing") {
|
|
7260
|
+
setCachedCodemapBody(key, version, body);
|
|
7261
|
+
}
|
|
7262
|
+
sendJsonBody(res, 200, body, "MISS");
|
|
7263
|
+
} catch (err) {
|
|
7264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7265
|
+
sendJson(res, 503, { error: "CodeMap index unavailable", detail: msg }, "BYPASS");
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
async function handleCodemapPackages(res, deps2) {
|
|
7269
|
+
await serveCachedGraph(
|
|
7270
|
+
res,
|
|
7271
|
+
deps2,
|
|
7272
|
+
"packages",
|
|
7273
|
+
() => packageGraphService({
|
|
7274
|
+
projectRoot: deps2.projectRoot,
|
|
7275
|
+
...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
|
|
7276
|
+
})
|
|
7277
|
+
);
|
|
7278
|
+
}
|
|
7279
|
+
async function handleCodemapFiles(res, deps2, pkg) {
|
|
7280
|
+
if (!pkg) {
|
|
7281
|
+
sendJson(res, 400, { error: 'Missing "package" query parameter' });
|
|
7282
|
+
return;
|
|
7283
|
+
}
|
|
7284
|
+
await serveCachedGraph(
|
|
7285
|
+
res,
|
|
7286
|
+
deps2,
|
|
7287
|
+
`files:${pkg}`,
|
|
7288
|
+
() => fileGraphService({
|
|
7289
|
+
projectRoot: deps2.projectRoot,
|
|
7290
|
+
packageFilter: pkg,
|
|
7291
|
+
...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
|
|
7292
|
+
})
|
|
7293
|
+
);
|
|
7294
|
+
}
|
|
7295
|
+
async function handleCodemapSymbols(res, deps2, file) {
|
|
7296
|
+
if (!file) {
|
|
7297
|
+
sendJson(res, 400, { error: 'Missing "file" query parameter' });
|
|
7298
|
+
return;
|
|
7299
|
+
}
|
|
7300
|
+
await serveCachedGraph(
|
|
7301
|
+
res,
|
|
7302
|
+
deps2,
|
|
7303
|
+
`symbols:${file}`,
|
|
7304
|
+
() => symbolGraphService({
|
|
7305
|
+
projectRoot: deps2.projectRoot,
|
|
7306
|
+
fileFilter: file,
|
|
7307
|
+
...deps2.indexDir ? { indexDir: deps2.indexDir } : {}
|
|
7308
|
+
})
|
|
7309
|
+
);
|
|
7310
|
+
}
|
|
7311
|
+
|
|
6855
7312
|
// src/server/http-server/api-handlers.ts
|
|
7313
|
+
import { sanitizeApiError } from "@wrongstack/core/security";
|
|
6856
7314
|
async function handleApiSessions(res, globalRoot) {
|
|
6857
7315
|
if (!globalRoot) {
|
|
6858
7316
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -6888,7 +7346,7 @@ async function handleApiSessions(res, globalRoot) {
|
|
|
6888
7346
|
res.end(JSON.stringify(result));
|
|
6889
7347
|
} catch (err) {
|
|
6890
7348
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6891
|
-
res.end(JSON.stringify({ error:
|
|
7349
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
6892
7350
|
}
|
|
6893
7351
|
}
|
|
6894
7352
|
async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
@@ -6923,7 +7381,7 @@ async function handleApiSessionAgents(res, globalRoot, sessionId) {
|
|
|
6923
7381
|
}));
|
|
6924
7382
|
} catch (err) {
|
|
6925
7383
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
6926
|
-
res.end(JSON.stringify({ error:
|
|
7384
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
6927
7385
|
}
|
|
6928
7386
|
}
|
|
6929
7387
|
function blocksToText(content) {
|
|
@@ -7070,11 +7528,16 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
7070
7528
|
);
|
|
7071
7529
|
} catch (err) {
|
|
7072
7530
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
7073
|
-
res.end(JSON.stringify({ error:
|
|
7531
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
7074
7532
|
}
|
|
7075
7533
|
}
|
|
7076
7534
|
function readJsonBody2(req) {
|
|
7077
7535
|
return new Promise((resolve15, reject) => {
|
|
7536
|
+
const contentType = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
7537
|
+
if (contentType !== "application/json") {
|
|
7538
|
+
reject(new Error(`Unsupported Content-Type: ${contentType || "(absent)"}`));
|
|
7539
|
+
return;
|
|
7540
|
+
}
|
|
7078
7541
|
let data = "";
|
|
7079
7542
|
req.on("data", (chunk) => {
|
|
7080
7543
|
data += chunk;
|
|
@@ -7139,7 +7602,7 @@ async function handleApiSessionMessage(res, req, globalRoot, sessionId) {
|
|
|
7139
7602
|
res.end(JSON.stringify({ ok: true, id: sent.id, to, type, delivered: entry.status }));
|
|
7140
7603
|
} catch (err) {
|
|
7141
7604
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
7142
|
-
res.end(JSON.stringify({ error:
|
|
7605
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
7143
7606
|
}
|
|
7144
7607
|
}
|
|
7145
7608
|
async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
@@ -7192,7 +7655,7 @@ async function handleApiSessionMailbox(res, globalRoot, sessionId) {
|
|
|
7192
7655
|
res.end(JSON.stringify({ sessionId, leader: leaderAddr, status: entry.status, thread }));
|
|
7193
7656
|
} catch (err) {
|
|
7194
7657
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
7195
|
-
res.end(JSON.stringify({ error:
|
|
7658
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
7196
7659
|
}
|
|
7197
7660
|
}
|
|
7198
7661
|
async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
@@ -7233,7 +7696,7 @@ async function handleApiSessionInterrupt(res, req, globalRoot, sessionId) {
|
|
|
7233
7696
|
res.end(JSON.stringify({ ok: true, id: sent.id, to, delivered: entry.status }));
|
|
7234
7697
|
} catch (err) {
|
|
7235
7698
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
7236
|
-
res.end(JSON.stringify({ error:
|
|
7699
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
7237
7700
|
}
|
|
7238
7701
|
}
|
|
7239
7702
|
async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
@@ -7302,22 +7765,157 @@ async function handleApiFleetBroadcast(res, req, globalRoot) {
|
|
|
7302
7765
|
res.end(JSON.stringify({ ok: true, delivered, targets: targets.length }));
|
|
7303
7766
|
} catch (err) {
|
|
7304
7767
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
7305
|
-
res.end(JSON.stringify({ error:
|
|
7768
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
7306
7769
|
}
|
|
7307
7770
|
}
|
|
7308
7771
|
|
|
7309
|
-
// src/server/
|
|
7772
|
+
// src/server/memory-diagnostics.ts
|
|
7310
7773
|
import * as fs8 from "node:fs/promises";
|
|
7311
7774
|
import * as path11 from "node:path";
|
|
7775
|
+
var DEFAULT_TAIL_BYTES = 1024 * 1024;
|
|
7776
|
+
var MAX_PROCESSES = 32;
|
|
7777
|
+
function finiteNumber(record, key) {
|
|
7778
|
+
const value = record[key];
|
|
7779
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7780
|
+
}
|
|
7781
|
+
function stringValue(record, key) {
|
|
7782
|
+
const value = record[key];
|
|
7783
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
7784
|
+
}
|
|
7785
|
+
function toProcessDiagnostic(record) {
|
|
7786
|
+
const pid = finiteNumber(record, "pid");
|
|
7787
|
+
const ts = stringValue(record, "ts");
|
|
7788
|
+
if (pid === void 0 || ts === void 0) return null;
|
|
7789
|
+
const queueEntries = finiteNumber(record, "hqQueueEntries");
|
|
7790
|
+
const queueBytes = finiteNumber(record, "hqQueueBytes");
|
|
7791
|
+
const queueMaxBytes = finiteNumber(record, "hqQueueMaxBytes");
|
|
7792
|
+
const snapshotInFlight = finiteNumber(record, "hqSnapshotInFlight");
|
|
7793
|
+
const snapshotPending = finiteNumber(record, "hqSnapshotPending");
|
|
7794
|
+
const snapshotTimerScheduled = finiteNumber(record, "hqSnapshotTimerScheduled");
|
|
7795
|
+
const eventInFlight = finiteNumber(record, "hqEventInFlight");
|
|
7796
|
+
const eventPending = finiteNumber(record, "hqEventPending");
|
|
7797
|
+
const eventCoalesced = finiteNumber(record, "hqEventCoalesced");
|
|
7798
|
+
const eventDropped = finiteNumber(record, "hqEventDropped");
|
|
7799
|
+
return {
|
|
7800
|
+
pid,
|
|
7801
|
+
surface: stringValue(record, "surface") ?? "unknown",
|
|
7802
|
+
...stringValue(record, "sessionId") !== void 0 ? { sessionId: stringValue(record, "sessionId") } : {},
|
|
7803
|
+
ts,
|
|
7804
|
+
memory: {
|
|
7805
|
+
rss: finiteNumber(record, "rss") ?? 0,
|
|
7806
|
+
heapUsed: finiteNumber(record, "heapUsed") ?? 0,
|
|
7807
|
+
heapTotal: finiteNumber(record, "heapTotal") ?? 0,
|
|
7808
|
+
retainedHeapUsed: finiteNumber(record, "retainedHeapUsed"),
|
|
7809
|
+
nativeResidual: finiteNumber(record, "nativeResidual"),
|
|
7810
|
+
external: finiteNumber(record, "external"),
|
|
7811
|
+
arrayBuffers: finiteNumber(record, "arrayBuffers")
|
|
7812
|
+
},
|
|
7813
|
+
signal: stringValue(record, "memorySignal"),
|
|
7814
|
+
heapGrowthBytesPerHour: finiteNumber(record, "heapGrowthBytesPerHour"),
|
|
7815
|
+
rssGrowthBytesPerHour: finiteNumber(record, "rssGrowthBytesPerHour"),
|
|
7816
|
+
workload: {
|
|
7817
|
+
messages: finiteNumber(record, "messages"),
|
|
7818
|
+
messageEstimatedTokens: finiteNumber(record, "messageEstimatedTokens"),
|
|
7819
|
+
historyEntries: finiteNumber(record, "historyEntries"),
|
|
7820
|
+
historyMountedEntries: finiteNumber(record, "historyMountedEntries"),
|
|
7821
|
+
historyCachedGroups: finiteNumber(record, "historyCachedGroups"),
|
|
7822
|
+
appRenders: finiteNumber(record, "appRenders"),
|
|
7823
|
+
metricsDroppedObservations: finiteNumber(record, "metricsDroppedObservations"),
|
|
7824
|
+
kanbanSyncActive: finiteNumber(record, "kanbanSyncActive") === 1,
|
|
7825
|
+
kanbanSyncPendingBoards: finiteNumber(record, "kanbanSyncPendingBoards"),
|
|
7826
|
+
kanbanSyncFullRescanPending: finiteNumber(record, "kanbanSyncFullRescanPending") === 1,
|
|
7827
|
+
kanbanSyncRemoteApplyQueued: finiteNumber(record, "kanbanSyncRemoteApplyQueued") === 1,
|
|
7828
|
+
kanbanSyncPendingRemoteBoards: finiteNumber(record, "kanbanSyncPendingRemoteBoards"),
|
|
7829
|
+
kanbanSyncPublishRuns: finiteNumber(record, "kanbanSyncPublishRuns"),
|
|
7830
|
+
kanbanSyncCoalescedRefreshes: finiteNumber(record, "kanbanSyncCoalescedRefreshes"),
|
|
7831
|
+
kanbanSupervisorSnapshots: finiteNumber(record, "kanbanSupervisorSnapshots"),
|
|
7832
|
+
kanbanSupervisorScheduledBoards: finiteNumber(record, "kanbanSupervisorScheduledBoards"),
|
|
7833
|
+
kanbanSupervisorAgentCooldowns: finiteNumber(record, "kanbanSupervisorAgentCooldowns"),
|
|
7834
|
+
kanbanSupervisorRunningAgents: finiteNumber(record, "kanbanSupervisorRunningAgents")
|
|
7835
|
+
},
|
|
7836
|
+
resources: {
|
|
7837
|
+
active: finiteNumber(record, "activeResources"),
|
|
7838
|
+
types: stringValue(record, "activeResourceTypes")
|
|
7839
|
+
},
|
|
7840
|
+
...queueEntries !== void 0 && queueBytes !== void 0 && queueMaxBytes !== void 0 ? {
|
|
7841
|
+
hqQueue: {
|
|
7842
|
+
entries: queueEntries,
|
|
7843
|
+
bytes: queueBytes,
|
|
7844
|
+
maxBytes: queueMaxBytes,
|
|
7845
|
+
droppedFrames: finiteNumber(record, "hqQueueDroppedFrames") ?? 0,
|
|
7846
|
+
droppedBytes: finiteNumber(record, "hqQueueDroppedBytes") ?? 0,
|
|
7847
|
+
coalescedFrames: finiteNumber(record, "hqQueueCoalescedFrames") ?? 0,
|
|
7848
|
+
coalescedBytes: finiteNumber(record, "hqQueueCoalescedBytes") ?? 0
|
|
7849
|
+
}
|
|
7850
|
+
} : {},
|
|
7851
|
+
...snapshotInFlight !== void 0 || snapshotPending !== void 0 || snapshotTimerScheduled !== void 0 ? {
|
|
7852
|
+
hqSnapshot: {
|
|
7853
|
+
inFlight: snapshotInFlight === 1,
|
|
7854
|
+
pending: snapshotPending === 1,
|
|
7855
|
+
timerScheduled: snapshotTimerScheduled === 1,
|
|
7856
|
+
eventInFlight: eventInFlight === 1,
|
|
7857
|
+
pendingEvents: eventPending ?? 0,
|
|
7858
|
+
coalescedEvents: eventCoalesced ?? 0,
|
|
7859
|
+
droppedEvents: eventDropped ?? 0
|
|
7860
|
+
}
|
|
7861
|
+
} : {},
|
|
7862
|
+
profileTopStack: stringValue(record, "memoryProfileTopStack"),
|
|
7863
|
+
profileTopStackBytes: finiteNumber(record, "memoryProfileTopStackBytes")
|
|
7864
|
+
};
|
|
7865
|
+
}
|
|
7866
|
+
async function readRecentProcessMemoryDiagnostics(globalRoot, tailBytes = DEFAULT_TAIL_BYTES) {
|
|
7867
|
+
if (!globalRoot) return [];
|
|
7868
|
+
const heapLog = path11.join(globalRoot, "logs", "heap.jsonl");
|
|
7869
|
+
let handle;
|
|
7870
|
+
try {
|
|
7871
|
+
handle = await fs8.open(heapLog, "r");
|
|
7872
|
+
const stat3 = await handle.stat();
|
|
7873
|
+
const length = Math.min(stat3.size, Math.max(1, tailBytes));
|
|
7874
|
+
if (length === 0) return [];
|
|
7875
|
+
const start = stat3.size - length;
|
|
7876
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
7877
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
7878
|
+
let text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
7879
|
+
if (start > 0) {
|
|
7880
|
+
const firstNewline = text.indexOf("\n");
|
|
7881
|
+
if (firstNewline === -1) return [];
|
|
7882
|
+
text = text.slice(firstNewline + 1);
|
|
7883
|
+
}
|
|
7884
|
+
const newestByPid = /* @__PURE__ */ new Map();
|
|
7885
|
+
const lines = text.split(/\r?\n/u);
|
|
7886
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
7887
|
+
const line = lines[index]?.trim();
|
|
7888
|
+
if (!line) continue;
|
|
7889
|
+
try {
|
|
7890
|
+
const parsed = JSON.parse(line);
|
|
7891
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
|
|
7892
|
+
const diagnostic = toProcessDiagnostic(parsed);
|
|
7893
|
+
if (!diagnostic || newestByPid.has(diagnostic.pid)) continue;
|
|
7894
|
+
newestByPid.set(diagnostic.pid, diagnostic);
|
|
7895
|
+
if (newestByPid.size >= MAX_PROCESSES) break;
|
|
7896
|
+
} catch {
|
|
7897
|
+
}
|
|
7898
|
+
}
|
|
7899
|
+
return [...newestByPid.values()].sort((a, b) => b.ts.localeCompare(a.ts));
|
|
7900
|
+
} catch {
|
|
7901
|
+
return [];
|
|
7902
|
+
} finally {
|
|
7903
|
+
await handle?.close().catch(() => void 0);
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
|
|
7907
|
+
// src/server/projects-manifest.ts
|
|
7908
|
+
import * as fs9 from "node:fs/promises";
|
|
7909
|
+
import * as path12 from "node:path";
|
|
7312
7910
|
import { ConfigError } from "@wrongstack/core/types";
|
|
7313
7911
|
import { projectSlug, withFileLock } from "@wrongstack/core/utils";
|
|
7314
7912
|
function projectsJsonPath(globalConfigPath) {
|
|
7315
|
-
const base =
|
|
7316
|
-
return
|
|
7913
|
+
const base = path12.dirname(globalConfigPath);
|
|
7914
|
+
return path12.join(base, "projects.json");
|
|
7317
7915
|
}
|
|
7318
7916
|
async function loadManifest(globalConfigPath) {
|
|
7319
7917
|
try {
|
|
7320
|
-
const raw = await
|
|
7918
|
+
const raw = await fs9.readFile(projectsJsonPath(globalConfigPath), "utf8");
|
|
7321
7919
|
const parsed = JSON.parse(raw);
|
|
7322
7920
|
return { projects: parsed.projects ?? [] };
|
|
7323
7921
|
} catch {
|
|
@@ -7326,37 +7924,37 @@ async function loadManifest(globalConfigPath) {
|
|
|
7326
7924
|
}
|
|
7327
7925
|
async function saveManifest(manifest, globalConfigPath) {
|
|
7328
7926
|
const file = projectsJsonPath(globalConfigPath);
|
|
7329
|
-
await
|
|
7330
|
-
await
|
|
7927
|
+
await fs9.mkdir(path12.dirname(file), { recursive: true });
|
|
7928
|
+
await fs9.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
7331
7929
|
}
|
|
7332
7930
|
function generateProjectSlug(rootPath) {
|
|
7333
7931
|
return projectSlug(rootPath);
|
|
7334
7932
|
}
|
|
7335
7933
|
async function ensureProjectDataDir(slug, globalConfigPath) {
|
|
7336
|
-
const base =
|
|
7337
|
-
const dir =
|
|
7338
|
-
await
|
|
7934
|
+
const base = path12.dirname(globalConfigPath);
|
|
7935
|
+
const dir = path12.join(base, "projects", slug);
|
|
7936
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
7339
7937
|
return dir;
|
|
7340
7938
|
}
|
|
7341
7939
|
async function touchProjectInManifest(options, globalConfigPath) {
|
|
7342
|
-
const root =
|
|
7940
|
+
const root = path12.resolve(options.projectRoot);
|
|
7343
7941
|
const file = projectsJsonPath(globalConfigPath);
|
|
7344
7942
|
let entry;
|
|
7345
7943
|
await withFileLock(file, async () => {
|
|
7346
7944
|
const manifest = await loadManifest(globalConfigPath);
|
|
7347
7945
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7348
|
-
entry = manifest.projects.find((candidate) =>
|
|
7946
|
+
entry = manifest.projects.find((candidate) => path12.resolve(candidate.root) === root);
|
|
7349
7947
|
if (entry) {
|
|
7350
7948
|
entry.lastSeen = now;
|
|
7351
|
-
if (options.workingDir) entry.lastWorkingDir =
|
|
7949
|
+
if (options.workingDir) entry.lastWorkingDir = path12.resolve(options.workingDir);
|
|
7352
7950
|
} else {
|
|
7353
7951
|
entry = {
|
|
7354
|
-
name: options.name ??
|
|
7952
|
+
name: options.name ?? path12.basename(root),
|
|
7355
7953
|
root,
|
|
7356
7954
|
slug: generateProjectSlug(root),
|
|
7357
7955
|
createdAt: now,
|
|
7358
7956
|
lastSeen: now,
|
|
7359
|
-
lastWorkingDir: options.workingDir ?
|
|
7957
|
+
lastWorkingDir: options.workingDir ? path12.resolve(options.workingDir) : void 0
|
|
7360
7958
|
};
|
|
7361
7959
|
manifest.projects.push(entry);
|
|
7362
7960
|
}
|
|
@@ -7598,11 +8196,20 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
7598
8196
|
function isLoopbackHostname(hostname) {
|
|
7599
8197
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
7600
8198
|
}
|
|
7601
|
-
function
|
|
8199
|
+
function effectivePort(url) {
|
|
8200
|
+
if (url.port) return url.port;
|
|
8201
|
+
return url.protocol === "https:" ? "443" : "80";
|
|
8202
|
+
}
|
|
8203
|
+
function isTrustedLoopbackOrigin(origin, hostHeader) {
|
|
7602
8204
|
try {
|
|
7603
8205
|
const url = new URL(origin);
|
|
7604
8206
|
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
7605
|
-
|
|
8207
|
+
if (!isLoopbackHostname(url.hostname)) return false;
|
|
8208
|
+
const host = (hostHeader ?? "").trim();
|
|
8209
|
+
if (!host) return false;
|
|
8210
|
+
const hostUrl = new URL(`${url.protocol}//${host}`);
|
|
8211
|
+
if (!isLoopbackHostname(hostUrl.hostname)) return false;
|
|
8212
|
+
return effectivePort(url) === effectivePort(hostUrl);
|
|
7606
8213
|
} catch {
|
|
7607
8214
|
return false;
|
|
7608
8215
|
}
|
|
@@ -7661,6 +8268,23 @@ function hostHeaderOk(input) {
|
|
|
7661
8268
|
}
|
|
7662
8269
|
return isLoopbackHostname(hostname) || allowedHostname(hostname, input.allowedHostnames);
|
|
7663
8270
|
}
|
|
8271
|
+
function httpRequestOriginOk(input) {
|
|
8272
|
+
const { origin, hostHeader, wsHost, allowedHostnames } = input;
|
|
8273
|
+
if (!hostHeaderOk({ hostHeader, wsHost, allowedHostnames })) return false;
|
|
8274
|
+
if (origin === void 0 || origin === "") return true;
|
|
8275
|
+
if (origin === "null") return false;
|
|
8276
|
+
try {
|
|
8277
|
+
const parsed = new URL(origin);
|
|
8278
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
8279
|
+
const rawHost = (hostHeader ?? "").trim();
|
|
8280
|
+
if (!rawHost) return false;
|
|
8281
|
+
const requestHost = new URL(`${parsed.protocol}//${rawHost}`).host.toLowerCase();
|
|
8282
|
+
if (parsed.host.toLowerCase() === requestHost) return true;
|
|
8283
|
+
return allowedHostname(parsed.hostname, allowedHostnames);
|
|
8284
|
+
} catch {
|
|
8285
|
+
return false;
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
7664
8288
|
function verifyClient(input) {
|
|
7665
8289
|
const {
|
|
7666
8290
|
origin,
|
|
@@ -7681,13 +8305,13 @@ function verifyClient(input) {
|
|
|
7681
8305
|
const remoteIp = remoteAddress ?? "";
|
|
7682
8306
|
const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
|
|
7683
8307
|
if (!isRemoteLoopback && isWildcardBind(wsHost) && !urlTokenOk && !cookieTokenOk) return false;
|
|
7684
|
-
return urlTokenOk || cookieTokenOk
|
|
8308
|
+
return urlTokenOk || cookieTokenOk;
|
|
7685
8309
|
}
|
|
7686
8310
|
try {
|
|
7687
8311
|
const { hostname: originHostname } = new URL(origin);
|
|
7688
8312
|
if (isLoopbackHostname(originHostname)) {
|
|
7689
8313
|
if (requireToken || !isLoopbackBind(wsHost)) return cookieTokenOk;
|
|
7690
|
-
return isTrustedLoopbackOrigin(origin);
|
|
8314
|
+
return cookieTokenOk || isTrustedLoopbackOrigin(origin, hostHeader);
|
|
7691
8315
|
}
|
|
7692
8316
|
return cookieTokenOk || Boolean(allowBrowserUrlToken) && urlTokenOk && allowedHostname(originHostname, allowedHostnames);
|
|
7693
8317
|
} catch {
|
|
@@ -7749,7 +8373,7 @@ function cspSourceFromUrl(rawUrl) {
|
|
|
7749
8373
|
return void 0;
|
|
7750
8374
|
}
|
|
7751
8375
|
}
|
|
7752
|
-
var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'"
|
|
8376
|
+
var EXTRA_SCRIPT_SOURCES = ["'wasm-unsafe-eval'"];
|
|
7753
8377
|
function buildCspHeader(publicWsUrl, host, port) {
|
|
7754
8378
|
const connect = /* @__PURE__ */ new Set(["'self'"]);
|
|
7755
8379
|
const publicWsSource = publicWsUrl ? cspSourceFromUrl(publicWsUrl) : void 0;
|
|
@@ -7767,9 +8391,9 @@ function buildCspHeader(publicWsUrl, host, port) {
|
|
|
7767
8391
|
return `default-src 'self'; script-src ${scriptSrc}; style-src 'self' 'unsafe-inline'; connect-src ${Array.from(connect).join(" ")}; img-src 'self' data:; font-src 'self' data:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
|
|
7768
8392
|
}
|
|
7769
8393
|
function isInsideDist(candidate, distDir) {
|
|
7770
|
-
const root =
|
|
7771
|
-
const resolved =
|
|
7772
|
-
return resolved === root || resolved.startsWith(root +
|
|
8394
|
+
const root = path13.resolve(distDir);
|
|
8395
|
+
const resolved = path13.resolve(candidate);
|
|
8396
|
+
return resolved === root || resolved.startsWith(root + path13.sep);
|
|
7773
8397
|
}
|
|
7774
8398
|
function decodeSessionId(segment) {
|
|
7775
8399
|
try {
|
|
@@ -7789,8 +8413,18 @@ function strictDecodeParam(segment, res) {
|
|
|
7789
8413
|
}
|
|
7790
8414
|
function createHttpServer(opts) {
|
|
7791
8415
|
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
7792
|
-
const distDir =
|
|
8416
|
+
const distDir = path13.resolve(opts.distDir);
|
|
7793
8417
|
const requireAccessToken = Boolean(opts.requireToken) || !isLoopbackBind(opts.host);
|
|
8418
|
+
const trustedHostnames = (() => {
|
|
8419
|
+
const names = [...opts.allowedHostnames ?? []];
|
|
8420
|
+
if (opts.publicWsUrl) {
|
|
8421
|
+
try {
|
|
8422
|
+
names.push(new URL(opts.publicWsUrl).hostname);
|
|
8423
|
+
} catch {
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
return names;
|
|
8427
|
+
})();
|
|
7794
8428
|
let techStackRuntime = null;
|
|
7795
8429
|
const getTechStackRuntime = async () => {
|
|
7796
8430
|
if (!opts.projectRoot) throw new Error("Project root not configured");
|
|
@@ -7805,6 +8439,16 @@ function createHttpServer(opts) {
|
|
|
7805
8439
|
const server = http.createServer(async (req, res) => {
|
|
7806
8440
|
try {
|
|
7807
8441
|
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
8442
|
+
if (!httpRequestOriginOk({
|
|
8443
|
+
origin: req.headers.origin,
|
|
8444
|
+
hostHeader: req.headers.host,
|
|
8445
|
+
wsHost: opts.host,
|
|
8446
|
+
allowedHostnames: trustedHostnames
|
|
8447
|
+
})) {
|
|
8448
|
+
res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
8449
|
+
res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
|
|
8450
|
+
return;
|
|
8451
|
+
}
|
|
7808
8452
|
const providedAccessToken = requestToken(req, url);
|
|
7809
8453
|
const accessTokenOk = Boolean(opts.apiToken) && tokenMatches(providedAccessToken, opts.apiToken ?? "");
|
|
7810
8454
|
const shouldSetAuthCookie = Boolean(opts.apiToken) && tokenMatches(url.searchParams.get("token") ?? void 0, opts.apiToken ?? "");
|
|
@@ -8008,42 +8652,6 @@ function createHttpServer(opts) {
|
|
|
8008
8652
|
);
|
|
8009
8653
|
return;
|
|
8010
8654
|
}
|
|
8011
|
-
if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
|
|
8012
|
-
if (requireAccessToken && !accessTokenOk) {
|
|
8013
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
8014
|
-
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
8015
|
-
return;
|
|
8016
|
-
}
|
|
8017
|
-
if (!opts.projectRoot) {
|
|
8018
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
8019
|
-
res.end(JSON.stringify({ error: "Project root not configured" }));
|
|
8020
|
-
return;
|
|
8021
|
-
}
|
|
8022
|
-
const deadCodeDeps = {
|
|
8023
|
-
projectRoot: opts.projectRoot,
|
|
8024
|
-
...opts.indexDir ? { indexDir: opts.indexDir } : {}
|
|
8025
|
-
};
|
|
8026
|
-
await handleDeadCodeScan(res, deadCodeDeps, req);
|
|
8027
|
-
return;
|
|
8028
|
-
}
|
|
8029
|
-
if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
|
|
8030
|
-
if (requireAccessToken && !accessTokenOk) {
|
|
8031
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
8032
|
-
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
8033
|
-
return;
|
|
8034
|
-
}
|
|
8035
|
-
if (!opts.projectRoot) {
|
|
8036
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
8037
|
-
res.end(JSON.stringify({ error: "Project root not configured" }));
|
|
8038
|
-
return;
|
|
8039
|
-
}
|
|
8040
|
-
const deadCodeDeps = {
|
|
8041
|
-
projectRoot: opts.projectRoot,
|
|
8042
|
-
...opts.indexDir ? { indexDir: opts.indexDir } : {}
|
|
8043
|
-
};
|
|
8044
|
-
await handleDeadCodeActionPlan(res, deadCodeDeps, req);
|
|
8045
|
-
return;
|
|
8046
|
-
}
|
|
8047
8655
|
if (url.pathname.startsWith("/api/techstack/")) {
|
|
8048
8656
|
if (requireAccessToken && !accessTokenOk) {
|
|
8049
8657
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
@@ -8117,11 +8725,7 @@ function createHttpServer(opts) {
|
|
|
8117
8725
|
if (researchMatch && req.method === "POST") {
|
|
8118
8726
|
const pkg = strictDecodeParam(researchMatch[1], res);
|
|
8119
8727
|
if (pkg === null) return;
|
|
8120
|
-
await handleTechStackDependencyResearch(
|
|
8121
|
-
res,
|
|
8122
|
-
deps2,
|
|
8123
|
-
pkg
|
|
8124
|
-
);
|
|
8728
|
+
await handleTechStackDependencyResearch(res, deps2, pkg);
|
|
8125
8729
|
return;
|
|
8126
8730
|
}
|
|
8127
8731
|
} catch (error2) {
|
|
@@ -8129,7 +8733,7 @@ function createHttpServer(opts) {
|
|
|
8129
8733
|
res.end(
|
|
8130
8734
|
JSON.stringify({
|
|
8131
8735
|
error: "TechStack store unavailable",
|
|
8132
|
-
detail:
|
|
8736
|
+
detail: sanitizeApiError2(error2)
|
|
8133
8737
|
})
|
|
8134
8738
|
);
|
|
8135
8739
|
return;
|
|
@@ -8157,6 +8761,7 @@ function createHttpServer(opts) {
|
|
|
8157
8761
|
return;
|
|
8158
8762
|
}
|
|
8159
8763
|
if (url.pathname === "/debug/system" && req.method === "GET") {
|
|
8764
|
+
const processes = await readRecentProcessMemoryDiagnostics(opts.globalRoot);
|
|
8160
8765
|
res.writeHead(200, {
|
|
8161
8766
|
"Content-Type": "application/json",
|
|
8162
8767
|
"Cache-Control": "no-store"
|
|
@@ -8169,31 +8774,56 @@ function createHttpServer(opts) {
|
|
|
8169
8774
|
uptime: process.uptime(),
|
|
8170
8775
|
cpuUsage: process.cpuUsage(),
|
|
8171
8776
|
codebaseIndexServer: getIndexState2().server,
|
|
8777
|
+
processes,
|
|
8172
8778
|
timestamp: Date.now()
|
|
8173
8779
|
})
|
|
8174
8780
|
);
|
|
8175
8781
|
return;
|
|
8176
8782
|
}
|
|
8783
|
+
if (url.pathname === "/api/deadcode/scan" && req.method === "POST") {
|
|
8784
|
+
if (!opts.projectRoot) {
|
|
8785
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
8786
|
+
res.end(JSON.stringify({ error: "Project root not configured" }));
|
|
8787
|
+
return;
|
|
8788
|
+
}
|
|
8789
|
+
await handleDeadCodeScan(res, {
|
|
8790
|
+
projectRoot: opts.projectRoot,
|
|
8791
|
+
indexDir: opts.indexDir
|
|
8792
|
+
}, req);
|
|
8793
|
+
return;
|
|
8794
|
+
}
|
|
8795
|
+
if (url.pathname === "/api/deadcode/action-plan" && req.method === "POST") {
|
|
8796
|
+
await handleDeadCodeActionPlan(res, {
|
|
8797
|
+
projectRoot: opts.projectRoot ?? "",
|
|
8798
|
+
indexDir: opts.indexDir
|
|
8799
|
+
}, req);
|
|
8800
|
+
return;
|
|
8801
|
+
}
|
|
8802
|
+
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
|
|
8803
|
+
res.writeHead(404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
8804
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
8805
|
+
return;
|
|
8806
|
+
}
|
|
8177
8807
|
let filePath;
|
|
8178
8808
|
if (url.pathname === "/" || url.pathname === "") {
|
|
8179
|
-
filePath =
|
|
8809
|
+
filePath = path13.join(distDir, "index.html");
|
|
8180
8810
|
} else {
|
|
8181
|
-
filePath =
|
|
8811
|
+
filePath = path13.join(distDir, url.pathname);
|
|
8182
8812
|
}
|
|
8183
|
-
const resolvedPath =
|
|
8813
|
+
const resolvedPath = path13.resolve(filePath);
|
|
8184
8814
|
if (!isInsideDist(resolvedPath, distDir)) {
|
|
8185
8815
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
8186
8816
|
res.end("Forbidden");
|
|
8187
8817
|
return;
|
|
8188
8818
|
}
|
|
8189
|
-
const ext =
|
|
8819
|
+
const ext = path13.extname(resolvedPath);
|
|
8190
8820
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
8191
8821
|
res.setHeader("Content-Type", contentType);
|
|
8192
8822
|
setStaticSecurityHeaders(res);
|
|
8193
8823
|
if (ext === ".html") {
|
|
8194
8824
|
if (!shouldSetAuthCookie) res.setHeader("Cache-Control", "no-cache");
|
|
8195
8825
|
res.setHeader("Content-Security-Policy", buildCspHeader(opts.publicWsUrl, opts.host, port));
|
|
8196
|
-
const html = await
|
|
8826
|
+
const html = await fs10.readFile(resolvedPath, "utf8");
|
|
8197
8827
|
res.writeHead(200);
|
|
8198
8828
|
res.end(injectWsConfig(html, { publicWsUrl: opts.publicWsUrl }));
|
|
8199
8829
|
return;
|
|
@@ -8204,13 +8834,13 @@ function createHttpServer(opts) {
|
|
|
8204
8834
|
url.pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=3600"
|
|
8205
8835
|
);
|
|
8206
8836
|
}
|
|
8207
|
-
const fileContent = await
|
|
8837
|
+
const fileContent = await fs10.readFile(resolvedPath);
|
|
8208
8838
|
res.writeHead(200);
|
|
8209
8839
|
res.end(fileContent);
|
|
8210
8840
|
} catch (err) {
|
|
8211
8841
|
if (err.code === "ENOENT") {
|
|
8212
8842
|
try {
|
|
8213
|
-
const html = await
|
|
8843
|
+
const html = await fs10.readFile(path13.join(distDir, "index.html"), "utf8");
|
|
8214
8844
|
setStaticSecurityHeaders(res);
|
|
8215
8845
|
res.writeHead(200, {
|
|
8216
8846
|
"Content-Type": "text/html",
|
|
@@ -8240,14 +8870,14 @@ function createHttpServer(opts) {
|
|
|
8240
8870
|
|
|
8241
8871
|
// src/server/instance-registry.ts
|
|
8242
8872
|
import * as os from "node:os";
|
|
8243
|
-
import * as
|
|
8244
|
-
import * as
|
|
8873
|
+
import * as path14 from "node:path";
|
|
8874
|
+
import * as fs11 from "node:fs/promises";
|
|
8245
8875
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
8246
8876
|
function defaultBaseDir() {
|
|
8247
|
-
return
|
|
8877
|
+
return path14.join(os.homedir(), ".wrongstack");
|
|
8248
8878
|
}
|
|
8249
8879
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
8250
|
-
return
|
|
8880
|
+
return path14.join(baseDir, "webui-instances.json");
|
|
8251
8881
|
}
|
|
8252
8882
|
function isPidAlive(pid) {
|
|
8253
8883
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -8260,7 +8890,7 @@ function isPidAlive(pid) {
|
|
|
8260
8890
|
}
|
|
8261
8891
|
async function load(file) {
|
|
8262
8892
|
try {
|
|
8263
|
-
const raw = await
|
|
8893
|
+
const raw = await fs11.readFile(file, "utf8");
|
|
8264
8894
|
const parsed = JSON.parse(raw);
|
|
8265
8895
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
8266
8896
|
return parsed;
|
|
@@ -8472,6 +9102,11 @@ async function handleIntrospectionRoute(ctx, ws, message) {
|
|
|
8472
9102
|
cwd: ctx.getProjectRoot(),
|
|
8473
9103
|
sessionId: ctx.getSessionId(),
|
|
8474
9104
|
tools: { count: tools.length, names: tools.map((tool) => tool.name) },
|
|
9105
|
+
maxTools: actx.provider.maxToolsCount ?? 0,
|
|
9106
|
+
droppedTools: (() => {
|
|
9107
|
+
const mt = actx.provider.maxToolsCount ?? 0;
|
|
9108
|
+
return mt > 0 ? Math.max(0, tools.length - mt) : 0;
|
|
9109
|
+
})(),
|
|
8475
9110
|
features: {
|
|
8476
9111
|
memory: !!config.features?.memory,
|
|
8477
9112
|
skills: !!config.features?.skills,
|
|
@@ -8573,40 +9208,17 @@ async function handleIntrospectionRoute(ctx, ws, message) {
|
|
|
8573
9208
|
}
|
|
8574
9209
|
|
|
8575
9210
|
// src/server/kanban-dispatch.ts
|
|
8576
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8577
9211
|
import {
|
|
8578
9212
|
areDependenciesMet,
|
|
8579
9213
|
getServerKanbanStore
|
|
8580
9214
|
} from "@wrongstack/kanban";
|
|
8581
9215
|
import { recordKanbanVerificationEvidence } from "@wrongstack/tools";
|
|
8582
|
-
function parseResolvedDispatchRoute(summary) {
|
|
8583
|
-
const tags = summary.match(/Spawned subagent\s+\S+\s+\((.*?)\)\s+for task/i)?.[1];
|
|
8584
|
-
if (!tags) return {};
|
|
8585
|
-
const parts = tags.split(/\s+\/\s+/).map((part) => part.trim());
|
|
8586
|
-
const positional = parts.filter((part) => !part.includes("=") && !part.startsWith('"'));
|
|
8587
|
-
const fallbackProfile = parts.find((part) => part.startsWith("profile="))?.slice(8);
|
|
8588
|
-
const fallback = parts.find((part) => part.startsWith("fallback="))?.slice(9);
|
|
8589
|
-
return {
|
|
8590
|
-
...positional[0] ? { provider: positional[0] } : {},
|
|
8591
|
-
...positional[1] ? { model: positional[1] } : {},
|
|
8592
|
-
...fallbackProfile ? { fallbackProfile } : {},
|
|
8593
|
-
...fallback ? { fallbackModels: fallback.split(",").filter(Boolean) } : {}
|
|
8594
|
-
};
|
|
8595
|
-
}
|
|
8596
9216
|
function reply(ws, type, success, value) {
|
|
8597
9217
|
send(ws, {
|
|
8598
9218
|
type,
|
|
8599
9219
|
payload: success ? { success: true, data: value } : { success: false, error: String(value) }
|
|
8600
9220
|
});
|
|
8601
9221
|
}
|
|
8602
|
-
function activityContext(ctx, note, expectedLeaseId) {
|
|
8603
|
-
const sessionId = ctx.context?.session?.id;
|
|
8604
|
-
return {
|
|
8605
|
-
...sessionId ? { sessionId } : {},
|
|
8606
|
-
...note?.trim() ? { note: note.trim() } : {},
|
|
8607
|
-
...expectedLeaseId ? { expectedLeaseId } : {}
|
|
8608
|
-
};
|
|
8609
|
-
}
|
|
8610
9222
|
async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
8611
9223
|
const boardId = payload?.boardId;
|
|
8612
9224
|
const taskId = payload?.taskId;
|
|
@@ -8645,45 +9257,39 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
8645
9257
|
}
|
|
8646
9258
|
const modelRouting = payload?.modelRouting ?? task.assignment?.modelRouting ?? (payload?.provider || payload?.model || task.assignment?.provider || task.assignment?.model ? "fixed" : "session");
|
|
8647
9259
|
const useSessionModel = modelRouting === "session";
|
|
8648
|
-
const leaseId = randomUUID3();
|
|
8649
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8650
9260
|
const leaseTtlMs = 30 * 60 * 1e3;
|
|
8651
|
-
const
|
|
8652
|
-
const assignment = {
|
|
8653
|
-
agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
|
|
8654
|
-
name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
|
|
8655
|
-
role: payload?.role ?? task.assignment?.role,
|
|
8656
|
-
modelRouting,
|
|
8657
|
-
provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
|
|
8658
|
-
model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
|
|
8659
|
-
fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
|
|
8660
|
-
fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
|
|
8661
|
-
skills: payload?.skills ?? task.assignment?.skills,
|
|
8662
|
-
tools: payload?.tools ?? task.assignment?.tools,
|
|
8663
|
-
allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities,
|
|
8664
|
-
maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
|
|
8665
|
-
costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
|
|
8666
|
-
retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy,
|
|
8667
|
-
attempt: (task.assignment?.attempt ?? 0) + 1,
|
|
8668
|
-
status: "queued",
|
|
8669
|
-
dispatchedAt: now,
|
|
8670
|
-
leaseId,
|
|
8671
|
-
claimedAt: now,
|
|
8672
|
-
heartbeatAt: now,
|
|
8673
|
-
leaseExpiresAt
|
|
8674
|
-
};
|
|
8675
|
-
const assignedBoard = await store.assignTask(
|
|
9261
|
+
const reserved = await store.reserveKanbanDispatch({
|
|
8676
9262
|
boardId,
|
|
8677
|
-
task.id,
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
9263
|
+
taskId: task.id,
|
|
9264
|
+
routing: {
|
|
9265
|
+
agentId: payload?.agentId ?? task.assignment?.agentId ?? task.assignedAgent,
|
|
9266
|
+
name: payload?.name ?? task.assignment?.name ?? task.assignedAgent,
|
|
9267
|
+
role: payload?.role ?? task.assignment?.role,
|
|
9268
|
+
modelRouting,
|
|
9269
|
+
provider: useSessionModel ? ctx.context?.provider.id : payload?.provider ?? task.assignment?.provider,
|
|
9270
|
+
model: useSessionModel ? ctx.context?.model : payload?.model ?? task.assignment?.model,
|
|
9271
|
+
fallbackProfile: modelRouting === "fallback_profile" ? payload?.fallbackProfile ?? task.assignment?.fallbackProfile : void 0,
|
|
9272
|
+
fallbackModels: payload?.fallbackModels ?? task.assignment?.fallbackModels,
|
|
9273
|
+
skills: payload?.skills ?? task.assignment?.skills,
|
|
9274
|
+
tools: payload?.tools ?? task.assignment?.tools,
|
|
9275
|
+
allowedCapabilities: payload?.allowedCapabilities ?? task.assignment?.allowedCapabilities
|
|
9276
|
+
},
|
|
9277
|
+
budget: {
|
|
9278
|
+
maxAttempts: payload?.maxAttempts ?? task.assignment?.maxAttempts,
|
|
9279
|
+
costCeilingUsd: payload?.costCeilingUsd ?? task.assignment?.costCeilingUsd ?? task.costCeilingUsd,
|
|
9280
|
+
retryPolicy: payload?.retryPolicy ?? task.assignment?.retryPolicy ?? task.retryPolicy
|
|
9281
|
+
},
|
|
9282
|
+
leaseTtlMs,
|
|
9283
|
+
heartbeatIntervalMs: Math.floor(leaseTtlMs / 2)
|
|
9284
|
+
});
|
|
9285
|
+
if (!reserved) {
|
|
9286
|
+
reply(ws, "kanban.task.dispatch", false, `Failed to reserve task "${task.title}". The task may have been claimed by another agent or removed.`);
|
|
8683
9287
|
return;
|
|
8684
9288
|
}
|
|
9289
|
+
const dispatchLeaseId = reserved.lease.leaseId;
|
|
8685
9290
|
try {
|
|
8686
|
-
const
|
|
9291
|
+
const assignment = reserved.task.assignment ?? { status: "queued" };
|
|
9292
|
+
const summary = await ctx.dispatchTask(buildKanbanAgentPrompt(board, task, assignment, dispatchLeaseId), {
|
|
8687
9293
|
...assignment.provider ? { provider: assignment.provider } : {},
|
|
8688
9294
|
...assignment.model ? { model: assignment.model } : {},
|
|
8689
9295
|
...assignment.fallbackModels ? { fallbackModels: assignment.fallbackModels } : {},
|
|
@@ -8692,69 +9298,40 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
8692
9298
|
...assignment.tools ? { tools: assignment.tools } : {},
|
|
8693
9299
|
...assignment.name ? { name: assignment.name } : {},
|
|
8694
9300
|
...assignment.allowedCapabilities ? { allowedCapabilities: assignment.allowedCapabilities } : {},
|
|
8695
|
-
context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId } },
|
|
9301
|
+
context: { kanban: { boardId, taskId: task.id, projectRoot: ctx.projectRoot, leaseId: dispatchLeaseId } },
|
|
8696
9302
|
onDone: async (result) => {
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
}
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
if (result.status === "completed" && terminalWrite) {
|
|
8709
|
-
try {
|
|
8710
|
-
const finalized = await store.finalizeTaskCompletion(boardId, task.id, {
|
|
8711
|
-
eventContext: activityContext(ctx, void 0, leaseId)
|
|
8712
|
-
});
|
|
8713
|
-
if (finalized?.gate.report && ctx.context) {
|
|
8714
|
-
recordKanbanVerificationEvidence(ctx.context, finalized.gate.report);
|
|
8715
|
-
}
|
|
8716
|
-
} catch (error2) {
|
|
8717
|
-
console.warn(
|
|
8718
|
-
`[kanban-dispatch] completion gate failed for task ${task.id}: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
8719
|
-
);
|
|
8720
|
-
}
|
|
8721
|
-
if (terminalWrite.lifecycle?.mode === "managed") {
|
|
8722
|
-
const managedTask = terminalWrite.tasks.find(
|
|
8723
|
-
(candidate) => candidate.id === task.id
|
|
8724
|
-
);
|
|
8725
|
-
if (managedTask && managedTask.lifecycle?.currentStage === "running") {
|
|
8726
|
-
const actor = ctx.context?.agentId ?? "kanban-agent";
|
|
8727
|
-
const comment = typeof result.result === "string" && result.result.trim().length > 0 ? result.result.trim().slice(0, 1e3) : "Work completed.";
|
|
8728
|
-
try {
|
|
8729
|
-
await store.transitionTask(boardId, task.id, {
|
|
8730
|
-
to: "review",
|
|
8731
|
-
actor,
|
|
8732
|
-
comment,
|
|
8733
|
-
attachment: {
|
|
8734
|
-
url: `kanban://task/${task.id}/result`,
|
|
8735
|
-
title: "Worker completion result",
|
|
8736
|
-
type: "file"
|
|
8737
|
-
}
|
|
8738
|
-
});
|
|
8739
|
-
} catch (transitionError) {
|
|
8740
|
-
console.warn(
|
|
8741
|
-
`[kanban-dispatch] auto-transition to review failed for task ${task.id}: ${transitionError instanceof Error ? transitionError.message : String(transitionError)}`
|
|
8742
|
-
);
|
|
8743
|
-
}
|
|
8744
|
-
}
|
|
9303
|
+
if (result.status === "completed") {
|
|
9304
|
+
const completed = await store.completeKanbanDispatch({
|
|
9305
|
+
boardId,
|
|
9306
|
+
taskId: task.id,
|
|
9307
|
+
leaseId: dispatchLeaseId,
|
|
9308
|
+
actor: ctx.context?.agentId ?? "kanban-agent",
|
|
9309
|
+
result: result.result,
|
|
9310
|
+
evidence: result.result ? { url: `kanban://task/${task.id}/result`, title: "Worker completion result", type: "file" } : void 0
|
|
9311
|
+
});
|
|
9312
|
+
if (completed?.task.verificationReport && ctx.context) {
|
|
9313
|
+
recordKanbanVerificationEvidence(ctx.context, completed.task.verificationReport);
|
|
8745
9314
|
}
|
|
9315
|
+
} else {
|
|
9316
|
+
await store.failKanbanDispatch({
|
|
9317
|
+
boardId,
|
|
9318
|
+
taskId: task.id,
|
|
9319
|
+
leaseId: dispatchLeaseId,
|
|
9320
|
+
actor: ctx.context?.agentId ?? "kanban-agent",
|
|
9321
|
+
error: result.error ?? "Task failed."
|
|
9322
|
+
});
|
|
8746
9323
|
}
|
|
8747
9324
|
const reconciled = await store.reconcileBoard(boardId);
|
|
8748
|
-
const
|
|
8749
|
-
const completedTask =
|
|
9325
|
+
const completedBoard = reconciled?.board ?? await store.getBoard(boardId);
|
|
9326
|
+
const completedTask = completedBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
|
|
8750
9327
|
ctx.broadcast?.({
|
|
8751
9328
|
type: "kanban.task.update",
|
|
8752
9329
|
payload: { success: true, data: { boardId: board.id, task: completedTask } }
|
|
8753
9330
|
});
|
|
8754
|
-
if (
|
|
9331
|
+
if (completedBoard) {
|
|
8755
9332
|
ctx.broadcast?.({
|
|
8756
9333
|
type: "kanban.get",
|
|
8757
|
-
payload: { success: true, data: { board:
|
|
9334
|
+
payload: { success: true, data: { board: completedBoard } }
|
|
8758
9335
|
});
|
|
8759
9336
|
}
|
|
8760
9337
|
ctx.broadcast?.({
|
|
@@ -8765,55 +9342,32 @@ async function handleKanbanTaskDispatch(ws, payload, ctx) {
|
|
|
8765
9342
|
});
|
|
8766
9343
|
const subagentId = summary.match(/Spawned subagent\s+([^\s]+)/)?.[1];
|
|
8767
9344
|
const runTaskId = summary.match(/\bfor task\s+([^\s.]+)/i)?.[1];
|
|
8768
|
-
const
|
|
8769
|
-
const updated = await store.updateTaskAssignment(
|
|
9345
|
+
const started = await store.startKanbanDispatch({
|
|
8770
9346
|
boardId,
|
|
8771
|
-
task.id,
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
lastResult: summary
|
|
8779
|
-
},
|
|
8780
|
-
activityContext(ctx, void 0, leaseId)
|
|
8781
|
-
);
|
|
8782
|
-
let transitionBoard = updated;
|
|
8783
|
-
if (updated?.lifecycle?.mode === "managed") {
|
|
8784
|
-
const managedTask = updated.tasks.find((candidate) => candidate.id === task.id);
|
|
8785
|
-
if (managedTask && managedTask.lifecycle?.currentStage === "todo") {
|
|
8786
|
-
try {
|
|
8787
|
-
const tr = await store.transitionTask(boardId, task.id, {
|
|
8788
|
-
to: "running",
|
|
8789
|
-
actor: ctx.context?.agentId ?? "kanban-agent",
|
|
8790
|
-
comment: "Work started."
|
|
8791
|
-
});
|
|
8792
|
-
if (tr) transitionBoard = tr.board;
|
|
8793
|
-
} catch (transitionErr) {
|
|
8794
|
-
console.warn(
|
|
8795
|
-
`[kanban-dispatch] auto-transition to Running failed for task ${task.id}: ${transitionErr instanceof Error ? transitionErr.message : String(transitionErr)}`
|
|
8796
|
-
);
|
|
8797
|
-
}
|
|
8798
|
-
}
|
|
8799
|
-
}
|
|
8800
|
-
const runningTask = transitionBoard?.tasks.find((candidate) => candidate.id === task.id) ?? task;
|
|
9347
|
+
taskId: task.id,
|
|
9348
|
+
leaseId: dispatchLeaseId,
|
|
9349
|
+
actor: ctx.context?.agentId ?? "kanban-agent",
|
|
9350
|
+
...subagentId ? { subagentId } : {},
|
|
9351
|
+
...runTaskId ? { runTaskId } : {}
|
|
9352
|
+
});
|
|
9353
|
+
const runningTask = started?.task ?? (started?.board.tasks.find((candidate) => candidate.id === task.id) ?? task);
|
|
8801
9354
|
ctx.broadcast?.({
|
|
8802
9355
|
type: "kanban.task.update",
|
|
8803
9356
|
payload: { success: true, data: { boardId: board.id, task: runningTask } }
|
|
8804
9357
|
});
|
|
8805
|
-
if (
|
|
8806
|
-
ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board:
|
|
9358
|
+
if (started?.board) {
|
|
9359
|
+
ctx.broadcast?.({ type: "kanban.get", payload: { success: true, data: { board: started.board } } });
|
|
8807
9360
|
}
|
|
8808
9361
|
reply(ws, "kanban.task.dispatch", true, { boardId: board.id, task: runningTask, summary });
|
|
8809
9362
|
} catch (error2) {
|
|
8810
9363
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
8811
|
-
await store.
|
|
9364
|
+
await store.failKanbanDispatch({
|
|
8812
9365
|
boardId,
|
|
8813
|
-
task.id,
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
9366
|
+
taskId: task.id,
|
|
9367
|
+
leaseId: dispatchLeaseId,
|
|
9368
|
+
actor: ctx.context?.agentId ?? "kanban-agent",
|
|
9369
|
+
error: message
|
|
9370
|
+
});
|
|
8817
9371
|
reply(ws, "kanban.task.dispatch", false, message);
|
|
8818
9372
|
}
|
|
8819
9373
|
}
|
|
@@ -8967,7 +9521,7 @@ function fail(ws, type, message) {
|
|
|
8967
9521
|
function has(payload, key) {
|
|
8968
9522
|
return payload !== void 0 && Object.hasOwn(payload, key);
|
|
8969
9523
|
}
|
|
8970
|
-
function
|
|
9524
|
+
function activityContext(ctx, actor, note) {
|
|
8971
9525
|
const sessionId = ctx.context?.session?.id;
|
|
8972
9526
|
return {
|
|
8973
9527
|
...sessionId ? { sessionId } : {},
|
|
@@ -9204,7 +9758,7 @@ async function handleTaskActivityAdd(ws, type, payload, ctx) {
|
|
|
9204
9758
|
outcome,
|
|
9205
9759
|
...typeof payload?.details === "string" && payload.details.trim() ? { details: payload.details.trim() } : {}
|
|
9206
9760
|
},
|
|
9207
|
-
|
|
9761
|
+
activityContext(
|
|
9208
9762
|
ctx,
|
|
9209
9763
|
payload?.actor ?? ctx.context?.agentId ?? "webui"
|
|
9210
9764
|
)
|
|
@@ -9518,7 +10072,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9518
10072
|
...payload?.labels ? { labels: payload.labels } : {},
|
|
9519
10073
|
...has(payload, "boundary") ? { boundary: payload?.boundary } : {}
|
|
9520
10074
|
},
|
|
9521
|
-
|
|
10075
|
+
activityContext(ctx, "webui", payload?.activityNote)
|
|
9522
10076
|
);
|
|
9523
10077
|
result ? ok(ws, type, result.task) : fail(ws, type, `Board not found: ${boardId}`);
|
|
9524
10078
|
return true;
|
|
@@ -9602,7 +10156,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9602
10156
|
boundary: payload?.boundary ?? null
|
|
9603
10157
|
} : {}
|
|
9604
10158
|
},
|
|
9605
|
-
|
|
10159
|
+
activityContext(ctx, "webui", payload?.activityNote)
|
|
9606
10160
|
);
|
|
9607
10161
|
if (!board) {
|
|
9608
10162
|
fail(ws, type, "Board or task not found");
|
|
@@ -9653,7 +10207,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9653
10207
|
taskId,
|
|
9654
10208
|
columnId,
|
|
9655
10209
|
payload?.order,
|
|
9656
|
-
|
|
10210
|
+
activityContext(ctx, "webui", payload?.activityNote)
|
|
9657
10211
|
);
|
|
9658
10212
|
if (!board) {
|
|
9659
10213
|
fail(ws, type, "Move failed");
|
|
@@ -9770,7 +10324,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9770
10324
|
...payload?.unit ? { unit: payload.unit } : {},
|
|
9771
10325
|
...payload?.notes ? { notes: payload.notes } : {}
|
|
9772
10326
|
},
|
|
9773
|
-
|
|
10327
|
+
activityContext(
|
|
9774
10328
|
ctx,
|
|
9775
10329
|
"webui",
|
|
9776
10330
|
payload?.activityNote ?? `Goal metric added: ${name2}.`
|
|
@@ -9800,7 +10354,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9800
10354
|
...payload?.unit ? { unit: payload.unit } : {},
|
|
9801
10355
|
...payload?.notes ? { notes: payload.notes } : {}
|
|
9802
10356
|
},
|
|
9803
|
-
|
|
10357
|
+
activityContext(
|
|
9804
10358
|
ctx,
|
|
9805
10359
|
"webui",
|
|
9806
10360
|
payload?.activityNote ?? "Goal metric updated in WebUI."
|
|
@@ -9837,7 +10391,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9837
10391
|
...payload?.costCeilingUsd !== void 0 ? { costCeilingUsd: Number(payload.costCeilingUsd) } : {},
|
|
9838
10392
|
...payload?.retryPolicy ? { retryPolicy: payload.retryPolicy } : {}
|
|
9839
10393
|
},
|
|
9840
|
-
|
|
10394
|
+
activityContext(ctx, void 0, payload?.activityNote)
|
|
9841
10395
|
);
|
|
9842
10396
|
board ? ok(ws, type, findTask2(board.tasks, taskId)) : fail(ws, type, "Board or task not found");
|
|
9843
10397
|
return true;
|
|
@@ -9859,7 +10413,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9859
10413
|
type: payload?.checkType ?? "manual",
|
|
9860
10414
|
status: payload?.status ?? "pending"
|
|
9861
10415
|
},
|
|
9862
|
-
|
|
10416
|
+
activityContext(
|
|
9863
10417
|
ctx,
|
|
9864
10418
|
"webui",
|
|
9865
10419
|
payload?.activityNote ?? `Acceptance check added: ${description}.`
|
|
@@ -9884,7 +10438,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9884
10438
|
{
|
|
9885
10439
|
...has(payload, "status") ? { status: payload?.status } : {}
|
|
9886
10440
|
},
|
|
9887
|
-
|
|
10441
|
+
activityContext(
|
|
9888
10442
|
ctx,
|
|
9889
10443
|
"webui",
|
|
9890
10444
|
payload?.activityNote ?? `Acceptance check updated${payload?.status ? ` to ${String(payload.status)}` : ""}.`
|
|
@@ -9911,7 +10465,7 @@ async function handleKanbanRoute(ws, msg, ctx) {
|
|
|
9911
10465
|
author,
|
|
9912
10466
|
content
|
|
9913
10467
|
},
|
|
9914
|
-
|
|
10468
|
+
activityContext(ctx, author)
|
|
9915
10469
|
);
|
|
9916
10470
|
board ? ok(ws, type, board) : fail(ws, type, "Board or task not found");
|
|
9917
10471
|
return true;
|
|
@@ -10058,14 +10612,14 @@ function registerShutdownHandlers(res) {
|
|
|
10058
10612
|
}
|
|
10059
10613
|
|
|
10060
10614
|
// src/server/config-doctor.ts
|
|
10061
|
-
import * as
|
|
10615
|
+
import * as fs12 from "node:fs/promises";
|
|
10062
10616
|
import {
|
|
10063
10617
|
repairConfigDefaults
|
|
10064
10618
|
} from "@wrongstack/core/storage";
|
|
10065
10619
|
import { atomicWrite as atomicWrite5 } from "@wrongstack/core/utils";
|
|
10066
10620
|
import { decryptConfigSecrets } from "@wrongstack/core/security";
|
|
10067
10621
|
async function readConfig(file, vault) {
|
|
10068
|
-
const raw = await
|
|
10622
|
+
const raw = await fs12.readFile(file, "utf8");
|
|
10069
10623
|
const parsed = JSON.parse(raw);
|
|
10070
10624
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10071
10625
|
throw new Error("Active profile config must contain a JSON object.");
|
|
@@ -10129,6 +10683,7 @@ async function handleConfigDoctor(ws, apply, deps2) {
|
|
|
10129
10683
|
|
|
10130
10684
|
// src/server/mailbox-handlers.ts
|
|
10131
10685
|
import {
|
|
10686
|
+
actionToAckInput,
|
|
10132
10687
|
getSharedProjectMailbox,
|
|
10133
10688
|
isMailboxMessageVisibleTo,
|
|
10134
10689
|
MAILBOX_TYPE_PROPERTIES,
|
|
@@ -10145,6 +10700,43 @@ function getMailboxForDeps(deps2) {
|
|
|
10145
10700
|
const dir = resolveProjectDir(projectRoot, globalRoot);
|
|
10146
10701
|
return getSharedProjectMailbox(dir, deps2.events);
|
|
10147
10702
|
}
|
|
10703
|
+
async function handleMailboxAction(ws, deps2, payload) {
|
|
10704
|
+
const mb = getMailboxForDeps(deps2);
|
|
10705
|
+
if (!mb) {
|
|
10706
|
+
send(ws, {
|
|
10707
|
+
type: "mailbox.action_result",
|
|
10708
|
+
payload: {
|
|
10709
|
+
requestId: payload.requestId,
|
|
10710
|
+
success: false,
|
|
10711
|
+
error: "No project root available"
|
|
10712
|
+
}
|
|
10713
|
+
});
|
|
10714
|
+
return;
|
|
10715
|
+
}
|
|
10716
|
+
try {
|
|
10717
|
+
const message = payload.action === "soft-delete" ? await mb.softDelete(payload.mailId, payload.readerId) : await mb.ack(actionToAckInput(payload.action, payload));
|
|
10718
|
+
send(ws, {
|
|
10719
|
+
type: "mailbox.action_result",
|
|
10720
|
+
payload: {
|
|
10721
|
+
requestId: payload.requestId,
|
|
10722
|
+
success: message !== null,
|
|
10723
|
+
action: payload.action,
|
|
10724
|
+
mailId: payload.mailId
|
|
10725
|
+
}
|
|
10726
|
+
});
|
|
10727
|
+
} catch (err) {
|
|
10728
|
+
send(ws, {
|
|
10729
|
+
type: "mailbox.action_result",
|
|
10730
|
+
payload: {
|
|
10731
|
+
requestId: payload.requestId,
|
|
10732
|
+
success: false,
|
|
10733
|
+
action: payload.action,
|
|
10734
|
+
mailId: payload.mailId,
|
|
10735
|
+
error: errMessage(err)
|
|
10736
|
+
}
|
|
10737
|
+
});
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10148
10740
|
async function handleMailboxSend(ws, deps2, payload) {
|
|
10149
10741
|
const mb = getMailboxForDeps(deps2);
|
|
10150
10742
|
if (!mb) {
|
|
@@ -10160,7 +10752,7 @@ async function handleMailboxSend(ws, deps2, payload) {
|
|
|
10160
10752
|
}
|
|
10161
10753
|
try {
|
|
10162
10754
|
const message = await mb.send({
|
|
10163
|
-
from: "webui",
|
|
10755
|
+
from: payload.from ?? "webui",
|
|
10164
10756
|
to: payload.to,
|
|
10165
10757
|
type: payload.type,
|
|
10166
10758
|
audience: payload.audience,
|
|
@@ -10175,6 +10767,7 @@ async function handleMailboxSend(ws, deps2, payload) {
|
|
|
10175
10767
|
requestId: payload.requestId,
|
|
10176
10768
|
success: true,
|
|
10177
10769
|
messageId: message.id,
|
|
10770
|
+
from: message.from,
|
|
10178
10771
|
to: message.to,
|
|
10179
10772
|
audience: message.audience ?? "all"
|
|
10180
10773
|
}
|
|
@@ -10219,6 +10812,7 @@ async function handleMailboxMessages(ws, deps2, payload) {
|
|
|
10219
10812
|
send(ws, {
|
|
10220
10813
|
type: "mailbox.messages",
|
|
10221
10814
|
payload: {
|
|
10815
|
+
...payload?.unreadOnly === true ? { unreadOnly: true } : {},
|
|
10222
10816
|
messages: visibleMessages.map((m) => {
|
|
10223
10817
|
const readByMe = payload?.agentId !== void 0 ? payload.agentId in m.readBy : false;
|
|
10224
10818
|
const completedByMe = payload?.agentId !== void 0 ? m.completedBy === payload.agentId : false;
|
|
@@ -10334,6 +10928,14 @@ function createMailboxRouteHandlers(ctx) {
|
|
|
10334
10928
|
...ctx.events ? { events: ctx.events } : {}
|
|
10335
10929
|
};
|
|
10336
10930
|
return {
|
|
10931
|
+
action: (ws, msg) => {
|
|
10932
|
+
const parsed = validateMailboxActionPayload(msg.payload);
|
|
10933
|
+
if (!parsed.ok) {
|
|
10934
|
+
sendResult2(ws, false, parsed.message);
|
|
10935
|
+
return;
|
|
10936
|
+
}
|
|
10937
|
+
return handleMailboxAction(ws, deps2, parsed.value);
|
|
10938
|
+
},
|
|
10337
10939
|
send: (ws, msg) => {
|
|
10338
10940
|
const parsed = validateMailboxSendPayload(msg.payload);
|
|
10339
10941
|
if (!parsed.ok) {
|
|
@@ -10384,6 +10986,9 @@ function createMailboxRouteHandlers(ctx) {
|
|
|
10384
10986
|
}
|
|
10385
10987
|
async function handleMailboxRoute(ws, msg, handlers) {
|
|
10386
10988
|
switch (msg.type) {
|
|
10989
|
+
case "mailbox.action":
|
|
10990
|
+
await handlers.action(ws, msg);
|
|
10991
|
+
return true;
|
|
10387
10992
|
case "mailbox.send":
|
|
10388
10993
|
await handlers.send(ws, msg);
|
|
10389
10994
|
return true;
|
|
@@ -10409,12 +11014,14 @@ async function handleMailboxRoute(ws, msg, handlers) {
|
|
|
10409
11014
|
|
|
10410
11015
|
// src/server/mcp-handlers.ts
|
|
10411
11016
|
import { allServers } from "@wrongstack/core/infrastructure";
|
|
11017
|
+
import { DefaultSecretScrubber, isSecretField } from "@wrongstack/core/security";
|
|
10412
11018
|
import {
|
|
10413
11019
|
addMcp,
|
|
10414
11020
|
disableMcp,
|
|
10415
11021
|
discoverMcp,
|
|
10416
11022
|
enableMcp,
|
|
10417
11023
|
listMcp,
|
|
11024
|
+
MCP_ENV_MASK,
|
|
10418
11025
|
removeMcp,
|
|
10419
11026
|
restartMcp,
|
|
10420
11027
|
updateMcp
|
|
@@ -10434,6 +11041,15 @@ function mapStatus(raw) {
|
|
|
10434
11041
|
return "stopped";
|
|
10435
11042
|
}
|
|
10436
11043
|
}
|
|
11044
|
+
var envScrubber = new DefaultSecretScrubber();
|
|
11045
|
+
function maskServerEnv(env) {
|
|
11046
|
+
const out = {};
|
|
11047
|
+
for (const [key, value] of Object.entries(env)) {
|
|
11048
|
+
const secret = isSecretField(key) || typeof value === "string" && envScrubber.scrub(value) !== value;
|
|
11049
|
+
out[key] = secret ? MCP_ENV_MASK : value;
|
|
11050
|
+
}
|
|
11051
|
+
return out;
|
|
11052
|
+
}
|
|
10437
11053
|
function toView(info, health) {
|
|
10438
11054
|
const view = {
|
|
10439
11055
|
name: info.name,
|
|
@@ -10448,7 +11064,7 @@ function toView(info, health) {
|
|
|
10448
11064
|
if (info.lazy !== void 0) view.lazy = info.lazy;
|
|
10449
11065
|
if (info.command !== void 0) view.command = info.command;
|
|
10450
11066
|
if (info.args !== void 0) view.args = info.args;
|
|
10451
|
-
if (info.env !== void 0) view.env = info.env;
|
|
11067
|
+
if (info.env !== void 0) view.env = maskServerEnv(info.env);
|
|
10452
11068
|
if (info.url !== void 0) view.url = info.url;
|
|
10453
11069
|
if (health !== void 0) view.health = health;
|
|
10454
11070
|
return view;
|
|
@@ -10487,7 +11103,15 @@ async function handleMcpList(ws, _msg, globalConfigPath, mcpRegistry) {
|
|
|
10487
11103
|
async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
10488
11104
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
10489
11105
|
if (!d) return;
|
|
10490
|
-
const
|
|
11106
|
+
const validated = validateMcpServerPayload(msg.payload, "mcp.add");
|
|
11107
|
+
if (!validated.ok) {
|
|
11108
|
+
send(ws, {
|
|
11109
|
+
type: "mcp.operation_result",
|
|
11110
|
+
payload: { success: false, message: validated.message }
|
|
11111
|
+
});
|
|
11112
|
+
return;
|
|
11113
|
+
}
|
|
11114
|
+
const result = await addMcp(validated.value, d);
|
|
10491
11115
|
if (result.ok && result.server) {
|
|
10492
11116
|
send(ws, { type: "mcp.server.added", payload: { server: toView(result.server) } });
|
|
10493
11117
|
if (result.registryError) {
|
|
@@ -10507,7 +11131,15 @@ async function handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
10507
11131
|
async function handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry) {
|
|
10508
11132
|
const d = deps(ws, globalConfigPath, mcpRegistry);
|
|
10509
11133
|
if (!d) return;
|
|
10510
|
-
const
|
|
11134
|
+
const validated = validateMcpServerPayload(msg.payload, "mcp.update");
|
|
11135
|
+
if (!validated.ok) {
|
|
11136
|
+
send(ws, {
|
|
11137
|
+
type: "mcp.operation_result",
|
|
11138
|
+
payload: { success: false, message: validated.message }
|
|
11139
|
+
});
|
|
11140
|
+
return;
|
|
11141
|
+
}
|
|
11142
|
+
const result = await updateMcp(validated.value, d);
|
|
10511
11143
|
if (result.ok && result.server) {
|
|
10512
11144
|
send(ws, { type: "mcp.server.updated", payload: { server: toView(result.server) } });
|
|
10513
11145
|
}
|
|
@@ -11024,7 +11656,7 @@ async function handleSageDelete(ws, msg, memoryStore) {
|
|
|
11024
11656
|
}
|
|
11025
11657
|
try {
|
|
11026
11658
|
await Sage.deleteSage(id, reason, {
|
|
11027
|
-
force: force
|
|
11659
|
+
force: force === true,
|
|
11028
11660
|
neverInject: neverInject === true
|
|
11029
11661
|
});
|
|
11030
11662
|
send(ws, {
|
|
@@ -11607,11 +12239,11 @@ function createModelOperations(context) {
|
|
|
11607
12239
|
}
|
|
11608
12240
|
|
|
11609
12241
|
// src/server/port-utils.ts
|
|
11610
|
-
import * as
|
|
12242
|
+
import * as net2 from "node:net";
|
|
11611
12243
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
11612
12244
|
function isPortFree(host, port) {
|
|
11613
12245
|
return new Promise((resolve15) => {
|
|
11614
|
-
const srv =
|
|
12246
|
+
const srv = net2.createServer();
|
|
11615
12247
|
srv.once("error", () => resolve15(false));
|
|
11616
12248
|
srv.once("listening", () => {
|
|
11617
12249
|
srv.close(() => resolve15(true));
|
|
@@ -11839,6 +12471,7 @@ function seedContextMeta(config, context) {
|
|
|
11839
12471
|
meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
11840
12472
|
meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
11841
12473
|
meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
12474
|
+
meta["fallbackBridge"] = config.fallbackBridge ?? "";
|
|
11842
12475
|
meta["fallbackProfiles"] = config.fallbackProfiles ?? {};
|
|
11843
12476
|
meta["favoriteModels"] = config.favoriteModels ?? [];
|
|
11844
12477
|
meta["favoriteModelsOnly"] = config.favoriteModelsOnly === true;
|
|
@@ -11875,7 +12508,7 @@ function seedContextMeta(config, context) {
|
|
|
11875
12508
|
meta["refinerModel"] = autonomyCfg["refinerModel"] ?? "";
|
|
11876
12509
|
meta["refinerFallbackProfile"] = autonomyCfg["refinerFallbackProfile"] ?? "";
|
|
11877
12510
|
meta["thinkingWord"] = autonomyCfg["thinkingWord"] ?? "thinking";
|
|
11878
|
-
meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "
|
|
12511
|
+
meta["statuslineMode"] = autonomyCfg["statuslineMode"] ?? "minimum";
|
|
11879
12512
|
meta["animationStyle"] = autonomyCfg["animationStyle"] ?? "rainbow";
|
|
11880
12513
|
meta["showModelReasoning"] = autonomyCfg["showModelReasoning"] !== false;
|
|
11881
12514
|
meta["breakerEnabled"] = config.circuitBreaker?.enabled === true;
|
|
@@ -11921,7 +12554,10 @@ function seedContextMeta(config, context) {
|
|
|
11921
12554
|
try {
|
|
11922
12555
|
const mgr = new FallbackProfileManager(config);
|
|
11923
12556
|
const named = autoReviewExt?.["fallbackProfile"];
|
|
11924
|
-
resolvedChain = typeof named === "string" && named.length > 0 ? mgr.
|
|
12557
|
+
resolvedChain = typeof named === "string" && named.length > 0 ? mgr.resolveEffective({ fallbackProfile: named, fallbackAuto: false }) : mgr.resolveEffective({
|
|
12558
|
+
fallbackModels: config.fallbackModels,
|
|
12559
|
+
fallbackAuto: config.fallbackAuto !== false
|
|
12560
|
+
});
|
|
11925
12561
|
} catch {
|
|
11926
12562
|
resolvedChain = [];
|
|
11927
12563
|
}
|
|
@@ -11930,8 +12566,8 @@ function seedContextMeta(config, context) {
|
|
|
11930
12566
|
}
|
|
11931
12567
|
|
|
11932
12568
|
// src/server/pref-helpers.ts
|
|
11933
|
-
import * as
|
|
11934
|
-
import * as
|
|
12569
|
+
import * as fs13 from "node:fs/promises";
|
|
12570
|
+
import * as path15 from "node:path";
|
|
11935
12571
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
11936
12572
|
import { atomicWrite as atomicWrite6, backupConfigFile, FORBIDDEN_PROTO_KEYS as FORBIDDEN_PROTO_KEYS2 } from "@wrongstack/core/utils";
|
|
11937
12573
|
var PREF_KEYS = [
|
|
@@ -12027,11 +12663,11 @@ function prefSnapshot(contextMeta) {
|
|
|
12027
12663
|
return snapshot;
|
|
12028
12664
|
}
|
|
12029
12665
|
async function writeGlobalConfigFile(filePath, vault, mutate, logger, errorLabel) {
|
|
12030
|
-
const globalRoot =
|
|
12666
|
+
const globalRoot = path15.dirname(filePath);
|
|
12031
12667
|
await backupConfigFile(filePath, { globalRoot });
|
|
12032
12668
|
let raw;
|
|
12033
12669
|
try {
|
|
12034
|
-
raw = await
|
|
12670
|
+
raw = await fs13.readFile(filePath, "utf8");
|
|
12035
12671
|
} catch {
|
|
12036
12672
|
raw = "{}";
|
|
12037
12673
|
}
|
|
@@ -12065,7 +12701,28 @@ async function updateGlobalConfig(deps2, holder, mutate, errorLabel) {
|
|
|
12065
12701
|
);
|
|
12066
12702
|
}
|
|
12067
12703
|
}
|
|
12068
|
-
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
12704
|
+
var DISPLAY_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
12705
|
+
"groupToolCalls",
|
|
12706
|
+
"showThinkingLogs",
|
|
12707
|
+
"autoReviewFallbackModels",
|
|
12708
|
+
// v11 Display parity: agent-swarm panel + inverse fsAccess flag.
|
|
12709
|
+
// The TUI settings picker mirrors these so the browser can keep the
|
|
12710
|
+
// panel state in sync without round-tripping through config.
|
|
12711
|
+
"showAgentSwarmPanel",
|
|
12712
|
+
"allowOutsideProjectRoot",
|
|
12713
|
+
// v13 Display parity: codebase-index read-tool, SAGE memory-inject
|
|
12714
|
+
// surfacing, pre-refine countdown, inject threshold, multi-diff
|
|
12715
|
+
// footer threshold, refine-panel enhance countdown. All six are
|
|
12716
|
+
// pure panel-toggles; the underlying engine knobs (when applicable)
|
|
12717
|
+
// live in the TUI keyboard shortcut or the agent runtime, not in
|
|
12718
|
+
// config.
|
|
12719
|
+
"readSymbols",
|
|
12720
|
+
"showSageMemoryInject",
|
|
12721
|
+
"preRefineSeconds",
|
|
12722
|
+
"sageMemoryInjectThreshold",
|
|
12723
|
+
"multiDiffSummaryThreshold",
|
|
12724
|
+
"enhanceCountdownMs"
|
|
12725
|
+
]);
|
|
12069
12726
|
async function persistPrefsToConfig(deps2, holder, payload) {
|
|
12070
12727
|
payload = { ...payload };
|
|
12071
12728
|
for (const k of DISPLAY_ONLY_KEYS) delete payload[k];
|
|
@@ -12080,7 +12737,7 @@ async function persistPrefsToConfig(deps2, holder, payload) {
|
|
|
12080
12737
|
autonomyCfg[key] = val;
|
|
12081
12738
|
autonomyTouched = true;
|
|
12082
12739
|
};
|
|
12083
|
-
if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto"].includes(payload["autonomy"])) {
|
|
12740
|
+
if (typeof payload["autonomy"] === "string" && ["off", "suggest", "auto", "eternal", "eternal-parallel"].includes(payload["autonomy"])) {
|
|
12084
12741
|
setAutonomy("defaultMode", payload["autonomy"]);
|
|
12085
12742
|
}
|
|
12086
12743
|
if (typeof payload["autonomyDelayMs"] === "number")
|
|
@@ -12376,14 +13033,14 @@ import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
|
12376
13033
|
import { getProcessRegistry as getProcessRegistry2 } from "@wrongstack/tools";
|
|
12377
13034
|
|
|
12378
13035
|
// src/server/privileged-actions.ts
|
|
12379
|
-
import { randomUUID as
|
|
13036
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
12380
13037
|
import {
|
|
12381
13038
|
isTrustDecisionAllowed
|
|
12382
13039
|
} from "@wrongstack/core/security";
|
|
12383
13040
|
async function authorizeWebUIAction(boundary, action, logger) {
|
|
12384
13041
|
const request = {
|
|
12385
13042
|
version: 1,
|
|
12386
|
-
requestId:
|
|
13043
|
+
requestId: randomUUID3(),
|
|
12387
13044
|
actor: {
|
|
12388
13045
|
kind: "remote-client",
|
|
12389
13046
|
...action.sessionId ? { sessionId: action.sessionId } : {}
|
|
@@ -12506,8 +13163,8 @@ async function handleProcessRoute(ws, msg, handlers) {
|
|
|
12506
13163
|
}
|
|
12507
13164
|
|
|
12508
13165
|
// src/server/project-handlers.ts
|
|
12509
|
-
import * as
|
|
12510
|
-
import * as
|
|
13166
|
+
import * as fs14 from "node:fs/promises";
|
|
13167
|
+
import * as path16 from "node:path";
|
|
12511
13168
|
import { DefaultSessionStore } from "@wrongstack/core/storage";
|
|
12512
13169
|
import { resolveWstackPaths as resolveWstackPaths4 } from "@wrongstack/core/utils";
|
|
12513
13170
|
function createProjectHandlers(ctx) {
|
|
@@ -12559,10 +13216,10 @@ function createProjectHandlers(ctx) {
|
|
|
12559
13216
|
});
|
|
12560
13217
|
return;
|
|
12561
13218
|
}
|
|
12562
|
-
const resolved =
|
|
12563
|
-
const name2 = parsed.value.name?.trim() ||
|
|
13219
|
+
const resolved = path16.resolve(parsed.value.root);
|
|
13220
|
+
const name2 = parsed.value.name?.trim() || path16.basename(resolved);
|
|
12564
13221
|
try {
|
|
12565
|
-
const stat3 = await
|
|
13222
|
+
const stat3 = await fs14.stat(resolved).catch(() => null);
|
|
12566
13223
|
if (!stat3?.isDirectory()) {
|
|
12567
13224
|
sendTo(ws, {
|
|
12568
13225
|
type: "projects.added",
|
|
@@ -12571,7 +13228,7 @@ function createProjectHandlers(ctx) {
|
|
|
12571
13228
|
return;
|
|
12572
13229
|
}
|
|
12573
13230
|
const before = await loadManifest(ctx.globalConfigPath);
|
|
12574
|
-
const already = before.projects.some((project) =>
|
|
13231
|
+
const already = before.projects.some((project) => path16.resolve(project.root) === resolved);
|
|
12575
13232
|
const entry = await touchProjectInManifest(
|
|
12576
13233
|
{ projectRoot: resolved, workingDir: resolved, name: name2 },
|
|
12577
13234
|
ctx.globalConfigPath
|
|
@@ -12601,8 +13258,8 @@ function createProjectHandlers(ctx) {
|
|
|
12601
13258
|
});
|
|
12602
13259
|
return;
|
|
12603
13260
|
}
|
|
12604
|
-
const resolved =
|
|
12605
|
-
const name2 = parsed.value.name?.trim() ||
|
|
13261
|
+
const resolved = path16.resolve(parsed.value.root);
|
|
13262
|
+
const name2 = parsed.value.name?.trim() || path16.basename(resolved);
|
|
12606
13263
|
if (!ctx.allowProjectMutations) {
|
|
12607
13264
|
sendTo(ws, {
|
|
12608
13265
|
type: "projects.selected",
|
|
@@ -12615,7 +13272,7 @@ function createProjectHandlers(ctx) {
|
|
|
12615
13272
|
return;
|
|
12616
13273
|
}
|
|
12617
13274
|
try {
|
|
12618
|
-
const stat3 = await
|
|
13275
|
+
const stat3 = await fs14.stat(resolved).catch(() => null);
|
|
12619
13276
|
if (!stat3?.isDirectory()) {
|
|
12620
13277
|
sendTo(ws, {
|
|
12621
13278
|
type: "projects.selected",
|
|
@@ -12645,7 +13302,7 @@ function createProjectHandlers(ctx) {
|
|
|
12645
13302
|
const previousIdentityTarget = {
|
|
12646
13303
|
projectSlug: previousPaths.projectSlug,
|
|
12647
13304
|
projectRoot: previousProjectRoot,
|
|
12648
|
-
projectName:
|
|
13305
|
+
projectName: path16.basename(previousProjectRoot),
|
|
12649
13306
|
workingDir: ctx.context.workingDir
|
|
12650
13307
|
};
|
|
12651
13308
|
const previousUsage = ctx.tokenCounter.total();
|
|
@@ -12742,7 +13399,7 @@ function createProjectHandlers(ctx) {
|
|
|
12742
13399
|
|
|
12743
13400
|
// src/server/provider-handlers.ts
|
|
12744
13401
|
import { resolveProviderModelList } from "@wrongstack/core/models";
|
|
12745
|
-
import { DefaultSecretScrubber } from "@wrongstack/core/security";
|
|
13402
|
+
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
12746
13403
|
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
|
|
12747
13404
|
import {
|
|
12748
13405
|
beginOAuthLogin
|
|
@@ -12808,14 +13465,14 @@ async function searchCatalogModels(registry, rawQuery, limit = 8) {
|
|
|
12808
13465
|
}
|
|
12809
13466
|
|
|
12810
13467
|
// src/server/provider-config-io.ts
|
|
12811
|
-
import * as
|
|
13468
|
+
import * as fs15 from "node:fs/promises";
|
|
12812
13469
|
import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
|
|
12813
13470
|
import { atomicWrite as atomicWrite7 } from "@wrongstack/core/utils";
|
|
12814
13471
|
import { decryptConfigSecrets as decryptConfigSecrets3, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
12815
13472
|
async function loadSavedProviders(configPath, vault) {
|
|
12816
13473
|
let raw;
|
|
12817
13474
|
try {
|
|
12818
|
-
raw = await
|
|
13475
|
+
raw = await fs15.readFile(configPath, "utf8");
|
|
12819
13476
|
} catch {
|
|
12820
13477
|
return {};
|
|
12821
13478
|
}
|
|
@@ -12833,7 +13490,7 @@ async function saveProviders(configPath, vault, providers, profileConfigPath) {
|
|
|
12833
13490
|
let raw;
|
|
12834
13491
|
let fileExists = true;
|
|
12835
13492
|
try {
|
|
12836
|
-
raw = await
|
|
13493
|
+
raw = await fs15.readFile(targetPath, "utf8");
|
|
12837
13494
|
} catch (err) {
|
|
12838
13495
|
if (err.code !== "ENOENT") {
|
|
12839
13496
|
throw new ConfigError2({
|
|
@@ -13014,7 +13671,7 @@ function projectSavedProviders(providers) {
|
|
|
13014
13671
|
return view;
|
|
13015
13672
|
});
|
|
13016
13673
|
}
|
|
13017
|
-
var probeScrubber = new
|
|
13674
|
+
var probeScrubber = new DefaultSecretScrubber2();
|
|
13018
13675
|
async function probeModelDescriptors(cfg) {
|
|
13019
13676
|
if (!cfg.baseUrl) return [];
|
|
13020
13677
|
try {
|
|
@@ -13536,6 +14193,7 @@ var CLIENT_COLLABORATION_MESSAGE_TYPES = [
|
|
|
13536
14193
|
"collab.resume",
|
|
13537
14194
|
"collab.grant_control",
|
|
13538
14195
|
"collab.inject_tool",
|
|
14196
|
+
"mailbox.action",
|
|
13539
14197
|
"mailbox.agents",
|
|
13540
14198
|
"mailbox.clear",
|
|
13541
14199
|
"mailbox.compact",
|
|
@@ -13801,6 +14459,7 @@ var SERVER_COLLABORATION_MESSAGE_TYPES = [
|
|
|
13801
14459
|
"collab.pause.granted",
|
|
13802
14460
|
"collab.pause.released",
|
|
13803
14461
|
"collab.state",
|
|
14462
|
+
"mailbox.action_result",
|
|
13804
14463
|
"mailbox.agent_registered",
|
|
13805
14464
|
"mailbox.agents",
|
|
13806
14465
|
"mailbox.cleared",
|
|
@@ -14024,13 +14683,13 @@ function isRegisteredMessageType(type, direction) {
|
|
|
14024
14683
|
// src/protocol/decoder.ts
|
|
14025
14684
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
14026
14685
|
var MAX_PAYLOAD_DEPTH = 32;
|
|
14027
|
-
function inspectValue(value,
|
|
14686
|
+
function inspectValue(value, path30, depth) {
|
|
14028
14687
|
if (depth > MAX_PAYLOAD_DEPTH) {
|
|
14029
|
-
return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path:
|
|
14688
|
+
return { code: "too_deep", message: "Protocol payload exceeds the nesting limit", path: path30 };
|
|
14030
14689
|
}
|
|
14031
14690
|
if (value === null || typeof value !== "object") return null;
|
|
14032
14691
|
for (const key of Object.keys(value)) {
|
|
14033
|
-
const childPath = `${
|
|
14692
|
+
const childPath = `${path30}.${key}`;
|
|
14034
14693
|
if (FORBIDDEN_KEYS.has(key)) {
|
|
14035
14694
|
return { code: "unsafe_key", message: `Unsafe protocol key: ${key}`, path: childPath };
|
|
14036
14695
|
}
|
|
@@ -15726,8 +16385,8 @@ function createRouteFamilyDispatcher(options) {
|
|
|
15726
16385
|
}
|
|
15727
16386
|
|
|
15728
16387
|
// src/server/shell-open.ts
|
|
15729
|
-
import * as
|
|
15730
|
-
import * as
|
|
16388
|
+
import * as fs16 from "node:fs/promises";
|
|
16389
|
+
import * as path17 from "node:path";
|
|
15731
16390
|
import { spawn } from "node:child_process";
|
|
15732
16391
|
function normalizeShellOpenTarget(target) {
|
|
15733
16392
|
return target === "terminal" ? "terminal" : "file-manager";
|
|
@@ -15738,11 +16397,11 @@ function shellQuote(s) {
|
|
|
15738
16397
|
}
|
|
15739
16398
|
async function handleShellOpen(req, logger, options) {
|
|
15740
16399
|
try {
|
|
15741
|
-
const resolved =
|
|
16400
|
+
const resolved = path17.resolve(req.path);
|
|
15742
16401
|
if (options?.projectRoot) {
|
|
15743
|
-
const root =
|
|
15744
|
-
const relative5 =
|
|
15745
|
-
const escapes = relative5.startsWith("..") ||
|
|
16402
|
+
const root = path17.resolve(options.projectRoot);
|
|
16403
|
+
const relative5 = path17.relative(root, resolved);
|
|
16404
|
+
const escapes = relative5.startsWith("..") || path17.isAbsolute(relative5);
|
|
15746
16405
|
if (escapes) {
|
|
15747
16406
|
return {
|
|
15748
16407
|
success: false,
|
|
@@ -15750,7 +16409,7 @@ async function handleShellOpen(req, logger, options) {
|
|
|
15750
16409
|
};
|
|
15751
16410
|
}
|
|
15752
16411
|
}
|
|
15753
|
-
await
|
|
16412
|
+
await fs16.access(resolved);
|
|
15754
16413
|
if (METACHAR_REGEX.test(resolved)) {
|
|
15755
16414
|
return { success: false, message: "Path contains unsupported characters." };
|
|
15756
16415
|
}
|
|
@@ -15801,7 +16460,12 @@ async function handleShellOpen(req, logger, options) {
|
|
|
15801
16460
|
}
|
|
15802
16461
|
|
|
15803
16462
|
// src/server/sdd-board-ws-handler.ts
|
|
15804
|
-
import {
|
|
16463
|
+
import {
|
|
16464
|
+
enqueueKanbanWorkflowCommand,
|
|
16465
|
+
kanbanWorkflowId,
|
|
16466
|
+
listBoards as listBoards3,
|
|
16467
|
+
listKanbanWorkflowStates
|
|
16468
|
+
} from "@wrongstack/kanban";
|
|
15805
16469
|
import {
|
|
15806
16470
|
applySddLifecycle,
|
|
15807
16471
|
extractVerificationCommand,
|
|
@@ -15828,16 +16492,17 @@ var SddBoardWebSocketHandler = class {
|
|
|
15828
16492
|
clients = /* @__PURE__ */ new Set();
|
|
15829
16493
|
lifecycle;
|
|
15830
16494
|
security;
|
|
15831
|
-
|
|
16495
|
+
standalonePollingEnabled;
|
|
15832
16496
|
latest = null;
|
|
15833
16497
|
poll = null;
|
|
15834
16498
|
pollInFlight = false;
|
|
15835
16499
|
unsub = null;
|
|
16500
|
+
disposed = false;
|
|
15836
16501
|
constructor(boardsDir, events, lifecycle, security) {
|
|
15837
16502
|
this.store = new SddBoardStore({ baseDir: boardsDir });
|
|
15838
16503
|
this.lifecycle = lifecycle;
|
|
15839
16504
|
this.security = security;
|
|
15840
|
-
this.
|
|
16505
|
+
this.standalonePollingEnabled = events === void 0;
|
|
15841
16506
|
if (events) {
|
|
15842
16507
|
const handler = (e) => {
|
|
15843
16508
|
this.latest = e.snapshot;
|
|
@@ -15847,6 +16512,7 @@ var SddBoardWebSocketHandler = class {
|
|
|
15847
16512
|
}
|
|
15848
16513
|
}
|
|
15849
16514
|
addClient(ws) {
|
|
16515
|
+
if (this.disposed) return;
|
|
15850
16516
|
const client = { ws, id: crypto.randomUUID() };
|
|
15851
16517
|
this.clients.add(client);
|
|
15852
16518
|
const remove = () => {
|
|
@@ -15872,7 +16538,7 @@ var SddBoardWebSocketHandler = class {
|
|
|
15872
16538
|
return;
|
|
15873
16539
|
}
|
|
15874
16540
|
if (msg.type === "sdd.board.list") {
|
|
15875
|
-
const boards = await this.
|
|
16541
|
+
const boards = await this.listBoardEntries();
|
|
15876
16542
|
this.broadcast({ type: "sdd.board.list", payload: { boards } });
|
|
15877
16543
|
return;
|
|
15878
16544
|
}
|
|
@@ -15885,7 +16551,8 @@ var SddBoardWebSocketHandler = class {
|
|
|
15885
16551
|
const verificationCommands = [];
|
|
15886
16552
|
if (action === "set_task_verification") {
|
|
15887
16553
|
const command = msg.payload?.verificationCommand;
|
|
15888
|
-
if (command !== void 0 && (typeof command !== "string" || command.length > 8192))
|
|
16554
|
+
if (command !== void 0 && (typeof command !== "string" || command.length > 8192))
|
|
16555
|
+
return;
|
|
15889
16556
|
if (typeof command === "string" && command.trim()) {
|
|
15890
16557
|
verificationCommands.push({ command, operation: "sdd.set_task_verification" });
|
|
15891
16558
|
}
|
|
@@ -15919,18 +16586,26 @@ var SddBoardWebSocketHandler = class {
|
|
|
15919
16586
|
);
|
|
15920
16587
|
if (!authorization.allowed) return;
|
|
15921
16588
|
}
|
|
15922
|
-
const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.
|
|
16589
|
+
const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.listBoardEntries())[0]?.runId;
|
|
15923
16590
|
if (runId) {
|
|
15924
|
-
|
|
15925
|
-
|
|
15926
|
-
|
|
15927
|
-
|
|
15928
|
-
|
|
16591
|
+
if (this.lifecycle && this.lifecycle.controlTransport !== "legacy-file") {
|
|
16592
|
+
await enqueueKanbanWorkflowCommand(
|
|
16593
|
+
this.lifecycle.projectRoot,
|
|
16594
|
+
kanbanWorkflowId("sdd", runId),
|
|
16595
|
+
{ type: action, payload: msg.payload }
|
|
16596
|
+
);
|
|
16597
|
+
} else {
|
|
16598
|
+
await this.store.appendControl(runId, {
|
|
16599
|
+
ts: Date.now(),
|
|
16600
|
+
type: action,
|
|
16601
|
+
payload: msg.payload
|
|
16602
|
+
});
|
|
16603
|
+
}
|
|
15929
16604
|
}
|
|
15930
16605
|
}
|
|
15931
16606
|
}
|
|
15932
16607
|
/**
|
|
15933
|
-
* Apply a cleanup/rollback/destroy from
|
|
16608
|
+
* Apply a cleanup/rollback/destroy from durable state and broadcast a structured
|
|
15934
16609
|
* `sdd.board.lifecycle_result`. Refuses (no-op) while a run is still active —
|
|
15935
16610
|
* the user must stop it first; the UI gates the buttons on `!active` and the
|
|
15936
16611
|
* Destroy flow auto-stops then waits before sending `destroy`.
|
|
@@ -15939,7 +16614,11 @@ var SddBoardWebSocketHandler = class {
|
|
|
15939
16614
|
if (!this.lifecycle) {
|
|
15940
16615
|
this.broadcast({
|
|
15941
16616
|
type: "sdd.board.lifecycle_result",
|
|
15942
|
-
payload: {
|
|
16617
|
+
payload: {
|
|
16618
|
+
op,
|
|
16619
|
+
ok: false,
|
|
16620
|
+
reason: "Lifecycle operations are not available in this session."
|
|
16621
|
+
}
|
|
15943
16622
|
});
|
|
15944
16623
|
return;
|
|
15945
16624
|
}
|
|
@@ -15955,6 +16634,7 @@ var SddBoardWebSocketHandler = class {
|
|
|
15955
16634
|
projectRoot: this.lifecycle.projectRoot,
|
|
15956
16635
|
paths: this.lifecycle.paths,
|
|
15957
16636
|
runId,
|
|
16637
|
+
stateTransport: this.lifecycle.stateTransport ?? "kanban",
|
|
15958
16638
|
revertMerged: payload?.revertMerged === true
|
|
15959
16639
|
});
|
|
15960
16640
|
this.broadcast({ type: "sdd.board.lifecycle_result", payload: result });
|
|
@@ -15974,31 +16654,40 @@ var SddBoardWebSocketHandler = class {
|
|
|
15974
16654
|
}
|
|
15975
16655
|
}
|
|
15976
16656
|
dispose() {
|
|
16657
|
+
this.disposed = true;
|
|
15977
16658
|
this.stopPolling();
|
|
15978
16659
|
this.unsub?.();
|
|
15979
16660
|
this.unsub = null;
|
|
16661
|
+
this.clients.clear();
|
|
16662
|
+
this.latest = null;
|
|
15980
16663
|
}
|
|
15981
16664
|
// ── internal ────────────────────────────────────────────────────────────
|
|
15982
16665
|
async pollLatest() {
|
|
15983
16666
|
if (this.pollInFlight) return;
|
|
15984
16667
|
this.pollInFlight = true;
|
|
15985
16668
|
try {
|
|
15986
|
-
const
|
|
15987
|
-
if (
|
|
15988
|
-
if (
|
|
16669
|
+
const snap = await this.loadLatestSnapshot();
|
|
16670
|
+
if (this.disposed) return;
|
|
16671
|
+
if (!snap) return;
|
|
16672
|
+
if (this.latest && this.latest.updatedAt >= snap.updatedAt && this.latest.runId === snap.runId) {
|
|
15989
16673
|
return;
|
|
15990
16674
|
}
|
|
15991
|
-
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
16675
|
+
this.latest = snap;
|
|
16676
|
+
this.broadcast({ type: "sdd.board.snapshot", payload: snap });
|
|
16677
|
+
} catch (err) {
|
|
16678
|
+
console.warn(
|
|
16679
|
+
JSON.stringify({
|
|
16680
|
+
level: "warn",
|
|
16681
|
+
event: "sdd_board.poll_failed",
|
|
16682
|
+
message: err instanceof Error ? err.message : String(err)
|
|
16683
|
+
})
|
|
16684
|
+
);
|
|
15996
16685
|
} finally {
|
|
15997
16686
|
this.pollInFlight = false;
|
|
15998
16687
|
}
|
|
15999
16688
|
}
|
|
16000
16689
|
startPolling() {
|
|
16001
|
-
if (!this.
|
|
16690
|
+
if (!this.standalonePollingEnabled || this.poll !== null || this.clients.size === 0) return;
|
|
16002
16691
|
this.poll = setInterval(() => void this.pollLatest(), 1e3);
|
|
16003
16692
|
this.poll.unref?.();
|
|
16004
16693
|
}
|
|
@@ -16008,17 +16697,44 @@ var SddBoardWebSocketHandler = class {
|
|
|
16008
16697
|
this.poll = null;
|
|
16009
16698
|
}
|
|
16010
16699
|
async sendCurrent(client) {
|
|
16011
|
-
const snap = this.latest ?? await this.
|
|
16700
|
+
const snap = this.latest ?? await this.loadLatestSnapshot();
|
|
16012
16701
|
if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
|
|
16013
16702
|
}
|
|
16014
16703
|
async broadcastCurrent() {
|
|
16015
|
-
const snap = this.latest ?? await this.
|
|
16704
|
+
const snap = this.latest ?? await this.loadLatestSnapshot();
|
|
16016
16705
|
if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
|
|
16017
16706
|
}
|
|
16018
|
-
async
|
|
16707
|
+
async loadLatestSnapshot() {
|
|
16708
|
+
if (this.usesKanbanState()) {
|
|
16709
|
+
const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
|
|
16710
|
+
const snapshots = states.map((state) => state.value).filter(isSddBoardSnapshot).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
16711
|
+
return snapshots[0] ?? null;
|
|
16712
|
+
}
|
|
16019
16713
|
const entry = await this.store.latest();
|
|
16020
16714
|
return entry ? this.store.load(entry.runId) : null;
|
|
16021
16715
|
}
|
|
16716
|
+
async listBoardEntries() {
|
|
16717
|
+
if (!this.usesKanbanState()) return this.store.list();
|
|
16718
|
+
const states = await listKanbanWorkflowStates(this.lifecycle.projectRoot, "sdd:");
|
|
16719
|
+
return states.flatMap((state) => {
|
|
16720
|
+
if (!isSddBoardSnapshot(state.value)) return [];
|
|
16721
|
+
const snapshot = state.value;
|
|
16722
|
+
return [
|
|
16723
|
+
{
|
|
16724
|
+
runId: snapshot.runId,
|
|
16725
|
+
...snapshot.specId ? { specId: snapshot.specId } : {},
|
|
16726
|
+
title: snapshot.title,
|
|
16727
|
+
status: snapshot.status,
|
|
16728
|
+
total: snapshot.progress.total,
|
|
16729
|
+
completed: snapshot.progress.completed,
|
|
16730
|
+
updatedAt: snapshot.updatedAt
|
|
16731
|
+
}
|
|
16732
|
+
];
|
|
16733
|
+
});
|
|
16734
|
+
}
|
|
16735
|
+
usesKanbanState() {
|
|
16736
|
+
return Boolean(this.lifecycle && this.lifecycle.stateTransport !== "legacy-file");
|
|
16737
|
+
}
|
|
16022
16738
|
broadcast(msg) {
|
|
16023
16739
|
const data = JSON.stringify(msg);
|
|
16024
16740
|
for (const client of this.clients) {
|
|
@@ -16029,17 +16745,20 @@ var SddBoardWebSocketHandler = class {
|
|
|
16029
16745
|
sendSerialized(client.ws, JSON.stringify(msg));
|
|
16030
16746
|
}
|
|
16031
16747
|
};
|
|
16748
|
+
function isSddBoardSnapshot(value) {
|
|
16749
|
+
if (!value || typeof value !== "object") return false;
|
|
16750
|
+
const snapshot = value;
|
|
16751
|
+
return typeof snapshot.runId === "string" && typeof snapshot.title === "string" && typeof snapshot.status === "string" && typeof snapshot.updatedAt === "number" && Array.isArray(snapshot.tasks) && Boolean(snapshot.progress && typeof snapshot.progress.total === "number");
|
|
16752
|
+
}
|
|
16032
16753
|
|
|
16033
16754
|
// src/server/sdd-wizard-wiring.ts
|
|
16034
|
-
import * as
|
|
16035
|
-
import {
|
|
16036
|
-
DefaultTaskStore,
|
|
16037
|
-
TaskTracker
|
|
16038
|
-
} from "@wrongstack/core/tasking";
|
|
16755
|
+
import * as path18 from "node:path";
|
|
16756
|
+
import { DefaultTaskStore, TaskTracker } from "@wrongstack/core/tasking";
|
|
16039
16757
|
import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
|
|
16040
16758
|
import { WorktreeManager as WorktreeManager2 } from "@wrongstack/core/worktree";
|
|
16041
16759
|
import {
|
|
16042
16760
|
cleanupStaleSddWorktrees,
|
|
16761
|
+
createKanbanSddSessionPersistence,
|
|
16043
16762
|
decomposeNonAtomicTasks,
|
|
16044
16763
|
gatherProjectContext,
|
|
16045
16764
|
makeAcceptanceCriteriaVerifier,
|
|
@@ -16057,7 +16776,7 @@ import {
|
|
|
16057
16776
|
} from "@wrongstack/sdd";
|
|
16058
16777
|
async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
|
|
16059
16778
|
const runTracker = tracker ?? (() => {
|
|
16060
|
-
const t = new TaskTracker({ store: new DefaultTaskStore() });
|
|
16779
|
+
const t = new TaskTracker({ store: deps2.taskStore ?? new DefaultTaskStore() });
|
|
16061
16780
|
t.setGraph(graph);
|
|
16062
16781
|
return t;
|
|
16063
16782
|
})();
|
|
@@ -16066,7 +16785,8 @@ async function startSddRunFromGraph(graph, deps2, config = {}, tracker) {
|
|
|
16066
16785
|
if (worktreesEnabled && await isGitWorkTree(deps2.projectRoot)) {
|
|
16067
16786
|
void cleanupStaleSddWorktrees({
|
|
16068
16787
|
projectRoot: deps2.projectRoot,
|
|
16069
|
-
boardsDir: deps2.projectSddBoards
|
|
16788
|
+
boardsDir: deps2.projectSddBoards,
|
|
16789
|
+
stateTransport: "kanban"
|
|
16070
16790
|
}).catch(() => void 0);
|
|
16071
16791
|
worktrees = new WorktreeManager2({
|
|
16072
16792
|
projectRoot: deps2.projectRoot,
|
|
@@ -16145,7 +16865,8 @@ function buildSddWizardDeps(opts) {
|
|
|
16145
16865
|
}).catch(() => {
|
|
16146
16866
|
projectContext = "";
|
|
16147
16867
|
});
|
|
16148
|
-
const
|
|
16868
|
+
const legacySessionPath = opts.paths.projectSddSession ?? path18.join(opts.paths.projectDir, "sdd-session.json");
|
|
16869
|
+
const sessionPersistence = createKanbanSddSessionPersistence(opts.projectRoot, legacySessionPath);
|
|
16149
16870
|
const specStore = new SpecStore({ baseDir: opts.paths.projectSpecs });
|
|
16150
16871
|
const graphStore = new TaskGraphStore({ baseDir: opts.paths.projectTaskGraphs });
|
|
16151
16872
|
const runIsolatedTurn = async (prompt, name2) => {
|
|
@@ -16166,7 +16887,7 @@ function buildSddWizardDeps(opts) {
|
|
|
16166
16887
|
const makeDriver = () => new SddInterviewDriver({
|
|
16167
16888
|
specStore,
|
|
16168
16889
|
graphStore,
|
|
16169
|
-
|
|
16890
|
+
sessionPersistence,
|
|
16170
16891
|
projectContext
|
|
16171
16892
|
});
|
|
16172
16893
|
const launchFromGraph = async (graph, config, tracker) => {
|
|
@@ -16178,6 +16899,7 @@ function buildSddWizardDeps(opts) {
|
|
|
16178
16899
|
projectRoot: opts.projectRoot,
|
|
16179
16900
|
subagentFactory: opts.subagentFactory,
|
|
16180
16901
|
projectSddBoards: opts.paths.projectSddBoards,
|
|
16902
|
+
taskStore: graphStore,
|
|
16181
16903
|
registry,
|
|
16182
16904
|
runIsolatedTurn,
|
|
16183
16905
|
...opts.brain ? { brain: opts.brain } : {}
|
|
@@ -16247,8 +16969,19 @@ var SddWizardWebSocketHandler = class {
|
|
|
16247
16969
|
lastAgentText = "";
|
|
16248
16970
|
/** Guards against overlapping interview turns (one in flight at a time). */
|
|
16249
16971
|
busy = false;
|
|
16250
|
-
/**
|
|
16972
|
+
/** Set when authoritative session state could not be read during bootstrap. */
|
|
16973
|
+
resumeError = null;
|
|
16974
|
+
/** Resolves once project-context gather + durable resume finish. */
|
|
16251
16975
|
ready;
|
|
16976
|
+
/**
|
|
16977
|
+
* Single-flight slot for the resume probe. `handleMessage` awaits this
|
|
16978
|
+
* when `resumeError` is set, so concurrent frames cannot race the retry
|
|
16979
|
+
* — one probe either clears the gate or re-latches it, then every
|
|
16980
|
+
* queued frame re-checks and proceeds. `null` when no probe is in
|
|
16981
|
+
* flight (or after the probe has settled).
|
|
16982
|
+
*/
|
|
16983
|
+
resumeProbe = null;
|
|
16984
|
+
disposed = false;
|
|
16252
16985
|
async bootstrap() {
|
|
16253
16986
|
try {
|
|
16254
16987
|
await this.deps.ensureReady?.();
|
|
@@ -16256,26 +16989,37 @@ var SddWizardWebSocketHandler = class {
|
|
|
16256
16989
|
}
|
|
16257
16990
|
await this.tryResume();
|
|
16258
16991
|
}
|
|
16259
|
-
/** Rehydrate a persisted interview if one exists
|
|
16992
|
+
/** Rehydrate a persisted interview if one exists. */
|
|
16260
16993
|
async tryResume() {
|
|
16261
16994
|
if (this.driver) return;
|
|
16262
16995
|
try {
|
|
16263
16996
|
const driver = this.deps.makeDriver();
|
|
16264
|
-
|
|
16997
|
+
const loaded = await driver.loadExisting();
|
|
16998
|
+
if (this.disposed) return;
|
|
16999
|
+
if (loaded) {
|
|
16265
17000
|
this.driver = driver;
|
|
16266
17001
|
this.lastAgentText = driver.getLastAgentText() ?? "";
|
|
16267
17002
|
}
|
|
16268
|
-
|
|
17003
|
+
this.resumeError = null;
|
|
17004
|
+
} catch (error2) {
|
|
17005
|
+
if (this.disposed) return;
|
|
16269
17006
|
this.driver = null;
|
|
16270
17007
|
this.lastAgentText = "";
|
|
17008
|
+
this.resumeError = error2 instanceof Error ? error2.message : String(error2);
|
|
16271
17009
|
}
|
|
16272
17010
|
}
|
|
16273
17011
|
addClient(ws) {
|
|
17012
|
+
if (this.disposed) return;
|
|
16274
17013
|
const client = { ws, id: crypto.randomUUID() };
|
|
16275
17014
|
this.clients.add(client);
|
|
16276
17015
|
ws.on("close", () => this.clients.delete(client));
|
|
16277
17016
|
ws.on("error", () => this.clients.delete(client));
|
|
16278
17017
|
void this.ready.then(() => {
|
|
17018
|
+
if (this.disposed || !this.clients.has(client)) return;
|
|
17019
|
+
if (this.resumeError) {
|
|
17020
|
+
this.send(client, { type: "sdd.spec.error", payload: { message: this.resumeError } });
|
|
17021
|
+
return;
|
|
17022
|
+
}
|
|
16279
17023
|
if (this.driver) {
|
|
16280
17024
|
this.send(client, this.snapshotMsg());
|
|
16281
17025
|
if (this.lastAgentText) {
|
|
@@ -16284,9 +17028,34 @@ var SddWizardWebSocketHandler = class {
|
|
|
16284
17028
|
}
|
|
16285
17029
|
});
|
|
16286
17030
|
}
|
|
17031
|
+
dispose() {
|
|
17032
|
+
this.disposed = true;
|
|
17033
|
+
this.clients.clear();
|
|
17034
|
+
this.driver = null;
|
|
17035
|
+
this.lastAgentText = "";
|
|
17036
|
+
this.busy = false;
|
|
17037
|
+
this.resumeProbe = null;
|
|
17038
|
+
}
|
|
16287
17039
|
async handleMessage(msg) {
|
|
17040
|
+
if (this.disposed) return;
|
|
16288
17041
|
try {
|
|
16289
17042
|
await this.ready;
|
|
17043
|
+
if (this.resumeError) {
|
|
17044
|
+
if (this.resumeProbe === null) {
|
|
17045
|
+
const probe = Promise.resolve().then(() => this.tryResume());
|
|
17046
|
+
this.resumeProbe = probe;
|
|
17047
|
+
void probe.finally(() => {
|
|
17048
|
+
setTimeout(() => {
|
|
17049
|
+
if (this.resumeProbe === probe) this.resumeProbe = null;
|
|
17050
|
+
}, 0);
|
|
17051
|
+
});
|
|
17052
|
+
}
|
|
17053
|
+
await this.resumeProbe;
|
|
17054
|
+
if (this.resumeError) {
|
|
17055
|
+
this.broadcast({ type: "sdd.spec.error", payload: { message: this.resumeError } });
|
|
17056
|
+
return;
|
|
17057
|
+
}
|
|
17058
|
+
}
|
|
16290
17059
|
switch (msg.type) {
|
|
16291
17060
|
case "sdd.spec.start":
|
|
16292
17061
|
await this.onStart(String(msg.payload?.goal ?? "").trim(), {
|
|
@@ -16303,7 +17072,10 @@ var SddWizardWebSocketHandler = class {
|
|
|
16303
17072
|
if (this.driver) {
|
|
16304
17073
|
this.broadcast(this.snapshotMsg());
|
|
16305
17074
|
if (this.lastAgentText) {
|
|
16306
|
-
this.broadcast({
|
|
17075
|
+
this.broadcast({
|
|
17076
|
+
type: "sdd.spec.agent_text",
|
|
17077
|
+
payload: { text: this.lastAgentText }
|
|
17078
|
+
});
|
|
16307
17079
|
}
|
|
16308
17080
|
}
|
|
16309
17081
|
break;
|
|
@@ -16483,6 +17255,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
16483
17255
|
this.broadcast(this.snapshotMsg());
|
|
16484
17256
|
try {
|
|
16485
17257
|
const text = await this.deps.runInterviewTurn(prompt);
|
|
17258
|
+
if (this.disposed) return;
|
|
16486
17259
|
this.lastAgentText = text;
|
|
16487
17260
|
if (this.driver) {
|
|
16488
17261
|
await this.driver.ingestAgentOutput(text);
|
|
@@ -16508,6 +17281,7 @@ var SddWizardWebSocketHandler = class {
|
|
|
16508
17281
|
}
|
|
16509
17282
|
}
|
|
16510
17283
|
send(client, msg) {
|
|
17284
|
+
if (!this.clients.has(client)) return;
|
|
16511
17285
|
sendSerialized(client.ws, JSON.stringify(msg));
|
|
16512
17286
|
}
|
|
16513
17287
|
};
|
|
@@ -16517,10 +17291,10 @@ import { recordTaskFileActivity } from "@wrongstack/kanban";
|
|
|
16517
17291
|
|
|
16518
17292
|
// src/server/setup-events-fleet-broadcaster.ts
|
|
16519
17293
|
import { watch as fsWatch } from "node:fs";
|
|
16520
|
-
import * as
|
|
17294
|
+
import * as path19 from "node:path";
|
|
16521
17295
|
function registerSetupEventsFleetBroadcaster(deps2) {
|
|
16522
17296
|
const { globalConfigPath, wpaths, context, clients, broadcast: broadcast2, onFleetBroadcaster, isDisposed } = deps2;
|
|
16523
|
-
const globalRoot = globalConfigPath ?
|
|
17297
|
+
const globalRoot = globalConfigPath ? path19.dirname(globalConfigPath) : void 0;
|
|
16524
17298
|
if (!globalRoot) return void 0;
|
|
16525
17299
|
const disposers = [];
|
|
16526
17300
|
const broadcastSessions = async () => {
|
|
@@ -16530,8 +17304,8 @@ function registerSetupEventsFleetBroadcaster(deps2) {
|
|
|
16530
17304
|
const sessions = await registry.list();
|
|
16531
17305
|
const ownEntry = sessions.find((s) => s.pid === process.pid);
|
|
16532
17306
|
const mySlug = ownEntry?.projectSlug ?? wpaths?.projectSlug;
|
|
16533
|
-
const myRoot =
|
|
16534
|
-
const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug :
|
|
17307
|
+
const myRoot = path19.resolve(context.projectRoot);
|
|
17308
|
+
const live = sessions.filter((s) => s.status === "active" || s.status === "idle").filter((s) => mySlug ? s.projectSlug === mySlug : path19.resolve(s.projectRoot) === myRoot).map((s) => ({
|
|
16535
17309
|
sessionId: s.sessionId,
|
|
16536
17310
|
projectName: s.projectName,
|
|
16537
17311
|
projectSlug: s.projectSlug,
|
|
@@ -16721,14 +17495,14 @@ function createSetupEventSessionHelpers(context, sessionBridge) {
|
|
|
16721
17495
|
|
|
16722
17496
|
// src/server/setup-events-status-watcher.ts
|
|
16723
17497
|
import { watch as fsWatch2 } from "node:fs";
|
|
16724
|
-
import * as
|
|
16725
|
-
import * as
|
|
17498
|
+
import * as fs17 from "node:fs/promises";
|
|
17499
|
+
import * as path21 from "node:path";
|
|
16726
17500
|
|
|
16727
17501
|
// src/server/setup-events-watcher.ts
|
|
16728
|
-
import * as
|
|
17502
|
+
import * as path20 from "node:path";
|
|
16729
17503
|
function statusProjectHashFromWatchFilename(projectsDir, filename) {
|
|
16730
17504
|
const raw = String(filename);
|
|
16731
|
-
const relative5 =
|
|
17505
|
+
const relative5 = path20.isAbsolute(raw) ? path20.relative(projectsDir, raw) : raw;
|
|
16732
17506
|
const parts = relative5.split(/[\\/]+/).filter(Boolean);
|
|
16733
17507
|
if (parts.length < 2 || parts.at(-1) !== "status.json") return null;
|
|
16734
17508
|
return parts.at(-2) ?? null;
|
|
@@ -16763,7 +17537,7 @@ function logFileWatcherMetrics(metrics) {
|
|
|
16763
17537
|
function registerSetupEventsStatusWatcher(deps2) {
|
|
16764
17538
|
const { wpaths, watcherMetrics, clients, broadcast: broadcast2, on, isDisposed } = deps2;
|
|
16765
17539
|
if (!wpaths?.projectStatus || !wpaths.globalRoot) return void 0;
|
|
16766
|
-
const projectsDir =
|
|
17540
|
+
const projectsDir = path21.join(wpaths.globalRoot, "projects");
|
|
16767
17541
|
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
16768
17542
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
16769
17543
|
const DEBOUNCE_MS = 150;
|
|
@@ -16808,7 +17582,7 @@ function registerSetupEventsStatusWatcher(deps2) {
|
|
|
16808
17582
|
let watcher;
|
|
16809
17583
|
const startWatcher = async () => {
|
|
16810
17584
|
try {
|
|
16811
|
-
await
|
|
17585
|
+
await fs17.mkdir(projectsDir, { recursive: true });
|
|
16812
17586
|
if (isDisposed()) return;
|
|
16813
17587
|
watcher = fsWatch2(
|
|
16814
17588
|
projectsDir,
|
|
@@ -16822,8 +17596,8 @@ function registerSetupEventsStatusWatcher(deps2) {
|
|
|
16822
17596
|
if (!knownProjectHashes.has(projectHash)) return;
|
|
16823
17597
|
if (watcherMetrics) watcherMetrics.filesProcessed++;
|
|
16824
17598
|
try {
|
|
16825
|
-
const targetFile =
|
|
16826
|
-
const content = await
|
|
17599
|
+
const targetFile = path21.join(projectsDir, projectHash, "status.json");
|
|
17600
|
+
const content = await fs17.readFile(targetFile, "utf-8");
|
|
16827
17601
|
const statusData = JSON.parse(content);
|
|
16828
17602
|
scheduleBroadcast(projectHash, statusData);
|
|
16829
17603
|
} catch {
|
|
@@ -16880,8 +17654,8 @@ function registerSetupEventsStatusWatcher(deps2) {
|
|
|
16880
17654
|
}
|
|
16881
17655
|
|
|
16882
17656
|
// src/server/setup-events-core-watchers.ts
|
|
16883
|
-
import * as
|
|
16884
|
-
import * as
|
|
17657
|
+
import * as fs18 from "node:fs/promises";
|
|
17658
|
+
import * as path22 from "node:path";
|
|
16885
17659
|
function registerSetupEventsCoreWatchers(deps2) {
|
|
16886
17660
|
const { broadcast: broadcast2, clients, context } = deps2;
|
|
16887
17661
|
const disposers = [];
|
|
@@ -16917,9 +17691,9 @@ function registerSetupEventsClientStatusWriter(deps2) {
|
|
|
16917
17691
|
if (wpaths?.projectStatus) {
|
|
16918
17692
|
try {
|
|
16919
17693
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
16920
|
-
const dir =
|
|
16921
|
-
await
|
|
16922
|
-
await
|
|
17694
|
+
const dir = path22.dirname(statusFile);
|
|
17695
|
+
await fs18.mkdir(dir, { recursive: true });
|
|
17696
|
+
await fs18.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
16923
17697
|
} catch (err) {
|
|
16924
17698
|
console.error(
|
|
16925
17699
|
JSON.stringify({
|
|
@@ -17274,6 +18048,7 @@ function setupEvents(deps2) {
|
|
|
17274
18048
|
});
|
|
17275
18049
|
pendingConfirms.set(id, {
|
|
17276
18050
|
resolve: e.resolve,
|
|
18051
|
+
sessionId: e.sessionId,
|
|
17277
18052
|
decisionSource: e.decisionSource,
|
|
17278
18053
|
riskTier: e.riskTier,
|
|
17279
18054
|
boundaryReason: e.boundaryReason,
|
|
@@ -17802,6 +18577,9 @@ var SpecsWebSocketHandler = class {
|
|
|
17802
18577
|
ws.on("error", () => this.clients.delete(client));
|
|
17803
18578
|
void this.sendList(client);
|
|
17804
18579
|
}
|
|
18580
|
+
dispose() {
|
|
18581
|
+
this.clients.clear();
|
|
18582
|
+
}
|
|
17805
18583
|
async handleMessage(msg) {
|
|
17806
18584
|
switch (msg.type) {
|
|
17807
18585
|
case "specs.list":
|
|
@@ -17937,14 +18715,15 @@ var SpecsWebSocketHandler = class {
|
|
|
17937
18715
|
}
|
|
17938
18716
|
}
|
|
17939
18717
|
send(client, msg) {
|
|
18718
|
+
if (!this.clients.has(client)) return;
|
|
17940
18719
|
sendSerialized(client.ws, JSON.stringify(msg));
|
|
17941
18720
|
}
|
|
17942
18721
|
};
|
|
17943
18722
|
|
|
17944
18723
|
// src/server/start-webui.ts
|
|
17945
|
-
import { randomUUID as
|
|
18724
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
17946
18725
|
import * as http2 from "node:http";
|
|
17947
|
-
import * as
|
|
18726
|
+
import * as path29 from "node:path";
|
|
17948
18727
|
import { createDefaultPipelines } from "@wrongstack/core/agent";
|
|
17949
18728
|
import { getSharedProjectMailbox as getSharedProjectMailbox4, resolveProjectDir as resolveProjectDir3 } from "@wrongstack/core/coordination";
|
|
17950
18729
|
import { createCompatibilityTrustBoundary as createCompatibilityTrustBoundary3 } from "@wrongstack/core/security";
|
|
@@ -17958,7 +18737,7 @@ import { DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2 } fro
|
|
|
17958
18737
|
import {
|
|
17959
18738
|
expectDefined as expectDefined3,
|
|
17960
18739
|
sessionScopedPath as sessionScopedPath3,
|
|
17961
|
-
|
|
18740
|
+
startSharedHeapWatchdog,
|
|
17962
18741
|
toErrorMessage as toErrorMessage13,
|
|
17963
18742
|
wstackGlobalRoot as wstackGlobalRoot2
|
|
17964
18743
|
} from "@wrongstack/core/utils";
|
|
@@ -17968,9 +18747,7 @@ import { ensureSessionShell } from "@wrongstack/tools";
|
|
|
17968
18747
|
|
|
17969
18748
|
// src/server/backend-services.ts
|
|
17970
18749
|
import { join as join12 } from "node:path";
|
|
17971
|
-
import {
|
|
17972
|
-
Agent
|
|
17973
|
-
} from "@wrongstack/core/agent";
|
|
18750
|
+
import { Agent } from "@wrongstack/core/agent";
|
|
17974
18751
|
import {
|
|
17975
18752
|
BrainDecisionLedger,
|
|
17976
18753
|
BrainMonitor,
|
|
@@ -17982,8 +18759,8 @@ import {
|
|
|
17982
18759
|
mailboxSessionTag,
|
|
17983
18760
|
ObservableBrainArbiter as ObservableBrainArbiterCtor
|
|
17984
18761
|
} from "@wrongstack/core/coordination";
|
|
18762
|
+
import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/types";
|
|
17985
18763
|
import { installDesignStudioMiddleware } from "@wrongstack/core/design";
|
|
17986
|
-
import { DEFAULT_TOOLS_CONFIG } from "@wrongstack/core/defaults";
|
|
17987
18764
|
import {
|
|
17988
18765
|
AutoCompactionMiddleware as AutoCompactionMiddlewareCtor,
|
|
17989
18766
|
createBrainRuntime,
|
|
@@ -17993,7 +18770,9 @@ import {
|
|
|
17993
18770
|
} from "@wrongstack/core/execution";
|
|
17994
18771
|
import { TOKENS } from "@wrongstack/core/kernel";
|
|
17995
18772
|
import { SessionMemoryConsolidator } from "@wrongstack/core/storage";
|
|
17996
|
-
import {
|
|
18773
|
+
import {
|
|
18774
|
+
resolveContextWindowPolicy as resolveContextWindowPolicy2
|
|
18775
|
+
} from "@wrongstack/core/types";
|
|
17997
18776
|
import {
|
|
17998
18777
|
estimateRequestTokensCalibrated,
|
|
17999
18778
|
toErrorMessage as toErrorMessage9
|
|
@@ -18385,9 +19164,9 @@ function clampDim(value, fallback) {
|
|
|
18385
19164
|
|
|
18386
19165
|
// src/server/worktree-ws-handler.ts
|
|
18387
19166
|
import { join as join11, resolve as resolve12, sep as sep5 } from "node:path";
|
|
19167
|
+
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
18388
19168
|
import { WorktreeManager as WorktreeManager3 } from "@wrongstack/core/worktree";
|
|
18389
19169
|
import { cleanupStaleSddWorktrees as cleanupStaleSddWorktrees2 } from "@wrongstack/sdd";
|
|
18390
|
-
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
18391
19170
|
var MAX_ACTIVITY = 6;
|
|
18392
19171
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["allocating", "active", "committing", "merging"]);
|
|
18393
19172
|
var MANAGED_BRANCH_RE = /^wstack\/ap\/[A-Za-z0-9._/-]+$/;
|
|
@@ -18406,6 +19185,20 @@ var WorktreeWebSocketHandler = class {
|
|
|
18406
19185
|
baseBranch = "";
|
|
18407
19186
|
broadcastInterval = null;
|
|
18408
19187
|
offs = [];
|
|
19188
|
+
/**
|
|
19189
|
+
* Single-flight guard for orphan scans. Two concurrent `scanAndBroadcast()`
|
|
19190
|
+
* calls would race: the first call reads `wt.listManaged()`, then the
|
|
19191
|
+
* second call's mutation completes (e.g. `removeOne`), then the first call
|
|
19192
|
+
* broadcasts a stale orphan list — the user sees a row persist and clicks
|
|
19193
|
+
* Remove a second time, hitting "remove failed (not a managed worktree?)".
|
|
19194
|
+
*
|
|
19195
|
+
* We coalesce concurrent triggers: while a scan is in flight, additional
|
|
19196
|
+
* callers are remembered as "need a re-scan" and exactly one follow-up
|
|
19197
|
+
* scan runs after the in-flight one finishes. The re-scan picks up any
|
|
19198
|
+
* mutations that landed during the previous scan's `listManaged()` window.
|
|
19199
|
+
*/
|
|
19200
|
+
scanInFlight = null;
|
|
19201
|
+
scanRescanNeeded = false;
|
|
18409
19202
|
addClient(ws) {
|
|
18410
19203
|
this.clients.add(ws);
|
|
18411
19204
|
ws.on("close", () => this.clients.delete(ws));
|
|
@@ -18424,7 +19217,10 @@ var WorktreeWebSocketHandler = class {
|
|
|
18424
19217
|
return true;
|
|
18425
19218
|
}
|
|
18426
19219
|
if (msg.type === "worktree.remove") {
|
|
18427
|
-
await this.removeOne(
|
|
19220
|
+
await this.removeOne(
|
|
19221
|
+
msg.payload?.["dir"],
|
|
19222
|
+
msg.payload?.["branch"]
|
|
19223
|
+
);
|
|
18428
19224
|
return true;
|
|
18429
19225
|
}
|
|
18430
19226
|
if (msg.type === "worktree.merge") {
|
|
@@ -18432,7 +19228,10 @@ var WorktreeWebSocketHandler = class {
|
|
|
18432
19228
|
return true;
|
|
18433
19229
|
}
|
|
18434
19230
|
if (msg.type === "worktree.diff") {
|
|
18435
|
-
await this.diffOne(
|
|
19231
|
+
await this.diffOne(
|
|
19232
|
+
msg.payload?.["dir"],
|
|
19233
|
+
msg.payload?.["baseBranch"]
|
|
19234
|
+
);
|
|
18436
19235
|
return true;
|
|
18437
19236
|
}
|
|
18438
19237
|
return false;
|
|
@@ -18441,6 +19240,10 @@ var WorktreeWebSocketHandler = class {
|
|
|
18441
19240
|
for (const off of this.offs) off();
|
|
18442
19241
|
this.offs.length = 0;
|
|
18443
19242
|
this.stopBroadcast();
|
|
19243
|
+
this.clients.clear();
|
|
19244
|
+
this.handles.clear();
|
|
19245
|
+
this.baseBranch = "";
|
|
19246
|
+
this.scanRescanNeeded = false;
|
|
18444
19247
|
}
|
|
18445
19248
|
// ── orphan management ─────────────────────────────────────────────────────
|
|
18446
19249
|
/** Absolute managed-worktrees root for this project. */
|
|
@@ -18461,12 +19264,43 @@ var WorktreeWebSocketHandler = class {
|
|
|
18461
19264
|
}
|
|
18462
19265
|
return live;
|
|
18463
19266
|
}
|
|
19267
|
+
/**
|
|
19268
|
+
* Coalesced orphan scan entry point. Multiple concurrent callers share one
|
|
19269
|
+
* in-flight scan; if a caller arrives during a scan, a single follow-up
|
|
19270
|
+
* scan is scheduled to pick up mutations that landed mid-scan. Returns the
|
|
19271
|
+
* shared promise so `await this.scanAndBroadcast()` still works for callers
|
|
19272
|
+
* that want to chain after the next broadcast.
|
|
19273
|
+
*/
|
|
19274
|
+
scanAndBroadcast() {
|
|
19275
|
+
if (this.scanInFlight) {
|
|
19276
|
+
this.scanRescanNeeded = true;
|
|
19277
|
+
return this.scanInFlight;
|
|
19278
|
+
}
|
|
19279
|
+
this.scanInFlight = this.runScanAndMaybeRescan();
|
|
19280
|
+
return this.scanInFlight;
|
|
19281
|
+
}
|
|
19282
|
+
/**
|
|
19283
|
+
* Run the actual scan, then drain a pending re-scan request if one came in
|
|
19284
|
+
* during the scan. The re-scan is bounded (one follow-up) — additional
|
|
19285
|
+
* requests that arrive during the follow-up coalesce into the next one.
|
|
19286
|
+
*/
|
|
19287
|
+
async runScanAndMaybeRescan() {
|
|
19288
|
+
try {
|
|
19289
|
+
await this.runScanOnce();
|
|
19290
|
+
while (this.scanRescanNeeded) {
|
|
19291
|
+
this.scanRescanNeeded = false;
|
|
19292
|
+
await this.runScanOnce();
|
|
19293
|
+
}
|
|
19294
|
+
} finally {
|
|
19295
|
+
this.scanInFlight = null;
|
|
19296
|
+
}
|
|
19297
|
+
}
|
|
18464
19298
|
/**
|
|
18465
19299
|
* Scan the disk for managed worktrees/branches NOT owned by a live in-session
|
|
18466
19300
|
* run and broadcast them as orphans, with whether it is safe to clean now.
|
|
18467
19301
|
* No-op (empty inventory) when management deps were not wired.
|
|
18468
19302
|
*/
|
|
18469
|
-
async
|
|
19303
|
+
async runScanOnce() {
|
|
18470
19304
|
if (!this.management) {
|
|
18471
19305
|
this.broadcast({ type: "worktree.orphans", payload: { orphans: [], canClean: false } });
|
|
18472
19306
|
return;
|
|
@@ -18522,7 +19356,8 @@ var WorktreeWebSocketHandler = class {
|
|
|
18522
19356
|
}
|
|
18523
19357
|
const res = await cleanupStaleSddWorktrees2({
|
|
18524
19358
|
projectRoot: this.management.projectRoot,
|
|
18525
|
-
boardsDir: this.management.boardsDir
|
|
19359
|
+
boardsDir: this.management.boardsDir,
|
|
19360
|
+
stateTransport: "kanban"
|
|
18526
19361
|
});
|
|
18527
19362
|
if (res.skippedReason) {
|
|
18528
19363
|
this.broadcast({
|
|
@@ -18535,26 +19370,45 @@ var WorktreeWebSocketHandler = class {
|
|
|
18535
19370
|
for (const [id, h] of [...this.handles]) {
|
|
18536
19371
|
if (!ACTIVE_STATUSES.has(h.status)) this.handles.delete(id);
|
|
18537
19372
|
}
|
|
18538
|
-
this.broadcast({
|
|
19373
|
+
this.broadcast({
|
|
19374
|
+
type: "worktree.cleanup_result",
|
|
19375
|
+
payload: { ok: true, removed: res.removed }
|
|
19376
|
+
});
|
|
18539
19377
|
this.broadcastState();
|
|
18540
19378
|
await this.scanAndBroadcast();
|
|
18541
19379
|
}
|
|
18542
19380
|
/** Remove/discard ONE worktree + branch. Refused while a live run owns it. */
|
|
18543
19381
|
async removeOne(dir, branch) {
|
|
18544
19382
|
if (!this.management || !dir && !branch) {
|
|
18545
|
-
this.broadcast({
|
|
19383
|
+
this.broadcast({
|
|
19384
|
+
type: "worktree.cleanup_result",
|
|
19385
|
+
payload: { ok: false, removed: 0, reason: "nothing to remove" }
|
|
19386
|
+
});
|
|
18546
19387
|
return;
|
|
18547
19388
|
}
|
|
18548
19389
|
if (branch && !MANAGED_BRANCH_RE.test(branch)) {
|
|
18549
|
-
this.broadcast({
|
|
19390
|
+
this.broadcast({
|
|
19391
|
+
type: "worktree.cleanup_result",
|
|
19392
|
+
payload: { ok: false, removed: 0, reason: "not a managed worktree branch" }
|
|
19393
|
+
});
|
|
18550
19394
|
return;
|
|
18551
19395
|
}
|
|
18552
19396
|
if (dir && !this.underRoot(dir)) {
|
|
18553
|
-
this.broadcast({
|
|
19397
|
+
this.broadcast({
|
|
19398
|
+
type: "worktree.cleanup_result",
|
|
19399
|
+
payload: { ok: false, removed: 0, reason: "path is outside the managed worktrees root" }
|
|
19400
|
+
});
|
|
18554
19401
|
return;
|
|
18555
19402
|
}
|
|
18556
19403
|
if (branch && this.liveActiveBranches().has(branch)) {
|
|
18557
|
-
this.broadcast({
|
|
19404
|
+
this.broadcast({
|
|
19405
|
+
type: "worktree.cleanup_result",
|
|
19406
|
+
payload: {
|
|
19407
|
+
ok: false,
|
|
19408
|
+
removed: 0,
|
|
19409
|
+
reason: "a run is live on this worktree \u2014 stop it first"
|
|
19410
|
+
}
|
|
19411
|
+
});
|
|
18558
19412
|
return;
|
|
18559
19413
|
}
|
|
18560
19414
|
let removed = false;
|
|
@@ -18563,31 +19417,54 @@ var WorktreeWebSocketHandler = class {
|
|
|
18563
19417
|
({ removed } = await wt.removeOne(dir, branch));
|
|
18564
19418
|
}
|
|
18565
19419
|
for (const [id, h] of [...this.handles]) {
|
|
18566
|
-
if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId))
|
|
19420
|
+
if (branch && h.branch === branch || dir && h.handleId && dir.endsWith(h.handleId))
|
|
19421
|
+
this.handles.delete(id);
|
|
18567
19422
|
}
|
|
18568
|
-
this.broadcast({
|
|
19423
|
+
this.broadcast({
|
|
19424
|
+
type: "worktree.cleanup_result",
|
|
19425
|
+
payload: {
|
|
19426
|
+
ok: removed,
|
|
19427
|
+
removed: removed ? 1 : 0,
|
|
19428
|
+
reason: removed ? void 0 : "remove failed (not a managed worktree?)"
|
|
19429
|
+
}
|
|
19430
|
+
});
|
|
18569
19431
|
this.broadcastState();
|
|
18570
19432
|
await this.scanAndBroadcast();
|
|
18571
19433
|
}
|
|
18572
19434
|
/** Squash-merge ONE branch into base. Refused while a live run owns it. */
|
|
18573
19435
|
async mergeBranch(branch) {
|
|
18574
19436
|
if (!this.management || !branch) {
|
|
18575
|
-
this.broadcast({
|
|
19437
|
+
this.broadcast({
|
|
19438
|
+
type: "worktree.merge_result",
|
|
19439
|
+
payload: { ok: false, branch: branch ?? "", reason: "no branch" }
|
|
19440
|
+
});
|
|
18576
19441
|
return;
|
|
18577
19442
|
}
|
|
18578
19443
|
if (!MANAGED_BRANCH_RE.test(branch)) {
|
|
18579
|
-
this.broadcast({
|
|
19444
|
+
this.broadcast({
|
|
19445
|
+
type: "worktree.merge_result",
|
|
19446
|
+
payload: { ok: false, branch, reason: "not a managed worktree branch" }
|
|
19447
|
+
});
|
|
18580
19448
|
return;
|
|
18581
19449
|
}
|
|
18582
19450
|
if (this.liveActiveBranches().has(branch)) {
|
|
18583
|
-
this.broadcast({
|
|
19451
|
+
this.broadcast({
|
|
19452
|
+
type: "worktree.merge_result",
|
|
19453
|
+
payload: { ok: false, branch, reason: "a run is live on this worktree \u2014 stop it first" }
|
|
19454
|
+
});
|
|
18584
19455
|
return;
|
|
18585
19456
|
}
|
|
18586
19457
|
const wt = new WorktreeManager3({ projectRoot: this.management.projectRoot });
|
|
18587
19458
|
const res = await wt.mergeBranch(branch);
|
|
18588
19459
|
this.broadcast({
|
|
18589
19460
|
type: "worktree.merge_result",
|
|
18590
|
-
payload: {
|
|
19461
|
+
payload: {
|
|
19462
|
+
ok: res.ok,
|
|
19463
|
+
branch,
|
|
19464
|
+
conflict: res.conflict,
|
|
19465
|
+
conflictFiles: res.conflictFiles,
|
|
19466
|
+
reason: res.reason
|
|
19467
|
+
}
|
|
18591
19468
|
});
|
|
18592
19469
|
await this.scanAndBroadcast();
|
|
18593
19470
|
}
|
|
@@ -18629,8 +19506,14 @@ var WorktreeWebSocketHandler = class {
|
|
|
18629
19506
|
}),
|
|
18630
19507
|
on("worktree.committed", (p) => {
|
|
18631
19508
|
const e = p;
|
|
18632
|
-
this.patch(e.handleId, {
|
|
18633
|
-
|
|
19509
|
+
this.patch(e.handleId, {
|
|
19510
|
+
status: "committing",
|
|
19511
|
+
insertions: e.insertions,
|
|
19512
|
+
deletions: e.deletions,
|
|
19513
|
+
files: e.files
|
|
19514
|
+
});
|
|
19515
|
+
if (e.committed)
|
|
19516
|
+
this.activity(e.handleId, "committed", `+${e.insertions}/-${e.deletions} (${e.files}f)`);
|
|
18634
19517
|
this.broadcastState();
|
|
18635
19518
|
}),
|
|
18636
19519
|
on("worktree.merged", (p) => {
|
|
@@ -18671,10 +19554,15 @@ var WorktreeWebSocketHandler = class {
|
|
|
18671
19554
|
activity(id, kind, text) {
|
|
18672
19555
|
const cur = this.handles.get(id);
|
|
18673
19556
|
if (cur) {
|
|
18674
|
-
const recentActivity = [...cur.recentActivity, { kind, text, at: Date.now() }].slice(
|
|
19557
|
+
const recentActivity = [...cur.recentActivity, { kind, text, at: Date.now() }].slice(
|
|
19558
|
+
-MAX_ACTIVITY
|
|
19559
|
+
);
|
|
18675
19560
|
this.handles.set(id, { ...cur, recentActivity });
|
|
18676
19561
|
}
|
|
18677
|
-
this.broadcast({
|
|
19562
|
+
this.broadcast({
|
|
19563
|
+
type: "worktree.event",
|
|
19564
|
+
payload: { kind, handleId: id, text, at: Date.now() }
|
|
19565
|
+
});
|
|
18678
19566
|
}
|
|
18679
19567
|
stateMessage() {
|
|
18680
19568
|
return {
|
|
@@ -18753,7 +19641,13 @@ async function createAgentServices(input) {
|
|
|
18753
19641
|
minImportance: config.Sage?.inject?.minImportance,
|
|
18754
19642
|
repeatCooldownMs: config.Sage?.inject?.repeatCooldownMs,
|
|
18755
19643
|
verifyOnMutation: config.Sage?.hygiene?.autoOnFileChange,
|
|
18756
|
-
triggers: config.Sage?.inject?.triggers
|
|
19644
|
+
triggers: config.Sage?.inject?.triggers,
|
|
19645
|
+
// Resolve the live session so retrieval and cooldown are session-scoped
|
|
19646
|
+
// (matching the CLI wiring in wiring/sage.ts). Without this, the
|
|
19647
|
+
// middleware falls back to ctx.session.id for cooldown but passes
|
|
19648
|
+
// undefined to retrieval, causing owned session-scoped memories to be
|
|
19649
|
+
// silently excluded from tool-call injection.
|
|
19650
|
+
getSessionId: () => input.sessionGetter().id
|
|
18757
19651
|
})
|
|
18758
19652
|
);
|
|
18759
19653
|
}
|
|
@@ -18877,6 +19771,7 @@ async function createAgentServices(input) {
|
|
|
18877
19771
|
perIterationOutputCapBytes: config.tools?.perIterationOutputCapBytes ?? DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
|
|
18878
19772
|
tracer: void 0
|
|
18879
19773
|
});
|
|
19774
|
+
input.installToolBoundary?.(pipelines);
|
|
18880
19775
|
const webuiLogger = container.resolve(TOKENS.Logger);
|
|
18881
19776
|
void discoverMailboxBridgeForWebui({
|
|
18882
19777
|
projectRoot,
|
|
@@ -18981,33 +19876,35 @@ async function createAgentServices(input) {
|
|
|
18981
19876
|
brainLog.push(entry);
|
|
18982
19877
|
if (brainLog.length > 20) brainLog.shift();
|
|
18983
19878
|
};
|
|
18984
|
-
|
|
18985
|
-
|
|
18986
|
-
|
|
18987
|
-
|
|
18988
|
-
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
|
|
18994
|
-
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
18998
|
-
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19002
|
-
|
|
19003
|
-
|
|
19004
|
-
|
|
19005
|
-
|
|
19006
|
-
|
|
19007
|
-
|
|
19008
|
-
|
|
19009
|
-
|
|
19010
|
-
|
|
19879
|
+
const brainLogOffs = [
|
|
19880
|
+
events.on(
|
|
19881
|
+
"brain.decision_answered",
|
|
19882
|
+
(e) => pushBrainLog({
|
|
19883
|
+
at: e.at,
|
|
19884
|
+
kind: "answered",
|
|
19885
|
+
question: e.request.question,
|
|
19886
|
+
outcome: e.decision.type === "answer" ? e.decision.optionId ?? e.decision.text : ""
|
|
19887
|
+
})
|
|
19888
|
+
),
|
|
19889
|
+
events.on(
|
|
19890
|
+
"brain.decision_ask_human",
|
|
19891
|
+
(e) => pushBrainLog({
|
|
19892
|
+
at: e.at,
|
|
19893
|
+
kind: "ask_human",
|
|
19894
|
+
question: e.request.question,
|
|
19895
|
+
outcome: "needs human judgement"
|
|
19896
|
+
})
|
|
19897
|
+
),
|
|
19898
|
+
events.on(
|
|
19899
|
+
"brain.decision_denied",
|
|
19900
|
+
(e) => pushBrainLog({
|
|
19901
|
+
at: e.at,
|
|
19902
|
+
kind: "denied",
|
|
19903
|
+
question: e.request.question,
|
|
19904
|
+
outcome: e.decision.type === "deny" ? e.decision.reason : ""
|
|
19905
|
+
})
|
|
19906
|
+
)
|
|
19907
|
+
];
|
|
19011
19908
|
const brainMailbox = getSharedProjectMailbox2(wpaths.projectDir, events);
|
|
19012
19909
|
brainMonitor = new BrainMonitor({
|
|
19013
19910
|
events,
|
|
@@ -19087,7 +19984,8 @@ async function createAgentServices(input) {
|
|
|
19087
19984
|
// subagent's fallback extension's tracker hooks are undefined and
|
|
19088
19985
|
// round-robin keeps reassigning the doomed model. Mirrors the CLI
|
|
19089
19986
|
// factory wiring at host-subagent-factory.ts:337.
|
|
19090
|
-
statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker)
|
|
19987
|
+
statusTracker: container.safeResolve(TOKENS.ProviderModelStatusTracker),
|
|
19988
|
+
...input.installToolBoundary ? { installToolBoundary: input.installToolBoundary } : {}
|
|
19091
19989
|
}),
|
|
19092
19990
|
paths: {
|
|
19093
19991
|
projectSpecs: wpaths.projectSpecs,
|
|
@@ -19119,6 +20017,19 @@ async function createAgentServices(input) {
|
|
|
19119
20017
|
getActiveSessionId: () => context.session.id
|
|
19120
20018
|
}
|
|
19121
20019
|
);
|
|
20020
|
+
let realtimeHandlersDisposed = false;
|
|
20021
|
+
const disposeRealtimeHandlers = () => {
|
|
20022
|
+
if (realtimeHandlersDisposed) return;
|
|
20023
|
+
realtimeHandlersDisposed = true;
|
|
20024
|
+
for (const off of brainLogOffs) off();
|
|
20025
|
+
goalHandler.dispose();
|
|
20026
|
+
sddBoardHandler.dispose();
|
|
20027
|
+
sddWizardHandler.dispose();
|
|
20028
|
+
specsHandler.dispose();
|
|
20029
|
+
worktreeHandler.dispose();
|
|
20030
|
+
terminalHandler.dispose();
|
|
20031
|
+
collabHandler.dispose();
|
|
20032
|
+
};
|
|
19122
20033
|
return {
|
|
19123
20034
|
collabBus,
|
|
19124
20035
|
compactor,
|
|
@@ -19144,12 +20055,16 @@ async function createAgentServices(input) {
|
|
|
19144
20055
|
worktreeHandler,
|
|
19145
20056
|
terminalHandler,
|
|
19146
20057
|
collabHandler,
|
|
20058
|
+
disposeRealtimeHandlers,
|
|
19147
20059
|
updateAutoCompactionMaxContext
|
|
19148
20060
|
};
|
|
19149
20061
|
}
|
|
19150
20062
|
|
|
19151
20063
|
// src/server/connection-handler.ts
|
|
19152
|
-
var
|
|
20064
|
+
var DEFAULT_WS_RATE_LIMIT = 600;
|
|
20065
|
+
var rawRateLimit = process.env["WEBUI_RATE_LIMIT"];
|
|
20066
|
+
var parsedRateLimit = rawRateLimit ? Number.parseInt(rawRateLimit, 10) : DEFAULT_WS_RATE_LIMIT;
|
|
20067
|
+
var RATE_LIMIT_MESSAGES = Number.isFinite(parsedRateLimit) && parsedRateLimit >= 0 ? parsedRateLimit : DEFAULT_WS_RATE_LIMIT;
|
|
19153
20068
|
var VERBOSE_WS_LOGGING = process.env["WEBUI_VERBOSE_WS"] === "1" || process.env["WEBUI_VERBOSE_WS"] === "true";
|
|
19154
20069
|
function readRemoteAddress(ws) {
|
|
19155
20070
|
const socket = ws._socket;
|
|
@@ -19229,8 +20144,74 @@ function createConnectionHandler(options) {
|
|
|
19229
20144
|
};
|
|
19230
20145
|
}
|
|
19231
20146
|
|
|
20147
|
+
// src/server/governance-runtime.ts
|
|
20148
|
+
import {
|
|
20149
|
+
createGovernanceMutationSnapshotBridge
|
|
20150
|
+
} from "@wrongstack/runtime/governance-mutation-snapshot-bridge";
|
|
20151
|
+
import { sanitizeGovernanceMessage } from "@wrongstack/runtime/governance-sanitize";
|
|
20152
|
+
var WRONGSTACK_GOVERNANCE_ENV = "WRONGSTACK_GOVERNANCE";
|
|
20153
|
+
var DEFAULT_DEPENDENCIES = {
|
|
20154
|
+
load: () => import("@wrongstack/runtime/governance-bootstrap"),
|
|
20155
|
+
createMutationBridge: createGovernanceMutationSnapshotBridge
|
|
20156
|
+
};
|
|
20157
|
+
function webUiGovernanceRuntimeEnabled(environment) {
|
|
20158
|
+
return environment[WRONGSTACK_GOVERNANCE_ENV]?.trim() === "1";
|
|
20159
|
+
}
|
|
20160
|
+
async function setupWebUiGovernance(input, dependencies = DEFAULT_DEPENDENCIES) {
|
|
20161
|
+
if (!webUiGovernanceRuntimeEnabled(input.environment)) return void 0;
|
|
20162
|
+
let loaded;
|
|
20163
|
+
try {
|
|
20164
|
+
loaded = await dependencies.load();
|
|
20165
|
+
} catch (error2) {
|
|
20166
|
+
input.logger.warn("governance: standalone WebUI runtime module unavailable", {
|
|
20167
|
+
message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
|
|
20168
|
+
});
|
|
20169
|
+
return void 0;
|
|
20170
|
+
}
|
|
20171
|
+
let result;
|
|
20172
|
+
try {
|
|
20173
|
+
result = await loaded.bootstrapGovernanceRuntime({
|
|
20174
|
+
projectRoot: input.projectRoot,
|
|
20175
|
+
projectId: input.projectId,
|
|
20176
|
+
adminClientId: `webui-control-${process.pid}-${input.sessionId}`,
|
|
20177
|
+
modelClientId: `webui-model-${input.sessionId}`,
|
|
20178
|
+
modelCapabilities: ["task_read", "command_submit", "shadow_observe"]
|
|
20179
|
+
});
|
|
20180
|
+
} catch (error2) {
|
|
20181
|
+
input.logger.warn("governance: standalone WebUI bootstrap failed open", {
|
|
20182
|
+
message: sanitizeGovernanceMessage(error2 instanceof Error ? error2.message : String(error2))
|
|
20183
|
+
});
|
|
20184
|
+
return void 0;
|
|
20185
|
+
}
|
|
20186
|
+
if (result.mode !== "governed") {
|
|
20187
|
+
input.contextMeta["governance"] = result;
|
|
20188
|
+
input.logger.warn(`governance: standalone WebUI legacy fallback (${result.code})`, {
|
|
20189
|
+
phase: result.phase,
|
|
20190
|
+
cleanup: result.cleanup,
|
|
20191
|
+
message: result.message
|
|
20192
|
+
});
|
|
20193
|
+
return void 0;
|
|
20194
|
+
}
|
|
20195
|
+
const snapshot = result.handle.snapshot();
|
|
20196
|
+
input.contextMeta["governance"] = snapshot;
|
|
20197
|
+
input.logger.info(`governance: ${snapshot.source} standalone WebUI control plane`);
|
|
20198
|
+
const mutationSnapshots = dependencies.createMutationBridge({
|
|
20199
|
+
events: input.events,
|
|
20200
|
+
sink: result.handle,
|
|
20201
|
+
captureWorkspaceCheckpoint: input.captureWorkspaceCheckpoint,
|
|
20202
|
+
logger: input.logger
|
|
20203
|
+
});
|
|
20204
|
+
return Object.freeze({
|
|
20205
|
+
installToolBoundary: (pipelines) => mutationSnapshots.installToolBoundary(pipelines),
|
|
20206
|
+
close: async () => {
|
|
20207
|
+
await mutationSnapshots.close();
|
|
20208
|
+
return result.handle.close();
|
|
20209
|
+
}
|
|
20210
|
+
});
|
|
20211
|
+
}
|
|
20212
|
+
|
|
19232
20213
|
// src/server/message-dispatcher.ts
|
|
19233
|
-
import
|
|
20214
|
+
import path23 from "node:path";
|
|
19234
20215
|
function createMessageDispatcher(opts) {
|
|
19235
20216
|
const { state, deps: deps2, routes, promptsCtx, codebaseIndexing, runLock, pendingConfirms } = opts;
|
|
19236
20217
|
function makeWorklistContext() {
|
|
@@ -19251,7 +20232,7 @@ function createMessageDispatcher(opts) {
|
|
|
19251
20232
|
skillLoader: deps2.skillLoader,
|
|
19252
20233
|
skillInstaller: deps2.skillInstaller,
|
|
19253
20234
|
projectRoot,
|
|
19254
|
-
projectSkillsDir:
|
|
20235
|
+
projectSkillsDir: path23.join(projectRoot, ".wrongstack", "skills"),
|
|
19255
20236
|
globalSkillsDir: deps2.wpaths.globalSkills
|
|
19256
20237
|
};
|
|
19257
20238
|
}
|
|
@@ -19503,7 +20484,7 @@ function createMessageDispatcher(opts) {
|
|
|
19503
20484
|
|
|
19504
20485
|
// src/server/pre-context-services.ts
|
|
19505
20486
|
import { createRequire as createRequire3 } from "node:module";
|
|
19506
|
-
import * as
|
|
20487
|
+
import * as path26 from "node:path";
|
|
19507
20488
|
import { Context, DefaultSystemPromptBuilder } from "@wrongstack/core/agent";
|
|
19508
20489
|
import {
|
|
19509
20490
|
getSharedProjectMailbox as getSharedProjectMailbox3,
|
|
@@ -19557,8 +20538,8 @@ import { configureDangerBypass, configureExecPolicy } from "@wrongstack/tools";
|
|
|
19557
20538
|
import { attachSessionKanbanMirror, hydrateSessionKanban } from "@wrongstack/tools/session-kanban";
|
|
19558
20539
|
|
|
19559
20540
|
// src/server/model-auto-discovery.ts
|
|
19560
|
-
import * as
|
|
19561
|
-
import * as
|
|
20541
|
+
import * as fs19 from "node:fs/promises";
|
|
20542
|
+
import * as path24 from "node:path";
|
|
19562
20543
|
import { COMPATIBLE_PRESETS, discoverOpenAICompatibleModels } from "@wrongstack/providers";
|
|
19563
20544
|
function isOverlayRegistry(value) {
|
|
19564
20545
|
return !!value && typeof value === "object" && typeof value.mergeOverlay === "function";
|
|
@@ -19584,7 +20565,7 @@ function eligibleProviders(config) {
|
|
|
19584
20565
|
}
|
|
19585
20566
|
async function readCache(file) {
|
|
19586
20567
|
try {
|
|
19587
|
-
return JSON.parse(await
|
|
20568
|
+
return JSON.parse(await fs19.readFile(file, "utf8"));
|
|
19588
20569
|
} catch {
|
|
19589
20570
|
return {};
|
|
19590
20571
|
}
|
|
@@ -19594,7 +20575,7 @@ async function discoverAndMergeWebuiProviders(opts) {
|
|
|
19594
20575
|
if (!isOverlayRegistry(registry)) return;
|
|
19595
20576
|
const targets = eligibleProviders(opts.config);
|
|
19596
20577
|
if (targets.length === 0) return;
|
|
19597
|
-
const cacheFile =
|
|
20578
|
+
const cacheFile = path24.join(opts.cacheDir, "discovered-models-cache.json");
|
|
19598
20579
|
const cache2 = await readCache(cacheFile);
|
|
19599
20580
|
let cacheDirty = false;
|
|
19600
20581
|
await Promise.all(
|
|
@@ -19631,8 +20612,8 @@ async function discoverAndMergeWebuiProviders(opts) {
|
|
|
19631
20612
|
);
|
|
19632
20613
|
if (cacheDirty) {
|
|
19633
20614
|
try {
|
|
19634
|
-
await
|
|
19635
|
-
await
|
|
20615
|
+
await fs19.mkdir(path24.dirname(cacheFile), { recursive: true });
|
|
20616
|
+
await fs19.writeFile(cacheFile, JSON.stringify(cache2), "utf8");
|
|
19636
20617
|
} catch {
|
|
19637
20618
|
opts.logger?.debug?.("provider auto-discovery cache write failed");
|
|
19638
20619
|
}
|
|
@@ -19728,7 +20709,7 @@ function resolveSetupProvider(opts) {
|
|
|
19728
20709
|
}
|
|
19729
20710
|
|
|
19730
20711
|
// src/server/standalone-session-identity.ts
|
|
19731
|
-
import * as
|
|
20712
|
+
import * as path25 from "node:path";
|
|
19732
20713
|
import {
|
|
19733
20714
|
AgentStatusTracker,
|
|
19734
20715
|
FleetNotifier,
|
|
@@ -19747,7 +20728,7 @@ async function createStandaloneSessionIdentityLifecycle(opts) {
|
|
|
19747
20728
|
let activeTarget = {
|
|
19748
20729
|
projectSlug: paths.projectSlug,
|
|
19749
20730
|
projectRoot: paths.projectRoot,
|
|
19750
|
-
projectName:
|
|
20731
|
+
projectName: path25.basename(paths.projectRoot),
|
|
19751
20732
|
workingDir: opts.workingDir
|
|
19752
20733
|
};
|
|
19753
20734
|
let pendingClaim;
|
|
@@ -19978,7 +20959,7 @@ async function createPreContextServices(input) {
|
|
|
19978
20959
|
await discoverAndMergeWebuiProviders({
|
|
19979
20960
|
config,
|
|
19980
20961
|
registry: modelsRegistry,
|
|
19981
|
-
cacheDir:
|
|
20962
|
+
cacheDir: path26.dirname(wpaths.modelsCache),
|
|
19982
20963
|
logger
|
|
19983
20964
|
});
|
|
19984
20965
|
} catch (err) {
|
|
@@ -20030,7 +21011,7 @@ async function createPreContextServices(input) {
|
|
|
20030
21011
|
configureChildEnvGitIdentity(config.git?.identity ?? null);
|
|
20031
21012
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
20032
21013
|
const mcpTokenStore = new MCPVaultTokenStore(
|
|
20033
|
-
|
|
21014
|
+
path26.join(wpaths.projectDir, "mcp-auth.json"),
|
|
20034
21015
|
vault
|
|
20035
21016
|
);
|
|
20036
21017
|
const mcpAuthorizationManager = new MCPAuthorizationManager({ store: mcpTokenStore });
|
|
@@ -20136,7 +21117,7 @@ async function createPreContextServices(input) {
|
|
|
20136
21117
|
};
|
|
20137
21118
|
const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
|
|
20138
21119
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
20139
|
-
manifestPath:
|
|
21120
|
+
manifestPath: path26.join(wpaths.configDir, "installed-skills.json"),
|
|
20140
21121
|
projectSkillsDir: wpaths.inProjectSkills,
|
|
20141
21122
|
globalSkillsDir: wpaths.globalSkills,
|
|
20142
21123
|
projectHash: wpaths.projectHash,
|
|
@@ -20146,8 +21127,8 @@ async function createPreContextServices(input) {
|
|
|
20146
21127
|
const bundledPromptsDir = promptsEnabled ? (() => {
|
|
20147
21128
|
try {
|
|
20148
21129
|
const req = createRequire3(import.meta.url);
|
|
20149
|
-
return
|
|
20150
|
-
|
|
21130
|
+
return path26.join(
|
|
21131
|
+
path26.dirname(req.resolve("@wrongstack/core/package.json")),
|
|
20151
21132
|
"data",
|
|
20152
21133
|
"prompts"
|
|
20153
21134
|
);
|
|
@@ -20251,7 +21232,7 @@ async function createPreContextServices(input) {
|
|
|
20251
21232
|
}
|
|
20252
21233
|
|
|
20253
21234
|
// src/server/routes.ts
|
|
20254
|
-
import
|
|
21235
|
+
import path27 from "node:path";
|
|
20255
21236
|
import { makeProviderFromConfig as makeProviderFromConfig2, withCatalogCapabilities } from "@wrongstack/providers";
|
|
20256
21237
|
|
|
20257
21238
|
// src/server/mode-handlers.ts
|
|
@@ -20543,7 +21524,7 @@ function buildRoutes(state, deps2, cb) {
|
|
|
20543
21524
|
};
|
|
20544
21525
|
const mailboxRoutes = createMailboxRouteHandlers({
|
|
20545
21526
|
getProjectRoot: state.getProjectRoot,
|
|
20546
|
-
getGlobalRoot: () =>
|
|
21527
|
+
getGlobalRoot: () => path27.dirname(deps2.globalConfigPath),
|
|
20547
21528
|
events: deps2.events
|
|
20548
21529
|
});
|
|
20549
21530
|
const mcpRoutes = {
|
|
@@ -20616,7 +21597,7 @@ function buildRoutes(state, deps2, cb) {
|
|
|
20616
21597
|
}
|
|
20617
21598
|
|
|
20618
21599
|
// src/server/server-runtime.ts
|
|
20619
|
-
import * as
|
|
21600
|
+
import * as path28 from "node:path";
|
|
20620
21601
|
import { createRequire as createRequire4 } from "node:module";
|
|
20621
21602
|
import { fileURLToPath } from "node:url";
|
|
20622
21603
|
import { WebSocketServer } from "ws";
|
|
@@ -20677,7 +21658,7 @@ function createSessionStartPayload(g) {
|
|
|
20677
21658
|
inputCost,
|
|
20678
21659
|
outputCost,
|
|
20679
21660
|
cacheReadCost,
|
|
20680
|
-
projectName:
|
|
21661
|
+
projectName: path28.basename(projectRoot) || projectRoot,
|
|
20681
21662
|
projectRoot,
|
|
20682
21663
|
cwd: g.getWorkingDir(),
|
|
20683
21664
|
mode: g.getModeId(),
|
|
@@ -20765,13 +21746,13 @@ function armEvents(wssPrimary, wssSecondary, wsHost, httpPort, setupInput, watch
|
|
|
20765
21746
|
};
|
|
20766
21747
|
}
|
|
20767
21748
|
function resolveWebuiDistDir(fromUrl, explicitDistDir) {
|
|
20768
|
-
if (explicitDistDir) return
|
|
21749
|
+
if (explicitDistDir) return path28.resolve(explicitDistDir);
|
|
20769
21750
|
try {
|
|
20770
21751
|
const requireFromHere2 = createRequire4(fromUrl);
|
|
20771
21752
|
const serverEntry = requireFromHere2.resolve("@wrongstack/webui");
|
|
20772
|
-
return
|
|
21753
|
+
return path28.dirname(serverEntry);
|
|
20773
21754
|
} catch {
|
|
20774
|
-
return
|
|
21755
|
+
return path28.resolve(path28.dirname(fileURLToPath(fromUrl)), "..", "..", "dist");
|
|
20775
21756
|
}
|
|
20776
21757
|
}
|
|
20777
21758
|
function startHttpServer(opts) {
|
|
@@ -20933,6 +21914,17 @@ async function startWebUI(opts = {}) {
|
|
|
20933
21914
|
const prefSnapshot2 = () => prefSnapshot(context.meta);
|
|
20934
21915
|
const persistPrefsToConfig2 = async (payload) => persistPrefsToConfig(prefHelperDeps, configWriteLock, payload);
|
|
20935
21916
|
const trustBoundary = opts.trustBoundary ?? createCompatibilityTrustBoundary3({ policyId: "webui-trusted-host-compat-v1" });
|
|
21917
|
+
const governanceHandle = opts.installToolBoundary === void 0 ? await setupWebUiGovernance({
|
|
21918
|
+
environment: process.env,
|
|
21919
|
+
projectRoot,
|
|
21920
|
+
projectId: wpaths.projectSlug,
|
|
21921
|
+
sessionId: session.id,
|
|
21922
|
+
contextMeta: context.meta,
|
|
21923
|
+
events,
|
|
21924
|
+
logger,
|
|
21925
|
+
captureWorkspaceCheckpoint: async () => sessionStore.captureWorkspaceCheckpoint?.(session.id, 0)
|
|
21926
|
+
}) : void 0;
|
|
21927
|
+
const installToolBoundary = opts.installToolBoundary ?? governanceHandle?.installToolBoundary;
|
|
20936
21928
|
const agentServices = await createAgentServices({
|
|
20937
21929
|
trustBoundary,
|
|
20938
21930
|
config,
|
|
@@ -20955,6 +21947,7 @@ async function startWebUI(opts = {}) {
|
|
|
20955
21947
|
skillInstaller,
|
|
20956
21948
|
tokenCounter,
|
|
20957
21949
|
pipelines: createDefaultPipelines(),
|
|
21950
|
+
...installToolBoundary ? { installToolBoundary } : {},
|
|
20958
21951
|
modelCapabilitiesRef,
|
|
20959
21952
|
sessionGetter: () => session,
|
|
20960
21953
|
sessionReader,
|
|
@@ -20985,6 +21978,7 @@ async function startWebUI(opts = {}) {
|
|
|
20985
21978
|
worktreeHandler,
|
|
20986
21979
|
terminalHandler,
|
|
20987
21980
|
collabHandler,
|
|
21981
|
+
disposeRealtimeHandlers,
|
|
20988
21982
|
updateAutoCompactionMaxContext
|
|
20989
21983
|
} = agentServices;
|
|
20990
21984
|
if (typeof context.meta["yolo"] === "boolean") {
|
|
@@ -21035,7 +22029,7 @@ async function startWebUI(opts = {}) {
|
|
|
21035
22029
|
const input = toLanguagePackageInput(operation, workspace);
|
|
21036
22030
|
const use = {
|
|
21037
22031
|
type: "tool_use",
|
|
21038
|
-
id: `techstack-remediation-${
|
|
22032
|
+
id: `techstack-remediation-${randomUUID4()}`,
|
|
21039
22033
|
name: "language_package",
|
|
21040
22034
|
input: { ...input }
|
|
21041
22035
|
};
|
|
@@ -21163,21 +22157,21 @@ async function startWebUI(opts = {}) {
|
|
|
21163
22157
|
});
|
|
21164
22158
|
}
|
|
21165
22159
|
async function touchProjectEntry(root, workDir) {
|
|
21166
|
-
const resolved =
|
|
22160
|
+
const resolved = path29.resolve(root);
|
|
21167
22161
|
const manifest = await loadManifest(globalConfigPath);
|
|
21168
22162
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21169
|
-
const existing = manifest.projects.find((p) =>
|
|
22163
|
+
const existing = manifest.projects.find((p) => path29.resolve(p.root) === resolved);
|
|
21170
22164
|
if (existing) {
|
|
21171
22165
|
existing.lastSeen = now;
|
|
21172
|
-
if (workDir) existing.lastWorkingDir =
|
|
22166
|
+
if (workDir) existing.lastWorkingDir = path29.resolve(workDir);
|
|
21173
22167
|
} else {
|
|
21174
22168
|
manifest.projects.push({
|
|
21175
|
-
name:
|
|
22169
|
+
name: path29.basename(resolved),
|
|
21176
22170
|
root: resolved,
|
|
21177
22171
|
slug: generateProjectSlug(resolved),
|
|
21178
22172
|
createdAt: now,
|
|
21179
22173
|
lastSeen: now,
|
|
21180
|
-
lastWorkingDir: workDir ?
|
|
22174
|
+
lastWorkingDir: workDir ? path29.resolve(workDir) : void 0
|
|
21181
22175
|
});
|
|
21182
22176
|
}
|
|
21183
22177
|
await saveManifest(manifest, globalConfigPath);
|
|
@@ -21304,7 +22298,8 @@ async function startWebUI(opts = {}) {
|
|
|
21304
22298
|
patchConfig(state.getConfig(), {
|
|
21305
22299
|
providers: snapshot.providers,
|
|
21306
22300
|
...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
|
|
21307
|
-
...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {}
|
|
22301
|
+
...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
|
|
22302
|
+
fallbackBridge: snapshot.fallbackBridge ?? ""
|
|
21308
22303
|
})
|
|
21309
22304
|
);
|
|
21310
22305
|
deps2.configStore.update({
|
|
@@ -21312,6 +22307,7 @@ async function startWebUI(opts = {}) {
|
|
|
21312
22307
|
...snapshot.apiKey !== void 0 ? { apiKey: snapshot.apiKey } : {},
|
|
21313
22308
|
...snapshot.baseUrl !== void 0 ? { baseUrl: snapshot.baseUrl } : {},
|
|
21314
22309
|
...snapshot.fallbackModels !== void 0 ? { fallbackModels: snapshot.fallbackModels } : {},
|
|
22310
|
+
fallbackBridge: snapshot.fallbackBridge ?? "",
|
|
21315
22311
|
...snapshot.fallbackProfiles !== void 0 ? { fallbackProfiles: snapshot.fallbackProfiles } : {},
|
|
21316
22312
|
...snapshot.favoriteModels !== void 0 ? { favoriteModels: snapshot.favoriteModels } : {},
|
|
21317
22313
|
...snapshot.favoriteModelsOnly !== void 0 ? { favoriteModelsOnly: snapshot.favoriteModelsOnly } : {},
|
|
@@ -21354,7 +22350,20 @@ async function startWebUI(opts = {}) {
|
|
|
21354
22350
|
);
|
|
21355
22351
|
credentialWatcherClose = credentialWatcher.close;
|
|
21356
22352
|
}
|
|
21357
|
-
const stopHeapWatchdog =
|
|
22353
|
+
const stopHeapWatchdog = startSharedHeapWatchdog({
|
|
22354
|
+
collectStats: () => ({
|
|
22355
|
+
surface: opts.surface ?? "webui",
|
|
22356
|
+
sessionId: context.session.id,
|
|
22357
|
+
messages: context.state.messages.length,
|
|
22358
|
+
messageEstimatedTokens: context.state.messages.reduce(
|
|
22359
|
+
(sum, message) => sum + (message._estTokens ?? 0),
|
|
22360
|
+
0
|
|
22361
|
+
),
|
|
22362
|
+
webClients: clients.size,
|
|
22363
|
+
pendingConfirms: pendingConfirms.size,
|
|
22364
|
+
runActive: runLockControl.get() !== null
|
|
22365
|
+
})
|
|
22366
|
+
});
|
|
21358
22367
|
const routes = buildRoutes(state, deps2, cb);
|
|
21359
22368
|
const handleMessage = createMessageDispatcher({
|
|
21360
22369
|
state,
|
|
@@ -21432,6 +22441,13 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
21432
22441
|
await todosCheckpoint.detach();
|
|
21433
22442
|
await stopHeapWatchdog();
|
|
21434
22443
|
credentialWatcherClose?.();
|
|
22444
|
+
disposeRealtimeHandlers();
|
|
22445
|
+
const governanceCleanup = await governanceHandle?.close();
|
|
22446
|
+
if (governanceCleanup && !governanceCleanup.ok) {
|
|
22447
|
+
logger.warn(`governance: standalone WebUI ${governanceCleanup.action} cleanup failed`, {
|
|
22448
|
+
message: governanceCleanup.message
|
|
22449
|
+
});
|
|
22450
|
+
}
|
|
21435
22451
|
brainMonitor.stop();
|
|
21436
22452
|
await agentServices.brainLedger?.stop().catch(() => {
|
|
21437
22453
|
});
|
|
@@ -21455,7 +22471,7 @@ projectRoot: ${ev.projectRoot ?? "?"}`,
|
|
|
21455
22471
|
await memoryStore.dispose().catch(
|
|
21456
22472
|
(err) => logger.warn(`sage connection disposal failed: ${toErrorMessage13(err)}`)
|
|
21457
22473
|
);
|
|
21458
|
-
await unregisterInstance(process.pid,
|
|
22474
|
+
await unregisterInstance(process.pid, path29.dirname(globalConfigPath));
|
|
21459
22475
|
}
|
|
21460
22476
|
});
|
|
21461
22477
|
}
|
|
@@ -21548,7 +22564,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
|
|
|
21548
22564
|
console.error(err instanceof Error ? err.message : String(err));
|
|
21549
22565
|
process.exit(1);
|
|
21550
22566
|
}
|
|
21551
|
-
const
|
|
22567
|
+
const open2 = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
|
|
21552
22568
|
const requireToken = argv.includes("--require-token") || envFlag2("WEBUI_REQUIRE_TOKEN");
|
|
21553
22569
|
console.log(`[WebUI] Starting standalone server on ${wsHost} (http:${httpPort})...`);
|
|
21554
22570
|
startWebUI({
|
|
@@ -21558,7 +22574,7 @@ if (argv.includes("--help") || argv.includes("-h")) {
|
|
|
21558
22574
|
publicUrl,
|
|
21559
22575
|
publicWsUrl,
|
|
21560
22576
|
requireToken,
|
|
21561
|
-
open,
|
|
22577
|
+
open: open2,
|
|
21562
22578
|
distDir
|
|
21563
22579
|
}).catch((err) => {
|
|
21564
22580
|
console.error(JSON.stringify({
|