@desplega.ai/agent-swarm 2.5.3 → 2.5.4
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/dist/{acp-adapter-gr2fcyyj.js → acp-adapter-ek38cwsv.js} +4 -4
- package/dist/{actions-wv766ghf.js → actions-92mepq03.js} +17 -17
- package/dist/{amp-adapter-speb906t.js → amp-adapter-9gaxg015.js} +2 -2
- package/dist/{app-ag29n14f.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-zyr5kz47.js} +19 -19
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-f0afqnne.js} +4 -4
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-bqzwjd9x.js} +1 -1
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-vwn81jbs.js → cli-19202kew.js} +3 -3
- package/dist/{cli-svrj71pg.js → cli-1r5ad7j5.js} +7 -7
- package/dist/{cli-hhxjnjzs.js → cli-25n3mv5d.js} +8 -6
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-hcpj2xqz.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-35gtk1p6.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-40jsnrjm.js → cli-5bnsc0e1.js} +7 -5
- package/dist/{cli-fzcn3d87.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-s49hpn37.js → cli-64mh9mf3.js} +19 -5
- package/dist/{cli-e8jyq7d8.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-p9rafdrb.js → cli-7p4jxgtt.js} +4 -4
- package/dist/{cli-w1zwctj2.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-ce0ak9ms.js → cli-abs8ndtr.js} +11 -11
- package/dist/{cli-aecdcr77.js → cli-asq5ydm4.js} +123 -50
- package/dist/{cli-n45mr7zh.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-210xd1f4.js → cli-c61mbdds.js} +3 -3
- package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
- package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
- package/dist/{cli-s53vp709.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-3ngdk5te.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-99xgw26r.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-jjznk3f7.js → cli-gbfxp48k.js} +62 -28
- package/dist/{cli-z21whrrg.js → cli-gn5af0x5.js} +14 -14
- package/dist/{cli-1fc85mvt.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-avdvgcay.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-kxawjrxg.js → cli-jb30dzvk.js} +332 -36
- package/dist/{cli-25j01dpj.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-qzme5949.js → cli-k4tjsdx9.js} +44 -39
- package/dist/{cli-8s5wx3nz.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-502eqv93.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-fsdprwrg.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-egjggehm.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-qgf8emrn.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-p0rzjkya.js → cli-wx2nj19m.js} +29 -10
- package/dist/{cli-e2ar4bp1.js → cli-x0f2v69c.js} +111 -42
- package/dist/{cli-y561h5a9.js → cli-xtrz6bzs.js} +5 -5
- package/dist/{cli-gpsz7g88.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-wch12q99.js} +2 -2
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-t04rt2pc.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-ja0sd95p.js} +27 -1
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-xp05dj6x.js} +16 -16
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-a4m7haw0.js → db-47vj40dy.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-7b8e4z2j.js} +15 -15
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-2ezw3xea.js} +1 -2
- package/dist/{external-channel-j1e2p34p.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-ys7gaqee.js} +5 -5
- package/dist/{handlers-19xr6dnz.js → handlers-wy1dr6ja.js} +18 -18
- package/dist/{heartbeat-vtesda5d.js → heartbeat-w3mntghj.js} +17 -17
- package/dist/{hook-8wqaszda.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-b2hhj6rd.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-mxcqhhcs.js → http-tkp7d4kb.js} +267 -75
- package/dist/{index-srg522m6.js → index-1ef1myq2.js} +15 -15
- package/dist/{index-1r8wfpzs.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-kw2mmat7.js → index-nmm79qgr.js} +17 -17
- package/dist/{index-n1vbvta9.js → index-q7m26kvs.js} +24 -22
- package/dist/{index-spwxjgay.js → index-xdaahthb.js} +15 -15
- package/dist/{keepalive-dsd8rtgx.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-zyerk1x2.js → lead-sawnyqxk.js} +33 -33
- package/dist/{maintenance-jqmqwnet.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-w4yd7s8t.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-jmpst4q4.js} +3 -3
- package/dist/{otel-impl-f36byzbd.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-hgytz8nn.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-2dvq348g.js → resume-frdtgfa0.js} +8 -6
- package/dist/{session-access-7f21ekq9.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-epffxtbk.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-xz1a5rqk.js → work-objects-kfahhjsq.js} +18 -18
- package/dist/{worker-42t8emjt.js → worker-v5e5rxj3.js} +33 -33
- package/openapi.json +68 -3
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/db/approvals.ts +48 -0
- package/src/be/db/tasks/write.ts +15 -5
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +318 -7
- package/src/be/db.ts +67 -17
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +2 -0
- package/src/be/users.ts +9 -0
- package/src/commands/controller.ts +33 -0
- package/src/commands/runner.ts +6 -2
- package/src/extensions/apply-task-create.ts +57 -11
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +25 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/config.ts +48 -16
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/workflows.ts +129 -24
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +29 -4
- package/src/providers/claude-managed-adapter.ts +26 -0
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +12 -0
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +275 -0
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/extensions-pre-task.test.ts +273 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +3 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/sessions-tree.test.ts +37 -0
- package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
package/src/be/db.ts
CHANGED
|
@@ -222,7 +222,14 @@ import {
|
|
|
222
222
|
rowToAgentTaskSummary,
|
|
223
223
|
} from "./db/tasks/read";
|
|
224
224
|
import { configureTaskWriteDependencies, failTask } from "./db/tasks/write";
|
|
225
|
-
import {
|
|
225
|
+
import { tombstoneWorkflowIdempotencyKeys } from "./db/workflow-idempotency";
|
|
226
|
+
import {
|
|
227
|
+
emitChildRunFinished,
|
|
228
|
+
getRunLineageBlock,
|
|
229
|
+
getStepExecutionBlock,
|
|
230
|
+
WorkflowStepNotExecutableError,
|
|
231
|
+
wakeParentsOfDeletedRuns,
|
|
232
|
+
} from "./db/workflow-runs";
|
|
226
233
|
import { scrubJsonValue } from "./scrub-json";
|
|
227
234
|
import { openSealedJson, sealedJsonForDisplay, sealJson } from "./sealed-json";
|
|
228
235
|
import { configSecretName, registerStoredSecret } from "./secret-registry";
|
|
@@ -2855,6 +2862,14 @@ export async function createTaskExtended(
|
|
|
2855
2862
|
const claim = await getDbClient().transaction<
|
|
2856
2863
|
{ existing: ExistingTrackerContextWork } | { row: AgentTaskRow }
|
|
2857
2864
|
>(async () => {
|
|
2865
|
+
// A workflow step's task commits only while the step may still start
|
|
2866
|
+
// work. Checked in the INSERT's transaction: a cascade cancel of the
|
|
2867
|
+
// step's run tree either commits first and is seen here, or sees and
|
|
2868
|
+
// cancels this task.
|
|
2869
|
+
if (options?.workflowRunStepId) {
|
|
2870
|
+
const blocked = await getStepExecutionBlock(options.workflowRunStepId);
|
|
2871
|
+
if (blocked) throw new WorkflowStepNotExecutableError(options.workflowRunStepId, blocked);
|
|
2872
|
+
}
|
|
2858
2873
|
const existingTrackerWork = options?.bypassTrackerContextDedup
|
|
2859
2874
|
? null
|
|
2860
2875
|
: await findExistingLinearTrackerContextWork(options?.contextKey);
|
|
@@ -4739,7 +4754,7 @@ export async function createSessionLogs(logs: {
|
|
|
4739
4754
|
for (let start = 0; start < logs.lines.length; start += BATCH_SIZE) {
|
|
4740
4755
|
// Yield to the event loop between batches.
|
|
4741
4756
|
if (start > 0) await new Promise<void>((r) => setTimeout(r, 5));
|
|
4742
|
-
const batch:
|
|
4757
|
+
const batch: string[] = [];
|
|
4743
4758
|
const end = Math.min(start + BATCH_SIZE, logs.lines.length);
|
|
4744
4759
|
for (let i = start; i < end; i++) {
|
|
4745
4760
|
const line = logs.lines[i];
|
|
@@ -4748,10 +4763,22 @@ export async function createSessionLogs(logs: {
|
|
|
4748
4763
|
// content that is already scrubbed at the adapter emit site. We scrub
|
|
4749
4764
|
// again here so any future write path that bypasses the adapter still
|
|
4750
4765
|
// lands clean text in the persistent session_logs table.
|
|
4751
|
-
batch.push(
|
|
4766
|
+
batch.push(scrubSecrets(line));
|
|
4752
4767
|
}
|
|
4753
4768
|
await getDbClient().transaction(async (tx) => {
|
|
4754
|
-
|
|
4769
|
+
// The worker flushes one (sessionId, iteration) in many POSTs, so the
|
|
4770
|
+
// lineNumber continues from the newest stored row instead of restarting
|
|
4771
|
+
// at the POST's index. Reading the newest row by rowid walks the
|
|
4772
|
+
// sessionId index backwards and stops at the first hit, where MAX()
|
|
4773
|
+
// would scan the whole session. Rows written here are monotonic in
|
|
4774
|
+
// rowid, so this equals MAX + 1 for them.
|
|
4775
|
+
const last = await tx.get<{ lineNumber: number }>(
|
|
4776
|
+
`SELECT lineNumber FROM session_logs WHERE sessionId = ? AND iteration = ?
|
|
4777
|
+
ORDER BY rowid DESC LIMIT 1`,
|
|
4778
|
+
[logs.sessionId, logs.iteration],
|
|
4779
|
+
);
|
|
4780
|
+
let lineNumber = last ? last.lineNumber + 1 : 0;
|
|
4781
|
+
for (const content of batch) {
|
|
4755
4782
|
await tx.run(
|
|
4756
4783
|
`INSERT INTO session_logs (id, taskId, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
4757
4784
|
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,
|
|
@@ -4762,7 +4789,7 @@ export async function createSessionLogs(logs: {
|
|
|
4762
4789
|
logs.iteration,
|
|
4763
4790
|
logs.cli,
|
|
4764
4791
|
content,
|
|
4765
|
-
lineNumber
|
|
4792
|
+
lineNumber++,
|
|
4766
4793
|
],
|
|
4767
4794
|
);
|
|
4768
4795
|
}
|
|
@@ -4770,6 +4797,10 @@ export async function createSessionLogs(logs: {
|
|
|
4770
4797
|
}
|
|
4771
4798
|
}
|
|
4772
4799
|
|
|
4800
|
+
// Session log reads order by rowid, which is insert order. Rows written before
|
|
4801
|
+
// lineNumber was assigned server-side restart at 0 on every worker flush, so
|
|
4802
|
+
// only rowid orders them. The API never runs VACUUM (runbooks/db-retention.md);
|
|
4803
|
+
// a manual VACUUM may renumber rowids but copies rows in rowid order.
|
|
4773
4804
|
export async function getSessionLogsByTaskId(
|
|
4774
4805
|
taskId: string,
|
|
4775
4806
|
limit?: number,
|
|
@@ -4777,16 +4808,16 @@ export async function getSessionLogsByTaskId(
|
|
|
4777
4808
|
if (typeof limit === "number" && limit > 0) {
|
|
4778
4809
|
const rows = await getDbClient().query<SessionLogRow>(
|
|
4779
4810
|
`SELECT * FROM (
|
|
4780
|
-
SELECT * FROM session_logs WHERE taskId = ?
|
|
4781
|
-
ORDER BY
|
|
4811
|
+
SELECT rowid AS seq, * FROM session_logs WHERE taskId = ?
|
|
4812
|
+
ORDER BY rowid DESC
|
|
4782
4813
|
LIMIT ?
|
|
4783
|
-
) ORDER BY
|
|
4814
|
+
) ORDER BY seq ASC`,
|
|
4784
4815
|
[taskId, limit],
|
|
4785
4816
|
);
|
|
4786
4817
|
return rows.map(rowToSessionLog);
|
|
4787
4818
|
}
|
|
4788
4819
|
const rows = await getDbClient().query<SessionLogRow>(
|
|
4789
|
-
"SELECT * FROM session_logs WHERE taskId = ? ORDER BY
|
|
4820
|
+
"SELECT * FROM session_logs WHERE taskId = ? ORDER BY rowid ASC",
|
|
4790
4821
|
[taskId],
|
|
4791
4822
|
);
|
|
4792
4823
|
return rows.map(rowToSessionLog);
|
|
@@ -4797,7 +4828,7 @@ export async function getSessionLogsBySession(
|
|
|
4797
4828
|
iteration: number,
|
|
4798
4829
|
): Promise<SessionLog[]> {
|
|
4799
4830
|
const rows = await getDbClient().query<SessionLogRow>(
|
|
4800
|
-
"SELECT * FROM session_logs WHERE sessionId = ? AND iteration = ? ORDER BY
|
|
4831
|
+
"SELECT * FROM session_logs WHERE sessionId = ? AND iteration = ? ORDER BY rowid ASC",
|
|
4801
4832
|
[sessionId, iteration],
|
|
4802
4833
|
);
|
|
4803
4834
|
return rows.map(rowToSessionLog);
|
|
@@ -7790,7 +7821,9 @@ async function deleteWorkflowRows(id: string, source?: "api" | "mcp"): Promise<b
|
|
|
7790
7821
|
await reclassifyTaskHumanFree(linkedTaskIds);
|
|
7791
7822
|
// 4. Delete every user's favorite of it (no FK to the workflow)
|
|
7792
7823
|
await client.run("DELETE FROM user_favorites WHERE itemType = 'workflow' AND itemId = ?", [id]);
|
|
7793
|
-
// 5.
|
|
7824
|
+
// 5. Keep Idempotency-Keys that created or triggered it taken, without the stored response
|
|
7825
|
+
await tombstoneWorkflowIdempotencyKeys(id);
|
|
7826
|
+
// 6. Delete workflow
|
|
7794
7827
|
const result = await client.run("DELETE FROM workflows WHERE id = ?", [id]);
|
|
7795
7828
|
const deleted = result.changes > 0;
|
|
7796
7829
|
if (deleted) {
|
|
@@ -9787,15 +9820,29 @@ export async function createApprovalRequest(data: {
|
|
|
9787
9820
|
const row = await getDbClient().transaction(async () => {
|
|
9788
9821
|
let status: ApprovalRequest["status"] = "pending";
|
|
9789
9822
|
let resolutionReason: string | null = null;
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9823
|
+
// A request linked to an existing run, with or without a step, is born
|
|
9824
|
+
// cancelled unless the run is live and its tree is not cancelled: a
|
|
9825
|
+
// pending request in a cancelled tree would be open work no cancel may
|
|
9826
|
+
// leave behind. A missing run is refused only on the workflow (HITL) path.
|
|
9827
|
+
const actionable = data.requireActionableWorkflow && data.workflowRunStepId;
|
|
9828
|
+
const run = data.workflowRunId ? await getWorkflowRun(data.workflowRunId, "display") : null;
|
|
9829
|
+
if (data.workflowRunId && (run || actionable)) {
|
|
9830
|
+
const step = actionable ? await getWorkflowRunStep(data.workflowRunStepId!) : undefined;
|
|
9793
9831
|
if (!run || (run.status !== "running" && run.status !== "waiting")) {
|
|
9794
9832
|
status = "cancelled";
|
|
9795
9833
|
resolutionReason = run?.error ?? `Workflow run is ${run?.status ?? "missing"}`;
|
|
9796
|
-
} else if (
|
|
9834
|
+
} else if (
|
|
9835
|
+
step !== undefined &&
|
|
9836
|
+
(!step || step.runId !== run.id || step.status !== "running")
|
|
9837
|
+
) {
|
|
9797
9838
|
status = "cancelled";
|
|
9798
9839
|
resolutionReason = `Human-in-the-loop step is ${step?.status ?? "missing"}`;
|
|
9840
|
+
} else {
|
|
9841
|
+
const blocked = await getRunLineageBlock(run.id);
|
|
9842
|
+
if (blocked) {
|
|
9843
|
+
status = "cancelled";
|
|
9844
|
+
resolutionReason = blocked;
|
|
9845
|
+
}
|
|
9799
9846
|
}
|
|
9800
9847
|
}
|
|
9801
9848
|
|
|
@@ -12892,7 +12939,8 @@ export async function listTaskTemplates(opts?: {
|
|
|
12892
12939
|
/**
|
|
12893
12940
|
* Walk the chain rooted at `rootTaskId`, ordered by `createdAt` (root first; `rowid` breaks
|
|
12894
12941
|
* ties as idx_agent_tasks_created did). `CROSS JOIN` keeps the chain as the outer loop: with
|
|
12895
|
-
* a plain JOIN, SQLite scanned all of agent_tasks to skip the sort.
|
|
12942
|
+
* a plain JOIN, SQLite scanned all of agent_tasks to skip the sort. Each task carries its
|
|
12943
|
+
* `totalCostUsd`, so the session header can total the whole chain, not only the root.
|
|
12896
12944
|
*/
|
|
12897
12945
|
export async function getRootTaskChain(rootTaskId: string): Promise<AgentTask[]> {
|
|
12898
12946
|
const rows = await getDbClient().query<AgentTaskRow>(
|
|
@@ -12902,7 +12950,9 @@ export async function getRootTaskChain(rootTaskId: string): Promise<AgentTask[]>
|
|
|
12902
12950
|
SELECT t.id FROM agent_tasks t
|
|
12903
12951
|
JOIN chain c ON t.parentTaskId = c.id
|
|
12904
12952
|
)
|
|
12905
|
-
SELECT t
|
|
12953
|
+
SELECT t.*,
|
|
12954
|
+
(SELECT SUM(totalCostUsd) FROM session_costs WHERE session_costs.taskId = t.id) AS totalCostUsd
|
|
12955
|
+
FROM chain
|
|
12906
12956
|
CROSS JOIN agent_tasks t ON t.id = chain.id
|
|
12907
12957
|
ORDER BY t.createdAt, t.rowid`,
|
|
12908
12958
|
[rootTaskId],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- Idempotency-Key records for POST /api/workflows and POST /api/workflows/{id}/trigger.
|
|
2
|
+
--
|
|
3
|
+
-- One row is one operation the server accepted under a client key. It commits
|
|
4
|
+
-- in the same transaction as the workflow (create) or the run and its initial
|
|
5
|
+
-- state (trigger), so a retry after a lost response, a restart or a concurrent
|
|
6
|
+
-- duplicate finds it and gets the original result instead of a second
|
|
7
|
+
-- workflow or run.
|
|
8
|
+
--
|
|
9
|
+
-- The scope is (principal, operation, targetId, keyHash): the caller's stable
|
|
10
|
+
-- identity (never the bearer value), the operation, the triggered workflow id
|
|
11
|
+
-- ('' for create) and a SHA-256 of the client key. requestFingerprint is a
|
|
12
|
+
-- SHA-256 of the canonical request; a different request under the same key is
|
|
13
|
+
-- a conflict. responseBody is the original response, sealed (sealJson). When
|
|
14
|
+
-- the workflow is deleted, responseBody is cleared and resourceDeletedAt set:
|
|
15
|
+
-- the key stays taken and is never accepted again.
|
|
16
|
+
--
|
|
17
|
+
-- No FK to workflows: a row outlives the workflow as that tombstone.
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS workflow_idempotency_keys (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
principal TEXT NOT NULL,
|
|
22
|
+
operation TEXT NOT NULL CHECK (operation IN ('workflow.create', 'workflow.trigger')),
|
|
23
|
+
targetId TEXT NOT NULL DEFAULT '',
|
|
24
|
+
keyHash TEXT NOT NULL,
|
|
25
|
+
requestFingerprint TEXT NOT NULL,
|
|
26
|
+
workflowId TEXT NOT NULL,
|
|
27
|
+
runId TEXT,
|
|
28
|
+
responseStatus INTEGER NOT NULL,
|
|
29
|
+
responseBody TEXT,
|
|
30
|
+
resourceDeletedAt TEXT,
|
|
31
|
+
createdAt TEXT NOT NULL,
|
|
32
|
+
created_by TEXT,
|
|
33
|
+
updated_by TEXT
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_idempotency_keys_scope
|
|
37
|
+
ON workflow_idempotency_keys(principal, operation, targetId, keyHash);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_idempotency_keys_workflowId
|
|
40
|
+
ON workflow_idempotency_keys(workflowId);
|
package/src/be/sealed-json.ts
CHANGED
|
@@ -30,6 +30,7 @@ export const SEALED_REPLAY_COLUMNS: readonly { table: string; column: string }[]
|
|
|
30
30
|
{ table: "workflow_run_steps", column: "output_replay" },
|
|
31
31
|
{ table: "workflow_runs", column: "context_replay" },
|
|
32
32
|
{ table: "approval_requests", column: "responses" },
|
|
33
|
+
{ table: "workflow_idempotency_keys", column: "responseBody" },
|
|
33
34
|
];
|
|
34
35
|
|
|
35
36
|
/** SQL `EXISTS` arms matching a sealed value in each `SEALED_REPLAY_COLUMNS` entry. */
|
|
@@ -574,14 +574,14 @@ export default async function compoundInsights(args: any, ctx: any) {
|
|
|
574
574
|
const rows = rowsToObjects(
|
|
575
575
|
await ctx.swarm.db_query({
|
|
576
576
|
sql: `WITH raw AS (
|
|
577
|
-
SELECT sessionId, iteration, lineNumber, content,
|
|
577
|
+
SELECT rowid AS seq, sessionId, iteration, lineNumber, content,
|
|
578
578
|
json_extract(content, '$.tool_name') as jsonToolName
|
|
579
579
|
FROM session_logs
|
|
580
580
|
WHERE createdAt > ${w}
|
|
581
581
|
AND (content LIKE '%"type":"tool_use"%' OR json_extract(content, '$.tool_name') IS NOT NULL)
|
|
582
582
|
)
|
|
583
583
|
SELECT sessionId, iteration, lineNumber, jsonToolName, content
|
|
584
|
-
FROM raw ORDER BY sessionId, iteration,
|
|
584
|
+
FROM raw ORDER BY sessionId, iteration, seq LIMIT 100`,
|
|
585
585
|
}),
|
|
586
586
|
);
|
|
587
587
|
const bySession = new Map<string, string[]>();
|
|
@@ -574,14 +574,14 @@ export default async function compoundInsights(args: any, ctx: any) {
|
|
|
574
574
|
const rows = rowsToObjects(
|
|
575
575
|
await ctx.swarm.db_query({
|
|
576
576
|
sql: `WITH raw AS (
|
|
577
|
-
SELECT sessionId, iteration, lineNumber, content,
|
|
577
|
+
SELECT rowid AS seq, sessionId, iteration, lineNumber, content,
|
|
578
578
|
json_extract(content, '$.tool_name') as jsonToolName
|
|
579
579
|
FROM session_logs
|
|
580
580
|
WHERE createdAt > ${w}
|
|
581
581
|
AND (content LIKE '%"type":"tool_use"%' OR json_extract(content, '$.tool_name') IS NOT NULL)
|
|
582
582
|
)
|
|
583
583
|
SELECT sessionId, iteration, lineNumber, jsonToolName, content
|
|
584
|
-
FROM raw ORDER BY sessionId, iteration,
|
|
584
|
+
FROM raw ORDER BY sessionId, iteration, seq LIMIT 100`,
|
|
585
585
|
}),
|
|
586
586
|
);
|
|
587
587
|
const bySession = new Map<string, string[]>();
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"delegate-work": [
|
|
19
19
|
{
|
|
20
20
|
"path": "scripts/codex-exec.sh",
|
|
21
|
-
"content": "#!/usr/bin/env bash\n#\n# codex-exec.sh — invoke `codex exec` (non-interactive) for delegated\n# implementation work. This is the SINGLE SOURCE OF TRUTH for the invocation\n# flags. The delegate-work skill calls only this script; its routing matrix\n# decides WHICH gpt-
|
|
21
|
+
"content": "#!/usr/bin/env bash\n#\n# codex-exec.sh — invoke `codex exec` (non-interactive) for delegated\n# implementation work. This is the SINGLE SOURCE OF TRUTH for the invocation\n# flags. The delegate-work skill calls only this script; its routing matrix\n# decides WHICH gpt-6 variant (-m) and effort (-e) each task gets:\n# gpt-6-luna = trivial/mechanical gpt-6.1-sol = default (medium) to hard (high/xhigh)\n# gpt-6-astra = escalation only effort: medium|high|xhigh|max (never ultra)\n#\n# Usage:\n# codex-exec.sh [-m <model>] [-e <effort>] -C <workdir> -o <last-message-file> [-l <log-file>] \"PROMPT\"\n# printf '%s' \"$PROMPT\" | codex-exec.sh -C <workdir> -o <file> # prompt via stdin (preferred)\n#\n# Notes:\n# * -C sets codex's working root (the git worktree, for parallel isolation).\n# * -o writes codex's final message (its report) to a file you can read back.\n# * Reads the prompt from stdin when no positional PROMPT is given.\n# * In `exec` mode approvals are already \"never\", so nothing hangs waiting for\n# a human. The sandbox mode below bounds what codex may WRITE.\n#\n# Sandbox default: `workspace-write` — codex may freely edit files inside its\n# working root (the worktree) and read elsewhere on disk (so it can read the\n# plan by absolute path), but cannot write outside the workspace. This keeps\n# parallel worktree runs from clobbering each other or the main tree.\n#\n# Env overrides (flags -m/-e take precedence):\n# CODEX_MODEL default: gpt-6.1-sol gpt-6.1-sol | gpt-6-astra | gpt-6-luna\n# CODEX_EFFORT default: high reasoning effort (never \"fast\"/low)\n# CODEX_SANDBOX default: workspace-write (passed to codex's -s option)\n# CODEX_BYPASS set to 1 to use --dangerously-bypass-approvals-and-sandbox\n# (fully unsandboxed — opt in only when a phase must write\n# outside its worktree; overrides CODEX_SANDBOX)\nset -euo pipefail\n\nWORKDIR=\".\"\nOUTFILE=\"\"\nLOGFILE=\"\"\nPROMPT=\"\"\nMODEL_FLAG=\"\"\nEFFORT_FLAG=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n -m|--model) MODEL_FLAG=\"$2\"; shift 2 ;;\n -e|--effort) EFFORT_FLAG=\"$2\"; shift 2 ;;\n -C|--cd) WORKDIR=\"$2\"; shift 2 ;;\n -o|--out) OUTFILE=\"$2\"; shift 2 ;;\n -l|--log) LOGFILE=\"$2\"; shift 2 ;;\n --) shift; PROMPT=\"${1:-}\"; break ;;\n -*) echo \"codex-exec.sh: unknown flag: $1\" >&2; exit 2 ;;\n *) PROMPT=\"$1\"; shift ;;\n esac\ndone\n\nMODEL=\"${MODEL_FLAG:-${CODEX_MODEL:-gpt-6.1-sol}}\"\nEFFORT=\"${EFFORT_FLAG:-${CODEX_EFFORT:-high}}\"\nSANDBOX=\"${CODEX_SANDBOX:-workspace-write}\"\n\nargs=( exec\n -m \"$MODEL\"\n -c \"model_reasoning_effort=\\\"$EFFORT\\\"\"\n -C \"$WORKDIR\"\n)\nif [[ \"${CODEX_BYPASS:-0}\" == \"1\" ]]; then\n args+=( --dangerously-bypass-approvals-and-sandbox )\nelse\n args+=( -s \"$SANDBOX\" )\nfi\n[[ -n \"$OUTFILE\" ]] && args+=( -o \"$OUTFILE\" )\n\n# Prompt source: positional arg if present, otherwise stdin (`-`).\nif [[ -n \"$PROMPT\" ]]; then\n set -- \"$PROMPT\"\nelse\n set -- \"-\"\nfi\n\nif [[ -n \"$LOGFILE\" ]]; then\n codex \"${args[@]}\" \"$@\" 2>&1 | tee \"$LOGFILE\"\nelse\n codex \"${args[@]}\" \"$@\"\nfi\n"
|
|
22
22
|
}
|
|
23
23
|
],
|
|
24
24
|
"design-docs": [
|
|
@@ -51,7 +51,13 @@ export async function resolveSqlDsn(
|
|
|
51
51
|
// so the parameterless case goes through the tagged-template path instead. Never use `.simple()`.
|
|
52
52
|
// The statement runs as a server-side cursor fetched in batches, so the API never holds more than
|
|
53
53
|
// one batch past the caps. DECLARE only accepts SELECT, VALUES, TABLE and WITH ... SELECT.
|
|
54
|
-
|
|
54
|
+
// Bun >= 1.4.4 rejects an enumerable `raw`, so shape it like a real (frozen) template object.
|
|
55
|
+
const asTemplate = (text: string): TemplateStringsArray =>
|
|
56
|
+
Object.freeze(
|
|
57
|
+
Object.defineProperty([text], "raw", { value: Object.freeze([text]) }) as string[] & {
|
|
58
|
+
raw: readonly string[];
|
|
59
|
+
},
|
|
60
|
+
);
|
|
55
61
|
|
|
56
62
|
const defaultExecutor: SqlExecutor = async (dsn, query, params, take) => {
|
|
57
63
|
// Bun.SQL global, not `import { SQL } from "bun"`: the node-target CLI bundle cannot import "bun".
|
|
@@ -610,6 +610,8 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
610
610
|
// (~80k chars) it risks the SIGTERM-143 context-saturation failure mode
|
|
611
611
|
// the cap exists to prevent (see context-preamble.ts).
|
|
612
612
|
...boundedIntegerValidators(["CONTEXT_PREAMBLE_MAX_TOKENS"], 100, 20000),
|
|
613
|
+
// 0 turns the one-time context-bound note off; no model window reaches 10M.
|
|
614
|
+
...boundedIntegerValidators(["CONTEXT_BOUND_NOTE_TOKENS"], 0, 10_000_000),
|
|
613
615
|
MEMORY_MIN_SIMILARITY: (value) =>
|
|
614
616
|
validateFloatRange("MEMORY_MIN_SIMILARITY", value, 0, 1, "between 0 and 1 inclusive"),
|
|
615
617
|
MEMORY_ACCESS_BOOST_MAX: (value) =>
|
package/src/be/users.ts
CHANGED
|
@@ -632,6 +632,15 @@ export async function mintScriptRunToken(
|
|
|
632
632
|
return await insertSessionToken(agentId, null, null, ttlMs, null, undefined, null, null, true);
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
+
/** Whether a session token was minted by `mintScriptRunToken`. */
|
|
636
|
+
export async function isScriptRunToken(tokenId: string): Promise<boolean> {
|
|
637
|
+
const row = await getDbClient().get<{ scriptRun: number }>(
|
|
638
|
+
"SELECT scriptRun FROM session_tokens WHERE id = ?",
|
|
639
|
+
[tokenId],
|
|
640
|
+
);
|
|
641
|
+
return row?.scriptRun === 1;
|
|
642
|
+
}
|
|
643
|
+
|
|
635
644
|
export async function insertSessionToken(
|
|
636
645
|
agentId: string,
|
|
637
646
|
taskId: string | null,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRuntimeProvider } from "../runtime-providers";
|
|
2
2
|
import { BuiltinController } from "../runtime-providers/controller";
|
|
3
3
|
import {
|
|
4
|
+
CONTROLLER_PING_INTERVAL_MS,
|
|
4
5
|
CONTROLLER_PROTOCOL_VERSION,
|
|
5
6
|
type ControllerCommand,
|
|
6
7
|
ControllerServerMessageSchema,
|
|
@@ -37,13 +38,21 @@ async function executeCommand(
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// A network cut never closes the socket on this side, so the client watches inbound
|
|
42
|
+
// traffic. The API pings every CONTROLLER_PING_INTERVAL_MS and the client pings on
|
|
43
|
+
// each check; Bun surfaces both as `ping` / `pong` events. Three ping intervals
|
|
44
|
+
// without any frame means the link is half-open.
|
|
45
|
+
export const CONTROLLER_LIVENESS_TIMEOUT_MS = 3 * CONTROLLER_PING_INTERVAL_MS;
|
|
46
|
+
|
|
40
47
|
export function startControllerClient(options: {
|
|
41
48
|
apiUrl: string;
|
|
42
49
|
token: string;
|
|
43
50
|
provider: RuntimeProvider;
|
|
44
51
|
target: RuntimeTargetConfig;
|
|
45
52
|
onFatal?: () => void;
|
|
53
|
+
livenessTimeoutMs?: number;
|
|
46
54
|
}): { stop: () => void } {
|
|
55
|
+
const livenessTimeoutMs = options.livenessTimeoutMs ?? CONTROLLER_LIVENESS_TIMEOUT_MS;
|
|
47
56
|
const url = new URL("/api/controllers/connect", options.apiUrl);
|
|
48
57
|
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
49
58
|
throw new Error("Controller API URL must use HTTP or HTTPS");
|
|
@@ -52,6 +61,7 @@ export function startControllerClient(options: {
|
|
|
52
61
|
let stopped = false;
|
|
53
62
|
let socket: WebSocket | undefined;
|
|
54
63
|
let reconnect: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
+
let liveness: ReturnType<typeof setInterval> | undefined;
|
|
55
65
|
let attempts = 0;
|
|
56
66
|
const log = (message: string) => console.log(scrubSecrets(`[runtime-controller] ${message}`));
|
|
57
67
|
|
|
@@ -63,7 +73,28 @@ export function startControllerClient(options: {
|
|
|
63
73
|
socket = ws;
|
|
64
74
|
let controller: BuiltinController | undefined;
|
|
65
75
|
let connectedAt = 0;
|
|
76
|
+
let lastTrafficAt = Date.now();
|
|
77
|
+
const markAlive = () => {
|
|
78
|
+
lastTrafficAt = Date.now();
|
|
79
|
+
};
|
|
80
|
+
ws.addEventListener("ping", markAlive);
|
|
81
|
+
ws.addEventListener("pong", markAlive);
|
|
82
|
+
clearInterval(liveness);
|
|
83
|
+
liveness = setInterval(
|
|
84
|
+
() => {
|
|
85
|
+
if (socket !== ws) return;
|
|
86
|
+
if (Date.now() - lastTrafficAt > livenessTimeoutMs) {
|
|
87
|
+
log(`No traffic for ${livenessTimeoutMs}ms. Dropping the connection`);
|
|
88
|
+
clearInterval(liveness);
|
|
89
|
+
ws.terminate();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (ws.readyState === WebSocket.OPEN) ws.ping();
|
|
93
|
+
},
|
|
94
|
+
Math.ceil(livenessTimeoutMs / 3),
|
|
95
|
+
);
|
|
66
96
|
ws.onmessage = (event) => {
|
|
97
|
+
markAlive();
|
|
67
98
|
void (async () => {
|
|
68
99
|
const message = ControllerServerMessageSchema.parse(JSON.parse(String(event.data)));
|
|
69
100
|
if (message.type === "welcome") {
|
|
@@ -106,6 +137,7 @@ export function startControllerClient(options: {
|
|
|
106
137
|
ws.onerror = () => log("Connection failed");
|
|
107
138
|
ws.onclose = (event) => {
|
|
108
139
|
if (stopped || socket !== ws) return;
|
|
140
|
+
clearInterval(liveness);
|
|
109
141
|
if (event.code === 1008) {
|
|
110
142
|
stopped = true;
|
|
111
143
|
log("Controller access rejected or token revoked. Mint a token and restart this process.");
|
|
@@ -129,6 +161,7 @@ export function startControllerClient(options: {
|
|
|
129
161
|
stop: () => {
|
|
130
162
|
stopped = true;
|
|
131
163
|
if (reconnect) clearTimeout(reconnect);
|
|
164
|
+
clearInterval(liveness);
|
|
132
165
|
socket?.close(1000, "Controller stopped");
|
|
133
166
|
},
|
|
134
167
|
};
|
package/src/commands/runner.ts
CHANGED
|
@@ -3352,6 +3352,8 @@ export interface PollOptions {
|
|
|
3352
3352
|
pollTimeout: number;
|
|
3353
3353
|
/** Push scheduling fetches once, then returns control to the runner. */
|
|
3354
3354
|
singlePoll?: boolean;
|
|
3355
|
+
/** Stop an open poll window before its next claim request. */
|
|
3356
|
+
shouldStop?: () => boolean;
|
|
3355
3357
|
/** Distinguishes a failed fetch from an empty successful response. */
|
|
3356
3358
|
onPollFailure?: () => void;
|
|
3357
3359
|
since?: string; // Optional: for filtering finished tasks
|
|
@@ -3669,6 +3671,7 @@ async function pollForTriggerOnce(opts: PollOptions): Promise<Trigger | null> {
|
|
|
3669
3671
|
}
|
|
3670
3672
|
|
|
3671
3673
|
while (Date.now() - startTime < opts.pollTimeout) {
|
|
3674
|
+
if (opts.shouldStop?.()) return null;
|
|
3672
3675
|
try {
|
|
3673
3676
|
// Build URL with optional since parameter
|
|
3674
3677
|
let url = `${opts.apiUrl}/api/poll`;
|
|
@@ -7099,7 +7102,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
7099
7102
|
}
|
|
7100
7103
|
}
|
|
7101
7104
|
|
|
7102
|
-
// Only poll if we have capacity and the API is
|
|
7105
|
+
// Only poll if we have capacity and neither the API nor worker is draining
|
|
7103
7106
|
const schedule = pushDelivery.schedule({
|
|
7104
7107
|
mode: state.deliveryMode ?? "poll",
|
|
7105
7108
|
push: deliveryChecks.push,
|
|
@@ -7108,7 +7111,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
7108
7111
|
exitAfterTasks === null
|
|
7109
7112
|
? state.maxConcurrent
|
|
7110
7113
|
: Math.min(state.maxConcurrent, Math.max(0, exitAfterTasks - state.tasksProcessed)),
|
|
7111
|
-
draining: state.apiDraining,
|
|
7114
|
+
draining: state.apiDraining || shuttingDown,
|
|
7112
7115
|
lastPushPingAt,
|
|
7113
7116
|
pollInterval: PollIntervalMs,
|
|
7114
7117
|
});
|
|
@@ -7146,6 +7149,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
7146
7149
|
runtimeInstanceId,
|
|
7147
7150
|
pollTimeout: effectiveTimeout,
|
|
7148
7151
|
singlePoll: schedule.singlePoll,
|
|
7152
|
+
shouldStop: () => shuttingDown,
|
|
7149
7153
|
onPollFailure,
|
|
7150
7154
|
harnessProvider: state.harnessProvider,
|
|
7151
7155
|
onApiDrainSignal: (draining) => applyApiDrainSignal(state, role, draining),
|
|
@@ -2,7 +2,8 @@ import { explicitModelErrorForAgent } from "../be/model-validation";
|
|
|
2
2
|
import type { RequestInfo } from "../tools/utils";
|
|
3
3
|
import { type CreateTaskOptions, CreateTaskOptionsSchema } from "../types";
|
|
4
4
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
5
|
-
import
|
|
5
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
6
|
+
import type { TaskBlockedEvent, TaskCreateModify, TaskCreateOrigin } from "./contract";
|
|
6
7
|
import { dispatchPre, extensionIdForAgent, listRegistered } from "./dispatcher";
|
|
7
8
|
|
|
8
9
|
const MODIFIABLE_OPTION_KEYS = [
|
|
@@ -69,6 +70,14 @@ function warnDroppedKey(extensionName: string, key: string): void {
|
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function scrubFields<T extends object>(fields: T): T {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
Object.entries(fields)
|
|
76
|
+
.filter(([, value]) => value !== undefined)
|
|
77
|
+
.map(([key, value]) => [key, typeof value === "string" ? scrubSecrets(value) : value]),
|
|
78
|
+
) as T;
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
export async function applyPreTaskCreate(
|
|
73
82
|
args: ApplyPreTaskCreateArgs,
|
|
74
83
|
): Promise<ApplyPreTaskCreateResult> {
|
|
@@ -80,6 +89,48 @@ export async function applyPreTaskCreate(
|
|
|
80
89
|
? (`extension:${creatingExtension.record.name}` as const)
|
|
81
90
|
: args.origin;
|
|
82
91
|
|
|
92
|
+
const blocked = (
|
|
93
|
+
reason: string,
|
|
94
|
+
extension: TaskBlockedEvent["extension"],
|
|
95
|
+
): ApplyPreTaskCreateResult => {
|
|
96
|
+
const { options, requestInfo } = args;
|
|
97
|
+
const event: TaskBlockedEvent = {
|
|
98
|
+
extension,
|
|
99
|
+
reason: scrubSecrets(reason),
|
|
100
|
+
origin,
|
|
101
|
+
source: options.source,
|
|
102
|
+
requester: scrubFields({
|
|
103
|
+
userId: options.requestedByUserId,
|
|
104
|
+
agentId: requestInfo?.agentId ?? options.creatorAgentId,
|
|
105
|
+
slackUserId: options.slackUserId,
|
|
106
|
+
}),
|
|
107
|
+
context: scrubFields({
|
|
108
|
+
contextKey: options.contextKey ?? requestInfo?.contextKey,
|
|
109
|
+
scheduleId: options.scheduleId,
|
|
110
|
+
workflowRunId: options.workflowRunId,
|
|
111
|
+
workflowRunStepId: options.workflowRunStepId,
|
|
112
|
+
parentTaskId: options.parentTaskId,
|
|
113
|
+
vcsRepo: options.vcsRepo,
|
|
114
|
+
vcsNumber: options.vcsNumber,
|
|
115
|
+
slackChannelId: options.slackChannelId,
|
|
116
|
+
slackThreadTs: options.slackThreadTs,
|
|
117
|
+
}),
|
|
118
|
+
description: scrubSecrets(args.description).slice(0, 500),
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
workflowEventBus.emit("task.create_blocked", {
|
|
122
|
+
...event,
|
|
123
|
+
skipExtensionId: creatingExtensionId,
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(
|
|
127
|
+
"[extensions] task.create_blocked event not emitted:",
|
|
128
|
+
scrubSecrets(error instanceof Error ? error.message : String(error)),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return { kind: "blocked", reason: event.reason, extension };
|
|
132
|
+
};
|
|
133
|
+
|
|
83
134
|
// The last extension that rewrote `model` or `agentId`. The final pair is re-checked
|
|
84
135
|
// against the assignee's harness, because the caller validated the pre-hook pair only.
|
|
85
136
|
let modelEditor: { id: string; name: string } | undefined;
|
|
@@ -125,11 +176,7 @@ export async function applyPreTaskCreate(
|
|
|
125
176
|
);
|
|
126
177
|
|
|
127
178
|
if (result.action === "block") {
|
|
128
|
-
return
|
|
129
|
-
kind: "blocked",
|
|
130
|
-
reason: result.reason,
|
|
131
|
-
extension: result.extension,
|
|
132
|
-
};
|
|
179
|
+
return blocked(result.reason, result.extension);
|
|
133
180
|
}
|
|
134
181
|
if (result.action === "continue") {
|
|
135
182
|
return { kind: "proceed", description: args.description, options: args.options };
|
|
@@ -146,11 +193,10 @@ export async function applyPreTaskCreate(
|
|
|
146
193
|
agentId: options.agentId,
|
|
147
194
|
});
|
|
148
195
|
if (modelError) {
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
};
|
|
196
|
+
return blocked(
|
|
197
|
+
`pre.task.create rewrite from extension "${modelEditor.name}" is invalid: ${modelError}`,
|
|
198
|
+
modelEditor,
|
|
199
|
+
);
|
|
154
200
|
}
|
|
155
201
|
}
|
|
156
202
|
return {
|