@openchambery/web 1.19.7-beta.25 → 1.19.7-beta.26
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-DI2zs2WH.js → AgentAvatar-CX0a1rKE.js} +1 -1
- package/dist/assets/AssistantView-BYHD7Fcy.js +2 -0
- package/dist/assets/{DiagramView-DRIEYUNV.js → DiagramView-BUucK4Mn.js} +1 -1
- package/dist/assets/{FadeInOnReveal-BWJcUp5P.js → FadeInOnReveal-CK4asqVR.js} +1 -1
- package/dist/assets/{Icon-4My8YZwt.js → Icon-BYM02QyU.js} +1 -1
- package/dist/assets/{MarkdownLoadingSkeleton-Vezjvk9B.js → MarkdownLoadingSkeleton-DWs4Jg7b.js} +1 -1
- package/dist/assets/{MarkdownRenderer-SLKCbxwI.js → MarkdownRenderer-uiTtRexW.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-D9kcSswH.js → MarkdownRendererImpl-DszSA6px.js} +1 -1
- package/dist/assets/MarkstreamRendererImpl-DkjZ7QYA.js +1 -0
- package/dist/assets/{MobileDetailNavigation-C5lZVW1v.js → MobileDetailNavigation-BElZdvTL.js} +1 -1
- package/dist/assets/MobileShareBridge-DiXOck30.js +4 -0
- package/dist/assets/{MobileSurface-CtMp7rI8.js → MobileSurface-BCIZyDOc.js} +1 -1
- package/dist/assets/{MultiRunWindow-6-E944pd.js → MultiRunWindow-XXWgGd6d.js} +1 -1
- package/dist/assets/{OnboardingScreen-BNEtPmvw.js → OnboardingScreen-C3-wzeSX.js} +1 -1
- package/dist/assets/{OverlayScrollbar-BZB21Yp_.js → OverlayScrollbar-KUpvYHdv.js} +1 -1
- package/dist/assets/{SettingsView-Cy_D8K8y.js → SettingsView-CdYIJh0f.js} +5 -5
- package/dist/assets/{SettingsWindow-BA9QjT8d.js → SettingsWindow-CLr0dXVg.js} +1 -1
- package/dist/assets/{TerminalView-izdQm4WZ.js → TerminalView-BdtasXsi.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CXbfvZ3M.js → ToolOutputDialog-D22RYqMI.js} +1 -1
- package/dist/assets/{appThemeRegistry-Eq3WEH-h.js → appThemeRegistry-D1wTvHoc.js} +1 -1
- package/dist/assets/{button-Or0daT9o.js → button-C0cth9tl.js} +1 -1
- package/dist/assets/{clipboard-BXxLshVq.js → clipboard-DJ4nQEuY.js} +1 -1
- package/dist/assets/{collapsible-C-kzSGkq.js → collapsible-DFT9cE80.js} +1 -1
- package/dist/assets/{context-menu-Dpjla5p4.js → context-menu-CAp74NfV.js} +1 -1
- package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
- package/dist/assets/{desktopRelayRestore-BaKjk1kQ.js → desktopRelayRestore-9EbBYQ9s.js} +1 -1
- package/dist/assets/{diagram-D0mT3mo1.js → diagram-BryzMuta.js} +1 -1
- package/dist/assets/{dialog-BkhHBTd4.js → dialog-CgbV5ljg.js} +1 -1
- package/dist/assets/{fileQueries-DFY6Kiy1.js → fileQueries-BY-9OWzp.js} +1 -1
- package/dist/assets/{fileReferenceActions-Di_DoDex.js → fileReferenceActions-BJJtX_S5.js} +1 -1
- package/dist/assets/{gitApi-SIviXHpZ.js → gitApi-RhwRTt2X.js} +1 -1
- package/dist/assets/{input-BUsGnzwe.js → input-DFLpqjGR.js} +1 -1
- package/dist/assets/{insertionBoundaries-q72MeGLB.js → insertionBoundaries-D-UUqego.js} +1 -1
- package/dist/assets/{main-Ct6J19qT.js → main-BRB8Kly8.js} +2 -2
- package/dist/assets/{main-VXSSCHDb.js → main-BaSeqhTm.js} +11 -11
- package/dist/assets/{markdownTheme-CVS01wHf.js → markdownTheme-CzZm3riB.js} +1 -1
- package/dist/assets/{miniChat-D_SEWy1a.js → miniChat-CDZ48aEf.js} +2 -2
- package/dist/assets/{mobile-Cx032aiN.js → mobile-B2Hpp_5U.js} +2 -2
- package/dist/assets/{multirun-BKGQQeek.js → multirun-CfvHH_3s.js} +1 -1
- package/dist/assets/{outsideFileGrants-BB4GPvCq.js → outsideFileGrants-DnplRxr-.js} +1 -1
- package/dist/assets/persistence-DdUac14I.js +3 -0
- package/dist/assets/platform-B4KKLnZ5.js +2 -0
- package/dist/assets/{projectMeta-yRCsEkJK.js → projectMeta-vCCip4-G.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BnDRaSFx.js → renderElectronMiniChatApp-D9RarEOS.js} +2 -2
- package/dist/assets/{renderMobileApp-BAGhZKMW.js → renderMobileApp-DOiQLCcY.js} +7 -7
- package/dist/assets/{runtimeConfig-D5H05aTj.js → runtimeConfig-CkWKMmAR.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BwSh51Vc.js → runtimeEndpointReset-aW8qogl0.js} +1 -1
- package/dist/assets/{sessionChangeSummary-CX2MJIkc.js → sessionChangeSummary-DOq8EN_H.js} +1 -1
- package/dist/assets/{sessionLookup-CSkeoKrV.js → sessionLookup-BNO4Ta46.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-C5gGnikj.js → sortable-tabs-strip-C8_9kHqu.js} +1 -1
- package/dist/assets/{terminalApi-CIIs9vFx.js → terminalApi-947l7Ra3.js} +1 -1
- package/dist/assets/{textarea-CIO34TZz.js → textarea-Cgo_nWUU.js} +1 -1
- package/dist/assets/{tooltip-BSrJYYYc.js → tooltip-BifLJ8o0.js} +1 -1
- package/dist/assets/{url-CE_gAfNa.js → url-Pk6tQ1EZ.js} +1 -1
- package/dist/assets/{useAppFontEffects-Dh_NwW5O.js → useAppFontEffects-BqBeuVr8.js} +6 -6
- package/dist/assets/{useAssistantUIStore-BHacrxe_.js → useAssistantUIStore-me6s63b2.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-CV96SEmA.js → useDesktopWindowControlsLayout-CnX7JJa1.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-XSKpr7Fy.js → useEffectiveDirectory-F2fNcQ22.js} +1 -1
- package/dist/assets/{useFontPreferences-BxOHKuVw.js → useFontPreferences-BCOBlaAd.js} +1 -1
- package/dist/assets/useMobileNavigationStore-CBfSvE2P.js +413 -0
- package/dist/assets/{useSessionAutoCleanup-Da5ILm3s.js → useSessionAutoCleanup-BVJKWCtK.js} +1 -1
- package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-dC60TMnO.js → useWorkerHighlightedLines-WYqpkPyP.js} +1 -1
- package/dist/assets/{utils-B0mQXN9L.js → utils-BiK3tBGM.js} +1 -1
- package/dist/index.html +8 -8
- package/dist/mini-chat.html +8 -8
- package/dist/mobile.html +8 -8
- package/package.json +1 -1
- package/server/lib/assistants/contact-tools.test.js +6 -6
- package/server/lib/assistants/harness.test.js +3 -3
- package/server/lib/assistants/service.test.js +1 -1
- 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/opencode/DOCUMENTATION.md +2 -1
- package/server/lib/opencode/proxy.js +103 -9
- package/server/lib/opencode/proxy.test.js +177 -1
- package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -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/AssistantView-BdZ8qKY7.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-DnjoP9Et.js +0 -1
- package/dist/assets/MobileShareBridge-BeYYag96.js +0 -4
- package/dist/assets/desktopHosts-BzSE0sK7.js +0 -1
- package/dist/assets/persistence-CSZp1CNB.js +0 -3
- package/dist/assets/platform-CenlWqxu.js +0 -2
- package/dist/assets/useMobileNavigationStore-BrQwx0Jz.js +0 -412
- package/dist/assets/useUIStore-CqqO11ZI.js +0 -1
|
@@ -22,9 +22,13 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
22
22
|
}) {
|
|
23
23
|
const clients = new Set();
|
|
24
24
|
const clientLastEventIds = new Map();
|
|
25
|
+
const clientReasoningFilters = new Map();
|
|
25
26
|
const readyClients = new Set();
|
|
26
27
|
|
|
27
28
|
const removeClient = (socket) => {
|
|
29
|
+
const filter = clientReasoningFilters.get(socket);
|
|
30
|
+
filter?.dispose?.();
|
|
31
|
+
clientReasoningFilters.delete(socket);
|
|
28
32
|
clients.delete(socket);
|
|
29
33
|
clientLastEventIds.delete(socket);
|
|
30
34
|
readyClients.delete(socket);
|
|
@@ -32,10 +36,12 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
const replayEvents = (socket, requestedLastEventId) => {
|
|
39
|
+
const reasoningFilter = clientReasoningFilters.get(socket);
|
|
35
40
|
for (const entry of globalHub.replayAfter(requestedLastEventId)) {
|
|
36
41
|
const sent = sendMessageStreamWsEvent(socket, entry.payload, {
|
|
37
42
|
directory: entry.directory,
|
|
38
43
|
eventId: entry.eventId,
|
|
44
|
+
reasoningFilter,
|
|
39
45
|
});
|
|
40
46
|
if (!sent) {
|
|
41
47
|
removeClient(socket);
|
|
@@ -104,6 +110,7 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
104
110
|
const sent = sendMessageStreamWsEvent(socket, payload, {
|
|
105
111
|
directory,
|
|
106
112
|
eventId,
|
|
113
|
+
reasoningFilter: clientReasoningFilters.get(socket),
|
|
107
114
|
});
|
|
108
115
|
if (!sent) {
|
|
109
116
|
removeClient(socket);
|
|
@@ -115,7 +122,10 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
115
122
|
if (!readyClients.has(socket)) {
|
|
116
123
|
continue;
|
|
117
124
|
}
|
|
118
|
-
const sent = sendMessageStreamWsEvent(socket, syntheticPayload, {
|
|
125
|
+
const sent = sendMessageStreamWsEvent(socket, syntheticPayload, {
|
|
126
|
+
directory: 'global',
|
|
127
|
+
reasoningFilter: clientReasoningFilters.get(socket),
|
|
128
|
+
});
|
|
119
129
|
if (!sent) {
|
|
120
130
|
removeClient(socket);
|
|
121
131
|
}
|
|
@@ -168,7 +178,7 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
168
178
|
}
|
|
169
179
|
});
|
|
170
180
|
|
|
171
|
-
const accept = (socket, { requestedLastEventId = '' } = {}) => {
|
|
181
|
+
const accept = (socket, { requestedLastEventId = '', reasoningFilter = null } = {}) => {
|
|
172
182
|
const pingInterval = setInterval(() => {
|
|
173
183
|
if (socket.readyState !== 1) {
|
|
174
184
|
return;
|
|
@@ -185,7 +195,10 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
185
195
|
return;
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, {
|
|
198
|
+
sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, {
|
|
199
|
+
directory: 'global',
|
|
200
|
+
reasoningFilter: clientReasoningFilters.get(socket),
|
|
201
|
+
});
|
|
189
202
|
}, heartbeatIntervalMs);
|
|
190
203
|
|
|
191
204
|
socket.on('close', () => {
|
|
@@ -201,6 +214,9 @@ export function createGlobalMessageStreamWsBridge({
|
|
|
201
214
|
|
|
202
215
|
clients.add(socket);
|
|
203
216
|
clientLastEventIds.set(socket, requestedLastEventId);
|
|
217
|
+
if (reasoningFilter) {
|
|
218
|
+
clientReasoningFilters.set(socket, reasoningFilter);
|
|
219
|
+
}
|
|
204
220
|
globalHub.start();
|
|
205
221
|
if (globalHub.isConnected()) {
|
|
206
222
|
markReady(socket, requestedLastEventId);
|
|
@@ -126,9 +126,19 @@ export function sendMessageStreamWsFrame(socket, payload) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
export function sendMessageStreamWsEvent(socket, payload, options = {}) {
|
|
129
|
+
// Optional per-connection reasoning projector (includeReasoning=false).
|
|
130
|
+
// null from projectEvent means drop this frame entirely.
|
|
131
|
+
let candidate = payload;
|
|
132
|
+
if (options.reasoningFilter && typeof options.reasoningFilter.projectEvent === 'function') {
|
|
133
|
+
candidate = options.reasoningFilter.projectEvent(payload);
|
|
134
|
+
if (candidate == null) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
// Summarize FileDiff bodies before fan-out so Relay/WS never carries full
|
|
130
140
|
// patch frames. Identity is preserved for non-diff and already-summary events.
|
|
131
|
-
const outbound = summarizeOutboundEventPayload(
|
|
141
|
+
const outbound = summarizeOutboundEventPayload(candidate);
|
|
132
142
|
return sendMessageStreamWsFrame(socket, {
|
|
133
143
|
type: 'event',
|
|
134
144
|
payload: outbound,
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenChamber outbound reasoning projection.
|
|
3
|
+
*
|
|
4
|
+
* `includeReasoning=false` (strict string only) strips reasoning from Host→client
|
|
5
|
+
* HTTP/SSE/WS payloads. Missing/other values keep current behavior. OpenChamber
|
|
6
|
+
* projection only — never forward the param to OpenCode. Hub/upstream stay full.
|
|
7
|
+
*
|
|
8
|
+
* Shared by HTTP snapshots and stateful stream filters:
|
|
9
|
+
* drop type=reasoning parts, their deltas, session.next.reasoning.* events.
|
|
10
|
+
* Keep tokens.reasoning totals, tools, text, status, permissions/errors.
|
|
11
|
+
* Never mutate hub/replay objects.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const REASONING_DELTA_FIELDS = new Set([
|
|
15
|
+
'reasoning',
|
|
16
|
+
'reasoning_content',
|
|
17
|
+
'reasoning_details',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/** Match UI opencode-event-normalizer: strip terminal `.N` version suffix for dispatch. */
|
|
21
|
+
const VERSIONED_TYPE = /^(.*)\.(\d+)$/;
|
|
22
|
+
|
|
23
|
+
const PART_TYPE_CACHE_LIMIT = 8192;
|
|
24
|
+
|
|
25
|
+
const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
|
|
27
|
+
/** @param {string} type */
|
|
28
|
+
const baseEventType = (type) => {
|
|
29
|
+
const match = VERSIONED_TYPE.exec(type);
|
|
30
|
+
return match?.[1] ?? type;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strict: only string `'false'` disables. Missing / other keep include.
|
|
35
|
+
* @param {unknown} value
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
export function shouldIncludeReasoning(value) {
|
|
39
|
+
if (Array.isArray(value) && value.length > 0) return value[0] !== 'false';
|
|
40
|
+
return value !== 'false';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {unknown} query */
|
|
44
|
+
export function readIncludeReasoningQuery(query) {
|
|
45
|
+
if (!query || typeof query !== 'object') return true;
|
|
46
|
+
return shouldIncludeReasoning(/** @type {Record<string, unknown>} */ (query).includeReasoning);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} rawUrl */
|
|
50
|
+
export function readIncludeReasoningFromUrl(rawUrl) {
|
|
51
|
+
if (typeof rawUrl !== 'string' || !rawUrl.includes('includeReasoning=')) return true;
|
|
52
|
+
try {
|
|
53
|
+
return shouldIncludeReasoning(new URL(rawUrl, 'http://openchamber.local').searchParams.get('includeReasoning'));
|
|
54
|
+
} catch {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Strip OpenChamber-only `includeReasoning` before OpenCode upstream.
|
|
61
|
+
* @param {string} requestUrl
|
|
62
|
+
*/
|
|
63
|
+
export function stripIncludeReasoningParam(requestUrl) {
|
|
64
|
+
if (typeof requestUrl !== 'string' || !requestUrl.includes('includeReasoning=')) return requestUrl;
|
|
65
|
+
try {
|
|
66
|
+
const url = new URL(requestUrl, 'http://openchamber.local');
|
|
67
|
+
if (!url.searchParams.has('includeReasoning')) return requestUrl;
|
|
68
|
+
url.searchParams.delete('includeReasoning');
|
|
69
|
+
const search = url.searchParams.toString();
|
|
70
|
+
if (requestUrl.startsWith('http://') || requestUrl.startsWith('https://')) {
|
|
71
|
+
return `${url.origin}${url.pathname}${search ? `?${search}` : ''}${url.hash || ''}`;
|
|
72
|
+
}
|
|
73
|
+
return `${url.pathname}${search ? `?${search}` : ''}${url.hash || ''}`;
|
|
74
|
+
} catch {
|
|
75
|
+
return requestUrl;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {unknown} parts */
|
|
80
|
+
function projectParts(parts) {
|
|
81
|
+
if (!Array.isArray(parts) || parts.length === 0) return parts;
|
|
82
|
+
let changed = false;
|
|
83
|
+
const next = [];
|
|
84
|
+
for (const part of parts) {
|
|
85
|
+
if (isPlainObject(part) && part.type === 'reasoning') {
|
|
86
|
+
changed = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
next.push(part);
|
|
90
|
+
}
|
|
91
|
+
return changed ? next : parts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One message record: `{ info, parts }` (turn-page / exact / cache / session.messages).
|
|
96
|
+
* @param {unknown} record
|
|
97
|
+
*/
|
|
98
|
+
function projectRecord(record) {
|
|
99
|
+
if (!isPlainObject(record) || !Array.isArray(record.parts)) return record;
|
|
100
|
+
const parts = projectParts(record.parts);
|
|
101
|
+
return parts === record.parts ? record : { ...record, parts };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Real Host payload shapes only:
|
|
106
|
+
* - bare array of records
|
|
107
|
+
* - `{ records: [...] }` (turn-page / reconcile / cache session)
|
|
108
|
+
* - `{ record }` (cache message)
|
|
109
|
+
* - `{ info, parts }` (exact message)
|
|
110
|
+
*
|
|
111
|
+
* @param {unknown} payload
|
|
112
|
+
*/
|
|
113
|
+
export function projectMessagesPayloadWithoutReasoning(payload) {
|
|
114
|
+
if (Array.isArray(payload)) {
|
|
115
|
+
let changed = false;
|
|
116
|
+
const next = payload.map((entry) => {
|
|
117
|
+
const projected = projectRecord(entry);
|
|
118
|
+
if (projected !== entry) changed = true;
|
|
119
|
+
return projected;
|
|
120
|
+
});
|
|
121
|
+
return changed ? next : payload;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!isPlainObject(payload)) return payload;
|
|
125
|
+
|
|
126
|
+
let next = payload;
|
|
127
|
+
let changed = false;
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(payload.records)) {
|
|
130
|
+
const records = projectMessagesPayloadWithoutReasoning(payload.records);
|
|
131
|
+
if (records !== payload.records) {
|
|
132
|
+
next = { ...next, records };
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isPlainObject(payload.record)) {
|
|
138
|
+
const record = projectRecord(payload.record);
|
|
139
|
+
if (record !== payload.record) {
|
|
140
|
+
next = { ...next, record };
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Exact message / single record body: top-level parts.
|
|
146
|
+
if (Array.isArray(payload.parts)) {
|
|
147
|
+
const projected = projectRecord(next);
|
|
148
|
+
if (projected !== next) {
|
|
149
|
+
next = projected;
|
|
150
|
+
changed = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return changed ? next : payload;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {unknown} payload
|
|
159
|
+
* @param {boolean} includeReasoning
|
|
160
|
+
*/
|
|
161
|
+
export function projectMessagesPayloadForReasoning(payload, includeReasoning) {
|
|
162
|
+
if (includeReasoning) return payload;
|
|
163
|
+
return projectMessagesPayloadWithoutReasoning(payload);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const eventBodyKeys = /** @type {const} */ (['properties', 'data']);
|
|
167
|
+
|
|
168
|
+
/** @param {unknown} payload */
|
|
169
|
+
function eventBodyOf(payload) {
|
|
170
|
+
if (!isPlainObject(payload)) return null;
|
|
171
|
+
for (const key of eventBodyKeys) {
|
|
172
|
+
const body = payload[key];
|
|
173
|
+
if (isPlainObject(body)) return body;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Stateful per-connection outbound filter. Classification map is bounded.
|
|
180
|
+
* @returns {{ projectEvent: (payload: unknown) => unknown | null, knownPartCount: () => number, dispose: () => void }}
|
|
181
|
+
*/
|
|
182
|
+
export function createReasoningOutboundFilter() {
|
|
183
|
+
/** @type {Map<string, string>} */
|
|
184
|
+
const partTypes = new Map();
|
|
185
|
+
|
|
186
|
+
const partKey = (messageID, partID) => `${messageID}\0${partID}`;
|
|
187
|
+
|
|
188
|
+
const rememberPartType = (messageID, partID, type) => {
|
|
189
|
+
if (typeof messageID !== 'string' || !messageID) return;
|
|
190
|
+
if (typeof partID !== 'string' || !partID) return;
|
|
191
|
+
if (typeof type !== 'string' || !type) return;
|
|
192
|
+
const key = partKey(messageID, partID);
|
|
193
|
+
if (partTypes.size >= PART_TYPE_CACHE_LIMIT && !partTypes.has(key)) {
|
|
194
|
+
const oldest = partTypes.keys().next().value;
|
|
195
|
+
if (oldest !== undefined) partTypes.delete(oldest);
|
|
196
|
+
}
|
|
197
|
+
partTypes.set(key, type);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const forgetPart = (messageID, partID) => {
|
|
201
|
+
if (typeof messageID === 'string' && typeof partID === 'string') {
|
|
202
|
+
partTypes.delete(partKey(messageID, partID));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const forgetMessage = (messageID) => {
|
|
207
|
+
if (typeof messageID !== 'string' || !messageID) return;
|
|
208
|
+
const prefix = `${messageID}\0`;
|
|
209
|
+
for (const key of Array.from(partTypes.keys())) {
|
|
210
|
+
if (key.startsWith(prefix)) partTypes.delete(key);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {unknown} payload
|
|
216
|
+
* @returns {unknown | null} null = drop; otherwise original or shallow-cloned payload (type string preserved)
|
|
217
|
+
*/
|
|
218
|
+
const projectEvent = (payload) => {
|
|
219
|
+
if (!isPlainObject(payload)) return payload;
|
|
220
|
+
|
|
221
|
+
const rawType = typeof payload.type === 'string' ? payload.type : '';
|
|
222
|
+
const type = baseEventType(rawType);
|
|
223
|
+
|
|
224
|
+
// session.next.reasoning.* (incl. versioned …reasoning.delta.1)
|
|
225
|
+
if (type.startsWith('session.next.reasoning.')) return null;
|
|
226
|
+
|
|
227
|
+
if (type === 'message.part.updated') {
|
|
228
|
+
const body = eventBodyOf(payload);
|
|
229
|
+
const part = body && isPlainObject(body.part) ? body.part : null;
|
|
230
|
+
if (part) {
|
|
231
|
+
const messageID =
|
|
232
|
+
(typeof part.messageID === 'string' && part.messageID)
|
|
233
|
+
|| (typeof body.messageID === 'string' && body.messageID)
|
|
234
|
+
|| '';
|
|
235
|
+
const partID = typeof part.id === 'string' ? part.id : '';
|
|
236
|
+
if (typeof part.type === 'string') rememberPartType(messageID, partID, part.type);
|
|
237
|
+
if (part.type === 'reasoning') return null;
|
|
238
|
+
}
|
|
239
|
+
return payload;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (type === 'message.part.delta') {
|
|
243
|
+
const body = eventBodyOf(payload);
|
|
244
|
+
if (!body) return null;
|
|
245
|
+
|
|
246
|
+
const messageID = typeof body.messageID === 'string' ? body.messageID : '';
|
|
247
|
+
const partID = typeof body.partID === 'string' ? body.partID : '';
|
|
248
|
+
const field = typeof body.field === 'string' ? body.field : '';
|
|
249
|
+
|
|
250
|
+
if (REASONING_DELTA_FIELDS.has(field)) return null;
|
|
251
|
+
|
|
252
|
+
const learned = messageID && partID ? partTypes.get(partKey(messageID, partID)) : undefined;
|
|
253
|
+
if (learned === 'reasoning' || learned === undefined) return null;
|
|
254
|
+
return payload;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (type === 'message.part.removed') {
|
|
258
|
+
const body = eventBodyOf(payload);
|
|
259
|
+
if (body) {
|
|
260
|
+
forgetPart(
|
|
261
|
+
typeof body.messageID === 'string' ? body.messageID : '',
|
|
262
|
+
typeof body.partID === 'string' ? body.partID : '',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return payload;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (type === 'message.removed') {
|
|
269
|
+
const body = eventBodyOf(payload);
|
|
270
|
+
if (body) {
|
|
271
|
+
const messageID =
|
|
272
|
+
(typeof body.messageID === 'string' && body.messageID)
|
|
273
|
+
|| (isPlainObject(body.info) && typeof body.info.id === 'string' && body.info.id)
|
|
274
|
+
|| '';
|
|
275
|
+
forgetMessage(messageID);
|
|
276
|
+
}
|
|
277
|
+
return payload;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// message.updated / message.created may embed parts on properties or data.
|
|
281
|
+
if (type === 'message.updated' || type === 'message.created') {
|
|
282
|
+
let next = payload;
|
|
283
|
+
let changed = false;
|
|
284
|
+
for (const key of eventBodyKeys) {
|
|
285
|
+
const body = next[key];
|
|
286
|
+
if (!isPlainObject(body) || !Array.isArray(body.parts)) continue;
|
|
287
|
+
const parts = projectParts(body.parts);
|
|
288
|
+
if (parts === body.parts) continue;
|
|
289
|
+
next = { ...next, [key]: { ...body, parts } };
|
|
290
|
+
changed = true;
|
|
291
|
+
}
|
|
292
|
+
return changed ? next : payload;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return payload;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
projectEvent,
|
|
300
|
+
knownPartCount: () => partTypes.size,
|
|
301
|
+
dispose: () => {
|
|
302
|
+
partTypes.clear();
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Project SSE data JSON; preserve GlobalEvent `{ directory, payload }` wrap.
|
|
309
|
+
* @param {unknown} dataJson
|
|
310
|
+
* @param {{ projectEvent: (payload: unknown) => unknown | null }} filter
|
|
311
|
+
*/
|
|
312
|
+
function projectSseDataJson(dataJson, filter) {
|
|
313
|
+
if (!isPlainObject(dataJson)) {
|
|
314
|
+
return { action: 'keep', value: dataJson, changed: false };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isPlainObject(dataJson.payload) && typeof dataJson.payload.type === 'string') {
|
|
318
|
+
const projected = filter.projectEvent(dataJson.payload);
|
|
319
|
+
if (projected == null) return { action: 'drop' };
|
|
320
|
+
if (projected === dataJson.payload) return { action: 'keep', value: dataJson, changed: false };
|
|
321
|
+
return { action: 'keep', value: { ...dataJson, payload: projected }, changed: true };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const projected = filter.projectEvent(dataJson);
|
|
325
|
+
if (projected == null) return { action: 'drop' };
|
|
326
|
+
if (projected === dataJson) return { action: 'keep', value: dataJson, changed: false };
|
|
327
|
+
return { action: 'keep', value: projected, changed: true };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Filter one SSE block (no trailing separator). null = drop.
|
|
332
|
+
* @param {string} block
|
|
333
|
+
* @param {{ projectEvent: (payload: unknown) => unknown | null }} filter
|
|
334
|
+
* @returns {string | null}
|
|
335
|
+
*/
|
|
336
|
+
export function filterSseBlock(block, filter) {
|
|
337
|
+
if (typeof block !== 'string' || block.trim().length === 0) return null;
|
|
338
|
+
if (!block.includes('data:')) return `${block}\n\n`;
|
|
339
|
+
|
|
340
|
+
const lines = block.split('\n');
|
|
341
|
+
const dataLines = [];
|
|
342
|
+
const otherLines = [];
|
|
343
|
+
for (const line of lines) {
|
|
344
|
+
if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^\s/, ''));
|
|
345
|
+
else otherLines.push(line);
|
|
346
|
+
}
|
|
347
|
+
if (dataLines.length === 0) return `${block}\n\n`;
|
|
348
|
+
|
|
349
|
+
const payloadText = dataLines.join('\n').trim();
|
|
350
|
+
if (!payloadText) return `${block}\n\n`;
|
|
351
|
+
|
|
352
|
+
let parsed;
|
|
353
|
+
try {
|
|
354
|
+
parsed = JSON.parse(payloadText);
|
|
355
|
+
} catch {
|
|
356
|
+
return `${block}\n\n`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const result = projectSseDataJson(parsed, filter);
|
|
360
|
+
if (result.action === 'drop') return null;
|
|
361
|
+
if (!result.changed) return `${block}\n\n`;
|
|
362
|
+
return [...otherLines, `data: ${JSON.stringify(result.value)}`, '', ''].join('\n');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Incremental SSE block splitter. Holds a trailing CR so `\r` + `\n` across
|
|
367
|
+
* chunks never invents a false blank line (which would split one event in two).
|
|
368
|
+
*/
|
|
369
|
+
export function createSseBlockSplitter() {
|
|
370
|
+
const decoder = new TextDecoder();
|
|
371
|
+
let buffer = '';
|
|
372
|
+
/** @type {string} trailing CR held until the next chunk clarifies `\r\n` vs lone `\r` */
|
|
373
|
+
let carryCr = '';
|
|
374
|
+
|
|
375
|
+
const appendNormalized = (text) => {
|
|
376
|
+
if (!text && !carryCr) return;
|
|
377
|
+
let input = carryCr + (text || '');
|
|
378
|
+
carryCr = '';
|
|
379
|
+
if (input.endsWith('\r')) {
|
|
380
|
+
carryCr = '\r';
|
|
381
|
+
input = input.slice(0, -1);
|
|
382
|
+
}
|
|
383
|
+
// Complete CRLF pairs first, then any remaining lone CR → LF.
|
|
384
|
+
buffer += input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const drainBlocks = () => {
|
|
388
|
+
const blocks = [];
|
|
389
|
+
while (true) {
|
|
390
|
+
const idx = buffer.indexOf('\n\n');
|
|
391
|
+
if (idx === -1) break;
|
|
392
|
+
const block = buffer.slice(0, idx);
|
|
393
|
+
buffer = buffer.slice(idx + 2);
|
|
394
|
+
if (block.trim().length > 0) blocks.push(block);
|
|
395
|
+
}
|
|
396
|
+
return blocks;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
/**
|
|
401
|
+
* @param {Uint8Array | string} chunk
|
|
402
|
+
* @returns {string[]}
|
|
403
|
+
*/
|
|
404
|
+
push(chunk) {
|
|
405
|
+
const text = typeof chunk === 'string'
|
|
406
|
+
? chunk
|
|
407
|
+
: decoder.decode(chunk, { stream: true });
|
|
408
|
+
if (!text) return [];
|
|
409
|
+
appendNormalized(text);
|
|
410
|
+
return drainBlocks();
|
|
411
|
+
},
|
|
412
|
+
/** @returns {string[]} */
|
|
413
|
+
finish() {
|
|
414
|
+
const tail = decoder.decode();
|
|
415
|
+
if (tail) appendNormalized(tail);
|
|
416
|
+
if (carryCr) {
|
|
417
|
+
buffer += '\n';
|
|
418
|
+
carryCr = '';
|
|
419
|
+
}
|
|
420
|
+
const remaining = buffer;
|
|
421
|
+
buffer = '';
|
|
422
|
+
return remaining.trim().length > 0 ? [remaining] : [];
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|