@yadsh/dsh-qa-surface 0.18.0 → 0.20.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 (243) hide show
  1. package/README.md +201 -22
  2. package/lib/access/model-choices.js +103 -0
  3. package/lib/access/model-choices.js.map +1 -0
  4. package/lib/access/model-policy.js +79 -7
  5. package/lib/access/model-policy.js.map +1 -1
  6. package/lib/access/model.js +29 -1
  7. package/lib/access/model.js.map +1 -1
  8. package/lib/access/queue-policy.js +36 -0
  9. package/lib/access/queue-policy.js.map +1 -0
  10. package/lib/access/service.js +124 -7
  11. package/lib/access/service.js.map +1 -1
  12. package/lib/account-remotes.js +10 -1
  13. package/lib/account-remotes.js.map +1 -1
  14. package/lib/accounts/database.js +44 -3
  15. package/lib/accounts/database.js.map +1 -1
  16. package/lib/accounts/store.js +28 -0
  17. package/lib/accounts/store.js.map +1 -1
  18. package/lib/admin/service.js +6 -1
  19. package/lib/admin/service.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/search.js +89 -0
  23. package/lib/chat-sharing/search.js.map +1 -0
  24. package/lib/chat-sharing/transport.js +8 -9
  25. package/lib/chat-sharing/transport.js.map +1 -1
  26. package/lib/client/QaAccountsController.js +43 -8
  27. package/lib/client/QaAccountsController.js.map +1 -1
  28. package/lib/client/QaSessionController.js +106 -32
  29. package/lib/client/QaSessionController.js.map +1 -1
  30. package/lib/client/QaSurface.js +64 -5
  31. package/lib/client/QaSurface.js.map +1 -1
  32. package/lib/client/admin/ModelPolicyEditor.js +28 -2
  33. package/lib/client/admin/ModelPolicyEditor.js.map +1 -1
  34. package/lib/client/admin/RoleEditor.js +18 -1
  35. package/lib/client/admin/RoleEditor.js.map +1 -1
  36. package/lib/client/admin/pages/SkillFiles.js +1 -1
  37. package/lib/client/admin/pages/SkillFiles.js.map +1 -1
  38. package/lib/client/admin/pages/Users.js +36 -16
  39. package/lib/client/admin/pages/Users.js.map +1 -1
  40. package/lib/client/admin/styles.js +3 -0
  41. package/lib/client/admin/styles.js.map +1 -1
  42. package/lib/client/code-source-styles.js +12 -0
  43. package/lib/client/code-source-styles.js.map +1 -0
  44. package/lib/client/components/QaChangelog.js +96 -0
  45. package/lib/client/components/QaChangelog.js.map +1 -1
  46. package/lib/client/components/QaChatActions.js +36 -14
  47. package/lib/client/components/QaChatActions.js.map +1 -1
  48. package/lib/client/components/QaCodeSourcePicker.js +18 -0
  49. package/lib/client/components/QaCodeSourcePicker.js.map +1 -0
  50. package/lib/client/components/QaComposer.js +11 -12
  51. package/lib/client/components/QaComposer.js.map +1 -1
  52. package/lib/client/components/QaComposerSettings.js +202 -0
  53. package/lib/client/components/QaComposerSettings.js.map +1 -0
  54. package/lib/client/components/QaFormItem.js +74 -38
  55. package/lib/client/components/QaFormItem.js.map +1 -1
  56. package/lib/client/components/QaHistoryCatalog.js +261 -0
  57. package/lib/client/components/QaHistoryCatalog.js.map +1 -0
  58. package/lib/client/components/QaInputForm.js +115 -32
  59. package/lib/client/components/QaInputForm.js.map +1 -1
  60. package/lib/client/components/QaModelSelector.js +21 -0
  61. package/lib/client/components/QaModelSelector.js.map +1 -0
  62. package/lib/client/components/QaRequestWait.js +15 -0
  63. package/lib/client/components/QaRequestWait.js.map +1 -0
  64. package/lib/client/components/QaSidebar.js +25 -24
  65. package/lib/client/components/QaSidebar.js.map +1 -1
  66. package/lib/client/composer-settings-styles.js +21 -0
  67. package/lib/client/composer-settings-styles.js.map +1 -0
  68. package/lib/client/forms-styles.js +35 -11
  69. package/lib/client/forms-styles.js.map +1 -1
  70. package/lib/client/history-organization-styles.js +31 -0
  71. package/lib/client/history-organization-styles.js.map +1 -0
  72. package/lib/client/history-organization.js +155 -0
  73. package/lib/client/history-organization.js.map +1 -0
  74. package/lib/client/index.js +13 -2
  75. package/lib/client/index.js.map +1 -1
  76. package/lib/client/project-session-state.js.map +1 -1
  77. package/lib/client/request-wait-styles.js +7 -0
  78. package/lib/client/request-wait-styles.js.map +1 -0
  79. package/lib/client/settings/sections/attachments.js +7 -1
  80. package/lib/client/settings/sections/attachments.js.map +1 -1
  81. package/lib/client/settings/sections/session.js +1 -1
  82. package/lib/client/settings/sections/session.js.map +1 -1
  83. package/lib/client/styles.js +20 -1
  84. package/lib/client/styles.js.map +1 -1
  85. package/lib/client/types.js.map +1 -1
  86. package/lib/client/use-chat-code-source.js +156 -0
  87. package/lib/client/use-chat-code-source.js.map +1 -0
  88. package/lib/client/use-history-organization.js +48 -0
  89. package/lib/client/use-history-organization.js.map +1 -0
  90. package/lib/client/use-model-selection.js +96 -0
  91. package/lib/client/use-model-selection.js.map +1 -0
  92. package/lib/client/use-qa-file-drop.js +81 -0
  93. package/lib/client/use-qa-file-drop.js.map +1 -0
  94. package/lib/client/user-settings/IntegrationTokensPage.js +42 -6
  95. package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -1
  96. package/lib/client/user-settings/UserSettingsDialog.js +1 -1
  97. package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
  98. package/lib/client.js +131 -27
  99. package/lib/client.js.map +1 -1
  100. package/lib/code-source-selection.js +102 -0
  101. package/lib/code-source-selection.js.map +1 -0
  102. package/lib/config-resolvers/accounts.js +26 -1
  103. package/lib/config-resolvers/accounts.js.map +1 -1
  104. package/lib/config-resolvers/defaults.js +5 -0
  105. package/lib/config-resolvers/defaults.js.map +1 -1
  106. package/lib/config-resolvers/integration.js +8 -1
  107. package/lib/config-resolvers/integration.js.map +1 -1
  108. package/lib/config-resolvers/session.js +2 -0
  109. package/lib/config-resolvers/session.js.map +1 -1
  110. package/lib/config.js +51 -3
  111. package/lib/config.js.map +1 -1
  112. package/lib/enforcement/skill-policy.js +37 -2
  113. package/lib/enforcement/skill-policy.js.map +1 -1
  114. package/lib/filesystem-intent.js +39 -0
  115. package/lib/filesystem-intent.js.map +1 -0
  116. package/lib/forms/attachments.js +18 -2
  117. package/lib/forms/attachments.js.map +1 -1
  118. package/lib/forms/errors.js +6 -1
  119. package/lib/forms/errors.js.map +1 -1
  120. package/lib/forms/gate.js +28 -2
  121. package/lib/forms/gate.js.map +1 -1
  122. package/lib/forms/schema-errors.js +47 -0
  123. package/lib/forms/schema-errors.js.map +1 -0
  124. package/lib/forms/schema.js +29 -20
  125. package/lib/forms/schema.js.map +1 -1
  126. package/lib/index.js +189 -84
  127. package/lib/index.js.map +1 -1
  128. package/lib/integration/http.js +9 -10
  129. package/lib/integration/http.js.map +1 -1
  130. package/lib/integration/service.js +9 -0
  131. package/lib/integration/service.js.map +1 -1
  132. package/lib/personal-skills/provider.js +9 -1
  133. package/lib/personal-skills/provider.js.map +1 -1
  134. package/lib/personal-skills/service.js +4 -0
  135. package/lib/personal-skills/service.js.map +1 -1
  136. package/lib/personal-skills/shared-seed.js +65 -0
  137. package/lib/personal-skills/shared-seed.js.map +1 -0
  138. package/lib/prompt-notes.js +19 -1
  139. package/lib/prompt-notes.js.map +1 -1
  140. package/lib/provenance/collector.js +1 -0
  141. package/lib/provenance/collector.js.map +1 -1
  142. package/lib/provenance/extractors.js +6 -1
  143. package/lib/provenance/extractors.js.map +1 -1
  144. package/lib/qa-tools/catalog.js +18 -1
  145. package/lib/qa-tools/catalog.js.map +1 -1
  146. package/lib/qa-tools/code-files.js +207 -0
  147. package/lib/qa-tools/code-files.js.map +1 -0
  148. package/lib/qa-tools/code-tools.js +158 -0
  149. package/lib/qa-tools/code-tools.js.map +1 -0
  150. package/lib/qa-tools/docs-tools.js +49 -12
  151. package/lib/qa-tools/docs-tools.js.map +1 -1
  152. package/lib/qa-tools/index.js +4 -0
  153. package/lib/qa-tools/index.js.map +1 -1
  154. package/lib/qa-tools/request-user-form.js +27 -7
  155. package/lib/qa-tools/request-user-form.js.map +1 -1
  156. package/lib/request-scheduler.js +262 -0
  157. package/lib/request-scheduler.js.map +1 -0
  158. package/lib/resolve-config.js +9 -1
  159. package/lib/resolve-config.js.map +1 -1
  160. package/lib/secure-session.js +62 -56
  161. package/lib/secure-session.js.map +1 -1
  162. package/lib/session-create.js +125 -0
  163. package/lib/session-create.js.map +1 -0
  164. package/lib/session-model.js +43 -1
  165. package/lib/session-model.js.map +1 -1
  166. package/lib/shared/history-folder.js +18 -0
  167. package/lib/shared/history-folder.js.map +1 -0
  168. package/lib/typert.host.js +793 -159
  169. package/lib/typert.remote-client.d.ts +19 -7
  170. package/lib/typert.remote-client.js +726 -148
  171. package/lib/types/access/model-choices.d.ts +16 -0
  172. package/lib/types/access/model-policy.d.ts +13 -7
  173. package/lib/types/access/model.d.ts +9 -0
  174. package/lib/types/access/queue-policy.d.ts +30 -0
  175. package/lib/types/access/service.d.ts +27 -0
  176. package/lib/types/account-remotes.d.ts +5 -4
  177. package/lib/types/accounts/database.d.ts +5 -0
  178. package/lib/types/accounts/store.d.ts +6 -0
  179. package/lib/types/attachment-access.d.ts +29 -0
  180. package/lib/types/chat-sharing/search.d.ts +5 -0
  181. package/lib/types/chat-sharing/transport.d.ts +1 -1
  182. package/lib/types/client/QaAccountsController.d.ts +3 -1
  183. package/lib/types/client/QaSessionController.d.ts +31 -14
  184. package/lib/types/client/QaSurface.d.ts +2 -0
  185. package/lib/types/client/admin/ModelPolicyEditor.d.ts +2 -0
  186. package/lib/types/client/code-source-styles.d.ts +2 -0
  187. package/lib/types/client/components/QaChatActions.d.ts +3 -1
  188. package/lib/types/client/components/QaCodeSourcePicker.d.ts +9 -0
  189. package/lib/types/client/components/QaComposer.d.ts +2 -0
  190. package/lib/types/client/components/QaComposerSettings.d.ts +16 -0
  191. package/lib/types/client/components/QaFormItem.d.ts +4 -2
  192. package/lib/types/client/components/QaHistoryCatalog.d.ts +19 -0
  193. package/lib/types/client/components/QaInputForm.d.ts +3 -2
  194. package/lib/types/client/components/QaModelSelector.d.ts +4 -0
  195. package/lib/types/client/components/QaRequestWait.d.ts +6 -0
  196. package/lib/types/client/components/QaSidebar.d.ts +3 -0
  197. package/lib/types/client/composer-settings-styles.d.ts +2 -0
  198. package/lib/types/client/forms-styles.d.ts +1 -1
  199. package/lib/types/client/history-organization-styles.d.ts +3 -0
  200. package/lib/types/client/history-organization.d.ts +38 -0
  201. package/lib/types/client/project-session-state.d.ts +2 -4
  202. package/lib/types/client/request-wait-styles.d.ts +2 -0
  203. package/lib/types/client/types.d.ts +15 -7
  204. package/lib/types/client/use-chat-code-source.d.ts +20 -0
  205. package/lib/types/client/use-history-organization.d.ts +8 -0
  206. package/lib/types/client/use-model-selection.d.ts +24 -0
  207. package/lib/types/client/use-qa-file-drop.d.ts +12 -0
  208. package/lib/types/code-source-selection.d.ts +28 -0
  209. package/lib/types/code-sources.js +2 -0
  210. package/lib/types/code-sources.js.map +1 -0
  211. package/lib/types/config-resolvers/integration.d.ts +7 -1
  212. package/lib/types/filesystem-intent.d.ts +12 -0
  213. package/lib/types/forms/attachments.d.ts +5 -2
  214. package/lib/types/forms/errors.d.ts +3 -2
  215. package/lib/types/forms/gate.d.ts +4 -2
  216. package/lib/types/forms/schema-errors.d.ts +7 -0
  217. package/lib/types/forms/types.d.ts +14 -0
  218. package/lib/types/index.d.ts +41 -15
  219. package/lib/types/integration/http.d.ts +1 -3
  220. package/lib/types/integration/service.d.ts +2 -0
  221. package/lib/types/personal-skills/shared-seed.d.ts +12 -0
  222. package/lib/types/prompt-notes.d.ts +5 -1
  223. package/lib/types/qa-tools/catalog.d.ts +4 -3
  224. package/lib/types/qa-tools/code-files.d.ts +59 -0
  225. package/lib/types/qa-tools/code-tools.d.ts +7 -0
  226. package/lib/types/qa-tools/docs-tools.d.ts +4 -0
  227. package/lib/types/qa-tools/index.d.ts +3 -1
  228. package/lib/types/qa-tools/request-user-form.d.ts +1 -1
  229. package/lib/types/request-scheduler.d.ts +47 -0
  230. package/lib/types/secure-session.d.ts +12 -26
  231. package/lib/types/session-create.d.ts +29 -0
  232. package/lib/types/session-model.d.ts +11 -2
  233. package/lib/types/shared/history-folder.d.ts +4 -0
  234. package/lib/types/types/accounts.d.ts +11 -0
  235. package/lib/types/types/capability.d.ts +31 -4
  236. package/lib/types/types/code-sources.d.ts +12 -0
  237. package/lib/types/types/config.d.ts +20 -0
  238. package/lib/types/types/session.d.ts +8 -0
  239. package/lib/types/types.d.ts +1 -0
  240. package/lib/types/user-workspace.d.ts +4 -6
  241. package/lib/user-workspace.js +20 -71
  242. package/lib/user-workspace.js.map +1 -1
  243. package/package.json +13 -10
package/README.md CHANGED
@@ -23,10 +23,74 @@ Session and Agent Loop.
23
23
 
24
24
  ## What it does
25
25
 
26
+ Chat history supports virtual folders and a separate pinned section. Create,
27
+ rename, collapse and delete folders; drag chats into folders, out to the root,
28
+ or into the pinned section. Drag rows or folder headers to arrange them, or use
29
+ the chat menu and folder settings with a keyboard. Unpinning returns a chat to
30
+ its previous folder. Moving a pinned chat into a folder unpins it. Deleting a
31
+ folder leaves its chats in history, preserving any pins. Search includes folder
32
+ names and reveals matching chats inside collapsed folders.
33
+
34
+ This organization is browser-local and scoped to the QA route and account ID.
35
+ It survives reloads and updates other tabs on the same origin through storage
36
+ events; it does not synchronize between devices. Folder membership and pins
37
+ change only presentation metadata: session IDs, workspace roots, physical files,
38
+ ownership and access remain as they were.
39
+
40
+ Under **Integration tokens**, choose **Folder for new chats** when creating a
41
+ credential, or change the destination on an existing active token without
42
+ reissuing it. This default is stored on the server as a virtual folder ID and
43
+ display name. Each newly created API conversation snapshots it before its first
44
+ answer, even when no browser is open. Authenticated history imports that folder
45
+ and assignment once; manual moves and folder deletions are preserved. Changing
46
+ or clearing a token's default affects future conversations, not continuations
47
+ of an existing `session_id`. The default does not change workspace paths or
48
+ grant access to another account's chats. The folder picker lists folders from
49
+ this browser's history and retains an existing server selection from another
50
+ browser. New API chats can recreate a selected folder that was deleted locally;
51
+ clear or change the token default to stop using that destination.
52
+
26
53
  Corporate sign-in through Keycloak or another OpenID Connect provider is opt-in.
27
54
  The source tree includes `plugins/dsh-qa-surface/docs/oidc.md` with client settings,
28
55
  account linking, role admission, session limits and deployment acceptance.
29
56
 
57
+ The composer has one compact **Chat settings** menu beside the attachment
58
+ button. Its summary shows the current model and code version. The **Model**
59
+ and **Code version** entries open nested choice lists with a checkmark for the
60
+ current value; Back, Escape and arrow keys navigate the levels. The menu closes
61
+ after a successful choice and stays open with an error if saving fails. Model
62
+ choices appear when the role or account permits alternatives; code choices
63
+ appear when a code-source catalog is configured.
64
+
65
+ Choosing a model applies to the current chat and
66
+ survives reopening it; new chats keep the configured default. Switching waits
67
+ until the chat has finished its response and has no queued request. Readers,
68
+ participants and delegated-agent views cannot change the owner's model.
69
+
70
+ Administrators set the default and **Alternative models for the user** in the
71
+ role's Model tab or the account's model settings. `model` keeps its existing
72
+ priority: account, role, deployment. The optional `allowedModels` array adds
73
+ provider/model pairs to that default. An omitted account array inherits its
74
+ role's alternatives; `[]` explicitly restricts that account to its default.
75
+ An omitted role array grants no alternatives. Saved policies keep their
76
+ previous single-model behavior until an administrator grants alternatives.
77
+ The server filters the catalog and validates every selection against the
78
+ owner's current policy, including model replacement rules.
79
+
80
+ Chat-local selection uses the Session controller's guarded selection commit
81
+ capability, which records the native `model/selection` event and updates the
82
+ next-request selection without saving the Host operator's global default.
83
+ A Host without that capability rejects the switch rather than changing a
84
+ deployment-wide preference. The rest of the QA surface remains available.
85
+
86
+ The focused packed browser check is `pnpm --filter @yadsh/dsh-qa-surface
87
+ e2e:browser --model-selection`. Set `DSH_QA_SMOKE_VERSION` to the Host version
88
+ being checked, and optionally `DSH_QA_HISTORY_EVIDENCE_DIR` to a local evidence
89
+ directory. This pass exercises synthetic model routes without real provider
90
+ credentials, including access filtering, switching, reload persistence,
91
+ new-chat defaults, and desktop/phone layout. It does not run the separate LAN
92
+ deployment-overlay acceptance pass.
93
+
30
94
  - contributes one root-scoped entry to the additive `shell.overlay` slot;
31
95
  - stays invisible outside the configured route;
32
96
  - creates or restores one real DSH Session;
@@ -423,6 +487,83 @@ dynamically attached tool gets the same scrutiny as an allow-listed one. Tool
423
487
  visibility is not an authorization boundary — a QA tool that writes must still
424
488
  enforce its own permissions.
425
489
 
490
+ ### Multiple code sources
491
+
492
+ `codeSources` defines named code trees independently of folder and branch
493
+ names. Configure it on the QA Surface row; preset-mounted Git tools read
494
+ this same catalogue unless their own non-empty catalogue overrides it.
495
+
496
+ ```yaml
497
+ codeSources:
498
+ defaultSource: stable
499
+ entries:
500
+ - id: stable
501
+ label: "Stable release"
502
+ path: /srv/code/release-a
503
+ docsVersion: "2.0"
504
+ - id: preview
505
+ label: "Development"
506
+ path: /srv/code/work-in-progress
507
+ docsVersion: "2.0"
508
+ lockdown:
509
+ sharedReadOnlyRoots:
510
+ - /srv/code
511
+ ```
512
+
513
+ Grant Git access separately with `repositoryRoots: [/srv/code]` on the
514
+ Git plugin row mounted in the agent preset. Adding a catalogue entry does
515
+ not grant either filesystem or Git access. The chat's writable scratch
516
+ directory stays separate from the read-only code trees.
517
+
518
+ After QA tool activation, `code_sources {}` reports the catalogue, its default
519
+ and each tree's read availability. `code_read` and `code_search` accept
520
+ `source: stable` or `source: preview` and source-relative `path` values.
521
+ Omitting `source` uses `defaultSource`, or the only entry when there is one;
522
+ several entries without a default require an explicit choice. A missing,
523
+ unreadable or unknown source fails without falling back to another tree.
524
+ Use the same id in `dsh_git_*` calls. Generic `read`, `glob` and `grep` still
525
+ use explicit paths; use the source-aware tools for version-scoped work.
526
+
527
+ Search is literal and case-insensitive, bounded to 1,000 attempted text
528
+ files, 10,000 visited entries, 8 MiB of input, three seconds and 32 KiB of
529
+ results. It skips links, binary/oversized files and common generated
530
+ directories (`.git`, `node_modules`, `.nx`, `.cache`, `bin`, `obj`, `dist`,
531
+ `lib`). `truncated` means incomplete: narrow `path` or query before
532
+ concluding code is absent. Reading accepts UTF-8 files up to 1 MiB and
533
+ returns at most 400 lines and 32 KiB. Both tools refuse traversal and nested
534
+ symbolic links. Results carry the source id, label and root; file provenance
535
+ preserves the source id and actual file address.
536
+
537
+ `docs_search` accepts `source` and uses its optional `docsVersion` mapping.
538
+ When neither `source`, `version` nor `path` is given, the default source's
539
+ mapping takes precedence over `tools.docsDefaultVersion`. Without a mapping,
540
+ the existing documentation default stays in force. An explicit `version`
541
+ or `path` without `source` retains its override behaviour. An explicit
542
+ `source` without a mapping is refused, as is a conflicting explicit edition.
543
+ `docs_read` validates the file's edition against the chat's selected source
544
+ unless a different source is explicitly named. Release and development sources
545
+ may share a documentation edition.
546
+
547
+ The **Версия кода** entry in the composer's shared settings menu saves an explicit source for
548
+ the chat on the server. Reopening the chat or using another browser restores
549
+ that choice. **По умолчанию** clears the override and follows the deployment
550
+ default. Draft selection creates no session: it is saved before the first
551
+ prompt, and a failed save prevents the prompt and can be retried.
552
+
553
+ Only the chat owner can change the source. Shared readers and collaborators
554
+ see the same choice, and changes are refused while the chat or its subagents
555
+ are running, while input is queued, or while the chat waits for a server slot.
556
+ The plugin stores preferences in `qa-code-sources.db` beside the accounts DB;
557
+ include that database in deployment backups. New delegated children and forks
558
+ snapshot the parent's effective source. Descendants keep that snapshot if the
559
+ parent later changes its choice; an ordinary fork can change its own source.
560
+
561
+ An omitted `source` in Git, code or documentation tools uses the persisted
562
+ session choice. Explicit tool parameters still allow comparisons with another
563
+ configured source and do not change the saved choice. Removing a selected
564
+ source from configuration makes it unavailable rather than selecting another
565
+ version. No directory is created, moved or updated by these tools.
566
+
426
567
  ### Writable per-user research space
427
568
 
428
569
  Set `accounts.perUserWorkspace: true` only together with accounts, a registered
@@ -519,6 +660,34 @@ then collapses to `Worked for ...` before the final answer. Tool capability is
519
660
  still controlled exclusively by `lockdown.toolPolicy.allow`; the display flags
520
661
  do not grant tools.
521
662
 
663
+ When a provider retires a model identifier, `session.modelReplacements` defines
664
+ exact central redirects after account → role → deployment precedence:
665
+
666
+ ```yaml
667
+ session:
668
+ provider: local
669
+ model: model-v2
670
+ modelReplacements:
671
+ - from:
672
+ provider: local
673
+ model: model-v1
674
+ to:
675
+ provider: local
676
+ model: model-v2
677
+ ```
678
+
679
+ The rule covers saved account and role pins as well as the deployment default;
680
+ unrelated pairs keep their own selection. The terminal target must exist in the
681
+ Host catalog. Cycles and duplicate sources are configuration errors. Chained
682
+ redirects let a later transition cover older pins without editing every account;
683
+ retain a rule while stored policies still refer to its source. The stored pins
684
+ remain intact. New chats use the replacement immediately. On access attestation,
685
+ an idle owned QA chat can move along an exact redirect to its current policy;
686
+ an active turn is left unchanged. Ownership, workspace and adoption checks still
687
+ precede reconciliation. A missing target fails explicitly instead of selecting
688
+ another model from catalog order. The journal records the original and applied
689
+ pairs, and delegated experts resolve the owning chat's policy through Host lineage.
690
+
522
691
  ### User profile
523
692
 
524
693
  Clicking the account name in the sidebar footer opens the signed-in user's
@@ -669,28 +838,37 @@ stays quiet until a message is actually sent, and the old session stays intact
669
838
  for operator inspection. The sidebar orders chats by the host's last update,
670
839
  so merely opening a chat never moves it.
671
840
 
672
- A stand whose model runs locally can also refuse to take questions it cannot
673
- answer at the same time: `session.maxActiveRequests` names that ceiling, and 0 —
674
- the default — names none. The count is the Host's, read from the agents that are
675
- answering right now, so a question that arrived through the HTTP API occupies a
676
- place too and a browser never has to guess about chats that are not its own.
677
- Before a send the browser asks that read; a question with no place left is never
678
- sent — no session is materialized, nothing enters the transcript, the composer
679
- keeps the text, and the visitor reads how many requests the stand is already
680
- working. That count includes this visitor's own turns, which is why the dialog
681
- names occupied places rather than people ahead: a refused question is not queued
682
- behind a number the stand cannot attribute.
683
- It is a ceiling rather than a lock: the prompt itself rides the native session
684
- RPC, so two questions pressed in the same instant can still overshoot by one.
685
- What the ceiling buys is the steady state, and an unreadable count sends the
686
- question anyway.
687
- It bounds questions, not every other send. A message typed while a chat is
688
- answering joins that chat's own queue and costs no second place, so it is admitted
689
- without the read; a human command from the palette goes to the Host's command
690
- runtime and is never held back, because that surface is how a visitor inspects or
691
- repairs a saturated stand and this plugin cannot tell which commands wake the
692
- model. A command that does wake it enters the same Host count, so the next
693
- question waits behind it like behind any other turn.
841
+ A deployment can bound concurrent chats with `session.maxActiveRequests`; the
842
+ default 0 leaves concurrency unbounded. Admission is enforced on the server
843
+ before a root agent takes its first step, covering browser, integration and
844
+ native prompts. Simultaneous sends reserve places atomically; waiting chats
845
+ start in FIFO order without resending their questions. A complete turn keeps
846
+ its place through tool calls and review. Delegated experts run within their
847
+ chat's place, including background descendants still running after the parent
848
+ turn ends. A subsequent turn rejoins the queue behind earlier waiters.
849
+
850
+ An inline status above the composer shows the waiting chat's real queue
851
+ position. After at least three completed turns the server can also provide an
852
+ approximate delay based on recent durations. Before that, no duration is shown.
853
+ The status survives closing and reopening the page while Host stays running;
854
+ the stop button cancels a waiting request. Queue status reveals global counts
855
+ only, and a chat's position requires current read access. An unavailable status
856
+ read does not bypass server admission.
857
+
858
+ Changing the limit does not interrupt already active turns: lowering it admits
859
+ no new chat until the active count fits, and raising it releases more waiters.
860
+ Commands that do not run a turn remain usable while the stand is full.
861
+
862
+ The number is the chain's last layer, and the two above it override it per turn:
863
+ a subrole may carry `maxActiveRequests` (edited on the «Саброли» page's model
864
+ tab), and an account may carry one (the user card's «Слоты очереди этого
865
+ аккаунта» select). The account beats the role, the role beats the deployment,
866
+ and 0 is a real value that lifts the ceiling for that role's or account's turns
867
+ outright. A turn whose principal raises the number fits while a lower ceiling
868
+ would refuse it, and one full principal does not starve the queue behind it.
869
+ The status read answers the caller's own chain: a chat's read uses the chat's
870
+ account, the pre-send read uses the visitor's, and a stand without accounts
871
+ keeps the one deployment number.
694
872
 
695
873
  Regeneration: the last committed answer offers a retry action. The session log
696
874
  is append-only, so "regenerate" sends a hidden instruction as an ordinary
@@ -1268,6 +1446,7 @@ integration:
1268
1446
  requestTimeoutMs: 3600000 # one-hour answer budget; see docs/INTEGRATION-API.md §2.5
1269
1447
  maxConcurrent: 4
1270
1448
  requestsPerMinute: 60
1449
+ maxAttachments: 0 # 0 inherits attachments.maxPending; positive values set an API limit (1-40)
1271
1450
  maxAnswerCharacters: 4096 # the answer the ticket comment can hold
1272
1451
  ```
1273
1452
 
@@ -0,0 +1,103 @@
1
+ import { SessionId } from "@deepseek-ai/dsh-session/types";
2
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
3
+ import { sessionModelSelection } from "../session-model.js";
4
+ import { modelPolicySatisfied, projectModelCatalog, } from "./model-policy.js";
5
+ /** Only explicit policy grants are exposed to an ordinary account. */
6
+ export async function userModelOptions(ctx, access, token, sessionId, subroleId, preview) {
7
+ const resolve = () => {
8
+ if (sessionId === null)
9
+ return access.draftModelPolicy(token, subroleId, preview);
10
+ return access.userSessionModelPolicy(token, sessionId);
11
+ };
12
+ resolve(); // Authenticate before reading the Host catalog.
13
+ const catalog = projectModelCatalog(await ctx.sessionController.modelCatalog());
14
+ const policy = resolve(); // Recheck the token and grants after the async read.
15
+ const granted = [
16
+ ...(policy.pair === undefined ? [] : [policy.pair]),
17
+ ...(policy.allowedModels ?? []),
18
+ ];
19
+ const models = catalog.filter((entry) => granted.some((pair) => pair.provider === entry.provider && pair.model === entry.model));
20
+ const agent = sessionId === null ? undefined : ctx.agents.get(SessionId(sessionId));
21
+ const facts = agent === undefined ? policy.pair : sessionModelSelection(ctx, agent);
22
+ const selected = facts?.provider === undefined || facts.model === undefined
23
+ ? undefined
24
+ : {
25
+ provider: facts.provider,
26
+ model: facts.model,
27
+ ...(facts.reasoningEffort === undefined
28
+ ? {}
29
+ : { reasoningEffort: facts.reasoningEffort }),
30
+ };
31
+ return {
32
+ models,
33
+ choices: granted,
34
+ ...(policy.pair === undefined ? {} : { defaultModel: policy.pair }),
35
+ ...(selected === undefined ? {} : { selected }),
36
+ };
37
+ }
38
+ /** Validate both policy membership and live availability before a user mutation. */
39
+ export async function assertUserModelSelection(ctx, policy, request) {
40
+ if (typeof request.provider !== "string" || typeof request.model !== "string")
41
+ throw new Error("Choose a complete provider and model pair.");
42
+ const pair = {
43
+ provider: request.provider,
44
+ model: request.model,
45
+ ...(request.reasoningEffort == null
46
+ ? {}
47
+ : { reasoningEffort: request.reasoningEffort }),
48
+ };
49
+ const catalog = projectModelCatalog(await ctx.sessionController.modelCatalog());
50
+ const entry = catalog.find((item) => item.provider === pair.provider && item.model === pair.model);
51
+ if (entry === undefined)
52
+ throw new Error("This model is currently unavailable.");
53
+ if (pair.reasoningEffort !== undefined &&
54
+ !entry.reasoningEfforts.includes(pair.reasoningEffort))
55
+ throw new Error("This reasoning effort is unavailable for the selected model.");
56
+ const effective = policy();
57
+ if ((effective.pair === undefined && effective.allowedModels === undefined) ||
58
+ !modelPolicySatisfied(effective, pair))
59
+ throw new Error("This model is not allowed by the chat policy.");
60
+ }
61
+ /** Native durable per-session selection, without the operator-wide default save. */
62
+ export async function selectUserModel(ctx, access, token, request) {
63
+ const id = String(request.sessionId);
64
+ const policy = () => access.userSessionModelPolicy(token, id);
65
+ policy();
66
+ const agent = ctx.agents.get(request.sessionId);
67
+ if (agent === undefined)
68
+ throw new Error("The chat is unavailable.");
69
+ // DSH's public selectModel also saves the operator-wide default. Its owned
70
+ // agent controller has the narrower commit primitive. Keep this compatibility
71
+ // bridge guarded and fail closed when the Host no longer exposes it; never
72
+ // fall back to changing a deployment-wide preference from a QA account.
73
+ const controller = ctx.sessionController.agents;
74
+ if (typeof controller?.serializeImageAdmission !== "function" ||
75
+ typeof controller.selectForNextRequest !== "function")
76
+ throw new Error("This Host does not support chat-local model selection.");
77
+ return controller.serializeImageAdmission(agent, async () => {
78
+ if (agent.status !== "idle")
79
+ throw new Error("Wait for the current response before changing the model.");
80
+ await assertUserModelSelection(ctx, policy, request);
81
+ const selected = await ctx.llm.resolveCallConfig({
82
+ provider: request.provider,
83
+ model: request.model,
84
+ ...(request.reasoningEffort === undefined
85
+ ? {}
86
+ : { reasoningEffort: ReasoningEffortId(request.reasoningEffort) }),
87
+ });
88
+ if (!modelPolicySatisfied(policy(), selected))
89
+ throw new Error("This model is not allowed by the chat policy.");
90
+ if (agent.status !== "idle")
91
+ throw new Error("Wait for the current response before changing the model.");
92
+ const pair = {
93
+ provider: selected.provider,
94
+ model: selected.model,
95
+ ...(selected.reasoningEffort === undefined
96
+ ? {}
97
+ : { reasoningEffort: selected.reasoningEffort }),
98
+ };
99
+ controller.selectForNextRequest(agent, pair);
100
+ return { selected: pair };
101
+ });
102
+ }
103
+ //# sourceMappingURL=model-choices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-choices.js","sourceRoot":"","sources":["../../src/access/model-choices.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,28 @@
1
+ /** Preserve an explicit empty override and reject malformed or excessive grants. */
2
+ export function normalizeAllowedModels(value, label) {
3
+ if (value === undefined)
4
+ return undefined;
5
+ if (!Array.isArray(value) || value.length > 100)
6
+ throw new TypeError(`${label} must be an array of at most 100 model pairs`);
7
+ const pairs = value.map((entry, index) => {
8
+ const pair = normalizeModelPair(entry, `${label}[${index}]`);
9
+ if (pair === undefined)
10
+ throw new TypeError(`${label} needs complete pairs`);
11
+ return pair;
12
+ });
13
+ return Object.freeze([
14
+ ...new Map(pairs.map((pair) => [JSON.stringify(pair), pair])).values(),
15
+ ]);
16
+ }
17
+ /** Add explicit grants without changing which model a new chat starts on. */
18
+ export function withAllowedModels(policy, models, replacements) {
19
+ if (models === undefined)
20
+ return policy;
21
+ return Object.freeze({
22
+ ...policy,
23
+ allowedModels: Object.freeze(models.map((pair) => replaceModelPair(pair, replacements))),
24
+ });
25
+ }
1
26
  function text(value) {
2
27
  return typeof value === "string" ? value.trim() : "";
3
28
  }
@@ -47,18 +72,62 @@ export function deploymentModelPair(session) {
47
72
  * @param layers - the layers in priority order, `undefined` where a layer keeps
48
73
  * no opinion.
49
74
  */
50
- export function resolveModelPolicy(layers) {
75
+ export function resolveModelPolicy(layers, replacements = []) {
51
76
  const order = [
52
77
  "account",
53
78
  "subrole",
54
79
  "deployment",
55
80
  ];
56
81
  const layer = order.find((candidate) => layers[candidate] !== undefined);
82
+ const original = layer === undefined ? undefined : layers[layer];
83
+ const pair = replaceModelPair(original, replacements);
57
84
  return Object.freeze({
58
- pair: layer === undefined ? undefined : layers[layer],
85
+ pair,
59
86
  layer,
87
+ ...(original !== undefined && pair !== original
88
+ ? { redirectedFrom: original }
89
+ : {}),
60
90
  });
61
91
  }
92
+ /** Resolve only explicitly configured exact redirects; never guess from catalog order. */
93
+ export function replaceModelPair(pair, replacements) {
94
+ if (pair === undefined)
95
+ return undefined;
96
+ let current = pair;
97
+ const visited = new Set();
98
+ for (;;) {
99
+ const key = JSON.stringify([current.provider, current.model]);
100
+ if (visited.has(key))
101
+ throw new TypeError("session.modelReplacements contains a cycle");
102
+ visited.add(key);
103
+ const next = replacements.find(({ from }) => from.provider === current.provider && from.model === current.model);
104
+ if (next === undefined)
105
+ return current;
106
+ current = next.to;
107
+ }
108
+ }
109
+ export function normalizeModelReplacements(input) {
110
+ if (input === undefined)
111
+ return Object.freeze([]);
112
+ if (!Array.isArray(input) || input.length > 100)
113
+ throw new TypeError("session.modelReplacements must be an array of at most 100 exact replacements");
114
+ const keys = new Set();
115
+ const replacements = input.map((entry, index) => {
116
+ const label = `session.modelReplacements[${index}]`;
117
+ const from = normalizeModelPair(entry?.from, `${label}.from`);
118
+ const to = normalizeModelPair(entry?.to, `${label}.to`);
119
+ if (from === undefined || to === undefined)
120
+ throw new TypeError(`${label} needs complete from/to pairs`);
121
+ const key = JSON.stringify([from.provider, from.model]);
122
+ if (keys.has(key))
123
+ throw new TypeError(`${label} duplicates a source pair`);
124
+ keys.add(key);
125
+ return Object.freeze({ from, to });
126
+ });
127
+ for (const { from } of replacements)
128
+ replaceModelPair(from, replacements);
129
+ return Object.freeze(replacements);
130
+ }
62
131
  /**
63
132
  * Whether one agent's selection sits inside the session's model policy.
64
133
  *
@@ -70,12 +139,15 @@ export function resolveModelPolicy(layers) {
70
139
  */
71
140
  export function modelPolicySatisfied(policy, selection) {
72
141
  const pair = policy.pair;
73
- if (pair === undefined)
142
+ if (pair === undefined && policy.allowedModels === undefined)
74
143
  return true;
75
- return (pair.provider === selection.provider &&
76
- pair.model === selection.model &&
77
- (pair.reasoningEffort === undefined ||
78
- pair.reasoningEffort === selection.reasoningEffort));
144
+ return [
145
+ ...(pair === undefined ? [] : [pair]),
146
+ ...(policy.allowedModels ?? []),
147
+ ].some((candidate) => candidate.provider === selection.provider &&
148
+ candidate.model === selection.model &&
149
+ (candidate.reasoningEffort === undefined ||
150
+ candidate.reasoningEffort === selection.reasoningEffort));
79
151
  }
80
152
  /**
81
153
  * Project the harness catalog into the pairs the administration surface lists.
@@ -1 +1 @@
1
- {"version":3,"file":"model-policy.js","sourceRoot":"","sources":["../../src/access/model-policy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model-policy.js","sourceRoot":"","sources":["../../src/access/model-policy.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,27 @@
1
- import { normalizeModelPair } from "./model-policy.js";
1
+ import { normalizeModelPair, normalizeAllowedModels } from "./model-policy.js";
2
+ import { QA_MAX_ACTIVE_REQUESTS_MAX } from "../config-resolvers/session.js";
2
3
  import { normalizeSkillOverride, resolveSkillVisibility, skillHealth, } from "./skill-metadata.js";
3
4
  const CAPABILITY_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,199}$/u;
4
5
  const SUBROLE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/u;
6
+ /**
7
+ * Normalize one queue-slots override.
8
+ *
9
+ * An absent field keeps no opinion and the layer below it speaks. 0 is a real
10
+ * value — it lifts the ceiling for this role or account — while anything that
11
+ * is not an integer from 0 to {@link QA_MAX_ACTIVE_REQUESTS_MAX} is refused,
12
+ * the same range the deployment's own field accepts.
13
+ */
14
+ export function normalizeQueueSlots(value, label) {
15
+ if (value === undefined || value === null)
16
+ return undefined;
17
+ if (typeof value !== "number" ||
18
+ !Number.isSafeInteger(value) ||
19
+ value < 0 ||
20
+ value > QA_MAX_ACTIVE_REQUESTS_MAX) {
21
+ throw new TypeError(`${label} must be an integer from 0 to ${String(QA_MAX_ACTIVE_REQUESTS_MAX)}`);
22
+ }
23
+ return value;
24
+ }
5
25
  function uniqueIds(values, label) {
6
26
  const result = [
7
27
  ...new Set(values.map((value) => {
@@ -78,6 +98,8 @@ export function normalizeSubrole(value) {
78
98
  const icon = value.ui?.icon?.trim();
79
99
  const accent = value.ui?.accent?.trim();
80
100
  const model = normalizeModelPair(value.model, `subrole ${id}.model`);
101
+ const allowedModels = normalizeAllowedModels(value.allowedModels, `subrole ${id}.allowedModels`);
102
+ const maxActiveRequests = normalizeQueueSlots(value.maxActiveRequests, `subrole ${id}.maxActiveRequests`);
81
103
  return Object.freeze({
82
104
  id,
83
105
  name,
@@ -85,6 +107,8 @@ export function normalizeSubrole(value) {
85
107
  enabled: value.enabled === true,
86
108
  capabilities: normalizeCapabilitySelection(value.capabilities),
87
109
  ...(model === undefined ? {} : { model }),
110
+ ...(allowedModels === undefined ? {} : { allowedModels }),
111
+ ...(maxActiveRequests === undefined ? {} : { maxActiveRequests }),
88
112
  ...(icon === undefined && accent === undefined
89
113
  ? {}
90
114
  : {
@@ -196,10 +220,14 @@ export function normalizeUserAccess(value, config) {
196
220
  ? requestedDefault
197
221
  : allowed[0];
198
222
  const model = normalizeModelPair(value?.model, "account model");
223
+ const allowedModels = normalizeAllowedModels(value?.allowedModels, "account allowedModels");
224
+ const maxActiveRequests = normalizeQueueSlots(value?.maxActiveRequests, "account maxActiveRequests");
199
225
  return Object.freeze({
200
226
  allowedSubroles: Object.freeze(allowed),
201
227
  defaultSubrole,
202
228
  ...(model === undefined ? {} : { model }),
229
+ ...(allowedModels === undefined ? {} : { allowedModels }),
230
+ ...(maxActiveRequests === undefined ? {} : { maxActiveRequests }),
203
231
  });
204
232
  }
205
233
  function available(values, installed) {