@vantran-se/openchamber-web 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -28
- package/bin/cli.js +3 -1
- package/bin/cli.test.js +13 -4
- package/bin/lib/DOCUMENTATION.md +4 -1
- package/bin/lib/commands-serve.js +9 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B82Oye3V.js → CodeMirrorCommentWidgets-DZF8ntaq.js} +1 -1
- package/dist/assets/{DiffView-DJLmWn6Z.js → DiffView-D2p4Ar0H.js} +1 -1
- package/dist/assets/{FilesView-DjksmZRo.js → FilesView-BeTtbsip.js} +1 -1
- package/dist/assets/{GitView-DiUsUIgr.js → GitView-DwQwqDvu.js} +1 -1
- package/dist/assets/{GuestAttachDialog-CYwWYLr3.js → GuestAttachDialog-CoXM0gh_.js} +1 -1
- package/dist/assets/{GuestSurfacePane-CmctyRI4.js → GuestSurfacePane-DIJvkkM-.js} +1 -1
- package/dist/assets/{JsonTreeView-DOHHcbgJ.js → JsonTreeView-Cq_27v0y.js} +1 -1
- package/dist/assets/{LinearIssuesView-DaqnylcV.js → LinearIssuesView-CsSSRMhn.js} +1 -1
- package/dist/assets/{MarkdownImageGallery-HJNvWt7L.js → MarkdownImageGallery-CY0dWTaE.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-nDZRGg2I.js → MarkdownRendererImpl-ILdXzLrt.js} +1 -1
- package/dist/assets/{OnboardingScreen-C2B5Bsof.js → OnboardingScreen-ClatMQAX.js} +1 -1
- package/dist/assets/{PatchDiff-C4FLBjpt.js → PatchDiff-DOwlrIK9.js} +1 -1
- package/dist/assets/{PierreDiffViewer-C_1S9c9n.js → PierreDiffViewer-CrcGqqD_.js} +1 -1
- package/dist/assets/{PlanView-_eZvb-G5.js → PlanView-BGLsMgQk.js} +1 -1
- package/dist/assets/{PluginPane-sM8fcZIV.js → PluginPane-B39JEUL8.js} +1 -1
- package/dist/assets/{SettingsView-BRfr46hb.js → SettingsView-13B2_G-A.js} +1 -1
- package/dist/assets/{SettingsWindow-CcUDwQHg.js → SettingsWindow-B0BAy-f9.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-CrN1ZGwl.js → SubmoduleDiffSummary-fcdDzCSW.js} +1 -1
- package/dist/assets/{ToolOutputDialog-Cij4jI09.js → ToolOutputDialog-B7xaUz2H.js} +1 -1
- package/dist/assets/{ToolPartDiffPreview-BXzwQ-JY.js → ToolPartDiffPreview-CkWCaUmZ.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-DSWTLJNH.js → VirtualizedFileDiff-DONC4eI2.js} +1 -1
- package/dist/assets/{WalkthroughView-CpUUgY8H.js → WalkthroughView-CXDImOXW.js} +1 -1
- package/dist/assets/{appThemeRegistry-B5tHTkR8.js → appThemeRegistry-Br2TLSlb.js} +1 -1
- package/dist/assets/{commitAndPush-D7D8qV8p.js → commitAndPush-CcI4ol7c.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-cq9uQ30i.js → computeVirtualFileMetrics-_-20umCs.js} +1 -1
- package/dist/assets/{de-CUO1-KqF.js → de-BtYnUgII.js} +1 -1
- package/dist/assets/{es-BlbC6sGJ.js → es-crP5qPye.js} +1 -1
- package/dist/assets/{fr-D2nwe_0W.js → fr-CLSRuALk.js} +1 -1
- package/dist/assets/{index-BQOoYIOu.js → index-OKDYjcMF.js} +1 -1
- package/dist/assets/{isDiffPlainText-D0obJ2p8.js → isDiffPlainText-D77M6q1f.js} +1 -1
- package/dist/assets/{isFilePlainText-DB3_rjyW.js → isFilePlainText-C54B4Mns.js} +1 -1
- package/dist/assets/{ja-CA7vnDsZ.js → ja-SZ1-ucLV.js} +1 -1
- package/dist/assets/{ko-Chy6a9l-.js → ko-HthbT_Ve.js} +1 -1
- package/dist/assets/{main-B9JFK3vF.js → main-BJklC7Cl.js} +4 -4
- package/dist/assets/{main-Bhiqnrhd.js → main-CGEvLMd2.js} +2 -2
- package/dist/assets/{markdownCore-tfqOFaBd.js → markdownCore-BzQuETm0.js} +1 -1
- package/dist/assets/{miniChat-B0UOL5zA.js → miniChat-BSxkRQLj.js} +2 -2
- package/dist/assets/{mobile-BJppFIEo.js → mobile-C5KRTNOB.js} +2 -2
- package/dist/assets/{patchFileDiff-Db6KuwCP.js → patchFileDiff-DGw2KryM.js} +1 -1
- package/dist/assets/{pl-CTR5A_1P.js → pl-C8eYfjL1.js} +1 -1
- package/dist/assets/{pt-BR-BE1O1KEx.js → pt-BR-cYOrZdxS.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-Yz5sfoFh.js → renderElectronMiniChatApp-CnkAQV6o.js} +2 -2
- package/dist/assets/{renderMobileApp-QQ-yB8aY.js → renderMobileApp-CMxjwisj.js} +3 -3
- package/dist/assets/{runtimeEndpointReset-DKDdzRzm.js → runtimeEndpointReset-C45jrFq4.js} +3 -3
- package/dist/assets/{scheduledTasksApi-C5sCuNqf.js → scheduledTasksApi-CkK1F9s2.js} +1 -1
- package/dist/assets/{shikiHighlight-xURoo5t_.js → shikiHighlight-4O0ZM14D.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-BZLGuYTU.js → textMateThemeFromAppTheme-yNq7b0DZ.js} +1 -1
- package/dist/assets/{tr-Bh5vK6cW.js → tr-B4IQ5t-T.js} +1 -1
- package/dist/assets/{uk-CLDkZFy_.js → uk-DCoXG6ve.js} +1 -1
- package/dist/assets/{useAppFontEffects-BxrtCnfR.js → useAppFontEffects-Bzyszv2N.js} +48 -48
- package/dist/assets/{useInlineCommentController-CeXuR-SR.js → useInlineCommentController-zOf6ORAq.js} +1 -1
- package/dist/assets/{useRootScrollLock-DgWxezc1.js → useRootScrollLock-DCaZ4BWi.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-DOuaQUZ0.js → vendor-codemirror-language-data-6cEFPNUR.js} +2 -2
- package/dist/assets/{web-components-63H1h21j.js → web-components-BEo4-ISu.js} +1 -1
- package/dist/assets/{zh-CN-DODgcmYj.js → zh-CN-BXBoxsFk.js} +1 -1
- package/dist/assets/{zh-TW-ByNfIwLA.js → zh-TW-C6oelpnL.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/mini-chat.html +1 -1
- package/dist/mobile.html +1 -1
- package/package.json +1 -1
- package/server/index.d.ts +2 -0
- package/server/index.js +114 -52
- package/server/lib/agent-tool/DOCUMENTATION.md +47 -127
- package/server/lib/agent-tool/runtime.js +66 -47
- package/server/lib/agent-tool/runtime.test.js +2 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/global-hub.js +22 -6
- package/server/lib/event-stream/global-hub.test.js +93 -2
- package/server/lib/event-stream/rebind.test.js +1 -0
- package/server/lib/event-stream/runtime.js +1 -2
- package/server/lib/opencode/DOCUMENTATION.md +23 -23
- package/server/lib/opencode/lifecycle.js +126 -0
- package/server/lib/opencode/lifecycle.test.js +228 -1
- package/server/lib/opencode/network-runtime.js +16 -7
- package/server/lib/opencode/network-runtime.test.js +29 -1
- package/server/lib/opencode/proxy.js +15 -14
- package/server/lib/opencode/server-startup-runtime.js +25 -0
- package/server/lib/opencode/server-startup-runtime.test.js +67 -1
- package/server/lib/opencode/shared-service-runtime.js +155 -0
- package/server/lib/opencode/shared-service-runtime.test.js +180 -0
- package/server/lib/opencode/shutdown-runtime.js +11 -5
- package/server/lib/opencode/shutdown-runtime.test.js +77 -2
- package/server/lib/opencode/startup-pipeline-runtime.js +5 -4
- package/server/lib/opencode/startup-pipeline-runtime.test.js +18 -7
- package/server/lib/opencode/watcher.test.js +54 -0
- package/server/opencode-proxy.test.js +158 -0
- package/server/proxy-headers.js +8 -2
- package/server/proxy-headers.test.js +20 -0
|
@@ -167,6 +167,40 @@ const resolveConcreteBoundAddress = (value) => {
|
|
|
167
167
|
* answer with an empty completion instead of an error, and the `oneOf` branches
|
|
168
168
|
* are what carry the per-action descriptions the model reads.
|
|
169
169
|
*/
|
|
170
|
+
const createEnvironmentExecutionSource = (name) => `const endpoint = process.env.OPENCHAMBER_AGENT_TOOL_URL
|
|
171
|
+
const token = process.env.OPENCHAMBER_AGENT_TOOL_TOKEN
|
|
172
|
+
const callbackHeaders = {}
|
|
173
|
+
if (!endpoint || !token) {
|
|
174
|
+
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: "OpenChamber managed tool connection is unavailable" } })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const response = await fetch(endpoint, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: {
|
|
181
|
+
authorization: "Bearer " + token,
|
|
182
|
+
"content-type": "application/json",
|
|
183
|
+
...callbackHeaders,
|
|
184
|
+
},
|
|
185
|
+
// OpenCode 2 no longer hands a tool its session directory, so the
|
|
186
|
+
// session id goes over instead and OpenChamber resolves the
|
|
187
|
+
// directory on its own side. The signal fires when the session is
|
|
188
|
+
// aborted, so the OpenChamber side stops the action too.
|
|
189
|
+
body: JSON.stringify({ input: args, sessionID: context.sessionID, tool: ${JSON.stringify(name)} }),
|
|
190
|
+
signal: context.signal,
|
|
191
|
+
})
|
|
192
|
+
const content = await response.text()
|
|
193
|
+
let result = null
|
|
194
|
+
try { result = JSON.parse(content) } catch {}
|
|
195
|
+
const valid = result?.schemaVersion === ${TOOL_SCHEMA_VERSION} && typeof result?.ok === "boolean" && typeof result?.action === "string"
|
|
196
|
+
await progress({ ok: valid && result.ok === true })
|
|
197
|
+
if (valid) return { content, metadata: { openchamber: { schemaVersion: ${TOOL_SCHEMA_VERSION}, action: args.action, description: title, ok: result.ok === true } } }
|
|
198
|
+
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: "OpenChamber returned an invalid response", kind: "runtime", status: response.status } })
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: error instanceof Error ? error.message : String(error), kind: "runtime" } })
|
|
201
|
+
}`;
|
|
202
|
+
|
|
203
|
+
|
|
170
204
|
const createToolEntry = ({ name, description, definitions, parameters }) => String.raw` tools.add({
|
|
171
205
|
name: ${JSON.stringify(name)},
|
|
172
206
|
description: ${JSON.stringify(description)},
|
|
@@ -199,8 +233,6 @@ const createToolEntry = ({ name, description, definitions, parameters }) => Stri
|
|
|
199
233
|
},
|
|
200
234
|
})
|
|
201
235
|
await progress()
|
|
202
|
-
const endpoint = process.env.OPENCHAMBER_AGENT_TOOL_URL
|
|
203
|
-
const token = process.env.OPENCHAMBER_AGENT_TOOL_TOKEN
|
|
204
236
|
// No output schema is declared, so a result must never carry an output
|
|
205
237
|
// field: OpenCode rejects that with "Tool result declared output
|
|
206
238
|
// without an output schema". The envelope travels as text content.
|
|
@@ -208,39 +240,17 @@ const createToolEntry = ({ name, description, definitions, parameters }) => Stri
|
|
|
208
240
|
content: JSON.stringify(payload),
|
|
209
241
|
metadata: { openchamber: { schemaVersion: ${TOOL_SCHEMA_VERSION}, action: args.action, description: title, ok: false } },
|
|
210
242
|
})
|
|
211
|
-
|
|
212
|
-
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: "OpenChamber managed tool connection is unavailable" } })
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const response = await fetch(endpoint, {
|
|
217
|
-
method: "POST",
|
|
218
|
-
headers: {
|
|
219
|
-
authorization: "Bearer " + token,
|
|
220
|
-
"content-type": "application/json",
|
|
221
|
-
},
|
|
222
|
-
// OpenCode 2 no longer hands a tool its session directory, so the
|
|
223
|
-
// session id goes over instead and OpenChamber resolves the
|
|
224
|
-
// directory on its own side. The signal fires when the session is
|
|
225
|
-
// aborted, so the OpenChamber side stops the action too.
|
|
226
|
-
body: JSON.stringify({ input: args, sessionID: context.sessionID, tool: ${JSON.stringify(name)} }),
|
|
227
|
-
signal: context.signal,
|
|
228
|
-
})
|
|
229
|
-
const content = await response.text()
|
|
230
|
-
let result = null
|
|
231
|
-
try { result = JSON.parse(content) } catch {}
|
|
232
|
-
const valid = result?.schemaVersion === ${TOOL_SCHEMA_VERSION} && typeof result?.ok === "boolean" && typeof result?.action === "string"
|
|
233
|
-
await progress({ ok: valid && result.ok === true })
|
|
234
|
-
if (valid) return { content, metadata: { openchamber: { schemaVersion: ${TOOL_SCHEMA_VERSION}, action: args.action, description: title, ok: result.ok === true } } }
|
|
235
|
-
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: "OpenChamber returned an invalid response", kind: "runtime", status: response.status } })
|
|
236
|
-
} catch (error) {
|
|
237
|
-
return failure({ schemaVersion: ${TOOL_SCHEMA_VERSION}, ok: false, action: args.action, error: { message: error instanceof Error ? error.message : String(error), kind: "runtime" } })
|
|
238
|
-
}
|
|
243
|
+
${createEnvironmentExecutionSource(name)}
|
|
239
244
|
},
|
|
240
245
|
})
|
|
241
246
|
`;
|
|
242
247
|
|
|
243
|
-
|
|
248
|
+
|
|
249
|
+
export const createPluginSource = ({
|
|
250
|
+
includeControl,
|
|
251
|
+
includeWeb,
|
|
252
|
+
includeMemory,
|
|
253
|
+
}) => {
|
|
244
254
|
const entries = [];
|
|
245
255
|
if (includeControl) {
|
|
246
256
|
entries.push(createToolEntry({
|
|
@@ -267,15 +277,13 @@ const createPluginSource = ({ includeControl, includeWeb, includeMemory }) => {
|
|
|
267
277
|
}));
|
|
268
278
|
}
|
|
269
279
|
|
|
270
|
-
// The callback carries
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
if (!endpoint || !URL.canParse(endpoint)) return
|
|
278
|
-
const host = new URL(endpoint).hostname.replace(/^\\[|\\]$/g, "")
|
|
280
|
+
// The callback carries a bearer token over plain HTTP. With a proxy in the
|
|
281
|
+
// OpenCode environment, fetch could hand a non-loopback callback and token to
|
|
282
|
+
// that proxy. Add the callback host to both proxy exemption variables before
|
|
283
|
+
// a request.
|
|
284
|
+
return `const exemptCallbackFromProxy = (callbackEndpoint = process.env.OPENCHAMBER_AGENT_TOOL_URL) => {
|
|
285
|
+
if (!callbackEndpoint || !URL.canParse(callbackEndpoint)) return
|
|
286
|
+
const host = new URL(callbackEndpoint).hostname.replace(/^\\[|\\]$/g, "")
|
|
279
287
|
for (const key of ["NO_PROXY", "no_proxy"]) {
|
|
280
288
|
const entries = (process.env[key] || "").split(",").map((entry) => entry.trim()).filter(Boolean)
|
|
281
289
|
if (!entries.includes(host)) process.env[key] = [...entries, host].join(",")
|
|
@@ -295,7 +303,7 @@ ${entries.join('')} })
|
|
|
295
303
|
|
|
296
304
|
// A configured plugin has to be a directory carrying a package.json that
|
|
297
305
|
// resolves an entrypoint; OpenCode skips a plain .js path.
|
|
298
|
-
const
|
|
306
|
+
export const AGENT_TOOL_PLUGIN_PACKAGE_JSON = `${JSON.stringify({
|
|
299
307
|
name: 'openchamber-agent-tool',
|
|
300
308
|
version: '0.0.0',
|
|
301
309
|
private: true,
|
|
@@ -334,8 +342,12 @@ export const createAgentToolRuntime = (dependencies) => {
|
|
|
334
342
|
throw new Error('At least one OpenChamber managed tool must be enabled to inject the plugin');
|
|
335
343
|
}
|
|
336
344
|
await fsPromises.mkdir(pluginDirectory, { recursive: true });
|
|
337
|
-
await fsPromises.writeFile(pluginManifestPath,
|
|
338
|
-
await fsPromises.writeFile(pluginPath, createPluginSource({
|
|
345
|
+
await fsPromises.writeFile(pluginManifestPath, AGENT_TOOL_PLUGIN_PACKAGE_JSON, { mode: 0o600 });
|
|
346
|
+
await fsPromises.writeFile(pluginPath, createPluginSource({
|
|
347
|
+
includeControl,
|
|
348
|
+
includeWeb,
|
|
349
|
+
includeMemory,
|
|
350
|
+
}), { mode: 0o600 });
|
|
339
351
|
return pluginDirectory;
|
|
340
352
|
};
|
|
341
353
|
|
|
@@ -371,15 +383,21 @@ export const createAgentToolRuntime = (dependencies) => {
|
|
|
371
383
|
return boundAddress !== null && normalizeAddress(value) === boundAddress;
|
|
372
384
|
};
|
|
373
385
|
|
|
374
|
-
const
|
|
375
|
-
if (!
|
|
386
|
+
const verifyBearer = (req, token) => {
|
|
387
|
+
if (!token) return false;
|
|
376
388
|
const header = asNonEmptyString(req.headers?.authorization);
|
|
377
389
|
if (!header?.startsWith('Bearer ')) return false;
|
|
378
390
|
const provided = Buffer.from(header.slice(7));
|
|
379
|
-
const expected = Buffer.from(
|
|
391
|
+
const expected = Buffer.from(token);
|
|
380
392
|
return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);
|
|
381
393
|
};
|
|
382
394
|
|
|
395
|
+
|
|
396
|
+
const authorizeManagedChild = (req) => (
|
|
397
|
+
isSameMachineAddress(req.socket?.remoteAddress) && verifyBearer(req, activeToken)
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
|
|
383
401
|
const execute = async (payload = {}, options = {}) => {
|
|
384
402
|
const requested = asNonEmptyString(payload.input?.action);
|
|
385
403
|
// Resolved against the calling tool's own actions: models drop the
|
|
@@ -455,8 +473,9 @@ export const createAgentToolRuntime = (dependencies) => {
|
|
|
455
473
|
};
|
|
456
474
|
|
|
457
475
|
const registerRoutes = (app, express) => {
|
|
476
|
+
|
|
458
477
|
app.post('/api/openchamber/agent-tool', express.json({ limit: '1mb' }), async (req, res) => {
|
|
459
|
-
if (!
|
|
478
|
+
if (!authorizeManagedChild(req)) return res.status(401).json({ error: 'Unauthorized' });
|
|
460
479
|
const controller = new AbortController();
|
|
461
480
|
const abortOnDisconnect = () => {
|
|
462
481
|
if (!res.writableEnded) controller.abort();
|
|
@@ -445,6 +445,8 @@ describe('managed agent tool runtime', () => {
|
|
|
445
445
|
expect(response.body).toEqual(expect.objectContaining({ ok: true, action: 'projects.list' }));
|
|
446
446
|
});
|
|
447
447
|
|
|
448
|
+
|
|
449
|
+
|
|
448
450
|
it.each([
|
|
449
451
|
['0.0.0.0', 'http://127.0.0.1:3901/api/openchamber/agent-tool'],
|
|
450
452
|
['::', 'http://127.0.0.1:3901/api/openchamber/agent-tool'],
|
|
@@ -29,7 +29,7 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
|
|
|
29
29
|
- `serializeMessageStreamWsEvent(payload, options)` and `sendSerializedMessageStreamWsFrame(socket, frame)` separate encoding from per-socket delivery. Delivery retains ready-state and backpressure checks.
|
|
30
30
|
|
|
31
31
|
### Runtime helpers
|
|
32
|
-
- `createGlobalMessageStreamHub(...)`: creates a shared `/global/event` upstream SSE hub with event/status subscribers and bounded event-id replay.
|
|
32
|
+
- `createGlobalMessageStreamHub(...)`: creates a shared `/global/event` upstream SSE hub with event/status subscribers and bounded event-id replay. The composition root owns the singleton and stops it during application shutdown; watchers and WebSocket bridges that receive it are borrowers and must not stop it.
|
|
33
33
|
- `createGlobalUiEventBroadcaster({ sseClients, wsClients, writeSseEvent })`: returns a broadcaster that fans out the same synthetic UI event to SSE and WS clients.
|
|
34
34
|
- `createMessageStreamWsRuntime(...)`: mounts the message-stream WS server, upgrade handler, and SSE-to-WS bridge onto the web HTTP server.
|
|
35
35
|
|
|
@@ -59,7 +59,7 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
|
|
|
59
59
|
- The hub numbers every event that arrives without an SSE id (`oc-<process>-<sequence>`). OpenCode 1.18 sends no ids at all, and an event without an id used to skip the replay buffer, so reconnecting clients had no cursor and lost whatever fell into the gap. The process prefix makes a cursor from before a server restart miss, which reports `replayReset`, instead of matching an unrelated sequence number.
|
|
60
60
|
- Directory WS clients still attach one upstream `/event?directory=...` SSE reader per connection because directory streams are scoped.
|
|
61
61
|
- If an upstream SSE stream stalls after the browser WS is already ready, the reader aborts that upstream fetch and reconnects upstream with `Last-Event-ID`, keeping the browser WS alive when recovery is fast.
|
|
62
|
-
- When the shared global upstream reconnects after it was previously ready, the global WS bridge sends a fresh `ready` frame to already-ready browser clients. The browser treats this as a reconnect edge and
|
|
62
|
+
- When the shared global upstream reconnects after it was previously ready, the global WS bridge sends a fresh `ready` frame to already-ready browser clients. The browser treats this as a reconnect edge and runs its established scoped authoritative repair without requiring the browser WS to close. Shared-service recovery uses this edge rather than manufacturing a server-side interruption. Rebinding retires the old reader generation before starting its replacement, so delayed events, errors, and disconnect completion from the old reader cannot overwrite the replacement's connection state.
|
|
63
63
|
- Health checks are reserved for initial upstream connect failures and explicit upstream-unavailable responses, not for ordinary stall recovery on an already-established stream.
|
|
64
64
|
- Global synthetic events such as `openchamber:session-status`, `openchamber:session-activity`, `openchamber:notification`, and `openchamber:heartbeat` are preserved on the WS path, but heartbeat frames are emitted only while an upstream SSE stream is actively attached.
|
|
65
65
|
- Global UI broadcasts are fan-out capable across both SSE and WS clients.
|
|
@@ -40,9 +40,9 @@ export function createGlobalMessageStreamHub({
|
|
|
40
40
|
|
|
41
41
|
let controller = null;
|
|
42
42
|
let reader = null;
|
|
43
|
+
let readerGeneration = 0;
|
|
43
44
|
let connected = false;
|
|
44
45
|
let everConnected = false;
|
|
45
|
-
let buildUrlFailed = false;
|
|
46
46
|
|
|
47
47
|
const notifySubscriber = (kind, subscriber, payload) => {
|
|
48
48
|
try {
|
|
@@ -124,8 +124,11 @@ export function createGlobalMessageStreamHub({
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
controller = new AbortController();
|
|
127
|
+
const generation = ++readerGeneration;
|
|
128
|
+
const currentController = controller;
|
|
129
|
+
let buildUrlFailed = false;
|
|
127
130
|
reader = createUpstreamSseReader({
|
|
128
|
-
signal:
|
|
131
|
+
signal: currentController.signal,
|
|
129
132
|
stallTimeoutMs: upstreamStallTimeoutMs,
|
|
130
133
|
reconnectDelayMs: upstreamReconnectDelayMs,
|
|
131
134
|
fetchImpl,
|
|
@@ -140,20 +143,23 @@ export function createGlobalMessageStreamHub({
|
|
|
140
143
|
},
|
|
141
144
|
getHeaders: getOpenCodeAuthHeaders,
|
|
142
145
|
onConnect() {
|
|
146
|
+
if (generation !== readerGeneration) return;
|
|
143
147
|
connected = true;
|
|
144
148
|
const wasReady = everConnected;
|
|
145
149
|
everConnected = true;
|
|
146
150
|
notifyStatus({ type: 'connect', wasReady });
|
|
147
151
|
},
|
|
148
152
|
onDisconnect({ reason }) {
|
|
153
|
+
if (generation !== readerGeneration) return;
|
|
149
154
|
connected = false;
|
|
150
155
|
notifyStatus({ type: 'disconnect', reason });
|
|
151
156
|
},
|
|
152
157
|
onEvent(event) {
|
|
158
|
+
if (generation !== readerGeneration) return;
|
|
153
159
|
coalescer.push(event);
|
|
154
160
|
},
|
|
155
161
|
onError(error) {
|
|
156
|
-
if (
|
|
162
|
+
if (generation !== readerGeneration || currentController.signal.aborted) {
|
|
157
163
|
return;
|
|
158
164
|
}
|
|
159
165
|
|
|
@@ -168,8 +174,9 @@ export function createGlobalMessageStreamHub({
|
|
|
168
174
|
void reader.start();
|
|
169
175
|
};
|
|
170
176
|
|
|
171
|
-
const
|
|
177
|
+
const stopReader = ({ resetConnectionHistory }) => {
|
|
172
178
|
connected = false;
|
|
179
|
+
readerGeneration += 1;
|
|
173
180
|
// Text that already arrived belongs in the retained replay suffix.
|
|
174
181
|
coalescer.flush();
|
|
175
182
|
reader?.stop();
|
|
@@ -178,13 +185,22 @@ export function createGlobalMessageStreamHub({
|
|
|
178
185
|
}
|
|
179
186
|
reader = null;
|
|
180
187
|
controller = null;
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
if (resetConnectionHistory) {
|
|
189
|
+
everConnected = false;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const stop = () => {
|
|
194
|
+
stopReader({ resetConnectionHistory: true });
|
|
183
195
|
};
|
|
184
196
|
|
|
185
197
|
return {
|
|
186
198
|
start,
|
|
187
199
|
stop,
|
|
200
|
+
rebind() {
|
|
201
|
+
stopReader({ resetConnectionHistory: false });
|
|
202
|
+
start();
|
|
203
|
+
},
|
|
188
204
|
isConnected() {
|
|
189
205
|
return connected;
|
|
190
206
|
},
|
|
@@ -25,7 +25,7 @@ it('bounds a contiguous replay suffix by UTF-8 bytes and event count', async ()
|
|
|
25
25
|
} finally { hub.stop(); }
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
function createSseResponse({ blocks = [] } = {}) {
|
|
28
|
+
function createSseResponse({ blocks = [], signal, holdOpen = false } = {}) {
|
|
29
29
|
const encoder = new TextEncoder();
|
|
30
30
|
let index = 0;
|
|
31
31
|
|
|
@@ -38,7 +38,18 @@ function createSseResponse({ blocks = [] } = {}) {
|
|
|
38
38
|
if (index < blocks.length) {
|
|
39
39
|
return { value: encoder.encode(blocks[index++]), done: false };
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
if (!holdOpen) {
|
|
42
|
+
return { value: undefined, done: true };
|
|
43
|
+
}
|
|
44
|
+
return new Promise((_resolve, reject) => {
|
|
45
|
+
const onAbort = () => {
|
|
46
|
+
signal.removeEventListener('abort', onAbort);
|
|
47
|
+
const error = new Error('Aborted');
|
|
48
|
+
error.name = 'AbortError';
|
|
49
|
+
reject(error);
|
|
50
|
+
};
|
|
51
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
52
|
+
});
|
|
42
53
|
},
|
|
43
54
|
};
|
|
44
55
|
},
|
|
@@ -224,6 +235,86 @@ describe('delta coalescing in the global hub', () => {
|
|
|
224
235
|
});
|
|
225
236
|
|
|
226
237
|
describe('createGlobalMessageStreamHub', () => {
|
|
238
|
+
it('uses the replacement service endpoint and auth after rebind', async () => {
|
|
239
|
+
let generation = 0;
|
|
240
|
+
const urls = ['http://127.0.0.1:4096/api/event', 'http://127.0.0.1:5096/api/event'];
|
|
241
|
+
const auth = ['Basic first', 'Basic second'];
|
|
242
|
+
const fetchImpl = vi.fn(async (_url, options) => createSseResponse({
|
|
243
|
+
signal: options.signal,
|
|
244
|
+
holdOpen: true,
|
|
245
|
+
}));
|
|
246
|
+
const hub = createGlobalMessageStreamHub({
|
|
247
|
+
buildOpenCodeUrl: () => urls[generation],
|
|
248
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: auth[generation] }),
|
|
249
|
+
fetchImpl,
|
|
250
|
+
upstreamReconnectDelayMs: 60_000,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
hub.start();
|
|
255
|
+
await waitForAssertion(() => expect(fetchImpl).toHaveBeenCalledTimes(1));
|
|
256
|
+
|
|
257
|
+
generation = 1;
|
|
258
|
+
hub.rebind();
|
|
259
|
+
|
|
260
|
+
await waitForAssertion(() => expect(fetchImpl).toHaveBeenCalledTimes(2));
|
|
261
|
+
expect(fetchImpl).toHaveBeenLastCalledWith(urls[1], expect.objectContaining({
|
|
262
|
+
headers: expect.objectContaining({ Authorization: auth[1] }),
|
|
263
|
+
}));
|
|
264
|
+
} finally {
|
|
265
|
+
hub.stop();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('ignores a retired reader disconnect that completes after its replacement connects', async () => {
|
|
270
|
+
let finishRetiredRead;
|
|
271
|
+
let fetchCount = 0;
|
|
272
|
+
const statuses = [];
|
|
273
|
+
const fetchImpl = vi.fn(async (_url, options) => {
|
|
274
|
+
fetchCount += 1;
|
|
275
|
+
if (fetchCount === 1) {
|
|
276
|
+
return {
|
|
277
|
+
ok: true,
|
|
278
|
+
body: {
|
|
279
|
+
getReader() {
|
|
280
|
+
return {
|
|
281
|
+
read() {
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
finishRetiredRead = () => resolve({ value: undefined, done: true });
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return createSseResponse({ signal: options.signal, holdOpen: true });
|
|
292
|
+
});
|
|
293
|
+
const hub = createGlobalMessageStreamHub({
|
|
294
|
+
buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
|
|
295
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
296
|
+
fetchImpl,
|
|
297
|
+
upstreamReconnectDelayMs: 60_000,
|
|
298
|
+
});
|
|
299
|
+
hub.subscribeStatus((status) => statuses.push(status.type));
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
hub.start();
|
|
303
|
+
await waitForAssertion(() => expect(statuses).toEqual(['connect']));
|
|
304
|
+
|
|
305
|
+
hub.rebind();
|
|
306
|
+
await waitForAssertion(() => expect(statuses).toEqual(['connect', 'connect']));
|
|
307
|
+
expect(hub.isConnected()).toBe(true);
|
|
308
|
+
|
|
309
|
+
finishRetiredRead();
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
311
|
+
expect(statuses).toEqual(['connect', 'connect']);
|
|
312
|
+
expect(hub.isConnected()).toBe(true);
|
|
313
|
+
} finally {
|
|
314
|
+
hub.stop();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
227
318
|
it('continues fanout when an event subscriber throws', async () => {
|
|
228
319
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
229
320
|
const received = [];
|
|
@@ -132,6 +132,7 @@ describe('rebindUpstream (#2638)', () => {
|
|
|
132
132
|
expect(fetchCalls).toBe(2);
|
|
133
133
|
expect(fetchImpl.mock.calls[1][0]).toContain(':5000/api/event');
|
|
134
134
|
expect(socket.sent.some((frame) => frame.type === 'event' && frame.eventId === 'evt-2')).toBe(true);
|
|
135
|
+
expect(socket.sent.filter((frame) => frame.type === 'ready' && frame.scope === 'global')).toHaveLength(2);
|
|
135
136
|
|
|
136
137
|
socket.close();
|
|
137
138
|
await runtime.close();
|
|
@@ -182,8 +182,7 @@ export function createMessageStreamWsRuntime({
|
|
|
182
182
|
* `Last-Event-ID` and re-establish the stream against the new port.
|
|
183
183
|
*/
|
|
184
184
|
rebindUpstream() {
|
|
185
|
-
globalHub.
|
|
186
|
-
globalHub.start();
|
|
185
|
+
globalHub.rebind();
|
|
187
186
|
for (const socket of Array.from(directorySockets)) {
|
|
188
187
|
try {
|
|
189
188
|
socket.close(1012, 'OpenCode upstream restarted');
|
|
@@ -11,13 +11,14 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
11
11
|
- `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, parses CLI options, and starts server bootstrap.
|
|
12
12
|
- `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
|
|
13
13
|
- `packages/web/server/lib/opencode/v1-migration-topup.js`: re-arms OpenCode's own V1 -> V2 session import for V1 sessions created after that migration already completed; runs only before a managed spawn. See "v1-migration-topup.js" below.
|
|
14
|
-
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime
|
|
14
|
+
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode connection policy and the managed process lifecycle runtime. The composition helper selects `explicit-external` when `OPENCODE_HOST` is configured, `managed-owned` for Desktop, and `shared-local` for ordinary Web. The managed path still owns startup, restart, readiness, health monitoring, orphan reaping, credentials, and V1 migration top-up. After readiness it warms the most recently used directories (`getWarmupDirectories` dep, sequential and best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open.
|
|
15
15
|
- `packages/web/server/lib/opencode/provider-env-aliases.js`: mirrors known provider credential env aliases into the managed OpenCode process environment (for example `GEMINI_API_KEY` → `GOOGLE_GENERATIVE_AI_API_KEY`) so OpenCode connection detection and the upstream AI SDK agree on the same key names. Canonical implementation shared by web lifecycle and the VS Code managed spawn path (`packages/vscode/src/provider-env-aliases.ts` re-exports this module).
|
|
16
16
|
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
|
|
17
17
|
- `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
|
|
18
18
|
- `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
|
|
19
19
|
- `packages/web/server/lib/opencode/bootstrap-runtime.js`: base app bootstrap runtime for status/auth/tts/notification/OpenChamber route wiring.
|
|
20
|
-
- `packages/web/server/lib/opencode/network-runtime.js`: OpenCode URL construction, health-probe readiness checks, and API prefix runtime.
|
|
20
|
+
- `packages/web/server/lib/opencode/network-runtime.js`: OpenCode URL construction, health-probe readiness checks, and API prefix runtime. URL and service headers are resolved for every request so shared-service endpoint or credential rotation is immediate; lowercase service `authorization` is normalized to `Authorization` while other service headers are preserved.
|
|
21
|
+
- `packages/web/server/lib/opencode/shared-service-runtime.js`: discovery-first connection used by ordinary Web when no explicit `OPENCODE_HOST` is configured. It probes `/api/info` before accepting an endpoint, refuses to replace an incompatible discovered service, and never owns or stops the shared process. Because the pinned client's discovery hides incompatible and unhealthy registrations, Web checks that the registration file is genuinely absent before calling its destructive `ensure`; an existing hidden registration fails startup untouched. Shared restart and health controls recover the connection, then rebind live event readers so their established reconnect edge repairs authoritative client state without synthetic interruption. Desktop does not instantiate this runtime.
|
|
21
22
|
- `packages/web/server/lib/opencode/project-directory-runtime.js`: request-scoped and settings-backed project directory resolution/validation runtime.
|
|
22
23
|
- `packages/web/server/lib/opencode/config-entity-routes.js`: route registration for agent/command/MCP config orchestration. OpenCode 2 watches these files, so a write is live as soon as it lands and the route answers plain success.
|
|
23
24
|
- `packages/web/server/lib/opencode/websearch-config.js`: writes OpenCode's `websearch` choice (`PUT /api/config/websearch` in `routes.js`; body `{ selection: false | null | "random" | "<provider id>" }`, `null` removes the key, so OpenCode falls back to the answer given in its chat consent form, which it keeps in its own store) to `OPENCODE_CONFIG` when set, else the user config. `GET /api/config/websearch` returns `{ projectPath }`: the project config whose `websearch` overrides that write (OpenCode merges user < project < `OPENCODE_CONFIG`), so Settings disables the choice and names the file instead of letting it snap back; `findWebSearchProjectOverride` in `config-v2.js` holds the rule. The pure transform is `writeWebSearchSelection` in `config-v2.js`, shared with the VS Code bridge (`api:config/websearch`).
|
|
@@ -25,18 +26,18 @@ This module provides OpenCode server integration utilities for the web server ru
|
|
|
25
26
|
- `packages/web/server/lib/opencode/snippets.js`: opencode-snippets-compatible snippet file CRUD, discovery, and hashtag expansion.
|
|
26
27
|
- `packages/web/server/lib/opencode/cli-options.js`: CLI/environment option parsing for server startup arguments.
|
|
27
28
|
- `packages/web/server/lib/opencode/core-routes.js`: server status/system routes, auth/access guard routes, and settings utility route registration.
|
|
28
|
-
- `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration
|
|
29
|
-
- `packages/web/server/lib/opencode/server-startup-runtime.js`: server listen
|
|
30
|
-
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset
|
|
31
|
-
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition
|
|
32
|
-
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot
|
|
33
|
-
- `packages/web/server/lib/opencode/upgrade-capability.js`:
|
|
34
|
-
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service
|
|
35
|
-
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`:
|
|
29
|
+
- `packages/web/server/lib/opencode/shutdown-runtime.js`: graceful shutdown orchestration. Only `managed-owned` may close an OpenCode child, kill by port, or wait for port release. Shared and external connections dispose local state only.
|
|
30
|
+
- `packages/web/server/lib/opencode/server-startup-runtime.js`: server listen, startup tunnel flow, and process or signal handler orchestration.
|
|
31
|
+
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset and SPA fallback route registration.
|
|
32
|
+
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition for dynamic route modules.
|
|
33
|
+
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot for settings routes and diagnostics.
|
|
34
|
+
- `packages/web/server/lib/opencode/upgrade-capability.js`: upgrade ownership policy. Bundled, external, and unresolved runtimes fail closed; only managed non-bundled runtimes delegate upgrades to OpenCode.
|
|
35
|
+
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service and active-port wiring.
|
|
36
|
+
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: terminal, proxy, static route, and listen orchestration. Shared Web starts no managed-tool installation or registration step.
|
|
36
37
|
- `packages/web/server/lib/opencode/startup-performance.js`: opt-in startup phase diagnostics with fixed labels and numeric metadata allowlists.
|
|
37
|
-
- `packages/web/server/lib/agent-tool/runtime.js`:
|
|
38
|
-
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the `OPENCODE_CONFIG_CONTENT` merge used only
|
|
39
|
-
- `packages/web/server/lib/opencode/managed-config-file.js`:
|
|
38
|
+
- `packages/web/server/lib/agent-tool/runtime.js`: Desktop's custom-tool schema and source generation, managed plugin materialization, environment injection, same-machine authentication, and fixed action dispatch.
|
|
39
|
+
- `packages/web/server/lib/opencode/managed-plugin-config.js`: the `OPENCODE_CONFIG_CONTENT` merge used only when the user's environment owns `OPENCODE_CONFIG`.
|
|
40
|
+
- `packages/web/server/lib/opencode/managed-config-file.js`: Desktop's managed OpenCode config layer for environment-backed OpenChamber tools and the system prompt optimizer.
|
|
40
41
|
|
|
41
42
|
### Managed plugins on OpenCode 2.x
|
|
42
43
|
A configured plugin must be a DIRECTORY holding a `package.json` that resolves an
|
|
@@ -296,7 +297,8 @@ Installer output is discarded, not forwarded to clients or logs.
|
|
|
296
297
|
The runtime maintains active-session count incrementally from idempotent activity phase transitions. Upstream stall-timeout and lifecycle health checks read it in O(1); the hourly cleanup removes activity phases older than 24 hours without broadcasting synthetic state transitions. Snapshot generation remains reserved for the session-activity API.
|
|
297
298
|
|
|
298
299
|
## Public exports (lifecycle.js)
|
|
299
|
-
- `
|
|
300
|
+
- `createOpenCodeRecoveryCallbacks(dependencies)`: composes the production recovery callbacks used by `index.js`. Managed restart resets providers, rebinds event readers, calls `interruptBusySessionsAfterRestart()`, and broadcasts the existing interruption notification. Shared recovery resets providers and rebinds readers only, leaving authoritative browser reconnect repair to the existing second `ready` edge.
|
|
301
|
+
- `createOpenCodeLifecycleRuntime(dependencies)`: creates lifecycle runtime for managed/external OpenCode process orchestration. The optional `onOpenCodeRestarted` dependency (default `null`) is fired after a successful managed restart. `index.js` supplies the managed callback above.
|
|
300
302
|
- Returned API:
|
|
301
303
|
- `startOpenCode()`
|
|
302
304
|
- `restartOpenCode()`
|
|
@@ -396,7 +398,7 @@ ConPTY or Console Window Host behavior.
|
|
|
396
398
|
- `setupBaseRoutes(app, options)`
|
|
397
399
|
|
|
398
400
|
## Public exports (network-runtime.js)
|
|
399
|
-
- `createOpenCodeNetworkRuntime(dependencies)`: creates runtime for OpenCode network and URL concerns.
|
|
401
|
+
- `createOpenCodeNetworkRuntime(dependencies)`: creates runtime for OpenCode network and URL concerns. `getOpenCodeBaseUrl` and `getOpenCodeAuthHeaders` are read at call time; credentials remain request headers and never enter generated URLs.
|
|
400
402
|
- Returned API:
|
|
401
403
|
- `waitForReady(url, timeoutMs?)`
|
|
402
404
|
- `normalizeApiPrefix(prefix)`
|
|
@@ -751,15 +753,15 @@ headers }` or v1 `{ npm, options }`. The stored entry is always a
|
|
|
751
753
|
- `setupProxy(app)`
|
|
752
754
|
|
|
753
755
|
## Public exports (shutdown-runtime.js)
|
|
754
|
-
- `createGracefulShutdownRuntime(dependencies)`: creates graceful shutdown
|
|
755
|
-
- Daemon signals, `POST /api/system/shutdown`, and embedded `stop()` share one shutdown promise. Guest admission closes
|
|
756
|
-
-
|
|
756
|
+
- `createGracefulShutdownRuntime(dependencies)`: creates graceful shutdown for Web-owned resources and explicit OpenCode process ownership.
|
|
757
|
+
- Daemon signals, `POST /api/system/shutdown`, and embedded `stop()` share one shutdown promise. Guest admission closes before asynchronous cleanup. Each cleanup is best-effort and runs once, including after partial startup. The application-owned global event hub is stopped here after its watcher, so embedded shutdown aborts the upstream event fetch and reconnect loop; watcher and WebSocket bridges do not stop injected hubs.
|
|
758
|
+
- Only `managed-owned` may close the OpenCode child, kill by port, or wait for port release. `shared-local` disposes only local connection state. `explicit-external` performs no OpenCode process teardown. TCP connection tracking closes the Web listener and remaining sockets after owned runtimes stop, while shared or external OpenCode and TUI work continue.
|
|
757
759
|
- Returned API:
|
|
758
760
|
- `gracefulShutdown(options?)`
|
|
759
761
|
- `trackServerConnections(server)`: call once before listening; closed sockets leave the tracking set, and the server close event removes the connection listener.
|
|
760
762
|
|
|
761
763
|
## Public exports (server-startup-runtime.js)
|
|
762
|
-
- `createServerStartupRuntime(dependencies)`: creates runtime for server bind/startup tunnel and process handler wiring.
|
|
764
|
+
- `createServerStartupRuntime(dependencies)`: creates runtime for server bind/startup tunnel and process handler wiring. Daemon readiness is emitted only after listener-dependent startup completes. A startup failure sends `openchamber:error`, closes the listener and accepted connections, and rejects startup without a ready event.
|
|
763
765
|
- Returned API:
|
|
764
766
|
- `resolveBindHost(host)`
|
|
765
767
|
- `startListeningAndMaybeTunnel(options)`
|
|
@@ -786,13 +788,11 @@ headers }` or v1 `{ npm, options }`. The stored entry is always a
|
|
|
786
788
|
- `initialize(app, initialPort, hasUiPassword)`
|
|
787
789
|
|
|
788
790
|
## Public exports (startup-pipeline-runtime.js)
|
|
789
|
-
- `createStartupPipelineRuntime(dependencies)`: creates
|
|
791
|
+
- `createStartupPipelineRuntime(dependencies)`: creates terminal, proxy, static route, and server listen orchestration.
|
|
790
792
|
- Returned API:
|
|
791
793
|
- `run(options)`
|
|
792
794
|
|
|
793
|
-
The pipeline
|
|
794
|
-
before starting managed OpenCode. The managed custom tool therefore receives
|
|
795
|
-
an authoritative loopback callback URL even when OpenChamber binds port `0`.
|
|
795
|
+
The pipeline publishes the listener port before bootstrapping OpenCode, then waits for connection authentication and version validation before daemon readiness. Desktop's generated custom tool therefore receives the correct loopback callback URL even when OpenChamber binds port `0`, while ordinary Web cannot advertise readiness with an unusable shared service. Shared Web does not install a plugin or publish callback registration.
|
|
796
796
|
|
|
797
797
|
## Public exports (openchamber-routes.js)
|
|
798
798
|
Browser completion checks use `appType=web&updateStatus=true` to stay on the
|