@desplega.ai/agent-swarm 1.166.0 → 1.167.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/dist/acp-adapter-zpvy6321.js +34 -0
- package/dist/{actions-qz3m692w.js → actions-fq2zrwtf.js} +25 -25
- package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-5gbk7501.js} +4 -4
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-43vrdmv8.js → app-nctetn1e.js} +10 -9
- package/dist/{assistant-ertffmka.js → assistant-0aq8gjvj.js} +26 -26
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-998jtpdg.js → boot-reembed-1ze5853n.js} +9 -8
- package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-fmp5ww69.js} +9 -8
- package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-cbp8xd4e.js} +10 -9
- package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-22tx8a53.js} +12 -11
- package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-0jhgeh1d.js} +24 -22
- package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
- package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
- package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-wh77rh83.js} +22 -22
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-dw2q9f7g.js → cli-1yrcfa5s.js} +4 -4
- package/dist/{cli-jvj6a0cv.js → cli-577bxkf6.js} +27 -17
- package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
- package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
- package/dist/{cli-cpfmqz3n.js → cli-6qxmjpem.js} +5 -5
- package/dist/{cli-aqcey741.js → cli-6vfz44sr.js} +1 -1
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-gt8azyet.js → cli-c2tpw9ck.js} +1 -1
- package/dist/{cli-ecknp7p1.js → cli-c8r99akr.js} +10 -10
- package/dist/{cli-3qn4r334.js → cli-cexctgn5.js} +20 -14
- package/dist/{cli-vzc73wet.js → cli-cf2vgn9d.js} +87 -69
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/{cli-j2jbr8af.js → cli-kw0jcfje.js} +1 -1
- package/dist/{cli-8t229cmr.js → cli-m7vmeepg.js} +30 -17
- package/dist/{cli-yshaazs1.js → cli-mkdsy1nq.js} +62 -20
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-0qjcsxgz.js → cli-mxry91sa.js} +30 -9
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-rggdq8wq.js → cli-p8rt8spp.js} +3 -3
- package/dist/{cli-v2a8x45d.js → cli-pkj1w46r.js} +10 -5
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-66zh26sp.js → cli-qzmzwegq.js} +57 -17
- package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-0nhdwxd5.js → cli-tx2zvn23.js} +3 -3
- package/dist/{cli-mwegs4af.js → cli-ty4bzkqr.js} +3 -3
- package/dist/{cli-ysnyhws9.js → cli-v1pwxreq.js} +54 -51
- package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/{cli-df56tqdw.js → cli-wydz3feg.js} +108 -57
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-x7kpr72a.js → cli-y0kjpfm9.js} +2 -2
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-fn697bnt.js → cli-ykt8f7va.js} +21 -8
- package/dist/{cli-4r1sjfc4.js → cli-ymmc1zxg.js} +8 -8
- package/dist/{cli-8kgvmkak.js → cli-ypgba1a8.js} +1 -1
- package/dist/{cli-rsy933yz.js → cli-z1dqhc9y.js} +30 -19
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +26 -26
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-cgesnjkr.js} +18 -18
- package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
- package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-8kd7ddhr.js} +18 -18
- package/dist/{commands-mhxam6z2.js → commands-h6zaaxym.js} +9 -8
- package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
- package/dist/{db-sjks418k.js → db-qg3myn53.js} +9 -8
- package/dist/{dispatcher-1es759rn.js → dispatcher-7vnwj9qw.js} +24 -24
- package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-tz6hfp5r.js} +5 -5
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-jdt2dz2s.js} +18 -8
- package/dist/{handlers-1gkbhktr.js → handlers-2qtkpncr.js} +27 -27
- package/dist/{hook-ds2zxq9v.js → hook-51yfcgsv.js} +42 -31
- package/dist/{hook-vxa3tcsy.js → hook-qnbdgwvd.js} +14 -14
- package/dist/{http-3h21z9md.js → http-jsmtag08.js} +320 -98
- package/dist/{index-9b65j1m5.js → index-2t9kesn1.js} +9 -8
- package/dist/{index-dkfxpkmm.js → index-bt8wkv1w.js} +25 -25
- package/dist/{index-f8xnqrye.js → index-cx1sb9bq.js} +33 -24
- package/dist/{index-bn6cy8ps.js → index-ekywyv2v.js} +24 -24
- package/dist/{index-n99fycq7.js → index-t4g06w93.js} +24 -24
- package/dist/{keepalive-3xspa9jw.js → keepalive-4aezx4bj.js} +11 -10
- package/dist/{lead-6qphc124.js → lead-r44bkpm5.js} +47 -47
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-0teem1bb.js → maintenance-grz5jrzh.js} +9 -8
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
- package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-wm4ctjfj.js} +10 -9
- package/dist/{onboard-54qmnqxe.js → onboard-v60zgqf0.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-ycqzp2a1.js → otel-impl-ghh4dyf8.js} +1 -1
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
- package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-mjrqvnvw.js} +11 -10
- package/dist/{rbac-roles-j78wdksm.js → rbac-roles-axv4d74x.js} +10 -9
- package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-fd5tskj9.js} +9 -8
- package/dist/{render-v2-pxjs49fx.js → render-v2-k50j33qd.js} +10 -9
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-ymej0xtx.js → setup-4syh0pd2.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-3nh4ymzb.js → work-objects-jcdbg8rr.js} +27 -27
- package/dist/{worker-qa9fkz5s.js → worker-x8j8c9sx.js} +47 -47
- package/openapi.json +1 -1
- package/package.json +8 -8
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +13 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/providers/acp-targets.ts +9 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +26 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/dsh-adapter.ts +10 -1
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/server.ts +63 -3
- package/src/tests/acp-adapter.test.ts +152 -12
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/dsh-adapter.test.ts +23 -1
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/request-human-input.ts +7 -2
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-yzfxn8vj.js +0 -23
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
- package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
package/src/commands/runner.ts
CHANGED
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
import { type RelevantMemory, renderMemoriesPrompt } from "../prompts/memories.ts";
|
|
22
22
|
import { configureHttpResolver, resolveTemplateAsync } from "../prompts/resolver.ts";
|
|
23
23
|
import { renderSteeringDelivery } from "../prompts/steering-delivery.ts";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
authJsonToCredentialSelection,
|
|
26
|
+
credentialsToAuthJson,
|
|
27
|
+
} from "../providers/codex-oauth/auth-json.js";
|
|
25
28
|
import { materializeCodexAuthJson } from "../providers/codex-oauth/auth-json-fs.js";
|
|
26
29
|
import {
|
|
27
30
|
CODEX_OAUTH_POOL_SLOTS_ENV,
|
|
@@ -1937,17 +1940,9 @@ export async function resolveCodexOAuthCredentialInfo(
|
|
|
1937
1940
|
await materializeCodexAuthJson(selectedSlot, slotEntry.creds, {
|
|
1938
1941
|
includeRefreshToken: false,
|
|
1939
1942
|
});
|
|
1940
|
-
const authJson = {
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
tokens: {
|
|
1944
|
-
id_token: slotEntry.creds.access,
|
|
1945
|
-
access_token: slotEntry.creds.access,
|
|
1946
|
-
refresh_token: slotEntry.creds.refresh,
|
|
1947
|
-
account_id: slotEntry.creds.accountId,
|
|
1948
|
-
},
|
|
1949
|
-
last_refresh: new Date(slotEntry.creds.expires).toISOString(),
|
|
1950
|
-
};
|
|
1943
|
+
const authJson = credentialsToAuthJson(slotEntry.creds, {
|
|
1944
|
+
includeRefreshToken: false,
|
|
1945
|
+
});
|
|
1951
1946
|
const sel = {
|
|
1952
1947
|
...authJsonToCredentialSelection(authJson, selectedSlot, slots.length),
|
|
1953
1948
|
authFailureFence,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import pkg from "../../package.json";
|
|
1
2
|
import {
|
|
2
3
|
autoCancelStaleApprovalRequests,
|
|
3
4
|
timeoutExpiredApprovalRequests,
|
|
@@ -247,6 +248,15 @@ function isHeartbeatChecklistDisabled(): boolean {
|
|
|
247
248
|
return isEnvFlagEnabled("HEARTBEAT_CHECKLIST_DISABLE", false);
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Whether to create the boot-triage task even when the boot is clean (no
|
|
253
|
+
* interrupted, stalled, orphaned, or resume-less superseded work). Off by
|
|
254
|
+
* default: a clean boot only logs a one-line note.
|
|
255
|
+
*/
|
|
256
|
+
function isBootTriageAlways(): boolean {
|
|
257
|
+
return isEnvFlagEnabled("HEARTBEAT_BOOT_TRIAGE_ALWAYS", false);
|
|
258
|
+
}
|
|
259
|
+
|
|
250
260
|
// ============================================================================
|
|
251
261
|
// Types
|
|
252
262
|
// ============================================================================
|
|
@@ -1576,24 +1586,7 @@ export async function gatherSystemStatus(options?: { isBootTriage?: boolean }):
|
|
|
1576
1586
|
}
|
|
1577
1587
|
|
|
1578
1588
|
// Orphaned pending/offered tasks (assigned to workers with no active session)
|
|
1579
|
-
const orphanedTasks
|
|
1580
|
-
|
|
1581
|
-
for (const status of ["pending", "offered"] as const) {
|
|
1582
|
-
const tasks = await getTasksByStatus(status);
|
|
1583
|
-
|
|
1584
|
-
for (const task of tasks) {
|
|
1585
|
-
// 'pending' tasks carry their holder in agentId; 'offered' tasks have
|
|
1586
|
-
// not been accepted yet, so agentId is still NULL and the offeree
|
|
1587
|
-
// lives in offeredTo instead (#1190) — `!task.agentId` alone used to
|
|
1588
|
-
// skip every offered row here, hiding this whole class of orphan.
|
|
1589
|
-
const holderId = status === "offered" ? task.offeredTo : task.agentId;
|
|
1590
|
-
if (!holderId) continue;
|
|
1591
|
-
const agent = agents.find((a) => a.id === holderId);
|
|
1592
|
-
if (!agent || agent.status === "offline") {
|
|
1593
|
-
orphanedTasks.push(task);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1589
|
+
const orphanedTasks = await getOrphanedPendingOrOfferedTasks(agents);
|
|
1597
1590
|
|
|
1598
1591
|
if (orphanedTasks.length > 0) {
|
|
1599
1592
|
sections.push("");
|
|
@@ -1617,6 +1610,60 @@ export async function gatherSystemStatus(options?: { isBootTriage?: boolean }):
|
|
|
1617
1610
|
return sections.join("\n");
|
|
1618
1611
|
}
|
|
1619
1612
|
|
|
1613
|
+
/** Pending/offered tasks whose holder is offline or no longer registered. */
|
|
1614
|
+
async function getOrphanedPendingOrOfferedTasks(
|
|
1615
|
+
agents: Awaited<ReturnType<typeof getAllAgents>>,
|
|
1616
|
+
): Promise<AgentTask[]> {
|
|
1617
|
+
const orphanedTasks: AgentTask[] = [];
|
|
1618
|
+
|
|
1619
|
+
for (const status of ["pending", "offered"] as const) {
|
|
1620
|
+
const tasks = await getTasksByStatus(status);
|
|
1621
|
+
|
|
1622
|
+
for (const task of tasks) {
|
|
1623
|
+
// 'pending' tasks carry their holder in agentId; 'offered' tasks have
|
|
1624
|
+
// not been accepted yet, so agentId is still NULL and the offeree
|
|
1625
|
+
// lives in offeredTo instead (#1190) — `!task.agentId` alone used to
|
|
1626
|
+
// skip every offered row here, hiding this whole class of orphan.
|
|
1627
|
+
const holderId = status === "offered" ? task.offeredTo : task.agentId;
|
|
1628
|
+
if (!holderId) continue;
|
|
1629
|
+
const agent = agents.find((a) => a.id === holderId);
|
|
1630
|
+
if (!agent || agent.status === "offline") {
|
|
1631
|
+
orphanedTasks.push(task);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
return orphanedTasks;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
export interface BootTriageFindings {
|
|
1640
|
+
rebootInterrupted: number;
|
|
1641
|
+
stalled: number;
|
|
1642
|
+
orphaned: number;
|
|
1643
|
+
supersededWithoutResume: number;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* Count the boot signals the Lead would have to act on. All zero means a
|
|
1648
|
+
* clean boot: nothing was interrupted, stuck, orphaned, or left without a
|
|
1649
|
+
* resume, so a Lead task would only echo the deploy.
|
|
1650
|
+
*/
|
|
1651
|
+
export async function getBootTriageFindings(): Promise<BootTriageFindings> {
|
|
1652
|
+
const now = Date.now();
|
|
1653
|
+
const agents = await getAllAgents();
|
|
1654
|
+
return {
|
|
1655
|
+
rebootInterrupted: getRebootAffectedTasks().length,
|
|
1656
|
+
stalled: (await getStalledInProgressTasks(stallThresholdMinutes())).length,
|
|
1657
|
+
orphaned: (await getOrphanedPendingOrOfferedTasks(agents)).length,
|
|
1658
|
+
supersededWithoutResume: (
|
|
1659
|
+
await getSupersededTasksWithoutResume(
|
|
1660
|
+
new Date(now - ORPHAN_SUPERSEDE_MIN_AGE_MS).toISOString(),
|
|
1661
|
+
new Date(now - ORPHAN_SUPERSEDE_MAX_AGE_MS).toISOString(),
|
|
1662
|
+
)
|
|
1663
|
+
).length,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1620
1667
|
/**
|
|
1621
1668
|
* Check HEARTBEAT.md content and create a checklist task for the lead if needed.
|
|
1622
1669
|
*/
|
|
@@ -1858,6 +1905,8 @@ export function stopHeartbeat(): void {
|
|
|
1858
1905
|
/**
|
|
1859
1906
|
* Create a one-off boot triage task for the lead after a server restart.
|
|
1860
1907
|
* Uses the same HEARTBEAT.md content but with reboot-specific context prepended.
|
|
1908
|
+
* Skipped on a clean boot (see getBootTriageFindings) unless
|
|
1909
|
+
* HEARTBEAT_BOOT_TRIAGE_ALWAYS is set.
|
|
1861
1910
|
*/
|
|
1862
1911
|
export async function createBootTriageTask(): Promise<void> {
|
|
1863
1912
|
const lead = await getLeadAgent();
|
|
@@ -1876,7 +1925,30 @@ export async function createBootTriageTask(): Promise<void> {
|
|
|
1876
1925
|
);
|
|
1877
1926
|
if (existing) return;
|
|
1878
1927
|
|
|
1879
|
-
|
|
1928
|
+
if (!isBootTriageAlways()) {
|
|
1929
|
+
// A failed read falls through to the Lead task: an unknown boot is not a
|
|
1930
|
+
// clean one.
|
|
1931
|
+
const findings = await getBootTriageFindings().catch((err) => {
|
|
1932
|
+
console.error(
|
|
1933
|
+
"[Heartbeat] Boot triage findings failed, creating Lead task:",
|
|
1934
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
1935
|
+
);
|
|
1936
|
+
return null;
|
|
1937
|
+
});
|
|
1938
|
+
if (findings && Object.values(findings).every((count) => count === 0)) {
|
|
1939
|
+
console.log(
|
|
1940
|
+
`[Heartbeat] Clean boot (v${pkg.version}): 0 interrupted, stalled, orphaned, or resume-less superseded tasks; no boot-triage task for lead ${lead.name}`,
|
|
1941
|
+
);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// A reader that failed above can fail again here; the task must still land.
|
|
1947
|
+
const systemStatus = await gatherSystemStatus({ isBootTriage: true }).catch((err) => {
|
|
1948
|
+
const reason = scrubSecrets(err instanceof Error ? err.message : String(err));
|
|
1949
|
+
console.error("[Heartbeat] Boot triage status gathering failed:", reason);
|
|
1950
|
+
return `## System status unavailable\n\nStatus gathering failed after this boot (${reason}). Investigate the task, agent, and DB state directly.`;
|
|
1951
|
+
});
|
|
1880
1952
|
|
|
1881
1953
|
const result = resolveTemplate("heartbeat.boot-triage", {
|
|
1882
1954
|
system_status: systemStatus,
|
package/src/hooks/hook.ts
CHANGED
|
@@ -168,12 +168,25 @@ export async function postHookProfileUpdate({
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
|
-
* Hook
|
|
171
|
+
* Hook JSON responses. Claude Code parses stdout as JSON only when the whole
|
|
172
|
+
* stdout is one JSON object, so these must be the only stdout of the event.
|
|
172
173
|
* See: https://code.claude.com/docs/en/hooks
|
|
173
174
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
type HookBlockResponse =
|
|
176
|
+
| {
|
|
177
|
+
hookSpecificOutput: {
|
|
178
|
+
hookEventName: "PreToolUse";
|
|
179
|
+
permissionDecision: "deny";
|
|
180
|
+
permissionDecisionReason: string;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
| { decision: "block"; reason: string };
|
|
184
|
+
|
|
185
|
+
interface HookContextResponse {
|
|
186
|
+
hookSpecificOutput: {
|
|
187
|
+
hookEventName: "PreToolUse" | "PostToolUse";
|
|
188
|
+
additionalContext: string;
|
|
189
|
+
};
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
/**
|
|
@@ -850,12 +863,33 @@ export async function handleHook(): Promise<void> {
|
|
|
850
863
|
|
|
851
864
|
/**
|
|
852
865
|
* Output a blocking response to stop Claude from continuing.
|
|
853
|
-
*
|
|
866
|
+
* Must be the only stdout of the event: any plain line before it makes
|
|
867
|
+
* Claude Code read the whole stdout as text and run the tool anyway.
|
|
854
868
|
*/
|
|
855
869
|
const outputBlockResponse = (reason: string): void => {
|
|
856
|
-
const response: HookBlockResponse =
|
|
857
|
-
|
|
858
|
-
|
|
870
|
+
const response: HookBlockResponse =
|
|
871
|
+
msg.hook_event_name === "PreToolUse"
|
|
872
|
+
? {
|
|
873
|
+
hookSpecificOutput: {
|
|
874
|
+
hookEventName: "PreToolUse",
|
|
875
|
+
permissionDecision: "deny",
|
|
876
|
+
permissionDecisionReason: reason,
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
: { decision: "block", reason };
|
|
880
|
+
console.log(JSON.stringify(response));
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Output context for the model on a tool event. Plain stdout on PreToolUse
|
|
885
|
+
* and PostToolUse never reaches the model; additionalContext does.
|
|
886
|
+
*/
|
|
887
|
+
const outputToolContext = (
|
|
888
|
+
hookEventName: "PreToolUse" | "PostToolUse",
|
|
889
|
+
additionalContext: string,
|
|
890
|
+
): void => {
|
|
891
|
+
const response: HookContextResponse = {
|
|
892
|
+
hookSpecificOutput: { hookEventName, additionalContext },
|
|
859
893
|
};
|
|
860
894
|
console.log(JSON.stringify(response));
|
|
861
895
|
};
|
|
@@ -988,8 +1022,23 @@ export async function handleHook(): Promise<void> {
|
|
|
988
1022
|
// Get current agent info
|
|
989
1023
|
const agentInfo = await getAgentInfo();
|
|
990
1024
|
|
|
991
|
-
//
|
|
992
|
-
|
|
1025
|
+
// A block must be the only stdout of the event, so a cancelled task is
|
|
1026
|
+
// checked before the status line goes out.
|
|
1027
|
+
if (
|
|
1028
|
+
msg.hook_event_name === "UserPromptSubmit" &&
|
|
1029
|
+
agentInfo &&
|
|
1030
|
+
!agentInfo.isLead &&
|
|
1031
|
+
agentInfo.status === "busy"
|
|
1032
|
+
) {
|
|
1033
|
+
if (await checkAndBlockIfCancelled(true)) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Output agent status with system tray. Skipped on tool events: their plain
|
|
1039
|
+
// stdout never reaches the model and would break a JSON decision.
|
|
1040
|
+
const isToolEvent = msg.hook_event_name === "PreToolUse" || msg.hook_event_name === "PostToolUse";
|
|
1041
|
+
if (agentInfo && !isToolEvent) {
|
|
993
1042
|
// Base status line
|
|
994
1043
|
console.log(
|
|
995
1044
|
`You are registered as ${agentInfo.isLead ? "lead" : "worker"} agent "${agentInfo.name}" (ID: ${agentInfo.id}, status: ${agentInfo.status}).`,
|
|
@@ -1004,7 +1053,7 @@ export async function handleHook(): Promise<void> {
|
|
|
1004
1053
|
}
|
|
1005
1054
|
} else if (
|
|
1006
1055
|
shouldShowRegistrationNudge({
|
|
1007
|
-
agentInfoPresent:
|
|
1056
|
+
agentInfoPresent: Boolean(agentInfo),
|
|
1008
1057
|
eventType: msg.hook_event_name,
|
|
1009
1058
|
hasAgentIdHeader: hasAgentIdHeader(),
|
|
1010
1059
|
})
|
|
@@ -1131,8 +1180,7 @@ export async function handleHook(): Promise<void> {
|
|
|
1131
1180
|
}
|
|
1132
1181
|
|
|
1133
1182
|
// Block `gh pr create|edit` when the body for a public repo leaks internal
|
|
1134
|
-
// identifiers. Exit code 2 blocks the call
|
|
1135
|
-
// above already went to stdout, and stderr reaches the model.
|
|
1183
|
+
// identifiers. Exit code 2 blocks the call and stderr reaches the model.
|
|
1136
1184
|
if (msg.tool_name === "Bash") {
|
|
1137
1185
|
const prBodyBlock = await guardGhPrBody(msg.tool_input, msg.cwd ?? process.cwd());
|
|
1138
1186
|
if (prBodyBlock) {
|
|
@@ -1142,6 +1190,9 @@ export async function handleHook(): Promise<void> {
|
|
|
1142
1190
|
}
|
|
1143
1191
|
}
|
|
1144
1192
|
|
|
1193
|
+
// Emitted last, so a later block stays the only stdout of the event.
|
|
1194
|
+
let loopWarning: string | undefined;
|
|
1195
|
+
|
|
1145
1196
|
// Tool loop detection (workers only, when processing a task)
|
|
1146
1197
|
if (agentInfo && !agentInfo.isLead && agentInfo.status === "busy") {
|
|
1147
1198
|
const loopTaskFile = await readTaskFile();
|
|
@@ -1162,7 +1213,7 @@ export async function handleHook(): Promise<void> {
|
|
|
1162
1213
|
}
|
|
1163
1214
|
|
|
1164
1215
|
if (loopResult.severity === "warning" && loopResult.reason) {
|
|
1165
|
-
|
|
1216
|
+
loopWarning = `Warning: ${loopResult.reason}`;
|
|
1166
1217
|
}
|
|
1167
1218
|
}
|
|
1168
1219
|
}
|
|
@@ -1179,6 +1230,9 @@ export async function handleHook(): Promise<void> {
|
|
|
1179
1230
|
}
|
|
1180
1231
|
}
|
|
1181
1232
|
|
|
1233
|
+
if (loopWarning) {
|
|
1234
|
+
outputToolContext("PreToolUse", loopWarning);
|
|
1235
|
+
}
|
|
1182
1236
|
break;
|
|
1183
1237
|
}
|
|
1184
1238
|
|
|
@@ -1200,7 +1254,8 @@ export async function handleHook(): Promise<void> {
|
|
|
1200
1254
|
method: "PUT",
|
|
1201
1255
|
headers: mcpConfig!.headers,
|
|
1202
1256
|
}).catch((e) => {
|
|
1203
|
-
|
|
1257
|
+
// stderr: stdout on this event is reserved for one JSON object.
|
|
1258
|
+
console.error("Failed to update agent activity timestamp:", e);
|
|
1204
1259
|
});
|
|
1205
1260
|
}
|
|
1206
1261
|
|
|
@@ -1270,7 +1325,8 @@ export async function handleHook(): Promise<void> {
|
|
|
1270
1325
|
if (msg.tool_name?.endsWith("send-task")) {
|
|
1271
1326
|
const maybeTaskId = (msg.tool_response as { task?: { id?: string } })?.task?.id;
|
|
1272
1327
|
|
|
1273
|
-
|
|
1328
|
+
outputToolContext(
|
|
1329
|
+
"PostToolUse",
|
|
1274
1330
|
`Task sent successfully.${maybeTaskId ? ` Task ID: ${maybeTaskId}.` : ""} Monitor progress using the get-task-details tool periodically.`,
|
|
1275
1331
|
);
|
|
1276
1332
|
}
|
|
@@ -1278,17 +1334,6 @@ export async function handleHook(): Promise<void> {
|
|
|
1278
1334
|
}
|
|
1279
1335
|
break;
|
|
1280
1336
|
|
|
1281
|
-
case "UserPromptSubmit": {
|
|
1282
|
-
// For worker agents, check if their task has been cancelled
|
|
1283
|
-
// This catches cancellations at the start of a new iteration
|
|
1284
|
-
if (agentInfo && !agentInfo.isLead && agentInfo.status === "busy") {
|
|
1285
|
-
if (await checkAndBlockIfCancelled(true)) {
|
|
1286
|
-
return; // Exit early
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
break;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
1337
|
case "Stop":
|
|
1293
1338
|
// Clean up any artifact tunnels managed by PM2
|
|
1294
1339
|
try {
|
package/src/http/mcp.ts
CHANGED
|
@@ -244,6 +244,11 @@ export async function handleMcp(
|
|
|
244
244
|
}
|
|
245
245
|
const server = await createServer({
|
|
246
246
|
preloadedTools,
|
|
247
|
+
// Agent row, then global row, then deployment env. A blank agent row
|
|
248
|
+
// resolves to "unset" for that agent.
|
|
249
|
+
enabledTools:
|
|
250
|
+
configs.find((config) => config.key === "SWARM_ENABLED_TOOLS")?.value ??
|
|
251
|
+
process.env.SWARM_ENABLED_TOOLS,
|
|
247
252
|
scriptsOnly: resolveScriptsOnlyMode({
|
|
248
253
|
env: process.env.SCRIPTS_ONLY_MCP,
|
|
249
254
|
configValue,
|
package/src/http/onboarding.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "../be/onboarding";
|
|
22
22
|
import { assertUrlSafe, publicEndpointSsrfOptions } from "../oauth/mcp-wrapper";
|
|
23
23
|
import { ProviderNameSchema } from "../types";
|
|
24
|
+
import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
|
|
24
25
|
import { ensureConfigAdmin } from "./config";
|
|
25
26
|
import { scheduleIntegrationsReload } from "./core";
|
|
26
27
|
import { route } from "./route-def";
|
|
@@ -255,6 +256,7 @@ async function handleMemoryProbe(body: OnboardingMemoryRequest): Promise<Onboard
|
|
|
255
256
|
const client = new OpenAI({
|
|
256
257
|
baseURL: endpoint.toString(),
|
|
257
258
|
apiKey,
|
|
259
|
+
defaultHeaders: getOpenRouterAttributionHeaders(endpoint.toString()),
|
|
258
260
|
timeout: 15_000,
|
|
259
261
|
maxRetries: 0,
|
|
260
262
|
fetchOptions: { redirect: "manual" },
|
|
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { normalizeGrokModel } from "@desplega/model-catalog";
|
|
5
|
+
import type { Config } from "@opencode-ai/sdk";
|
|
5
6
|
import { clampContextPercent, getContextWindowSize } from "../utils/context-window";
|
|
6
7
|
import {
|
|
7
8
|
ACP_TARGET_IDS,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
getAcpTargetCatalogEntry,
|
|
10
11
|
isAcpTarget,
|
|
11
12
|
} from "./acp-target-catalog";
|
|
13
|
+
import { applyOpenRouterBaseUrlOverride } from "./opencode-adapter";
|
|
12
14
|
import { applyReasoningEffort } from "./reasoning-effort";
|
|
13
15
|
import type { CostData, CostModelUsage, ProviderEvent, ProviderSessionConfig } from "./types";
|
|
14
16
|
|
|
@@ -124,8 +126,8 @@ function configuredOptions(config: ProviderSessionConfig): Record<string, string
|
|
|
124
126
|
return options;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
+
function withOpencodeConfig(env: Record<string, string>, model: string): void {
|
|
130
|
+
const key = "OPENCODE_CONFIG_CONTENT";
|
|
129
131
|
let value: Record<string, unknown> = {};
|
|
130
132
|
const existing = env[key];
|
|
131
133
|
if (existing) {
|
|
@@ -138,7 +140,9 @@ function withModelInJsonEnv(env: Record<string, string>, key: string, model: str
|
|
|
138
140
|
console.warn(`\x1b[33m[acp]\x1b[0m Replacing invalid ${key} JSON for model fallback`);
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
|
-
|
|
143
|
+
if (model.trim()) value.model = model.trim();
|
|
144
|
+
applyOpenRouterBaseUrlOverride(value as Config, env);
|
|
145
|
+
env[key] = JSON.stringify(value);
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
function parseCommand(command: string, args: string | undefined): string[] {
|
|
@@ -203,7 +207,8 @@ const opencodeTargetProfile: AcpTargetProfile = {
|
|
|
203
207
|
const env = baseTargetEnv(config);
|
|
204
208
|
const entry = getAcpTargetCatalogEntry("opencode");
|
|
205
209
|
copyEnvKeys(config, env, entry.envKeys);
|
|
206
|
-
|
|
210
|
+
copyEnvKeys(config, env, ["OPENROUTER_APP_ATTRIBUTION"]);
|
|
211
|
+
withOpencodeConfig(env, config.model);
|
|
207
212
|
return env;
|
|
208
213
|
},
|
|
209
214
|
configuredOptions,
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
* default; the image installs exactly this version and the per-task settings
|
|
45
45
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
46
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
47
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1791460855-g1f688c";
|
|
48
48
|
|
|
49
49
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
50
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { withFileLock } from "../utils/file-lock";
|
|
35
35
|
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
36
36
|
import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
|
|
37
|
+
import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
|
|
37
38
|
import {
|
|
38
39
|
detachedProcessGroup,
|
|
39
40
|
registerProcessGroup,
|
|
@@ -738,6 +739,30 @@ export function buildClaudeCodeRuntimeEnv(
|
|
|
738
739
|
};
|
|
739
740
|
}
|
|
740
741
|
|
|
742
|
+
/**
|
|
743
|
+
* `ANTHROPIC_CUSTOM_HEADERS` ("Name: value" per line) with OpenRouter app
|
|
744
|
+
* attribution appended, when `ANTHROPIC_BASE_URL` points at openrouter.ai.
|
|
745
|
+
* The operator's lines are kept in order (they may carry gateway
|
|
746
|
+
* credentials); only lines naming one of our headers are replaced. Returns
|
|
747
|
+
* `{}` when nothing changes, so the inherited value stays byte-identical.
|
|
748
|
+
*
|
|
749
|
+
* Exported for unit testing.
|
|
750
|
+
*/
|
|
751
|
+
export function claudeOpenRouterAttributionEnv(env: Record<string, string | undefined>): {
|
|
752
|
+
ANTHROPIC_CUSTOM_HEADERS?: string;
|
|
753
|
+
} {
|
|
754
|
+
const headers = getOpenRouterAttributionHeaders(env.ANTHROPIC_BASE_URL ?? "", env);
|
|
755
|
+
const names = new Set(Object.keys(headers).map((name) => name.toLowerCase()));
|
|
756
|
+
if (names.size === 0) return {};
|
|
757
|
+
const kept = (env.ANTHROPIC_CUSTOM_HEADERS ?? "").split("\n").filter((line) => {
|
|
758
|
+
const colon = line.indexOf(":");
|
|
759
|
+
const name = colon === -1 ? "" : line.slice(0, colon).trim().toLowerCase();
|
|
760
|
+
return line.trim() !== "" && !names.has(name);
|
|
761
|
+
});
|
|
762
|
+
const ours = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
763
|
+
return { ANTHROPIC_CUSTOM_HEADERS: [...kept, ...ours].join("\n") };
|
|
764
|
+
}
|
|
765
|
+
|
|
741
766
|
export function buildClaudeSessionEnvironment(
|
|
742
767
|
config: ProviderSessionConfig,
|
|
743
768
|
model: string,
|
|
@@ -751,6 +776,7 @@ export function buildClaudeSessionEnvironment(
|
|
|
751
776
|
env: {
|
|
752
777
|
ENABLE_PROMPT_CACHING_1H: "1",
|
|
753
778
|
...sourceEnv,
|
|
779
|
+
...claudeOpenRouterAttributionEnv(sourceEnv),
|
|
754
780
|
...buildClaudeCodeRuntimeEnv(sourceEnv),
|
|
755
781
|
...buildClaudeCodeOtelEnv(sourceEnv),
|
|
756
782
|
...(reasoningApplication.kind === "claude-env" ? reasoningApplication.env : {}),
|
|
@@ -147,6 +147,8 @@ export type ThreadEvent =
|
|
|
147
147
|
usage?: Usage;
|
|
148
148
|
}
|
|
149
149
|
| { type: "turn.interrupted"; usage?: Usage }
|
|
150
|
+
/** Latest single-request usage: a context-window snapshot, not billable usage. */
|
|
151
|
+
| { type: "context.updated"; usage: Usage; contextWindow?: number }
|
|
150
152
|
| { type: "error"; message: string; codexErrorInfo?: string | null; willRetry?: boolean };
|
|
151
153
|
|
|
152
154
|
interface CodexThread {
|
|
@@ -833,8 +835,25 @@ class AppServerCodexThread implements CodexThread {
|
|
|
833
835
|
}
|
|
834
836
|
break;
|
|
835
837
|
case "thread/tokenUsage/updated": {
|
|
836
|
-
const tokenUsage = params.tokenUsage as
|
|
838
|
+
const tokenUsage = params.tokenUsage as
|
|
839
|
+
| { total?: unknown; last?: unknown; modelContextWindow?: unknown }
|
|
840
|
+
| undefined;
|
|
841
|
+
// `total` is cumulative for the thread and drives cost accounting at
|
|
842
|
+
// turn end. `last` is the most recent model request, which is the
|
|
843
|
+
// only figure that reflects what currently sits in the context window.
|
|
837
844
|
this.latestTotalUsage = normalizeUsage(tokenUsage?.total);
|
|
845
|
+
const last = normalizeUsage(tokenUsage?.last);
|
|
846
|
+
if (last) {
|
|
847
|
+
push({
|
|
848
|
+
type: "context.updated",
|
|
849
|
+
usage: last,
|
|
850
|
+
contextWindow:
|
|
851
|
+
typeof tokenUsage?.modelContextWindow === "number" &&
|
|
852
|
+
tokenUsage.modelContextWindow > 0
|
|
853
|
+
? tokenUsage.modelContextWindow
|
|
854
|
+
: undefined,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
838
857
|
break;
|
|
839
858
|
}
|
|
840
859
|
case "error": {
|
|
@@ -1394,6 +1413,9 @@ export class CodexSession implements ProviderSession {
|
|
|
1394
1413
|
case "turn.interrupted":
|
|
1395
1414
|
this.accountUsage(event.usage);
|
|
1396
1415
|
break;
|
|
1416
|
+
case "context.updated":
|
|
1417
|
+
this.emitContextUsage(event.usage, event.contextWindow ?? this.contextWindow);
|
|
1418
|
+
break;
|
|
1397
1419
|
case "error": {
|
|
1398
1420
|
const { message } = this.formatTerminalError(event.message, event.codexErrorInfo);
|
|
1399
1421
|
if (event.willRetry === true) {
|
|
@@ -1420,7 +1442,12 @@ export class CodexSession implements ProviderSession {
|
|
|
1420
1442
|
this.accumulatedUsage.reasoning_output_tokens + usage.reasoning_output_tokens,
|
|
1421
1443
|
}
|
|
1422
1444
|
: usage;
|
|
1445
|
+
// No context_usage here: a turn's usage sums every model request in the
|
|
1446
|
+
// turn, so it measures consumption, not window occupancy. Context comes
|
|
1447
|
+
// from the per-request `context.updated` snapshots instead.
|
|
1448
|
+
}
|
|
1423
1449
|
|
|
1450
|
+
private emitContextUsage(usage: Usage, contextWindow: number): void {
|
|
1424
1451
|
// Codex input tokens already include the cached read and write subsets.
|
|
1425
1452
|
// Passing either subset again would inflate context usage.
|
|
1426
1453
|
const contextUsed = computeContextUsedUnified({
|
|
@@ -1432,8 +1459,8 @@ export class CodexSession implements ProviderSession {
|
|
|
1432
1459
|
this.emit({
|
|
1433
1460
|
type: "context_usage",
|
|
1434
1461
|
contextUsedTokens: contextUsed,
|
|
1435
|
-
contextTotalTokens:
|
|
1436
|
-
contextPercent: clampContextPercent(contextUsed,
|
|
1462
|
+
contextTotalTokens: contextWindow,
|
|
1463
|
+
contextPercent: clampContextPercent(contextUsed, contextWindow) ?? 0,
|
|
1437
1464
|
outputTokens: usage.output_tokens,
|
|
1438
1465
|
contextFormula: CONTEXT_FORMULA,
|
|
1439
1466
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Conversion utilities between CodexOAuthCredentials (our internal type)
|
|
3
3
|
* and ~/.codex/auth.json (the format the Codex CLI reads natively).
|
|
4
4
|
*
|
|
5
|
-
* The Codex CLI expects auth.json
|
|
5
|
+
* The Codex CLI expects these native auth.json fields:
|
|
6
6
|
* {
|
|
7
7
|
* "auth_mode": "chatgpt",
|
|
8
8
|
* "OPENAI_API_KEY": null,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* },
|
|
15
15
|
* "last_refresh": "<ISO 8601>"
|
|
16
16
|
* }
|
|
17
|
+
* Swarm also writes optional `expires` metadata for lossless conversion.
|
|
17
18
|
*
|
|
18
19
|
* Note: `id_token` is set to the `access_token` value because the token
|
|
19
20
|
* exchange endpoint doesn't return a separate `id_token`. This matches
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import { codexPlanFromClaim } from "../../utils/subscription-plans.js";
|
|
26
|
-
import { extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
|
|
27
|
+
import { decodeJwt, extractChatgptPlanType, extractChatgptUserId } from "./flow.js";
|
|
27
28
|
import type { CodexAuthJson, CodexOAuthCredentials } from "./types.js";
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -96,15 +97,31 @@ export function credentialsToAuthJson(
|
|
|
96
97
|
refresh_token: includeRefreshToken ? creds.refresh : "",
|
|
97
98
|
account_id: creds.accountId,
|
|
98
99
|
},
|
|
99
|
-
last_refresh: new Date(
|
|
100
|
+
last_refresh: new Date().toISOString(),
|
|
101
|
+
// Swarm-only metadata keeps opaque-token round trips lossless. Codex
|
|
102
|
+
// ignores this field and may drop it when it rewrites auth.json.
|
|
103
|
+
expires: creds.expires,
|
|
100
104
|
};
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
export function authJsonToCredentials(auth: CodexAuthJson): CodexOAuthCredentials {
|
|
108
|
+
// A CLI refresh may replace the access token and discard our metadata.
|
|
109
|
+
// Prefer its JWT expiry, which describes the token actually in the file.
|
|
110
|
+
const exp = decodeJwt(auth.tokens.access_token)?.exp;
|
|
111
|
+
const jwtExpires = typeof exp === "number" ? exp * 1000 : NaN;
|
|
112
|
+
const expires =
|
|
113
|
+
Number.isFinite(jwtExpires) && jwtExpires >= 0
|
|
114
|
+
? jwtExpires
|
|
115
|
+
: typeof auth.expires === "number" && Number.isFinite(auth.expires)
|
|
116
|
+
? auth.expires
|
|
117
|
+
: new Date(auth.last_refresh).getTime();
|
|
118
|
+
|
|
104
119
|
return {
|
|
105
120
|
access: auth.tokens.access_token,
|
|
106
121
|
refresh: auth.tokens.refresh_token,
|
|
107
|
-
|
|
122
|
+
// Legacy swarm files put expiry in last_refresh; keep that fallback
|
|
123
|
+
// only when neither the token nor the explicit metadata carries it.
|
|
124
|
+
expires,
|
|
108
125
|
accountId: auth.tokens.account_id,
|
|
109
126
|
};
|
|
110
127
|
}
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
} from "../utils/context-window";
|
|
10
10
|
import { addDshStepUsage, type DshStepUsage, normalizeDshStepUsage } from "../utils/dsh-usage";
|
|
11
11
|
import { swarmRuntimeInstanceId } from "../utils/multi-runtime";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getOpenRouterAttributionHeaders,
|
|
14
|
+
getOpenRouterBaseUrl,
|
|
15
|
+
} from "../utils/openrouter-base-url";
|
|
13
16
|
import {
|
|
14
17
|
detachedProcessGroup,
|
|
15
18
|
registerProcessGroup,
|
|
@@ -342,6 +345,9 @@ export class DshAdapter implements ProviderAdapter {
|
|
|
342
345
|
const directory = await mkdtemp(join(tmpdir(), "swarm-dsh-"));
|
|
343
346
|
try {
|
|
344
347
|
const patchPath = join(directory, "patch.json");
|
|
348
|
+
const openRouterHeaders = openrouter
|
|
349
|
+
? getOpenRouterAttributionHeaders(getOpenRouterBaseUrl(env), env)
|
|
350
|
+
: {};
|
|
345
351
|
const runtimeInstanceId = swarmRuntimeInstanceId();
|
|
346
352
|
// JSON is valid YAML. Values stay data, including arbitrary system prompts.
|
|
347
353
|
await writeFile(
|
|
@@ -364,6 +370,9 @@ export class DshAdapter implements ProviderAdapter {
|
|
|
364
370
|
openrouter: {
|
|
365
371
|
apiKeyEnv: keyEnv,
|
|
366
372
|
baseURL: getOpenRouterBaseUrl(env),
|
|
373
|
+
...(Object.keys(openRouterHeaders).length > 0
|
|
374
|
+
? { headers: openRouterHeaders }
|
|
375
|
+
: {}),
|
|
367
376
|
api: "openai-completions",
|
|
368
377
|
// Declare the selected ID even if the bundled catalog predates it.
|
|
369
378
|
models: [
|