@yadsh/dsh-qa-surface 0.2.0 → 0.4.0

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 (325) hide show
  1. package/README.md +236 -20
  2. package/compatibility.json +11 -3
  3. package/lib/account-remotes.js +100 -0
  4. package/lib/account-remotes.js.map +1 -0
  5. package/lib/accounts/credentials.js +27 -0
  6. package/lib/accounts/credentials.js.map +1 -0
  7. package/lib/accounts/errors.js +9 -0
  8. package/lib/accounts/errors.js.map +1 -0
  9. package/lib/accounts/file.js +101 -0
  10. package/lib/accounts/file.js.map +1 -0
  11. package/lib/accounts/index.js +2 -0
  12. package/lib/accounts/index.js.map +1 -0
  13. package/lib/accounts/store.js +461 -0
  14. package/lib/accounts/store.js.map +1 -0
  15. package/lib/accounts/token.js +61 -0
  16. package/lib/accounts/token.js.map +1 -0
  17. package/lib/attachment-rules.js +111 -0
  18. package/lib/attachment-rules.js.map +1 -0
  19. package/lib/attestation.js.map +1 -1
  20. package/lib/cli.js +256 -0
  21. package/lib/cli.js.map +1 -0
  22. package/lib/client/QaAccountsController.js +359 -0
  23. package/lib/client/QaAccountsController.js.map +1 -0
  24. package/lib/client/QaSessionController.js +238 -180
  25. package/lib/client/QaSessionController.js.map +1 -1
  26. package/lib/client/QaSurface.js +281 -105
  27. package/lib/client/QaSurface.js.map +1 -1
  28. package/lib/client/QaTranscriptAdapter.js +95 -244
  29. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  30. package/lib/client/attachments.js +150 -0
  31. package/lib/client/attachments.js.map +1 -0
  32. package/lib/client/attestation.js +7 -1
  33. package/lib/client/attestation.js.map +1 -1
  34. package/lib/client/base64.js +16 -0
  35. package/lib/client/base64.js.map +1 -0
  36. package/lib/client/chat-files.js +48 -0
  37. package/lib/client/chat-files.js.map +1 -0
  38. package/lib/client/components/Markdown.js +54 -13
  39. package/lib/client/components/Markdown.js.map +1 -1
  40. package/lib/client/components/QaAuthGate.js +40 -0
  41. package/lib/client/components/QaAuthGate.js.map +1 -0
  42. package/lib/client/components/QaChangelog.js +131 -0
  43. package/lib/client/components/QaChangelog.js.map +1 -0
  44. package/lib/client/components/QaComposer.js +56 -74
  45. package/lib/client/components/QaComposer.js.map +1 -1
  46. package/lib/client/components/QaFileAttachment.js +19 -0
  47. package/lib/client/components/QaFileAttachment.js.map +1 -0
  48. package/lib/client/components/QaFilesPanel.js +45 -0
  49. package/lib/client/components/QaFilesPanel.js.map +1 -0
  50. package/lib/client/components/QaHeader.js +34 -0
  51. package/lib/client/components/QaHeader.js.map +1 -0
  52. package/lib/client/components/QaMessage.js +61 -18
  53. package/lib/client/components/QaMessage.js.map +1 -1
  54. package/lib/client/components/QaModal.js +31 -0
  55. package/lib/client/components/QaModal.js.map +1 -0
  56. package/lib/client/components/QaProfile.js +64 -0
  57. package/lib/client/components/QaProfile.js.map +1 -0
  58. package/lib/client/components/QaRightRail.js +15 -0
  59. package/lib/client/components/QaRightRail.js.map +1 -0
  60. package/lib/client/components/QaSidebar.js +68 -20
  61. package/lib/client/components/QaSidebar.js.map +1 -1
  62. package/lib/client/components/QaSourcesPanel.js +127 -0
  63. package/lib/client/components/QaSourcesPanel.js.map +1 -0
  64. package/lib/client/components/QaTurnRail.js +296 -0
  65. package/lib/client/components/QaTurnRail.js.map +1 -0
  66. package/lib/client/components/QaWelcomeNotice.js +125 -0
  67. package/lib/client/components/QaWelcomeNotice.js.map +1 -0
  68. package/lib/client/components/QaWidthHandle.js +147 -0
  69. package/lib/client/components/QaWidthHandle.js.map +1 -0
  70. package/lib/client/components/QaWorkGroup.js +19 -20
  71. package/lib/client/components/QaWorkGroup.js.map +1 -1
  72. package/lib/client/components/source-refs.js +78 -0
  73. package/lib/client/components/source-refs.js.map +1 -0
  74. package/lib/client/components/surface-utils.js +28 -0
  75. package/lib/client/components/surface-utils.js.map +1 -0
  76. package/lib/client/components/thinking-phrases.js +24 -0
  77. package/lib/client/components/thinking-phrases.js.map +1 -0
  78. package/lib/client/create-session.js +26 -0
  79. package/lib/client/create-session.js.map +1 -0
  80. package/lib/client/failure-copy.js +63 -0
  81. package/lib/client/failure-copy.js.map +1 -0
  82. package/lib/client/index.js +135 -16
  83. package/lib/client/index.js.map +1 -1
  84. package/lib/client/project-session-state.js +78 -0
  85. package/lib/client/project-session-state.js.map +1 -0
  86. package/lib/client/prompt-content.js +54 -0
  87. package/lib/client/prompt-content.js.map +1 -0
  88. package/lib/client/session-admission.js +38 -0
  89. package/lib/client/session-admission.js.map +1 -0
  90. package/lib/client/session-assets.js +52 -0
  91. package/lib/client/session-assets.js.map +1 -0
  92. package/lib/client/session-sources.js +61 -0
  93. package/lib/client/session-sources.js.map +1 -0
  94. package/lib/client/settings/card.js +143 -0
  95. package/lib/client/settings/card.js.map +1 -0
  96. package/lib/client/settings/fields.js +145 -0
  97. package/lib/client/settings/fields.js.map +1 -0
  98. package/lib/client/settings/format.js +255 -0
  99. package/lib/client/settings/format.js.map +1 -0
  100. package/lib/client/settings/sections/access.js +25 -0
  101. package/lib/client/settings/sections/access.js.map +1 -0
  102. package/lib/client/settings/sections/accounts.js +47 -0
  103. package/lib/client/settings/sections/accounts.js.map +1 -0
  104. package/lib/client/settings/sections/attachments.js +46 -0
  105. package/lib/client/settings/sections/attachments.js.map +1 -0
  106. package/lib/client/settings/sections/branding.js +28 -0
  107. package/lib/client/settings/sections/branding.js.map +1 -0
  108. package/lib/client/settings/sections/common.js +20 -0
  109. package/lib/client/settings/sections/common.js.map +1 -0
  110. package/lib/client/settings/sections/embedding.js +19 -0
  111. package/lib/client/settings/sections/embedding.js.map +1 -0
  112. package/lib/client/settings/sections/interface.js +60 -0
  113. package/lib/client/settings/sections/interface.js.map +1 -0
  114. package/lib/client/settings/sections/lockdown.js +82 -0
  115. package/lib/client/settings/sections/lockdown.js.map +1 -0
  116. package/lib/client/settings/sections/session.js +53 -0
  117. package/lib/client/settings/sections/session.js.map +1 -0
  118. package/lib/client/settings/sections/sources.js +72 -0
  119. package/lib/client/settings/sections/sources.js.map +1 -0
  120. package/lib/client/settings/sections/status.js +37 -0
  121. package/lib/client/settings/sections/status.js.map +1 -0
  122. package/lib/client/settings/sections.js +23 -0
  123. package/lib/client/settings/sections.js.map +1 -0
  124. package/lib/client/settings/styles.js +57 -0
  125. package/lib/client/settings/styles.js.map +1 -0
  126. package/lib/client/settlement.js +24 -0
  127. package/lib/client/settlement.js.map +1 -0
  128. package/lib/client/stream-publisher.js +47 -0
  129. package/lib/client/stream-publisher.js.map +1 -0
  130. package/lib/client/styles.js +225 -17
  131. package/lib/client/styles.js.map +1 -1
  132. package/lib/client/tool-presentation.js +102 -0
  133. package/lib/client/tool-presentation.js.map +1 -0
  134. package/lib/client/turn-sources.js +98 -0
  135. package/lib/client/turn-sources.js.map +1 -0
  136. package/lib/client/types.js +2 -0
  137. package/lib/client/types.js.map +1 -1
  138. package/lib/client/use-right-rail.js +96 -0
  139. package/lib/client/use-right-rail.js.map +1 -0
  140. package/lib/client/use-session-ui-state.js +75 -0
  141. package/lib/client/use-session-ui-state.js.map +1 -0
  142. package/lib/client/use-transcript-view.js +44 -0
  143. package/lib/client/use-transcript-view.js.map +1 -0
  144. package/lib/client.js +9052 -1345
  145. package/lib/client.js.map +1 -1
  146. package/lib/config-resolvers/accounts.js +60 -0
  147. package/lib/config-resolvers/accounts.js.map +1 -0
  148. package/lib/config-resolvers/attachments.js +28 -0
  149. package/lib/config-resolvers/attachments.js.map +1 -0
  150. package/lib/config-resolvers/basics.js +59 -0
  151. package/lib/config-resolvers/basics.js.map +1 -0
  152. package/lib/config-resolvers/defaults.js +132 -0
  153. package/lib/config-resolvers/defaults.js.map +1 -0
  154. package/lib/config-resolvers/lockdown.js +79 -0
  155. package/lib/config-resolvers/lockdown.js.map +1 -0
  156. package/lib/config-resolvers/session.js +42 -0
  157. package/lib/config-resolvers/session.js.map +1 -0
  158. package/lib/config-resolvers/shared.js +42 -0
  159. package/lib/config-resolvers/shared.js.map +1 -0
  160. package/lib/config-resolvers/sources.js +81 -0
  161. package/lib/config-resolvers/sources.js.map +1 -0
  162. package/lib/config-resolvers/ui.js +20 -0
  163. package/lib/config-resolvers/ui.js.map +1 -0
  164. package/lib/config.js +279 -102
  165. package/lib/config.js.map +1 -1
  166. package/lib/entry-redirect.js +45 -0
  167. package/lib/entry-redirect.js.map +1 -0
  168. package/lib/host-route.js +25 -1
  169. package/lib/host-route.js.map +1 -1
  170. package/lib/index.js +258 -23
  171. package/lib/index.js.map +1 -1
  172. package/lib/launch-token.js +42 -0
  173. package/lib/launch-token.js.map +1 -0
  174. package/lib/profile.js +214 -0
  175. package/lib/profile.js.map +1 -0
  176. package/lib/prompt-notes.js +231 -0
  177. package/lib/prompt-notes.js.map +1 -0
  178. package/lib/provenance/collector.js +40 -0
  179. package/lib/provenance/collector.js.map +1 -0
  180. package/lib/provenance/connectors.js +147 -0
  181. package/lib/provenance/connectors.js.map +1 -0
  182. package/lib/provenance/dedupe.js +89 -0
  183. package/lib/provenance/dedupe.js.map +1 -0
  184. package/lib/provenance/extractors.js +340 -0
  185. package/lib/provenance/extractors.js.map +1 -0
  186. package/lib/provenance/file-preview.js +39 -0
  187. package/lib/provenance/file-preview.js.map +1 -0
  188. package/lib/provenance/host-store.js +387 -0
  189. package/lib/provenance/host-store.js.map +1 -0
  190. package/lib/provenance/index.js +9 -0
  191. package/lib/provenance/index.js.map +1 -0
  192. package/lib/provenance/normalize.js +128 -0
  193. package/lib/provenance/normalize.js.map +1 -0
  194. package/lib/provenance/reported.js +53 -0
  195. package/lib/provenance/reported.js.map +1 -0
  196. package/lib/provenance/types.js +2 -0
  197. package/lib/provenance/types.js.map +1 -0
  198. package/lib/resolve-config.js +35 -249
  199. package/lib/resolve-config.js.map +1 -1
  200. package/lib/secure-session.js +122 -14
  201. package/lib/secure-session.js.map +1 -1
  202. package/lib/shared/session-key.js +18 -0
  203. package/lib/shared/session-key.js.map +1 -0
  204. package/lib/shared/settings.js +10 -0
  205. package/lib/shared/settings.js.map +1 -0
  206. package/lib/thinking-phrases.js +68 -0
  207. package/lib/thinking-phrases.js.map +1 -0
  208. package/lib/typert.host.js +749 -11
  209. package/lib/typert.remote-client.d.ts +23 -3
  210. package/lib/typert.remote-client.js +593 -6
  211. package/lib/types/account-remotes.d.ts +42 -0
  212. package/lib/types/accounts/credentials.d.ts +14 -0
  213. package/lib/types/accounts/errors.d.ts +11 -0
  214. package/lib/types/accounts/file.d.ts +82 -0
  215. package/lib/types/accounts/index.d.ts +2 -0
  216. package/lib/types/accounts/store.d.ts +144 -0
  217. package/lib/types/accounts/token.d.ts +31 -0
  218. package/lib/types/attachment-rules.d.ts +44 -0
  219. package/lib/types/attestation.d.ts +1 -1
  220. package/lib/types/cli.d.ts +9 -0
  221. package/lib/types/client/QaAccountsController.d.ts +102 -0
  222. package/lib/types/client/QaConfigController.d.ts +1 -1
  223. package/lib/types/client/QaSessionController.d.ts +61 -22
  224. package/lib/types/client/QaSurface.d.ts +14 -3
  225. package/lib/types/client/QaTranscriptAdapter.d.ts +5 -10
  226. package/lib/types/client/attachments.d.ts +54 -0
  227. package/lib/types/client/base64.d.ts +10 -0
  228. package/lib/types/client/chat-files.d.ts +30 -0
  229. package/lib/types/client/components/Markdown.d.ts +11 -1
  230. package/lib/types/client/components/QaAgentsDrawer.d.ts +1 -1
  231. package/lib/types/client/components/QaAuthGate.d.ts +15 -0
  232. package/lib/types/client/components/QaChangelog.d.ts +21 -0
  233. package/lib/types/client/components/QaComposer.d.ts +8 -4
  234. package/lib/types/client/components/QaFileAttachment.d.ts +14 -0
  235. package/lib/types/client/components/QaFilesPanel.d.ts +15 -0
  236. package/lib/types/client/components/QaHeader.d.ts +41 -0
  237. package/lib/types/client/components/QaMessage.d.ts +12 -3
  238. package/lib/types/client/components/QaModal.d.ts +26 -0
  239. package/lib/types/client/components/QaProfile.d.ts +25 -0
  240. package/lib/types/client/components/QaRightRail.d.ts +23 -0
  241. package/lib/types/client/components/QaSidebar.d.ts +44 -2
  242. package/lib/types/client/components/QaSourcesPanel.d.ts +23 -0
  243. package/lib/types/client/components/QaTurnRail.d.ts +65 -0
  244. package/lib/types/client/components/QaWelcomeNotice.d.ts +27 -0
  245. package/lib/types/client/components/QaWidthHandle.d.ts +43 -0
  246. package/lib/types/client/components/QaWorkGroup.d.ts +5 -3
  247. package/lib/types/client/components/source-refs.d.ts +16 -0
  248. package/lib/types/client/components/surface-utils.d.ts +11 -0
  249. package/lib/types/client/components/thinking-phrases.d.ts +9 -0
  250. package/lib/types/client/create-session.d.ts +13 -0
  251. package/lib/types/client/failure-copy.d.ts +19 -0
  252. package/lib/types/client/index.d.ts +1 -0
  253. package/lib/types/client/project-session-state.d.ts +30 -0
  254. package/lib/types/client/prompt-content.d.ts +30 -0
  255. package/lib/types/client/session-admission.d.ts +23 -0
  256. package/lib/types/client/session-assets.d.ts +20 -0
  257. package/lib/types/client/session-sources.d.ts +28 -0
  258. package/lib/types/client/settings/card.d.ts +23 -0
  259. package/lib/types/client/settings/fields.d.ts +139 -0
  260. package/lib/types/client/settings/format.d.ts +108 -0
  261. package/lib/types/client/settings/sections/access.d.ts +9 -0
  262. package/lib/types/client/settings/sections/accounts.d.ts +9 -0
  263. package/lib/types/client/settings/sections/attachments.d.ts +15 -0
  264. package/lib/types/client/settings/sections/branding.d.ts +8 -0
  265. package/lib/types/client/settings/sections/common.d.ts +28 -0
  266. package/lib/types/client/settings/sections/embedding.d.ts +8 -0
  267. package/lib/types/client/settings/sections/interface.d.ts +9 -0
  268. package/lib/types/client/settings/sections/lockdown.d.ts +9 -0
  269. package/lib/types/client/settings/sections/session.d.ts +9 -0
  270. package/lib/types/client/settings/sections/sources.d.ts +9 -0
  271. package/lib/types/client/settings/sections/status.d.ts +22 -0
  272. package/lib/types/client/settings/sections.d.ts +25 -0
  273. package/lib/types/client/settings/styles.d.ts +8 -0
  274. package/lib/types/client/settlement.d.ts +6 -0
  275. package/lib/types/client/stream-publisher.d.ts +28 -0
  276. package/lib/types/client/tool-presentation.d.ts +6 -0
  277. package/lib/types/client/turn-sources.d.ts +14 -0
  278. package/lib/types/client/types.d.ts +101 -9
  279. package/lib/types/client/use-right-rail.d.ts +56 -0
  280. package/lib/types/client/use-session-ui-state.d.ts +41 -0
  281. package/lib/types/client/use-transcript-view.d.ts +20 -0
  282. package/lib/types/config-resolvers/accounts.d.ts +15 -0
  283. package/lib/types/config-resolvers/attachments.d.ts +6 -0
  284. package/lib/types/config-resolvers/basics.d.ts +8 -0
  285. package/lib/types/config-resolvers/defaults.d.ts +4 -0
  286. package/lib/types/config-resolvers/lockdown.d.ts +10 -0
  287. package/lib/types/config-resolvers/session.d.ts +6 -0
  288. package/lib/types/config-resolvers/shared.d.ts +16 -0
  289. package/lib/types/config-resolvers/sources.d.ts +6 -0
  290. package/lib/types/config-resolvers/ui.d.ts +6 -0
  291. package/lib/types/entry-redirect.d.ts +20 -0
  292. package/lib/types/host-route.d.ts +14 -1
  293. package/lib/types/index.d.ts +54 -3
  294. package/lib/types/launch-token.d.ts +21 -0
  295. package/lib/types/profile.d.ts +76 -0
  296. package/lib/types/prompt-notes.d.ts +110 -0
  297. package/lib/types/provenance/collector.d.ts +26 -0
  298. package/lib/types/provenance/connectors.d.ts +11 -0
  299. package/lib/types/provenance/dedupe.d.ts +6 -0
  300. package/lib/types/provenance/extractors.d.ts +14 -0
  301. package/lib/types/provenance/file-preview.d.ts +10 -0
  302. package/lib/types/provenance/host-store.d.ts +54 -0
  303. package/lib/types/provenance/index.d.ts +10 -0
  304. package/lib/types/provenance/normalize.d.ts +13 -0
  305. package/lib/types/provenance/reported.d.ts +5 -0
  306. package/lib/types/provenance/types.d.ts +78 -0
  307. package/lib/types/resolve-config.d.ts +8 -3
  308. package/lib/types/secure-session.d.ts +36 -2
  309. package/lib/types/shared/session-key.d.ts +23 -0
  310. package/lib/types/shared/settings.d.ts +10 -0
  311. package/lib/types/thinking-phrases.d.ts +17 -0
  312. package/lib/types/types.d.ts +290 -16
  313. package/lib/types/user-workspace.d.ts +38 -0
  314. package/lib/user-workspace.js +231 -0
  315. package/lib/user-workspace.js.map +1 -0
  316. package/package.json +71 -47
  317. package/CHANGELOG.md +0 -93
  318. package/SPEC-dsh-qa-surface.md +0 -2425
  319. package/docs/ARCHITECTURE.md +0 -66
  320. package/docs/COMPATIBILITY.md +0 -14
  321. package/docs/CONFIGURATION.md +0 -112
  322. package/docs/EMBEDDING.md +0 -13
  323. package/lib/client/components/QaSourcesDrawer.js +0 -46
  324. package/lib/client/components/QaSourcesDrawer.js.map +0 -1
  325. package/lib/types/client/components/QaSourcesDrawer.d.ts +0 -8
@@ -1,16 +1,18 @@
1
- function bytesToBase64(bytes) {
2
- let binary = "";
3
- for (let index = 0; index < bytes.length; index += 0x8000) {
4
- binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
5
- }
6
- return btoa(binary);
7
- }
8
- import { QaPolicyAttestationError, attestationHint, attestationReasonOf, proofMatchesConfig, } from "./attestation.js";
1
+ import { bytesToBase64 } from "./base64.js";
2
+ import { qaStorageNamespace } from "../shared/session-key.js";
3
+ import { QaPolicyAttestationError } from "./attestation.js";
9
4
  import { QaChatIndex } from "./chat-index.js";
5
+ import { SessionAssetRepository } from "./session-assets.js";
6
+ import { createQaSession } from "./create-session.js";
7
+ import { buildQaPromptContent, stageQaFiles } from "./prompt-content.js";
8
+ import { attestQaPolicy } from "./session-admission.js";
9
+ import { QaHostSourceBridge } from "./session-sources.js";
10
+ import { StreamPublisher } from "./stream-publisher.js";
10
11
  import { QA_SESSION_IDLE_STATE } from "./types.js";
11
12
  import { waitFor } from "./wait-for.js";
12
- import { projectSources } from "./QaTranscriptAdapter.js";
13
- import { projectTranscript, QA_REGENERATE_MARKER, } from "./QaTranscriptAdapter.js";
13
+ import { QA_REGENERATE_MARKER } from "./QaTranscriptAdapter.js";
14
+ import { projectBoundSessionState } from "./project-session-state.js";
15
+ import { projectTurnSources } from "./turn-sources.js";
14
16
  const CONFIGURATION_ERROR = "Настройки помощника недоступны.";
15
17
  /**
16
18
  * The only module that couples QA behavior to DSH Session/client APIs. It owns
@@ -20,16 +22,26 @@ const CONFIGURATION_ERROR = "Настройки помощника недост
20
22
  export class QaSessionController {
21
23
  listeners = new Set();
22
24
  sessions;
23
- api;
25
+ createSessionRemote;
26
+ conversation;
24
27
  connection;
25
28
  config;
26
29
  secureSessionRemote;
30
+ sourceApi;
27
31
  chats;
32
+ accounts;
33
+ /** Resolved per send: a page without the upload plugin has no receipts. */
34
+ fileUpload;
35
+ /** Per-chat attachment URL cache; blob URLs die with the chat binding. */
36
+ assets = new SessionAssetRepository();
28
37
  timeoutMs;
29
- streamIntervalMs;
38
+ /** Spacing for a running turn's stream frames; the policy lives there. */
39
+ streamPublisher;
30
40
  state = QA_SESSION_IDLE_STATE;
31
41
  session;
42
+ conversationBinding;
32
43
  unsubscribeSession;
44
+ unsubscribeChat;
33
45
  unsubscribeConnection;
34
46
  ensuring;
35
47
  materializing;
@@ -37,7 +49,6 @@ export class QaSessionController {
37
49
  admissionPending = false;
38
50
  policyReady = false;
39
51
  drafting = false;
40
- streamFlushTimer;
41
52
  /** The chat session to return to when a subagent view closes. */
42
53
  chatSessionId = null;
43
54
  viewingSubagent = null;
@@ -45,15 +56,30 @@ export class QaSessionController {
45
56
  disposed = false;
46
57
  generation = 0;
47
58
  chatsRevision = 0;
59
+ /** Host-side provenance of the bound chat, merged into the projection. */
60
+ hostSources;
48
61
  constructor(options) {
49
62
  this.sessions = options.sessions;
50
- this.api = options.api;
63
+ this.createSessionRemote = options.createSession;
64
+ this.conversation = options.conversation;
51
65
  this.connection = options.connection;
52
66
  this.config = options.config;
53
67
  this.secureSessionRemote = options.secureSession;
54
- this.chats = new QaChatIndex(options.storage, `${options.config.session.storageKey}:v1:${options.config.route.path}`);
68
+ this.sourceApi =
69
+ options.sourceApi ??
70
+ {
71
+ sources: async () => ({ ok: true, value: [] }),
72
+ readSourceFile: async () => ({
73
+ ok: false,
74
+ error: "unavailable",
75
+ }),
76
+ };
77
+ this.chats = new QaChatIndex(options.storage, qaStorageNamespace(options.config));
78
+ this.accounts = options.accounts;
79
+ this.fileUpload = options.fileUpload ?? (() => undefined);
80
+ this.hostSources = new QaHostSourceBridge(this.sourceApi, () => this.accounts?.token() ?? "");
55
81
  this.timeoutMs = options.timeoutMs ?? 15_000;
56
- this.streamIntervalMs = options.streamIntervalMs ?? 66;
82
+ this.streamPublisher = new StreamPublisher(options.streamIntervalMs ?? 66);
57
83
  this.connectedOnce = this.connection.getSnapshot() !== undefined;
58
84
  this.unsubscribeConnection = this.connection.subscribe(() => {
59
85
  if (this.connection.getSnapshot() !== undefined)
@@ -76,9 +102,12 @@ export class QaSessionController {
76
102
  this.ensuring = operation;
77
103
  return operation;
78
104
  }
79
- async send(text, images = []) {
105
+ async send(text, attachments = []) {
80
106
  const prompt = text.trim();
81
- if ((prompt === "" && images.length === 0) || !this.state.canSend) {
107
+ // Images ride the prompt inline; files must be staged first, because the
108
+ // wire carries a receipt rather than bytes.
109
+ const files = attachments.filter((attachment) => attachment.kind === "file");
110
+ if ((prompt === "" && attachments.length === 0) || !this.state.canSend) {
82
111
  return false;
83
112
  }
84
113
  if (prompt.startsWith("/")) {
@@ -96,25 +125,49 @@ export class QaSessionController {
96
125
  }
97
126
  }
98
127
  this.operationError = null;
99
- let attested = await this.attestPolicy();
100
- if (!attested) {
101
- attested = await this.recoverForSend();
102
- if (!attested)
128
+ const step = await this.attestPolicy();
129
+ if (step.kind === "stale") {
130
+ // The user moved to another chat while the proof was pending; that
131
+ // binding attests through its own flow and must not receive a prompt
132
+ // meant for the old one.
133
+ return false;
134
+ }
135
+ let attestedId = step.kind === "ok" ? step.sessionId : null;
136
+ if (attestedId === null) {
137
+ attestedId = await this.recoverForSend();
138
+ if (attestedId === null)
103
139
  return false;
104
140
  }
141
+ // Nothing can interleave between the last await and here, so matching the
142
+ // attested id proves the prompt rides exactly the session it proved.
143
+ const target = this.session;
144
+ if (this.disposed ||
145
+ target === undefined ||
146
+ String(target.sessionId) !== attestedId) {
147
+ return false;
148
+ }
149
+ let receipts = new Map();
150
+ if (files.length > 0) {
151
+ const outcome = await stageQaFiles(this.fileUpload(), target, files);
152
+ if (outcome.kind !== "ok") {
153
+ if (this.session === target) {
154
+ this.operationError =
155
+ outcome.kind === "unavailable"
156
+ ? "Вложения недоступны на этом сервере."
157
+ : "Не удалось приложить файл.";
158
+ this.publish();
159
+ }
160
+ return false;
161
+ }
162
+ receipts = outcome.receipts;
163
+ }
105
164
  this.admissionPending = true;
106
165
  this.publish();
107
166
  try {
108
- const content = [
109
- ...(prompt === "" ? [] : [{ type: "text", text: prompt }]),
110
- ...images.map((image) => ({
111
- type: "image",
112
- mediaType: image.mediaType,
113
- data: image.data,
114
- name: image.name,
115
- })),
116
- ];
117
- const result = await this.session.prompt(content, "queue");
167
+ const content = buildQaPromptContent(prompt, attachments, receipts);
168
+ const result = await target.prompt(content, "queue");
169
+ if (this.session !== target)
170
+ return false;
118
171
  if (!result.ok) {
119
172
  this.admissionPending = false;
120
173
  this.operationError = "Не удалось отправить сообщение.";
@@ -127,8 +180,10 @@ export class QaSessionController {
127
180
  catch (error) {
128
181
  this.admissionPending = false;
129
182
  console.error("dsh-qa-surface: prompt failed", error);
130
- this.operationError = "Не удалось отправить сообщение.";
131
- this.publish();
183
+ if (this.session === target) {
184
+ this.operationError = "Не удалось отправить сообщение.";
185
+ this.publish();
186
+ }
132
187
  return false;
133
188
  }
134
189
  }
@@ -143,15 +198,20 @@ export class QaSessionController {
143
198
  return this.send(QA_REGENERATE_MARKER);
144
199
  }
145
200
  async stop() {
146
- if (this.session === undefined || !this.state.canStop)
201
+ const target = this.session;
202
+ if (target === undefined || !this.state.canStop)
147
203
  return;
148
204
  try {
149
- const result = await this.session.cancel();
205
+ const result = await target.cancel();
206
+ if (this.session !== target)
207
+ return;
150
208
  if (!result.ok)
151
209
  this.operationError = "Не удалось остановить ответ.";
152
210
  }
153
211
  catch (error) {
154
212
  console.error("dsh-qa-surface: stop failed", error);
213
+ if (this.session !== target)
214
+ return;
155
215
  this.operationError = "Не удалось остановить ответ.";
156
216
  }
157
217
  this.publish();
@@ -200,7 +260,10 @@ export class QaSessionController {
200
260
  const attempt = (async () => {
201
261
  try {
202
262
  await this.waitForConnection();
203
- const id = await this.createSession();
263
+ const id = await createQaSession({
264
+ createSession: this.createSessionRemote,
265
+ token: this.accounts?.token() ?? "",
266
+ });
204
267
  if (this.disposed || operation !== this.generation)
205
268
  return false;
206
269
  await this.bind(id);
@@ -213,6 +276,8 @@ export class QaSessionController {
213
276
  return true;
214
277
  }
215
278
  catch (error) {
279
+ if (this.disposed || operation !== this.generation)
280
+ return false;
216
281
  this.fail(this.operationError === CONFIGURATION_ERROR
217
282
  ? this.operationError
218
283
  : "Не удалось начать чат.", error);
@@ -267,6 +332,9 @@ export class QaSessionController {
267
332
  this.chats.saveActive(sessionId);
268
333
  }
269
334
  catch (error) {
335
+ // A newer operation superseded this switch: its own outcome governs.
336
+ if (this.disposed || operation !== this.generation)
337
+ return;
270
338
  this.fail(this.operationError === CONFIGURATION_ERROR
271
339
  ? this.operationError
272
340
  : "Не удалось открыть этот чат.", error);
@@ -305,6 +373,9 @@ export class QaSessionController {
305
373
  await this.bind(id, { attest: false, track: false, report: false });
306
374
  }
307
375
  catch (error) {
376
+ // A newer operation superseded this view request: leave its state alone.
377
+ if (this.disposed || operation !== this.generation)
378
+ return;
308
379
  this.viewingSubagent = null;
309
380
  this.fail("Не удалось открыть транскрипт субагента.", error);
310
381
  }
@@ -318,9 +389,14 @@ export class QaSessionController {
318
389
  return;
319
390
  await this.switchTo(chat);
320
391
  }
321
- /** This browser's indexed chat ids, most recently used first. */
392
+ /** This browser's chat ids: the owned list when accounts are on, else the
393
+ * local index. Both intersect the host session list at projection time. */
322
394
  chatIds() {
323
- return this.chats.chatIds();
395
+ const owned = this.accounts?.ownedIds() ?? [];
396
+ if (owned.length === 0)
397
+ return this.chats.chatIds();
398
+ const local = this.chats.chatIds();
399
+ return [...new Set([...owned, ...local])];
324
400
  }
325
401
  /** The bound session id, or null while no chat is bound. */
326
402
  activeSessionId() {
@@ -328,12 +404,21 @@ export class QaSessionController {
328
404
  }
329
405
  /**
330
406
  * Resolve one durable image attachment of the bound session into a
331
- * browser-usable URL. The caller caches; failures surface as broken views.
407
+ * browser-usable URL. Resolutions are cached per chat in the asset
408
+ * repository (attachment ids repeat across chats) and revoked when the chat
409
+ * is unbound; failures surface as broken views and retry on demand.
332
410
  */
333
411
  async readImage(attachmentId) {
334
412
  if (this.session === undefined)
335
413
  throw new Error("no bound session");
336
- const result = await this.session.readAttachment(attachmentId);
414
+ const sessionId = String(this.session.sessionId);
415
+ return this.assets.resolve(sessionId, attachmentId, (id) => this.readAttachmentUrl(id));
416
+ }
417
+ async readAttachmentUrl(attachmentId) {
418
+ const session = this.session;
419
+ if (session === undefined)
420
+ throw new Error("no bound session");
421
+ const result = await session.readAttachment(attachmentId);
337
422
  if (!result.ok) {
338
423
  throw new Error(`${result.error.code}: ${result.error.message}`);
339
424
  }
@@ -374,9 +459,10 @@ export class QaSessionController {
374
459
  return;
375
460
  this.disposed = true;
376
461
  this.generation += 1;
377
- this.clearStreamFlush();
462
+ this.streamPublisher.clear();
378
463
  this.unsubscribeConnection();
379
464
  this.unbind();
465
+ this.assets.dispose();
380
466
  this.listeners.clear();
381
467
  }
382
468
  async ensureSessionNow() {
@@ -410,7 +496,10 @@ export class QaSessionController {
410
496
  }
411
497
  }
412
498
  if (id === null) {
413
- id = await this.createSession();
499
+ id = await createQaSession({
500
+ createSession: this.createSessionRemote,
501
+ token: this.accounts?.token() ?? "",
502
+ });
414
503
  }
415
504
  if (this.disposed || operation !== this.generation)
416
505
  return;
@@ -421,6 +510,10 @@ export class QaSessionController {
421
510
  await this.bind(id, { report: !restored });
422
511
  }
423
512
  catch (error) {
513
+ // A stale restore must not start its recovery ladder against a newer
514
+ // operation; rethrow for the generation-guarded outer catch.
515
+ if (this.disposed || operation !== this.generation)
516
+ throw error;
424
517
  if (!restored ||
425
518
  this.config.session.policy !== "browser-persistent" ||
426
519
  !(error instanceof QaPolicyAttestationError)) {
@@ -432,7 +525,10 @@ export class QaSessionController {
432
525
  this.unbind();
433
526
  this.operationError = null;
434
527
  this.chats.clearActive();
435
- id = await this.createSession();
528
+ id = await createQaSession({
529
+ createSession: this.createSessionRemote,
530
+ token: this.accounts?.token() ?? "",
531
+ });
436
532
  if (this.disposed || operation !== this.generation)
437
533
  return;
438
534
  await this.bind(id);
@@ -444,6 +540,9 @@ export class QaSessionController {
444
540
  }
445
541
  }
446
542
  catch (error) {
543
+ // A newer operation superseded this bootstrap: its own outcome governs.
544
+ if (this.disposed || operation !== this.generation)
545
+ return;
447
546
  this.fail(this.operationError === CONFIGURATION_ERROR
448
547
  ? this.operationError
449
548
  : "Не удалось начать чат.", error);
@@ -456,14 +555,15 @@ export class QaSessionController {
456
555
  * re-opening re-materializes it. Step 2 — when that refused session is
457
556
  * still blank (it never received the prompt, so there is nothing to lose) —
458
557
  * starts a fresh attested session instead. A session with history is left
459
- * alone: recreating it would silently drop the conversation.
558
+ * alone: recreating it would silently drop the conversation. Returns the
559
+ * attested session id, or null when nothing was attested.
460
560
  */
461
561
  async recoverForSend() {
462
562
  const id = this.session === undefined ? null : String(this.session.sessionId);
463
563
  if (id === null ||
464
564
  this.disposed ||
465
565
  this.config.session.policy === "fixed") {
466
- return false;
566
+ return null;
467
567
  }
468
568
  const operation = ++this.generation;
469
569
  this.unbind();
@@ -476,72 +576,43 @@ export class QaSessionController {
476
576
  try {
477
577
  await this.bind(id);
478
578
  if (this.disposed || operation !== this.generation)
479
- return false;
579
+ return null;
480
580
  if (this.policyReady)
481
- return true;
581
+ return id;
482
582
  }
483
583
  catch (error) {
484
584
  if (!(error instanceof QaPolicyAttestationError))
485
585
  throw error;
486
586
  if (this.disposed || operation !== this.generation)
487
- return false;
587
+ return null;
488
588
  }
489
589
  if (this.session !== undefined && !this.session.getSnapshot().blank) {
490
590
  this.fail(CONFIGURATION_ERROR, new QaPolicyAttestationError());
491
- return false;
591
+ return null;
492
592
  }
493
593
  this.unbind();
494
594
  this.chats.clearActive();
495
- const created = await this.createSession();
595
+ const created = await createQaSession({
596
+ createSession: this.createSessionRemote,
597
+ token: this.accounts?.token() ?? "",
598
+ });
496
599
  if (this.disposed || operation !== this.generation)
497
- return false;
600
+ return null;
498
601
  await this.bind(created);
499
602
  if (this.disposed || operation !== this.generation)
500
- return false;
603
+ return null;
501
604
  this.chats.saveActive(created);
502
- return this.policyReady;
605
+ return this.policyReady ? created : null;
503
606
  }
504
607
  catch (error) {
608
+ if (this.disposed || operation !== this.generation)
609
+ return null;
505
610
  this.fail(this.operationError === CONFIGURATION_ERROR
506
611
  ? this.operationError
507
612
  : "Не удалось открыть этот чат.", error);
508
- return false;
613
+ return null;
509
614
  }
510
615
  }
511
- async createSession() {
512
- const created = await this.sessions.create({
513
- ...(this.config.session.workspaceId !== null
514
- ? { workspaceId: this.config.session.workspaceId }
515
- : this.config.session.cwd !== null
516
- ? { cwd: this.config.session.cwd }
517
- : {}),
518
- });
519
- const id = String(created);
520
- if (this.config.session.agentPreset !== null) {
521
- const selectedPreset = await this.api.selectAgentPreset({
522
- sessionId: id,
523
- agentPreset: this.config.session.agentPreset,
524
- });
525
- if (!selectedPreset.result.ok) {
526
- throw new Error(selectedPreset.result.error.code);
527
- }
528
- this.sessions.noteAgentPreset(id, this.config.session.agentPreset);
529
- }
530
- if (this.config.session.provider !== null &&
531
- this.config.session.model !== null) {
532
- const selected = await this.api.selectModel({
533
- sessionId: id,
534
- provider: this.config.session.provider,
535
- model: this.config.session.model,
536
- ...(this.config.session.reasoningEffort === null
537
- ? {}
538
- : { reasoningEffort: this.config.session.reasoningEffort }),
539
- });
540
- if (!selected.result.ok)
541
- throw new Error(selected.result.error.code);
542
- }
543
- return id;
544
- }
545
616
  async bind(id, options = {}) {
546
617
  const { report = true, attest = true, track = true } = options;
547
618
  this.sessions.open(id);
@@ -554,65 +625,61 @@ export class QaSessionController {
554
625
  throw new Error("Session binding is unavailable.");
555
626
  this.unbind();
556
627
  this.session = binding.session;
628
+ this.conversationBinding = this.conversation.binding(id);
557
629
  this.policyReady = false;
558
630
  this.unsubscribeSession = binding.session.subscribe(() => {
559
631
  this.admissionPending = false;
560
632
  this.operationError = null;
561
633
  this.publishSessionUpdate();
562
634
  });
635
+ // Subscribing the Chat target activates it, so the assembled transcript
636
+ // (nodes, partials, running calls) materializes for the projection.
637
+ this.unsubscribeChat = this.conversationBinding
638
+ .target("chat")
639
+ .subscribe(() => {
640
+ this.publishSessionUpdate();
641
+ });
563
642
  await waitFor(binding.session, (snapshot) => snapshot.openState === "open" || snapshot.openState === "error", this.timeoutMs);
564
643
  if (binding.session.getSnapshot().openState !== "open") {
565
644
  throw new Error("Session binding could not be opened.");
566
645
  }
567
- if (attest && !(await this.attestPolicy(report))) {
568
- throw new QaPolicyAttestationError();
646
+ if (attest) {
647
+ const step = await this.attestPolicy(report);
648
+ if (step.kind !== "ok")
649
+ throw new QaPolicyAttestationError();
569
650
  }
570
651
  if (track) {
571
652
  this.chatSessionId = String(this.session.sessionId);
572
653
  this.viewingSubagent = null;
573
654
  this.chats.addChat(String(this.session.sessionId));
655
+ this.accounts?.onSessionCreated(String(this.session.sessionId));
574
656
  }
575
657
  this.publish();
576
658
  }
577
659
  unbind() {
578
- this.clearStreamFlush();
660
+ this.streamPublisher.clear();
579
661
  this.unsubscribeSession?.();
580
662
  this.unsubscribeSession = undefined;
663
+ this.unsubscribeChat?.();
664
+ this.unsubscribeChat = undefined;
665
+ this.conversationBinding = undefined;
666
+ if (this.session !== undefined) {
667
+ // Leaving the chat retires its attachment URLs; a later re-bind
668
+ // re-resolves them from the durable store.
669
+ this.assets.release(String(this.session.sessionId));
670
+ }
581
671
  this.session = undefined;
672
+ this.hostSources.reset();
582
673
  this.admissionPending = false;
583
674
  this.policyReady = false;
584
675
  }
585
676
  waitForConnection() {
586
677
  return waitFor(this.connection, (description) => description !== undefined, this.timeoutMs);
587
678
  }
588
- /**
589
- * Project one session notification. A running turn's frames arrive at
590
- * animation-frame cadence, so re-projections are spaced at least
591
- * {@link streamIntervalMs} apart: the first frame of a window projects at
592
- * once and further frames are absorbed (the memoized render path replays
593
- * nothing, so absorbing a frame only defers it). Everything outside a
594
- * running turn - phase flips, errors, turn completion - projects
595
- * immediately.
596
- */
679
+ /** Project one session notification; the running-turn spacing policy
680
+ * (first frame at once, further frames absorbed) lives in the publisher. */
597
681
  publishSessionUpdate() {
598
- if (this.streamIntervalMs <= 0 ||
599
- this.session?.getSnapshot().running !== true) {
600
- this.clearStreamFlush();
601
- this.publish();
602
- return;
603
- }
604
- if (this.streamFlushTimer !== undefined)
605
- return;
606
- this.publish();
607
- this.streamFlushTimer = setTimeout(() => {
608
- this.streamFlushTimer = undefined;
609
- }, this.streamIntervalMs);
610
- }
611
- clearStreamFlush() {
612
- if (this.streamFlushTimer === undefined)
613
- return;
614
- clearTimeout(this.streamFlushTimer);
615
- this.streamFlushTimer = undefined;
682
+ this.streamPublisher.publish(this.session?.getSnapshot().running === true, () => this.publish());
616
683
  }
617
684
  publish() {
618
685
  if (this.disposed)
@@ -646,38 +713,29 @@ export class QaSessionController {
646
713
  return;
647
714
  }
648
715
  const snapshot = this.session.getSnapshot();
649
- const blocked = snapshot.pending.length > 0;
650
- const error = this.operationError ??
651
- (blocked
652
- ? "Для этого запроса нужно действие, недоступное в режиме помощника."
653
- : snapshot.removed || snapshot.openState === "error"
654
- ? "Этот чат больше недоступен."
655
- : null);
656
- const phase = !connected
657
- ? "reconnecting"
658
- : blocked
659
- ? "blocked"
660
- : snapshot.removed || snapshot.openState === "error"
661
- ? "error"
662
- : snapshot.openState !== "open"
663
- ? "creating"
664
- : snapshot.running || this.admissionPending
665
- ? "running"
666
- : "ready";
667
- this.state = {
668
- phase,
669
- sessionId: String(this.session.sessionId),
670
- messages: projectTranscript(snapshot, {
671
- showToolActivity: this.config.ui.showToolActivity,
672
- showReasoning: this.config.ui.showReasoning,
673
- }),
674
- error,
675
- canSend: connected && phase === "ready" && this.policyReady,
676
- canStop: connected && snapshot.running && this.config.ui.showStop && !blocked,
716
+ const sessionId = String(this.session.sessionId);
717
+ const conversationSnapshot = this.conversationBinding?.snapshot.getSnapshot();
718
+ const projectedSourceBundles = projectTurnSources(conversationSnapshot, sessionId, this.config.session.cwd ?? undefined);
719
+ const sourceBundles = this.hostSources.merge(projectedSourceBundles);
720
+ if (!snapshot.running) {
721
+ void this.hostSources.refresh(sessionId, () => this.publish());
722
+ }
723
+ this.state = projectBoundSessionState({
724
+ connected,
725
+ sessionId,
726
+ sessionSnapshot: snapshot,
727
+ conversationSnapshot,
728
+ sourceBundles,
729
+ // Ownership is chat-level: every user message of a foreign chat
730
+ // carries its owner's name when an admin reads it.
731
+ author: this.accounts?.messageAuthorOf(sessionId),
732
+ operationError: this.operationError,
733
+ policyReady: this.policyReady,
734
+ admissionPending: this.admissionPending,
677
735
  chatsRevision: this.chatsRevision,
678
- sources: projectSources(snapshot),
679
736
  viewingSubagent: this.viewingSubagent,
680
- };
737
+ config: this.config,
738
+ });
681
739
  this.emit();
682
740
  }
683
741
  fail(message, error) {
@@ -697,48 +755,48 @@ export class QaSessionController {
697
755
  };
698
756
  this.emit();
699
757
  }
758
+ /**
759
+ * Attest the currently bound session. `ok` carries the session id the proof
760
+ * was issued for; `stale` means the binding changed while the request was
761
+ * in flight, so nothing was written — the newer binding manages its own
762
+ * attestation; `refused` is a genuine admission refusal of this binding.
763
+ */
700
764
  async attestPolicy(reportFailure = true) {
701
- if (this.session === undefined)
702
- return false;
765
+ const session = this.session;
766
+ if (session === undefined)
767
+ return { kind: "refused" };
768
+ const sessionId = String(session.sessionId);
703
769
  if (!this.config.lockdown.enabled) {
704
770
  this.policyReady = true;
705
- return true;
771
+ return { kind: "ok", sessionId };
706
772
  }
707
773
  this.policyReady = false;
708
774
  this.publish();
709
- // One precise console diagnostic per refusal: the coarse Host reason code
710
- // plus an operator hint. The QA-facing message stays generic by design.
711
- const reject = (reason) => {
712
- if (reportFailure) {
713
- console.error(`dsh-qa-surface: policy attestation failed (reason: ${reason ?? "unknown"}). ${attestationHint(reason)}`);
714
- }
715
- this.policyReady = false;
716
- this.operationError = CONFIGURATION_ERROR;
717
- this.publish();
718
- return false;
719
- };
720
- try {
721
- const result = await this.secureSessionRemote(String(this.session.sessionId));
722
- if (!result.ok) {
723
- return reject(attestationReasonOf(result.error));
724
- }
725
- if (!proofMatchesConfig(result.value, this.config.lockdown, String(this.session.sessionId))) {
726
- return reject("proof-mismatch");
727
- }
775
+ const outcome = await attestQaPolicy({
776
+ secureSession: this.secureSessionRemote,
777
+ token: this.accounts?.token() ?? "",
778
+ sessionId,
779
+ lockdown: this.config.lockdown,
780
+ report: reportFailure,
781
+ });
782
+ if (this.session !== session)
783
+ return { kind: "stale" };
784
+ if (outcome.ok) {
728
785
  this.policyReady = true;
729
786
  this.operationError = null;
730
787
  this.publish();
731
- return true;
788
+ return { kind: "ok", sessionId };
732
789
  }
733
- catch (error) {
734
- if (reportFailure) {
735
- console.error("dsh-qa-surface: policy attestation request failed", error);
736
- }
737
- this.policyReady = false;
738
- this.operationError = CONFIGURATION_ERROR;
739
- this.publish();
740
- return false;
790
+ if (outcome.authRequired) {
791
+ // The identity expired or was rotated: back to the gate instead of
792
+ // the generic configuration error.
793
+ this.accounts?.onAuthRequired();
794
+ return { kind: "refused" };
741
795
  }
796
+ this.policyReady = false;
797
+ this.operationError = CONFIGURATION_ERROR;
798
+ this.publish();
799
+ return { kind: "refused" };
742
800
  }
743
801
  emit() {
744
802
  for (const listener of this.listeners)