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