@nocobase/app-plugin-agents 2.0.0-beta.6 → 2.0.0-beta.7

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 (218) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -2
  3. package/dist/client/chat/controller.d.ts.map +1 -1
  4. package/dist/client/chat/controller.js +3 -2
  5. package/dist/client/chat/controller.js.map +1 -1
  6. package/dist/client/chat/notices.d.ts +12 -0
  7. package/dist/client/chat/notices.d.ts.map +1 -0
  8. package/dist/client/chat/notices.js +15 -0
  9. package/dist/client/chat/notices.js.map +1 -0
  10. package/dist/client/chat/submissions.d.ts +16 -0
  11. package/dist/client/chat/submissions.d.ts.map +1 -1
  12. package/dist/client/chat/submissions.js +22 -0
  13. package/dist/client/chat/submissions.js.map +1 -1
  14. package/dist/client/chat.d.ts +1 -0
  15. package/dist/client/chat.d.ts.map +1 -1
  16. package/dist/client/chat.js +1 -0
  17. package/dist/client/chat.js.map +1 -1
  18. package/dist/client/components/ui/alert-dialog.d.ts +3 -1
  19. package/dist/client/components/ui/alert-dialog.d.ts.map +1 -1
  20. package/dist/client/components/ui/alert-dialog.js +2 -2
  21. package/dist/client/components/ui/alert-dialog.js.map +1 -1
  22. package/dist/client/locales/de-DE.d.ts.map +1 -1
  23. package/dist/client/locales/de-DE.js +134 -109
  24. package/dist/client/locales/de-DE.js.map +1 -1
  25. package/dist/client/locales/en-US.d.ts +1 -0
  26. package/dist/client/locales/en-US.d.ts.map +1 -1
  27. package/dist/client/locales/en-US.js +3 -2
  28. package/dist/client/locales/en-US.js.map +1 -1
  29. package/dist/client/locales/es-ES.d.ts.map +1 -1
  30. package/dist/client/locales/es-ES.js +211 -186
  31. package/dist/client/locales/es-ES.js.map +1 -1
  32. package/dist/client/locales/fr-FR.d.ts.map +1 -1
  33. package/dist/client/locales/fr-FR.js +143 -118
  34. package/dist/client/locales/fr-FR.js.map +1 -1
  35. package/dist/client/locales/id-ID.d.ts.map +1 -1
  36. package/dist/client/locales/id-ID.js +132 -107
  37. package/dist/client/locales/id-ID.js.map +1 -1
  38. package/dist/client/locales/index.d.ts.map +1 -1
  39. package/dist/client/locales/index.js +2 -0
  40. package/dist/client/locales/index.js.map +1 -1
  41. package/dist/client/locales/ja-JP.d.ts.map +1 -1
  42. package/dist/client/locales/ja-JP.js +215 -190
  43. package/dist/client/locales/ja-JP.js.map +1 -1
  44. package/dist/client/locales/ko-KR.d.ts +5 -0
  45. package/dist/client/locales/ko-KR.d.ts.map +1 -0
  46. package/dist/client/locales/ko-KR.js +1311 -0
  47. package/dist/client/locales/ko-KR.js.map +1 -0
  48. package/dist/client/locales/pt-BR.d.ts.map +1 -1
  49. package/dist/client/locales/pt-BR.js +205 -180
  50. package/dist/client/locales/pt-BR.js.map +1 -1
  51. package/dist/client/locales/ru-RU.d.ts +5 -0
  52. package/dist/client/locales/ru-RU.d.ts.map +1 -0
  53. package/dist/client/locales/ru-RU.js +1341 -0
  54. package/dist/client/locales/ru-RU.js.map +1 -0
  55. package/dist/client/locales/runtimes.en-US.d.ts +1 -0
  56. package/dist/client/locales/runtimes.en-US.d.ts.map +1 -1
  57. package/dist/client/locales/runtimes.en-US.js +3 -2
  58. package/dist/client/locales/runtimes.en-US.js.map +1 -1
  59. package/dist/client/locales/runtimes.zh-CN.d.ts.map +1 -1
  60. package/dist/client/locales/runtimes.zh-CN.js +3 -2
  61. package/dist/client/locales/runtimes.zh-CN.js.map +1 -1
  62. package/dist/client/locales/vi-VN.d.ts.map +1 -1
  63. package/dist/client/locales/vi-VN.js +171 -146
  64. package/dist/client/locales/vi-VN.js.map +1 -1
  65. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  66. package/dist/client/locales/zh-CN.js +3 -2
  67. package/dist/client/locales/zh-CN.js.map +1 -1
  68. package/dist/client/locales/zh-TW.d.ts.map +1 -1
  69. package/dist/client/locales/zh-TW.js +148 -123
  70. package/dist/client/locales/zh-TW.js.map +1 -1
  71. package/dist/client/pages/runtimes/quota.d.ts +3 -2
  72. package/dist/client/pages/runtimes/quota.d.ts.map +1 -1
  73. package/dist/client/pages/runtimes/quota.js +8 -3
  74. package/dist/client/pages/runtimes/quota.js.map +1 -1
  75. package/dist/client/pages/runtimes/runner-tools.d.ts.map +1 -1
  76. package/dist/client/pages/runtimes/runner-tools.js +7 -16
  77. package/dist/client/pages/runtimes/runner-tools.js.map +1 -1
  78. package/dist/client/pages/usage/date-range-picker.d.ts.map +1 -1
  79. package/dist/client/pages/usage/date-range-picker.js +3 -1
  80. package/dist/client/pages/usage/date-range-picker.js.map +1 -1
  81. package/dist/database/migrations/.manifest.json +8 -0
  82. package/dist/database/migrations/202611070001_ag_run_runner_pin.d.ts +4 -0
  83. package/dist/database/migrations/202611070001_ag_run_runner_pin.d.ts.map +1 -0
  84. package/dist/database/migrations/202611070001_ag_run_runner_pin.js +17 -0
  85. package/dist/database/migrations/202611070001_ag_run_runner_pin.js.map +1 -0
  86. package/dist/database/migrations/202611080002_ag_run_failure_params.d.ts +4 -0
  87. package/dist/database/migrations/202611080002_ag_run_failure_params.d.ts.map +1 -0
  88. package/dist/database/migrations/202611080002_ag_run_failure_params.js +17 -0
  89. package/dist/database/migrations/202611080002_ag_run_failure_params.js.map +1 -0
  90. package/dist/package.json +1 -1
  91. package/dist/server/composition.d.ts +2 -0
  92. package/dist/server/composition.d.ts.map +1 -1
  93. package/dist/server/composition.js +1 -1
  94. package/dist/server/composition.js.map +1 -1
  95. package/dist/server/core/agents/agent.service.d.ts +2 -1
  96. package/dist/server/core/agents/agent.service.d.ts.map +1 -1
  97. package/dist/server/core/agents/agent.service.js.map +1 -1
  98. package/dist/server/core/agents/agent.store.d.ts +2 -2
  99. package/dist/server/core/agents/agent.store.d.ts.map +1 -1
  100. package/dist/server/core/agents/agent.store.js +8 -2
  101. package/dist/server/core/agents/agent.store.js.map +1 -1
  102. package/dist/server/core/brief/brief.d.ts +10 -5
  103. package/dist/server/core/brief/brief.d.ts.map +1 -1
  104. package/dist/server/core/brief/brief.js +36 -13
  105. package/dist/server/core/brief/brief.js.map +1 -1
  106. package/dist/server/core/brief/index.d.ts +1 -1
  107. package/dist/server/core/brief/index.d.ts.map +1 -1
  108. package/dist/server/core/brief/index.js +1 -1
  109. package/dist/server/core/brief/index.js.map +1 -1
  110. package/dist/server/core/runs/claim.d.ts +13 -3
  111. package/dist/server/core/runs/claim.d.ts.map +1 -1
  112. package/dist/server/core/runs/claim.js +203 -36
  113. package/dist/server/core/runs/claim.js.map +1 -1
  114. package/dist/server/core/runs/extensions.d.ts +4 -1
  115. package/dist/server/core/runs/extensions.d.ts.map +1 -1
  116. package/dist/server/core/runs/extensions.js +3 -1
  117. package/dist/server/core/runs/extensions.js.map +1 -1
  118. package/dist/server/core/runs/git-credentials.d.ts.map +1 -1
  119. package/dist/server/core/runs/git-credentials.js +24 -2
  120. package/dist/server/core/runs/git-credentials.js.map +1 -1
  121. package/dist/server/core/runs/identity.d.ts.map +1 -1
  122. package/dist/server/core/runs/identity.js +9 -0
  123. package/dist/server/core/runs/identity.js.map +1 -1
  124. package/dist/server/core/runs/lifecycle.d.ts.map +1 -1
  125. package/dist/server/core/runs/lifecycle.js +14 -0
  126. package/dist/server/core/runs/lifecycle.js.map +1 -1
  127. package/dist/server/core/runs/ports.d.ts +7 -0
  128. package/dist/server/core/runs/ports.d.ts.map +1 -1
  129. package/dist/server/core/runs/ports.js.map +1 -1
  130. package/dist/server/core/runs/preview.d.ts +1 -4
  131. package/dist/server/core/runs/preview.d.ts.map +1 -1
  132. package/dist/server/core/runs/preview.js +10 -5
  133. package/dist/server/core/runs/preview.js.map +1 -1
  134. package/dist/server/core/runs/quota-wait.d.ts +7 -0
  135. package/dist/server/core/runs/quota-wait.d.ts.map +1 -0
  136. package/dist/server/core/runs/quota-wait.js +56 -0
  137. package/dist/server/core/runs/quota-wait.js.map +1 -0
  138. package/dist/server/core/runs/run-data-purge.d.ts.map +1 -1
  139. package/dist/server/core/runs/run-data-purge.js +1 -0
  140. package/dist/server/core/runs/run-data-purge.js.map +1 -1
  141. package/dist/server/core/runs/run.service.d.ts +13 -0
  142. package/dist/server/core/runs/run.service.d.ts.map +1 -1
  143. package/dist/server/core/runs/run.service.js +84 -3
  144. package/dist/server/core/runs/run.service.js.map +1 -1
  145. package/dist/server/core/runs/run.store.d.ts +2 -0
  146. package/dist/server/core/runs/run.store.d.ts.map +1 -1
  147. package/dist/server/core/runs/run.store.js +3 -0
  148. package/dist/server/core/runs/run.store.js.map +1 -1
  149. package/dist/server/core/runs/sweeper.d.ts.map +1 -1
  150. package/dist/server/core/runs/sweeper.js +2 -0
  151. package/dist/server/core/runs/sweeper.js.map +1 -1
  152. package/dist/server/core/runs/transitions.d.ts +1 -0
  153. package/dist/server/core/runs/transitions.d.ts.map +1 -1
  154. package/dist/server/core/runs/transitions.js +15 -1
  155. package/dist/server/core/runs/transitions.js.map +1 -1
  156. package/dist/server/core/runs/workload.d.ts +1 -1
  157. package/dist/server/core/runs/workload.d.ts.map +1 -1
  158. package/dist/server/core/runs/workload.js +19 -4
  159. package/dist/server/core/runs/workload.js.map +1 -1
  160. package/dist/server/kernel/clock.d.ts +2 -0
  161. package/dist/server/kernel/clock.d.ts.map +1 -1
  162. package/dist/server/kernel/clock.js +14 -0
  163. package/dist/server/kernel/clock.js.map +1 -1
  164. package/dist/server/kernel/events.d.ts +4 -1
  165. package/dist/server/kernel/events.d.ts.map +1 -1
  166. package/dist/server/kernel/events.js.map +1 -1
  167. package/dist/server/locales/de-DE.js +3 -3
  168. package/dist/server/locales/de-DE.js.map +1 -1
  169. package/dist/server/locales/es-ES.js +6 -6
  170. package/dist/server/locales/es-ES.js.map +1 -1
  171. package/dist/server/locales/fr-FR.js +4 -4
  172. package/dist/server/locales/fr-FR.js.map +1 -1
  173. package/dist/server/locales/id-ID.js +5 -5
  174. package/dist/server/locales/id-ID.js.map +1 -1
  175. package/dist/server/locales/index.d.ts.map +1 -1
  176. package/dist/server/locales/index.js +2 -0
  177. package/dist/server/locales/index.js.map +1 -1
  178. package/dist/server/locales/ja-JP.d.ts.map +1 -1
  179. package/dist/server/locales/ja-JP.js +6 -6
  180. package/dist/server/locales/ja-JP.js.map +1 -1
  181. package/dist/server/locales/ko-KR.d.ts +5 -0
  182. package/dist/server/locales/ko-KR.d.ts.map +1 -0
  183. package/dist/server/locales/ko-KR.js +59 -0
  184. package/dist/server/locales/ko-KR.js.map +1 -0
  185. package/dist/server/locales/pt-BR.js +6 -6
  186. package/dist/server/locales/pt-BR.js.map +1 -1
  187. package/dist/server/locales/ru-RU.d.ts +5 -0
  188. package/dist/server/locales/ru-RU.d.ts.map +1 -0
  189. package/dist/server/locales/ru-RU.js +59 -0
  190. package/dist/server/locales/ru-RU.js.map +1 -0
  191. package/dist/server/locales/vi-VN.js +6 -6
  192. package/dist/server/locales/vi-VN.js.map +1 -1
  193. package/dist/server/locales/zh-TW.d.ts.map +1 -1
  194. package/dist/server/locales/zh-TW.js +6 -6
  195. package/dist/server/locales/zh-TW.js.map +1 -1
  196. package/dist/server/routes/admin.d.ts.map +1 -1
  197. package/dist/server/routes/admin.js +27 -1
  198. package/dist/server/routes/admin.js.map +1 -1
  199. package/dist/server/routes/runners/admin.d.ts.map +1 -1
  200. package/dist/server/routes/runners/admin.js +3 -3
  201. package/dist/server/routes/runners/admin.js.map +1 -1
  202. package/dist/server/routes/runners/runner.d.ts.map +1 -1
  203. package/dist/server/routes/runners/runner.js +13 -22
  204. package/dist/server/routes/runners/runner.js.map +1 -1
  205. package/dist/server/routes/schemas.d.ts +4 -0
  206. package/dist/server/routes/schemas.d.ts.map +1 -1
  207. package/dist/server/routes/schemas.js +17 -4
  208. package/dist/server/routes/schemas.js.map +1 -1
  209. package/dist/server/runners/runner.service.d.ts.map +1 -1
  210. package/dist/server/runners/runner.service.js +17 -5
  211. package/dist/server/runners/runner.service.js.map +1 -1
  212. package/dist/shared/briefs.d.ts +2 -6
  213. package/dist/shared/briefs.d.ts.map +1 -1
  214. package/dist/shared/runs.d.ts +4 -0
  215. package/dist/shared/runs.d.ts.map +1 -1
  216. package/dist/shared/runs.js.map +1 -1
  217. package/package.json +11 -11
  218. package/skills/nocobase-app-plugin-agents/references/runners.md +1 -1
@@ -19,6 +19,12 @@ import { ACTIVE_JOB, jobsRepo } from '../../jobs/job.store.js';
19
19
  * agent's entries whose tool the runner runs and has room for, so an agent listing two tools runs with the second while
20
20
  * the first is full; a run none of whose tools has room is passed over for the runs behind it.
21
21
  *
22
+ * Runners share a queue in proportion to each tool's limit, not by who asks first: before it locks anything, a claim
23
+ * waits until the run has been queued for the share of the tool's limit the runner already runs, times one second
24
+ * (`claimPace`). The runners' route hands out one run per request and the runner asks again at once, so of several
25
+ * runners woken for the same runs, the least busy with each tool takes the next one. A request that ends while it
26
+ * waits takes nothing.
27
+ *
22
28
  * Variables are checked and opened in the claim's own transaction, on its connection: which ones the assembled payload
23
29
  * gets, whether any is for shared runners only, and their values, so what the runner receives is what was checked. The
24
30
  * names taken from the runner go in `workspace.passthrough`, without values.
@@ -48,7 +54,7 @@ import { ANNOUNCED_FAILURES, isProtocolSupported, policyAllowsAgent, policyAllow
48
54
  import { chatEffortCapabilities, quotaForAgent, runnerLowOnDisk, runsEntry, toolLimit, supportsWorkspaceKey, } from '../../../shared/runners.js';
49
55
  import { queueStart } from './selection.js';
50
56
  import { createHash } from 'node:crypto';
51
- import { later } from '../../kernel/clock.js';
57
+ import { later, pause } from '../../kernel/clock.js';
52
58
  import { notFound } from '../../kernel/errors.js';
53
59
  import { runSecretsKey } from '../../kernel/redaction.js';
54
60
  import { cleanList, covers, jsonObject } from '../../kernel/values.js';
@@ -57,7 +63,7 @@ import { joinBrief, renderBrief } from '../brief/index.js';
57
63
  import { cliPackageFor } from './cli-package.js';
58
64
  import { executionHistory } from './execution.js';
59
65
  import { describeVariables, variableRefs, } from '../variables/index.js';
60
- import { mountsFor, prepareExtensions, repoAccessFor, responsibleOf, sessionKeyOf, withSections, } from './extensions.js';
66
+ import { mountsFor, prepareExtensions, repoAccessFor, RepoAccessError, responsibleOf, sessionKeyOf, withSections, } from './extensions.js';
61
67
  import { MAX_CLAIM_FAILURES, toolPolicyFor } from './policy.js';
62
68
  import { mintRunToken } from './run-tokens.js';
63
69
  import { ACTIVE, countActive, eventsRepo, findRunRecord, markDelivered, pendingInputs, runsOfKey, runsRepo, inputsRepo, sessionsRepo, toInput, toExecutions, toRun, } from './run.store.js';
@@ -71,6 +77,8 @@ import { consumeReset, saveBrief } from './workspace.store.js';
71
77
  const CANDIDATES = 50;
72
78
  /** The initial page plus at most two further pages per claim request, shared by all free slots. */
73
79
  const CANDIDATE_PAGES = 3;
80
+ /** The longest a claim waits its turn: what a runner waits for a run when it already runs its tool at the limit. */
81
+ const PACE_MS = 1000;
74
82
  export function createClaimScan() {
75
83
  return { remainingPages: CANDIDATE_PAGES };
76
84
  }
@@ -109,6 +117,22 @@ function toolRoom(tools) {
109
117
  },
110
118
  };
111
119
  }
120
+ /**
121
+ * How long `runner` waits before it takes `run` with `tool`, at `now`: the share of the tool's limit it already runs
122
+ * (`toolLimit`), of the busier of what this application counts (`byTool`) and what the runner last reported
123
+ * (`Runner.toolLoad`), times one second, counted from when the run entered the queue (`queueStart`). Several runners
124
+ * woken for one run take it in order of how busy each is with its tool, whichever asked first and however many runs
125
+ * each took before; an idle runner, and work queued longer than its share, wait nothing.
126
+ */
127
+ export function claimPace(runner, tool, byTool, run, now) {
128
+ const limit = toolLimit(runner, tool);
129
+ if (limit <= 0)
130
+ return 0;
131
+ const load = runner.toolLoad?.[tool];
132
+ const used = Math.max(byTool[tool] ?? 0, load ? load.slots - load.free : 0);
133
+ const share = (Math.min(used, limit) / limit) * PACE_MS;
134
+ return Math.min(share, Math.max(0, queueStart(run) + share - now));
135
+ }
112
136
  /**
113
137
  * The entry `runner` takes a run of `agent` with: the first whose tool it has enabled, installed and signed in, with
114
138
  * the entry's model among those it reports (`runsEntry`). Null when it has none of them.
@@ -146,9 +170,24 @@ export function fitsActor(runner, agent, actorUserId, requires, now) {
146
170
  return false;
147
171
  return runner.trust === 'team' || runner.ownerUserId === actorUserId;
148
172
  }
173
+ /** A strict continuation keeps the tool/model that owns its original session. */
174
+ function quotaAgent(run, agent) {
175
+ if (!run.runnerPinId || run.freshSession)
176
+ return agent;
177
+ const last = toExecutions(run.executionHistory).at(-1);
178
+ const tool = last?.tool ?? run.tool;
179
+ const model = last ? last.model : run.model;
180
+ return tool
181
+ ? {
182
+ ...agent,
183
+ modelEntries: agent.modelEntries.filter((entry) => entry.tool === tool && entry.model === model),
184
+ }
185
+ : agent;
186
+ }
149
187
  /** Whether `runner` may run `run` of `agent`, before any lock is taken and before its payload is known. */
150
188
  export function fits(runner, agent, run, now) {
151
- return fitsActor(runner, agent, run.actorUserId, toRun(run).requires, now);
189
+ return ((!run.runnerPinId || run.runnerPinId === runner.id) &&
190
+ fitsActor(runner, quotaAgent(run, agent), run.actorUserId, toRun(run).requires, now));
152
191
  }
153
192
  /** The runner features a payload needs. */
154
193
  export function payloadRequires(dirs, skills, variables, workspaceKey = false) {
@@ -185,7 +224,7 @@ export function skillTargets(_agent, assembly) {
185
224
  ...assembly.dirs.flatMap((dir) => dir.scopeId ? [{ scope: 'workdir', scopeId: dir.scopeId }] : []),
186
225
  ];
187
226
  }
188
- function fingerprint(agent, entry, layers, skills) {
227
+ function fingerprint(agent, entry, layers, skills, readOnly) {
189
228
  return createHash('sha256')
190
229
  .update(JSON.stringify([
191
230
  entry.tool,
@@ -194,12 +233,16 @@ function fingerprint(agent, entry, layers, skills) {
194
233
  layers.system,
195
234
  layers.agent,
196
235
  skills.map((skill) => [skill.slug, skill.hash]),
236
+ // Keep existing writable fingerprints unchanged across upgrades.
237
+ ...(readOnly ? [{ readOnly: true }] : []),
197
238
  ]))
198
239
  .digest('hex');
199
240
  }
200
241
  /** The brief for `assembly`, with the skills a run would get. */
201
242
  export function briefOf(agent, assembly, skills, cli, appName) {
202
- const rendered = renderBrief({
243
+ // Custom system prompts (such as conversations) keep their own layout.
244
+ const stablePrefix = assembly.briefLayout === 'stable-prefix' && assembly.system === undefined;
245
+ const input = {
203
246
  agentName: agent.name,
204
247
  subject: { noun: assembly.subject.noun, key: assembly.subject.key },
205
248
  task: assembly.task,
@@ -210,14 +253,20 @@ export function briefOf(agent, assembly, skills, cli, appName) {
210
253
  cli,
211
254
  appName,
212
255
  ...(assembly.guidance ? { guidance: assembly.guidance } : {}),
213
- });
256
+ };
257
+ const rendered = renderBrief(input);
214
258
  const layers = assembly.system === undefined
215
259
  ? rendered
216
260
  : { ...rendered, system: assembly.system };
217
261
  const turn = assembly.turn.previousSummary
218
262
  ? `${assembly.turn.prompt}\n\nWhat earlier runs on this already did:\n\n${assembly.turn.previousSummary}`
219
263
  : assembly.turn.prompt;
220
- return { layers, turn, prompt: joinBrief(layers) };
264
+ // Keep the logical layers (and their session fingerprint) unchanged. Only the outgoing text is reordered,
265
+ // so an ordinary follow-up can still resume the session created by an independent review.
266
+ const prompt = stablePrefix
267
+ ? joinBrief(renderBrief({ ...input, stablePrefix: true }), true)
268
+ : joinBrief(layers);
269
+ return { layers, turn, prompt };
221
270
  }
222
271
  /** Whether `runner` asks for repository credentials on demand rather than taking them with the claim. */
223
272
  export function asksCredentialsOnDemand(runner) {
@@ -684,6 +733,7 @@ export function createClaimService(deps) {
684
733
  const token = await mintRunToken(conn, deps, run, runner.id);
685
734
  const git = await repoAccessFor(conn, deps.repoAccess, {
686
735
  claim,
736
+ readOnly: assembly.readOnly === true,
687
737
  repos: dirs.filter((dir) => dir.kind === 'repo'),
688
738
  onDemand: asksCredentialsOnDemand(runner),
689
739
  tools: wrapsHostTools(runner),
@@ -700,17 +750,20 @@ export function createClaimService(deps) {
700
750
  });
701
751
  const render = async () => briefOf(agent, await withSections(conn, deps.sections, effectiveClaim, assembly, prepared), skills, deps.cli.name, deps.app.name);
702
752
  let brief = await render();
703
- const briefFingerprint = fingerprint(agent, entry, brief.layers, skills);
704
- const resume = !run.freshSession &&
705
- binding.sessionPolicy?.(effectiveClaim) !== 'fresh' &&
706
- !clean &&
707
- session &&
708
- !session.poisoned &&
709
- session.fingerprint === briefFingerprint;
753
+ const briefFingerprint = fingerprint(agent, entry, brief.layers, skills, assembly.readOnly === true);
754
+ const strict = Boolean(run.runnerPinId && !run.freshSession);
755
+ const resume = strict
756
+ ? Boolean(run.sessionId)
757
+ : !run.freshSession &&
758
+ binding.sessionPolicy?.(effectiveClaim) !== 'fresh' &&
759
+ !clean &&
760
+ session &&
761
+ !session.poisoned &&
762
+ session.fingerprint === briefFingerprint;
710
763
  // A kept session that cannot be resumed: the new one is told what came before, as a first one is. The history
711
764
  // is outside the fingerprint, so it does not change.
712
765
  let recoveryContext;
713
- if (session) {
766
+ if (session && !strict) {
714
767
  const fresh = await binding.context.assemble(conn, { ...claim, freshSession: true }, prepared.get('subject'));
715
768
  if (resume) {
716
769
  // Should the kept session be gone on the runner, the new one hears what came before from this.
@@ -744,6 +797,11 @@ export function createClaimService(deps) {
744
797
  'The previous attempt was interrupted by a runner upgrade. Continue from the current state.',
745
798
  ]
746
799
  : []),
800
+ ...(strict
801
+ ? [
802
+ 'Quota recovery: continue the interrupted step from the existing session, progress and files.',
803
+ ]
804
+ : []),
747
805
  ...resumeNotes,
748
806
  ...(sessionNote ? [sessionNote] : []),
749
807
  ].join('\n\n');
@@ -756,6 +814,7 @@ export function createClaimService(deps) {
756
814
  createdAt: nowText,
757
815
  });
758
816
  const payload = {
817
+ ...(strict ? { session: { strict: true } } : {}),
759
818
  ...(run.subjectKind === 'conversation' &&
760
819
  runner.features.includes('outputBlocks')
761
820
  ? { outputBlocks: true }
@@ -786,8 +845,14 @@ export function createClaimService(deps) {
786
845
  prompt: {
787
846
  system: brief.prompt,
788
847
  turn,
789
- session: resume ? 'resume' : 'fresh',
790
- ...(resume ? { resumeSessionId: session.sessionId } : {}),
848
+ session: strict || resume ? 'resume' : 'fresh',
849
+ ...(strict
850
+ ? run.sessionId
851
+ ? { resumeSessionId: run.sessionId }
852
+ : {}
853
+ : resume && session
854
+ ? { resumeSessionId: session.sessionId }
855
+ : {}),
791
856
  ...(resume && recoveryContext ? { recoveryContext } : {}),
792
857
  },
793
858
  inputs,
@@ -1051,7 +1116,9 @@ export function createClaimService(deps) {
1051
1116
  const pinned = await pin.runnerOf(conn, toRun(run));
1052
1117
  return pinned === null || pinned === runner.id;
1053
1118
  }
1054
- async function attempt(runner, agent, candidate, reported) {
1119
+ async function attempt(runner, agent, candidate, reported, pass) {
1120
+ if (candidate.runnerPinId && candidate.runnerPinId !== runner.id)
1121
+ return { kind: 'skipped' };
1055
1122
  const conn = tx.read();
1056
1123
  // Rules that do not depend on who the run acts as come first, and settle the rest of the pass: an agent at its
1057
1124
  // limit is checked once per poll, not once per queued run.
@@ -1079,6 +1146,17 @@ export function createClaimService(deps) {
1079
1146
  const consent = await commentConsentFor(conn, runner, candidate);
1080
1147
  if (!consent.allowed)
1081
1148
  return { kind: 'skipped' };
1149
+ // Wait this runner's turn for the run, after every read-only rule and before any lock or write. The transaction
1150
+ // checks again whether the run is still queued and which tool has room.
1151
+ const paced = pickOpenEntry(runner, agent, (tool) => (pass.byTool[tool] ?? 0) < toolLimit(runner, tool) &&
1152
+ reported.has(tool), clock.now().getTime());
1153
+ if (!paced)
1154
+ return { kind: 'skipped' };
1155
+ const wait = claimPace(runner, paced.tool, pass.byTool, candidate, clock.now().getTime());
1156
+ if (wait > 0)
1157
+ await (deps.pause ?? pause)(wait, pass.signal);
1158
+ if (pass.signal?.aborted)
1159
+ return { kind: 'aborted' };
1082
1160
  // Read once, before the transaction, and handed to every repository access hook of this claim. A failed read
1083
1161
  // leaves the run queued for the next poll rather than letting a provider pick an identity without it.
1084
1162
  let responsibleUserId = null;
@@ -1154,6 +1232,8 @@ export function createClaimService(deps) {
1154
1232
  status: 'dispatched',
1155
1233
  failureReason: null,
1156
1234
  failureDetail: null,
1235
+ failureParams: null,
1236
+ claimFailures: 0,
1157
1237
  runnerId: runner.id,
1158
1238
  // Brief providers observe this reservation inside the claim transaction.
1159
1239
  // Replace a previous attempt's tool before computing the roster.
@@ -1241,7 +1321,16 @@ export function createClaimService(deps) {
1241
1321
  : null,
1242
1322
  },
1243
1323
  });
1244
- unit.emit({ type: 'run.changed', runId: run.id, status: 'dispatched' });
1324
+ if (current.runnerPinId && !current.freshSession)
1325
+ await deps.subjects
1326
+ .get(run.subjectKind)
1327
+ ?.sink?.onQuotaResumed?.(unit, toRun(run));
1328
+ unit.emit({
1329
+ type: 'run.changed',
1330
+ ...(current.runnerPinId ? { quotaWait: false } : {}),
1331
+ runId: run.id,
1332
+ status: 'dispatched',
1333
+ });
1245
1334
  if (variableRefs(candidate.teamOnlyVariables).length > 0)
1246
1335
  unit.emit({
1247
1336
  type: 'notice.cleared',
@@ -1284,28 +1373,98 @@ export function createClaimService(deps) {
1284
1373
  }
1285
1374
  }
1286
1375
  }
1287
- /** Counts a failed assembly; the run fails once it has failed too often. */
1288
- async function recordFailure(runId, error) {
1289
- deps.onClaimFailure?.(runId, error.cause);
1376
+ /** Repository failures have their own budget; ordinary assembly failures keep the existing three-try limit. */
1377
+ async function recordFailure(candidate, error) {
1378
+ deps.onClaimFailure?.(candidate.id, error.cause);
1290
1379
  await tx.run(async (unit) => {
1291
- const run = await findRunRecord(unit.conn, runId);
1292
- if (!run || run.status !== 'queued')
1380
+ const run = await findRunRecord(unit.conn, candidate.id);
1381
+ if (!run ||
1382
+ run.status !== 'queued' ||
1383
+ run.revision !== candidate.revision ||
1384
+ Number(run.attempt) !== Number(candidate.attempt) ||
1385
+ run.actorUserId !== candidate.actorUserId ||
1386
+ !(await queuedIdentityCurrent(unit.conn, deps, run)))
1387
+ return;
1388
+ const previous = jsonObject(run.failureParams);
1389
+ const cause = error.cause;
1390
+ if (cause instanceof RepoAccessError) {
1391
+ const now = clock.now().getTime();
1392
+ const repoFailures = Number(previous.repoFailures ?? 0) + 1;
1393
+ const first = typeof previous.repoFirstFailureAt === 'string'
1394
+ ? Date.parse(previous.repoFirstFailureAt)
1395
+ : now;
1396
+ const retryAt = typeof cause.params.retryAt === 'string'
1397
+ ? Date.parse(cause.params.retryAt)
1398
+ : NaN;
1399
+ const delays = [1, 2, 4, 8, 15];
1400
+ const next = Math.max(now + (delays[repoFailures - 1] ?? 15) * 60_000, Number.isFinite(retryAt) ? retryAt : 0);
1401
+ const params = {
1402
+ ...cause.params,
1403
+ repoFailures,
1404
+ repoFirstFailureAt: new Date(first).toISOString(),
1405
+ };
1406
+ const reason = cause.kind === 'denied'
1407
+ ? 'repoAccessDenied'
1408
+ : 'repoAccessUnavailable';
1409
+ if (cause.kind === 'denied' ||
1410
+ repoFailures >= 6 ||
1411
+ next > first + 60 * 60_000) {
1412
+ await finishRun(unit, deps, run, {
1413
+ status: 'failed',
1414
+ reason,
1415
+ detail: cause.message,
1416
+ failureParams: params,
1417
+ }, true);
1418
+ return;
1419
+ }
1420
+ const availableAt = new Date(next).toISOString();
1421
+ const changed = await runsRepo(unit.conn).updateMany({
1422
+ filter: {
1423
+ id: run.id,
1424
+ status: 'queued',
1425
+ revision: run.revision,
1426
+ attempt: Number(run.attempt),
1427
+ actorUserId: run.actorUserId,
1428
+ },
1429
+ values: {
1430
+ availableAt,
1431
+ failureReason: reason,
1432
+ failureDetail: cause.message.slice(0, 10_000),
1433
+ failureParams: params,
1434
+ updatedAt: clock.now().toISOString(),
1435
+ },
1436
+ });
1437
+ if (changed.updatedCount > 0) {
1438
+ unit.emit({ type: 'run.changed', runId: run.id, status: 'queued' });
1439
+ unit.emit({ type: 'run.requeued', runId: run.id, availableAt });
1440
+ }
1293
1441
  return;
1442
+ }
1294
1443
  const failures = Number(run.claimFailures) + 1;
1444
+ // Preserve only the independent repository retry budget across an unrelated preparation failure.
1445
+ const budget = previous.repoFailures
1446
+ ? {
1447
+ repoFailures: previous.repoFailures,
1448
+ repoFirstFailureAt: previous.repoFirstFailureAt,
1449
+ repoAccess: previous.repoAccess ?? previous,
1450
+ }
1451
+ : null;
1295
1452
  if (failures >= MAX_CLAIM_FAILURES) {
1296
1453
  await finishRun(unit, deps, run, {
1297
1454
  status: 'failed',
1298
1455
  reason: 'setupFailed',
1299
1456
  detail: error.message,
1300
- });
1457
+ failureParams: null,
1458
+ }, true);
1301
1459
  return;
1302
1460
  }
1303
1461
  await runsRepo(unit.conn).updateMany({
1304
- filter: { id: runId },
1462
+ filter: { id: run.id, status: 'queued', revision: run.revision },
1305
1463
  values: {
1306
1464
  claimFailures: failures,
1307
1465
  failureReason: 'setupFailed',
1308
1466
  failureDetail: error.message.slice(0, 10_000),
1467
+ failureParams: budget,
1309
1468
  },
1310
1469
  });
1311
1470
  });
@@ -1529,8 +1688,8 @@ export function createClaimService(deps) {
1529
1688
  }
1530
1689
  return true;
1531
1690
  }
1532
- /** False stops the whole request when a transactional capacity check finds the runner full. */
1533
- async function claimPage(runner, seen, reported, queued, full) {
1691
+ /** False stops the whole request when a transactional capacity check finds the runner full, or the request ended. */
1692
+ async function claimPage(runner, seen, reported, queued, full, pass) {
1534
1693
  const conn = tx.read();
1535
1694
  const affinity = await affinityBatch(queued, runner);
1536
1695
  const agents = await findAgents(conn, queued
@@ -1542,10 +1701,13 @@ export function createClaimService(deps) {
1542
1701
  ? deps.runners.all(conn)
1543
1702
  : Promise.resolve([runner]));
1544
1703
  for (const candidate of queued) {
1545
- if (seen.has(candidate.id) || full.has(candidate.agentId))
1704
+ if (seen.has(candidate.id) ||
1705
+ full.has(candidate.agentId) ||
1706
+ (candidate.runnerPinId && candidate.runnerPinId !== runner.id))
1546
1707
  continue;
1547
1708
  seen.add(candidate.id);
1548
- const agent = agents.get(candidate.agentId);
1709
+ const configured = agents.get(candidate.agentId);
1710
+ const agent = configured ? quotaAgent(candidate, configured) : undefined;
1549
1711
  if (!agent ||
1550
1712
  (!deps.subjects.get(candidate.subjectKind)?.actsAs &&
1551
1713
  !fits(runner, agent, candidate, clock.now().getTime())))
@@ -1555,7 +1717,8 @@ export function createClaimService(deps) {
1555
1717
  continue;
1556
1718
  // Work bound to another runner waits for it; work bound here takes no other preference.
1557
1719
  const pin = deps.subjects.get(candidate.subjectKind)?.runnerPin;
1558
- const pinned = pin ? await pin.runnerOf(conn, toRun(candidate)) : null;
1720
+ const pinned = candidate.runnerPinId ??
1721
+ (pin ? await pin.runnerOf(conn, toRun(candidate)) : null);
1559
1722
  if (pinned !== null && pinned !== runner.id)
1560
1723
  continue;
1561
1724
  const preferred = affinity.preferred.get(candidate.id);
@@ -1569,9 +1732,11 @@ export function createClaimService(deps) {
1569
1732
  if (!pickOpenEntry(runner, agent, (tool) => reported.has(tool), clock.now().getTime()))
1570
1733
  continue;
1571
1734
  try {
1572
- const result = await attempt(runner, agent, candidate, reported);
1735
+ const result = await attempt(runner, agent, candidate, reported, pass);
1573
1736
  if (result.kind === 'claimed')
1574
1737
  return result.payload;
1738
+ if (result.kind === 'aborted')
1739
+ return false;
1575
1740
  if (result.kind === 'full') {
1576
1741
  if (result.scope === 'runner')
1577
1742
  return false;
@@ -1586,7 +1751,7 @@ export function createClaimService(deps) {
1586
1751
  continue;
1587
1752
  if (!(error instanceof AssemblyError))
1588
1753
  throw error;
1589
- await recordFailure(candidate.id, error);
1754
+ await recordFailure(candidate, error);
1590
1755
  }
1591
1756
  }
1592
1757
  return null;
@@ -1633,7 +1798,7 @@ export function createClaimService(deps) {
1633
1798
  const agent = await findAgent(conn, run.agentId);
1634
1799
  return agent ? selectionFacts(conn, toRun(run), agent, false) : null;
1635
1800
  },
1636
- async claim(runner, free, tools, scan = createClaimScan()) {
1801
+ async claim(runner, free, tools, scan = createClaimScan(), signal) {
1637
1802
  if (scan.remainingPages <= 0 ||
1638
1803
  runner.status !== 'online' ||
1639
1804
  runner.draining ||
@@ -1646,8 +1811,10 @@ export function createClaimService(deps) {
1646
1811
  const pages = candidates(runner, scan);
1647
1812
  let queued = [];
1648
1813
  while (payloads.length < limit) {
1649
- if ((await deps.slotsUsed(tx.read(), runner.id)) >= runner.slots)
1814
+ const held = await deps.slotsHeld(tx.read(), runner.id);
1815
+ if (held.runs + held.jobs >= runner.slots)
1650
1816
  break;
1817
+ const pass = { byTool: held.byTool ?? {}, signal };
1651
1818
  const full = new Set();
1652
1819
  let payload;
1653
1820
  do {
@@ -1657,7 +1824,7 @@ export function createClaimService(deps) {
1657
1824
  return payloads;
1658
1825
  queued = page.value;
1659
1826
  }
1660
- payload = await claimPage(runner, seen, reported, queued, full);
1827
+ payload = await claimPage(runner, seen, reported, queued, full, pass);
1661
1828
  if (payload === false)
1662
1829
  return payloads;
1663
1830
  if (!payload)