@nocobase/app-plugin-agents 1.0.0-beta.15 → 1.0.0-beta.17

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 (273) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +12 -1
  3. package/dist/client/chat/api.d.ts +1 -1
  4. package/dist/client/chat/api.d.ts.map +1 -1
  5. package/dist/client/chat/api.js +2 -2
  6. package/dist/client/chat/api.js.map +1 -1
  7. package/dist/client/chat/keys.d.ts +1 -0
  8. package/dist/client/chat/keys.d.ts.map +1 -1
  9. package/dist/client/chat/keys.js +1 -0
  10. package/dist/client/chat/keys.js.map +1 -1
  11. package/dist/client/chat/provider.d.ts +10 -4
  12. package/dist/client/chat/provider.d.ts.map +1 -1
  13. package/dist/client/chat/provider.js +86 -15
  14. package/dist/client/chat/provider.js.map +1 -1
  15. package/dist/client/chat/use-chat.d.ts +3 -1
  16. package/dist/client/chat/use-chat.d.ts.map +1 -1
  17. package/dist/client/chat/use-chat.js +2 -2
  18. package/dist/client/chat/use-chat.js.map +1 -1
  19. package/dist/client/chat/use-conversation-model.d.ts +12 -0
  20. package/dist/client/chat/use-conversation-model.d.ts.map +1 -0
  21. package/dist/client/chat/use-conversation-model.js +64 -0
  22. package/dist/client/chat/use-conversation-model.js.map +1 -0
  23. package/dist/client/chat.d.ts +1 -0
  24. package/dist/client/chat.d.ts.map +1 -1
  25. package/dist/client/chat.js +1 -0
  26. package/dist/client/chat.js.map +1 -1
  27. package/dist/client/locales/de-DE.d.ts +5 -0
  28. package/dist/client/locales/de-DE.d.ts.map +1 -0
  29. package/dist/client/locales/de-DE.js +1219 -0
  30. package/dist/client/locales/de-DE.js.map +1 -0
  31. package/dist/client/locales/en-US.d.ts +8 -0
  32. package/dist/client/locales/en-US.d.ts.map +1 -1
  33. package/dist/client/locales/en-US.js +8 -0
  34. package/dist/client/locales/en-US.js.map +1 -1
  35. package/dist/client/locales/es-ES.d.ts +5 -0
  36. package/dist/client/locales/es-ES.d.ts.map +1 -0
  37. package/dist/client/locales/es-ES.js +1228 -0
  38. package/dist/client/locales/es-ES.js.map +1 -0
  39. package/dist/client/locales/fr-FR.d.ts +5 -0
  40. package/dist/client/locales/fr-FR.d.ts.map +1 -0
  41. package/dist/client/locales/fr-FR.js +1229 -0
  42. package/dist/client/locales/fr-FR.js.map +1 -0
  43. package/dist/client/locales/id-ID.d.ts +5 -0
  44. package/dist/client/locales/id-ID.d.ts.map +1 -0
  45. package/dist/client/locales/id-ID.js +1219 -0
  46. package/dist/client/locales/id-ID.js.map +1 -0
  47. package/dist/client/locales/index.d.ts.map +1 -1
  48. package/dist/client/locales/index.js +8 -0
  49. package/dist/client/locales/index.js.map +1 -1
  50. package/dist/client/locales/ja-JP.d.ts +5 -0
  51. package/dist/client/locales/ja-JP.d.ts.map +1 -0
  52. package/dist/client/locales/ja-JP.js +1219 -0
  53. package/dist/client/locales/ja-JP.js.map +1 -0
  54. package/dist/client/locales/pt-BR.d.ts +5 -0
  55. package/dist/client/locales/pt-BR.d.ts.map +1 -0
  56. package/dist/client/locales/pt-BR.js +1229 -0
  57. package/dist/client/locales/pt-BR.js.map +1 -0
  58. package/dist/client/locales/vi-VN.d.ts +5 -0
  59. package/dist/client/locales/vi-VN.d.ts.map +1 -0
  60. package/dist/client/locales/vi-VN.js +1219 -0
  61. package/dist/client/locales/vi-VN.js.map +1 -0
  62. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  63. package/dist/client/locales/zh-CN.js +8 -0
  64. package/dist/client/locales/zh-CN.js.map +1 -1
  65. package/dist/client/locales/zh-TW.d.ts +5 -0
  66. package/dist/client/locales/zh-TW.d.ts.map +1 -0
  67. package/dist/client/locales/zh-TW.js +1219 -0
  68. package/dist/client/locales/zh-TW.js.map +1 -0
  69. package/dist/client/model-suggestion.d.ts +6 -0
  70. package/dist/client/model-suggestion.d.ts.map +1 -0
  71. package/dist/client/model-suggestion.js +25 -0
  72. package/dist/client/model-suggestion.js.map +1 -0
  73. package/dist/client/pages/agents/agent-dialog.d.ts.map +1 -1
  74. package/dist/client/pages/agents/agent-dialog.js +2 -9
  75. package/dist/client/pages/agents/agent-dialog.js.map +1 -1
  76. package/dist/client/pages/usage/date-range-picker.d.ts.map +1 -1
  77. package/dist/client/pages/usage/date-range-picker.js +14 -2
  78. package/dist/client/pages/usage/date-range-picker.js.map +1 -1
  79. package/dist/client/pages/usage/index.d.ts.map +1 -1
  80. package/dist/client/pages/usage/index.js +17 -5
  81. package/dist/client/pages/usage/index.js.map +1 -1
  82. package/dist/client/pages/usage/prices/model.d.ts +4 -4
  83. package/dist/client/pages/usage/prices/model.d.ts.map +1 -1
  84. package/dist/client/pages/usage/prices/model.js +2 -2
  85. package/dist/client/pages/usage/prices/model.js.map +1 -1
  86. package/dist/client/pages/usage/prices/price-cells.js +2 -2
  87. package/dist/client/pages/usage/prices/price-cells.js.map +1 -1
  88. package/dist/client/pages/usage/prices/prices-section.d.ts.map +1 -1
  89. package/dist/client/pages/usage/prices/prices-section.js +13 -13
  90. package/dist/client/pages/usage/prices/prices-section.js.map +1 -1
  91. package/dist/package.json +5 -2
  92. package/dist/server/cli/online-credential.d.ts +16 -0
  93. package/dist/server/cli/online-credential.d.ts.map +1 -0
  94. package/dist/server/cli/online-credential.js +51 -0
  95. package/dist/server/cli/online-credential.js.map +1 -0
  96. package/dist/server/cli/run-credential.d.ts +2 -0
  97. package/dist/server/cli/run-credential.d.ts.map +1 -1
  98. package/dist/server/cli/run-credential.js +12 -1
  99. package/dist/server/cli/run-credential.js.map +1 -1
  100. package/dist/server/cli/surface.js +2 -2
  101. package/dist/server/cli/surface.js.map +1 -1
  102. package/dist/server/composition.d.ts +2 -0
  103. package/dist/server/composition.d.ts.map +1 -1
  104. package/dist/server/composition.js +17 -3
  105. package/dist/server/composition.js.map +1 -1
  106. package/dist/server/core/brief/brief.js +1 -1
  107. package/dist/server/core/brief/brief.js.map +1 -1
  108. package/dist/server/core/conversations/chat-settings.d.ts +1 -0
  109. package/dist/server/core/conversations/chat-settings.d.ts.map +1 -1
  110. package/dist/server/core/conversations/chat-settings.js +71 -57
  111. package/dist/server/core/conversations/chat-settings.js.map +1 -1
  112. package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
  113. package/dist/server/core/conversations/conversation.service.js +3 -4
  114. package/dist/server/core/conversations/conversation.service.js.map +1 -1
  115. package/dist/server/core/reports/report.service.d.ts +1 -0
  116. package/dist/server/core/reports/report.service.d.ts.map +1 -1
  117. package/dist/server/core/reports/report.service.js +5 -2
  118. package/dist/server/core/reports/report.service.js.map +1 -1
  119. package/dist/server/core/runs/claim.d.ts +1 -1
  120. package/dist/server/core/runs/claim.d.ts.map +1 -1
  121. package/dist/server/core/runs/claim.js +337 -58
  122. package/dist/server/core/runs/claim.js.map +1 -1
  123. package/dist/server/core/runs/lifecycle.d.ts.map +1 -1
  124. package/dist/server/core/runs/lifecycle.js +4 -2
  125. package/dist/server/core/runs/lifecycle.js.map +1 -1
  126. package/dist/server/core/runs/runner-affinity.d.ts +6 -0
  127. package/dist/server/core/runs/runner-affinity.d.ts.map +1 -0
  128. package/dist/server/core/runs/runner-affinity.js +16 -0
  129. package/dist/server/core/runs/runner-affinity.js.map +1 -0
  130. package/dist/server/core/skills/skill.service.d.ts +2 -0
  131. package/dist/server/core/skills/skill.service.d.ts.map +1 -1
  132. package/dist/server/core/skills/skill.service.js +1 -0
  133. package/dist/server/core/skills/skill.service.js.map +1 -1
  134. package/dist/server/locales/de-DE.d.ts +5 -0
  135. package/dist/server/locales/de-DE.d.ts.map +1 -0
  136. package/dist/server/locales/de-DE.js +54 -0
  137. package/dist/server/locales/de-DE.js.map +1 -0
  138. package/dist/server/locales/es-ES.d.ts +5 -0
  139. package/dist/server/locales/es-ES.d.ts.map +1 -0
  140. package/dist/server/locales/es-ES.js +54 -0
  141. package/dist/server/locales/es-ES.js.map +1 -0
  142. package/dist/server/locales/fr-FR.d.ts +5 -0
  143. package/dist/server/locales/fr-FR.d.ts.map +1 -0
  144. package/dist/server/locales/fr-FR.js +54 -0
  145. package/dist/server/locales/fr-FR.js.map +1 -0
  146. package/dist/server/locales/id-ID.d.ts +5 -0
  147. package/dist/server/locales/id-ID.d.ts.map +1 -0
  148. package/dist/server/locales/id-ID.js +54 -0
  149. package/dist/server/locales/id-ID.js.map +1 -0
  150. package/dist/server/locales/index.d.ts.map +1 -1
  151. package/dist/server/locales/index.js +8 -0
  152. package/dist/server/locales/index.js.map +1 -1
  153. package/dist/server/locales/ja-JP.d.ts +5 -0
  154. package/dist/server/locales/ja-JP.d.ts.map +1 -0
  155. package/dist/server/locales/ja-JP.js +54 -0
  156. package/dist/server/locales/ja-JP.js.map +1 -0
  157. package/dist/server/locales/pt-BR.d.ts +5 -0
  158. package/dist/server/locales/pt-BR.d.ts.map +1 -0
  159. package/dist/server/locales/pt-BR.js +54 -0
  160. package/dist/server/locales/pt-BR.js.map +1 -0
  161. package/dist/server/locales/vi-VN.d.ts +5 -0
  162. package/dist/server/locales/vi-VN.d.ts.map +1 -0
  163. package/dist/server/locales/vi-VN.js +54 -0
  164. package/dist/server/locales/vi-VN.js.map +1 -0
  165. package/dist/server/locales/zh-TW.d.ts +5 -0
  166. package/dist/server/locales/zh-TW.d.ts.map +1 -0
  167. package/dist/server/locales/zh-TW.js +54 -0
  168. package/dist/server/locales/zh-TW.js.map +1 -0
  169. package/dist/server/online/agent/configuration.d.ts +2 -2
  170. package/dist/server/online/agent/configuration.d.ts.map +1 -1
  171. package/dist/server/online/agent/configuration.js +21 -7
  172. package/dist/server/online/agent/configuration.js.map +1 -1
  173. package/dist/server/online/agent/index.d.ts +2 -1
  174. package/dist/server/online/agent/index.d.ts.map +1 -1
  175. package/dist/server/online/agent/index.js.map +1 -1
  176. package/dist/server/online/agent/native/backend.d.ts +2 -0
  177. package/dist/server/online/agent/native/backend.d.ts.map +1 -1
  178. package/dist/server/online/agent/native/backend.js +14 -12
  179. package/dist/server/online/agent/native/backend.js.map +1 -1
  180. package/dist/server/online/agent/native/execution.d.ts +2 -0
  181. package/dist/server/online/agent/native/execution.d.ts.map +1 -1
  182. package/dist/server/online/agent/native/execution.js +51 -22
  183. package/dist/server/online/agent/native/execution.js.map +1 -1
  184. package/dist/server/online/agent/native/types.d.ts +6 -0
  185. package/dist/server/online/agent/native/types.d.ts.map +1 -1
  186. package/dist/server/online/agent/runner/backend.d.ts.map +1 -1
  187. package/dist/server/online/agent/runner/backend.js +4 -3
  188. package/dist/server/online/agent/runner/backend.js.map +1 -1
  189. package/dist/server/online/agent/service.d.ts +2 -0
  190. package/dist/server/online/agent/service.d.ts.map +1 -1
  191. package/dist/server/online/agent/service.js +3 -1
  192. package/dist/server/online/agent/service.js.map +1 -1
  193. package/dist/server/online/agent/types.d.ts +15 -1
  194. package/dist/server/online/agent/types.d.ts.map +1 -1
  195. package/dist/server/online/sandbox/cli-command.d.ts +6 -0
  196. package/dist/server/online/sandbox/cli-command.d.ts.map +1 -0
  197. package/dist/server/online/sandbox/cli-command.js +370 -0
  198. package/dist/server/online/sandbox/cli-command.js.map +1 -0
  199. package/dist/server/online/sandbox/execution.d.ts +25 -0
  200. package/dist/server/online/sandbox/execution.d.ts.map +1 -0
  201. package/dist/server/online/sandbox/execution.js +63 -0
  202. package/dist/server/online/sandbox/execution.js.map +1 -0
  203. package/dist/server/online/sandbox/filesystem.d.ts +8 -0
  204. package/dist/server/online/sandbox/filesystem.d.ts.map +1 -0
  205. package/dist/server/online/sandbox/filesystem.js +72 -0
  206. package/dist/server/online/sandbox/filesystem.js.map +1 -0
  207. package/dist/server/online/sandbox/limits.d.ts +4 -0
  208. package/dist/server/online/sandbox/limits.d.ts.map +1 -0
  209. package/dist/server/online/sandbox/limits.js +46 -0
  210. package/dist/server/online/sandbox/limits.js.map +1 -0
  211. package/dist/server/online/sandbox/provider.d.ts +9 -0
  212. package/dist/server/online/sandbox/provider.d.ts.map +1 -0
  213. package/dist/server/online/sandbox/provider.js +237 -0
  214. package/dist/server/online/sandbox/provider.js.map +1 -0
  215. package/dist/server/online/sandbox/skills.d.ts +14 -0
  216. package/dist/server/online/sandbox/skills.d.ts.map +1 -0
  217. package/dist/server/online/sandbox/skills.js +56 -0
  218. package/dist/server/online/sandbox/skills.js.map +1 -0
  219. package/dist/server/online/sandbox/tools.d.ts +3 -0
  220. package/dist/server/online/sandbox/tools.d.ts.map +1 -0
  221. package/dist/server/online/sandbox/tools.js +35 -0
  222. package/dist/server/online/sandbox/tools.js.map +1 -0
  223. package/dist/server/online/sandbox/types.d.ts +72 -0
  224. package/dist/server/online/sandbox/types.d.ts.map +1 -0
  225. package/dist/server/online/sandbox/types.js +2 -0
  226. package/dist/server/online/sandbox/types.js.map +1 -0
  227. package/dist/server/providers/agents.d.ts +5 -2
  228. package/dist/server/providers/agents.d.ts.map +1 -1
  229. package/dist/server/providers/agents.js +63 -2
  230. package/dist/server/providers/agents.js.map +1 -1
  231. package/dist/server/providers/authorization.d.ts +1 -1
  232. package/dist/server/providers/authorization.d.ts.map +1 -1
  233. package/dist/server/providers/authorization.js +3 -2
  234. package/dist/server/providers/authorization.js.map +1 -1
  235. package/dist/server/routes/chat.d.ts +1 -1
  236. package/dist/server/routes/chat.d.ts.map +1 -1
  237. package/dist/server/routes/chat.js +15 -6
  238. package/dist/server/routes/chat.js.map +1 -1
  239. package/dist/server/routes/index.d.ts.map +1 -1
  240. package/dist/server/routes/index.js +17 -2
  241. package/dist/server/routes/index.js.map +1 -1
  242. package/dist/server/routes/openapi.js +1 -1
  243. package/dist/server/routes/openapi.js.map +1 -1
  244. package/dist/server/routes/schemas.js +1 -1
  245. package/dist/server/routes/schemas.js.map +1 -1
  246. package/dist/server/runners/runner.service.d.ts +1 -1
  247. package/dist/server/runners/runner.service.d.ts.map +1 -1
  248. package/dist/server/runners/runner.service.js +5 -1
  249. package/dist/server/runners/runner.service.js.map +1 -1
  250. package/dist/server/runners/runner.store.d.ts.map +1 -1
  251. package/dist/server/runners/runner.store.js +3 -1
  252. package/dist/server/runners/runner.store.js.map +1 -1
  253. package/dist/server/vendor/THIRD_PARTY_LICENSES.txt +782 -0
  254. package/dist/server/vendor/just-bash.d.ts +137 -0
  255. package/dist/server/vendor/just-bash.js +2102 -0
  256. package/dist/shared/access.d.ts +1 -1
  257. package/dist/shared/access.d.ts.map +1 -1
  258. package/dist/shared/access.js +1 -1
  259. package/dist/shared/access.js.map +1 -1
  260. package/dist/shared/conversations.d.ts +2 -1
  261. package/dist/shared/conversations.d.ts.map +1 -1
  262. package/dist/shared/conversations.js.map +1 -1
  263. package/dist/shared/locales/access.en-US.js +3 -3
  264. package/dist/shared/locales/access.zh-CN.js +4 -4
  265. package/dist/shared/runners.d.ts +1 -1
  266. package/dist/shared/runners.d.ts.map +1 -1
  267. package/dist/shared/runners.js +2 -0
  268. package/dist/shared/runners.js.map +1 -1
  269. package/docs/native-sandbox.md +55 -0
  270. package/docs/ui-message-clients.md +10 -0
  271. package/package.json +17 -14
  272. package/skills/nocobase-app-plugin-agents/SKILL.md +1 -0
  273. package/skills/nocobase-app-plugin-agents/references/online-agent.md +2 -2
@@ -1,3 +1,5 @@
1
+ import { upgradeReservation } from './runner-affinity.js';
2
+ import { ACTIVE_JOB, jobsRepo } from '../../jobs/job.store.js';
1
3
  /**
2
4
  * Claiming: handing queued runs to a runner that fits them, within each agent's concurrency limit, one run per
3
5
  * (agent, subject, thread) at a time, through the runners' one long-poll (`routes/runners/runner.ts`), which hands
@@ -6,7 +8,7 @@
6
8
  * team or belongs to the person who woke the agent, its features cover what the run requires (worked out from the
7
9
  * payload: checkouts, directories, skills, secrets), its owner's local policy (`Runner.policy`) lets it take the
8
10
  * agent's runs on the run's subject and repositories, and it has a free slot. A working directory that is a directory
9
- * on one runner pins the run to that runner, one run at a time per directory. A run's variables go to the runner that
11
+ * on one runner pins the run to that runner; multiple runs may share that directory. A run's variables go to the runner that
10
12
  * takes it, personal ones included, unless one of them is for team runners only (`teamRunnersOnly`): then only a team
11
13
  * runner takes the run, and the variables that asked for one are noted on it (`teamOnlyVariables`), so its wait says
12
14
  * which.
@@ -75,7 +77,7 @@ class AssemblyError extends Error {
75
77
  this.name = 'AssemblyError';
76
78
  }
77
79
  }
78
- /** The run does not fit this runner after all (its payload needs another runner, or a busy directory). */
80
+ /** The run does not fit this runner after all (its payload needs another runner or unavailable capabilities). */
79
81
  class NotForThisRunner extends Error {
80
82
  constructor() {
81
83
  super('The run does not fit this runner.');
@@ -224,20 +226,6 @@ function directoryKeyOf(runnerId, dirs) {
224
226
  .map((dir) => `${runnerId}:${dir.path}`);
225
227
  return paths.length > 0 ? paths.join(DIRECTORY_KEY_SEPARATOR) : null;
226
228
  }
227
- /** Whether another run held by the runner works in any of `key`'s directories. */
228
- async function directoryBusy(conn, runnerId, runId, key) {
229
- const wanted = new Set(key.split(DIRECTORY_KEY_SEPARATOR));
230
- const held = await runsRepo(conn).findMany({
231
- filter: (f) => f.and([
232
- f.string('runnerId').eq(runnerId),
233
- f.string('id').ne(runId),
234
- f.or(ACTIVE.map((status) => f.string('status').eq(status))),
235
- ]),
236
- });
237
- return held.some((run) => (run.directoryKey ?? '')
238
- .split(DIRECTORY_KEY_SEPARATOR)
239
- .some((part) => part !== '' && wanted.has(part)));
240
- }
241
229
  /** Resolve omitted subject input in the claim transaction, keeping it in the audit history. */
242
230
  async function settleClaimInputs(conn, pending, effective, now) {
243
231
  const ids = new Set(effective.map((input) => input.id));
@@ -270,10 +258,16 @@ export function createClaimService(deps) {
270
258
  (assembly.workspaceKey && !supportsWorkspaceKey(runner)))
271
259
  return 'incompatible';
272
260
  const { skills, variables } = await resources(agent, assembly);
261
+ return compatibleAssembly(run, agent, runner, assembly, skills.length, variables);
262
+ }
263
+ function compatibleAssembly(run, agent, runner, assembly, skillCount, variables) {
264
+ if (!isProtocolSupported(runner.protocolVersion) ||
265
+ (assembly.workspaceKey && !supportsWorkspaceKey(runner)))
266
+ return 'incompatible';
273
267
  const names = variables.names;
274
268
  if (!covers(runner.features, [
275
269
  ...run.requires,
276
- ...payloadRequires(payloadDirs(assembly.dirs), skills.length, names.length, Boolean(assembly.workspaceKey)),
270
+ ...payloadRequires(payloadDirs(assembly.dirs), skillCount, names.length, Boolean(assembly.workspaceKey)),
277
271
  ]))
278
272
  return 'incompatible';
279
273
  if (!fitsActor(runner, agent, run.actorUserId, run.requires, clock.now().getTime()) ||
@@ -307,7 +301,8 @@ export function createClaimService(deps) {
307
301
  const entries = [];
308
302
  for (const entry of agent.modelEntries) {
309
303
  const single = { ...agent, modelEntries: [entry] };
310
- const candidates = registered.filter((runner) => runner.status !== 'revoked');
304
+ const reserved = upgradeReservation(run.executions ?? [], registered, now.getTime());
305
+ const candidates = registered.filter((runner) => runner.status !== 'revoked' && (!reserved || runner.id === reserved));
311
306
  const connected = candidates.filter((runner) => (runner.status === 'online' ||
312
307
  runner.status === 'upgrade_required') &&
313
308
  runner.lastSeenAt !== null &&
@@ -321,7 +316,6 @@ export function createClaimService(deps) {
321
316
  let unknown = false;
322
317
  let incompatible = false;
323
318
  let ready = false;
324
- let directoryBlocked = false;
325
319
  for (const runner of online) {
326
320
  if (!isProtocolSupported(runner.protocolVersion) ||
327
321
  runner.status === 'upgrade_required') {
@@ -377,16 +371,11 @@ export function createClaimService(deps) {
377
371
  const held = await deps.slotsHeld(conn, runner.id);
378
372
  const byTool = held.byTool ?? {};
379
373
  const used = held.jobs + held.runs;
380
- const directory = directoryKeyOf(runner.id, payloadDirs(assembly.dirs));
381
- const blockedDirectory = directory !== null &&
382
- (await directoryBusy(conn, runner.id, run.id, directory));
383
- directoryBlocked ||= blockedDirectory;
384
374
  const hasRoom = !runner.draining &&
385
375
  (runner.globalFree ?? 1) > 0 &&
386
376
  used < runner.slots &&
387
377
  (byTool[entry.tool] ?? 0) < toolLimit(runner, entry.tool) &&
388
- (runner.toolLoad?.[entry.tool]?.free ?? 1) > 0 &&
389
- !blockedDirectory;
378
+ (runner.toolLoad?.[entry.tool]?.free ?? 1) > 0;
390
379
  ready ||= hasRoom && runner.globalFree !== undefined;
391
380
  if (hasRoom && runner.globalFree === undefined)
392
381
  unknown = true;
@@ -398,10 +387,10 @@ export function createClaimService(deps) {
398
387
  ? 'unknown'
399
388
  : 'ready'
400
389
  : runner.draining
401
- ? runner.draining
402
- : blockedDirectory
403
- ? 'restricted'
404
- : 'busy',
390
+ ? runner.draining === 'diskLow'
391
+ ? 'busy'
392
+ : runner.draining
393
+ : 'busy',
405
394
  lastSeenAt: runner.lastSeenAt,
406
395
  used,
407
396
  limit: runner.slots,
@@ -426,8 +415,6 @@ export function createClaimService(deps) {
426
415
  : connected.length
427
416
  ? 'toolNotInstalled'
428
417
  : 'offline';
429
- if (directoryBlocked && !ready)
430
- reason = 'restricted';
431
418
  if (!ready &&
432
419
  compatible.length > 0 &&
433
420
  loads.every((runner) => runner.reason === 'updatePending' || runner.reason === 'pruning'))
@@ -465,6 +452,126 @@ export function createClaimService(deps) {
465
452
  entries,
466
453
  };
467
454
  }
455
+ /** Metadata only: batch new scopes, reuse common scopes across machines and visited candidates. */
456
+ function admissionResources(conn, agents = []) {
457
+ const variableScopes = new Set();
458
+ const skillScopes = new Set();
459
+ const variables = [];
460
+ const skills = [];
461
+ function missing(targets, seen) {
462
+ const fresh = new Map();
463
+ for (const target of targets) {
464
+ const key = JSON.stringify([target.scope, target.scopeId]);
465
+ if (!seen.has(key))
466
+ fresh.set(key, target);
467
+ }
468
+ for (const key of fresh.keys())
469
+ seen.add(key);
470
+ return [...fresh.values()];
471
+ }
472
+ return {
473
+ async load(checks) {
474
+ const vars = missing([
475
+ ...checks.flatMap(({ agent, assembly }) => variableTargets(agent, assembly)),
476
+ ...agents.map((agent) => ({ scope: 'agent', scopeId: agent.id })),
477
+ ], variableScopes);
478
+ const attached = missing(checks.flatMap(({ agent, assembly }) => skillTargets(agent, assembly)), skillScopes);
479
+ if (vars.length)
480
+ variables.push(...(await deps.variables.admissionMetadata(conn, vars)));
481
+ if (attached.length)
482
+ skills.push(...(await deps.skills.admissionMetadata(conn, attached)));
483
+ },
484
+ get(agent, assembly) {
485
+ const merged = new Map();
486
+ for (const target of variableTargets(agent, assembly))
487
+ for (const row of variables)
488
+ if (row.scope === target.scope && row.scopeId === target.scopeId)
489
+ merged.set(row.name, row);
490
+ const targets = skillTargets(agent, assembly);
491
+ return {
492
+ skills: skills.filter((row) => targets.some((target) => target.scope === row.scope && target.scopeId === row.scopeId)),
493
+ variables: {
494
+ names: [...merged.values()]
495
+ .filter((row) => row.kind === 'value')
496
+ .map((row) => row.name),
497
+ teamOnly: [...merged.values()].filter((row) => row.kind === 'value' && row.teamRunnersOnly),
498
+ passthrough: [...merged.values()]
499
+ .filter((row) => row.kind === 'runner')
500
+ .map((row) => row.name),
501
+ },
502
+ };
503
+ },
504
+ };
505
+ }
506
+ /** Eligibility needs existence/compatibility, not occupancy, other models or a full wait projection. */
507
+ async function preferredModelAvailable(conn, run, agent, inputs, metadata, inventory) {
508
+ const now = clock.now().getTime();
509
+ const registered = await inventory();
510
+ const reserved = upgradeReservation(run.executions ?? [], registered, now);
511
+ const entry = agent.modelEntries[0];
512
+ const single = { ...agent, modelEntries: [entry] };
513
+ const binding = deps.subjects.get(run.subject.kind);
514
+ if (!binding)
515
+ throw new Error(`No context provider for ${run.subject.kind}.`);
516
+ const checks = [];
517
+ for (const runner of registered) {
518
+ if ((reserved && runner.id !== reserved) ||
519
+ runner.status !== 'online' ||
520
+ !runner.lastSeenAt ||
521
+ Date.parse(runner.lastSeenAt) + TIMINGS.offlineAfterMs <= now ||
522
+ !isProtocolSupported(runner.protocolVersion) ||
523
+ !runsEntry(runner, entry, now) ||
524
+ (runner.trust !== 'team' && runner.ownerUserId !== run.actorUserId))
525
+ continue;
526
+ const assembly = await binding.context
527
+ .assemble(conn, {
528
+ run,
529
+ agent,
530
+ runner,
531
+ inputs,
532
+ cli: deps.cli.name,
533
+ appName: deps.app.name,
534
+ })
535
+ .catch(() => null);
536
+ // The diagnostic path treats an unreadable context as unknown and waits for the preferred model.
537
+ if (!assembly)
538
+ return true;
539
+ checks.push({ agent: single, runner, assembly });
540
+ }
541
+ await metadata.load(checks);
542
+ for (const { runner, assembly } of checks) {
543
+ const { skills, variables } = metadata.get(single, assembly);
544
+ if (skills.some((skill) => skill.hash === null))
545
+ throw notFound('Skill version');
546
+ if (compatibleAssembly(run, single, runner, assembly, skills.length, variables) !== 'ready')
547
+ continue;
548
+ if (variables.passthrough.length && runner.variables == null)
549
+ return true;
550
+ if (variables.passthrough.some((name) => !runner.variables?.includes(name)))
551
+ continue;
552
+ return true;
553
+ }
554
+ return false;
555
+ }
556
+ /** The same ordered-model decision is used for dispatch and for immediate runner affinity. */
557
+ async function modelAdmission(conn, run, agent, entry, inputs, metadata = admissionResources(conn), inventory = () => deps.runners.all(conn)) {
558
+ if (agent.modelEntries.indexOf(entry) <= 0 || !deps.runners)
559
+ return undefined;
560
+ const policy = await deps.selectionPolicies?.forRun(conn, run);
561
+ if (!policy)
562
+ return undefined;
563
+ const elapsed = Math.max(0, (clock.now().getTime() - queueStart(run)) / 1000);
564
+ const preferredAvailable = await preferredModelAvailable(conn, run, agent, inputs, metadata, inventory);
565
+ if (preferredAvailable && elapsed < policy.fallbackAfterSeconds)
566
+ return false;
567
+ return {
568
+ reason: preferredAvailable ? 'preferredQueued' : 'preferredUnavailable',
569
+ preferredTool: agent.modelEntries[0].tool,
570
+ preferredModel: agent.modelEntries[0].model,
571
+ queueEnteredAt: new Date(queueStart(run)).toISOString(),
572
+ waitedSeconds: elapsed,
573
+ };
574
+ }
468
575
  async function assemble({ conn }, run, agent, runner, inputs, leaseExpiresAt, prepared, entry, resources = claimResources(conn, run.id)) {
469
576
  const binding = deps.subjects.get(run.subjectKind);
470
577
  if (!binding)
@@ -494,9 +601,6 @@ export function createClaimService(deps) {
494
601
  throw new NotForThisRunner();
495
602
  const dirs = payloadDirs(assembly.dirs);
496
603
  const directoryKey = directoryKeyOf(runner.id, dirs);
497
- if (directoryKey !== null &&
498
- (await directoryBusy(conn, runner.id, run.id, directoryKey)))
499
- throw new NotForThisRunner();
500
604
  const { skills, variables } = await resources(agent, assembly);
501
605
  const names = variables.names;
502
606
  const requires = cleanList([
@@ -982,27 +1086,10 @@ export function createClaimService(deps) {
982
1086
  // Admission must precede preflight assembly: a missing or obsolete
983
1087
  // subject is cancelled normally, rather than counted as setup failure.
984
1088
  const resources = claimResources(conn, run.id);
985
- const selection = await selectionFacts(conn, toRun(current), agent, true, undefined, resources);
986
- const elapsed = Math.max(0, (now.getTime() - queueStart(toRun(current))) / 1000);
987
- const preferredAvailable = (selection?.entries[0]?.compatibleCount ?? 0) > 0 ||
988
- selection?.entries[0]?.reason === 'unknown' ||
989
- selection?.entries[0]?.blockers?.includes('unknown') === true;
990
- const chosenIndex = agent.modelEntries.indexOf(entry);
991
- if (selection && chosenIndex > 0) {
992
- // Throw to roll back the provisional dispatch; the original run keeps
993
- // waiting for its first choice without consuming an attempt or token.
994
- if (preferredAvailable && elapsed < selection.fallbackAfterSeconds)
995
- throw new NotForThisRunner();
996
- selectionSnapshot = {
997
- reason: preferredAvailable
998
- ? 'preferredQueued'
999
- : 'preferredUnavailable',
1000
- preferredTool: agent.modelEntries[0].tool,
1001
- preferredModel: agent.modelEntries[0].model,
1002
- queueEnteredAt: selection.queueEnteredAt,
1003
- waitedSeconds: elapsed,
1004
- };
1005
- }
1089
+ const admission = await modelAdmission(conn, toRun(current), agent, entry, pending.map(toInput));
1090
+ if (admission === false)
1091
+ throw new NotForThisRunner();
1092
+ selectionSnapshot = admission;
1006
1093
  assembled = await assemble(unit, run, agent, runner, pending.map(toInput), leaseExpiresAt, prepared, entry, resources);
1007
1094
  }
1008
1095
  catch (error) {
@@ -1118,9 +1205,9 @@ export function createClaimService(deps) {
1118
1205
  });
1119
1206
  }
1120
1207
  /** Queued runs that may be claimed now, in claim order. */
1121
- async function candidates() {
1208
+ async function candidates(runner) {
1122
1209
  const now = clock.now();
1123
- return runsRepo(tx.read()).findMany({
1210
+ const ordinary = await runsRepo(tx.read()).findMany({
1124
1211
  filter: (f) => f.and([
1125
1212
  f.string('status').eq('queued'),
1126
1213
  f.or([
@@ -1135,15 +1222,198 @@ export function createClaimService(deps) {
1135
1222
  ],
1136
1223
  limit: CANDIDATES,
1137
1224
  });
1225
+ if (ordinary.length === 0)
1226
+ return [];
1227
+ // A separate scan prevents ordinary candidate pages from hiding upgrade resumptions.
1228
+ const recovery = [];
1229
+ for (let offset = 0;; offset += CANDIDATES) {
1230
+ const page = await runsRepo(tx.read()).findMany({
1231
+ filter: (f) => f.and([
1232
+ f.string('status').eq('queued'),
1233
+ f.string('failureReason').eq('runnerUpgrade'),
1234
+ f
1235
+ .date('queueEnteredAt')
1236
+ .after(new Date(now.getTime() - 5 * 60_000)),
1237
+ f.or([
1238
+ f.date('availableAt').empty(),
1239
+ f.date('availableAt').notAfter(now),
1240
+ ]),
1241
+ ]),
1242
+ sort: (sort) => [
1243
+ sort.field('queueEnteredAt').asc(),
1244
+ sort.field('id').asc(),
1245
+ ],
1246
+ limit: CANDIDATES,
1247
+ offset,
1248
+ });
1249
+ recovery.push(...page.filter((run) => toExecutions(run.executionHistory).at(-1)?.runnerId === runner.id));
1250
+ if (page.length < CANDIDATES || recovery.length >= CANDIDATES)
1251
+ break;
1252
+ }
1253
+ return [
1254
+ ...new Map([...recovery, ...ordinary].map((run) => [run.id, run])).values(),
1255
+ ];
1256
+ }
1257
+ /** One batch for locations, one for machines, and occupancy batches only when a preference exists. */
1258
+ async function affinityBatch(queued, runner) {
1259
+ const conn = tx.read();
1260
+ if (queued.length === 0)
1261
+ return {
1262
+ machines: [runner],
1263
+ preferred: new Map(),
1264
+ held: [],
1265
+ jobs: [],
1266
+ };
1267
+ const locations = await sessionsRepo(conn).groupBy({
1268
+ by: ['runnerId', 'subjectKind', 'subjectId'],
1269
+ aggregate: (a) => ({ updatedAt: a.max('updatedAt') }),
1270
+ filter: (f) => f.or(queued.map((run) => f.and([
1271
+ f.string('subjectKind').eq(run.subjectKind),
1272
+ f.string('subjectId').eq(run.subjectId),
1273
+ ]))),
1274
+ sort: (sort) => sort.field('updatedAt').desc(),
1275
+ });
1276
+ const activeLocations = await runsRepo(conn).groupBy({
1277
+ by: ['runnerId', 'subjectKind', 'subjectId'],
1278
+ aggregate: (a) => ({ updatedAt: a.max('updatedAt') }),
1279
+ filter: (f) => f.and([
1280
+ f.string('runnerId').notEmpty(),
1281
+ f.or(ACTIVE.map((status) => f.string('status').eq(status))),
1282
+ f.or(queued.map((run) => f.and([
1283
+ f.string('subjectKind').eq(run.subjectKind),
1284
+ f.string('subjectId').eq(run.subjectId),
1285
+ ]))),
1286
+ ]),
1287
+ });
1288
+ const machineIds = [
1289
+ ...new Set([
1290
+ runner.id,
1291
+ ...queued.map((run) => toExecutions(run.executionHistory).at(-1)?.runnerId),
1292
+ ...locations.map((item) => item.runnerId),
1293
+ ...activeLocations.map((item) => item.runnerId),
1294
+ ].filter((id) => Boolean(id))),
1295
+ ];
1296
+ const machines = deps.runners
1297
+ ? await deps.runners.all(conn, machineIds)
1298
+ : [runner];
1299
+ const preferred = new Map();
1300
+ for (const run of queued) {
1301
+ const last = toExecutions(run.executionHistory).at(-1)?.runnerId;
1302
+ const current = activeLocations
1303
+ .filter((item) => item.subjectKind === run.subjectKind &&
1304
+ item.subjectId === run.subjectId)
1305
+ .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0]?.runnerId;
1306
+ const location = locations.find((item) => item.subjectKind === run.subjectKind &&
1307
+ item.subjectId === run.subjectId)?.runnerId;
1308
+ const machine = machines.find((item) => item.id === (last ?? current ?? location));
1309
+ if (machine)
1310
+ preferred.set(run.id, machine);
1311
+ }
1312
+ const ids = [...new Set([...preferred.values()].map((item) => item.id))];
1313
+ const held = ids.length === 0
1314
+ ? []
1315
+ : await runsRepo(conn).groupBy({
1316
+ by: ['runnerId', 'tool'],
1317
+ aggregate: (a) => ({ count: a.count() }),
1318
+ filter: (f) => f.and([
1319
+ f.or(ids.map((id) => f.string('runnerId').eq(id))),
1320
+ f.or(ACTIVE.map((status) => f.string('status').eq(status))),
1321
+ ]),
1322
+ });
1323
+ const jobs = ids.length === 0
1324
+ ? []
1325
+ : await jobsRepo(conn).groupBy({
1326
+ by: ['runnerId'],
1327
+ aggregate: (a) => ({ count: a.count() }),
1328
+ filter: (f) => f.and([
1329
+ f.or(ids.map((id) => f.string('runnerId').eq(id))),
1330
+ f.or(ACTIVE_JOB.map((status) => f.string('status').eq(status))),
1331
+ ]),
1332
+ });
1333
+ return { machines, preferred, held, jobs };
1334
+ }
1335
+ function hasImmediateRoom(run, agent, preferred, batch) {
1336
+ const now = clock.now().getTime();
1337
+ if (preferred.status !== 'online' ||
1338
+ !preferred.lastSeenAt ||
1339
+ Date.parse(preferred.lastSeenAt) + TIMINGS.offlineAfterMs <= now ||
1340
+ preferred.draining ||
1341
+ (preferred.globalFree ?? 1) <= 0 ||
1342
+ !fits(preferred, agent, run, now))
1343
+ return false;
1344
+ const held = batch.held.filter((item) => item.runnerId === preferred.id);
1345
+ if (held.reduce((count, item) => count + Number(item.count), 0) +
1346
+ batch.jobs
1347
+ .filter((item) => item.runnerId === preferred.id)
1348
+ .reduce((count, item) => count + Number(item.count), 0) >=
1349
+ preferred.slots)
1350
+ return false;
1351
+ if (!pickOpenEntry(preferred, agent, (tool) => held
1352
+ .filter((item) => item.tool === tool)
1353
+ .reduce((count, item) => count + Number(item.count), 0) <
1354
+ toolLimit(preferred, tool) &&
1355
+ (preferred.toolLoad?.[tool]?.free ?? 1) > 0, now))
1356
+ return false;
1357
+ return true;
1358
+ }
1359
+ /** Evaluate only the candidate reached in queue order, reusing this poll's batched capacity and metadata. */
1360
+ async function readyPreference(run, agent, batch, claimant, metadata, inventory) {
1361
+ const conn = tx.read();
1362
+ const runner = batch.preferred.get(run.id);
1363
+ if (!runner ||
1364
+ runner.id === claimant.id ||
1365
+ !fits(claimant, agent, run, clock.now().getTime()) ||
1366
+ upgradeReservation(toExecutions(run.executionHistory), batch.machines, clock.now().getTime()) ||
1367
+ !hasImmediateRoom(run, agent, runner, batch))
1368
+ return false;
1369
+ const binding = deps.subjects.get(run.subjectKind);
1370
+ if (!binding || !(await commentConsentFor(conn, runner, run)).allowed)
1371
+ return false;
1372
+ const assembly = await binding.context
1373
+ .assemble(conn, {
1374
+ run: toRun(run),
1375
+ agent,
1376
+ runner,
1377
+ inputs: [],
1378
+ cli: deps.cli.name,
1379
+ appName: deps.app.name,
1380
+ })
1381
+ .catch(() => null);
1382
+ if (!assembly)
1383
+ return false;
1384
+ await metadata.load([{ agent, assembly }]);
1385
+ const { skills, variables } = metadata.get(agent, assembly);
1386
+ if (!variables.passthrough.every((name) => runner.variables?.includes(name)) ||
1387
+ skills.some((row) => row.hash === null) ||
1388
+ compatibleAssembly(toRun(run), agent, runner, assembly, skills.length, variables) !== 'ready')
1389
+ return false;
1390
+ const entry = pickOpenEntry(runner, agent, (tool) => batch.held
1391
+ .filter((item) => item.runnerId === runner.id && item.tool === tool)
1392
+ .reduce((count, item) => count + Number(item.count), 0) <
1393
+ toolLimit(runner, tool) && (runner.toolLoad?.[tool]?.free ?? 1) > 0, clock.now().getTime());
1394
+ if (!entry)
1395
+ return false;
1396
+ if (agent.modelEntries.indexOf(entry) > 0) {
1397
+ const inputs = (await pendingInputs(conn, run.id)).map(toInput);
1398
+ if ((await modelAdmission(conn, toRun(run), agent, entry, inputs, metadata, inventory)) === false)
1399
+ return false;
1400
+ }
1401
+ return true;
1138
1402
  }
1139
1403
  async function claimOne(runner, seen, reported) {
1140
1404
  if (runner.draining)
1141
1405
  return null;
1142
1406
  const conn = tx.read();
1143
- const queued = await candidates();
1407
+ const queued = await candidates(runner);
1408
+ const affinity = await affinityBatch(queued, runner);
1144
1409
  const agents = await findAgents(conn, queued
1145
1410
  .filter((candidate) => !seen.has(candidate.id))
1146
1411
  .map((candidate) => candidate.agentId));
1412
+ const metadata = admissionResources(conn, [...agents.values()]);
1413
+ let inventoryRead;
1414
+ const inventory = () => (inventoryRead ??= deps.runners
1415
+ ? deps.runners.all(conn)
1416
+ : Promise.resolve([runner]));
1147
1417
  const full = new Set();
1148
1418
  for (const candidate of queued) {
1149
1419
  if (seen.has(candidate.id) || full.has(candidate.agentId))
@@ -1154,6 +1424,15 @@ export function createClaimService(deps) {
1154
1424
  (!deps.subjects.get(candidate.subjectKind)?.actsAs &&
1155
1425
  !fits(runner, agent, candidate, clock.now().getTime())))
1156
1426
  continue;
1427
+ const reserved = upgradeReservation(toExecutions(candidate.executionHistory), affinity.machines, clock.now().getTime());
1428
+ if (reserved && reserved !== runner.id)
1429
+ continue;
1430
+ const preferred = affinity.preferred.get(candidate.id);
1431
+ if (!reserved &&
1432
+ preferred &&
1433
+ preferred.id !== runner.id &&
1434
+ (await readyPreference(candidate, agent, affinity, runner, metadata, inventory)))
1435
+ continue;
1157
1436
  // None of its tools has room on the runner's side: the runs behind it may still fit.
1158
1437
  if (!pickOpenEntry(runner, agent, (tool) => reported.has(tool), clock.now().getTime()))
1159
1438
  continue;