@desplega.ai/agent-swarm 1.113.0 → 1.115.0
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/README.md +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildRoutingAffinityFromAgent,
|
|
2
3
|
createTaskExtended,
|
|
3
4
|
getActiveTaskCount,
|
|
4
5
|
getAgentById,
|
|
@@ -10,7 +11,7 @@ import {
|
|
|
10
11
|
} from "../be/db";
|
|
11
12
|
import { repointTrackerSyncBySwarmId } from "../be/db-queries/tracker";
|
|
12
13
|
import { resolveTemplate } from "../prompts/resolver";
|
|
13
|
-
import type { AgentTask, ResumeReason, TaskAttachment } from "../types";
|
|
14
|
+
import type { Agent, AgentTask, ResumeReason, TaskAttachment } from "../types";
|
|
14
15
|
import { taskAttachmentDisplayUrl } from "../utils/task-attachment-links";
|
|
15
16
|
// Side-effect import: registers task lifecycle templates in the in-memory registry.
|
|
16
17
|
import "../tools/templates";
|
|
@@ -69,6 +70,26 @@ export const CRASH_RECOVERY_PIN_TAG = "crash-recovery-pin";
|
|
|
69
70
|
*/
|
|
70
71
|
export const GRACEFUL_SHUTDOWN_PIN_TAG = "graceful-shutdown-pin";
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Tag set on a reboot-sweep retry child that was pinned back to its
|
|
75
|
+
* original agent (routing-affinity Phase 3). Same reaper-scoping purpose as
|
|
76
|
+
* `CRASH_RECOVERY_PIN_TAG`/`GRACEFUL_SHUTDOWN_PIN_TAG` — kept in sync with
|
|
77
|
+
* the literal duplicated in `getStalePinnedResumes` (src/be/db.ts).
|
|
78
|
+
*/
|
|
79
|
+
export const REBOOT_RETRY_PIN_TAG = "reboot-retry-pin";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Shared "is this agent even a pin candidate" gate: the row still exists and
|
|
83
|
+
* is not `offline`. Used identically by `createResumeFollowUp`'s same-agent
|
|
84
|
+
* pin and the heartbeat's reboot-retry pin (`runRebootSweep`) — both then
|
|
85
|
+
* apply their own capacity (and, for resumes, freshness) rules on top.
|
|
86
|
+
*/
|
|
87
|
+
export function getPinCandidateAgent(agentId: string): Agent | null {
|
|
88
|
+
const candidate = getAgentById(agentId);
|
|
89
|
+
if (!candidate || candidate.status === "offline") return null;
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
export function getResumeGeneration(task: Pick<AgentTask, "tags">): number {
|
|
73
94
|
const tag = task.tags.find((value) => value.startsWith(RESUME_GENERATION_TAG_PREFIX));
|
|
74
95
|
if (!tag) return 0;
|
|
@@ -264,8 +285,8 @@ export function createResumeFollowUp(args: {
|
|
|
264
285
|
// responsive, so keep requiring `fresh`.
|
|
265
286
|
let preferredAgentId: string | undefined;
|
|
266
287
|
if (parent.agentId) {
|
|
267
|
-
const candidate =
|
|
268
|
-
if (candidate
|
|
288
|
+
const candidate = getPinCandidateAgent(parent.agentId);
|
|
289
|
+
if (candidate) {
|
|
269
290
|
const lastActivity = candidate.lastActivityAt ? Date.parse(candidate.lastActivityAt) : 0;
|
|
270
291
|
const fresh =
|
|
271
292
|
Number.isFinite(lastActivity) &&
|
|
@@ -328,6 +349,17 @@ export function createResumeFollowUp(args: {
|
|
|
328
349
|
// deliberately excluded there so the resume task resolves to the claiming
|
|
329
350
|
// agent's own provider/model — never the parent's concrete model string.
|
|
330
351
|
// We only override what's SPECIFIC to the resume task here.
|
|
352
|
+
//
|
|
353
|
+
// Routing affinity: stamp a FRESH snapshot from the parent's own agent —
|
|
354
|
+
// this covers every leg (pinned AND the pool-fallback legs) so a resume
|
|
355
|
+
// that falls to the unassigned pool is still role/capability-gated. When
|
|
356
|
+
// the agent row is already gone, `buildRoutingAffinityFromAgent` returns
|
|
357
|
+
// `null` and we pass `undefined`, letting `createTaskExtended`'s
|
|
358
|
+
// parentTaskId inheritance block fall back to the parent's OWN
|
|
359
|
+
// (already-inherited) `routingAffinity` instead.
|
|
360
|
+
const routingAffinity = parent.agentId
|
|
361
|
+
? (buildRoutingAffinityFromAgent(parent.agentId) ?? undefined)
|
|
362
|
+
: undefined;
|
|
331
363
|
const created = createTaskExtended(followUpDescription, {
|
|
332
364
|
agentId: preferredAgentId,
|
|
333
365
|
creatorAgentId: parent.creatorAgentId,
|
|
@@ -336,6 +368,7 @@ export function createResumeFollowUp(args: {
|
|
|
336
368
|
tags,
|
|
337
369
|
priority,
|
|
338
370
|
parentTaskId: parent.id,
|
|
371
|
+
routingAffinity,
|
|
339
372
|
});
|
|
340
373
|
|
|
341
374
|
// Repoint Linear / Jira `tracker_sync` rows from the (now terminal) parent
|
|
@@ -447,3 +480,67 @@ export function createRerouteDecisionTask(args: {
|
|
|
447
480
|
|
|
448
481
|
return { kind: "created", task: created };
|
|
449
482
|
}
|
|
483
|
+
|
|
484
|
+
/** Result of `createPoolStarvationDecisionTask`. */
|
|
485
|
+
export type CreatePoolStarvationDecisionResult =
|
|
486
|
+
| { kind: "created"; task: AgentTask }
|
|
487
|
+
| { kind: "skipped"; reason: "lead_not_found" | "duplicate_exists" };
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Hand the Lead a re-delegation DECISION task for a pooled, affinity-tagged
|
|
491
|
+
* task that has sat `unassigned` past `POOL_AFFINITY_ESCALATION_MIN` with
|
|
492
|
+
* ZERO eligible registered agents (routing affinity Phase 3). This is the
|
|
493
|
+
* "queue, then escalate" fallback for the pool legs (7/8) that `createResumeFollowUp`
|
|
494
|
+
* and the reboot-retry pin can fall to — mirrors `createRerouteDecisionTask`'s
|
|
495
|
+
* shape (same `taskType: "reroute-decision"` discriminator, so it reuses the
|
|
496
|
+
* same idempotency check and Slack re-delegation carve-out) but for a task
|
|
497
|
+
* that was never pinned in the first place, so there is no `staleResume` /
|
|
498
|
+
* resume-generation budget to derive.
|
|
499
|
+
*
|
|
500
|
+
* Invoked by the heartbeat (`escalateStarvedPoolTasks`), which has already
|
|
501
|
+
* confirmed no registered agent (any status) satisfies `isAgentEligibleForTask`
|
|
502
|
+
* for this task.
|
|
503
|
+
*/
|
|
504
|
+
export function createPoolStarvationDecisionTask(args: {
|
|
505
|
+
original: AgentTask;
|
|
506
|
+
}): CreatePoolStarvationDecisionResult {
|
|
507
|
+
const { original } = args;
|
|
508
|
+
|
|
509
|
+
const leadAgent = getLeadAgent();
|
|
510
|
+
if (!leadAgent) return { kind: "skipped", reason: "lead_not_found" };
|
|
511
|
+
|
|
512
|
+
if (hasNonTerminalRerouteDecisionChild(original.id)) {
|
|
513
|
+
return { kind: "skipped", reason: "duplicate_exists" };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const affinity = original.routingAffinity;
|
|
517
|
+
const requiredRole = affinity?.role ?? "(none declared)";
|
|
518
|
+
const requiredCapabilities =
|
|
519
|
+
affinity?.capabilities && affinity.capabilities.length > 0
|
|
520
|
+
? affinity.capabilities.join(", ")
|
|
521
|
+
: "(none declared)";
|
|
522
|
+
const attachmentsBlock = formatAttachmentsBlock(getTaskAttachments(original.id));
|
|
523
|
+
|
|
524
|
+
const decision = resolveTemplate("task.pool.starved.decision", {
|
|
525
|
+
original_task_id: original.id,
|
|
526
|
+
task_desc: original.task.slice(0, 200),
|
|
527
|
+
required_role: requiredRole,
|
|
528
|
+
required_capabilities: requiredCapabilities,
|
|
529
|
+
artifacts_block: attachmentsBlock,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const created = createTaskExtended(decision.text, {
|
|
533
|
+
agentId: leadAgent.id,
|
|
534
|
+
creatorAgentId: original.creatorAgentId,
|
|
535
|
+
source: "system",
|
|
536
|
+
taskType: "reroute-decision",
|
|
537
|
+
tags: ["reroute-decision", "pool-starvation"],
|
|
538
|
+
priority: Math.min(100, (original.priority ?? 50) + 10),
|
|
539
|
+
parentTaskId: original.id,
|
|
540
|
+
// Same rationale as createRerouteDecisionTask: don't hold the Lead's
|
|
541
|
+
// re-delegation decision to the original work's output contract.
|
|
542
|
+
inheritParentOutputSchema: false,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return { kind: "created", task: created };
|
|
546
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlinkSync } from "node:fs";
|
|
3
3
|
import { handleMessageReceived } from "../agentmail/handlers";
|
|
4
4
|
import type { AgentMailWebhookPayload } from "../agentmail/types";
|
|
@@ -69,7 +69,19 @@ function makePayload(opts: {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
// Per-test (not per-file) DB reset: bun's default `retry` (bunfig.toml) retries
|
|
73
|
+
// a failing test in place, re-invoking beforeEach/afterEach around each
|
|
74
|
+
// attempt. With a file-level beforeAll/afterAll instead, a transient failure
|
|
75
|
+
// on attempt 1 (of any cause) leaves its side effects (the just-created user)
|
|
76
|
+
// in place for the retry, which then finds that user already resolved and
|
|
77
|
+
// silently no-ops the create — turning one flaky attempt into a deterministic
|
|
78
|
+
// "before+1 != after" mismatch on every subsequent attempt. Resetting to a
|
|
79
|
+
// fresh DB (and re-registering the lead) before every test/retry makes each
|
|
80
|
+
// attempt idempotent regardless of how many times it's retried, and also
|
|
81
|
+
// re-runs initDb's global resolver registration right before this file's own
|
|
82
|
+
// resolveTemplate() calls, minimizing the window for another concurrently
|
|
83
|
+
// executing test file's own initDb() to have repointed it in between.
|
|
84
|
+
beforeEach(async () => {
|
|
73
85
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
74
86
|
try {
|
|
75
87
|
unlinkSync(`${TEST_DB_PATH}${suffix}`);
|
|
@@ -79,9 +91,17 @@ beforeAll(() => {
|
|
|
79
91
|
// Ensure a lead exists so handler's findLeadAgent() returns truthy and a
|
|
80
92
|
// task gets created on the "no inbox mapping" path.
|
|
81
93
|
createAgent({ name: "LeadAgent", isLead: true, status: "idle" });
|
|
94
|
+
// Re-register agentmail templates — prompt-template-resolver.test.ts and
|
|
95
|
+
// prompt-template-session.test.ts call clearTemplateDefinitions() and never
|
|
96
|
+
// restore the shared (process-wide) registry, so if either runs first in
|
|
97
|
+
// the same bun worker, resolveTemplate("agentmail.email.*", ...) silently
|
|
98
|
+
// returns { text: "", skipped: true } here instead of throwing — producing
|
|
99
|
+
// a task with an empty body rather than a visible failure. Same defensive
|
|
100
|
+
// pattern as heartbeat-checklist.test.ts's beforeEach.
|
|
101
|
+
await import(`../agentmail/templates?t=${Date.now()}`);
|
|
82
102
|
});
|
|
83
103
|
|
|
84
|
-
|
|
104
|
+
afterEach(() => {
|
|
85
105
|
closeDb();
|
|
86
106
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
87
107
|
try {
|
|
@@ -111,6 +131,10 @@ describe("handleMessageReceived — identity auto-link via findOrCreateUserByEma
|
|
|
111
131
|
const task = getTaskById(result.taskId!);
|
|
112
132
|
expect(task).not.toBeNull();
|
|
113
133
|
expect(task!.requestedByUserId).toBe(user!.id);
|
|
134
|
+
// The resolved canonical name renders in the task text — never the raw
|
|
135
|
+
// From header (Alice's display name + email as typed by the sender).
|
|
136
|
+
expect(task!.task).toContain("Alice Newcomer (email:alice.newcomer@example.com)");
|
|
137
|
+
expect(task!.task).not.toContain("Alice Newcomer <alice.newcomer@example.com>");
|
|
114
138
|
});
|
|
115
139
|
|
|
116
140
|
test("KNOWN sender (existing users.email) → no duplicate row, auto_merge event, task requestedByUserId populated", async () => {
|
|
@@ -162,5 +186,10 @@ describe("handleMessageReceived — identity auto-link via findOrCreateUserByEma
|
|
|
162
186
|
|
|
163
187
|
const task = getTaskById(result.taskId!);
|
|
164
188
|
expect(task!.requestedByUserId).toBeFalsy();
|
|
189
|
+
// No display name — not even the raw provider label — is ever rendered
|
|
190
|
+
// for an unresolved sender. The task text carries only the non-name
|
|
191
|
+
// sentinel shape, never the raw From header.
|
|
192
|
+
expect(task!.task).toContain("email:unknown (unknown user)");
|
|
193
|
+
expect(task!.task).not.toContain("Unknown Sender (no address)");
|
|
165
194
|
});
|
|
166
195
|
});
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createAgent,
|
|
7
7
|
createApprovalRequest,
|
|
8
8
|
createTaskExtended,
|
|
9
|
+
createUser,
|
|
9
10
|
getAgentCurrentTask,
|
|
10
11
|
getApprovalRequestById,
|
|
11
12
|
getApprovalRequestByStepId,
|
|
@@ -257,6 +258,24 @@ describe("Approval Requests", () => {
|
|
|
257
258
|
const result = createApprovalRequest(data);
|
|
258
259
|
expect(result.questions).toEqual(questions);
|
|
259
260
|
});
|
|
261
|
+
|
|
262
|
+
test("stamps createdBy when provided, round-trips through getApprovalRequestById", () => {
|
|
263
|
+
const requester = createUser({
|
|
264
|
+
name: "Approval Provenance User",
|
|
265
|
+
email: "approval-provenance@example.com",
|
|
266
|
+
});
|
|
267
|
+
const data = makeApprovalData({ createdBy: requester.id });
|
|
268
|
+
const created = createApprovalRequest(data);
|
|
269
|
+
expect(created.createdBy).toBe(requester.id);
|
|
270
|
+
|
|
271
|
+
const fetched = getApprovalRequestById(created.id);
|
|
272
|
+
expect(fetched?.createdBy).toBe(requester.id);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("createdBy is undefined when not provided", () => {
|
|
276
|
+
const result = createApprovalRequest(makeApprovalData());
|
|
277
|
+
expect(result.createdBy).toBeUndefined();
|
|
278
|
+
});
|
|
260
279
|
});
|
|
261
280
|
|
|
262
281
|
describe("DB: getApprovalRequestById", () => {
|
|
@@ -588,6 +607,48 @@ describe("Approval Requests", () => {
|
|
|
588
607
|
expect(created!.workflowRunStepId).toBe(stepId);
|
|
589
608
|
});
|
|
590
609
|
|
|
610
|
+
test("stamps createdBy from meta.requestedByUserId (workflow-run provenance)", async () => {
|
|
611
|
+
const requester = createUser({
|
|
612
|
+
name: "HITL Requester",
|
|
613
|
+
email: "hitl-requester@example.com",
|
|
614
|
+
});
|
|
615
|
+
const stepId = crypto.randomUUID();
|
|
616
|
+
const meta = { ...mockMeta, stepId, requestedByUserId: requester.id };
|
|
617
|
+
const executor = new HumanInTheLoopExecutor(mockDeps);
|
|
618
|
+
|
|
619
|
+
await executor.run({
|
|
620
|
+
config: {
|
|
621
|
+
title: "Deploy approval",
|
|
622
|
+
questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
|
|
623
|
+
approvers: { policy: "any" },
|
|
624
|
+
},
|
|
625
|
+
context: {},
|
|
626
|
+
meta,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const created = getApprovalRequestByStepId(stepId);
|
|
630
|
+
expect(created?.createdBy).toBe(requester.id);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("leaves createdBy unset when the workflow run carries no requester", async () => {
|
|
634
|
+
const stepId = crypto.randomUUID();
|
|
635
|
+
const meta = { ...mockMeta, stepId };
|
|
636
|
+
const executor = new HumanInTheLoopExecutor(mockDeps);
|
|
637
|
+
|
|
638
|
+
await executor.run({
|
|
639
|
+
config: {
|
|
640
|
+
title: "Deploy approval",
|
|
641
|
+
questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
|
|
642
|
+
approvers: { policy: "any" },
|
|
643
|
+
},
|
|
644
|
+
context: {},
|
|
645
|
+
meta,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const created = getApprovalRequestByStepId(stepId);
|
|
649
|
+
expect(created?.createdBy).toBeUndefined();
|
|
650
|
+
});
|
|
651
|
+
|
|
591
652
|
test("idempotency: returns async marker for pending existing request", async () => {
|
|
592
653
|
const stepId = crypto.randomUUID();
|
|
593
654
|
// Pre-create an approval request for this step
|
|
@@ -11,9 +11,12 @@ import { unlink } from "node:fs/promises";
|
|
|
11
11
|
import type { IncomingMessage } from "node:http";
|
|
12
12
|
import { Readable } from "node:stream";
|
|
13
13
|
import { resolveHttpAuditUserId, resolveTaskAuditUserId } from "../be/audit-user";
|
|
14
|
-
import { closeDb, createAgent, createTaskExtended, createUser, initDb } from "../be/db";
|
|
14
|
+
import { closeDb, createAgent, createTaskExtended, createUser, initDb, startTask } from "../be/db";
|
|
15
|
+
import { type IdentityActor, linkIdentity } from "../be/users";
|
|
15
16
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
16
17
|
|
|
18
|
+
const SYSTEM_ACTOR: IdentityActor = { kind: "system", id: "test" };
|
|
19
|
+
|
|
17
20
|
const TEST_DB_PATH = "./test-audit-user.sqlite";
|
|
18
21
|
|
|
19
22
|
let agentId: string;
|
|
@@ -95,6 +98,49 @@ describe("resolveTaskAuditUserId", () => {
|
|
|
95
98
|
test("returns null when owned task has no human requester", () => {
|
|
96
99
|
expect(resolveTaskAuditUserId(noRequesterTaskId, agentId)).toBeNull();
|
|
97
100
|
});
|
|
101
|
+
|
|
102
|
+
test("ambient-task fallback: no sourceTaskId resolves via the caller's current in-progress task", () => {
|
|
103
|
+
const ambientAgent = createAgent({ name: "ambient-agent", isLead: false, status: "idle" });
|
|
104
|
+
const ambientTask = createTaskExtended("ambient task", {
|
|
105
|
+
agentId: ambientAgent.id,
|
|
106
|
+
requestedByUserId: humanUserId,
|
|
107
|
+
});
|
|
108
|
+
startTask(ambientTask.id);
|
|
109
|
+
|
|
110
|
+
expect(resolveTaskAuditUserId(undefined, ambientAgent.id)).toBe(humanUserId);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("ambient-task fallback: no in-progress task for the caller still returns null", () => {
|
|
114
|
+
const idleAgent = createAgent({ name: "idle-agent", isLead: false, status: "idle" });
|
|
115
|
+
expect(resolveTaskAuditUserId(undefined, idleAgent.id)).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("machine-carried external-ID fallback: no requestedByUserId but a linked Slack id resolves the user", () => {
|
|
119
|
+
const slackLinkedUser = createUser({ name: "Slack Fallback User" });
|
|
120
|
+
linkIdentity(slackLinkedUser.id, "slack", "U_AUDIT_FALLBACK", SYSTEM_ACTOR);
|
|
121
|
+
|
|
122
|
+
const fallbackAgent = createAgent({ name: "fallback-agent", isLead: false, status: "idle" });
|
|
123
|
+
const fallbackTask = createTaskExtended("slack-originated task, requester never stamped", {
|
|
124
|
+
agentId: fallbackAgent.id,
|
|
125
|
+
slackUserId: "U_AUDIT_FALLBACK",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(resolveTaskAuditUserId(fallbackTask.id, fallbackAgent.id)).toBe(slackLinkedUser.id);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("machine-carried external-ID fallback: unlinked Slack id still returns null (no guess)", () => {
|
|
132
|
+
const fallbackAgent = createAgent({
|
|
133
|
+
name: "fallback-agent-unlinked",
|
|
134
|
+
isLead: false,
|
|
135
|
+
status: "idle",
|
|
136
|
+
});
|
|
137
|
+
const fallbackTask = createTaskExtended("slack-originated task, unlinked sender", {
|
|
138
|
+
agentId: fallbackAgent.id,
|
|
139
|
+
slackUserId: "U_NEVER_LINKED_AUDIT",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(resolveTaskAuditUserId(fallbackTask.id, fallbackAgent.id)).toBeNull();
|
|
143
|
+
});
|
|
98
144
|
});
|
|
99
145
|
|
|
100
146
|
// ─── resolveHttpAuditUserId ──────────────────────────────────────────────────
|
|
@@ -255,8 +255,8 @@ describe("CodexSession event mapping", () => {
|
|
|
255
255
|
expect(contextUsage).toBeDefined();
|
|
256
256
|
if (contextUsage && contextUsage.type === "context_usage") {
|
|
257
257
|
expect(contextUsage.contextUsedTokens).toBe(150);
|
|
258
|
-
expect(contextUsage.contextTotalTokens).toBe(
|
|
259
|
-
expect(contextUsage.contextPercent).toBeCloseTo((150 /
|
|
258
|
+
expect(contextUsage.contextTotalTokens).toBe(1_050_000);
|
|
259
|
+
expect(contextUsage.contextPercent).toBeCloseTo((150 / 1_050_000) * 100, 6);
|
|
260
260
|
expect(contextUsage.contextFormula).toBe("input-cache-output");
|
|
261
261
|
}
|
|
262
262
|
|
|
@@ -282,7 +282,7 @@ describe("CodexSession event mapping", () => {
|
|
|
282
282
|
expect(result.sessionId).toBe("thread-abc");
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
test("Phase 9: chatty turn
|
|
285
|
+
test("Phase 9: chatty turn uses the models.dev context window under the unified formula", async () => {
|
|
286
286
|
// Phase 9 deliberately swapped Codex's per-adapter peak-proxy formula
|
|
287
287
|
// (`(input - cached) + output`) for the unified `input + output` formula
|
|
288
288
|
// shared with every other provider. The trade-off: a chatty Codex turn
|
|
@@ -303,7 +303,7 @@ describe("CodexSession event mapping", () => {
|
|
|
303
303
|
{
|
|
304
304
|
type: "turn.completed",
|
|
305
305
|
usage: {
|
|
306
|
-
input_tokens: 357142, // total
|
|
306
|
+
input_tokens: 357142, // total exceeded the old hardcoded 200k window
|
|
307
307
|
cached_input_tokens: 278912, // most of input is cache reuse
|
|
308
308
|
output_tokens: 2156,
|
|
309
309
|
},
|
|
@@ -320,11 +320,10 @@ describe("CodexSession event mapping", () => {
|
|
|
320
320
|
const contextUsage = emitted.find((e) => e.type === "context_usage");
|
|
321
321
|
expect(contextUsage).toBeDefined();
|
|
322
322
|
if (contextUsage && contextUsage.type === "context_usage") {
|
|
323
|
-
// Phase 9 unified: input + output = 357142 + 2156 = 359298
|
|
323
|
+
// Phase 9 unified: input + output = 357142 + 2156 = 359298.
|
|
324
324
|
expect(contextUsage.contextUsedTokens).toBe(359298);
|
|
325
|
-
expect(contextUsage.contextTotalTokens).toBe(
|
|
326
|
-
|
|
327
|
-
expect(contextUsage.contextPercent).toBe(100);
|
|
325
|
+
expect(contextUsage.contextTotalTokens).toBe(1_050_000);
|
|
326
|
+
expect(contextUsage.contextPercent).toBeCloseTo((359298 / 1_050_000) * 100, 6);
|
|
328
327
|
expect(contextUsage.contextFormula).toBe("input-cache-output");
|
|
329
328
|
}
|
|
330
329
|
|
|
@@ -438,7 +437,7 @@ describe("CodexSession event mapping", () => {
|
|
|
438
437
|
if (errorEvent && errorEvent.type === "error") {
|
|
439
438
|
expect(errorEvent.message).toContain("[context-overflow]");
|
|
440
439
|
expect(errorEvent.message).toContain("gpt-5.4");
|
|
441
|
-
expect(errorEvent.message).toContain("
|
|
440
|
+
expect(errorEvent.message).toContain("1,050,000 tokens");
|
|
442
441
|
// original error preserved at the end
|
|
443
442
|
expect(errorEvent.message).toContain("context length exceeded");
|
|
444
443
|
}
|
|
@@ -748,16 +747,16 @@ describe("resolveCodexModel", () => {
|
|
|
748
747
|
expect(resolveCodexModel("")).toBe(CODEX_DEFAULT_MODEL);
|
|
749
748
|
});
|
|
750
749
|
|
|
751
|
-
test("claude shortname 'opus' → gpt-5.
|
|
752
|
-
expect(resolveCodexModel("opus")).toBe("gpt-5.
|
|
750
|
+
test("claude shortname 'opus' → gpt-5.6-sol", () => {
|
|
751
|
+
expect(resolveCodexModel("opus")).toBe("gpt-5.6-sol");
|
|
753
752
|
});
|
|
754
753
|
|
|
755
|
-
test("claude shortname 'sonnet' → gpt-5.
|
|
756
|
-
expect(resolveCodexModel("sonnet")).toBe("gpt-5.
|
|
754
|
+
test("claude shortname 'sonnet' → gpt-5.6-terra", () => {
|
|
755
|
+
expect(resolveCodexModel("sonnet")).toBe("gpt-5.6-terra");
|
|
757
756
|
});
|
|
758
757
|
|
|
759
|
-
test("claude shortname 'haiku' → gpt-5.
|
|
760
|
-
expect(resolveCodexModel("haiku")).toBe("gpt-5.
|
|
758
|
+
test("claude shortname 'haiku' → gpt-5.6-luna", () => {
|
|
759
|
+
expect(resolveCodexModel("haiku")).toBe("gpt-5.6-luna");
|
|
761
760
|
});
|
|
762
761
|
|
|
763
762
|
test("passthrough 'gpt-5.4-mini' → gpt-5.4-mini", () => {
|
|
@@ -768,6 +767,12 @@ describe("resolveCodexModel", () => {
|
|
|
768
767
|
expect(resolveCodexModel("gpt-5.5")).toBe("gpt-5.5");
|
|
769
768
|
});
|
|
770
769
|
|
|
770
|
+
test("passthrough GPT-5.6 Codex models", () => {
|
|
771
|
+
expect(resolveCodexModel("gpt-5.6-sol")).toBe("gpt-5.6-sol");
|
|
772
|
+
expect(resolveCodexModel("gpt-5.6-terra")).toBe("gpt-5.6-terra");
|
|
773
|
+
expect(resolveCodexModel("gpt-5.6-luna")).toBe("gpt-5.6-luna");
|
|
774
|
+
});
|
|
775
|
+
|
|
771
776
|
test("passthrough 'gpt-5.3-codex' → gpt-5.3-codex", () => {
|
|
772
777
|
expect(resolveCodexModel("gpt-5.3-codex")).toBe("gpt-5.3-codex");
|
|
773
778
|
});
|
|
@@ -787,24 +792,30 @@ describe("resolveCodexModel", () => {
|
|
|
787
792
|
});
|
|
788
793
|
|
|
789
794
|
describe("getCodexContextWindow", () => {
|
|
790
|
-
test("gpt-5.4 →
|
|
791
|
-
expect(getCodexContextWindow("gpt-5.4")).toBe(
|
|
795
|
+
test("gpt-5.4 → models.dev context", () => {
|
|
796
|
+
expect(getCodexContextWindow("gpt-5.4")).toBe(1_050_000);
|
|
792
797
|
});
|
|
793
798
|
|
|
794
|
-
test("gpt-5.4-mini →
|
|
795
|
-
expect(getCodexContextWindow("gpt-5.4-mini")).toBe(
|
|
799
|
+
test("gpt-5.4-mini → models.dev context", () => {
|
|
800
|
+
expect(getCodexContextWindow("gpt-5.4-mini")).toBe(400_000);
|
|
796
801
|
});
|
|
797
802
|
|
|
798
803
|
test("gpt-5.5 → 1_050_000", () => {
|
|
799
804
|
expect(getCodexContextWindow("gpt-5.5")).toBe(1_050_000);
|
|
800
805
|
});
|
|
801
806
|
|
|
802
|
-
test("
|
|
803
|
-
expect(getCodexContextWindow("gpt-5.
|
|
807
|
+
test("GPT-5.6 Codex models → 1_050_000", () => {
|
|
808
|
+
expect(getCodexContextWindow("gpt-5.6-sol")).toBe(1_050_000);
|
|
809
|
+
expect(getCodexContextWindow("gpt-5.6-terra")).toBe(1_050_000);
|
|
810
|
+
expect(getCodexContextWindow("gpt-5.6-luna")).toBe(1_050_000);
|
|
804
811
|
});
|
|
805
812
|
|
|
806
|
-
test("gpt-5.
|
|
807
|
-
expect(getCodexContextWindow("gpt-5.
|
|
813
|
+
test("gpt-5.3-codex → models.dev context", () => {
|
|
814
|
+
expect(getCodexContextWindow("gpt-5.3-codex")).toBe(400_000);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("gpt-5.2-codex → models.dev context", () => {
|
|
818
|
+
expect(getCodexContextWindow("gpt-5.2-codex")).toBe(400_000);
|
|
808
819
|
});
|
|
809
820
|
});
|
|
810
821
|
|
|
@@ -821,6 +832,12 @@ describe("computeCodexCostUsd", () => {
|
|
|
821
832
|
expect(cost).toBeCloseTo(35, 4);
|
|
822
833
|
});
|
|
823
834
|
|
|
835
|
+
test("GPT-5.6 Codex model pricing matches OpenAI model docs", () => {
|
|
836
|
+
expect(computeCodexCostUsd("gpt-5.6-sol", 1_000_000, 0, 1_000_000)).toBeCloseTo(35, 4);
|
|
837
|
+
expect(computeCodexCostUsd("gpt-5.6-terra", 1_000_000, 0, 1_000_000)).toBeCloseTo(17.5, 4);
|
|
838
|
+
expect(computeCodexCostUsd("gpt-5.6-luna", 1_000_000, 0, 1_000_000)).toBeCloseTo(7, 4);
|
|
839
|
+
});
|
|
840
|
+
|
|
824
841
|
test("gpt-5.4 with cached input applies the cached discount", () => {
|
|
825
842
|
// 1M input, 800k cached → 200k uncached.
|
|
826
843
|
// 200_000 × $2.50/M = $0.50
|
|
@@ -1133,6 +1150,12 @@ describe("buildCodexConfig", () => {
|
|
|
1133
1150
|
expect(merged.model_reasoning_effort).toBe("xhigh");
|
|
1134
1151
|
});
|
|
1135
1152
|
|
|
1153
|
+
test("reasoningEffort: 'max' on gpt-5.6-sol is applied", async () => {
|
|
1154
|
+
globalThis.fetch = stubFetch({ servers: [] });
|
|
1155
|
+
const merged = await buildCodexConfig(cfg({ reasoningEffort: "max" }), "gpt-5.6-sol", () => {});
|
|
1156
|
+
expect(merged.model_reasoning_effort).toBe("max");
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1136
1159
|
test("reasoningEffort: 'xhigh' on gpt-5.1-codex (non-max) is rejected — noop, no model_reasoning_effort key", async () => {
|
|
1137
1160
|
globalThis.fetch = stubFetch({ servers: [] });
|
|
1138
1161
|
const merged = await buildCodexConfig(
|
|
@@ -85,6 +85,7 @@ describe("credential broker", () => {
|
|
|
85
85
|
scope: "global",
|
|
86
86
|
active: true,
|
|
87
87
|
scopeId: null,
|
|
88
|
+
authKind: "config",
|
|
88
89
|
},
|
|
89
90
|
{
|
|
90
91
|
configKey: "QUERY_KEY",
|
|
@@ -93,6 +94,7 @@ describe("credential broker", () => {
|
|
|
93
94
|
scope: "global",
|
|
94
95
|
active: true,
|
|
95
96
|
scopeId: null,
|
|
97
|
+
authKind: "config",
|
|
96
98
|
},
|
|
97
99
|
]);
|
|
98
100
|
});
|
|
@@ -133,13 +135,14 @@ describe("credential broker", () => {
|
|
|
133
135
|
scope: "global",
|
|
134
136
|
scopeId: null,
|
|
135
137
|
active: true,
|
|
138
|
+
authKind: "config",
|
|
136
139
|
placeholder: "[REDACTED:GITHUB_TOKEN]",
|
|
137
140
|
value: "ghp_test",
|
|
138
141
|
},
|
|
139
142
|
]);
|
|
140
143
|
});
|
|
141
144
|
|
|
142
|
-
test("registers resolved broker config values with the scrubber", () => {
|
|
145
|
+
test("registers resolved broker config values with the scrubber", async () => {
|
|
143
146
|
const bindingsConfig = upsertSwarmConfig({
|
|
144
147
|
scope: "global",
|
|
145
148
|
key: CREDENTIAL_BINDINGS_CONFIG_KEY,
|
|
@@ -161,7 +164,7 @@ describe("credential broker", () => {
|
|
|
161
164
|
});
|
|
162
165
|
|
|
163
166
|
try {
|
|
164
|
-
const bindings = buildScriptCredentialBindings({});
|
|
167
|
+
const bindings = await buildScriptCredentialBindings({});
|
|
165
168
|
|
|
166
169
|
expect(bindings.some((binding) => binding.configKey === "VENDOR_SPECIAL_API_KEY")).toBe(true);
|
|
167
170
|
expect(scrubSecrets("echo not_a_standard_token_shape_12345")).toBe(
|