@yadsh/dsh-qa-surface 0.17.0 → 0.19.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 (213) hide show
  1. package/README.md +35 -25
  2. package/compatibility.json +1 -1
  3. package/lib/account-remotes.js +4 -0
  4. package/lib/account-remotes.js.map +1 -1
  5. package/lib/accounts/database.js +86 -1
  6. package/lib/accounts/database.js.map +1 -1
  7. package/lib/accounts/file.js.map +1 -1
  8. package/lib/accounts/store.js +111 -5
  9. package/lib/accounts/store.js.map +1 -1
  10. package/lib/accounts/token.js +8 -2
  11. package/lib/accounts/token.js.map +1 -1
  12. package/lib/admin/metrics.js +15 -2
  13. package/lib/admin/metrics.js.map +1 -1
  14. package/lib/admin/read-budget.js +28 -0
  15. package/lib/admin/read-budget.js.map +1 -0
  16. package/lib/admin/service.js +135 -40
  17. package/lib/admin/service.js.map +1 -1
  18. package/lib/admin/session-log.js +63 -4
  19. package/lib/admin/session-log.js.map +1 -1
  20. package/lib/attachment-access.js +147 -0
  21. package/lib/attachment-access.js.map +1 -0
  22. package/lib/chat-sharing/native-fence.js +20 -0
  23. package/lib/chat-sharing/native-fence.js.map +1 -1
  24. package/lib/chat-sharing/search.js +89 -0
  25. package/lib/chat-sharing/search.js.map +1 -0
  26. package/lib/chat-sharing/transport.js +10 -9
  27. package/lib/chat-sharing/transport.js.map +1 -1
  28. package/lib/chat-sharing/uploads.js +79 -6
  29. package/lib/chat-sharing/uploads.js.map +1 -1
  30. package/lib/cli.js +13 -0
  31. package/lib/cli.js.map +1 -1
  32. package/lib/client/QaAccountsController.js +61 -0
  33. package/lib/client/QaAccountsController.js.map +1 -1
  34. package/lib/client/QaRouteController.js +3 -1
  35. package/lib/client/QaRouteController.js.map +1 -1
  36. package/lib/client/QaSessionController.js +141 -35
  37. package/lib/client/QaSessionController.js.map +1 -1
  38. package/lib/client/QaSurface.js +43 -13
  39. package/lib/client/QaSurface.js.map +1 -1
  40. package/lib/client/account-sessions.js +74 -8
  41. package/lib/client/account-sessions.js.map +1 -1
  42. package/lib/client/admin/CapabilityPicker.js +50 -0
  43. package/lib/client/admin/CapabilityPicker.js.map +1 -0
  44. package/lib/client/admin/CoverageNotice.js +9 -0
  45. package/lib/client/admin/CoverageNotice.js.map +1 -0
  46. package/lib/client/admin/QaAdmin.js +3 -150
  47. package/lib/client/admin/QaAdmin.js.map +1 -1
  48. package/lib/client/admin/RoleEditor.js +46 -0
  49. package/lib/client/admin/RoleEditor.js.map +1 -0
  50. package/lib/client/admin/capability-selection.js +63 -0
  51. package/lib/client/admin/capability-selection.js.map +1 -0
  52. package/lib/client/admin/pages/Conversations.js +35 -17
  53. package/lib/client/admin/pages/Conversations.js.map +1 -1
  54. package/lib/client/admin/pages/Overview.js +3 -2
  55. package/lib/client/admin/pages/Overview.js.map +1 -1
  56. package/lib/client/admin/pages/Quality.js +77 -20
  57. package/lib/client/admin/pages/Quality.js.map +1 -1
  58. package/lib/client/admin/pages/SkillFiles.js +14 -3
  59. package/lib/client/admin/pages/SkillFiles.js.map +1 -1
  60. package/lib/client/admin/shared.js +68 -3
  61. package/lib/client/admin/shared.js.map +1 -1
  62. package/lib/client/components/QaAuthGate.js +31 -28
  63. package/lib/client/components/QaAuthGate.js.map +1 -1
  64. package/lib/client/components/QaChangelog.js +71 -0
  65. package/lib/client/components/QaChangelog.js.map +1 -1
  66. package/lib/client/components/QaComposer.js +27 -4
  67. package/lib/client/components/QaComposer.js.map +1 -1
  68. package/lib/client/components/QaFormItem.js +63 -0
  69. package/lib/client/components/QaFormItem.js.map +1 -0
  70. package/lib/client/components/QaInputForm.js +281 -0
  71. package/lib/client/components/QaInputForm.js.map +1 -0
  72. package/lib/client/components/QaModal.js +17 -4
  73. package/lib/client/components/QaModal.js.map +1 -1
  74. package/lib/client/components/QaRequestWait.js +15 -0
  75. package/lib/client/components/QaRequestWait.js.map +1 -0
  76. package/lib/client/forms-styles.js +14 -0
  77. package/lib/client/forms-styles.js.map +1 -0
  78. package/lib/client/forms.js +40 -0
  79. package/lib/client/forms.js.map +1 -0
  80. package/lib/client/index.js +18 -2
  81. package/lib/client/index.js.map +1 -1
  82. package/lib/client/oidc-browser.js +39 -0
  83. package/lib/client/oidc-browser.js.map +1 -0
  84. package/lib/client/panels/PanelHost.js +23 -1
  85. package/lib/client/panels/PanelHost.js.map +1 -1
  86. package/lib/client/project-session-state.js.map +1 -1
  87. package/lib/client/request-wait-styles.js +7 -0
  88. package/lib/client/request-wait-styles.js.map +1 -0
  89. package/lib/client/settings/sections/interface.js +2 -1
  90. package/lib/client/settings/sections/interface.js.map +1 -1
  91. package/lib/client/settings/sections/status.js +3 -1
  92. package/lib/client/settings/sections/status.js.map +1 -1
  93. package/lib/client/styles.js +5 -3
  94. package/lib/client/styles.js.map +1 -1
  95. package/lib/client/types.js.map +1 -1
  96. package/lib/client.js +471 -72918
  97. package/lib/client.js.map +1 -1
  98. package/lib/config-resolvers/accounts.js +5 -4
  99. package/lib/config-resolvers/accounts.js.map +1 -1
  100. package/lib/config-resolvers/defaults.js +16 -0
  101. package/lib/config-resolvers/defaults.js.map +1 -1
  102. package/lib/config-resolvers/oidc.js +53 -0
  103. package/lib/config-resolvers/oidc.js.map +1 -0
  104. package/lib/config.js +71 -0
  105. package/lib/config.js.map +1 -1
  106. package/lib/filesystem-intent.js +39 -0
  107. package/lib/filesystem-intent.js.map +1 -0
  108. package/lib/forms/attachments.js +230 -0
  109. package/lib/forms/attachments.js.map +1 -0
  110. package/lib/forms/errors.js +37 -0
  111. package/lib/forms/errors.js.map +1 -0
  112. package/lib/forms/fingerprint.js +21 -0
  113. package/lib/forms/fingerprint.js.map +1 -0
  114. package/lib/forms/gate.js +125 -0
  115. package/lib/forms/gate.js.map +1 -0
  116. package/lib/forms/schema.js +182 -0
  117. package/lib/forms/schema.js.map +1 -0
  118. package/lib/forms/service.js +220 -0
  119. package/lib/forms/service.js.map +1 -0
  120. package/lib/forms/store.js +194 -0
  121. package/lib/forms/store.js.map +1 -0
  122. package/lib/forms/types.js +2 -0
  123. package/lib/forms/types.js.map +1 -0
  124. package/lib/forms/validation.js +113 -0
  125. package/lib/forms/validation.js.map +1 -0
  126. package/lib/index.js +271 -81
  127. package/lib/index.js.map +1 -1
  128. package/lib/oidc/http.js +30 -0
  129. package/lib/oidc/http.js.map +1 -0
  130. package/lib/oidc/service.js +250 -0
  131. package/lib/oidc/service.js.map +1 -0
  132. package/lib/qa-tools/activation-manager.js +24 -2
  133. package/lib/qa-tools/activation-manager.js.map +1 -1
  134. package/lib/qa-tools/catalog.js +11 -1
  135. package/lib/qa-tools/catalog.js.map +1 -1
  136. package/lib/qa-tools/index.js +27 -0
  137. package/lib/qa-tools/index.js.map +1 -1
  138. package/lib/qa-tools/request-user-form.js +121 -0
  139. package/lib/qa-tools/request-user-form.js.map +1 -0
  140. package/lib/request-scheduler.js +235 -0
  141. package/lib/request-scheduler.js.map +1 -0
  142. package/lib/resolve-config.js +2 -0
  143. package/lib/resolve-config.js.map +1 -1
  144. package/lib/secure-session.js +60 -60
  145. package/lib/secure-session.js.map +1 -1
  146. package/lib/session-create.js +125 -0
  147. package/lib/session-create.js.map +1 -0
  148. package/lib/typert.host.js +4636 -1103
  149. package/lib/typert.remote-client.d.ts +135 -5
  150. package/lib/typert.remote-client.js +4502 -1095
  151. package/lib/types/accounts/database.d.ts +8 -0
  152. package/lib/types/accounts/file.d.ts +2 -0
  153. package/lib/types/accounts/store.d.ts +17 -0
  154. package/lib/types/accounts/token.d.ts +3 -1
  155. package/lib/types/admin/metrics.d.ts +5 -1
  156. package/lib/types/admin/read-budget.d.ts +8 -0
  157. package/lib/types/admin/service.d.ts +17 -0
  158. package/lib/types/admin/session-log.d.ts +30 -1
  159. package/lib/types/attachment-access.d.ts +29 -0
  160. package/lib/types/chat-sharing/search.d.ts +5 -0
  161. package/lib/types/chat-sharing/uploads.d.ts +2 -1
  162. package/lib/types/client/QaAccountsController.d.ts +4 -0
  163. package/lib/types/client/QaRouteController.d.ts +1 -0
  164. package/lib/types/client/QaSessionController.d.ts +24 -12
  165. package/lib/types/client/QaSurface.d.ts +2 -0
  166. package/lib/types/client/admin/CapabilityPicker.d.ts +14 -0
  167. package/lib/types/client/admin/CoverageNotice.d.ts +6 -0
  168. package/lib/types/client/admin/RoleEditor.d.ts +15 -0
  169. package/lib/types/client/admin/capability-selection.d.ts +27 -0
  170. package/lib/types/client/admin/shared.d.ts +39 -1
  171. package/lib/types/client/components/QaAuthGate.d.ts +2 -0
  172. package/lib/types/client/components/QaFormItem.d.ts +20 -0
  173. package/lib/types/client/components/QaInputForm.d.ts +18 -0
  174. package/lib/types/client/components/QaRequestWait.d.ts +6 -0
  175. package/lib/types/client/forms-styles.d.ts +2 -0
  176. package/lib/types/client/forms.d.ts +23 -0
  177. package/lib/types/client/oidc-browser.d.ts +12 -0
  178. package/lib/types/client/project-session-state.d.ts +2 -4
  179. package/lib/types/client/request-wait-styles.d.ts +2 -0
  180. package/lib/types/client/types.d.ts +10 -4
  181. package/lib/types/config-resolvers/oidc.d.ts +3 -0
  182. package/lib/types/filesystem-intent.d.ts +12 -0
  183. package/lib/types/forms/attachments.d.ts +51 -0
  184. package/lib/types/forms/errors.d.ts +11 -0
  185. package/lib/types/forms/fingerprint.d.ts +9 -0
  186. package/lib/types/forms/gate.d.ts +33 -0
  187. package/lib/types/forms/schema.d.ts +72 -0
  188. package/lib/types/forms/service.d.ts +15 -0
  189. package/lib/types/forms/store.d.ts +25 -0
  190. package/lib/types/forms/types.d.ts +215 -0
  191. package/lib/types/forms/validation.d.ts +20 -0
  192. package/lib/types/index.d.ts +33 -12
  193. package/lib/types/oidc/http.d.ts +5 -0
  194. package/lib/types/oidc/service.d.ts +30 -0
  195. package/lib/types/oidc.js +2 -0
  196. package/lib/types/oidc.js.map +1 -0
  197. package/lib/types/qa-tools/activation-manager.d.ts +5 -0
  198. package/lib/types/qa-tools/catalog.d.ts +4 -1
  199. package/lib/types/qa-tools/index.d.ts +5 -1
  200. package/lib/types/qa-tools/request-user-form.d.ts +5 -0
  201. package/lib/types/request-scheduler.d.ts +28 -0
  202. package/lib/types/secure-session.d.ts +13 -26
  203. package/lib/types/session-create.d.ts +29 -0
  204. package/lib/types/types/accounts.d.ts +2 -0
  205. package/lib/types/types/admin.d.ts +6 -0
  206. package/lib/types/types/config.d.ts +6 -0
  207. package/lib/types/types/oidc.d.ts +36 -0
  208. package/lib/types/types/session.d.ts +8 -0
  209. package/lib/types/types.d.ts +2 -0
  210. package/lib/types/user-workspace.d.ts +4 -6
  211. package/lib/user-workspace.js +13 -67
  212. package/lib/user-workspace.js.map +1 -1
  213. package/package.json +66 -62
@@ -36,15 +36,17 @@ const CONFIGURATION_ERROR = "Настройки помощника недост
36
36
  const SLASH_CATALOG_STALE_MS = 3_000;
37
37
  /**
38
38
  * How long a refused retain waits for its row to come into this browser's
39
- * Session catalog. Short on purpose, and shorter than the adoption timeout: the
40
- * row arrives either as a Host push or as the answer of the one pull asked for
41
- * here, so it is a round-trip away — while a whole `timeoutMs` of patience
42
- * would stack a second window onto the waits that follow and hold a chat that
43
- * never opens on screen for two of them. A row that misses this window loses
44
- * the chat, and the plate's «Повторить» re-runs the adoption on a catalog that
45
- * has had the pull.
39
+ * Session catalog. Shorter than the adoption timeout, but wide enough for the
40
+ * way the row actually arrives on a QA deployment: this surface has no
41
+ * forwarded-event push of its own — the row rides on the account's chat
42
+ * listing — and a listing that lands right after the create pays for the
43
+ * Host's shared catalog scan the create itself invalidated, which on a loaded
44
+ * stand takes longer than a round trip. The interval keeps asking for the
45
+ * listing while this waits, so the window only has to outlast one slow scan.
46
+ * A row that misses it loses the chat, and the plate's «Повторить» re-runs
47
+ * the adoption on a catalog that has had the pull.
46
48
  */
47
- const CATALOG_ROW_PATIENCE_MS = 5_000;
49
+ const CATALOG_ROW_PATIENCE_MS = 15_000;
48
50
  /**
49
51
  * Spacing of the parked-request poll. A request is answered by a person, so a
50
52
  * second of latency is invisible; the poll only runs while a turn runs.
@@ -129,7 +131,10 @@ export class QaSessionController {
129
131
  unsubscribeConnection;
130
132
  ensuring;
131
133
  materializing;
134
+ /** A successful create survives a failed adoption; retry must not create again. */
135
+ draftSessionId;
132
136
  operationError = null;
137
+ operationDiagnostic = null;
133
138
  admissionPending = false;
134
139
  pendingSubmission;
135
140
  pendingSequence = 0;
@@ -170,9 +175,9 @@ export class QaSessionController {
170
175
  * both a failed adoption and {@link unbind} take it back. The converse does
171
176
  * not hold: a held session may name nothing, which is the transcript of an
172
177
  * attestation this chat could not pass and whose caller still reads it. So a
173
- * chat whose first send fell short keeps its identity while naming no session,
174
- * and the retry — which brings a fresh id — stays in the composer the question
175
- * was typed into instead of reading as a move to another chat.
178
+ * chat whose first send fell short keeps its identity while naming no session;
179
+ * a retry adopts the successful create it already holds and preserves the
180
+ * composer the question was typed into.
176
181
  */
177
182
  namedSession = null;
178
183
  /**
@@ -193,6 +198,10 @@ export class QaSessionController {
193
198
  /** Host-side question requests of the bound chat waiting for the operator. */
194
199
  hostQuestions;
195
200
  queueRemote;
201
+ requestRemote;
202
+ requestWaitStatus = null;
203
+ requestReadRevision = 0;
204
+ requestReading = false;
196
205
  /** Last send the stand had no room for; drives the request-ceiling dialog. */
197
206
  requestQueueNotice = null;
198
207
  /** Set while a running turn is polled for parked requests. */
@@ -261,6 +270,7 @@ export class QaSessionController {
261
270
  this.hostApprovals = new QaHostApprovalBridge(options.approvalApi);
262
271
  this.hostQuestions = new QaHostQuestionBridge(options.questionApi);
263
272
  this.queueRemote = options.queueStatus;
273
+ this.requestRemote = options.requestStatus;
264
274
  // Cold account catalogs can outlive one short network round trip.
265
275
  // Keep bootstrap bounded without rejecting a valid slow restoration.
266
276
  this.timeoutMs = options.timeoutMs ?? 60_000;
@@ -287,6 +297,16 @@ export class QaSessionController {
287
297
  this.ensuring = operation;
288
298
  return operation;
289
299
  }
300
+ /** Retry opening a failed draft without abandoning its successful create. */
301
+ async retrySession() {
302
+ if (this.drafting)
303
+ await this.materializeDraft();
304
+ else
305
+ await this.ensureSession();
306
+ }
307
+ failureDiagnostic() {
308
+ return this.operationError === null ? null : this.operationDiagnostic;
309
+ }
290
310
  /**
291
311
  * Select a different capability profile for the next agent. The current
292
312
  * agent is never mutated: even a blank materialized chat is abandoned in
@@ -415,7 +435,7 @@ export class QaSessionController {
415
435
  // the submission rides exactly the way it always did.
416
436
  if (this.disposed || (bound !== undefined && this.session !== bound))
417
437
  return false;
418
- if (ceiling !== null && ceiling.full) {
438
+ if (ceiling !== null && ceiling.full && ceiling.managed !== true) {
419
439
  this.requestQueueNotice = ceiling;
420
440
  this.publish();
421
441
  return false;
@@ -538,10 +558,8 @@ export class QaSessionController {
538
558
  /**
539
559
  * What the stand answers about its load, or null when it does not answer.
540
560
  *
541
- * Every failure here sends the question anyway. The ceiling exists to keep a
542
- * weak model from drowning, not to be a door a browser cannot open: an
543
- * unreadable count says nothing about the load, and refusing a visitor on
544
- * that would be a worse answer than the one they would have got.
561
+ * A failed status read does not bypass the server queue. Submit normally;
562
+ * the Host owns admission even when the browser cannot read its load.
545
563
  */
546
564
  async readQueueStatus() {
547
565
  if (this.queueRemote === undefined)
@@ -555,6 +573,35 @@ export class QaSessionController {
555
573
  return null;
556
574
  }
557
575
  }
576
+ async refreshRequestStatus(sessionId, token) {
577
+ if (this.requestRemote === undefined || this.requestReading)
578
+ return;
579
+ this.requestReading = true;
580
+ const revision = this.requestReadRevision;
581
+ try {
582
+ const result = await this.requestRemote(token, sessionId);
583
+ if (this.disposed ||
584
+ revision !== this.requestReadRevision ||
585
+ String(this.session?.sessionId) !== sessionId)
586
+ return;
587
+ if (!result.ok)
588
+ return;
589
+ const status = result.value.managed === true && result.value.waiting === true
590
+ ? result.value
591
+ : null;
592
+ if (JSON.stringify(status) === JSON.stringify(this.requestWaitStatus))
593
+ return;
594
+ this.requestWaitStatus = status;
595
+ this.publish();
596
+ }
597
+ catch {
598
+ // Keep the last known position during a transient transport failure.
599
+ }
600
+ finally {
601
+ if (revision === this.requestReadRevision)
602
+ this.requestReading = false;
603
+ }
604
+ }
558
605
  /**
559
606
  * Close the request-ceiling dialog. Nothing is retried and nothing is
560
607
  * dropped: the held-back question is still the composer's own text, so asking
@@ -948,6 +995,7 @@ export class QaSessionController {
948
995
  enterDraft() {
949
996
  this.generation += 1;
950
997
  this.materializing = undefined;
998
+ this.draftSessionId = undefined;
951
999
  this.drafting = true;
952
1000
  if (this.namedSession !== null) {
953
1001
  this.openChat();
@@ -985,14 +1033,17 @@ export class QaSessionController {
985
1033
  const attempt = (async () => {
986
1034
  try {
987
1035
  await this.waitForConnection();
988
- const id = await createQaSession({
989
- createSession: this.createSessionRemote,
990
- token: this.accounts?.token() ?? "",
991
- subroleId: this.selectedSubrole,
992
- adminPreview: this.adminPreview,
993
- });
1036
+ const id = this.draftSessionId ??
1037
+ (await createQaSession({
1038
+ createSession: this.createSessionRemote,
1039
+ token: this.accounts?.token() ?? "",
1040
+ subroleId: this.selectedSubrole,
1041
+ adminPreview: this.adminPreview,
1042
+ }));
994
1043
  if (this.disposed || operation !== this.generation)
995
1044
  return false;
1045
+ this.draftSessionId = id;
1046
+ this.sessions.rememberAdmitted?.(id);
996
1047
  // A step back is not a materialized draft: this adoption never ended up
997
1048
  // holding a session, so the draft has to stay a draft. Calling it done
998
1049
  // leaves a chat with neither a session nor a draft behind, and every
@@ -1002,6 +1053,7 @@ export class QaSessionController {
1002
1053
  if (this.disposed || operation !== this.generation)
1003
1054
  return false;
1004
1055
  this.drafting = false;
1056
+ this.draftSessionId = undefined;
1005
1057
  if (this.config.session.policy === "browser-persistent") {
1006
1058
  this.chats.saveActive(id);
1007
1059
  }
@@ -1314,7 +1366,36 @@ export class QaSessionController {
1314
1366
  this.emit();
1315
1367
  try {
1316
1368
  await this.waitForConnection();
1317
- const list = await waitFor(this.sessions.list, (snapshot) => snapshot.phase === "ready", this.timeoutMs);
1369
+ const saved = this.config.session.policy === "browser-persistent"
1370
+ ? this.chats.activeId()
1371
+ : null;
1372
+ let admitted = null;
1373
+ const savedSummary = saved === null
1374
+ ? undefined
1375
+ : this.sessions.list.getSnapshot().byId[saved];
1376
+ if (saved !== null &&
1377
+ this.accounts !== undefined &&
1378
+ this.sharingApi !== undefined &&
1379
+ this.sessions.rememberAdmitted !== undefined &&
1380
+ (savedSummary === undefined || !isDelegatedSession(savedSummary))) {
1381
+ const identity = this.accounts.token();
1382
+ if (identity !== null) {
1383
+ const access = await this.sharingApi.access(identity, saved);
1384
+ if (this.disposed ||
1385
+ operation !== this.generation ||
1386
+ identity !== this.accounts.token())
1387
+ return;
1388
+ if (access.ok && access.value.sessionId === saved) {
1389
+ // A direct Host read admission is sufficient to reopen this exact
1390
+ // chat. A cold deployment-wide catalog scan must not gate restore.
1391
+ this.sessions.rememberAdmitted(saved);
1392
+ admitted = saved;
1393
+ }
1394
+ }
1395
+ }
1396
+ const list = admitted === null
1397
+ ? await waitFor(this.sessions.list, (snapshot) => snapshot.phase === "ready", this.timeoutMs)
1398
+ : this.sessions.list.getSnapshot();
1318
1399
  let id = null;
1319
1400
  let restored = false;
1320
1401
  let existing = false;
@@ -1335,8 +1416,8 @@ export class QaSessionController {
1335
1416
  // falls through to a draft instead of restoring a transcript nobody can
1336
1417
  // send into.
1337
1418
  if (stored !== null &&
1338
- summary !== undefined &&
1339
- !isDelegatedSession(summary)) {
1419
+ (stored === admitted ||
1420
+ (summary !== undefined && !isDelegatedSession(summary)))) {
1340
1421
  id = stored;
1341
1422
  restored = true;
1342
1423
  existing = true;
@@ -1603,7 +1684,7 @@ export class QaSessionController {
1603
1684
  // chat: it takes its own identity, so nothing the previous chat was holding
1604
1685
  // — an unsent question, staged attachments, an open drawer — walks into it.
1605
1686
  // A chat that names no session adopts under the identity it already has,
1606
- // which is what keeps a retried first send — it brings a fresh id — in the
1687
+ // which is what keeps a retried first send in the
1607
1688
  // composer the question was typed into.
1608
1689
  if (this.namedSession !== null && this.namedSession !== id)
1609
1690
  this.openChat();
@@ -1841,6 +1922,9 @@ export class QaSessionController {
1841
1922
  // answers for its own sends.
1842
1923
  this.requestQueueNotice = null;
1843
1924
  this.policyReady = false;
1925
+ this.requestWaitStatus = null;
1926
+ this.requestReadRevision += 1;
1927
+ this.requestReading = false;
1844
1928
  this.inheritedThroughSeq = undefined;
1845
1929
  this.compatibilityReadOnly = false;
1846
1930
  // The catalog belongs to the chat that produced it: another chat has a
@@ -1897,7 +1981,7 @@ export class QaSessionController {
1897
1981
  ? "reconnecting"
1898
1982
  : "idle",
1899
1983
  error: this.operationError,
1900
- requestQueue: this.requestQueueNotice,
1984
+ requestQueue: this.requestWaitStatus ?? this.requestQueueNotice,
1901
1985
  canSend: connected && !materializing,
1902
1986
  pendingMessage: this.pendingSubmission?.message ?? null,
1903
1987
  chatsRevision: this.chatsRevision,
@@ -1911,7 +1995,7 @@ export class QaSessionController {
1911
1995
  ...this.state,
1912
1996
  phase: !connected && this.connectedOnce ? "reconnecting" : this.state.phase,
1913
1997
  error: this.operationError ?? this.state.error,
1914
- requestQueue: this.requestQueueNotice,
1998
+ requestQueue: this.requestWaitStatus ?? this.requestQueueNotice,
1915
1999
  };
1916
2000
  this.emit();
1917
2001
  return;
@@ -1949,7 +2033,7 @@ export class QaSessionController {
1949
2033
  // carries its owner's name when an admin reads it.
1950
2034
  author: this.accounts?.messageAuthorOf(sessionId),
1951
2035
  operationError: this.operationError,
1952
- requestQueue: this.requestQueueNotice,
2036
+ requestQueue: this.requestWaitStatus ?? this.requestQueueNotice,
1953
2037
  policyReady: this.policyReady,
1954
2038
  compatibilityReadOnly: this.compatibilityReadOnly,
1955
2039
  admissionPending: this.admissionPending || this.pendingSubmission !== undefined,
@@ -2124,13 +2208,28 @@ export class QaSessionController {
2124
2208
  this.publish();
2125
2209
  }
2126
2210
  fail(message, error) {
2127
- // Policy attestation refusals already logged their precise reason; a
2128
- // second stack trace for the wrapper error is only console noise.
2129
- if (!(error instanceof QaPolicyAttestationError)) {
2130
- console.error("dsh-qa-surface: session operation failed", error);
2131
- }
2132
2211
  if (this.disposed)
2133
2212
  return;
2213
+ const detail = error instanceof Error ? error.message : String(error);
2214
+ const reason = /transport|network|fetch|HTTP|connection|timed? ?out|timeout/iu.test(detail)
2215
+ ? "Не удалось связаться со стендом или дождаться его ответа."
2216
+ : /sessions\.retain|unknown session|catalog/iu.test(detail)
2217
+ ? "Стенд создал чат, но его запись ещё недоступна в истории."
2218
+ : error instanceof QaPolicyAttestationError
2219
+ ? "Настройки чата не прошли проверку политики стенда."
2220
+ : "Стенд отклонил операцию. Подробности доступны оператору по коду обращения.";
2221
+ // A diagnostic label, not an authority token. This also works on a LAN
2222
+ // HTTP origin where crypto.randomUUID is unavailable.
2223
+ const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
2224
+ this.operationDiagnostic = { reason, id };
2225
+ if (error instanceof QaPolicyAttestationError) {
2226
+ // The policy probe already logged the precise refusal without a wrapper
2227
+ // stack. Add only the reference the user sees on the plate.
2228
+ console.info("dsh-qa-surface: policy failure reference", { id });
2229
+ }
2230
+ else {
2231
+ console.error("dsh-qa-surface: session operation failed", error, { id });
2232
+ }
2134
2233
  this.operationError = message;
2135
2234
  this.state = {
2136
2235
  ...QA_SESSION_IDLE_STATE,
@@ -2281,13 +2380,18 @@ export class QaSessionController {
2281
2380
  : false;
2282
2381
  const pending = this.hostApprovals.list().length > 0 ||
2283
2382
  this.hostQuestions.list().length > 0;
2284
- const wanted = ((running && (approvals || questions)) || pending) &&
2383
+ const queue = this.requestRemote !== undefined &&
2384
+ this.config.session.maxActiveRequests > 0;
2385
+ const wanted = ((running && (approvals || questions || queue)) || pending) &&
2285
2386
  // A subagent watched from the panel is a read-only view of a session the
2286
2387
  // chat owns: its own requests belong to the chat, not to this binding.
2287
2388
  this.viewingSubagent === null &&
2288
2389
  this.session !== undefined;
2289
2390
  if (!wanted) {
2290
2391
  this.stopPendingPolling();
2392
+ this.requestWaitStatus = null;
2393
+ this.requestReadRevision += 1;
2394
+ this.requestReading = false;
2291
2395
  return;
2292
2396
  }
2293
2397
  if (this.pendingTimer !== undefined || this.session === undefined)
@@ -2295,6 +2399,8 @@ export class QaSessionController {
2295
2399
  const sessionId = String(this.session.sessionId);
2296
2400
  const token = this.hostToken();
2297
2401
  const tick = () => {
2402
+ if (queue)
2403
+ void this.refreshRequestStatus(sessionId, token);
2298
2404
  // A seam still on screen is re-read even where its config gate has since
2299
2405
  // closed: the Host is the authority on what it holds, and an empty answer
2300
2406
  // is what takes a settled request off the screen.