@yadsh/dsh-qa-surface 0.7.4 → 0.9.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 (247) hide show
  1. package/README.md +169 -12
  2. package/compatibility.json +5 -0
  3. package/lib/access/capability-catalog.js +32 -3
  4. package/lib/access/capability-catalog.js.map +1 -1
  5. package/lib/access/model.js +24 -4
  6. package/lib/access/model.js.map +1 -1
  7. package/lib/access/role-repository.js +199 -91
  8. package/lib/access/role-repository.js.map +1 -1
  9. package/lib/access/service.js +223 -7
  10. package/lib/access/service.js.map +1 -1
  11. package/lib/account-remotes.js +2 -4
  12. package/lib/account-remotes.js.map +1 -1
  13. package/lib/accounts/database.js +447 -0
  14. package/lib/accounts/database.js.map +1 -0
  15. package/lib/accounts/errors.js.map +1 -1
  16. package/lib/accounts/file.js +27 -90
  17. package/lib/accounts/file.js.map +1 -1
  18. package/lib/accounts/store.js +224 -59
  19. package/lib/accounts/store.js.map +1 -1
  20. package/lib/admin/permissions.js +4 -0
  21. package/lib/admin/permissions.js.map +1 -1
  22. package/lib/admin/quality-store.js +270 -54
  23. package/lib/admin/quality-store.js.map +1 -1
  24. package/lib/admin/service.js +145 -7
  25. package/lib/admin/service.js.map +1 -1
  26. package/lib/admin/session-log.js +66 -5
  27. package/lib/admin/session-log.js.map +1 -1
  28. package/lib/cli.js +2 -1
  29. package/lib/cli.js.map +1 -1
  30. package/lib/client/QaSessionController.js +379 -39
  31. package/lib/client/QaSessionController.js.map +1 -1
  32. package/lib/client/QaSurface.js +94 -34
  33. package/lib/client/QaSurface.js.map +1 -1
  34. package/lib/client/QaTranscriptAdapter.js +40 -0
  35. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  36. package/lib/client/admin/QaAdmin.js +26 -8
  37. package/lib/client/admin/QaAdmin.js.map +1 -1
  38. package/lib/client/admin/{format.js → copy.js} +1 -1
  39. package/lib/client/admin/copy.js.map +1 -0
  40. package/lib/client/admin/pages/Conversations.js +34 -8
  41. package/lib/client/admin/pages/Conversations.js.map +1 -1
  42. package/lib/client/admin/pages/Overview.js +1 -1
  43. package/lib/client/admin/pages/Overview.js.map +1 -1
  44. package/lib/client/admin/pages/Quality.js +1 -1
  45. package/lib/client/admin/pages/Quality.js.map +1 -1
  46. package/lib/client/admin/pages/Review.js +1 -1
  47. package/lib/client/admin/pages/Review.js.map +1 -1
  48. package/lib/client/admin/pages/Users.js +6 -2
  49. package/lib/client/admin/pages/Users.js.map +1 -1
  50. package/lib/client/admin/shared.js +10 -1
  51. package/lib/client/admin/shared.js.map +1 -1
  52. package/lib/client/admin/styles.js +2 -0
  53. package/lib/client/admin/styles.js.map +1 -1
  54. package/lib/client/audit/QaAuditDialog.js +74 -0
  55. package/lib/client/audit/QaAuditDialog.js.map +1 -0
  56. package/lib/client/audit/QaAuditRowBadge.js +17 -0
  57. package/lib/client/audit/QaAuditRowBadge.js.map +1 -0
  58. package/lib/client/audit/controller.js +39 -0
  59. package/lib/client/audit/controller.js.map +1 -0
  60. package/lib/client/audit/types.js +36 -0
  61. package/lib/client/audit/types.js.map +1 -0
  62. package/lib/client/audit/use-chat-audits.js +139 -0
  63. package/lib/client/audit/use-chat-audits.js.map +1 -0
  64. package/lib/client/components/Markdown.js +14 -206
  65. package/lib/client/components/Markdown.js.map +1 -1
  66. package/lib/client/components/QaChangelog.js +91 -0
  67. package/lib/client/components/QaChangelog.js.map +1 -1
  68. package/lib/client/components/QaComposer.js +63 -3
  69. package/lib/client/components/QaComposer.js.map +1 -1
  70. package/lib/client/components/QaHeader.js +1 -3
  71. package/lib/client/components/QaHeader.js.map +1 -1
  72. package/lib/client/components/QaMessage.js +25 -4
  73. package/lib/client/components/QaMessage.js.map +1 -1
  74. package/lib/client/components/QaQuestions.js +1 -1
  75. package/lib/client/components/QaQuestions.js.map +1 -1
  76. package/lib/client/components/QaSidebar.js +17 -3
  77. package/lib/client/components/QaSidebar.js.map +1 -1
  78. package/lib/client/components/QaSourcesPanel.js +3 -18
  79. package/lib/client/components/QaSourcesPanel.js.map +1 -1
  80. package/lib/client/components/QaWorkGroup.js +4 -8
  81. package/lib/client/components/QaWorkGroup.js.map +1 -1
  82. package/lib/client/components/source-icon.js +26 -0
  83. package/lib/client/components/source-icon.js.map +1 -0
  84. package/lib/client/components/thinking-phrases.js +2 -11
  85. package/lib/client/components/thinking-phrases.js.map +1 -1
  86. package/lib/client/components/use-now.js +21 -0
  87. package/lib/client/components/use-now.js.map +1 -0
  88. package/lib/client/index.js +33 -5
  89. package/lib/client/index.js.map +1 -1
  90. package/lib/client/lineage.js +18 -0
  91. package/lib/client/lineage.js.map +1 -0
  92. package/lib/client/markdown/CodeBlock.js +28 -0
  93. package/lib/client/markdown/CodeBlock.js.map +1 -0
  94. package/lib/client/markdown/blocks.js +342 -0
  95. package/lib/client/markdown/blocks.js.map +1 -0
  96. package/lib/client/markdown/highlight.js +1089 -0
  97. package/lib/client/markdown/highlight.js.map +1 -0
  98. package/lib/client/markdown/inline.js +377 -0
  99. package/lib/client/markdown/inline.js.map +1 -0
  100. package/lib/client/markdown/render.js +147 -0
  101. package/lib/client/markdown/render.js.map +1 -0
  102. package/lib/client/markdown/source-chip.js +55 -0
  103. package/lib/client/markdown/source-chip.js.map +1 -0
  104. package/lib/client/markdown/use-transient-flag.js +27 -0
  105. package/lib/client/markdown/use-transient-flag.js.map +1 -0
  106. package/lib/client/project-session-state.js +15 -2
  107. package/lib/client/project-session-state.js.map +1 -1
  108. package/lib/client/prompt-content.js +26 -0
  109. package/lib/client/prompt-content.js.map +1 -1
  110. package/lib/client/role/RoleSelector.js +11 -2
  111. package/lib/client/role/RoleSelector.js.map +1 -1
  112. package/lib/client/role/preview.js +123 -0
  113. package/lib/client/role/preview.js.map +1 -0
  114. package/lib/client/settings/card.js +2 -2
  115. package/lib/client/settings/card.js.map +1 -1
  116. package/lib/client/settings/sections/lockdown.js +1 -6
  117. package/lib/client/settings/sections/lockdown.js.map +1 -1
  118. package/lib/client/settings/sections/slash.js +57 -0
  119. package/lib/client/settings/sections/slash.js.map +1 -0
  120. package/lib/client/settings/sections/status.js +9 -1
  121. package/lib/client/settings/sections/status.js.map +1 -1
  122. package/lib/client/settings/sections.js +1 -0
  123. package/lib/client/settings/sections.js.map +1 -1
  124. package/lib/client/slash/SlashPalette.js +48 -0
  125. package/lib/client/slash/SlashPalette.js.map +1 -0
  126. package/lib/client/slash/copy.js +54 -0
  127. package/lib/client/slash/copy.js.map +1 -0
  128. package/lib/client/slash/slash-router.js +49 -0
  129. package/lib/client/slash/slash-router.js.map +1 -0
  130. package/lib/client/slash/useSlashNavigation.js +95 -0
  131. package/lib/client/slash/useSlashNavigation.js.map +1 -0
  132. package/lib/client/styles.js +124 -20
  133. package/lib/client/styles.js.map +1 -1
  134. package/lib/client/types.js +11 -0
  135. package/lib/client/types.js.map +1 -1
  136. package/lib/client.js +7615 -1567
  137. package/lib/client.js.map +1 -1
  138. package/lib/config-resolvers/accounts.js +15 -0
  139. package/lib/config-resolvers/accounts.js.map +1 -1
  140. package/lib/config-resolvers/basics.js +11 -2
  141. package/lib/config-resolvers/basics.js.map +1 -1
  142. package/lib/config-resolvers/defaults.js +30 -0
  143. package/lib/config-resolvers/defaults.js.map +1 -1
  144. package/lib/config-resolvers/lockdown.js +8 -2
  145. package/lib/config-resolvers/lockdown.js.map +1 -1
  146. package/lib/config-resolvers/slash-commands.js +60 -0
  147. package/lib/config-resolvers/slash-commands.js.map +1 -0
  148. package/lib/config-resolvers/sources.js +16 -0
  149. package/lib/config-resolvers/sources.js.map +1 -1
  150. package/lib/config.js +118 -2
  151. package/lib/config.js.map +1 -1
  152. package/lib/index.js +183 -11
  153. package/lib/index.js.map +1 -1
  154. package/lib/lockdown-policy.js +19 -0
  155. package/lib/lockdown-policy.js.map +1 -1
  156. package/lib/personal-skills/service.js +16 -3
  157. package/lib/personal-skills/service.js.map +1 -1
  158. package/lib/provenance/host-store.js +25 -4
  159. package/lib/provenance/host-store.js.map +1 -1
  160. package/lib/provenance/retention.js +25 -0
  161. package/lib/provenance/retention.js.map +1 -0
  162. package/lib/provenance/snapshot-store.js +290 -35
  163. package/lib/provenance/snapshot-store.js.map +1 -1
  164. package/lib/questions.js +98 -38
  165. package/lib/questions.js.map +1 -1
  166. package/lib/resolve-config.js +5 -0
  167. package/lib/resolve-config.js.map +1 -1
  168. package/lib/secure-session.js +139 -21
  169. package/lib/secure-session.js.map +1 -1
  170. package/lib/slash/catalog.js +77 -0
  171. package/lib/slash/catalog.js.map +1 -0
  172. package/lib/slash/matcher.js +100 -0
  173. package/lib/slash/matcher.js.map +1 -0
  174. package/lib/slash/parser.js +79 -0
  175. package/lib/slash/parser.js.map +1 -0
  176. package/lib/slash/policy.js +72 -0
  177. package/lib/slash/policy.js.map +1 -0
  178. package/lib/slash/remotes.js +272 -0
  179. package/lib/slash/remotes.js.map +1 -0
  180. package/lib/typert.host.js +348 -56
  181. package/lib/typert.remote-client.d.ts +7 -1
  182. package/lib/typert.remote-client.js +274 -51
  183. package/lib/types/access/capability-catalog.d.ts +28 -1
  184. package/lib/types/access/role-repository.d.ts +38 -7
  185. package/lib/types/access/service.d.ts +106 -3
  186. package/lib/types/account-remotes.d.ts +8 -4
  187. package/lib/types/accounts/database.d.ts +84 -0
  188. package/lib/types/accounts/errors.d.ts +7 -1
  189. package/lib/types/accounts/file.d.ts +17 -39
  190. package/lib/types/accounts/store.d.ts +110 -19
  191. package/lib/types/admin/quality-store.d.ts +38 -2
  192. package/lib/types/admin/service.d.ts +34 -2
  193. package/lib/types/admin/session-log.d.ts +65 -1
  194. package/lib/types/client/QaSessionController.d.ts +84 -2
  195. package/lib/types/client/QaSurface.d.ts +14 -1
  196. package/lib/types/client/admin/{format.d.ts → copy.d.ts} +1 -1
  197. package/lib/types/client/admin/pages/Conversations.d.ts +4 -0
  198. package/lib/types/client/audit/QaAuditDialog.d.ts +21 -0
  199. package/lib/types/client/audit/QaAuditRowBadge.d.ts +18 -0
  200. package/lib/types/client/audit/controller.d.ts +27 -0
  201. package/lib/types/client/audit/types.d.ts +72 -0
  202. package/lib/types/client/audit/use-chat-audits.d.ts +20 -0
  203. package/lib/types/client/components/Markdown.d.ts +7 -8
  204. package/lib/types/client/components/QaComposer.d.ts +12 -2
  205. package/lib/types/client/components/QaQuestions.d.ts +7 -0
  206. package/lib/types/client/components/QaSidebar.d.ts +11 -1
  207. package/lib/types/client/components/QaSourcesPanel.d.ts +1 -3
  208. package/lib/types/client/components/source-icon.d.ts +12 -0
  209. package/lib/types/client/components/use-now.d.ts +8 -0
  210. package/lib/types/client/lineage.d.ts +15 -0
  211. package/lib/types/client/markdown/CodeBlock.d.ts +14 -0
  212. package/lib/types/client/markdown/blocks.d.ts +74 -0
  213. package/lib/types/client/markdown/highlight.d.ts +29 -0
  214. package/lib/types/client/markdown/inline.d.ts +40 -0
  215. package/lib/types/client/markdown/render.d.ts +13 -0
  216. package/lib/types/client/markdown/source-chip.d.ts +32 -0
  217. package/lib/types/client/markdown/use-transient-flag.d.ts +12 -0
  218. package/lib/types/client/project-session-state.d.ts +3 -1
  219. package/lib/types/client/prompt-content.d.ts +9 -1
  220. package/lib/types/client/role/RoleSelector.d.ts +11 -1
  221. package/lib/types/client/role/preview.d.ts +109 -0
  222. package/lib/types/client/settings/sections/slash.d.ts +16 -0
  223. package/lib/types/client/settings/sections.d.ts +1 -0
  224. package/lib/types/client/slash/SlashPalette.d.ts +24 -0
  225. package/lib/types/client/slash/copy.d.ts +30 -0
  226. package/lib/types/client/slash/slash-router.d.ts +67 -0
  227. package/lib/types/client/slash/useSlashNavigation.d.ts +47 -0
  228. package/lib/types/client/styles.d.ts +9 -0
  229. package/lib/types/client/types.d.ts +16 -1
  230. package/lib/types/config-resolvers/slash-commands.d.ts +20 -0
  231. package/lib/types/index.d.ts +83 -2
  232. package/lib/types/lockdown-policy.d.ts +16 -0
  233. package/lib/types/personal-skills/service.d.ts +5 -0
  234. package/lib/types/provenance/host-store.d.ts +20 -3
  235. package/lib/types/provenance/retention.d.ts +46 -0
  236. package/lib/types/provenance/snapshot-store.d.ts +63 -7
  237. package/lib/types/questions.d.ts +13 -0
  238. package/lib/types/secure-session.d.ts +27 -1
  239. package/lib/types/slash/catalog.d.ts +56 -0
  240. package/lib/types/slash/matcher.d.ts +51 -0
  241. package/lib/types/slash/parser.d.ts +46 -0
  242. package/lib/types/slash/policy.d.ts +40 -0
  243. package/lib/types/slash/remotes.d.ts +35 -0
  244. package/lib/types/types.d.ts +254 -5
  245. package/package.json +9 -5
  246. package/scripts/attach-workspace-sessions.mjs +472 -0
  247. package/lib/client/admin/format.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import type { Agent } from "@deepseek-ai/dsh-agent";
2
2
  import type { Context } from "@deepseek-ai/cordis";
3
+ import type { ScopeKey } from "@deepseek-ai/dsh-scope";
3
4
  import type { SkillSummary } from "@deepseek-ai/dsh-skill";
4
5
  import type { QaCapabilityDescriptor, QaSkillDescriptor } from "../types.js";
5
6
  export interface CapabilityCatalogSnapshot {
@@ -19,10 +20,36 @@ export declare class QaCapabilityCatalog {
19
20
  private readonly additionalToolNames;
20
21
  /** Role ids used to report unknown subrole ids in skill metadata. */
21
22
  private readonly knownSubroles;
23
+ /**
24
+ * The viewing scope an administrator's catalog is read with, for callers
25
+ * that have no agent of their own. Skills and tools an agent preset mounts
26
+ * (a kit's skill catalog, the preset's tool family) live in that preset's
27
+ * scope, so a global-only read reported an almost empty catalog: the
28
+ * administrator could neither see nor grant what every chat mounts.
29
+ * Resolving the scope may compose the preset; a failure degrades to the
30
+ * global view instead of failing the page.
31
+ */
32
+ private readonly presetScope;
22
33
  constructor(ctx: Context, additionalToolNames?: () => readonly string[],
23
34
  /** Role ids used to report unknown subrole ids in skill metadata. */
24
- knownSubroles?: () => readonly string[]);
35
+ knownSubroles?: () => readonly string[],
36
+ /**
37
+ * The viewing scope an administrator's catalog is read with, for callers
38
+ * that have no agent of their own. Skills and tools an agent preset mounts
39
+ * (a kit's skill catalog, the preset's tool family) live in that preset's
40
+ * scope, so a global-only read reported an almost empty catalog: the
41
+ * administrator could neither see nor grant what every chat mounts.
42
+ * Resolving the scope may compose the preset; a failure degrades to the
43
+ * global view instead of failing the page.
44
+ */
45
+ presetScope?: () => Promise<ScopeKey | undefined>);
25
46
  snapshot(agent?: Agent): Promise<CapabilityCatalogSnapshot>;
47
+ /**
48
+ * The scope an administrator's catalog is read with. A deployment that pins
49
+ * QA chats to an agent preset gets that preset's standing scope; without a
50
+ * pinned preset there is no scope to borrow, and the read stays global.
51
+ */
52
+ private resolvePresetScope;
26
53
  /**
27
54
  * Read `metadata.qa-surface` of every discovered skill.
28
55
  *
@@ -1,11 +1,27 @@
1
1
  import type { QaAccessAuditEvent, QaCapabilityConfig, QaCapabilitySelection, QaSkillAssignmentOverride, QaSubrole } from "../types.js";
2
2
  export declare function defaultCapabilityFilePath(): string;
3
- /** Atomic, externally reloadable source of truth for roles and their audit. */
3
+ /** The pre-SQLite policy file, imported once on first use. */
4
+ export declare function defaultLegacyCapabilityFilePath(): string;
5
+ /**
6
+ * Atomic, externally reloadable source of truth for roles and their audit.
7
+ *
8
+ * The policy is one row and a change appends one row beside it, so an edit
9
+ * writes what it changed instead of rewriting a growing document — the cost of
10
+ * an administrative change no longer depends on how long the deployment has
11
+ * been administering anything.
12
+ *
13
+ * Only the configuration is cached. An audit trail carries the full policy
14
+ * that preceded each change, so reading it is reserved for the callers that
15
+ * ask for it, and dropped whenever another process writes.
16
+ */
4
17
  export declare class QaRoleRepository {
5
18
  readonly filePath: string;
6
- private file;
7
- private fileStamp;
19
+ private readonly storage;
20
+ private config;
21
+ private auditCache;
22
+ private observedDataVersion;
8
23
  constructor(filePath?: string);
24
+ close(): void;
9
25
  snapshot(): QaCapabilityConfig;
10
26
  audit(): readonly QaAccessAuditEvent[];
11
27
  create(actorId: string, input: QaSubrole): QaSubrole;
@@ -16,7 +32,7 @@ export declare class QaRoleRepository {
16
32
  * Replace one skill's administrator overlay.
17
33
  *
18
34
  * An overlay with nothing left to declare is removed outright, so clearing a
19
- * row in the administration surface leaves the file as if it was never set.
35
+ * row in the administration surface leaves the store as if it was never set.
20
36
  * @param actorId - the administrator performing the change.
21
37
  * @param input - the overlay as edited in the browser.
22
38
  * @returns every overlay in force after the change.
@@ -24,9 +40,24 @@ export declare class QaRoleRepository {
24
40
  updateSkillOverride(actorId: string, input: QaSkillAssignmentOverride): readonly QaSkillAssignmentOverride[];
25
41
  recordAssignment(actorId: string, targetId: string, before: unknown, after: unknown): void;
26
42
  private replace;
43
+ /** Append one change and trim the trail to its newest events. */
27
44
  private appendAudit;
28
- private reload;
29
- private load;
30
- private persist;
45
+ private writeConfig;
46
+ private readConfig;
47
+ private readAudit;
48
+ private readDataVersion;
49
+ /**
50
+ * Re-read the policy when another process wrote it. The `qa-admin` surfaces
51
+ * and this store share one database, so a configuration an administrator
52
+ * just saved must be the one the next policy resolution sees.
53
+ */
54
+ private refresh;
55
+ /**
56
+ * Import a pre-SQLite `qa-capability-policies.json` exactly once, then rename
57
+ * it aside. A configuration the database already holds wins: a leftover file
58
+ * must not silently revert an administrator's policy.
59
+ */
60
+ importLegacyFile(legacyFilePath: string): void;
61
+ private assertImportArrived;
31
62
  }
32
63
  //# sourceMappingURL=role-repository.d.ts.map
@@ -1,9 +1,11 @@
1
1
  import type { Agent } from "@deepseek-ai/dsh-agent";
2
2
  import type { Context } from "@deepseek-ai/cordis";
3
+ import type { ScopeKey } from "@deepseek-ai/dsh-scope";
3
4
  import type { PluginLogger } from "@yadsh/dsh-plugin-log";
4
5
  import type { QaAccounts } from "../accounts/store.js";
5
6
  import type { QaAgentToolGrants } from "../enforcement/tool-grants.js";
6
- import type { QaAccessAdminSnapshot, QaCapabilityConfig, QaCapabilityDescriptor, QaCapabilitySelection, QaCurrentAccess, QaEffectiveCapabilityPolicy, QaSessionAccess, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, ResolvedQaSurfaceConfig } from "../types.js";
7
+ import type { QaAccessAdminSnapshot, QaCapabilityConfig, QaCapabilityDescriptor, QaCapabilitySelection, QaClaimResult, QaCurrentAccess, QaEffectiveCapabilityPolicy, QaSessionAccess, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, ResolvedQaSurfaceConfig } from "../types.js";
8
+ import type { QaSessionLogReader } from "../admin/session-log.js";
7
9
  import { QaCapabilityCatalog, type CapabilityCatalogSnapshot } from "./capability-catalog.js";
8
10
  import { QaRoleRepository } from "./role-repository.js";
9
11
  export interface QaResolvedSessionPolicy {
@@ -21,16 +23,109 @@ export interface QaResolvedSessionPolicy {
21
23
  }
22
24
  /** Server-only orchestration of roles, assignments, discovery and snapshots. */
23
25
  export declare class QaAccessService {
26
+ private readonly ctx;
24
27
  private readonly options;
25
28
  readonly roles: QaRoleRepository;
26
29
  readonly catalog: QaCapabilityCatalog;
30
+ private lastOwnershipSweepAt;
31
+ /** The sweep in flight, so a burst of readers runs one listing, not ten. */
32
+ private ownershipSweep;
33
+ /**
34
+ * Sessions the Host has listed as delegated children of another session.
35
+ * The live registry answers for a child that is running right now; this
36
+ * cache answers for the ones that finished and were dismantled, which is the
37
+ * state a browser meets when it opens an old subagent transcript.
38
+ */
39
+ private readonly delegatedSessions;
40
+ private lastDelegatedSweepAt;
41
+ private delegatedSweep;
27
42
  constructor(ctx: Context, options: {
28
43
  readonly accounts: () => QaAccounts | undefined;
29
44
  readonly config: () => ResolvedQaSurfaceConfig;
30
45
  readonly logger: PluginLogger;
31
46
  readonly repository?: QaRoleRepository;
32
47
  readonly dynamicToolNames?: () => readonly string[];
48
+ /**
49
+ * The viewing scope an administrator's catalog is read with. Skills and
50
+ * tools a deployment mounts through an agent preset live in that
51
+ * preset's scope, so a global-only catalog left the operator unable to
52
+ * see — or grant — what every QA chat actually mounts.
53
+ */
54
+ readonly presetScope?: () => Promise<ScopeKey | undefined>;
55
+ /**
56
+ * Durable session listing, used to tell a chat from a delegated child
57
+ * across Host runs. Without it the lineage answer degrades to the live
58
+ * registry, which knows nothing about a session this process has not
59
+ * materialized — the sweep then reclaims nothing rather than guessing.
60
+ */
61
+ readonly sessionLog?: QaSessionLogReader;
62
+ /**
63
+ * Called with the ids of chats the Harness no longer knows, once their
64
+ * ownership records are reclaimed. The record is the auth boundary, not
65
+ * the whole of what the deployment kept about a chat: ratings, reviews
66
+ * and queue entries are keyed by conversation, and they outlive their
67
+ * chat unless the caller drops them here.
68
+ */
69
+ readonly onVanishedSessions?: (sessionIds: readonly string[]) => void;
33
70
  });
71
+ /**
72
+ * Whether the Host can prove this session is a delegated child: a subagent's
73
+ * session, which is an implementation detail of one answer and never a chat.
74
+ * Used to refuse one wherever conversations are listed or claimed.
75
+ */
76
+ isDelegatedChild(sessionId: string): boolean;
77
+ /**
78
+ * The Host-registered facts of one session, for the accounts store's
79
+ * bounded auto-claim. Absent facts (the session registry is unavailable,
80
+ * or this chat is not materialized here) keep the historical claim.
81
+ */
82
+ private sessionFacts;
83
+ /**
84
+ * Reclaim ownership records of chats the Harness no longer knows.
85
+ *
86
+ * Nothing else removes them: the browser's "delete chat" hides a chat in one
87
+ * browser, and a record outlives its session forever, so a year of QA traffic
88
+ * accumulates one row per chat ever created. The sweep is throttled, runs
89
+ * off the reservation path and off the review reads, and is skipped entirely
90
+ * when the Harness cannot say what exists — an unanswerable question is
91
+ * never a licence to delete an auth boundary.
92
+ *
93
+ * What exists is both halves of the Harness's own knowledge: the sessions
94
+ * this process has open and the ones its storage still holds. The live store
95
+ * alone would make a cold chat — one nobody has opened since the last
96
+ * restart — look deleted, and the sweep would reclaim records of chats that
97
+ * are merely quiet. A listing that cannot see stored sessions therefore
98
+ * reclaims nothing.
99
+ *
100
+ * @returns the sweep, for callers that want to await it (tests, and the
101
+ * review reads that must show the console what still exists).
102
+ */
103
+ sweepVanishedOwnership(): Promise<void>;
104
+ private runOwnershipSweep;
105
+ /**
106
+ * Migrate a browser's local chat index into server-side ownership.
107
+ *
108
+ * A delegated session is not a chat: claiming one would write exactly the
109
+ * ownership record the admission refuses to create and put a subagent's
110
+ * transcript in the account's chat list. Its ids are left out of the batch
111
+ * and reported, so a stale browser index cannot reintroduce the record.
112
+ */
113
+ claimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
114
+ /**
115
+ * Reclaim ownership records that point at delegated sessions, and remember
116
+ * which sessions are children while doing it.
117
+ *
118
+ * Records like this are residue of the paths that used to claim a session
119
+ * without knowing its lineage: each one keeps a subagent's transcript in the
120
+ * chat list of whoever opened it. Only positively identified children lose
121
+ * their record, so a listing that cannot be read reclaims nothing, and every
122
+ * other record stays the auth boundary it is.
123
+ *
124
+ * @returns the sweep, for callers that want to await it (tests).
125
+ */
126
+ reclaimDelegatedSessions(): Promise<void>;
127
+ /** Throttled, off the critical path, and never awaited by a request. */
128
+ private maybeSweepDelegatedSessions;
34
129
  current(token: string): QaCurrentAccess;
35
130
  session(token: string, sessionId: string): QaSessionAccess;
36
131
  reserveSession(token: string, sessionId: string, requestedSubrole: string | null, adminPreview: boolean): import("../types.js").QaAccountUserPublic;
@@ -47,9 +142,17 @@ export declare class QaAccessService {
47
142
  deleteSubrole(token: string, id: string, replacementId: string | null): void;
48
143
  updateCommon(token: string, input: QaCapabilitySelection): QaCapabilitySelection;
49
144
  updateAssignment(token: string, userId: string, input: QaUserAccess): QaUserAccess;
145
+ /** The accounts store, or the refusal this operation names. */
50
146
  private requireAccounts;
51
- /** The activation diagnostic is runtime plumbing, not an administrator grant. */
52
- private systemRequiredTools;
147
+ /**
148
+ * Tools every session resolves regardless of the role it runs under: the
149
+ * deployment's pinned allow-list plus the activation diagnostic.
150
+ *
151
+ * Readable outside the service because the administration surface reports a
152
+ * profile's effective capabilities next to the configured ones, and the
153
+ * pinned set is part of that answer whatever the profile says.
154
+ */
155
+ systemRequiredTools(): readonly string[];
53
156
  private requireAdmin;
54
157
  }
55
158
  export declare function validateCapabilityConfig(config: QaCapabilityConfig): QaCapabilityConfig;
@@ -1,6 +1,6 @@
1
1
  import type { PluginLogger } from "@yadsh/dsh-plugin-log";
2
2
  import { QaAccounts } from "./accounts/store.js";
3
- import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaClaimResult, QaOwnershipEntry, QaWhoamiResult, ResolvedQaSurfaceConfig } from "./types.js";
3
+ import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaOwnershipEntry, QaWhoamiResult, ResolvedQaSurfaceConfig } from "./types.js";
4
4
  /**
5
5
  * Host-side bodies of the account remotes the `QaSurface` service exposes.
6
6
  * The `@Remote`-decorated signatures stay on the service class (typert code
@@ -23,9 +23,13 @@ export interface QaAccountRemotes {
23
23
  login(email: string, password: string): QaAccountSession;
24
24
  /** Identity probe; safe to call with an empty or expired token. */
25
25
  whoami(token: string): QaWhoamiResult;
26
- /** Migrate a browser's local chat index into server-side ownership. */
27
- claimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
28
- /** The token user's owned session ids; the sidebar list authority. */
26
+ /**
27
+ * The token user's owned session ids; the sidebar list authority.
28
+ *
29
+ * Migrating a browser's local chat index is NOT here: it has to refuse a
30
+ * delegated session, which takes session lineage, and that lives in
31
+ * `QaAccessService.claimSessions` — one entry point, no path around it.
32
+ */
29
33
  ownedSessions(token: string): {
30
34
  readonly ids: readonly string[];
31
35
  };
@@ -0,0 +1,84 @@
1
+ import { SqliteDatabase } from "@yadsh/dsh-plugin-kit/sqlite";
2
+ import type { QaSkillActivationRecord } from "../types.js";
3
+ import { type AccountsFile, type StoredOwnership, type StoredUser } from "./file.js";
4
+ /** One ownership record as it is written, before it becomes a row. */
5
+ export interface OwnershipWrite {
6
+ readonly sessionId: string;
7
+ readonly owner: StoredOwnership;
8
+ }
9
+ /**
10
+ * The deployment's accounts, as tables.
11
+ *
12
+ * The stores this replaces were one JSON document read, parsed and rewritten
13
+ * whole on every mutation — so a chat being opened rewrote every account and
14
+ * every chat the deployment had ever seen, and the cost of that write grew with
15
+ * the deployment's age. Here a mutation touches the row it changes: opening a
16
+ * chat inserts one ownership row, and the file's size stops being a per-turn
17
+ * cost.
18
+ *
19
+ * Cross-process freshness survives the change. The `qa-accounts` CLI writes the
20
+ * same database from a second process, so the in-memory model is invalidated by
21
+ * `PRAGMA data_version`, which SQLite bumps exactly when *another* connection
22
+ * commits — the same guarantee the previous mtime-and-size stamp provided, with
23
+ * no file read to notice it.
24
+ */
25
+ export declare class QaAccountsDatabase {
26
+ readonly filePath: string;
27
+ readonly storage: SqliteDatabase;
28
+ private observedDataVersion;
29
+ constructor(filePath: string);
30
+ /** The token secret, creating one for a database that has none yet. */
31
+ get secret(): string;
32
+ /** True while the in-memory model a caller holds is still current. */
33
+ get unchangedSinceLoad(): boolean;
34
+ /** Take the current data version as the baseline a caller's model reflects. */
35
+ markLoaded(): void;
36
+ /** Every account, ownership record and skill activation, in one read. */
37
+ loadAll(): AccountsFile;
38
+ /** Append one account, preserving the order accounts are listed in. */
39
+ insertUser(user: StoredUser): void;
40
+ /** Replace one account's row in place. */
41
+ updateUser(user: StoredUser): void;
42
+ /** Record a successful sign-in without rewriting the account. */
43
+ touchLogin(userId: string, at: string): void;
44
+ /** Claim one chat for an account. */
45
+ insertOwnership(sessionId: string, owner: StoredOwnership): void;
46
+ /** Claim many chats at once, which is how a browser's index migrates. */
47
+ insertOwnershipMany(rows: readonly OwnershipWrite[]): void;
48
+ /** Rewrite an existing chat's pinned role, preview flag or snapshot. */
49
+ updateOwnership(sessionId: string, owner: StoredOwnership): void;
50
+ /** Forget chats: a rolled-back reservation, or a chat the Harness lost. */
51
+ deleteOwnership(sessionIds: readonly string[]): void;
52
+ /**
53
+ * Append one skill activation and keep the newest `limit` of them. The list
54
+ * is bounded because a long session may activate skills many times, and the
55
+ * oldest entries stop being interesting long before the store does.
56
+ */
57
+ appendSkillActivation(sessionId: string, entry: QaSkillActivationRecord, limit: number): readonly QaSkillActivationRecord[];
58
+ /**
59
+ * Import a pre-SQLite `qa-accounts.json` exactly once, then rename it away.
60
+ *
61
+ * The import is verified *inside* the transaction and before the file is
62
+ * retired — every account and every ownership record must have arrived, and
63
+ * the token secret must be the one the file carried — because a silent
64
+ * partial import would either lock accounts out or hand a chat to the wrong
65
+ * owner. A database that already holds accounts is never overwritten by a
66
+ * leftover file.
67
+ */
68
+ importLegacyFile(legacyFilePath: string | undefined): AccountsFile | undefined;
69
+ private assertImportArrived;
70
+ close(): void;
71
+ private hasAccounts;
72
+ private readDataVersion;
73
+ private ensureSecret;
74
+ /** The account columns, in the order both write statements bind them. */
75
+ private accountColumns;
76
+ private writeUser;
77
+ private writeOwnership;
78
+ /** One row per distinct policy; the digest is the policy's content. */
79
+ private writeSnapshot;
80
+ /** Drop snapshots no chat refers to any more, so the table cannot drift up. */
81
+ private gcSnapshots;
82
+ private parseLegacyFile;
83
+ }
84
+ //# sourceMappingURL=database.d.ts.map
@@ -3,7 +3,13 @@
3
3
  * marker pattern the attestation path established; the browser maps them to
4
4
  * audience-safe copy and the precise cause stays in the Host logs.
5
5
  */
6
- export type QaAccountsErrorReason = "auth-required" | "admin-required" | "forbidden" | "invalid-credentials" | "account-disabled" | "email-taken" | "invalid-email" | "invalid-display-name" | "invalid-profile" | "profile-disabled" | "invalid-starters" | "starters-disabled" | "invalid-role" | "weak-password" | "registration-disabled" | "rate-limited" | "session-owned-elsewhere";
6
+ export type QaAccountsErrorReason = "auth-required" | "admin-required" | "forbidden" | "invalid-credentials" | "account-disabled" | "email-taken" | "invalid-email" | "invalid-display-name" | "invalid-profile" | "profile-disabled" | "invalid-starters" | "starters-disabled" | "invalid-role" | "weak-password" | "registration-disabled" | "rate-limited" | "session-owned-elsewhere"
7
+ /** The conversation a deletion names is not one this deployment knows. */
8
+ | "conversation-unknown"
9
+ /** The Harness still holds the conversation open, so its log would come back. */
10
+ | "conversation-live"
11
+ /** This deployment cannot remove stored conversations at all. */
12
+ | "conversation-not-removable";
7
13
  export declare class QaAccountsError extends Error {
8
14
  readonly reason: QaAccountsErrorReason;
9
15
  constructor(reason: QaAccountsErrorReason, message: string);
@@ -1,13 +1,12 @@
1
- import type { Stats } from "node:fs";
2
1
  import type { QaAccountRole, QaEffectiveCapabilityPolicy, QaSkillActivationRecord, QaUserAccess } from "../types.js";
3
- /** One account's self-declared profile as it rests in the accounts file. */
2
+ /** One account's self-declared profile as it rests in the accounts store. */
4
3
  export interface StoredProfile {
5
4
  fullName?: string;
6
5
  identities?: Record<string, string>;
7
6
  instructions?: string;
8
7
  updatedAt?: string;
9
8
  }
10
- /** One account's starter buttons as they rest in the accounts file. */
9
+ /** One account's starter buttons as they rest in the accounts store. */
11
10
  export interface StoredStarters {
12
11
  items?: {
13
12
  label?: string;
@@ -53,51 +52,30 @@ export interface StoredOwnership {
53
52
  /** Skill activations in order: what the model gained, and when it gained it. */
54
53
  readonly skillActivations?: readonly QaSkillActivationRecord[];
55
54
  }
55
+ /**
56
+ * The accounts model as the store holds it in memory, hydrated from the
57
+ * database and invalidated by `PRAGMA data_version` when another process
58
+ * writes. Snapshots appear inline here: the storage layer is what keeps one
59
+ * copy per distinct policy, not the callers.
60
+ */
56
61
  export interface AccountsFile {
57
62
  readonly version: 1;
58
- /** HMAC key for account tokens; rotated on password change by rewriting it. */
63
+ /** HMAC key for account tokens; created on first use. */
59
64
  readonly secret: string;
60
65
  readonly users: StoredUser[];
61
66
  readonly ownership: Record<string, StoredOwnership>;
62
67
  }
63
- /** mtime+size pair identifying one on-disk version of the accounts file. */
64
- export interface FileStamp {
65
- readonly mtimeMs: number;
66
- readonly size: number;
67
- }
68
- /**
69
- * Where the last-seen stamp lives. Load and persist keep it current in place,
70
- * so a caller holds one mutable ref instead of threading the stamp through
71
- * every read-modify-write.
72
- */
73
- export interface FileStampRef {
74
- current: FileStamp;
75
- }
76
- /** A stamp no real file can match, so the first load always reads the disk. */
77
- export declare function createFileStampRef(): FileStampRef;
78
- export declare function stampOf(stat: Stats): FileStamp;
79
- /** The accounts file lives next to the DSH home the launcher exports. */
68
+ /** The accounts database lives next to the DSH home the launcher exports. */
80
69
  export declare function defaultAccountsFilePath(): string;
81
70
  /**
82
- * Read the accounts file, or create it on first run with a fresh token
83
- * secret. The stamp ref is left pointing at the version just read.
84
- */
85
- export declare function loadAccountsFile(filePath: string, stamp: FileStampRef): AccountsFile;
86
- /**
87
- * Persist the accounts file atomically (temp file + rename) and leave the
88
- * stamp ref pointing at the freshly written version.
71
+ * Where the pre-SQLite accounts file lived. It is imported on first use and
72
+ * then renamed aside, so a deployment upgrades without re-registering anyone.
89
73
  */
90
- export declare function persistAccountsFile(filePath: string, file: AccountsFile, stamp: FileStampRef): void;
74
+ export declare function defaultLegacyAccountsFilePath(): string;
91
75
  /**
92
- * The qa-accounts CLI runs in its own process against the same file. Re-read
93
- * it whenever it changed on disk, before every read or read-modify-write:
94
- * without this a running Host would keep honoring tokens the CLI revoked
95
- * and its next save would resurrect the CLI's change from a stale snapshot.
96
- * One file, one writer at a time remains the deployment's discipline; LAN
97
- * scale keeps the stat per operation trivial.
98
- *
99
- * Returns the freshly loaded file, or undefined when the caller should keep
100
- * serving its in-memory state.
76
+ * A stable digest of one capability policy, so two chats admitted under the
77
+ * same policy share a stored snapshot. Keys are sorted because object key order
78
+ * is an artefact of construction, not part of the policy.
101
79
  */
102
- export declare function reloadAccountsFileIfChanged(filePath: string, stamp: FileStampRef): AccountsFile | undefined;
80
+ export declare function snapshotDigest(value: unknown): string;
103
81
  //# sourceMappingURL=file.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { QaAccountIdentityField, QaAccountProfile, QaAccountProfileInput, QaAccountRole, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaAccessUser, QaAdminAccountRow, QaClaimResult, QaEffectiveCapabilityPolicy, QaOwnershipEntry, QaSkillActivationRecord, QaUserAccess, QaWhoamiResult } from "../types.js";
2
- import type { StoredOwnership, StoredUser } from "./file.js";
2
+ import { type StoredOwnership, type StoredUser } from "./file.js";
3
3
  export { QaAccountsError } from "./errors.js";
4
4
  export type { QaAccountsErrorReason } from "./errors.js";
5
5
  export { defaultAccountsFilePath } from "./file.js";
@@ -15,18 +15,45 @@ export interface QaAccountsOptions {
15
15
  * omitting the list (the operator CLI) checks the key shape alone.
16
16
  */
17
17
  readonly identityFields?: readonly QaAccountIdentityField[];
18
+ /**
19
+ * Where a pre-SQLite accounts file may still live. Defaults to the `.json`
20
+ * sibling of the database; pass an explicit path when a deployment kept it
21
+ * somewhere else.
22
+ */
23
+ readonly legacyFilePath?: string;
18
24
  }
19
25
  /**
20
- * Host authority for QA accounts: the file-backed user list, the session
21
- * ownership map and the HMAC account tokens. All mutations are synchronous
22
- * and persisted atomically (temp file + rename); LAN scale keeps this trivial.
26
+ * What the caller knows about the live session it is gating. The store is
27
+ * file-backed and session-agnostic; the Host supplies these facts so the
28
+ * first-come auto-claim can refuse what it must not attach silently. Absent
29
+ * facts (the operator CLI, a session the Host has not materialized) keep the
30
+ * historical unbounded claim.
31
+ */
32
+ export interface QaSessionFacts {
33
+ /** Epoch ms the Host recorded as the session's creation. */
34
+ readonly createdAt?: number;
35
+ /** True when the session is a delegated child of another chat. */
36
+ readonly hasParent?: boolean;
37
+ }
38
+ /**
39
+ * How long an unowned session stays claimable at access time. Mirrors the
40
+ * admission bootstrap window: a session older than this has either user
41
+ * history or a settled composition, so silently attaching it to whoever
42
+ * asked first would hand over a conversation nobody assigned to them.
43
+ * Owners are unaffected — their claim already exists in the file.
44
+ */
45
+ export declare const QA_SESSION_CLAIM_WINDOW_MS = 120000;
46
+ /**
47
+ * Host authority for QA accounts: the account list, the session ownership map
48
+ * and the HMAC account tokens. Mutations are synchronous and land as the rows
49
+ * they change, in one transaction each.
23
50
  *
24
- * The class is the facade: token mechanics live in token.ts, the file format
25
- * and its atomic persistence plus the external-change probe in file.ts, and
26
- * the credential rules shared by registration and the operator CLI in
27
- * credentials.ts.
51
+ * The class is the facade: token mechanics live in token.ts, the tables and the
52
+ * legacy import in database.ts, and the credential rules shared by registration
53
+ * and the operator CLI in credentials.ts.
28
54
  */
29
55
  export declare class QaAccounts {
56
+ /** The database this store reads and writes. */
30
57
  readonly filePath: string;
31
58
  private file;
32
59
  private readonly authAttempts;
@@ -35,18 +62,26 @@ export declare class QaAccounts {
35
62
  private readonly allowRegistration;
36
63
  /** Character cap on a stored profile's agent guidance. */
37
64
  private readonly instructionsMaxLength;
38
- /** Declared handle fields; undefined lets an operator write any shape-valid key. */
39
- private readonly identityFields;
40
- /** mtime+size of the file as of the last load; the external-change probe. */
41
- private readonly fileStamp;
42
- constructor(filePath: string | undefined, options: QaAccountsOptions);
65
+ /** Whether a deployment declares which identity handles an account may hold. */
66
+ readonly identityFields: readonly QaAccountIdentityField[] | undefined;
43
67
  /**
44
- * Re-read the accounts file when it changed on disk, before every read or
45
- * read-modify-write. The probe itself and its cross-process rationale live
46
- * with the file layer (reloadAccountsFileIfChanged).
68
+ * The store's tables. Every mutation writes the rows it changes and the
69
+ * in-memory model is refreshed only when another process wrote — which is
70
+ * what keeps the cost of opening a chat independent of the deployment's age.
71
+ */
72
+ private readonly database;
73
+ constructor(configuredPath: string | undefined, options: QaAccountsOptions);
74
+ /** Close the underlying database; the store is unusable afterwards. */
75
+ close(): void;
76
+ /**
77
+ * Re-read the model when another process wrote the database, before every
78
+ * read or read-modify-write. The `qa-accounts` CLI runs in its own process
79
+ * against the same file, so without this a running Host would keep honoring
80
+ * tokens the CLI revoked. SQLite bumps `PRAGMA data_version` exactly when a
81
+ * *different* connection commits, which is the probe the previous
82
+ * mtime-and-size stamp of the JSON file used to provide.
47
83
  */
48
84
  private reloadIfChanged;
49
- private save;
50
85
  private pruneAuthAttempts;
51
86
  private assertAuthBudget;
52
87
  private userByEmail;
@@ -64,8 +99,15 @@ export declare class QaAccounts {
64
99
  * Ownership gate for one session: claim unowned sessions for the requesting
65
100
  * user (first come, first served — the pre-accounts migration path), refuse
66
101
  * foreign sessions unless the caller administers the deployment.
102
+ *
103
+ * The auto-claim is bounded when the caller can describe the live session
104
+ * ({@link QaSessionFacts}): a delegated subagent session is never claimed,
105
+ * and a session past the freshness window is refused instead of silently
106
+ * attached to whoever opened it first. Both refusals keep the file intact —
107
+ * the session stays unowned, so the operator CLI or a deliberate future
108
+ * claim can still resolve it.
67
109
  */
68
- ensureSessionAccess(token: string, sessionId: string): QaAccountUserPublic;
110
+ ensureSessionAccess(token: string, sessionId: string, facts?: QaSessionFacts): QaAccountUserPublic;
69
111
  /**
70
112
  * Atomically reserve a Host-generated session id for the token user before
71
113
  * the Session exists. Browser callers therefore never get a race window in
@@ -81,6 +123,48 @@ export declare class QaAccounts {
81
123
  claimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
82
124
  /** Session ids owned by the token's user, most recently claimed first. */
83
125
  ownedSessionIds(token: string): readonly string[];
126
+ /**
127
+ * Drop ownership records of sessions that are not chats at all.
128
+ *
129
+ * A delegated subagent session is an implementation detail of one answer:
130
+ * nothing legitimately owns one, so no grace period applies and every named
131
+ * id loses its record. Only ids the caller positively identified are
132
+ * touched — a record is an auth boundary, and an id that merely looks odd
133
+ * keeps its owner. What this reclaims is residue: records written before the
134
+ * claim and attestation paths learned to refuse a delegated child, each one
135
+ * keeping a subagent's session in some account's chat list.
136
+ *
137
+ * @param sessionIds - the ids the caller identified as delegated children.
138
+ * @returns the session ids whose records were dropped.
139
+ */
140
+ pruneDelegatedOwnership(sessionIds: ReadonlySet<string>): readonly string[];
141
+ /**
142
+ * Drop the ownership records of the named sessions, whatever their lineage.
143
+ *
144
+ * This is the record half of a deletion: the caller has already established
145
+ * that these sessions are gone (or is removing them as part of the same
146
+ * act), so the auth boundary goes with them. Nothing here decides anything —
147
+ * the sweeps above own the judgments, and both end in this.
148
+ *
149
+ * @param sessionIds - the ids whose records are dropped.
150
+ * @returns the session ids whose records were actually there.
151
+ */
152
+ forgetSessions(sessionIds: ReadonlySet<string>): readonly string[];
153
+ /**
154
+ * Drop ownership records for chats the Harness no longer knows.
155
+ *
156
+ * An ownership record is a chat's auth boundary — `ensureSessionAccess`
157
+ * claims an unowned session for whoever asks first — so a record is removed
158
+ * only when the caller reports the session gone *and* the claim is older
159
+ * than `graceHours`. A reservation racing its own creation therefore keeps
160
+ * its owner, and a live chat never becomes claimable by someone else. What
161
+ * this reclaims is the residue of deleted chats, which nothing else removes.
162
+ *
163
+ * @param isLive - whether the Harness still knows this session.
164
+ * @param graceHours - youngest claim the sweep may touch.
165
+ * @returns the session ids whose records were dropped.
166
+ */
167
+ pruneVanishedSessions(isLive: (sessionId: string) => boolean, graceHours: number): readonly string[];
84
168
  /**
85
169
  * Every ownership entry with the owner's display name resolved at read
86
170
  * time, oldest claim first. Admin-only: this is the cross-user view the
@@ -131,7 +215,14 @@ export declare class QaAccounts {
131
215
  readonly adminPreview?: boolean;
132
216
  readonly capabilitySnapshot?: QaEffectiveCapabilityPolicy;
133
217
  }[];
134
- /** Replace one account's authorization role, addressed by stable id. */
218
+ /**
219
+ * Replace one account's authorization role, addressed by stable id.
220
+ *
221
+ * The union is re-checked here even though the type names it: the value
222
+ * arrives from the browser's admin console over the wire, and a role outside
223
+ * the union would be stored as-is and then crash every later permission
224
+ * check (`PERMISSIONS[role]` lookup) for that account.
225
+ */
135
226
  setAccessRole(userId: string, role: QaAccountRole): QaAccountUserPublic;
136
227
  /**
137
228
  * Disable or enable one account by id. Same semantics as the operator