@nocobase/app-plugin-agents 1.0.0-beta.16 → 1.0.0-beta.18
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.
- package/CHANGELOG.md +68 -0
- package/README.md +32 -21
- package/dist/client/chat/api.d.ts +1 -1
- package/dist/client/chat/api.d.ts.map +1 -1
- package/dist/client/chat/api.js +2 -2
- package/dist/client/chat/api.js.map +1 -1
- package/dist/client/chat/keys.d.ts +1 -0
- package/dist/client/chat/keys.d.ts.map +1 -1
- package/dist/client/chat/keys.js +1 -0
- package/dist/client/chat/keys.js.map +1 -1
- package/dist/client/chat/provider.d.ts +10 -4
- package/dist/client/chat/provider.d.ts.map +1 -1
- package/dist/client/chat/provider.js +86 -15
- package/dist/client/chat/provider.js.map +1 -1
- package/dist/client/chat/use-chat.d.ts +3 -1
- package/dist/client/chat/use-chat.d.ts.map +1 -1
- package/dist/client/chat/use-chat.js +2 -2
- package/dist/client/chat/use-chat.js.map +1 -1
- package/dist/client/chat/use-conversation-model.d.ts +12 -0
- package/dist/client/chat/use-conversation-model.d.ts.map +1 -0
- package/dist/client/chat/use-conversation-model.js +64 -0
- package/dist/client/chat/use-conversation-model.js.map +1 -0
- package/dist/client/chat.d.ts +1 -0
- package/dist/client/chat.d.ts.map +1 -1
- package/dist/client/chat.js +1 -0
- package/dist/client/chat.js.map +1 -1
- package/dist/client/model-suggestion.d.ts.map +1 -1
- package/dist/client/model-suggestion.js +4 -2
- package/dist/client/model-suggestion.js.map +1 -1
- package/dist/client/pages/usage/index.d.ts.map +1 -1
- package/dist/client/pages/usage/index.js +17 -5
- package/dist/client/pages/usage/index.js.map +1 -1
- package/dist/client/pages/usage/prices/model.d.ts +4 -4
- package/dist/client/pages/usage/prices/model.d.ts.map +1 -1
- package/dist/client/pages/usage/prices/model.js +2 -2
- package/dist/client/pages/usage/prices/model.js.map +1 -1
- package/dist/client/pages/usage/prices/price-cells.js +2 -2
- package/dist/client/pages/usage/prices/price-cells.js.map +1 -1
- package/dist/client/pages/usage/prices/prices-section.d.ts.map +1 -1
- package/dist/client/pages/usage/prices/prices-section.js +13 -13
- package/dist/client/pages/usage/prices/prices-section.js.map +1 -1
- package/dist/package.json +5 -2
- package/dist/server/cli/online-credential.d.ts +16 -0
- package/dist/server/cli/online-credential.d.ts.map +1 -0
- package/dist/server/cli/online-credential.js +51 -0
- package/dist/server/cli/online-credential.js.map +1 -0
- package/dist/server/cli/run-credential.d.ts +2 -0
- package/dist/server/cli/run-credential.d.ts.map +1 -1
- package/dist/server/cli/run-credential.js +12 -1
- package/dist/server/cli/run-credential.js.map +1 -1
- package/dist/server/cli/surface.js +2 -2
- package/dist/server/cli/surface.js.map +1 -1
- package/dist/server/composition.d.ts +2 -0
- package/dist/server/composition.d.ts.map +1 -1
- package/dist/server/composition.js +25 -4
- package/dist/server/composition.js.map +1 -1
- package/dist/server/core/agents/agent.service.d.ts +5 -0
- package/dist/server/core/agents/agent.service.d.ts.map +1 -1
- package/dist/server/core/agents/agent.service.js +6 -1
- package/dist/server/core/agents/agent.service.js.map +1 -1
- package/dist/server/core/agents/agent.views.d.ts +2 -0
- package/dist/server/core/agents/agent.views.d.ts.map +1 -1
- package/dist/server/core/agents/agent.views.js +4 -3
- package/dist/server/core/agents/agent.views.js.map +1 -1
- package/dist/server/core/brief/brief.js +1 -1
- package/dist/server/core/brief/brief.js.map +1 -1
- package/dist/server/core/conversations/chat-settings.d.ts +1 -0
- package/dist/server/core/conversations/chat-settings.d.ts.map +1 -1
- package/dist/server/core/conversations/chat-settings.js +71 -57
- package/dist/server/core/conversations/chat-settings.js.map +1 -1
- package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
- package/dist/server/core/conversations/conversation.service.js +3 -4
- package/dist/server/core/conversations/conversation.service.js.map +1 -1
- package/dist/server/core/reports/report.service.d.ts +1 -0
- package/dist/server/core/reports/report.service.d.ts.map +1 -1
- package/dist/server/core/reports/report.service.js +5 -2
- package/dist/server/core/reports/report.service.js.map +1 -1
- package/dist/server/core/runs/claim.d.ts +3 -1
- package/dist/server/core/runs/claim.d.ts.map +1 -1
- package/dist/server/core/runs/claim.js +370 -60
- package/dist/server/core/runs/claim.js.map +1 -1
- package/dist/server/core/runs/extensions.d.ts +22 -2
- package/dist/server/core/runs/extensions.d.ts.map +1 -1
- package/dist/server/core/runs/extensions.js +21 -2
- package/dist/server/core/runs/extensions.js.map +1 -1
- package/dist/server/core/runs/git-credentials.d.ts +5 -1
- package/dist/server/core/runs/git-credentials.d.ts.map +1 -1
- package/dist/server/core/runs/git-credentials.js +15 -2
- package/dist/server/core/runs/git-credentials.js.map +1 -1
- package/dist/server/core/runs/lifecycle.d.ts.map +1 -1
- package/dist/server/core/runs/lifecycle.js +9 -5
- package/dist/server/core/runs/lifecycle.js.map +1 -1
- package/dist/server/core/runs/runner-affinity.d.ts +6 -0
- package/dist/server/core/runs/runner-affinity.d.ts.map +1 -0
- package/dist/server/core/runs/runner-affinity.js +16 -0
- package/dist/server/core/runs/runner-affinity.js.map +1 -0
- package/dist/server/core/skills/skill.service.d.ts +2 -0
- package/dist/server/core/skills/skill.service.d.ts.map +1 -1
- package/dist/server/core/skills/skill.service.js +1 -0
- package/dist/server/core/skills/skill.service.js.map +1 -1
- package/dist/server/jobs/job.service.d.ts.map +1 -1
- package/dist/server/jobs/job.service.js +7 -4
- package/dist/server/jobs/job.service.js.map +1 -1
- package/dist/server/kernel/values.d.ts +5 -0
- package/dist/server/kernel/values.d.ts.map +1 -1
- package/dist/server/kernel/values.js +19 -0
- package/dist/server/kernel/values.js.map +1 -1
- package/dist/server/online/agent/configuration.d.ts +2 -2
- package/dist/server/online/agent/configuration.d.ts.map +1 -1
- package/dist/server/online/agent/configuration.js +21 -7
- package/dist/server/online/agent/configuration.js.map +1 -1
- package/dist/server/online/agent/index.d.ts +2 -1
- package/dist/server/online/agent/index.d.ts.map +1 -1
- package/dist/server/online/agent/index.js.map +1 -1
- package/dist/server/online/agent/native/backend.d.ts +2 -0
- package/dist/server/online/agent/native/backend.d.ts.map +1 -1
- package/dist/server/online/agent/native/backend.js +14 -12
- package/dist/server/online/agent/native/backend.js.map +1 -1
- package/dist/server/online/agent/native/execution.d.ts +2 -0
- package/dist/server/online/agent/native/execution.d.ts.map +1 -1
- package/dist/server/online/agent/native/execution.js +51 -22
- package/dist/server/online/agent/native/execution.js.map +1 -1
- package/dist/server/online/agent/native/types.d.ts +6 -0
- package/dist/server/online/agent/native/types.d.ts.map +1 -1
- package/dist/server/online/agent/runner/backend.d.ts.map +1 -1
- package/dist/server/online/agent/runner/backend.js +4 -3
- package/dist/server/online/agent/runner/backend.js.map +1 -1
- package/dist/server/online/agent/service.d.ts +2 -0
- package/dist/server/online/agent/service.d.ts.map +1 -1
- package/dist/server/online/agent/service.js +3 -1
- package/dist/server/online/agent/service.js.map +1 -1
- package/dist/server/online/agent/types.d.ts +15 -1
- package/dist/server/online/agent/types.d.ts.map +1 -1
- package/dist/server/online/sandbox/cli-command.d.ts +6 -0
- package/dist/server/online/sandbox/cli-command.d.ts.map +1 -0
- package/dist/server/online/sandbox/cli-command.js +370 -0
- package/dist/server/online/sandbox/cli-command.js.map +1 -0
- package/dist/server/online/sandbox/execution.d.ts +25 -0
- package/dist/server/online/sandbox/execution.d.ts.map +1 -0
- package/dist/server/online/sandbox/execution.js +63 -0
- package/dist/server/online/sandbox/execution.js.map +1 -0
- package/dist/server/online/sandbox/filesystem.d.ts +8 -0
- package/dist/server/online/sandbox/filesystem.d.ts.map +1 -0
- package/dist/server/online/sandbox/filesystem.js +72 -0
- package/dist/server/online/sandbox/filesystem.js.map +1 -0
- package/dist/server/online/sandbox/limits.d.ts +4 -0
- package/dist/server/online/sandbox/limits.d.ts.map +1 -0
- package/dist/server/online/sandbox/limits.js +46 -0
- package/dist/server/online/sandbox/limits.js.map +1 -0
- package/dist/server/online/sandbox/provider.d.ts +9 -0
- package/dist/server/online/sandbox/provider.d.ts.map +1 -0
- package/dist/server/online/sandbox/provider.js +237 -0
- package/dist/server/online/sandbox/provider.js.map +1 -0
- package/dist/server/online/sandbox/skills.d.ts +14 -0
- package/dist/server/online/sandbox/skills.d.ts.map +1 -0
- package/dist/server/online/sandbox/skills.js +56 -0
- package/dist/server/online/sandbox/skills.js.map +1 -0
- package/dist/server/online/sandbox/tools.d.ts +3 -0
- package/dist/server/online/sandbox/tools.d.ts.map +1 -0
- package/dist/server/online/sandbox/tools.js +35 -0
- package/dist/server/online/sandbox/tools.js.map +1 -0
- package/dist/server/online/sandbox/types.d.ts +72 -0
- package/dist/server/online/sandbox/types.d.ts.map +1 -0
- package/dist/server/online/sandbox/types.js +2 -0
- package/dist/server/online/sandbox/types.js.map +1 -0
- package/dist/server/providers/agents.d.ts +5 -2
- package/dist/server/providers/agents.d.ts.map +1 -1
- package/dist/server/providers/agents.js +63 -2
- package/dist/server/providers/agents.js.map +1 -1
- package/dist/server/routes/admin.d.ts.map +1 -1
- package/dist/server/routes/admin.js +1 -0
- package/dist/server/routes/admin.js.map +1 -1
- package/dist/server/routes/chat.d.ts +1 -1
- package/dist/server/routes/chat.d.ts.map +1 -1
- package/dist/server/routes/chat.js +15 -6
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +17 -2
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/openapi.js +1 -1
- package/dist/server/routes/openapi.js.map +1 -1
- package/dist/server/routes/schemas.js +1 -1
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/server/runners/runner.service.d.ts +1 -1
- package/dist/server/runners/runner.service.d.ts.map +1 -1
- package/dist/server/runners/runner.service.js +5 -1
- package/dist/server/runners/runner.service.js.map +1 -1
- package/dist/server/runners/runner.store.d.ts.map +1 -1
- package/dist/server/runners/runner.store.js +3 -1
- package/dist/server/runners/runner.store.js.map +1 -1
- package/dist/server/vendor/THIRD_PARTY_LICENSES.txt +782 -0
- package/dist/server/vendor/just-bash.d.ts +137 -0
- package/dist/server/vendor/just-bash.js +2102 -0
- package/dist/shared/conversations.d.ts +2 -1
- package/dist/shared/conversations.d.ts.map +1 -1
- package/dist/shared/conversations.js.map +1 -1
- package/dist/shared/runners.d.ts +1 -1
- package/dist/shared/runners.d.ts.map +1 -1
- package/dist/shared/runners.js +2 -0
- package/dist/shared/runners.js.map +1 -1
- package/docs/native-sandbox.md +55 -0
- package/docs/runner-agents.md +2 -0
- package/docs/ui-message-clients.md +10 -0
- package/package.json +11 -8
- package/skills/nocobase-app-plugin-agents/SKILL.md +1 -0
- package/skills/nocobase-app-plugin-agents/references/extension-points.md +21 -19
- 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
|
|
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.
|
|
@@ -55,7 +57,7 @@ import { joinBrief, renderBrief } from '../brief/index.js';
|
|
|
55
57
|
import { cliPackageFor } from './cli-package.js';
|
|
56
58
|
import { executionHistory } from './execution.js';
|
|
57
59
|
import { describeVariables, variableRefs, } from '../variables/index.js';
|
|
58
|
-
import { mountsFor, prepareExtensions, repoAccessFor, sessionKeyOf, withSections, } from './extensions.js';
|
|
60
|
+
import { mountsFor, prepareExtensions, repoAccessFor, responsibleOf, sessionKeyOf, withSections, } from './extensions.js';
|
|
59
61
|
import { MAX_CLAIM_FAILURES, toolPolicyFor } from './policy.js';
|
|
60
62
|
import { mintRunToken } from './run-tokens.js';
|
|
61
63
|
import { ACTIVE, countActive, eventsRepo, findRunRecord, markDelivered, pendingInputs, runsOfKey, runsRepo, inputsRepo, sessionsRepo, toInput, toExecutions, toRun, } from './run.store.js';
|
|
@@ -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
|
|
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.');
|
|
@@ -216,6 +218,16 @@ export function briefOf(agent, assembly, skills, cli, appName) {
|
|
|
216
218
|
export function asksCredentialsOnDemand(runner) {
|
|
217
219
|
return runner.features.includes('gitCredentials');
|
|
218
220
|
}
|
|
221
|
+
/** Where a claim keeps the subject's responsible it read before its transaction (`Prepared`). */
|
|
222
|
+
const RESPONSIBLE_KEY = 'claim:responsibleUserId';
|
|
223
|
+
function responsibleIn(prepared) {
|
|
224
|
+
const value = prepared.get(RESPONSIBLE_KEY);
|
|
225
|
+
return typeof value === 'string' ? value : null;
|
|
226
|
+
}
|
|
227
|
+
/** Whether `runner` wraps code host CLIs with the run's credential (`RunGit.tools`). */
|
|
228
|
+
export function wrapsHostTools(runner) {
|
|
229
|
+
return runner.features.includes('gitTools');
|
|
230
|
+
}
|
|
219
231
|
const DIRECTORY_KEY_SEPARATOR = '\n';
|
|
220
232
|
/** The directories a run holds on its runner, as stored in `agRuns.directoryKey`. */
|
|
221
233
|
function directoryKeyOf(runnerId, dirs) {
|
|
@@ -224,20 +236,6 @@ function directoryKeyOf(runnerId, dirs) {
|
|
|
224
236
|
.map((dir) => `${runnerId}:${dir.path}`);
|
|
225
237
|
return paths.length > 0 ? paths.join(DIRECTORY_KEY_SEPARATOR) : null;
|
|
226
238
|
}
|
|
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
239
|
/** Resolve omitted subject input in the claim transaction, keeping it in the audit history. */
|
|
242
240
|
async function settleClaimInputs(conn, pending, effective, now) {
|
|
243
241
|
const ids = new Set(effective.map((input) => input.id));
|
|
@@ -270,10 +268,16 @@ export function createClaimService(deps) {
|
|
|
270
268
|
(assembly.workspaceKey && !supportsWorkspaceKey(runner)))
|
|
271
269
|
return 'incompatible';
|
|
272
270
|
const { skills, variables } = await resources(agent, assembly);
|
|
271
|
+
return compatibleAssembly(run, agent, runner, assembly, skills.length, variables);
|
|
272
|
+
}
|
|
273
|
+
function compatibleAssembly(run, agent, runner, assembly, skillCount, variables) {
|
|
274
|
+
if (!isProtocolSupported(runner.protocolVersion) ||
|
|
275
|
+
(assembly.workspaceKey && !supportsWorkspaceKey(runner)))
|
|
276
|
+
return 'incompatible';
|
|
273
277
|
const names = variables.names;
|
|
274
278
|
if (!covers(runner.features, [
|
|
275
279
|
...run.requires,
|
|
276
|
-
...payloadRequires(payloadDirs(assembly.dirs),
|
|
280
|
+
...payloadRequires(payloadDirs(assembly.dirs), skillCount, names.length, Boolean(assembly.workspaceKey)),
|
|
277
281
|
]))
|
|
278
282
|
return 'incompatible';
|
|
279
283
|
if (!fitsActor(runner, agent, run.actorUserId, run.requires, clock.now().getTime()) ||
|
|
@@ -307,7 +311,8 @@ export function createClaimService(deps) {
|
|
|
307
311
|
const entries = [];
|
|
308
312
|
for (const entry of agent.modelEntries) {
|
|
309
313
|
const single = { ...agent, modelEntries: [entry] };
|
|
310
|
-
const
|
|
314
|
+
const reserved = upgradeReservation(run.executions ?? [], registered, now.getTime());
|
|
315
|
+
const candidates = registered.filter((runner) => runner.status !== 'revoked' && (!reserved || runner.id === reserved));
|
|
311
316
|
const connected = candidates.filter((runner) => (runner.status === 'online' ||
|
|
312
317
|
runner.status === 'upgrade_required') &&
|
|
313
318
|
runner.lastSeenAt !== null &&
|
|
@@ -321,7 +326,6 @@ export function createClaimService(deps) {
|
|
|
321
326
|
let unknown = false;
|
|
322
327
|
let incompatible = false;
|
|
323
328
|
let ready = false;
|
|
324
|
-
let directoryBlocked = false;
|
|
325
329
|
for (const runner of online) {
|
|
326
330
|
if (!isProtocolSupported(runner.protocolVersion) ||
|
|
327
331
|
runner.status === 'upgrade_required') {
|
|
@@ -377,16 +381,11 @@ export function createClaimService(deps) {
|
|
|
377
381
|
const held = await deps.slotsHeld(conn, runner.id);
|
|
378
382
|
const byTool = held.byTool ?? {};
|
|
379
383
|
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
384
|
const hasRoom = !runner.draining &&
|
|
385
385
|
(runner.globalFree ?? 1) > 0 &&
|
|
386
386
|
used < runner.slots &&
|
|
387
387
|
(byTool[entry.tool] ?? 0) < toolLimit(runner, entry.tool) &&
|
|
388
|
-
(runner.toolLoad?.[entry.tool]?.free ?? 1) > 0
|
|
389
|
-
!blockedDirectory;
|
|
388
|
+
(runner.toolLoad?.[entry.tool]?.free ?? 1) > 0;
|
|
390
389
|
ready ||= hasRoom && runner.globalFree !== undefined;
|
|
391
390
|
if (hasRoom && runner.globalFree === undefined)
|
|
392
391
|
unknown = true;
|
|
@@ -398,10 +397,10 @@ export function createClaimService(deps) {
|
|
|
398
397
|
? 'unknown'
|
|
399
398
|
: 'ready'
|
|
400
399
|
: runner.draining
|
|
401
|
-
? runner.draining
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
400
|
+
? runner.draining === 'diskLow'
|
|
401
|
+
? 'busy'
|
|
402
|
+
: runner.draining
|
|
403
|
+
: 'busy',
|
|
405
404
|
lastSeenAt: runner.lastSeenAt,
|
|
406
405
|
used,
|
|
407
406
|
limit: runner.slots,
|
|
@@ -426,8 +425,6 @@ export function createClaimService(deps) {
|
|
|
426
425
|
: connected.length
|
|
427
426
|
? 'toolNotInstalled'
|
|
428
427
|
: 'offline';
|
|
429
|
-
if (directoryBlocked && !ready)
|
|
430
|
-
reason = 'restricted';
|
|
431
428
|
if (!ready &&
|
|
432
429
|
compatible.length > 0 &&
|
|
433
430
|
loads.every((runner) => runner.reason === 'updatePending' || runner.reason === 'pruning'))
|
|
@@ -465,6 +462,126 @@ export function createClaimService(deps) {
|
|
|
465
462
|
entries,
|
|
466
463
|
};
|
|
467
464
|
}
|
|
465
|
+
/** Metadata only: batch new scopes, reuse common scopes across machines and visited candidates. */
|
|
466
|
+
function admissionResources(conn, agents = []) {
|
|
467
|
+
const variableScopes = new Set();
|
|
468
|
+
const skillScopes = new Set();
|
|
469
|
+
const variables = [];
|
|
470
|
+
const skills = [];
|
|
471
|
+
function missing(targets, seen) {
|
|
472
|
+
const fresh = new Map();
|
|
473
|
+
for (const target of targets) {
|
|
474
|
+
const key = JSON.stringify([target.scope, target.scopeId]);
|
|
475
|
+
if (!seen.has(key))
|
|
476
|
+
fresh.set(key, target);
|
|
477
|
+
}
|
|
478
|
+
for (const key of fresh.keys())
|
|
479
|
+
seen.add(key);
|
|
480
|
+
return [...fresh.values()];
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
async load(checks) {
|
|
484
|
+
const vars = missing([
|
|
485
|
+
...checks.flatMap(({ agent, assembly }) => variableTargets(agent, assembly)),
|
|
486
|
+
...agents.map((agent) => ({ scope: 'agent', scopeId: agent.id })),
|
|
487
|
+
], variableScopes);
|
|
488
|
+
const attached = missing(checks.flatMap(({ agent, assembly }) => skillTargets(agent, assembly)), skillScopes);
|
|
489
|
+
if (vars.length)
|
|
490
|
+
variables.push(...(await deps.variables.admissionMetadata(conn, vars)));
|
|
491
|
+
if (attached.length)
|
|
492
|
+
skills.push(...(await deps.skills.admissionMetadata(conn, attached)));
|
|
493
|
+
},
|
|
494
|
+
get(agent, assembly) {
|
|
495
|
+
const merged = new Map();
|
|
496
|
+
for (const target of variableTargets(agent, assembly))
|
|
497
|
+
for (const row of variables)
|
|
498
|
+
if (row.scope === target.scope && row.scopeId === target.scopeId)
|
|
499
|
+
merged.set(row.name, row);
|
|
500
|
+
const targets = skillTargets(agent, assembly);
|
|
501
|
+
return {
|
|
502
|
+
skills: skills.filter((row) => targets.some((target) => target.scope === row.scope && target.scopeId === row.scopeId)),
|
|
503
|
+
variables: {
|
|
504
|
+
names: [...merged.values()]
|
|
505
|
+
.filter((row) => row.kind === 'value')
|
|
506
|
+
.map((row) => row.name),
|
|
507
|
+
teamOnly: [...merged.values()].filter((row) => row.kind === 'value' && row.teamRunnersOnly),
|
|
508
|
+
passthrough: [...merged.values()]
|
|
509
|
+
.filter((row) => row.kind === 'runner')
|
|
510
|
+
.map((row) => row.name),
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/** Eligibility needs existence/compatibility, not occupancy, other models or a full wait projection. */
|
|
517
|
+
async function preferredModelAvailable(conn, run, agent, inputs, metadata, inventory) {
|
|
518
|
+
const now = clock.now().getTime();
|
|
519
|
+
const registered = await inventory();
|
|
520
|
+
const reserved = upgradeReservation(run.executions ?? [], registered, now);
|
|
521
|
+
const entry = agent.modelEntries[0];
|
|
522
|
+
const single = { ...agent, modelEntries: [entry] };
|
|
523
|
+
const binding = deps.subjects.get(run.subject.kind);
|
|
524
|
+
if (!binding)
|
|
525
|
+
throw new Error(`No context provider for ${run.subject.kind}.`);
|
|
526
|
+
const checks = [];
|
|
527
|
+
for (const runner of registered) {
|
|
528
|
+
if ((reserved && runner.id !== reserved) ||
|
|
529
|
+
runner.status !== 'online' ||
|
|
530
|
+
!runner.lastSeenAt ||
|
|
531
|
+
Date.parse(runner.lastSeenAt) + TIMINGS.offlineAfterMs <= now ||
|
|
532
|
+
!isProtocolSupported(runner.protocolVersion) ||
|
|
533
|
+
!runsEntry(runner, entry, now) ||
|
|
534
|
+
(runner.trust !== 'team' && runner.ownerUserId !== run.actorUserId))
|
|
535
|
+
continue;
|
|
536
|
+
const assembly = await binding.context
|
|
537
|
+
.assemble(conn, {
|
|
538
|
+
run,
|
|
539
|
+
agent,
|
|
540
|
+
runner,
|
|
541
|
+
inputs,
|
|
542
|
+
cli: deps.cli.name,
|
|
543
|
+
appName: deps.app.name,
|
|
544
|
+
})
|
|
545
|
+
.catch(() => null);
|
|
546
|
+
// The diagnostic path treats an unreadable context as unknown and waits for the preferred model.
|
|
547
|
+
if (!assembly)
|
|
548
|
+
return true;
|
|
549
|
+
checks.push({ agent: single, runner, assembly });
|
|
550
|
+
}
|
|
551
|
+
await metadata.load(checks);
|
|
552
|
+
for (const { runner, assembly } of checks) {
|
|
553
|
+
const { skills, variables } = metadata.get(single, assembly);
|
|
554
|
+
if (skills.some((skill) => skill.hash === null))
|
|
555
|
+
throw notFound('Skill version');
|
|
556
|
+
if (compatibleAssembly(run, single, runner, assembly, skills.length, variables) !== 'ready')
|
|
557
|
+
continue;
|
|
558
|
+
if (variables.passthrough.length && runner.variables == null)
|
|
559
|
+
return true;
|
|
560
|
+
if (variables.passthrough.some((name) => !runner.variables?.includes(name)))
|
|
561
|
+
continue;
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
/** The same ordered-model decision is used for dispatch and for immediate runner affinity. */
|
|
567
|
+
async function modelAdmission(conn, run, agent, entry, inputs, metadata = admissionResources(conn), inventory = () => deps.runners.all(conn)) {
|
|
568
|
+
if (agent.modelEntries.indexOf(entry) <= 0 || !deps.runners)
|
|
569
|
+
return undefined;
|
|
570
|
+
const policy = await deps.selectionPolicies?.forRun(conn, run);
|
|
571
|
+
if (!policy)
|
|
572
|
+
return undefined;
|
|
573
|
+
const elapsed = Math.max(0, (clock.now().getTime() - queueStart(run)) / 1000);
|
|
574
|
+
const preferredAvailable = await preferredModelAvailable(conn, run, agent, inputs, metadata, inventory);
|
|
575
|
+
if (preferredAvailable && elapsed < policy.fallbackAfterSeconds)
|
|
576
|
+
return false;
|
|
577
|
+
return {
|
|
578
|
+
reason: preferredAvailable ? 'preferredQueued' : 'preferredUnavailable',
|
|
579
|
+
preferredTool: agent.modelEntries[0].tool,
|
|
580
|
+
preferredModel: agent.modelEntries[0].model,
|
|
581
|
+
queueEnteredAt: new Date(queueStart(run)).toISOString(),
|
|
582
|
+
waitedSeconds: elapsed,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
468
585
|
async function assemble({ conn }, run, agent, runner, inputs, leaseExpiresAt, prepared, entry, resources = claimResources(conn, run.id)) {
|
|
469
586
|
const binding = deps.subjects.get(run.subjectKind);
|
|
470
587
|
if (!binding)
|
|
@@ -494,9 +611,6 @@ export function createClaimService(deps) {
|
|
|
494
611
|
throw new NotForThisRunner();
|
|
495
612
|
const dirs = payloadDirs(assembly.dirs);
|
|
496
613
|
const directoryKey = directoryKeyOf(runner.id, dirs);
|
|
497
|
-
if (directoryKey !== null &&
|
|
498
|
-
(await directoryBusy(conn, runner.id, run.id, directoryKey)))
|
|
499
|
-
throw new NotForThisRunner();
|
|
500
614
|
const { skills, variables } = await resources(agent, assembly);
|
|
501
615
|
const names = variables.names;
|
|
502
616
|
const requires = cleanList([
|
|
@@ -536,6 +650,8 @@ export function createClaimService(deps) {
|
|
|
536
650
|
claim,
|
|
537
651
|
repos: dirs.filter((dir) => dir.kind === 'repo'),
|
|
538
652
|
onDemand: asksCredentialsOnDemand(runner),
|
|
653
|
+
tools: wrapsHostTools(runner),
|
|
654
|
+
responsibleUserId: responsibleIn(prepared),
|
|
539
655
|
}, prepared);
|
|
540
656
|
deps.secrets?.remember(runSecretsKey(run.id), [
|
|
541
657
|
...env.map((variable) => variable.value),
|
|
@@ -893,8 +1009,27 @@ export function createClaimService(deps) {
|
|
|
893
1009
|
const consent = await commentConsentFor(conn, runner, candidate);
|
|
894
1010
|
if (!consent.allowed)
|
|
895
1011
|
return { kind: 'skipped' };
|
|
1012
|
+
// Read once, before the transaction, and handed to every repository access hook of this claim. A failed read
|
|
1013
|
+
// leaves the run queued for the next poll rather than letting a provider pick an identity without it.
|
|
1014
|
+
let responsibleUserId = null;
|
|
1015
|
+
if ((deps.repoAccess?.list().length ?? 0) > 0)
|
|
1016
|
+
try {
|
|
1017
|
+
responsibleUserId = await responsibleOf(conn, deps.subjects, {
|
|
1018
|
+
kind: candidate.subjectKind,
|
|
1019
|
+
id: candidate.subjectId,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
deps.onClaimFailure?.(candidate.id, error);
|
|
1024
|
+
return { kind: 'skipped' };
|
|
1025
|
+
}
|
|
896
1026
|
// Before the transaction: on SQLite it holds the only connection, so the providers read what else they need now.
|
|
897
|
-
const prepared = new Map(await prepareExtensions(toRun(candidate), deps.sections, deps.mounts, (error) => deps.onClaimFailure?.(candidate.id, error), deps.repoAccess, {
|
|
1027
|
+
const prepared = new Map(await prepareExtensions(toRun(candidate), deps.sections, deps.mounts, (error) => deps.onClaimFailure?.(candidate.id, error), deps.repoAccess, {
|
|
1028
|
+
onDemand: asksCredentialsOnDemand(runner),
|
|
1029
|
+
tools: wrapsHostTools(runner),
|
|
1030
|
+
responsibleUserId,
|
|
1031
|
+
}));
|
|
1032
|
+
prepared.set(RESPONSIBLE_KEY, responsibleUserId);
|
|
898
1033
|
const subjectContext = deps.subjects.get(candidate.subjectKind)?.context;
|
|
899
1034
|
if (subjectContext?.prepare) {
|
|
900
1035
|
try {
|
|
@@ -982,27 +1117,10 @@ export function createClaimService(deps) {
|
|
|
982
1117
|
// Admission must precede preflight assembly: a missing or obsolete
|
|
983
1118
|
// subject is cancelled normally, rather than counted as setup failure.
|
|
984
1119
|
const resources = claimResources(conn, run.id);
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
}
|
|
1120
|
+
const admission = await modelAdmission(conn, toRun(current), agent, entry, pending.map(toInput));
|
|
1121
|
+
if (admission === false)
|
|
1122
|
+
throw new NotForThisRunner();
|
|
1123
|
+
selectionSnapshot = admission;
|
|
1006
1124
|
assembled = await assemble(unit, run, agent, runner, pending.map(toInput), leaseExpiresAt, prepared, entry, resources);
|
|
1007
1125
|
}
|
|
1008
1126
|
catch (error) {
|
|
@@ -1118,9 +1236,9 @@ export function createClaimService(deps) {
|
|
|
1118
1236
|
});
|
|
1119
1237
|
}
|
|
1120
1238
|
/** Queued runs that may be claimed now, in claim order. */
|
|
1121
|
-
async function candidates() {
|
|
1239
|
+
async function candidates(runner) {
|
|
1122
1240
|
const now = clock.now();
|
|
1123
|
-
|
|
1241
|
+
const ordinary = await runsRepo(tx.read()).findMany({
|
|
1124
1242
|
filter: (f) => f.and([
|
|
1125
1243
|
f.string('status').eq('queued'),
|
|
1126
1244
|
f.or([
|
|
@@ -1135,15 +1253,198 @@ export function createClaimService(deps) {
|
|
|
1135
1253
|
],
|
|
1136
1254
|
limit: CANDIDATES,
|
|
1137
1255
|
});
|
|
1256
|
+
if (ordinary.length === 0)
|
|
1257
|
+
return [];
|
|
1258
|
+
// A separate scan prevents ordinary candidate pages from hiding upgrade resumptions.
|
|
1259
|
+
const recovery = [];
|
|
1260
|
+
for (let offset = 0;; offset += CANDIDATES) {
|
|
1261
|
+
const page = await runsRepo(tx.read()).findMany({
|
|
1262
|
+
filter: (f) => f.and([
|
|
1263
|
+
f.string('status').eq('queued'),
|
|
1264
|
+
f.string('failureReason').eq('runnerUpgrade'),
|
|
1265
|
+
f
|
|
1266
|
+
.date('queueEnteredAt')
|
|
1267
|
+
.after(new Date(now.getTime() - 5 * 60_000)),
|
|
1268
|
+
f.or([
|
|
1269
|
+
f.date('availableAt').empty(),
|
|
1270
|
+
f.date('availableAt').notAfter(now),
|
|
1271
|
+
]),
|
|
1272
|
+
]),
|
|
1273
|
+
sort: (sort) => [
|
|
1274
|
+
sort.field('queueEnteredAt').asc(),
|
|
1275
|
+
sort.field('id').asc(),
|
|
1276
|
+
],
|
|
1277
|
+
limit: CANDIDATES,
|
|
1278
|
+
offset,
|
|
1279
|
+
});
|
|
1280
|
+
recovery.push(...page.filter((run) => toExecutions(run.executionHistory).at(-1)?.runnerId === runner.id));
|
|
1281
|
+
if (page.length < CANDIDATES || recovery.length >= CANDIDATES)
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
return [
|
|
1285
|
+
...new Map([...recovery, ...ordinary].map((run) => [run.id, run])).values(),
|
|
1286
|
+
];
|
|
1287
|
+
}
|
|
1288
|
+
/** One batch for locations, one for machines, and occupancy batches only when a preference exists. */
|
|
1289
|
+
async function affinityBatch(queued, runner) {
|
|
1290
|
+
const conn = tx.read();
|
|
1291
|
+
if (queued.length === 0)
|
|
1292
|
+
return {
|
|
1293
|
+
machines: [runner],
|
|
1294
|
+
preferred: new Map(),
|
|
1295
|
+
held: [],
|
|
1296
|
+
jobs: [],
|
|
1297
|
+
};
|
|
1298
|
+
const locations = await sessionsRepo(conn).groupBy({
|
|
1299
|
+
by: ['runnerId', 'subjectKind', 'subjectId'],
|
|
1300
|
+
aggregate: (a) => ({ updatedAt: a.max('updatedAt') }),
|
|
1301
|
+
filter: (f) => f.or(queued.map((run) => f.and([
|
|
1302
|
+
f.string('subjectKind').eq(run.subjectKind),
|
|
1303
|
+
f.string('subjectId').eq(run.subjectId),
|
|
1304
|
+
]))),
|
|
1305
|
+
sort: (sort) => sort.field('updatedAt').desc(),
|
|
1306
|
+
});
|
|
1307
|
+
const activeLocations = await runsRepo(conn).groupBy({
|
|
1308
|
+
by: ['runnerId', 'subjectKind', 'subjectId'],
|
|
1309
|
+
aggregate: (a) => ({ updatedAt: a.max('updatedAt') }),
|
|
1310
|
+
filter: (f) => f.and([
|
|
1311
|
+
f.string('runnerId').notEmpty(),
|
|
1312
|
+
f.or(ACTIVE.map((status) => f.string('status').eq(status))),
|
|
1313
|
+
f.or(queued.map((run) => f.and([
|
|
1314
|
+
f.string('subjectKind').eq(run.subjectKind),
|
|
1315
|
+
f.string('subjectId').eq(run.subjectId),
|
|
1316
|
+
]))),
|
|
1317
|
+
]),
|
|
1318
|
+
});
|
|
1319
|
+
const machineIds = [
|
|
1320
|
+
...new Set([
|
|
1321
|
+
runner.id,
|
|
1322
|
+
...queued.map((run) => toExecutions(run.executionHistory).at(-1)?.runnerId),
|
|
1323
|
+
...locations.map((item) => item.runnerId),
|
|
1324
|
+
...activeLocations.map((item) => item.runnerId),
|
|
1325
|
+
].filter((id) => Boolean(id))),
|
|
1326
|
+
];
|
|
1327
|
+
const machines = deps.runners
|
|
1328
|
+
? await deps.runners.all(conn, machineIds)
|
|
1329
|
+
: [runner];
|
|
1330
|
+
const preferred = new Map();
|
|
1331
|
+
for (const run of queued) {
|
|
1332
|
+
const last = toExecutions(run.executionHistory).at(-1)?.runnerId;
|
|
1333
|
+
const current = activeLocations
|
|
1334
|
+
.filter((item) => item.subjectKind === run.subjectKind &&
|
|
1335
|
+
item.subjectId === run.subjectId)
|
|
1336
|
+
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0]?.runnerId;
|
|
1337
|
+
const location = locations.find((item) => item.subjectKind === run.subjectKind &&
|
|
1338
|
+
item.subjectId === run.subjectId)?.runnerId;
|
|
1339
|
+
const machine = machines.find((item) => item.id === (last ?? current ?? location));
|
|
1340
|
+
if (machine)
|
|
1341
|
+
preferred.set(run.id, machine);
|
|
1342
|
+
}
|
|
1343
|
+
const ids = [...new Set([...preferred.values()].map((item) => item.id))];
|
|
1344
|
+
const held = ids.length === 0
|
|
1345
|
+
? []
|
|
1346
|
+
: await runsRepo(conn).groupBy({
|
|
1347
|
+
by: ['runnerId', 'tool'],
|
|
1348
|
+
aggregate: (a) => ({ count: a.count() }),
|
|
1349
|
+
filter: (f) => f.and([
|
|
1350
|
+
f.or(ids.map((id) => f.string('runnerId').eq(id))),
|
|
1351
|
+
f.or(ACTIVE.map((status) => f.string('status').eq(status))),
|
|
1352
|
+
]),
|
|
1353
|
+
});
|
|
1354
|
+
const jobs = ids.length === 0
|
|
1355
|
+
? []
|
|
1356
|
+
: await jobsRepo(conn).groupBy({
|
|
1357
|
+
by: ['runnerId'],
|
|
1358
|
+
aggregate: (a) => ({ count: a.count() }),
|
|
1359
|
+
filter: (f) => f.and([
|
|
1360
|
+
f.or(ids.map((id) => f.string('runnerId').eq(id))),
|
|
1361
|
+
f.or(ACTIVE_JOB.map((status) => f.string('status').eq(status))),
|
|
1362
|
+
]),
|
|
1363
|
+
});
|
|
1364
|
+
return { machines, preferred, held, jobs };
|
|
1365
|
+
}
|
|
1366
|
+
function hasImmediateRoom(run, agent, preferred, batch) {
|
|
1367
|
+
const now = clock.now().getTime();
|
|
1368
|
+
if (preferred.status !== 'online' ||
|
|
1369
|
+
!preferred.lastSeenAt ||
|
|
1370
|
+
Date.parse(preferred.lastSeenAt) + TIMINGS.offlineAfterMs <= now ||
|
|
1371
|
+
preferred.draining ||
|
|
1372
|
+
(preferred.globalFree ?? 1) <= 0 ||
|
|
1373
|
+
!fits(preferred, agent, run, now))
|
|
1374
|
+
return false;
|
|
1375
|
+
const held = batch.held.filter((item) => item.runnerId === preferred.id);
|
|
1376
|
+
if (held.reduce((count, item) => count + Number(item.count), 0) +
|
|
1377
|
+
batch.jobs
|
|
1378
|
+
.filter((item) => item.runnerId === preferred.id)
|
|
1379
|
+
.reduce((count, item) => count + Number(item.count), 0) >=
|
|
1380
|
+
preferred.slots)
|
|
1381
|
+
return false;
|
|
1382
|
+
if (!pickOpenEntry(preferred, agent, (tool) => held
|
|
1383
|
+
.filter((item) => item.tool === tool)
|
|
1384
|
+
.reduce((count, item) => count + Number(item.count), 0) <
|
|
1385
|
+
toolLimit(preferred, tool) &&
|
|
1386
|
+
(preferred.toolLoad?.[tool]?.free ?? 1) > 0, now))
|
|
1387
|
+
return false;
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
/** Evaluate only the candidate reached in queue order, reusing this poll's batched capacity and metadata. */
|
|
1391
|
+
async function readyPreference(run, agent, batch, claimant, metadata, inventory) {
|
|
1392
|
+
const conn = tx.read();
|
|
1393
|
+
const runner = batch.preferred.get(run.id);
|
|
1394
|
+
if (!runner ||
|
|
1395
|
+
runner.id === claimant.id ||
|
|
1396
|
+
!fits(claimant, agent, run, clock.now().getTime()) ||
|
|
1397
|
+
upgradeReservation(toExecutions(run.executionHistory), batch.machines, clock.now().getTime()) ||
|
|
1398
|
+
!hasImmediateRoom(run, agent, runner, batch))
|
|
1399
|
+
return false;
|
|
1400
|
+
const binding = deps.subjects.get(run.subjectKind);
|
|
1401
|
+
if (!binding || !(await commentConsentFor(conn, runner, run)).allowed)
|
|
1402
|
+
return false;
|
|
1403
|
+
const assembly = await binding.context
|
|
1404
|
+
.assemble(conn, {
|
|
1405
|
+
run: toRun(run),
|
|
1406
|
+
agent,
|
|
1407
|
+
runner,
|
|
1408
|
+
inputs: [],
|
|
1409
|
+
cli: deps.cli.name,
|
|
1410
|
+
appName: deps.app.name,
|
|
1411
|
+
})
|
|
1412
|
+
.catch(() => null);
|
|
1413
|
+
if (!assembly)
|
|
1414
|
+
return false;
|
|
1415
|
+
await metadata.load([{ agent, assembly }]);
|
|
1416
|
+
const { skills, variables } = metadata.get(agent, assembly);
|
|
1417
|
+
if (!variables.passthrough.every((name) => runner.variables?.includes(name)) ||
|
|
1418
|
+
skills.some((row) => row.hash === null) ||
|
|
1419
|
+
compatibleAssembly(toRun(run), agent, runner, assembly, skills.length, variables) !== 'ready')
|
|
1420
|
+
return false;
|
|
1421
|
+
const entry = pickOpenEntry(runner, agent, (tool) => batch.held
|
|
1422
|
+
.filter((item) => item.runnerId === runner.id && item.tool === tool)
|
|
1423
|
+
.reduce((count, item) => count + Number(item.count), 0) <
|
|
1424
|
+
toolLimit(runner, tool) && (runner.toolLoad?.[tool]?.free ?? 1) > 0, clock.now().getTime());
|
|
1425
|
+
if (!entry)
|
|
1426
|
+
return false;
|
|
1427
|
+
if (agent.modelEntries.indexOf(entry) > 0) {
|
|
1428
|
+
const inputs = (await pendingInputs(conn, run.id)).map(toInput);
|
|
1429
|
+
if ((await modelAdmission(conn, toRun(run), agent, entry, inputs, metadata, inventory)) === false)
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
return true;
|
|
1138
1433
|
}
|
|
1139
1434
|
async function claimOne(runner, seen, reported) {
|
|
1140
1435
|
if (runner.draining)
|
|
1141
1436
|
return null;
|
|
1142
1437
|
const conn = tx.read();
|
|
1143
|
-
const queued = await candidates();
|
|
1438
|
+
const queued = await candidates(runner);
|
|
1439
|
+
const affinity = await affinityBatch(queued, runner);
|
|
1144
1440
|
const agents = await findAgents(conn, queued
|
|
1145
1441
|
.filter((candidate) => !seen.has(candidate.id))
|
|
1146
1442
|
.map((candidate) => candidate.agentId));
|
|
1443
|
+
const metadata = admissionResources(conn, [...agents.values()]);
|
|
1444
|
+
let inventoryRead;
|
|
1445
|
+
const inventory = () => (inventoryRead ??= deps.runners
|
|
1446
|
+
? deps.runners.all(conn)
|
|
1447
|
+
: Promise.resolve([runner]));
|
|
1147
1448
|
const full = new Set();
|
|
1148
1449
|
for (const candidate of queued) {
|
|
1149
1450
|
if (seen.has(candidate.id) || full.has(candidate.agentId))
|
|
@@ -1154,6 +1455,15 @@ export function createClaimService(deps) {
|
|
|
1154
1455
|
(!deps.subjects.get(candidate.subjectKind)?.actsAs &&
|
|
1155
1456
|
!fits(runner, agent, candidate, clock.now().getTime())))
|
|
1156
1457
|
continue;
|
|
1458
|
+
const reserved = upgradeReservation(toExecutions(candidate.executionHistory), affinity.machines, clock.now().getTime());
|
|
1459
|
+
if (reserved && reserved !== runner.id)
|
|
1460
|
+
continue;
|
|
1461
|
+
const preferred = affinity.preferred.get(candidate.id);
|
|
1462
|
+
if (!reserved &&
|
|
1463
|
+
preferred &&
|
|
1464
|
+
preferred.id !== runner.id &&
|
|
1465
|
+
(await readyPreference(candidate, agent, affinity, runner, metadata, inventory)))
|
|
1466
|
+
continue;
|
|
1157
1467
|
// None of its tools has room on the runner's side: the runs behind it may still fit.
|
|
1158
1468
|
if (!pickOpenEntry(runner, agent, (tool) => reported.has(tool), clock.now().getTime()))
|
|
1159
1469
|
continue;
|