@openchambery/web 1.18.2-beta.3 → 1.18.2-beta.31
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-B2kZjgaf.js → AgentAvatar-SFzXgOdv.js} +1 -1
- package/dist/assets/AssistantView-BIn8F2Hj.js +1 -0
- package/dist/assets/{ChatContainer-Dg1fcQRH.js → ChatContainer-Ddon7i5x.js} +39 -39
- package/dist/assets/{DiagramView-Cl5iwEHD.js → DiagramView-BgqIMaEm.js} +1 -1
- package/dist/assets/{FadeInOnReveal-aeWlaqVB.js → FadeInOnReveal-9uJN0g-H.js} +1 -1
- package/dist/assets/{Icon-AXqa-fYB.js → Icon-CBdklpKH.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-N_HaSj0n.js → MarkdownRendererImpl-BxiDhVH9.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-JFidUnW2.js → MobileDetailNavigation-BsQmEqPO.js} +1 -1
- package/dist/assets/MobileShareBridge-CUwGZNSo.js +4 -0
- package/dist/assets/MobileSurface-DJAZpIrG.js +2 -0
- package/dist/assets/{MultiRunWindow-CFVe1qvV.js → MultiRunWindow-CTWluFXJ.js} +1 -1
- package/dist/assets/{OnboardingScreen-DiPnIwza.js → OnboardingScreen-CC4fqs0M.js} +1 -1
- package/dist/assets/OverlayScrollbar-C3WqFk2J.js +2 -0
- package/dist/assets/SettingsView-DpnDXF2_.js +26 -0
- package/dist/assets/{SettingsWindow-yYmo-0Ic.js → SettingsWindow-Dvi882vA.js} +1 -1
- package/dist/assets/{TerminalView-BNxPKANo.js → TerminalView-xrLSN4j7.js} +1 -1
- package/dist/assets/{ToolOutputDialog-COiOtTRX.js → ToolOutputDialog-C9bzo8sK.js} +1 -1
- package/dist/assets/appThemeRegistry-BN9k7vVa.js +2 -0
- package/dist/assets/assistant-guide-hero-wide-DiFIThB0.jpg +0 -0
- package/dist/assets/{button-CDvMVaiO.js → button-DimCg1lF.js} +1 -1
- package/dist/assets/capgoAdapter-LvGrONDp.js +1 -0
- package/dist/assets/{clipboard-CbqhDr1g.js → clipboard-B5V61jlQ.js} +1 -1
- package/dist/assets/{collapsible-C2aamDvD.js → collapsible-CF7TDu-T.js} +1 -1
- package/dist/assets/{context-menu-CKua77Jz.js → context-menu-BbwapTNT.js} +1 -1
- package/dist/assets/{desktopHosts-CBoBpJ1Z.js → desktopHosts-Tqz56Y0u.js} +1 -1
- package/dist/assets/{desktopRelayRestore-l2R9muux.js → desktopRelayRestore-DgHC01I5.js} +1 -1
- package/dist/assets/{diagram-CEon0BQs.js → diagram-DObb9lfm.js} +1 -1
- package/dist/assets/{dialog--cf4dygO.js → dialog-CnekXdCq.js} +1 -1
- package/dist/assets/{es-DCoGZljP.js → es-ZHyqPb3M.js} +2 -2
- package/dist/assets/{fileQueries-CE_DaZWQ.js → fileQueries-BjL2dHDF.js} +1 -1
- package/dist/assets/{fr-dkEoZpPz.js → fr-nXAAl4qu.js} +3 -3
- package/dist/assets/gitApi-k7AZQbpz.js +7 -0
- package/dist/assets/{input-k9DGGyZ6.js → input-BNj7D6h_.js} +1 -1
- package/dist/assets/insertionBoundaries-7W_1e-d-.js +1 -0
- package/dist/assets/{ja-bQwSsvRr.js → ja-CLcxQZ5K.js} +2 -2
- package/dist/assets/{ko-Kq5C3hsv.js → ko-KgjBt0nP.js} +2 -2
- package/dist/assets/main-BxsNKnDp.js +172 -0
- package/dist/assets/main-Ci6PEc-J.js +2 -0
- package/dist/assets/{markdown-worker-DkeWiER6.js → markdown-worker-CJ8ooXEK.js} +1 -1
- package/dist/assets/{markdownTheme-DNT93i5L.js → markdownTheme-CziN3c9i.js} +1 -1
- package/dist/assets/miniChat-B5GJJFgo.js +2 -0
- package/dist/assets/mobile-D1NLpIYA.js +2 -0
- package/dist/assets/multirun-DGbjKRAa.js +4 -0
- package/dist/assets/{persistence-DPNMIbF5.js → persistence-ZSrLIR97.js} +1 -1
- package/dist/assets/{pl-CYpMAgVn.js → pl-ZJkD7N-8.js} +1 -1
- package/dist/assets/platform-BiSk9jV3.js +2 -0
- package/dist/assets/projectMeta-Bqs1hCSs.js +3 -0
- package/dist/assets/{pt-BR-MSPTLfTm.js → pt-BR-B6nleSMM.js} +2 -2
- package/dist/assets/radio-Bvd_NJMe.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-DM3gwCyN.js +2 -0
- package/dist/assets/renderMobileApp-Cxrys3-9.js +23 -0
- package/dist/assets/runtimeConfig-pPnLM80M.js +2 -0
- package/dist/assets/runtimeEndpointReset-AnjZIaRp.js +145 -0
- package/dist/assets/{sortable-tabs-strip-BJcpkbMH.js → sortable-tabs-strip-C82FJb25.js} +1 -1
- package/dist/assets/src-Da2IGcZi.css +2 -0
- package/dist/assets/{store-OURVnLSN.js → store-BDxgw-Hx.js} +3 -3
- package/dist/assets/terminalApi-CkknnXhV.js +1 -0
- package/dist/assets/{tooltip-DMNR5rN0.js → tooltip-BE6WDmtb.js} +1 -1
- package/dist/assets/types-BuiqMmh_.js +1 -0
- package/dist/assets/{uk-cSkRAYFY.js → uk-KTDMzdyl.js} +2 -2
- package/dist/assets/{url-7MqoLs9X.js → url-DQDmlZku.js} +1 -1
- package/dist/assets/useAppFontEffects-BTAoI3qb.js +7 -0
- package/dist/assets/useDesktopWindowControlsLayout-Dn5rwAx1.js +1 -0
- package/dist/assets/useEffectiveDirectory-CfWfXRNf.js +1 -0
- package/dist/assets/{useFontPreferences-CIurCeNv.js → useFontPreferences-ZvBF445N.js} +1 -1
- package/dist/assets/useMobileNavigationStore-nmb4CWpS.js +476 -0
- package/dist/assets/useSessionAutoCleanup-DdhPDfkF.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-BdJWWBeW.js → useWorkerHighlightedLines-hY7VrbXR.js} +1 -1
- package/dist/assets/{utils-Co19EJEL.js → utils-Dh7UZFrA.js} +1 -1
- package/dist/assets/{zh-CN-BKqeT_uz.js → zh-CN-BilLDIh_.js} +2 -2
- package/dist/assets/{zh-TW-BaNjJ8Y4.js → zh-TW-DBDsyRLQ.js} +2 -2
- package/dist/index.html +12 -10
- package/dist/mini-chat.html +10 -8
- package/dist/mobile.html +10 -8
- package/package.json +1 -1
- package/server/index.js +7 -0
- package/server/lib/assistants/DOCUMENTATION.md +2 -2
- package/server/lib/assistants/history-state.js +23 -0
- package/server/lib/assistants/history-state.test.js +57 -0
- package/server/lib/assistants/service.js +44 -15
- package/server/lib/assistants/service.test.js +120 -5
- package/server/lib/config-sync/DOCUMENTATION.md +2 -2
- package/server/lib/config-sync/contract.js +3 -18
- package/server/lib/config-sync/index.js +0 -2
- package/server/lib/config-sync/tar.js +2 -3
- package/server/lib/config-sync/target-id.js +5 -1
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/diff-summary.js +39 -11
- package/server/lib/event-stream/diff-summary.test.js +73 -6
- package/server/lib/event-stream/protocol.test.js +70 -7
- package/server/lib/notifications/DOCUMENTATION.md +2 -2
- package/server/lib/notifications/runtime.js +68 -27
- package/server/lib/notifications/runtime.test.js +176 -0
- package/server/lib/opencode/DOCUMENTATION.md +1 -1
- package/server/lib/opencode/feature-routes-runtime.test.js +10 -1
- package/server/lib/session-index/DOCUMENTATION.md +12 -6
- package/server/lib/session-index/routes.js +34 -0
- package/server/lib/session-index/routes.test.js +76 -0
- package/server/lib/session-index/service.js +53 -4
- package/server/lib/session-index/service.test.js +72 -2
- package/server/lib/session-title/DOCUMENTATION.md +19 -14
- package/server/lib/session-title/runtime.js +8 -8
- package/server/lib/session-title/runtime.test.js +231 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +26 -6
- package/server/lib/session-turn-pages/changes.service.js +156 -0
- package/server/lib/session-turn-pages/changes.service.test.js +188 -0
- package/server/lib/session-turn-pages/reconcile.routes.test.js +52 -0
- package/server/lib/session-turn-pages/reconcile.service.js +3 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +44 -0
- package/server/lib/session-turn-pages/routes.js +273 -12
- package/server/lib/session-turn-pages/routes.test.js +387 -0
- package/server/lib/session-turn-pages/service.js +31 -7
- package/server/lib/session-turn-pages/service.test.js +40 -0
- package/server/lib/small-model/DOCUMENTATION.md +93 -38
- package/server/lib/small-model/catalog.js +10 -2
- package/server/lib/small-model/catalog.test.js +3 -1
- package/server/lib/small-model/index.js +52 -28
- package/server/lib/small-model/opencode-session.js +339 -0
- package/server/lib/small-model/opencode-session.test.js +177 -0
- package/server/lib/small-model/resolve.js +116 -5
- package/server/lib/small-model/resolve.test.js +76 -10
- package/server/lib/small-model/summary-settings.test.js +60 -3
- package/dist/assets/AssistantView-C-4WY4_Y.js +0 -1
- package/dist/assets/MobileShareBridge-Dtye6XbD.js +0 -4
- package/dist/assets/MobileSurface-uh9xGE4W.js +0 -2
- package/dist/assets/OverlayScrollbar-DV6Bf5MB.js +0 -1
- package/dist/assets/SettingsView-BJ0E268r.js +0 -25
- package/dist/assets/appThemeRegistry-DT9JSHvx.js +0 -2
- package/dist/assets/gitApi-B7IetQyZ.js +0 -7
- package/dist/assets/gitApiHttp-D8j1qRH4.js +0 -2
- package/dist/assets/insertionBoundaries-CXhYvvQ1.js +0 -1
- package/dist/assets/main-CLlU-0_3.js +0 -2
- package/dist/assets/main-DStWT6gv.js +0 -172
- package/dist/assets/miniChat-B4wfuAbi.js +0 -2
- package/dist/assets/mobile-C6CUAJNV.js +0 -2
- package/dist/assets/multirun-BvOVsUJY.js +0 -4
- package/dist/assets/projectMeta-COUPYr2M.js +0 -3
- package/dist/assets/radio-ByL02rGC.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-CmmF6m6P.js +0 -2
- package/dist/assets/renderMobileApp-BlkP7KQl.js +0 -23
- package/dist/assets/runtimeConfig-BtWi74Pm.js +0 -1
- package/dist/assets/runtimeEndpointReset-CJfmeHr6.js +0 -145
- package/dist/assets/src-CQN5yJYP.css +0 -2
- package/dist/assets/terminalApi-tB3qLcfa.js +0 -1
- package/dist/assets/useAppFontEffects-BLm1WFVC.js +0 -7
- package/dist/assets/useDesktopWindowControlsLayout-Bdc8u8ax.js +0 -1
- package/dist/assets/useEffectiveDirectory-By_ohSx-.js +0 -1
- package/dist/assets/useMobileNavigationStore-BQOQWNUY.js +0 -475
- package/dist/assets/useSessionAutoCleanup-BmPmMhJN.js +0 -1
|
@@ -24,9 +24,9 @@ import {
|
|
|
24
24
|
* remoteAuthFileExists: boolean,
|
|
25
25
|
* }} SyncProbeResult
|
|
26
26
|
*
|
|
27
|
-
* Payload for putTar.
|
|
28
|
-
*
|
|
29
|
-
* @typedef {Buffer | Uint8Array
|
|
27
|
+
* Payload for putTar. Callers always hand a fully buffered archive
|
|
28
|
+
* (SSH stdin and direct/relay HTTP bodies alike).
|
|
29
|
+
* @typedef {Buffer | Uint8Array} SyncTarPayload
|
|
30
30
|
*
|
|
31
31
|
* @typedef {{
|
|
32
32
|
* probe: (plan: object) => Promise<SyncProbeResult>,
|
|
@@ -64,21 +64,6 @@ export const RELAY_HOST_SYNC_CAPABILITIES = Object.freeze({
|
|
|
64
64
|
ocHttp: true,
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
export const RELAY_IDENTITY_CHANGED_CODE = 'relay_identity_changed';
|
|
68
|
-
|
|
69
|
-
export class RelayIdentityChangedError extends Error {
|
|
70
|
-
/**
|
|
71
|
-
* @param {{ expectedServerId: string, actualServerId?: string }} details
|
|
72
|
-
*/
|
|
73
|
-
constructor(details) {
|
|
74
|
-
super(`Relay identity changed (expected ${details.expectedServerId}, got ${details.actualServerId || 'unknown'})`);
|
|
75
|
-
this.name = 'RelayIdentityChangedError';
|
|
76
|
-
this.code = RELAY_IDENTITY_CHANGED_CODE;
|
|
77
|
-
this.expectedServerId = details.expectedServerId;
|
|
78
|
-
this.actualServerId = details.actualServerId;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
67
|
/**
|
|
83
68
|
* Build a namespaced SyncTarget for an SSH instance.
|
|
84
69
|
* Managed remotes expose posixShell/tarExtract/authFileWrite; others expose none
|
|
@@ -3,9 +3,8 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
/**
|
|
4
4
|
* Collect stdout from a local `tar …` into a Buffer.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* so a later relay transport can stream without changing the executor signature.
|
|
6
|
+
* Callers buffer the entire archive in memory; TargetExecutor.putTar accepts
|
|
7
|
+
* Buffer | Uint8Array only.
|
|
9
8
|
*
|
|
10
9
|
* @param {string[]} tarArgs
|
|
11
10
|
* @param {{ windowsHide?: boolean }} [spawnOptions]
|
|
@@ -12,7 +12,11 @@ export const syncTargetIdForDirectHost = (hostId) => {
|
|
|
12
12
|
return `host:${id}`;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} serverId relay host signing-key fingerprint
|
|
17
|
+
* Mirrored in packages/ui/src/lib/relay/relay-config-sync.ts (browser bundle cannot
|
|
18
|
+
* import this server module); keep both sides in sync when changing the prefix.
|
|
19
|
+
*/
|
|
16
20
|
export const syncTargetIdForRelayServer = (serverId) => {
|
|
17
21
|
const id = String(serverId || '').trim();
|
|
18
22
|
if (!id) throw new Error('Relay serverId is required');
|
|
@@ -9,7 +9,7 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
|
|
|
9
9
|
- `packages/web/server/lib/event-stream/global-ws-bridge.js`: browser-facing global WS bridge that subscribes clients to the shared global hub.
|
|
10
10
|
- `packages/web/server/lib/event-stream/directory-ws-bridge.js`: browser-facing per-directory WS bridge that owns one scoped upstream reader per connection.
|
|
11
11
|
- `packages/web/server/lib/event-stream/protocol.js`: path constants, SSE envelope parsing, and WebSocket frame serialization helpers.
|
|
12
|
-
- `packages/web/server/lib/event-stream/diff-summary.js`: pure outbound FileDiff
|
|
12
|
+
- `packages/web/server/lib/event-stream/diff-summary.js`: pure outbound FileDiff helpers. L1 message/session `summary.diffs` keep a slim file list `{ file, status?, additions, deletions }` plus additive `diffCount`/`hasDiffs` (never patch/before/after/from/to). `session.diff` event frames still keep file/status/additions/deletions and drop patch bodies. `summarizeFileDiff(s)` remains for explicit L2-style file lists.
|
|
13
13
|
- `packages/web/server/lib/event-stream/upstream-reader.js`: reusable upstream SSE reader with event-id tracking, stall recovery, and reconnect handling.
|
|
14
14
|
- `packages/web/server/lib/event-stream/runtime.js`: thin WebSocket server runtime for upgrade handling and path dispatch to the global/directory bridges.
|
|
15
15
|
- `packages/web/server/lib/event-stream/protocol.test.js`: unit tests for protocol helpers.
|
|
@@ -26,7 +26,7 @@ The following APIs are exported by their owning modules. `event-stream/index.js`
|
|
|
26
26
|
- `MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS`: heartbeat interval for browser-facing WS connections.
|
|
27
27
|
- `parseSseEventEnvelope(block)`: parses an SSE block into `{ eventId, directory, payload }`.
|
|
28
28
|
- `sendMessageStreamWsFrame(socket, payload)`: serializes and sends a JSON WS frame.
|
|
29
|
-
- `sendMessageStreamWsEvent(socket, payload, options)`: sends an event frame with optional `eventId` and `directory`. Outbound `session.diff`
|
|
29
|
+
- `sendMessageStreamWsEvent(socket, payload, options)`: sends an event frame with optional `eventId` and `directory`. Outbound `session.diff` keeps file/status/additions/deletions only (no full patch bodies). Message/session `summary.diffs` are L1-projected to a slim file list plus additive `diffCount`/`hasDiffs` (patch bodies never leave the host).
|
|
30
30
|
|
|
31
31
|
### Runtime helpers
|
|
32
32
|
- `createGlobalMessageStreamHub(...)`: creates a shared `/global/event` upstream SSE hub with event/status subscribers and bounded event-id replay.
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Outbound FileDiff summarization for message-stream fan-out.
|
|
2
|
+
// Outbound FileDiff summarization for message-stream fan-out and Host L1.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// L1 (ordinary first-packet / SSE / exact message GET):
|
|
5
|
+
// keep `info.summary.diffs` as a slim file list
|
|
6
|
+
// `{ file, status?, additions, deletions }` plus additive
|
|
7
|
+
// `diffCount` / `hasDiffs` markers. Never ship patch bodies on these paths.
|
|
8
|
+
//
|
|
9
|
+
// Explicit L2/L3 APIs may still use summarizeFileDiff(s) for file-list / patch
|
|
10
|
+
// responses. Pure JS — do not import UI TypeScript from the server package.
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
8
12
|
|
|
9
13
|
/** Lightweight FileDiff fields safe for outbound event frames (preview only). */
|
|
@@ -62,7 +66,15 @@ export function summarizeFileDiffs(diffs) {
|
|
|
62
66
|
return changed ? next : diffs;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
/**
|
|
70
|
+
* L1 projection: keep `summary.diffs` as a slim file-list array and set
|
|
71
|
+
* additive `diffCount` / `hasDiffs` markers. Drops patch/before/after/from/to.
|
|
72
|
+
* Keeps every other lightweight summary field. Identity when unchanged.
|
|
73
|
+
*
|
|
74
|
+
* @param {unknown} owner message info / session info / summary-bearing object
|
|
75
|
+
* @returns {unknown}
|
|
76
|
+
*/
|
|
77
|
+
export const projectMessageSummaryDiffSlim = (owner) => {
|
|
66
78
|
if (!owner || typeof owner !== 'object' || Array.isArray(owner)) {
|
|
67
79
|
return owner;
|
|
68
80
|
}
|
|
@@ -72,12 +84,18 @@ const summarizeSummaryDiffs = (owner) => {
|
|
|
72
84
|
return owner;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
|
-
if (!
|
|
87
|
+
if (!Object.prototype.hasOwnProperty.call(summary, 'diffs')) {
|
|
76
88
|
return owner;
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
const
|
|
80
|
-
|
|
91
|
+
const rawDiffs = summary.diffs;
|
|
92
|
+
const slimDiffs = Array.isArray(rawDiffs) ? summarizeFileDiffs(rawDiffs) : [];
|
|
93
|
+
const diffCount = slimDiffs.length;
|
|
94
|
+
const hasDiffs = diffCount > 0;
|
|
95
|
+
|
|
96
|
+
const diffsUnchanged = slimDiffs === rawDiffs;
|
|
97
|
+
const markersMatch = summary.diffCount === diffCount && summary.hasDiffs === hasDiffs;
|
|
98
|
+
if (diffsUnchanged && markersMatch) {
|
|
81
99
|
return owner;
|
|
82
100
|
}
|
|
83
101
|
|
|
@@ -85,17 +103,27 @@ const summarizeSummaryDiffs = (owner) => {
|
|
|
85
103
|
...owner,
|
|
86
104
|
summary: {
|
|
87
105
|
...summary,
|
|
88
|
-
diffs:
|
|
106
|
+
diffs: slimDiffs,
|
|
107
|
+
diffCount,
|
|
108
|
+
hasDiffs,
|
|
89
109
|
},
|
|
90
110
|
};
|
|
91
111
|
};
|
|
92
112
|
|
|
113
|
+
/** @deprecated Prefer projectMessageSummaryDiffSlim — same L1 slim-list behavior. */
|
|
114
|
+
export const projectMessageSummaryDiffCounts = projectMessageSummaryDiffSlim;
|
|
115
|
+
|
|
116
|
+
/** @deprecated Prefer projectMessageSummaryDiffSlim — L1 keeps slim file list + markers. */
|
|
117
|
+
export const summarizeMessageDiffSnapshots = projectMessageSummaryDiffSlim;
|
|
118
|
+
|
|
93
119
|
/**
|
|
94
120
|
* Summarize FileDiff bodies on outbound event payloads.
|
|
95
121
|
*
|
|
96
122
|
* Handles:
|
|
97
123
|
* - `session.diff` with legacy `properties.diff` or current `data.diff`
|
|
124
|
+
* (keeps file/status/additions/deletions; drops patch bodies)
|
|
98
125
|
* - message/session objects carrying `summary.diffs` under properties/data info
|
|
126
|
+
* (L1: slim file list + diffCount/hasDiffs; never patch bodies)
|
|
99
127
|
*
|
|
100
128
|
* Non-diff events keep identity.
|
|
101
129
|
*/
|
|
@@ -154,7 +182,7 @@ export function summarizeOutboundEventPayload(payload) {
|
|
|
154
182
|
|
|
155
183
|
// Common shape: properties.info.summary.diffs
|
|
156
184
|
if (body.info && typeof body.info === 'object' && !Array.isArray(body.info)) {
|
|
157
|
-
const nextInfo =
|
|
185
|
+
const nextInfo = projectMessageSummaryDiffSlim(body.info);
|
|
158
186
|
if (nextInfo !== body.info) {
|
|
159
187
|
next = {
|
|
160
188
|
...next,
|
|
@@ -169,7 +197,7 @@ export function summarizeOutboundEventPayload(payload) {
|
|
|
169
197
|
}
|
|
170
198
|
|
|
171
199
|
// Rare: properties.summary.diffs directly on the properties/data body
|
|
172
|
-
const nextBody =
|
|
200
|
+
const nextBody = projectMessageSummaryDiffSlim(body);
|
|
173
201
|
if (nextBody !== body) {
|
|
174
202
|
next = {
|
|
175
203
|
...next,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
projectMessageSummaryDiffCounts,
|
|
5
|
+
projectMessageSummaryDiffSlim,
|
|
4
6
|
summarizeFileDiff,
|
|
5
7
|
summarizeFileDiffs,
|
|
6
8
|
summarizeOutboundEventPayload,
|
|
@@ -48,6 +50,67 @@ describe('summarizeFileDiffs', () => {
|
|
|
48
50
|
});
|
|
49
51
|
});
|
|
50
52
|
|
|
53
|
+
describe('projectMessageSummaryDiffSlim', () => {
|
|
54
|
+
it('keeps slim summary.diffs and sets diffCount/hasDiffs', () => {
|
|
55
|
+
const owner = {
|
|
56
|
+
id: 'msg_1',
|
|
57
|
+
summary: {
|
|
58
|
+
title: 'turn',
|
|
59
|
+
diffs: [
|
|
60
|
+
{ file: 'a.ts', additions: 1, deletions: 0, patch: '@@' },
|
|
61
|
+
{ file: 'b.ts', additions: 2, deletions: 1, before: 'x', after: 'y' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const next = projectMessageSummaryDiffSlim(owner);
|
|
67
|
+
expect(next).not.toBe(owner);
|
|
68
|
+
expect(next.summary).toEqual({
|
|
69
|
+
title: 'turn',
|
|
70
|
+
diffs: [
|
|
71
|
+
{ file: 'a.ts', additions: 1, deletions: 0 },
|
|
72
|
+
{ file: 'b.ts', additions: 2, deletions: 1 },
|
|
73
|
+
],
|
|
74
|
+
diffCount: 2,
|
|
75
|
+
hasDiffs: true,
|
|
76
|
+
});
|
|
77
|
+
expect(JSON.stringify(next)).not.toContain('patch');
|
|
78
|
+
expect(JSON.stringify(next)).not.toContain('"before"');
|
|
79
|
+
expect(JSON.stringify(next)).not.toContain('"after"');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('projects empty arrays to slim list + diffCount 0 / hasDiffs false', () => {
|
|
83
|
+
const owner = {
|
|
84
|
+
summary: { diffs: [] },
|
|
85
|
+
};
|
|
86
|
+
expect(projectMessageSummaryDiffSlim(owner).summary).toEqual({
|
|
87
|
+
diffs: [],
|
|
88
|
+
diffCount: 0,
|
|
89
|
+
hasDiffs: false,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('keeps identity when summary has no diffs key', () => {
|
|
94
|
+
const owner = { summary: { title: 'x', diffCount: 3, hasDiffs: true } };
|
|
95
|
+
expect(projectMessageSummaryDiffSlim(owner)).toBe(owner);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('keeps identity when diffs already slim and markers match', () => {
|
|
99
|
+
const owner = {
|
|
100
|
+
summary: {
|
|
101
|
+
diffs: [{ file: 'a.ts', additions: 1, deletions: 0 }],
|
|
102
|
+
diffCount: 1,
|
|
103
|
+
hasDiffs: true,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
expect(projectMessageSummaryDiffSlim(owner)).toBe(owner);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('exports projectMessageSummaryDiffCounts as a compatible alias', () => {
|
|
110
|
+
expect(projectMessageSummaryDiffCounts).toBe(projectMessageSummaryDiffSlim);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
51
114
|
describe('summarizeOutboundEventPayload', () => {
|
|
52
115
|
it('summarizes session.diff properties.diff', () => {
|
|
53
116
|
const payload = {
|
|
@@ -97,7 +160,7 @@ describe('summarizeOutboundEventPayload', () => {
|
|
|
97
160
|
}]);
|
|
98
161
|
});
|
|
99
162
|
|
|
100
|
-
it('
|
|
163
|
+
it('projects message.updated info.summary.diffs to L1 slim list + markers', () => {
|
|
101
164
|
const payload = {
|
|
102
165
|
type: 'message.updated',
|
|
103
166
|
properties: {
|
|
@@ -109,6 +172,8 @@ describe('summarizeOutboundEventPayload', () => {
|
|
|
109
172
|
additions: 1,
|
|
110
173
|
deletions: 0,
|
|
111
174
|
patch: '@@',
|
|
175
|
+
from: 'blob-a',
|
|
176
|
+
to: 'blob-b',
|
|
112
177
|
}],
|
|
113
178
|
},
|
|
114
179
|
},
|
|
@@ -116,11 +181,13 @@ describe('summarizeOutboundEventPayload', () => {
|
|
|
116
181
|
};
|
|
117
182
|
|
|
118
183
|
const next = summarizeOutboundEventPayload(payload);
|
|
119
|
-
expect(next.properties.info.summary
|
|
120
|
-
file: 'a.ts',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
184
|
+
expect(next.properties.info.summary).toEqual({
|
|
185
|
+
diffs: [{ file: 'a.ts', additions: 1, deletions: 0 }],
|
|
186
|
+
diffCount: 1,
|
|
187
|
+
hasDiffs: true,
|
|
188
|
+
});
|
|
189
|
+
expect(JSON.stringify(next)).not.toContain('patch');
|
|
190
|
+
expect(JSON.stringify(next)).not.toContain('blob-a');
|
|
124
191
|
});
|
|
125
192
|
|
|
126
193
|
it('returns identity for session.status', () => {
|
|
@@ -223,7 +223,7 @@ describe('event stream protocol helpers', () => {
|
|
|
223
223
|
expect(rawPayload.length).toBeLessThan(64 * 1024);
|
|
224
224
|
});
|
|
225
225
|
|
|
226
|
-
it('
|
|
226
|
+
it('projects message.updated summary.diffs to L1 slim list + markers on outbound frames', () => {
|
|
227
227
|
let rawPayload = null;
|
|
228
228
|
const socket = {
|
|
229
229
|
readyState: 1,
|
|
@@ -246,6 +246,8 @@ describe('event stream protocol helpers', () => {
|
|
|
246
246
|
additions: 2,
|
|
247
247
|
deletions: 1,
|
|
248
248
|
patch: '@@ heavy patch body @@\n' + 'y'.repeat(1000),
|
|
249
|
+
before: 'old',
|
|
250
|
+
after: 'new',
|
|
249
251
|
}],
|
|
250
252
|
},
|
|
251
253
|
},
|
|
@@ -254,13 +256,74 @@ describe('event stream protocol helpers', () => {
|
|
|
254
256
|
|
|
255
257
|
expect(sent).toBe(true);
|
|
256
258
|
const frame = JSON.parse(rawPayload);
|
|
257
|
-
expect(frame.payload.properties.info.summary
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
259
|
+
expect(frame.payload.properties.info.summary).toEqual({
|
|
260
|
+
diffs: [{
|
|
261
|
+
file: 'a.ts',
|
|
262
|
+
status: 'modified',
|
|
263
|
+
additions: 2,
|
|
264
|
+
deletions: 1,
|
|
265
|
+
}],
|
|
266
|
+
diffCount: 1,
|
|
267
|
+
hasDiffs: true,
|
|
268
|
+
});
|
|
263
269
|
expect(JSON.stringify(frame)).not.toContain('patch');
|
|
270
|
+
expect(JSON.stringify(frame)).not.toContain('"before"');
|
|
271
|
+
expect(JSON.stringify(frame)).not.toContain('"after"');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('keeps L1 message.updated serialization under 256KiB for a 463-file / ~14MB patch summary', () => {
|
|
275
|
+
let rawPayload = null;
|
|
276
|
+
const socket = {
|
|
277
|
+
readyState: 1,
|
|
278
|
+
send(payload) {
|
|
279
|
+
rawPayload = payload;
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const patchBody = 'P'.repeat(Math.floor((14 * 1024 * 1024) / 463));
|
|
284
|
+
const diffs = Array.from({ length: 463 }, (_, i) => ({
|
|
285
|
+
file: `src/generated/file-${i}.ts`,
|
|
286
|
+
status: 'modified',
|
|
287
|
+
additions: i % 7,
|
|
288
|
+
deletions: i % 3,
|
|
289
|
+
patch: `${patchBody}-${i}`,
|
|
290
|
+
before: `before-${i}`,
|
|
291
|
+
after: `after-${i}`,
|
|
292
|
+
from: `from-${i}`,
|
|
293
|
+
to: `to-${i}`,
|
|
294
|
+
}));
|
|
295
|
+
|
|
296
|
+
const sent = sendMessageStreamWsEvent(socket, {
|
|
297
|
+
type: 'message.updated',
|
|
298
|
+
properties: {
|
|
299
|
+
info: {
|
|
300
|
+
id: 'msg_huge',
|
|
301
|
+
sessionID: 'ses_huge',
|
|
302
|
+
role: 'assistant',
|
|
303
|
+
summary: { title: 'huge', diffs },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(sent).toBe(true);
|
|
309
|
+
const frame = JSON.parse(rawPayload);
|
|
310
|
+
const summary = frame.payload.properties.info.summary;
|
|
311
|
+
expect(summary.diffCount).toBe(463);
|
|
312
|
+
expect(summary.hasDiffs).toBe(true);
|
|
313
|
+
expect(summary.diffs).toHaveLength(463);
|
|
314
|
+
expect(summary.diffs[0]).toEqual({
|
|
315
|
+
file: 'src/generated/file-0.ts',
|
|
316
|
+
status: 'modified',
|
|
317
|
+
additions: 0,
|
|
318
|
+
deletions: 0,
|
|
319
|
+
});
|
|
320
|
+
expect(rawPayload).not.toContain(patchBody.slice(0, 64));
|
|
321
|
+
expect(rawPayload).not.toContain('"patch"');
|
|
322
|
+
expect(rawPayload).not.toContain('"before"');
|
|
323
|
+
expect(rawPayload).not.toContain('"after"');
|
|
324
|
+
expect(rawPayload).not.toContain('"from"');
|
|
325
|
+
expect(rawPayload).not.toContain('"to"');
|
|
326
|
+
expect(Buffer.byteLength(rawPayload, 'utf8')).toBeLessThan(256 * 1024);
|
|
264
327
|
});
|
|
265
328
|
|
|
266
329
|
it('leaves ordinary session.status events unchanged', () => {
|
|
@@ -45,8 +45,8 @@ This module provides notification message preparation utilities for the web serv
|
|
|
45
45
|
- Returned API:
|
|
46
46
|
- `maybeSendPushForTrigger(payload)`
|
|
47
47
|
- Owns:
|
|
48
|
-
- top-level completion/question/permission trigger routing; completion is the sole task-status notification and child sessions are suppressed
|
|
49
|
-
- session
|
|
48
|
+
- top-level completion/question/permission trigger routing; completion is the sole task-status notification and child sessions plus small-model system sessions (`metadata.openchamber.smallModel.purpose`) are suppressed
|
|
49
|
+
- session meta cache for child-session and small-model suppression
|
|
50
50
|
- template resolution and fallback behavior
|
|
51
51
|
- native notification fanout and web push payload fanout
|
|
52
52
|
- push suppression while any fresh UI visibility heartbeat reports a focused client
|
|
@@ -111,8 +111,8 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
111
111
|
const notifiedPermissionRequests = new Set();
|
|
112
112
|
const lastReadyNotificationAt = new Map();
|
|
113
113
|
|
|
114
|
-
const
|
|
115
|
-
const
|
|
114
|
+
const sessionMetaCache = new Map();
|
|
115
|
+
const SESSION_META_CACHE_TTL_MS = 60 * 1000;
|
|
116
116
|
|
|
117
117
|
// Sessions where the client has enabled Permission Auto-Accept. Mirrored
|
|
118
118
|
// from the client-side permissionStore via POST /api/notifications/auto-accept
|
|
@@ -136,43 +136,67 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
136
136
|
return `/?session=${encodeURIComponent(sessionId)}`;
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
const
|
|
139
|
+
const getSessionMetaCacheKey = (sessionId, directory) => `${directory || ''}\0${sessionId}`;
|
|
140
140
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
const normalizeParentID = (value) => (
|
|
142
|
+
typeof value === 'string' && value.length > 0 ? value : null
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const normalizeSmallModelPurpose = (value) => (
|
|
146
|
+
typeof value === 'string' && value.length > 0 ? value : null
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const getCachedSessionMeta = (sessionId, directory) => {
|
|
150
|
+
const cacheKey = getSessionMetaCacheKey(sessionId, directory);
|
|
151
|
+
const entry = sessionMetaCache.get(cacheKey);
|
|
144
152
|
if (!entry) return undefined;
|
|
145
|
-
if (Date.now() - entry.at >
|
|
146
|
-
|
|
153
|
+
if (Date.now() - entry.at > SESSION_META_CACHE_TTL_MS) {
|
|
154
|
+
sessionMetaCache.delete(cacheKey);
|
|
147
155
|
return undefined;
|
|
148
156
|
}
|
|
149
|
-
return entry
|
|
157
|
+
return entry;
|
|
150
158
|
};
|
|
151
159
|
|
|
152
|
-
const
|
|
153
|
-
|
|
160
|
+
const setCachedSessionMeta = (sessionId, directory, meta) => {
|
|
161
|
+
const previous = getCachedSessionMeta(sessionId, directory);
|
|
162
|
+
sessionMetaCache.set(getSessionMetaCacheKey(sessionId, directory), {
|
|
163
|
+
parentID: meta.parentID !== undefined ? meta.parentID : (previous?.parentID ?? null),
|
|
164
|
+
smallModelPurpose: meta.smallModelPurpose !== undefined
|
|
165
|
+
? meta.smallModelPurpose
|
|
166
|
+
: (previous?.smallModelPurpose ?? null),
|
|
167
|
+
at: Date.now(),
|
|
168
|
+
});
|
|
154
169
|
};
|
|
155
170
|
|
|
156
171
|
const getParentIdFromPayload = (payload) => {
|
|
157
172
|
if (!payload || typeof payload !== 'object') return undefined;
|
|
158
173
|
if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
return normalizeParentID(payload.properties?.info?.parentID);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const getSmallModelPurposeFromPayload = (payload) => {
|
|
178
|
+
if (!payload || typeof payload !== 'object') return undefined;
|
|
179
|
+
if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
|
|
180
|
+
return normalizeSmallModelPurpose(payload.properties?.info?.metadata?.openchamber?.smallModel?.purpose);
|
|
161
181
|
};
|
|
162
182
|
|
|
163
|
-
const
|
|
183
|
+
const maybeCacheSessionMetaFromPayload = (payload) => {
|
|
164
184
|
const sessionId = extractSessionIdFromPayload(payload);
|
|
165
185
|
if (typeof sessionId !== 'string' || sessionId.length === 0) return;
|
|
166
186
|
const directory = extractDirectoryFromPayload(payload);
|
|
167
187
|
const parentID = getParentIdFromPayload(payload);
|
|
168
|
-
|
|
169
|
-
|
|
188
|
+
const smallModelPurpose = getSmallModelPurposeFromPayload(payload);
|
|
189
|
+
if (parentID === undefined && smallModelPurpose === undefined) return;
|
|
190
|
+
setCachedSessionMeta(sessionId, directory, {
|
|
191
|
+
...(parentID !== undefined ? { parentID } : {}),
|
|
192
|
+
...(smallModelPurpose !== undefined ? { smallModelPurpose } : {}),
|
|
193
|
+
});
|
|
170
194
|
};
|
|
171
195
|
|
|
172
|
-
const
|
|
196
|
+
const fetchSessionMeta = async (sessionId, directory) => {
|
|
173
197
|
if (!sessionId) return undefined;
|
|
174
198
|
|
|
175
|
-
const cached =
|
|
199
|
+
const cached = getCachedSessionMeta(sessionId, directory);
|
|
176
200
|
if (cached !== undefined) return cached;
|
|
177
201
|
|
|
178
202
|
try {
|
|
@@ -194,16 +218,22 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
194
218
|
return undefined;
|
|
195
219
|
}
|
|
196
220
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
:
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
const meta = {
|
|
222
|
+
parentID: normalizeParentID(session.parentID),
|
|
223
|
+
smallModelPurpose: normalizeSmallModelPurpose(session.metadata?.openchamber?.smallModel?.purpose),
|
|
224
|
+
};
|
|
225
|
+
setCachedSessionMeta(sessionId, directory, meta);
|
|
226
|
+
return meta;
|
|
202
227
|
} catch {
|
|
203
228
|
return undefined;
|
|
204
229
|
}
|
|
205
230
|
};
|
|
206
231
|
|
|
232
|
+
const fetchSessionParentId = async (sessionId, directory) => {
|
|
233
|
+
const meta = await fetchSessionMeta(sessionId, directory);
|
|
234
|
+
return meta?.parentID;
|
|
235
|
+
};
|
|
236
|
+
|
|
207
237
|
// Mirrors client-side autoRespondsPermission: a session auto-accepts if it
|
|
208
238
|
// OR any ancestor is flagged. Walks the parent chain via fetchSessionParentId.
|
|
209
239
|
const isSessionAutoAccepting = async (sessionId, directory) => {
|
|
@@ -225,6 +255,17 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
225
255
|
return typeof parentID === 'string' && parentID.length > 0;
|
|
226
256
|
};
|
|
227
257
|
|
|
258
|
+
const isSmallModelSession = async (sessionId, directory) => {
|
|
259
|
+
const meta = await fetchSessionMeta(sessionId, directory);
|
|
260
|
+
return typeof meta?.smallModelPurpose === 'string' && meta.smallModelPurpose.length > 0;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const shouldSkipSystemSessionNotification = async (sessionId, directory) => {
|
|
264
|
+
if (await isSubtaskSession(sessionId, directory)) return true;
|
|
265
|
+
if (await isSmallModelSession(sessionId, directory)) return true;
|
|
266
|
+
return false;
|
|
267
|
+
};
|
|
268
|
+
|
|
228
269
|
const extractSessionIdFromPayload = (payload) => {
|
|
229
270
|
if (!payload || typeof payload !== 'object') return null;
|
|
230
271
|
const props = payload.properties;
|
|
@@ -309,7 +350,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
309
350
|
return;
|
|
310
351
|
}
|
|
311
352
|
|
|
312
|
-
|
|
353
|
+
maybeCacheSessionMetaFromPayload(payload);
|
|
313
354
|
|
|
314
355
|
const sessionId = extractSessionIdFromPayload(payload);
|
|
315
356
|
const notificationDirectory = extractDirectoryFromPayload(payload);
|
|
@@ -340,7 +381,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
340
381
|
sessionId
|
|
341
382
|
&& info?.role === 'assistant'
|
|
342
383
|
&& (info?.finish === 'stop' || info?.finish === 'error')
|
|
343
|
-
&& await
|
|
384
|
+
&& await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)
|
|
344
385
|
) {
|
|
345
386
|
return;
|
|
346
387
|
}
|
|
@@ -433,7 +474,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
433
474
|
}
|
|
434
475
|
|
|
435
476
|
if (payload.type === 'question.asked' && sessionId) {
|
|
436
|
-
if (await
|
|
477
|
+
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
437
478
|
return;
|
|
438
479
|
}
|
|
439
480
|
const existingTimer = pushQuestionDebounceTimers.get(sessionId);
|
|
@@ -534,7 +575,7 @@ export const createNotificationTriggerRuntime = (deps) => {
|
|
|
534
575
|
}
|
|
535
576
|
|
|
536
577
|
if (payload.type === 'permission.asked' && sessionId) {
|
|
537
|
-
if (await
|
|
578
|
+
if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
|
|
538
579
|
return;
|
|
539
580
|
}
|
|
540
581
|
const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;
|