@desplega.ai/agent-swarm 1.166.0 → 1.168.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-zhejfecx.js +34 -0
- package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
- package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
- package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.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-20q3nzvg.js} +9 -8
- package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
- package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
- package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
- package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.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-7x7chdrs.js} +7608 -7596
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
- package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
- 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-yshaazs1.js → cli-74ez9b66.js} +63 -20
- 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-fn697bnt.js → cli-9q0bh411.js} +21 -8
- package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
- package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
- package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
- package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
- package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
- package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
- package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
- package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
- 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-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
- package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
- package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
- package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- 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-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
- package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +28 -28
- 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-n63k33d3.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-xkxfd81a.js} +18 -18
- package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
- package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
- package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
- package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
- package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.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-phrndeb8.js} +18 -8
- package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
- package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
- package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
- package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
- package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
- package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
- package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
- package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
- package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
- package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
- package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.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-ngskthwh.js} +10 -9
- package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.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-hhm64r6m.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-35f91nx2.js} +11 -10
- package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
- package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
- package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.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-mxf2c842.js} +27 -27
- package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
- package/openapi.json +72 -1
- package/package.json +10 -10
- 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/script-connections.ts +40 -6
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +14 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/extensions/catalog.generated.json +17 -0
- 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/http/script-connections.ts +35 -0
- package/src/providers/acp-targets.ts +10 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +32 -0
- package/src/providers/claude-background-keepalive.ts +100 -0
- package/src/providers/claude-sdk-session.ts +6 -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 +11 -2
- 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/scheduler/scheduler.ts +82 -13
- package/src/scripts-runtime/executors/registry.ts +1 -1
- 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-background-keepalive-wiring.test.ts +220 -0
- package/src/tests/claude-background-keepalive.fixture.ts +162 -0
- package/src/tests/claude-background-keepalive.test.ts +139 -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/connection-embedded-auth.test.ts +51 -0
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +24 -2
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -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/schedule-target-type.test.ts +136 -0
- package/src/tests/script-connections-http.test.ts +45 -0
- package/src/tests/script-connections-mcp.test.ts +41 -0
- package/src/tests/script-executor-registry.test.ts +18 -0
- package/src/tests/send-task-self-send.test.ts +107 -0
- 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/tools/script-connections/tool.ts +35 -3
- package/src/tools/send-task.ts +9 -1
- 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/extensions/README.md +1 -0
- package/templates/extensions/slack-reply-gate/README.md +98 -0
- package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
- package/templates/extensions/slack-reply-gate/manifest.json +8 -0
- 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/swarm-scripts/SKILL.md +10 -0
- package/templates/skills/swarm-scripts/content.md +10 -0
- 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/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" },
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type ConnectionAuthSummary,
|
|
27
27
|
connectionAuthInputFromFlat,
|
|
28
28
|
connectionAuthSummary,
|
|
29
|
+
deleteScriptConnection,
|
|
29
30
|
findCredentialBindingByIdentity,
|
|
30
31
|
getCredentialBindingById,
|
|
31
32
|
getScriptConnectionById,
|
|
@@ -554,6 +555,27 @@ const setConnectionEnabledRoute = route({
|
|
|
554
555
|
rbac: { permission: "script-connection.manage" },
|
|
555
556
|
});
|
|
556
557
|
|
|
558
|
+
const deleteConnectionRoute = route({
|
|
559
|
+
method: "delete",
|
|
560
|
+
path: "/api/script-connections/{id}",
|
|
561
|
+
pattern: ["api", "script-connections", null],
|
|
562
|
+
operationId: "script_connections_delete",
|
|
563
|
+
summary: "Delete a script connection",
|
|
564
|
+
description:
|
|
565
|
+
"Hard-deletes the connection, its auto-managed credential binding, and any derived inline secret it owns.",
|
|
566
|
+
tags: ["Script Connections"],
|
|
567
|
+
params: idParamsSchema,
|
|
568
|
+
responses: {
|
|
569
|
+
200: {
|
|
570
|
+
description: "Script connection deleted",
|
|
571
|
+
schema: z.object({ deleted: z.literal(true), id: z.string() }),
|
|
572
|
+
},
|
|
573
|
+
403: { description: "Only the lead agent can manage script connections" },
|
|
574
|
+
404: { description: "Script connection not found" },
|
|
575
|
+
},
|
|
576
|
+
rbac: { permission: "script-connection.manage" },
|
|
577
|
+
});
|
|
578
|
+
|
|
557
579
|
const listCredentialBindingsRoute = route({
|
|
558
580
|
method: "get",
|
|
559
581
|
path: "/api/credential-bindings",
|
|
@@ -2111,6 +2133,19 @@ export async function handleScriptConnections(
|
|
|
2111
2133
|
return true;
|
|
2112
2134
|
}
|
|
2113
2135
|
|
|
2136
|
+
if (deleteConnectionRoute.match(req.method, pathSegments)) {
|
|
2137
|
+
const parsed = await deleteConnectionRoute.parse(req, res, pathSegments, queryParams);
|
|
2138
|
+
if (!parsed) return true;
|
|
2139
|
+
if (!(await ensureConnectionAdmin(req, res, agentId))) return true;
|
|
2140
|
+
const deleted = await deleteScriptConnection(parsed.params.id);
|
|
2141
|
+
if (!deleted) {
|
|
2142
|
+
jsonError(res, "Script connection not found.", 404);
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
deleteConnectionRoute.respond(res, 200, { deleted: true, id: deleted.id });
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2114
2149
|
if (listCredentialBindingsRoute.match(req.method, pathSegments)) {
|
|
2115
2150
|
const includeManaged = queryParams.get("includeManaged") === "true";
|
|
2116
2151
|
listCredentialBindingsRoute.respond(res, 200, {
|
|
@@ -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,
|
|
@@ -248,7 +253,7 @@ const geminiTargetProfile: AcpTargetProfile = {
|
|
|
248
253
|
/**
|
|
249
254
|
* Copilot CLI has no system-prompt flag. Under `--acp` it reads custom
|
|
250
255
|
* instructions from the git root, the cwd, and every directory listed in
|
|
251
|
-
* COPILOT_CUSTOM_INSTRUCTIONS_DIRS. Verified against 1.0.
|
|
256
|
+
* COPILOT_CUSTOM_INSTRUCTIONS_DIRS. Verified against 1.0.94: from those extra
|
|
252
257
|
* directories it loads `.github/instructions/*.instructions.md`, but not
|
|
253
258
|
* `AGENTS.md`. So the swarm prompt goes into a per-task directory outside the
|
|
254
259
|
* repo, and the repo's own AGENTS.md is never touched.
|
|
@@ -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.1791547250-gb79b2a";
|
|
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,12 +34,14 @@ 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,
|
|
40
41
|
terminateProcessGroup,
|
|
41
42
|
} from "../utils/process-group";
|
|
42
43
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
44
|
+
import { ClaudeBackgroundKeepalive } from "./claude-background-keepalive";
|
|
43
45
|
import { normalizeClaudeMessage } from "./claude-session-events";
|
|
44
46
|
import { resolveSlashSkillPrompt } from "./codex-skill-resolver";
|
|
45
47
|
import { CTX_MODE_NUDGE_EVERY } from "./ctx-mode-env";
|
|
@@ -738,6 +740,30 @@ export function buildClaudeCodeRuntimeEnv(
|
|
|
738
740
|
};
|
|
739
741
|
}
|
|
740
742
|
|
|
743
|
+
/**
|
|
744
|
+
* `ANTHROPIC_CUSTOM_HEADERS` ("Name: value" per line) with OpenRouter app
|
|
745
|
+
* attribution appended, when `ANTHROPIC_BASE_URL` points at openrouter.ai.
|
|
746
|
+
* The operator's lines are kept in order (they may carry gateway
|
|
747
|
+
* credentials); only lines naming one of our headers are replaced. Returns
|
|
748
|
+
* `{}` when nothing changes, so the inherited value stays byte-identical.
|
|
749
|
+
*
|
|
750
|
+
* Exported for unit testing.
|
|
751
|
+
*/
|
|
752
|
+
export function claudeOpenRouterAttributionEnv(env: Record<string, string | undefined>): {
|
|
753
|
+
ANTHROPIC_CUSTOM_HEADERS?: string;
|
|
754
|
+
} {
|
|
755
|
+
const headers = getOpenRouterAttributionHeaders(env.ANTHROPIC_BASE_URL ?? "", env);
|
|
756
|
+
const names = new Set(Object.keys(headers).map((name) => name.toLowerCase()));
|
|
757
|
+
if (names.size === 0) return {};
|
|
758
|
+
const kept = (env.ANTHROPIC_CUSTOM_HEADERS ?? "").split("\n").filter((line) => {
|
|
759
|
+
const colon = line.indexOf(":");
|
|
760
|
+
const name = colon === -1 ? "" : line.slice(0, colon).trim().toLowerCase();
|
|
761
|
+
return line.trim() !== "" && !names.has(name);
|
|
762
|
+
});
|
|
763
|
+
const ours = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
764
|
+
return { ANTHROPIC_CUSTOM_HEADERS: [...kept, ...ours].join("\n") };
|
|
765
|
+
}
|
|
766
|
+
|
|
741
767
|
export function buildClaudeSessionEnvironment(
|
|
742
768
|
config: ProviderSessionConfig,
|
|
743
769
|
model: string,
|
|
@@ -751,6 +777,7 @@ export function buildClaudeSessionEnvironment(
|
|
|
751
777
|
env: {
|
|
752
778
|
ENABLE_PROMPT_CACHING_1H: "1",
|
|
753
779
|
...sourceEnv,
|
|
780
|
+
...claudeOpenRouterAttributionEnv(sourceEnv),
|
|
754
781
|
...buildClaudeCodeRuntimeEnv(sourceEnv),
|
|
755
782
|
...buildClaudeCodeOtelEnv(sourceEnv),
|
|
756
783
|
...(reasoningApplication.kind === "claude-env" ? reasoningApplication.env : {}),
|
|
@@ -878,6 +905,8 @@ class ClaudeSession implements ProviderSession {
|
|
|
878
905
|
private lastAssistantText = "";
|
|
879
906
|
/** Per-session stream-json transcript used by the parent-owned session summarizer. */
|
|
880
907
|
private transcript: string[];
|
|
908
|
+
/** Keeps the session heartbeat fresh while Claude idles on background tasks. */
|
|
909
|
+
private backgroundKeepalive: ClaudeBackgroundKeepalive;
|
|
881
910
|
readonly deliverSteering?: (delivery: SteerDelivery) => Promise<SteerDeliveryResult>;
|
|
882
911
|
|
|
883
912
|
constructor(
|
|
@@ -899,6 +928,7 @@ class ClaudeSession implements ProviderSession {
|
|
|
899
928
|
this.contextWindowSize = getContextWindowSize(model);
|
|
900
929
|
this.systemPromptFile = systemPromptFile;
|
|
901
930
|
this.transcript = [`User: ${scrubSecrets(config.prompt)}`];
|
|
931
|
+
this.backgroundKeepalive = new ClaudeBackgroundKeepalive(config);
|
|
902
932
|
const cmd = this.buildCommand();
|
|
903
933
|
|
|
904
934
|
console.log(
|
|
@@ -1132,6 +1162,7 @@ class ClaudeSession implements ProviderSession {
|
|
|
1132
1162
|
try {
|
|
1133
1163
|
await Promise.all([stdoutPromise, stderrPromise]);
|
|
1134
1164
|
} finally {
|
|
1165
|
+
this.backgroundKeepalive.stop();
|
|
1135
1166
|
this.closeStdin();
|
|
1136
1167
|
}
|
|
1137
1168
|
await logFileHandle.end();
|
|
@@ -1200,6 +1231,7 @@ class ClaudeSession implements ProviderSession {
|
|
|
1200
1231
|
if (json.type === "result") {
|
|
1201
1232
|
this.closeStdin();
|
|
1202
1233
|
}
|
|
1234
|
+
this.backgroundKeepalive.observe(json);
|
|
1203
1235
|
|
|
1204
1236
|
const normalized = normalizeClaudeMessage(json, {
|
|
1205
1237
|
taskId: this.config.taskId,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a Claude session's heartbeat fresh while it waits on background work.
|
|
3
|
+
*
|
|
4
|
+
* A Claude session that starts a `run_in_background` Bash job (or backgrounds a
|
|
5
|
+
* subagent) can end its turn and sit idle until the job's task_notification
|
|
6
|
+
* wakes it. During that wait it makes no tool calls and streams no output, so
|
|
7
|
+
* neither liveness input (the PostToolUse heartbeat and session-log ingestion)
|
|
8
|
+
* moves `active_sessions.lastHeartbeatAt`, and the stalled-task sweep classifies
|
|
9
|
+
* the live session as stale after `HEARTBEAT_STALL_STALE_HB_MIN`.
|
|
10
|
+
*
|
|
11
|
+
* Claude Code emits `system/background_tasks_changed` with the full set of live
|
|
12
|
+
* background tasks on every membership change (REPLACE semantics). While that
|
|
13
|
+
* set holds at least one non-ambient task, this pings the session heartbeat on
|
|
14
|
+
* an interval. With no background work the timer never runs, so a hung session
|
|
15
|
+
* still goes stale. A background job that never settles is bounded too: the
|
|
16
|
+
* keepalive stops once the session has emitted nothing for
|
|
17
|
+
* `BACKGROUND_KEEPALIVE_MAX_IDLE_MS`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Ping interval; well inside the 15-minute stale-session threshold. */
|
|
21
|
+
export const BACKGROUND_KEEPALIVE_INTERVAL_MS = 60_000;
|
|
22
|
+
/** Longest silent wait on background work that the keepalive will cover. */
|
|
23
|
+
export const BACKGROUND_KEEPALIVE_MAX_IDLE_MS = 2 * 60 * 60_000;
|
|
24
|
+
|
|
25
|
+
type BackgroundTaskEntry = { task_id?: unknown; ambient?: unknown };
|
|
26
|
+
|
|
27
|
+
export interface ClaudeBackgroundKeepaliveOptions {
|
|
28
|
+
apiUrl: string;
|
|
29
|
+
apiKey: string;
|
|
30
|
+
agentId: string;
|
|
31
|
+
taskId: string;
|
|
32
|
+
intervalMs?: number;
|
|
33
|
+
maxIdleMs?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ClaudeBackgroundKeepalive {
|
|
37
|
+
private liveTaskIds: string[] = [];
|
|
38
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
39
|
+
private lastMessageAt = Date.now();
|
|
40
|
+
private stopped = false;
|
|
41
|
+
private readonly intervalMs: number;
|
|
42
|
+
private readonly maxIdleMs: number;
|
|
43
|
+
|
|
44
|
+
constructor(private readonly options: ClaudeBackgroundKeepaliveOptions) {
|
|
45
|
+
this.intervalMs = options.intervalMs ?? BACKGROUND_KEEPALIVE_INTERVAL_MS;
|
|
46
|
+
this.maxIdleMs = options.maxIdleMs ?? BACKGROUND_KEEPALIVE_MAX_IDLE_MS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Feed every protocol message the Claude process emits. */
|
|
50
|
+
observe(message: { type?: string; subtype?: string; tasks?: unknown }): void {
|
|
51
|
+
if (this.stopped) return;
|
|
52
|
+
this.lastMessageAt = Date.now();
|
|
53
|
+
if (message.type === "system" && message.subtype === "background_tasks_changed") {
|
|
54
|
+
const tasks = Array.isArray(message.tasks) ? (message.tasks as BackgroundTaskEntry[]) : [];
|
|
55
|
+
// Ambient tasks (live-update watchers, skip_transcript tasks) are not
|
|
56
|
+
// activity; Claude Code asks hosts to leave them out of activity signals.
|
|
57
|
+
this.liveTaskIds = tasks
|
|
58
|
+
.filter((task) => task && task.ambient !== true && typeof task.task_id === "string")
|
|
59
|
+
.map((task) => task.task_id as string);
|
|
60
|
+
}
|
|
61
|
+
// Any message restarts a keepalive the idle cap stopped, while work is live.
|
|
62
|
+
if (this.liveTaskIds.length > 0) this.start();
|
|
63
|
+
else this.clearTimer();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Stop pinging for good; call when the session ends. */
|
|
67
|
+
stop(): void {
|
|
68
|
+
this.stopped = true;
|
|
69
|
+
this.liveTaskIds = [];
|
|
70
|
+
this.clearTimer();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private start(): void {
|
|
74
|
+
if (this.timer !== undefined) return;
|
|
75
|
+
if (!this.options.apiUrl || !this.options.taskId) return;
|
|
76
|
+
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private tick(): void {
|
|
80
|
+
if (Date.now() - this.lastMessageAt > this.maxIdleMs) {
|
|
81
|
+
this.clearTimer();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { apiUrl, apiKey, agentId, taskId } = this.options;
|
|
85
|
+
void fetch(`${apiUrl}/api/active-sessions/heartbeat/${encodeURIComponent(taskId)}`, {
|
|
86
|
+
method: "PUT",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
Authorization: `Bearer ${apiKey}`,
|
|
90
|
+
"X-Agent-ID": agentId,
|
|
91
|
+
},
|
|
92
|
+
}).catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private clearTimer(): void {
|
|
96
|
+
if (this.timer === undefined) return;
|
|
97
|
+
clearInterval(this.timer);
|
|
98
|
+
this.timer = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
cleanupTaskFile,
|
|
30
30
|
runClaudeSessionSummary,
|
|
31
31
|
} from "./claude-adapter";
|
|
32
|
+
import { ClaudeBackgroundKeepalive } from "./claude-background-keepalive";
|
|
32
33
|
import type { ClaudeProtocolMessage } from "./claude-session-events";
|
|
33
34
|
import { normalizeClaudeMessage } from "./claude-session-events";
|
|
34
35
|
import type {
|
|
@@ -317,8 +318,11 @@ class ClaudeSdkSession implements ProviderSession {
|
|
|
317
318
|
private acceptingInput = true;
|
|
318
319
|
private errorTracker = new SessionErrorTracker();
|
|
319
320
|
private shutdownPromise: Promise<void> | undefined;
|
|
321
|
+
/** Keeps the session heartbeat fresh while Claude idles on background tasks. */
|
|
322
|
+
private backgroundKeepalive: ClaudeBackgroundKeepalive;
|
|
320
323
|
|
|
321
324
|
constructor(private options: CreateClaudeSdkSessionOptions) {
|
|
325
|
+
this.backgroundKeepalive = new ClaudeBackgroundKeepalive(options.config);
|
|
322
326
|
this.model = options.model;
|
|
323
327
|
this.contextWindowSize = getContextWindowSize(options.model);
|
|
324
328
|
this.transcript = [`User: ${scrubSecrets(options.config.prompt)}`];
|
|
@@ -406,6 +410,7 @@ class ClaudeSdkSession implements ProviderSession {
|
|
|
406
410
|
|
|
407
411
|
private handleMessage(message: SDKMessage): boolean {
|
|
408
412
|
const json = message as unknown as ClaudeProtocolMessage;
|
|
413
|
+
this.backgroundKeepalive.observe(json);
|
|
409
414
|
const normalized = normalizeClaudeMessage(json, {
|
|
410
415
|
taskId: this.options.config.taskId,
|
|
411
416
|
agentId: this.options.config.agentId,
|
|
@@ -514,6 +519,7 @@ class ClaudeSdkSession implements ProviderSession {
|
|
|
514
519
|
this.emit({ type: "error", message, category: "sdk_transport" });
|
|
515
520
|
}
|
|
516
521
|
} finally {
|
|
522
|
+
this.backgroundKeepalive.stop();
|
|
517
523
|
this.acceptingInput = false;
|
|
518
524
|
this.input.close();
|
|
519
525
|
if (this.aborted) {
|
|
@@ -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
|
});
|