@superblocksteam/vite-plugin-file-sync 2.0.129 → 2.0.130-next.1

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 (356) hide show
  1. package/dist/ai-service/agent/middleware.d.ts.map +1 -1
  2. package/dist/ai-service/agent/middleware.js +51 -1
  3. package/dist/ai-service/agent/middleware.js.map +1 -1
  4. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
  5. package/dist/ai-service/agent/prompts/build-base-system-prompt.js +44 -4
  6. package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
  7. package/dist/ai-service/agent/subagents/coding/run-coding-subagent.d.ts.map +1 -1
  8. package/dist/ai-service/agent/subagents/coding/run-coding-subagent.js +25 -0
  9. package/dist/ai-service/agent/subagents/coding/run-coding-subagent.js.map +1 -1
  10. package/dist/ai-service/agent/tools/apis/build-api-artifact.d.ts.map +1 -1
  11. package/dist/ai-service/agent/tools/apis/build-api-artifact.js +4 -3
  12. package/dist/ai-service/agent/tools/apis/build-api-artifact.js.map +1 -1
  13. package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
  14. package/dist/ai-service/agent/tools/apis/test-api.js +22 -0
  15. package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
  16. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts +25 -0
  17. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
  18. package/dist/ai-service/agent/tools/build-capture-screenshot.js +117 -11
  19. package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
  20. package/dist/ai-service/agent/tools/build-manage-checklist.d.ts.map +1 -1
  21. package/dist/ai-service/agent/tools/build-manage-checklist.js +22 -0
  22. package/dist/ai-service/agent/tools/build-manage-checklist.js.map +1 -1
  23. package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts +291 -0
  24. package/dist/ai-service/agent/tools/databases/dev-database-tasks.d.ts.map +1 -0
  25. package/dist/ai-service/agent/tools/databases/dev-database-tasks.js +386 -0
  26. package/dist/ai-service/agent/tools/databases/dev-database-tasks.js.map +1 -0
  27. package/dist/ai-service/agent/tools/databases/dev-database-tools.d.ts +17 -0
  28. package/dist/ai-service/agent/tools/databases/dev-database-tools.d.ts.map +1 -0
  29. package/dist/ai-service/agent/tools/databases/dev-database-tools.js +19 -0
  30. package/dist/ai-service/agent/tools/databases/dev-database-tools.js.map +1 -0
  31. package/dist/ai-service/agent/tools/index.d.ts +0 -1
  32. package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
  33. package/dist/ai-service/agent/tools/index.js +0 -1
  34. package/dist/ai-service/agent/tools/index.js.map +1 -1
  35. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +1 -1
  36. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts.map +1 -1
  37. package/dist/ai-service/agent/tools/integrations/execute-request.js +20 -9
  38. package/dist/ai-service/agent/tools/integrations/execute-request.js.map +1 -1
  39. package/dist/ai-service/agent/tools/integrations/internal.d.ts +5 -1
  40. package/dist/ai-service/agent/tools/integrations/internal.d.ts.map +1 -1
  41. package/dist/ai-service/agent/tools/integrations/internal.js +2 -1
  42. package/dist/ai-service/agent/tools/integrations/internal.js.map +1 -1
  43. package/dist/ai-service/agent/tools/screenshot-selector.d.ts +22 -0
  44. package/dist/ai-service/agent/tools/screenshot-selector.d.ts.map +1 -0
  45. package/dist/ai-service/agent/tools/screenshot-selector.js +158 -0
  46. package/dist/ai-service/agent/tools/screenshot-selector.js.map +1 -0
  47. package/dist/ai-service/agent/tools.d.ts +17 -0
  48. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  49. package/dist/ai-service/agent/tools.js +65 -6
  50. package/dist/ai-service/agent/tools.js.map +1 -1
  51. package/dist/ai-service/agent/tools2/access-control.d.ts.map +1 -1
  52. package/dist/ai-service/agent/tools2/access-control.js +6 -1
  53. package/dist/ai-service/agent/tools2/access-control.js.map +1 -1
  54. package/dist/ai-service/agent/tools2/tools/cancel-task.d.ts +20 -0
  55. package/dist/ai-service/agent/tools2/tools/cancel-task.d.ts.map +1 -0
  56. package/dist/ai-service/agent/tools2/tools/cancel-task.js +39 -0
  57. package/dist/ai-service/agent/tools2/tools/cancel-task.js.map +1 -0
  58. package/dist/ai-service/agent/tools2/tools/check-task.d.ts +37 -0
  59. package/dist/ai-service/agent/tools2/tools/check-task.d.ts.map +1 -0
  60. package/dist/ai-service/agent/tools2/tools/check-task.js +53 -0
  61. package/dist/ai-service/agent/tools2/tools/check-task.js.map +1 -0
  62. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
  63. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +10 -0
  64. package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
  65. package/dist/ai-service/agent/tools2/tools/git.d.ts +1 -1
  66. package/dist/ai-service/agent/tools2/tools/git.js +1 -1
  67. package/dist/ai-service/agent/tools2/tools/git.js.map +1 -1
  68. package/dist/ai-service/agent/tools2/tools/grep.d.ts +6 -1
  69. package/dist/ai-service/agent/tools2/tools/grep.d.ts.map +1 -1
  70. package/dist/ai-service/agent/tools2/tools/grep.js +37 -13
  71. package/dist/ai-service/agent/tools2/tools/grep.js.map +1 -1
  72. package/dist/ai-service/agent/tools2/tools/index.d.ts +4 -0
  73. package/dist/ai-service/agent/tools2/tools/index.d.ts.map +1 -1
  74. package/dist/ai-service/agent/tools2/tools/index.js +8 -0
  75. package/dist/ai-service/agent/tools2/tools/index.js.map +1 -1
  76. package/dist/ai-service/agent/tools2/tools/list-tasks.d.ts +21 -0
  77. package/dist/ai-service/agent/tools2/tools/list-tasks.d.ts.map +1 -0
  78. package/dist/ai-service/agent/tools2/tools/list-tasks.js +35 -0
  79. package/dist/ai-service/agent/tools2/tools/list-tasks.js.map +1 -0
  80. package/dist/ai-service/agent/tools2/tools/wait-for-task.d.ts +46 -0
  81. package/dist/ai-service/agent/tools2/tools/wait-for-task.d.ts.map +1 -0
  82. package/dist/ai-service/agent/tools2/tools/wait-for-task.js +83 -0
  83. package/dist/ai-service/agent/tools2/tools/wait-for-task.js.map +1 -0
  84. package/dist/ai-service/agent/tools2/types.d.ts +3 -0
  85. package/dist/ai-service/agent/tools2/types.d.ts.map +1 -1
  86. package/dist/ai-service/agent/tools2/types.js.map +1 -1
  87. package/dist/ai-service/agent/utils.d.ts.map +1 -1
  88. package/dist/ai-service/agent/utils.js +37 -2
  89. package/dist/ai-service/agent/utils.js.map +1 -1
  90. package/dist/ai-service/app-interface/npm-package-lookup.d.ts +22 -0
  91. package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -1
  92. package/dist/ai-service/app-interface/npm-package-lookup.js +88 -1
  93. package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -1
  94. package/dist/ai-service/app-interface/npm-registry.d.ts +7 -0
  95. package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
  96. package/dist/ai-service/app-interface/npm-registry.js +74 -4
  97. package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
  98. package/dist/ai-service/app-interface/shell.d.ts +32 -1
  99. package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
  100. package/dist/ai-service/app-interface/shell.js +86 -3
  101. package/dist/ai-service/app-interface/shell.js.map +1 -1
  102. package/dist/ai-service/app-skills/manager.d.ts +4 -1
  103. package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
  104. package/dist/ai-service/app-skills/manager.js +16 -1
  105. package/dist/ai-service/app-skills/manager.js.map +1 -1
  106. package/dist/ai-service/chat/chat-session-store.d.ts +10 -0
  107. package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
  108. package/dist/ai-service/chat/chat-session-store.js +55 -28
  109. package/dist/ai-service/chat/chat-session-store.js.map +1 -1
  110. package/dist/ai-service/context-download.d.ts.map +1 -1
  111. package/dist/ai-service/context-download.js +50 -40
  112. package/dist/ai-service/context-download.js.map +1 -1
  113. package/dist/ai-service/dev-database-client.d.ts +27 -4
  114. package/dist/ai-service/dev-database-client.d.ts.map +1 -1
  115. package/dist/ai-service/dev-database-client.js +35 -1
  116. package/dist/ai-service/dev-database-client.js.map +1 -1
  117. package/dist/ai-service/dev-database-file-sync.d.ts +41 -0
  118. package/dist/ai-service/dev-database-file-sync.d.ts.map +1 -0
  119. package/dist/ai-service/dev-database-file-sync.js +29 -0
  120. package/dist/ai-service/dev-database-file-sync.js.map +1 -0
  121. package/dist/ai-service/features.d.ts +14 -0
  122. package/dist/ai-service/features.d.ts.map +1 -1
  123. package/dist/ai-service/features.js +14 -0
  124. package/dist/ai-service/features.js.map +1 -1
  125. package/dist/ai-service/index.d.ts +136 -5
  126. package/dist/ai-service/index.d.ts.map +1 -1
  127. package/dist/ai-service/index.js +748 -99
  128. package/dist/ai-service/index.js.map +1 -1
  129. package/dist/ai-service/integrations/store.d.ts +2 -0
  130. package/dist/ai-service/integrations/store.d.ts.map +1 -1
  131. package/dist/ai-service/integrations/store.js +4 -0
  132. package/dist/ai-service/integrations/store.js.map +1 -1
  133. package/dist/ai-service/knowledge/knowledge-manager.d.ts +8 -10
  134. package/dist/ai-service/knowledge/knowledge-manager.d.ts.map +1 -1
  135. package/dist/ai-service/knowledge/knowledge-manager.js +135 -119
  136. package/dist/ai-service/knowledge/knowledge-manager.js.map +1 -1
  137. package/dist/ai-service/llm/chaos-fetch.d.ts.map +1 -1
  138. package/dist/ai-service/llm/chaos-fetch.js +7 -3
  139. package/dist/ai-service/llm/chaos-fetch.js.map +1 -1
  140. package/dist/ai-service/llm/client.d.ts.map +1 -1
  141. package/dist/ai-service/llm/client.js +23 -7
  142. package/dist/ai-service/llm/client.js.map +1 -1
  143. package/dist/ai-service/llm/context-v2/adapter.d.ts +3 -0
  144. package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
  145. package/dist/ai-service/llm/context-v2/adapter.js +9 -0
  146. package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
  147. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +2 -0
  148. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -1
  149. package/dist/ai-service/llm/context-v2/compaction/server-side.js +6 -2
  150. package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -1
  151. package/dist/ai-service/llm/context-v2/context-management.d.ts +3 -9
  152. package/dist/ai-service/llm/context-v2/context-management.d.ts.map +1 -1
  153. package/dist/ai-service/llm/context-v2/context-management.js +3 -9
  154. package/dist/ai-service/llm/context-v2/context-management.js.map +1 -1
  155. package/dist/ai-service/llm/context-v2/context-metrics.d.ts +23 -0
  156. package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -0
  157. package/dist/ai-service/llm/context-v2/context-metrics.js +144 -0
  158. package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -0
  159. package/dist/ai-service/llm/context-v2/context.d.ts +16 -1
  160. package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
  161. package/dist/ai-service/llm/context-v2/context.js +138 -26
  162. package/dist/ai-service/llm/context-v2/context.js.map +1 -1
  163. package/dist/ai-service/llm/context-v2/conversation-context.d.ts +26 -1
  164. package/dist/ai-service/llm/context-v2/conversation-context.d.ts.map +1 -1
  165. package/dist/ai-service/llm/context-v2/manager.d.ts +15 -1
  166. package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
  167. package/dist/ai-service/llm/context-v2/manager.js +97 -40
  168. package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
  169. package/dist/ai-service/llm/context-v2/storage/local.d.ts.map +1 -1
  170. package/dist/ai-service/llm/context-v2/storage/local.js +3 -0
  171. package/dist/ai-service/llm/context-v2/storage/local.js.map +1 -1
  172. package/dist/ai-service/llm/context-v2/types.d.ts +27 -0
  173. package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
  174. package/dist/ai-service/llm/context-v2/types.js +108 -23
  175. package/dist/ai-service/llm/context-v2/types.js.map +1 -1
  176. package/dist/ai-service/llm/error.d.ts +21 -0
  177. package/dist/ai-service/llm/error.d.ts.map +1 -1
  178. package/dist/ai-service/llm/error.js +72 -12
  179. package/dist/ai-service/llm/error.js.map +1 -1
  180. package/dist/ai-service/llm/stream/compaction-text-block.d.ts +14 -0
  181. package/dist/ai-service/llm/stream/compaction-text-block.d.ts.map +1 -0
  182. package/dist/ai-service/llm/stream/compaction-text-block.js +29 -0
  183. package/dist/ai-service/llm/stream/compaction-text-block.js.map +1 -0
  184. package/dist/ai-service/llm/stream/idle-monitor.d.ts.map +1 -1
  185. package/dist/ai-service/llm/stream/idle-monitor.js +12 -28
  186. package/dist/ai-service/llm/stream/idle-monitor.js.map +1 -1
  187. package/dist/ai-service/llm/stream/managed-stream.d.ts.map +1 -1
  188. package/dist/ai-service/llm/stream/managed-stream.js +22 -0
  189. package/dist/ai-service/llm/stream/managed-stream.js.map +1 -1
  190. package/dist/ai-service/llm/stream/observers/context.d.ts +4 -0
  191. package/dist/ai-service/llm/stream/observers/context.d.ts.map +1 -1
  192. package/dist/ai-service/llm/stream/observers/context.js +57 -5
  193. package/dist/ai-service/llm/stream/observers/context.js.map +1 -1
  194. package/dist/ai-service/policy-gate-step.d.ts +17 -0
  195. package/dist/ai-service/policy-gate-step.d.ts.map +1 -0
  196. package/dist/ai-service/policy-gate-step.js +52 -0
  197. package/dist/ai-service/policy-gate-step.js.map +1 -0
  198. package/dist/ai-service/quota-client.d.ts +28 -0
  199. package/dist/ai-service/quota-client.d.ts.map +1 -0
  200. package/dist/ai-service/quota-client.js +68 -0
  201. package/dist/ai-service/quota-client.js.map +1 -0
  202. package/dist/ai-service/security/index.d.ts +1 -1
  203. package/dist/ai-service/security/index.d.ts.map +1 -1
  204. package/dist/ai-service/security/index.js +1 -1
  205. package/dist/ai-service/security/index.js.map +1 -1
  206. package/dist/ai-service/security/safety-classifier.d.ts +9 -26
  207. package/dist/ai-service/security/safety-classifier.d.ts.map +1 -1
  208. package/dist/ai-service/security/safety-classifier.js +29 -44
  209. package/dist/ai-service/security/safety-classifier.js.map +1 -1
  210. package/dist/ai-service/skills/system/_registry.generated.d.ts.map +1 -1
  211. package/dist/ai-service/skills/system/_registry.generated.js +2 -0
  212. package/dist/ai-service/skills/system/_registry.generated.js.map +1 -1
  213. package/dist/ai-service/skills/system/common-import-issues/skill.generated.d.ts +2 -0
  214. package/dist/ai-service/skills/system/common-import-issues/skill.generated.d.ts.map +1 -0
  215. package/dist/ai-service/skills/system/common-import-issues/skill.generated.js +93 -0
  216. package/dist/ai-service/skills/system/common-import-issues/skill.generated.js.map +1 -0
  217. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts +1 -1
  218. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts.map +1 -1
  219. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js +15 -0
  220. package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js.map +1 -1
  221. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts +1 -1
  222. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts.map +1 -1
  223. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js +7 -2
  224. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js.map +1 -1
  225. package/dist/ai-service/state-machine/clark-fsm.d.ts +74 -1
  226. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  227. package/dist/ai-service/state-machine/clark-fsm.js +26 -7
  228. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  229. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  230. package/dist/ai-service/state-machine/handlers/agent-planning.js +223 -9
  231. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  232. package/dist/ai-service/state-machine/handlers/idle.d.ts.map +1 -1
  233. package/dist/ai-service/state-machine/handlers/idle.js +42 -6
  234. package/dist/ai-service/state-machine/handlers/idle.js.map +1 -1
  235. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  236. package/dist/ai-service/state-machine/handlers/llm-generating.js +16 -6
  237. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  238. package/dist/ai-service/state-machine/helpers/peer.d.ts.map +1 -1
  239. package/dist/ai-service/state-machine/helpers/peer.js +9 -1
  240. package/dist/ai-service/state-machine/helpers/peer.js.map +1 -1
  241. package/dist/ai-service/tasks/access.d.ts +23 -0
  242. package/dist/ai-service/tasks/access.d.ts.map +1 -0
  243. package/dist/ai-service/tasks/access.js +26 -0
  244. package/dist/ai-service/tasks/access.js.map +1 -0
  245. package/dist/ai-service/tasks/factory.d.ts +92 -0
  246. package/dist/ai-service/tasks/factory.d.ts.map +1 -0
  247. package/dist/ai-service/tasks/factory.js +54 -0
  248. package/dist/ai-service/tasks/factory.js.map +1 -0
  249. package/dist/ai-service/tasks/index.d.ts +8 -0
  250. package/dist/ai-service/tasks/index.d.ts.map +1 -0
  251. package/dist/ai-service/tasks/index.js +7 -0
  252. package/dist/ai-service/tasks/index.js.map +1 -0
  253. package/dist/ai-service/tasks/peer-push.d.ts +78 -0
  254. package/dist/ai-service/tasks/peer-push.d.ts.map +1 -0
  255. package/dist/ai-service/tasks/peer-push.js +74 -0
  256. package/dist/ai-service/tasks/peer-push.js.map +1 -0
  257. package/dist/ai-service/tasks/poll-loop.d.ts +55 -0
  258. package/dist/ai-service/tasks/poll-loop.d.ts.map +1 -0
  259. package/dist/ai-service/tasks/poll-loop.js +73 -0
  260. package/dist/ai-service/tasks/poll-loop.js.map +1 -0
  261. package/dist/ai-service/tasks/task-store.d.ts +106 -0
  262. package/dist/ai-service/tasks/task-store.d.ts.map +1 -0
  263. package/dist/ai-service/tasks/task-store.js +292 -0
  264. package/dist/ai-service/tasks/task-store.js.map +1 -0
  265. package/dist/ai-service/tasks/test-utils.d.ts +23 -0
  266. package/dist/ai-service/tasks/test-utils.d.ts.map +1 -0
  267. package/dist/ai-service/tasks/test-utils.js +53 -0
  268. package/dist/ai-service/tasks/test-utils.js.map +1 -0
  269. package/dist/ai-service/tasks/types.d.ts +205 -0
  270. package/dist/ai-service/tasks/types.d.ts.map +1 -0
  271. package/dist/ai-service/tasks/types.js +24 -0
  272. package/dist/ai-service/tasks/types.js.map +1 -0
  273. package/dist/ai-service/template-renderer.d.ts.map +1 -1
  274. package/dist/ai-service/template-renderer.js +1 -0
  275. package/dist/ai-service/template-renderer.js.map +1 -1
  276. package/dist/ai-service/test-utils/metrics-harness.d.ts +32 -0
  277. package/dist/ai-service/test-utils/metrics-harness.d.ts.map +1 -0
  278. package/dist/ai-service/test-utils/metrics-harness.js +67 -0
  279. package/dist/ai-service/test-utils/metrics-harness.js.map +1 -0
  280. package/dist/ai-service/test-utils/tracer-harness.d.ts +8 -0
  281. package/dist/ai-service/test-utils/tracer-harness.d.ts.map +1 -0
  282. package/dist/ai-service/test-utils/tracer-harness.js +26 -0
  283. package/dist/ai-service/test-utils/tracer-harness.js.map +1 -0
  284. package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.d.ts +40 -0
  285. package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.d.ts.map +1 -0
  286. package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.js +118 -0
  287. package/dist/ai-service/transform/lucide-dynamic-type-imports/transformer.js.map +1 -0
  288. package/dist/ai-service/util/llm-config-utils.d.ts +2 -6
  289. package/dist/ai-service/util/llm-config-utils.d.ts.map +1 -1
  290. package/dist/ai-service/util/llm-config-utils.js +2 -7
  291. package/dist/ai-service/util/llm-config-utils.js.map +1 -1
  292. package/dist/ai-service/util/rpc-timeout.d.ts +17 -7
  293. package/dist/ai-service/util/rpc-timeout.d.ts.map +1 -1
  294. package/dist/ai-service/util/rpc-timeout.js +17 -7
  295. package/dist/ai-service/util/rpc-timeout.js.map +1 -1
  296. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  297. package/dist/file-sync-vite-plugin.js +76 -6
  298. package/dist/file-sync-vite-plugin.js.map +1 -1
  299. package/dist/inject-index-vite-plugin.d.ts.map +1 -1
  300. package/dist/inject-index-vite-plugin.js +4 -0
  301. package/dist/inject-index-vite-plugin.js.map +1 -1
  302. package/dist/migration/migration-routes.d.ts +14 -0
  303. package/dist/migration/migration-routes.d.ts.map +1 -1
  304. package/dist/migration/migration-routes.js +284 -5
  305. package/dist/migration/migration-routes.js.map +1 -1
  306. package/dist/migration/migration-session-reconcile.d.ts +23 -0
  307. package/dist/migration/migration-session-reconcile.d.ts.map +1 -0
  308. package/dist/migration/migration-session-reconcile.js +129 -0
  309. package/dist/migration/migration-session-reconcile.js.map +1 -0
  310. package/dist/migration/restructure.d.ts.map +1 -1
  311. package/dist/migration/restructure.js +2 -0
  312. package/dist/migration/restructure.js.map +1 -1
  313. package/dist/migration-templates/app-fullstack/css.d.ts +3 -0
  314. package/dist/migration-templates/app-fullstack/package.json +2 -2
  315. package/dist/npm/normalize-workspace-protocol-for-npm.d.ts +15 -0
  316. package/dist/npm/normalize-workspace-protocol-for-npm.d.ts.map +1 -0
  317. package/dist/npm/normalize-workspace-protocol-for-npm.js +36 -0
  318. package/dist/npm/normalize-workspace-protocol-for-npm.js.map +1 -0
  319. package/dist/npm/prepare-package-json-for-cloud-npm.d.ts +21 -0
  320. package/dist/npm/prepare-package-json-for-cloud-npm.d.ts.map +1 -0
  321. package/dist/npm/prepare-package-json-for-cloud-npm.js +44 -0
  322. package/dist/npm/prepare-package-json-for-cloud-npm.js.map +1 -0
  323. package/dist/npm/rewrite-platform-workspace-deps.d.ts +17 -0
  324. package/dist/npm/rewrite-platform-workspace-deps.d.ts.map +1 -0
  325. package/dist/npm/rewrite-platform-workspace-deps.js +37 -0
  326. package/dist/npm/rewrite-platform-workspace-deps.js.map +1 -0
  327. package/dist/plugin-options.d.ts +8 -0
  328. package/dist/plugin-options.d.ts.map +1 -1
  329. package/dist/plugin-options.js.map +1 -1
  330. package/dist/policy-gate-run-registry.d.ts +28 -0
  331. package/dist/policy-gate-run-registry.d.ts.map +1 -0
  332. package/dist/policy-gate-run-registry.js +40 -0
  333. package/dist/policy-gate-run-registry.js.map +1 -0
  334. package/dist/policy-gate-runner.d.ts +2 -0
  335. package/dist/policy-gate-runner.d.ts.map +1 -1
  336. package/dist/policy-gate-runner.js +14 -1
  337. package/dist/policy-gate-runner.js.map +1 -1
  338. package/dist/socket-manager.d.ts.map +1 -1
  339. package/dist/socket-manager.js +55 -0
  340. package/dist/socket-manager.js.map +1 -1
  341. package/dist/sync-service/download.d.ts.map +1 -1
  342. package/dist/sync-service/download.js +6 -4
  343. package/dist/sync-service/download.js.map +1 -1
  344. package/dist/sync-service/draft-reconcile.d.ts +22 -0
  345. package/dist/sync-service/draft-reconcile.d.ts.map +1 -0
  346. package/dist/sync-service/draft-reconcile.js +24 -0
  347. package/dist/sync-service/draft-reconcile.js.map +1 -0
  348. package/dist/sync-service/index.d.ts +30 -5
  349. package/dist/sync-service/index.d.ts.map +1 -1
  350. package/dist/sync-service/index.js +59 -8
  351. package/dist/sync-service/index.js.map +1 -1
  352. package/package.json +10 -10
  353. package/dist/ai-service/agent/tools/databases/dev-database.d.ts +0 -103
  354. package/dist/ai-service/agent/tools/databases/dev-database.d.ts.map +0 -1
  355. package/dist/ai-service/agent/tools/databases/dev-database.js +0 -117
  356. package/dist/ai-service/agent/tools/databases/dev-database.js.map +0 -1
@@ -3,17 +3,20 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { generateObject, hasToolCall, stepCountIs, streamText as aiStreamText, } from "ai";
5
5
  import { z } from "zod";
6
- import { AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
7
- import { addTracingToMethods, classifyAttachmentDelivery, FactAccessType, FactCreatedSource, FactEntityScope, TracedEventEmitter, } from "@superblocksteam/shared";
6
+ import { AiGenerationState, AiMode, resolveAttachmentFileName, } from "@superblocksteam/library-shared/types";
7
+ import { addTracingToMethods, classifyAttachmentDelivery, FactAccessType, FactCreatedSource, FactEntityScope, TracedEventEmitter, getAiQuotaPaywallReasonFromMessage, } from "@superblocksteam/shared";
8
+ import { npmRegistryEmitter } from "@superblocksteam/telemetry";
8
9
  import { buildTranslationPrompt } from "../migration/translation-prompt.js";
10
+ import { POLICY_GATE_RUN_IN_PROGRESS_ERROR, policyGateRunRegistry, } from "../policy-gate-run-registry.js";
9
11
  import { getErrorMeta, getLogger, setDefaultLogger } from "../util/logger.js";
10
12
  import { composeSecurityAgentPrompt, } from "./agent/prompts/build-security-scan-prompt.js";
11
13
  import { getToolCallArguments, getToolErrorCode, getToolOutput, } from "./agent/tool-message-utils.js";
12
- import { buildSecurityScanTools, } from "./agent/tools.js";
14
+ import { buildSecurityScanTools, isBackgroundTaskFrameworkEnabled, } from "./agent/tools.js";
13
15
  import { hasPendingEscalations, flushPendingEscalations, } from "./agent/tools/apis/api-validation-orchestrator.js";
14
16
  import { safeSampleJson } from "./agent/tools/apis/sample-json.js";
15
17
  import { resolveSdkApiEntryPoint } from "./agent/tools/apis/sdk-registry.js";
16
18
  import { readFile } from "./agent/tools/build-read-file.js";
19
+ import { createDevDatabaseTaskTypes, wireDevDatabaseIntegrationPriming, } from "./agent/tools/databases/dev-database-tasks.js";
17
20
  import { debugCache } from "./agent/tools/debug-cache.js";
18
21
  import { buildReadFileToolFactory } from "./agent/tools/index.js";
19
22
  import { securityScanResultSchema, } from "./agent/tools/report-security-findings.js";
@@ -33,6 +36,8 @@ import { API_MIGRATION_ORIGINS, readPersistedChecklist, updateChecklistItem, } f
33
36
  import { downloadContextFromBucketeer, downloadLatestContextFromBucketeer, restoreContextFromCheckpoint, } from "./context-download.js";
34
37
  import { uploadContextToBucketeer } from "./context-upload.js";
35
38
  import { AppContextStore } from "./context/app-context.js";
39
+ import { createDevDatabaseHttpTransport, DevDatabaseClient, } from "./dev-database-client.js";
40
+ import { buildEnsureDevDatabaseFilesSynced, } from "./dev-database-file-sync.js";
36
41
  import { filterDisabledToolsForMigration } from "./filter-disabled-tools-for-migration.js";
37
42
  import { IntegrationStore } from "./integrations/store.js";
38
43
  import { JudgeService } from "./judge/judge-service.js";
@@ -49,11 +54,15 @@ import { traceLLM } from "./llmobs/helpers.js";
49
54
  import { getJwtTraceTags } from "./llmobs/helpers.js";
50
55
  import llmobs from "./llmobs/index.js";
51
56
  import { PlaywrightMcpServerManager } from "./mcp/playwright-server.js";
57
+ import { policyGateStreamPartToAgentStep } from "./policy-gate-step.js";
52
58
  import { ClarkProfiler } from "./profiler/clark-profiler.js";
53
59
  import { explainEventHandlerCodePrompt } from "./prompts/explain-code.js";
54
60
  import { buildSummarizeApiUsagePrompt } from "./prompts/summarize-api-usage.js";
61
+ import { checkAiQuota } from "./quota-client.js";
55
62
  import { RecordingManager, createRecordingFetch } from "./recording/index.js";
56
63
  import { RequestDeduplicator } from "./request-deduplicator.js";
64
+ import { scanContentForSecrets } from "./security/secret-scanner-service.js";
65
+ import { SecretRedactor } from "./security/secret-scanner.js";
57
66
  import { ClarkStateNames, Clark, SERVICE_STARTED_WITH_DRAFT, USER_ACCEPTED_DRAFT, USER_REJECTED_DRAFT, USER_SENT_PROMPT, USER_CANCELED, AGENT_CANCELED, } from "./state-machine/clark-fsm.js";
58
67
  import { doAgentPlanning } from "./state-machine/handlers/agent-planning.js";
59
68
  import { doAwaitingUser } from "./state-machine/handlers/awaiting-user.js";
@@ -64,13 +73,14 @@ import { doPostProcessing } from "./state-machine/handlers/post-processing.js";
64
73
  import { doRuntimeReviewing } from "./state-machine/handlers/runtime-reviewing.js";
65
74
  import { classificationHelper } from "./state-machine/helpers/classification.js";
66
75
  import { getContextId } from "./state-machine/helpers/context-id.js";
76
+ import { sendUserGenerationStateChannel } from "./state-machine/helpers/peer.js";
67
77
  import { sendUserMessageChannel, clearPendingToolPermissionRequest, } from "./state-machine/helpers/peer.js";
68
78
  import { StablePeer } from "./state-machine/helpers/stable-peer.js";
69
79
  import { transitionFrom } from "./state-machine/helpers/transition.js";
80
+ import { isTaskAccessibleTo, registerBackgroundTaskTypes, resyncActiveTaskStatusesToPeer, TaskStore, wireTaskStatusPushToPeer, } from "./tasks/index.js";
70
81
  import { TemplateRenderer } from "./template-renderer.js";
71
82
  import { createJsonStreamParser } from "./util/json-stream-parser.js";
72
83
  import { processLLMConfig } from "./util/llm-config-utils.js";
73
- import { buildModeMessage } from "./util/mode-message.js";
74
84
  import { parseJwt } from "./util/parse-jwt.js";
75
85
  import { getToolCallSignature } from "./util/tool-signature.js";
76
86
  export { AiServiceFeatureFlags } from "./features.js";
@@ -292,6 +302,14 @@ const mergePlanContexts = (existing, incoming) => {
292
302
  enableTesting: incoming?.enableTesting ?? existing?.enableTesting,
293
303
  };
294
304
  };
305
+ const APP_CLARK_GENERATION_METADATA = {
306
+ origin: "app_clark_generation",
307
+ surface: "app",
308
+ };
309
+ const POLICY_GATE_USAGE_METADATA = {
310
+ origin: "policy_gate",
311
+ surface: "policy_gate",
312
+ };
295
313
  export class AiService extends TracedEventEmitter {
296
314
  config;
297
315
  templateRenderer;
@@ -312,6 +330,19 @@ export class AiService extends TracedEventEmitter {
312
330
  requestDeduplicator = new RequestDeduplicator();
313
331
  entityPermissionStore = new SessionEntityPermissionStore();
314
332
  _onGenerationCompleteCallback;
333
+ /** JWT-derived userId of the currently-authenticated peer, set by
334
+ * `notifyAuthenticatedRpc` and cleared on disconnect / peer swap.
335
+ * Only trusted while `authenticatedPeerId` still matches the bound
336
+ * `stablePeer.peerId` (see `getAuthenticatedPeerUserId`). Used by
337
+ * `wireTaskStatusPushToPeer` to owner-filter live status pushes. */
338
+ authenticatedPeerUserId;
339
+ /** The `peerId` that `authenticatedPeerUserId` was authenticated for.
340
+ * `handleUserConnected` can bind a replacement peer before the
341
+ * previous peer's disconnect is processed; keying the identity to its
342
+ * peerId means a live push in that window is gated against `undefined`
343
+ * (suppressed) instead of the previous user, closing the cross-user
344
+ * leak from PR #19718 Comment 59. */
345
+ authenticatedPeerId;
315
346
  tokenManagerJwt;
316
347
  /**
317
348
  * Listener body for `tokenManager.on("tokenUpdated", ...)`. Defined as a
@@ -355,6 +386,7 @@ export class AiService extends TracedEventEmitter {
355
386
  _runtimeGitOperationLock;
356
387
  _onGitOperationComplete;
357
388
  _syncContextProvider;
389
+ taskStore;
358
390
  get appContext() {
359
391
  return this._appContext;
360
392
  }
@@ -506,21 +538,21 @@ export class AiService extends TracedEventEmitter {
506
538
  useSuperblocksAuthorizationHeader: config.providerSettings?.useSuperblocksAuthorizationHeader,
507
539
  };
508
540
  // Seed `tokenManagerJwt` synchronously from the manager's current
509
- // value before any consumer that calls `getJwt()` is constructed. The
541
+ // value before the npm-registry client calls `getNpmRegistryJwt()`. The
510
542
  // `tokenUpdated` listener below covers FUTURE refreshes; this read
511
543
  // covers the cold-start case where the CLI (or any earlier code) has
512
544
  // already called `tokenManager.updateToken(...)` before this AiService
513
545
  // was constructed. Without it, the prior emission is lost — Node
514
546
  // `EventEmitter` has no replay — and `NpmRegistryClient.getJwt()`
515
- // would time out via `waitForJwt(...)`, downgrading `syncHomeNpmrc`
547
+ // would time out via `waitForNpmRegistryJwt(...)`, downgrading `syncHomeNpmrc`
516
548
  // to `unreachable` and leaving `~/.npmrc` unwritten on cold boot.
517
549
  this.tokenManagerJwt = this.config.tokenManager?.getCurrentToken();
518
550
  // Resolve the npm registry client once and reuse across TemplateRenderer
519
551
  // and AppShell so both install paths share the same in-memory cache and
520
552
  // 401-refresh state. Explicit injection wins (tests); otherwise we
521
553
  // construct a default that reads the LD flag from `config.features` and
522
- // routes JWT through the AiService's own `getJwt()` (which prefers the
523
- // tokenManager token and falls back to clark.context.jwt). Empty
554
+ // routes JWT through the AiService's npm-registry JWT source (which
555
+ // prefers the tokenManager token and falls back to clark.context.jwt). Empty
524
556
  // tokens are refused upstream of `attempt()` so the server doesn't
525
557
  // see `Bearer ` and reply 400 (which the client would now treat as a
526
558
  // hard fail rather than a transient outage).
@@ -541,7 +573,7 @@ export class AiService extends TracedEventEmitter {
541
573
  // `tokenUpdated` event rather than throwing immediately, so the
542
574
  // first install in the shared template dir actually gets a
543
575
  // `.npmrc` instead of silently downgrading to "not configured".
544
- getJwt: () => this.waitForJwt(NPM_REGISTRY_JWT_WAIT_MS),
576
+ getJwt: () => this.waitForNpmRegistryJwt(NPM_REGISTRY_JWT_WAIT_MS),
545
577
  // Push-based refresh hook: on 401, wait briefly for the next
546
578
  // `tokenUpdated` event before re-reading the JWT. Without this,
547
579
  // a 401 retry fires immediately with the same expired token and
@@ -556,6 +588,12 @@ export class AiService extends TracedEventEmitter {
556
588
  }),
557
589
  });
558
590
  this.npmRegistryClient = npmRegistryClient;
591
+ // Kill-switch gauge (APPS-4378): `superblocks.npm.killswitch.enabled` reads
592
+ // `1` when the npm-registry feature is suppressed (LD flag off), `0`
593
+ // otherwise — one series per process. Registered here, the single place
594
+ // that owns the flag value, so on-call can answer "did anyone hit the
595
+ // kill-switch in the last 24h?" without reading logs.
596
+ npmRegistryEmitter.setKillswitch(() => config.features.npmRegistryEnabled !== true);
559
597
  // One process-lifetime `NpmPackageLookup` shared across every
560
598
  // `buildTools()` call. The `build_lookupNpmPackage` tool factory used
561
599
  // to construct a fresh instance per LLM turn; each construction
@@ -569,7 +607,11 @@ export class AiService extends TracedEventEmitter {
569
607
  // `npmRegistryClient` / `templateRenderer` / `appShell` — means the
570
608
  // gauge wires up exactly once and the 60s TTL cache stays warm across
571
609
  // turns instead of being thrown away.
572
- this.npmPackageLookup = new NpmPackageLookup({ client: npmRegistryClient });
610
+ this.npmPackageLookup = new NpmPackageLookup({
611
+ client: npmRegistryClient,
612
+ // org_id rides on the `npm_registry.lookup_package` span only.
613
+ orgId: config.organizationId,
614
+ });
573
615
  this.templateRenderer =
574
616
  config.templateRenderer ??
575
617
  new TemplateRenderer({
@@ -589,6 +631,24 @@ export class AiService extends TracedEventEmitter {
589
631
  draftInterface: config.draftInterface,
590
632
  templateRenderer: this.templateRenderer,
591
633
  npmRegistryClient,
634
+ // APPS-4191 / P6.3: ship blocked-install audit events to the server over
635
+ // the authenticated socket. Fire-and-forget — the server derives org,
636
+ // actor, AND app attribution from the authenticated connection
637
+ // (`ctx.jwtClaims.app_id` on the scoped JWT this socket carries), then
638
+ // throttles, sanitizes, and persists. A transport failure must never
639
+ // surface to (or break) the install. We intentionally do NOT spread the
640
+ // local `applicationId` into the payload — that would be client-
641
+ // controlled and forgeable by any authenticated socket caller.
642
+ reportNpmInstallBlocked: (report) => {
643
+ void this.config.rpcClient
644
+ .call(async (client) => {
645
+ const rpcClient = client;
646
+ return await rpcClient.call.v1.audit.npmInstallBlocked(report);
647
+ })
648
+ .catch((error) => {
649
+ this.getLogger().debug("[ai-service] Failed to report npm-install-blocked audit event", getErrorMeta(error));
650
+ });
651
+ },
592
652
  virtualFileSystem: createDefaultVirtualFileSystem({
593
653
  useSystemSkills: config.features.useSystemSkills,
594
654
  knowledgeManager: this.knowledgeManager,
@@ -616,6 +676,68 @@ export class AiService extends TracedEventEmitter {
616
676
  });
617
677
  this.fileSystemInterface = new FileSystemInterface();
618
678
  this.integrationStore = new IntegrationStore(undefined, config.appRootDirPath, config.features.metadataCacheMaxTotalSizeMB, config.features.metadataCacheMaxSingleFileSizeMB, config.pluginExecutionVersions, config.superblocksBaseUrl, config.applicationId, () => this.clark?.context?.jwt);
679
+ // Background-task framework: per-session TaskStore + registered task
680
+ // types. Gated by `isBackgroundTaskFrameworkEnabled` — the same
681
+ // `managedNativeDbEnabled` rollout check that `agent/tools.ts`
682
+ // consults. When the gate is off, `taskStore` stays undefined and
683
+ // `agent/tools.ts` skips registering the start*/check*/wait*/cancel*/list*
684
+ // tools (those have an `&& services.taskStore` guard, plus the same
685
+ // flag check).
686
+ if (isBackgroundTaskFrameworkEnabled(config.features)) {
687
+ this.taskStore = new TaskStore();
688
+ // Heterogeneous list of task types funneled into
689
+ // `registerBackgroundTaskTypes` (which itself takes ReadonlyArray<TaskType<any, any, any>>),
690
+ // hence the `any` widening: `TaskType` is contravariant in `Input`.
691
+ const taskTypes = [];
692
+ {
693
+ // Dev-database lifecycle is unconditionally wired as a pair of
694
+ // background tasks (provision + applyMigrations) — the legacy
695
+ // blocking triplet (createDevDb / applyMigrations /
696
+ // getDevDbProgress) was removed in favor of this path. Gated only
697
+ // by the framework flag above.
698
+ const devDbTypes = createDevDatabaseTaskTypes({
699
+ applicationId: config.applicationId,
700
+ // The working-state SQL files in the user's local repo —
701
+ // including drafts the LLM just wrote this turn — must be
702
+ // flushed to the server before the lifecycle worker reads
703
+ // them, otherwise migrations apply against stale content and
704
+ // the new migration silently succeeds against an unchanged
705
+ // schema. Matches master's `applyMigrationsToolFactory`
706
+ // registration, which threaded `syncCurrentDirectoryWithDraftsNow`
707
+ // — NOT the autosync drain `uploadDirectoryNowIfNeeded`.
708
+ // `_runtimeSyncService` is injected by `setRuntimeGitServices`
709
+ // AFTER this constructor runs, so the helper resolves it
710
+ // lazily at task-start time.
711
+ ensureFilesSynced: buildEnsureDevDatabaseFilesSynced(() => this._runtimeSyncService),
712
+ getClient: () => {
713
+ const jwt = this.clark?.context?.jwt;
714
+ if (!jwt) {
715
+ throw new Error("[devDatabase] cannot start background task: no JWT available on Clark context");
716
+ }
717
+ return new DevDatabaseClient({
718
+ transport: createDevDatabaseHttpTransport({
719
+ jwt,
720
+ superblocksBaseUrl: config.superblocksBaseUrl,
721
+ }),
722
+ });
723
+ },
724
+ });
725
+ taskTypes.push(devDbTypes.provisionDevDatabaseTaskType, devDbTypes.applyDevDatabaseMigrationsTaskType);
726
+ wireDevDatabaseIntegrationPriming(this.taskStore, this.integrationStore);
727
+ }
728
+ registerBackgroundTaskTypes(this.taskStore, taskTypes);
729
+ // Push every status transition (including the initial in-store
730
+ // snapshot after `start`) to the editor for the bottom-of-chat
731
+ // indicator. StablePeer handles disconnect buffering so the helper
732
+ // itself doesn't retry. The unsubscribe fn is intentionally not
733
+ // captured — the TaskStore is per-AiService and disposed alongside
734
+ // it, which clears all listeners.
735
+ wireTaskStatusPushToPeer(this.taskStore, {
736
+ getPeer: () => this.stablePeer,
737
+ getCallerUserId: () => this.getAuthenticatedPeerUserId(),
738
+ logger: this.getLogger(),
739
+ });
740
+ }
619
741
  this._appContext = new AppContextStore(this.fileSystemInterface);
620
742
  this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
621
743
  this.chatSessionStore = new ChatSessionStore({
@@ -653,7 +775,7 @@ export class AiService extends TracedEventEmitter {
653
775
  unifiedProviderEnabled: config.features.unifiedProviderEnabled ?? false,
654
776
  organizationId: config.organizationId,
655
777
  applicationId: config.applicationId,
656
- getJwt: () => this.getJwt(),
778
+ getJwt: () => this.requireInferenceJwt(),
657
779
  onUsageRecorded: (usage) => {
658
780
  // Accumulate workflow-level metrics
659
781
  this.clark.updateContext((context) => {
@@ -845,15 +967,7 @@ export class AiService extends TracedEventEmitter {
845
967
  integrationStore: this.integrationStore,
846
968
  knowledgeManager: this.knowledgeManager,
847
969
  appContextStore: this.appContext,
848
- llmProvider: createLLMProvider(this.getLLMProviderSettings(clark.context.llmConfig), () => this.getJwt(), clark.context.llmConfig, () => ({
849
- prompt_id: clark.context.promptId,
850
- chatmessage_id: clark.context.chatmessageId,
851
- application_id: this.config.applicationId,
852
- session_id: this.sessionId,
853
- // Note: retry_count for LLM retries is added by orchestrator via providerOptions
854
- mode: clark.context.currentMode === AiMode.PLAN ? "plan" : "build",
855
- plan_size: clark.context.planSize,
856
- })),
970
+ llmProvider: createLLMProvider(this.getLLMProviderSettings(clark.context.llmConfig), () => this.requireInferenceJwt(), clark.context.llmConfig, () => this.getLlmAiAttributes(clark)),
857
971
  chatSessionStore: this.chatSessionStore,
858
972
  clarkProfiler: this.clarkProfiler,
859
973
  contextManagerV2: this.contextManagerV2,
@@ -867,6 +981,18 @@ export class AiService extends TracedEventEmitter {
867
981
  syncService: this._runtimeSyncService,
868
982
  gitOperationLock: this._runtimeGitOperationLock,
869
983
  onGitOperationComplete: this._onGitOperationComplete,
984
+ taskStore: this.taskStore,
985
+ // Resolve `ownerUserId` from the *current* JWT at each call site
986
+ // (kickoff / read / cancel). Reading lazily — instead of capturing
987
+ // the JWT at handler construction — means the wired identity stays
988
+ // correct across `updateClarkJwt` refreshes that happen mid-turn
989
+ // when a user's token rotates. Tests pin this lazy contract.
990
+ getCurrentOwnerId: () => {
991
+ const jwt = clark.context.jwt;
992
+ if (!jwt)
993
+ return undefined;
994
+ return parseJwt(jwt)?.userId;
995
+ },
870
996
  };
871
997
  const clarkStateHandlers = Object.fromEntries(Object.entries(clarkStateHandlerFactories).map(([state, handlerFactory]) => [state, handlerFactory(clark, params)]));
872
998
  const { from, to, event } = transition;
@@ -885,6 +1011,14 @@ export class AiService extends TracedEventEmitter {
885
1011
  // Trigger callback when Clark enters idle states (generation complete)
886
1012
  if (to === ClarkStateNames.Idle ||
887
1013
  to === ClarkStateNames.AwaitingUser) {
1014
+ // The turn is over: drop per-turn billing attribution so stray
1015
+ // LLM calls before the next prompt bill as default generation.
1016
+ if (clark.context.usageMetadata) {
1017
+ clark.updateContext((context) => ({
1018
+ ...context,
1019
+ usageMetadata: undefined,
1020
+ }));
1021
+ }
888
1022
  this.triggerGenerationComplete();
889
1023
  }
890
1024
  }
@@ -907,6 +1041,16 @@ export class AiService extends TracedEventEmitter {
907
1041
  };
908
1042
  };
909
1043
  }
1044
+ getInferenceJwt() {
1045
+ return this.clark?.context?.jwt;
1046
+ }
1047
+ requireInferenceJwt() {
1048
+ const jwt = this.getInferenceJwt();
1049
+ if (!jwt) {
1050
+ throw new Error("[ai-service] no Clark JWT available for AI inference");
1051
+ }
1052
+ return jwt;
1053
+ }
910
1054
  /**
911
1055
  * Resolves the conversation context for a contextId that is not present
912
1056
  * locally, by downloading it from Bucketeer. Wired into ContextManagerV2 as
@@ -925,7 +1069,7 @@ export class AiService extends TracedEventEmitter {
925
1069
  * commitId — and GET by commit. The resolution is logged either way.
926
1070
  */
927
1071
  async remoteRestoreFallback(id) {
928
- const jwt = this.getJwt();
1072
+ const jwt = this.getInferenceJwt();
929
1073
  const branchName = this._syncContextProvider?.getBranchName();
930
1074
  if (!jwt || !branchName) {
931
1075
  // A missing JWT is expected startup churn on a fresh pod (this runs
@@ -980,43 +1124,51 @@ export class AiService extends TracedEventEmitter {
980
1124
  appRootDirPath: this.config.appRootDirPath,
981
1125
  });
982
1126
  }
983
- getJwt() {
1127
+ requireProvidedInferenceJwt(jwt, source) {
1128
+ if (!jwt) {
1129
+ throw new Error(`[ai-service] no JWT available for ${source} AI inference`);
1130
+ }
1131
+ return jwt;
1132
+ }
1133
+ getNpmRegistryJwt() {
984
1134
  if (this.tokenManagerJwt) {
985
1135
  return this.tokenManagerJwt;
986
1136
  }
987
- // fallback to clark context if no token manager is provided
1137
+ // Fallback to Clark context if no token manager token is provided.
988
1138
  return this.clark?.context?.jwt;
989
1139
  }
990
1140
  buildContextManagementForCurrentClark(llmConfig) {
991
- const modeInstructions = this.clark.context.currentMode
992
- ? `Preserve the following mode context in your summary: ${buildModeMessage(this.clark.context.currentMode, this.clark.context.planContext?.approved)}`
993
- : undefined;
994
1141
  return selectContextManagementStrategy({
995
1142
  serverContextManagementEnabled: llmConfig?.serverContextManagement?.enabled,
996
- modeInstructions,
997
1143
  thresholds: llmConfig?.serverContextManagement,
998
- summarizationModel: () => createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.getJwt(), llmConfig, () => ({
999
- prompt_id: this.clark.context.promptId,
1000
- chatmessage_id: this.clark.context.chatmessageId,
1001
- application_id: this.config.applicationId,
1002
- session_id: this.sessionId,
1003
- mode: this.clark.context.currentMode === AiMode.PLAN ? "plan" : "build",
1004
- plan_size: this.clark.context.planSize,
1005
- })).modelForTask("summarizeMessages"),
1144
+ summarizationModel: () => createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.getInferenceJwt(), llmConfig, () => this.getLlmAiAttributes(this.clark)).modelForTask("summarizeMessages"),
1006
1145
  });
1007
1146
  }
1147
+ getLlmAiAttributes(clark) {
1148
+ const usageMetadata = clark.context.usageMetadata ?? APP_CLARK_GENERATION_METADATA;
1149
+ return {
1150
+ prompt_id: clark.context.promptId,
1151
+ chatmessage_id: clark.context.chatmessageId,
1152
+ application_id: this.config.applicationId,
1153
+ session_id: this.sessionId,
1154
+ // Note: retry_count for LLM retries is added by orchestrator via providerOptions
1155
+ mode: clark.context.currentMode === AiMode.PLAN ? "plan" : "build",
1156
+ plan_size: clark.context.planSize,
1157
+ metadata: usageMetadata,
1158
+ };
1159
+ }
1008
1160
  /**
1009
1161
  * Resolve a JWT for the npm-registry fetch, blocking briefly on the
1010
1162
  * first `tokenUpdated` event when no JWT is yet primed. The
1011
1163
  * `TemplateRenderer` prefetch fires during AiService construction —
1012
1164
  * before the `tokenManager` subscription has emitted — so a synchronous
1013
- * `getJwt()` would observe `undefined` and silently downgrade the
1165
+ * `getNpmRegistryJwt()` would observe `undefined` and silently downgrade the
1014
1166
  * private-registry install to public npm. Blocking here is the
1015
1167
  * narrowest fix: callers that already have a primed JWT take the fast
1016
1168
  * path and never await.
1017
1169
  */
1018
- async waitForJwt(timeoutMs) {
1019
- const immediate = this.getJwt();
1170
+ async waitForNpmRegistryJwt(timeoutMs) {
1171
+ const immediate = this.getNpmRegistryJwt();
1020
1172
  if (immediate) {
1021
1173
  return immediate;
1022
1174
  }
@@ -1024,7 +1176,7 @@ export class AiService extends TracedEventEmitter {
1024
1176
  throw new Error("[npm-registry] no JWT available and no tokenManager wired to wait for one");
1025
1177
  }
1026
1178
  await this.waitForTokenUpdate(timeoutMs);
1027
- const after = this.getJwt();
1179
+ const after = this.getNpmRegistryJwt();
1028
1180
  if (!after) {
1029
1181
  throw new Error("[npm-registry] no JWT available after waiting for tokenManager update");
1030
1182
  }
@@ -1038,7 +1190,7 @@ export class AiService extends TracedEventEmitter {
1038
1190
  *
1039
1191
  * Resolution only signals "a wait completed" — never that a JWT actually
1040
1192
  * changed. The constructor's `tokenUpdated` listener (above) is the only
1041
- * code that mutates `tokenManagerJwt`. Callers (`waitForJwt`, the
1193
+ * code that mutates `tokenManagerJwt`. Callers (`waitForNpmRegistryJwt`, the
1042
1194
  * npm-registry `refreshJwt` adapter) must re-read the JWT after this
1043
1195
  * resolves and treat an unchanged value the same as a refresh that
1044
1196
  * never happened.
@@ -1071,20 +1223,6 @@ export class AiService extends TracedEventEmitter {
1071
1223
  });
1072
1224
  };
1073
1225
  }
1074
- async callServer({ jwt, path, }) {
1075
- const url = `${this.config.superblocksBaseUrl}/api/${path}`;
1076
- const response = await fetch(url, {
1077
- method: "GET",
1078
- headers: {
1079
- Authorization: `Bearer ${jwt}`,
1080
- "Content-Type": "application/json",
1081
- },
1082
- });
1083
- if (!response.ok) {
1084
- throw new Error(`Call to ${path} failed with status ${response.status}`);
1085
- }
1086
- return (await response.json());
1087
- }
1088
1226
  /**
1089
1227
  * Non-throwing AI quota check. Returns `{ allowed: true }` when the org is
1090
1228
  * within its limit, and also when the check itself can't run (no JWT, or the
@@ -1092,33 +1230,14 @@ export class AiService extends TracedEventEmitter {
1092
1230
  * blocks legitimate work. When the org is over limit, returns the reason and
1093
1231
  * a user-facing message instead of throwing, so callers that don't surface
1094
1232
  * exceptions (e.g. the background migration turn) can react.
1233
+ * Logic lives in `quota-client.ts` (with the `quota.check` span).
1095
1234
  */
1096
1235
  async checkAiQuota(jwt) {
1097
- const token = jwt ?? this.getJwt();
1098
- if (!token) {
1099
- this.getLogger().warn("[ai-service] No JWT available for quota check, skipping");
1100
- return { allowed: true };
1101
- }
1102
- try {
1103
- const quota = await this.callServer({
1104
- jwt: token,
1105
- path: "v1/ai/quota",
1106
- });
1107
- if (!quota.allowed) {
1108
- const reason = quota.reason ?? "token_limit_exceeded";
1109
- const message = reason === "trial_expired"
1110
- ? "Your AI trial period has ended."
1111
- : reason === "no_seat_assigned"
1112
- ? "You don't have an AI Builder license assigned. Ask your admin to assign a license."
1113
- : "Your organization has reached its AI usage limit.";
1114
- return { allowed: false, reason, message };
1115
- }
1116
- return { allowed: true };
1117
- }
1118
- catch (error) {
1119
- this.getLogger().warn("[ai-service] Quota check failed, allowing request", getErrorMeta(error));
1120
- return { allowed: true };
1121
- }
1236
+ return checkAiQuota({
1237
+ superblocksBaseUrl: this.config.superblocksBaseUrl,
1238
+ jwt: jwt ?? this.getInferenceJwt(),
1239
+ logger: this.getLogger(),
1240
+ });
1122
1241
  }
1123
1242
  async ensureAiQuotaAvailable(jwt) {
1124
1243
  const quota = await this.checkAiQuota(jwt);
@@ -1200,9 +1319,20 @@ export class AiService extends TracedEventEmitter {
1200
1319
  llmConfig: migrationLlmConfig,
1201
1320
  mode: AiMode.BUILD,
1202
1321
  };
1322
+ const promptId = randomUUID();
1323
+ const branchName = this._syncContextProvider?.getBranchName();
1324
+ const migrationUsageMetadata = {
1325
+ applicationId: this.config.applicationId,
1326
+ branchName,
1327
+ migrationSource: "superblocks_2_0",
1328
+ origin: "app_migration",
1329
+ pendingSeedApiCount,
1330
+ retryMode: request.retryMode ?? "none",
1331
+ };
1203
1332
  this.clark.updateContext((context) => ({
1204
1333
  ...context,
1205
- promptId: randomUUID(),
1334
+ promptId,
1335
+ usageMetadata: migrationUsageMetadata,
1206
1336
  llmConfig: migrationLlmConfig,
1207
1337
  }));
1208
1338
  const transitionTo = transitionFrom(this.clark);
@@ -1234,6 +1364,312 @@ export class AiService extends TracedEventEmitter {
1234
1364
  return result.config;
1235
1365
  }
1236
1366
  migrationTurnTriggered = false;
1367
+ /**
1368
+ * Rejects prompt submission while a policy gate run is executing against the
1369
+ * current application so concurrent Clark activity cannot mutate the app
1370
+ * mid-run (APPS-4466). Enforced here at the dev server, not only in the UI.
1371
+ */
1372
+ assertNoActivePolicyGateRun() {
1373
+ if (policyGateRunRegistry.isRunning(this.config.applicationId)) {
1374
+ this.getLogger().warn("[ai-service] Rejecting prompt while a policy gate run is in progress", { applicationId: this.config.applicationId });
1375
+ throw new Error(POLICY_GATE_RUN_IN_PROGRESS_ERROR);
1376
+ }
1377
+ }
1378
+ /**
1379
+ * Run the pre-call quota check for an incoming user request. If quota is
1380
+ * denied with a recognized paywall reason, persist the prompt + advice
1381
+ * and open the modal via `handlePreCallPaywall`, and return `true` so
1382
+ * the caller can short-circuit. Returns `false` if quota is fine.
1383
+ * Non-paywall errors from the quota check are re-thrown unchanged.
1384
+ *
1385
+ * Shared between `handleAiGenerate` (the normal entry point) and
1386
+ * `interruptAndContinue` (the interrupt-mid-generation entry point) so
1387
+ * both surfaces produce the same user-visible behavior on quota denial —
1388
+ * chat record + modal — instead of the interrupt path silently rejecting
1389
+ * with no UI state. (APPS-4080)
1390
+ */
1391
+ async tryHandlePaywallForRequest(request) {
1392
+ try {
1393
+ await this.ensureAiQuotaAvailable(request.jwt);
1394
+ return false;
1395
+ }
1396
+ catch (error) {
1397
+ if (error instanceof Error) {
1398
+ const reason = getAiQuotaPaywallReasonFromMessage(error.message);
1399
+ if (reason) {
1400
+ await this.handlePreCallPaywall(request, reason);
1401
+ return true;
1402
+ }
1403
+ }
1404
+ throw error;
1405
+ }
1406
+ }
1407
+ /**
1408
+ * Persist the user's prompt + the "credits ran out" advice and open the
1409
+ * paywall modal when the pre-call quota check denies the request. Mirrors
1410
+ * what `idle.ts:AGENT_CANCELED` does for the mid-stream paywall path so
1411
+ * both paths leave an identical chat record. Used only when
1412
+ * `ensureAiQuotaAvailable` throws a paywall error in `handleAiGenerate`.
1413
+ *
1414
+ * - For organic user prompts (no `responseMetadata`), persists via
1415
+ * `recordUser`.
1416
+ * - For widget responses (`multi_choice_response`, `plan_response`,
1417
+ * `confirm_response`, `searchable_dropdown_response`,
1418
+ * `remember_knowledge_draft_response`), persists via
1419
+ * `recordUserResponse` with the full metadata — without this, the
1420
+ * user's exact choice is silently dropped (they lose their selection
1421
+ * from chat history AND the resume turn's LLM has no record of what
1422
+ * they picked). Mirrors `agent-planning.ts:USER_SENT_PROMPT`.
1423
+ * - `tool_permission_response` is intentionally NOT persisted — those
1424
+ * responses don't have a chat representation; the approval is just
1425
+ * side-effect state. On resume the user will see the permission
1426
+ * prompt again, which is acceptable UX.
1427
+ * - Uses fire-and-forget peer pushes after each `await` on the store so
1428
+ * the chat is durable across refresh even if the live peer push drops.
1429
+ */
1430
+ async handlePreCallPaywall(request, paywallReason) {
1431
+ this.getLogger().info(`[ai-service] Pre-call paywall denial; persisting prompt + advice for chat history`, { paywallReason });
1432
+ // APPS-4080: pre-cache the CANCELLED + paywallReason snapshot into
1433
+ // `lastGenerationState` BEFORE any chat-persistence await. Without
1434
+ // this, a socket reconnect during the awaits in steps 1-2 below
1435
+ // would replay the stale IDLE snapshot held over from before the
1436
+ // request, and the UI saga (`paywallReason && state === CANCELLED`
1437
+ // -> openAiQuotaPaywallModal) would never see the paywall payload —
1438
+ // so the modal would not reopen on refresh. The full
1439
+ // `sendUserGenerationStateChannel({...})` in step 3 below re-caches
1440
+ // and pushes to the peer so the LIVE modal opens AFTER the advice
1441
+ // is in chat. (Cursor PR-19341 review.)
1442
+ this.clark.updateContext({
1443
+ lastGenerationState: {
1444
+ state: AiGenerationState.CANCELLED,
1445
+ hasError: true,
1446
+ paywallReason,
1447
+ },
1448
+ });
1449
+ // 1. Persist + echo the user's prompt so the chat doesn't lose it.
1450
+ const responseMetadata = request.responseMetadata;
1451
+ if (!responseMetadata) {
1452
+ try {
1453
+ const chatmessageId = await this.chatSessionStore.recordUser(request.prompt, { attachments: request.promptContext?.attachments });
1454
+ if (chatmessageId && this.clark.context.peer) {
1455
+ void this.clark.context.peer.call
1456
+ .aiPushMessage({
1457
+ role: "user",
1458
+ type: "text",
1459
+ content: request.prompt,
1460
+ id: chatmessageId,
1461
+ timestamp: Date.now(),
1462
+ attachments: request.promptContext?.attachments,
1463
+ })
1464
+ .catch((err) => {
1465
+ this.getLogger().warn("[ai-service] Failed to echo pre-call paywall user prompt", getErrorMeta(err));
1466
+ });
1467
+ }
1468
+ }
1469
+ catch (err) {
1470
+ this.getLogger().warn("[ai-service] Failed to persist pre-call paywall user prompt", getErrorMeta(err));
1471
+ }
1472
+ }
1473
+ else if (responseMetadata.type !== "tool_permission_response") {
1474
+ // 1b. Widget response (multi_choice_response, plan_response, etc.).
1475
+ // Persist via `recordUserResponse` with the same metadata mapping
1476
+ // `agent-planning.ts:USER_SENT_PROMPT` uses, so the user's exact
1477
+ // choice survives in chat history and is visible on the resume
1478
+ // turn. Without this branch, the user's selection is silently
1479
+ // dropped and they lose their selection from chat history.
1480
+ //
1481
+ // `tool_permission_response` is intentionally skipped — those
1482
+ // don't have a persisted chat representation. The user will see
1483
+ // the permission prompt again on resume.
1484
+ const metadata = responseMetadata.type === "multi_choice_response"
1485
+ ? {
1486
+ responseToMessageId: responseMetadata.responseToMessageId,
1487
+ selectedChoiceIndices: responseMetadata.selectedChoiceIndices,
1488
+ selectedScopes: responseMetadata.selectedScopes,
1489
+ }
1490
+ : responseMetadata.type === "remember_knowledge_draft_response"
1491
+ ? {
1492
+ responseToMessageId: responseMetadata.responseToMessageId,
1493
+ selectedCandidateIndices: responseMetadata.selectedCandidateIndices,
1494
+ selectedScopes: responseMetadata.selectedScopes,
1495
+ }
1496
+ : responseMetadata.type === "confirm_response"
1497
+ ? {
1498
+ responseToMessageId: responseMetadata.responseToMessageId,
1499
+ approved: responseMetadata.approved,
1500
+ selectedScopes: responseMetadata.selectedScopes,
1501
+ }
1502
+ : responseMetadata.type === "searchable_dropdown_response"
1503
+ ? {
1504
+ responseToMessageId: responseMetadata.responseToMessageId,
1505
+ selectedValue: responseMetadata.selectedValue,
1506
+ selectedLabel: responseMetadata.selectedLabel,
1507
+ }
1508
+ : {
1509
+ responseToMessageId: responseMetadata.responseToMessageId,
1510
+ approved: responseMetadata.approved,
1511
+ enableTesting: responseMetadata.enableTesting,
1512
+ };
1513
+ try {
1514
+ const chatmessageId = await this.chatSessionStore.recordUserResponse(request.prompt, responseMetadata.type, metadata);
1515
+ if (chatmessageId && this.clark.context.peer) {
1516
+ void this.clark.context.peer.call
1517
+ .aiPushMessage({
1518
+ role: "user",
1519
+ type: responseMetadata.type,
1520
+ content: request.prompt,
1521
+ id: chatmessageId,
1522
+ timestamp: Date.now(),
1523
+ responseToMessageId: responseMetadata.responseToMessageId,
1524
+ })
1525
+ .catch((err) => {
1526
+ this.getLogger().warn("[ai-service] Failed to echo pre-call paywall widget response", getErrorMeta(err));
1527
+ });
1528
+ }
1529
+ }
1530
+ catch (err) {
1531
+ this.getLogger().warn("[ai-service] Failed to persist pre-call paywall widget response", getErrorMeta(err));
1532
+ }
1533
+ }
1534
+ // 2. Persist + echo the credit advice. Same canned text as
1535
+ // `agent-planning.ts:LLM_ERRORED` so users see consistent guidance
1536
+ // across pre-call and mid-stream paywall paths.
1537
+ const advice = "I stopped because credits ran out. Add credits, then send any message to pick up from here.";
1538
+ const adviceId = randomUUID();
1539
+ try {
1540
+ await this.chatSessionStore.recordAssistant({
1541
+ role: "assistant",
1542
+ type: "error",
1543
+ content: advice,
1544
+ id: adviceId,
1545
+ timestamp: Date.now(),
1546
+ });
1547
+ }
1548
+ catch (err) {
1549
+ this.getLogger().warn("[ai-service] Failed to persist pre-call paywall advice", getErrorMeta(err));
1550
+ }
1551
+ if (this.clark.context.peer) {
1552
+ void this.clark.context.peer.call
1553
+ .aiPushMessage({
1554
+ role: "assistant",
1555
+ type: "error",
1556
+ content: advice,
1557
+ id: adviceId,
1558
+ timestamp: Date.now(),
1559
+ })
1560
+ .catch((err) => {
1561
+ this.getLogger().warn("[ai-service] Failed to echo pre-call paywall advice", getErrorMeta(err));
1562
+ });
1563
+ }
1564
+ // 3. Open the paywall modal via the same generation-state event the
1565
+ // mid-stream path uses. The UI saga listens for
1566
+ // `paywallReason && state === CANCELLED` and opens the modal.
1567
+ // Route through `sendUserGenerationStateChannel` (NOT raw
1568
+ // `peer.call.aiSetGenerationState`) so the CANCELLED + hasError +
1569
+ // paywallReason payload is cached in `lastGenerationState`. On
1570
+ // socket reconnect, `handleUserConnected` replays that cached
1571
+ // state — without this, a refresh / reconnect after a pre-call
1572
+ // paywall would replay stale generation state and the modal
1573
+ // would not reopen. (APPS-4080)
1574
+ void sendUserGenerationStateChannel(this.clark)({
1575
+ state: AiGenerationState.CANCELLED,
1576
+ hasError: true,
1577
+ paywallReason,
1578
+ }).catch((err) => {
1579
+ this.getLogger().warn("[ai-service] Failed to send pre-call paywall generation state", getErrorMeta(err));
1580
+ });
1581
+ // 4. Mark this turn as paused-for-credits so the resume hint fires on
1582
+ // the next `USER_SENT_PROMPT` (same as the mid-stream LLM_ERRORED path
1583
+ // does in agent-planning.ts).
1584
+ this.clark.updateContext({ pausedForCreditsReason: paywallReason });
1585
+ // 5. Persist the failed prompt into ContextV2 so the LLM actually sees
1586
+ // it on the resume turn. ContextV2 is a per-turn context layer that
1587
+ // only grows when an LLM call runs (`ctx.startTurn(user)` +
1588
+ // `ctx.addResponse(...)` inside `streamText`). Pre-call paywall
1589
+ // throws BEFORE `transitionTo<$AgentPlanning>`, so `streamText`
1590
+ // never runs and the failed prompt is invisible to the LLM on the
1591
+ // next turn — recordUser above only writes to chat_message + the
1592
+ // live UI socket, not to ContextV2. Without this step the resume
1593
+ // LLM call has no memory of the failed task and (correctly!)
1594
+ // answers "I have no memory of prior tasks" because for *this*
1595
+ // context that's literally true. Add the failed prompt directly
1596
+ // so the resume turn sees it.
1597
+ // Add the failed prompt/response text to ContextV2 for both organic
1598
+ // user prompts AND widget responses (multi_choice, plan, etc.) so the
1599
+ // resume turn's LLM sees what the user said. The only case we skip is
1600
+ // tool_permission_response — those have no persisted chat
1601
+ // representation; the approval is just side-effect state that gets
1602
+ // re-prompted on resume.
1603
+ if (request.responseMetadata?.type !== "tool_permission_response") {
1604
+ try {
1605
+ // `getContextId` reads `clark.context.jwt` for the userId, but the
1606
+ // JWT is normally only stored into clark context by the
1607
+ // AgentPlanning transition further down the flow — which we never
1608
+ // reach on the pre-call paywall path. Always overwrite from
1609
+ // `request.jwt` (the authoritative source for THIS turn) rather
1610
+ // than reusing whatever happens to be in clark.context.jwt — a
1611
+ // stale token left over from a prior peer/reconnect would resolve
1612
+ // to the wrong userId and write this user's prompt into another
1613
+ // user's ContextV2 history.
1614
+ if (request.jwt) {
1615
+ this.clark.updateContext({ jwt: request.jwt });
1616
+ }
1617
+ const contextId = getContextId(this.clark, this.config);
1618
+ const llmConfig = request.llmConfig;
1619
+ if (contextId && llmConfig) {
1620
+ // Redact secrets from the prompt before adding to ContextV2.
1621
+ // The resume turn's safety pass uses `scope: "last"`, which only
1622
+ // covers the NEXT user message — so this replayed message is
1623
+ // outside the normal redaction window. Without this redact-on-
1624
+ // persist step, a user pasting a token into a quota-denied
1625
+ // prompt would still forward that token to the model provider
1626
+ // on resume. Mirrors the same scanContentForSecrets +
1627
+ // SecretRedactor pipeline `llm/client.ts:streamText` runs for
1628
+ // organic user prompts. Fail-open on scan errors (same as
1629
+ // mainline) so a flaky scanner doesn't break the resume path —
1630
+ // logged so we can audit if this ever fires in prod.
1631
+ let promptForContext = request.prompt;
1632
+ try {
1633
+ const scanResult = await scanContentForSecrets(request.prompt);
1634
+ if (scanResult.success && scanResult.hasSecrets) {
1635
+ this.getLogger().warn(`[secret-scan] Redacting ${scanResult.findings.length} secret(s) from pre-call paywall prompt before ContextV2 persist`, {
1636
+ findingCount: scanResult.findings.length,
1637
+ hasVerified: scanResult.hasVerifiedSecrets,
1638
+ detectorTypes: scanResult.findings.map((f) => f.detectorType),
1639
+ });
1640
+ const redactor = new SecretRedactor(scanResult.findings);
1641
+ promptForContext = redactor.redact(request.prompt);
1642
+ }
1643
+ else if (!scanResult.success) {
1644
+ // Do NOT log `scanResult.error` directly — it can contain
1645
+ // raw TruffleHog stdout/stderr payloads, which on some
1646
+ // failure paths include excerpts of the scanned prompt
1647
+ // (i.e. unredacted user secret material). Log only that the
1648
+ // scan failed; operators can correlate via timestamp +
1649
+ // peerId. (APPS-4080 security review)
1650
+ this.getLogger().warn("[secret-scan] Secret scan failed on pre-call paywall prompt; persisting unredacted (fail-open, matches mainline)");
1651
+ }
1652
+ }
1653
+ catch (scanErr) {
1654
+ // Same reason — don't pipe raw scanner error meta to logs.
1655
+ // `getErrorMeta` extracts message/stack which can include
1656
+ // unredacted prompt content from the scanner subprocess.
1657
+ this.getLogger().warn("[secret-scan] Secret scan threw on pre-call paywall prompt; persisting unredacted (fail-open, matches mainline)", {
1658
+ errorKind: scanErr instanceof Error ? scanErr.name : typeof scanErr,
1659
+ });
1660
+ }
1661
+ const ctx = await this.contextManagerV2.getOrCreateContext(contextId, llmConfig.contextOptionsV2);
1662
+ await ctx.addUserMessage({
1663
+ role: "user",
1664
+ content: promptForContext,
1665
+ });
1666
+ }
1667
+ }
1668
+ catch (err) {
1669
+ this.getLogger().warn("[ai-service] Failed to add pre-call paywall user prompt to ContextV2 (chat_message persistence unaffected)", getErrorMeta(err));
1670
+ }
1671
+ }
1672
+ }
1237
1673
  async handleAiGenerate(request) {
1238
1674
  this.getLogger().info(`[ai-service] handleAiGenerate: peerId=${this.stablePeer.peerId ?? "unknown"}`);
1239
1675
  if (this.clark.state === ClarkStateNames.Dead) {
@@ -1244,11 +1680,24 @@ export class AiService extends TracedEventEmitter {
1244
1680
  this.getLogger().info("[ai-service] Ignoring duplicate aiGenerate re-send", { requestId: request.requestId });
1245
1681
  return;
1246
1682
  }
1683
+ this.assertNoActivePolicyGateRun();
1247
1684
  if (this.isBusy()) {
1248
1685
  this.getLogger().warn("[ai-service] Service is busy. State:", this.clark.state);
1249
1686
  throw new Error("Service is busy");
1250
1687
  }
1251
- await this.ensureAiQuotaAvailable(request.jwt);
1688
+ // Pre-call quota check. When denied, `ensureAiQuotaAvailable` throws so
1689
+ // the UI gets a paywall reason and opens the modal. Without intervention
1690
+ // the throw means we never reach `transitionTo<$AgentPlanning>` below,
1691
+ // so `agent-planning.ts:USER_SENT_PROMPT` never records the user prompt
1692
+ // and `idle.ts:AGENT_CANCELED` never persists the credit advice — the UI
1693
+ // ends up with the optimistic user message removed (thunks.ts catch path)
1694
+ // and no record of what the user tried. Catch it here, persist prompt +
1695
+ // advice + open modal via the same `aiSetGenerationState(CANCELLED,
1696
+ // paywallReason)` saga path the mid-stream paywall uses, then resolve
1697
+ // the socket call so the UI's catch doesn't run. (APPS-4080)
1698
+ if (await this.tryHandlePaywallForRequest(request)) {
1699
+ return;
1700
+ }
1252
1701
  if (request.requestId &&
1253
1702
  !this.requestDeduplicator.markIfNew(request.requestId)) {
1254
1703
  this.getLogger().warn("[ai-service] Ignoring duplicate aiGenerate re-send (lost dedup race)", { requestId: request.requestId });
@@ -1278,6 +1727,11 @@ export class AiService extends TracedEventEmitter {
1278
1727
  return {
1279
1728
  ...context,
1280
1729
  promptId,
1730
+ // Third-party import turns are billed as app_migration; everything
1731
+ // else falls back to the default in-app Clark generation metadata.
1732
+ usageMetadata: request.importSource
1733
+ ? { origin: "app_migration", source: request.importSource }
1734
+ : undefined,
1281
1735
  useMockGeneration: request.mockGeneration ?? false,
1282
1736
  planContext: mergePlanContexts(context.planContext, request.planContext),
1283
1737
  browserContext: request.browserContext,
@@ -1372,7 +1826,7 @@ export class AiService extends TracedEventEmitter {
1372
1826
  promptId,
1373
1827
  applicationId: this.config.applicationId,
1374
1828
  });
1375
- const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
1829
+ const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
1376
1830
  prompt_id: promptId,
1377
1831
  application_id: this.config.applicationId,
1378
1832
  session_id: this.sessionId,
@@ -1539,7 +1993,7 @@ export class AiService extends TracedEventEmitter {
1539
1993
  const llmProviderSettings = this.getLLMProviderSettings(llmConfig);
1540
1994
  const promptId = randomUUID();
1541
1995
  getLogger().info("[ai-service] Generated prompt_id for summarize api usage", { promptId, applicationId: this.config.applicationId });
1542
- const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
1996
+ const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
1543
1997
  prompt_id: promptId,
1544
1998
  application_id: this.config.applicationId,
1545
1999
  session_id: this.sessionId,
@@ -1655,7 +2109,7 @@ export class AiService extends TracedEventEmitter {
1655
2109
  promptId,
1656
2110
  applicationId: this.config.applicationId,
1657
2111
  });
1658
- const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
2112
+ const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
1659
2113
  prompt_id: promptId,
1660
2114
  application_id: this.config.applicationId,
1661
2115
  session_id: this.sessionId,
@@ -1749,7 +2203,7 @@ export class AiService extends TracedEventEmitter {
1749
2203
  promptId,
1750
2204
  applicationId: this.config.applicationId,
1751
2205
  });
1752
- const llmProvider = createLLMProvider(llmProviderSettings, () => request.jwt, llmConfig, () => ({
2206
+ const llmProvider = createLLMProvider(llmProviderSettings, () => this.requireProvidedInferenceJwt(request.jwt, "live-edit request"), llmConfig, () => ({
1753
2207
  prompt_id: promptId,
1754
2208
  application_id: this.config.applicationId,
1755
2209
  session_id: this.sessionId,
@@ -1920,9 +2374,52 @@ export class AiService extends TracedEventEmitter {
1920
2374
  this.clark.updateContext((context) => ({
1921
2375
  ...context,
1922
2376
  promptId: undefined,
2377
+ usageMetadata: undefined,
1923
2378
  }));
1924
2379
  void this.clark.send({ type: USER_CANCELED });
1925
2380
  }
2381
+ /**
2382
+ * UI-initiated cancellation of a background task. Delegates to
2383
+ * `TaskStore.cancel`, which aborts the local poll loop and best-effort
2384
+ * invokes the task type's `cancel` hook on the external system. The
2385
+ * status listener wired in the constructor pushes the resulting
2386
+ * `cancelled` summary to the UI; this method just returns whether a
2387
+ * transition actually happened so the thunk can short-circuit retries.
2388
+ *
2389
+ * `callerUserId` is the JWT-derived identity of the RPC caller (the
2390
+ * socket handler extracts it from `peerAuthorization`). When the
2391
+ * target task has a recorded `ownerUserId` that does not match, we
2392
+ * return the same `{ cancelled: false }` no-op as the unknown /
2393
+ * already-terminal cases. This is intentional: surfacing a distinct
2394
+ * "not your task" error would let a malicious caller probe the
2395
+ * existence of other users' task IDs. See PR #19718 review feedback.
2396
+ *
2397
+ * Returns `{ cancelled: false }` when:
2398
+ * - the background-tasks framework is disabled
2399
+ * - the task is unknown or already terminal
2400
+ * - the caller is not the task's owner
2401
+ */
2402
+ async cancelBackgroundTask(taskId, callerUserId) {
2403
+ if (!this.taskStore) {
2404
+ this.getLogger().warn(`[ai-service] cancelBackgroundTask called but background-tasks framework is disabled; taskId=${taskId}`);
2405
+ return { cancelled: false };
2406
+ }
2407
+ const before = this.taskStore.get(taskId);
2408
+ if (!before || before.state !== "running") {
2409
+ return { cancelled: false };
2410
+ }
2411
+ if (!isTaskAccessibleTo(before, callerUserId)) {
2412
+ // Opaque deny: same shape as unknown/terminal so callers cannot
2413
+ // distinguish "your task is not running" from "this is not your
2414
+ // task". Log at warn so cross-user cancellation attempts are
2415
+ // observable in ops without leaking to the client.
2416
+ this.getLogger().warn(`[ai-service] cancelBackgroundTask denied: cross-owner attempt; taskId=${taskId}`);
2417
+ return { cancelled: false };
2418
+ }
2419
+ await this.taskStore.cancel(taskId);
2420
+ const after = this.taskStore.get(taskId);
2421
+ return { cancelled: after?.state === "cancelled" };
2422
+ }
1926
2423
  /**
1927
2424
  * Submit a prompt with interrupt behavior.
1928
2425
  * If not busy, starts immediately. Otherwise interrupts and continues.
@@ -1939,6 +2436,7 @@ export class AiService extends TracedEventEmitter {
1939
2436
  this.getLogger().info("[ai-service] Ignoring duplicate aiGenerateWithQueue re-send", { requestId: request.requestId, mode });
1940
2437
  return { status: "started" };
1941
2438
  }
2439
+ this.assertNoActivePolicyGateRun();
1942
2440
  if (!this.isBusy()) {
1943
2441
  await this.handleAiGenerate(request);
1944
2442
  return { status: "started" };
@@ -1951,7 +2449,15 @@ export class AiService extends TracedEventEmitter {
1951
2449
  * The partial response is preserved in chat history for context.
1952
2450
  */
1953
2451
  async interruptAndContinue(request) {
1954
- await this.ensureAiQuotaAvailable(request.jwt);
2452
+ // The user interrupted — abort the in-flight generation FIRST, before
2453
+ // anything else. The caller (UI saga) signals `interrupted` immediately
2454
+ // on this method's return, so the old run must actually be dead by
2455
+ // then. An earlier version of this method checked quota before
2456
+ // aborting, which let the old run keep producing tokens while the
2457
+ // paywall UI was rendering — racing the paywall state on the wire.
2458
+ // The right shape: abort -> wait -> install fresh AbortController ->
2459
+ // persist any partial -> then decide whether to continue with a new
2460
+ // generation (or paywall the new prompt). (APPS-4080)
1955
2461
  const partialResponse = this.clark.context.partialResponse?.text ?? "";
1956
2462
  const preservedContext = {
1957
2463
  jwt: this.clark.context.jwt,
@@ -1962,14 +2468,19 @@ export class AiService extends TracedEventEmitter {
1962
2468
  this.clark.updateContext({ isInterruptAbort: true });
1963
2469
  this.clark.context.abortController?.abort();
1964
2470
  await this.waitForAbortComplete();
2471
+ // Note: `interruptedPreviousGeneration` and `interruptOriginalPrompt`
2472
+ // are NOT set yet — they're set below, AFTER the paywall check, so
2473
+ // they don't survive into the next turn on a paywall return. If we
2474
+ // set them here and then the paywall path returned, `agent-planning.ts`
2475
+ // would preferentially use the stale `interruptOriginalPrompt` over
2476
+ // the user's actual follow-up message on the resume turn (it has a
2477
+ // `clark.context.interruptOriginalPrompt ?? request.prompt` fallback).
1965
2478
  this.clark.updateContext({
1966
2479
  jwt: preservedContext.jwt,
1967
2480
  llmConfig: preservedContext.llmConfig,
1968
2481
  availableIntegrations: preservedContext.availableIntegrations,
1969
2482
  abortController: new AbortController(),
1970
2483
  partialResponse: undefined,
1971
- interruptedPreviousGeneration: true,
1972
- interruptOriginalPrompt: request.prompt,
1973
2484
  });
1974
2485
  if (partialResponse.trim().length > 10) {
1975
2486
  await this.chatSessionStore.recordAssistant({
@@ -1978,6 +2489,24 @@ export class AiService extends TracedEventEmitter {
1978
2489
  type: "text",
1979
2490
  });
1980
2491
  }
2492
+ // Now that the old run is reliably dead, check quota for the new
2493
+ // prompt. If paywalled, persist + open modal + return — same flow as
2494
+ // `handleAiGenerate`. The aborted run is NOT resumed; the user can
2495
+ // start fresh once credits are restored via the same "send any
2496
+ // message" affordance the standard paywall uses.
2497
+ if (await this.tryHandlePaywallForRequest(request)) {
2498
+ return;
2499
+ }
2500
+ // Only set the interrupt-state fields once we're committed to calling
2501
+ // `handleAiGenerate` below — `agent-planning.ts` consumes
2502
+ // `interruptOriginalPrompt` on the immediately-next `USER_SENT_PROMPT`
2503
+ // and clears it. Setting them only on this happy path keeps them from
2504
+ // leaking into a future unrelated turn if anything between here and
2505
+ // the LLM call throws.
2506
+ this.clark.updateContext({
2507
+ interruptedPreviousGeneration: true,
2508
+ interruptOriginalPrompt: request.prompt,
2509
+ });
1981
2510
  const enhancedPrompt = `[The user interrupted the previous generation to say this. It may be an update/correction to what you were doing, or a new direction entirely - use your judgment based on the message:]\n\n${request.prompt}`;
1982
2511
  const enhancedRequest = {
1983
2512
  ...request,
@@ -2372,6 +2901,17 @@ export class AiService extends TracedEventEmitter {
2372
2901
  // disconnected flush in order as part of setInner.
2373
2902
  this.stablePeer.setInner({ peer, peerId });
2374
2903
  this.getLogger().info(`[ai-service] Peer set: ${peerId}`);
2904
+ // A replacement (or freshly reconnecting) peer must re-prove its
2905
+ // identity via `notifyAuthenticatedRpc` before live task pushes
2906
+ // trust it. Drop any identity carried over from the peer we just
2907
+ // replaced so the bound peerId and the authenticated identity can't
2908
+ // disagree — otherwise a peer that binds before the previous peer's
2909
+ // disconnect is processed would receive the previous user's task
2910
+ // pushes (PR #19718 Comment 59).
2911
+ if (this.authenticatedPeerId !== peerId) {
2912
+ this.authenticatedPeerId = undefined;
2913
+ this.authenticatedPeerUserId = undefined;
2914
+ }
2375
2915
  // 1. Sync generation state if generation is in progress
2376
2916
  if (this.clark.context.lastGenerationState) {
2377
2917
  this.getLogger().info(`[ai-service] Syncing generation state on reconnect: ${this.clark.context.lastGenerationState.state}`);
@@ -2415,6 +2955,84 @@ export class AiService extends TracedEventEmitter {
2415
2955
  this.getLogger().warn(`[ai-service] Failed to clear stale tool permission on reconnect`, error);
2416
2956
  });
2417
2957
  }
2958
+ // 3. Defer the background-task summary resync until the peer
2959
+ // proves auth via its first authenticated RPC. The socket
2960
+ // upgrade path only validates `applicationId`; auth is enforced
2961
+ // by RPC middleware, which hasn't run yet at connect time. An
2962
+ // unauthenticated client that manages to open the socket would
2963
+ // otherwise receive the active task list. The resync drains
2964
+ // inside `notifyAuthenticatedRpc(peerId)`, called by the
2965
+ // auth middleware (see `socket-manager.ts`) on first success.
2966
+ if (this.taskStore) {
2967
+ this.pendingTaskResyncPeerIds.add(peerId);
2968
+ }
2969
+ }
2970
+ /**
2971
+ * Per-session set of peer IDs that connected since their last task
2972
+ * resync drained. Populated by `handleUserConnected`, drained inside
2973
+ * `notifyAuthenticatedRpc` exactly once per (connect, first auth
2974
+ * success) pair — so a fresh tab gets the active task list as soon
2975
+ * as auth is proven, but never before.
2976
+ */
2977
+ pendingTaskResyncPeerIds = new Set();
2978
+ /**
2979
+ * The authenticated peer's identity, but only while that same peer is
2980
+ * still the bound `stablePeer`. `handleUserConnected` can swap in a
2981
+ * replacement peer before the previous peer's disconnect is processed;
2982
+ * keying the identity to its peerId means a live status push in that
2983
+ * window is gated against `undefined` (suppressed) rather than the
2984
+ * previous user — closing the cross-user leak in PR #19718 Comment 59.
2985
+ * Returns `undefined` until the currently-bound peer proves auth via
2986
+ * `notifyAuthenticatedRpc`, so owned-task pushes fail closed.
2987
+ */
2988
+ getAuthenticatedPeerUserId() {
2989
+ return this.authenticatedPeerId === this.stablePeer.peerId
2990
+ ? this.authenticatedPeerUserId
2991
+ : undefined;
2992
+ }
2993
+ /**
2994
+ * Hook for the socket-manager auth middleware to call after every
2995
+ * successful authorization check. The first call per peer drains any
2996
+ * deferred state pushes registered by `handleUserConnected` (today:
2997
+ * the active background-task summary resync). Subsequent calls for
2998
+ * the same connection are no-ops; the set is repopulated on the
2999
+ * next connect/reconnect.
3000
+ *
3001
+ * `callerUserId` is the JWT-derived identity of the just-authenticated
3002
+ * RPC caller. The resync filters by `isTaskAccessibleTo(task, callerUserId)`
3003
+ * so user B reconnecting to an AiService that's running user A's task
3004
+ * does not receive A's `taskId` / label / message. Tasks with no
3005
+ * recorded `ownerUserId` (legacy/local-dev) remain visible to anyone,
3006
+ * matching the LLM-facing `listTasks` semantics.
3007
+ *
3008
+ * NOTE: The legacy `aiSetGenerationState` / tool-permission pushes
3009
+ * in `handleUserConnected` have the same pre-auth exposure but are
3010
+ * out of scope for this gate — they were not introduced by the
3011
+ * background-task framework. Move them here when tightening that
3012
+ * threat surface (see PR #19718 review feedback).
3013
+ */
3014
+ notifyAuthenticatedRpc(peerId, callerUserId) {
3015
+ // Track the authenticated peer's identity (keyed to its peerId) so
3016
+ // `wireTaskStatusPushToPeer` can owner-filter live status
3017
+ // transitions (Comment 57, PR #19718).
3018
+ if (this.stablePeer.peerId === peerId) {
3019
+ this.authenticatedPeerId = peerId;
3020
+ this.authenticatedPeerUserId = callerUserId;
3021
+ }
3022
+ if (!this.pendingTaskResyncPeerIds.has(peerId))
3023
+ return;
3024
+ // Delete before invoking the push so concurrent middleware calls
3025
+ // for the same peer don't double-fire.
3026
+ this.pendingTaskResyncPeerIds.delete(peerId);
3027
+ // Defense in depth: only push for the currently-bound peer. A
3028
+ // late auth notification for a peer that's already been replaced
3029
+ // shouldn't leak state to the now-current peer's history.
3030
+ if (this.taskStore && this.stablePeer.peerId === peerId) {
3031
+ resyncActiveTaskStatusesToPeer(this.taskStore, {
3032
+ getPeer: () => this.stablePeer,
3033
+ logger: this.getLogger(),
3034
+ }, callerUserId);
3035
+ }
2418
3036
  }
2419
3037
  handleUserDisconnected(peerId) {
2420
3038
  const currentPeerId = this.stablePeer.peerId;
@@ -2427,6 +3045,13 @@ export class AiService extends TracedEventEmitter {
2427
3045
  return; // Don't clear the current peer on stale disconnect
2428
3046
  }
2429
3047
  this.getLogger().info(`[ai-service] peer disconnecting: ${peerId}`);
3048
+ // Drop any deferred state-push registration for this peer so a
3049
+ // disconnect that races a never-arrived auth proof doesn't leave
3050
+ // the entry around forever (the auth middleware never gets to
3051
+ // drain it on a dead connection).
3052
+ this.pendingTaskResyncPeerIds.delete(peerId);
3053
+ this.authenticatedPeerUserId = undefined;
3054
+ this.authenticatedPeerId = undefined;
2430
3055
  const contextId = this.getContextId();
2431
3056
  if (contextId) {
2432
3057
  this.contextManagerV2.clearContext(contextId);
@@ -2617,10 +3242,6 @@ export class AiService extends TracedEventEmitter {
2617
3242
  */
2618
3243
  async handleJudgeEvaluation(request) {
2619
3244
  this.getLogger().info(`[ai-service] Judge evaluation requested for promptId=${request.promptId}, appId=${request.appId}`);
2620
- // Temporarily set the JWT for this evaluation
2621
- const previousJwt = this.tokenManagerJwt;
2622
- this.tokenManagerJwt = request.jwt;
2623
- this.getLogger().info(`[ai-service] Set tokenManagerJwt for judge evaluation. hasJwt=${!!request.jwt}, jwtLength=${request.jwt?.length || 0}`);
2624
3245
  // Judge storage location is configurable via JUDGE_STORAGE_PATH env var
2625
3246
  // Supports both directory paths (writes to evaluations.csv) and full file paths
2626
3247
  // Default: <appRoot>/.superblocks/judge-evaluations/evaluations.csv
@@ -2651,7 +3272,7 @@ export class AiService extends TracedEventEmitter {
2651
3272
  this.config.logger.info(`[ai-service] Judge storage configured: ${path.join(storageDir, storageFilename)}`);
2652
3273
  // Create LLM provider for judge with JWT getter
2653
3274
  // Use getLLMProviderSettings to ensure proper configuration (including upstreamProvider)
2654
- const judgeLlmProvider = createLLMProvider(this.getLLMProviderSettings(request.llmConfig), () => this.getJwt(), request.llmConfig, undefined);
3275
+ const judgeLlmProvider = createLLMProvider(this.getLLMProviderSettings(request.llmConfig), () => this.requireProvidedInferenceJwt(request.jwt, "judge evaluation"), request.llmConfig, undefined);
2655
3276
  const judgeService = new JudgeService({
2656
3277
  llmClient: this.llmClient,
2657
3278
  llmProvider: judgeLlmProvider,
@@ -2692,10 +3313,6 @@ export class AiService extends TracedEventEmitter {
2692
3313
  this.getLogger().error(`[ai-service] Judge evaluation failed: ${String(error)}`, getErrorMeta(error));
2693
3314
  throw error;
2694
3315
  }
2695
- finally {
2696
- // Restore previous JWT
2697
- this.tokenManagerJwt = previousJwt;
2698
- }
2699
3316
  }
2700
3317
  async handlePolicyGateExecution(request) {
2701
3318
  const logger = this.getLogger();
@@ -2703,9 +3320,10 @@ export class AiService extends TracedEventEmitter {
2703
3320
  const requestLlmConfig = request.llmConfig;
2704
3321
  const contextLlmConfig = this.clark.context.llmConfig;
2705
3322
  const llmConfig = requestLlmConfig ?? contextLlmConfig;
2706
- const llmProvider = createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.getJwt(), llmConfig, () => ({
3323
+ const llmProvider = createLLMProvider(this.getLLMProviderSettings(llmConfig), () => this.requireProvidedInferenceJwt(request.jwt, "policy gate"), llmConfig, () => ({
2707
3324
  ai_agent_run_id: request.aiAgentRunId,
2708
3325
  application_id: request.applicationId,
3326
+ metadata: POLICY_GATE_USAGE_METADATA,
2709
3327
  session_id: this.sessionId,
2710
3328
  trigger_type: "policy_gate",
2711
3329
  }));
@@ -2777,7 +3395,30 @@ export class AiService extends TracedEventEmitter {
2777
3395
  tools,
2778
3396
  stopWhen: stepCountIs(30),
2779
3397
  });
3398
+ // Best-effort: surface the agent's latest tool/reasoning step in the editor
3399
+ // so a running policy row shows live activity instead of an opaque spinner.
3400
+ // Only meaningful when an editor session is connected; never blocks the run.
3401
+ const peer = this.clark.context.peer;
3402
+ const emitAgentStep = (part) => {
3403
+ if (!this.hasConnectedPeer() || !peer) {
3404
+ return;
3405
+ }
3406
+ const step = policyGateStreamPartToAgentStep(part);
3407
+ if (!step) {
3408
+ return;
3409
+ }
3410
+ void peer.call
3411
+ .aiPushPolicyAgentStep({
3412
+ applicationId: request.applicationId,
3413
+ policyId: request.reviewPolicyId,
3414
+ step,
3415
+ })
3416
+ .catch((error) => {
3417
+ logger.warn("[ai-service] Failed to push policy agent step", getErrorMeta(error));
3418
+ });
3419
+ };
2780
3420
  for await (const part of result.fullStream) {
3421
+ emitAgentStep(part);
2781
3422
  const partType = typeof part.type === "string"
2782
3423
  ? part.type
2783
3424
  : "unknown";
@@ -2915,13 +3556,21 @@ export class AiService extends TracedEventEmitter {
2915
3556
  pendingToolPermissionRequest: undefined,
2916
3557
  });
2917
3558
  }
3559
+ /** Clears local LLM context so migration-era memory does not survive abort. */
3560
+ async clearMigrationAbortLocalContext() {
3561
+ const contextId = getContextId(this.clark, this.config);
3562
+ await this.contextManagerV2.clearAllContexts();
3563
+ if (contextId) {
3564
+ await this.contextManagerV2.persistEmptyContext(contextId);
3565
+ }
3566
+ }
2918
3567
  /**
2919
3568
  * Upload the (now-empty) context to Bucketeer.
2920
3569
  * commitId is captured before newChat() clears the session.
2921
3570
  * Failures are logged but do not fail clear chat.
2922
3571
  */
2923
3572
  async uploadEmptyContextToBucketeer(userId, commitId) {
2924
- const jwt = this.getJwt();
3573
+ const jwt = this.getInferenceJwt();
2925
3574
  const branchName = this._syncContextProvider?.getBranchName();
2926
3575
  if (!jwt || !commitId || !branchName) {
2927
3576
  return;