@nocobase/app-plugin-agents 0.1.0-beta.10 → 0.1.0-beta.11
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 +43 -0
- package/README.md +23 -1
- package/dist/client/locales/en-US.d.ts +1 -0
- package/dist/client/locales/en-US.d.ts.map +1 -1
- package/dist/client/locales/en-US.js +1 -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 +1 -0
- package/dist/client/locales/skills.en-US.d.ts.map +1 -1
- package/dist/client/locales/skills.en-US.js +2 -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 +2 -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 +1 -0
- package/dist/client/locales/zh-CN.js.map +1 -1
- 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 +7 -10
- 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/composition.d.ts +2 -0
- package/dist/server/composition.d.ts.map +1 -1
- package/dist/server/composition.js +13 -1
- package/dist/server/composition.js.map +1 -1
- package/dist/server/core/agents/agent.store.d.ts +1 -1
- package/dist/server/core/agents/agent.store.d.ts.map +1 -1
- package/dist/server/core/agents/agent.store.js +3 -6
- package/dist/server/core/agents/agent.store.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/runs/claim.d.ts +15 -3
- package/dist/server/core/runs/claim.d.ts.map +1 -1
- package/dist/server/core/runs/claim.js +257 -24
- 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/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/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 +24 -18
- package/dist/server/core/runs/lifecycle.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 +12 -0
- package/dist/server/core/runs/run.service.d.ts.map +1 -1
- package/dist/server/core/runs/run.service.js +518 -33
- package/dist/server/core/runs/run.service.js.map +1 -1
- package/dist/server/core/runs/run.store.d.ts +5 -0
- package/dist/server/core/runs/run.store.d.ts.map +1 -1
- package/dist/server/core/runs/run.store.js +3 -0
- package/dist/server/core/runs/run.store.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 +1 -0
- package/dist/server/core/runs/sweeper.js.map +1 -1
- package/dist/server/core/runs/transitions.d.ts +1 -1
- package/dist/server/core/runs/transitions.d.ts.map +1 -1
- package/dist/server/core/runs/transitions.js +11 -3
- package/dist/server/core/runs/transitions.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 +43 -58
- 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/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/providers/agents.d.ts +4 -0
- package/dist/server/providers/agents.d.ts.map +1 -1
- package/dist/server/providers/agents.js +3 -0
- package/dist/server/providers/agents.js.map +1 -1
- package/dist/server/providers/runners.d.ts.map +1 -1
- package/dist/server/providers/runners.js +23 -0
- package/dist/server/providers/runners.js.map +1 -1
- package/dist/server/routes/admin.d.ts.map +1 -1
- package/dist/server/routes/admin.js +18 -8
- package/dist/server/routes/admin.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/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.map +1 -1
- package/dist/server/routes/schemas.js +14 -2
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/server/runners/runner.service.d.ts +11 -1
- package/dist/server/runners/runner.service.d.ts.map +1 -1
- package/dist/server/runners/runner.service.js +19 -1
- package/dist/server/runners/runner.service.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/runs.d.ts +10 -0
- package/dist/shared/runs.d.ts.map +1 -1
- package/dist/shared/runs.js.map +1 -1
- package/dist/shared/skills.d.ts +8 -4
- 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 +8 -8
- package/skills/nocobase-app-plugin-agents/references/extension-points.md +19 -19
- package/skills/nocobase-app-plugin-agents/references/variables-skills-usage.md +3 -1
|
@@ -29,6 +29,16 @@
|
|
|
29
29
|
*
|
|
30
30
|
* Portable by design: the database layer offers no `FOR UPDATE SKIP LOCKED` or advisory locks, and the guarded update
|
|
31
31
|
* plus the agent-row write give the same guarantees on every dialect (SQLite serializes writers anyway).
|
|
32
|
+
*
|
|
33
|
+
* Polls are the hot path: every runner walks the same candidates, so a write per candidate is a write per candidate per
|
|
34
|
+
* runner per poll. Everything that rules a candidate out is read outside any transaction — identity, `fits()`, slots,
|
|
35
|
+
* concurrency and comment consent — and a transaction opens only for the run about to be claimed. The agent row is
|
|
36
|
+
* the only row a claim locks with a self-update, and only in that final transaction after `fits()` (or when a stale
|
|
37
|
+
* identity has to be repaired first, which current work never needs). PostgreSQL runs at READ COMMITTED, where each
|
|
38
|
+
* statement sees the latest committed rows, so no transaction needs no-op writes to refresh a snapshot; comment
|
|
39
|
+
* consent needs no chain lock, because confirmation is final and rejection or Run as me end the chain's queued runs,
|
|
40
|
+
* which the guarded update sees. A new write or lock here needs a measured reason (`tests/claim-concurrency.test.ts`
|
|
41
|
+
* counts them, and the lock waits on PostgreSQL).
|
|
32
42
|
|
|
33
43
|
*/
|
|
34
44
|
import { type AgentTool, type RunApp, type RunnerFeature, type RunPayload, type RunSkill, type WorkspaceDir } from '@nocobase/agent-protocol';
|
|
@@ -42,7 +52,7 @@ import { type Clock } from '../../kernel/clock.js';
|
|
|
42
52
|
import type { People } from '../../kernel/people.js';
|
|
43
53
|
import type { IdSource } from '../../kernel/ids.js';
|
|
44
54
|
import { type SecretMemory } from '../../kernel/redaction.js';
|
|
45
|
-
import type { TxRunner } from '../../kernel/tx.js';
|
|
55
|
+
import type { Tx, TxRunner } from '../../kernel/tx.js';
|
|
46
56
|
import type { SkillService, SkillTarget } from '../skills/index.js';
|
|
47
57
|
import { type VariableService, type VariableTarget } from '../variables/index.js';
|
|
48
58
|
import { type RepoAccessRegistry, type BriefSectionRegistry, type RunMountRegistry } from './extensions.js';
|
|
@@ -51,6 +61,8 @@ import { type AgentCli } from './policy.js';
|
|
|
51
61
|
import { type RunRecord } from './run.store.js';
|
|
52
62
|
import { type TransitionDeps } from './transitions.js';
|
|
53
63
|
export interface ClaimService {
|
|
64
|
+
reconcileCommentConsent(unit: Tx, runId: string): Promise<void>;
|
|
65
|
+
reconcileCommentQueue(): Promise<void>;
|
|
54
66
|
/** Structured ordered-model diagnostics; machine visibility is enforced by the caller. */
|
|
55
67
|
selectionWait(runId: string): Promise<import('../../../shared/runs.js').ModelSelectionWait | null>;
|
|
56
68
|
/** Read-only capacity and compatibility for a real subject and candidate agent, as the run's actor. */
|
|
@@ -120,8 +132,8 @@ export declare function payloadRequires(dirs: readonly WorkspaceDir[], skills: n
|
|
|
120
132
|
export declare function payloadDirs(dirs: readonly SubjectDir[]): WorkspaceDir[];
|
|
121
133
|
/** Where a run's variables come from, in merge order: the subject's scopes, its directories', the agent's. */
|
|
122
134
|
export declare function variableTargets(agent: Pick<Agent, 'id'>, assembly: Pick<SubjectAssembly, 'scopes' | 'dirs'>): VariableTarget[];
|
|
123
|
-
/**
|
|
124
|
-
export declare function skillTargets(
|
|
135
|
+
/** Team defaults, application scopes and directories, from lowest to highest priority. */
|
|
136
|
+
export declare function skillTargets(_agent: Pick<Agent, 'id'>, assembly: Pick<SubjectAssembly, 'scopes' | 'dirs'>): SkillTarget[];
|
|
125
137
|
/** The brief of a run on a subject, as the claim renders it; the preview renders it the same way. */
|
|
126
138
|
export interface RenderedBrief {
|
|
127
139
|
readonly layers: BriefLayers;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../../../server/core/runs/claim.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../../../server/core/runs/claim.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,7 +49,7 @@ 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';
|
|
52
|
+
import { cleanList, covers, jsonObject } from '../../kernel/values.js';
|
|
42
53
|
import { findAgent, lockAgentForClaim } from '../agents/index.js';
|
|
43
54
|
import { joinBrief, renderBrief } from '../brief/index.js';
|
|
44
55
|
import { cliPackageFor } from './cli-package.js';
|
|
@@ -48,10 +59,12 @@ import { mountsFor, prepareExtensions, repoAccessFor, sessionKeyOf, withSections
|
|
|
48
59
|
import { MAX_CLAIM_FAILURES, toolPolicyFor } from './policy.js';
|
|
49
60
|
import { mintRunToken } from './run-tokens.js';
|
|
50
61
|
import { runsHeldByTool } from './runner-view.js';
|
|
51
|
-
import { ACTIVE, countActive, eventsRepo, findRunRecord, markDelivered, pendingInputs, runsOfKey, runsRepo, sessionsRepo, toInput, toRun, } from './run.store.js';
|
|
62
|
+
import { ACTIVE, countActive, eventsRepo, findRunRecord, markDelivered, pendingInputs, runsOfKey, runsRepo, inputsRepo, sessionsRepo, toInput, toExecutions, toRun, } from './run.store.js';
|
|
52
63
|
import { dropInputs } from './input-gate.js';
|
|
53
64
|
import { finishRun } from './transitions.js';
|
|
54
|
-
import { refreshQueuedIdentity } from './identity.js';
|
|
65
|
+
import { queuedIdentityCurrent, refreshQueuedIdentity } from './identity.js';
|
|
66
|
+
import { currentCommentOwner, commentConsent, commentFromOwner, withdrawCommentRequests, } from './comment-consent.js';
|
|
67
|
+
import { insertRunRequest, runRequestsRepo } from './run-requests.js';
|
|
55
68
|
import { consumeReset, saveBrief } from './workspace.store.js';
|
|
56
69
|
/** Queued runs looked at per claim, oldest first by priority. */
|
|
57
70
|
const CANDIDATES = 50;
|
|
@@ -158,11 +171,11 @@ export function variableTargets(agent, assembly) {
|
|
|
158
171
|
{ scope: 'agent', scopeId: agent.id },
|
|
159
172
|
];
|
|
160
173
|
}
|
|
161
|
-
/**
|
|
162
|
-
export function skillTargets(
|
|
174
|
+
/** Team defaults, application scopes and directories, from lowest to highest priority. */
|
|
175
|
+
export function skillTargets(_agent, assembly) {
|
|
163
176
|
return [
|
|
164
|
-
|
|
165
|
-
...assembly.scopes,
|
|
177
|
+
GLOBAL_SKILL_TARGET,
|
|
178
|
+
...assembly.scopes.filter((target) => target.scope !== 'agent' && target.scope !== 'global'),
|
|
166
179
|
...assembly.dirs.flatMap((dir) => dir.scopeId ? [{ scope: 'workdir', scopeId: dir.scopeId }] : []),
|
|
167
180
|
];
|
|
168
181
|
}
|
|
@@ -257,13 +270,14 @@ export function createClaimService(deps) {
|
|
|
257
270
|
return 'restricted';
|
|
258
271
|
return 'ready';
|
|
259
272
|
}
|
|
260
|
-
async function selectionFacts(conn, run, agent, requirePolicy = true) {
|
|
273
|
+
async function selectionFacts(conn, run, agent, requirePolicy = true, trust) {
|
|
261
274
|
const policy = await deps.selectionPolicies?.forRun(conn, run);
|
|
262
275
|
if ((requirePolicy && !policy) || !deps.runners)
|
|
263
276
|
return null;
|
|
264
277
|
const now = clock.now();
|
|
265
278
|
const start = queueStart(run);
|
|
266
|
-
const registered = await deps.runners.all(conn)
|
|
279
|
+
const registered = (await deps.runners.all(conn)).filter((runner) => !trust ||
|
|
280
|
+
(trust === 'team' ? runner.trust === 'team' : runner.trust !== 'team'));
|
|
267
281
|
const binding = deps.subjects.get(run.subject.kind);
|
|
268
282
|
if (!binding)
|
|
269
283
|
throw new Error(`No context provider for ${run.subject.kind}.`);
|
|
@@ -536,7 +550,7 @@ export function createClaimService(deps) {
|
|
|
536
550
|
const sessionNote = resume ? assembly.turn.resumed : assembly.turn.fresh;
|
|
537
551
|
const turn = [
|
|
538
552
|
brief.turn,
|
|
539
|
-
...(run.failureReason === 'runnerUpgrade'
|
|
553
|
+
...(toExecutions(run.executionHistory).find((execution) => execution.attempt === Number(run.attempt) - 1)?.failureReason === 'runnerUpgrade'
|
|
540
554
|
? [
|
|
541
555
|
'The previous attempt was interrupted by a runner upgrade. Continue from the current state.',
|
|
542
556
|
]
|
|
@@ -657,17 +671,175 @@ export function createClaimService(deps) {
|
|
|
657
671
|
});
|
|
658
672
|
});
|
|
659
673
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
674
|
+
/** What reconciling a comment run would change, read without writing: most runs need nothing. */
|
|
675
|
+
async function planCommentConsent(conn, runId) {
|
|
676
|
+
const none = { kind: 'none' };
|
|
677
|
+
const run = await findRunRecord(conn, runId);
|
|
678
|
+
if (!run?.commentRootRunId)
|
|
679
|
+
return none;
|
|
680
|
+
const root = await findRunRecord(conn, run.commentRootRunId);
|
|
681
|
+
if (!root)
|
|
682
|
+
return none;
|
|
683
|
+
if (run.status !== 'queued') {
|
|
684
|
+
const pending = await runRequestsRepo(conn).findMany({
|
|
685
|
+
filter: {
|
|
686
|
+
commentRootRunId: root.id,
|
|
687
|
+
commentRunId: run.id,
|
|
688
|
+
purpose: 'personalRunner',
|
|
689
|
+
status: 'pending',
|
|
690
|
+
},
|
|
691
|
+
limit: 1,
|
|
692
|
+
});
|
|
693
|
+
return pending.length > 0 ? { kind: 'withdraw', rootId: root.id } : none;
|
|
694
|
+
}
|
|
695
|
+
if (!(await queuedIdentityCurrent(conn, deps, run)))
|
|
696
|
+
return { kind: 'refresh', run };
|
|
697
|
+
if (!(await currentCommentOwner(conn, deps.subjects, root)))
|
|
698
|
+
return {
|
|
699
|
+
kind: 'cancel',
|
|
700
|
+
run,
|
|
701
|
+
detail: 'The comment belongs to an earlier owner assignment.',
|
|
702
|
+
};
|
|
703
|
+
if (root.commentExecution === 'requester' ||
|
|
704
|
+
run.actorUserId !== root.ownerUserId)
|
|
705
|
+
return none;
|
|
706
|
+
const { decision } = await commentConsent(conn, root);
|
|
707
|
+
if (decision === 'confirmed' || decision === 'pending')
|
|
708
|
+
return none;
|
|
709
|
+
if (await commentFromOwner(conn, root))
|
|
710
|
+
return none;
|
|
711
|
+
if (decision === 'rejected')
|
|
712
|
+
return {
|
|
713
|
+
kind: 'cancel',
|
|
714
|
+
run,
|
|
715
|
+
detail: 'The comment personal runner authorization is no longer valid.',
|
|
716
|
+
};
|
|
717
|
+
const agent = await findAgent(conn, run.agentId);
|
|
718
|
+
if (!agent)
|
|
719
|
+
return none;
|
|
720
|
+
const team = await selectionFacts(conn, toRun(run), agent, false, 'team');
|
|
721
|
+
if (!team ||
|
|
722
|
+
team.entries.some((e) => e.compatibleCount > 0 ||
|
|
723
|
+
e.reason === 'unknown' ||
|
|
724
|
+
e.blockers?.includes('unknown')))
|
|
725
|
+
return none;
|
|
726
|
+
const personal = await selectionFacts(conn, toRun(run), agent, false, 'personal');
|
|
727
|
+
if (!personal?.entries.some((e) => e.compatibleCount > 0))
|
|
728
|
+
return none;
|
|
729
|
+
const sourceInputs = await inputsRepo(conn).findMany({
|
|
730
|
+
filter: { runId: root.id },
|
|
731
|
+
sort: (sort) => [sort.field('createdAt').asc(), sort.field('id').asc()],
|
|
732
|
+
});
|
|
733
|
+
const input = sourceInputs.find((input) => input.type === 'comment' &&
|
|
734
|
+
jsonObject(input.payload).commentSourceSnapshot === true) ?? sourceInputs.find((input) => input.type === 'comment');
|
|
735
|
+
return input ? { kind: 'request', run, root, input } : none;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Brings a comment run's consent up to date. It takes no lock of its own: callers that may create a request hold
|
|
739
|
+
* the run's agent row (enqueue, retry and the sweeper do), which serializes the duplicate check in insertRunRequest
|
|
740
|
+
* with every other enqueue and claim of that agent. Everything else is a guarded update.
|
|
741
|
+
*/
|
|
742
|
+
async function reconcileCommentConsent(unit, runId) {
|
|
743
|
+
let plan = await planCommentConsent(unit.conn, runId);
|
|
744
|
+
if (plan.kind === 'refresh') {
|
|
745
|
+
if (!(await refreshQueuedIdentity(unit, deps, plan.run)))
|
|
746
|
+
return;
|
|
747
|
+
plan = await planCommentConsent(unit.conn, runId);
|
|
748
|
+
}
|
|
749
|
+
switch (plan.kind) {
|
|
750
|
+
case 'withdraw':
|
|
751
|
+
await withdrawCommentRequests(unit, plan.rootId, clock.now().toISOString(), runId);
|
|
752
|
+
return;
|
|
753
|
+
case 'cancel':
|
|
754
|
+
await finishRun(unit, deps, plan.run, {
|
|
755
|
+
status: 'cancelled',
|
|
756
|
+
reason: null,
|
|
757
|
+
detail: plan.detail,
|
|
758
|
+
});
|
|
759
|
+
return;
|
|
760
|
+
case 'request': {
|
|
761
|
+
const { run, root, input } = plan;
|
|
762
|
+
await insertRunRequest(unit, deps, {
|
|
763
|
+
purpose: 'personalRunner',
|
|
764
|
+
commentRootRunId: root.id,
|
|
765
|
+
commentRunId: run.id,
|
|
766
|
+
commentOwnerKey: root.commentOwnerKey ?? null,
|
|
767
|
+
agentId: run.agentId,
|
|
768
|
+
subject: toRun(run).subject,
|
|
769
|
+
threadScope: run.threadScope,
|
|
770
|
+
// The plan only asks for runs acting as the chain's owner.
|
|
771
|
+
responsibleUserId: run.actorUserId,
|
|
772
|
+
requestedByUserId: root.requestedByUserId ?? root.actorUserId,
|
|
773
|
+
ownerUserId: run.actorUserId,
|
|
774
|
+
priority: Number(run.priority),
|
|
775
|
+
requires: toRun(run).requires,
|
|
776
|
+
fireAt: run.availableAt,
|
|
777
|
+
maxAttempts: Number(run.maxAttempts),
|
|
778
|
+
input: toInput(input),
|
|
779
|
+
});
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
default:
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Whether this runner may take this candidate under comment consent, read outside any transaction. Immutable facts
|
|
788
|
+
* settle most candidates without a query: no comment chain, work run as the commenter, or work not run as the
|
|
789
|
+
* chain's owner. The rest read the chain's requests once. A decision never needs a lock: confirmation is final,
|
|
790
|
+
* and rejection or Run as me end the chain's queued runs, which the claim's guarded `status = 'queued'` update sees.
|
|
791
|
+
*/
|
|
792
|
+
async function commentConsentFor(conn, runner, run) {
|
|
793
|
+
const allowed = { allowed: true, confirmedByUserId: null };
|
|
794
|
+
const refused = { allowed: false, confirmedByUserId: null };
|
|
795
|
+
if (!run.commentRootRunId || run.commentExecution === 'requester')
|
|
796
|
+
return allowed;
|
|
797
|
+
const root = await findRunRecord(conn, run.commentRootRunId);
|
|
798
|
+
if (!root || root.commentRootRunId !== root.id)
|
|
799
|
+
return refused;
|
|
800
|
+
if (root.commentExecution === 'requester' ||
|
|
801
|
+
run.actorUserId !== root.ownerUserId)
|
|
802
|
+
return allowed;
|
|
803
|
+
const { decision, confirmation } = await commentConsent(conn, root);
|
|
804
|
+
if (decision === 'rejected')
|
|
805
|
+
return refused;
|
|
806
|
+
if (decision === 'confirmed')
|
|
807
|
+
return {
|
|
808
|
+
allowed: true,
|
|
809
|
+
confirmedByUserId: confirmation?.settledById ?? null,
|
|
810
|
+
};
|
|
811
|
+
if (runner.trust === 'team')
|
|
812
|
+
return allowed;
|
|
813
|
+
return (await commentFromOwner(conn, root)) ? allowed : refused;
|
|
814
|
+
}
|
|
815
|
+
/** Withdraws the request a team claim made obsolete, writing only when one is pending. */
|
|
816
|
+
async function withdrawIfPending(rootId, runId) {
|
|
817
|
+
try {
|
|
818
|
+
const pending = await runRequestsRepo(tx.read()).findMany({
|
|
819
|
+
filter: {
|
|
820
|
+
commentRootRunId: rootId,
|
|
821
|
+
commentRunId: runId,
|
|
822
|
+
purpose: 'personalRunner',
|
|
823
|
+
status: 'pending',
|
|
824
|
+
},
|
|
825
|
+
limit: 1,
|
|
826
|
+
});
|
|
827
|
+
if (pending.length > 0)
|
|
828
|
+
await tx.run((unit) => withdrawCommentRequests(unit, rootId, clock.now().toISOString(), runId));
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
deps.onClaimFailure?.(runId, error);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function admit(unit, run, agent, runner, inputs, prepared) {
|
|
835
|
+
const rejection = await deps.subjects.get(run.subjectKind)?.context.admit?.(unit.conn, {
|
|
664
836
|
run: toRun(run),
|
|
665
837
|
agent,
|
|
666
838
|
runner,
|
|
667
839
|
inputs,
|
|
668
840
|
cli: deps.cli.name,
|
|
669
841
|
appName: deps.app.name,
|
|
670
|
-
});
|
|
842
|
+
}, prepared);
|
|
671
843
|
if (!rejection)
|
|
672
844
|
return true;
|
|
673
845
|
await finishRun(unit, deps, run, {
|
|
@@ -678,8 +850,17 @@ export function createClaimService(deps) {
|
|
|
678
850
|
return false;
|
|
679
851
|
}
|
|
680
852
|
async function attempt(runner, agent, candidate, reported) {
|
|
681
|
-
|
|
682
|
-
|
|
853
|
+
const conn = tx.read();
|
|
854
|
+
// Rules that do not depend on who the run acts as come first, and settle the rest of the pass: an agent at its
|
|
855
|
+
// limit is checked once per poll, not once per queued run.
|
|
856
|
+
if ((await deps.slotsUsed(conn, runner.id)) >= runner.slots)
|
|
857
|
+
return { kind: 'full', scope: 'runner' };
|
|
858
|
+
if ((await countActive(conn, 'agentId', agent.id)) >= agent.maxConcurrentRuns)
|
|
859
|
+
return { kind: 'full', scope: 'agent' };
|
|
860
|
+
// Repair pre-upgrade queued work before even checking personal runner ownership. Reading first keeps a poll over
|
|
861
|
+
// current work free of transactions and row locks; only work whose identity changed is written.
|
|
862
|
+
if (deps.subjects.get(candidate.subjectKind)?.actsAs &&
|
|
863
|
+
!(await queuedIdentityCurrent(conn, deps, candidate))) {
|
|
683
864
|
const current = await tx.run(async (unit) => {
|
|
684
865
|
await lockAgentForClaim(unit.conn, agent.id, clock.now().toISOString());
|
|
685
866
|
const run = await findRunRecord(unit.conn, candidate.id);
|
|
@@ -692,13 +873,12 @@ export function createClaimService(deps) {
|
|
|
692
873
|
candidate = current;
|
|
693
874
|
}
|
|
694
875
|
// Avoid assembling or asking authorization when a cheap claim rule already rules this work out.
|
|
695
|
-
const conn = tx.read();
|
|
696
876
|
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
877
|
(await runsOfKey(conn, candidate, ACTIVE)).length > 0)
|
|
701
878
|
return { kind: 'skipped' };
|
|
879
|
+
const consent = await commentConsentFor(conn, runner, candidate);
|
|
880
|
+
if (!consent.allowed)
|
|
881
|
+
return { kind: 'skipped' };
|
|
702
882
|
// Before the transaction: on SQLite it holds the only connection, so the providers read what else they need now.
|
|
703
883
|
const prepared = new Map(await prepareExtensions(toRun(candidate), deps.sections, deps.mounts, (error) => deps.onClaimFailure?.(candidate.id, error), deps.repoAccess, { onDemand: asksCredentialsOnDemand(runner) }));
|
|
704
884
|
const subjectContext = deps.subjects.get(candidate.subjectKind)?.context;
|
|
@@ -753,6 +933,8 @@ export function createClaimService(deps) {
|
|
|
753
933
|
]),
|
|
754
934
|
values: {
|
|
755
935
|
status: 'dispatched',
|
|
936
|
+
failureReason: null,
|
|
937
|
+
failureDetail: null,
|
|
756
938
|
runnerId: runner.id,
|
|
757
939
|
// Brief providers observe this reservation inside the claim transaction.
|
|
758
940
|
// Replace a previous attempt's tool before computing the roster.
|
|
@@ -762,16 +944,21 @@ export function createClaimService(deps) {
|
|
|
762
944
|
lastActivityAt: nowText,
|
|
763
945
|
availableAt: null,
|
|
764
946
|
updatedAt: nowText,
|
|
947
|
+
...(consent.confirmedByUserId
|
|
948
|
+
? { confirmedByUserId: consent.confirmedByUserId }
|
|
949
|
+
: {}),
|
|
765
950
|
},
|
|
766
951
|
});
|
|
767
952
|
if (claimed.updatedCount !== 1)
|
|
768
953
|
return null;
|
|
769
954
|
const run = (await findRunRecord(conn, candidate.id));
|
|
770
|
-
const pending = await pendingInputs(conn, run.id)
|
|
955
|
+
const pending = (await pendingInputs(conn, run.id)).filter((input) => !run.commentRootRunId ||
|
|
956
|
+
input.type !== 'comment' ||
|
|
957
|
+
jsonObject(input.payload).commentRootRunId === run.commentRootRunId);
|
|
771
958
|
let assembled;
|
|
772
959
|
let selectionSnapshot;
|
|
773
960
|
try {
|
|
774
|
-
if (!(await admit(unit, run, agent, runner, pending.map(toInput))))
|
|
961
|
+
if (!(await admit(unit, run, agent, runner, pending.map(toInput), prepared.get('subject'))))
|
|
775
962
|
return null;
|
|
776
963
|
// Admission must precede preflight assembly: a missing or obsolete
|
|
777
964
|
// subject is cancelled normally, rather than counted as setup failure.
|
|
@@ -865,6 +1052,11 @@ export function createClaimService(deps) {
|
|
|
865
1052
|
await discardRepoAccess(candidate, prepared);
|
|
866
1053
|
return { kind: 'skipped' };
|
|
867
1054
|
}
|
|
1055
|
+
// After the claim commits, never inside it: decisions settle the request before touching runs, so a claim that
|
|
1056
|
+
// held its run while waiting for the request could deadlock with them. A request left pending by a crash here is
|
|
1057
|
+
// withdrawn by the next reconciliation, and confirming it finds the run no longer waiting.
|
|
1058
|
+
if (candidate.commentRootRunId && runner.trust === 'team')
|
|
1059
|
+
await withdrawIfPending(candidate.commentRootRunId, candidate.id);
|
|
868
1060
|
return { kind: 'claimed', payload: taken };
|
|
869
1061
|
}
|
|
870
1062
|
async function discardRepoAccess(run, prepared) {
|
|
@@ -897,6 +1089,7 @@ export function createClaimService(deps) {
|
|
|
897
1089
|
filter: { id: runId },
|
|
898
1090
|
values: {
|
|
899
1091
|
claimFailures: failures,
|
|
1092
|
+
failureReason: 'setupFailed',
|
|
900
1093
|
failureDetail: error.message.slice(0, 10_000),
|
|
901
1094
|
},
|
|
902
1095
|
});
|
|
@@ -927,8 +1120,9 @@ export function createClaimService(deps) {
|
|
|
927
1120
|
const conn = tx.read();
|
|
928
1121
|
const queued = await candidates();
|
|
929
1122
|
const agents = new Map();
|
|
1123
|
+
const full = new Set();
|
|
930
1124
|
for (const candidate of queued) {
|
|
931
|
-
if (seen.has(candidate.id))
|
|
1125
|
+
if (seen.has(candidate.id) || full.has(candidate.agentId))
|
|
932
1126
|
continue;
|
|
933
1127
|
seen.add(candidate.id);
|
|
934
1128
|
if (!agents.has(candidate.agentId))
|
|
@@ -945,6 +1139,13 @@ export function createClaimService(deps) {
|
|
|
945
1139
|
const result = await attempt(runner, agent, candidate, reported);
|
|
946
1140
|
if (result.kind === 'claimed')
|
|
947
1141
|
return result.payload;
|
|
1142
|
+
if (result.kind === 'full') {
|
|
1143
|
+
if (result.scope === 'runner')
|
|
1144
|
+
return null;
|
|
1145
|
+
// Seen candidates are passed over for good in this claim; unvisited ones stay eligible for the next pass.
|
|
1146
|
+
seen.delete(candidate.id);
|
|
1147
|
+
full.add(candidate.agentId);
|
|
1148
|
+
}
|
|
948
1149
|
}
|
|
949
1150
|
catch (error) {
|
|
950
1151
|
// Rolled back: the run stays queued for a runner it fits.
|
|
@@ -958,6 +1159,38 @@ export function createClaimService(deps) {
|
|
|
958
1159
|
return null;
|
|
959
1160
|
}
|
|
960
1161
|
return {
|
|
1162
|
+
reconcileCommentConsent,
|
|
1163
|
+
async reconcileCommentQueue() {
|
|
1164
|
+
const conn = tx.read();
|
|
1165
|
+
const queued = await runsRepo(conn).findMany({
|
|
1166
|
+
filter: (f) => f.and([
|
|
1167
|
+
f.string('status').eq('queued'),
|
|
1168
|
+
f.string('commentRootRunId').notEmpty(),
|
|
1169
|
+
]),
|
|
1170
|
+
sort: (s) => [s.field('createdAt').asc()],
|
|
1171
|
+
limit: 100,
|
|
1172
|
+
});
|
|
1173
|
+
const pending = await runRequestsRepo(conn).findMany({
|
|
1174
|
+
filter: { purpose: 'personalRunner', status: 'pending' },
|
|
1175
|
+
limit: 100,
|
|
1176
|
+
});
|
|
1177
|
+
for (const id of new Set([
|
|
1178
|
+
...queued.map((run) => run.id),
|
|
1179
|
+
...pending
|
|
1180
|
+
.map((request) => request.commentRunId)
|
|
1181
|
+
.filter((id) => Boolean(id)),
|
|
1182
|
+
])) {
|
|
1183
|
+
// Read first: a sweep over settled chains opens no transaction at all.
|
|
1184
|
+
const plan = await planCommentConsent(conn, id);
|
|
1185
|
+
if (plan.kind === 'none')
|
|
1186
|
+
continue;
|
|
1187
|
+
await tx.run(async (unit) => {
|
|
1188
|
+
if (plan.kind === 'request' || plan.kind === 'refresh')
|
|
1189
|
+
await lockAgentForClaim(unit.conn, plan.run.agentId, clock.now().toISOString());
|
|
1190
|
+
await reconcileCommentConsent(unit, id);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
961
1194
|
diagnose: (conn, run, agent) => selectionFacts(conn, run, agent, false),
|
|
962
1195
|
async selectionWait(runId) {
|
|
963
1196
|
const conn = tx.read();
|