@openchambery/web 1.19.21-beta.1 → 2.0.0-beta.1

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.
Files changed (195) hide show
  1. package/README.md +13 -11
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +165 -0
  6. package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
  7. package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
  8. package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
  10. package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
  13. package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
  15. package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
  16. package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
  17. package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
  19. package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
  20. package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
  21. package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
  22. package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
  24. package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
  25. package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
  26. package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
  27. package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
  28. package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
  29. package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
  30. package/dist/assets/es-c52LTHHe.js +15 -0
  31. package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
  32. package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
  33. package/dist/assets/gitApi-ButwzQ0N.js +9 -0
  34. package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
  36. package/dist/assets/ja-C8Ikun3Z.js +15 -0
  37. package/dist/assets/ko-ClFys5o2.js +15 -0
  38. package/dist/assets/main-CCE_4y5F.js +2 -0
  39. package/dist/assets/main-Dn9rpsgl.js +169 -0
  40. package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
  41. package/dist/assets/miniChat-BpjxVqgg.js +2 -0
  42. package/dist/assets/mobile-nMJCaNq1.js +2 -0
  43. package/dist/assets/multirun-BvSG94mW.js +4 -0
  44. package/dist/assets/pl-CnaKWN5F.js +15 -0
  45. package/dist/assets/platform-CWTIp02q.js +4 -0
  46. package/dist/assets/projectMeta-DOe49dM_.js +3 -0
  47. package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
  48. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
  49. package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
  50. package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
  51. package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
  52. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
  53. package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
  54. package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
  55. package/dist/assets/src-BzX5UI-c.css +2 -0
  56. package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
  57. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
  58. package/dist/assets/textarea-3XR8sUfy.js +1 -0
  59. package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
  60. package/dist/assets/uk-CaX0iqkB.js +15 -0
  61. package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
  62. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
  63. package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
  64. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
  66. package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
  67. package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
  68. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  69. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  70. package/dist/assets/zh-CN-20MYEjha.js +15 -0
  71. package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
  72. package/dist/index.html +6 -6
  73. package/dist/mini-chat.html +6 -6
  74. package/dist/mobile.html +6 -6
  75. package/package.json +2 -2
  76. package/server/index.js +107 -0
  77. package/server/lib/assistants/DOCUMENTATION.md +18 -18
  78. package/server/lib/assistants/assign.js +2 -1
  79. package/server/lib/assistants/contact-store.js +37 -19
  80. package/server/lib/assistants/projection-history.test.js +207 -0
  81. package/server/lib/assistants/routes.js +5 -5
  82. package/server/lib/assistants/service.js +493 -123
  83. package/server/lib/assistants/service.test.js +203 -133
  84. package/server/lib/conversations/DOCUMENTATION.md +4 -4
  85. package/server/lib/conversations/service.js +86 -57
  86. package/server/lib/conversations/service.test.js +116 -100
  87. package/server/lib/event-stream/global-hub.js +2 -1
  88. package/server/lib/llm/DOCUMENTATION.md +71 -84
  89. package/server/lib/llm/catalog.js +111 -45
  90. package/server/lib/llm/catalog.test.js +121 -59
  91. package/server/lib/llm/completions.js +37 -12
  92. package/server/lib/llm/completions.test.js +45 -4
  93. package/server/lib/llm/generate.js +421 -450
  94. package/server/lib/llm/generate.test.js +553 -487
  95. package/server/lib/llm/routes.js +6 -4
  96. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  97. package/server/lib/message-queue/opencode-adapter.js +191 -104
  98. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  99. package/server/lib/message-queue/runtime.test.js +6 -6
  100. package/server/lib/message-queue/worker.js +7 -1
  101. package/server/lib/opencode/DOCUMENTATION.md +28 -5
  102. package/server/lib/opencode/config-entity-routes.js +84 -13
  103. package/server/lib/opencode/config-entity-routes.test.js +41 -10
  104. package/server/lib/opencode/ensure-cli.js +239 -0
  105. package/server/lib/opencode/ensure-cli.test.js +119 -0
  106. package/server/lib/opencode/env-runtime.js +193 -110
  107. package/server/lib/opencode/env-runtime.test.js +221 -17
  108. package/server/lib/opencode/feature-routes-runtime.js +37 -1
  109. package/server/lib/opencode/lifecycle.js +204 -70
  110. package/server/lib/opencode/lifecycle.test.js +376 -6
  111. package/server/lib/opencode/network-runtime.js +28 -16
  112. package/server/lib/opencode/network-runtime.test.js +68 -4
  113. package/server/lib/opencode/opencode2-pin.js +139 -0
  114. package/server/lib/opencode/opencode2-pin.test.js +87 -0
  115. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
  116. package/server/lib/opencode/plugin-compatibility.js +15 -0
  117. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  118. package/server/lib/opencode/plugins.js +4 -3
  119. package/server/lib/opencode/plugins.test.js +12 -0
  120. package/server/lib/opencode/proxy.js +153 -16
  121. package/server/lib/opencode/proxy.test.js +120 -0
  122. package/server/lib/opencode/routes.js +25 -32
  123. package/server/lib/opencode/routes.test.js +51 -52
  124. package/server/lib/opencode/server-utils-runtime.js +2 -0
  125. package/server/lib/opencode/skill-routes.js +6 -7
  126. package/server/lib/opencode/skill-routes.test.js +36 -5
  127. package/server/lib/opencode/v1-migration-gate.js +157 -0
  128. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  129. package/server/lib/opencode/v2-client.js +49 -0
  130. package/server/lib/opencode/watcher.js +2 -1
  131. package/server/lib/opencode/watcher.test.js +1 -1
  132. package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
  133. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  134. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  135. package/server/lib/scheduled-tasks/routes.js +27 -0
  136. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  137. package/server/lib/scheduled-tasks/runtime.js +331 -308
  138. package/server/lib/scheduled-tasks/runtime.test.js +530 -318
  139. package/server/lib/session-goal/DOCUMENTATION.md +65 -14
  140. package/server/lib/session-goal/capability.js +57 -0
  141. package/server/lib/session-goal/capability.test.js +33 -0
  142. package/server/lib/session-goal/routes.js +231 -2
  143. package/server/lib/session-goal/routes.test.js +211 -0
  144. package/server/lib/session-goal/runtime.js +46 -4
  145. package/server/lib/session-goal/runtime.test.js +62 -1
  146. package/server/lib/session-index/sync-runtime.js +20 -17
  147. package/server/lib/session-index/sync-runtime.test.js +9 -8
  148. package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
  149. package/server/lib/session-metadata/routes.js +94 -0
  150. package/server/lib/session-metadata/routes.test.js +111 -0
  151. package/server/lib/session-metadata/session-metadata-store.js +235 -0
  152. package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
  153. package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
  154. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  155. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  156. package/server/lib/session-turn-pages/routes.js +204 -71
  157. package/server/lib/session-turn-pages/routes.test.js +285 -0
  158. package/server/lib/session-turn-pages/service.js +6 -2
  159. package/server/lib/session-turn-pages/service.test.js +34 -0
  160. package/server/lib/small-model/DOCUMENTATION.md +5 -4
  161. package/server/lib/small-model/catalog.js +56 -6
  162. package/server/lib/small-model/catalog.test.js +47 -0
  163. package/server/lib/small-model/index.js +3 -3
  164. package/server/lib/small-model/opencode-session.js +50 -206
  165. package/server/lib/small-model/opencode-session.test.js +58 -88
  166. package/server/opencode-proxy.test.js +61 -12
  167. package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
  168. package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
  169. package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
  170. package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
  171. package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
  172. package/dist/assets/es-rqMiRP7-.js +0 -15
  173. package/dist/assets/gitApi-CcIuekQ3.js +0 -7
  174. package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
  175. package/dist/assets/ja-Bgr8-dOg.js +0 -15
  176. package/dist/assets/ko-B_Gxh-0l.js +0 -15
  177. package/dist/assets/main-CFLmpNC5.js +0 -169
  178. package/dist/assets/main-Ckj_IpUy.js +0 -2
  179. package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
  180. package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
  181. package/dist/assets/multirun-BGUKIJys.js +0 -4
  182. package/dist/assets/pl-DW2XJGC4.js +0 -15
  183. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  184. package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
  185. package/dist/assets/pt-BR-CIsanf4T.js +0 -15
  186. package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
  187. package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
  188. package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
  189. package/dist/assets/src-rS3zCcsC.css +0 -2
  190. package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
  191. package/dist/assets/uk-BPmPEUYJ.js +0 -15
  192. package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
  193. package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
  194. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  195. package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
@@ -0,0 +1,207 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { createRequire } from 'node:module';
6
+ import { createAssistantsService } from './service.js';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const root = () => fs.mkdtempSync(path.join(os.tmpdir(), 'assistants-proj-'));
10
+
11
+ const v2Error = (result) => {
12
+ if (!result?.error) return result;
13
+ const error = new Error(result.error.code || 'upstream');
14
+ error.status = result.error.status ?? result.error.statusCode ?? result.status;
15
+ error.cause = { status: error.status };
16
+ error.reason = 'UnexpectedStatus';
17
+ throw error;
18
+ };
19
+ const createV2Client = (overrides = {}) => {
20
+ const create = overrides.create ?? (async () => ({ id: crypto.randomUUID() }));
21
+ const get = overrides.get ?? (async () => ({ id: 'present' }));
22
+ const prompt = overrides.prompt ?? overrides.promptAsync ?? (async () => ({ id: 'inbox_1', type: 'user' }));
23
+ const compact = overrides.compact ?? overrides.summarize ?? (async () => ({ id: 'inbox_compact', type: 'compaction' }));
24
+ const interrupt = overrides.interrupt ?? overrides.abort ?? (async () => {});
25
+ const rename = overrides.rename ?? (async () => {});
26
+ const remove = overrides.remove ?? (async () => {});
27
+ const list = overrides.messages ?? overrides.list ?? (async () => ({ data: [], cursor: {} }));
28
+ const switchAgent = overrides.switchAgent ?? (async () => {});
29
+ const switchModel = overrides.switchModel ?? (async () => {});
30
+ const putInstruction = overrides.putInstruction ?? (async () => {});
31
+ return {
32
+ session: {
33
+ create: async (input) => create(input),
34
+ get: async (input) => get(input),
35
+ rename,
36
+ remove,
37
+ prompt: async (input) => prompt(input),
38
+ compact: async (input) => compact(input),
39
+ interrupt: async (input) => interrupt(input),
40
+ switchAgent,
41
+ switchModel,
42
+ instructions: { entry: { put: putInstruction } },
43
+ },
44
+ message: {
45
+ list: async (input) => {
46
+ const result = await list({ ...input, before: input?.cursor });
47
+ if (result?.error) return result;
48
+ if (Array.isArray(result?.data) || result?.cursor || result?.response) return result;
49
+ if (Array.isArray(result)) return { data: result, cursor: {} };
50
+ return result;
51
+ },
52
+ },
53
+ };
54
+ };
55
+ const setup = (directory = root(), client = {}, options = {}) => {
56
+ const { enabled = true, fetchImpl, ...serviceOptions } = options;
57
+ const service = createAssistantsService({
58
+ dbPath: path.join(directory, 'assistants.sqlite'),
59
+ dataDir: directory,
60
+ getAllowedRoots: () => [directory],
61
+ buildOpenCodeUrl: () => 'http://opencode.test',
62
+ getOpenCodeAuthHeaders: () => ({ authorization: 'Bearer test' }),
63
+ fetchImpl,
64
+ clientFactory: () => createV2Client(client),
65
+ ...serviceOptions,
66
+ });
67
+ if (enabled) {
68
+ const snapshot = service.snapshot();
69
+ if (!snapshot.enabled) service.setEnabled({ enabled: true, expectedRevision: snapshot.revision });
70
+ }
71
+ return service;
72
+ };
73
+ const assistantInput = { name: 'A', providerID: 'p', modelID: 'm' };
74
+
75
+ const projectionEntry = (sessionID, id, created, text = id) => ({
76
+ info: { id, sessionID, role: 'assistant', time: { created } },
77
+ parts: [{ id: `prt_${id}`, sessionID, messageID: id, type: 'text', text }],
78
+ });
79
+
80
+ describe('ticket 11 assistants read from OpenCode projections', () => {
81
+ it('does not persist message bodies after send, share, or live events', async () => {
82
+ const directory = root();
83
+ const service = setup(directory, {
84
+ create: async () => ({ data: { id: 'ses_1' } }),
85
+ prompt: async () => ({ id: 'inbox_1', type: 'user' }),
86
+ });
87
+ const assistant = service.createAssistant(assistantInput);
88
+ const binding = await service.ensure(assistant.id);
89
+ await service.send(assistant.id, { ...binding, messageID: 'msg_send', parts: [{ type: 'text', text: 'hello' }] });
90
+ await service.share(assistant.id, { operationID: 'share_1', payload: { messageID: 'msg_share', parts: [{ type: 'text', text: 'shared' }] } });
91
+ service.processEvent({ type: 'message.updated', properties: { info: { id: 'msg_event', sessionID: binding.sessionID, role: 'assistant', time: { created: 1 } } } });
92
+ service.processEvent({ type: 'message.part.updated', properties: { sessionID: binding.sessionID, part: { id: 'prt_1', messageID: 'msg_event', type: 'text', text: 'live' } } });
93
+ const db = new (require('better-sqlite3'))(path.join(directory, 'assistants.sqlite'));
94
+ expect(db.prepare('SELECT COUNT(*) AS count FROM assistant_message_mirror').get().count).toBe(0);
95
+ expect(db.prepare('SELECT COUNT(*) AS count FROM assistant_message_part_mirror').get().count).toBe(0);
96
+ expect(db.prepare('SELECT COUNT(*) AS count FROM assistant_share_operation').get().count).toBe(1);
97
+ expect(db.prepare('SELECT session_id, message_id FROM assistant_share_operation').get()).toMatchObject({ session_id: binding.sessionID, message_id: 'msg_share' });
98
+ db.close();
99
+ service.close();
100
+ });
101
+
102
+ it('opens history from OpenCode projections per binding and ignores leftover mirrors', async () => {
103
+ const directory = root();
104
+ let creates = 0;
105
+ const calls = [];
106
+ const service = setup(directory, {
107
+ create: async () => ({ data: { id: `ses_${++creates}` } }),
108
+ messages: async ({ sessionID, limit, order }) => {
109
+ calls.push({ sessionID, limit, order });
110
+ if (sessionID === 'ses_1') return { data: [projectionEntry('ses_1', 'msg_old', 1, 'from-projection')] };
111
+ return { data: [projectionEntry(sessionID, 'msg_live', 2, 'current')] };
112
+ },
113
+ });
114
+ const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
115
+ const first = await service.ensure(assistant.id);
116
+ await service.createNew(assistant.id);
117
+ const Database = require('better-sqlite3');
118
+ const seed = new Database(path.join(directory, 'assistants.sqlite'));
119
+ seed.prepare('INSERT INTO assistant_message_mirror(assistant_id,session_id,message_id,info_json,ordinal,covered,updated_at) VALUES (?,?,?,?,?,?,?)').run(
120
+ assistant.id,
121
+ first.sessionID,
122
+ 'msg_mirror',
123
+ JSON.stringify({ id: 'msg_mirror', sessionID: first.sessionID, role: 'user', time: { created: 99 }, openchamberAssistantAdmission: true }),
124
+ 99,
125
+ 1,
126
+ 1,
127
+ );
128
+ seed.close();
129
+ const page = await service.historicalMessages(assistant.id, { limit: 10 });
130
+ expect(page.entries.map((entry) => entry.info.id)).toEqual(['msg_old', 'msg_live']);
131
+ expect(page.entries[0]?.parts[0]?.text).toBe('from-projection');
132
+ expect(page.entries.some((entry) => entry.info.id === 'msg_mirror')).toBe(false);
133
+ expect(calls.every((call) => call.order === 'desc')).toBe(true);
134
+ expect(page.complete).toBe(true);
135
+ service.close();
136
+ });
137
+
138
+ it('keeps complete sessions when one projection page fails and does not treat total failure as empty success', async () => {
139
+ const directory = root();
140
+ let creates = 0;
141
+ const service = setup(directory, {
142
+ create: async () => ({ data: { id: `ses_${++creates}` } }),
143
+ messages: async ({ sessionID }) => {
144
+ if (sessionID === 'ses_2') return { error: { status: 503 } };
145
+ return { data: [projectionEntry(sessionID, `msg_${sessionID}`, 1)] };
146
+ },
147
+ });
148
+ const assistant = service.createAssistant({ ...assistantInput, mode: 'stateless' });
149
+ await service.ensure(assistant.id);
150
+ await service.createNew(assistant.id);
151
+ await service.createNew(assistant.id);
152
+ const page = await service.historicalMessages(assistant.id, { limit: 10 });
153
+ expect(page.entries.map((entry) => [entry.sessionID, entry.info.id])).toEqual([
154
+ ['ses_1', 'msg_ses_1'],
155
+ ['ses_3', 'msg_ses_3'],
156
+ ]);
157
+ expect(page.entries.some((entry) => entry.sessionID === 'ses_2')).toBe(false);
158
+
159
+ const isolated = setup(root(), {
160
+ create: async () => ({ data: { id: 'ses_only' } }),
161
+ messages: async () => ({ error: { status: 500 } }),
162
+ });
163
+ const lonely = isolated.createAssistant(assistantInput);
164
+ await isolated.ensure(lonely.id);
165
+ await expect(isolated.historicalMessages(lonely.id, { limit: 10 })).rejects.toMatchObject({ code: 'upstream_error' });
166
+ isolated.close();
167
+ service.close();
168
+ });
169
+
170
+ it('uses ticket 03 projection GET /session/:id/message when fetchImpl is provided', async () => {
171
+ const directory = root();
172
+ const urls = [];
173
+ const service = setup(directory, {
174
+ create: async () => ({ data: { id: 'ses_proj' } }),
175
+ }, {
176
+ fetchImpl: async (input) => {
177
+ const url = new URL(String(input));
178
+ urls.push(url);
179
+ return new Response(JSON.stringify({ data: [projectionEntry('ses_proj', 'msg_fetch', 1)] }), {
180
+ status: 200,
181
+ headers: { 'content-type': 'application/json' },
182
+ });
183
+ },
184
+ });
185
+ const assistant = service.createAssistant(assistantInput);
186
+ await service.ensure(assistant.id);
187
+ const page = await service.historicalMessages(assistant.id, { limit: 20 });
188
+ expect(page.entries.map((entry) => entry.info.id)).toEqual(['msg_fetch']);
189
+ expect(urls).toHaveLength(1);
190
+ expect(urls[0].pathname).toBe('/session/ses_proj/message');
191
+ expect(urls[0].searchParams.get('limit')).toBe('20');
192
+ expect(urls[0].searchParams.get('order')).toBe('desc');
193
+ expect(urls[0].searchParams.get('directory')).toEqual(expect.any(String));
194
+ service.close();
195
+ });
196
+
197
+ it('exposes V2 sharing as unavailable and never reads mirrors to fake a share', async () => {
198
+ const service = setup();
199
+ expect(await service.capability()).toMatchObject({ sharingAvailable: false, archiveMetadataAvailable: false, sessionMetadataAvailable: false });
200
+ const source = fs.readFileSync(new URL('./service.js', import.meta.url), 'utf8');
201
+ expect(source).toContain('sharingAvailable: false');
202
+ expect(/historicalMessages[\s\S]*JOIN assistant_message_mirror/.test(source)).toBe(false);
203
+ expect(/sendWithConfig[\s\S]*mirrorAdmittedUserMessage\(/.test(source)).toBe(false);
204
+ expect(/submitClaim[\s\S]*mirrorAdmittedUserMessage\(/.test(source)).toBe(false);
205
+ service.close();
206
+ });
207
+ });
@@ -3,9 +3,9 @@ import { isSafeInlineImageMime } from './contact-attachments.js';
3
3
  import { createChatCompletion } from '../llm/completions.js';
4
4
  import { ensureLlmTempDirectory } from '../llm/temp-directory.js';
5
5
  import { setAssignedSessionSettleHandler } from '../session-goal/runtime.js';
6
- import { createOpencodeClient } from '@opencode-ai/sdk/v2';
6
+ import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
7
7
 
8
- const respond = (res, work, success = 200) => Promise.resolve().then(work).then((body) => res.status(success).json(body)).catch((error) => { const code = error instanceof AssistantError ? error.code : error?.code || 'internal_error'; const status = code === 'not_found' ? 404 : ['revision_conflict', 'idempotency_conflict', 'contact_generation_conflict'].includes(code) ? 409 : code === 'assistant_disabled' ? 403 : code === 'no_provider' ? 400 : code === 'upstream_error' ? 502 : 400; res.status(status).json({ ok: false, error: code, message: typeof error?.message === 'string' && error.message.trim() ? error.message : code }); });
8
+ const respond = (res, work, success = 200) => Promise.resolve().then(work).then((body) => res.status(success).json(body)).catch((error) => { const code = error instanceof AssistantError ? error.code : error?.code || 'internal_error'; const status = code === 'not_found' ? 404 : ['revision_conflict', 'idempotency_conflict', 'contact_generation_conflict'].includes(code) ? 409 : code === 'assistant_disabled' ? 403 : code === 'no_provider' ? 400 : code === 'upstream_error' || code === 'llm_attachment_generation_unavailable' ? 502 : 400; res.status(status).json({ ok: false, error: code, message: typeof error?.message === 'string' && error.message.trim() ? error.message : code }); });
9
9
  const gone = (_req, res) => res.status(410).json({ ok: false, error: 'assistant_topics_retired' });
10
10
 
11
11
  export const registerAssistantRoutes = (app, dependencies) => {
@@ -13,12 +13,12 @@ export const registerAssistantRoutes = (app, dependencies) => {
13
13
  ...input,
14
14
  buildOpenCodeUrl: dependencies.buildOpenCodeUrl,
15
15
  getOpenCodeAuthHeaders: dependencies.getOpenCodeAuthHeaders,
16
- clientFactory: () => createOpencodeClient({
16
+ clientFactory: () => makeOpenCodeV2Client({
17
17
  baseUrl: dependencies.buildOpenCodeUrl('/', '').replace(/\/$/, ''),
18
- headers: dependencies.getOpenCodeAuthHeaders(),
18
+ authHeaders: dependencies.getOpenCodeAuthHeaders(),
19
19
  }),
20
20
  ensureTempDirectory: ensureLlmTempDirectory,
21
- // Throwaway generate may subscribe for real token deltas (contact harness).
21
+ // Attachment-session generate may subscribe for session.text.delta (contact harness).
22
22
  globalEventHub: input?.globalEventHub ?? dependencies.globalEventHub,
23
23
  });
24
24
  const service = createAssistantsService({