@openchambery/web 1.18.1 → 1.18.2-beta.10
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-BzRb9sP6.js} +1 -1
- package/dist/assets/{AssistantView-f5eK9QED.js → AssistantView-Bcx9ER-E.js} +1 -1
- package/dist/assets/{ChatContainer-BvJoxcjJ.js → ChatContainer-DaBnpayh.js} +38 -38
- package/dist/assets/{DiagramView-Vu2JnmC-.js → DiagramView-DaZSw3PM.js} +1 -1
- package/dist/assets/{FadeInOnReveal-aeWlaqVB.js → FadeInOnReveal-oeyB_nIi.js} +1 -1
- package/dist/assets/{Icon-AXqa-fYB.js → Icon-DHaR0peD.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-CSxVZ_eA.js → MarkdownRendererImpl-CoMjP1t6.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-3NIRdxpt.js → MobileDetailNavigation-Bsi54FsV.js} +1 -1
- package/dist/assets/{MobileShareBridge-B7jqfJ-S.js → MobileShareBridge-BQd5tCJN.js} +1 -1
- package/dist/assets/MobileSurface-C0bqzAcb.js +2 -0
- package/dist/assets/{MultiRunWindow-DkYTqcPq.js → MultiRunWindow-CJLa67M0.js} +1 -1
- package/dist/assets/{OnboardingScreen-CCfdEtW6.js → OnboardingScreen-BZHN7C2r.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DeL1aO59.js → OverlayScrollbar-BLbgO_QF.js} +1 -1
- package/dist/assets/SettingsView-pHa9Y38q.js +25 -0
- package/dist/assets/{SettingsWindow--nLXOo-L.js → SettingsWindow-q6zT03-N.js} +1 -1
- package/dist/assets/{TerminalView-B80SmYHn.js → TerminalView-BFb17E29.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CtbnU35x.js → ToolOutputDialog-sVkggrhW.js} +1 -1
- package/dist/assets/appThemeRegistry-Bh6gmBoH.js +2 -0
- package/dist/assets/{button-CDvMVaiO.js → button-DCUMIXDC.js} +1 -1
- package/dist/assets/{clipboard-Jdqe05db.js → clipboard-DF5tFLDb.js} +1 -1
- package/dist/assets/{collapsible-C2aamDvD.js → collapsible-DNg5bLKV.js} +1 -1
- package/dist/assets/{context-menu-D16ocNrk.js → context-menu-VD4qvZ6C.js} +1 -1
- package/dist/assets/{desktopHosts-CBoBpJ1Z.js → desktopHosts-DSgGxDpI.js} +1 -1
- package/dist/assets/{desktopRelayRestore-l2R9muux.js → desktopRelayRestore-DVIx7jsz.js} +1 -1
- package/dist/assets/{diagram-CEon0BQs.js → diagram-cjvFqVa8.js} +1 -1
- package/dist/assets/{dialog-BzZ3cKxG.js → dialog-DaSIYgrj.js} +1 -1
- package/dist/assets/{es-C25PRSjo.js → es-DCoGZljP.js} +1 -1
- package/dist/assets/{fileQueries-CE_DaZWQ.js → fileQueries-DXvhIL_U.js} +1 -1
- package/dist/assets/{fr-Cv8ZWQdW.js → fr-dkEoZpPz.js} +2 -2
- package/dist/assets/{gitApi-B1CIqazO.js → gitApi-cPhxx3NZ.js} +1 -1
- package/dist/assets/gitApiHttp-SLrnr8a5.js +2 -0
- package/dist/assets/{input-k9DGGyZ6.js → input-QS3IWgY5.js} +1 -1
- package/dist/assets/{insertionBoundaries-B9cwRNac.js → insertionBoundaries-XUyczmFs.js} +1 -1
- package/dist/assets/{ja-Ue9m9eSe.js → ja-bQwSsvRr.js} +1 -1
- package/dist/assets/{ko-DWBUCCV6.js → ko-Kq5C3hsv.js} +1 -1
- package/dist/assets/{main-BSVnX01h.js → main-D4CTy7xa.js} +2 -2
- package/dist/assets/{main-DzZumJ48.js → main-Mkx9wBnO.js} +23 -23
- package/dist/assets/{markdown-worker-DkeWiER6.js → markdown-worker-D-OR2e_G.js} +1 -1
- package/dist/assets/{markdownTheme-DNT93i5L.js → markdownTheme-y_Rf6mLa.js} +1 -1
- package/dist/assets/{miniChat-CrEtOV4u.js → miniChat-Bq6uelzq.js} +2 -2
- package/dist/assets/mobile-C1L9OCdh.js +2 -0
- package/dist/assets/{multirun-G-ZwKVUq.js → multirun-9OIhLOWP.js} +1 -1
- package/dist/assets/{persistence-DPNMIbF5.js → persistence-oSFWyKgV.js} +1 -1
- package/dist/assets/{pl-DaBaPCyE.js → pl-CYpMAgVn.js} +2 -2
- package/dist/assets/projectMeta-BXPWMXjv.js +3 -0
- package/dist/assets/{pt-BR-C2cJZ43o.js → pt-BR-MSPTLfTm.js} +1 -1
- package/dist/assets/{radio-BtilB34p.js → radio-CqpSp0pL.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-D1HWiWMn.js → renderElectronMiniChatApp-B8cSB7U4.js} +2 -2
- package/dist/assets/renderMobileApp-DmBjzy65.js +23 -0
- package/dist/assets/{runtimeConfig-BtWi74Pm.js → runtimeConfig-n4QAk1lU.js} +1 -1
- package/dist/assets/runtimeEndpointReset-Bw-RBy5a.js +145 -0
- package/dist/assets/{sortable-tabs-strip-BpvOof2G.js → sortable-tabs-strip-B4wI7VAP.js} +1 -1
- package/dist/assets/src-BhvNwAr-.css +2 -0
- package/dist/assets/{store-SqV3zfE8.js → store-OURVnLSN.js} +2 -2
- package/dist/assets/{terminalApi-tB3qLcfa.js → terminalApi-_NnfkxgP.js} +1 -1
- package/dist/assets/{tooltip-DMNR5rN0.js → tooltip-xEnco9xY.js} +1 -1
- package/dist/assets/{uk-DJz_P8OZ.js → uk-cSkRAYFY.js} +1 -1
- package/dist/assets/{url-7MqoLs9X.js → url-DIZgk80M.js} +1 -1
- package/dist/assets/{useAppFontEffects-BBxqFIta.js → useAppFontEffects-BHoHqFhx.js} +3 -3
- package/dist/assets/{useDesktopWindowControlsLayout-D8fjPyqI.js → useDesktopWindowControlsLayout-DwmEdp3O.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-C5OxWqUH.js → useEffectiveDirectory-Bjo9xE9q.js} +1 -1
- package/dist/assets/{useFontPreferences-CIurCeNv.js → useFontPreferences-B-QHyR3C.js} +1 -1
- package/dist/assets/useMobileNavigationStore-SCw9NohC.js +476 -0
- package/dist/assets/useSessionAutoCleanup-KwCKRF_d.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CJozblYF.js → useWorkerHighlightedLines-Cm3UQxhi.js} +1 -1
- package/dist/assets/{utils-Co19EJEL.js → utils-Dxsi3J3l.js} +1 -1
- package/dist/assets/{zh-CN-DLAKDOM9.js → zh-CN-BKqeT_uz.js} +1 -1
- package/dist/assets/{zh-TW-s3Jjx1_n.js → zh-TW-BaNjJ8Y4.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/index.d.ts +6 -0
- package/server/index.js +6 -0
- package/server/lib/config-sync/DOCUMENTATION.md +70 -0
- package/server/lib/config-sync/config-sync.test.js +197 -0
- package/server/lib/config-sync/constants.js +39 -0
- package/server/lib/config-sync/contract.js +130 -0
- package/server/lib/config-sync/credential-auth.js +39 -0
- package/server/lib/config-sync/engine.js +108 -0
- package/server/lib/config-sync/index.js +67 -0
- package/server/lib/config-sync/local-backup.js +171 -0
- package/server/lib/config-sync/plan.js +318 -0
- package/server/lib/config-sync/receiver.js +330 -0
- package/server/lib/config-sync/routes.js +141 -0
- package/server/lib/config-sync/routes.test.js +154 -0
- package/server/lib/config-sync/scripts.js +264 -0
- package/server/lib/config-sync/selections.js +112 -0
- package/server/lib/config-sync/tar.js +35 -0
- package/server/lib/config-sync/target-id.js +24 -0
- package/server/lib/event-stream/DOCUMENTATION.md +2 -2
- package/server/lib/event-stream/diff-summary.js +35 -16
- package/server/lib/event-stream/diff-summary.test.js +46 -6
- package/server/lib/event-stream/protocol.test.js +7 -7
- package/server/lib/git/DOCUMENTATION.md +2 -1
- package/server/lib/git/service.js +24 -5
- package/server/lib/git/service.test.js +51 -0
- package/server/lib/opencode/DOCUMENTATION.md +4 -1
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/feature-routes-runtime.test.js +10 -1
- package/server/lib/opencode/openchamber-routes.js +7 -0
- package/server/lib/opencode/settings-runtime.js +82 -51
- package/server/lib/opencode/settings-runtime.test.js +42 -0
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- 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 +48 -0
- package/server/lib/session-turn-pages/reconcile.service.js +3 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +40 -0
- package/server/lib/session-turn-pages/routes.js +273 -12
- package/server/lib/session-turn-pages/routes.test.js +359 -0
- package/server/lib/session-turn-pages/service.js +29 -7
- package/server/lib/session-turn-pages/service.test.js +30 -0
- package/dist/assets/MobileSurface-BL3hired.js +0 -2
- package/dist/assets/SettingsView-CyrIDyDl.js +0 -25
- package/dist/assets/appThemeRegistry-D2ZUcQmr.js +0 -2
- package/dist/assets/gitApiHttp-D8j1qRH4.js +0 -2
- package/dist/assets/mobile-DMjO-f5u.js +0 -2
- package/dist/assets/projectMeta-owiDYjF9.js +0 -3
- package/dist/assets/renderMobileApp-Cqxwb3Ub.js +0 -23
- package/dist/assets/runtimeEndpointReset-C0oDuy2R.js +0 -145
- package/dist/assets/src-oHrYIqHF.css +0 -2
- package/dist/assets/useMobileNavigationStore-U3b_hG4m.js +0 -475
- package/dist/assets/useSessionAutoCleanup-CDYDm76L.js +0 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenChamber-owned session Changes API (L2 file list / L3 single-file patch).
|
|
3
|
+
*
|
|
4
|
+
* L2 (no `file`): read official `session.message`, project summary.diffs to
|
|
5
|
+
* `{ files: [{ file, status?, additions, deletions }] }` — never patch bodies
|
|
6
|
+
* or the full message envelope.
|
|
7
|
+
* L3 (with `file`): call official `session.diff({ sessionID, messageID })`,
|
|
8
|
+
* return `{ diff: SnapshotFileDiff }` for the exact file string match.
|
|
9
|
+
*
|
|
10
|
+
* Dependency-injected for unit tests (`fetchMessage`, `fetchDiff`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Project a raw summary.diffs entry to L2 file-list scalars.
|
|
15
|
+
* @param {unknown} entry
|
|
16
|
+
* @returns {{ file: string, status?: string, additions: number, deletions: number } | null}
|
|
17
|
+
*/
|
|
18
|
+
export const projectChangeFileEntry = (entry) => {
|
|
19
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const file = typeof entry.file === 'string' ? entry.file : null;
|
|
23
|
+
if (!file || file.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Built from allowlist only — patch/before/after/from/to never copied. */
|
|
28
|
+
const out = { file };
|
|
29
|
+
if (typeof entry.status === 'string') {
|
|
30
|
+
out.status = entry.status;
|
|
31
|
+
}
|
|
32
|
+
out.additions = typeof entry.additions === 'number' && Number.isFinite(entry.additions)
|
|
33
|
+
? entry.additions
|
|
34
|
+
: 0;
|
|
35
|
+
out.deletions = typeof entry.deletions === 'number' && Number.isFinite(entry.deletions)
|
|
36
|
+
? entry.deletions
|
|
37
|
+
: 0;
|
|
38
|
+
return out;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract L2 file list from a session.message response shape.
|
|
43
|
+
* Accepts `{ info, parts }`, `{ data: { info, parts } }`, or bare info.
|
|
44
|
+
* @param {unknown} messagePayload
|
|
45
|
+
* @returns {{ files: Array<{ file: string, status?: string, additions: number, deletions: number }> }}
|
|
46
|
+
*/
|
|
47
|
+
export const projectChangeFileList = (messagePayload) => {
|
|
48
|
+
const record = messagePayload && typeof messagePayload === 'object' && !Array.isArray(messagePayload)
|
|
49
|
+
? messagePayload
|
|
50
|
+
: null;
|
|
51
|
+
const info = record?.info && typeof record.info === 'object' && !Array.isArray(record.info)
|
|
52
|
+
? record.info
|
|
53
|
+
: (record?.summary ? record : null);
|
|
54
|
+
const summary = info?.summary && typeof info.summary === 'object' && !Array.isArray(info.summary)
|
|
55
|
+
? info.summary
|
|
56
|
+
: null;
|
|
57
|
+
const diffs = Array.isArray(summary?.diffs) ? summary.diffs : [];
|
|
58
|
+
|
|
59
|
+
const files = [];
|
|
60
|
+
for (const entry of diffs) {
|
|
61
|
+
const projected = projectChangeFileEntry(entry);
|
|
62
|
+
if (projected) files.push(projected);
|
|
63
|
+
}
|
|
64
|
+
return { files };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find exact file match in a session.diff array.
|
|
69
|
+
* @param {unknown} diffs
|
|
70
|
+
* @param {string} file
|
|
71
|
+
* @returns {unknown | null}
|
|
72
|
+
*/
|
|
73
|
+
export const findChangeFileDiff = (diffs, file) => {
|
|
74
|
+
if (!Array.isArray(diffs) || typeof file !== 'string') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of diffs) {
|
|
78
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry) && entry.file === file) {
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {{
|
|
87
|
+
* fetchMessage: (args: { sessionID: string, messageID: string, directory?: string, signal?: AbortSignal }) => Promise<unknown>,
|
|
88
|
+
* fetchDiff: (args: { sessionID: string, messageID: string, directory?: string, signal?: AbortSignal }) => Promise<unknown>,
|
|
89
|
+
* logger?: { warn?: Function },
|
|
90
|
+
* }} deps
|
|
91
|
+
*/
|
|
92
|
+
export const createSessionChangesService = (deps) => {
|
|
93
|
+
const {
|
|
94
|
+
fetchMessage,
|
|
95
|
+
fetchDiff,
|
|
96
|
+
logger = console,
|
|
97
|
+
} = deps;
|
|
98
|
+
|
|
99
|
+
if (typeof fetchMessage !== 'function' || typeof fetchDiff !== 'function') {
|
|
100
|
+
throw new Error('createSessionChangesService requires fetchMessage and fetchDiff');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {{
|
|
105
|
+
* sessionID: string,
|
|
106
|
+
* messageID: string,
|
|
107
|
+
* directory?: string,
|
|
108
|
+
* file?: string,
|
|
109
|
+
* signal?: AbortSignal,
|
|
110
|
+
* }} input
|
|
111
|
+
*/
|
|
112
|
+
const loadChanges = async (input) => {
|
|
113
|
+
const sessionID = typeof input?.sessionID === 'string' ? input.sessionID : '';
|
|
114
|
+
const messageID = typeof input?.messageID === 'string' ? input.messageID : '';
|
|
115
|
+
const directory = typeof input?.directory === 'string' && input.directory.length > 0
|
|
116
|
+
? input.directory
|
|
117
|
+
: undefined;
|
|
118
|
+
const file = typeof input?.file === 'string' && input.file.length > 0
|
|
119
|
+
? input.file
|
|
120
|
+
: undefined;
|
|
121
|
+
const signal = input?.signal;
|
|
122
|
+
|
|
123
|
+
if (!sessionID || !messageID) {
|
|
124
|
+
return { ok: false, error: 'invalid_params' };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if (!file) {
|
|
129
|
+
const payload = await fetchMessage({ sessionID, messageID, directory, signal });
|
|
130
|
+
return { ok: true, body: projectChangeFileList(payload) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const diffs = await fetchDiff({ sessionID, messageID, directory, signal });
|
|
134
|
+
if (!Array.isArray(diffs)) {
|
|
135
|
+
logger?.warn?.('[session-changes] session.diff malformed payload');
|
|
136
|
+
return { ok: false, error: 'upstream' };
|
|
137
|
+
}
|
|
138
|
+
const match = findChangeFileDiff(diffs, file);
|
|
139
|
+
if (!match) {
|
|
140
|
+
return { ok: false, error: 'change_not_found' };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, body: { diff: match } };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error?.name === 'AbortError' || error?.code === 'aborted') {
|
|
145
|
+
return { ok: false, error: 'aborted' };
|
|
146
|
+
}
|
|
147
|
+
if (typeof error?.code === 'string' && error.code.length > 0) {
|
|
148
|
+
return { ok: false, error: error.code };
|
|
149
|
+
}
|
|
150
|
+
logger?.warn?.('[session-changes] upstream failure');
|
|
151
|
+
return { ok: false, error: 'upstream' };
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return { loadChanges };
|
|
156
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createSessionChangesService,
|
|
5
|
+
findChangeFileDiff,
|
|
6
|
+
projectChangeFileEntry,
|
|
7
|
+
projectChangeFileList,
|
|
8
|
+
} from './changes.service.js';
|
|
9
|
+
|
|
10
|
+
describe('projectChangeFileEntry', () => {
|
|
11
|
+
it('keeps file/status/additions/deletions and drops patch bodies', () => {
|
|
12
|
+
expect(projectChangeFileEntry({
|
|
13
|
+
file: 'src/a.ts',
|
|
14
|
+
status: 'modified',
|
|
15
|
+
additions: 3,
|
|
16
|
+
deletions: 1,
|
|
17
|
+
patch: '@@',
|
|
18
|
+
before: 'old',
|
|
19
|
+
after: 'new',
|
|
20
|
+
from: 'f',
|
|
21
|
+
to: 't',
|
|
22
|
+
})).toEqual({
|
|
23
|
+
file: 'src/a.ts',
|
|
24
|
+
status: 'modified',
|
|
25
|
+
additions: 3,
|
|
26
|
+
deletions: 1,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns null when file is missing', () => {
|
|
31
|
+
expect(projectChangeFileEntry({ additions: 1 })).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('projectChangeFileList', () => {
|
|
36
|
+
it('projects summary.diffs from a message envelope and never returns the message', () => {
|
|
37
|
+
const list = projectChangeFileList({
|
|
38
|
+
info: {
|
|
39
|
+
id: 'msg_1',
|
|
40
|
+
summary: {
|
|
41
|
+
diffs: [
|
|
42
|
+
{ file: 'a.ts', status: 'added', additions: 2, deletions: 0, patch: 'big' },
|
|
43
|
+
{ file: 'b.ts', additions: 1, deletions: 1, before: 'x' },
|
|
44
|
+
{ additions: 9 },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
parts: [{ type: 'text', text: 'secret' }],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(list).toEqual({
|
|
52
|
+
files: [
|
|
53
|
+
{ file: 'a.ts', status: 'added', additions: 2, deletions: 0 },
|
|
54
|
+
{ file: 'b.ts', additions: 1, deletions: 1 },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
expect(list.info).toBeUndefined();
|
|
58
|
+
expect(list.parts).toBeUndefined();
|
|
59
|
+
expect(JSON.stringify(list)).not.toContain('patch');
|
|
60
|
+
expect(JSON.stringify(list)).not.toContain('secret');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns empty files when summary.diffs is absent', () => {
|
|
64
|
+
expect(projectChangeFileList({ info: { id: 'msg_1' }, parts: [] })).toEqual({ files: [] });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('findChangeFileDiff', () => {
|
|
69
|
+
it('matches exact file string only', () => {
|
|
70
|
+
const diffs = [
|
|
71
|
+
{ file: 'src/a.ts', patch: 'a' },
|
|
72
|
+
{ file: 'src/b.ts', patch: 'b' },
|
|
73
|
+
];
|
|
74
|
+
expect(findChangeFileDiff(diffs, 'src/b.ts')).toEqual({ file: 'src/b.ts', patch: 'b' });
|
|
75
|
+
expect(findChangeFileDiff(diffs, 'src/missing.ts')).toBeNull();
|
|
76
|
+
expect(findChangeFileDiff(diffs, 'src/A.ts')).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('createSessionChangesService', () => {
|
|
81
|
+
it('L2 path calls fetchMessage and returns projected files', async () => {
|
|
82
|
+
const fetchMessage = vi.fn(async () => ({
|
|
83
|
+
info: {
|
|
84
|
+
summary: {
|
|
85
|
+
diffs: [{
|
|
86
|
+
file: 'hot.ts',
|
|
87
|
+
status: 'modified',
|
|
88
|
+
additions: 4,
|
|
89
|
+
deletions: 2,
|
|
90
|
+
patch: 'HUGE',
|
|
91
|
+
}],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
parts: [],
|
|
95
|
+
}));
|
|
96
|
+
const fetchDiff = vi.fn();
|
|
97
|
+
const service = createSessionChangesService({ fetchMessage, fetchDiff });
|
|
98
|
+
|
|
99
|
+
const result = await service.loadChanges({
|
|
100
|
+
sessionID: 'ses_1',
|
|
101
|
+
messageID: 'msg_1',
|
|
102
|
+
directory: '/repo',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.ok).toBe(true);
|
|
106
|
+
expect(result.body).toEqual({
|
|
107
|
+
files: [{
|
|
108
|
+
file: 'hot.ts',
|
|
109
|
+
status: 'modified',
|
|
110
|
+
additions: 4,
|
|
111
|
+
deletions: 2,
|
|
112
|
+
}],
|
|
113
|
+
});
|
|
114
|
+
expect(fetchMessage).toHaveBeenCalledOnce();
|
|
115
|
+
expect(fetchDiff).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('L3 path calls fetchDiff and returns the matched SnapshotFileDiff', async () => {
|
|
119
|
+
const fetchMessage = vi.fn();
|
|
120
|
+
const fetchDiff = vi.fn(async () => ([
|
|
121
|
+
{ file: 'a.ts', patch: 'a-patch', additions: 1, deletions: 0 },
|
|
122
|
+
{ file: 'b.ts', patch: 'b-patch', additions: 2, deletions: 1, status: 'modified' },
|
|
123
|
+
]));
|
|
124
|
+
const service = createSessionChangesService({ fetchMessage, fetchDiff });
|
|
125
|
+
|
|
126
|
+
const result = await service.loadChanges({
|
|
127
|
+
sessionID: 'ses_1',
|
|
128
|
+
messageID: 'msg_1',
|
|
129
|
+
file: 'b.ts',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result.ok).toBe(true);
|
|
133
|
+
expect(result.body).toEqual({
|
|
134
|
+
diff: {
|
|
135
|
+
file: 'b.ts',
|
|
136
|
+
patch: 'b-patch',
|
|
137
|
+
additions: 2,
|
|
138
|
+
deletions: 1,
|
|
139
|
+
status: 'modified',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
expect(fetchDiff).toHaveBeenCalledOnce();
|
|
143
|
+
expect(fetchMessage).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns change_not_found when file is absent from session.diff', async () => {
|
|
147
|
+
const service = createSessionChangesService({
|
|
148
|
+
fetchMessage: vi.fn(),
|
|
149
|
+
fetchDiff: vi.fn(async () => [{ file: 'other.ts', patch: 'x' }]),
|
|
150
|
+
});
|
|
151
|
+
const result = await service.loadChanges({
|
|
152
|
+
sessionID: 'ses_1',
|
|
153
|
+
messageID: 'msg_1',
|
|
154
|
+
file: 'missing.ts',
|
|
155
|
+
});
|
|
156
|
+
expect(result).toEqual({ ok: false, error: 'change_not_found' });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('maps abort errors to aborted', async () => {
|
|
160
|
+
const service = createSessionChangesService({
|
|
161
|
+
fetchMessage: vi.fn(async () => {
|
|
162
|
+
const error = new Error('aborted');
|
|
163
|
+
error.name = 'AbortError';
|
|
164
|
+
throw error;
|
|
165
|
+
}),
|
|
166
|
+
fetchDiff: vi.fn(),
|
|
167
|
+
});
|
|
168
|
+
const result = await service.loadChanges({
|
|
169
|
+
sessionID: 'ses_1',
|
|
170
|
+
messageID: 'msg_1',
|
|
171
|
+
});
|
|
172
|
+
expect(result).toEqual({ ok: false, error: 'aborted' });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('maps malformed/upstream failures to upstream', async () => {
|
|
176
|
+
const service = createSessionChangesService({
|
|
177
|
+
fetchMessage: vi.fn(),
|
|
178
|
+
fetchDiff: vi.fn(async () => ({ not: 'array' })),
|
|
179
|
+
logger: { warn: vi.fn() },
|
|
180
|
+
});
|
|
181
|
+
const result = await service.loadChanges({
|
|
182
|
+
sessionID: 'ses_1',
|
|
183
|
+
messageID: 'msg_1',
|
|
184
|
+
file: 'a.ts',
|
|
185
|
+
});
|
|
186
|
+
expect(result).toEqual({ ok: false, error: 'upstream' });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -209,6 +209,54 @@ describe('registerSessionTurnPageRoutes — reconcile', () => {
|
|
|
209
209
|
});
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
it('summarizes message diff snapshots while preserving full reconcile parts', async () => {
|
|
213
|
+
const patch = 'diff-body-'.repeat(20_000);
|
|
214
|
+
const reconcile = vi.fn(async () => successPage({
|
|
215
|
+
records: [{
|
|
216
|
+
info: {
|
|
217
|
+
id: 'msg_u2',
|
|
218
|
+
role: 'user',
|
|
219
|
+
summary: {
|
|
220
|
+
diffs: [{
|
|
221
|
+
file: 'src/large.ts',
|
|
222
|
+
status: 'modified',
|
|
223
|
+
additions: 5,
|
|
224
|
+
deletions: 1,
|
|
225
|
+
patch,
|
|
226
|
+
}],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
parts: [{ id: 'prt_1', type: 'text', text: 'full recovery text' }],
|
|
230
|
+
}],
|
|
231
|
+
}));
|
|
232
|
+
const { app, route } = registry();
|
|
233
|
+
registerSessionTurnPageRoutes(app, {
|
|
234
|
+
sessionTurnPageService: { loadPage: vi.fn() },
|
|
235
|
+
sessionReconcileService: { reconcile },
|
|
236
|
+
});
|
|
237
|
+
const res = response();
|
|
238
|
+
|
|
239
|
+
await route('GET', RECONCILE_ROUTE)({
|
|
240
|
+
params: { sessionID: 'ses_42' },
|
|
241
|
+
query: { anchor: 'msg_u2' },
|
|
242
|
+
headers: {},
|
|
243
|
+
}, res);
|
|
244
|
+
|
|
245
|
+
expect(res.statusCode).toBe(200);
|
|
246
|
+
expect(res.body.records[0].info.summary).toEqual({
|
|
247
|
+
diffCount: 1,
|
|
248
|
+
hasDiffs: true,
|
|
249
|
+
});
|
|
250
|
+
expect(res.body.records[0].info.summary.diffs).toBeUndefined();
|
|
251
|
+
expect(res.body.records[0].parts[0]).toEqual({
|
|
252
|
+
id: 'prt_1',
|
|
253
|
+
type: 'text',
|
|
254
|
+
text: 'full recovery text',
|
|
255
|
+
});
|
|
256
|
+
expect(JSON.stringify(res.body)).not.toContain(patch);
|
|
257
|
+
expect(JSON.stringify(res.body)).not.toContain('src/large.ts');
|
|
258
|
+
});
|
|
259
|
+
|
|
212
260
|
it('returns HTTP 200 for resetRequired (anchor lost / budget rebuild)', async () => {
|
|
213
261
|
const reconcile = vi.fn(async () => successPage({
|
|
214
262
|
records: [],
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
16
16
|
|
|
17
|
+
import { projectMessageDiffSummaries } from './service.js';
|
|
18
|
+
|
|
17
19
|
/** Host-owned signed reconcile continuation version prefix. */
|
|
18
20
|
const CONTINUATION_PREFIX = 'ocr2.';
|
|
19
21
|
|
|
@@ -666,7 +668,7 @@ export const createSessionReconcileService = ({
|
|
|
666
668
|
state.scannedRecords += records.length;
|
|
667
669
|
state.scannedBytes += measureRecordsBytes(records);
|
|
668
670
|
|
|
669
|
-
candidates.unshift(...prepend);
|
|
671
|
+
candidates.unshift(...projectMessageDiffSummaries(prepend));
|
|
670
672
|
|
|
671
673
|
if (state.returnedThroughID) {
|
|
672
674
|
applyReturnedBoundary();
|
|
@@ -481,6 +481,46 @@ describe('createSessionReconcileService', () => {
|
|
|
481
481
|
expect(result.records.length).toBeLessThan(5);
|
|
482
482
|
});
|
|
483
483
|
|
|
484
|
+
it('counts summarized message diff snapshots against the reconcile byte budget', async () => {
|
|
485
|
+
const patch = 'diff-body-'.repeat(20_000);
|
|
486
|
+
const fetchPage = vi.fn(async () => pageResult([
|
|
487
|
+
user('msg_u1', [], {
|
|
488
|
+
summary: {
|
|
489
|
+
diffs: [{
|
|
490
|
+
file: 'src/large.ts',
|
|
491
|
+
status: 'modified',
|
|
492
|
+
additions: 7,
|
|
493
|
+
deletions: 2,
|
|
494
|
+
patch,
|
|
495
|
+
}],
|
|
496
|
+
},
|
|
497
|
+
}),
|
|
498
|
+
assistant('msg_a1'),
|
|
499
|
+
]));
|
|
500
|
+
const service = createSessionReconcileService(serviceOptions(fetchPage, {
|
|
501
|
+
pageRecordLimit: 10,
|
|
502
|
+
pageByteLimit: 1024,
|
|
503
|
+
totalPageLimit: 10,
|
|
504
|
+
totalByteLimit: 10_000,
|
|
505
|
+
}));
|
|
506
|
+
|
|
507
|
+
const result = await service.reconcile({
|
|
508
|
+
sessionID: 'ses_1',
|
|
509
|
+
directory: '/repo',
|
|
510
|
+
anchor: 'msg_u1',
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
expect(result.ok).toBe(true);
|
|
514
|
+
expect(result.records[0].info.summary).toEqual({
|
|
515
|
+
diffCount: 1,
|
|
516
|
+
hasDiffs: true,
|
|
517
|
+
});
|
|
518
|
+
expect(result.records[0].info.summary.diffs).toBeUndefined();
|
|
519
|
+
expect(result.responseBytes).toBeLessThanOrEqual(1024);
|
|
520
|
+
expect(JSON.stringify(result.records)).not.toContain(patch);
|
|
521
|
+
expect(JSON.stringify(result.records)).not.toContain('src/large.ts');
|
|
522
|
+
});
|
|
523
|
+
|
|
484
524
|
it('rejects continuation that does not bind runtime/directory/session/anchor/head', async () => {
|
|
485
525
|
const fetchPage = vi.fn(async () => pageResult([user('msg_u1')], null));
|
|
486
526
|
const service = createSessionReconcileService(serviceOptions(fetchPage));
|