@openchambery/web 1.19.7-beta.8 → 1.19.7
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/assets/AgentAvatar-CX0a1rKE.js +1 -0
- package/dist/assets/AssistantView-nVF0kdFh.js +2 -0
- package/dist/assets/DiagramView-B2UtQ8mw.js +1 -0
- package/dist/assets/FadeInOnReveal-CK4asqVR.js +1 -0
- package/dist/assets/Icon-BYM02QyU.js +1 -0
- package/dist/assets/MarkdownLoadingSkeleton-DWs4Jg7b.js +3 -0
- package/dist/assets/MarkdownRenderer-DT_GN1Ex.js +2 -0
- package/dist/assets/MarkdownRendererImpl-Cslh4XN_.js +7 -0
- package/dist/assets/MarkstreamRendererImpl-CsNwy1T5.css +1 -0
- package/dist/assets/MarkstreamRendererImpl-Ymm12x6J.js +1 -0
- package/dist/assets/MobileDetailNavigation-DD29ENgU.js +1 -0
- package/dist/assets/MobileShareBridge-CrTe_QND.js +4 -0
- package/dist/assets/MobileSurface-DjxchpUC.js +2 -0
- package/dist/assets/{MultiRunWindow-D_jo8cqz.js → MultiRunWindow-jG5dCEMH.js} +1 -1
- package/dist/assets/OnboardingScreen-C3-wzeSX.js +1 -0
- package/dist/assets/OverlayScrollbar-KUpvYHdv.js +2 -0
- package/dist/assets/SettingsView-UKlEewdq.js +25 -0
- package/dist/assets/{SettingsWindow-CzMoz2fi.js → SettingsWindow-DO1aAplT.js} +1 -1
- package/dist/assets/TerminalView-BG1UPLsz.js +8 -0
- package/dist/assets/ToolOutputDialog-CWr5eVTK.js +10 -0
- package/dist/assets/appThemeRegistry-BI5drVpC.js +1 -0
- package/dist/assets/{button-z0GOBtWi.js → button-C0cth9tl.js} +1 -1
- package/dist/assets/clipboard-DJ4nQEuY.js +1 -0
- package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-DFT9cE80.js} +1 -1
- package/dist/assets/context-menu-CAp74NfV.js +1 -0
- package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
- package/dist/assets/desktopRelayRestore-9EbBYQ9s.js +1 -0
- package/dist/assets/{diagram-CoEqmReY.js → diagram-BryzMuta.js} +1 -1
- package/dist/assets/{dialog-x2hirWBd.js → dialog-CgbV5ljg.js} +1 -1
- package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
- package/dist/assets/{es-C8Sqjsia.js → es-BHqnSjoZ.js} +1 -1
- package/dist/assets/fileQueries-BY-9OWzp.js +1 -0
- package/dist/assets/fileReferenceActions-BJJtX_S5.js +1 -0
- package/dist/assets/{fr-p-Zwm_j2.js → fr-BfrlJY9i.js} +1 -1
- package/dist/assets/{gitApi-BE0vA4zm.js → gitApi-D9hFlE3T.js} +3 -3
- package/dist/assets/{input-BG3VWTIF.js → input-DFLpqjGR.js} +1 -1
- package/dist/assets/insertionBoundaries-D2L7D0Oe.js +4 -0
- package/dist/assets/{ja-B6REu1qP.js → ja-DP-7qt-x.js} +1 -1
- package/dist/assets/{ko-CJBNH9IZ.js → ko-E2B5vGQP.js} +1 -1
- package/dist/assets/main-BRedOs_V.js +169 -0
- package/dist/assets/main-DlXpJ0Vw.js +2 -0
- package/dist/assets/markdown-worker-BuveaH3O.js +1 -0
- package/dist/assets/markdownTheme-CzZm3riB.js +2 -0
- package/dist/assets/miniChat-CBLbFpfy.js +2 -0
- package/dist/assets/mobile-CoZsWdWk.js +2 -0
- package/dist/assets/multirun-BF-I0r2_.js +4 -0
- package/dist/assets/outsideFileGrants-DnplRxr-.js +1 -0
- package/dist/assets/path-utils-jGozTg9d.js +1 -0
- package/dist/assets/persistence-DdUac14I.js +3 -0
- package/dist/assets/{pl-B2cBI_7K.js → pl-r8X5gRM5.js} +1 -1
- package/dist/assets/platform-B4KKLnZ5.js +2 -0
- package/dist/assets/projectMeta-2t4L5_4a.js +3 -0
- package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-fl7O3iHy.js +2 -0
- package/dist/assets/renderMobileApp-CKYDwf4U.js +28 -0
- package/dist/assets/runtimeConfig-B4eb4ZiU.js +2 -0
- package/dist/assets/runtimeEndpointReset-BHuTYyzc.js +147 -0
- package/dist/assets/sessionChangeSummary-DOq8EN_H.js +1 -0
- package/dist/assets/sessionLookup-DNc0ZeR3.js +1 -0
- package/dist/assets/sortable-tabs-strip-C8_9kHqu.js +1 -0
- package/dist/assets/src-DasvfkEg.css +2 -0
- package/dist/assets/{store-CL3WP48T.js → store-BDFdnk6l.js} +2 -2
- package/dist/assets/terminalApi-947l7Ra3.js +1 -0
- package/dist/assets/textarea-Cgo_nWUU.js +1 -0
- package/dist/assets/toolHelpers-DEvVP9U0.js +2 -0
- package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BifLJ8o0.js} +1 -1
- package/dist/assets/{uk-C8YKAz0x.js → uk-Di0vl9V-.js} +1 -1
- package/dist/assets/url-Pk6tQ1EZ.js +1 -0
- package/dist/assets/{ChatContainer-D3onaUkL.js → useAppFontEffects-Dle8xs5h.js} +44 -39
- package/dist/assets/useAssistantUIStore-me6s63b2.js +1 -0
- package/dist/assets/useDesktopWindowControlsLayout-_QAwKcQn.js +1 -0
- package/dist/assets/useEffectiveDirectory-BSyuAgz6.js +1 -0
- package/dist/assets/useFontPreferences-BCOBlaAd.js +1 -0
- package/dist/assets/useMobileNavigationStore-f8xHJk7C.js +413 -0
- package/dist/assets/useSessionAutoCleanup-BHiXaW0J.js +1 -0
- package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
- package/dist/assets/useWorkerHighlightedLines-DuvXTSm_.js +7 -0
- package/dist/assets/utils-BiK3tBGM.js +1 -0
- package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
- package/dist/assets/{zh-CN-CCYwUnmB.js → zh-CN-coFCfeQE.js} +1 -1
- package/dist/assets/{zh-TW-D8Y6py08.js → zh-TW-oDRZ1-yb.js} +1 -1
- package/dist/index.html +9 -11
- package/dist/mini-chat.html +9 -11
- package/dist/mobile.html +15 -17
- package/package.json +2 -1
- package/server/index.js +2 -0
- package/server/lib/assistant-delivery-parts.js +3 -1
- package/server/lib/assistant-delivery-parts.test.js +11 -0
- package/server/lib/assistants/DOCUMENTATION.md +76 -3
- package/server/lib/assistants/assign.js +225 -0
- package/server/lib/assistants/assign.test.js +118 -0
- package/server/lib/assistants/bubbles.js +45 -0
- package/server/lib/assistants/bubbles.test.js +36 -0
- package/server/lib/assistants/cards.js +148 -0
- package/server/lib/assistants/cards.test.js +116 -0
- package/server/lib/assistants/contact-store.js +284 -0
- package/server/lib/assistants/contact-store.test.js +131 -0
- package/server/lib/assistants/contact-tools.js +761 -0
- package/server/lib/assistants/contact-tools.test.js +328 -0
- package/server/lib/assistants/contracts.js +20 -1
- package/server/lib/assistants/harness.js +551 -0
- package/server/lib/assistants/harness.test.js +607 -0
- package/server/lib/assistants/routes.js +44 -4
- package/server/lib/assistants/service.js +930 -17
- package/server/lib/assistants/service.test.js +1163 -45
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/directory-ws-bridge.js +11 -2
- package/server/lib/event-stream/global-ws-bridge.js +19 -3
- package/server/lib/event-stream/protocol.js +11 -1
- package/server/lib/event-stream/reasoning-projection.js +425 -0
- package/server/lib/event-stream/reasoning-projection.test.js +300 -0
- package/server/lib/event-stream/runtime.js +10 -0
- package/server/lib/event-stream/runtime.test.js +53 -0
- package/server/lib/llm/DOCUMENTATION.md +84 -0
- package/server/lib/llm/catalog.js +100 -0
- package/server/lib/llm/catalog.test.js +78 -0
- package/server/lib/llm/completions.js +151 -0
- package/server/lib/llm/completions.test.js +127 -0
- package/server/lib/llm/generate.js +570 -0
- package/server/lib/llm/generate.test.js +459 -0
- package/server/lib/llm/routes.js +47 -0
- package/server/lib/llm/temp-directory.js +37 -0
- package/server/lib/notifications/APNS.md +19 -13
- package/server/lib/notifications/DOCUMENTATION.md +4 -3
- package/server/lib/notifications/runtime.js +153 -38
- package/server/lib/notifications/runtime.test.js +122 -0
- package/server/lib/opencode/DOCUMENTATION.md +2 -1
- package/server/lib/opencode/feature-routes-runtime.js +30 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
- package/server/lib/opencode/proxy.js +103 -9
- package/server/lib/opencode/proxy.test.js +177 -1
- package/server/lib/opencode/settings-runtime.js +2 -2
- package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
- package/server/lib/session-goal/DOCUMENTATION.md +8 -1
- package/server/lib/session-goal/runtime.js +20 -0
- package/server/lib/session-index/DOCUMENTATION.md +3 -1
- package/server/lib/session-index/service.js +10 -4
- package/server/lib/session-index/service.test.js +37 -0
- package/server/lib/session-title/runtime.js +1 -0
- package/server/lib/session-title/runtime.test.js +11 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
- package/server/lib/session-turn-pages/routes.js +22 -3
- package/server/lib/session-turn-pages/routes.test.js +43 -0
- package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
- package/server/lib/transcript-cache/routes.js +9 -2
- package/server/lib/transcript-cache/routes.test.js +32 -0
- package/dist/assets/AgentAvatar-Di8F4QJh.js +0 -1
- package/dist/assets/AssistantView-DYdXsGDh.js +0 -1
- package/dist/assets/ContextPanel-Cj6RVUAe.js +0 -285
- package/dist/assets/DiagramView-BoRTG67Z.js +0 -1
- package/dist/assets/FadeInOnReveal-CV2s6YN8.js +0 -1
- package/dist/assets/Icon-CLpn1mIz.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DwUKY9k4.js +0 -7
- package/dist/assets/MarkstreamRendererImpl-BRvprELo.js +0 -1
- package/dist/assets/MarkstreamRendererImpl-Ca7RATbs.css +0 -1
- package/dist/assets/MobileDetailNavigation-C0Z8beRL.js +0 -1
- package/dist/assets/MobileShareBridge-DTvmm9vz.js +0 -4
- package/dist/assets/MobileSurface-CfrI-5uf.js +0 -2
- package/dist/assets/OnboardingScreen-D6rtE7rj.js +0 -1
- package/dist/assets/OverlayScrollbar-DlNZho3q.js +0 -2
- package/dist/assets/SettingsView-BOT158gD.js +0 -25
- package/dist/assets/TerminalView-sf_LfP6Z.js +0 -8
- package/dist/assets/ToolOutputDialog-BI5vwuaT.js +0 -10
- package/dist/assets/appThemeRegistry-G_zwgqAH.js +0 -2
- package/dist/assets/clipboard-D35XSwID.js +0 -1
- package/dist/assets/desktop-CfLlmafK.js +0 -1
- package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
- package/dist/assets/desktopRelayRestore-8pvI_dSc.js +0 -1
- package/dist/assets/fileQueries-DGHZqZHc.js +0 -1
- package/dist/assets/insertionBoundaries-CJksyugt.js +0 -4
- package/dist/assets/main-BTeHOAU_.js +0 -29
- package/dist/assets/main-BUBx-y1E.js +0 -2
- package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
- package/dist/assets/markdownTheme-SPe4jOwX.js +0 -2
- package/dist/assets/miniChat-Btnf42TH.js +0 -2
- package/dist/assets/mobile-CAjwV6ly.js +0 -2
- package/dist/assets/multirun-Rpz1_-fz.js +0 -4
- package/dist/assets/persistence-w-AiX2i4.js +0 -3
- package/dist/assets/platform-4eCmsdol.js +0 -2
- package/dist/assets/projectMeta-CvUuy-j5.js +0 -3
- package/dist/assets/radio-D_a28OlM.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-DCzBQk2w.js +0 -2
- package/dist/assets/renderMobileApp-U5sclQRp.js +0 -28
- package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
- package/dist/assets/runtimeConfig-6inIl99b.js +0 -2
- package/dist/assets/runtimeEndpointReset-B_rUFDT5.js +0 -4
- package/dist/assets/shikiHighlight-CMpSAuTl.js +0 -1
- package/dist/assets/sortable-tabs-strip-CNDgyX0B.js +0 -1
- package/dist/assets/src-DZt1nWf5.css +0 -2
- package/dist/assets/terminalApi-CGtOtc8I.js +0 -1
- package/dist/assets/url-KeihbE0d.js +0 -1
- package/dist/assets/useAppFontEffects-3xHRVJcf.js +0 -7
- package/dist/assets/useAssistantUIStore-I0VWrB5V.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-QJlddPRn.js +0 -1
- package/dist/assets/useEffectiveDirectory-Bcj1wP95.js +0 -1
- package/dist/assets/useFontPreferences-BLZ3y6mN.js +0 -1
- package/dist/assets/useMobileNavigationStore-CD0GRjaV.js +0 -412
- package/dist/assets/useSessionAutoCleanup-CN_6ABvx.js +0 -1
- package/dist/assets/useUIStore-CNLmCoPU.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-DX9znDoW.js +0 -7
- package/dist/assets/utils-CTL16t2f.js +0 -1
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay integration evidence for Host reasoning projection (`includeReasoning=false`).
|
|
3
|
+
*
|
|
4
|
+
* Stack (real components, not helper-only stubs):
|
|
5
|
+
* - Layer 1: `@openchambery/relay-server` private relay (real auth gates)
|
|
6
|
+
* - Host: `startRelayHost` + tunnel-host loopback dispatcher
|
|
7
|
+
* - Client: TS `createRelayTunnelClient` (E2EE decrypt on receive)
|
|
8
|
+
* - Origin: Express + `createUiAuth` (bearer) + `registerOpenCodeProxy`
|
|
9
|
+
* (the real Host SSE/list filtering routes)
|
|
10
|
+
* - Upstream: controllable fake OpenCode (SSE + session.messages)
|
|
11
|
+
*
|
|
12
|
+
* Asserts disabled clients never decrypt reasoning body markers; enabled clients
|
|
13
|
+
* still receive them; Host strips `includeReasoning` before OpenCode; text /
|
|
14
|
+
* tokens.reasoning / session status survive. Synthetic markers only — no real
|
|
15
|
+
* user content or credentials in assertions.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// @ts-nocheck — match relay-server.e2e.test.ts: runtime Vitest/bun-test shim, not tsc project.
|
|
19
|
+
import { afterEach, describe, expect, it } from 'bun:test';
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
import express from 'express';
|
|
22
|
+
import http from 'node:http';
|
|
23
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
24
|
+
import os from 'node:os';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { startPrivateRelayServer } from '../../../../relay-server/src/index.js';
|
|
28
|
+
import { createRelayTunnelClient } from '../../../../ui/src/lib/relay/tunnel-client.ts';
|
|
29
|
+
import { exportPublicKeyJwk, generateEcdhKeyPair, importEcdhPrivateKey } from './e2ee.js';
|
|
30
|
+
import { startRelayHost } from './host-client.js';
|
|
31
|
+
import { registerOpenCodeProxy } from '../opencode/proxy.js';
|
|
32
|
+
import { createUiAuth } from '../ui-auth/ui-auth.js';
|
|
33
|
+
|
|
34
|
+
const timeoutMs = 30_000;
|
|
35
|
+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
|
|
37
|
+
const eventually = async (
|
|
38
|
+
predicate: () => boolean | Promise<boolean>,
|
|
39
|
+
message: string,
|
|
40
|
+
timeout = timeoutMs,
|
|
41
|
+
) => {
|
|
42
|
+
const deadline = Date.now() + timeout;
|
|
43
|
+
while (Date.now() < deadline) {
|
|
44
|
+
if (await predicate()) return;
|
|
45
|
+
await wait(25);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(message);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Synthetic neutral markers — never real message text or secrets. */
|
|
51
|
+
const TEXT_MARKER = 'syn-text-visible-marker-bbbb';
|
|
52
|
+
const REASONING_MARKER = 'syn-reason-hidden-marker-aaaa';
|
|
53
|
+
const REASONING_DELTA_PAD = 'x'.repeat(768);
|
|
54
|
+
const REASONING_DELTA_COUNT = 128;
|
|
55
|
+
const SESSION_STATUS_TYPE = 'session.status';
|
|
56
|
+
const TOKEN_REASONING = 42;
|
|
57
|
+
|
|
58
|
+
const buildIdentity = async () => {
|
|
59
|
+
const enc = await generateEcdhKeyPair();
|
|
60
|
+
const encPrivJwk = await globalThis.crypto.subtle.exportKey('jwk', enc.privateKey);
|
|
61
|
+
const { privateKey: signPriv, publicKey: signPub } = crypto.generateKeyPairSync('ec', {
|
|
62
|
+
namedCurve: 'P-256',
|
|
63
|
+
});
|
|
64
|
+
const signPubJwk = signPub.export({ format: 'jwk' }) as JsonWebKey;
|
|
65
|
+
const canonical = JSON.stringify({
|
|
66
|
+
crv: signPubJwk.crv,
|
|
67
|
+
kty: signPubJwk.kty,
|
|
68
|
+
x: signPubJwk.x,
|
|
69
|
+
y: signPubJwk.y,
|
|
70
|
+
});
|
|
71
|
+
const serverId = crypto.createHash('sha256').update(canonical).digest('base64url');
|
|
72
|
+
return {
|
|
73
|
+
serverId,
|
|
74
|
+
hostEncPubJwk: await exportPublicKeyJwk(enc.publicKey),
|
|
75
|
+
hostEncPrivateKey: await importEcdhPrivateKey(encPrivJwk),
|
|
76
|
+
signRelayAuth: (role: string, connectionId?: string | null) => {
|
|
77
|
+
const ts = Date.now();
|
|
78
|
+
const sig = crypto
|
|
79
|
+
.sign('SHA256', Buffer.from(`${ts}.${serverId}.${role}.${connectionId ?? ''}`), {
|
|
80
|
+
key: signPriv,
|
|
81
|
+
dsaEncoding: 'ieee-p1363',
|
|
82
|
+
})
|
|
83
|
+
.toString('base64url');
|
|
84
|
+
return {
|
|
85
|
+
ts,
|
|
86
|
+
sig,
|
|
87
|
+
pk: Buffer.from(canonical, 'utf8').toString('base64url'),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const readResponseText = async (response: Response): Promise<string> => {
|
|
94
|
+
if (!response.body) return '';
|
|
95
|
+
const reader = response.body.getReader();
|
|
96
|
+
const chunks: Uint8Array[] = [];
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done) break;
|
|
100
|
+
if (value && value.length > 0) chunks.push(value);
|
|
101
|
+
}
|
|
102
|
+
return new TextDecoder().decode(Buffer.concat(chunks.map((c) => Buffer.from(c))));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const countOccurrences = (haystack: string, needle: string): number => {
|
|
106
|
+
if (!needle) return 0;
|
|
107
|
+
let count = 0;
|
|
108
|
+
let idx = 0;
|
|
109
|
+
while (true) {
|
|
110
|
+
const found = haystack.indexOf(needle, idx);
|
|
111
|
+
if (found === -1) break;
|
|
112
|
+
count += 1;
|
|
113
|
+
idx = found + needle.length;
|
|
114
|
+
}
|
|
115
|
+
return count;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const closeServer = (server?: http.Server | null) =>
|
|
119
|
+
new Promise((resolve, reject) => {
|
|
120
|
+
if (!server) {
|
|
121
|
+
resolve();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
type UpstreamObservation = {
|
|
128
|
+
urls: string[];
|
|
129
|
+
methods: string[];
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Controllable fake OpenCode: fixed reasoning + text parts/deltas and session status.
|
|
134
|
+
* Records full request URLs so the test can prove includeReasoning never arrives.
|
|
135
|
+
*/
|
|
136
|
+
const startFakeOpenCode = async (): Promise<{
|
|
137
|
+
port: number;
|
|
138
|
+
observation: UpstreamObservation;
|
|
139
|
+
stop: () => Promise<void>;
|
|
140
|
+
}> => {
|
|
141
|
+
const observation: UpstreamObservation = { urls: [], methods: [] };
|
|
142
|
+
const app = express();
|
|
143
|
+
|
|
144
|
+
app.use((req, _res, next) => {
|
|
145
|
+
observation.urls.push(req.originalUrl || req.url || '');
|
|
146
|
+
observation.methods.push(req.method || 'GET');
|
|
147
|
+
next();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
app.get('/global/event', (req, res) => {
|
|
151
|
+
res.writeHead(200, {
|
|
152
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
153
|
+
'cache-control': 'no-cache',
|
|
154
|
+
connection: 'keep-alive',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Seed part types so field=text deltas classify correctly on Host filter.
|
|
158
|
+
res.write(
|
|
159
|
+
`id: seed-r\ndata: ${JSON.stringify({
|
|
160
|
+
type: 'message.part.updated',
|
|
161
|
+
properties: {
|
|
162
|
+
part: {
|
|
163
|
+
id: 'prt_reason',
|
|
164
|
+
messageID: 'msg_1',
|
|
165
|
+
type: 'reasoning',
|
|
166
|
+
text: REASONING_MARKER,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
})}\n\n`,
|
|
170
|
+
);
|
|
171
|
+
res.write(
|
|
172
|
+
`id: seed-t\ndata: ${JSON.stringify({
|
|
173
|
+
type: 'message.part.updated',
|
|
174
|
+
properties: {
|
|
175
|
+
part: {
|
|
176
|
+
id: 'prt_text',
|
|
177
|
+
messageID: 'msg_1',
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: TEXT_MARKER,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
})}\n\n`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// High-volume reasoning deltas (field=text on known reasoning part).
|
|
186
|
+
for (let i = 0; i < REASONING_DELTA_COUNT; i += 1) {
|
|
187
|
+
res.write(
|
|
188
|
+
`id: rd-${i}\ndata: ${JSON.stringify({
|
|
189
|
+
type: 'message.part.delta',
|
|
190
|
+
properties: {
|
|
191
|
+
messageID: 'msg_1',
|
|
192
|
+
partID: 'prt_reason',
|
|
193
|
+
field: 'text',
|
|
194
|
+
delta: `${REASONING_MARKER}-${i}-${REASONING_DELTA_PAD}`,
|
|
195
|
+
},
|
|
196
|
+
})}\n\n`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Ordinary text delta must survive filtering.
|
|
201
|
+
res.write(
|
|
202
|
+
`id: td-1\ndata: ${JSON.stringify({
|
|
203
|
+
type: 'message.part.delta',
|
|
204
|
+
properties: {
|
|
205
|
+
messageID: 'msg_1',
|
|
206
|
+
partID: 'prt_text',
|
|
207
|
+
field: 'text',
|
|
208
|
+
delta: `${TEXT_MARKER}-delta`,
|
|
209
|
+
},
|
|
210
|
+
})}\n\n`,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Versioned current-style reasoning stream event — must drop when disabled.
|
|
214
|
+
res.write(
|
|
215
|
+
`id: snr-1\ndata: ${JSON.stringify({
|
|
216
|
+
type: 'session.next.reasoning.delta',
|
|
217
|
+
properties: { sessionID: 'ses_1', delta: `${REASONING_MARKER}-snr` },
|
|
218
|
+
})}\n\n`,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Session status + message snapshot with tokens.reasoning must keep.
|
|
222
|
+
res.write(
|
|
223
|
+
`id: st-1\ndata: ${JSON.stringify({
|
|
224
|
+
type: SESSION_STATUS_TYPE,
|
|
225
|
+
properties: { sessionID: 'ses_1', status: 'busy' },
|
|
226
|
+
})}\n\n`,
|
|
227
|
+
);
|
|
228
|
+
res.write(
|
|
229
|
+
`id: mu-1\ndata: ${JSON.stringify({
|
|
230
|
+
type: 'message.updated',
|
|
231
|
+
properties: {
|
|
232
|
+
info: {
|
|
233
|
+
id: 'msg_1',
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
tokens: { input: 1, output: 2, reasoning: TOKEN_REASONING },
|
|
236
|
+
},
|
|
237
|
+
parts: [
|
|
238
|
+
{ id: 'prt_reason', type: 'reasoning', text: REASONING_MARKER },
|
|
239
|
+
{ id: 'prt_text', type: 'text', text: TEXT_MARKER },
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
})}\n\n`,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
res.end();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
app.get('/session/:sessionID/message', (req, res) => {
|
|
249
|
+
res.json([
|
|
250
|
+
{
|
|
251
|
+
info: {
|
|
252
|
+
id: 'msg_1',
|
|
253
|
+
role: 'assistant',
|
|
254
|
+
tokens: { input: 3, output: 4, reasoning: TOKEN_REASONING },
|
|
255
|
+
},
|
|
256
|
+
parts: [
|
|
257
|
+
{ id: 'prt_reason', type: 'reasoning', text: REASONING_MARKER },
|
|
258
|
+
{ id: 'prt_text', type: 'text', text: TEXT_MARKER },
|
|
259
|
+
{ id: 'prt_tool', type: 'tool', tool: 'bash' },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const server = await new Promise<http.Server>((resolve, reject) => {
|
|
266
|
+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
267
|
+
s.once('error', reject);
|
|
268
|
+
});
|
|
269
|
+
const port = (server.address() as { port: number }).port;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
port,
|
|
273
|
+
observation,
|
|
274
|
+
stop: () => closeServer(server),
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* OpenChamber origin: real UI auth + real OpenCode proxy (Host filtering boundary).
|
|
280
|
+
* Tunnel-host dials this loopback port.
|
|
281
|
+
*/
|
|
282
|
+
const startOpenChamberOrigin = async (openCodePort: number) => {
|
|
283
|
+
const clientAuthController = {
|
|
284
|
+
authenticateBearerToken: async (token: string) =>
|
|
285
|
+
token === 'client-token'
|
|
286
|
+
? { ok: true, clientId: 'device-relay-reason', sessionToken: 'client:device-relay-reason' }
|
|
287
|
+
: { ok: false },
|
|
288
|
+
};
|
|
289
|
+
const uiAuth = createUiAuth({
|
|
290
|
+
requireClientAuth: true,
|
|
291
|
+
clientAuthController: clientAuthController as never,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const app = express();
|
|
295
|
+
app.get('/health', (_req, res) => {
|
|
296
|
+
res.json({ ok: true, service: 'openchamber-origin' });
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Real auth gate before Host proxy routes (parity with remote clients).
|
|
300
|
+
app.use((req, res, next) => {
|
|
301
|
+
if (req.path === '/health') {
|
|
302
|
+
next();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
void uiAuth.requireAuth(req, res, next);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
registerOpenCodeProxy(app, {
|
|
309
|
+
fs: { promises: { realpath: async (value: string) => value } },
|
|
310
|
+
os: {},
|
|
311
|
+
path: {},
|
|
312
|
+
OPEN_CODE_READY_GRACE_MS: 0,
|
|
313
|
+
SSE_UPSTREAM_CONNECT_TIMEOUT_MS: 5_000,
|
|
314
|
+
SSE_UPSTREAM_STALL_TIMEOUT_MS: 10_000,
|
|
315
|
+
getRuntime: () => ({
|
|
316
|
+
openCodePort,
|
|
317
|
+
isOpenCodeReady: true,
|
|
318
|
+
openCodeNotReadySince: 0,
|
|
319
|
+
isRestartingOpenCode: false,
|
|
320
|
+
openCodeBaseUrl: `http://127.0.0.1:${openCodePort}`,
|
|
321
|
+
}),
|
|
322
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Bearer opencode-upstream-token' }),
|
|
323
|
+
buildOpenCodeUrl: (requestPath: string) => {
|
|
324
|
+
const normalized = requestPath.startsWith('/') ? requestPath : `/${requestPath}`;
|
|
325
|
+
return `http://127.0.0.1:${openCodePort}${normalized}`;
|
|
326
|
+
},
|
|
327
|
+
ensureOpenCodeApiPrefix: () => {},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const server = await new Promise<http.Server>((resolve, reject) => {
|
|
331
|
+
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
|
332
|
+
s.once('error', reject);
|
|
333
|
+
});
|
|
334
|
+
const port = (server.address() as { port: number }).port;
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
port,
|
|
338
|
+
stop: async () => {
|
|
339
|
+
await closeServer(server);
|
|
340
|
+
uiAuth.dispose();
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
describe('relay reasoning projection e2e (Host filter over private relay)', () => {
|
|
346
|
+
let cleanup: (() => Promise<void>) | undefined;
|
|
347
|
+
|
|
348
|
+
afterEach(async () => {
|
|
349
|
+
await cleanup?.();
|
|
350
|
+
cleanup = undefined;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it(
|
|
354
|
+
'filters reasoning on SSE + session.messages through real relay tunnel while enabled path keeps them',
|
|
355
|
+
{ timeout: 90_000 },
|
|
356
|
+
async () => {
|
|
357
|
+
const temp = await mkdtemp(path.join(os.tmpdir(), 'openchamber-relay-reason-'));
|
|
358
|
+
const previousDataDir = process.env.OPENCHAMBER_DATA_DIR;
|
|
359
|
+
process.env.OPENCHAMBER_DATA_DIR = temp;
|
|
360
|
+
|
|
361
|
+
let relay: Awaited<ReturnType<typeof startPrivateRelayServer>> | undefined;
|
|
362
|
+
let upstream: Awaited<ReturnType<typeof startFakeOpenCode>> | undefined;
|
|
363
|
+
let origin: Awaited<ReturnType<typeof startOpenChamberOrigin>> | undefined;
|
|
364
|
+
let host: ReturnType<typeof startRelayHost> | undefined;
|
|
365
|
+
let client: ReturnType<typeof createRelayTunnelClient> | undefined;
|
|
366
|
+
|
|
367
|
+
cleanup = async () => {
|
|
368
|
+
const results = await Promise.allSettled([
|
|
369
|
+
Promise.resolve(client?.close()),
|
|
370
|
+
Promise.resolve(host?.stop()),
|
|
371
|
+
origin?.stop(),
|
|
372
|
+
upstream?.stop(),
|
|
373
|
+
relay?.stop(),
|
|
374
|
+
]);
|
|
375
|
+
if (previousDataDir === undefined) delete process.env.OPENCHAMBER_DATA_DIR;
|
|
376
|
+
else process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
|
|
377
|
+
await rm(temp, { recursive: true, force: true });
|
|
378
|
+
const failed = results.find((result) => result.status === 'rejected');
|
|
379
|
+
if (failed?.status === 'rejected') throw failed.reason;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
upstream = await startFakeOpenCode();
|
|
383
|
+
origin = await startOpenChamberOrigin(upstream.port);
|
|
384
|
+
relay = await startPrivateRelayServer({ host: '127.0.0.1', port: 0 });
|
|
385
|
+
const relayUrl = relay.wsUrl;
|
|
386
|
+
if (!relayUrl) throw new Error('relay did not expose wsUrl');
|
|
387
|
+
|
|
388
|
+
const identity = await buildIdentity();
|
|
389
|
+
host = startRelayHost({
|
|
390
|
+
relayUrl,
|
|
391
|
+
identity,
|
|
392
|
+
getLocalPort: () => origin!.port,
|
|
393
|
+
onStatus: () => {},
|
|
394
|
+
logger: { warn: () => {}, info: () => {} },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
client = createRelayTunnelClient({
|
|
398
|
+
relayUrl,
|
|
399
|
+
serverId: identity.serverId,
|
|
400
|
+
hostEncPubJwk: identity.hostEncPubJwk,
|
|
401
|
+
helloRetryMs: 50,
|
|
402
|
+
helloTimeoutMs: 2_000,
|
|
403
|
+
pingIntervalMs: 500,
|
|
404
|
+
pingTimeoutMs: 250,
|
|
405
|
+
reconnectBaseDelayMs: 50,
|
|
406
|
+
reconnectMaxDelayMs: 250,
|
|
407
|
+
batchWindowMs: 5,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await eventually(
|
|
411
|
+
() => host!.getStatus().state === 'connected' && client!.getStatus().state === 'connected',
|
|
412
|
+
'host and tunnel client did not connect over private relay',
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const authHeaders = { authorization: 'Bearer client-token' };
|
|
416
|
+
|
|
417
|
+
// --- SSE: includeReasoning=false (must go through Host proxy filter) ---
|
|
418
|
+
const disabledSse = await client.fetch('/api/global/event?includeReasoning=false', {
|
|
419
|
+
headers: authHeaders,
|
|
420
|
+
});
|
|
421
|
+
expect(disabledSse.status).toBe(200);
|
|
422
|
+
const disabledSseText = await readResponseText(disabledSse);
|
|
423
|
+
const disabledSseBytes = Buffer.byteLength(disabledSseText, 'utf8');
|
|
424
|
+
|
|
425
|
+
// --- SSE: default enabled (byte path keeps reasoning) ---
|
|
426
|
+
const enabledSse = await client.fetch('/api/global/event', {
|
|
427
|
+
headers: authHeaders,
|
|
428
|
+
});
|
|
429
|
+
expect(enabledSse.status).toBe(200);
|
|
430
|
+
const enabledSseText = await readResponseText(enabledSse);
|
|
431
|
+
const enabledSseBytes = Buffer.byteLength(enabledSseText, 'utf8');
|
|
432
|
+
|
|
433
|
+
// Disabled: zero reasoning body leakage after tunnel decrypt.
|
|
434
|
+
expect(disabledSseText).not.toContain(REASONING_MARKER);
|
|
435
|
+
expect(disabledSseText).not.toContain('session.next.reasoning');
|
|
436
|
+
expect(disabledSseText).not.toContain('"type":"reasoning"');
|
|
437
|
+
expect(countOccurrences(disabledSseText, REASONING_MARKER)).toBe(0);
|
|
438
|
+
|
|
439
|
+
// Disabled: text + session status + tokens.reasoning survive.
|
|
440
|
+
expect(disabledSseText).toContain(TEXT_MARKER);
|
|
441
|
+
expect(disabledSseText).toContain(SESSION_STATUS_TYPE);
|
|
442
|
+
expect(disabledSseText).toContain(`"reasoning":${TOKEN_REASONING}`);
|
|
443
|
+
expect(disabledSseText).toContain('"type":"text"');
|
|
444
|
+
expect(disabledSseText).toContain(`${TEXT_MARKER}-delta`);
|
|
445
|
+
|
|
446
|
+
// Enabled: reasoning still present (proves upstream + passthrough path).
|
|
447
|
+
expect(enabledSseText).toContain(REASONING_MARKER);
|
|
448
|
+
expect(enabledSseText).toContain('session.next.reasoning.delta');
|
|
449
|
+
expect(enabledSseText).toContain('"type":"reasoning"');
|
|
450
|
+
expect(countOccurrences(enabledSseText, REASONING_MARKER)).toBeGreaterThanOrEqual(
|
|
451
|
+
REASONING_DELTA_COUNT,
|
|
452
|
+
);
|
|
453
|
+
expect(enabledSseText).toContain(TEXT_MARKER);
|
|
454
|
+
expect(enabledSseText).toContain(SESSION_STATUS_TYPE);
|
|
455
|
+
|
|
456
|
+
// Byte comparison: disabled drops the KB-scale reasoning flood.
|
|
457
|
+
expect(enabledSseBytes).toBeGreaterThan(disabledSseBytes);
|
|
458
|
+
expect(enabledSseBytes - disabledSseBytes).toBeGreaterThan(
|
|
459
|
+
REASONING_DELTA_COUNT * 400,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// Text event parity: both paths keep the ordinary text delta once.
|
|
463
|
+
const disabledTextDeltas = countOccurrences(disabledSseText, `${TEXT_MARKER}-delta`);
|
|
464
|
+
const enabledTextDeltas = countOccurrences(enabledSseText, `${TEXT_MARKER}-delta`);
|
|
465
|
+
expect(disabledTextDeltas).toBe(1);
|
|
466
|
+
expect(enabledTextDeltas).toBe(1);
|
|
467
|
+
|
|
468
|
+
// Observable run metrics for coordinator acceptance (synthetic markers only).
|
|
469
|
+
// eslint-disable-next-line no-console
|
|
470
|
+
console.log(
|
|
471
|
+
JSON.stringify({
|
|
472
|
+
reasoningDeltaCount: REASONING_DELTA_COUNT,
|
|
473
|
+
reasoningMarkerHitsDisabled: countOccurrences(disabledSseText, REASONING_MARKER),
|
|
474
|
+
reasoningMarkerHitsEnabled: countOccurrences(enabledSseText, REASONING_MARKER),
|
|
475
|
+
disabledSseBytes,
|
|
476
|
+
enabledSseBytes,
|
|
477
|
+
savedBytes: enabledSseBytes - disabledSseBytes,
|
|
478
|
+
disabledTextDeltas,
|
|
479
|
+
enabledTextDeltas,
|
|
480
|
+
upstreamUrlCount: upstream.observation.urls.length,
|
|
481
|
+
}),
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// --- HTTP session.messages list (optional second surface) ---
|
|
485
|
+
const disabledList = await client.fetch(
|
|
486
|
+
'/api/session/ses_1/message?limit=30&includeReasoning=false&directory=%2Frepo',
|
|
487
|
+
{ headers: authHeaders },
|
|
488
|
+
);
|
|
489
|
+
expect(disabledList.status).toBe(200);
|
|
490
|
+
const disabledListJson = (await disabledList.json()) as Array<{
|
|
491
|
+
info: { tokens: { reasoning: number } };
|
|
492
|
+
parts: Array<{ type: string; text?: string }>;
|
|
493
|
+
}>;
|
|
494
|
+
expect(disabledListJson[0].info.tokens.reasoning).toBe(TOKEN_REASONING);
|
|
495
|
+
expect(disabledListJson[0].parts.every((p) => p.type !== 'reasoning')).toBe(true);
|
|
496
|
+
expect(JSON.stringify(disabledListJson)).not.toContain(REASONING_MARKER);
|
|
497
|
+
expect(disabledListJson[0].parts.some((p) => p.type === 'text' && p.text === TEXT_MARKER)).toBe(
|
|
498
|
+
true,
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const enabledList = await client.fetch('/api/session/ses_1/message?directory=%2Frepo', {
|
|
502
|
+
headers: authHeaders,
|
|
503
|
+
});
|
|
504
|
+
expect(enabledList.status).toBe(200);
|
|
505
|
+
const enabledListJson = (await enabledList.json()) as Array<{
|
|
506
|
+
parts: Array<{ type: string; text?: string }>;
|
|
507
|
+
}>;
|
|
508
|
+
expect(enabledListJson[0].parts.some((p) => p.type === 'reasoning')).toBe(true);
|
|
509
|
+
expect(JSON.stringify(enabledListJson)).toContain(REASONING_MARKER);
|
|
510
|
+
|
|
511
|
+
// Host must strip includeReasoning before OpenCode upstream (all observed URLs).
|
|
512
|
+
expect(upstream.observation.urls.length).toBeGreaterThan(0);
|
|
513
|
+
for (const url of upstream.observation.urls) {
|
|
514
|
+
expect(url).not.toContain('includeReasoning');
|
|
515
|
+
}
|
|
516
|
+
// Sanity: upstream still saw the event + message routes.
|
|
517
|
+
expect(upstream.observation.urls.some((u) => u.includes('/global/event'))).toBe(true);
|
|
518
|
+
expect(upstream.observation.urls.some((u) => u.includes('/session/ses_1/message'))).toBe(
|
|
519
|
+
true,
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// Unauthenticated tunnel request still hits real auth gate (not open).
|
|
523
|
+
const denied = await client.fetch('/api/global/event?includeReasoning=false', {
|
|
524
|
+
headers: { authorization: 'Bearer wrong-token' },
|
|
525
|
+
});
|
|
526
|
+
expect(denied.status).toBe(401);
|
|
527
|
+
},
|
|
528
|
+
);
|
|
529
|
+
});
|
|
@@ -135,7 +135,10 @@ before touching the filesystem). Rationale: metadata rides every
|
|
|
135
135
|
continuation prompt using the last assistant message's
|
|
136
136
|
provider/model/agent — the goal spends the session's own subscription.
|
|
137
137
|
4. Settling (`complete`/`blocked`/`budgetLimited`) fires the injected
|
|
138
|
-
`emitGoalNotification` so the user hears about it even with the UI closed
|
|
138
|
+
`emitGoalNotification` so the user hears about it even with the UI closed.
|
|
139
|
+
The same settle also notifies a registered Assistant contact reporter
|
|
140
|
+
(`setAssignedSessionSettleHandler`) so an assigned/watched session updates
|
|
141
|
+
its contact card — read-only, no second scheduler:
|
|
139
142
|
desktop + UI broadcast + the standard push fanout (web-push with full
|
|
140
143
|
text; APNs with a generic per-type title and the session name as body).
|
|
141
144
|
It obeys the notify-on-completion setting. Conversely, while a goal is
|
|
@@ -180,6 +183,10 @@ The loop here picks it up from session events like any other goal.
|
|
|
180
183
|
|
|
181
184
|
## Limitations
|
|
182
185
|
|
|
186
|
+
- TODO(watch): Assistant contact assign skipped a watch tool this slice.
|
|
187
|
+
Goal settle already notifies. Do not invent a second scheduler. A later
|
|
188
|
+
thin contact tool can post a read-only session status card when
|
|
189
|
+
`metadata.openchamber.goal` settles.
|
|
183
190
|
- Web-server feature: VS Code (extension-only) renders goal state via
|
|
184
191
|
`session.updated` but does not run the loop.
|
|
185
192
|
- A goal on a session with no assistant reply yet starts after the first
|
|
@@ -286,6 +286,18 @@ export const accountGoalTokenSpend = ({ messages, goal }) => {
|
|
|
286
286
|
return { tokensUsed, lastAccountedMessageID };
|
|
287
287
|
};
|
|
288
288
|
|
|
289
|
+
let assignedSessionSettle = null;
|
|
290
|
+
|
|
291
|
+
export const setAssignedSessionSettleHandler = (handler) => {
|
|
292
|
+
assignedSessionSettle = typeof handler === 'function' ? handler : null;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const contactStatusFromGoal = (status) => {
|
|
296
|
+
if (status === 'complete' || status === 'budgetLimited') return 'complete';
|
|
297
|
+
if (status === 'blocked') return 'error';
|
|
298
|
+
return null;
|
|
299
|
+
};
|
|
300
|
+
|
|
289
301
|
export const createSessionGoalRuntime = ({
|
|
290
302
|
buildOpenCodeUrl,
|
|
291
303
|
getOpenCodeAuthHeaders,
|
|
@@ -401,6 +413,14 @@ export const createSessionGoalRuntime = ({
|
|
|
401
413
|
console.warn('[session-goal] notification failed:', error?.message || error);
|
|
402
414
|
}
|
|
403
415
|
}
|
|
416
|
+
const contactStatus = contactStatusFromGoal(status);
|
|
417
|
+
if (contactStatus && typeof assignedSessionSettle === 'function') {
|
|
418
|
+
try {
|
|
419
|
+
assignedSessionSettle({ sessionId, directory, status: contactStatus });
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.warn('[session-goal] assigned session settle failed:', error?.message || error);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
404
424
|
};
|
|
405
425
|
|
|
406
426
|
const runAudit = async ({ goal, assistantText, directory, lastAssistantInfo }) => {
|
|
@@ -15,7 +15,9 @@ Sessions titled `smartfetch-secondary` are temporary SmartFetch model calls;
|
|
|
15
15
|
the index excludes them from every snapshot and clears prior summaries when a
|
|
16
16
|
matching session update arrives. System-owned sessions are excluded by
|
|
17
17
|
authoritative metadata only: a non-empty
|
|
18
|
-
`metadata.openchamber.assistant.assistantID
|
|
18
|
+
`metadata.openchamber.assistant.assistantID` (unless
|
|
19
|
+
`metadata.openchamber.assigned.from === 'contact'` — those are visible
|
|
20
|
+
coding workers the Assistant subscribed to),
|
|
19
21
|
`metadata.openchamber.scheduledTask.taskID`, or
|
|
20
22
|
`metadata.openchamber.smallModel.purpose`. Title prefixes are human labels and
|
|
21
23
|
never participate in this filter. Metadata is the ownership/isolation signal;
|
|
@@ -10,10 +10,14 @@ const HIDDEN_SESSION_TITLES = new Set(['smartfetch-secondary']);
|
|
|
10
10
|
const nonEmptySystemID = (value) => typeof value === 'string' && value.length > 0;
|
|
11
11
|
|
|
12
12
|
/** System sessions are isolated by authoritative metadata only — never by title prefix. */
|
|
13
|
+
const isContactAssignedSession = (openchamber) => openchamber?.assigned?.from === 'contact';
|
|
14
|
+
|
|
13
15
|
const isSystemSession = (session) => {
|
|
14
16
|
const openchamber = session?.metadata?.openchamber;
|
|
15
17
|
if (!openchamber || typeof openchamber !== 'object') return false;
|
|
16
|
-
|
|
18
|
+
// Contact-assigned workers stay visible in Chat. Only archived Assistant
|
|
19
|
+
// bindings use `openchamber.assistant` as a hide marker.
|
|
20
|
+
if (nonEmptySystemID(openchamber.assistant?.assistantID) && !isContactAssignedSession(openchamber)) return true;
|
|
17
21
|
if (nonEmptySystemID(openchamber.scheduledTask?.taskID)) return true;
|
|
18
22
|
if (nonEmptySystemID(openchamber.smallModel?.purpose)) return true;
|
|
19
23
|
return false;
|
|
@@ -52,7 +56,9 @@ const toSummary = (session, fallbackDirectory) => {
|
|
|
52
56
|
if (!session || !isVisibleSession(session) || typeof session.id !== 'string' || !session.id) return null;
|
|
53
57
|
const directory = normalizeDirectory(session.directory ?? session.project?.worktree ?? fallbackDirectory);
|
|
54
58
|
if (!directory) return null;
|
|
55
|
-
const
|
|
59
|
+
const openchamber = session.metadata?.openchamber;
|
|
60
|
+
const assistant = openchamber?.assistant;
|
|
61
|
+
const hideAssistantMarker = isContactAssignedSession(openchamber);
|
|
56
62
|
return {
|
|
57
63
|
id: session.id,
|
|
58
64
|
directory,
|
|
@@ -63,8 +69,8 @@ const toSummary = (session, fallbackDirectory) => {
|
|
|
63
69
|
archivedAt: toTimestamp(session.time?.archived),
|
|
64
70
|
parentID: typeof session.parentID === 'string' ? session.parentID : null,
|
|
65
71
|
hasChildren: typeof session.hasChildren === 'boolean' ? session.hasChildren : null,
|
|
66
|
-
assistantID: typeof assistant?.assistantID
|
|
67
|
-
assistantName: typeof assistant?.name
|
|
72
|
+
assistantID: hideAssistantMarker || typeof assistant?.assistantID !== 'string' ? null : assistant.assistantID,
|
|
73
|
+
assistantName: hideAssistantMarker || typeof assistant?.name !== 'string' ? null : assistant.name,
|
|
68
74
|
};
|
|
69
75
|
};
|
|
70
76
|
|
|
@@ -100,6 +100,43 @@ describe('Electron session index', () => {
|
|
|
100
100
|
service.close();
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
it('keeps contact-assigned worker sessions visible in ordinary sidebar summaries', () => {
|
|
104
|
+
const runtimeRef = { value: 'http://runtime-a.test' };
|
|
105
|
+
const service = createService(runtimeRef);
|
|
106
|
+
const worker = {
|
|
107
|
+
...session('ses_worker', 100),
|
|
108
|
+
metadata: {
|
|
109
|
+
openchamber: {
|
|
110
|
+
assigned: { from: 'contact', assistantID: 'assistant_1', name: 'A' },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const legacyWorker = {
|
|
115
|
+
...session('ses_legacy_worker', 99),
|
|
116
|
+
metadata: {
|
|
117
|
+
openchamber: {
|
|
118
|
+
assistant: { assistantID: 'assistant_1', name: 'A' },
|
|
119
|
+
assigned: { from: 'contact' },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const hiddenBinding = {
|
|
124
|
+
...session('ses_binding', 98),
|
|
125
|
+
metadata: { openchamber: { assistant: { assistantID: 'assistant_1', name: 'A' } } },
|
|
126
|
+
};
|
|
127
|
+
service.replaceDirectory({
|
|
128
|
+
directory: '/repo',
|
|
129
|
+
sessions: [worker, legacyWorker, hiddenBinding],
|
|
130
|
+
cursor: null,
|
|
131
|
+
hasMore: false,
|
|
132
|
+
});
|
|
133
|
+
expect(service.snapshot().directories[0].sessions.map((item) => item.id)).toEqual([
|
|
134
|
+
'ses_worker',
|
|
135
|
+
'ses_legacy_worker',
|
|
136
|
+
]);
|
|
137
|
+
service.close();
|
|
138
|
+
});
|
|
139
|
+
|
|
103
140
|
it('keeps runtime targets isolated in one Electron database', () => {
|
|
104
141
|
const runtimeRef = { value: 'http://runtime-a.test' };
|
|
105
142
|
const service = createService(runtimeRef);
|
|
@@ -163,6 +163,7 @@ const extractSessionStatus = (payload) => {
|
|
|
163
163
|
export const isSystemOwnedSession = (sessionOrInfo) => {
|
|
164
164
|
const openchamber = sessionOrInfo?.metadata?.openchamber;
|
|
165
165
|
if (!openchamber || typeof openchamber !== 'object') return false;
|
|
166
|
+
if (openchamber.assigned?.from === 'contact') return false;
|
|
166
167
|
if (typeof openchamber.assistant?.assistantID === 'string' && openchamber.assistant.assistantID) return true;
|
|
167
168
|
if (typeof openchamber.scheduledTask?.taskID === 'string' && openchamber.scheduledTask.taskID) return true;
|
|
168
169
|
if (typeof openchamber.smallModel?.purpose === 'string' && openchamber.smallModel.purpose) return true;
|