@desplega.ai/agent-swarm 1.149.0 → 1.150.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 +7 -3
- package/dist/{acp-adapter-4jncb126.js → acp-adapter-d5mzggyk.js} +1 -1
- package/dist/{actions-31vp3ae7.js → actions-wn6ew0p7.js} +11 -10
- package/dist/{anthropic-messages-vs2rabzp.js → anthropic-messages-ec35ktvx.js} +4 -4
- package/dist/{app-1hh67x8w.js → app-dty7p9hg.js} +7 -7
- package/dist/{artifact-gj9arwrr.js → artifact-td4q4dch.js} +1 -1
- package/dist/{assistant-z66dmz6p.js → assistant-q4vhgjtx.js} +16 -17
- package/dist/{boot-reembed-wxa8ya0s.js → boot-reembed-92292pgf.js} +9 -9
- package/dist/{boot-reembed-fqzxxzjh.js → boot-reembed-n15q6s9x.js} +8 -8
- package/dist/{boot-scrub-logs-jnyndh79.js → boot-scrub-logs-44pc955m.js} +5 -5
- package/dist/{claude-adapter-3qpa9w0j.js → claude-adapter-cjegjbje.js} +14 -14
- package/dist/{claude-managed-adapter-qvsmh6mv.js → claude-managed-adapter-p3ts5m9m.js} +2 -2
- package/dist/{claude-sdk-session-2yxgjs5d.js → claude-sdk-session-7ywbn12v.js} +14 -14
- package/dist/{cli-cr12paw2.js → cli-111ey3ra.js} +5 -5
- package/dist/{cli-4wtq5jjv.js → cli-1mx65de9.js} +1 -1
- package/dist/{cli-9d8ntetj.js → cli-2658f6at.js} +1 -1
- package/dist/{cli-n3dz2942.js → cli-3ckzxdt1.js} +10 -6
- package/dist/{cli-yb9qhqam.js → cli-3mh119qv.js} +184 -67
- package/dist/{cli-ykxrnd1q.js → cli-72xdszyx.js} +2 -2
- package/dist/{cli-xm87hzbw.js → cli-7fesa1ky.js} +1 -1
- package/dist/{cli-3q5ejx9p.js → cli-7kwmt3r5.js} +2 -2
- package/dist/{cli-2qts1hys.js → cli-8chp8xab.js} +2 -2
- package/dist/{cli-qaacms9y.js → cli-9gqddmxs.js} +122 -97
- package/dist/{cli-zvz62chv.js → cli-a45d5v9b.js} +4 -4
- package/dist/{cli-9xvdm3bx.js → cli-asvrh7kj.js} +1 -1
- package/dist/{cli-kgz4np97.js → cli-b8x19vbh.js} +4 -4
- package/dist/{cli-9fy6nk6g.js → cli-ceb5y160.js} +1 -1
- package/dist/{cli-k0cr4kat.js → cli-ck1vqzdj.js} +1 -1
- package/dist/{cli-e29gmdmw.js → cli-dv6cdjs2.js} +2 -1
- package/dist/{cli-jecc01cb.js → cli-dyx0237s.js} +1 -1
- package/dist/{cli-kjwt6hdf.js → cli-gm0fhz13.js} +3 -3
- package/dist/{cli-sd5pv3b1.js → cli-h505yhex.js} +2 -2
- package/dist/{handlers-nbzdzn9k.js → cli-hbagregr.js} +15 -38
- package/dist/{cli-08b07b4r.js → cli-hmyynq9b.js} +1 -1
- package/dist/{cli-d8ftsp62.js → cli-kt7wevqh.js} +20 -13
- package/dist/{cli-gjhjfeeg.js → cli-kyysm7m3.js} +1 -1
- package/dist/{cli-1ebpar1q.js → cli-mzb0n642.js} +6 -6
- package/dist/{cli-q50ef8g0.js → cli-n1yj5fd5.js} +1 -1
- package/dist/{cli-3apbzgyk.js → cli-n75jt793.js} +13 -9
- package/dist/{cli-19f354qr.js → cli-q04q68be.js} +2 -2
- package/dist/{cli-yab68w40.js → cli-qp69dd5d.js} +29 -14
- package/dist/{cli-6pqcb4q0.js → cli-rnsr8asm.js} +12 -11
- package/dist/{cli-ygweqcn4.js → cli-rwp3ksm7.js} +91 -108
- package/dist/{cli-phsxnkp6.js → cli-ryz8q8p1.js} +74 -58
- package/dist/{cli-e95cx1eb.js → cli-taym3dmg.js} +10 -10
- package/dist/{cli-nye12xk5.js → cli-ttje5a4z.js} +7 -4
- package/dist/{cli-9hspd3dp.js → cli-vr6qj104.js} +1 -1
- package/dist/{cli-mnmsfd1w.js → cli-xjb9wghj.js} +7 -3
- package/dist/{cli-gv88e1vf.js → cli-ydk0wjj8.js} +1 -1
- package/dist/{cli-aae6hz6a.js → cli-zd21jrrc.js} +3 -3
- package/dist/{cli-7smrsr25.js → cli-zthatyrx.js} +23 -11
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-8meawydj.js → codex-adapter-rn3aq7jm.js} +7 -7
- package/dist/{codex-hook-hfrt9shr.js → codex-hook-gthcmm0g.js} +3 -3
- package/dist/{codex-session-runner-gjgxyp5j.js → codex-session-runner-bge0cbsw.js} +7 -7
- package/dist/{commands-hz3nb95f.js → commands-26phw0pc.js} +5 -5
- package/dist/{db-ggz97zbm.js → db-jxjx6s1b.js} +6 -6
- package/dist/{e2b-p04vmtx0.js → e2b-413b8t4v.js} +1 -1
- package/dist/handlers-vr1djg4n.js +41 -0
- package/dist/{hook-bks2q1f6.js → hook-jw0ztn96.js} +9 -9
- package/dist/{hook-kq9wp0ep.js → hook-t6fqsggw.js} +10 -10
- package/dist/{http-hrf77v87.js → http-hyc7s57s.js} +80 -71
- package/dist/{index-n28pw1zt.js → index-1d9r6a74.js} +18 -18
- package/dist/{index-cqtebbhb.js → index-bcyx5djw.js} +17 -17
- package/dist/{index-6nsbg88z.js → index-sfhaq59d.js} +408 -74
- package/dist/{index-8hmeg8j1.js → index-wtwpthe1.js} +17 -17
- package/dist/{keepalive-dxc12gvg.js → keepalive-fbsak4ya.js} +7 -7
- package/dist/{lead-yckfn18d.js → lead-rdzdm627.js} +39 -39
- package/dist/{maintenance-6kfb2m5e.js → maintenance-25z1c1vf.js} +9 -9
- package/dist/{node-x1g4v0pq.js → node-209sxmg3.js} +9 -9
- package/dist/{oauth-refresh-sweep-1x1ac722.js → oauth-refresh-sweep-j3d7gtse.js} +6 -6
- package/dist/{onboard-dcyt1609.js → onboard-dkcwrc75.js} +2 -2
- package/dist/{otel-impl-tbk3e5dr.js → otel-impl-8rgyy3j8.js} +1 -1
- package/dist/{pi-mono-adapter-zvshk5cj.js → pi-mono-adapter-4s2bbkgq.js} +8 -8
- package/dist/{pricing-refresh-kaqg567e.js → pricing-refresh-9cek71nq.js} +7 -7
- package/dist/{rbac-roles-cf97v1vd.js → rbac-roles-vk8mqp9v.js} +6 -6
- package/dist/{rbac-roles-ammq6rd2.js → rbac-roles-z98s2ypy.js} +5 -5
- package/dist/{render-v2-80w603vy.js → render-v2-3j26g0qh.js} +7 -7
- package/dist/{seed-pricing-zbrn83xv.js → seed-pricing-7rxhcnzm.js} +6 -6
- package/dist/{setup-fe66kczs.js → setup-e2t7vz0c.js} +2 -2
- package/dist/work-objects-8d3nd3x2.js +141 -0
- package/dist/{worker-qmb2d1e2.js → worker-0x9fn7nq.js} +39 -39
- package/openapi.json +15 -4
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +5 -0
- package/src/be/budget-refusal-notify.ts +1 -0
- package/src/be/db/tasks/read.ts +3 -0
- package/src/be/db/tasks/write.ts +13 -1
- package/src/be/db.ts +4 -2
- package/src/be/migrations/153_deferred_task_wait_members.sql +25 -0
- package/src/be/migrations/154_routing_source.sql +3 -0
- package/src/be/scripts/typecheck.ts +7 -3
- package/src/be/seed/automation-toggle.ts +69 -0
- package/src/be/seed/schedules-seeder.ts +53 -8
- package/src/be/seed/workflows-seeder.ts +35 -6
- package/src/be/seed-scripts/catalog/delegate.ts +3 -1
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/steering.ts +1 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/credential-refresh.ts +70 -0
- package/src/commands/provider-credentials.ts +2 -25
- package/src/commands/runner.ts +23 -56
- package/src/github/handlers.ts +9 -0
- package/src/gitlab/handlers.ts +4 -0
- package/src/heartbeat/heartbeat.ts +3 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +1 -0
- package/src/http/config.ts +4 -1
- package/src/http/core.ts +4 -2
- package/src/http/index.ts +16 -3
- package/src/http/script-runs.ts +5 -0
- package/src/http/tasks.ts +5 -0
- package/src/integrations/kapso/inbound.ts +1 -0
- package/src/jira/sync.ts +2 -0
- package/src/linear/sync.ts +2 -0
- package/src/prompts/session-templates.ts +13 -9
- package/src/providers/claude-adapter.ts +21 -8
- package/src/scheduler/deferred-task-waits.ts +34 -5
- package/src/scheduler/schedule-task.ts +5 -1
- package/src/scheduler/scheduler.ts +104 -123
- package/src/scripts-runtime/types/stdlib.d.ts +7 -3
- package/src/scripts-runtime/types/swarm-sdk.d.ts +7 -3
- package/src/slack/HEURISTICS.md +2 -0
- package/src/slack/actions.ts +1 -0
- package/src/slack/app.ts +2 -0
- package/src/slack/assistant.ts +4 -12
- package/src/slack/handlers.ts +9 -14
- package/src/slack/inbound-files.ts +3 -3
- package/src/slack/render-v2.ts +17 -3
- package/src/slack/thread-buffer.ts +66 -23
- package/src/slack/work-objects.test.ts +252 -0
- package/src/slack/work-objects.ts +118 -0
- package/src/tasks/worker-follow-up.ts +4 -0
- package/src/tests/acp-adapter.test.ts +10 -10
- package/src/tests/agent-fs-live-url.test.ts +40 -0
- package/src/tests/agent-fs-provision-seeder.test.ts +12 -12
- package/src/tests/api-key-tracking.test.ts +18 -18
- package/src/tests/api-key.test.ts +4 -2
- package/src/tests/apps-rbac.test.ts +1 -1
- package/src/tests/apps-spike5.test.ts +1 -1
- package/src/tests/apps-sync-engine.test.ts +8 -8
- package/src/tests/apps-user-config.test.ts +1 -1
- package/src/tests/artifact-sdk.test.ts +8 -8
- package/src/tests/asset-key-mcp.test.ts +2 -0
- package/src/tests/automation-preflight-alert.test.ts +1 -1
- package/src/tests/base-prompt.test.ts +6 -6
- package/src/tests/budget-user-scope.test.ts +1 -1
- package/src/tests/budgets-routes.test.ts +1 -1
- package/src/tests/claude-adapter-binary.test.ts +22 -40
- package/src/tests/claude-adapter-otel.test.ts +13 -7
- package/src/tests/claude-adapter.test.ts +8 -6
- package/src/tests/claude-hook-credentials.test.ts +3 -3
- package/src/tests/claude-managed-adapter.test.ts +4 -4
- package/src/tests/claude-managed-setup.test.ts +3 -3
- package/src/tests/claude-sdk-lifecycle.test.ts +1 -1
- package/src/tests/claude-sdk-transport.test.ts +6 -6
- package/src/tests/claude-stop-hook.test.ts +8 -8
- package/src/tests/claude-worker-parity.test.ts +3 -3
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +5 -5
- package/src/tests/codex-adapter.test.ts +7 -5
- package/src/tests/codex-app-server.test.ts +2 -2
- package/src/tests/codex-login.test.ts +15 -15
- package/src/tests/codex-oauth-credential-info.test.ts +4 -4
- package/src/tests/codex-oauth-keep-warm.test.ts +4 -4
- package/src/tests/codex-oauth-refresh-lock.test.ts +1 -1
- package/src/tests/codex-oauth-storage.test.ts +7 -7
- package/src/tests/codex-pool.test.ts +1 -1
- package/src/tests/codex-steering-hook.test.ts +1 -1
- package/src/tests/codex-swarm-events.test.ts +1 -1
- package/src/tests/config-api-only-keys.test.ts +1 -1
- package/src/tests/config-session-auth.test.ts +2 -2
- package/src/tests/connection-embedded-auth.test.ts +20 -18
- package/src/tests/core-auth.test.ts +1 -1
- package/src/tests/create-page-tool.test.ts +3 -3
- package/src/tests/credential-refresh.test.ts +115 -0
- package/src/tests/credential-wait.test.ts +2 -2
- package/src/tests/credentials.test.ts +6 -4
- package/src/tests/db-queries-oauth.test.ts +40 -40
- package/src/tests/defer-task.test.ts +73 -8
- package/src/tests/deferred-task-wait-migration.test.ts +64 -0
- package/src/tests/deferred-task-wake.test.ts +190 -1
- package/src/tests/devin-adapter.test.ts +1 -1
- package/src/tests/devin-api.test.ts +1 -1
- package/src/tests/e2b-dispatch.test.ts +21 -19
- package/src/tests/e2e-claude-transport.test.ts +3 -3
- package/src/tests/e2e-redact.test.ts +6 -6
- package/src/tests/ensure-token.test.ts +68 -68
- package/src/tests/entrypoint-api-readiness.test.ts +1 -1
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +13 -13
- package/src/tests/events-http.test.ts +1 -1
- package/src/tests/fs-routes.test.ts +1 -1
- package/src/tests/gitlab-auth.test.ts +10 -10
- package/src/tests/harness-provider-resolution.test.ts +8 -5
- package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
- package/src/tests/helper-credentials.test.ts +137 -0
- package/src/tests/http/context-routes.test.ts +2 -2
- package/src/tests/http-api-integration.test.ts +5 -2
- package/src/tests/http-users.test.ts +1 -1
- package/src/tests/identity-refresh.test.ts +2 -2
- package/src/tests/internal-ai/complete-structured.test.ts +2 -2
- package/src/tests/internal-ai/credentials.test.ts +19 -17
- package/src/tests/jira-metadata.test.ts +1 -1
- package/src/tests/jira-oauth.test.ts +5 -5
- package/src/tests/jira-sync.test.ts +1 -1
- package/src/tests/jira-webhook-lifecycle.test.ts +3 -3
- package/src/tests/jira-webhook.test.ts +2 -2
- package/src/tests/kapso-inbound.test.ts +2 -2
- package/src/tests/kv-http.test.ts +1 -1
- package/src/tests/kv-namespace-resolution.test.ts +1 -1
- package/src/tests/kv-page-proxy.test.ts +2 -2
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/linear-webhook.test.ts +1 -1
- package/src/tests/mcp-oauth-dcr-reuse.test.ts +6 -6
- package/src/tests/mcp-oauth-ensure-token.test.ts +28 -22
- package/src/tests/mcp-oauth-manual-client.test.ts +16 -12
- package/src/tests/mcp-oauth-queries.test.ts +35 -32
- package/src/tests/mcp-oauth-resolve-secrets.test.ts +8 -8
- package/src/tests/mcp-oauth-wrapper.test.ts +20 -20
- package/src/tests/mcp-user-route.test.ts +1 -1
- package/src/tests/memory-edges.test.ts +1 -1
- package/src/tests/memory-rate-endpoint.test.ts +1 -1
- package/src/tests/memory-rate-tool.test.ts +2 -2
- package/src/tests/memory-rater-e2e.test.ts +1 -1
- package/src/tests/memory-rater-llm-summarizer.test.ts +3 -3
- package/src/tests/memory-rater-llm.test.ts +1 -1
- package/src/tests/model-control.test.ts +3 -0
- package/src/tests/multi-runtime-registration.test.ts +97 -3
- package/src/tests/oauth-access-token-tool.test.ts +18 -18
- package/src/tests/oauth-callback-flow.test.ts +8 -8
- package/src/tests/oauth-credential-bindings.test.ts +29 -27
- package/src/tests/oauth-keepalive.test.ts +10 -10
- package/src/tests/oauth-presets.test.ts +13 -5
- package/src/tests/oauth-refresh-failure.test.ts +9 -9
- package/src/tests/oauth-refresh-sweep.test.ts +20 -13
- package/src/tests/oauth-wrapper.test.ts +1 -1
- package/src/tests/onboard-compose.test.ts +3 -3
- package/src/tests/onboard-env.test.ts +26 -26
- package/src/tests/onboard-non-interactive.test.ts +4 -4
- package/src/tests/opencode-adapter.test.ts +1 -1
- package/src/tests/opencode-plugin.test.ts +2 -2
- package/src/tests/otel-session-cost-metrics.test.ts +1 -1
- package/src/tests/page-proxy-authed.test.ts +2 -2
- package/src/tests/page-proxy.test.ts +2 -2
- package/src/tests/page-session.test.ts +7 -7
- package/src/tests/page-viewer-auth.test.ts +2 -2
- package/src/tests/pages-authed-mode.test.ts +1 -1
- package/src/tests/pages-http.test.ts +1 -1
- package/src/tests/pages-password-mode.test.ts +1 -1
- package/src/tests/pages-public-authed-401.test.ts +1 -1
- package/src/tests/pages-public-html.test.ts +2 -2
- package/src/tests/pages-view-count.test.ts +1 -1
- package/src/tests/pi-mono-adapter.test.ts +10 -10
- package/src/tests/pi-mono-extension.test.ts +2 -2
- package/src/tests/pricing-routes.test.ts +1 -1
- package/src/tests/profile-sync-warning.test.ts +8 -8
- package/src/tests/profile-sync.test.ts +8 -8
- package/src/tests/prompt-template-session.test.ts +36 -5
- package/src/tests/provider-process-group.test.ts +95 -4
- package/src/tests/providers/pi-cost.test.ts +1 -1
- package/src/tests/queue-stall-alarm.test.ts +1 -1
- package/src/tests/rbac-admission.test.ts +1 -1
- package/src/tests/rbac-charact-http.test.ts +1 -1
- package/src/tests/rbac-e2e-helpers.ts +1 -1
- package/src/tests/realtime-room-surfaces.test.ts +1 -1
- package/src/tests/reload-config.test.ts +1 -1
- package/src/tests/routing-reason-contract.test.ts +76 -3
- package/src/tests/routing-reason-inventory.test.ts +11 -6
- package/src/tests/routing-source.test.ts +53 -0
- package/src/tests/runner-agent-fs-provision-retry.test.ts +1 -1
- package/src/tests/runner-context-preamble.test.ts +1 -1
- package/src/tests/runner-deployment-resume.test.ts +1 -1
- package/src/tests/runner-fallback-output.test.ts +1 -1
- package/src/tests/runner-identity-refresh.test.ts +1 -1
- package/src/tests/runner-skills-refresh.test.ts +1 -1
- package/src/tests/schedule-target-type.test.ts +3 -3
- package/src/tests/scheduled-tasks.test.ts +14 -5
- package/src/tests/scheduler-occurrence.test.ts +347 -0
- package/src/tests/script-apis-mcp.test.ts +1 -1
- package/src/tests/script-connections-http.test.ts +61 -58
- package/src/tests/script-connections-mcp.test.ts +1 -1
- package/src/tests/script-connections.test.ts +1 -1
- package/src/tests/script-runs-http.test.ts +15 -7
- package/src/tests/script-workflows-runtime-e2e.test.ts +1 -1
- package/src/tests/scripts-external-api.test.ts +1 -1
- package/src/tests/scripts-http.test.ts +1 -1
- package/src/tests/scripts-mcp-e2e.test.ts +1 -1
- package/src/tests/scripts-only-gating.test.ts +2 -2
- package/src/tests/scripts-retention.test.ts +1 -1
- package/src/tests/scripts-runtime-identity.test.ts +1 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -1
- package/src/tests/scripts-runtime.test.ts +6 -4
- package/src/tests/scripts-typecheck.test.ts +21 -0
- package/src/tests/sdk-allowlist.test.ts +1 -1
- package/src/tests/secret-scrubber.test.ts +38 -31
- package/src/tests/seed-registry.test.ts +46 -8
- package/src/tests/seed-schedules.test.ts +139 -8
- package/src/tests/seed-workflows.test.ts +93 -7
- package/src/tests/send-task-output-schema.test.ts +9 -1
- package/src/tests/send-task-requested-by.test.ts +1 -0
- package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
- package/src/tests/session-costs-codex-recompute.test.ts +1 -1
- package/src/tests/session-costs-golden.test.ts +2 -2
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -2
- package/src/tests/session-costs-recompute-all-providers.test.ts +1 -1
- package/src/tests/session-tokens.test.ts +1 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +1 -1
- package/src/tests/slack-file-tools-attachments.test.ts +1 -1
- package/src/tests/slack-inbound-files.test.ts +222 -86
- package/src/tests/slack-reaction-config.test.ts +5 -4
- package/src/tests/slack-read-boundaries.test.ts +1 -1
- package/src/tests/slack-render-v2.test.ts +120 -1
- package/src/tests/status.test.ts +7 -7
- package/src/tests/steering-core.test.ts +1 -1
- package/src/tests/steering-transport.test.ts +4 -4
- package/src/tests/store-progress-output-guidance.test.ts +34 -0
- package/src/tests/swarm-tool-result-gate.test.ts +11 -11
- package/src/tests/swarm-x-tool.test.ts +7 -5
- package/src/tests/sync-ai-toolbox-skills.test.ts +19 -0
- package/src/tests/task-supersede-resume.test.ts +1 -1
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tasks-requested-by-filter.test.ts +2 -2
- package/src/tests/telemetry-init.test.ts +11 -3
- package/src/tests/tool-output-agent-id.test.ts +1 -0
- package/src/tests/tracker-fold.test.ts +7 -7
- package/src/tests/tracker-tools.test.ts +3 -3
- package/src/tests/ui-followup-lead-delegation.test.ts +1 -0
- package/src/tests/unified-oauth-migration.test.ts +5 -5
- package/src/tests/user-token-rest-auth.test.ts +1 -1
- package/src/tests/user-token-routes.test.ts +1 -1
- package/src/tests/whoami-route.test.ts +1 -1
- package/src/tests/workflow-agent-task.test.ts +15 -13
- package/src/tests/workflow-ctx.test.ts +1 -1
- package/src/tests/workflow-e2e.test.ts +1 -1
- package/src/tests/workflow-engine-v2.test.ts +3 -1
- package/src/tests/workflow-executors.test.ts +2 -2
- package/src/tests/workflow-http-v2.test.ts +8 -6
- package/src/tests/workflow-input-redaction.test.ts +7 -5
- package/src/tests/workflow-integration-io.test.ts +2 -2
- package/src/tests/workflow-llm.test.ts +5 -5
- package/src/tests/workflow-registry.test.ts +1 -1
- package/src/tests/workflow-swarm-script.test.ts +1 -1
- package/src/tests/workflow-triggers-v2.test.ts +23 -21
- package/src/tests/x-composio.test.ts +9 -7
- package/src/tests/x402-client.test.ts +1 -1
- package/src/tests/x402-config.test.ts +28 -41
- package/src/tools/defer-task.ts +156 -24
- package/src/tools/send-task.ts +21 -1
- package/src/tools/skills/skill-publish.ts +1 -0
- package/src/tools/store-progress.ts +1 -1
- package/src/tools/templates.ts +2 -1
- package/src/types.ts +11 -0
- package/src/utils/constants.ts +7 -1
- package/src/utils/credentials.test.ts +16 -16
- package/src/workflows/executors/agent-task.ts +5 -0
- package/templates/ai-toolbox.manifest.json +7 -1
- package/templates/schedules/daily-blocker-digest/config.json +6 -1
- package/templates/schedules/daily-compounding-reflection/config.json +1 -0
- package/templates/schedules/daily-hn-briefing/config.json +1 -1
- package/templates/schedules/daily-status-report/config.json +6 -1
- package/templates/schedules/daily-swarm-update-check/config.json +19 -0
- package/templates/schedules/daily-swarm-update-check/content.md +49 -0
- package/templates/schedules/daily-workflow-health-audit/config.json +6 -1
- package/templates/schedules/gtm-weekly-review/config.json +1 -1
- package/templates/schedules/weekly-code-health-reports/config.json +1 -1
- package/templates/schedules/weekly-dependabot-triage/config.json +1 -1
- package/templates/schedules/weekly-dora-metrics/config.json +1 -1
- package/templates/schedules/weekly-harness-upgrade-check/config.json +1 -1
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +1 -1
- package/templates/skills/serply-search/SKILL.md +95 -0
- package/templates/skills/serply-search/config.json +13 -0
- package/templates/skills/serply-search/content.md +90 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/templates/workflows/claude-code-changelog-watch/config.json +1 -1
- package/templates/workflows/gsc-topic-miner/config.json +1 -1
- package/templates/workflows/linear-drain-loop/config.json +1 -1
- package/templates/workflows/llm-safe-release-context/config.json +1 -1
- package/templates/workflows/ralph-loop/config.json +1 -1
- package/dist/{azure-openai-responses-cqar64rc.js → azure-openai-responses-g85jv7m8.js} +8 -8
- package/dist/{cli-4xz3pmrq.js → cli-8qx7jnrk.js} +5 -5
- package/dist/{cli-6664w7y4.js → cli-wk0gjrn3.js} +3 -3
- package/dist/{google-vertex-ppw8ydpa.js → google-vertex-pkhpkbq9.js} +3 -3
- package/dist/{openai-codex-responses-g7w48y30.js → openai-codex-responses-pm5gnyfa.js} +4 -4
- package/dist/{openai-completions-0bqmsajw.js → openai-completions-qdcmkfnr.js} +12 -12
- package/dist/{openai-responses-nmeyptke.js → openai-responses-vwvk1taj.js} +13 -13
|
@@ -19,8 +19,8 @@ import { handleSessionData } from "../http/session-data";
|
|
|
19
19
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
20
20
|
import { listenOnFreePort } from "./test-net";
|
|
21
21
|
|
|
22
|
-
const TEST_DB_PATH = "./test-model-key-normalize.sqlite";
|
|
23
|
-
const API_KEY = "test-model-key-normalize";
|
|
22
|
+
const TEST_DB_PATH = "./example-test-model-key-normalize.sqlite";
|
|
23
|
+
const API_KEY = "example-test-model-key-normalize";
|
|
24
24
|
|
|
25
25
|
async function removeDbFiles(path: string): Promise<void> {
|
|
26
26
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
@@ -17,7 +17,7 @@ import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
|
17
17
|
import { listenOnFreePort } from "./test-net";
|
|
18
18
|
|
|
19
19
|
const TEST_DB_PATH = "./test-recompute-all-providers.sqlite";
|
|
20
|
-
const API_KEY = "test-recompute-all";
|
|
20
|
+
const API_KEY = "example-test-recompute-all";
|
|
21
21
|
|
|
22
22
|
async function removeDbFiles(path: string): Promise<void> {
|
|
23
23
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
@@ -23,7 +23,7 @@ import { handleSessions } from "../http/sessions";
|
|
|
23
23
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
24
24
|
import { listenOnFreePort } from "./test-net";
|
|
25
25
|
|
|
26
|
-
const API_KEY = "test-session-token-key";
|
|
26
|
+
const API_KEY = "example-test-session-token-key";
|
|
27
27
|
|
|
28
28
|
function createTestServer(apiKey: string): Server {
|
|
29
29
|
return createHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
* rather than executed.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
const SECRET = "
|
|
21
|
+
const SECRET = "example-shutdown-token-value";
|
|
22
22
|
|
|
23
23
|
const shutdownSourcePath = `${import.meta.dir}/../http/index.ts`;
|
|
24
24
|
const shutdownSource = await Bun.file(shutdownSourcePath).text();
|
|
@@ -21,7 +21,7 @@ import type { SlackFile } from "../slack/files";
|
|
|
21
21
|
import { createSlackTaskWithFiles, fetchSlackFiles } from "../slack/inbound-files";
|
|
22
22
|
|
|
23
23
|
const TEST_DB_PATH = "./test-slack-file-tools-attachments.sqlite";
|
|
24
|
-
const BOT_TOKEN = "
|
|
24
|
+
const BOT_TOKEN = "example-slack-file-tools-token";
|
|
25
25
|
const AGENT_ID = "aaaaaaaa-0000-4000-8000-00000000f001";
|
|
26
26
|
const OTHER_ID = "aaaaaaaa-0000-4000-8000-00000000f002";
|
|
27
27
|
const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 9, 9]);
|
|
@@ -6,7 +6,17 @@
|
|
|
6
6
|
* task unclaimable until its attachments exist, and both Slack ingress
|
|
7
7
|
* handlers end to end against a real DB and the local file provider.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
afterAll,
|
|
11
|
+
afterEach,
|
|
12
|
+
beforeAll,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
mock,
|
|
17
|
+
spyOn,
|
|
18
|
+
test,
|
|
19
|
+
} from "bun:test";
|
|
10
20
|
import { mkdtemp, rm, unlink } from "node:fs/promises";
|
|
11
21
|
import { tmpdir } from "node:os";
|
|
12
22
|
import { join } from "node:path";
|
|
@@ -16,13 +26,17 @@ import {
|
|
|
16
26
|
createTaskExtended,
|
|
17
27
|
getDbClient,
|
|
18
28
|
getLogsByTaskId,
|
|
29
|
+
getSlackTasksInThread,
|
|
30
|
+
getSteeringMessagesForTask,
|
|
19
31
|
getTaskAttachments,
|
|
20
32
|
getTaskById,
|
|
21
33
|
initDb,
|
|
22
34
|
promoteAbandonedDraftTasks,
|
|
35
|
+
startTask,
|
|
23
36
|
} from "../be/db";
|
|
24
37
|
import { MAX_TASK_ATTACHMENT_BYTES } from "../be/task-attachment-store";
|
|
25
38
|
import { getFileStorageProvider, resetFileStorageProviderForTests } from "../fs/registry";
|
|
39
|
+
import * as slackAppModule from "../slack/app";
|
|
26
40
|
import { createAssistant } from "../slack/assistant";
|
|
27
41
|
import * as slackEnrichModule from "../slack/enrich";
|
|
28
42
|
import type { SlackFile } from "../slack/files";
|
|
@@ -34,10 +48,10 @@ import {
|
|
|
34
48
|
notifySlackFileFailures,
|
|
35
49
|
setDraftLeaseRefreshMsForTests,
|
|
36
50
|
} from "../slack/inbound-files";
|
|
37
|
-
import { instantFlush } from "../slack/thread-buffer";
|
|
51
|
+
import { getBufferMessageCount, instantFlush } from "../slack/thread-buffer";
|
|
38
52
|
|
|
39
53
|
const TEST_DB_PATH = "./test-slack-inbound-files.sqlite";
|
|
40
|
-
const BOT_TOKEN = "
|
|
54
|
+
const BOT_TOKEN = "example-slack-inbound-files-token";
|
|
41
55
|
const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]);
|
|
42
56
|
|
|
43
57
|
let slackFiles: ReturnType<typeof Bun.serve>;
|
|
@@ -51,6 +65,7 @@ const previousEnv = {
|
|
|
51
65
|
SLACK_RENDER_V2: process.env.SLACK_RENDER_V2,
|
|
52
66
|
STEERING_ENABLED: process.env.STEERING_ENABLED,
|
|
53
67
|
SLACK_THREAD_STEERING: process.env.SLACK_THREAD_STEERING,
|
|
68
|
+
SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION: process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION,
|
|
54
69
|
};
|
|
55
70
|
|
|
56
71
|
const HTML_FILE = new TextEncoder().encode("<html><body>quarterly report</body></html>");
|
|
@@ -708,113 +723,234 @@ describe("Slack ingress with files", () => {
|
|
|
708
723
|
expect(await getTaskAttachments(followUp!.id)).toHaveLength(1);
|
|
709
724
|
});
|
|
710
725
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
726
|
+
describe("buffered follow-ups with files", () => {
|
|
727
|
+
let earlier: Awaited<ReturnType<typeof createTaskExtended>>;
|
|
728
|
+
let client: ReturnType<typeof slackClient>;
|
|
729
|
+
const getApp = spyOn(slackAppModule, "getSlackApp");
|
|
730
|
+
const channelId = "C0BUFFERFILES";
|
|
731
|
+
const threadTs = "1950000000.000001";
|
|
732
|
+
const key = `${channelId}:${threadTs}`;
|
|
733
|
+
|
|
734
|
+
beforeEach(async () => {
|
|
735
|
+
const lead = await createAgent({
|
|
736
|
+
name: "lead",
|
|
737
|
+
isLead: true,
|
|
738
|
+
status: "busy",
|
|
739
|
+
harnessProvider: "pi",
|
|
740
|
+
});
|
|
741
|
+
earlier = await createTaskExtended("earlier work", {
|
|
742
|
+
agentId: lead.id,
|
|
743
|
+
source: "slack",
|
|
744
|
+
slackChannelId: channelId,
|
|
745
|
+
slackThreadTs: threadTs,
|
|
746
|
+
});
|
|
747
|
+
await startTask(earlier.id);
|
|
748
|
+
process.env.ADDITIVE_SLACK = "true";
|
|
749
|
+
process.env.STEERING_ENABLED = "true";
|
|
750
|
+
process.env.SLACK_THREAD_STEERING = "lead";
|
|
751
|
+
process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION = "false";
|
|
752
|
+
client = slackClient();
|
|
753
|
+
getApp.mockReturnValue({ client } as never);
|
|
754
|
+
});
|
|
723
755
|
|
|
724
|
-
|
|
756
|
+
afterEach(async () => {
|
|
757
|
+
await instantFlush(key);
|
|
758
|
+
process.env.ADDITIVE_SLACK = "false";
|
|
759
|
+
restoreSteeringEnv();
|
|
760
|
+
const requireMention = previousEnv.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION;
|
|
761
|
+
if (requireMention === undefined) delete process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION;
|
|
762
|
+
else process.env.SLACK_THREAD_FOLLOWUP_REQUIRE_MENTION = requireMention;
|
|
763
|
+
getApp.mockReturnValue(null);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
afterAll(() => getApp.mockRestore());
|
|
767
|
+
|
|
768
|
+
async function sendBuffered(text: string, files: SlackFile[] = []) {
|
|
725
769
|
seq += 1;
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
770
|
+
const ts = `1950000001.${String(seq).padStart(6, "0")}`;
|
|
771
|
+
await channelMessage({
|
|
772
|
+
event: {
|
|
773
|
+
type: "message",
|
|
774
|
+
subtype: files.length ? "file_share" : undefined,
|
|
775
|
+
channel: channelId,
|
|
776
|
+
thread_ts: threadTs,
|
|
777
|
+
ts,
|
|
731
778
|
user: "U_HUMAN",
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
files: [slackFile()],
|
|
779
|
+
text,
|
|
780
|
+
files,
|
|
735
781
|
},
|
|
736
782
|
body: { event_id: `evt_inbound_files_${seq}` },
|
|
737
783
|
client,
|
|
738
784
|
say: mock(async () => ({})),
|
|
739
|
-
setStatus: mock(async () => {}),
|
|
740
|
-
setTitle: mock(async () => {}),
|
|
741
|
-
getThreadContext: mock(async () => ({})),
|
|
742
785
|
});
|
|
743
|
-
|
|
744
|
-
process.env.ADDITIVE_SLACK = "false";
|
|
786
|
+
return ts;
|
|
745
787
|
}
|
|
746
788
|
|
|
747
|
-
|
|
748
|
-
await
|
|
749
|
-
|
|
750
|
-
restoreSteeringEnv();
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
async function assertQueuedFollowUp() {
|
|
754
|
-
expect(fileRequests).toEqual([]);
|
|
755
|
-
expect(client.chat.postMessage).toHaveBeenCalledTimes(1);
|
|
756
|
-
const [args] = client.chat.postMessage.mock.calls[0] as unknown as [{ text: string }];
|
|
757
|
-
expect(args.text).toContain("`screenshot.png` (follow-ups queued by ADDITIVE_SLACK");
|
|
758
|
-
|
|
759
|
-
// Drain the debounce buffer now, while the DB is still open.
|
|
760
|
-
await instantFlush(`${first!.slackChannelId}:${first!.slackThreadTs}`);
|
|
761
|
-
const queued = await getDbClient().get<{ task: string }>(
|
|
762
|
-
"SELECT task FROM agent_tasks WHERE slackThreadTs = ? AND id != ? ORDER BY createdAt DESC",
|
|
763
|
-
[first!.slackThreadTs, first!.id],
|
|
789
|
+
async function followUp() {
|
|
790
|
+
const tasks = (await getSlackTasksInThread(channelId, threadTs)).filter(
|
|
791
|
+
(task) => task.id !== earlier.id,
|
|
764
792
|
);
|
|
765
|
-
expect(
|
|
793
|
+
expect(tasks).toHaveLength(1);
|
|
794
|
+
return tasks[0]!;
|
|
766
795
|
}
|
|
767
|
-
});
|
|
768
796
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
source: "slack",
|
|
775
|
-
slackChannelId: "C0NOWTEST1",
|
|
776
|
-
slackThreadTs: threadTs,
|
|
777
|
-
});
|
|
778
|
-
await getDbClient().run("UPDATE agent_tasks SET status = 'in_progress' WHERE id = ?", [
|
|
779
|
-
earlier.id,
|
|
780
|
-
]);
|
|
781
|
-
process.env.ADDITIVE_SLACK = "true";
|
|
782
|
-
process.env.STEERING_ENABLED = "false";
|
|
783
|
-
delete process.env.SLACK_THREAD_STEERING;
|
|
784
|
-
const client = slackClient();
|
|
785
|
-
seq += 1;
|
|
797
|
+
function failureNotices() {
|
|
798
|
+
return (client.chat.postMessage.mock.calls as unknown as [{ text: string }][])
|
|
799
|
+
.map(([args]) => args.text)
|
|
800
|
+
.filter((text) => text.includes("Couldn't attach"));
|
|
801
|
+
}
|
|
786
802
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
channel:
|
|
803
|
+
test("assistant follow-ups retain files until flush and resolve id-only metadata", async () => {
|
|
804
|
+
client.files.info.mockResolvedValue({ ok: true, file: slackFile() } as never);
|
|
805
|
+
seq += 1;
|
|
806
|
+
await assistantMessage({
|
|
807
|
+
message: {
|
|
808
|
+
channel: channelId,
|
|
793
809
|
thread_ts: threadTs,
|
|
794
810
|
ts: `1950000001.${String(seq).padStart(6, "0")}`,
|
|
795
811
|
user: "U_HUMAN",
|
|
796
|
-
|
|
797
|
-
|
|
812
|
+
subtype: "file_share",
|
|
813
|
+
text: "",
|
|
814
|
+
files: [{ id: "F0SHOT0001" }],
|
|
798
815
|
},
|
|
799
816
|
body: { event_id: `evt_inbound_files_${seq}` },
|
|
800
817
|
client,
|
|
801
818
|
say: mock(async () => ({})),
|
|
819
|
+
setStatus: mock(async () => {}),
|
|
820
|
+
setTitle: mock(async () => {}),
|
|
821
|
+
getThreadContext: mock(async () => ({})),
|
|
822
|
+
});
|
|
823
|
+
expect(fileRequests).toEqual([]);
|
|
824
|
+
expect(client.files.info).not.toHaveBeenCalled();
|
|
825
|
+
expect(failureNotices()).toEqual([]);
|
|
826
|
+
|
|
827
|
+
await instantFlush(key);
|
|
828
|
+
const task = await followUp();
|
|
829
|
+
expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES]);
|
|
830
|
+
expect(task.task).toContain("[File: screenshot.png");
|
|
831
|
+
expect(task.task).not.toContain("not attached");
|
|
832
|
+
expect(await wasDraft(task.id)).toBe(true);
|
|
833
|
+
expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
|
|
834
|
+
expect(failureNotices()).toEqual([]);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
for (const steering of ["off", "lead"]) {
|
|
838
|
+
test(`timed flush attaches every message's files with steering ${steering}`, async () => {
|
|
839
|
+
process.env.SLACK_THREAD_STEERING = steering;
|
|
840
|
+
const nativeSetTimeout = globalThis.setTimeout;
|
|
841
|
+
const bufferMs = Number(process.env.ADDITIVE_SLACK_BUFFER_MS) || 10_000;
|
|
842
|
+
const timer = spyOn(globalThis, "setTimeout").mockImplementation((fn, ms, ...args) =>
|
|
843
|
+
nativeSetTimeout(fn, ms === bufferMs ? 250 : ms, ...args),
|
|
844
|
+
);
|
|
845
|
+
const otherBytes = new Uint8Array([...PNG_BYTES, 4]);
|
|
846
|
+
bytesById.set("F0OTHER0001", otherBytes);
|
|
847
|
+
let lastTs: string;
|
|
848
|
+
try {
|
|
849
|
+
await sendBuffered("", [slackFile()]);
|
|
850
|
+
lastTs = await sendBuffered("compare these", [
|
|
851
|
+
slackFile({ id: "F0OTHER0001", name: "other.png", size: otherBytes.length }),
|
|
852
|
+
]);
|
|
853
|
+
expect(getBufferMessageCount(key)).toBe(2);
|
|
854
|
+
expect(fileRequests).toEqual([]);
|
|
855
|
+
expect(failureNotices()).toEqual([]);
|
|
856
|
+
} finally {
|
|
857
|
+
timer.mockRestore();
|
|
858
|
+
}
|
|
859
|
+
// Wait for the real timer callback and draft promotion, not instantFlush.
|
|
860
|
+
for (let i = 0; i < 200; i++) {
|
|
861
|
+
const tasks = await getSlackTasksInThread(channelId, threadTs);
|
|
862
|
+
if (tasks.some((task) => task.id !== earlier.id && task.status === "pending")) break;
|
|
863
|
+
await Bun.sleep(10);
|
|
864
|
+
}
|
|
865
|
+
const task = await followUp();
|
|
866
|
+
expect(task.status).toBe("pending");
|
|
867
|
+
expect(task.dependsOn).toEqual([earlier.id]);
|
|
868
|
+
expect(task.slackTriggerMessageTs).toBe(lastTs!);
|
|
869
|
+
expect(task.task).toContain("2 message(s) buffered");
|
|
870
|
+
expect(task.task).toContain("compare these");
|
|
871
|
+
expect(task.task).not.toContain("not attached");
|
|
872
|
+
expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES, otherBytes]);
|
|
873
|
+
expect(await wasDraft(task.id)).toBe(true);
|
|
874
|
+
expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
|
|
875
|
+
expect(failureNotices()).toEqual([]);
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test(`!now with files flushes existing files without dependency with steering ${steering}`, async () => {
|
|
879
|
+
process.env.SLACK_THREAD_STEERING = steering;
|
|
880
|
+
await sendBuffered("earlier screenshot", [slackFile()]);
|
|
881
|
+
expect(fileRequests).toEqual([]);
|
|
882
|
+
const otherBytes = new Uint8Array([...PNG_BYTES, 4]);
|
|
883
|
+
bytesById.set("F0OTHER0001", otherBytes);
|
|
884
|
+
await sendBuffered("!now", [
|
|
885
|
+
slackFile({ id: "F0OTHER0001", name: "other.png", size: otherBytes.length }),
|
|
886
|
+
]);
|
|
887
|
+
const task = await followUp();
|
|
888
|
+
expect(task.task).toContain("earlier screenshot");
|
|
889
|
+
expect(task.task).not.toContain("!now");
|
|
890
|
+
expect(task.dependsOn).toEqual([]);
|
|
891
|
+
expect(await getTaskAttachments(task.id)).toHaveLength(2);
|
|
892
|
+
expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES, otherBytes]);
|
|
893
|
+
expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
|
|
894
|
+
expect(failureNotices()).toEqual([]);
|
|
802
895
|
});
|
|
803
|
-
} finally {
|
|
804
|
-
process.env.ADDITIVE_SLACK = "false";
|
|
805
|
-
restoreSteeringEnv();
|
|
806
896
|
}
|
|
807
897
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
[
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
|
|
898
|
+
test("text-only follow-ups still steer the running task", async () => {
|
|
899
|
+
await sendBuffered("!now use the safer approach");
|
|
900
|
+
const steering = await getSteeringMessagesForTask(earlier.id);
|
|
901
|
+
expect(steering).toHaveLength(1);
|
|
902
|
+
expect(steering[0]!.body).toContain("use the safer approach");
|
|
903
|
+
expect(await getSlackTasksInThread(channelId, threadTs)).toHaveLength(1);
|
|
904
|
+
expect(fileRequests).toEqual([]);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("download failures and oversize files are reported at flush while good files attach", async () => {
|
|
908
|
+
await sendBuffered("inspect these", [
|
|
909
|
+
slackFile(),
|
|
910
|
+
slackFile({
|
|
911
|
+
id: "F0DENIED001",
|
|
912
|
+
name: "denied.png",
|
|
913
|
+
url_private_download: `${slackFiles.url}forbidden`,
|
|
914
|
+
}),
|
|
915
|
+
slackFile({ id: "F0HUGE0001", name: "huge.png", size: HUGE_BYTES }),
|
|
916
|
+
]);
|
|
917
|
+
expect(failureNotices()).toEqual([]);
|
|
918
|
+
await instantFlush(key);
|
|
919
|
+
const task = await followUp();
|
|
920
|
+
expect(await readStoredBytes(task.id)).toEqual([PNG_BYTES]);
|
|
921
|
+
expect(task.task).toContain("not attached: download failed (HTTP 403)");
|
|
922
|
+
expect(task.task).not.toContain("follow-ups queued by ADDITIVE_SLACK");
|
|
923
|
+
expect(failureNotices()).toHaveLength(1);
|
|
924
|
+
expect(failureNotices()[0]).toContain("`denied.png` (download failed (HTTP 403))");
|
|
925
|
+
expect(failureNotices()[0]).toContain("`huge.png`");
|
|
926
|
+
expect(failureNotices()[0]).not.toContain("`screenshot.png`");
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
test("storage failures are reported after flush", async () => {
|
|
930
|
+
const upload = spyOn(getFileStorageProvider(), "upload").mockRejectedValue(
|
|
931
|
+
new Error("storage unavailable"),
|
|
932
|
+
);
|
|
933
|
+
try {
|
|
934
|
+
await sendBuffered("!now inspect this", [slackFile()]);
|
|
935
|
+
const task = await followUp();
|
|
936
|
+
expect(task.status).toBe("pending");
|
|
937
|
+
expect(await getTaskAttachments(task.id)).toEqual([]);
|
|
938
|
+
expect(failureNotices()).toHaveLength(1);
|
|
939
|
+
expect(failureNotices()[0]).toContain("storage unavailable");
|
|
940
|
+
} finally {
|
|
941
|
+
upload.mockRestore();
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test("an unavailable Slack app retains the buffered file failure fallback", async () => {
|
|
946
|
+
getApp.mockReturnValue(null);
|
|
947
|
+
await sendBuffered("!now inspect this", [slackFile()]);
|
|
948
|
+
const task = await followUp();
|
|
949
|
+
expect(await getTaskAttachments(task.id)).toEqual([]);
|
|
950
|
+
expect(task.task).toContain("[File: screenshot.png");
|
|
951
|
+
expect(task.task).toContain("not attached: follow-ups queued by ADDITIVE_SLACK");
|
|
952
|
+
expect(await getSteeringMessagesForTask(earlier.id)).toEqual([]);
|
|
953
|
+
});
|
|
818
954
|
});
|
|
819
955
|
|
|
820
956
|
test("a channel message that mentions the bot with a file attaches it", async () => {
|
|
@@ -20,6 +20,7 @@ const ALL_EVENTS: SlackReactionEvent[] = [
|
|
|
20
20
|
|
|
21
21
|
/** The four acceptance-stage names `main` removed as a fixed list at finalize. */
|
|
22
22
|
const DEFAULT_ACCEPTANCE_NAMES = ["eyes", "heavy_plus_sign", "zap", "speech_balloon"];
|
|
23
|
+
const SECRET_SHAPED_GITHUB_PAT = `github_pat_${"a".repeat(32)}`;
|
|
23
24
|
|
|
24
25
|
function clearReactionEnv() {
|
|
25
26
|
for (const key of Object.values(SLACK_REACTION_CONFIG_KEYS)) delete process.env[key];
|
|
@@ -320,7 +321,7 @@ describe("reaction-shortcode.ts", () => {
|
|
|
320
321
|
test("a generic (non-invalid_name) add failure redacts a secret-shaped error message in the emitted log", async () => {
|
|
321
322
|
const logSpy = spyOn(console, "log");
|
|
322
323
|
logSpy.mockClear();
|
|
323
|
-
const leaked =
|
|
324
|
+
const leaked = SECRET_SHAPED_GITHUB_PAT;
|
|
324
325
|
const add = async () => {
|
|
325
326
|
throw new Error(`rate_limited: ${leaked}`);
|
|
326
327
|
};
|
|
@@ -339,7 +340,7 @@ describe("reaction-shortcode.ts", () => {
|
|
|
339
340
|
test("a generic (non-invalid_name/no_reaction) remove failure redacts a secret-shaped error message in the emitted log", async () => {
|
|
340
341
|
const logSpy = spyOn(console, "log");
|
|
341
342
|
logSpy.mockClear();
|
|
342
|
-
const leaked =
|
|
343
|
+
const leaked = SECRET_SHAPED_GITHUB_PAT;
|
|
343
344
|
const remove = async () => {
|
|
344
345
|
const error = new Error(`rate_limited: ${leaked}`) as Error & { data: unknown };
|
|
345
346
|
error.data = { error: "rate_limited" };
|
|
@@ -361,7 +362,7 @@ describe("reaction-shortcode.ts", () => {
|
|
|
361
362
|
test("an invalid_name add failure redacts a secret-shaped reaction name in the emitted log", async () => {
|
|
362
363
|
const errorSpy = spyOn(console, "error");
|
|
363
364
|
errorSpy.mockClear();
|
|
364
|
-
const leaked =
|
|
365
|
+
const leaked = SECRET_SHAPED_GITHUB_PAT;
|
|
365
366
|
const add = async () => {
|
|
366
367
|
throw { data: { error: "invalid_name" } };
|
|
367
368
|
};
|
|
@@ -380,7 +381,7 @@ describe("reaction-shortcode.ts", () => {
|
|
|
380
381
|
test("a fallback add failure after invalid_name redacts a secret-shaped error message in the emitted log", async () => {
|
|
381
382
|
const logSpy = spyOn(console, "log");
|
|
382
383
|
logSpy.mockClear();
|
|
383
|
-
const leaked =
|
|
384
|
+
const leaked = SECRET_SHAPED_GITHUB_PAT;
|
|
384
385
|
const add = async ({ name }: { name: string }) => {
|
|
385
386
|
if (name === "not_a_real_emoji") throw { data: { error: "invalid_name" } };
|
|
386
387
|
throw new Error(`rate_limited: ${leaked}`);
|
|
@@ -9,7 +9,7 @@ import { mcpOverflowNamespace } from "../tools/utils";
|
|
|
9
9
|
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
10
10
|
|
|
11
11
|
const TEST_DB_PATH = "./test-slack-read-boundaries.sqlite";
|
|
12
|
-
const API_KEY = "test-slack-read-boundaries-key-1234567890";
|
|
12
|
+
const API_KEY = "example-test-slack-read-boundaries-key-1234567890";
|
|
13
13
|
const AGENT_ID = "aaaaaaaa-0000-4000-8000-000000000001";
|
|
14
14
|
const CHANNEL_ID = "C_BUSY_BOUNDARY";
|
|
15
15
|
const FULL_MESSAGE_COUNT = 20;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getSlackTreeMessages,
|
|
20
20
|
getTaskById,
|
|
21
21
|
initDb,
|
|
22
|
+
insertTaskAttachment,
|
|
22
23
|
isPendingSlackMessage,
|
|
23
24
|
markTaskSlackReplySent,
|
|
24
25
|
startTask,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
} from "../slack/render-v2";
|
|
40
41
|
import { getAgentDisplayName, getAgentEmoji } from "../slack/responses";
|
|
41
42
|
import { slackContextKey } from "../tasks/context-key";
|
|
42
|
-
import type { AgentTask } from "../types";
|
|
43
|
+
import type { AgentTask, TaskAttachment } from "../types";
|
|
43
44
|
import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
|
|
44
45
|
|
|
45
46
|
const TEST_DB_PATH = "./test-slack-render-v2.sqlite";
|
|
@@ -1183,6 +1184,95 @@ describe("Slack renderer v2", () => {
|
|
|
1183
1184
|
expect(calls.some((call) => call.method === "chat.appendStream")).toBe(false);
|
|
1184
1185
|
});
|
|
1185
1186
|
|
|
1187
|
+
test.each<{
|
|
1188
|
+
label: string;
|
|
1189
|
+
attachment: Partial<TaskAttachment> & Pick<TaskAttachment, "kind" | "name">;
|
|
1190
|
+
expected: string;
|
|
1191
|
+
}>([
|
|
1192
|
+
{
|
|
1193
|
+
label: "plain ASCII positive control",
|
|
1194
|
+
attachment: { kind: "agent-fs", name: "report.txt", path: "/reports/report.txt" },
|
|
1195
|
+
expected:
|
|
1196
|
+
"📎 [report.txt](https://files.example.test/file/~/org-1/drive-1/reports/report.txt)",
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
label: "raw path spaces and parentheses",
|
|
1200
|
+
attachment: { kind: "agent-fs", name: "final report", path: "/shared reports/final (v1).md" },
|
|
1201
|
+
expected:
|
|
1202
|
+
"📎 [final report](https://files.example.test/file/~/org-1/drive-1/shared%20reports/final%20%28v1%29.md)",
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
label: "already encoded positive control",
|
|
1206
|
+
attachment: { kind: "agent-fs", name: "report", path: "/reports/final%20%28v1%29.md" },
|
|
1207
|
+
expected:
|
|
1208
|
+
"📎 [report](https://files.example.test/file/~/org-1/drive-1/reports/final%20%28v1%29.md)",
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
label: "label delimiters, backslashes, and whitespace",
|
|
1212
|
+
attachment: {
|
|
1213
|
+
kind: "url",
|
|
1214
|
+
name: " \t[report] (final)\\\r\n\t copy ",
|
|
1215
|
+
url: "https://example.test/report",
|
|
1216
|
+
},
|
|
1217
|
+
expected: "📎 [\\[report\\] \\(final\\)\\\\ copy](https://example.test/report)",
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
label: "URL positive control with an IPv6 host",
|
|
1221
|
+
attachment: { kind: "url", name: "report", url: "http://[::1]/report.txt?q=a%20b&x=1#part" },
|
|
1222
|
+
expected: "📎 [report](http://[::1]/report.txt?q=a%20b&x=1#part)",
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
label: "URL delimiters with existing escapes and query parameters",
|
|
1226
|
+
attachment: {
|
|
1227
|
+
kind: "url",
|
|
1228
|
+
name: "report",
|
|
1229
|
+
url: "https://example.test/a%20b) [c]<(d)>\\file?q=one two&x=1#part",
|
|
1230
|
+
},
|
|
1231
|
+
expected:
|
|
1232
|
+
"📎 [report](https://example.test/a%20b%29%20[c]%3C%28d%29%3E%5Cfile?q=one%20two&x=1#part)",
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
label: "non-HTTP fallback stays omitted",
|
|
1236
|
+
attachment: { kind: "url", name: "report", url: "agent-fs:/reports/report.md" },
|
|
1237
|
+
expected: "",
|
|
1238
|
+
},
|
|
1239
|
+
])("streams safe attachment Markdown: $label", async ({ attachment, expected }) => {
|
|
1240
|
+
const originalHost = process.env.AGENT_FS_LIVE_URL;
|
|
1241
|
+
process.env.AGENT_FS_LIVE_URL = "https://files.example.test";
|
|
1242
|
+
try {
|
|
1243
|
+
const lead = await createAgent({ name: "Attachment Lead", isLead: true, status: "idle" });
|
|
1244
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_OUTCOME_ATTACHMENT");
|
|
1245
|
+
const ask = await createTaskExtended("render attachment link", {
|
|
1246
|
+
agentId: lead.id,
|
|
1247
|
+
source: "slack",
|
|
1248
|
+
slackChannelId: channelId,
|
|
1249
|
+
slackThreadTs: threadTs,
|
|
1250
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
1251
|
+
});
|
|
1252
|
+
await insertTaskAttachment({
|
|
1253
|
+
...attachment,
|
|
1254
|
+
taskId: ask.id,
|
|
1255
|
+
agentId: lead.id,
|
|
1256
|
+
orgId: "org-1",
|
|
1257
|
+
driveId: "drive-1",
|
|
1258
|
+
isPrimary: true,
|
|
1259
|
+
});
|
|
1260
|
+
await startTask(ask.id);
|
|
1261
|
+
await ensureSlackThreadTree([ask.id]);
|
|
1262
|
+
await completeTask(ask.id, "Done");
|
|
1263
|
+
calls.length = 0;
|
|
1264
|
+
_resetSlackRenderV2ForTests();
|
|
1265
|
+
|
|
1266
|
+
await processSlackRenderV2();
|
|
1267
|
+
|
|
1268
|
+
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1269
|
+
expect(started?.payload.markdown_text).toBe(`✅\n\nDone${expected ? `\n\n${expected}` : ""}`);
|
|
1270
|
+
} finally {
|
|
1271
|
+
if (originalHost === undefined) delete process.env.AGENT_FS_LIVE_URL;
|
|
1272
|
+
else process.env.AGENT_FS_LIVE_URL = originalHost;
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1186
1276
|
test("truncates oversized Markdown before a code fence and links the full task", async () => {
|
|
1187
1277
|
const lead = await createAgent({ name: "Overflow Lead", isLead: true, status: "idle" });
|
|
1188
1278
|
const { channelId, threadTs } = uniqueSlackAddress("C_OUTCOME_OVERFLOW");
|
|
@@ -1626,6 +1716,35 @@ describe("Slack renderer v2", () => {
|
|
|
1626
1716
|
expect(outcome?.finalizedAt).toBeDefined();
|
|
1627
1717
|
});
|
|
1628
1718
|
|
|
1719
|
+
test("keeps a deferral's human-facing line even when the agent already sent a slack-reply", async () => {
|
|
1720
|
+
const lead = await createAgent({ name: "Deferring Lead", isLead: true, status: "idle" });
|
|
1721
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_DEFER_AFTER_REPLY");
|
|
1722
|
+
const ask = await createTaskExtended("defer after replying by hand", {
|
|
1723
|
+
agentId: lead.id,
|
|
1724
|
+
source: "slack",
|
|
1725
|
+
slackChannelId: channelId,
|
|
1726
|
+
slackThreadTs: threadTs,
|
|
1727
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
1728
|
+
});
|
|
1729
|
+
await startTask(ask.id);
|
|
1730
|
+
await ensureSlackThreadTree([ask.id]);
|
|
1731
|
+
// Agent posts a hand-written slack-reply first (e.g. a transcription),
|
|
1732
|
+
// then defer-task completes the task with the engine-authored deferral
|
|
1733
|
+
// line and tags it "deferred" (mirrors defer-task.ts's completeTask call).
|
|
1734
|
+
await markTaskSlackReplySent(ask.id);
|
|
1735
|
+
const deferralLine =
|
|
1736
|
+
"Deferred until today 17:30:19 UTC ([715bf847](https://app.agent-swarm.dev/schedules/715bf847-fe3e-40e9-9fef-3297a62d9afd)) -> checking the new defer card";
|
|
1737
|
+
await completeTask(ask.id, deferralLine, { addTags: ["deferred"] });
|
|
1738
|
+
calls.length = 0;
|
|
1739
|
+
_resetSlackRenderV2ForTests();
|
|
1740
|
+
|
|
1741
|
+
await processSlackRenderV2();
|
|
1742
|
+
|
|
1743
|
+
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1744
|
+
expect(started?.payload.markdown_text).toBe(`✅\n\n${deferralLine}`);
|
|
1745
|
+
expect(started?.payload.markdown_text).not.toBe(`✅ ${lead.name} completed`);
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1629
1748
|
test("refreshes a stream started with stale content before finalizing it", async () => {
|
|
1630
1749
|
const lead = await createAgent({ name: "Refresh Lead", isLead: true, status: "idle" });
|
|
1631
1750
|
const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_REFRESH_STALE");
|