@openchambery/web 1.18.2-beta.2 → 1.18.2-beta.21
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-B4fj9PIe.js} +1 -1
- package/dist/assets/{AssistantView-DvoyOGkx.js → AssistantView-C00aiEHK.js} +1 -1
- package/dist/assets/{ChatContainer-D6yfuigV.js → ChatContainer-BDFeDKu-.js} +39 -39
- package/dist/assets/{DiagramView-MGGo9iME.js → DiagramView-CUfSFI3e.js} +1 -1
- package/dist/assets/{FadeInOnReveal-aeWlaqVB.js → FadeInOnReveal-CKKU6fKI.js} +1 -1
- package/dist/assets/{Icon-AXqa-fYB.js → Icon-DiIuWOuC.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-jVOkZkyX.js → MarkdownRendererImpl-Cen6E9Lj.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-Q2IzHqGA.js → MobileDetailNavigation-DILi0tN7.js} +1 -1
- package/dist/assets/{MobileShareBridge-apXM34Lb.js → MobileShareBridge-PZMRiieI.js} +1 -1
- package/dist/assets/MobileSurface-BLYcR0tO.js +2 -0
- package/dist/assets/{MultiRunWindow-B-l_O-bd.js → MultiRunWindow-D_2mWZyv.js} +1 -1
- package/dist/assets/{OnboardingScreen-CUsgwt4y.js → OnboardingScreen-CYeRZznl.js} +1 -1
- package/dist/assets/{OverlayScrollbar-D6NySAxi.js → OverlayScrollbar-BgMLtMIq.js} +1 -1
- package/dist/assets/SettingsView-CbDBfQp1.js +25 -0
- package/dist/assets/{SettingsWindow-DfVgTQ4I.js → SettingsWindow-Br7XLcSd.js} +1 -1
- package/dist/assets/{TerminalView-DNyjfkvT.js → TerminalView-CM5jRKlj.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DLG1iThm.js → ToolOutputDialog-D3U6aTZ4.js} +1 -1
- package/dist/assets/appThemeRegistry-CDxBJNNB.js +2 -0
- package/dist/assets/assistant-guide-hero-BeUHQcxE.jpg +0 -0
- package/dist/assets/{button-CDvMVaiO.js → button-DaAoVF4X.js} +1 -1
- package/dist/assets/{clipboard-BW7nk_Cx.js → clipboard-BnElHSnO.js} +1 -1
- package/dist/assets/{collapsible-C2aamDvD.js → collapsible-BmuQu0dq.js} +1 -1
- package/dist/assets/{context-menu-BaxmodD8.js → context-menu-DBOMervd.js} +1 -1
- package/dist/assets/{desktopHosts-CBoBpJ1Z.js → desktopHosts-ZK_vkdzL.js} +1 -1
- package/dist/assets/{desktopRelayRestore-l2R9muux.js → desktopRelayRestore-CewM6-g5.js} +1 -1
- package/dist/assets/{diagram-CEon0BQs.js → diagram-DAE4CZy2.js} +1 -1
- package/dist/assets/{dialog-DjYfj1Sn.js → dialog-COrLBm9O.js} +1 -1
- package/dist/assets/{es-5Lhudh0n.js → es-DIVYdoxi.js} +1 -1
- package/dist/assets/{fileQueries-CE_DaZWQ.js → fileQueries-CmWYnRVz.js} +1 -1
- package/dist/assets/{fr-Q_UhFmKT.js → fr-CNTlVypq.js} +2 -2
- package/dist/assets/{gitApi-BYxg_qGJ.js → gitApi-BQT3QXD2.js} +1 -1
- package/dist/assets/gitApiHttp-BxGAFLTf.js +2 -0
- package/dist/assets/{input-k9DGGyZ6.js → input-XiL2-2gG.js} +1 -1
- package/dist/assets/insertionBoundaries-9a0RuIDc.js +1 -0
- package/dist/assets/{ja-CzMPz7bT.js → ja-C_eW5ZM8.js} +1 -1
- package/dist/assets/{ko-B-EslUsR.js → ko-CE3cOv7v.js} +1 -1
- package/dist/assets/{main-LDphyc-v.js → main-BGfX81Y9.js} +20 -20
- package/dist/assets/{main-CO6z5yRB.js → main-CEqSDnE2.js} +2 -2
- package/dist/assets/{markdown-worker-DkeWiER6.js → markdown-worker-BDyY0vt6.js} +1 -1
- package/dist/assets/{markdownTheme-DNT93i5L.js → markdownTheme-D-aLQZaM.js} +1 -1
- package/dist/assets/{miniChat-BZQwTZpx.js → miniChat-CKucSAhg.js} +2 -2
- package/dist/assets/mobile-C4bTWKry.js +2 -0
- package/dist/assets/{multirun-ZlwlkIy5.js → multirun-DeNtSkgb.js} +1 -1
- package/dist/assets/{persistence-DPNMIbF5.js → persistence-D1lIuyqO.js} +1 -1
- package/dist/assets/{pl-Dnea6Ppc.js → pl-BZKZOE99.js} +1 -1
- package/dist/assets/projectMeta-BunN9pH5.js +3 -0
- package/dist/assets/{pt-BR-Ca6QTem7.js → pt-BR-DHDgp8S3.js} +1 -1
- package/dist/assets/{radio-CJuhbQ5a.js → radio-Bziafaje.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-D8-FDgWE.js +2 -0
- package/dist/assets/renderMobileApp-CdU0aJ-e.js +23 -0
- package/dist/assets/{runtimeConfig-BtWi74Pm.js → runtimeConfig-BXCt7ZsQ.js} +1 -1
- package/dist/assets/runtimeEndpointReset-CK1GhIsm.js +145 -0
- package/dist/assets/{sortable-tabs-strip-D8JHUI0z.js → sortable-tabs-strip-pWlBorwQ.js} +1 -1
- package/dist/assets/src-8kQFmtjw.css +2 -0
- package/dist/assets/{store-wa1ersRf.js → store-CUWqzRt7.js} +2 -2
- package/dist/assets/{terminalApi-tB3qLcfa.js → terminalApi-DpiAJlil.js} +1 -1
- package/dist/assets/{tooltip-DMNR5rN0.js → tooltip-BLugzWq-.js} +1 -1
- package/dist/assets/{uk-xGc9Pzge.js → uk-CfQK6cz1.js} +1 -1
- package/dist/assets/{url-7MqoLs9X.js → url-DMs_PgIC.js} +1 -1
- package/dist/assets/useAppFontEffects-IEUQDvE8.js +7 -0
- package/dist/assets/useDesktopWindowControlsLayout-MlqJX89k.js +1 -0
- package/dist/assets/{useEffectiveDirectory-aUC3zzl_.js → useEffectiveDirectory-GbhkdO6m.js} +1 -1
- package/dist/assets/{useFontPreferences-CIurCeNv.js → useFontPreferences-92Qg-98H.js} +1 -1
- package/dist/assets/useMobileNavigationStore-DQHF_ETu.js +476 -0
- package/dist/assets/useSessionAutoCleanup-BuU_S_Sh.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CiYQjBe6.js → useWorkerHighlightedLines-Ca6OgNHF.js} +1 -1
- package/dist/assets/{utils-Co19EJEL.js → utils-BxJEIWv5.js} +1 -1
- package/dist/assets/{zh-CN-sL4ktGB3.js → zh-CN-DvY2fKWp.js} +1 -1
- package/dist/assets/{zh-TW-Cfxps4gq.js → zh-TW-eWe0lm3-.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 +13 -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 +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 +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-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/MobileSurface-htGkf7Ua.js +0 -2
- package/dist/assets/SettingsView-DxMpVLlt.js +0 -25
- package/dist/assets/appThemeRegistry-CgfOR_0L.js +0 -2
- package/dist/assets/gitApiHttp-D8j1qRH4.js +0 -2
- package/dist/assets/insertionBoundaries-DzXMTleb.js +0 -1
- package/dist/assets/mobile-BktO3_4R.js +0 -2
- package/dist/assets/projectMeta-v-LP2mzz.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-Db8uO-6q.js +0 -2
- package/dist/assets/renderMobileApp-BCV81Ryl.js +0 -23
- package/dist/assets/runtimeEndpointReset-Dg3DMy9v.js +0 -145
- package/dist/assets/src-oHrYIqHF.css +0 -2
- package/dist/assets/useAppFontEffects-CM0VKsdo.js +0 -7
- package/dist/assets/useDesktopWindowControlsLayout-Cq-r4Hcb.js +0 -1
- package/dist/assets/useMobileNavigationStore-DnzT4jO2.js +0 -475
- package/dist/assets/useSessionAutoCleanup-DxZeceKO.js +0 -1
|
@@ -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,58 @@ 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
|
+
diffs: [{
|
|
248
|
+
file: 'src/large.ts',
|
|
249
|
+
status: 'modified',
|
|
250
|
+
additions: 5,
|
|
251
|
+
deletions: 1,
|
|
252
|
+
}],
|
|
253
|
+
diffCount: 1,
|
|
254
|
+
hasDiffs: true,
|
|
255
|
+
});
|
|
256
|
+
expect(res.body.records[0].parts[0]).toEqual({
|
|
257
|
+
id: 'prt_1',
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: 'full recovery text',
|
|
260
|
+
});
|
|
261
|
+
expect(JSON.stringify(res.body)).not.toContain(patch);
|
|
262
|
+
});
|
|
263
|
+
|
|
212
264
|
it('returns HTTP 200 for resetRequired (anchor lost / budget rebuild)', async () => {
|
|
213
265
|
const reconcile = vi.fn(async () => successPage({
|
|
214
266
|
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,50 @@ 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
|
+
diffs: [{
|
|
516
|
+
file: 'src/large.ts',
|
|
517
|
+
status: 'modified',
|
|
518
|
+
additions: 7,
|
|
519
|
+
deletions: 2,
|
|
520
|
+
}],
|
|
521
|
+
diffCount: 1,
|
|
522
|
+
hasDiffs: true,
|
|
523
|
+
});
|
|
524
|
+
expect(result.responseBytes).toBeLessThanOrEqual(1024);
|
|
525
|
+
expect(JSON.stringify(result.records)).not.toContain(patch);
|
|
526
|
+
});
|
|
527
|
+
|
|
484
528
|
it('rejects continuation that does not bind runtime/directory/session/anchor/head', async () => {
|
|
485
529
|
const fetchPage = vi.fn(async () => pageResult([user('msg_u1')], null));
|
|
486
530
|
const service = createSessionReconcileService(serviceOptions(fetchPage));
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
2
|
+
import { projectMessageSummaryDiffCounts } from '../event-stream/diff-summary.js';
|
|
3
|
+
import {
|
|
4
|
+
createSessionChangesService,
|
|
5
|
+
} from './changes.service.js';
|
|
2
6
|
import {
|
|
3
7
|
createSessionReconcileService,
|
|
4
8
|
MAX_ANCHOR_LENGTH,
|
|
@@ -6,6 +10,7 @@ import {
|
|
|
6
10
|
} from './reconcile.service.js';
|
|
7
11
|
import {
|
|
8
12
|
createSessionTurnPageService,
|
|
13
|
+
projectMessageDiffSummaries,
|
|
9
14
|
projectSlimParts,
|
|
10
15
|
SLIM_PARTS_PROJECTION,
|
|
11
16
|
} from './service.js';
|
|
@@ -76,15 +81,27 @@ const RECONCILE_TOTAL_BYTE_LIMIT = (() => {
|
|
|
76
81
|
/** Test/inspect helper — resolved host-local scan chunk. */
|
|
77
82
|
export const getInnerSessionTurnScanLimit = () => _inner_scanLimit;
|
|
78
83
|
|
|
84
|
+
const MESSAGE_ID_MIN = 1;
|
|
85
|
+
const MESSAGE_ID_MAX = 512;
|
|
86
|
+
const SESSION_ID_MIN = 1;
|
|
87
|
+
const SESSION_ID_MAX = 512;
|
|
88
|
+
const FILE_PATH_MIN = 1;
|
|
89
|
+
const FILE_PATH_MAX = 4096;
|
|
90
|
+
const EXACT_MESSAGE_TIMEOUT_MS = 45_000;
|
|
91
|
+
const CHANGES_TIMEOUT_MS = 45_000;
|
|
92
|
+
|
|
79
93
|
/** Safe client-facing error strings — no paths, tokens, or upstream bodies. */
|
|
80
94
|
const SAFE_ERRORS = {
|
|
81
95
|
invalid_turns: 'turns must be an integer between 1 and 10',
|
|
82
96
|
invalid_scan_limit: 'scanLimit must be an integer between 10 and 200',
|
|
83
97
|
invalid_session: 'sessionID is required',
|
|
98
|
+
invalid_message: 'messageID is required',
|
|
99
|
+
invalid_file: 'file is invalid',
|
|
84
100
|
invalid_cursor: 'invalid cursor',
|
|
85
101
|
invalid_anchor: 'anchor is required and must be a non-empty message id',
|
|
86
102
|
invalid_continuation: 'invalid continuation',
|
|
87
103
|
invalid_reconcile_params: 'provide exactly one of anchor or continuation',
|
|
104
|
+
change_not_found: 'change file not found',
|
|
88
105
|
upstream: 'upstream',
|
|
89
106
|
unavailable: 'upstream unavailable',
|
|
90
107
|
aborted: 'aborted',
|
|
@@ -97,6 +114,23 @@ const SAFE_ERRORS = {
|
|
|
97
114
|
internal: 'internal error',
|
|
98
115
|
};
|
|
99
116
|
|
|
117
|
+
/** Reject control characters / NUL in change file selectors. */
|
|
118
|
+
const hasControlOrNul = (value) => {
|
|
119
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
120
|
+
const code = value.charCodeAt(i);
|
|
121
|
+
if (code === 0 || (code >= 1 && code <= 31) || code === 127) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isBoundedId = (value, { min = MESSAGE_ID_MIN, max = MESSAGE_ID_MAX } = {}) => (
|
|
129
|
+
typeof value === 'string'
|
|
130
|
+
&& value.length >= min
|
|
131
|
+
&& value.length <= max
|
|
132
|
+
);
|
|
133
|
+
|
|
100
134
|
const parsePositiveInt = (value) => {
|
|
101
135
|
if (value === undefined || value === null || value === '') return null;
|
|
102
136
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
@@ -172,11 +206,22 @@ const requestSignal = (req, res) => {
|
|
|
172
206
|
return controller.signal;
|
|
173
207
|
};
|
|
174
208
|
|
|
209
|
+
const createSdkClient = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders }) => {
|
|
210
|
+
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
211
|
+
const headers = typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {};
|
|
212
|
+
return createOpencodeClient({ baseUrl, headers });
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const throwUpstream = (logger, label) => {
|
|
216
|
+
logger?.warn?.(label);
|
|
217
|
+
const error = new Error('upstream');
|
|
218
|
+
error.code = 'upstream';
|
|
219
|
+
throw error;
|
|
220
|
+
};
|
|
221
|
+
|
|
175
222
|
const createSdkFetchPage = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }) => {
|
|
176
223
|
return async ({ sessionID, directory, before, limit, signal }) => {
|
|
177
|
-
const
|
|
178
|
-
const headers = typeof getOpenCodeAuthHeaders === 'function' ? getOpenCodeAuthHeaders() : {};
|
|
179
|
-
const client = createOpencodeClient({ baseUrl, headers });
|
|
224
|
+
const client = createSdkClient({ buildOpenCodeUrl, getOpenCodeAuthHeaders });
|
|
180
225
|
|
|
181
226
|
const result = await client.session.messages({
|
|
182
227
|
sessionID,
|
|
@@ -186,10 +231,7 @@ const createSdkFetchPage = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }
|
|
|
186
231
|
}, { signal });
|
|
187
232
|
|
|
188
233
|
if (result?.error) {
|
|
189
|
-
logger
|
|
190
|
-
const error = new Error('upstream');
|
|
191
|
-
error.code = 'upstream';
|
|
192
|
-
throw error;
|
|
234
|
+
throwUpstream(logger, '[session-turn-pages] session.messages SDK error');
|
|
193
235
|
}
|
|
194
236
|
|
|
195
237
|
const data = result?.data;
|
|
@@ -199,10 +241,7 @@ const createSdkFetchPage = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }
|
|
|
199
241
|
? data.items
|
|
200
242
|
: null;
|
|
201
243
|
if (!records) {
|
|
202
|
-
logger
|
|
203
|
-
const error = new Error('upstream');
|
|
204
|
-
error.code = 'upstream';
|
|
205
|
-
throw error;
|
|
244
|
+
throwUpstream(logger, '[session-turn-pages] session.messages malformed payload');
|
|
206
245
|
}
|
|
207
246
|
|
|
208
247
|
const headerCursor = result?.response?.headers?.get?.('x-next-cursor');
|
|
@@ -218,6 +257,70 @@ const createSdkFetchPage = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }
|
|
|
218
257
|
};
|
|
219
258
|
};
|
|
220
259
|
|
|
260
|
+
const createSdkFetchMessage = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }) => {
|
|
261
|
+
return async ({ sessionID, messageID, directory, signal }) => {
|
|
262
|
+
const client = createSdkClient({ buildOpenCodeUrl, getOpenCodeAuthHeaders });
|
|
263
|
+
const result = await client.session.message({
|
|
264
|
+
sessionID,
|
|
265
|
+
messageID,
|
|
266
|
+
...(typeof directory === 'string' && directory.length > 0 ? { directory } : {}),
|
|
267
|
+
}, { signal });
|
|
268
|
+
|
|
269
|
+
if (result?.error) {
|
|
270
|
+
const status = result?.response?.status;
|
|
271
|
+
if (status === 404) {
|
|
272
|
+
const error = new Error('not_found');
|
|
273
|
+
error.code = 'not_found';
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
throwUpstream(logger, '[session-turn-pages] session.message SDK error');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const data = result?.data;
|
|
280
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
281
|
+
throwUpstream(logger, '[session-turn-pages] session.message malformed payload');
|
|
282
|
+
}
|
|
283
|
+
return data;
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const createSdkFetchDiff = ({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }) => {
|
|
288
|
+
return async ({ sessionID, messageID, directory, signal }) => {
|
|
289
|
+
const client = createSdkClient({ buildOpenCodeUrl, getOpenCodeAuthHeaders });
|
|
290
|
+
const result = await client.session.diff({
|
|
291
|
+
sessionID,
|
|
292
|
+
messageID,
|
|
293
|
+
...(typeof directory === 'string' && directory.length > 0 ? { directory } : {}),
|
|
294
|
+
}, { signal });
|
|
295
|
+
|
|
296
|
+
if (result?.error) {
|
|
297
|
+
throwUpstream(logger, '[session-changes] session.diff SDK error');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const data = result?.data;
|
|
301
|
+
if (!Array.isArray(data)) {
|
|
302
|
+
throwUpstream(logger, '[session-changes] session.diff malformed payload');
|
|
303
|
+
}
|
|
304
|
+
return data;
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Project exact message GET payload: keep original `{ info, parts }` shape,
|
|
310
|
+
* only L1-project `info.summary.diffs` → slim file list + diffCount/hasDiffs.
|
|
311
|
+
*/
|
|
312
|
+
export const projectExactMessagePayload = (payload) => {
|
|
313
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
314
|
+
return payload;
|
|
315
|
+
}
|
|
316
|
+
if (payload.info && typeof payload.info === 'object' && !Array.isArray(payload.info)) {
|
|
317
|
+
const nextInfo = projectMessageSummaryDiffCounts(payload.info);
|
|
318
|
+
if (nextInfo === payload.info) return payload;
|
|
319
|
+
return { ...payload, info: nextInfo };
|
|
320
|
+
}
|
|
321
|
+
return projectMessageSummaryDiffCounts(payload);
|
|
322
|
+
};
|
|
323
|
+
|
|
221
324
|
const mapServiceError = (error) => {
|
|
222
325
|
const code = typeof error === 'string' ? error : String(error ?? 'upstream');
|
|
223
326
|
if (
|
|
@@ -265,6 +368,21 @@ const mapServiceError = (error) => {
|
|
|
265
368
|
if (code === 'invalid_reconcile_params') {
|
|
266
369
|
return { status: 400, body: { error: SAFE_ERRORS.invalid_reconcile_params } };
|
|
267
370
|
}
|
|
371
|
+
if (code === 'invalid_message' || code === 'invalid_params') {
|
|
372
|
+
return { status: 400, body: { error: SAFE_ERRORS.invalid_message, code: 'invalid_message' } };
|
|
373
|
+
}
|
|
374
|
+
if (code === 'invalid_file') {
|
|
375
|
+
return { status: 400, body: { error: SAFE_ERRORS.invalid_file, code: 'invalid_file' } };
|
|
376
|
+
}
|
|
377
|
+
if (code === 'change_not_found') {
|
|
378
|
+
return {
|
|
379
|
+
status: 404,
|
|
380
|
+
body: { error: SAFE_ERRORS.change_not_found, code: 'change_not_found' },
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (code === 'not_found') {
|
|
384
|
+
return { status: 404, body: { error: 'not found', code: 'not_found' } };
|
|
385
|
+
}
|
|
268
386
|
return { status: 502, body: { error: SAFE_ERRORS.upstream } };
|
|
269
387
|
};
|
|
270
388
|
|
|
@@ -291,6 +409,8 @@ const logInternalError = (logger, error, context = {}) => {
|
|
|
291
409
|
* Register OpenChamber-owned session message routes:
|
|
292
410
|
* - GET /api/openchamber/sessions/:sessionID/messages
|
|
293
411
|
* - GET /api/openchamber/sessions/:sessionID/messages/reconcile
|
|
412
|
+
* - GET /api/openchamber/sessions/:sessionID/changes
|
|
413
|
+
* - GET /api/session/:sessionID/message/:messageID (L1 exact message; before proxy)
|
|
294
414
|
*
|
|
295
415
|
* Global /api auth is enforced by core-routes requireApiAuth before feature
|
|
296
416
|
* routes. This module does not add redundant auth middleware.
|
|
@@ -303,6 +423,8 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
303
423
|
const {
|
|
304
424
|
sessionTurnPageService: injectedService,
|
|
305
425
|
sessionReconcileService: injectedReconcileService,
|
|
426
|
+
sessionChangesService: injectedChangesService,
|
|
427
|
+
fetchExactMessage: injectedFetchExactMessage,
|
|
306
428
|
buildOpenCodeUrl,
|
|
307
429
|
getOpenCodeAuthHeaders,
|
|
308
430
|
logger = console,
|
|
@@ -332,6 +454,15 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
332
454
|
scanLimit: _inner_scanLimit,
|
|
333
455
|
});
|
|
334
456
|
|
|
457
|
+
const fetchExactMessage = injectedFetchExactMessage
|
|
458
|
+
?? createSdkFetchMessage({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger });
|
|
459
|
+
|
|
460
|
+
const changesService = injectedChangesService ?? createSessionChangesService({
|
|
461
|
+
fetchMessage: fetchExactMessage,
|
|
462
|
+
fetchDiff: createSdkFetchDiff({ buildOpenCodeUrl, getOpenCodeAuthHeaders, logger }),
|
|
463
|
+
logger,
|
|
464
|
+
});
|
|
465
|
+
|
|
335
466
|
// More-specific path first (before generic messages route).
|
|
336
467
|
app.get('/api/openchamber/sessions/:sessionID/messages/reconcile', async (req, res) => {
|
|
337
468
|
const sessionID = req.params?.sessionID;
|
|
@@ -379,7 +510,7 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
379
510
|
}
|
|
380
511
|
|
|
381
512
|
return res.status(200).json({
|
|
382
|
-
records: Array.isArray(result.records) ? result.records : [],
|
|
513
|
+
records: projectMessageDiffSummaries(Array.isArray(result.records) ? result.records : []),
|
|
383
514
|
anchorFound: result.anchorFound === true,
|
|
384
515
|
capturedHeadMessageID: result.capturedHeadMessageID ?? null,
|
|
385
516
|
latestHeadMessageID: result.latestHeadMessageID ?? null,
|
|
@@ -491,4 +622,134 @@ export const registerSessionTurnPageRoutes = (app, dependencies = {}) => {
|
|
|
491
622
|
timed.clear();
|
|
492
623
|
}
|
|
493
624
|
});
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Exact message GET — registered before generic `/api` proxy so materialize /
|
|
628
|
+
* recovery never relay raw multi-megabyte patch bodies in `summary.diffs`.
|
|
629
|
+
* Returns the official `{ info, parts }` shape with L1 slim-list projection.
|
|
630
|
+
*/
|
|
631
|
+
app.get('/api/session/:sessionID/message/:messageID', async (req, res) => {
|
|
632
|
+
const sessionID = req.params?.sessionID;
|
|
633
|
+
const messageID = req.params?.messageID;
|
|
634
|
+
if (!isBoundedId(sessionID, { min: SESSION_ID_MIN, max: SESSION_ID_MAX })) {
|
|
635
|
+
return res.status(400).json({ error: SAFE_ERRORS.invalid_session, code: 'invalid_session' });
|
|
636
|
+
}
|
|
637
|
+
if (!isBoundedId(messageID, { min: MESSAGE_ID_MIN, max: MESSAGE_ID_MAX })) {
|
|
638
|
+
return res.status(400).json({ error: SAFE_ERRORS.invalid_message, code: 'invalid_message' });
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const directory = typeof req.query?.directory === 'string' && req.query.directory.length > 0
|
|
642
|
+
? req.query.directory
|
|
643
|
+
: undefined;
|
|
644
|
+
|
|
645
|
+
const parentSignal = requestSignal(req, res);
|
|
646
|
+
const timed = timeoutSignal(EXACT_MESSAGE_TIMEOUT_MS, parentSignal);
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
const payload = await fetchExactMessage({
|
|
650
|
+
sessionID,
|
|
651
|
+
messageID,
|
|
652
|
+
directory,
|
|
653
|
+
signal: timed.signal,
|
|
654
|
+
});
|
|
655
|
+
return res.status(200).json(projectExactMessagePayload(payload));
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (error?.name === 'AbortError' || error?.code === 'aborted') {
|
|
658
|
+
if (!res.headersSent) {
|
|
659
|
+
return res.status(499).json({ error: SAFE_ERRORS.aborted });
|
|
660
|
+
}
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
if (error?.code === 'not_found') {
|
|
664
|
+
if (!res.headersSent) {
|
|
665
|
+
return res.status(404).json({ error: 'not found', code: 'not_found' });
|
|
666
|
+
}
|
|
667
|
+
return undefined;
|
|
668
|
+
}
|
|
669
|
+
logger?.warn?.('[session-turn-pages] exact message failed', {
|
|
670
|
+
sessionID,
|
|
671
|
+
messageID,
|
|
672
|
+
hasDirectory: directory != null,
|
|
673
|
+
});
|
|
674
|
+
if (!res.headersSent) {
|
|
675
|
+
return res.status(502).json({ error: SAFE_ERRORS.upstream });
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
} finally {
|
|
679
|
+
timed.clear();
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Changes API — L2 file list (no `file`) or L3 single-file patch (`file=`).
|
|
685
|
+
* Never returns the full message envelope or unrelated file patches.
|
|
686
|
+
*/
|
|
687
|
+
app.get('/api/openchamber/sessions/:sessionID/changes', async (req, res) => {
|
|
688
|
+
const sessionID = req.params?.sessionID;
|
|
689
|
+
if (!isBoundedId(sessionID, { min: SESSION_ID_MIN, max: SESSION_ID_MAX })) {
|
|
690
|
+
return res.status(400).json({ error: SAFE_ERRORS.invalid_session, code: 'invalid_session' });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const messageIDRaw = req.query?.messageID;
|
|
694
|
+
if (!isBoundedId(messageIDRaw, { min: MESSAGE_ID_MIN, max: MESSAGE_ID_MAX })) {
|
|
695
|
+
return res.status(400).json({ error: SAFE_ERRORS.invalid_message, code: 'invalid_message' });
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const fileRaw = req.query?.file;
|
|
699
|
+
let file;
|
|
700
|
+
if (fileRaw !== undefined && fileRaw !== null && fileRaw !== '') {
|
|
701
|
+
if (
|
|
702
|
+
typeof fileRaw !== 'string'
|
|
703
|
+
|| fileRaw.length < FILE_PATH_MIN
|
|
704
|
+
|| fileRaw.length > FILE_PATH_MAX
|
|
705
|
+
|| hasControlOrNul(fileRaw)
|
|
706
|
+
) {
|
|
707
|
+
return res.status(400).json({ error: SAFE_ERRORS.invalid_file, code: 'invalid_file' });
|
|
708
|
+
}
|
|
709
|
+
file = fileRaw;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const directory = typeof req.query?.directory === 'string' && req.query.directory.length > 0
|
|
713
|
+
? req.query.directory
|
|
714
|
+
: undefined;
|
|
715
|
+
|
|
716
|
+
const parentSignal = requestSignal(req, res);
|
|
717
|
+
const timed = timeoutSignal(CHANGES_TIMEOUT_MS, parentSignal);
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
const result = await changesService.loadChanges({
|
|
721
|
+
sessionID,
|
|
722
|
+
messageID: messageIDRaw,
|
|
723
|
+
directory,
|
|
724
|
+
file,
|
|
725
|
+
signal: timed.signal,
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
if (!result?.ok) {
|
|
729
|
+
const mapped = mapServiceError(result?.error);
|
|
730
|
+
return res.status(mapped.status).json(mapped.body);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return res.status(200).json(result.body);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (error?.name === 'AbortError') {
|
|
736
|
+
if (!res.headersSent) {
|
|
737
|
+
return res.status(499).json({ error: SAFE_ERRORS.aborted });
|
|
738
|
+
}
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
logger?.warn?.('[session-changes] loadChanges failed', {
|
|
742
|
+
sessionID,
|
|
743
|
+
messageID: messageIDRaw,
|
|
744
|
+
hasDirectory: directory != null,
|
|
745
|
+
hasFile: file != null,
|
|
746
|
+
});
|
|
747
|
+
if (!res.headersSent) {
|
|
748
|
+
return res.status(502).json({ error: SAFE_ERRORS.upstream });
|
|
749
|
+
}
|
|
750
|
+
return undefined;
|
|
751
|
+
} finally {
|
|
752
|
+
timed.clear();
|
|
753
|
+
}
|
|
754
|
+
});
|
|
494
755
|
};
|