@yadsh/dsh-qa-surface 0.12.0 → 0.14.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 (341) hide show
  1. package/README.md +148 -22
  2. package/compatibility.json +7 -3
  3. package/lib/access/capability-catalog.js +22 -6
  4. package/lib/access/capability-catalog.js.map +1 -1
  5. package/lib/access/model.js +36 -2
  6. package/lib/access/model.js.map +1 -1
  7. package/lib/access/service.js +19 -4
  8. package/lib/access/service.js.map +1 -1
  9. package/lib/account-remotes.js +4 -0
  10. package/lib/account-remotes.js.map +1 -1
  11. package/lib/accounts/database.js +49 -9
  12. package/lib/accounts/database.js.map +1 -1
  13. package/lib/accounts/errors.js.map +1 -1
  14. package/lib/accounts/file.js.map +1 -1
  15. package/lib/accounts/store.js +53 -8
  16. package/lib/accounts/store.js.map +1 -1
  17. package/lib/admin/permissions.js +6 -0
  18. package/lib/admin/permissions.js.map +1 -1
  19. package/lib/admin/quality-store.js +95 -19
  20. package/lib/admin/quality-store.js.map +1 -1
  21. package/lib/admin/service.js +432 -52
  22. package/lib/admin/service.js.map +1 -1
  23. package/lib/cli.js +24 -4
  24. package/lib/cli.js.map +1 -1
  25. package/lib/client/QaAccountsController.js +70 -5
  26. package/lib/client/QaAccountsController.js.map +1 -1
  27. package/lib/client/QaConfigController.js.map +1 -1
  28. package/lib/client/QaSessionController.js +626 -63
  29. package/lib/client/QaSessionController.js.map +1 -1
  30. package/lib/client/QaSurface.js +279 -43
  31. package/lib/client/QaSurface.js.map +1 -1
  32. package/lib/client/QaSurfaceGuard.js +2 -2
  33. package/lib/client/QaSurfaceGuard.js.map +1 -1
  34. package/lib/client/QaTranscriptAdapter.js +4 -2
  35. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  36. package/lib/client/admin/QaAdmin.js +59 -34
  37. package/lib/client/admin/QaAdmin.js.map +1 -1
  38. package/lib/client/admin/pages/Conversations.js +21 -14
  39. package/lib/client/admin/pages/Conversations.js.map +1 -1
  40. package/lib/client/admin/pages/ExpertMemory.js +210 -0
  41. package/lib/client/admin/pages/ExpertMemory.js.map +1 -0
  42. package/lib/client/admin/pages/Overview.js +5 -5
  43. package/lib/client/admin/pages/Overview.js.map +1 -1
  44. package/lib/client/admin/pages/Quality.js +6 -6
  45. package/lib/client/admin/pages/Quality.js.map +1 -1
  46. package/lib/client/admin/pages/Review.js +13 -9
  47. package/lib/client/admin/pages/Review.js.map +1 -1
  48. package/lib/client/admin/pages/SkillFiles.js +3 -3
  49. package/lib/client/admin/pages/SkillFiles.js.map +1 -1
  50. package/lib/client/admin/pages/Users.js +12 -12
  51. package/lib/client/admin/pages/Users.js.map +1 -1
  52. package/lib/client/admin/routes.js +4 -0
  53. package/lib/client/admin/routes.js.map +1 -1
  54. package/lib/client/admin/shared.js +57 -10
  55. package/lib/client/admin/shared.js.map +1 -1
  56. package/lib/client/admin/styles.js +3 -0
  57. package/lib/client/admin/styles.js.map +1 -1
  58. package/lib/client/attestation.js +11 -2
  59. package/lib/client/attestation.js.map +1 -1
  60. package/lib/client/audit/QaAuditDialog.js +2 -2
  61. package/lib/client/audit/QaAuditDialog.js.map +1 -1
  62. package/lib/client/audit/QaAuditRowBadge.js +1 -1
  63. package/lib/client/audit/QaAuditRowBadge.js.map +1 -1
  64. package/lib/client/components/Markdown.js +7 -2
  65. package/lib/client/components/Markdown.js.map +1 -1
  66. package/lib/client/components/QaAgentsDrawer.js +7 -5
  67. package/lib/client/components/QaAgentsDrawer.js.map +1 -1
  68. package/lib/client/components/QaApproval.js +2 -2
  69. package/lib/client/components/QaApproval.js.map +1 -1
  70. package/lib/client/components/QaAuthGate.js +6 -6
  71. package/lib/client/components/QaAuthGate.js.map +1 -1
  72. package/lib/client/components/QaChangelog.js +87 -0
  73. package/lib/client/components/QaChangelog.js.map +1 -1
  74. package/lib/client/components/QaComposer.js +9 -6
  75. package/lib/client/components/QaComposer.js.map +1 -1
  76. package/lib/client/components/QaFileAttachment.js +1 -1
  77. package/lib/client/components/QaFileAttachment.js.map +1 -1
  78. package/lib/client/components/QaFilesPanel.js +7 -7
  79. package/lib/client/components/QaFilesPanel.js.map +1 -1
  80. package/lib/client/components/QaHeader.js +4 -4
  81. package/lib/client/components/QaHeader.js.map +1 -1
  82. package/lib/client/components/QaMessage.js +12 -12
  83. package/lib/client/components/QaMessage.js.map +1 -1
  84. package/lib/client/components/QaModal.js +11 -2
  85. package/lib/client/components/QaModal.js.map +1 -1
  86. package/lib/client/components/QaQuestions.js +4 -4
  87. package/lib/client/components/QaQuestions.js.map +1 -1
  88. package/lib/client/components/QaQueueDock.js +137 -0
  89. package/lib/client/components/QaQueueDock.js.map +1 -0
  90. package/lib/client/components/QaRequestQueue.js +22 -0
  91. package/lib/client/components/QaRequestQueue.js.map +1 -0
  92. package/lib/client/components/QaRightRail.js +2 -2
  93. package/lib/client/components/QaRightRail.js.map +1 -1
  94. package/lib/client/components/QaSidebar.js +66 -6
  95. package/lib/client/components/QaSidebar.js.map +1 -1
  96. package/lib/client/components/QaSourcesPanel.js +10 -8
  97. package/lib/client/components/QaSourcesPanel.js.map +1 -1
  98. package/lib/client/components/QaThemeSwitcher.js +19 -0
  99. package/lib/client/components/QaThemeSwitcher.js.map +1 -0
  100. package/lib/client/components/QaTurnNotice.js +40 -0
  101. package/lib/client/components/QaTurnNotice.js.map +1 -0
  102. package/lib/client/components/QaTurnRail.js +3 -3
  103. package/lib/client/components/QaTurnRail.js.map +1 -1
  104. package/lib/client/components/QaWelcomeNotice.js +2 -6
  105. package/lib/client/components/QaWelcomeNotice.js.map +1 -1
  106. package/lib/client/components/QaWidthHandle.js +2 -2
  107. package/lib/client/components/QaWidthHandle.js.map +1 -1
  108. package/lib/client/components/QaWorkGroup.js +5 -5
  109. package/lib/client/components/QaWorkGroup.js.map +1 -1
  110. package/lib/client/components/QaWorkspaceBrowser.js +10 -10
  111. package/lib/client/components/QaWorkspaceBrowser.js.map +1 -1
  112. package/lib/client/components/VariantSwitcher.js +1 -1
  113. package/lib/client/components/VariantSwitcher.js.map +1 -1
  114. package/lib/client/failure-copy.js +26 -8
  115. package/lib/client/failure-copy.js.map +1 -1
  116. package/lib/client/focus-ring.js +193 -0
  117. package/lib/client/focus-ring.js.map +1 -0
  118. package/lib/client/index.js +58 -24
  119. package/lib/client/index.js.map +1 -1
  120. package/lib/client/lineage.js +3 -16
  121. package/lib/client/lineage.js.map +1 -1
  122. package/lib/client/markdown/CodeBlock.js +2 -2
  123. package/lib/client/markdown/CodeBlock.js.map +1 -1
  124. package/lib/client/markdown/blocks.js +118 -31
  125. package/lib/client/markdown/blocks.js.map +1 -1
  126. package/lib/client/markdown/math.js +1 -1
  127. package/lib/client/markdown/math.js.map +1 -1
  128. package/lib/client/markdown/render.js +32 -12
  129. package/lib/client/markdown/render.js.map +1 -1
  130. package/lib/client/markdown/source-chip.js +3 -3
  131. package/lib/client/markdown/source-chip.js.map +1 -1
  132. package/lib/client/notifications/notification-dispatcher.js +90 -0
  133. package/lib/client/notifications/notification-dispatcher.js.map +1 -0
  134. package/lib/client/notifications/preferences.js +61 -0
  135. package/lib/client/notifications/preferences.js.map +1 -0
  136. package/lib/client/notifications/turn-completion-source.js +82 -0
  137. package/lib/client/notifications/turn-completion-source.js.map +1 -0
  138. package/lib/client/notifications/use-turn-notifications.js +108 -0
  139. package/lib/client/notifications/use-turn-notifications.js.map +1 -0
  140. package/lib/client/panels/PanelHost.js +5 -5
  141. package/lib/client/panels/PanelHost.js.map +1 -1
  142. package/lib/client/panels/PanelLauncher.js +2 -2
  143. package/lib/client/panels/PanelLauncher.js.map +1 -1
  144. package/lib/client/project-session-state.js +156 -4
  145. package/lib/client/project-session-state.js.map +1 -1
  146. package/lib/client/rating-harvest.js +138 -0
  147. package/lib/client/rating-harvest.js.map +1 -0
  148. package/lib/client/role/RoleSelector.js +9 -4
  149. package/lib/client/role/RoleSelector.js.map +1 -1
  150. package/lib/client/settings/card.js +58 -17
  151. package/lib/client/settings/card.js.map +1 -1
  152. package/lib/client/settings/fields.js +11 -10
  153. package/lib/client/settings/fields.js.map +1 -1
  154. package/lib/client/settings/format.js +0 -9
  155. package/lib/client/settings/format.js.map +1 -1
  156. package/lib/client/settings/sections/access.js +5 -5
  157. package/lib/client/settings/sections/access.js.map +1 -1
  158. package/lib/client/settings/sections/accounts.js +10 -10
  159. package/lib/client/settings/sections/accounts.js.map +1 -1
  160. package/lib/client/settings/sections/attachments.js +6 -6
  161. package/lib/client/settings/sections/attachments.js.map +1 -1
  162. package/lib/client/settings/sections/branding.js +7 -7
  163. package/lib/client/settings/sections/branding.js.map +1 -1
  164. package/lib/client/settings/sections/common.js +4 -2
  165. package/lib/client/settings/sections/common.js.map +1 -1
  166. package/lib/client/settings/sections/docs.js +4 -3
  167. package/lib/client/settings/sections/docs.js.map +1 -1
  168. package/lib/client/settings/sections/embedding.js +2 -2
  169. package/lib/client/settings/sections/embedding.js.map +1 -1
  170. package/lib/client/settings/sections/interface.js +12 -12
  171. package/lib/client/settings/sections/interface.js.map +1 -1
  172. package/lib/client/settings/sections/lockdown.js +9 -9
  173. package/lib/client/settings/sections/lockdown.js.map +1 -1
  174. package/lib/client/settings/sections/notes.js +12 -12
  175. package/lib/client/settings/sections/notes.js.map +1 -1
  176. package/lib/client/settings/sections/session.js +15 -12
  177. package/lib/client/settings/sections/session.js.map +1 -1
  178. package/lib/client/settings/sections/slash.js +9 -9
  179. package/lib/client/settings/sections/slash.js.map +1 -1
  180. package/lib/client/settings/sections/sources.js +26 -26
  181. package/lib/client/settings/sections/sources.js.map +1 -1
  182. package/lib/client/settings/sections/status.js +2 -2
  183. package/lib/client/settings/sections/status.js.map +1 -1
  184. package/lib/client/settings/styles.js +15 -9
  185. package/lib/client/settings/styles.js.map +1 -1
  186. package/lib/client/slash/SlashPalette.js +4 -4
  187. package/lib/client/slash/SlashPalette.js.map +1 -1
  188. package/lib/client/styles.js +139 -66
  189. package/lib/client/styles.js.map +1 -1
  190. package/lib/client/theme-preference.js +162 -0
  191. package/lib/client/theme-preference.js.map +1 -0
  192. package/lib/client/types.js +8 -0
  193. package/lib/client/types.js.map +1 -1
  194. package/lib/client/use-session-ui-state.js +7 -5
  195. package/lib/client/use-session-ui-state.js.map +1 -1
  196. package/lib/client/user-settings/GeneralSettingsPage.js +6 -6
  197. package/lib/client/user-settings/GeneralSettingsPage.js.map +1 -1
  198. package/lib/client/user-settings/IntegrationTokensPage.js +6 -6
  199. package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -1
  200. package/lib/client/user-settings/NotificationSettingsPage.js +107 -0
  201. package/lib/client/user-settings/NotificationSettingsPage.js.map +1 -0
  202. package/lib/client/user-settings/PasswordSettingsPage.js +1 -1
  203. package/lib/client/user-settings/PasswordSettingsPage.js.map +1 -1
  204. package/lib/client/user-settings/ProfileSettingsPage.js +2 -2
  205. package/lib/client/user-settings/ProfileSettingsPage.js.map +1 -1
  206. package/lib/client/user-settings/SkillCatalog.js +3 -3
  207. package/lib/client/user-settings/SkillCatalog.js.map +1 -1
  208. package/lib/client/user-settings/SkillEditor.js +26 -8
  209. package/lib/client/user-settings/SkillEditor.js.map +1 -1
  210. package/lib/client/user-settings/SkillToolPicker.js +3 -3
  211. package/lib/client/user-settings/SkillToolPicker.js.map +1 -1
  212. package/lib/client/user-settings/SkillsSettingsPage.js +1 -1
  213. package/lib/client/user-settings/SkillsSettingsPage.js.map +1 -1
  214. package/lib/client/user-settings/StartersSettingsPage.js +6 -6
  215. package/lib/client/user-settings/StartersSettingsPage.js.map +1 -1
  216. package/lib/client/user-settings/UserSettingsDialog.js +7 -2
  217. package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
  218. package/lib/client/user-settings/copy.js +2 -0
  219. package/lib/client/user-settings/copy.js.map +1 -1
  220. package/lib/client/user-settings/draft.js +4 -1
  221. package/lib/client/user-settings/draft.js.map +1 -1
  222. package/lib/client/user-settings/fields.js +5 -5
  223. package/lib/client/user-settings/fields.js.map +1 -1
  224. package/lib/client.js +7483 -2075
  225. package/lib/client.js.map +1 -1
  226. package/lib/config-resolvers/defaults.js +7 -0
  227. package/lib/config-resolvers/defaults.js.map +1 -1
  228. package/lib/config-resolvers/lockdown.js +7 -0
  229. package/lib/config-resolvers/lockdown.js.map +1 -1
  230. package/lib/config-resolvers/notifications.js +15 -0
  231. package/lib/config-resolvers/notifications.js.map +1 -0
  232. package/lib/config-resolvers/session.js +11 -1
  233. package/lib/config-resolvers/session.js.map +1 -1
  234. package/lib/config.js +67 -18
  235. package/lib/config.js.map +1 -1
  236. package/lib/enforcement/tool-grants.js +6 -6
  237. package/lib/enforcement/tool-grants.js.map +1 -1
  238. package/lib/index.js +171 -39
  239. package/lib/index.js.map +1 -1
  240. package/lib/integration/answer.js +74 -8
  241. package/lib/integration/answer.js.map +1 -1
  242. package/lib/integration/expert-memory.js +39 -0
  243. package/lib/integration/expert-memory.js.map +1 -0
  244. package/lib/integration/host-runner.js +26 -14
  245. package/lib/integration/host-runner.js.map +1 -1
  246. package/lib/notifications.js +66 -0
  247. package/lib/notifications.js.map +1 -0
  248. package/lib/personal-skills/errors.js.map +1 -1
  249. package/lib/personal-skills/service.js +81 -7
  250. package/lib/personal-skills/service.js.map +1 -1
  251. package/lib/prompt-notes.js +2 -10
  252. package/lib/prompt-notes.js.map +1 -1
  253. package/lib/provenance/extractors.js +1 -1
  254. package/lib/provenance/extractors.js.map +1 -1
  255. package/lib/provenance/host-store.js +5 -1
  256. package/lib/provenance/host-store.js.map +1 -1
  257. package/lib/qa-tools/docs-tools.js +64 -11
  258. package/lib/qa-tools/docs-tools.js.map +1 -1
  259. package/lib/qa-tools/lifecycle.js +6 -3
  260. package/lib/qa-tools/lifecycle.js.map +1 -1
  261. package/lib/request-queue.js +32 -0
  262. package/lib/request-queue.js.map +1 -0
  263. package/lib/resolve-config.js +2 -0
  264. package/lib/resolve-config.js.map +1 -1
  265. package/lib/secure-session.js +7 -2
  266. package/lib/secure-session.js.map +1 -1
  267. package/lib/shared/settings.js +19 -2
  268. package/lib/shared/settings.js.map +1 -1
  269. package/lib/turn-failure-log.js +67 -0
  270. package/lib/turn-failure-log.js.map +1 -0
  271. package/lib/typert.host.js +1459 -647
  272. package/lib/typert.remote-client.d.ts +25 -9
  273. package/lib/typert.remote-client.js +1356 -636
  274. package/lib/types/access/capability-catalog.d.ts +20 -4
  275. package/lib/types/access/model.d.ts +23 -0
  276. package/lib/types/access/service.d.ts +2 -3
  277. package/lib/types/account-remotes.d.ts +8 -1
  278. package/lib/types/accounts/database.d.ts +13 -3
  279. package/lib/types/accounts/errors.d.ts +9 -1
  280. package/lib/types/accounts/file.d.ts +12 -0
  281. package/lib/types/accounts/store.d.ts +30 -2
  282. package/lib/types/admin/quality-store.d.ts +33 -0
  283. package/lib/types/admin/service.d.ts +119 -9
  284. package/lib/types/client/QaAccountsController.d.ts +37 -3
  285. package/lib/types/client/QaConfigController.d.ts +2 -2
  286. package/lib/types/client/QaSessionController.d.ts +199 -6
  287. package/lib/types/client/QaSurface.d.ts +6 -1
  288. package/lib/types/client/admin/pages/ExpertMemory.d.ts +19 -0
  289. package/lib/types/client/admin/routes.d.ts +3 -1
  290. package/lib/types/client/admin/shared.d.ts +20 -1
  291. package/lib/types/client/components/Markdown.d.ts +8 -1
  292. package/lib/types/client/components/QaHeader.d.ts +3 -1
  293. package/lib/types/client/components/QaModal.d.ts +7 -0
  294. package/lib/types/client/components/QaQueueDock.d.ts +27 -0
  295. package/lib/types/client/components/QaRequestQueue.d.ts +20 -0
  296. package/lib/types/client/components/QaThemeSwitcher.d.ts +14 -0
  297. package/lib/types/client/components/QaTurnNotice.d.ts +55 -0
  298. package/lib/types/client/focus-ring.d.ts +65 -0
  299. package/lib/types/client/lineage.d.ts +3 -13
  300. package/lib/types/client/markdown/blocks.d.ts +30 -2
  301. package/lib/types/client/markdown/render.d.ts +4 -1
  302. package/lib/types/client/notifications/notification-dispatcher.d.ts +68 -0
  303. package/lib/types/client/notifications/preferences.d.ts +44 -0
  304. package/lib/types/client/notifications/turn-completion-source.d.ts +114 -0
  305. package/lib/types/client/notifications/use-turn-notifications.d.ts +51 -0
  306. package/lib/types/client/project-session-state.d.ts +35 -1
  307. package/lib/types/client/rating-harvest.d.ts +49 -0
  308. package/lib/types/client/role/RoleSelector.d.ts +7 -1
  309. package/lib/types/client/settings/card.d.ts +36 -5
  310. package/lib/types/client/settings/fields.d.ts +24 -0
  311. package/lib/types/client/settings/format.d.ts +0 -5
  312. package/lib/types/client/settings/sections/common.d.ts +3 -1
  313. package/lib/types/client/settings/sections/session.d.ts +2 -1
  314. package/lib/types/client/settings/styles.d.ts +9 -4
  315. package/lib/types/client/settings-extensions/user-session.d.ts +1 -1
  316. package/lib/types/client/theme-preference.d.ts +69 -0
  317. package/lib/types/client/types.d.ts +36 -5
  318. package/lib/types/client/use-session-ui-state.d.ts +6 -4
  319. package/lib/types/client/user-settings/GeneralSettingsPage.d.ts +5 -5
  320. package/lib/types/client/user-settings/NotificationSettingsPage.d.ts +40 -0
  321. package/lib/types/client/user-settings/UserSettingsDialog.d.ts +12 -2
  322. package/lib/types/client/user-settings/draft.d.ts +4 -1
  323. package/lib/types/client/user-settings/fields.d.ts +7 -0
  324. package/lib/types/config-resolvers/notifications.d.ts +10 -0
  325. package/lib/types/config-resolvers/session.d.ts +6 -0
  326. package/lib/types/config.d.ts +20 -1
  327. package/lib/types/index.d.ts +70 -10
  328. package/lib/types/integration/answer.d.ts +27 -0
  329. package/lib/types/integration/expert-memory.d.ts +49 -0
  330. package/lib/types/notifications.d.ts +32 -0
  331. package/lib/types/personal-skills/errors.d.ts +1 -1
  332. package/lib/types/personal-skills/service.d.ts +6 -0
  333. package/lib/types/prompt-notes.d.ts +17 -4
  334. package/lib/types/qa-tools/docs-tools.d.ts +2 -2
  335. package/lib/types/request-queue.d.ts +32 -0
  336. package/lib/types/shared/settings.d.ts +19 -2
  337. package/lib/types/turn-failure-log.d.ts +18 -0
  338. package/lib/types/types.d.ts +200 -4
  339. package/package.json +64 -62
  340. package/scripts/attach-workspace-sessions.mjs +24 -5
  341. package/scripts/repair-session-events.mjs +25 -5
@@ -1,9 +1,11 @@
1
1
  import { QaAccountsError } from "../accounts/store.js";
2
2
  import { defaultCapabilityConfig, normalizeUserAccess, resolveSkillAccess, } from "../access/model.js";
3
+ import { expertMemoryReasonOf } from "../integration/expert-memory.js";
3
4
  import { allows } from "./permissions.js";
4
5
  import { aggregateQuality } from "./metrics.js";
5
6
  import { paginate } from "./paging.js";
6
7
  import { deriveQueue } from "./queue.js";
8
+ import { QA_FEEDBACK_RATINGS } from "./quality-store.js";
7
9
  import { defaultAdminRedactor } from "./redaction.js";
8
10
  import { projectTranscript, } from "./conversation-log.js";
9
11
  /**
@@ -22,9 +24,36 @@ export const QA_ADMIN_PAGE_MAX = 100;
22
24
  * says so; moving these counters onto a real index is the next iteration.
23
25
  */
24
26
  export const QA_ADMIN_SCAN_LIMIT = 200;
25
- /** How long one projected log may serve the list and metrics before re-reading. */
27
+ /**
28
+ * How long one projected log may serve the list and metrics before it is read
29
+ * again — and only while the Harness still holds that conversation. A log no
30
+ * process holds cannot gain events, so re-reading it is pure cost: on a
31
+ * deployment with a durable store one read pays for a listing of every stored
32
+ * session and a replay of the whole log before it reaches the one conversation
33
+ * asked for. Reading the scan window again on every page load is what took the
34
+ * aggregate pages minutes; keeping what cannot have changed is what makes the
35
+ * next load free.
36
+ */
26
37
  const TRANSCRIPT_TTL_MS = 30_000;
27
- const TRANSCRIPT_CACHE_MAX = 128;
38
+ /**
39
+ * How many projections the cache holds. It has to cover one scan window with
40
+ * room for a page of summaries: a cache narrower than the window evicts the
41
+ * conversations a pass has already read, and the next pass pays for them again.
42
+ */
43
+ const TRANSCRIPT_CACHE_MAX = QA_ADMIN_SCAN_LIMIT + QA_ADMIN_PAGE_MAX;
44
+ /**
45
+ * What one harvest request may carry. The browser replays its ratings in
46
+ * batches far below this; the ceiling is what a caller that ignored them is
47
+ * refused outright, rather than half-applied.
48
+ */
49
+ const MAX_HARVEST_BATCH = 500;
50
+ /** One harvested identifier, or undefined when the entry cannot be filed. */
51
+ function harvestId(value) {
52
+ if (typeof value !== "string")
53
+ return undefined;
54
+ const trimmed = value.trim();
55
+ return trimmed === "" ? undefined : trimmed;
56
+ }
28
57
  /**
29
58
  * Every session one conversation owns: the chat itself and everything
30
59
  * delegated from it, transitively. A subagent's session is part of the answer
@@ -87,9 +116,26 @@ function reviewStatusOf(reviews, conversationId) {
87
116
  ? "needs_followup"
88
117
  : "reviewed";
89
118
  }
119
+ /**
120
+ * Whether a running pass has no caller left to answer. A caller that passed no
121
+ * cancellation cannot leave, so it keeps the pass alive.
122
+ */
123
+ function nobodyWaits(waiting) {
124
+ for (const signal of waiting) {
125
+ if (signal === undefined || !signal.aborted)
126
+ return false;
127
+ }
128
+ return true;
129
+ }
130
+ function sameWindow(left, right) {
131
+ return (left.length === right.length &&
132
+ left.every((id, index) => id === right[index]));
133
+ }
90
134
  export class QaAdminService {
91
135
  options;
92
136
  redactor;
137
+ /** The aggregate pass running now, shared by the callers waiting for it. */
138
+ scanning;
93
139
  transcripts = new Map();
94
140
  constructor(options) {
95
141
  this.options = options;
@@ -176,30 +222,116 @@ export class QaAdminService {
176
222
  })
177
223
  .filter((row) => row !== undefined);
178
224
  }
179
- /** One conversation's projected log, cached for the TTL unless asked fresh. */
225
+ /**
226
+ * One conversation's projected log, held across calls.
227
+ *
228
+ * A log the Harness no longer holds is finished — nothing can append to it —
229
+ * so its projection stays valid until the cache evicts it or the conversation
230
+ * is deleted. Only a log this process is still writing is re-read once the TTL
231
+ * expires. A cache hit counts as a use: an aggregate pass walks the whole
232
+ * window, and evicting by read time would drop the conversations the pages ask
233
+ * for most often and then read them again.
234
+ *
235
+ * The one answer this can hold back is a conversation resumed and finished
236
+ * between two page loads: its counters lag until the log is read for another
237
+ * reason. The conversation itself is never affected — the detail view always
238
+ * reads the log — and what the aggregate pages show instead is the newest
239
+ * projection of every finished conversation, which is the cheaper lie.
240
+ */
180
241
  async transcript(conversationId, fresh = false) {
181
242
  const cached = this.transcripts.get(conversationId);
182
243
  const now = this.options.clock?.now() ?? Date.now();
183
- if (!fresh && cached !== undefined && now - cached.at < TRANSCRIPT_TTL_MS) {
184
- return { ok: true, project: cached.project };
244
+ if (cached !== undefined) {
245
+ const stale = fresh ||
246
+ (this.options.sessionLog.live(conversationId) &&
247
+ now - cached.at >= TRANSCRIPT_TTL_MS);
248
+ if (!stale) {
249
+ this.retain(conversationId, cached);
250
+ return { ok: true, project: cached.project };
251
+ }
252
+ this.transcripts.delete(conversationId);
185
253
  }
186
254
  const result = await this.options.sessionLog.read(conversationId);
187
255
  if (!result.ok) {
188
- this.transcripts.delete(conversationId);
189
256
  return { ok: false, reason: result.reason };
190
257
  }
191
258
  const project = projectTranscript(result.events, this.redactor);
192
- this.transcripts.set(conversationId, { at: now, project });
193
- if (this.transcripts.size > TRANSCRIPT_CACHE_MAX) {
194
- const oldest = [...this.transcripts.entries()].sort((left, right) => left[1].at - right[1].at)[0];
195
- if (oldest !== undefined)
196
- this.transcripts.delete(oldest[0]);
197
- }
259
+ this.retain(conversationId, {
260
+ at: this.options.clock?.now() ?? Date.now(),
261
+ project,
262
+ });
198
263
  return { ok: true, project };
199
264
  }
200
- async facts(rows) {
265
+ /** Insert or re-touch one projection, keeping the cache inside its budget. */
266
+ retain(conversationId, entry) {
267
+ this.transcripts.delete(conversationId);
268
+ this.transcripts.set(conversationId, entry);
269
+ while (this.transcripts.size > TRANSCRIPT_CACHE_MAX) {
270
+ const oldest = this.transcripts.keys().next();
271
+ if (oldest.done === true)
272
+ break;
273
+ this.transcripts.delete(oldest.value);
274
+ }
275
+ }
276
+ /**
277
+ * The aggregate rows of the newest scan window, read in one pass.
278
+ *
279
+ * The message counts and the tool failures come from the same projections, so
280
+ * one walk fills both. While a walk runs, a caller that wants the same window
281
+ * joins it rather than starting a second scan of the deployment's logs, and the
282
+ * walk stops once no caller waits for it: a scan nobody is reading is only ever
283
+ * a load on the store.
284
+ */
285
+ async scanOf(rows, signal) {
286
+ signal?.throwIfAborted();
287
+ const window = [...rows]
288
+ .sort((left, right) => right.createdAt - left.createdAt)
289
+ .slice(0, QA_ADMIN_SCAN_LIMIT);
290
+ const ids = window.map((row) => row.conversationId);
291
+ const running = this.scanning;
292
+ if (running !== undefined &&
293
+ !nobodyWaits(running.waiting) &&
294
+ sameWindow(running.window, ids)) {
295
+ running.waiting.add(signal);
296
+ try {
297
+ return await running.promise;
298
+ }
299
+ finally {
300
+ running.waiting.delete(signal);
301
+ }
302
+ }
201
303
  const facts = [];
202
- for (const row of rows) {
304
+ const failures = [];
305
+ const waiting = new Set([signal]);
306
+ const promise = this.walk(window, waiting, facts, failures);
307
+ const pass = { window: ids, promise, waiting };
308
+ this.scanning = pass;
309
+ void promise
310
+ .catch(() => undefined)
311
+ .then(() => {
312
+ if (this.scanning === pass)
313
+ this.scanning = undefined;
314
+ });
315
+ try {
316
+ return await promise;
317
+ }
318
+ finally {
319
+ waiting.delete(signal);
320
+ }
321
+ }
322
+ /**
323
+ * Walk one scan window, filling the counts and the failure signals as it goes.
324
+ *
325
+ * `waiting` is the caller set of the pass this walk belongs to: the walk gives
326
+ * up between two reads once every caller has left, so a reviewer who abandons a
327
+ * page that has not answered does not keep the deployment reading logs for an
328
+ * answer nobody will see.
329
+ */
330
+ async walk(window, waiting, facts, failures) {
331
+ for (const row of window) {
332
+ if (nobodyWaits(waiting)) {
333
+ throw new Error("qa admin aggregate scan abandoned: no caller left");
334
+ }
203
335
  const transcript = await this.transcript(row.conversationId);
204
336
  facts.push({
205
337
  conversationId: row.conversationId,
@@ -210,18 +342,21 @@ export class QaAdminService {
210
342
  : 0,
211
343
  createdAt: row.createdAt,
212
344
  });
345
+ if (transcript.ok) {
346
+ failures.push(...this.failuresOf(row.conversationId, transcript.project));
347
+ }
213
348
  }
214
- return facts;
349
+ return { facts, failures };
215
350
  }
216
351
  // -------------------------------------------------------------------------
217
352
  // Overview and metrics
218
353
  // -------------------------------------------------------------------------
219
- async overview(token) {
354
+ async overview(token, signal) {
220
355
  this.require(token, "conversations.read.all");
221
356
  const rows = await this.index();
222
- const metrics = await this.metricsOf(rows);
357
+ const metrics = await this.metricsOf(rows, signal);
223
358
  const fresh = [...this.options.quality().allFeedback()].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
224
- const queue = await this.queueRows(rows);
359
+ const queue = await this.queueRows(rows, signal);
225
360
  return Object.freeze({
226
361
  metrics,
227
362
  alerts: this.alertsOf(metrics),
@@ -229,19 +364,16 @@ export class QaAdminService {
229
364
  queue: queue.slice(0, 5),
230
365
  });
231
366
  }
232
- async metrics(token) {
367
+ async metrics(token, signal) {
233
368
  this.require(token, "analytics.read");
234
- return this.metricsOf(await this.index());
369
+ return this.metricsOf(await this.index(), signal);
235
370
  }
236
- async metricsOf(rows) {
371
+ async metricsOf(rows, signal) {
237
372
  const quality = this.options.quality();
238
373
  // Feedback and reviews are exact; the message counts come from the bounded
239
374
  // log scan and are labelled as such where they are shown.
240
- const scanned = [...rows]
241
- .sort((left, right) => right.createdAt - left.createdAt)
242
- .slice(0, QA_ADMIN_SCAN_LIMIT);
243
375
  return aggregateQuality({
244
- conversations: await this.facts(scanned),
376
+ conversations: (await this.scanOf(rows, signal)).facts,
245
377
  feedback: quality.allFeedback(),
246
378
  reviews: quality.allReviews(),
247
379
  });
@@ -534,8 +666,9 @@ export class QaAdminService {
534
666
  // -------------------------------------------------------------------------
535
667
  // Conversations
536
668
  // -------------------------------------------------------------------------
537
- async conversations(token, query, cursor, limit) {
669
+ async conversations(token, query, cursor, limit, signal) {
538
670
  this.require(token, "conversations.read.all");
671
+ signal?.throwIfAborted();
539
672
  const rows = await this.index();
540
673
  const quality = this.options.quality();
541
674
  const feedback = quality.allFeedback();
@@ -545,17 +678,23 @@ export class QaAdminService {
545
678
  const ratingFilter = given(query.rating);
546
679
  const reviewFilter = given(query.reviewStatus);
547
680
  const search = given(query.search) ?? "";
681
+ const searching = search.trim() !== "";
548
682
  const from = epochOf(given(query.from) ?? undefined);
549
683
  const to = epochOf(given(query.to) ?? undefined);
550
684
  // A search may name a title, which only the log knows, so the candidate
551
- // set is bounded by the newest logs before the text is available.
552
- const searchable = search.trim() === ""
553
- ? rows
554
- : [...rows]
685
+ // set is bounded by the newest logs before the text is available. Without
686
+ // search text no filter here needs a log at all — and reading one per
687
+ // candidate to answer a page of twenty-five rows made this call cost every
688
+ // conversation the deployment stores, each read paying for a listing of all
689
+ // of them before it reaches the one log asked for.
690
+ const searchable = searching
691
+ ? [...rows]
555
692
  .sort((left, right) => right.createdAt - left.createdAt)
556
- .slice(0, QA_ADMIN_SCAN_LIMIT);
693
+ .slice(0, QA_ADMIN_SCAN_LIMIT)
694
+ : rows;
557
695
  const candidates = [];
558
696
  for (const row of searchable) {
697
+ signal?.throwIfAborted();
559
698
  if (userIdFilter !== undefined && row.userId !== userIdFilter)
560
699
  continue;
561
700
  if (subroleFilter !== undefined && row.subroleId !== subroleFilter) {
@@ -574,10 +713,13 @@ export class QaAdminService {
574
713
  reviewStatusOf(reviews, row.conversationId) !== reviewFilter) {
575
714
  continue;
576
715
  }
577
- // One projection per candidate, reused by the search match and then by
578
- // the summary of a page row: the read is cached either way.
579
- const transcript = await this.transcript(row.conversationId);
580
- const title = transcript.ok ? transcript.project.title : undefined;
716
+ // Only a search reads a candidate's log, and the projection stays cached
717
+ // for the summary of the rows that survive the filter.
718
+ let title;
719
+ if (searching) {
720
+ const transcript = await this.transcript(row.conversationId);
721
+ title = transcript.ok ? transcript.project.title : undefined;
722
+ }
581
723
  if (!matches(search, row.displayName, row.email, row.userId, row.conversationId, title)) {
582
724
  continue;
583
725
  }
@@ -593,6 +735,7 @@ export class QaAdminService {
593
735
  });
594
736
  const items = [];
595
737
  for (const row of page.items) {
738
+ signal?.throwIfAborted();
596
739
  items.push(await this.summaryOf(row, feedback, reviews));
597
740
  }
598
741
  return {
@@ -741,7 +884,10 @@ export class QaAdminService {
741
884
  const qualityRows = this.options.quality().dropConversations(targets);
742
885
  for (const sessionId of targets)
743
886
  this.options.dropSources?.(sessionId);
744
- this.transcripts.delete(conversationId);
887
+ // A removed log is never read again, so its projection must not survive to
888
+ // answer a later page: the cache holds it as if the conversation existed.
889
+ for (const sessionId of targets)
890
+ this.transcripts.delete(sessionId);
745
891
  this.options.quality().appendAudit({
746
892
  actorId: actor.id,
747
893
  action: "conversation.deleted",
@@ -783,16 +929,6 @@ export class QaAdminService {
783
929
  }
784
930
  return signals;
785
931
  }
786
- async failureSignals(rows) {
787
- const signals = [];
788
- for (const row of rows) {
789
- const transcript = await this.transcript(row.conversationId);
790
- if (!transcript.ok)
791
- continue;
792
- signals.push(...this.failuresOf(row.conversationId, transcript.project));
793
- }
794
- return signals;
795
- }
796
932
  // -------------------------------------------------------------------------
797
933
  // Feedback
798
934
  // -------------------------------------------------------------------------
@@ -817,6 +953,74 @@ export class QaAdminService {
817
953
  });
818
954
  return record;
819
955
  }
956
+ /**
957
+ * Replay the ratings a browser still holds from the window in which a thumbs
958
+ * never reached the Host.
959
+ *
960
+ * A harvest never overwrites. The browser's map stores no timestamp, so an
961
+ * entry may well be a rating the same user has since filed together with a
962
+ * reason and a comment; replacing that record with the bare thumbs would
963
+ * delete exactly what a reviewer reads. An entry the Host already answers for
964
+ * is counted as present and left alone.
965
+ *
966
+ * One unusable or foreign entry costs that entry alone: a browser keeps
967
+ * ratings of chats it no longer owns, and the rest of the replay must not be
968
+ * lost because of them.
969
+ */
970
+ harvestFeedback(token, entries) {
971
+ const { accounts, actor } = this.require(token, "conversations.read.own");
972
+ if (!Array.isArray(entries)) {
973
+ throw new TypeError("entries must be an array");
974
+ }
975
+ if (entries.length > MAX_HARVEST_BATCH) {
976
+ throw new QaAccountsError("auth-required", "harvest batch is too large");
977
+ }
978
+ const quality = this.options.quality();
979
+ let recorded = 0;
980
+ let present = 0;
981
+ let rejected = 0;
982
+ for (const entry of entries) {
983
+ const conversationId = harvestId(entry?.conversationId);
984
+ const messageId = harvestId(entry?.messageId);
985
+ const rating = entry?.rating;
986
+ if (conversationId === undefined ||
987
+ messageId === undefined ||
988
+ !QA_FEEDBACK_RATINGS.includes(rating)) {
989
+ rejected += 1;
990
+ continue;
991
+ }
992
+ if (accounts.ownerIdOf(conversationId) !== actor.id) {
993
+ rejected += 1;
994
+ continue;
995
+ }
996
+ if (quality.feedbackOf(conversationId, messageId, actor.id) !== undefined) {
997
+ present += 1;
998
+ continue;
999
+ }
1000
+ try {
1001
+ quality.rateFeedback({ conversationId, messageId, userId: actor.id }, { rating });
1002
+ recorded += 1;
1003
+ }
1004
+ catch (error) {
1005
+ // The store re-validates what it is given, and one row it refuses is
1006
+ // that row's problem rather than the batch's.
1007
+ this.options.logger.warn("admin.feedback-harvest-entry-refused", {
1008
+ conversationId,
1009
+ messageId,
1010
+ userId: actor.id,
1011
+ reason: error instanceof Error ? error.message : String(error),
1012
+ });
1013
+ rejected += 1;
1014
+ }
1015
+ }
1016
+ this.options.logger.info("admin.feedback-harvested", {
1017
+ userId: actor.id,
1018
+ recorded,
1019
+ present,
1020
+ rejected,
1021
+ });
1022
+ return { recorded, present, rejected };
1023
+ }
820
1024
  async feedback(token, query, cursor, limit) {
821
1025
  this.require(token, "reviews.read");
822
1026
  const rows = await this.index();
@@ -893,7 +1097,7 @@ export class QaAdminService {
893
1097
  // -------------------------------------------------------------------------
894
1098
  // Review queue and results
895
1099
  // -------------------------------------------------------------------------
896
- async queueRows(rows) {
1100
+ async queueRows(rows, signal) {
897
1101
  const conversations = rows ?? (await this.index());
898
1102
  const quality = this.options.quality();
899
1103
  const reviews = quality.allReviews();
@@ -902,12 +1106,11 @@ export class QaAdminService {
902
1106
  feedback: quality.allFeedback(),
903
1107
  reviews,
904
1108
  manual: quality.manualQueue(),
905
- failures: await this.failureSignals([...conversations]
906
- .sort((left, right) => right.createdAt - left.createdAt)
907
- .slice(0, QA_ADMIN_SCAN_LIMIT)),
1109
+ failures: (await this.scanOf(conversations, signal)).failures,
908
1110
  });
909
1111
  const result = [];
910
1112
  for (const item of queue) {
1113
+ signal?.throwIfAborted();
911
1114
  const row = byId.get(item.conversationId);
912
1115
  if (row === undefined)
913
1116
  continue;
@@ -931,9 +1134,9 @@ export class QaAdminService {
931
1134
  }
932
1135
  return Object.freeze(result);
933
1136
  }
934
- async reviewQueue(token, cursor, limit) {
1137
+ async reviewQueue(token, cursor, limit, signal) {
935
1138
  this.require(token, "reviews.read");
936
- const rows = await this.queueRows();
1139
+ const rows = await this.queueRows(await this.index(), signal);
937
1140
  return paginate({
938
1141
  rows,
939
1142
  keyOf: (row) => `${row.priority}:${row.raisedAt}`,
@@ -1134,6 +1337,183 @@ export class QaAdminService {
1134
1337
  return this.options.skills().get(target.scope, name);
1135
1338
  }
1136
1339
  // -------------------------------------------------------------------------
1340
+ // Expert memory
1341
+ // -------------------------------------------------------------------------
1342
+ /**
1343
+ * The domain-experts maintenance seam, or the refusal that says this stand
1344
+ * runs no experts.
1345
+ *
1346
+ * Reach is host-plane on purpose: the sibling plugin's remotes carry no token
1347
+ * and check no permission, because inside the Harness their caller is already
1348
+ * a trusted component. This console answers a browser on a LAN port, so it
1349
+ * calls the service directly and gates every call with its own `require`.
1350
+ */
1351
+ expertMemory() {
1352
+ const admin = this.options.domainExperts?.()?.memoryAdmin;
1353
+ if (admin === undefined) {
1354
+ throw new QaAccountsError("memory-unavailable", "no domain-experts plugin is composed on this deployment");
1355
+ }
1356
+ return admin;
1357
+ }
1358
+ /**
1359
+ * Run one memory call, translating the sibling plugin's refusal.
1360
+ *
1361
+ * The reason crosses the wire, never the message: a memory refusal names
1362
+ * namespaces, and a storage failure names the database file behind them.
1363
+ */
1364
+ async memoryCall(action, operation) {
1365
+ const admin = this.expertMemory();
1366
+ try {
1367
+ return await operation(admin);
1368
+ }
1369
+ catch (error) {
1370
+ if (error instanceof QaAccountsError)
1371
+ throw error;
1372
+ const reason = expertMemoryReasonOf(error);
1373
+ this.options.logger.warn("admin.memory-refused", {
1374
+ action,
1375
+ reason: reason ?? "unknown",
1376
+ });
1377
+ if (reason === undefined)
1378
+ throw error;
1379
+ throw new QaAccountsError(reason, `expert memory refused the ${action}`);
1380
+ }
1381
+ }
1382
+ /**
1383
+ * What an audit row keeps about one record.
1384
+ *
1385
+ * A remembered line runs to a few pages, and the trail holds a capped JSON
1386
+ * snapshot; the identity of the record and the head of its text is what an
1387
+ * operator reads back to decide whether the deletion was wrong.
1388
+ */
1389
+ memoryImage(record) {
1390
+ return {
1391
+ namespace: record.namespace,
1392
+ key: record.key,
1393
+ tags: record.tags,
1394
+ updatedAt: record.updatedAt,
1395
+ text: record.text.slice(0, 200),
1396
+ };
1397
+ }
1398
+ /** The record one key holds now, if it still holds one. */
1399
+ async rememberedRecord(admin, namespace, key) {
1400
+ const page = await admin.search(namespace, key, QA_ADMIN_PAGE_MAX, 0);
1401
+ return page.records.find((record) => record.key === key);
1402
+ }
1403
+ /** Every expert memory namespace on the stand, with its owner and size. */
1404
+ async memoryScopes(token) {
1405
+ this.require(token, "memory.read");
1406
+ return await this.memoryCall("scopes", (admin) => admin.scopes());
1407
+ }
1408
+ /** One page of one namespace, newest first, with the filter's match count. */
1409
+ async memoryRecords(token, namespace, query, limit, offset) {
1410
+ this.require(token, "memory.read");
1411
+ const size = clampLimit(limit === null ? undefined : limit);
1412
+ const from = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
1413
+ const page = await this.memoryCall("search", (admin) => admin.search(namespace, query, size, from));
1414
+ return {
1415
+ records: page.records,
1416
+ total: page.total,
1417
+ offset: from,
1418
+ limit: size,
1419
+ };
1420
+ }
1421
+ /**
1422
+ * Rewrite one remembered line.
1423
+ *
1424
+ * The key is not editable: it is what the expert's own notes and the audit
1425
+ * trail point at, and a console that renamed it would orphan both. The write
1426
+ * refuses a record that vanished meanwhile rather than re-creating it — an
1427
+ * operator editing a stale row should reload, not resurrect.
1428
+ */
1429
+ async correctMemory(token, namespace, key, draft) {
1430
+ const { actor } = this.require(token, "memory.manage");
1431
+ const before = await this.memoryCall("read-before", (admin) => this.rememberedRecord(admin, namespace, key));
1432
+ const record = await this.memoryCall("correct", (admin) => admin.correct({ namespace, key, text: draft.text, tags: draft.tags }));
1433
+ this.options.quality().appendAudit({
1434
+ actorId: actor.id,
1435
+ action: "memory.corrected",
1436
+ targetType: "expert-memory",
1437
+ targetId: `${namespace}/${key}`,
1438
+ ...(before === undefined ? {} : { before: this.memoryImage(before) }),
1439
+ after: this.memoryImage(record),
1440
+ });
1441
+ this.options.logger.info("admin.memory-corrected", {
1442
+ actor: actor.id,
1443
+ namespace,
1444
+ key,
1445
+ });
1446
+ return record;
1447
+ }
1448
+ /**
1449
+ * Drop one or several remembered lines.
1450
+ *
1451
+ * A single deletion keeps the text it removed; a bulk one keeps only the
1452
+ * count and the keys, because an audit trail that copies every page of memory
1453
+ * it deletes becomes the thing an operator has to clean up.
1454
+ */
1455
+ async forgetMemory(token, namespace, keys) {
1456
+ const { actor } = this.require(token, "memory.manage");
1457
+ const wanted = [
1458
+ ...new Set(keys.map((key) => key.trim()).filter((key) => key !== "")),
1459
+ ];
1460
+ if (wanted.length === 0) {
1461
+ throw new QaAccountsError("invalid-memory", "a memory deletion needs at least one record key");
1462
+ }
1463
+ const sole = wanted.length === 1 ? wanted[0] : undefined;
1464
+ const before = sole === undefined
1465
+ ? undefined
1466
+ : await this.memoryCall("read-before", (admin) => this.rememberedRecord(admin, namespace, sole));
1467
+ const removed = await this.memoryCall("forget", async (admin) => sole === undefined
1468
+ ? await admin.removeMany(namespace, wanted)
1469
+ : (await admin.remove(namespace, sole))
1470
+ ? 1
1471
+ : 0);
1472
+ this.options.quality().appendAudit({
1473
+ actorId: actor.id,
1474
+ action: "memory.deleted",
1475
+ targetType: "expert-memory",
1476
+ targetId: namespace,
1477
+ before: before === undefined
1478
+ ? { keys: wanted, count: wanted.length }
1479
+ : this.memoryImage(before),
1480
+ });
1481
+ this.options.logger.info("admin.memory-deleted", {
1482
+ actor: actor.id,
1483
+ namespace,
1484
+ named: wanted.length,
1485
+ removed,
1486
+ });
1487
+ return removed;
1488
+ }
1489
+ /** Empty one expert's private namespace. */
1490
+ async wipeMemory(token, namespace, expectedRecords) {
1491
+ const { actor } = this.require(token, "memory.manage");
1492
+ const page = await this.memoryCall("read-before", (admin) => admin.search(namespace, "", 1, 0));
1493
+ if (expectedRecords !== null &&
1494
+ Number.isFinite(expectedRecords) &&
1495
+ expectedRecords >= 0 &&
1496
+ page.total !== expectedRecords) {
1497
+ // The console asks for the count it showed. Memory an expert wrote while
1498
+ // the operator was deciding is not memory the operator agreed to erase.
1499
+ throw new QaAccountsError("memory-record-unknown", "the namespace holds a different number of records than the one confirmed");
1500
+ }
1501
+ const cleared = await this.memoryCall("wipe", (current) => current.wipe(namespace));
1502
+ this.options.quality().appendAudit({
1503
+ actorId: actor.id,
1504
+ action: "memory.wiped",
1505
+ targetType: "expert-memory",
1506
+ targetId: namespace,
1507
+ after: { cleared },
1508
+ });
1509
+ this.options.logger.info("admin.memory-wiped", {
1510
+ actor: actor.id,
1511
+ namespace,
1512
+ cleared,
1513
+ });
1514
+ return cleared;
1515
+ }
1516
+ // -------------------------------------------------------------------------
1137
1517
  // Audit
1138
1518
  // -------------------------------------------------------------------------
1139
1519
  /**