@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
|
@@ -9,16 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { ProtocolError, TERMINAL_RUN_STATUSES, } from '@nocobase/agent-protocol';
|
|
11
11
|
import { conflict, forbidden, invalid, notFound, precondition, } from '../../kernel/errors.js';
|
|
12
|
-
import { asJson, cleanList, jsonObject } from '../../kernel/values.js';
|
|
12
|
+
import { asJson, cleanList, jsonObject, stringArray, } from '../../kernel/values.js';
|
|
13
13
|
import { findAgent, lockAgentForClaim } from '../agents/index.js';
|
|
14
14
|
import { agentNamesOf } from '../agents/agent.store.js';
|
|
15
15
|
import { createRunRequestService, insertRunRequest, } from './run-requests.js';
|
|
16
16
|
import { authenticateRunToken } from './run-tokens.js';
|
|
17
|
-
import { ACTIVE, eventsRepo, findRunRecord, inputsOnSince, inputsRepo, isActive, isTerminal, lastSummary, openRunsOn, pendingInputs, reposRepo, runsOfKey, runsRepo, toInput, toInputView, toRepo, toRun, usageRepo, } from './run.store.js';
|
|
17
|
+
import { ACTIVE, eventsRepo, findRunRecord, inputsOnSince, inputsRepo, isActive, isTerminal, lastSummary, openRunsOn, openRunsOnEach, pendingInputs, reposRepo, runsOfKey, runsRepo, toInput, toInputView, toRepo, toRun, usageRepo, } from './run.store.js';
|
|
18
18
|
import { readWorkload } from './workload.js';
|
|
19
19
|
import { finishRun } from './transitions.js';
|
|
20
20
|
import { gateInputs, gatePending } from './input-gate.js';
|
|
21
21
|
import { findBrief, requestReset } from './workspace.store.js';
|
|
22
|
+
import { findCommentRoot, currentCommentOwner, commentConsent, commentFromOwner, } from './comment-consent.js';
|
|
22
23
|
export const DEFAULT_THREAD = 'main';
|
|
23
24
|
/**
|
|
24
25
|
* The run work went into, for a caller that names no responsible (a conversation, a consultation): such work is never
|
|
@@ -31,7 +32,9 @@ export function queuedRun(result) {
|
|
|
31
32
|
}
|
|
32
33
|
/** Who a chain of work a run belongs to started with: who confirmed it, else who it was requested by. */
|
|
33
34
|
function chainSourceOf(run) {
|
|
34
|
-
return run.
|
|
35
|
+
return run.commentRootRunId
|
|
36
|
+
? (run.requestedByUserId ?? run.actorUserId)
|
|
37
|
+
: (run.confirmedByUserId ?? run.requestedByUserId ?? run.actorUserId);
|
|
35
38
|
}
|
|
36
39
|
/** A delay as the moment a run may be claimed, or null for now (and for any moment already past). */
|
|
37
40
|
function fireAtOf(value, now) {
|
|
@@ -42,8 +45,20 @@ function fireAtOf(value, now) {
|
|
|
42
45
|
throw invalid('fireAt is not a date.');
|
|
43
46
|
return at.getTime() > now.getTime() ? at.toISOString() : null;
|
|
44
47
|
}
|
|
48
|
+
const COMMENT_CANCEL_DETAIL = 'The personal runner request was declined or switched to the commenter.';
|
|
45
49
|
export function createRunService(deps) {
|
|
46
50
|
const { tx, ids, clock } = deps;
|
|
51
|
+
/** Ends one unfinished run of a declined chain; a run that moved since it was read rolls the decision back. */
|
|
52
|
+
async function endCommentRun(unit, run, byUserId) {
|
|
53
|
+
const ended = await finishRun(unit, deps, run, {
|
|
54
|
+
status: 'cancelled',
|
|
55
|
+
reason: null,
|
|
56
|
+
detail: COMMENT_CANCEL_DETAIL,
|
|
57
|
+
cancelledById: byUserId,
|
|
58
|
+
});
|
|
59
|
+
if (!ended)
|
|
60
|
+
throw conflict('The comment work changed; read it again.');
|
|
61
|
+
}
|
|
47
62
|
const require = async (conn, id) => {
|
|
48
63
|
const run = await findRunRecord(conn, id);
|
|
49
64
|
if (!run)
|
|
@@ -104,6 +119,11 @@ export function createRunService(deps) {
|
|
|
104
119
|
subjectKind: values.subject.kind,
|
|
105
120
|
subjectId: values.subject.id,
|
|
106
121
|
threadScope: values.threadScope,
|
|
122
|
+
commentRootRunId: values.commentRootRunId === ''
|
|
123
|
+
? id
|
|
124
|
+
: (values.commentRootRunId ?? null),
|
|
125
|
+
commentExecution: values.commentExecution ?? null,
|
|
126
|
+
commentOwnerKey: values.commentOwnerKey ?? null,
|
|
107
127
|
actorUserId: values.actorUserId,
|
|
108
128
|
requestedByUserId: values.requestedByUserId,
|
|
109
129
|
confirmedByUserId: values.confirmedByUserId,
|
|
@@ -207,13 +227,93 @@ export function createRunService(deps) {
|
|
|
207
227
|
actualModels: models.get(run.id) ?? [],
|
|
208
228
|
});
|
|
209
229
|
}
|
|
230
|
+
/** Cross-root appends require both sources to retain the same owner's authorization. */
|
|
231
|
+
async function equivalentCommentAuthority(unit, run, work) {
|
|
232
|
+
if (run.ownerUserId !== work.ownerUserId ||
|
|
233
|
+
(run.commentOwnerKey ?? null) !== (work.commentOwnerKey ?? null) ||
|
|
234
|
+
(run.commentExecution ?? 'owner') !== (work.commentExecution ?? 'owner'))
|
|
235
|
+
return false;
|
|
236
|
+
if ((run.commentRootRunId ?? null) === (work.commentRootRunId ?? null)) {
|
|
237
|
+
if (!run.commentRootRunId)
|
|
238
|
+
return true;
|
|
239
|
+
const root = await findCommentRoot(unit.conn, run.commentRootRunId);
|
|
240
|
+
// Requester execution keeps its own identity; it does not borrow the owner's consent.
|
|
241
|
+
if (root.commentExecution === 'requester')
|
|
242
|
+
return (root.actorUserId === work.actorUserId &&
|
|
243
|
+
(await currentCommentOwner(unit.conn, deps.subjects, root)));
|
|
244
|
+
return await authorizedRoot(root);
|
|
245
|
+
}
|
|
246
|
+
if (work.commentExecution === 'requester' ||
|
|
247
|
+
run.commentExecution === 'requester' ||
|
|
248
|
+
!work.sourceRunId)
|
|
249
|
+
return false;
|
|
250
|
+
const source = await findRunRecord(unit.conn, work.sourceRunId);
|
|
251
|
+
if (!source ||
|
|
252
|
+
source.subjectKind !== run.subjectKind ||
|
|
253
|
+
source.subjectId !== run.subjectId ||
|
|
254
|
+
source.ownerUserId !== run.ownerUserId ||
|
|
255
|
+
source.actorUserId !== run.actorUserId)
|
|
256
|
+
return false;
|
|
257
|
+
const sourceRoot = source.commentRootRunId
|
|
258
|
+
? await findCommentRoot(unit.conn, source.commentRootRunId)
|
|
259
|
+
: source;
|
|
260
|
+
const targetRoot = run.commentRootRunId
|
|
261
|
+
? await findCommentRoot(unit.conn, run.commentRootRunId)
|
|
262
|
+
: run;
|
|
263
|
+
return ((await authorizedRoot(sourceRoot)) && (await authorizedRoot(targetRoot)));
|
|
264
|
+
async function authorizedRoot(root) {
|
|
265
|
+
if (!(await currentCommentOwner(unit.conn, deps.subjects, root)))
|
|
266
|
+
return false;
|
|
267
|
+
if (!root.commentRootRunId)
|
|
268
|
+
return chainSourceOf(root) === root.ownerUserId;
|
|
269
|
+
const { decision } = await commentConsent(unit.conn, root);
|
|
270
|
+
if (decision === 'pending' || decision === 'rejected')
|
|
271
|
+
return false;
|
|
272
|
+
return (decision === 'confirmed' || (await commentFromOwner(unit.conn, root)));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
210
275
|
/**
|
|
211
276
|
* Queues `work` for `agent`: it joins the run working on its key while that run can still be told, else the run
|
|
212
277
|
* waiting for it, else starts a new run.
|
|
213
278
|
*/
|
|
214
279
|
async function queue(unit, agent, work) {
|
|
280
|
+
if (work.commentRootRunId) {
|
|
281
|
+
// A plain read: the root's subject never changes, and the agent lock below serializes the chain's enqueues.
|
|
282
|
+
const root = await findCommentRoot(unit.conn, work.commentRootRunId);
|
|
283
|
+
if (root.subjectKind !== work.subject.kind ||
|
|
284
|
+
root.subjectId !== work.subject.id)
|
|
285
|
+
throw invalid('A comment authorization cannot cross subjects.');
|
|
286
|
+
}
|
|
215
287
|
// Serializes enqueues and claims for this agent, so a key never gets two waiting runs.
|
|
216
288
|
await lockAgentForClaim(unit.conn, agent.id, clock.now().toISOString());
|
|
289
|
+
if (work.preferActive) {
|
|
290
|
+
const commentId = jsonObject(work.input.payload).commentId;
|
|
291
|
+
if (typeof commentId === 'string') {
|
|
292
|
+
const prior = await inputsRepo(unit.conn).findOne({
|
|
293
|
+
filter: (f) => f.and([
|
|
294
|
+
f.string('type').eq('comment'),
|
|
295
|
+
f.json('payload').path(['commentId']).eq(commentId),
|
|
296
|
+
f
|
|
297
|
+
.relation('run')
|
|
298
|
+
.some((r) => r.and([
|
|
299
|
+
r.string('agentId').eq(agent.id),
|
|
300
|
+
r.string('subjectKind').eq(work.subject.kind),
|
|
301
|
+
r.string('subjectId').eq(work.subject.id),
|
|
302
|
+
])),
|
|
303
|
+
]),
|
|
304
|
+
});
|
|
305
|
+
if (prior) {
|
|
306
|
+
const run = (await findRunRecord(unit.conn, prior.runId));
|
|
307
|
+
return {
|
|
308
|
+
runId: run.id,
|
|
309
|
+
inputId: prior.id,
|
|
310
|
+
outcome: 'appended',
|
|
311
|
+
status: run.status,
|
|
312
|
+
duplicate: true,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
217
317
|
const key = {
|
|
218
318
|
agentId: agent.id,
|
|
219
319
|
subjectKind: work.subject.kind,
|
|
@@ -222,24 +322,87 @@ export function createRunService(deps) {
|
|
|
222
322
|
};
|
|
223
323
|
// Only into work done as the same person: merged into another's run, it would borrow their identity, or wait
|
|
224
324
|
// for a runner that never takes it.
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
(run.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
325
|
+
const unfiltered = await runsOfKey(unit.conn, key, ['queued', ...ACTIVE], work.actorUserId);
|
|
326
|
+
const open = unfiltered.filter((run) => (run.commentRootRunId ?? null) === (work.commentRootRunId ?? null) &&
|
|
327
|
+
(run.commentExecution ?? null) === (work.commentExecution ?? null));
|
|
328
|
+
let working;
|
|
329
|
+
for (let candidate of work.preferActive ? unfiltered : open) {
|
|
330
|
+
if (work.freshSession ||
|
|
331
|
+
candidate.cancelRequestedAt ||
|
|
332
|
+
!(candidate.status === 'dispatched' ||
|
|
333
|
+
(candidate.status === 'running' && Boolean(candidate.acceptsInput))))
|
|
334
|
+
continue;
|
|
335
|
+
if (work.preferActive) {
|
|
336
|
+
if (!candidate.runnerId ||
|
|
337
|
+
!['claude', 'codex', 'pi', 'opencode'].includes(candidate.tool ?? ''))
|
|
338
|
+
continue;
|
|
339
|
+
const runner = await deps.runners.find(unit.conn, candidate.runnerId);
|
|
340
|
+
if (!runner?.features.includes('steer'))
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
344
|
+
if (work.preferActive &&
|
|
345
|
+
!(await equivalentCommentAuthority(unit, candidate, work)))
|
|
346
|
+
break;
|
|
347
|
+
const input = {
|
|
234
348
|
id: '',
|
|
235
349
|
at: clock.now().toISOString(),
|
|
236
350
|
...work.input,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
351
|
+
...(candidate.commentRootRunId
|
|
352
|
+
? {
|
|
353
|
+
payload: {
|
|
354
|
+
...jsonObject(work.input.payload),
|
|
355
|
+
commentRootRunId: candidate.commentRootRunId,
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
: {}),
|
|
359
|
+
};
|
|
360
|
+
if ((await gateInputs(unit.conn, deps, candidate, agent, [input]))
|
|
361
|
+
.length === 0)
|
|
362
|
+
break;
|
|
363
|
+
const touched = await runsRepo(unit.conn).updateMany({
|
|
364
|
+
filter: (f) => f.and([
|
|
365
|
+
f.string('id').eq(candidate.id),
|
|
366
|
+
f.string('status').eq(candidate.status),
|
|
367
|
+
f.number('revision').eq(candidate.revision),
|
|
368
|
+
f.date('cancelRequestedAt').empty(),
|
|
369
|
+
]),
|
|
370
|
+
values: { updatedAt: clock.now().toISOString() },
|
|
371
|
+
});
|
|
372
|
+
if (touched.updatedCount === 1) {
|
|
373
|
+
working = candidate;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
const current = await findRunRecord(unit.conn, candidate.id);
|
|
377
|
+
if (!current ||
|
|
378
|
+
current.cancelRequestedAt ||
|
|
379
|
+
!(current.status === 'dispatched' ||
|
|
380
|
+
(current.status === 'running' && current.acceptsInput)))
|
|
381
|
+
break;
|
|
382
|
+
// A claim/start may change the holder or tool; re-evaluate those on a subsequent dispatch.
|
|
383
|
+
if (current.actorUserId !== work.actorUserId ||
|
|
384
|
+
current.threadScope !== work.threadScope ||
|
|
385
|
+
current.runnerId !== candidate.runnerId ||
|
|
386
|
+
current.tool !== candidate.tool)
|
|
387
|
+
break;
|
|
388
|
+
candidate = current;
|
|
389
|
+
if (attempt === 3)
|
|
390
|
+
throw conflict('The run revision changed during append; retry the dispatch.');
|
|
391
|
+
}
|
|
392
|
+
if (working)
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
241
395
|
const waiting = open.find((run) => run.status === 'queued');
|
|
242
396
|
const target = working ?? waiting;
|
|
397
|
+
const commentInput = (rootId) => rootId
|
|
398
|
+
? {
|
|
399
|
+
...work.input,
|
|
400
|
+
payload: {
|
|
401
|
+
...jsonObject(work.input.payload),
|
|
402
|
+
commentRootRunId: rootId,
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
: work.input;
|
|
243
406
|
if (!working && waiting)
|
|
244
407
|
await hurry(unit, waiting, work.priority, work.fireAt);
|
|
245
408
|
if (target) {
|
|
@@ -248,7 +411,7 @@ export function createRunService(deps) {
|
|
|
248
411
|
filter: { id: target.id, status: 'queued' },
|
|
249
412
|
values: { freshSession: true, updatedAt: clock.now().toISOString() },
|
|
250
413
|
});
|
|
251
|
-
const inputId = await insertInput(unit, target.id,
|
|
414
|
+
const inputId = await insertInput(unit, target.id, commentInput(target.commentRootRunId));
|
|
252
415
|
return {
|
|
253
416
|
runId: target.id,
|
|
254
417
|
outcome: working ? 'appended' : 'merged',
|
|
@@ -257,6 +420,9 @@ export function createRunService(deps) {
|
|
|
257
420
|
};
|
|
258
421
|
}
|
|
259
422
|
const runId = await createRun(unit, agent, {
|
|
423
|
+
commentRootRunId: work.commentRootRunId,
|
|
424
|
+
commentExecution: work.commentExecution,
|
|
425
|
+
commentOwnerKey: work.commentOwnerKey,
|
|
260
426
|
freshSession: work.freshSession,
|
|
261
427
|
subject: work.subject,
|
|
262
428
|
threadScope: work.threadScope,
|
|
@@ -271,16 +437,23 @@ export function createRunService(deps) {
|
|
|
271
437
|
...(work.parentRunId ? { parentRunId: work.parentRunId } : {}),
|
|
272
438
|
...(work.maxAttempts ? { maxAttempts: work.maxAttempts } : {}),
|
|
273
439
|
});
|
|
274
|
-
const inputId = await insertInput(unit, runId, work.
|
|
440
|
+
const inputId = await insertInput(unit, runId, commentInput(work.commentRootRunId === '' ? runId : work.commentRootRunId));
|
|
275
441
|
return { runId, outcome: 'created', status: 'queued', inputId };
|
|
276
442
|
}
|
|
277
443
|
/** The source of the chain of work a request belongs to (`EnqueueRequest.requestedByUserId`). */
|
|
278
444
|
async function sourceOf(unit, request) {
|
|
445
|
+
if (request.comment &&
|
|
446
|
+
request.input.actor.kind === 'user' &&
|
|
447
|
+
jsonObject(request.input.payload).byAgent !== true &&
|
|
448
|
+
(request.requestedByUserId ?? request.actorUserId))
|
|
449
|
+
return (request.requestedByUserId ?? request.actorUserId);
|
|
279
450
|
if (request.causedByRunId) {
|
|
280
451
|
const cause = await findRunRecord(unit.conn, request.causedByRunId);
|
|
281
452
|
if (!cause)
|
|
282
453
|
throw invalid('causedByRunId names no run.');
|
|
283
|
-
return
|
|
454
|
+
return request.comment
|
|
455
|
+
? (cause.requestedByUserId ?? cause.actorUserId)
|
|
456
|
+
: chainSourceOf(cause);
|
|
284
457
|
}
|
|
285
458
|
const source = request.requestedByUserId ??
|
|
286
459
|
request.actorUserId ??
|
|
@@ -289,13 +462,15 @@ export function createRunService(deps) {
|
|
|
289
462
|
throw invalid('Name who woke the agent: actorUserId, requestedByUserId or responsibleUserId.');
|
|
290
463
|
return source;
|
|
291
464
|
}
|
|
292
|
-
async function selectedIdentity(unit, request) {
|
|
465
|
+
async function selectedIdentity(unit, request, commentExecution) {
|
|
293
466
|
const identity = await deps.subjects
|
|
294
467
|
.get(request.subject.kind)
|
|
295
468
|
?.actsAs?.(unit.conn, {
|
|
296
469
|
subjectId: request.subject.id,
|
|
297
470
|
agentId: request.agentId,
|
|
298
471
|
causedByRunId: request.causedByRunId,
|
|
472
|
+
commentExecution,
|
|
473
|
+
actorUserId: request.actorUserId,
|
|
299
474
|
inputs: [request.input],
|
|
300
475
|
});
|
|
301
476
|
if (identity && !identity.actorUserId)
|
|
@@ -322,11 +497,185 @@ export function createRunService(deps) {
|
|
|
322
497
|
mayInvoke: (agent, userId) => deps.agents.mayInvoke(agent, userId),
|
|
323
498
|
requireInvocable,
|
|
324
499
|
requireRunnerFor,
|
|
500
|
+
prepareCommentActor: async (record, byUserId) => {
|
|
501
|
+
const mayRead = deps.subjects.get(record.subjectKind)?.commentActorMayRead;
|
|
502
|
+
if (mayRead && !(await mayRead(record.subjectId, byUserId)))
|
|
503
|
+
throw forbidden('The acting user may no longer read this issue.');
|
|
504
|
+
},
|
|
505
|
+
checkCommentRequest: async (unit, record) => {
|
|
506
|
+
const root = await findCommentRoot(unit.conn, record.commentRootRunId);
|
|
507
|
+
if (!(await currentCommentOwner(unit.conn, deps.subjects, root)) ||
|
|
508
|
+
root.commentExecution !== 'owner' ||
|
|
509
|
+
root.ownerUserId !== record.responsibleUserId ||
|
|
510
|
+
(root.commentOwnerKey ?? null) !== (record.commentOwnerKey ?? null))
|
|
511
|
+
throw precondition('RUN_REQUEST_SETTLED', 'This comment request belongs to an earlier owner assignment.');
|
|
512
|
+
},
|
|
513
|
+
releaseCommentRequest: async (unit, record, byUserId) => {
|
|
514
|
+
const run = await findRunRecord(unit.conn, record.commentRunId);
|
|
515
|
+
if (!run ||
|
|
516
|
+
run.status !== 'queued' ||
|
|
517
|
+
run.commentRootRunId !== record.commentRootRunId)
|
|
518
|
+
throw precondition('RUN_REQUEST_SETTLED', 'The requested work is no longer waiting.');
|
|
519
|
+
const inputs = await pendingInputs(unit.conn, run.id);
|
|
520
|
+
const now = clock.now().toISOString();
|
|
521
|
+
// Guarded on the requested run itself: a claim that took it first wins, and this confirmation rolls back.
|
|
522
|
+
const released = await runsRepo(unit.conn).updateMany({
|
|
523
|
+
filter: { id: run.id, status: 'queued' },
|
|
524
|
+
values: { confirmedByUserId: byUserId, updatedAt: now },
|
|
525
|
+
});
|
|
526
|
+
if (released.updatedCount !== 1)
|
|
527
|
+
throw precondition('RUN_REQUEST_SETTLED', 'The requested work is no longer waiting.');
|
|
528
|
+
await runsRepo(unit.conn).updateMany({
|
|
529
|
+
filter: {
|
|
530
|
+
commentRootRunId: record.commentRootRunId,
|
|
531
|
+
status: 'queued',
|
|
532
|
+
actorUserId: record.responsibleUserId,
|
|
533
|
+
},
|
|
534
|
+
values: { confirmedByUserId: byUserId, updatedAt: now },
|
|
535
|
+
});
|
|
536
|
+
unit.emit({ type: 'run.changed', runId: run.id, status: 'queued' });
|
|
537
|
+
return {
|
|
538
|
+
runId: run.id,
|
|
539
|
+
outcome: 'merged',
|
|
540
|
+
status: 'queued',
|
|
541
|
+
inputId: inputs[0].id,
|
|
542
|
+
};
|
|
543
|
+
},
|
|
544
|
+
cancelCommentRequest: async (unit, record, byUserId) => {
|
|
545
|
+
for (const run of await runsRepo(unit.conn).findMany({
|
|
546
|
+
filter: { commentRootRunId: record.commentRootRunId },
|
|
547
|
+
}))
|
|
548
|
+
if (!isTerminal(run.status))
|
|
549
|
+
await endCommentRun(unit, run, byUserId);
|
|
550
|
+
},
|
|
551
|
+
runCommentAsRequester: async (unit, record, byUserId) => {
|
|
552
|
+
// Queuing the replacements takes their agents' rows, as claims do before they take a run. Taking every agent of
|
|
553
|
+
// the chain first, in one order, keeps this from holding a run a claim waits on while it waits for that claim.
|
|
554
|
+
const agentIds = new Set((await runsRepo(unit.conn).findMany({
|
|
555
|
+
filter: { commentRootRunId: record.commentRootRunId },
|
|
556
|
+
})).map((run) => run.agentId));
|
|
557
|
+
for (const agentId of [...agentIds].sort())
|
|
558
|
+
await lockAgentForClaim(unit.conn, agentId, clock.now().toISOString());
|
|
559
|
+
const chain = await runsRepo(unit.conn).findMany({
|
|
560
|
+
filter: { commentRootRunId: record.commentRootRunId },
|
|
561
|
+
sort: (sort) => [sort.field('createdAt').asc(), sort.field('id').asc()],
|
|
562
|
+
});
|
|
563
|
+
const targets = [];
|
|
564
|
+
for (const old of chain) {
|
|
565
|
+
// Rejection ends the queued chain, but leaves its inputs unhandled. Do not revive independently ended work.
|
|
566
|
+
const declined = record.status === 'rejected' &&
|
|
567
|
+
old.status === 'cancelled' &&
|
|
568
|
+
old.cancelledById === record.responsibleUserId &&
|
|
569
|
+
old.failureDetail === COMMENT_CANCEL_DETAIL;
|
|
570
|
+
if (isTerminal(old.status) && !declined)
|
|
571
|
+
continue;
|
|
572
|
+
const inputs = await pendingInputs(unit.conn, old.id);
|
|
573
|
+
if (inputs.length === 0)
|
|
574
|
+
continue;
|
|
575
|
+
await selectedIdentity(unit, {
|
|
576
|
+
agentId: old.agentId,
|
|
577
|
+
subject: { kind: old.subjectKind, id: old.subjectId },
|
|
578
|
+
input: toInput(inputs[0]),
|
|
579
|
+
actorUserId: byUserId,
|
|
580
|
+
}, 'requester');
|
|
581
|
+
const agent = await requireInvocable(unit, old.agentId, byUserId);
|
|
582
|
+
const requires = stringArray(old.requires);
|
|
583
|
+
await requireRunnerFor(unit, agent, byUserId, requires);
|
|
584
|
+
targets.push({ old, inputs, agent, requires });
|
|
585
|
+
}
|
|
586
|
+
if (!targets.length)
|
|
587
|
+
throw precondition('RUN_REQUEST_SETTLED', 'No comment work is still waiting.');
|
|
588
|
+
// Validate all targets before stopping any of them. Each stop is guarded on the status read, so a run a claim took
|
|
589
|
+
// meanwhile rolls this back rather than being replaced twice; the settled request covers repeat clicks.
|
|
590
|
+
for (const old of chain)
|
|
591
|
+
if (!isTerminal(old.status))
|
|
592
|
+
await endCommentRun(unit, old, byUserId);
|
|
593
|
+
const replacements = new Map();
|
|
594
|
+
let rootId;
|
|
595
|
+
for (const { old, inputs, agent, requires } of targets) {
|
|
596
|
+
const first = inputs[0];
|
|
597
|
+
const next = await queue(unit, agent, {
|
|
598
|
+
commentRootRunId: rootId ?? '',
|
|
599
|
+
commentExecution: 'requester',
|
|
600
|
+
commentOwnerKey: record.commentOwnerKey ?? null,
|
|
601
|
+
freshSession: Boolean(old.freshSession),
|
|
602
|
+
subject: { kind: old.subjectKind, id: old.subjectId },
|
|
603
|
+
threadScope: old.threadScope,
|
|
604
|
+
actorUserId: byUserId,
|
|
605
|
+
requestedByUserId: byUserId,
|
|
606
|
+
confirmedByUserId: null,
|
|
607
|
+
ownerUserId: old.ownerUserId,
|
|
608
|
+
priority: Number(old.priority),
|
|
609
|
+
fireAt: fireAtOf(old.availableAt ?? undefined, clock.now()),
|
|
610
|
+
requires,
|
|
611
|
+
maxAttempts: Number(old.maxAttempts),
|
|
612
|
+
input: toInput(first),
|
|
613
|
+
});
|
|
614
|
+
rootId ??= next.runId;
|
|
615
|
+
replacements.set(old.id, next);
|
|
616
|
+
for (const input of inputs.slice(1))
|
|
617
|
+
await insertInput(unit, next.runId, {
|
|
618
|
+
...toInput(input),
|
|
619
|
+
payload: { ...jsonObject(input.payload), commentRootRunId: rootId },
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
// Remap causal references only for replaced work; completed ancestors remain the original historical runs.
|
|
623
|
+
for (const { old } of targets) {
|
|
624
|
+
const next = replacements.get(old.id);
|
|
625
|
+
if (old.parentRunId)
|
|
626
|
+
await runsRepo(unit.conn).updateMany({
|
|
627
|
+
filter: { id: next.runId },
|
|
628
|
+
values: {
|
|
629
|
+
parentRunId: replacements.get(old.parentRunId)?.runId ?? old.parentRunId,
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
for (const input of await pendingInputs(unit.conn, next.runId)) {
|
|
633
|
+
const payload = jsonObject(input.payload);
|
|
634
|
+
const cause = typeof payload.causedByRunId === 'string'
|
|
635
|
+
? replacements.get(payload.causedByRunId)
|
|
636
|
+
: undefined;
|
|
637
|
+
if (cause)
|
|
638
|
+
await inputsRepo(unit.conn).updateMany({
|
|
639
|
+
filter: { id: input.id },
|
|
640
|
+
values: {
|
|
641
|
+
payload: asJson({ ...payload, causedByRunId: cause.runId }),
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const rootInputs = await pendingInputs(unit.conn, rootId);
|
|
647
|
+
if (!rootInputs.some((input) => input.type === record.inputType &&
|
|
648
|
+
input.text === record.inputText &&
|
|
649
|
+
jsonObject(input.payload).commentId ===
|
|
650
|
+
jsonObject(record.inputPayload).commentId)) {
|
|
651
|
+
const sourceId = await insertInput(unit, rootId, {
|
|
652
|
+
type: record.inputType,
|
|
653
|
+
actor: {
|
|
654
|
+
kind: record.inputActorKind,
|
|
655
|
+
id: record.inputActorId,
|
|
656
|
+
name: record.inputActorName,
|
|
657
|
+
},
|
|
658
|
+
text: record.inputText,
|
|
659
|
+
payload: {
|
|
660
|
+
...jsonObject(record.inputPayload),
|
|
661
|
+
commentSourceSnapshot: true,
|
|
662
|
+
commentRootRunId: rootId,
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
await inputsRepo(unit.conn).updateMany({
|
|
666
|
+
filter: { id: sourceId },
|
|
667
|
+
values: { handledAt: clock.now().toISOString() },
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return (replacements.get(record.commentRunId) ??
|
|
671
|
+
replacements.values().next().value);
|
|
672
|
+
},
|
|
325
673
|
direct: async (unit, run) => {
|
|
326
674
|
const identity = await selectedIdentity(unit, {
|
|
327
675
|
agentId: run.agent.id,
|
|
328
676
|
subject: run.subject,
|
|
329
677
|
input: run.input,
|
|
678
|
+
actorUserId: run.actorUserId,
|
|
330
679
|
});
|
|
331
680
|
const actorUserId = identity?.actorUserId ?? run.actorUserId;
|
|
332
681
|
const agent = await requireInvocable(unit, run.agent.id, actorUserId);
|
|
@@ -339,7 +688,6 @@ export function createRunService(deps) {
|
|
|
339
688
|
confirmedByUserId: run.confirmedByUserId,
|
|
340
689
|
ownerUserId: identity ? actorUserId : run.ownerUserId,
|
|
341
690
|
priority: run.priority,
|
|
342
|
-
// As it was asked: a moment already past when it is confirmed means now.
|
|
343
691
|
fireAt: fireAtOf(run.fireAt ?? undefined, clock.now()),
|
|
344
692
|
requires: run.requires,
|
|
345
693
|
...(run.maxAttempts === null ? {} : { maxAttempts: run.maxAttempts }),
|
|
@@ -350,7 +698,21 @@ export function createRunService(deps) {
|
|
|
350
698
|
return {
|
|
351
699
|
enqueue: (request, outer) => tx.run(async (unit) => {
|
|
352
700
|
const source = await sourceOf(unit, request);
|
|
353
|
-
const
|
|
701
|
+
const cause = request.causedByRunId
|
|
702
|
+
? await findRunRecord(unit.conn, request.causedByRunId)
|
|
703
|
+
: null;
|
|
704
|
+
const inherited = !request.comment &&
|
|
705
|
+
cause?.commentRootRunId &&
|
|
706
|
+
cause.subjectKind === request.subject.kind &&
|
|
707
|
+
cause.subjectId === request.subject.id
|
|
708
|
+
? cause
|
|
709
|
+
: null;
|
|
710
|
+
const commentRootRunId = request.comment?.rootRunId ??
|
|
711
|
+
inherited?.commentRootRunId ??
|
|
712
|
+
(request.comment ? '' : null);
|
|
713
|
+
const commentExecution = inherited?.commentExecution ?? (request.comment ? 'owner' : null);
|
|
714
|
+
const commentOwnerKey = request.comment?.ownerKey ?? inherited?.commentOwnerKey ?? null;
|
|
715
|
+
const identity = await selectedIdentity(unit, request, commentExecution);
|
|
354
716
|
const responsible = identity
|
|
355
717
|
? null
|
|
356
718
|
: (request.responsibleUserId ?? null);
|
|
@@ -388,19 +750,30 @@ export function createRunService(deps) {
|
|
|
388
750
|
}
|
|
389
751
|
// As the responsible when they caused it; as the source when they run it as themselves (`mine`); as whoever
|
|
390
752
|
// woke the agent when nobody answers for the subject.
|
|
391
|
-
const actorUserId =
|
|
753
|
+
const actorUserId = (commentExecution === 'requester'
|
|
754
|
+
? inherited?.actorUserId
|
|
755
|
+
: identity?.actorUserId) ??
|
|
392
756
|
(foreign ? source : (responsible ?? request.actorUserId ?? source));
|
|
393
757
|
const agent = await requireInvocable(unit, request.agentId, actorUserId);
|
|
394
758
|
if (foreign)
|
|
395
759
|
await requireRunnerFor(unit, agent, actorUserId, request.requires ?? []);
|
|
396
|
-
|
|
760
|
+
const result = await queue(unit, agent, {
|
|
761
|
+
preferActive: request.preferActive,
|
|
762
|
+
sourceRunId: request.causedByRunId,
|
|
763
|
+
commentRootRunId,
|
|
764
|
+
commentExecution,
|
|
765
|
+
commentOwnerKey,
|
|
397
766
|
freshSession: request.freshSession,
|
|
398
767
|
subject: request.subject,
|
|
399
768
|
threadScope,
|
|
400
769
|
actorUserId,
|
|
401
770
|
requestedByUserId: source,
|
|
402
|
-
confirmedByUserId: null,
|
|
403
|
-
ownerUserId:
|
|
771
|
+
confirmedByUserId: inherited?.confirmedByUserId ?? null,
|
|
772
|
+
ownerUserId: commentExecution === 'requester'
|
|
773
|
+
? (inherited?.ownerUserId ?? request.ownerUserId ?? null)
|
|
774
|
+
: identity
|
|
775
|
+
? actorUserId
|
|
776
|
+
: (request.ownerUserId ?? null),
|
|
404
777
|
priority: request.priority ?? 0,
|
|
405
778
|
fireAt: fireAtOf(request.fireAt, clock.now()),
|
|
406
779
|
requires: request.requires ?? [],
|
|
@@ -408,6 +781,9 @@ export function createRunService(deps) {
|
|
|
408
781
|
...(request.maxAttempts ? { maxAttempts: request.maxAttempts } : {}),
|
|
409
782
|
input: request.input,
|
|
410
783
|
});
|
|
784
|
+
if (commentRootRunId !== null)
|
|
785
|
+
await deps.reconcileCommentConsent?.(unit, result.runId);
|
|
786
|
+
return result;
|
|
411
787
|
}, outer),
|
|
412
788
|
requests,
|
|
413
789
|
identityInputs: async (conn, runId) => {
|
|
@@ -419,8 +795,31 @@ export function createRunService(deps) {
|
|
|
419
795
|
},
|
|
420
796
|
addInput: (runId, input, outer) => tx.run(async (unit) => {
|
|
421
797
|
const run = await require(unit.conn, runId);
|
|
798
|
+
if (run.commentRootRunId &&
|
|
799
|
+
input.type === 'comment' &&
|
|
800
|
+
jsonObject(input.payload).commentRootRunId !== run.commentRootRunId)
|
|
801
|
+
throw forbidden('A comment must be queued in its own authorized run chain.');
|
|
422
802
|
if (isTerminal(run.status))
|
|
423
803
|
throw new ProtocolError('RUN_NOT_ACTIVE', `The run is ${run.status}.`);
|
|
804
|
+
if (isActive(run.status)) {
|
|
805
|
+
let current = run;
|
|
806
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
807
|
+
const updated = await runsRepo(unit.conn).updateMany({
|
|
808
|
+
filter: {
|
|
809
|
+
id: current.id,
|
|
810
|
+
status: current.status,
|
|
811
|
+
revision: current.revision,
|
|
812
|
+
},
|
|
813
|
+
values: { updatedAt: clock.now().toISOString() },
|
|
814
|
+
});
|
|
815
|
+
if (updated.updatedCount === 1)
|
|
816
|
+
return insertInput(unit, current.id, input);
|
|
817
|
+
current = await require(unit.conn, run.id);
|
|
818
|
+
if (!isActive(current.status))
|
|
819
|
+
throw conflict('The run moved on before input was appended.');
|
|
820
|
+
}
|
|
821
|
+
throw conflict('The run revision changed during append; retry the input.');
|
|
822
|
+
}
|
|
424
823
|
return insertInput(unit, run.id, input);
|
|
425
824
|
}, outer),
|
|
426
825
|
cancel: (runId, byUserId, outer) => tx.run(async (unit) => {
|
|
@@ -514,6 +913,24 @@ export function createRunService(deps) {
|
|
|
514
913
|
const previousInputs = await inputsRepo(unit.conn).findMany({
|
|
515
914
|
filter: { runId: run.id },
|
|
516
915
|
});
|
|
916
|
+
if (!run.commentRootRunId &&
|
|
917
|
+
deps.subjects.get(run.subjectKind)?.commentOwnerKey &&
|
|
918
|
+
previousInputs.some((input) => input.type === 'comment' &&
|
|
919
|
+
!(input.actorKind === 'user' &&
|
|
920
|
+
input.actorId === run.ownerUserId &&
|
|
921
|
+
jsonObject(input.payload).byAgent !== true)))
|
|
922
|
+
throw precondition('RUN_NOT_RETRYABLE', 'Post the comment again to start a new authorized comment chain.');
|
|
923
|
+
const oldRoot = run.commentRootRunId
|
|
924
|
+
? await findRunRecord(unit.conn, run.commentRootRunId)
|
|
925
|
+
: null;
|
|
926
|
+
const renewComment = oldRoot
|
|
927
|
+
? !(await currentCommentOwner(unit.conn, deps.subjects, oldRoot))
|
|
928
|
+
: false;
|
|
929
|
+
const commentExecution = renewComment &&
|
|
930
|
+
run.commentExecution === 'requester' &&
|
|
931
|
+
byUserId !== run.actorUserId
|
|
932
|
+
? 'owner'
|
|
933
|
+
: run.commentExecution;
|
|
517
934
|
const identity = await deps.subjects
|
|
518
935
|
.get(run.subjectKind)
|
|
519
936
|
?.actsAs?.(unit.conn, {
|
|
@@ -521,34 +938,87 @@ export function createRunService(deps) {
|
|
|
521
938
|
agentId: run.agentId,
|
|
522
939
|
inputs: previousInputs.map(toInput),
|
|
523
940
|
causedByRunId: run.retryOfRunId ?? undefined,
|
|
941
|
+
commentExecution,
|
|
942
|
+
actorUserId: run.actorUserId,
|
|
524
943
|
});
|
|
525
944
|
if (identity && !identity.actorUserId)
|
|
526
945
|
throw forbidden(identity.reason ?? 'noOwner');
|
|
527
|
-
const actorUserId =
|
|
946
|
+
const actorUserId = commentExecution === 'requester'
|
|
947
|
+
? run.actorUserId
|
|
948
|
+
: (identity?.actorUserId ?? byUserId);
|
|
528
949
|
const agent = await requireInvocable(unit, run.agentId, actorUserId);
|
|
529
950
|
const open = await runsOfKey(unit.conn, run, ['queued', ...ACTIVE], actorUserId);
|
|
530
|
-
|
|
951
|
+
const sameChain = run.commentRootRunId
|
|
952
|
+
? open.filter((candidate) => candidate.commentRootRunId === run.commentRootRunId)
|
|
953
|
+
: open;
|
|
954
|
+
if (sameChain.length > 0)
|
|
531
955
|
throw precondition('AGENT_BUSY', 'The agent is already working on this.', {
|
|
532
|
-
runId:
|
|
956
|
+
runId: sameChain[0].id,
|
|
533
957
|
});
|
|
534
958
|
const newId = await createRun(unit, agent, {
|
|
535
959
|
subject: { kind: run.subjectKind, id: run.subjectId },
|
|
536
960
|
threadScope: run.threadScope,
|
|
537
961
|
actorUserId,
|
|
538
|
-
|
|
962
|
+
commentRootRunId: renewComment ? '' : run.commentRootRunId,
|
|
963
|
+
commentExecution,
|
|
964
|
+
commentOwnerKey: renewComment
|
|
965
|
+
? ((await deps.subjects
|
|
966
|
+
.get(run.subjectKind)
|
|
967
|
+
?.commentOwnerKey?.(unit.conn, run.subjectId)) ?? null)
|
|
968
|
+
: run.commentOwnerKey,
|
|
969
|
+
requestedByUserId: run.commentRootRunId
|
|
970
|
+
? (run.requestedByUserId ?? run.actorUserId)
|
|
971
|
+
: byUserId,
|
|
539
972
|
confirmedByUserId: null,
|
|
540
|
-
ownerUserId:
|
|
973
|
+
ownerUserId: run.commentRootRunId
|
|
974
|
+
? ((await deps.subjects
|
|
975
|
+
.get(run.subjectKind)
|
|
976
|
+
?.responsibleUserId?.(unit.conn, run.subjectId)) ?? null)
|
|
977
|
+
: identity
|
|
978
|
+
? actorUserId
|
|
979
|
+
: run.ownerUserId,
|
|
541
980
|
priority: Number(run.priority),
|
|
542
981
|
requires: toRun(run).requires,
|
|
543
982
|
retryOfRunId: run.id,
|
|
544
983
|
freshSession: run.freshSession,
|
|
545
984
|
});
|
|
985
|
+
if (renewComment && oldRoot) {
|
|
986
|
+
const origins = await inputsRepo(unit.conn).findMany({
|
|
987
|
+
filter: { runId: oldRoot.id },
|
|
988
|
+
sort: (s) => [s.field('createdAt').asc(), s.field('id').asc()],
|
|
989
|
+
});
|
|
990
|
+
const origin = origins.find((input) => input.type === 'comment' &&
|
|
991
|
+
jsonObject(input.payload).commentSourceSnapshot === true) ?? origins.find((input) => input.type === 'comment');
|
|
992
|
+
if (origin) {
|
|
993
|
+
const snapshotId = await insertInput(unit, newId, {
|
|
994
|
+
type: 'comment',
|
|
995
|
+
actor: toInput(origin).actor,
|
|
996
|
+
text: origin.text,
|
|
997
|
+
payload: {
|
|
998
|
+
...jsonObject(origin.payload),
|
|
999
|
+
commentRootRunId: newId,
|
|
1000
|
+
commentSourceSnapshot: true,
|
|
1001
|
+
},
|
|
1002
|
+
});
|
|
1003
|
+
await inputsRepo(unit.conn).updateMany({
|
|
1004
|
+
filter: { id: snapshotId },
|
|
1005
|
+
values: { handledAt: clock.now().toISOString() },
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
546
1009
|
for (const input of await pendingInputs(unit.conn, run.id))
|
|
547
1010
|
await insertInput(unit, newId, {
|
|
548
1011
|
type: input.type,
|
|
549
1012
|
actor: toInput(input).actor,
|
|
550
1013
|
text: input.text,
|
|
551
|
-
...(input.payload === null
|
|
1014
|
+
...(input.payload === null
|
|
1015
|
+
? {}
|
|
1016
|
+
: {
|
|
1017
|
+
payload: {
|
|
1018
|
+
...jsonObject(input.payload),
|
|
1019
|
+
...(renewComment ? { commentRootRunId: newId } : {}),
|
|
1020
|
+
},
|
|
1021
|
+
}),
|
|
552
1022
|
});
|
|
553
1023
|
await insertInput(unit, newId, {
|
|
554
1024
|
type: 'retry',
|
|
@@ -571,6 +1041,8 @@ export function createRunService(deps) {
|
|
|
571
1041
|
retryOfRunId: run.id,
|
|
572
1042
|
previousStatus: run.status,
|
|
573
1043
|
});
|
|
1044
|
+
if (run.commentRootRunId)
|
|
1045
|
+
await deps.reconcileCommentConsent?.(unit, newId);
|
|
574
1046
|
return toRun(await require(unit.conn, newId));
|
|
575
1047
|
});
|
|
576
1048
|
},
|
|
@@ -621,6 +1093,9 @@ export function createRunService(deps) {
|
|
|
621
1093
|
return {
|
|
622
1094
|
...(await readRuns([run]))[0],
|
|
623
1095
|
actorName: (await deps.people.names(conn, [run.actorUserId])).get(run.actorUserId) ?? run.actorUserId,
|
|
1096
|
+
confirmedByName: run.confirmedByUserId
|
|
1097
|
+
? ((await deps.people.names(conn, [run.confirmedByUserId])).get(run.confirmedByUserId) ?? run.confirmedByUserId)
|
|
1098
|
+
: undefined,
|
|
624
1099
|
requestedByName: (await deps.people.names(conn, [
|
|
625
1100
|
run.requestedByUserId ?? run.actorUserId,
|
|
626
1101
|
])).get(run.requestedByUserId ?? run.actorUserId) ??
|
|
@@ -760,7 +1235,16 @@ export function createRunService(deps) {
|
|
|
760
1235
|
lastSeq: events.length > 0 ? events[events.length - 1].seq : afterSeq,
|
|
761
1236
|
};
|
|
762
1237
|
},
|
|
763
|
-
authenticateToken: (value) =>
|
|
1238
|
+
authenticateToken: async (value) => {
|
|
1239
|
+
const conn = tx.read();
|
|
1240
|
+
const identity = await authenticateRunToken(conn, clock, value);
|
|
1241
|
+
if (identity.run.commentRootRunId) {
|
|
1242
|
+
const root = await findRunRecord(conn, identity.run.commentRootRunId);
|
|
1243
|
+
if (!root || !(await currentCommentOwner(conn, deps.subjects, root)))
|
|
1244
|
+
throw precondition('RUN_TOKEN_INVALID', 'The comment authorization belongs to an earlier owner assignment.');
|
|
1245
|
+
}
|
|
1246
|
+
return identity;
|
|
1247
|
+
},
|
|
764
1248
|
async self({ run }) {
|
|
765
1249
|
const conn = tx.read();
|
|
766
1250
|
const agent = await findAgent(conn, run.agentId);
|
|
@@ -805,6 +1289,7 @@ export function createRunService(deps) {
|
|
|
805
1289
|
return assembly.data;
|
|
806
1290
|
},
|
|
807
1291
|
openOn: openRunsOn,
|
|
1292
|
+
openOnEach: openRunsOnEach,
|
|
808
1293
|
pendingInputs: async (conn, runId) => (await pendingInputs(conn, runId)).map(toInput),
|
|
809
1294
|
inputsSince: inputsOnSince,
|
|
810
1295
|
lastSummary,
|