@nocobase/app-plugin-agents 0.1.0-beta.10 → 0.1.0-beta.12

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 (286) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +33 -3
  3. package/dist/client/api/client.d.ts +3 -2
  4. package/dist/client/api/client.d.ts.map +1 -1
  5. package/dist/client/api/client.js +3 -0
  6. package/dist/client/api/client.js.map +1 -1
  7. package/dist/client/api/runners.d.ts +2 -2
  8. package/dist/client/api/runners.d.ts.map +1 -1
  9. package/dist/client/api/runners.js.map +1 -1
  10. package/dist/client/components/variables/variable-dialogs.d.ts.map +1 -1
  11. package/dist/client/components/variables/variable-dialogs.js +7 -7
  12. package/dist/client/components/variables/variable-dialogs.js.map +1 -1
  13. package/dist/client/config.d.ts +2 -0
  14. package/dist/client/config.d.ts.map +1 -1
  15. package/dist/client/config.js +2 -0
  16. package/dist/client/config.js.map +1 -1
  17. package/dist/client/lib/skill-settings-context.d.ts +9 -0
  18. package/dist/client/lib/skill-settings-context.d.ts.map +1 -0
  19. package/dist/client/lib/skill-settings-context.js +4 -0
  20. package/dist/client/lib/skill-settings-context.js.map +1 -0
  21. package/dist/client/lib/skill-settings.d.ts +12 -0
  22. package/dist/client/lib/skill-settings.d.ts.map +1 -0
  23. package/dist/client/lib/skill-settings.js +37 -0
  24. package/dist/client/lib/skill-settings.js.map +1 -0
  25. package/dist/client/locales/en-US.d.ts +2 -0
  26. package/dist/client/locales/en-US.d.ts.map +1 -1
  27. package/dist/client/locales/en-US.js +2 -0
  28. package/dist/client/locales/en-US.js.map +1 -1
  29. package/dist/client/locales/runtimes.en-US.d.ts +0 -1
  30. package/dist/client/locales/runtimes.en-US.d.ts.map +1 -1
  31. package/dist/client/locales/runtimes.en-US.js +0 -1
  32. package/dist/client/locales/runtimes.en-US.js.map +1 -1
  33. package/dist/client/locales/runtimes.zh-CN.d.ts.map +1 -1
  34. package/dist/client/locales/runtimes.zh-CN.js +0 -1
  35. package/dist/client/locales/runtimes.zh-CN.js.map +1 -1
  36. package/dist/client/locales/skills.en-US.d.ts +7 -0
  37. package/dist/client/locales/skills.en-US.d.ts.map +1 -1
  38. package/dist/client/locales/skills.en-US.js +8 -1
  39. package/dist/client/locales/skills.en-US.js.map +1 -1
  40. package/dist/client/locales/skills.zh-CN.d.ts.map +1 -1
  41. package/dist/client/locales/skills.zh-CN.js +8 -1
  42. package/dist/client/locales/skills.zh-CN.js.map +1 -1
  43. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  44. package/dist/client/locales/zh-CN.js +2 -0
  45. package/dist/client/locales/zh-CN.js.map +1 -1
  46. package/dist/client/pages/agents/agent-dialog.d.ts.map +1 -1
  47. package/dist/client/pages/agents/agent-dialog.js +4 -4
  48. package/dist/client/pages/agents/agent-dialog.js.map +1 -1
  49. package/dist/client/pages/runtimes/connect.js.map +1 -1
  50. package/dist/client/pages/runtimes/index.js.map +1 -1
  51. package/dist/client/pages/runtimes/runner-cells.d.ts +3 -3
  52. package/dist/client/pages/runtimes/runner-cells.d.ts.map +1 -1
  53. package/dist/client/pages/runtimes/runner-sheet.d.ts +2 -2
  54. package/dist/client/pages/runtimes/runner-sheet.d.ts.map +1 -1
  55. package/dist/client/pages/runtimes/runner-sheet.js +15 -1
  56. package/dist/client/pages/runtimes/runner-sheet.js.map +1 -1
  57. package/dist/client/pages/runtimes/workspace-diagnostics.d.ts +2 -2
  58. package/dist/client/pages/runtimes/workspace-diagnostics.d.ts.map +1 -1
  59. package/dist/client/pages/runtimes/workspace-diagnostics.js +3 -5
  60. package/dist/client/pages/runtimes/workspace-diagnostics.js.map +1 -1
  61. package/dist/client/pages/skills/detail/index.d.ts.map +1 -1
  62. package/dist/client/pages/skills/detail/index.js +11 -15
  63. package/dist/client/pages/skills/detail/index.js.map +1 -1
  64. package/dist/client/pages/skills/index.d.ts.map +1 -1
  65. package/dist/client/pages/skills/index.js +2 -2
  66. package/dist/client/pages/skills/index.js.map +1 -1
  67. package/dist/database/migrations/.manifest.json +14 -2
  68. package/dist/database/migrations/202610020003_ag_create_runs.d.ts.map +1 -1
  69. package/dist/database/migrations/202610020003_ag_create_runs.js +0 -1
  70. package/dist/database/migrations/202610020003_ag_create_runs.js.map +1 -1
  71. package/dist/database/migrations/202610250001_ag_comment_runner_consent.d.ts +4 -0
  72. package/dist/database/migrations/202610250001_ag_comment_runner_consent.d.ts.map +1 -0
  73. package/dist/database/migrations/202610250001_ag_comment_runner_consent.js +41 -0
  74. package/dist/database/migrations/202610250001_ag_comment_runner_consent.js.map +1 -0
  75. package/dist/database/migrations/202610260001_ag_drop_directory_index.d.ts +4 -0
  76. package/dist/database/migrations/202610260001_ag_drop_directory_index.d.ts.map +1 -0
  77. package/dist/database/migrations/202610260001_ag_drop_directory_index.js +24 -0
  78. package/dist/database/migrations/202610260001_ag_drop_directory_index.js.map +1 -0
  79. package/dist/database/migrations/202610260001_ag_run_revision.d.ts +4 -0
  80. package/dist/database/migrations/202610260001_ag_run_revision.d.ts.map +1 -0
  81. package/dist/database/migrations/202610260001_ag_run_revision.js +19 -0
  82. package/dist/database/migrations/202610260001_ag_run_revision.js.map +1 -0
  83. package/dist/package.json +1 -1
  84. package/dist/server/cli/surface.d.ts.map +1 -1
  85. package/dist/server/cli/surface.js +4 -1
  86. package/dist/server/cli/surface.js.map +1 -1
  87. package/dist/server/composition.d.ts +4 -0
  88. package/dist/server/composition.d.ts.map +1 -1
  89. package/dist/server/composition.js +23 -4
  90. package/dist/server/composition.js.map +1 -1
  91. package/dist/server/core/agents/agent.service.d.ts.map +1 -1
  92. package/dist/server/core/agents/agent.service.js +5 -1
  93. package/dist/server/core/agents/agent.service.js.map +1 -1
  94. package/dist/server/core/agents/agent.store.d.ts +3 -1
  95. package/dist/server/core/agents/agent.store.d.ts.map +1 -1
  96. package/dist/server/core/agents/agent.store.js +13 -6
  97. package/dist/server/core/agents/agent.store.js.map +1 -1
  98. package/dist/server/core/agents/index.d.ts +1 -1
  99. package/dist/server/core/agents/index.d.ts.map +1 -1
  100. package/dist/server/core/agents/index.js +1 -1
  101. package/dist/server/core/agents/index.js.map +1 -1
  102. package/dist/server/core/brief/brief.js +3 -3
  103. package/dist/server/core/brief/brief.js.map +1 -1
  104. package/dist/server/core/callers/index.d.ts +9 -0
  105. package/dist/server/core/callers/index.d.ts.map +1 -1
  106. package/dist/server/core/callers/index.js +15 -0
  107. package/dist/server/core/callers/index.js.map +1 -1
  108. package/dist/server/core/conversations/chat-settings.d.ts.map +1 -1
  109. package/dist/server/core/conversations/chat-settings.js +12 -2
  110. package/dist/server/core/conversations/chat-settings.js.map +1 -1
  111. package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
  112. package/dist/server/core/conversations/conversation.service.js +3 -0
  113. package/dist/server/core/conversations/conversation.service.js.map +1 -1
  114. package/dist/server/core/reports/model-usage.d.ts.map +1 -1
  115. package/dist/server/core/reports/model-usage.js +42 -44
  116. package/dist/server/core/reports/model-usage.js.map +1 -1
  117. package/dist/server/core/reports/prices.d.ts.map +1 -1
  118. package/dist/server/core/reports/prices.js +29 -5
  119. package/dist/server/core/reports/prices.js.map +1 -1
  120. package/dist/server/core/reports/report.service.d.ts +4 -2
  121. package/dist/server/core/reports/report.service.d.ts.map +1 -1
  122. package/dist/server/core/reports/report.service.js +147 -68
  123. package/dist/server/core/reports/report.service.js.map +1 -1
  124. package/dist/server/core/runs/claim.d.ts +17 -3
  125. package/dist/server/core/runs/claim.d.ts.map +1 -1
  126. package/dist/server/core/runs/claim.js +325 -70
  127. package/dist/server/core/runs/claim.js.map +1 -1
  128. package/dist/server/core/runs/comment-consent.d.ts +24 -0
  129. package/dist/server/core/runs/comment-consent.d.ts.map +1 -0
  130. package/dist/server/core/runs/comment-consent.js +116 -0
  131. package/dist/server/core/runs/comment-consent.js.map +1 -0
  132. package/dist/server/core/runs/execution.d.ts +1 -1
  133. package/dist/server/core/runs/execution.d.ts.map +1 -1
  134. package/dist/server/core/runs/execution.js +7 -2
  135. package/dist/server/core/runs/execution.js.map +1 -1
  136. package/dist/server/core/runs/identity.d.ts +3 -0
  137. package/dist/server/core/runs/identity.d.ts.map +1 -1
  138. package/dist/server/core/runs/identity.js +38 -12
  139. package/dist/server/core/runs/identity.js.map +1 -1
  140. package/dist/server/core/runs/index.d.ts +2 -2
  141. package/dist/server/core/runs/index.d.ts.map +1 -1
  142. package/dist/server/core/runs/index.js +2 -2
  143. package/dist/server/core/runs/index.js.map +1 -1
  144. package/dist/server/core/runs/input-gate.d.ts.map +1 -1
  145. package/dist/server/core/runs/input-gate.js +4 -0
  146. package/dist/server/core/runs/input-gate.js.map +1 -1
  147. package/dist/server/core/runs/lifecycle.d.ts.map +1 -1
  148. package/dist/server/core/runs/lifecycle.js +71 -35
  149. package/dist/server/core/runs/lifecycle.js.map +1 -1
  150. package/dist/server/core/runs/policy.d.ts +9 -2
  151. package/dist/server/core/runs/policy.d.ts.map +1 -1
  152. package/dist/server/core/runs/policy.js +17 -8
  153. package/dist/server/core/runs/policy.js.map +1 -1
  154. package/dist/server/core/runs/ports.d.ts +8 -1
  155. package/dist/server/core/runs/ports.d.ts.map +1 -1
  156. package/dist/server/core/runs/ports.js.map +1 -1
  157. package/dist/server/core/runs/run-data-purge.d.ts +15 -0
  158. package/dist/server/core/runs/run-data-purge.d.ts.map +1 -0
  159. package/dist/server/core/runs/run-data-purge.js +82 -0
  160. package/dist/server/core/runs/run-data-purge.js.map +1 -0
  161. package/dist/server/core/runs/run-requests.d.ts +25 -0
  162. package/dist/server/core/runs/run-requests.d.ts.map +1 -1
  163. package/dist/server/core/runs/run-requests.js +131 -63
  164. package/dist/server/core/runs/run-requests.js.map +1 -1
  165. package/dist/server/core/runs/run.service.d.ts +19 -1
  166. package/dist/server/core/runs/run.service.d.ts.map +1 -1
  167. package/dist/server/core/runs/run.service.js +523 -35
  168. package/dist/server/core/runs/run.service.js.map +1 -1
  169. package/dist/server/core/runs/run.store.d.ts +10 -0
  170. package/dist/server/core/runs/run.store.d.ts.map +1 -1
  171. package/dist/server/core/runs/run.store.js +38 -11
  172. package/dist/server/core/runs/run.store.js.map +1 -1
  173. package/dist/server/core/runs/runner-view.d.ts +5 -0
  174. package/dist/server/core/runs/runner-view.d.ts.map +1 -1
  175. package/dist/server/core/runs/runner-view.js +5 -1
  176. package/dist/server/core/runs/runner-view.js.map +1 -1
  177. package/dist/server/core/runs/sweeper.d.ts +1 -0
  178. package/dist/server/core/runs/sweeper.d.ts.map +1 -1
  179. package/dist/server/core/runs/sweeper.js +13 -8
  180. package/dist/server/core/runs/sweeper.js.map +1 -1
  181. package/dist/server/core/runs/transitions.d.ts +5 -2
  182. package/dist/server/core/runs/transitions.d.ts.map +1 -1
  183. package/dist/server/core/runs/transitions.js +42 -14
  184. package/dist/server/core/runs/transitions.js.map +1 -1
  185. package/dist/server/core/runs/workload.d.ts +17 -6
  186. package/dist/server/core/runs/workload.d.ts.map +1 -1
  187. package/dist/server/core/runs/workload.js +182 -126
  188. package/dist/server/core/runs/workload.js.map +1 -1
  189. package/dist/server/core/skills/skill.service.d.ts +6 -2
  190. package/dist/server/core/skills/skill.service.d.ts.map +1 -1
  191. package/dist/server/core/skills/skill.service.js +87 -66
  192. package/dist/server/core/skills/skill.service.js.map +1 -1
  193. package/dist/server/core/skills/skill.store.d.ts +14 -0
  194. package/dist/server/core/skills/skill.store.d.ts.map +1 -1
  195. package/dist/server/core/skills/skill.store.js +30 -0
  196. package/dist/server/core/skills/skill.store.js.map +1 -1
  197. package/dist/server/core/transaction-read.d.ts +4 -0
  198. package/dist/server/core/transaction-read.d.ts.map +1 -0
  199. package/dist/server/core/transaction-read.js +30 -0
  200. package/dist/server/core/transaction-read.js.map +1 -0
  201. package/dist/server/core/variables/index.d.ts +1 -1
  202. package/dist/server/core/variables/index.d.ts.map +1 -1
  203. package/dist/server/core/variables/index.js.map +1 -1
  204. package/dist/server/core/variables/variable.service.d.ts +11 -0
  205. package/dist/server/core/variables/variable.service.d.ts.map +1 -1
  206. package/dist/server/core/variables/variable.service.js +63 -20
  207. package/dist/server/core/variables/variable.service.js.map +1 -1
  208. package/dist/server/kernel/scopes.d.ts +6 -0
  209. package/dist/server/kernel/scopes.d.ts.map +1 -1
  210. package/dist/server/kernel/scopes.js +3 -1
  211. package/dist/server/kernel/scopes.js.map +1 -1
  212. package/dist/server/online/services.d.ts.map +1 -1
  213. package/dist/server/online/services.js +101 -24
  214. package/dist/server/online/services.js.map +1 -1
  215. package/dist/server/providers/agents.d.ts +12 -0
  216. package/dist/server/providers/agents.d.ts.map +1 -1
  217. package/dist/server/providers/agents.js +6 -0
  218. package/dist/server/providers/agents.js.map +1 -1
  219. package/dist/server/providers/runners.d.ts +3 -1
  220. package/dist/server/providers/runners.d.ts.map +1 -1
  221. package/dist/server/providers/runners.js +35 -3
  222. package/dist/server/providers/runners.js.map +1 -1
  223. package/dist/server/routes/admin.d.ts +2 -0
  224. package/dist/server/routes/admin.d.ts.map +1 -1
  225. package/dist/server/routes/admin.js +30 -8
  226. package/dist/server/routes/admin.js.map +1 -1
  227. package/dist/server/routes/chat.d.ts.map +1 -1
  228. package/dist/server/routes/chat.js +27 -6
  229. package/dist/server/routes/chat.js.map +1 -1
  230. package/dist/server/routes/index.d.ts.map +1 -1
  231. package/dist/server/routes/index.js +3 -0
  232. package/dist/server/routes/index.js.map +1 -1
  233. package/dist/server/routes/run-requests.js +3 -3
  234. package/dist/server/routes/run-requests.js.map +1 -1
  235. package/dist/server/routes/runners/admin.d.ts.map +1 -1
  236. package/dist/server/routes/runners/admin.js +55 -9
  237. package/dist/server/routes/runners/admin.js.map +1 -1
  238. package/dist/server/routes/runners/install.d.ts.map +1 -1
  239. package/dist/server/routes/runners/install.js +7 -2
  240. package/dist/server/routes/runners/install.js.map +1 -1
  241. package/dist/server/routes/runners/runner.d.ts.map +1 -1
  242. package/dist/server/routes/runners/runner.js +2 -0
  243. package/dist/server/routes/runners/runner.js.map +1 -1
  244. package/dist/server/routes/schemas.d.ts +7 -1
  245. package/dist/server/routes/schemas.d.ts.map +1 -1
  246. package/dist/server/routes/schemas.js +201 -66
  247. package/dist/server/routes/schemas.js.map +1 -1
  248. package/dist/server/runners/runner.service.d.ts +22 -2
  249. package/dist/server/runners/runner.service.d.ts.map +1 -1
  250. package/dist/server/runners/runner.service.js +138 -56
  251. package/dist/server/runners/runner.service.js.map +1 -1
  252. package/dist/server/runners/runner.store.d.ts +2 -2
  253. package/dist/server/runners/runner.store.d.ts.map +1 -1
  254. package/dist/server/runners/runner.store.js +8 -5
  255. package/dist/server/runners/runner.store.js.map +1 -1
  256. package/dist/server/runners/slots.d.ts +5 -2
  257. package/dist/server/runners/slots.d.ts.map +1 -1
  258. package/dist/server/runners/slots.js +3 -4
  259. package/dist/server/runners/slots.js.map +1 -1
  260. package/dist/server/tokens.d.ts +1 -0
  261. package/dist/server/tokens.d.ts.map +1 -1
  262. package/dist/server/tokens.js +1 -0
  263. package/dist/server/tokens.js.map +1 -1
  264. package/dist/shared/agents.d.ts +0 -2
  265. package/dist/shared/agents.d.ts.map +1 -1
  266. package/dist/shared/agents.js.map +1 -1
  267. package/dist/shared/conversations.d.ts +2 -0
  268. package/dist/shared/conversations.d.ts.map +1 -1
  269. package/dist/shared/conversations.js.map +1 -1
  270. package/dist/shared/runners.d.ts +11 -2
  271. package/dist/shared/runners.d.ts.map +1 -1
  272. package/dist/shared/runners.js.map +1 -1
  273. package/dist/shared/runs.d.ts +18 -0
  274. package/dist/shared/runs.d.ts.map +1 -1
  275. package/dist/shared/runs.js +22 -0
  276. package/dist/shared/runs.js.map +1 -1
  277. package/dist/shared/skills.d.ts +11 -5
  278. package/dist/shared/skills.d.ts.map +1 -1
  279. package/dist/shared/skills.js +7 -2
  280. package/dist/shared/skills.js.map +1 -1
  281. package/dist/shared/variables.js +1 -1
  282. package/dist/shared/variables.js.map +1 -1
  283. package/package.json +15 -15
  284. package/skills/nocobase-app-plugin-agents/references/extension-points.md +19 -19
  285. package/skills/nocobase-app-plugin-agents/references/runners.md +6 -0
  286. package/skills/nocobase-app-plugin-agents/references/variables-skills-usage.md +3 -1
@@ -1,3 +1,4 @@
1
+ import { GLOBAL_SKILL_TARGET } from '../../../shared/skills.js';
1
2
  /**
2
3
  * Claiming: handing queued runs to a runner that fits them, within each agent's concurrency limit, one run per
3
4
  * (agent, subject, thread) at a time, through the runners' one long-poll (`routes/runners/runner.ts`), which hands
@@ -29,6 +30,16 @@
29
30
  *
30
31
  * Portable by design: the database layer offers no `FOR UPDATE SKIP LOCKED` or advisory locks, and the guarded update
31
32
  * plus the agent-row write give the same guarantees on every dialect (SQLite serializes writers anyway).
33
+ *
34
+ * Polls are the hot path: every runner walks the same candidates, so a write per candidate is a write per candidate per
35
+ * runner per poll. Everything that rules a candidate out is read outside any transaction — identity, `fits()`, slots,
36
+ * concurrency and comment consent — and a transaction opens only for the run about to be claimed. The agent row is
37
+ * the only row a claim locks with a self-update, and only in that final transaction after `fits()` (or when a stale
38
+ * identity has to be repaired first, which current work never needs). PostgreSQL runs at READ COMMITTED, where each
39
+ * statement sees the latest committed rows, so no transaction needs no-op writes to refresh a snapshot; comment
40
+ * consent needs no chain lock, because confirmation is final and rejection or Run as me end the chain's queued runs,
41
+ * which the guarded update sees. A new write or lock here needs a measured reason (`tests/claim-concurrency.test.ts`
42
+ * counts them, and the lock waits on PostgreSQL).
32
43
 
33
44
  */
34
45
  import { ANNOUNCED_FAILURES, isProtocolSupported, policyAllowsAgent, policyAllowsRepo, policyAllowsSubject, TIMINGS, } from '@nocobase/agent-protocol';
@@ -38,8 +49,8 @@ import { createHash } from 'node:crypto';
38
49
  import { later } from '../../kernel/clock.js';
39
50
  import { notFound } from '../../kernel/errors.js';
40
51
  import { runSecretsKey } from '../../kernel/redaction.js';
41
- import { cleanList, covers } from '../../kernel/values.js';
42
- import { findAgent, lockAgentForClaim } from '../agents/index.js';
52
+ import { cleanList, covers, jsonObject } from '../../kernel/values.js';
53
+ import { findAgent, findAgents, lockAgentForClaim } from '../agents/index.js';
43
54
  import { joinBrief, renderBrief } from '../brief/index.js';
44
55
  import { cliPackageFor } from './cli-package.js';
45
56
  import { executionHistory } from './execution.js';
@@ -47,11 +58,12 @@ import { describeVariables, variableRefs, } from '../variables/index.js';
47
58
  import { mountsFor, prepareExtensions, repoAccessFor, sessionKeyOf, withSections, } from './extensions.js';
48
59
  import { MAX_CLAIM_FAILURES, toolPolicyFor } from './policy.js';
49
60
  import { mintRunToken } from './run-tokens.js';
50
- import { runsHeldByTool } from './runner-view.js';
51
- import { ACTIVE, countActive, eventsRepo, findRunRecord, markDelivered, pendingInputs, runsOfKey, runsRepo, sessionsRepo, toInput, toRun, } from './run.store.js';
61
+ import { ACTIVE, countActive, eventsRepo, findRunRecord, markDelivered, pendingInputs, runsOfKey, runsRepo, inputsRepo, sessionsRepo, toInput, toExecutions, toRun, } from './run.store.js';
52
62
  import { dropInputs } from './input-gate.js';
53
63
  import { finishRun } from './transitions.js';
54
- import { refreshQueuedIdentity } from './identity.js';
64
+ import { queuedIdentityCurrent, refreshQueuedIdentity } from './identity.js';
65
+ import { currentCommentOwner, commentConsent, commentFromOwner, withdrawCommentRequests, } from './comment-consent.js';
66
+ import { insertRunRequest, runRequestsRepo } from './run-requests.js';
55
67
  import { consumeReset, saveBrief } from './workspace.store.js';
56
68
  /** Queued runs looked at per claim, oldest first by priority. */
57
69
  const CANDIDATES = 50;
@@ -158,11 +170,11 @@ export function variableTargets(agent, assembly) {
158
170
  { scope: 'agent', scopeId: agent.id },
159
171
  ];
160
172
  }
161
- /** Where a run's skills come from, the agent's first so its own win a shared slug. */
162
- export function skillTargets(agent, assembly) {
173
+ /** Team defaults, application scopes and directories, from lowest to highest priority. */
174
+ export function skillTargets(_agent, assembly) {
163
175
  return [
164
- { scope: 'agent', scopeId: agent.id },
165
- ...assembly.scopes,
176
+ GLOBAL_SKILL_TARGET,
177
+ ...assembly.scopes.filter((target) => target.scope !== 'agent' && target.scope !== 'global'),
166
178
  ...assembly.dirs.flatMap((dir) => dir.scopeId ? [{ scope: 'workdir', scopeId: dir.scopeId }] : []),
167
179
  ];
168
180
  }
@@ -234,14 +246,31 @@ async function settleClaimInputs(conn, pending, effective, now) {
234
246
  }
235
247
  export function createClaimService(deps) {
236
248
  const { tx, clock } = deps;
249
+ /** Reuse only identical ordered targets, within this attempt and connection. */
250
+ function claimResources(conn, runId) {
251
+ const cache = new Map();
252
+ return (agent, assembly) => {
253
+ const skills = skillTargets(agent, assembly);
254
+ const variables = variableTargets(agent, assembly);
255
+ const key = JSON.stringify([skills, variables]);
256
+ let result = cache.get(key);
257
+ if (!result) {
258
+ result = (async () => ({
259
+ skills: await deps.skills.forRun(conn, skills, runId),
260
+ variables: await deps.variables.prepareRun(conn, variables),
261
+ }))();
262
+ cache.set(key, result);
263
+ }
264
+ return result;
265
+ };
266
+ }
237
267
  /** Database reads only; used by the actual claim and ordered-model diagnostics. */
238
- async function assemblyCompatibility(conn, run, agent, runner, assembly) {
268
+ async function assemblyCompatibility(conn, run, agent, runner, assembly, resources = claimResources(conn, run.id)) {
239
269
  if (!isProtocolSupported(runner.protocolVersion) ||
240
270
  (assembly.workspaceKey && !supportsWorkspaceKey(runner)))
241
271
  return 'incompatible';
242
- const skills = await deps.skills.forRun(conn, skillTargets(agent, assembly), run.id);
243
- const targets = variableTargets(agent, assembly);
244
- const names = await deps.variables.namesOf(conn, targets);
272
+ const { skills, variables } = await resources(agent, assembly);
273
+ const names = variables.names;
245
274
  if (!covers(runner.features, [
246
275
  ...run.requires,
247
276
  ...payloadRequires(payloadDirs(assembly.dirs), skills.length, names.length, Boolean(assembly.workspaceKey)),
@@ -252,18 +281,18 @@ export function createClaimService(deps) {
252
281
  assembly.dirs.some((dir) => (dir.kind === 'repo' && !policyAllowsRepo(runner.policy, dir.url)) ||
253
282
  (dir.kind === 'directory' && dir.runnerId !== runner.id)))
254
283
  return 'restricted';
255
- if (runner.trust !== 'team' &&
256
- (await deps.variables.teamOnly(conn, targets)).length > 0)
284
+ if (runner.trust !== 'team' && variables.teamOnly.length > 0)
257
285
  return 'restricted';
258
286
  return 'ready';
259
287
  }
260
- async function selectionFacts(conn, run, agent, requirePolicy = true) {
288
+ async function selectionFacts(conn, run, agent, requirePolicy = true, trust, resources = claimResources(conn, run.id)) {
261
289
  const policy = await deps.selectionPolicies?.forRun(conn, run);
262
290
  if ((requirePolicy && !policy) || !deps.runners)
263
291
  return null;
264
292
  const now = clock.now();
265
293
  const start = queueStart(run);
266
- const registered = await deps.runners.all(conn);
294
+ const registered = (await deps.runners.all(conn)).filter((runner) => !trust ||
295
+ (trust === 'team' ? runner.trust === 'team' : runner.trust !== 'team'));
267
296
  const binding = deps.subjects.get(run.subject.kind);
268
297
  if (!binding)
269
298
  throw new Error(`No context provider for ${run.subject.kind}.`);
@@ -325,7 +354,7 @@ export function createClaimService(deps) {
325
354
  });
326
355
  continue;
327
356
  }
328
- const reason = await assemblyCompatibility(conn, run, single, runner, assembly);
357
+ const reason = await assemblyCompatibility(conn, run, single, runner, assembly, resources);
329
358
  if (reason !== 'ready') {
330
359
  incompatible ||= reason === 'incompatible';
331
360
  loads.push({
@@ -336,7 +365,8 @@ export function createClaimService(deps) {
336
365
  });
337
366
  continue;
338
367
  }
339
- const requiredVariables = await deps.variables.passthroughOf(conn, variableTargets(single, assembly));
368
+ const requiredVariables = (await resources(single, assembly)).variables
369
+ .passthrough;
340
370
  if (requiredVariables.length > 0 && runner.variables == null) {
341
371
  unknown = true;
342
372
  continue;
@@ -344,8 +374,9 @@ export function createClaimService(deps) {
344
374
  if (requiredVariables.some((name) => !runner.variables?.includes(name)))
345
375
  continue;
346
376
  compatible.push(runner);
347
- const byTool = await runsHeldByTool(conn, runner.id);
348
- const used = await deps.slotsUsed(conn, runner.id);
377
+ const held = await deps.slotsHeld(conn, runner.id);
378
+ const byTool = held.byTool ?? {};
379
+ const used = held.jobs + held.runs;
349
380
  const directory = directoryKeyOf(runner.id, payloadDirs(assembly.dirs));
350
381
  const blockedDirectory = directory !== null &&
351
382
  (await directoryBusy(conn, runner.id, run.id, directory));
@@ -434,7 +465,7 @@ export function createClaimService(deps) {
434
465
  entries,
435
466
  };
436
467
  }
437
- async function assemble({ conn }, run, agent, runner, inputs, leaseExpiresAt, prepared, entry) {
468
+ async function assemble({ conn }, run, agent, runner, inputs, leaseExpiresAt, prepared, entry, resources = claimResources(conn, run.id)) {
438
469
  const binding = deps.subjects.get(run.subjectKind);
439
470
  if (!binding)
440
471
  throw new Error(`No context provider is registered for subjects of kind "${run.subjectKind}".`);
@@ -449,7 +480,7 @@ export function createClaimService(deps) {
449
480
  };
450
481
  const assembly = await binding.context.assemble(conn, claim, prepared.get('subject'));
451
482
  // workspace.key is required for coordinating runs; old runners must not silently ignore it.
452
- if ((await assemblyCompatibility(conn, toRun(run), agent, runner, assembly)) === 'incompatible')
483
+ if ((await assemblyCompatibility(conn, toRun(run), agent, runner, assembly, resources)) === 'incompatible')
453
484
  throw new NotForThisRunner();
454
485
  inputs = assembly.inputs ?? inputs;
455
486
  const effectiveClaim = { ...claim, inputs };
@@ -466,9 +497,8 @@ export function createClaimService(deps) {
466
497
  if (directoryKey !== null &&
467
498
  (await directoryBusy(conn, runner.id, run.id, directoryKey)))
468
499
  throw new NotForThisRunner();
469
- const skills = await deps.skills.forRun(conn, skillTargets(agent, assembly), run.id);
470
- const variables = variableTargets(agent, assembly);
471
- const names = await deps.variables.namesOf(conn, variables);
500
+ const { skills, variables } = await resources(agent, assembly);
501
+ const names = variables.names;
472
502
  const requires = cleanList([
473
503
  ...view.requires,
474
504
  ...payloadRequires(dirs, skills.length, names.length, Boolean(assembly.workspaceKey)),
@@ -477,19 +507,19 @@ export function createClaimService(deps) {
477
507
  throw new NotForThisRunner();
478
508
  // A variable for team runners only keeps a personal runner off the run.
479
509
  if (names.length > 0 && runner.trust !== 'team') {
480
- const teamOnly = await deps.variables.teamOnly(conn, variables);
510
+ const teamOnly = variables.teamOnly;
481
511
  if (teamOnly.length > 0)
482
512
  throw new TeamRunnersOnly(teamOnly);
483
513
  }
484
514
  const env = names.length > 0
485
- ? await deps.variables.forRun(conn, variables, {
515
+ ? await variables.deliver({
486
516
  runId: run.id,
487
517
  runnerId: runner.id,
488
518
  })
489
519
  : [];
490
520
  // Variables taken from the runner: names only. A runner that does not provide one fails the run while preparing
491
521
  // it, saying how to provide it; runners are not chosen by them.
492
- const passthrough = await deps.variables.passthroughOf(conn, variables);
522
+ const passthrough = variables.passthrough;
493
523
  const nowText = clock.now().toISOString();
494
524
  const clean = await consumeReset(conn, { kind: run.subjectKind, id: run.subjectId }, run.id, nowText);
495
525
  const session = await sessionsRepo(conn).findOne({
@@ -536,7 +566,7 @@ export function createClaimService(deps) {
536
566
  const sessionNote = resume ? assembly.turn.resumed : assembly.turn.fresh;
537
567
  const turn = [
538
568
  brief.turn,
539
- ...(run.failureReason === 'runnerUpgrade'
569
+ ...(toExecutions(run.executionHistory).find((execution) => execution.attempt === Number(run.attempt) - 1)?.failureReason === 'runnerUpgrade'
540
570
  ? [
541
571
  'The previous attempt was interrupted by a runner upgrade. Continue from the current state.',
542
572
  ]
@@ -657,17 +687,175 @@ export function createClaimService(deps) {
657
687
  });
658
688
  });
659
689
  }
660
- async function admit(unit, run, agent, runner, inputs) {
661
- const rejection = await deps.subjects
662
- .get(run.subjectKind)
663
- ?.context.admit?.(unit.conn, {
690
+ /** What reconciling a comment run would change, read without writing: most runs need nothing. */
691
+ async function planCommentConsent(conn, runId) {
692
+ const none = { kind: 'none' };
693
+ const run = await findRunRecord(conn, runId);
694
+ if (!run?.commentRootRunId)
695
+ return none;
696
+ const root = await findRunRecord(conn, run.commentRootRunId);
697
+ if (!root)
698
+ return none;
699
+ if (run.status !== 'queued') {
700
+ const pending = await runRequestsRepo(conn).findMany({
701
+ filter: {
702
+ commentRootRunId: root.id,
703
+ commentRunId: run.id,
704
+ purpose: 'personalRunner',
705
+ status: 'pending',
706
+ },
707
+ limit: 1,
708
+ });
709
+ return pending.length > 0 ? { kind: 'withdraw', rootId: root.id } : none;
710
+ }
711
+ if (!(await queuedIdentityCurrent(conn, deps, run)))
712
+ return { kind: 'refresh', run };
713
+ if (!(await currentCommentOwner(conn, deps.subjects, root)))
714
+ return {
715
+ kind: 'cancel',
716
+ run,
717
+ detail: 'The comment belongs to an earlier owner assignment.',
718
+ };
719
+ if (root.commentExecution === 'requester' ||
720
+ run.actorUserId !== root.ownerUserId)
721
+ return none;
722
+ const { decision } = await commentConsent(conn, root);
723
+ if (decision === 'confirmed' || decision === 'pending')
724
+ return none;
725
+ if (await commentFromOwner(conn, root))
726
+ return none;
727
+ if (decision === 'rejected')
728
+ return {
729
+ kind: 'cancel',
730
+ run,
731
+ detail: 'The comment personal runner authorization is no longer valid.',
732
+ };
733
+ const agent = await findAgent(conn, run.agentId);
734
+ if (!agent)
735
+ return none;
736
+ const team = await selectionFacts(conn, toRun(run), agent, false, 'team');
737
+ if (!team ||
738
+ team.entries.some((e) => e.compatibleCount > 0 ||
739
+ e.reason === 'unknown' ||
740
+ e.blockers?.includes('unknown')))
741
+ return none;
742
+ const personal = await selectionFacts(conn, toRun(run), agent, false, 'personal');
743
+ if (!personal?.entries.some((e) => e.compatibleCount > 0))
744
+ return none;
745
+ const sourceInputs = await inputsRepo(conn).findMany({
746
+ filter: { runId: root.id },
747
+ sort: (sort) => [sort.field('createdAt').asc(), sort.field('id').asc()],
748
+ });
749
+ const input = sourceInputs.find((input) => input.type === 'comment' &&
750
+ jsonObject(input.payload).commentSourceSnapshot === true) ?? sourceInputs.find((input) => input.type === 'comment');
751
+ return input ? { kind: 'request', run, root, input } : none;
752
+ }
753
+ /**
754
+ * Brings a comment run's consent up to date. It takes no lock of its own: callers that may create a request hold
755
+ * the run's agent row (enqueue, retry and the sweeper do), which serializes the duplicate check in insertRunRequest
756
+ * with every other enqueue and claim of that agent. Everything else is a guarded update.
757
+ */
758
+ async function reconcileCommentConsent(unit, runId) {
759
+ let plan = await planCommentConsent(unit.conn, runId);
760
+ if (plan.kind === 'refresh') {
761
+ if (!(await refreshQueuedIdentity(unit, deps, plan.run)))
762
+ return;
763
+ plan = await planCommentConsent(unit.conn, runId);
764
+ }
765
+ switch (plan.kind) {
766
+ case 'withdraw':
767
+ await withdrawCommentRequests(unit, plan.rootId, clock.now().toISOString(), runId);
768
+ return;
769
+ case 'cancel':
770
+ await finishRun(unit, deps, plan.run, {
771
+ status: 'cancelled',
772
+ reason: null,
773
+ detail: plan.detail,
774
+ });
775
+ return;
776
+ case 'request': {
777
+ const { run, root, input } = plan;
778
+ await insertRunRequest(unit, deps, {
779
+ purpose: 'personalRunner',
780
+ commentRootRunId: root.id,
781
+ commentRunId: run.id,
782
+ commentOwnerKey: root.commentOwnerKey ?? null,
783
+ agentId: run.agentId,
784
+ subject: toRun(run).subject,
785
+ threadScope: run.threadScope,
786
+ // The plan only asks for runs acting as the chain's owner.
787
+ responsibleUserId: run.actorUserId,
788
+ requestedByUserId: root.requestedByUserId ?? root.actorUserId,
789
+ ownerUserId: run.actorUserId,
790
+ priority: Number(run.priority),
791
+ requires: toRun(run).requires,
792
+ fireAt: run.availableAt,
793
+ maxAttempts: Number(run.maxAttempts),
794
+ input: toInput(input),
795
+ });
796
+ return;
797
+ }
798
+ default:
799
+ return;
800
+ }
801
+ }
802
+ /**
803
+ * Whether this runner may take this candidate under comment consent, read outside any transaction. Immutable facts
804
+ * settle most candidates without a query: no comment chain, work run as the commenter, or work not run as the
805
+ * chain's owner. The rest read the chain's requests once. A decision never needs a lock: confirmation is final,
806
+ * and rejection or Run as me end the chain's queued runs, which the claim's guarded `status = 'queued'` update sees.
807
+ */
808
+ async function commentConsentFor(conn, runner, run) {
809
+ const allowed = { allowed: true, confirmedByUserId: null };
810
+ const refused = { allowed: false, confirmedByUserId: null };
811
+ if (!run.commentRootRunId || run.commentExecution === 'requester')
812
+ return allowed;
813
+ const root = await findRunRecord(conn, run.commentRootRunId);
814
+ if (!root || root.commentRootRunId !== root.id)
815
+ return refused;
816
+ if (root.commentExecution === 'requester' ||
817
+ run.actorUserId !== root.ownerUserId)
818
+ return allowed;
819
+ const { decision, confirmation } = await commentConsent(conn, root);
820
+ if (decision === 'rejected')
821
+ return refused;
822
+ if (decision === 'confirmed')
823
+ return {
824
+ allowed: true,
825
+ confirmedByUserId: confirmation?.settledById ?? null,
826
+ };
827
+ if (runner.trust === 'team')
828
+ return allowed;
829
+ return (await commentFromOwner(conn, root)) ? allowed : refused;
830
+ }
831
+ /** Withdraws the request a team claim made obsolete, writing only when one is pending. */
832
+ async function withdrawIfPending(rootId, runId) {
833
+ try {
834
+ const pending = await runRequestsRepo(tx.read()).findMany({
835
+ filter: {
836
+ commentRootRunId: rootId,
837
+ commentRunId: runId,
838
+ purpose: 'personalRunner',
839
+ status: 'pending',
840
+ },
841
+ limit: 1,
842
+ });
843
+ if (pending.length > 0)
844
+ await tx.run((unit) => withdrawCommentRequests(unit, rootId, clock.now().toISOString(), runId));
845
+ }
846
+ catch (error) {
847
+ deps.onClaimFailure?.(runId, error);
848
+ }
849
+ }
850
+ async function admit(unit, run, agent, runner, inputs, prepared) {
851
+ const rejection = await deps.subjects.get(run.subjectKind)?.context.admit?.(unit.conn, {
664
852
  run: toRun(run),
665
853
  agent,
666
854
  runner,
667
855
  inputs,
668
856
  cli: deps.cli.name,
669
857
  appName: deps.app.name,
670
- });
858
+ }, prepared);
671
859
  if (!rejection)
672
860
  return true;
673
861
  await finishRun(unit, deps, run, {
@@ -678,8 +866,15 @@ export function createClaimService(deps) {
678
866
  return false;
679
867
  }
680
868
  async function attempt(runner, agent, candidate, reported) {
681
- // Repair pre-upgrade queued work before even checking personal runner ownership.
682
- if (deps.subjects.get(candidate.subjectKind)?.actsAs) {
869
+ const conn = tx.read();
870
+ // Rules that do not depend on who the run acts as come first, and settle the rest of the pass: an agent at its
871
+ // limit is checked once per poll, not once per queued run.
872
+ if ((await countActive(conn, 'agentId', agent.id)) >= agent.maxConcurrentRuns)
873
+ return { kind: 'full', scope: 'agent' };
874
+ // Repair pre-upgrade queued work before even checking personal runner ownership. Reading first keeps a poll over
875
+ // current work free of transactions and row locks; only work whose identity changed is written.
876
+ if (deps.subjects.get(candidate.subjectKind)?.actsAs &&
877
+ !(await queuedIdentityCurrent(conn, deps, candidate))) {
683
878
  const current = await tx.run(async (unit) => {
684
879
  await lockAgentForClaim(unit.conn, agent.id, clock.now().toISOString());
685
880
  const run = await findRunRecord(unit.conn, candidate.id);
@@ -692,13 +887,12 @@ export function createClaimService(deps) {
692
887
  candidate = current;
693
888
  }
694
889
  // Avoid assembling or asking authorization when a cheap claim rule already rules this work out.
695
- const conn = tx.read();
696
890
  if (!fits(runner, agent, candidate) ||
697
- (await deps.slotsUsed(conn, runner.id)) >= runner.slots ||
698
- (await countActive(conn, 'agentId', agent.id)) >=
699
- agent.maxConcurrentRuns ||
700
891
  (await runsOfKey(conn, candidate, ACTIVE)).length > 0)
701
892
  return { kind: 'skipped' };
893
+ const consent = await commentConsentFor(conn, runner, candidate);
894
+ if (!consent.allowed)
895
+ return { kind: 'skipped' };
702
896
  // Before the transaction: on SQLite it holds the only connection, so the providers read what else they need now.
703
897
  const prepared = new Map(await prepareExtensions(toRun(candidate), deps.sections, deps.mounts, (error) => deps.onClaimFailure?.(candidate.id, error), deps.repoAccess, { onDemand: asksCredentialsOnDemand(runner) }));
704
898
  const subjectContext = deps.subjects.get(candidate.subjectKind)?.context;
@@ -711,6 +905,7 @@ export function createClaimService(deps) {
711
905
  }
712
906
  }
713
907
  let taken;
908
+ let runnerFull = false;
714
909
  try {
715
910
  taken = await tx.run(async (unit) => {
716
911
  const { conn } = unit;
@@ -728,11 +923,14 @@ export function createClaimService(deps) {
728
923
  agent.maxConcurrentRuns)
729
924
  return null;
730
925
  // Slots are shared with the jobs the runner holds.
731
- if ((await deps.slotsUsed(conn, runner.id)) >= runner.slots)
926
+ const held = await deps.slotsHeld(conn, runner.id);
927
+ if (held.runs + held.jobs >= runner.slots) {
928
+ runnerFull = true;
732
929
  return null;
733
- // The first of the agent's tools with room, by its limit here and by what the runner said it can take.
734
- const byTool = await runsHeldByTool(conn, runner.id);
735
- const current = await findRunRecord(conn, candidate.id);
930
+ }
931
+ // Total and per-tool room come from the same locked snapshot.
932
+ const byTool = held.byTool ?? {};
933
+ const current = identity;
736
934
  if (!current ||
737
935
  current.status !== 'queued' ||
738
936
  (current.availableAt &&
@@ -744,38 +942,47 @@ export function createClaimService(deps) {
744
942
  const busy = await runsOfKey(conn, candidate, ACTIVE);
745
943
  if (busy.length > 0)
746
944
  return null;
747
- const leaseExpiresAt = later(now, TIMINGS.leaseMs);
945
+ const leaseExpiresAt = later(now, TIMINGS.startTimeoutMs);
946
+ const dispatchValues = {
947
+ status: 'dispatched',
948
+ failureReason: null,
949
+ failureDetail: null,
950
+ runnerId: runner.id,
951
+ // Brief providers observe this reservation inside the claim transaction.
952
+ // Replace a previous attempt's tool before computing the roster.
953
+ tool: entry.tool,
954
+ leaseExpiresAt,
955
+ dispatchedAt: nowText,
956
+ lastActivityAt: nowText,
957
+ availableAt: null,
958
+ updatedAt: nowText,
959
+ ...(consent.confirmedByUserId
960
+ ? { confirmedByUserId: consent.confirmedByUserId }
961
+ : {}),
962
+ };
748
963
  const claimed = await runsRepo(conn).updateMany({
749
964
  filter: (f) => f.and([
750
965
  f.string('id').eq(candidate.id),
751
966
  f.string('status').eq('queued'),
752
967
  f.string('actorUserId').eq(candidate.actorUserId),
753
968
  ]),
754
- values: {
755
- status: 'dispatched',
756
- runnerId: runner.id,
757
- // Brief providers observe this reservation inside the claim transaction.
758
- // Replace a previous attempt's tool before computing the roster.
759
- tool: entry.tool,
760
- leaseExpiresAt,
761
- dispatchedAt: nowText,
762
- lastActivityAt: nowText,
763
- availableAt: null,
764
- updatedAt: nowText,
765
- },
969
+ values: dispatchValues,
766
970
  });
767
971
  if (claimed.updatedCount !== 1)
768
972
  return null;
769
- const run = (await findRunRecord(conn, candidate.id));
770
- const pending = await pendingInputs(conn, run.id);
973
+ const run = { ...current, ...dispatchValues };
974
+ const pending = (await pendingInputs(conn, run.id)).filter((input) => !run.commentRootRunId ||
975
+ input.type !== 'comment' ||
976
+ jsonObject(input.payload).commentRootRunId === run.commentRootRunId);
771
977
  let assembled;
772
978
  let selectionSnapshot;
773
979
  try {
774
- if (!(await admit(unit, run, agent, runner, pending.map(toInput))))
980
+ if (!(await admit(unit, run, agent, runner, pending.map(toInput), prepared.get('subject'))))
775
981
  return null;
776
982
  // Admission must precede preflight assembly: a missing or obsolete
777
983
  // subject is cancelled normally, rather than counted as setup failure.
778
- const selection = await selectionFacts(conn, toRun(current), agent);
984
+ const resources = claimResources(conn, run.id);
985
+ const selection = await selectionFacts(conn, toRun(current), agent, true, undefined, resources);
779
986
  const elapsed = Math.max(0, (now.getTime() - queueStart(toRun(current))) / 1000);
780
987
  const preferredAvailable = (selection?.entries[0]?.compatibleCount ?? 0) > 0 ||
781
988
  selection?.entries[0]?.reason === 'unknown' ||
@@ -796,7 +1003,7 @@ export function createClaimService(deps) {
796
1003
  waitedSeconds: elapsed,
797
1004
  };
798
1005
  }
799
- assembled = await assemble(unit, run, agent, runner, pending.map(toInput), leaseExpiresAt, prepared, entry);
1006
+ assembled = await assemble(unit, run, agent, runner, pending.map(toInput), leaseExpiresAt, prepared, entry, resources);
800
1007
  }
801
1008
  catch (error) {
802
1009
  if (error instanceof NotForThisRunner)
@@ -863,8 +1070,15 @@ export function createClaimService(deps) {
863
1070
  }
864
1071
  if (!taken) {
865
1072
  await discardRepoAccess(candidate, prepared);
866
- return { kind: 'skipped' };
1073
+ return runnerFull
1074
+ ? { kind: 'full', scope: 'runner' }
1075
+ : { kind: 'skipped' };
867
1076
  }
1077
+ // After the claim commits, never inside it: decisions settle the request before touching runs, so a claim that
1078
+ // held its run while waiting for the request could deadlock with them. A request left pending by a crash here is
1079
+ // withdrawn by the next reconciliation, and confirming it finds the run no longer waiting.
1080
+ if (candidate.commentRootRunId && runner.trust === 'team')
1081
+ await withdrawIfPending(candidate.commentRootRunId, candidate.id);
868
1082
  return { kind: 'claimed', payload: taken };
869
1083
  }
870
1084
  async function discardRepoAccess(run, prepared) {
@@ -897,6 +1111,7 @@ export function createClaimService(deps) {
897
1111
  filter: { id: runId },
898
1112
  values: {
899
1113
  claimFailures: failures,
1114
+ failureReason: 'setupFailed',
900
1115
  failureDetail: error.message.slice(0, 10_000),
901
1116
  },
902
1117
  });
@@ -926,13 +1141,14 @@ export function createClaimService(deps) {
926
1141
  return null;
927
1142
  const conn = tx.read();
928
1143
  const queued = await candidates();
929
- const agents = new Map();
1144
+ const agents = await findAgents(conn, queued
1145
+ .filter((candidate) => !seen.has(candidate.id))
1146
+ .map((candidate) => candidate.agentId));
1147
+ const full = new Set();
930
1148
  for (const candidate of queued) {
931
- if (seen.has(candidate.id))
1149
+ if (seen.has(candidate.id) || full.has(candidate.agentId))
932
1150
  continue;
933
1151
  seen.add(candidate.id);
934
- if (!agents.has(candidate.agentId))
935
- agents.set(candidate.agentId, await findAgent(conn, candidate.agentId));
936
1152
  const agent = agents.get(candidate.agentId);
937
1153
  if (!agent ||
938
1154
  (!deps.subjects.get(candidate.subjectKind)?.actsAs &&
@@ -945,6 +1161,13 @@ export function createClaimService(deps) {
945
1161
  const result = await attempt(runner, agent, candidate, reported);
946
1162
  if (result.kind === 'claimed')
947
1163
  return result.payload;
1164
+ if (result.kind === 'full') {
1165
+ if (result.scope === 'runner')
1166
+ return null;
1167
+ // Seen candidates are passed over for good in this claim; unvisited ones stay eligible for the next pass.
1168
+ seen.delete(candidate.id);
1169
+ full.add(candidate.agentId);
1170
+ }
948
1171
  }
949
1172
  catch (error) {
950
1173
  // Rolled back: the run stays queued for a runner it fits.
@@ -958,6 +1181,38 @@ export function createClaimService(deps) {
958
1181
  return null;
959
1182
  }
960
1183
  return {
1184
+ reconcileCommentConsent,
1185
+ async reconcileCommentQueue() {
1186
+ const conn = tx.read();
1187
+ const queued = await runsRepo(conn).findMany({
1188
+ filter: (f) => f.and([
1189
+ f.string('status').eq('queued'),
1190
+ f.string('commentRootRunId').notEmpty(),
1191
+ ]),
1192
+ sort: (s) => [s.field('createdAt').asc()],
1193
+ limit: 100,
1194
+ });
1195
+ const pending = await runRequestsRepo(conn).findMany({
1196
+ filter: { purpose: 'personalRunner', status: 'pending' },
1197
+ limit: 100,
1198
+ });
1199
+ for (const id of new Set([
1200
+ ...queued.map((run) => run.id),
1201
+ ...pending
1202
+ .map((request) => request.commentRunId)
1203
+ .filter((id) => Boolean(id)),
1204
+ ])) {
1205
+ // Read first: a sweep over settled chains opens no transaction at all.
1206
+ const plan = await planCommentConsent(conn, id);
1207
+ if (plan.kind === 'none')
1208
+ continue;
1209
+ await tx.run(async (unit) => {
1210
+ if (plan.kind === 'request' || plan.kind === 'refresh')
1211
+ await lockAgentForClaim(unit.conn, plan.run.agentId, clock.now().toISOString());
1212
+ await reconcileCommentConsent(unit, id);
1213
+ });
1214
+ }
1215
+ },
961
1216
  diagnose: (conn, run, agent) => selectionFacts(conn, run, agent, false),
962
1217
  async selectionWait(runId) {
963
1218
  const conn = tx.read();
@@ -970,13 +1225,13 @@ export function createClaimService(deps) {
970
1225
  async claim(runner, free, tools) {
971
1226
  if (runner.status !== 'online' || runnerLowOnDisk(runner))
972
1227
  return [];
973
- if ((await deps.slotsUsed(tx.read(), runner.id)) >= runner.slots)
974
- return [];
975
1228
  const payloads = [];
976
1229
  const seen = new Set();
977
1230
  const limit = Math.min(free, runner.slots);
978
1231
  const reported = toolRoom(tools);
979
1232
  while (payloads.length < limit) {
1233
+ if ((await deps.slotsUsed(tx.read(), runner.id)) >= runner.slots)
1234
+ break;
980
1235
  const payload = await claimOne(runner, seen, reported);
981
1236
  if (!payload)
982
1237
  break;