@qvac/core 0.0.0 → 0.1.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 (586) hide show
  1. package/LICENSE.md +198 -0
  2. package/NOTICE.md +13 -0
  3. package/README.md +694 -0
  4. package/SECURITY.md +6 -0
  5. package/dist/assistant.bundle +47256 -0
  6. package/dist/assistant.cjs +18 -0
  7. package/dist/assistant.d.ts +8 -0
  8. package/dist/prebuilds-manifest.json +380 -0
  9. package/dist-lib/client.d.ts +2 -0
  10. package/dist-lib/client.js +1 -0
  11. package/dist-lib/index.d.ts +13 -0
  12. package/dist-lib/index.js +10 -0
  13. package/dist-lib/lib/agent-icons.d.ts +3 -0
  14. package/dist-lib/lib/agent-icons.js +1 -0
  15. package/dist-lib/lib/api/agents.d.ts +5 -0
  16. package/dist-lib/lib/api/agents.js +38 -0
  17. package/dist-lib/lib/api/app-config.d.ts +5 -0
  18. package/dist-lib/lib/api/app-config.js +108 -0
  19. package/dist-lib/lib/api/approval-rules.d.ts +3 -0
  20. package/dist-lib/lib/api/approval-rules.js +9 -0
  21. package/dist-lib/lib/api/attachments.d.ts +8 -0
  22. package/dist-lib/lib/api/attachments.js +15 -0
  23. package/dist-lib/lib/api/chat-groups.d.ts +5 -0
  24. package/dist-lib/lib/api/chat-groups.js +12 -0
  25. package/dist-lib/lib/api/chat.d.ts +5 -0
  26. package/dist-lib/lib/api/chat.js +38 -0
  27. package/dist-lib/lib/api/chunks.d.ts +5 -0
  28. package/dist-lib/lib/api/chunks.js +61 -0
  29. package/dist-lib/lib/api/config.d.ts +5 -0
  30. package/dist-lib/lib/api/config.js +15 -0
  31. package/dist-lib/lib/api/current-mesh.d.ts +5 -0
  32. package/dist-lib/lib/api/current-mesh.js +12 -0
  33. package/dist-lib/lib/api/custom-models.d.ts +5 -0
  34. package/dist-lib/lib/api/custom-models.js +45 -0
  35. package/dist-lib/lib/api/devices.d.ts +8 -0
  36. package/dist-lib/lib/api/devices.js +125 -0
  37. package/dist-lib/lib/api/diagnostics.d.ts +7 -0
  38. package/dist-lib/lib/api/diagnostics.js +7 -0
  39. package/dist-lib/lib/api/effective-ctx-size.d.ts +2 -0
  40. package/dist-lib/lib/api/effective-ctx-size.js +10 -0
  41. package/dist-lib/lib/api/execution.d.ts +3 -0
  42. package/dist-lib/lib/api/execution.js +45 -0
  43. package/dist-lib/lib/api/files.d.ts +3 -0
  44. package/dist-lib/lib/api/files.js +48 -0
  45. package/dist-lib/lib/api/index.d.ts +62 -0
  46. package/dist-lib/lib/api/index.js +172 -0
  47. package/dist-lib/lib/api/instrument.d.ts +2 -0
  48. package/dist-lib/lib/api/instrument.js +86 -0
  49. package/dist-lib/lib/api/knowledge.d.ts +8 -0
  50. package/dist-lib/lib/api/knowledge.js +26 -0
  51. package/dist-lib/lib/api/logs.d.ts +14 -0
  52. package/dist-lib/lib/api/logs.js +116 -0
  53. package/dist-lib/lib/api/mesh.d.ts +13 -0
  54. package/dist-lib/lib/api/mesh.js +25 -0
  55. package/dist-lib/lib/api/model-download.d.ts +5 -0
  56. package/dist-lib/lib/api/model-download.js +47 -0
  57. package/dist-lib/lib/api/network.d.ts +8 -0
  58. package/dist-lib/lib/api/network.js +6 -0
  59. package/dist-lib/lib/api/notifications.d.ts +21 -0
  60. package/dist-lib/lib/api/notifications.js +108 -0
  61. package/dist-lib/lib/api/rag.d.ts +7 -0
  62. package/dist-lib/lib/api/rag.js +216 -0
  63. package/dist-lib/lib/api/sync.d.ts +3 -0
  64. package/dist-lib/lib/api/sync.js +6 -0
  65. package/dist-lib/lib/api/tool-activity.d.ts +5 -0
  66. package/dist-lib/lib/api/tool-activity.js +53 -0
  67. package/dist-lib/lib/api/tool-configs.d.ts +6 -0
  68. package/dist-lib/lib/api/tool-configs.js +27 -0
  69. package/dist-lib/lib/api/watchable.d.ts +12 -0
  70. package/dist-lib/lib/api/watchable.js +121 -0
  71. package/dist-lib/lib/assistant-client.d.ts +21 -0
  72. package/dist-lib/lib/assistant-client.js +52 -0
  73. package/dist-lib/lib/assistant-tree.d.ts +50 -0
  74. package/dist-lib/lib/assistant-tree.js +184 -0
  75. package/dist-lib/lib/assistant.d.ts +425 -0
  76. package/dist-lib/lib/assistant.js +2706 -0
  77. package/dist-lib/lib/audio-format.d.ts +4 -0
  78. package/dist-lib/lib/audio-format.js +4 -0
  79. package/dist-lib/lib/chat-title.d.ts +5 -0
  80. package/dist-lib/lib/chat-title.js +58 -0
  81. package/dist-lib/lib/client.d.ts +25 -0
  82. package/dist-lib/lib/client.js +53 -0
  83. package/dist-lib/lib/collabora/convert.d.ts +9 -0
  84. package/dist-lib/lib/collabora/convert.js +62 -0
  85. package/dist-lib/lib/collabora/entry.d.ts +1 -0
  86. package/dist-lib/lib/collabora/entry.js +44 -0
  87. package/dist-lib/lib/collabora/protocol.d.ts +15 -0
  88. package/dist-lib/lib/collabora/protocol.js +24 -0
  89. package/dist-lib/lib/collabora/worker.d.ts +5 -0
  90. package/dist-lib/lib/collabora/worker.js +111 -0
  91. package/dist-lib/lib/compaction.d.ts +30 -0
  92. package/dist-lib/lib/compaction.js +95 -0
  93. package/dist-lib/lib/constants.d.ts +4 -0
  94. package/dist-lib/lib/constants.js +4 -0
  95. package/dist-lib/lib/core/agent.d.ts +15 -0
  96. package/dist-lib/lib/core/agent.js +149 -0
  97. package/dist-lib/lib/core/app-build.d.ts +7 -0
  98. package/dist-lib/lib/core/app-build.js +1 -0
  99. package/dist-lib/lib/core/approval-rules.d.ts +15 -0
  100. package/dist-lib/lib/core/approval-rules.js +41 -0
  101. package/dist-lib/lib/core/async-iterable.d.ts +13 -0
  102. package/dist-lib/lib/core/async-iterable.js +65 -0
  103. package/dist-lib/lib/core/attachment-link.d.ts +7 -0
  104. package/dist-lib/lib/core/attachment-link.js +6 -0
  105. package/dist-lib/lib/core/audio-attachment.d.ts +1 -0
  106. package/dist-lib/lib/core/audio-attachment.js +4 -0
  107. package/dist-lib/lib/core/blind-peers.d.ts +6 -0
  108. package/dist-lib/lib/core/blind-peers.js +13 -0
  109. package/dist-lib/lib/core/chat-activity.d.ts +7 -0
  110. package/dist-lib/lib/core/chat-activity.js +244 -0
  111. package/dist-lib/lib/core/chat-group.d.ts +14 -0
  112. package/dist-lib/lib/core/chat-group.js +79 -0
  113. package/dist-lib/lib/core/chat-search.d.ts +17 -0
  114. package/dist-lib/lib/core/chat-search.js +199 -0
  115. package/dist-lib/lib/core/chat.d.ts +37 -0
  116. package/dist-lib/lib/core/chat.js +196 -0
  117. package/dist-lib/lib/core/chunk.d.ts +61 -0
  118. package/dist-lib/lib/core/chunk.js +212 -0
  119. package/dist-lib/lib/core/contract/activity.d.ts +11 -0
  120. package/dist-lib/lib/core/contract/activity.js +45 -0
  121. package/dist-lib/lib/core/contract/agents.d.ts +2 -0
  122. package/dist-lib/lib/core/contract/agents.js +29 -0
  123. package/dist-lib/lib/core/contract/approval-rules.d.ts +2 -0
  124. package/dist-lib/lib/core/contract/approval-rules.js +14 -0
  125. package/dist-lib/lib/core/contract/attestations.d.ts +2 -0
  126. package/dist-lib/lib/core/contract/attestations.js +14 -0
  127. package/dist-lib/lib/core/contract/chat-group.d.ts +2 -0
  128. package/dist-lib/lib/core/contract/chat-group.js +20 -0
  129. package/dist-lib/lib/core/contract/chat.d.ts +2 -0
  130. package/dist-lib/lib/core/contract/chat.js +18 -0
  131. package/dist-lib/lib/core/contract/chunks.d.ts +2 -0
  132. package/dist-lib/lib/core/contract/chunks.js +156 -0
  133. package/dist-lib/lib/core/contract/devices.d.ts +2 -0
  134. package/dist-lib/lib/core/contract/devices.js +110 -0
  135. package/dist-lib/lib/core/contract/index.d.ts +2 -0
  136. package/dist-lib/lib/core/contract/index.js +30 -0
  137. package/dist-lib/lib/core/contract/indexing.d.ts +4 -0
  138. package/dist-lib/lib/core/contract/indexing.js +82 -0
  139. package/dist-lib/lib/core/contract/knowledge.d.ts +2 -0
  140. package/dist-lib/lib/core/contract/knowledge.js +55 -0
  141. package/dist-lib/lib/core/contract/members.d.ts +2 -0
  142. package/dist-lib/lib/core/contract/members.js +96 -0
  143. package/dist-lib/lib/core/contract/seed-defaults.d.ts +2 -0
  144. package/dist-lib/lib/core/contract/seed-defaults.js +29 -0
  145. package/dist-lib/lib/core/contract/tool-activity.d.ts +2 -0
  146. package/dist-lib/lib/core/contract/tool-activity.js +25 -0
  147. package/dist-lib/lib/core/contract/tool-configs.d.ts +2 -0
  148. package/dist-lib/lib/core/contract/tool-configs.js +50 -0
  149. package/dist-lib/lib/core/contract/types.d.ts +11 -0
  150. package/dist-lib/lib/core/contract/types.js +1 -0
  151. package/dist-lib/lib/core/contract/writers.d.ts +2 -0
  152. package/dist-lib/lib/core/contract/writers.js +112 -0
  153. package/dist-lib/lib/core/cpu-info-node.d.ts +3 -0
  154. package/dist-lib/lib/core/cpu-info-node.js +2 -0
  155. package/dist-lib/lib/core/departures.d.ts +41 -0
  156. package/dist-lib/lib/core/departures.js +145 -0
  157. package/dist-lib/lib/core/device-attestation.d.ts +6 -0
  158. package/dist-lib/lib/core/device-attestation.js +12 -0
  159. package/dist-lib/lib/core/devices.d.ts +14 -0
  160. package/dist-lib/lib/core/devices.js +11 -0
  161. package/dist-lib/lib/core/document-attachment.d.ts +22 -0
  162. package/dist-lib/lib/core/document-attachment.js +35 -0
  163. package/dist-lib/lib/core/files/attachments.d.ts +8 -0
  164. package/dist-lib/lib/core/files/attachments.js +50 -0
  165. package/dist-lib/lib/core/files/index.d.ts +133 -0
  166. package/dist-lib/lib/core/files/index.js +585 -0
  167. package/dist-lib/lib/core/files/knowledge.d.ts +9 -0
  168. package/dist-lib/lib/core/files/knowledge.js +31 -0
  169. package/dist-lib/lib/core/files/preview/image.d.ts +4 -0
  170. package/dist-lib/lib/core/files/preview/image.js +42 -0
  171. package/dist-lib/lib/core/files/preview/index.d.ts +33 -0
  172. package/dist-lib/lib/core/files/preview/index.js +161 -0
  173. package/dist-lib/lib/core/files/processing/claims.d.ts +26 -0
  174. package/dist-lib/lib/core/files/processing/claims.js +89 -0
  175. package/dist-lib/lib/core/files/processing/document.d.ts +100 -0
  176. package/dist-lib/lib/core/files/processing/document.js +101 -0
  177. package/dist-lib/lib/core/files/processing/index.d.ts +75 -0
  178. package/dist-lib/lib/core/files/processing/index.js +295 -0
  179. package/dist-lib/lib/core/files/processing/ownership.d.ts +11 -0
  180. package/dist-lib/lib/core/files/processing/ownership.js +41 -0
  181. package/dist-lib/lib/core/files/processing/protocol.d.ts +29 -0
  182. package/dist-lib/lib/core/files/processing/protocol.js +70 -0
  183. package/dist-lib/lib/core/files/processing/settle.d.ts +10 -0
  184. package/dist-lib/lib/core/files/processing/settle.js +75 -0
  185. package/dist-lib/lib/core/files/processing/source.d.ts +36 -0
  186. package/dist-lib/lib/core/files/processing/source.js +96 -0
  187. package/dist-lib/lib/core/files/processing/state.d.ts +64 -0
  188. package/dist-lib/lib/core/files/processing/state.js +118 -0
  189. package/dist-lib/lib/core/files/rag/artifact/codec.d.ts +3 -0
  190. package/dist-lib/lib/core/files/rag/artifact/codec.js +172 -0
  191. package/dist-lib/lib/core/files/rag/artifact/id.d.ts +10 -0
  192. package/dist-lib/lib/core/files/rag/artifact/id.js +39 -0
  193. package/dist-lib/lib/core/files/rag/artifact/types.d.ts +24 -0
  194. package/dist-lib/lib/core/files/rag/artifact/types.js +41 -0
  195. package/dist-lib/lib/core/files/rag/build.d.ts +25 -0
  196. package/dist-lib/lib/core/files/rag/build.js +60 -0
  197. package/dist-lib/lib/core/files/rag/chunk/chunking.d.ts +12 -0
  198. package/dist-lib/lib/core/files/rag/chunk/chunking.js +117 -0
  199. package/dist-lib/lib/core/files/rag/chunk/metadata.d.ts +30 -0
  200. package/dist-lib/lib/core/files/rag/chunk/metadata.js +89 -0
  201. package/dist-lib/lib/core/files/rag/chunk/tokenizer.d.ts +7 -0
  202. package/dist-lib/lib/core/files/rag/chunk/tokenizer.js +18 -0
  203. package/dist-lib/lib/core/files/rag/document-types.d.ts +11 -0
  204. package/dist-lib/lib/core/files/rag/document-types.js +94 -0
  205. package/dist-lib/lib/core/files/rag/extract/csv-text.d.ts +1 -0
  206. package/dist-lib/lib/core/files/rag/extract/csv-text.js +117 -0
  207. package/dist-lib/lib/core/files/rag/extract/document-types.d.ts +22 -0
  208. package/dist-lib/lib/core/files/rag/extract/document-types.js +152 -0
  209. package/dist-lib/lib/core/files/rag/extract/extract.d.ts +17 -0
  210. package/dist-lib/lib/core/files/rag/extract/extract.js +81 -0
  211. package/dist-lib/lib/core/files/rag/extract/html-table-text.d.ts +1 -0
  212. package/dist-lib/lib/core/files/rag/extract/html-table-text.js +125 -0
  213. package/dist-lib/lib/core/files/rag/extract/html-text.d.ts +1 -0
  214. package/dist-lib/lib/core/files/rag/extract/html-text.js +132 -0
  215. package/dist-lib/lib/core/files/rag/extract/office-collabora.d.ts +2 -0
  216. package/dist-lib/lib/core/files/rag/extract/office-collabora.js +8 -0
  217. package/dist-lib/lib/core/files/rag/extract/pdf-platform.d.ts +17 -0
  218. package/dist-lib/lib/core/files/rag/extract/pdf-platform.js +141 -0
  219. package/dist-lib/lib/core/files/rag/extract/pdf-text.d.ts +12 -0
  220. package/dist-lib/lib/core/files/rag/extract/pdf-text.js +43 -0
  221. package/dist-lib/lib/core/files/rag/extract/rtf-text.d.ts +1 -0
  222. package/dist-lib/lib/core/files/rag/extract/rtf-text.js +15 -0
  223. package/dist-lib/lib/core/files/rag/extract/text-decoding.d.ts +1 -0
  224. package/dist-lib/lib/core/files/rag/extract/text-decoding.js +91 -0
  225. package/dist-lib/lib/core/files/rag/extract/types.d.ts +36 -0
  226. package/dist-lib/lib/core/files/rag/extract/types.js +12 -0
  227. package/dist-lib/lib/core/files/rag/extract/utf.d.ts +2 -0
  228. package/dist-lib/lib/core/files/rag/extract/utf.js +18 -0
  229. package/dist-lib/lib/core/files/rag/index.d.ts +54 -0
  230. package/dist-lib/lib/core/files/rag/index.js +79 -0
  231. package/dist-lib/lib/core/files/rag/limits.d.ts +2 -0
  232. package/dist-lib/lib/core/files/rag/limits.js +2 -0
  233. package/dist-lib/lib/core/files/rag/load.d.ts +28 -0
  234. package/dist-lib/lib/core/files/rag/load.js +250 -0
  235. package/dist-lib/lib/core/files/rag/models.d.ts +1 -0
  236. package/dist-lib/lib/core/files/rag/models.js +1 -0
  237. package/dist-lib/lib/core/files/rag/policy.d.ts +2 -0
  238. package/dist-lib/lib/core/files/rag/policy.js +23 -0
  239. package/dist-lib/lib/core/files/rag/produce.d.ts +21 -0
  240. package/dist-lib/lib/core/files/rag/produce.js +214 -0
  241. package/dist-lib/lib/core/files/rag/retrieval/citation-match.d.ts +2 -0
  242. package/dist-lib/lib/core/files/rag/retrieval/citation-match.js +16 -0
  243. package/dist-lib/lib/core/files/rag/retrieval/coverage.d.ts +4 -0
  244. package/dist-lib/lib/core/files/rag/retrieval/coverage.js +48 -0
  245. package/dist-lib/lib/core/files/rag/retrieval/diversity.d.ts +10 -0
  246. package/dist-lib/lib/core/files/rag/retrieval/diversity.js +37 -0
  247. package/dist-lib/lib/core/files/rag/retrieval/floor.d.ts +5 -0
  248. package/dist-lib/lib/core/files/rag/retrieval/floor.js +21 -0
  249. package/dist-lib/lib/core/files/rag/retrieval/format.d.ts +6 -0
  250. package/dist-lib/lib/core/files/rag/retrieval/format.js +21 -0
  251. package/dist-lib/lib/core/files/rag/retrieval/full-text.d.ts +22 -0
  252. package/dist-lib/lib/core/files/rag/retrieval/full-text.js +137 -0
  253. package/dist-lib/lib/core/files/rag/retrieval/knowledge.d.ts +2 -0
  254. package/dist-lib/lib/core/files/rag/retrieval/knowledge.js +53 -0
  255. package/dist-lib/lib/core/files/rag/retrieval/notices.d.ts +16 -0
  256. package/dist-lib/lib/core/files/rag/retrieval/notices.js +27 -0
  257. package/dist-lib/lib/core/files/rag/retrieval/scoring.d.ts +8 -0
  258. package/dist-lib/lib/core/files/rag/retrieval/scoring.js +82 -0
  259. package/dist-lib/lib/core/files/rag/retrieval/search.d.ts +3 -0
  260. package/dist-lib/lib/core/files/rag/retrieval/search.js +46 -0
  261. package/dist-lib/lib/core/files/rag/retrieval/selection.d.ts +10 -0
  262. package/dist-lib/lib/core/files/rag/retrieval/selection.js +63 -0
  263. package/dist-lib/lib/core/files/rag/retrieval/source-tag-filter.d.ts +6 -0
  264. package/dist-lib/lib/core/files/rag/retrieval/source-tag-filter.js +79 -0
  265. package/dist-lib/lib/core/files/rag/retrieval/sources.d.ts +13 -0
  266. package/dist-lib/lib/core/files/rag/retrieval/sources.js +20 -0
  267. package/dist-lib/lib/core/files/rag/retrieval/target.d.ts +13 -0
  268. package/dist-lib/lib/core/files/rag/retrieval/target.js +89 -0
  269. package/dist-lib/lib/core/files/rag/retrieval/terms.d.ts +8 -0
  270. package/dist-lib/lib/core/files/rag/retrieval/terms.js +70 -0
  271. package/dist-lib/lib/core/files/rag/retrieval/types.d.ts +28 -0
  272. package/dist-lib/lib/core/files/rag/retrieval/types.js +1 -0
  273. package/dist-lib/lib/core/files/rag/retrieval/web-sources.d.ts +8 -0
  274. package/dist-lib/lib/core/files/rag/retrieval/web-sources.js +41 -0
  275. package/dist-lib/lib/core/files/rag/retrieval/whole-document-query.d.ts +1 -0
  276. package/dist-lib/lib/core/files/rag/retrieval/whole-document-query.js +96 -0
  277. package/dist-lib/lib/core/files/rag/vector/memory.d.ts +15 -0
  278. package/dist-lib/lib/core/files/rag/vector/memory.js +135 -0
  279. package/dist-lib/lib/core/files/rag/vector/types.d.ts +28 -0
  280. package/dist-lib/lib/core/files/rag/vector/types.js +1 -0
  281. package/dist-lib/lib/core/files/records/chunks.d.ts +3 -0
  282. package/dist-lib/lib/core/files/records/chunks.js +12 -0
  283. package/dist-lib/lib/core/files/records/index.d.ts +10 -0
  284. package/dist-lib/lib/core/files/records/index.js +8 -0
  285. package/dist-lib/lib/core/files/records/knowledge.d.ts +22 -0
  286. package/dist-lib/lib/core/files/records/knowledge.js +105 -0
  287. package/dist-lib/lib/core/files/records/linkage.d.ts +11 -0
  288. package/dist-lib/lib/core/files/records/linkage.js +27 -0
  289. package/dist-lib/lib/core/files/records/output.d.ts +8 -0
  290. package/dist-lib/lib/core/files/records/output.js +38 -0
  291. package/dist-lib/lib/core/files/records/reclaim.d.ts +5 -0
  292. package/dist-lib/lib/core/files/records/reclaim.js +73 -0
  293. package/dist-lib/lib/core/files/records/routing.d.ts +23 -0
  294. package/dist-lib/lib/core/files/records/routing.js +62 -0
  295. package/dist-lib/lib/core/files/records/source.d.ts +16 -0
  296. package/dist-lib/lib/core/files/records/source.js +70 -0
  297. package/dist-lib/lib/core/files/records/sweep.d.ts +2 -0
  298. package/dist-lib/lib/core/files/records/sweep.js +30 -0
  299. package/dist-lib/lib/core/files/store/blob.d.ts +29 -0
  300. package/dist-lib/lib/core/files/store/blob.js +98 -0
  301. package/dist-lib/lib/core/files/store/index.d.ts +36 -0
  302. package/dist-lib/lib/core/files/store/index.js +89 -0
  303. package/dist-lib/lib/core/files/store/inline.d.ts +8 -0
  304. package/dist-lib/lib/core/files/store/inline.js +38 -0
  305. package/dist-lib/lib/core/files/store/reference.d.ts +27 -0
  306. package/dist-lib/lib/core/files/store/reference.js +168 -0
  307. package/dist-lib/lib/core/files/store/server.d.ts +27 -0
  308. package/dist-lib/lib/core/files/store/server.js +114 -0
  309. package/dist-lib/lib/core/files/store/sweep.d.ts +2 -0
  310. package/dist-lib/lib/core/files/store/sweep.js +45 -0
  311. package/dist-lib/lib/core/files/upload-input.d.ts +27 -0
  312. package/dist-lib/lib/core/files/upload-input.js +52 -0
  313. package/dist-lib/lib/core/files/upload.d.ts +32 -0
  314. package/dist-lib/lib/core/files/upload.js +201 -0
  315. package/dist-lib/lib/core/fs-drives.d.ts +18 -0
  316. package/dist-lib/lib/core/fs-drives.js +58 -0
  317. package/dist-lib/lib/core/gpu-info-node.d.ts +3 -0
  318. package/dist-lib/lib/core/gpu-info-node.js +2 -0
  319. package/dist-lib/lib/core/handoff-channel.d.ts +46 -0
  320. package/dist-lib/lib/core/handoff-channel.js +102 -0
  321. package/dist-lib/lib/core/health-assistant.d.ts +7 -0
  322. package/dist-lib/lib/core/health-assistant.js +24 -0
  323. package/dist-lib/lib/core/helpers/index.d.ts +5 -0
  324. package/dist-lib/lib/core/helpers/index.js +74 -0
  325. package/dist-lib/lib/core/hosted-agents.d.ts +36 -0
  326. package/dist-lib/lib/core/hosted-agents.js +127 -0
  327. package/dist-lib/lib/core/id.d.ts +1 -0
  328. package/dist-lib/lib/core/id.js +5 -0
  329. package/dist-lib/lib/core/identity.d.ts +14 -0
  330. package/dist-lib/lib/core/identity.js +45 -0
  331. package/dist-lib/lib/core/image/cache-sweep.d.ts +12 -0
  332. package/dist-lib/lib/core/image/cache-sweep.js +67 -0
  333. package/dist-lib/lib/core/image/normalize.d.ts +3 -0
  334. package/dist-lib/lib/core/image/normalize.js +49 -0
  335. package/dist-lib/lib/core/image/sniff.d.ts +3 -0
  336. package/dist-lib/lib/core/image/sniff.js +149 -0
  337. package/dist-lib/lib/core/invite.d.ts +14 -0
  338. package/dist-lib/lib/core/invite.js +36 -0
  339. package/dist-lib/lib/core/local/index.d.ts +71 -0
  340. package/dist-lib/lib/core/local/index.js +391 -0
  341. package/dist-lib/lib/core/membership.d.ts +15 -0
  342. package/dist-lib/lib/core/membership.js +40 -0
  343. package/dist-lib/lib/core/mesh.d.ts +153 -0
  344. package/dist-lib/lib/core/mesh.js +509 -0
  345. package/dist-lib/lib/core/migrations/agent-catalog.d.ts +6 -0
  346. package/dist-lib/lib/core/migrations/agent-catalog.js +17 -0
  347. package/dist-lib/lib/core/migrations/index.d.ts +5 -0
  348. package/dist-lib/lib/core/migrations/index.js +7 -0
  349. package/dist-lib/lib/core/migrations/membership.d.ts +11 -0
  350. package/dist-lib/lib/core/migrations/membership.js +26 -0
  351. package/dist-lib/lib/core/min-version.d.ts +6 -0
  352. package/dist-lib/lib/core/min-version.js +31 -0
  353. package/dist-lib/lib/core/next-frame.d.ts +6 -0
  354. package/dist-lib/lib/core/next-frame.js +27 -0
  355. package/dist-lib/lib/core/notification.d.ts +6 -0
  356. package/dist-lib/lib/core/notification.js +162 -0
  357. package/dist-lib/lib/core/ota-config.d.ts +19 -0
  358. package/dist-lib/lib/core/ota-config.js +44 -0
  359. package/dist-lib/lib/core/prompt-history.d.ts +12 -0
  360. package/dist-lib/lib/core/prompt-history.js +40 -0
  361. package/dist-lib/lib/core/search-fold.d.ts +1 -0
  362. package/dist-lib/lib/core/search-fold.js +4 -0
  363. package/dist-lib/lib/core/secret-handoff.d.ts +17 -0
  364. package/dist-lib/lib/core/secret-handoff.js +29 -0
  365. package/dist-lib/lib/core/send.d.ts +18 -0
  366. package/dist-lib/lib/core/send.js +46 -0
  367. package/dist-lib/lib/core/skill-overlay-handoff.d.ts +25 -0
  368. package/dist-lib/lib/core/skill-overlay-handoff.js +50 -0
  369. package/dist-lib/lib/core/skill-overlay-receiver.d.ts +8 -0
  370. package/dist-lib/lib/core/skill-overlay-receiver.js +20 -0
  371. package/dist-lib/lib/core/skill-secrets.d.ts +25 -0
  372. package/dist-lib/lib/core/skill-secrets.js +79 -0
  373. package/dist-lib/lib/core/sync/chats.d.ts +3 -0
  374. package/dist-lib/lib/core/sync/chats.js +5 -0
  375. package/dist-lib/lib/core/sync/chunks.d.ts +9 -0
  376. package/dist-lib/lib/core/sync/chunks.js +16 -0
  377. package/dist-lib/lib/core/sync/devices.d.ts +4 -0
  378. package/dist-lib/lib/core/sync/devices.js +12 -0
  379. package/dist-lib/lib/core/sync/files.d.ts +16 -0
  380. package/dist-lib/lib/core/sync/files.js +109 -0
  381. package/dist-lib/lib/core/sync/index.d.ts +5 -0
  382. package/dist-lib/lib/core/sync/index.js +63 -0
  383. package/dist-lib/lib/core/sync-status.d.ts +17 -0
  384. package/dist-lib/lib/core/sync-status.js +129 -0
  385. package/dist-lib/lib/core/system-info.d.ts +24 -0
  386. package/dist-lib/lib/core/system-info.js +66 -0
  387. package/dist-lib/lib/core/tool-activity.d.ts +22 -0
  388. package/dist-lib/lib/core/tool-activity.js +39 -0
  389. package/dist-lib/lib/core/tool-configs.d.ts +13 -0
  390. package/dist-lib/lib/core/tool-configs.js +141 -0
  391. package/dist-lib/lib/core.d.ts +291 -0
  392. package/dist-lib/lib/core.js +2126 -0
  393. package/dist-lib/lib/credential-pusher.d.ts +67 -0
  394. package/dist-lib/lib/credential-pusher.js +290 -0
  395. package/dist-lib/lib/custom-model-pusher.d.ts +9 -0
  396. package/dist-lib/lib/custom-model-pusher.js +42 -0
  397. package/dist-lib/lib/custom-model-source.d.ts +15 -0
  398. package/dist-lib/lib/custom-model-source.js +46 -0
  399. package/dist-lib/lib/deadline.d.ts +9 -0
  400. package/dist-lib/lib/deadline.js +31 -0
  401. package/dist-lib/lib/diagnostics.d.ts +35 -0
  402. package/dist-lib/lib/diagnostics.js +14 -0
  403. package/dist-lib/lib/fetch-node.d.ts +1 -0
  404. package/dist-lib/lib/fetch-node.js +1 -0
  405. package/dist-lib/lib/fs-drive-bridge.d.ts +12 -0
  406. package/dist-lib/lib/fs-drive-bridge.js +186 -0
  407. package/dist-lib/lib/fs-grant-keys.d.ts +2 -0
  408. package/dist-lib/lib/fs-grant-keys.js +6 -0
  409. package/dist-lib/lib/fs-grants.d.ts +10 -0
  410. package/dist-lib/lib/fs-grants.js +44 -0
  411. package/dist-lib/lib/image-cache-dir.d.ts +1 -0
  412. package/dist-lib/lib/image-cache-dir.js +5 -0
  413. package/dist-lib/lib/integrations/index.d.ts +4 -0
  414. package/dist-lib/lib/integrations/index.js +3 -0
  415. package/dist-lib/lib/integrations/mcp-providers.d.ts +12 -0
  416. package/dist-lib/lib/integrations/mcp-providers.js +22 -0
  417. package/dist-lib/lib/integrations/oauth-providers.d.ts +14 -0
  418. package/dist-lib/lib/integrations/oauth-providers.js +116 -0
  419. package/dist-lib/lib/integrations/setup-routes.d.ts +12 -0
  420. package/dist-lib/lib/integrations/setup-routes.js +114 -0
  421. package/dist-lib/lib/knowledge-search.d.ts +7 -0
  422. package/dist-lib/lib/knowledge-search.js +53 -0
  423. package/dist-lib/lib/kv-cache-reclaim.d.ts +5 -0
  424. package/dist-lib/lib/kv-cache-reclaim.js +29 -0
  425. package/dist-lib/lib/kv-cache.d.ts +18 -0
  426. package/dist-lib/lib/kv-cache.js +33 -0
  427. package/dist-lib/lib/legacy-core-domain.d.ts +258 -0
  428. package/dist-lib/lib/legacy-core-domain.js +1 -0
  429. package/dist-lib/lib/llm-model-names.d.ts +1 -0
  430. package/dist-lib/lib/llm-model-names.js +1 -0
  431. package/dist-lib/lib/log-buffer.d.ts +7 -0
  432. package/dist-lib/lib/log-buffer.js +16 -0
  433. package/dist-lib/lib/log-levels.d.ts +7 -0
  434. package/dist-lib/lib/log-levels.js +1 -0
  435. package/dist-lib/lib/log.d.ts +60 -0
  436. package/dist-lib/lib/log.js +149 -0
  437. package/dist-lib/lib/logger-type.d.ts +13 -0
  438. package/dist-lib/lib/logger-type.js +1 -0
  439. package/dist-lib/lib/logger.d.ts +5 -0
  440. package/dist-lib/lib/logger.js +18 -0
  441. package/dist-lib/lib/model-catalog-revision.d.ts +9 -0
  442. package/dist-lib/lib/model-catalog-revision.js +65 -0
  443. package/dist-lib/lib/model-clamp.d.ts +6 -0
  444. package/dist-lib/lib/model-clamp.js +20 -0
  445. package/dist-lib/lib/model-context.d.ts +13 -0
  446. package/dist-lib/lib/model-context.js +71 -0
  447. package/dist-lib/lib/model-download-status.d.ts +29 -0
  448. package/dist-lib/lib/model-download-status.js +56 -0
  449. package/dist-lib/lib/model-download.d.ts +191 -0
  450. package/dist-lib/lib/model-download.js +787 -0
  451. package/dist-lib/lib/oauth/asana.d.ts +35 -0
  452. package/dist-lib/lib/oauth/asana.js +71 -0
  453. package/dist-lib/lib/oauth/browser-connect.d.ts +22 -0
  454. package/dist-lib/lib/oauth/browser-connect.js +83 -0
  455. package/dist-lib/lib/oauth/client.d.ts +9 -0
  456. package/dist-lib/lib/oauth/client.js +9 -0
  457. package/dist-lib/lib/oauth/common.d.ts +38 -0
  458. package/dist-lib/lib/oauth/common.js +36 -0
  459. package/dist-lib/lib/oauth/crypto.d.ts +3 -0
  460. package/dist-lib/lib/oauth/crypto.js +25 -0
  461. package/dist-lib/lib/oauth/fetch-platform.d.ts +1 -0
  462. package/dist-lib/lib/oauth/fetch-platform.js +1 -0
  463. package/dist-lib/lib/oauth/flow-core.d.ts +6 -0
  464. package/dist-lib/lib/oauth/flow-core.js +37 -0
  465. package/dist-lib/lib/oauth/flow.d.ts +3 -0
  466. package/dist-lib/lib/oauth/flow.js +24 -0
  467. package/dist-lib/lib/oauth/google.d.ts +37 -0
  468. package/dist-lib/lib/oauth/google.js +77 -0
  469. package/dist-lib/lib/oauth/host.d.ts +5 -0
  470. package/dist-lib/lib/oauth/host.js +3 -0
  471. package/dist-lib/lib/oauth/keys.d.ts +25 -0
  472. package/dist-lib/lib/oauth/keys.js +93 -0
  473. package/dist-lib/lib/oauth/loopback.d.ts +3 -0
  474. package/dist-lib/lib/oauth/loopback.js +119 -0
  475. package/dist-lib/lib/oauth/loopback.stub.d.ts +3 -0
  476. package/dist-lib/lib/oauth/loopback.stub.js +6 -0
  477. package/dist-lib/lib/oauth/notion.d.ts +34 -0
  478. package/dist-lib/lib/oauth/notion.js +82 -0
  479. package/dist-lib/lib/oauth/pkce-runtime.d.ts +3 -0
  480. package/dist-lib/lib/oauth/pkce-runtime.js +5 -0
  481. package/dist-lib/lib/oauth/pkce.d.ts +8 -0
  482. package/dist-lib/lib/oauth/pkce.js +6 -0
  483. package/dist-lib/lib/oauth/providers.d.ts +5 -0
  484. package/dist-lib/lib/oauth/providers.js +73 -0
  485. package/dist-lib/lib/oauth/redirect-types.d.ts +19 -0
  486. package/dist-lib/lib/oauth/redirect-types.js +1 -0
  487. package/dist-lib/lib/oauth/register.d.ts +3 -0
  488. package/dist-lib/lib/oauth/register.js +34 -0
  489. package/dist-lib/lib/oauth/resolve.d.ts +17 -0
  490. package/dist-lib/lib/oauth/resolve.js +57 -0
  491. package/dist-lib/lib/oauth/spotify.d.ts +32 -0
  492. package/dist-lib/lib/oauth/spotify.js +73 -0
  493. package/dist-lib/lib/oauth/token-exchange-core.d.ts +6 -0
  494. package/dist-lib/lib/oauth/token-exchange-core.js +97 -0
  495. package/dist-lib/lib/oauth/token-exchange.d.ts +5 -0
  496. package/dist-lib/lib/oauth/token-exchange.js +9 -0
  497. package/dist-lib/lib/oauth/types.d.ts +54 -0
  498. package/dist-lib/lib/oauth/types.js +1 -0
  499. package/dist-lib/lib/oauth.client.d.ts +5 -0
  500. package/dist-lib/lib/oauth.client.js +2 -0
  501. package/dist-lib/lib/oauth.d.ts +14 -0
  502. package/dist-lib/lib/oauth.js +12 -0
  503. package/dist-lib/lib/obsidian-key.d.ts +1 -0
  504. package/dist-lib/lib/obsidian-key.js +1 -0
  505. package/dist-lib/lib/race-resource.d.ts +5 -0
  506. package/dist-lib/lib/race-resource.js +25 -0
  507. package/dist-lib/lib/reconnecting-watch.d.ts +2 -0
  508. package/dist-lib/lib/reconnecting-watch.js +78 -0
  509. package/dist-lib/lib/retry-budget.d.ts +14 -0
  510. package/dist-lib/lib/retry-budget.js +39 -0
  511. package/dist-lib/lib/run-notices.d.ts +23 -0
  512. package/dist-lib/lib/run-notices.js +33 -0
  513. package/dist-lib/lib/run-transcript.d.ts +35 -0
  514. package/dist-lib/lib/run-transcript.js +106 -0
  515. package/dist-lib/lib/skill-readiness.d.ts +2 -0
  516. package/dist-lib/lib/skill-readiness.js +21 -0
  517. package/dist-lib/lib/skill-types.d.ts +70 -0
  518. package/dist-lib/lib/skill-types.js +1 -0
  519. package/dist-lib/lib/spec-loader.d.ts +14 -0
  520. package/dist-lib/lib/spec-loader.js +9 -0
  521. package/dist-lib/lib/suspend-lifecycle.d.ts +17 -0
  522. package/dist-lib/lib/suspend-lifecycle.js +33 -0
  523. package/dist-lib/lib/task.d.ts +26 -0
  524. package/dist-lib/lib/task.js +69 -0
  525. package/dist-lib/lib/unref.d.ts +3 -0
  526. package/dist-lib/lib/unref.js +4 -0
  527. package/dist-lib/lib/wav-duration.d.ts +1 -0
  528. package/dist-lib/lib/wav-duration.js +53 -0
  529. package/dist-lib/lib/write-queue.d.ts +22 -0
  530. package/dist-lib/lib/write-queue.js +39 -0
  531. package/dist-lib/schema/build.d.ts +1 -0
  532. package/dist-lib/schema/build.js +120 -0
  533. package/dist-lib/schema/config.d.ts +4 -0
  534. package/dist-lib/schema/config.js +7 -0
  535. package/dist-lib/schema/content.d.ts +20 -0
  536. package/dist-lib/schema/content.js +122 -0
  537. package/dist-lib/schema/engine.d.ts +2 -0
  538. package/dist-lib/schema/engine.js +2013 -0
  539. package/dist-lib/schema/field-order.d.ts +1 -0
  540. package/dist-lib/schema/field-order.js +95 -0
  541. package/dist-lib/schema/local.d.ts +2 -0
  542. package/dist-lib/schema/local.js +133 -0
  543. package/dist-lib/schema/qvac.d.ts +22 -0
  544. package/dist-lib/schema/qvac.js +722 -0
  545. package/dist-lib/spec/bind.d.ts +2 -0
  546. package/dist-lib/spec/bind.js +520 -0
  547. package/dist-lib/spec/calls.d.ts +3 -0
  548. package/dist-lib/spec/calls.js +129 -0
  549. package/dist-lib/spec/capabilities.d.ts +263 -0
  550. package/dist-lib/spec/config/hyperconf/index.d.ts +15 -0
  551. package/dist-lib/spec/config/hyperconf/index.js +24 -0
  552. package/dist-lib/spec/config/hyperconf/package.json +3 -0
  553. package/dist-lib/spec/config/hyperschema/index.js +81 -0
  554. package/dist-lib/spec/config/hyperschema/schema.json +19 -0
  555. package/dist-lib/spec/hrpc/hrpc.json +1500 -0
  556. package/dist-lib/spec/hrpc/index.d.ts +3 -0
  557. package/dist-lib/spec/hrpc/index.js +1545 -0
  558. package/dist-lib/spec/hrpc/messages.d.ts +4 -0
  559. package/dist-lib/spec/hrpc/messages.js +8373 -0
  560. package/dist-lib/spec/hyperschema/index.d.ts +20 -0
  561. package/dist-lib/spec/hyperschema/index.js +8373 -0
  562. package/dist-lib/spec/hyperschema/schema.json +6202 -0
  563. package/dist-lib/spec/hyperschema/types.d.ts +1545 -0
  564. package/dist-lib/spec/index.d.ts +36 -0
  565. package/dist-lib/spec/index.js +6 -0
  566. package/dist-lib/spec/local/hyperdb/db.json +186 -0
  567. package/dist-lib/spec/local/hyperdb/index.d.ts +9 -0
  568. package/dist-lib/spec/local/hyperdb/index.js +855 -0
  569. package/dist-lib/spec/local/hyperdb/messages.js +2189 -0
  570. package/dist-lib/spec/local/hyperschema/index.js +1208 -0
  571. package/dist-lib/spec/local/hyperschema/schema.json +639 -0
  572. package/dist-lib/spec/local/hyperschema/types.d.ts +159 -0
  573. package/dist-lib/spec/mesh/hyperdb/db.json +702 -0
  574. package/dist-lib/spec/mesh/hyperdb/index.d.ts +9 -0
  575. package/dist-lib/spec/mesh/hyperdb/index.js +2260 -0
  576. package/dist-lib/spec/mesh/hyperdb/messages.js +7453 -0
  577. package/dist-lib/spec/mesh/hyperdispatch/dispatch.json +174 -0
  578. package/dist-lib/spec/mesh/hyperdispatch/index.d.ts +13 -0
  579. package/dist-lib/spec/mesh/hyperdispatch/index.js +566 -0
  580. package/dist-lib/spec/mesh/hyperdispatch/messages.js +5713 -0
  581. package/dist-lib/spec/mesh/hyperschema/index.d.ts +14 -0
  582. package/dist-lib/spec/mesh/hyperschema/index.js +5713 -0
  583. package/dist-lib/spec/mesh/hyperschema/schema.json +3142 -0
  584. package/dist-lib/spec/mesh/hyperschema/types.d.ts +754 -0
  585. package/ducks.png +0 -0
  586. package/package.json +332 -1
@@ -0,0 +1,2706 @@
1
+ import defaultFetch from '#fetch';
2
+ import b4a from 'b4a';
3
+ import { AbortController } from 'bare-abort-controller';
4
+ import fs from 'fs';
5
+ import crypto from 'hypercore-crypto';
6
+ import os from 'os';
7
+ import path from 'path';
8
+ import { attachmentTurn, skillLoadedTurn } from '@qvac/harness/lib/capabilities/tool-turns.ts';
9
+ import { diffusionDownloadName, mediaModelNameFor } from '@qvac/harness/lib/models/model-catalog.ts';
10
+ import { readSetup as normalizeSkillSetup } from '@qvac/harness/lib/tools/skills/manifest-parse.ts';
11
+ import { SKILL_STATE } from '@qvac/harness/lib/tools/skills/skill-state.ts';
12
+ import { toolsetEnabled } from '@qvac/harness/lib/tools/toolsets.ts';
13
+ import { RAG_CITATION_LINE } from '@qvac/harness/lib/prompt.ts';
14
+ import { acceptedRepairCount, historyBranches, isPromptChunk } from "./core/prompt-history.js";
15
+ import ReadyGuard from 'ready-guard';
16
+ import ReadyResource from 'ready-resource';
17
+ import { local } from '../spec/index.js';
18
+ import { isMeshTransition } from "./api/current-mesh.js";
19
+ import { buildTitlePrompt, deriveChatTitle, isDefaultChatTitle, parseTitleResponse } from "./chat-title.js";
20
+ import { matchApprovalRule } from "./core/approval-rules.js";
21
+ import { attachTurnDocumentIds, groupAttachmentsByMessage, formatRagContext, chooseRagNotice, RAG_KNOWLEDGE_OFF_NOTICE, RAG_TRUNCATED_NOTICE, RAG_UNAVAILABLE_NOTICE, createRagSourceTagFilter, citedByPhraseOverlap, formatFullDocumentContext, isFullTextHit, isTruncatedFullTextHit, sourcesFromHits, WEB_TOOLS, webSourcesFromToolResult, withoutDataChannel } from "./core/files/index.js";
22
+ import { isAudioAttachment } from "./core/audio-attachment.js";
23
+ import { annotateDocumentAttachments, documentUnreadNotice, documentUnreadReason, isDocumentAttachment } from "./core/document-attachment.js";
24
+ import { HISTORY_CHUNK_TYPES } from "./core/chunk.js";
25
+ import { imageCacheName, sweepImageCache } from "./core/image/cache-sweep.js";
26
+ import { IMAGE_MIMES, normalizeImageForModel } from "./core/image/normalize.js";
27
+ import { summarizeRuns } from "./diagnostics.js";
28
+ import { resolveFsGrants } from "./fs-drive-bridge.js";
29
+ import { fsGrantKey, fsWriteGrantKey } from "./fs-grant-keys.js";
30
+ import { fsGrantsFrom, grantedDirsFrom } from "./fs-grants.js";
31
+ import { defaultImageCacheDir } from "./image-cache-dir.js";
32
+ import { ModelCatalogRevisionHub } from "./model-catalog-revision.js";
33
+ import { agentRekeyFingerprint, chatKvCacheKey, chatRepairKvCacheKey, disappearedChatIds } from "./kv-cache-reclaim.js";
34
+ import { buildCompactionPrompt, canCompact, foldReducesPrompt, COMPACTION_KEEP_TURNS, COMPACTION_TURN_PREFIX, isContextOverflowFailure, nearContextLimit, compactionBoundary, compactionTurn, foldCompactedChunks, lastCompactedThroughId, parseCompactionSummary } from "./compaction.js";
35
+ import { isMobilePlatform, runnableModelName } from "./model-clamp.js";
36
+ import { ModelDownloadReconciler } from "./model-download.js";
37
+ import { AbortedError, raceAbort } from '@qvac/harness/race-abort';
38
+ import { RunTranscript } from "./run-transcript.js";
39
+ import { searchKnowledge } from "./knowledge-search.js";
40
+ import { isLifecycleTeardown, knownAbortOutcome, RUN_ABORT_REASONS, RUN_FAILURE_REASONS, runFailureNotice } from "./run-notices.js";
41
+ import { encodeAgentSkillReadiness } from "./skill-readiness.js";
42
+ const QvacState = local.getEnum('@local/qvac-state');
43
+ export const IMAGE_ONLY_USER_PROMPT = 'The user sent one or more images without any accompanying text. Describe what the images show and respond to anything notable in them.';
44
+ export const DOCUMENT_ONLY_USER_PROMPT = 'The user sent one or more documents without any accompanying text. Summarize the attached documents.';
45
+ export const IMAGE_AND_DOCUMENT_USER_PROMPT = 'The user sent one or more images together with one or more documents, and no accompanying text. Describe what the images show and summarize the attached documents.';
46
+ function backfillDocumentOnlyTurn(messages) {
47
+ const last = messages.findLast((turn) => turn.role === 'user');
48
+ if (!last || last.content.trim())
49
+ return;
50
+ last.content = last.attachments?.length
51
+ ? IMAGE_AND_DOCUMENT_USER_PROMPT
52
+ : DOCUMENT_ONLY_USER_PROMPT;
53
+ }
54
+ function backfillImageOnlyTurns(messages) {
55
+ for (const turn of messages) {
56
+ if (turn.role !== 'user' || turn.content.trim())
57
+ continue;
58
+ if (turn.attachments?.length)
59
+ turn.content = IMAGE_ONLY_USER_PROMPT;
60
+ }
61
+ }
62
+ export function unreadableImagesNotice(fileNames) {
63
+ const plural = fileNames.length > 1;
64
+ return (`[Attached image${plural ? 's' : ''}: ${fileNames.join(', ')} — could not be read, so ` +
65
+ `${plural ? 'they are' : 'it is'} not visible to you. Tell the user the upload failed and ` +
66
+ `ask for a re-upload; never describe or recreate ${plural ? 'them' : 'it'}]`);
67
+ }
68
+ export function annotateUnreadableImages(unreadable) {
69
+ for (const [turn, names] of unreadable) {
70
+ const notice = unreadableImagesNotice(names);
71
+ turn.content = turn.content ? `${turn.content}\n${notice}` : notice;
72
+ }
73
+ }
74
+ async function raceInactivity(promise, signal, timeoutMs) {
75
+ let timer = null;
76
+ const expired = new Promise((_, reject) => {
77
+ timer = setTimeout(() => reject(new Error('blob read timed out')), timeoutMs);
78
+ });
79
+ try {
80
+ return await raceAbort(Promise.race([promise, expired]), signal);
81
+ }
82
+ finally {
83
+ if (timer !== null)
84
+ clearTimeout(timer);
85
+ }
86
+ }
87
+ async function drainBody(body, signal, timeoutMs) {
88
+ if (!body)
89
+ return null;
90
+ const reader = body.getReader();
91
+ const parts = [];
92
+ try {
93
+ for (;;) {
94
+ const read = await raceInactivity(reader.read(), signal, timeoutMs);
95
+ if (read.done)
96
+ break;
97
+ parts.push(b4a.from(read.value));
98
+ }
99
+ }
100
+ catch {
101
+ await reader.cancel();
102
+ return null;
103
+ }
104
+ return b4a.concat(parts);
105
+ }
106
+ export function unheardAudioNotice(label, reason, opts = {}) {
107
+ const retry = opts.retryable === false ? '' : ', and ask them to try again';
108
+ return (`${label} could not be transcribed (${reason}). Tell the user the audio was not ` +
109
+ `transcribed and why${retry}; never invent what it might contain.`);
110
+ }
111
+ function silentAudioNotice(fileName) {
112
+ return (`Attached audio "${fileName}" was transcribed and contained no detectable speech. ` +
113
+ 'Tell the user; never invent what it might contain.');
114
+ }
115
+ function audioTranscriptBlock(fileName, transcript) {
116
+ return [
117
+ `The audio file "${fileName}" has already been transcribed on-device. You cannot open the file itself — use the transcript below as its full content and answer from it.`,
118
+ 'Transcript:',
119
+ transcript
120
+ ].join('\n');
121
+ }
122
+ export function harnessListModels(harness) {
123
+ return async ({ type, cachedOnly, fullRegistry } = {}) => ({
124
+ models: [
125
+ ...((await harness.list?.({
126
+ type: type ?? undefined,
127
+ cachedOnly: cachedOnly ?? undefined,
128
+ fullRegistry: fullRegistry ?? undefined
129
+ })) ?? [])
130
+ ]
131
+ });
132
+ }
133
+ export function harnessDeleteModel(harness) {
134
+ return async ({ name }) => (await harness.deleteModel?.(name)) ?? { name, removed: 0, freedBytes: 0 };
135
+ }
136
+ export function harnessClearKvCache(harness) {
137
+ return async () => (await harness.clearKvCache?.()) ?? {
138
+ status: 'unsupported',
139
+ error: 'Harness does not support KV cache deletion'
140
+ };
141
+ }
142
+ export function harnessTranscribe(harness) {
143
+ return async ({ audio, model }) => {
144
+ if (!harness.transcribe)
145
+ throw new Error('harness does not support transcribe');
146
+ return { text: await harness.transcribe({ audio, model: model ?? '' }) };
147
+ };
148
+ }
149
+ export function harnessSpeak(harness) {
150
+ return async ({ text, model }) => {
151
+ if (!harness.speak)
152
+ throw new Error('harness does not support speak');
153
+ const { audio, sampleRate } = await harness.speak({ text, model: model ?? '' });
154
+ return { audio, sampleRate };
155
+ };
156
+ }
157
+ export function harnessSkillCatalog(harness, syncCredentials) {
158
+ return async (agentId, modelName, options) => {
159
+ if (options?.refresh)
160
+ await syncCredentials?.();
161
+ return {
162
+ skills: ((await harness.skills?.(agentId, modelName, options)) ?? []).map(toEngineSkill)
163
+ };
164
+ };
165
+ }
166
+ export function harnessSkillManagement(harness) {
167
+ return {
168
+ async listManagedSkills() {
169
+ return {
170
+ skills: ((await harness.listManagedSkills?.()) ?? []).map((skill) => ({
171
+ name: skill.name,
172
+ origin: skill.origin,
173
+ overridesBase: skill.overridesBase
174
+ }))
175
+ };
176
+ },
177
+ async readSkillFile(name) {
178
+ const file = await harness.readSkillFile?.(name);
179
+ return file ? { found: true, content: file.content, origin: file.origin } : { found: false };
180
+ },
181
+ async writeSkillFile(name, content) {
182
+ if (!harness.writeSkillFile)
183
+ return { ok: false };
184
+ return harness.writeSkillFile(name, content);
185
+ },
186
+ async copySkillForEdit(name) {
187
+ return { copied: (await harness.copyBaseForEdit?.(name))?.copied ?? false };
188
+ },
189
+ async deleteOverlaySkill(name) {
190
+ return { removed: (await harness.deleteOverlaySkill?.(name))?.removed ?? false };
191
+ }
192
+ };
193
+ }
194
+ export function harnessTools(harness) {
195
+ return async () => ({
196
+ tools: ((await harness.tools?.()) ?? []).map((tool) => ({
197
+ ...toolReportEntry(tool),
198
+ description: tool.description
199
+ }))
200
+ });
201
+ }
202
+ function toolReportEntry(tool) {
203
+ return {
204
+ name: tool.name,
205
+ promptChars: tool.promptChars ?? 0,
206
+ toolset: tool.toolset ?? null,
207
+ skillGated: tool.skillGated === true,
208
+ scopeRequired: tool.scopeRequired === true,
209
+ rootsRequired: tool.rootsRequired === true,
210
+ writeRootsRequired: tool.writeRootsRequired === true
211
+ };
212
+ }
213
+ export function harnessSpeakStream(harness) {
214
+ return async function* (text) {
215
+ if (!harness.speakStream)
216
+ throw new Error('harness does not support speakStream');
217
+ for await (const { audio, sampleRate } of harness.speakStream({ model: '', text })) {
218
+ yield { audio, sampleRate };
219
+ }
220
+ };
221
+ }
222
+ function toEngineSkill(skill) {
223
+ const setup = normalizeSkillSetup(skill.setup);
224
+ return {
225
+ name: skill.name,
226
+ description: skill.description,
227
+ tools: [...skill.tools],
228
+ credentials: [...skill.credentials],
229
+ allowList: [...skill.allowList],
230
+ platforms: [...skill.platform],
231
+ status: skill.status,
232
+ ...(skill.requires ? { requirements: toEngineSkillRequirements(skill.requires) } : {}),
233
+ ...(skill.install ? { install: skill.install.map(toEngineInstallStep) } : {}),
234
+ ...(setup ? { setup: toEngineSkillSetup(setup) } : {}),
235
+ ...(skill.stub !== undefined ? { stub: skill.stub } : {}),
236
+ ...(skill.minCapabilityProfile ? { minCapabilityProfile: skill.minCapabilityProfile } : {}),
237
+ ...(skill.emoji ? { emoji: skill.emoji } : {})
238
+ };
239
+ }
240
+ function toEngineSkillRequirements(requires) {
241
+ return {
242
+ binaries: (requires.bins ?? []).map((bin) => ({
243
+ bin,
244
+ minVersion: requires.binMinVersions?.[bin]?.min,
245
+ command: requires.binMinVersions?.[bin]?.command
246
+ })),
247
+ credentials: [...(requires.credentials ?? [])]
248
+ };
249
+ }
250
+ function toEngineInstallStep(step) {
251
+ return {
252
+ id: step.id,
253
+ kind: step.kind,
254
+ label: step.label,
255
+ ...(step.formula ? { formula: step.formula } : {}),
256
+ ...(step.command ? { command: step.command } : {}),
257
+ ...(step.manual !== undefined ? { manual: step.manual } : {}),
258
+ ...(step.bins ? { bins: [...step.bins] } : {})
259
+ };
260
+ }
261
+ function toEngineSkillSetup(setup) {
262
+ return {
263
+ ...(setup.summary ? { summary: setup.summary } : {}),
264
+ ...(setup.routes
265
+ ? {
266
+ routes: setup.routes.map((route) => ({
267
+ kind: route.kind,
268
+ label: route.label,
269
+ ...(route.provider ? { provider: route.provider } : {}),
270
+ ...(route.credentialKey ? { credentialKey: route.credentialKey } : {}),
271
+ ...(route.description ? { description: route.description } : {}),
272
+ ...(route.helpUrl ? { helpUrl: route.helpUrl } : {}),
273
+ ...(route.steps ? { steps: [...route.steps] } : {}),
274
+ ...(route.configSteps ? { configSteps: [...route.configSteps] } : {}),
275
+ ...(route.fields
276
+ ? {
277
+ fields: route.fields.map((field) => ({
278
+ key: field.key,
279
+ label: field.label,
280
+ ...(field.placeholder ? { placeholder: field.placeholder } : {}),
281
+ ...(field.secret !== undefined ? { secret: field.secret } : {}),
282
+ ...(field.required !== undefined ? { required: field.required } : {})
283
+ }))
284
+ }
285
+ : {}),
286
+ ...(route.satisfied !== undefined ? { satisfied: route.satisfied } : {})
287
+ }))
288
+ }
289
+ : {})
290
+ };
291
+ }
292
+ const FINAL_FLUSH_MS = 10_000;
293
+ const UPLOAD_FRAME_BYTES = 64 * 1024;
294
+ const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled']);
295
+ function settledWithin(write, ms) {
296
+ let timer;
297
+ const expired = new Promise((resolve) => {
298
+ timer = setTimeout(() => resolve(false), ms);
299
+ });
300
+ const landed = write.then(() => true, () => true);
301
+ return Promise.race([landed, expired]).finally(() => clearTimeout(timer));
302
+ }
303
+ function isDeniedResult(result) {
304
+ if (!result || typeof result !== 'object')
305
+ return false;
306
+ return result.denied === true;
307
+ }
308
+ const CONSUME_RETRY_MS = 1000;
309
+ const CONSUME_RETRY_MAX_MS = 30000;
310
+ const BLOB_READ_TIMEOUT_MS = 15_000;
311
+ function nextRetryDelay(err, current) {
312
+ return isMeshTransition(err) ? Math.min(current * 2, CONSUME_RETRY_MAX_MS) : CONSUME_RETRY_MS;
313
+ }
314
+ const SUSPENDED = RUN_ABORT_REASONS.suspended;
315
+ const OWNERSHIP_LOST = RUN_ABORT_REASONS.ownershipLost;
316
+ const RUN_SETTLED = RUN_ABORT_REASONS.runSettled;
317
+ const RUNNER_DIED = RUN_ABORT_REASONS.runnerDied;
318
+ const RUNNER_CLOSED = RUN_ABORT_REASONS.runnerClosed;
319
+ const HISTORY_TYPES = HISTORY_CHUNK_TYPES;
320
+ const SKILL_TOOL = 'skill';
321
+ function isSetUpHere(status) {
322
+ return status === SKILL_STATE.ready || status === SKILL_STATE.incompatibleModel;
323
+ }
324
+ function sameNameList(left, right) {
325
+ return left.length === right.length && left.every((name, index) => name === right[index]);
326
+ }
327
+ function withConfigDefaults(agent, config) {
328
+ if (agent.ctxSize || !config?.ctxSize)
329
+ return agent;
330
+ return { ...agent, ctxSize: config.ctxSize };
331
+ }
332
+ export async function listHistoryChunks(engine, chatId) {
333
+ const [history, toolResults, runStatuses] = await Promise.all([
334
+ engine.listChunks({ chatId, types: HISTORY_TYPES }),
335
+ engine.listChunks({ chatId, type: 'tool-result' }),
336
+ engine.listChunks({ chatId, type: 'run-status' })
337
+ ]);
338
+ return [
339
+ ...(history.chunks ?? []),
340
+ ...(toolResults.chunks ?? []),
341
+ ...(runStatuses.chunks ?? [])
342
+ ].sort((a, b) => a.seq - b.seq);
343
+ }
344
+ function latestAttemptStarts(chunks) {
345
+ const starts = new Map();
346
+ for (const chunk of chunks) {
347
+ if (chunk.type !== 'run-status' || !chunk.runId)
348
+ continue;
349
+ if (TERMINAL_STATES.has(chunk.runStatus?.state ?? ''))
350
+ continue;
351
+ starts.set(chunk.runId, chunk.seq);
352
+ }
353
+ return starts;
354
+ }
355
+ function beforeLatestAttempt(chunk, starts) {
356
+ if (!chunk.runId)
357
+ return false;
358
+ const start = starts.get(chunk.runId);
359
+ return start !== undefined && chunk.seq < start;
360
+ }
361
+ export function historyFromChunks(chunks, stale = {}) {
362
+ const attemptStarts = latestAttemptStarts(chunks);
363
+ const staleRunIds = stale.runIds ?? new Set();
364
+ const staleQuestionChunkIds = stale.questionChunkIds ?? new Set();
365
+ const folded = foldCompactedChunks(chunks);
366
+ const source = folded.chunks;
367
+ const attachments = [];
368
+ const userMessages = [];
369
+ for (const chunk of source) {
370
+ if (chunk.type === 'attachment')
371
+ attachments.push(chunk);
372
+ else if (chunk.userMessage)
373
+ userMessages.push(chunk);
374
+ }
375
+ const byMessage = groupAttachmentsByMessage(attachments, userMessages);
376
+ const history = [];
377
+ const replayedSkills = new Set();
378
+ if (folded.summary)
379
+ history.push({ role: 'user', content: compactionTurn(folded.summary) });
380
+ const byRun = new Map();
381
+ const anchor = (runId) => {
382
+ let turn = byRun.get(runId);
383
+ if (!turn) {
384
+ turn = { role: 'assistant', content: '' };
385
+ byRun.set(runId, turn);
386
+ history.push(turn);
387
+ }
388
+ return turn;
389
+ };
390
+ for (const chunk of source) {
391
+ if (chunk.userMessage) {
392
+ if (staleQuestionChunkIds.has(chunk.id))
393
+ continue;
394
+ const turn = { role: 'user', content: chunk.userMessage.text };
395
+ const refs = (byMessage.get(chunk.id) ?? [])
396
+ .map((attachmentChunk) => attachmentChunk.attachment)
397
+ .filter((ref) => !ref.deletedAt);
398
+ const images = refs.filter((ref) => IMAGE_MIMES.has(ref.mimeType.toLowerCase()));
399
+ const audio = refs.filter((ref) => isAudioAttachment(ref.fileName, ref.mimeType));
400
+ const documents = refs.filter((ref) => isDocumentAttachment(ref));
401
+ if (images.length)
402
+ turn.images = images;
403
+ if (audio.length)
404
+ turn.audioAttachments = audio;
405
+ if (documents.length)
406
+ turn.documents = documents;
407
+ history.push(turn);
408
+ continue;
409
+ }
410
+ if (chunk.attachment)
411
+ continue;
412
+ if (chunk.type === 'run-request' && chunk.runId) {
413
+ anchor(chunk.runId);
414
+ continue;
415
+ }
416
+ if (chunk.toolResult?.name === SKILL_TOOL) {
417
+ if (chunk.runId && staleRunIds.has(chunk.runId))
418
+ continue;
419
+ if (beforeLatestAttempt(chunk, attemptStarts))
420
+ continue;
421
+ const loaded = chunk.toolResult.result?.name;
422
+ if (typeof loaded === 'string' && !replayedSkills.has(loaded)) {
423
+ replayedSkills.add(loaded);
424
+ history.push({ role: 'tool', content: skillLoadedTurn(loaded) });
425
+ }
426
+ continue;
427
+ }
428
+ if (chunk.toolResult) {
429
+ if (chunk.runId && staleRunIds.has(chunk.runId))
430
+ continue;
431
+ if (beforeLatestAttempt(chunk, attemptStarts))
432
+ continue;
433
+ const produced = attachmentTurn(chunk.toolResult.name, chunk.toolResult.result);
434
+ if (produced)
435
+ history.push({ role: 'tool', content: produced });
436
+ continue;
437
+ }
438
+ if (!chunk.content)
439
+ continue;
440
+ if (chunk.runId && staleRunIds.has(chunk.runId))
441
+ continue;
442
+ if (beforeLatestAttempt(chunk, attemptStarts))
443
+ continue;
444
+ anchor(chunk.runId).content += chunk.content.text;
445
+ }
446
+ return history.filter((turn) => turn.role === 'user' || turn.content.length > 0);
447
+ }
448
+ function withoutOccupancy(metrics) {
449
+ if (!metrics)
450
+ return metrics;
451
+ const { cacheTokens, promptTokens, ...rest } = metrics;
452
+ return rest;
453
+ }
454
+ function inputAvailableToRun(chunk, requestSeq) {
455
+ const userInput = Boolean(chunk.userMessage || (chunk.attachment && !chunk.runId));
456
+ return !userInput || chunk.seq < requestSeq;
457
+ }
458
+ function normalizeQuestion(text) {
459
+ return text.trim().toLowerCase().replace(/\s+/g, ' ');
460
+ }
461
+ const GENERIC_ACKNOWLEDGEMENTS = new Set([
462
+ 'yes',
463
+ 'y',
464
+ 'yeah',
465
+ 'yep',
466
+ 'yup',
467
+ 'ok',
468
+ 'okay',
469
+ 'k',
470
+ 'sure',
471
+ 'fine',
472
+ 'no',
473
+ 'n',
474
+ 'nope',
475
+ 'nah',
476
+ 'again',
477
+ 'continue',
478
+ 'go on',
479
+ 'go ahead',
480
+ 'keep going',
481
+ 'proceed',
482
+ 'do it',
483
+ 'more',
484
+ 'please',
485
+ 'thanks',
486
+ 'thank you',
487
+ 'retry',
488
+ 'redo'
489
+ ]);
490
+ function isGenericAcknowledgement(question) {
491
+ return GENERIC_ACKNOWLEDGEMENTS.has(question.replace(/[?.!]+$/, '').trim());
492
+ }
493
+ export function staleToolRunIds(historyChunks, approvalChunks, currentRunId) {
494
+ const empty = { runIds: new Set(), questionChunkIds: new Set() };
495
+ const toolRuns = new Set();
496
+ for (const chunk of approvalChunks) {
497
+ if (chunk.runId)
498
+ toolRuns.add(chunk.runId);
499
+ }
500
+ if (!toolRuns.size)
501
+ return empty;
502
+ const turnOrder = [];
503
+ const turnText = new Map();
504
+ const turnRuns = new Map();
505
+ const runChunk = new Map();
506
+ let lastUser = null;
507
+ for (const chunk of historyChunks) {
508
+ if (chunk.userMessage) {
509
+ lastUser = { text: chunk.userMessage.text, chunkId: chunk.id };
510
+ continue;
511
+ }
512
+ if (chunk.type === 'run-request' && chunk.runId && lastUser !== null) {
513
+ const chunkId = lastUser.chunkId;
514
+ if (!turnText.has(chunkId)) {
515
+ turnOrder.push(chunkId);
516
+ turnText.set(chunkId, lastUser.text);
517
+ }
518
+ turnRuns.set(chunkId, [...(turnRuns.get(chunkId) ?? []), chunk.runId]);
519
+ runChunk.set(chunk.runId, chunkId);
520
+ }
521
+ }
522
+ const currentChunkId = runChunk.get(currentRunId) ?? lastUser?.chunkId;
523
+ if (currentChunkId === undefined)
524
+ return empty;
525
+ const currentText = turnText.get(currentChunkId) ?? lastUser?.text;
526
+ if (currentText === undefined)
527
+ return empty;
528
+ const target = normalizeQuestion(currentText);
529
+ if (isGenericAcknowledgement(target))
530
+ return empty;
531
+ const currentIndex = turnOrder.indexOf(currentChunkId);
532
+ const start = currentIndex === -1 ? turnOrder.length - 1 : currentIndex - 1;
533
+ const runIds = new Set();
534
+ const questionChunkIds = new Set();
535
+ let blockHadApproval = false;
536
+ for (let i = start; i >= 0; i--) {
537
+ const chunkId = turnOrder[i];
538
+ if (normalizeQuestion(turnText.get(chunkId)) !== target)
539
+ break;
540
+ questionChunkIds.add(chunkId);
541
+ for (const rid of turnRuns.get(chunkId) ?? []) {
542
+ runIds.add(rid);
543
+ if (toolRuns.has(rid))
544
+ blockHadApproval = true;
545
+ }
546
+ }
547
+ if (!blockHadApproval)
548
+ return empty;
549
+ return { runIds, questionChunkIds };
550
+ }
551
+ export class QvacAssistant extends ReadyResource {
552
+ _engine;
553
+ _harness;
554
+ _log;
555
+ _imageCacheDir;
556
+ _imageCacheMaxBytes;
557
+ _imageCacheMaxAgeMs;
558
+ _downloadRetryMs;
559
+ _platform;
560
+ _mobileClass;
561
+ _preloadCurrentAgentModel;
562
+ _drives;
563
+ _blobReadTimeoutMs;
564
+ _warmAbort = new AbortController();
565
+ _warmedModel = false;
566
+ _warmedModelName = null;
567
+ _modelStatusLogged = new Map();
568
+ _deviceId;
569
+ _info;
570
+ _lastCapabilities;
571
+ _readySkills;
572
+ _recordingReadiness = false;
573
+ _readinessDirty = false;
574
+ _reconciling;
575
+ _onlineBoot = new ReadyGuard();
576
+ _capability;
577
+ _downloads;
578
+ _state;
579
+ _harnessDiagnostics;
580
+ _harnessDeath;
581
+ _refreshingDiagnostics;
582
+ _requests;
583
+ _retry;
584
+ _retryMs = CONSUME_RETRY_MS;
585
+ _chatList;
586
+ _agentList;
587
+ _chatListWatch;
588
+ _agentWatch;
589
+ _runs;
590
+ _seen;
591
+ _lifecycle;
592
+ _audioTranscripts;
593
+ _chats;
594
+ _runImageCacheNames;
595
+ _reconciledCompactions;
596
+ _modelCatalog = new ModelCatalogRevisionHub();
597
+ _onHarnessStatus;
598
+ _onModelCatalogEvent;
599
+ constructor(engine, harness, { logger = null, imageCacheDir, imageCacheMaxBytes, imageCacheMaxAgeMs, downloadRetryMs, platform = os.platform(), preloadCurrentAgentModel = true, drives, blobReadTimeoutMs = BLOB_READ_TIMEOUT_MS } = {}) {
600
+ super();
601
+ this._engine = engine;
602
+ this._harness = harness;
603
+ this._drives = drives;
604
+ this._log = logger;
605
+ this._imageCacheDir = imageCacheDir ?? defaultImageCacheDir();
606
+ this._imageCacheMaxBytes = imageCacheMaxBytes;
607
+ this._imageCacheMaxAgeMs = imageCacheMaxAgeMs;
608
+ this._downloadRetryMs = downloadRetryMs;
609
+ this._platform = platform;
610
+ this._mobileClass = isMobilePlatform(platform);
611
+ this._preloadCurrentAgentModel = preloadCurrentAgentModel;
612
+ this._blobReadTimeoutMs = blobReadTimeoutMs;
613
+ this._deviceId = null;
614
+ this._info = { toolNames: [], skills: [], supportsImages: false };
615
+ this._lastCapabilities = null;
616
+ this._readySkills = null;
617
+ this._reconciling = false;
618
+ this._capability = null;
619
+ this._downloads = null;
620
+ this._state = QvacState.RESUME;
621
+ this._harnessDiagnostics = null;
622
+ this._harnessDeath = null;
623
+ this._refreshingDiagnostics = false;
624
+ this._requests = null;
625
+ this._retry = null;
626
+ this._chatList = null;
627
+ this._agentList = null;
628
+ this._chatListWatch = this._rearmingWatch('chat-list', () => this._watchChatList());
629
+ this._agentWatch = this._rearmingWatch('agent', () => this._watchAgents());
630
+ this._runs = new Map();
631
+ this._seen = new Set();
632
+ this._lifecycle = new AbortController();
633
+ this._audioTranscripts = new Map();
634
+ this._chats = new Map();
635
+ this._runImageCacheNames = new Map();
636
+ this._reconciledCompactions = new Map();
637
+ this._onHarnessStatus = (status) => {
638
+ this._log?.warn('assistant', 'harness status', status);
639
+ if (harnessDied(status)) {
640
+ this._harnessDeath = harnessDeathReason(status);
641
+ for (const run of this._runs.values())
642
+ run.controller.abort(RUNNER_DIED);
643
+ }
644
+ else {
645
+ this._harnessDeath = null;
646
+ }
647
+ this.emit('harness-status', status);
648
+ };
649
+ this._onModelCatalogEvent = (state) => {
650
+ this._logModelStatus(state);
651
+ if (modelCatalogChanged(state))
652
+ this._modelCatalog.notify();
653
+ const progress = imageDownloadProgress(state);
654
+ if (progress)
655
+ this._downloads?.observeImageProgress(progress);
656
+ };
657
+ }
658
+ async _open() {
659
+ this._log?.info('assistant', 'host platform resolved', {
660
+ platform: this._platform,
661
+ mobileClass: this._mobileClass
662
+ });
663
+ const info = await this._engine.deviceInfo({});
664
+ this._deviceId = info.id;
665
+ this._harness.on?.('status', this._onHarnessStatus);
666
+ this._harness.on?.('model', this._onModelCatalogEvent);
667
+ this._info = await this._describeHarness();
668
+ await this._clearPersistedPauses();
669
+ this._downloads = new ModelDownloadReconciler({
670
+ engine: this._engine,
671
+ harness: this._harness,
672
+ deviceId: this._deviceId,
673
+ onCacheChanged: () => {
674
+ this._modelCatalog.notify();
675
+ void this._reportCapabilities();
676
+ this._triggerModelWarm();
677
+ },
678
+ retryMs: this._downloadRetryMs,
679
+ logger: this._log,
680
+ mobileClass: this._mobileClass
681
+ });
682
+ this._downloads.start();
683
+ this._startConsume();
684
+ this._chatListWatch.start();
685
+ this._agentWatch.start();
686
+ void this._bootOnline();
687
+ this._triggerModelWarm();
688
+ void this.onlineReady().then(() => this._triggerModelWarm(), noop);
689
+ }
690
+ _triggerModelWarm() {
691
+ if (!this._preloadCurrentAgentModel)
692
+ return;
693
+ void this._warmCurrentAgentModel().catch((err) => this._log?.warn('assistant', 'current agent model warm failed', err));
694
+ }
695
+ async _warmCurrentAgentModel() {
696
+ if (this._warmedModel)
697
+ return;
698
+ const deviceId = this._deviceId;
699
+ const config = await this._engine.getConfig({}).catch(() => null);
700
+ const agent = await this._currentAgent(config?.activeAgentId ?? null, deviceId);
701
+ if (!agent?.modelName || !agent.providerDeviceId?.equals(deviceId) || this.suspended)
702
+ return;
703
+ const model = runnableModelName(agent.modelName, await this._ensureCapability(), this._mobileClass);
704
+ const rows = await this._harness.list?.({ cachedOnly: true }).catch(() => null);
705
+ if (!rows?.some((row) => row.name === model && row.isCached)) {
706
+ this._log?.info('models', 'model warm skipped', { model, reason: 'not cached' });
707
+ return;
708
+ }
709
+ if (this._warmedModel)
710
+ return;
711
+ this._warmedModel = true;
712
+ this._warmedModelName = model;
713
+ this._log?.info('models', 'model warm started', { model, agentModel: agent.modelName });
714
+ const started = Date.now();
715
+ await this._harness.warmModel?.({
716
+ model,
717
+ agent: withConfigDefaults(agent, config),
718
+ signal: this._warmAbort.signal
719
+ });
720
+ this._log?.info('models', 'model warm done', { model, ms: Date.now() - started });
721
+ }
722
+ async _currentAgent(activeAgentId, deviceId) {
723
+ if (activeAgentId) {
724
+ const agent = await this._engine.getAgent({ id: activeAgentId }).catch(() => null);
725
+ if (agent && !agent.deletedAt)
726
+ return agent;
727
+ }
728
+ const { agents } = await this._engine.listAgents({}).catch(() => ({ agents: [] }));
729
+ const all = agents ?? [];
730
+ return (all.find((agent) => agent.isDefault && agent.providerDeviceId?.equals(deviceId)) ??
731
+ all.find((agent) => agent.isDefault) ??
732
+ all[0] ??
733
+ null);
734
+ }
735
+ async _bootOnline() {
736
+ void this._onlineBoot.ready().catch(noop);
737
+ try {
738
+ await Promise.allSettled([
739
+ this._sweepImageCache(),
740
+ this._reportCapabilities(),
741
+ this._reconcileDeviceProfile()
742
+ ]);
743
+ }
744
+ finally {
745
+ this._onlineBoot.exit();
746
+ }
747
+ }
748
+ async onlineReady() {
749
+ if (!this.opened)
750
+ await this.ready();
751
+ await this._onlineBoot.ready();
752
+ }
753
+ async _clearPersistedPauses() {
754
+ try {
755
+ const config = await this._engine.getConfig({});
756
+ for (const name of config.pausedModels ?? []) {
757
+ await this._engine.setModelPaused({ name, paused: false });
758
+ }
759
+ }
760
+ catch (err) {
761
+ this._log?.warn('assistant', 'clearing persisted model pauses failed', err);
762
+ }
763
+ }
764
+ async ensureMediaModelUnpaused(category, model) {
765
+ try {
766
+ const name = model || mediaModelNameFor(category, await this._persistedProfile());
767
+ const config = await this._engine.getConfig({});
768
+ if (!(config.pausedModels ?? []).includes(name))
769
+ return true;
770
+ await this._engine.setModelPaused({ name, paused: false });
771
+ return true;
772
+ }
773
+ catch (err) {
774
+ this._log?.warn('assistant', 'ensuring media model unpaused failed', err);
775
+ return false;
776
+ }
777
+ }
778
+ async _persistedProfile() {
779
+ const deviceId = this._deviceId;
780
+ if (!deviceId)
781
+ return 'mid';
782
+ const { devices } = await this._engine.listDevices({}).catch(() => ({ devices: [] }));
783
+ const self = (devices ?? []).find((device) => device.id.equals(deviceId));
784
+ return self?.capabilityProfile ?? 'mid';
785
+ }
786
+ async _sweepImageCache() {
787
+ try {
788
+ const keep = await this._referencedImageNames();
789
+ await sweepImageCache(this._imageCacheDir, {
790
+ maxBytes: this._imageCacheMaxBytes,
791
+ maxAgeMs: this._imageCacheMaxAgeMs,
792
+ inUse: (name) => this._imageCacheInUse(name),
793
+ ...(keep ? { keep } : {})
794
+ });
795
+ }
796
+ catch (err) {
797
+ this._log?.warn('assistant', 'image cache sweep failed', err);
798
+ }
799
+ }
800
+ _imageCacheInUse(name) {
801
+ for (const names of this._runImageCacheNames.values())
802
+ if (names.has(name))
803
+ return true;
804
+ return false;
805
+ }
806
+ async _referencedImageNames() {
807
+ try {
808
+ const { chats } = await this._engine.listChats({});
809
+ if (!chats?.length)
810
+ return null;
811
+ const keep = new Set();
812
+ for (const chat of chats) {
813
+ const { chunks } = await this._engine.listChunks({ chatId: chat.id, type: 'attachment' });
814
+ for (const chunk of chunks ?? []) {
815
+ const attachment = chunk.attachment;
816
+ if (attachment && !attachment.deletedAt)
817
+ keep.add(imageCacheName(attachment));
818
+ }
819
+ }
820
+ return keep;
821
+ }
822
+ catch {
823
+ return null;
824
+ }
825
+ }
826
+ async _describeHarness() {
827
+ const described = await this._harness.describe?.();
828
+ return (described ?? {
829
+ toolNames: this._harness.toolNames ?? [],
830
+ skills: this._harness.skillNames ?? [],
831
+ supportsImages: this._harness.supportsImages ?? false
832
+ });
833
+ }
834
+ diagnosticsSlice() {
835
+ const harness = this._harness.diagnostics?.() ?? this._harnessDiagnostics;
836
+ void this._refreshHarnessDiagnostics();
837
+ const runs = [...this._runs.entries()].map(([runId, run]) => ({
838
+ runId,
839
+ chatId: run.chatId,
840
+ model: run.model
841
+ }));
842
+ return {
843
+ harness,
844
+ assistant: summarizeRuns(runs, this.suspended),
845
+ platform: this._platform,
846
+ mobileClass: this._mobileClass,
847
+ memoryBudget: this._capability?.memoryBudget ?? null
848
+ };
849
+ }
850
+ async _refreshHarnessDiagnostics() {
851
+ if (this._refreshingDiagnostics || !this._harness.fetchDiagnostics)
852
+ return;
853
+ this._refreshingDiagnostics = true;
854
+ try {
855
+ this._harnessDiagnostics = await this._harness.fetchDiagnostics();
856
+ }
857
+ catch {
858
+ }
859
+ finally {
860
+ this._refreshingDiagnostics = false;
861
+ }
862
+ }
863
+ async _reportCapabilities() {
864
+ let models;
865
+ try {
866
+ models = (await this._harness.list?.()) ?? [];
867
+ }
868
+ catch (err) {
869
+ this._log?.warn('assistant', '_reportCapabilities: harness.list failed — keeping last catalog', err);
870
+ return;
871
+ }
872
+ const installedModelNames = models.map((model) => model.name);
873
+ const cachedModelNames = models.filter((model) => model.isCached).map((model) => model.name);
874
+ let toolCatalog = null;
875
+ if (this._harness.tools) {
876
+ try {
877
+ toolCatalog = (await this._harness.tools()).map(toolReportEntry);
878
+ }
879
+ catch (err) {
880
+ this._log?.warn('assistant', '_reportCapabilities: harness.tools failed', err);
881
+ }
882
+ }
883
+ const key = JSON.stringify([
884
+ this._info.toolNames,
885
+ this._info.skills,
886
+ installedModelNames,
887
+ cachedModelNames,
888
+ this._readySkills,
889
+ toolCatalog
890
+ ]);
891
+ if (key === this._lastCapabilities)
892
+ return;
893
+ try {
894
+ await this._engine.saveDeviceCapabilities({
895
+ installedToolNames: this._info.toolNames,
896
+ installedSkillNames: this._info.skills,
897
+ installedModelNames,
898
+ cachedModelNames,
899
+ ...(this._readySkills ? { readySkillNames: this._readySkills } : {}),
900
+ ...(toolCatalog ? { toolCatalog } : {})
901
+ });
902
+ this._lastCapabilities = key;
903
+ }
904
+ catch (err) {
905
+ if (isMeshTransition(err))
906
+ return;
907
+ this._log?.error('assistant', 'saveDeviceCapabilities failed', err);
908
+ }
909
+ }
910
+ async _ensureCapability() {
911
+ if (this._capability)
912
+ return this._capability;
913
+ let probed;
914
+ try {
915
+ probed = await this._harness.capability?.();
916
+ }
917
+ catch (err) {
918
+ this._log?.warn('assistant', '_reconcileDeviceProfile: harness.capability failed', err);
919
+ return null;
920
+ }
921
+ if (!probed?.recommendedModelName)
922
+ return null;
923
+ this._capability = probed;
924
+ return probed;
925
+ }
926
+ async _reconcileDeviceProfile() {
927
+ const deviceId = this._deviceId;
928
+ if (!deviceId || this._reconciling)
929
+ return;
930
+ this._reconciling = true;
931
+ try {
932
+ const capability = await this._ensureCapability();
933
+ if (!capability)
934
+ return;
935
+ const { devices } = await this._engine.listDevices({}).catch(() => ({ devices: [] }));
936
+ const self = (devices ?? []).find((device) => device.id.equals(deviceId));
937
+ if (!self)
938
+ return;
939
+ const profileUpdated = !(self.capabilityProfile === capability.capabilityProfile &&
940
+ self.recommendedModelName === capability.recommendedModelName &&
941
+ self.platform === this._platform);
942
+ const profile = {
943
+ publicKey: b4a.toString(deviceId, 'hex'),
944
+ name: self.name,
945
+ platform: this._platform,
946
+ mobileClass: this._mobileClass,
947
+ capabilityProfile: capability.capabilityProfile,
948
+ recommendedModelName: capability.recommendedModelName,
949
+ ...(capability.memoryBudget ? { memoryBudget: capability.memoryBudget } : {})
950
+ };
951
+ if (profileUpdated) {
952
+ try {
953
+ await this._engine.saveDeviceProfile({
954
+ capabilityProfile: capability.capabilityProfile,
955
+ recommendedModelName: capability.recommendedModelName,
956
+ platform: this._platform
957
+ });
958
+ }
959
+ catch (err) {
960
+ this._log?.error('assistant', 'saveDeviceProfile failed', profile, err);
961
+ return;
962
+ }
963
+ }
964
+ this._log?.info('assistant', 'device profile resolved', { ...profile, profileUpdated });
965
+ }
966
+ finally {
967
+ this._reconciling = false;
968
+ }
969
+ }
970
+ async _close() {
971
+ if (this._retry)
972
+ clearTimeout(this._retry);
973
+ this._chatListWatch.stop();
974
+ this._agentWatch.stop();
975
+ this._harness.off?.('status', this._onHarnessStatus);
976
+ this._harness.off?.('model', this._onModelCatalogEvent);
977
+ this._warmAbort.abort(RUNNER_CLOSED);
978
+ this._onlineBoot.destroy(new Error('assistant is closing'));
979
+ for (const run of this._runs.values())
980
+ run.controller.abort(RUNNER_CLOSED);
981
+ await this._downloads?.close();
982
+ this._modelCatalog.close();
983
+ await this._requests?.return?.();
984
+ await this._chatList?.return?.();
985
+ await this._agentList?.return?.();
986
+ this._lifecycle.abort(RUNNER_CLOSED);
987
+ await Promise.allSettled([...this._runs.values()].map((run) => run.done));
988
+ }
989
+ get suspended() {
990
+ return this._state === QvacState.SUSPEND;
991
+ }
992
+ async presuspend() {
993
+ if (this.closing)
994
+ return;
995
+ this._state = QvacState.PRESUSPEND;
996
+ this._log?.info('assistant', 'presuspend');
997
+ }
998
+ async suspend() {
999
+ this._log?.info('assistant', 'suspend called');
1000
+ if (this._state === QvacState.SUSPEND)
1001
+ return;
1002
+ if (this.closing)
1003
+ return;
1004
+ this._state = QvacState.SUSPEND;
1005
+ this._log?.info('assistant', 'suspending', { runs: this._runs.size });
1006
+ if (this._retry)
1007
+ clearTimeout(this._retry);
1008
+ this._chatListWatch.stop();
1009
+ this._agentWatch.stop();
1010
+ await this._downloads?.suspend();
1011
+ await this._requests?.return?.();
1012
+ await this._chatList?.return?.();
1013
+ await this._agentList?.return?.();
1014
+ for (const run of this._runs.values())
1015
+ run.controller.abort(SUSPENDED);
1016
+ this._warmedModel = false;
1017
+ this._warmedModelName = null;
1018
+ this._modelCatalog.notify();
1019
+ }
1020
+ async wakeup() {
1021
+ if (this.closing)
1022
+ return;
1023
+ this._state = QvacState.WAKEUP;
1024
+ this._log?.info('assistant', 'wakeup');
1025
+ await this.resume();
1026
+ this._triggerModelWarm();
1027
+ }
1028
+ async resume() {
1029
+ this._log?.info('assistant', 'resume called');
1030
+ if (this._state === QvacState.RESUME)
1031
+ return;
1032
+ if (this.closing)
1033
+ return;
1034
+ if (this._state === QvacState.PRESUSPEND) {
1035
+ this._state = QvacState.RESUME;
1036
+ return;
1037
+ }
1038
+ this._state = QvacState.RESUME;
1039
+ this._log?.info('assistant', 'resuming');
1040
+ await Promise.allSettled([...this._runs.values()].map((run) => run.done));
1041
+ this._downloads?.resume();
1042
+ if (this.opened) {
1043
+ this._startConsume();
1044
+ this._chatListWatch.start();
1045
+ this._agentWatch.start();
1046
+ }
1047
+ }
1048
+ async listModels(options) {
1049
+ return (await this._harness.list?.(options)) ?? [];
1050
+ }
1051
+ async deleteModel(name) {
1052
+ return (await this._harness.deleteModel?.(name)) ?? { name, removed: 0, freedBytes: 0 };
1053
+ }
1054
+ modelCatalogWatch() {
1055
+ return this._modelCatalog.watch();
1056
+ }
1057
+ async retryModelDownload(name) {
1058
+ if (!this._downloads)
1059
+ throw new Error('model download reconciler is unavailable');
1060
+ await this._downloads.retry(name);
1061
+ }
1062
+ async skills(agentId, modelName, options) {
1063
+ if (!this.opened)
1064
+ await this.ready();
1065
+ if (agentId) {
1066
+ const agent = await this._engine.getAgent({ id: agentId }).catch(() => null);
1067
+ const deviceId = this._deviceId;
1068
+ if (agent && deviceId && !agent.providerDeviceId?.equals(deviceId)) {
1069
+ agentId = null;
1070
+ options = { ...options, skipCredentialProbe: true };
1071
+ }
1072
+ }
1073
+ const resolved = (await this._harness.skills?.(agentId, modelName, options)) ?? [];
1074
+ this._recordSkillReadiness().catch(noop);
1075
+ return resolved;
1076
+ }
1077
+ async _recordSkillReadiness() {
1078
+ this._readinessDirty = true;
1079
+ if (this._recordingReadiness)
1080
+ return;
1081
+ this._recordingReadiness = true;
1082
+ try {
1083
+ while (this._readinessDirty) {
1084
+ this._readinessDirty = false;
1085
+ const unscoped = (await this._harness.skills?.(null, null, { skipCredentialProbe: true })) ?? [];
1086
+ const ready = unscoped
1087
+ .filter((skill) => isSetUpHere(skill.status))
1088
+ .map((skill) => skill.name);
1089
+ const deviceId = this._deviceId;
1090
+ if (deviceId) {
1091
+ const { agents } = await this._engine.listAgents({});
1092
+ const local = (agents ?? []).filter((agent) => !agent.deletedAt && agent.providerDeviceId?.equals(deviceId));
1093
+ for (const agent of local) {
1094
+ const resolved = (await this._harness.skills?.(agent.id, agent.modelName, {
1095
+ skipCredentialProbe: true
1096
+ })) ?? [];
1097
+ ready.push(...encodeAgentSkillReadiness(agent.id, resolved.filter((skill) => isSetUpHere(skill.status)).map((skill) => skill.name)));
1098
+ }
1099
+ }
1100
+ ready.sort();
1101
+ if (this._readySkills && sameNameList(this._readySkills, ready))
1102
+ continue;
1103
+ this._readySkills = ready;
1104
+ await this._reportCapabilities();
1105
+ }
1106
+ }
1107
+ finally {
1108
+ this._recordingReadiness = false;
1109
+ }
1110
+ }
1111
+ async tools() {
1112
+ return (await this._harness.tools?.()) ?? [];
1113
+ }
1114
+ _startConsume() {
1115
+ this._consume().then(() => this._rearmConsume(null), (err) => this._rearmConsume(err));
1116
+ }
1117
+ _rearmingWatch(label, run) {
1118
+ let retryMs = CONSUME_RETRY_MS;
1119
+ let timer = null;
1120
+ const rearm = (err) => {
1121
+ if (this.closing || this.suspended)
1122
+ return;
1123
+ if (err !== null && !isMeshTransition(err)) {
1124
+ this._log?.error('assistant', `${label} watch failed — restarting`, err);
1125
+ }
1126
+ const delay = isMeshTransition(err) ? retryMs : CONSUME_RETRY_MS;
1127
+ retryMs = nextRetryDelay(err, delay);
1128
+ timer = setTimeout(start, delay);
1129
+ };
1130
+ const start = () => {
1131
+ run().then(() => rearm(null), (err) => rearm(err));
1132
+ };
1133
+ return {
1134
+ start,
1135
+ stop: () => {
1136
+ if (timer)
1137
+ clearTimeout(timer);
1138
+ }
1139
+ };
1140
+ }
1141
+ async _watchChatList() {
1142
+ const stream = this._engine.chatsWatch({});
1143
+ this._chatList = stream[Symbol.asyncIterator]();
1144
+ let known = null;
1145
+ while (true) {
1146
+ const { value, done } = await this._chatList.next();
1147
+ if (done || this.closing || this.suspended)
1148
+ return;
1149
+ const ids = (value.chats ?? []).map((chat) => chat.id);
1150
+ if (known !== null) {
1151
+ for (const chatId of disappearedChatIds(known, ids)) {
1152
+ this._reconciledCompactions.delete(chatId);
1153
+ void this._deleteChatKvCache(chatId, 'chat deleted');
1154
+ }
1155
+ }
1156
+ known = new Set(ids);
1157
+ await this._sweepImageCache();
1158
+ }
1159
+ }
1160
+ async _watchAgents() {
1161
+ const stream = this._engine.agentsWatch({});
1162
+ this._agentList = stream[Symbol.asyncIterator]();
1163
+ let known = null;
1164
+ while (true) {
1165
+ const { value, done } = await this._agentList.next();
1166
+ if (done || this.closing || this.suspended)
1167
+ return;
1168
+ const agents = value.agents ?? [];
1169
+ const next = new Map(agents.map((agent) => [agent.id, agentRekeyFingerprint(agent)]));
1170
+ if (known !== null) {
1171
+ for (const agent of agents) {
1172
+ const previous = known.get(agent.id);
1173
+ if (previous !== undefined && previous !== next.get(agent.id)) {
1174
+ void this._reclaimAgentChatCaches(agent.id);
1175
+ }
1176
+ }
1177
+ }
1178
+ known = next;
1179
+ }
1180
+ }
1181
+ async _reclaimAgentChatCaches(agentId) {
1182
+ try {
1183
+ const { chats } = await this._engine.listChats({});
1184
+ for (const chat of chats ?? []) {
1185
+ if (chat.agentId === agentId)
1186
+ await this._deleteChatKvCache(chat.id, 'agent re-key');
1187
+ }
1188
+ }
1189
+ catch (err) {
1190
+ this._log?.warn('assistant', 'agent re-key cache reclaim failed', { agentId, err });
1191
+ }
1192
+ }
1193
+ async _reconcileCompaction(chatId) {
1194
+ const deviceId = this._deviceId;
1195
+ const { chunks } = await this._engine
1196
+ .listChunks({ chatId, type: 'compaction' })
1197
+ .catch(() => ({ chunks: [] }));
1198
+ let newest = null;
1199
+ for (const chunk of chunks ?? [])
1200
+ if (!newest || chunk.seq > newest.seq)
1201
+ newest = chunk;
1202
+ if (!newest || newest.originDeviceId?.equals(deviceId))
1203
+ return;
1204
+ if (this._reconciledCompactions.get(chatId) === newest.id)
1205
+ return;
1206
+ const gone = await this._deleteChatKvCache(chatId, 'compaction from another device');
1207
+ if (gone)
1208
+ this._reconciledCompactions.set(chatId, newest.id);
1209
+ }
1210
+ async deleteChatKvCache(chatId) {
1211
+ return ((await this._harness.deleteKvCache?.(chatKvCacheKey(chatId))) ?? {
1212
+ status: 'unsupported',
1213
+ error: 'Harness does not support KV cache deletion'
1214
+ });
1215
+ }
1216
+ async _deleteChatKvCache(chatId, reason) {
1217
+ try {
1218
+ const result = await this.deleteChatKvCache(chatId);
1219
+ if (result.status === 'failure') {
1220
+ this._log?.warn('assistant', 'chat kv-cache deletion failed', {
1221
+ chatId,
1222
+ reason,
1223
+ error: result.error
1224
+ });
1225
+ return false;
1226
+ }
1227
+ return true;
1228
+ }
1229
+ catch (err) {
1230
+ this._log?.warn('assistant', 'chat kv-cache deletion failed', { chatId, reason, err });
1231
+ return false;
1232
+ }
1233
+ }
1234
+ _rearmConsume(err) {
1235
+ if (this.closing || this.suspended)
1236
+ return;
1237
+ if (err !== null && !isMeshTransition(err)) {
1238
+ this._log?.error('assistant', 'run-request watch failed — restarting', err);
1239
+ }
1240
+ else {
1241
+ this._log?.debug('assistant', 'rearming consume', {
1242
+ cause: err === null ? 'stream end' : 'mesh transition'
1243
+ });
1244
+ }
1245
+ this._lastCapabilities = null;
1246
+ const delay = isMeshTransition(err) ? this._retryMs : CONSUME_RETRY_MS;
1247
+ this._retryMs = nextRetryDelay(err, delay);
1248
+ this._retry = setTimeout(() => this._startConsume(), delay);
1249
+ }
1250
+ async _consume() {
1251
+ const stream = this._engine.runRequestsWatch({});
1252
+ this._requests = stream[Symbol.asyncIterator]();
1253
+ this._reportCapabilities().catch(noop);
1254
+ this._reconcileDeviceProfile().catch(noop);
1255
+ while (true) {
1256
+ const { value, done } = await this._requests.next();
1257
+ if (done || this.closing || this.suspended)
1258
+ return;
1259
+ const requests = value.chunks ?? [];
1260
+ for (const chunk of requests)
1261
+ this._dispatch(chunk);
1262
+ const live = new Set(requests.map((c) => c.runId));
1263
+ for (const id of this._seen)
1264
+ if (!live.has(id))
1265
+ this._seen.delete(id);
1266
+ }
1267
+ }
1268
+ _dispatch(request) {
1269
+ const runId = request.runId;
1270
+ if (!runId || this._runs.has(runId) || this._seen.has(runId))
1271
+ return;
1272
+ this._seen.add(runId);
1273
+ const settle = (async () => {
1274
+ const agentId = request.runRequest?.agentId;
1275
+ const agent = await this._engine.getAgent({ id: agentId }).catch(() => null);
1276
+ if (!agent) {
1277
+ this._seen.delete(runId);
1278
+ return;
1279
+ }
1280
+ if (!agent.providerDeviceId?.equals(this._deviceId))
1281
+ return;
1282
+ if (await this._isHandled(request))
1283
+ return;
1284
+ this._log?.info('runs', 'run claimed', { runId, chatId: request.chatId, agentId });
1285
+ const controller = new AbortController();
1286
+ const prior = this._chats.get(request.chatId) ?? Promise.resolve();
1287
+ const done = prior.then(() => this._execute(request, agent, controller));
1288
+ const link = done.catch(noop);
1289
+ this._chats.set(request.chatId, link);
1290
+ this._runs.set(runId, {
1291
+ controller,
1292
+ done,
1293
+ chatId: request.chatId,
1294
+ model: agent.modelName,
1295
+ executing: false
1296
+ });
1297
+ try {
1298
+ await done;
1299
+ }
1300
+ finally {
1301
+ this._runImageCacheNames.delete(runId);
1302
+ this._runs.delete(runId);
1303
+ this._yieldDownloads();
1304
+ if (this._chats.get(request.chatId) === link)
1305
+ this._chats.delete(request.chatId);
1306
+ this._reportCapabilities().catch(noop);
1307
+ }
1308
+ })();
1309
+ settle.catch((err) => this._log?.error('assistant', `run ${runId} failed`, err));
1310
+ }
1311
+ async _citeWebSources(transcript, runId, result) {
1312
+ const records = webSourcesFromToolResult(result);
1313
+ if (records.length === 0)
1314
+ return;
1315
+ try {
1316
+ await transcript.chunk({ type: 'sources', sources: { sources: records } });
1317
+ }
1318
+ catch (err) {
1319
+ this._log?.warn('assistant', 'web sources chunk skipped', { runId, err });
1320
+ }
1321
+ }
1322
+ async _searchKnowledge(chatId, messages, attachmentIds, signal, preparing) {
1323
+ let lastUser;
1324
+ for (let index = messages.length - 1; index >= 0; index--) {
1325
+ if (messages[index].role === 'user') {
1326
+ lastUser = messages[index];
1327
+ break;
1328
+ }
1329
+ }
1330
+ const query = lastUser?.content.trim();
1331
+ if (!lastUser || !query)
1332
+ return null;
1333
+ try {
1334
+ const search = await searchKnowledge(this._engine, {
1335
+ chatId,
1336
+ query,
1337
+ attachmentIds: attachmentIds.length > 0 ? [...attachmentIds] : undefined
1338
+ }, { signal, preparing });
1339
+ if (!search.enabled)
1340
+ return null;
1341
+ if (!search.hasKnowledge)
1342
+ return null;
1343
+ return { search, lastUser, hits: search.hits ?? [] };
1344
+ }
1345
+ catch (err) {
1346
+ if (err instanceof AbortedError)
1347
+ return null;
1348
+ this._log?.warn('assistant', 'knowledge search unavailable', err);
1349
+ return null;
1350
+ }
1351
+ }
1352
+ async _isHandled(request) {
1353
+ const { chunks } = await this._engine.listChunks({
1354
+ chatId: request.chatId,
1355
+ type: 'run-status'
1356
+ });
1357
+ return (chunks ?? []).some((c) => c.runId === request.runId && TERMINAL_STATES.has(c.runStatus?.state ?? ''));
1358
+ }
1359
+ async _execute(request, agent, controller) {
1360
+ const { chatId } = request;
1361
+ const runId = request.runId;
1362
+ const deviceId = this._deviceId;
1363
+ if (this.suspended) {
1364
+ this._seen.delete(runId);
1365
+ return;
1366
+ }
1367
+ if (await this._isHandled(request))
1368
+ return;
1369
+ const fresh = await this._engine.getAgent({ id: agent.id }).catch(() => null);
1370
+ if (!fresh || fresh.deletedAt || !fresh.providerDeviceId?.equals(deviceId))
1371
+ return;
1372
+ agent = fresh;
1373
+ await this._reconcileCompaction(chatId);
1374
+ if (request.parentId)
1375
+ await this._deleteChatKvCache(chatId, 'superseding run');
1376
+ const model = runnableModelName(agent.modelName, await this._ensureCapability(), this._mobileClass);
1377
+ if (model !== agent.modelName) {
1378
+ this._log?.warn('runs', 'model clamped to what this device can run', {
1379
+ runId,
1380
+ chatId,
1381
+ requested: agent.modelName,
1382
+ model
1383
+ });
1384
+ const inflight = this._runs.get(runId);
1385
+ if (inflight)
1386
+ this._runs.set(runId, { ...inflight, model });
1387
+ }
1388
+ const config = await this._engine.getConfig({}).catch(noop);
1389
+ const configuredAgent = withConfigDefaults(agent, config);
1390
+ this._log?.info('runs', 'run model resolved', {
1391
+ runId,
1392
+ model,
1393
+ agentModel: agent.modelName,
1394
+ warmedModel: this._warmedModelName
1395
+ });
1396
+ const transcript = new RunTranscript(this._engine, {
1397
+ chatId,
1398
+ runId,
1399
+ deviceId,
1400
+ model,
1401
+ log: this._log
1402
+ });
1403
+ await this._compactIfNearFull({
1404
+ chatId,
1405
+ transcript,
1406
+ trigger: 'before-run',
1407
+ signal: controller.signal
1408
+ });
1409
+ const toolConfigsPromise = this._loadToolConfigs(agent.id);
1410
+ const fsGrantsPromise = this._loadFsGrants(agent.id);
1411
+ const all = await listHistoryChunks(this._engine, chatId);
1412
+ const branches = historyBranches(all, runId);
1413
+ const currentRepair = branches.repairByRunId.get(runId);
1414
+ const repairParent = currentRepair
1415
+ ? all.find((chunk) => chunk.id === currentRepair.parentId)
1416
+ : undefined;
1417
+ const acceptedRepairs = acceptedRepairCount(branches, runId);
1418
+ const history = all.filter((chunk) => Boolean(chunk.compaction) ||
1419
+ (chunk.runId !== runId &&
1420
+ inputAvailableToRun(chunk, request.seq) &&
1421
+ (!repairParent || chunk.seq <= repairParent.seq || chunk.runId === repairParent.runId) &&
1422
+ isPromptChunk(chunk, branches)));
1423
+ const { chunks: priorAsks } = await this._engine.listChunks({
1424
+ chatId,
1425
+ type: 'approval-request'
1426
+ });
1427
+ const { chunks: priorResults } = await this._engine.listChunks({ chatId, type: 'tool-result' });
1428
+ let ran = (priorResults ?? []).some((chunk) => chunk.runId === runId &&
1429
+ chunk.toolResult?.name !== SKILL_TOOL &&
1430
+ !isDeniedResult(chunk.toolResult?.result));
1431
+ const messages = historyFromChunks(history, staleToolRunIds(history, priorAsks ?? [], runId));
1432
+ if (currentRepair)
1433
+ messages.push({ role: 'user', content: currentRepair.promptText });
1434
+ const diagramRepair = currentRepair !== undefined;
1435
+ const runAgent = diagramRepair
1436
+ ? {
1437
+ ...configuredAgent,
1438
+ toolsScope: 'selected',
1439
+ enabledTools: [],
1440
+ skillsScope: 'selected',
1441
+ enabledSkills: []
1442
+ }
1443
+ : configuredAgent;
1444
+ const seesImages = this._harness.supportsImagesFor
1445
+ ? this._harness.supportsImagesFor(model)
1446
+ : (this._harness.supportsImages ?? this._info.supportsImages) !== false;
1447
+ let unreadableImages = new Map();
1448
+ if (!seesImages) {
1449
+ const dropped = messages.reduce((count, turn) => count + (turn.images?.length ?? 0), 0);
1450
+ if (dropped && (this._harness.supportsImages ?? this._info.supportsImages)) {
1451
+ this._log?.warn('assistant', `no projection pairing for ${model} — dropped ${dropped} image(s)`);
1452
+ }
1453
+ for (const turn of messages) {
1454
+ if (turn.images?.length) {
1455
+ const names = turn.images.map((image) => image.fileName).join(', ');
1456
+ turn.content += `\n[Attached image${turn.images.length > 1 ? 's' : ''}: ${names} — not visible to this model]`;
1457
+ }
1458
+ delete turn.images;
1459
+ }
1460
+ }
1461
+ else {
1462
+ unreadableImages = await this._hydrateImages(messages, controller.signal, runId);
1463
+ }
1464
+ const settledWatch = this._watchSettled(chatId, runId, controller);
1465
+ const ownershipWatch = this._watchOwnership(agent.id, controller);
1466
+ let readinessError = null;
1467
+ try {
1468
+ await this._downloads?.waitUntilCached(model, controller.signal);
1469
+ }
1470
+ catch (err) {
1471
+ readinessError = err instanceof Error ? err : new Error(String(err));
1472
+ }
1473
+ if (controller.signal.aborted) {
1474
+ try {
1475
+ const outcome = this._abortOutcome(runId, controller.signal.reason);
1476
+ if (outcome.kind === 'replay')
1477
+ this._seen.delete(runId);
1478
+ if (outcome.kind === 'failed') {
1479
+ await transcript.status({ state: 'failed', reason: outcome.reason });
1480
+ }
1481
+ }
1482
+ finally {
1483
+ transcript.close();
1484
+ ownershipWatch.stop();
1485
+ settledWatch.stop();
1486
+ }
1487
+ return;
1488
+ }
1489
+ if (readinessError) {
1490
+ try {
1491
+ await transcript.status({ state: 'failed', reason: runFailureNotice(readinessError) });
1492
+ }
1493
+ finally {
1494
+ transcript.close();
1495
+ ownershipWatch.stop();
1496
+ settledWatch.stop();
1497
+ }
1498
+ return;
1499
+ }
1500
+ const inflight = this._runs.get(runId);
1501
+ if (inflight)
1502
+ this._runs.set(runId, { ...inflight, executing: true });
1503
+ this._yieldDownloads();
1504
+ const busyChatId = this._modelHolder(runId, chatId, model);
1505
+ const executing = await transcript.status(busyChatId
1506
+ ? { state: 'executing', progress: { phase: 'queued', busyChatId } }
1507
+ : { state: 'executing' });
1508
+ const runStatusId = executing.id;
1509
+ const approvals = this._createApprove(chatId, runId, deviceId, agent.id, () => ran);
1510
+ const modelWatch = this._watchModelProgress((runStatus) => transcript.progress(runStatusId, runStatus), busyChatId !== null);
1511
+ let metrics;
1512
+ const sourceTagFilter = createRagSourceTagFilter();
1513
+ const retrievedRecords = [];
1514
+ let answerText = '';
1515
+ let ragInjected = false;
1516
+ const flushTranscript = async () => {
1517
+ const tail = sourceTagFilter.flush();
1518
+ if (tail) {
1519
+ answerText += tail;
1520
+ await transcript.text('content', tail);
1521
+ }
1522
+ await transcript.flush();
1523
+ };
1524
+ let titleOutcome = null;
1525
+ try {
1526
+ const attachTurnIds = attachTurnDocumentIds(history);
1527
+ if (attachTurnIds.length > 0)
1528
+ backfillDocumentOnlyTurn(messages);
1529
+ const knowledge = !diagramRepair &&
1530
+ (toolsetEnabled('knowledge', runAgent.disabledToolsets) || attachTurnIds.length > 0)
1531
+ ? await this._searchKnowledge(chatId, messages, attachTurnIds, controller.signal, modelWatch.documents)
1532
+ : null;
1533
+ if (controller.signal.aborted) {
1534
+ const outcome = this._abortOutcome(runId, controller.signal.reason);
1535
+ if (outcome.kind === 'replay') {
1536
+ this._seen.delete(runId);
1537
+ return;
1538
+ }
1539
+ if (outcome.kind === 'handoff')
1540
+ return;
1541
+ await transcript.status({ state: 'failed', reason: outcome.reason, metrics });
1542
+ return;
1543
+ }
1544
+ let documentsInjected = false;
1545
+ let sourcesWritten = false;
1546
+ if (knowledge) {
1547
+ try {
1548
+ if (knowledge.search.errorMessage) {
1549
+ this._log?.warn('assistant', 'knowledge search failed', {
1550
+ error: knowledge.search.errorMessage
1551
+ });
1552
+ }
1553
+ const fullTextOnly = knowledge.hits.length > 0 && knowledge.hits.every(isFullTextHit);
1554
+ if (fullTextOnly) {
1555
+ knowledge.lastUser.content = `${formatFullDocumentContext(knowledge.hits)}\n\nThe user's message:\n${knowledge.lastUser.content}`;
1556
+ documentsInjected = true;
1557
+ if (knowledge.hits.some(isTruncatedFullTextHit)) {
1558
+ await transcript.chunk({
1559
+ type: 'sources',
1560
+ sources: { sources: [], notice: RAG_TRUNCATED_NOTICE }
1561
+ });
1562
+ sourcesWritten = true;
1563
+ }
1564
+ }
1565
+ else {
1566
+ const records = sourcesFromHits(knowledge.hits);
1567
+ const notice = chooseRagNotice({
1568
+ errorMessage: knowledge.search.errorMessage,
1569
+ hitCount: records.length,
1570
+ searchedCount: knowledge.search.searchedCount ?? 0,
1571
+ indexEmpty: Boolean(knowledge.search.indexEmpty),
1572
+ hasKnowledge: Boolean(knowledge.search.hasKnowledge),
1573
+ pendingReferenced: Boolean(knowledge.search.pendingReferenced),
1574
+ failedFiles: knowledge.search.failedFiles ?? [],
1575
+ pendingFiles: knowledge.search.pendingFiles ?? []
1576
+ });
1577
+ sourceTagFilter.add(records.map((record) => record.title));
1578
+ retrievedRecords.push(...records.map((record, index) => ({
1579
+ ...record,
1580
+ excerpt: knowledge.hits[index]?.content ?? record.excerpt
1581
+ })));
1582
+ await transcript.chunk({
1583
+ type: 'sources',
1584
+ sources: { sources: records, notice: notice ?? undefined }
1585
+ });
1586
+ sourcesWritten = true;
1587
+ if (records.length > 0) {
1588
+ knowledge.lastUser.content = `${formatRagContext(knowledge.hits, knowledge.search.availableFiles ?? [])}\n\nThe user's message:\n${knowledge.lastUser.content}\n\n${RAG_CITATION_LINE}`;
1589
+ ragInjected = true;
1590
+ documentsInjected = true;
1591
+ }
1592
+ else if (knowledge.search.availableFiles?.length) {
1593
+ knowledge.lastUser.content += `\n\n[Files indexed and available now: ${knowledge.search.availableFiles.join(', ')}. No relevant excerpts were retrieved for this question. Their current availability supersedes earlier indexing notices.]`;
1594
+ }
1595
+ }
1596
+ if (knowledge.search.pendingFiles?.length) {
1597
+ knowledge.lastUser.content += `\n\n[Files not yet available for this reply: ${knowledge.search.pendingFiles.join(', ')}.]`;
1598
+ }
1599
+ }
1600
+ catch (err) {
1601
+ this._log?.warn('assistant', 'knowledge injection skipped', err);
1602
+ }
1603
+ }
1604
+ const hasAudio = messages.some((turn) => turn.audioAttachments?.length);
1605
+ if (hasAudio)
1606
+ modelWatch.transcribe(true);
1607
+ await this._hydrateAudio(messages, controller.signal);
1608
+ if (hasAudio)
1609
+ modelWatch.transcribe(false);
1610
+ backfillImageOnlyTurns(messages);
1611
+ annotateDocumentAttachments(messages);
1612
+ if (!diagramRepair && attachTurnIds.length > 0 && !documentsInjected) {
1613
+ const reason = documentUnreadReason({
1614
+ knowledgeOn: toolsetEnabled('knowledge', runAgent.disabledToolsets),
1615
+ failedFiles: knowledge?.search?.failedFiles,
1616
+ attachedCount: attachTurnIds.length
1617
+ });
1618
+ const lastUser = messages.findLast((turn) => turn.role === 'user');
1619
+ if (lastUser) {
1620
+ lastUser.content = `${lastUser.content}\n${documentUnreadNotice(reason, attachTurnIds.length > 1)}`;
1621
+ }
1622
+ if (!sourcesWritten) {
1623
+ try {
1624
+ await transcript.chunk({
1625
+ type: 'sources',
1626
+ sources: {
1627
+ sources: [],
1628
+ notice: reason === 'knowledge-off' ? RAG_KNOWLEDGE_OFF_NOTICE : RAG_UNAVAILABLE_NOTICE
1629
+ }
1630
+ });
1631
+ }
1632
+ catch (err) {
1633
+ this._log?.warn('assistant', 'unread-document notice skipped', err);
1634
+ }
1635
+ }
1636
+ }
1637
+ annotateUnreadableImages(unreadableImages);
1638
+ const toolConfigs = await toolConfigsPromise;
1639
+ const { roots, write } = await fsGrantsPromise;
1640
+ const { grantedDirs, grantedDrives, driveOp } = await resolveFsGrants(roots, this._drives, (key, err) => this._log?.warn('assistant', 'drive grant dropped', {
1641
+ key,
1642
+ error: err instanceof Error ? err.message : String(err)
1643
+ }));
1644
+ if (await this._isHandled(request))
1645
+ return;
1646
+ const cacheKey = diagramRepair ? undefined : chatRepairKvCacheKey(chatId, acceptedRepairs);
1647
+ this._log?.info('runs', 'completion requested', {
1648
+ runId,
1649
+ model,
1650
+ cacheKey,
1651
+ turns: messages.length
1652
+ });
1653
+ const events = this._harness.completion({
1654
+ model,
1655
+ agent: runAgent,
1656
+ messages,
1657
+ cacheKey,
1658
+ thinkingDefault: config ? (config.thinkingEnabled ? 'enabled' : 'disabled') : undefined,
1659
+ toolConfigs,
1660
+ grantedDirs,
1661
+ grantedDrives,
1662
+ driveOp,
1663
+ grantedWriteDirs: write,
1664
+ approve: approvals.approve,
1665
+ attach: (input) => this._uploadGenerated(chatId, runId, deviceId, input),
1666
+ fetchAttachment: (query) => this._fetchAttachment(chatId, query),
1667
+ ragSearch: async (input) => {
1668
+ const found = await raceAbort(this._engine.ragSearch({
1669
+ chatId,
1670
+ query: input.query,
1671
+ mode: 'tool',
1672
+ limit: input.limit
1673
+ }), controller.signal);
1674
+ const hits = found.hits ?? [];
1675
+ const records = sourcesFromHits(hits);
1676
+ if (records.length > 0) {
1677
+ try {
1678
+ sourceTagFilter.add(records.map((record) => record.title));
1679
+ retrievedRecords.push(...records.map((record, index) => ({
1680
+ ...record,
1681
+ excerpt: hits[index]?.content ?? record.excerpt
1682
+ })));
1683
+ await transcript.chunk({
1684
+ type: 'sources',
1685
+ sources: { sources: records }
1686
+ });
1687
+ }
1688
+ catch (err) {
1689
+ this._log?.warn('assistant', 'rag tool sources chunk skipped', { runId, err });
1690
+ }
1691
+ }
1692
+ return {
1693
+ enabled: found.enabled === true,
1694
+ indexEmpty: found.indexEmpty === true,
1695
+ hasKnowledge: found.hasKnowledge === true,
1696
+ pendingReferenced: found.pendingReferenced === true,
1697
+ errorMessage: found.errorMessage ?? undefined,
1698
+ hits
1699
+ };
1700
+ },
1701
+ progress: (update) => modelWatch.image(update),
1702
+ generationParams: diagramRepair ? { reasoning_budget: 0 } : undefined,
1703
+ signal: controller.signal
1704
+ });
1705
+ let harnessEvents = 0;
1706
+ for await (const event of events) {
1707
+ harnessEvents++;
1708
+ modelWatch.clear();
1709
+ if (event.type === 'content' || event.type === 'thinking') {
1710
+ const text = event.type === 'content'
1711
+ ? sourceTagFilter.push(event.text)
1712
+ : event.text;
1713
+ if (text) {
1714
+ if (event.type === 'content')
1715
+ answerText += text;
1716
+ await transcript.text(event.type, text);
1717
+ }
1718
+ continue;
1719
+ }
1720
+ if (event.type === 'metrics') {
1721
+ const { type, ...rest } = event;
1722
+ metrics = rest;
1723
+ this._log?.info('runs', 'run metrics', { runId, ...runMetricsSummary(rest) });
1724
+ continue;
1725
+ }
1726
+ await flushTranscript();
1727
+ if (event.type === 'error') {
1728
+ const folded = isContextOverflowFailure(event.message) &&
1729
+ (await this._runCompaction({
1730
+ chatId,
1731
+ transcript,
1732
+ trigger: 'overflow',
1733
+ signal: controller.signal
1734
+ }));
1735
+ await transcript.status({
1736
+ state: 'failed',
1737
+ reason: runFailureNotice(event.message),
1738
+ metrics
1739
+ });
1740
+ if (folded) {
1741
+ await this._engine
1742
+ .runAgent({ chatId, agentId: agent.id, parentRunId: runId })
1743
+ .catch((err) => this._log?.warn('runs', 'compaction retry not queued', { runId, err }));
1744
+ }
1745
+ titleOutcome = 'ended';
1746
+ return;
1747
+ }
1748
+ if (event.type === 'aborted') {
1749
+ const outcome = this._abortOutcome(runId, controller.signal.reason);
1750
+ if (outcome.kind === 'replay') {
1751
+ this._seen.delete(runId);
1752
+ return;
1753
+ }
1754
+ if (outcome.kind === 'handoff')
1755
+ return;
1756
+ await transcript.status({ state: 'failed', reason: outcome.reason, metrics });
1757
+ titleOutcome = 'ended';
1758
+ return;
1759
+ }
1760
+ if (event.type === 'tool-call') {
1761
+ this._log?.info('runs', 'tool call', { runId, name: event.name });
1762
+ await transcript.chunk({
1763
+ type: 'tool-call',
1764
+ toolCall: {
1765
+ name: event.name,
1766
+ arguments: event.args
1767
+ },
1768
+ effect: event.effect === 'read' ? 'read' : 'write',
1769
+ display: event.display,
1770
+ rawCall: typeof event.rawCall === 'string' ? event.rawCall : undefined
1771
+ });
1772
+ }
1773
+ else if (event.type === 'tool-result') {
1774
+ if (isDeniedResult(event.result)) {
1775
+ this._log?.info('approvals', 'tool ran denied', { runId, tool: event.name });
1776
+ }
1777
+ else if (event.name !== SKILL_TOOL) {
1778
+ ran = true;
1779
+ }
1780
+ if (WEB_TOOLS.includes(String(event.name))) {
1781
+ await this._citeWebSources(transcript, runId, event.result);
1782
+ }
1783
+ await transcript.chunk({
1784
+ type: 'tool-result',
1785
+ toolResult: { name: event.name, result: withoutDataChannel(event.result) }
1786
+ });
1787
+ }
1788
+ else if (event.type === 'tool-activity') {
1789
+ void this._engine
1790
+ .recordToolActivity({
1791
+ agentId: runAgent.id,
1792
+ toolName: event.name,
1793
+ decision: event.decision,
1794
+ effect: event.effect,
1795
+ arguments: event.args,
1796
+ resource: event.resource ?? undefined,
1797
+ chatId,
1798
+ runId,
1799
+ at: Date.now()
1800
+ })
1801
+ .catch((err) => this._log?.warn('runs', 'tool activity record failed', { runId, err }));
1802
+ }
1803
+ else {
1804
+ this._log?.warn('assistant', 'unknown harness event dropped', { type: event.type });
1805
+ }
1806
+ }
1807
+ this._log?.info('runs', 'harness stream ended', { runId, events: harnessEvents });
1808
+ if (await settledWithin(flushTranscript(), FINAL_FLUSH_MS)) {
1809
+ this._log?.info('runs', 'final flush done', { runId });
1810
+ }
1811
+ else {
1812
+ this._log?.warn('runs', 'completing over a stranded flush', {
1813
+ runId,
1814
+ pending: transcript.pending()
1815
+ });
1816
+ }
1817
+ const citedTitles = ragInjected ? citedByPhraseOverlap(answerText, retrievedRecords) : null;
1818
+ const foldedNow = metrics
1819
+ ? await this._compactIfNearFull({
1820
+ chatId,
1821
+ transcript,
1822
+ trigger: 'turn-filled-window',
1823
+ metrics: metrics,
1824
+ signal: controller.signal
1825
+ })
1826
+ : false;
1827
+ const settled = foldedNow ? withoutOccupancy(metrics) : metrics;
1828
+ await transcript.status({
1829
+ state: 'completed',
1830
+ metrics: citedTitles ? { ...settled, citedTitles } : settled
1831
+ });
1832
+ titleOutcome = 'completed';
1833
+ }
1834
+ catch (err) {
1835
+ const outcome = controller.signal.aborted
1836
+ ? this._abortOutcome(runId, controller.signal.reason)
1837
+ : null;
1838
+ if (outcome?.kind === 'replay') {
1839
+ this._seen.delete(runId);
1840
+ return;
1841
+ }
1842
+ if (outcome?.kind === 'handoff')
1843
+ return;
1844
+ await flushTranscript();
1845
+ await transcript.status({ state: 'failed', reason: runFailureNotice(err), metrics });
1846
+ titleOutcome = 'ended';
1847
+ }
1848
+ finally {
1849
+ transcript.close();
1850
+ modelWatch.stop();
1851
+ ownershipWatch.stop();
1852
+ await approvals.settle({ preservePending: isLifecycleTeardown(controller.signal.reason) });
1853
+ settledWatch.stop();
1854
+ if (titleOutcome && !diagramRepair) {
1855
+ void this._autoTitle(chatId, messages, model, runAgent, titleOutcome === 'completed');
1856
+ }
1857
+ }
1858
+ }
1859
+ async _loadToolConfigs(agentId) {
1860
+ const { configs } = await this._engine.listToolConfigs({ agentId }).catch((err) => {
1861
+ this._log?.warn('assistant', 'tool configs read failed', err);
1862
+ return { configs: [] };
1863
+ });
1864
+ const map = {};
1865
+ for (const row of configs ?? []) {
1866
+ if (row?.toolName && row.settings && typeof row.settings === 'object') {
1867
+ map[row.toolName] = row.settings;
1868
+ }
1869
+ }
1870
+ return map;
1871
+ }
1872
+ async _loadFsGrants(agentId) {
1873
+ const [roots, write] = await Promise.all([
1874
+ this._readRootGrants(fsGrantKey(agentId)),
1875
+ this._readWriteGrants(fsWriteGrantKey(agentId))
1876
+ ]);
1877
+ return { roots, write };
1878
+ }
1879
+ async _readRootGrants(key) {
1880
+ const { value } = await this._engine.configGet({ key }).catch((err) => {
1881
+ this._log?.warn('assistant', 'fs grants read failed', { key, err });
1882
+ return { value: null };
1883
+ });
1884
+ return fsGrantsFrom(value);
1885
+ }
1886
+ async _readWriteGrants(key) {
1887
+ const { value } = await this._engine.configGet({ key }).catch((err) => {
1888
+ this._log?.warn('assistant', 'fs write grants read failed', { key, err });
1889
+ return { value: null };
1890
+ });
1891
+ return grantedDirsFrom(value);
1892
+ }
1893
+ async _fetchAttachment(chatId, { attachmentId, signal, meta } = {}) {
1894
+ if (!attachmentId) {
1895
+ const { chunks } = await raceAbort(this._engine.listChunks({ chatId, type: 'attachment' }), signal);
1896
+ attachmentId = (chunks ?? [])
1897
+ .map((chunk) => chunk.attachment)
1898
+ .filter((file) => file && !file.deletedAt && IMAGE_MIMES.has(file.mimeType.toLowerCase()))
1899
+ .at(-1)?.id;
1900
+ }
1901
+ if (!attachmentId)
1902
+ return null;
1903
+ const stream = this._engine.fileRead({ chatId, fileId: attachmentId, metadataOnly: meta });
1904
+ const abort = () => stream.destroy();
1905
+ const pieces = [];
1906
+ let head = null;
1907
+ stream.on('error', noop);
1908
+ signal?.addEventListener('abort', abort, { once: true });
1909
+ try {
1910
+ if (signal?.aborted)
1911
+ throw new AbortedError();
1912
+ for await (const frame of stream) {
1913
+ if (frame.metadata) {
1914
+ const { id, fileName, mimeType, byteLength } = frame.metadata;
1915
+ head = { id, fileName, mimeType, byteLength };
1916
+ }
1917
+ if (frame.data)
1918
+ pieces.push(frame.data);
1919
+ }
1920
+ if (signal?.aborted)
1921
+ throw new AbortedError();
1922
+ return head && (meta ? head : { ...head, bytes: Buffer.concat(pieces) });
1923
+ }
1924
+ finally {
1925
+ signal?.removeEventListener('abort', abort);
1926
+ stream.destroy();
1927
+ }
1928
+ }
1929
+ async _readBlob(url, signal) {
1930
+ if (!url || signal?.aborted)
1931
+ return null;
1932
+ const pending = defaultFetch(url).catch(() => null);
1933
+ try {
1934
+ const response = await raceInactivity(pending, signal, this._blobReadTimeoutMs);
1935
+ const bytes = response?.ok
1936
+ ? await drainBody(response.body, signal, this._blobReadTimeoutMs)
1937
+ : null;
1938
+ return bytes?.length ? bytes : null;
1939
+ }
1940
+ catch {
1941
+ pending.then((response) => response?.body?.cancel(), noop);
1942
+ return null;
1943
+ }
1944
+ }
1945
+ async _uploadGenerated(chatId, runId, deviceId, { fileName, mimeType, bytes, raw }) {
1946
+ const duplex = this._engine.uploadAttachment();
1947
+ duplex.write({ chatId, runId, originDeviceId: deviceId, fileName, mimeType, raw });
1948
+ const buf = Buffer.isBuffer(bytes)
1949
+ ? bytes
1950
+ : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1951
+ for (let i = 0; i < buf.length; i += UPLOAD_FRAME_BYTES) {
1952
+ duplex.write({ data: buf.subarray(i, i + UPLOAD_FRAME_BYTES) });
1953
+ }
1954
+ duplex.end();
1955
+ let attachment = null;
1956
+ for await (const frame of duplex) {
1957
+ if (frame.attachment)
1958
+ attachment = frame.attachment;
1959
+ }
1960
+ if (!attachment)
1961
+ throw new Error('attachment upload ended without a record');
1962
+ return attachment;
1963
+ }
1964
+ async _hydrateAudio(messages, signal) {
1965
+ const turns = messages.filter((turn) => turn.audioAttachments?.length);
1966
+ if (!turns.length)
1967
+ return;
1968
+ for (const turn of turns) {
1969
+ for (const ref of turn.audioAttachments ?? []) {
1970
+ if (signal.aborted)
1971
+ return;
1972
+ const block = await this._transcribeAttachment(ref, signal);
1973
+ turn.content = turn.content.trim() ? `${turn.content}\n\n${block}` : block;
1974
+ }
1975
+ delete turn.audioAttachments;
1976
+ }
1977
+ }
1978
+ async _transcribeAttachment(ref, signal) {
1979
+ const label = `Attached audio "${ref.fileName}"`;
1980
+ const cached = this._audioTranscripts.get(ref.blobId);
1981
+ if (cached !== undefined) {
1982
+ return cached ? audioTranscriptBlock(ref.fileName, cached) : silentAudioNotice(ref.fileName);
1983
+ }
1984
+ if (!this._harness.transcribe) {
1985
+ return unheardAudioNotice(label, 'transcription is unavailable on this device', {
1986
+ retryable: false
1987
+ });
1988
+ }
1989
+ try {
1990
+ const bytes = await this._readBlob(ref.url, signal);
1991
+ if (!bytes) {
1992
+ return unheardAudioNotice(label, 'its content has not reached this device — the upload may still be syncing');
1993
+ }
1994
+ const model = mediaModelNameFor('transcription', await this._persistedProfile());
1995
+ await this.ensureMediaModelUnpaused('transcription', model);
1996
+ await this._downloads?.waitUntilCached(model, signal);
1997
+ const text = (await this._harness.transcribe({ model: '', audio: bytes, signal })).trim();
1998
+ this._audioTranscripts.set(ref.blobId, text);
1999
+ if (!text)
2000
+ return silentAudioNotice(ref.fileName);
2001
+ return audioTranscriptBlock(ref.fileName, text);
2002
+ }
2003
+ catch (err) {
2004
+ if (signal.aborted)
2005
+ throw err;
2006
+ this._log?.warn('assistant', `audio transcription failed — ${ref.fileName}`, err);
2007
+ return unheardAudioNotice(label, 'transcription failed on this device');
2008
+ }
2009
+ }
2010
+ async _hydrateImages(messages, signal, runId) {
2011
+ const unreadable = new Map();
2012
+ const turns = messages.filter((turn) => turn.images?.length);
2013
+ if (!turns.length)
2014
+ return unreadable;
2015
+ const inUse = new Set(turns.flatMap((turn) => (turn.images ?? []).map(imageCacheName)));
2016
+ this._runImageCacheNames.set(runId, inUse);
2017
+ const dir = this._imageCacheDir;
2018
+ await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
2019
+ for (const turn of turns) {
2020
+ const dropped = [];
2021
+ for (const image of turn.images ?? []) {
2022
+ try {
2023
+ const file = await this._hydrateImage(dir, image, signal);
2024
+ turn.attachments = [...(turn.attachments ?? []), { path: file }];
2025
+ }
2026
+ catch (err) {
2027
+ if (signal.aborted)
2028
+ throw err;
2029
+ this._log?.error('assistant', `image hydration failed — skipped ${image.fileName}`, err);
2030
+ dropped.push(image.fileName);
2031
+ }
2032
+ }
2033
+ if (dropped.length)
2034
+ unreadable.set(turn, dropped);
2035
+ delete turn.images;
2036
+ }
2037
+ return unreadable;
2038
+ }
2039
+ async _hydrateImage(dir, image, signal) {
2040
+ const file = path.join(dir, imageCacheName(image));
2041
+ const cached = await fs.promises.stat(file).catch(() => null);
2042
+ if (!cached?.size) {
2043
+ const raw = await this._readBlob(image.url, signal);
2044
+ if (signal.aborted)
2045
+ throw new Error('hydration aborted');
2046
+ if (!raw)
2047
+ throw new Error(`empty download for ${image.fileName}`);
2048
+ const bytes = await normalizeImageForModel(raw);
2049
+ const staging = `${file}.partial-${crypto.randomBytes(4).toString('hex')}`;
2050
+ await fs.promises.writeFile(staging, bytes, { mode: 0o600 });
2051
+ await fs.promises.rename(staging, file);
2052
+ }
2053
+ else {
2054
+ const now = new Date();
2055
+ await fs.promises.utimes(file, now, now).catch(() => null);
2056
+ }
2057
+ return file;
2058
+ }
2059
+ async _compactIfNearFull(params) {
2060
+ const { chatId, transcript, trigger, signal } = params;
2061
+ const metrics = params.metrics ?? (await this._lastRunMetrics(chatId));
2062
+ if (!nearContextLimit(metrics.cacheTokens, metrics.ctxSize))
2063
+ return false;
2064
+ if (!canCompact(metrics.ctxSize)) {
2065
+ this._log?.debug('runs', 'chat near its window but under the compaction floor', {
2066
+ chatId,
2067
+ ctxSize: metrics.ctxSize
2068
+ });
2069
+ return false;
2070
+ }
2071
+ return this._runCompaction({ chatId, transcript, trigger, signal, occupancy: metrics });
2072
+ }
2073
+ async _runCompaction(params) {
2074
+ const { chatId, transcript, trigger, signal } = params;
2075
+ let statusId = null;
2076
+ const started = Date.now();
2077
+ try {
2078
+ const result = await this.compactChat(chatId, {
2079
+ onFold: async () => {
2080
+ const status = await transcript.status({
2081
+ state: 'executing',
2082
+ progress: { phase: 'compacting' }
2083
+ });
2084
+ statusId = status.id;
2085
+ },
2086
+ ...(signal ? { signal } : {})
2087
+ });
2088
+ this._log?.info('runs', 'compaction', {
2089
+ chatId,
2090
+ trigger,
2091
+ compacted: result.compacted,
2092
+ reason: result.reason ?? null,
2093
+ foldedTurns: result.foldedTurns ?? 0,
2094
+ cacheTokens: params.occupancy?.cacheTokens ?? null,
2095
+ ctxSize: params.occupancy?.ctxSize ?? null,
2096
+ announced: statusId !== null,
2097
+ ms: Date.now() - started
2098
+ });
2099
+ return result.compacted;
2100
+ }
2101
+ catch (err) {
2102
+ this._log?.warn('runs', 'compaction failed', { chatId, trigger, err });
2103
+ return false;
2104
+ }
2105
+ finally {
2106
+ if (trigger === 'before-run' && statusId) {
2107
+ await transcript.progress(statusId, { state: 'executing' }).catch(noop);
2108
+ }
2109
+ }
2110
+ }
2111
+ async _lastRunMetrics(chatId) {
2112
+ const deviceId = this._deviceId;
2113
+ const { chunks } = await this._engine
2114
+ .listChunks({ chatId, type: 'run-status' })
2115
+ .catch(() => ({ chunks: [] }));
2116
+ const statuses = chunks ?? [];
2117
+ for (let index = statuses.length - 1; index >= 0; index--) {
2118
+ const chunk = statuses[index];
2119
+ const metrics = chunk.runStatus?.metrics;
2120
+ if (chunk.runStatus?.state !== 'completed' || !metrics)
2121
+ continue;
2122
+ if (!chunk.originDeviceId?.equals(deviceId))
2123
+ return {};
2124
+ const parsed = metrics;
2125
+ return { cacheTokens: parsed.cacheTokens, ctxSize: parsed.ctxSize };
2126
+ }
2127
+ return {};
2128
+ }
2129
+ async compactChat(chatId, options = {}) {
2130
+ if (!this.opened)
2131
+ await this.ready();
2132
+ const deviceId = this._deviceId;
2133
+ const chat = await this._engine.getChat({ id: chatId }).catch(() => null);
2134
+ if (!chat)
2135
+ return { compacted: false, reason: 'unknown chat' };
2136
+ const agent = await this._engine.getAgent({ id: chat.agentId }).catch(() => null);
2137
+ if (!agent || !agent.providerDeviceId?.equals(deviceId)) {
2138
+ return { compacted: false, reason: 'not the provider device' };
2139
+ }
2140
+ const model = runnableModelName(agent.modelName, await this._ensureCapability(), this._mobileClass);
2141
+ const config = await this._engine.getConfig({}).catch(() => null);
2142
+ const ctxSize = agent.ctxSize || config?.ctxSize || undefined;
2143
+ if (ctxSize !== undefined && !canCompact(ctxSize)) {
2144
+ return { compacted: false, reason: 'below-floor' };
2145
+ }
2146
+ const stored = await listHistoryChunks(this._engine, chatId);
2147
+ const branches = historyBranches(stored);
2148
+ const chunks = stored.filter((chunk) => isPromptChunk(chunk, branches));
2149
+ const { chunks: priorAsks } = await this._engine
2150
+ .listChunks({ chatId, type: 'approval-request' })
2151
+ .catch(() => ({ chunks: [] }));
2152
+ const lastThroughId = lastCompactedThroughId(chunks);
2153
+ const lastThroughSeq = chunks.find((chunk) => chunk.id === lastThroughId)?.seq ?? -1;
2154
+ const userTurns = chunks.filter((chunk) => chunk.userMessage).length;
2155
+ let announced = false;
2156
+ let keepTurns = COMPACTION_KEEP_TURNS;
2157
+ for (;;) {
2158
+ const boundary = compactionBoundary(chunks, keepTurns);
2159
+ if (!boundary)
2160
+ return { compacted: false, reason: 'nothing-to-fold' };
2161
+ const through = chunks.find((chunk) => chunk.id === boundary.throughId);
2162
+ if (!through)
2163
+ return { compacted: false, reason: 'nothing-to-fold' };
2164
+ if (through.seq <= lastThroughSeq)
2165
+ return { compacted: false, reason: 'already-compacted' };
2166
+ const inRange = chunks.filter((chunk) => chunk.seq <= through.seq);
2167
+ const folded = historyFromChunks(inRange, staleToolRunIds(inRange, priorAsks ?? [], ''));
2168
+ if (!announced) {
2169
+ await options.onFold?.();
2170
+ announced = true;
2171
+ }
2172
+ const attempt = await this._summarize(folded, model, withConfigDefaults(agent, config), options.signal);
2173
+ if (!attempt.summary) {
2174
+ if (attempt.overflowed && boundary.foldedTurns > 1) {
2175
+ keepTurns = userTurns - Math.floor(boundary.foldedTurns / 2);
2176
+ continue;
2177
+ }
2178
+ return { compacted: false, reason: 'the model produced no summary' };
2179
+ }
2180
+ const summary = attempt.summary;
2181
+ if (!foldReducesPrompt(folded, summary)) {
2182
+ return { compacted: false, reason: 'summary-not-smaller' };
2183
+ }
2184
+ if (options.signal?.aborted)
2185
+ return { compacted: false, reason: 'canceled' };
2186
+ await this._engine.appendChunk({
2187
+ chatId,
2188
+ type: 'compaction',
2189
+ compaction: { summary, throughId: boundary.throughId },
2190
+ originDeviceId: deviceId
2191
+ });
2192
+ await this._deleteChatKvCache(chatId, 'compaction');
2193
+ this._log?.info('runs', 'chat compacted', {
2194
+ chatId,
2195
+ foldedTurns: boundary.foldedTurns,
2196
+ summaryChars: summary.length
2197
+ });
2198
+ return { compacted: true, foldedTurns: boundary.foldedTurns };
2199
+ }
2200
+ }
2201
+ async _summarize(turns, model, agent, signal) {
2202
+ const hasPriorSummary = turns[0]?.content.startsWith(COMPACTION_TURN_PREFIX) === true;
2203
+ const events = this._harness.completion({
2204
+ model,
2205
+ agent: {
2206
+ ...agent,
2207
+ toolsScope: 'selected',
2208
+ enabledTools: [],
2209
+ skillsScope: 'selected',
2210
+ enabledSkills: []
2211
+ },
2212
+ messages: [
2213
+ {
2214
+ role: 'user',
2215
+ content: buildCompactionPrompt(turns.map((turn) => ({ role: turn.role, content: turn.content })), hasPriorSummary)
2216
+ }
2217
+ ],
2218
+ generationParams: { reasoning_budget: 0 },
2219
+ signal: signal ?? this._lifecycle.signal
2220
+ });
2221
+ let text = '';
2222
+ let failed = null;
2223
+ for await (const event of events) {
2224
+ if (event.type === 'content' && typeof event.text === 'string')
2225
+ text += event.text;
2226
+ else if (event.type === 'error') {
2227
+ failed = typeof event.message === 'string' ? event.message : 'summarizer failed';
2228
+ break;
2229
+ }
2230
+ else if (event.type === 'aborted') {
2231
+ failed = 'summarizer aborted';
2232
+ break;
2233
+ }
2234
+ else if (event.type === 'done')
2235
+ break;
2236
+ }
2237
+ if (failed !== null) {
2238
+ this._log?.warn('runs', 'summarizer failed mid-stream', { chars: text.length, failed });
2239
+ return { summary: null, overflowed: isContextOverflowFailure(failed) };
2240
+ }
2241
+ return { summary: parseCompactionSummary(text), overflowed: false };
2242
+ }
2243
+ async _autoTitle(chatId, messages, model, agent, probe) {
2244
+ try {
2245
+ const first = messages.find((turn) => turn.role === 'user' && turn.content.trim());
2246
+ if (!first)
2247
+ return;
2248
+ const chat = await this._engine.getChat({ id: chatId });
2249
+ if (!isDefaultChatTitle(chat?.title))
2250
+ return;
2251
+ await this._generateChatTitle({ chatId, input: first.content, model, agent, probe });
2252
+ }
2253
+ catch (err) {
2254
+ this._log?.warn('runs', 'auto-title skipped', { chatId, err });
2255
+ }
2256
+ }
2257
+ async _generateChatTitle(params) {
2258
+ const { chatId, input, model, agent, probe } = params;
2259
+ const prompt = input.trim();
2260
+ if (!prompt)
2261
+ return null;
2262
+ let generated = null;
2263
+ if (probe && !this.closing) {
2264
+ try {
2265
+ generated = await this._titleFromCompletion({
2266
+ input: prompt,
2267
+ model,
2268
+ agent,
2269
+ signal: this._lifecycle.signal
2270
+ });
2271
+ }
2272
+ catch (err) {
2273
+ this._log?.warn('runs', 'title completion failed', { chatId, err });
2274
+ }
2275
+ }
2276
+ const title = generated ?? deriveChatTitle(prompt);
2277
+ if (!title)
2278
+ return null;
2279
+ await this._engine.renameChat({ id: chatId, title });
2280
+ this._log?.info('runs', 'chat titled', {
2281
+ chatId,
2282
+ title,
2283
+ source: generated ? 'model' : 'heuristic'
2284
+ });
2285
+ return title;
2286
+ }
2287
+ async _titleFromCompletion(params) {
2288
+ const { input, model, agent, signal } = params;
2289
+ const events = this._harness.completion({
2290
+ model,
2291
+ agent: {
2292
+ ...agent,
2293
+ toolsScope: 'selected',
2294
+ enabledTools: [],
2295
+ skillsScope: 'selected',
2296
+ enabledSkills: []
2297
+ },
2298
+ messages: [{ role: 'user', content: buildTitlePrompt(input) }],
2299
+ generationParams: { reasoning_budget: 0 },
2300
+ responseFormat: {
2301
+ type: 'json_schema',
2302
+ json_schema: {
2303
+ name: 'chat_title',
2304
+ schema: {
2305
+ type: 'object',
2306
+ properties: { title: { type: 'string' } },
2307
+ required: ['title'],
2308
+ additionalProperties: false
2309
+ }
2310
+ }
2311
+ },
2312
+ signal
2313
+ });
2314
+ let text = '';
2315
+ for await (const event of events) {
2316
+ if (event.type === 'content' && typeof event.text === 'string')
2317
+ text += event.text;
2318
+ else if (event.type === 'error' || event.type === 'aborted' || event.type === 'done')
2319
+ break;
2320
+ }
2321
+ return parseTitleResponse(text);
2322
+ }
2323
+ _createApprove(chatId, runId, deviceId, agentId, hasRun) {
2324
+ const waiters = new Map();
2325
+ let latestChunks = [];
2326
+ let watcher = null;
2327
+ let closing = false;
2328
+ const pump = async (source) => {
2329
+ while (true) {
2330
+ const { value, done } = await source.next();
2331
+ if (done)
2332
+ return;
2333
+ latestChunks = value.chunks ?? [];
2334
+ for (const chunk of latestChunks) {
2335
+ const status = chunk.approvalRequest?.status;
2336
+ if (!status || status === 'pending')
2337
+ continue;
2338
+ const waiter = waiters.get(chunk.id);
2339
+ if (!waiter)
2340
+ continue;
2341
+ waiters.delete(chunk.id);
2342
+ this._log?.info('approvals', `answer ${status}`, {
2343
+ runId,
2344
+ chatId,
2345
+ chunkId: chunk.id,
2346
+ tool: waiter.tool
2347
+ });
2348
+ waiter.resolve(status === 'approved');
2349
+ }
2350
+ }
2351
+ };
2352
+ const ensureWatch = () => {
2353
+ if (watcher)
2354
+ return;
2355
+ const opened = this._engine
2356
+ .chunksWatch({ chatId, type: 'approval-request' })[Symbol.asyncIterator]();
2357
+ watcher = opened;
2358
+ pump(opened)
2359
+ .catch((err) => {
2360
+ if (closing)
2361
+ this._log?.debug('assistant', 'approval watch closed', err);
2362
+ else
2363
+ this._log?.error('assistant', 'approval watch failed', err);
2364
+ })
2365
+ .finally(() => {
2366
+ for (const [id, waiter] of waiters) {
2367
+ this._log?.warn('approvals', 'deny: answer watch closed', {
2368
+ runId,
2369
+ chunkId: id,
2370
+ tool: waiter.tool
2371
+ });
2372
+ waiter.resolve(false);
2373
+ }
2374
+ if (watcher === opened)
2375
+ watcher = null;
2376
+ });
2377
+ };
2378
+ const asksForRun = async () => {
2379
+ const { chunks } = await this._engine
2380
+ .listChunks({ chatId, type: 'approval-request' })
2381
+ .catch(() => ({ chunks: [] }));
2382
+ return (chunks ?? []).filter((chunk) => chunk.runId === runId);
2383
+ };
2384
+ const sameCall = (chunk, call) => {
2385
+ const ask = chunk.approvalRequest;
2386
+ if (!ask || ask.name !== call.name)
2387
+ return false;
2388
+ return ((ask.resource?.kind ?? null) === (call.resource?.kind ?? null) &&
2389
+ (ask.resource?.label ?? null) === (call.resource?.label ?? null) &&
2390
+ JSON.stringify(ask.arguments) === JSON.stringify(call.arguments));
2391
+ };
2392
+ const park = (chunkId, tool) => {
2393
+ const status = latestChunks.find((c) => c.id === chunkId)?.approvalRequest?.status;
2394
+ if (status && status !== 'pending') {
2395
+ this._log?.info('approvals', `answer ${status} (raced the ask)`, { runId, chunkId, tool });
2396
+ return Promise.resolve(status === 'approved');
2397
+ }
2398
+ return new Promise((resolve) => waiters.set(chunkId, { resolve, tool }));
2399
+ };
2400
+ const approve = async (call) => {
2401
+ const { rules } = await this._engine.listApprovalRules({}).catch((err) => {
2402
+ this._log?.warn('approvals', 'rule lookup failed, asking instead', { runId, err });
2403
+ return { rules: [] };
2404
+ });
2405
+ const rule = matchApprovalRule(rules ?? [], {
2406
+ toolName: call.name,
2407
+ resource: call.resource,
2408
+ agentId
2409
+ });
2410
+ if (rule) {
2411
+ this._log?.info('approvals', `rule ${rule.decision}`, {
2412
+ runId,
2413
+ chatId,
2414
+ tool: call.name,
2415
+ resource: call.resource?.label
2416
+ });
2417
+ return rule.decision === 'allow';
2418
+ }
2419
+ ensureWatch();
2420
+ const prior = (await asksForRun()).findLast((chunk) => sameCall(chunk, call));
2421
+ if (prior) {
2422
+ const status = prior.approvalRequest?.status;
2423
+ if (!status || status === 'pending') {
2424
+ this._log?.info('approvals', 'rebinding the ask this run parked on', {
2425
+ runId,
2426
+ chatId,
2427
+ chunkId: prior.id,
2428
+ tool: call.name
2429
+ });
2430
+ return park(prior.id, call.name);
2431
+ }
2432
+ if (!hasRun()) {
2433
+ this._log?.info('approvals', `answer ${status} (from before the app closed)`, {
2434
+ runId,
2435
+ chatId,
2436
+ chunkId: prior.id,
2437
+ tool: call.name
2438
+ });
2439
+ return status === 'approved';
2440
+ }
2441
+ this._log?.info('approvals', 'asking again: the replay would repeat a call that ran', {
2442
+ runId,
2443
+ chatId,
2444
+ priorChunkId: prior.id,
2445
+ tool: call.name
2446
+ });
2447
+ }
2448
+ const approvalRequest = {
2449
+ status: 'pending',
2450
+ name: call.name,
2451
+ arguments: call.arguments,
2452
+ resource: call.resource ?? undefined,
2453
+ agentId
2454
+ };
2455
+ const chunk = await this._engine.appendChunk({
2456
+ chatId,
2457
+ runId,
2458
+ type: 'approval-request',
2459
+ approvalRequest,
2460
+ originDeviceId: deviceId,
2461
+ effect: call.effect === 'read' ? 'read' : 'write',
2462
+ intent: call.intent ?? undefined,
2463
+ display: call.display ?? undefined
2464
+ });
2465
+ this._log?.info('approvals', 'ask', {
2466
+ runId,
2467
+ chatId,
2468
+ chunkId: chunk.id,
2469
+ tool: call.name,
2470
+ resource: call.resource?.label
2471
+ });
2472
+ return park(chunk.id, call.name);
2473
+ };
2474
+ const settle = async ({ preservePending = false } = {}) => {
2475
+ if (!preservePending) {
2476
+ for (const ask of await asksForRun()) {
2477
+ if (ask.approvalRequest?.status !== 'pending')
2478
+ continue;
2479
+ this._log?.warn('approvals', 'deny: unanswered at run end', {
2480
+ runId,
2481
+ chunkId: ask.id,
2482
+ tool: ask.approvalRequest?.name
2483
+ });
2484
+ await this._engine.resolveApproval({ id: ask.id, approved: false });
2485
+ }
2486
+ for (const waiter of waiters.values())
2487
+ waiter.resolve(false);
2488
+ }
2489
+ waiters.clear();
2490
+ closing = true;
2491
+ await watcher?.return?.();
2492
+ watcher = null;
2493
+ };
2494
+ return { approve, settle };
2495
+ }
2496
+ _abortOutcome(runId, reason) {
2497
+ const known = knownAbortOutcome(reason);
2498
+ if (reason === RUNNER_DIED && this._harnessDeath) {
2499
+ return { kind: 'failed', reason: this._harnessDeath };
2500
+ }
2501
+ if (known)
2502
+ return known;
2503
+ if (this.suspended)
2504
+ return { kind: 'replay' };
2505
+ if (reason !== undefined) {
2506
+ this._log?.warn('runs', 'run aborted for an unrecognised reason', { runId, reason });
2507
+ }
2508
+ return { kind: 'failed', reason: RUN_FAILURE_REASONS.runtimeEndedTurn };
2509
+ }
2510
+ _watchSettled(chatId, runId, controller) {
2511
+ const it = this._engine.chunksWatch({ chatId, type: 'run-status' })[Symbol.asyncIterator]();
2512
+ const watching = (async () => {
2513
+ while (true) {
2514
+ const { value, done } = await it.next();
2515
+ if (done)
2516
+ return;
2517
+ const settled = (value.chunks ?? []).some((c) => c.runId === runId && TERMINAL_STATES.has(c.runStatus?.state ?? ''));
2518
+ if (settled) {
2519
+ controller.abort(RUN_SETTLED);
2520
+ return;
2521
+ }
2522
+ }
2523
+ })();
2524
+ watching.catch((err) => {
2525
+ if (this.closing || isStreamDestroyed(err))
2526
+ return;
2527
+ this._log?.error('assistant', 'settled watch failed', err);
2528
+ });
2529
+ return {
2530
+ stop: () => {
2531
+ it.return?.()?.catch?.(noop);
2532
+ }
2533
+ };
2534
+ }
2535
+ _watchOwnership(agentId, controller) {
2536
+ const deviceId = this._deviceId;
2537
+ const it = this._engine.agentsWatch({})[Symbol.asyncIterator]();
2538
+ const watching = (async () => {
2539
+ while (true) {
2540
+ const { value, done } = await it.next();
2541
+ if (done)
2542
+ return;
2543
+ const row = (value.agents ?? []).find((a) => a.id === agentId);
2544
+ if (!row || row.deletedAt || !row.providerDeviceId?.equals(deviceId)) {
2545
+ controller.abort(OWNERSHIP_LOST);
2546
+ return;
2547
+ }
2548
+ }
2549
+ })();
2550
+ watching.catch((err) => {
2551
+ if (this.closing || isStreamDestroyed(err))
2552
+ return;
2553
+ this._log?.error('assistant', 'ownership watch failed', err);
2554
+ });
2555
+ return {
2556
+ stop: () => {
2557
+ it.return?.()?.catch?.(noop);
2558
+ }
2559
+ };
2560
+ }
2561
+ _yieldDownloads() {
2562
+ this._downloads?.yieldToRuns([...this._runs.values()].filter((run) => run.executing).map((run) => run.model));
2563
+ }
2564
+ _logModelStatus(state) {
2565
+ if (typeof state !== 'object' || state === null)
2566
+ return;
2567
+ if (!('status' in state) || typeof state.status !== 'string')
2568
+ return;
2569
+ if (!('model' in state) || typeof state.model !== 'string')
2570
+ return;
2571
+ const { status, model } = state;
2572
+ if (status === 'downloading' || this._modelStatusLogged.get(model) === status)
2573
+ return;
2574
+ this._modelStatusLogged.set(model, status);
2575
+ this._log?.info('models', `model ${status}`, { model });
2576
+ }
2577
+ _modelHolder(runId, chatId, model) {
2578
+ for (const [id, run] of this._runs) {
2579
+ if (id !== runId && run.executing && run.chatId !== chatId && run.model === model) {
2580
+ return run.chatId;
2581
+ }
2582
+ }
2583
+ return null;
2584
+ }
2585
+ _watchModelProgress(update, queued = false) {
2586
+ let last = '';
2587
+ let showing = queued;
2588
+ let transcribing = false;
2589
+ let documents = false;
2590
+ const apply = (progress) => {
2591
+ const runStatus = progress
2592
+ ? { state: 'executing', progress }
2593
+ : { state: 'executing' };
2594
+ if (documents)
2595
+ runStatus.reason = 'preparing-documents';
2596
+ const key = JSON.stringify(runStatus);
2597
+ if (key === last)
2598
+ return;
2599
+ last = key;
2600
+ showing = progress !== null || documents;
2601
+ update(runStatus).catch(noop);
2602
+ };
2603
+ const onModel = (state) => {
2604
+ if (state.status === 'loading')
2605
+ apply({ phase: 'loading', model: state.model });
2606
+ else if (state.status === 'downloading') {
2607
+ const percentage = Math.floor((state.percentage ?? 0) / 10) * 10;
2608
+ apply({ phase: 'downloading', model: state.model, percentage });
2609
+ }
2610
+ else
2611
+ apply(transcribing ? { phase: 'transcribing' } : null);
2612
+ };
2613
+ if (this._harness.on && this._harness.off)
2614
+ this._harness.on('model', onModel);
2615
+ return {
2616
+ documents: (active) => {
2617
+ documents = active;
2618
+ apply(null);
2619
+ },
2620
+ transcribe: (active) => {
2621
+ transcribing = active;
2622
+ apply(active ? { phase: 'transcribing' } : null);
2623
+ },
2624
+ image: (progress) => {
2625
+ if (!progress)
2626
+ return apply(null);
2627
+ apply({ phase: 'generating', percentage: Math.floor(progress.percentage / 10) * 10 });
2628
+ },
2629
+ stop: () => this._harness.off?.('model', onModel),
2630
+ clear: () => {
2631
+ if (!showing)
2632
+ return;
2633
+ showing = false;
2634
+ last = JSON.stringify({ state: 'executing' });
2635
+ update({ state: 'executing' }).catch(noop);
2636
+ }
2637
+ };
2638
+ }
2639
+ }
2640
+ function noop() { }
2641
+ function runMetricsSummary(metrics) {
2642
+ const { timeToFirstToken, promptTokens, promptTokensPerSecond, cacheTokens, tokensPerSecond, totalTokens, durationMs } = metrics;
2643
+ return {
2644
+ timeToFirstToken,
2645
+ promptTokens,
2646
+ promptTokensPerSecond,
2647
+ cacheTokens,
2648
+ tokensPerSecond,
2649
+ totalTokens,
2650
+ durationMs
2651
+ };
2652
+ }
2653
+ function isStreamDestroyed(err) {
2654
+ return typeof err === 'object' && err !== null && 'code' in err && err.code === 'STREAM_DESTROYED';
2655
+ }
2656
+ function harnessDeathReason(status) {
2657
+ const HARNESS_DIED = RUN_FAILURE_REASONS.harnessDied;
2658
+ if (typeof status !== 'object' || status === null)
2659
+ return HARNESS_DIED;
2660
+ const record = status;
2661
+ const detail = typeof record.error === 'string' && record.error ? record.error : null;
2662
+ const code = typeof record.code === 'string' && record.code ? record.code : null;
2663
+ if (!detail)
2664
+ return code ? `${HARNESS_DIED} (${code})` : HARNESS_DIED;
2665
+ return code ? `${HARNESS_DIED}: ${detail} (${code})` : `${HARNESS_DIED}: ${detail}`;
2666
+ }
2667
+ function harnessDied(status) {
2668
+ if (typeof status !== 'object' || status === null || !('state' in status))
2669
+ return false;
2670
+ return status.state === 'died';
2671
+ }
2672
+ function modelCatalogChanged(state) {
2673
+ return (typeof state === 'object' &&
2674
+ state !== null &&
2675
+ 'catalogChanged' in state &&
2676
+ state.catalogChanged === true);
2677
+ }
2678
+ function imageDownloadProgress(state) {
2679
+ if (typeof state !== 'object' ||
2680
+ state === null ||
2681
+ !('status' in state) ||
2682
+ (state.status !== 'downloading' && state.status !== 'downloaded') ||
2683
+ !('model' in state) ||
2684
+ typeof state.model !== 'string' ||
2685
+ !('percentage' in state) ||
2686
+ typeof state.percentage !== 'number' ||
2687
+ !Number.isFinite(state.percentage) ||
2688
+ !('downloaded' in state) ||
2689
+ typeof state.downloaded !== 'number' ||
2690
+ !Number.isFinite(state.downloaded) ||
2691
+ !('total' in state) ||
2692
+ typeof state.total !== 'number' ||
2693
+ !Number.isFinite(state.total)) {
2694
+ return null;
2695
+ }
2696
+ const model = diffusionDownloadName(state.model);
2697
+ if (!model)
2698
+ return null;
2699
+ return {
2700
+ model,
2701
+ phase: state.status === 'downloading' ? 'downloading' : 'complete',
2702
+ percentage: state.percentage,
2703
+ downloaded: state.downloaded,
2704
+ total: state.total
2705
+ };
2706
+ }