@desplega.ai/agent-swarm 2.7.6 → 2.7.7
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-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
- package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
- package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
- package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
- package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
- package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
- package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
- package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
- package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
- package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
- package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
- package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
- package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
- package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
- package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
- package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
- package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +21 -21
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
- package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
- package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
- package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
- package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
- package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
- package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
- package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
- package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
- package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
- package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +197 -4
- package/package.json +5 -5
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/tasks/read.ts +9 -1
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +107 -7
- package/src/be/db.ts +34 -2
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +3 -0
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +142 -2
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +45 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/handlers-by3sfxyx.js +0 -32
- package/dist/index-4f2pvxae.js +0 -37
- package/dist/index-98gz7tmp.js +0 -30
- package/dist/index-k08z2cak.js +0 -33
- package/dist/lead-enq35nqp.js +0 -70
- package/dist/worker-gg1576j3.js +0 -70
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
package/src/pages/version.ts
CHANGED
|
@@ -35,6 +35,9 @@ export async function snapshotPage(
|
|
|
35
35
|
passwordHash: page.passwordHash,
|
|
36
36
|
body: page.body,
|
|
37
37
|
needsCredentials: page.needsCredentials,
|
|
38
|
+
theme: page.theme,
|
|
39
|
+
themeTokens: page.themeTokens,
|
|
40
|
+
designVersion: page.designVersion,
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
return createPageVersion({
|
|
@@ -45,7 +45,7 @@ import type {
|
|
|
45
45
|
* default; the image installs exactly this version and the per-task settings
|
|
46
46
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
47
47
|
*/
|
|
48
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
48
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1793450357-gd44904";
|
|
49
49
|
|
|
50
50
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
51
51
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -2053,6 +2053,7 @@ export async function createInProcessCodexSession(
|
|
|
2053
2053
|
const appServerFactory = opts.appServerFactory ?? ((options) => new CodexAppServer(options));
|
|
2054
2054
|
const client = appServerFactory({
|
|
2055
2055
|
...(codexPathOverride ? { codexPath: codexPathOverride } : {}),
|
|
2056
|
+
...(gateway ? { configOverrides: codexGatewayConfig(gateway) } : {}),
|
|
2056
2057
|
env,
|
|
2057
2058
|
onStderr: (line) => {
|
|
2058
2059
|
if (liveSession) liveSession.emitTransportStderr(line);
|
|
@@ -26,6 +26,7 @@ export interface CodexAppServerOptions {
|
|
|
26
26
|
env: Readonly<Record<string, string | undefined>>;
|
|
27
27
|
requestTimeoutMs?: number;
|
|
28
28
|
onStderr?: (line: string) => void;
|
|
29
|
+
configOverrides?: Readonly<Record<string, CodexConfigValue | undefined>>;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
type PendingRequest = {
|
|
@@ -53,6 +54,25 @@ export class CodexAppServerRpcError extends Error {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** `-c key=<TOML value>` pairs, one per top-level key; tables become inline tables. */
|
|
58
|
+
export function configOverrideArgs(
|
|
59
|
+
overrides: Readonly<Record<string, CodexConfigValue | undefined>> = {},
|
|
60
|
+
): string[] {
|
|
61
|
+
return Object.entries(overrides).flatMap(([key, value]) =>
|
|
62
|
+
value === undefined ? [] : ["-c", `${key}=${tomlValue(value)}`],
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function tomlValue(value: CodexConfigValue): string {
|
|
67
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
68
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
69
|
+
if (Array.isArray(value)) return `[${value.map(tomlValue).join(", ")}]`;
|
|
70
|
+
const fields = Object.entries(value as Record<string, CodexConfigValue | undefined>).flatMap(
|
|
71
|
+
([key, field]) => (field === undefined ? [] : [`${JSON.stringify(key)} = ${tomlValue(field)}`]),
|
|
72
|
+
);
|
|
73
|
+
return `{ ${fields.join(", ")} }`;
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
/** Thin JSONL client for one `codex app-server --listen stdio://` process. */
|
|
57
77
|
export class CodexAppServer {
|
|
58
78
|
private readonly proc: ReturnType<typeof Bun.spawn>;
|
|
@@ -79,7 +99,13 @@ export class CodexAppServer {
|
|
|
79
99
|
this.onStderr = options.onStderr;
|
|
80
100
|
|
|
81
101
|
const codexPath = options.codexPath ?? resolveCodexPath(options.env.PATH);
|
|
82
|
-
const command = [
|
|
102
|
+
const command = [
|
|
103
|
+
codexPath,
|
|
104
|
+
...configOverrideArgs(options.configOverrides),
|
|
105
|
+
"app-server",
|
|
106
|
+
"--listen",
|
|
107
|
+
"stdio://",
|
|
108
|
+
];
|
|
83
109
|
|
|
84
110
|
const env = Object.fromEntries(
|
|
85
111
|
Object.entries(options.env).filter(
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import type { ExtensionFactory, McpServerConfig } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
|
|
11
|
+
import {
|
|
12
|
+
advanceIdentityBaselines,
|
|
13
|
+
contentSha256,
|
|
14
|
+
PROFILE_FILE_IO,
|
|
15
|
+
type ProfileFileIO,
|
|
16
|
+
readIdentityBaselines,
|
|
17
|
+
} from "../commands/profile-sync";
|
|
11
18
|
import { checkToolLoop, clearToolHistory } from "../hooks/tool-loop-detection";
|
|
12
19
|
import { renderGoalReminder } from "../prompts/goal-reminder";
|
|
13
20
|
import { isSessionSummarySkipped } from "../utils/env-flag";
|
|
@@ -100,11 +107,16 @@ export async function fetchTaskDetails(
|
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Sync identity files (SOUL.md, IDENTITY.md, TOOLS.md) to server. A file whose
|
|
112
|
+
* hash still equals its baseline was not edited and is skipped, so a stale copy
|
|
113
|
+
* never overwrites a DB-side edit made during the session.
|
|
114
|
+
*/
|
|
104
115
|
export async function syncIdentityFilesToServer(
|
|
105
116
|
config: SwarmHooksConfig,
|
|
106
117
|
changeSource: "self_edit" | "session_sync" = "session_sync",
|
|
107
118
|
signal?: AbortSignal,
|
|
119
|
+
io: ProfileFileIO = PROFILE_FILE_IO,
|
|
108
120
|
): Promise<void> {
|
|
109
121
|
const updates: Record<string, string> = {};
|
|
110
122
|
const paths: Record<string, string> = {
|
|
@@ -112,15 +124,14 @@ export async function syncIdentityFilesToServer(
|
|
|
112
124
|
identityMd: "/workspace/IDENTITY.md",
|
|
113
125
|
toolsMd: "/workspace/TOOLS.md",
|
|
114
126
|
};
|
|
127
|
+
const baselines = await readIdentityBaselines(io.readFile);
|
|
115
128
|
|
|
116
129
|
for (const [key, path] of Object.entries(paths)) {
|
|
117
130
|
try {
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
updates[key] = content;
|
|
123
|
-
}
|
|
131
|
+
const content = await io.readFile(path);
|
|
132
|
+
if (content === undefined || baselines?.[key] === contentSha256(content)) continue;
|
|
133
|
+
if (content.trim() && content.length <= 65536) {
|
|
134
|
+
updates[key] = content;
|
|
124
135
|
}
|
|
125
136
|
} catch {
|
|
126
137
|
/* skip */
|
|
@@ -130,28 +141,27 @@ export async function syncIdentityFilesToServer(
|
|
|
130
141
|
if (Object.keys(updates).length === 0) return;
|
|
131
142
|
|
|
132
143
|
try {
|
|
133
|
-
await fetch(`${config.apiUrl}/api/agents/${config.agentId}/profile`, {
|
|
144
|
+
const resp = await fetch(`${config.apiUrl}/api/agents/${config.agentId}/profile`, {
|
|
134
145
|
method: "PUT",
|
|
135
146
|
headers: apiHeaders(config),
|
|
136
147
|
body: JSON.stringify({ ...updates, changeSource }),
|
|
137
148
|
signal,
|
|
138
149
|
});
|
|
150
|
+
if (resp.ok) await advanceIdentityBaselines(updates, io);
|
|
139
151
|
} catch {
|
|
140
152
|
/* silently fail */
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
/** Sync setup script to server */
|
|
145
|
-
async function syncSetupScriptToServer(
|
|
157
|
+
export async function syncSetupScriptToServer(
|
|
146
158
|
config: SwarmHooksConfig,
|
|
147
159
|
changeSource: "self_edit" | "session_sync" = "session_sync",
|
|
160
|
+
io: ProfileFileIO = PROFILE_FILE_IO,
|
|
148
161
|
): Promise<void> {
|
|
149
162
|
try {
|
|
150
|
-
const
|
|
151
|
-
if (!(
|
|
152
|
-
|
|
153
|
-
const raw = await file.text();
|
|
154
|
-
if (!raw.trim()) return;
|
|
163
|
+
const raw = await io.readFile("/workspace/start-up.sh");
|
|
164
|
+
if (!raw?.trim()) return;
|
|
155
165
|
|
|
156
166
|
const markerStart = "# === Agent-managed setup (from DB) ===";
|
|
157
167
|
const markerEnd = "# === End agent-managed setup ===";
|
|
@@ -166,12 +176,17 @@ async function syncSetupScriptToServer(
|
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
if (!content || content.length > 65536) return;
|
|
179
|
+
const baselines = await readIdentityBaselines(io.readFile);
|
|
180
|
+
if (baselines?.setupScript === contentSha256(content)) return;
|
|
169
181
|
|
|
170
|
-
await fetch(`${config.apiUrl}/api/agents/${config.agentId}/profile`, {
|
|
182
|
+
const resp = await fetch(`${config.apiUrl}/api/agents/${config.agentId}/profile`, {
|
|
171
183
|
method: "PUT",
|
|
172
184
|
headers: apiHeaders(config),
|
|
173
185
|
body: JSON.stringify({ setupScript: content, changeSource }),
|
|
174
186
|
});
|
|
187
|
+
if (resp.ok) {
|
|
188
|
+
await advanceIdentityBaselines({ setupScript: content }, io);
|
|
189
|
+
}
|
|
175
190
|
} catch {
|
|
176
191
|
/* silently fail */
|
|
177
192
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { createLogEntry, getDbClient } from "../be/db";
|
|
2
|
+
import { getScript } from "../be/scripts/db";
|
|
3
|
+
import { runSavedScriptAsAgent } from "../be/scripts/run-saved";
|
|
4
|
+
import type { AgentTask, ScriptRecord } from "../types";
|
|
5
|
+
import {
|
|
6
|
+
isFollowUpGateScheduleId,
|
|
7
|
+
parseFollowUpGateScheduleIds,
|
|
8
|
+
} from "../utils/follow-up-gate-schedule-ids";
|
|
9
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Follow-up gate, shadow mode. When a gated scheduled run's Lead follow-up is
|
|
13
|
+
* created, a configured verifier script checks the run after commit and the
|
|
14
|
+
* server records whether the follow-up could have been skipped. Shadow mode
|
|
15
|
+
* never skips or delays a follow-up.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const FOLLOW_UP_GATE_SHADOW_EVENT = "task_follow_up_gate_shadow" as const;
|
|
19
|
+
|
|
20
|
+
export type FollowUpGateConfig = {
|
|
21
|
+
mode: "off" | "shadow";
|
|
22
|
+
scheduleIds: ReadonlySet<string>;
|
|
23
|
+
verifierScript: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type GateIneligibleReason =
|
|
27
|
+
| "mode_off"
|
|
28
|
+
| "not_scheduled"
|
|
29
|
+
| "schedule_not_gated"
|
|
30
|
+
| "status_failed"
|
|
31
|
+
| "follow_up_instructions";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read the gate config on every call so a `swarm_config` reload applies. Any
|
|
35
|
+
* mode other than `shadow` reads as `off`, and malformed ids are ignored.
|
|
36
|
+
*/
|
|
37
|
+
export function readFollowUpGateConfig(env: NodeJS.ProcessEnv = process.env): FollowUpGateConfig {
|
|
38
|
+
const mode = env.FOLLOW_UP_GATE_MODE?.trim().toLowerCase() === "shadow" ? "shadow" : "off";
|
|
39
|
+
const scheduleIds = new Set(
|
|
40
|
+
parseFollowUpGateScheduleIds(env.FOLLOW_UP_GATE_SCHEDULE_IDS)
|
|
41
|
+
.filter(isFollowUpGateScheduleId)
|
|
42
|
+
.map((id) => id.toLowerCase()),
|
|
43
|
+
);
|
|
44
|
+
return { mode, scheduleIds, verifierScript: env.FOLLOW_UP_GATE_VERIFIER_SCRIPT?.trim() ?? "" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Pure decision on whether a follow-up's worker task may be verified. */
|
|
48
|
+
export function classifyFollowUpGate(input: {
|
|
49
|
+
task: Pick<AgentTask, "scheduleId" | "followUpConfig">;
|
|
50
|
+
status: "completed" | "failed";
|
|
51
|
+
config: FollowUpGateConfig;
|
|
52
|
+
}): { eligible: true } | { eligible: false; reason: GateIneligibleReason } {
|
|
53
|
+
const { task, status, config } = input;
|
|
54
|
+
if (config.mode === "off") return { eligible: false, reason: "mode_off" };
|
|
55
|
+
if (!task.scheduleId) return { eligible: false, reason: "not_scheduled" };
|
|
56
|
+
if (!config.scheduleIds.has(task.scheduleId.toLowerCase())) {
|
|
57
|
+
return { eligible: false, reason: "schedule_not_gated" };
|
|
58
|
+
}
|
|
59
|
+
if (status === "failed") return { eligible: false, reason: "status_failed" };
|
|
60
|
+
if (task.followUpConfig?.onCompleted?.trim() || task.followUpConfig?.onFailed?.trim()) {
|
|
61
|
+
return { eligible: false, reason: "follow_up_instructions" };
|
|
62
|
+
}
|
|
63
|
+
return { eligible: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type VerifierScript = Pick<ScriptRecord, "name" | "version" | "contentHash">;
|
|
67
|
+
|
|
68
|
+
export type FollowUpGateDeps = {
|
|
69
|
+
/** Resolve the global verifier script, or null when it does not exist. */
|
|
70
|
+
getVerifier: (name: string) => Promise<VerifierScript | null>;
|
|
71
|
+
/** Run the verifier as `agentId` and return its result, or throw. */
|
|
72
|
+
runVerifier: (
|
|
73
|
+
script: VerifierScript,
|
|
74
|
+
input: { taskId: string },
|
|
75
|
+
agentId: string,
|
|
76
|
+
signal: AbortSignal,
|
|
77
|
+
) => Promise<unknown>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const FOLLOW_UP_GATE_TIMEOUT_MS = 60_000;
|
|
81
|
+
const FOLLOW_UP_GATE_MAX_IN_FLIGHT = 2;
|
|
82
|
+
const TEXT_LIMIT = 300;
|
|
83
|
+
const MAX_UNVERIFIED = 20;
|
|
84
|
+
|
|
85
|
+
const defaultDeps: FollowUpGateDeps = {
|
|
86
|
+
getVerifier: (name) => getScript({ name, scope: "global" }),
|
|
87
|
+
runVerifier: async (script, input, agentId, signal) => {
|
|
88
|
+
const output = await runSavedScriptAsAgent({
|
|
89
|
+
script: script as ScriptRecord,
|
|
90
|
+
input,
|
|
91
|
+
agentId,
|
|
92
|
+
timeoutMs,
|
|
93
|
+
signal,
|
|
94
|
+
});
|
|
95
|
+
if (output.exitCode !== 0 || output.error || output.runtimeError) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
output.runtimeError?.message ??
|
|
98
|
+
output.error ??
|
|
99
|
+
`script exited with code ${output.exitCode}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return output.result;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let timeoutMs = FOLLOW_UP_GATE_TIMEOUT_MS;
|
|
107
|
+
let depsForTests: FollowUpGateDeps | null = null;
|
|
108
|
+
let inFlight = 0;
|
|
109
|
+
const pending = new Set<Promise<void>>();
|
|
110
|
+
|
|
111
|
+
/** Replace the verifier lookup and runner. `null` restores the defaults. */
|
|
112
|
+
export function setFollowUpGateDepsForTests(deps: FollowUpGateDeps | null): void {
|
|
113
|
+
depsForTests = deps;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Override the verifier deadline. `null` restores the 60s default. */
|
|
117
|
+
export function setFollowUpGateTimeoutMsForTests(ms: number | null): void {
|
|
118
|
+
timeoutMs = ms ?? FOLLOW_UP_GATE_TIMEOUT_MS;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Wait until every scheduled shadow hook has written its row. */
|
|
122
|
+
export async function drainFollowUpGateShadowForTest(): Promise<void> {
|
|
123
|
+
// Hooks run in FIFO order behind the client lock, so once this marker runs,
|
|
124
|
+
// every earlier hook has started and registered its promise.
|
|
125
|
+
await new Promise<void>((resolve) => getDbClient().afterCommit(resolve));
|
|
126
|
+
while (pending.size > 0) await Promise.allSettled([...pending]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type ShadowArgs = {
|
|
130
|
+
task: Pick<AgentTask, "id" | "scheduleId" | "followUpConfig">;
|
|
131
|
+
status: "completed" | "failed";
|
|
132
|
+
followUpTaskId: string;
|
|
133
|
+
leadAgentId: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type ShadowRecord = {
|
|
137
|
+
decision: "would_skip" | "would_keep" | "error" | "ineligible";
|
|
138
|
+
reason: string | null;
|
|
139
|
+
verdict?: { ok: boolean | null; check: string | null; detail: string; controlsOk: boolean };
|
|
140
|
+
unverified?: string[];
|
|
141
|
+
verifier?: VerifierScript | null;
|
|
142
|
+
latencyMs?: number | null;
|
|
143
|
+
error?: string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Record a shadow gate decision for a just-created follow-up. Returns
|
|
148
|
+
* synchronously: all work, including the row write, runs after commit. With
|
|
149
|
+
* the gate off or the schedule not gated, nothing is registered.
|
|
150
|
+
*/
|
|
151
|
+
export function scheduleFollowUpGateShadow(args: ShadowArgs): void {
|
|
152
|
+
const config = readFollowUpGateConfig();
|
|
153
|
+
const verdict = classifyFollowUpGate({ task: args.task, status: args.status, config });
|
|
154
|
+
if (
|
|
155
|
+
!verdict.eligible &&
|
|
156
|
+
(verdict.reason === "mode_off" ||
|
|
157
|
+
verdict.reason === "not_scheduled" ||
|
|
158
|
+
verdict.reason === "schedule_not_gated")
|
|
159
|
+
) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const deps = depsForTests ?? defaultDeps;
|
|
163
|
+
getDbClient().afterCommit(() => {
|
|
164
|
+
const run = verdict.eligible
|
|
165
|
+
? runShadow(args, config, deps)
|
|
166
|
+
: writeRecord(args, { decision: "ineligible", reason: verdict.reason });
|
|
167
|
+
pending.add(run);
|
|
168
|
+
const settle = () => {
|
|
169
|
+
pending.delete(run);
|
|
170
|
+
};
|
|
171
|
+
void run.then(settle, settle);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function runShadow(
|
|
176
|
+
args: ShadowArgs,
|
|
177
|
+
config: FollowUpGateConfig,
|
|
178
|
+
deps: FollowUpGateDeps,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
if (inFlight >= FOLLOW_UP_GATE_MAX_IN_FLIGHT) {
|
|
181
|
+
await writeRecord(args, { decision: "error", reason: "concurrency_cap" });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
inFlight++;
|
|
185
|
+
try {
|
|
186
|
+
await writeRecord(args, await verify(args, config, deps));
|
|
187
|
+
} finally {
|
|
188
|
+
inFlight--;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function verify(
|
|
193
|
+
args: ShadowArgs,
|
|
194
|
+
config: FollowUpGateConfig,
|
|
195
|
+
deps: FollowUpGateDeps,
|
|
196
|
+
): Promise<ShadowRecord> {
|
|
197
|
+
if (!config.verifierScript) return { decision: "error", reason: "no_verifier" };
|
|
198
|
+
const started = performance.now();
|
|
199
|
+
const latency = () => Math.round(performance.now() - started);
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const deadlineMs = timeoutMs;
|
|
202
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
203
|
+
let script: VerifierScript | null = null;
|
|
204
|
+
let timedOut = false;
|
|
205
|
+
try {
|
|
206
|
+
const run = (async () => {
|
|
207
|
+
script = await deps.getVerifier(config.verifierScript);
|
|
208
|
+
if (!script) return null;
|
|
209
|
+
return {
|
|
210
|
+
result: await deps.runVerifier(
|
|
211
|
+
script,
|
|
212
|
+
{ taskId: args.task.id },
|
|
213
|
+
args.leadAgentId,
|
|
214
|
+
controller.signal,
|
|
215
|
+
),
|
|
216
|
+
};
|
|
217
|
+
})();
|
|
218
|
+
// A run that settles after the deadline must not surface as an unhandled rejection.
|
|
219
|
+
run.catch(() => {});
|
|
220
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
221
|
+
timer = setTimeout(() => {
|
|
222
|
+
timedOut = true;
|
|
223
|
+
controller.abort();
|
|
224
|
+
reject(new Error(`verifier timed out after ${deadlineMs}ms`));
|
|
225
|
+
}, deadlineMs);
|
|
226
|
+
});
|
|
227
|
+
const outcome = await Promise.race([run, deadline]);
|
|
228
|
+
const verifier = verifierInfo(script);
|
|
229
|
+
if (!outcome) {
|
|
230
|
+
return { decision: "error", reason: "no_verifier", verifier, latencyMs: latency() };
|
|
231
|
+
}
|
|
232
|
+
const result = outcome.result;
|
|
233
|
+
if (!result || typeof result !== "object") {
|
|
234
|
+
return {
|
|
235
|
+
decision: "error",
|
|
236
|
+
reason: "verifier_error",
|
|
237
|
+
verifier,
|
|
238
|
+
latencyMs: latency(),
|
|
239
|
+
error: "verifier result is not an object",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return { ...mapVerdict(result as Record<string, unknown>), verifier, latencyMs: latency() };
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
245
|
+
return {
|
|
246
|
+
decision: "error",
|
|
247
|
+
reason: timedOut ? "timeout" : "verifier_error",
|
|
248
|
+
verifier: verifierInfo(script),
|
|
249
|
+
latencyMs: latency(),
|
|
250
|
+
error: clip(message),
|
|
251
|
+
};
|
|
252
|
+
} finally {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function verifierInfo(script: VerifierScript | null): VerifierScript | null {
|
|
258
|
+
return script
|
|
259
|
+
? { name: script.name, version: script.version, contentHash: script.contentHash }
|
|
260
|
+
: null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** `ok:true` with passing controls is `would_skip`; anything else keeps the follow-up. */
|
|
264
|
+
function mapVerdict(result: Record<string, unknown>): ShadowRecord {
|
|
265
|
+
const ok = typeof result.ok === "boolean" ? result.ok : null;
|
|
266
|
+
const controls = result.controls as { ok?: unknown } | null | undefined;
|
|
267
|
+
const controlsOk = controls?.ok === true;
|
|
268
|
+
const unverified = Array.isArray(result.unverified)
|
|
269
|
+
? result.unverified
|
|
270
|
+
.filter((name): name is string => typeof name === "string")
|
|
271
|
+
.slice(0, MAX_UNVERIFIED)
|
|
272
|
+
.map((name) => name.slice(0, 100))
|
|
273
|
+
: [];
|
|
274
|
+
return {
|
|
275
|
+
decision: ok === true && controlsOk ? "would_skip" : "would_keep",
|
|
276
|
+
reason: null,
|
|
277
|
+
verdict: {
|
|
278
|
+
ok,
|
|
279
|
+
check: typeof result.check === "string" ? result.check.slice(0, 100) : null,
|
|
280
|
+
detail: typeof result.detail === "string" ? clip(result.detail) : "",
|
|
281
|
+
controlsOk,
|
|
282
|
+
},
|
|
283
|
+
unverified,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function clip(text: string): string {
|
|
288
|
+
return scrubSecrets(text).slice(0, TEXT_LIMIT);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function writeRecord(args: ShadowArgs, record: ShadowRecord): Promise<void> {
|
|
292
|
+
try {
|
|
293
|
+
await createLogEntry({
|
|
294
|
+
eventType: FOLLOW_UP_GATE_SHADOW_EVENT,
|
|
295
|
+
taskId: args.task.id,
|
|
296
|
+
agentId: args.leadAgentId,
|
|
297
|
+
metadata: {
|
|
298
|
+
v: 1,
|
|
299
|
+
mode: "shadow",
|
|
300
|
+
followUpTaskId: args.followUpTaskId,
|
|
301
|
+
scheduleId: args.task.scheduleId ?? null,
|
|
302
|
+
status: args.status,
|
|
303
|
+
decision: record.decision,
|
|
304
|
+
reason: record.reason,
|
|
305
|
+
verdict: record.verdict ?? null,
|
|
306
|
+
unverified: record.unverified ?? [],
|
|
307
|
+
verifier: record.verifier ?? null,
|
|
308
|
+
latencyMs: record.latencyMs ?? null,
|
|
309
|
+
...(record.error ? { error: record.error } : {}),
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.warn(
|
|
314
|
+
`[follow-up-gate] Could not record shadow decision for task ${args.task.id.slice(0, 8)}:`,
|
|
315
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
35
35
|
import { taskAttachmentDisplayUrl } from "../utils/task-attachment-links";
|
|
36
36
|
import { chatThreadOfContextKey } from "./context-key";
|
|
37
|
+
import { scheduleFollowUpGateShadow } from "./follow-up-gate";
|
|
37
38
|
import { taskModelPin } from "./model-pin";
|
|
38
39
|
import { createTaskWithSiblingAwareness } from "./sibling-awareness";
|
|
39
40
|
// Side-effect import: registers task lifecycle templates in the in-memory registry.
|
|
@@ -261,6 +262,9 @@ async function findDeferredWakeFor(taskId: string, agentId: string): Promise<Def
|
|
|
261
262
|
* `all` wait with siblings still pending wakes nobody, so the follow-up stays.
|
|
262
263
|
* A `followUpConfig.onCompleted` / `onFailed` instruction for this status also
|
|
263
264
|
* keeps the follow-up, because the wake-up task does not carry that text.
|
|
265
|
+
*
|
|
266
|
+
* With `FOLLOW_UP_GATE_MODE=shadow`, a gated scheduled run also records a
|
|
267
|
+
* verifier's would-skip decision after commit; the follow-up is never skipped.
|
|
264
268
|
*/
|
|
265
269
|
export async function createWorkerTaskFollowUp(args: {
|
|
266
270
|
task: AgentTask;
|
|
@@ -392,7 +396,7 @@ export async function createWorkerTaskFollowUp(args: {
|
|
|
392
396
|
);
|
|
393
397
|
return null;
|
|
394
398
|
}
|
|
395
|
-
|
|
399
|
+
const followUp = await createTaskWithSiblingAwareness(
|
|
396
400
|
changes.description ?? followUpDescription,
|
|
397
401
|
{
|
|
398
402
|
agentId: followUpAgentId,
|
|
@@ -413,6 +417,22 @@ export async function createWorkerTaskFollowUp(args: {
|
|
|
413
417
|
},
|
|
414
418
|
{ origin: "followUp" },
|
|
415
419
|
);
|
|
420
|
+
if (followUp) {
|
|
421
|
+
try {
|
|
422
|
+
scheduleFollowUpGateShadow({
|
|
423
|
+
task,
|
|
424
|
+
status,
|
|
425
|
+
followUpTaskId: followUp.id,
|
|
426
|
+
leadAgentId: leadAgent.id,
|
|
427
|
+
});
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.warn(
|
|
430
|
+
`[worker-follow-up] Follow-up gate shadow failed for task ${task.id.slice(0, 8)}:`,
|
|
431
|
+
err instanceof Error ? err.message : String(err),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return followUp;
|
|
416
436
|
}
|
|
417
437
|
|
|
418
438
|
/** Hop cap for `deferredWorkChatContextKey`; a defer/resume chain is short. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import {
|
|
4
6
|
CodexAppServer,
|
|
@@ -205,4 +207,61 @@ describe("CodexAppServer", () => {
|
|
|
205
207
|
},
|
|
206
208
|
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
207
209
|
);
|
|
210
|
+
|
|
211
|
+
for (const { name, launchOnGateway } of [
|
|
212
|
+
{
|
|
213
|
+
name: "pinned Codex rejects a gateway turn on an app-server launched without it",
|
|
214
|
+
launchOnGateway: false,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "pinned Codex accepts a gateway turn on an app-server launched on the gateway",
|
|
218
|
+
launchOnGateway: true,
|
|
219
|
+
},
|
|
220
|
+
]) {
|
|
221
|
+
test(
|
|
222
|
+
name,
|
|
223
|
+
async () => {
|
|
224
|
+
const gateway = {
|
|
225
|
+
model_provider: "swarm-gateway",
|
|
226
|
+
model_providers: {
|
|
227
|
+
"swarm-gateway": {
|
|
228
|
+
name: "Swarm gateway",
|
|
229
|
+
base_url: "http://127.0.0.1:9/v1",
|
|
230
|
+
env_key: "CODEX_GATEWAY_API_KEY",
|
|
231
|
+
wire_api: "responses",
|
|
232
|
+
requires_openai_auth: false,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
const home = await mkdtemp(join(tmpdir(), "codex-app-server-provider-"));
|
|
237
|
+
const server = new CodexAppServer({
|
|
238
|
+
codexPath: join(import.meta.dir, "..", "..", "node_modules", ".bin", "codex"),
|
|
239
|
+
env: fixtureEnv({ HOME: home, CODEX_HOME: home, CODEX_GATEWAY_API_KEY: "example" }),
|
|
240
|
+
...(launchOnGateway ? { configOverrides: gateway } : {}),
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
const started = await server.request<{ thread: { id: string } }>("thread/start", {
|
|
244
|
+
cwd: home,
|
|
245
|
+
approvalPolicy: "never",
|
|
246
|
+
sandbox: "danger-full-access",
|
|
247
|
+
config: gateway,
|
|
248
|
+
ephemeral: true,
|
|
249
|
+
});
|
|
250
|
+
const turn = server.request("turn/start", {
|
|
251
|
+
threadId: started.thread.id,
|
|
252
|
+
input: [{ type: "text", text: "hi" }],
|
|
253
|
+
});
|
|
254
|
+
if (launchOnGateway) await turn;
|
|
255
|
+
else
|
|
256
|
+
await expect(turn).rejects.toThrow(
|
|
257
|
+
"This chat uses swarm-gateway, but your current provider is openai",
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
await server.close();
|
|
261
|
+
await rm(home, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
208
267
|
});
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* selection in the adapter, the presence gate, and the live check.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
7
|
+
import { afterAll, afterEach, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
8
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
8
11
|
import { validateProviderCredentials } from "../commands/provider-credentials";
|
|
9
12
|
import { CodexAdapter, checkCodexCredentials } from "../providers/codex-adapter";
|
|
10
13
|
import type { ProviderSessionConfig } from "../providers/types";
|
|
@@ -75,6 +78,22 @@ afterEach(() => {
|
|
|
75
78
|
globalThis.fetch = realFetch;
|
|
76
79
|
});
|
|
77
80
|
|
|
81
|
+
// The adapter reads `process.env.HOME` live (see `codexUserHome`) to decide
|
|
82
|
+
// whether `~/.codex/auth.json` exists. A developer's real codex login would
|
|
83
|
+
// make OAuth win over OPENAI_API_KEY and fail the API-key assertions, so run
|
|
84
|
+
// the file against an empty home. The real auth.json is never touched.
|
|
85
|
+
const realHome = process.env.HOME;
|
|
86
|
+
let isolatedHome: string | undefined;
|
|
87
|
+
beforeAll(() => {
|
|
88
|
+
isolatedHome = mkdtempSync(join(tmpdir(), "codex-gateway-route-home-"));
|
|
89
|
+
process.env.HOME = isolatedHome;
|
|
90
|
+
});
|
|
91
|
+
afterAll(() => {
|
|
92
|
+
if (realHome === undefined) delete process.env.HOME;
|
|
93
|
+
else process.env.HOME = realHome;
|
|
94
|
+
if (isolatedHome) rmSync(isolatedHome, { recursive: true, force: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
78
97
|
describe("codex gateway route: adapter", () => {
|
|
79
98
|
test("adds a Responses provider block and keeps OAuth and OPENAI_API_KEY out of the CLI env", async () => {
|
|
80
99
|
const log = spyOn(console, "log");
|
|
@@ -103,6 +122,36 @@ describe("codex gateway route: adapter", () => {
|
|
|
103
122
|
expect(logged.some((line) => line.endsWith("Using credential: gateway"))).toBe(true);
|
|
104
123
|
});
|
|
105
124
|
|
|
125
|
+
test("launches the app-server on the gateway provider, and on the default without a gateway", async () => {
|
|
126
|
+
const launched = async (env: Record<string, string>) => {
|
|
127
|
+
let overrides: unknown = "not launched";
|
|
128
|
+
const adapter = new CodexAdapter({
|
|
129
|
+
bypassSubprocess: true,
|
|
130
|
+
appServerFactory: ({ configOverrides }) => {
|
|
131
|
+
overrides = configOverrides;
|
|
132
|
+
throw new Error("test stops before app-server spawn");
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
await expect(adapter.createSession(testConfig({ env }))).rejects.toThrow(
|
|
136
|
+
"test stops before app-server spawn",
|
|
137
|
+
);
|
|
138
|
+
return overrides;
|
|
139
|
+
};
|
|
140
|
+
expect(await launched({ ...GATEWAY_ENV, OPENAI_API_KEY: "sk-openai" })).toEqual({
|
|
141
|
+
model_provider: "swarm-gateway",
|
|
142
|
+
model_providers: {
|
|
143
|
+
"swarm-gateway": {
|
|
144
|
+
name: "Swarm gateway",
|
|
145
|
+
base_url: "https://sub2api.example.com/v1",
|
|
146
|
+
env_key: "CODEX_GATEWAY_API_KEY",
|
|
147
|
+
wire_api: "responses",
|
|
148
|
+
requires_openai_auth: false,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
expect(await launched({ OPENAI_API_KEY: "sk-openai" })).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
106
155
|
test("a gateway URL on api.openai.com is not a gateway: no provider block", async () => {
|
|
107
156
|
const { codexConfig, env } = await captureSpawn({
|
|
108
157
|
CODEX_GATEWAY_BASE_URL: "https://api.openai.com/v1",
|