clawmini 0.0.7 → 0.0.9
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/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.github/workflows/release.yml +49 -0
- package/CHANGELOG.md +36 -0
- package/README.md +5 -4
- package/dist/adapter-discord/index.d.mts.map +1 -1
- package/dist/adapter-discord/index.mjs +465 -282
- package/dist/adapter-discord/index.mjs.map +1 -1
- package/dist/adapter-google-chat/index.mjs +367 -243
- package/dist/adapter-google-chat/index.mjs.map +1 -1
- package/dist/cli/index.mjs +684 -24
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lite.mjs +43 -13
- package/dist/cli/lite.mjs.map +1 -1
- package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
- package/dist/cli/manage-policies.mjs.map +1 -0
- package/dist/cli/run-host.d.mts +1 -0
- package/dist/cli/run-host.mjs +3090 -0
- package/dist/cli/run-host.mjs.map +1 -0
- package/dist/config-CPFQIGdG.mjs +57 -0
- package/dist/config-CPFQIGdG.mjs.map +1 -0
- package/dist/config-Dvl-Pov4.mjs +76 -0
- package/dist/config-Dvl-Pov4.mjs.map +1 -0
- package/dist/daemon/index.d.mts.map +1 -1
- package/dist/daemon/index.mjs +970 -332
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
- package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
- package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
- package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/dist/web/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/dist/web/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.Dr0ot9sV.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/dist/web/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +12 -12
- package/dist/workspace-oWmVh5mi.mjs +1001 -0
- package/dist/workspace-oWmVh5mi.mjs.map +1 -0
- package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
- package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
- package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
- package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
- package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
- package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
- package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
- package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
- package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
- package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
- package/docs/25_e2e_test_improvements/development_log.md +30 -0
- package/docs/25_e2e_test_improvements/notes.md +29 -0
- package/docs/25_e2e_test_improvements/prd.md +43 -0
- package/docs/25_e2e_test_improvements/questions.md +12 -0
- package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
- package/docs/25_e2e_test_improvements/tickets.md +22 -0
- package/docs/25_policy_cwd/development_log.md +30 -0
- package/docs/25_policy_cwd/notes.md +28 -0
- package/docs/25_policy_cwd/prd.md +77 -0
- package/docs/25_policy_cwd/questions.md +6 -0
- package/docs/25_policy_cwd/tickets.md +77 -0
- package/docs/CLI_REFERENCE.md +3 -1
- package/docs/PHILOSOPHY.md +35 -0
- package/docs/adapter-visibility/SPEC.md +461 -0
- package/docs/adapter-visibility/SPEC_v2.md +202 -0
- package/docs/auto-update/SPEC.md +344 -0
- package/docs/backups/SPEC.md +296 -0
- package/docs/backups/clawmini.gitignore +69 -0
- package/docs/guides/assets/clawmini-avatar.png +0 -0
- package/docs/guides/backups.md +332 -0
- package/docs/guides/discord_adapter_setup.md +1 -1
- package/docs/guides/google_chat_adapter_setup.md +81 -0
- package/docs/unified-startup/SPEC.md +203 -0
- package/e2e/_helpers/test-environment.test.ts +49 -0
- package/e2e/_helpers/test-environment.ts +548 -0
- package/e2e/adapters/_google-chat-fixtures.ts +340 -0
- package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
- package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
- package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
- package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
- package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
- package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
- package/e2e/agents/custom-api-env.test.ts +80 -0
- package/e2e/agents/export-lite-func.test.ts +104 -0
- package/e2e/agents/fallbacks.test.ts +124 -0
- package/e2e/agents/interrupt.test.ts +50 -0
- package/e2e/agents/no-reply-necessary.test.ts +57 -0
- package/e2e/agents/session-timeout-subagents.test.ts +76 -0
- package/e2e/agents/subagent-authorization.test.ts +246 -0
- package/e2e/agents/subagent-env.test.ts +49 -0
- package/e2e/agents/subagent-lifecycle.test.ts +782 -0
- package/e2e/agents/subagents-depth.test.ts +47 -0
- package/e2e/cli/agents.test.ts +176 -0
- package/e2e/cli/auto-update.test.ts +741 -0
- package/e2e/cli/basic.test.ts +44 -0
- package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
- package/e2e/cli/init-gitignore.test.ts +86 -0
- package/e2e/cli/init.test.ts +76 -0
- package/e2e/cli/messages.test.ts +363 -0
- package/e2e/cli/serve.test.ts +76 -0
- package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
- package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
- package/e2e/jobs/agent-jobs.test.ts +216 -0
- package/e2e/jobs/cron.test.ts +64 -0
- package/e2e/jobs/restart.test.ts +108 -0
- package/e2e/policies/approval-session.test.ts +69 -0
- package/e2e/policies/auto-create-policies-file.test.ts +35 -0
- package/e2e/policies/builtin-manage-policies.test.ts +184 -0
- package/e2e/policies/builtin-run-host.test.ts +180 -0
- package/e2e/policies/environment-policies.test.ts +177 -0
- package/e2e/policies/manage-policies.test.ts +566 -0
- package/e2e/policies/output-size.test.ts +98 -0
- package/e2e/policies/policies-context-cwd.test.ts +160 -0
- package/e2e/policies/relative-script-path.test.ts +60 -0
- package/e2e/policies/requests-show.test.ts +135 -0
- package/e2e/policies/requests.test.ts +208 -0
- package/e2e/policies/slash-policies.test.ts +308 -0
- package/e2e/policies/startup-cleanup.test.ts +48 -0
- package/e2e/routers/session-timeout.test.ts +106 -0
- package/e2e/routers/slash-model.test.ts +152 -0
- package/e2e/routers/slash-new.test.ts +50 -0
- package/e2e/routers/slash-restart-adapter.test.ts +96 -0
- package/e2e/routers/slash-restart.test.ts +114 -0
- package/e2e/routers/slash-shutdown.test.ts +55 -0
- package/e2e/routers/slash-stop.test.ts +232 -0
- package/e2e/routers/slash-upgrade.test.ts +88 -0
- package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
- package/eslint.config.js +6 -0
- package/napkin.md +1 -1
- package/package.json +8 -3
- package/src/adapter-discord/commands.test.ts +42 -0
- package/src/adapter-discord/commands.ts +33 -0
- package/src/adapter-discord/config.ts +12 -0
- package/src/adapter-discord/forwarder.test.ts +499 -21
- package/src/adapter-discord/forwarder.ts +343 -124
- package/src/adapter-discord/inbound-cache.test.ts +47 -0
- package/src/adapter-discord/inbound-cache.ts +37 -0
- package/src/adapter-discord/index.test.ts +67 -2
- package/src/adapter-discord/index.ts +84 -216
- package/src/adapter-discord/interactions.test.ts +54 -3
- package/src/adapter-discord/interactions.ts +97 -53
- package/src/adapter-discord/processMessage.ts +239 -0
- package/src/adapter-discord/state.ts +1 -0
- package/src/adapter-google-chat/auth.test.ts +9 -5
- package/src/adapter-google-chat/auth.ts +29 -23
- package/src/adapter-google-chat/cards.ts +7 -2
- package/src/adapter-google-chat/client.test.ts +37 -2
- package/src/adapter-google-chat/client.ts +138 -38
- package/src/adapter-google-chat/config.ts +19 -0
- package/src/adapter-google-chat/forwarder.test.ts +81 -56
- package/src/adapter-google-chat/forwarder.ts +394 -185
- package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
- package/src/adapter-google-chat/inbound-cache.ts +36 -0
- package/src/adapter-google-chat/state.test.ts +1 -0
- package/src/adapter-google-chat/state.ts +9 -1
- package/src/adapter-google-chat/subscriptions.ts +8 -6
- package/src/cli/builtin-policies.ts +44 -0
- package/src/cli/commands/agents.ts +59 -5
- package/src/cli/commands/down.ts +54 -2
- package/src/cli/commands/environments.ts +8 -2
- package/src/cli/commands/init.ts +31 -0
- package/src/cli/commands/logs.ts +116 -0
- package/src/cli/commands/policies.ts +6 -4
- package/src/cli/commands/serve.test.ts +67 -0
- package/src/cli/commands/serve.ts +284 -0
- package/src/cli/commands/up.ts +122 -2
- package/src/cli/commands/web-api/agents.ts +3 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/install-detection.test.ts +72 -0
- package/src/cli/install-detection.ts +48 -0
- package/src/cli/lite.ts +54 -22
- package/src/cli/manage-policies-utils.ts +104 -0
- package/src/cli/manage-policies.ts +291 -0
- package/src/cli/run-host.ts +45 -0
- package/src/cli/supervisor-actions.ts +267 -0
- package/src/cli/supervisor-control.test.ts +129 -0
- package/src/cli/supervisor-control.ts +155 -0
- package/src/cli/supervisor-pid.ts +68 -0
- package/src/cli/supervisor.ts +277 -0
- package/src/daemon/agent/agent-context.ts +11 -11
- package/src/daemon/agent/agent-session.ts +8 -1
- package/src/daemon/agent/chat-logger.test.ts +78 -9
- package/src/daemon/agent/chat-logger.ts +25 -5
- package/src/daemon/agent/turn-registry.test.ts +89 -0
- package/src/daemon/agent/turn-registry.ts +94 -0
- package/src/daemon/agent/types.ts +2 -0
- package/src/daemon/api/agent-policy-endpoints.ts +263 -0
- package/src/daemon/api/agent-router.ts +47 -126
- package/src/daemon/api/index.test.ts +1 -0
- package/src/daemon/api/policy-request.test.ts +7 -5
- package/src/daemon/api/router-utils.ts +6 -5
- package/src/daemon/api/subagent-router.ts +110 -74
- package/src/daemon/api/subagent-utils.test.ts +60 -0
- package/src/daemon/api/subagent-utils.ts +113 -87
- package/src/daemon/api/user-router.ts +34 -8
- package/src/daemon/auth.ts +1 -0
- package/src/daemon/cron.test.ts +62 -4
- package/src/daemon/cron.ts +42 -16
- package/src/daemon/events.ts +65 -0
- package/src/daemon/index.ts +24 -1
- package/src/daemon/message-interruption.test.ts +1 -0
- package/src/daemon/message-jobs.test.ts +1 -0
- package/src/daemon/message.ts +78 -14
- package/src/daemon/observation.test.ts +26 -18
- package/src/daemon/pending-replies.test.ts +112 -0
- package/src/daemon/pending-replies.ts +162 -0
- package/src/daemon/policy-request-service.ts +3 -1
- package/src/daemon/policy-utils.test.ts +66 -1
- package/src/daemon/policy-utils.ts +126 -1
- package/src/daemon/request-store.ts +31 -0
- package/src/daemon/routers/session-timeout.ts +4 -0
- package/src/daemon/routers/slash-model.test.ts +344 -0
- package/src/daemon/routers/slash-model.ts +207 -0
- package/src/daemon/routers/slash-policies.test.ts +38 -32
- package/src/daemon/routers/slash-policies.ts +84 -33
- package/src/daemon/routers/slash-restart.test.ts +69 -0
- package/src/daemon/routers/slash-restart.ts +36 -0
- package/src/daemon/routers/slash-shutdown.test.ts +50 -0
- package/src/daemon/routers/slash-shutdown.ts +28 -0
- package/src/daemon/routers/slash-upgrade.test.ts +116 -0
- package/src/daemon/routers/slash-upgrade.ts +76 -0
- package/src/daemon/routers/types.ts +7 -0
- package/src/daemon/routers.ts +16 -0
- package/src/shared/adapters/blockquote.test.ts +28 -0
- package/src/shared/adapters/blockquote.ts +20 -0
- package/src/shared/adapters/filtering.test.ts +224 -10
- package/src/shared/adapters/filtering.ts +95 -7
- package/src/shared/adapters/inbound-cache.test.ts +48 -0
- package/src/shared/adapters/inbound-cache.ts +54 -0
- package/src/shared/adapters/turn-log-buffer.ts +266 -0
- package/src/shared/adapters/turn-log.test.ts +389 -0
- package/src/shared/adapters/turn-log.ts +357 -0
- package/src/shared/agent-utils.ts +12 -5
- package/src/shared/chats.test.ts +4 -0
- package/src/shared/chats.ts +9 -0
- package/src/shared/config.ts +16 -1
- package/src/shared/lite.ts +76 -2
- package/src/shared/policies.ts +26 -0
- package/src/shared/template-manifest.ts +267 -0
- package/src/shared/utils/shell.ts +61 -0
- package/src/shared/version.ts +34 -0
- package/src/shared/workspace.test.ts +217 -0
- package/src/shared/workspace.ts +626 -48
- package/templates/environments/cladding/allowlist-domain.mjs +125 -0
- package/templates/environments/cladding/env.json +21 -1
- package/templates/environments/cladding/run-with-network.mjs +54 -0
- package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
- package/templates/environments/macos-proxy/env.json +8 -1
- package/templates/environments/macos-proxy/proxy.mjs +42 -13
- package/templates/gemini/template.json +5 -0
- package/templates/gemini-claw/template.json +13 -0
- package/templates/skills/clawmini-requests/SKILL.md +69 -10
- package/templates/skills/run-host/SKILL.md +51 -0
- package/templates/skills/skill-creator/SKILL.md +4 -3
- package/templates/skills/skill-creator/scripts/validate.sh +52 -0
- package/tsdown.config.ts +10 -1
- package/vitest.config.ts +2 -2
- package/web/.svelte-kit/ambient.d.ts +292 -176
- package/web/.svelte-kit/generated/server/internal.js +1 -1
- package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
- package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
- package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/{G_zz-Gou.js → wpfV79dV.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CYS8iApT.js → 0.DYyUA1au.js} +1 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
- package/{dist/web/_app/immutable/nodes/3.Dr0ot9sV.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.BBGQ_i84.js → 5.DLbPVJY2.js} +1 -1
- package/web/.svelte-kit/output/client/_app/version.json +1 -1
- package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
- package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
- package/web/.svelte-kit/output/server/chunks/client.js +1 -1
- package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
- package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
- package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
- package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
- package/web/.svelte-kit/output/server/chunks/root.js +739 -788
- package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
- package/web/.svelte-kit/output/server/index.js +126 -90
- package/web/.svelte-kit/output/server/manifest-full.js +1 -1
- package/web/.svelte-kit/output/server/manifest.js +1 -1
- package/web/.svelte-kit/output/server/nodes/0.js +1 -1
- package/web/.svelte-kit/output/server/nodes/1.js +1 -1
- package/web/.svelte-kit/output/server/nodes/2.js +1 -1
- package/web/.svelte-kit/output/server/nodes/3.js +1 -1
- package/web/.svelte-kit/output/server/nodes/4.js +1 -1
- package/web/.svelte-kit/output/server/nodes/5.js +1 -1
- package/web/.svelte-kit/output/server/remote-entry.js +245 -81
- package/web/.svelte-kit/tsconfig.json +4 -1
- package/dist/cli/propose-policy.mjs.map +0 -1
- package/dist/lite-CBxOT1y5.mjs +0 -241
- package/dist/lite-CBxOT1y5.mjs.map +0 -1
- package/dist/routing-D8rTxtaV.mjs +0 -245
- package/dist/routing-D8rTxtaV.mjs.map +0 -1
- package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
- package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/dist/web/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/dist/workspace-BJmJBfKi.mjs +0 -456
- package/dist/workspace-BJmJBfKi.mjs.map +0 -1
- package/src/cli/e2e/agents.test.ts +0 -140
- package/src/cli/e2e/basic.test.ts +0 -43
- package/src/cli/e2e/cron.test.ts +0 -132
- package/src/cli/e2e/export-lite-func.test.ts +0 -206
- package/src/cli/e2e/fallbacks.test.ts +0 -175
- package/src/cli/e2e/init.test.ts +0 -77
- package/src/cli/e2e/messages.test.ts +0 -332
- package/src/cli/e2e/propose-policy.test.ts +0 -203
- package/src/cli/e2e/requests.test.ts +0 -180
- package/src/cli/e2e/session-timeout.test.ts +0 -192
- package/src/cli/e2e/slash-new.test.ts +0 -93
- package/src/cli/e2e/subagents.test.ts +0 -106
- package/src/cli/e2e/utils.ts +0 -66
- package/src/cli/propose-policy.ts +0 -91
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.js +0 -60
- package/web/.svelte-kit/output/server/chunks/false.js +0 -4
- /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
- /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
|
@@ -8,25 +8,66 @@ import type {
|
|
|
8
8
|
ThreadChannel,
|
|
9
9
|
VoiceChannel,
|
|
10
10
|
StageChannel,
|
|
11
|
+
Message,
|
|
11
12
|
} from 'discord.js';
|
|
12
13
|
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, Colors } from 'discord.js';
|
|
13
14
|
import path from 'node:path';
|
|
14
15
|
import fs from 'node:fs';
|
|
15
16
|
import type { getTRPCClient } from './client.js';
|
|
17
|
+
import type { DiscordConfig } from './config.js';
|
|
16
18
|
import { readDiscordState, updateDiscordState, getDiscordStatePath } from './state.js';
|
|
19
|
+
import { resolveInbound } from './inbound-cache.js';
|
|
20
|
+
import { createTurnLogBuffer, type TurnLogBuffer } from '../shared/adapters/turn-log-buffer.js';
|
|
17
21
|
import type { ChatMessage } from '../shared/chats.js';
|
|
18
22
|
import { getWorkspaceRoot } from '../shared/workspace.js';
|
|
19
23
|
import {
|
|
20
|
-
|
|
24
|
+
routeMessage,
|
|
21
25
|
formatMessage,
|
|
26
|
+
type Destination,
|
|
22
27
|
type FilteringConfig,
|
|
23
28
|
} from '../shared/adapters/filtering.js';
|
|
24
29
|
|
|
30
|
+
type AnyTextChannel =
|
|
31
|
+
| TextChannel
|
|
32
|
+
| DMChannel
|
|
33
|
+
| NewsChannel
|
|
34
|
+
| ThreadChannel
|
|
35
|
+
| VoiceChannel
|
|
36
|
+
| StageChannel;
|
|
37
|
+
|
|
38
|
+
interface ThreadLogOptions {
|
|
39
|
+
maxToolPreview: number;
|
|
40
|
+
maxLogMessageChars: number;
|
|
41
|
+
editDebounceMs: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_THREAD_LOG_OPTS: ThreadLogOptions = {
|
|
45
|
+
maxToolPreview: 400,
|
|
46
|
+
// Discord caps messages at 2000 chars; leave headroom for the rollover marker.
|
|
47
|
+
maxLogMessageChars: 1800,
|
|
48
|
+
editDebounceMs: 1000,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function resolveThreadLogOpts(config?: DiscordConfig): ThreadLogOptions {
|
|
52
|
+
const v = config?.visibility?.threadLog;
|
|
53
|
+
return {
|
|
54
|
+
maxToolPreview: v?.maxToolPreview ?? DEFAULT_THREAD_LOG_OPTS.maxToolPreview,
|
|
55
|
+
maxLogMessageChars: v?.maxLogMessageChars ?? DEFAULT_THREAD_LOG_OPTS.maxLogMessageChars,
|
|
56
|
+
editDebounceMs: v?.editDebounceMs ?? DEFAULT_THREAD_LOG_OPTS.editDebounceMs,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Suppresses every form of mention (@everyone, @here, role, user) on bot
|
|
61
|
+
// posts. Tool payloads, agent output, and policy descriptions can contain
|
|
62
|
+
// arbitrary text; without this, an `@everyone` substring in (e.g.) a shell
|
|
63
|
+
// command echoed into the activity log would page the entire channel.
|
|
64
|
+
const NO_MENTIONS = { allowedMentions: { parse: [] as [] } } as const;
|
|
65
|
+
|
|
25
66
|
async function resolveDiscordDestination(
|
|
26
67
|
client: Client,
|
|
27
68
|
discordUserId: string,
|
|
28
69
|
chatId: string
|
|
29
|
-
): Promise<
|
|
70
|
+
): Promise<AnyTextChannel> {
|
|
30
71
|
const state = await readDiscordState();
|
|
31
72
|
const channelChatMap = state.channelChatMap || {};
|
|
32
73
|
|
|
@@ -64,11 +105,14 @@ export async function startDaemonToDiscordForwarder(
|
|
|
64
105
|
chatId?: string;
|
|
65
106
|
signal?: AbortSignal;
|
|
66
107
|
config?: FilteringConfig;
|
|
108
|
+
discordConfig?: DiscordConfig;
|
|
67
109
|
} = {}
|
|
68
110
|
) {
|
|
69
111
|
const defaultChatId = options.chatId ?? 'default';
|
|
70
112
|
const signal = options.signal;
|
|
71
113
|
const config = options.config ?? {};
|
|
114
|
+
const threadLogOpts = resolveThreadLogOpts(options.discordConfig);
|
|
115
|
+
const threadsGloballyEnabled = options.discordConfig?.visibility?.threads !== false;
|
|
72
116
|
|
|
73
117
|
const activeSubscriptions = new Map<string, { unsubscribe: () => void }>();
|
|
74
118
|
const activeTypingSubscriptions = new Map<string, { unsubscribe: () => void }>();
|
|
@@ -84,6 +128,258 @@ export async function startDaemonToDiscordForwarder(
|
|
|
84
128
|
}));
|
|
85
129
|
};
|
|
86
130
|
|
|
131
|
+
const postThreaded = async (anchor: ThreadChannel, text: string): Promise<string | undefined> => {
|
|
132
|
+
const sent = await anchor.send({ content: text || '', ...NO_MENTIONS });
|
|
133
|
+
return sent.id;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const editThreaded = async (
|
|
137
|
+
anchor: ThreadChannel,
|
|
138
|
+
messageId: string,
|
|
139
|
+
text: string
|
|
140
|
+
): Promise<void> => {
|
|
141
|
+
const msg = await anchor.messages.fetch(messageId);
|
|
142
|
+
await msg.edit({ content: text || '', ...NO_MENTIONS });
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Discord returns 10008 (Unknown Message) when an activity-log message has
|
|
146
|
+
// been deleted by the user; Cloudflare/HTTP layers may surface a generic
|
|
147
|
+
// 404. Either case means the same thing: open a fresh log message.
|
|
148
|
+
const isMissingMessageError = (err: unknown): boolean => {
|
|
149
|
+
const code = (err as { code?: number; status?: number })?.code ?? 0;
|
|
150
|
+
return code === 404 || code === 10008;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const turnLog: TurnLogBuffer<ThreadChannel> = createTurnLogBuffer<ThreadChannel>({
|
|
154
|
+
postThreaded,
|
|
155
|
+
editThreaded,
|
|
156
|
+
isMissingMessageError,
|
|
157
|
+
options: threadLogOpts,
|
|
158
|
+
threadsEnabled: threadsGloballyEnabled,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const collapseDestination = (dest: Destination, turnId?: string): Destination => {
|
|
162
|
+
// Both the global `visibility.threads: false` kill switch and the
|
|
163
|
+
// per-channel `threadsDisabled` flag mean "quiet bot": drop thread-log
|
|
164
|
+
// activity rather than promoting it top-level. Top-level spam is only
|
|
165
|
+
// opt-in via `filters` (e.g. `/show`), matching pre-threaded behavior.
|
|
166
|
+
if (dest.kind !== 'thread-log') return dest;
|
|
167
|
+
if (!threadsGloballyEnabled) return { kind: 'drop' };
|
|
168
|
+
if (turnId && turnLog.threadsDisabledFor(turnId)) return { kind: 'drop' };
|
|
169
|
+
return dest;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const channelThreadsDisabled = async (chatId: string): Promise<boolean> => {
|
|
173
|
+
const state = await readDiscordState();
|
|
174
|
+
for (const [, entry] of Object.entries(state.channelChatMap || {})) {
|
|
175
|
+
if (entry?.chatId === chatId) return entry.threadsDisabled === true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const openThreadForTurn = async (
|
|
181
|
+
externalRef: string | undefined
|
|
182
|
+
): Promise<ThreadChannel | undefined> => {
|
|
183
|
+
if (!externalRef) return undefined;
|
|
184
|
+
const inbound = resolveInbound(externalRef);
|
|
185
|
+
if (!inbound) return undefined;
|
|
186
|
+
let channel: AnyTextChannel | null;
|
|
187
|
+
try {
|
|
188
|
+
channel = (await client.channels.fetch(inbound.channelId)) as AnyTextChannel | null;
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.warn(`Failed to fetch channel ${inbound.channelId} for turn anchor:`, err);
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
if (!channel || !channel.isTextBased() || channel.isDMBased() || channel.isThread()) {
|
|
194
|
+
// DMs and existing threads can't host a new thread. Skip silently —
|
|
195
|
+
// proactive turns and DM-only flows simply have no activity log.
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const guildChannel = channel as TextChannel | NewsChannel;
|
|
199
|
+
let userMessage: Message;
|
|
200
|
+
try {
|
|
201
|
+
userMessage = await guildChannel.messages.fetch(inbound.messageId);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.warn(`Failed to fetch user message ${inbound.messageId} for turn anchor:`, err);
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
// Discord allows only one thread per message. The same inbound can anchor
|
|
207
|
+
// multiple turns (e.g. a follow-up turn fanned out from the original
|
|
208
|
+
// request), so reuse an existing thread rather than failing the second
|
|
209
|
+
// turn's activity log.
|
|
210
|
+
if (userMessage.hasThread && userMessage.thread) {
|
|
211
|
+
return userMessage.thread as ThreadChannel;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return await userMessage.startThread({
|
|
215
|
+
name: 'Activity log',
|
|
216
|
+
// 1 day. Long agent runs (refactors, builds) outlive the previous
|
|
217
|
+
// 60-minute archive window and end up posting into archived threads
|
|
218
|
+
// that fall off the channel sidebar.
|
|
219
|
+
autoArchiveDuration: 1440,
|
|
220
|
+
});
|
|
221
|
+
} catch (err) {
|
|
222
|
+
// Race: another turn for the same inbound created the thread between
|
|
223
|
+
// our `hasThread` check and `startThread`. Discord returns 160004
|
|
224
|
+
// (THREAD_ALREADY_CREATED_FOR_MESSAGE). Re-fetch and reuse.
|
|
225
|
+
const code = (err as { code?: number })?.code;
|
|
226
|
+
if (code === 160004) {
|
|
227
|
+
try {
|
|
228
|
+
const refreshed = await guildChannel.messages.fetch(inbound.messageId);
|
|
229
|
+
if (refreshed.hasThread && refreshed.thread) {
|
|
230
|
+
return refreshed.thread as ThreadChannel;
|
|
231
|
+
}
|
|
232
|
+
} catch (refetchErr) {
|
|
233
|
+
console.warn(
|
|
234
|
+
`Failed to refetch user message ${inbound.messageId} after thread-exists race:`,
|
|
235
|
+
refetchErr
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
console.warn(`Failed to start thread on message ${inbound.messageId}:`, err);
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleTurnStarted = async (chatId: string, turnId: string, externalRef?: string) => {
|
|
245
|
+
// Single source of truth for "is the activity log on for this turn":
|
|
246
|
+
// global kill switch OR per-channel opt-out. The buffer's `engaged()`
|
|
247
|
+
// and `collapseDestination`'s `threadsDisabledFor()` both consult the
|
|
248
|
+
// ctx flag set here, so we don't have to re-derive it later.
|
|
249
|
+
const threadsDisabled = !threadsGloballyEnabled || (await channelThreadsDisabled(chatId));
|
|
250
|
+
// Skip the API roundtrip when we already know the log is off.
|
|
251
|
+
const anchor = threadsDisabled ? undefined : await openThreadForTurn(externalRef);
|
|
252
|
+
// No anchor and threads enabled: proactive turn (cron, subagent, CLI),
|
|
253
|
+
// DM-only flow, or thread creation failed. Skip start entirely so the
|
|
254
|
+
// buffer doesn't accrue entries it can never flush.
|
|
255
|
+
if (!anchor && !threadsDisabled) return;
|
|
256
|
+
turnLog.start({ turnId, threadsDisabled, anchorThread: anchor });
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const handleTurnEnded = async (turnId: string) => {
|
|
260
|
+
await turnLog.end(turnId);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const sendPolicyCard = async (chatId: string, message: ChatMessage): Promise<boolean> => {
|
|
264
|
+
if (message.role !== 'policy' || message.status !== 'pending') return false;
|
|
265
|
+
try {
|
|
266
|
+
const dm = await resolveDiscordDestination(client, discordUserId, chatId);
|
|
267
|
+
|
|
268
|
+
const embed = new EmbedBuilder()
|
|
269
|
+
.setTitle('Action Required: Policy Request')
|
|
270
|
+
.setDescription(message.content || 'A pending policy request requires your attention.')
|
|
271
|
+
.setColor(Colors.Yellow);
|
|
272
|
+
|
|
273
|
+
const policyId = ('requestId' in message && message.requestId) || message.id;
|
|
274
|
+
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
275
|
+
new ButtonBuilder()
|
|
276
|
+
.setCustomId(`approve|${policyId}|${chatId}`)
|
|
277
|
+
.setLabel('Approve')
|
|
278
|
+
.setStyle(ButtonStyle.Success),
|
|
279
|
+
new ButtonBuilder()
|
|
280
|
+
.setCustomId(`reject|${policyId}|${chatId}`)
|
|
281
|
+
.setLabel('Reject')
|
|
282
|
+
.setStyle(ButtonStyle.Danger)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const optionsMsg: MessageCreateOptions = {
|
|
286
|
+
embeds: [embed],
|
|
287
|
+
components: [row],
|
|
288
|
+
...NO_MENTIONS,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await dm.send(optionsMsg);
|
|
293
|
+
} catch (richError) {
|
|
294
|
+
console.warn(
|
|
295
|
+
`Failed to send rich message to Discord user ${discordUserId}, falling back to plain text:`,
|
|
296
|
+
richError
|
|
297
|
+
);
|
|
298
|
+
await dm.send({
|
|
299
|
+
content: `Action Required: Policy Request\n\n${
|
|
300
|
+
message.content || 'A pending policy request requires your attention.'
|
|
301
|
+
}\n\nApprove: \`/approve ${policyId}\`\nReject: \`/reject ${policyId} <optional_rationale>\``,
|
|
302
|
+
...NO_MENTIONS,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(`Failed to send message to Discord user ${discordUserId}:`, error);
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const sendTopLevel = async (chatId: string, message: ChatMessage): Promise<void> => {
|
|
312
|
+
if ('level' in message && (message as { level?: string }).level === 'verbose') return;
|
|
313
|
+
|
|
314
|
+
const hasContent = !!message.content?.trim();
|
|
315
|
+
const files = 'files' in message ? ((message as { files?: string[] }).files ?? []) : [];
|
|
316
|
+
const hasFiles = Array.isArray(files) && files.length > 0;
|
|
317
|
+
|
|
318
|
+
let absoluteFiles: string[] = [];
|
|
319
|
+
if (hasFiles) {
|
|
320
|
+
const workspaceRoot = getWorkspaceRoot(process.cwd());
|
|
321
|
+
absoluteFiles = files.map((f) => path.resolve(workspaceRoot, f));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!hasContent && !hasFiles) return;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const dm = await resolveDiscordDestination(client, discordUserId, chatId);
|
|
328
|
+
const formattedContent = formatMessage(message);
|
|
329
|
+
|
|
330
|
+
if (formattedContent && formattedContent.length > 2000) {
|
|
331
|
+
const chunks = chunkString(formattedContent, 2000);
|
|
332
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
333
|
+
if (signal?.aborted) break;
|
|
334
|
+
const chunkOptions: MessageCreateOptions = {
|
|
335
|
+
content: chunks[i] as string,
|
|
336
|
+
...NO_MENTIONS,
|
|
337
|
+
};
|
|
338
|
+
if (i === chunks.length - 1 && hasFiles) {
|
|
339
|
+
chunkOptions.files = absoluteFiles;
|
|
340
|
+
}
|
|
341
|
+
await dm.send(chunkOptions);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
const optionsMsg: MessageCreateOptions = { ...NO_MENTIONS };
|
|
345
|
+
if (formattedContent) optionsMsg.content = formattedContent;
|
|
346
|
+
if (hasFiles) optionsMsg.files = absoluteFiles;
|
|
347
|
+
await dm.send(optionsMsg);
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error(`Failed to send message to Discord user ${discordUserId}:`, error);
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const handleMessageForChat = async (chatId: string, message: ChatMessage): Promise<void> => {
|
|
356
|
+
const routed = routeMessage(message, config);
|
|
357
|
+
const effective = collapseDestination(routed, message.turnId);
|
|
358
|
+
|
|
359
|
+
if (effective.kind === 'drop') return;
|
|
360
|
+
|
|
361
|
+
if (effective.kind === 'thread-log') {
|
|
362
|
+
if (!message.turnId) {
|
|
363
|
+
console.warn(`thread-log event for ${message.role} has no turnId — dropping.`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// No turn context: turnStarted may have been missed (adapter restart,
|
|
367
|
+
// subscription reconnect) or the turn had no anchor (proactive / DM).
|
|
368
|
+
// Drop silently rather than flooding the chat.
|
|
369
|
+
if (!turnLog.has(message.turnId)) return;
|
|
370
|
+
turnLog.append(message.turnId, message);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Top-level.
|
|
375
|
+
if (message.role === 'policy' && message.status === 'pending') {
|
|
376
|
+
await sendPolicyCard(chatId, message);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await sendTopLevel(chatId, message);
|
|
381
|
+
};
|
|
382
|
+
|
|
87
383
|
const startSubscriptionForChat = async (chatId: string) => {
|
|
88
384
|
if (activeSubscriptions.has(chatId)) return;
|
|
89
385
|
if (signal?.aborted) return;
|
|
@@ -116,6 +412,15 @@ export async function startDaemonToDiscordForwarder(
|
|
|
116
412
|
let subscription: { unsubscribe: () => void } | null = null;
|
|
117
413
|
let messageQueue = Promise.resolve();
|
|
118
414
|
|
|
415
|
+
type StreamItem =
|
|
416
|
+
| { kind: 'message'; message: ChatMessage }
|
|
417
|
+
| {
|
|
418
|
+
kind: 'turn';
|
|
419
|
+
event:
|
|
420
|
+
| { type: 'started'; turnId: string; rootMessageId: string; externalRef?: string }
|
|
421
|
+
| { type: 'ended'; turnId: string; outcome: 'ok' | 'error' };
|
|
422
|
+
};
|
|
423
|
+
|
|
119
424
|
const connect = () => {
|
|
120
425
|
if (signal?.aborted || !activeSubscriptions.has(chatId)) {
|
|
121
426
|
return;
|
|
@@ -124,141 +429,54 @@ export async function startDaemonToDiscordForwarder(
|
|
|
124
429
|
subscription = trpc.waitForMessages.subscribe(
|
|
125
430
|
{ chatId, lastMessageId },
|
|
126
431
|
{
|
|
127
|
-
onData: (
|
|
432
|
+
onData: (items) => {
|
|
128
433
|
retryDelay = 1000; // Reset retry delay on successful data
|
|
129
434
|
|
|
130
|
-
if (!Array.isArray(
|
|
435
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
131
436
|
return;
|
|
132
437
|
}
|
|
133
438
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const isPolicyRequest =
|
|
146
|
-
logMessage.role === 'policy' && logMessage.status === 'pending';
|
|
147
|
-
|
|
148
|
-
if (isPolicyRequest) {
|
|
439
|
+
messageQueue = messageQueue
|
|
440
|
+
.then(async () => {
|
|
441
|
+
for (const raw of items) {
|
|
442
|
+
if (signal?.aborted || !activeSubscriptions.has(chatId)) break;
|
|
443
|
+
|
|
444
|
+
const item = raw as StreamItem;
|
|
445
|
+
if (item.kind === 'turn') {
|
|
446
|
+
// Turn events do disk reads (state.json) and Discord API
|
|
447
|
+
// fetches; either can throw transiently. Catch here so a
|
|
448
|
+
// single bad event doesn't reject the .then and poison
|
|
449
|
+
// the chain — every subsequent batch would silently no-op.
|
|
149
450
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
.
|
|
154
|
-
.setDescription(
|
|
155
|
-
logMessage.content || 'A pending policy request requires your attention.'
|
|
156
|
-
)
|
|
157
|
-
.setColor(Colors.Yellow);
|
|
158
|
-
|
|
159
|
-
const policyId =
|
|
160
|
-
('requestId' in logMessage && logMessage.requestId) || logMessage.id;
|
|
161
|
-
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
162
|
-
new ButtonBuilder()
|
|
163
|
-
.setCustomId(`approve|${policyId}|${chatId}`)
|
|
164
|
-
.setLabel('Approve')
|
|
165
|
-
.setStyle(ButtonStyle.Success),
|
|
166
|
-
new ButtonBuilder()
|
|
167
|
-
.setCustomId(`reject|${policyId}|${chatId}`)
|
|
168
|
-
.setLabel('Reject')
|
|
169
|
-
.setStyle(ButtonStyle.Danger)
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const optionsMsg: MessageCreateOptions = {
|
|
173
|
-
embeds: [embed],
|
|
174
|
-
components: [row],
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await dm.send(optionsMsg);
|
|
179
|
-
} catch (richError) {
|
|
180
|
-
console.warn(
|
|
181
|
-
`Failed to send rich message to Discord user ${discordUserId}, falling back to plain text:`,
|
|
182
|
-
richError
|
|
183
|
-
);
|
|
184
|
-
await dm.send({
|
|
185
|
-
content: `Action Required: Policy Request\n\n${logMessage.content || 'A pending policy request requires your attention.'}\n\nApprove: \`/approve ${policyId}\`\nReject: \`/reject ${policyId} <optional_rationale>\``,
|
|
186
|
-
});
|
|
451
|
+
if (item.event.type === 'started') {
|
|
452
|
+
await handleTurnStarted(chatId, item.event.turnId, item.event.externalRef);
|
|
453
|
+
} else {
|
|
454
|
+
await handleTurnEnded(item.event.turnId);
|
|
187
455
|
}
|
|
188
|
-
} catch (
|
|
189
|
-
console.error(
|
|
190
|
-
`Failed to send message to Discord user ${discordUserId}:`,
|
|
191
|
-
error
|
|
192
|
-
);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.error('Failed to handle turn event:', err);
|
|
193
458
|
}
|
|
194
|
-
|
|
195
|
-
await saveLastMessageId(chatId, logMessage.id).catch(console.error);
|
|
196
|
-
lastMessageId = logMessage.id;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if ('level' in logMessage && logMessage.level === 'verbose') {
|
|
201
|
-
await saveLastMessageId(chatId, logMessage.id).catch(console.error);
|
|
202
|
-
lastMessageId = logMessage.id;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const hasContent = !!logMessage.content?.trim();
|
|
207
|
-
const files = 'files' in logMessage ? (logMessage.files as string[]) : undefined;
|
|
208
|
-
const hasFiles = Array.isArray(files) && files.length > 0;
|
|
209
|
-
|
|
210
|
-
// The daemon stores logMessage.files as paths relative to the WORKSPACE directory
|
|
211
|
-
// (the directory containing .clawmini). We must resolve these against the current
|
|
212
|
-
// workspace root so discord.js can successfully locate and read the files.
|
|
213
|
-
let absoluteFiles: string[] = [];
|
|
214
|
-
if (hasFiles && files) {
|
|
215
|
-
const workspaceRoot = getWorkspaceRoot(process.cwd());
|
|
216
|
-
absoluteFiles = files.map((f) => path.resolve(workspaceRoot, f));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (!hasContent && !hasFiles) {
|
|
220
|
-
await saveLastMessageId(chatId, logMessage.id).catch(console.error);
|
|
221
|
-
lastMessageId = logMessage.id;
|
|
222
459
|
continue;
|
|
223
460
|
}
|
|
224
461
|
|
|
462
|
+
const message = item.message;
|
|
225
463
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (signal?.aborted || !activeSubscriptions.has(chatId)) break;
|
|
233
|
-
const chunkOptions: MessageCreateOptions = { content: chunks[i] as string };
|
|
234
|
-
if (i === chunks.length - 1 && hasFiles) {
|
|
235
|
-
chunkOptions.files = absoluteFiles;
|
|
236
|
-
}
|
|
237
|
-
await dm.send(chunkOptions);
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
const optionsMsg: MessageCreateOptions = {};
|
|
241
|
-
if (formattedContent) {
|
|
242
|
-
optionsMsg.content = formattedContent;
|
|
243
|
-
}
|
|
244
|
-
if (hasFiles) {
|
|
245
|
-
optionsMsg.files = absoluteFiles;
|
|
246
|
-
}
|
|
247
|
-
await dm.send(optionsMsg);
|
|
248
|
-
}
|
|
249
|
-
} catch (error) {
|
|
250
|
-
console.error(
|
|
251
|
-
`Failed to send message to Discord user ${discordUserId}:`,
|
|
252
|
-
error
|
|
253
|
-
);
|
|
254
|
-
break; // don't advance lastMessageId
|
|
464
|
+
await handleMessageForChat(chatId, message);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.error('Failed to handle message:', err);
|
|
467
|
+
// Don't advance lastMessageId on a hard error so we retry on
|
|
468
|
+
// reconnect; matches prior behavior.
|
|
469
|
+
break;
|
|
255
470
|
}
|
|
256
|
-
}
|
|
257
471
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
472
|
+
await saveLastMessageId(chatId, message.id).catch(console.error);
|
|
473
|
+
lastMessageId = message.id;
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
// Belt-and-suspenders: anything that escapes the per-item
|
|
477
|
+
// try/catches above (sync throw before the loop, etc.) must
|
|
478
|
+
// not leave the chain in a rejected state.
|
|
479
|
+
.catch((err) => console.error('Message queue chain error:', err));
|
|
262
480
|
},
|
|
263
481
|
onError: (error) => {
|
|
264
482
|
console.error(
|
|
@@ -299,7 +517,7 @@ export async function startDaemonToDiscordForwarder(
|
|
|
299
517
|
{ chatId },
|
|
300
518
|
{
|
|
301
519
|
onData: async (event) => {
|
|
302
|
-
typingRetryDelay = 1000;
|
|
520
|
+
typingRetryDelay = 1000;
|
|
303
521
|
if (!event) return;
|
|
304
522
|
|
|
305
523
|
try {
|
|
@@ -416,6 +634,7 @@ export async function startDaemonToDiscordForwarder(
|
|
|
416
634
|
watcher.close();
|
|
417
635
|
for (const sub of activeSubscriptions.values()) sub.unsubscribe();
|
|
418
636
|
for (const sub of activeTypingSubscriptions.values()) sub.unsubscribe();
|
|
637
|
+
turnLog.shutdown();
|
|
419
638
|
resolve();
|
|
420
639
|
});
|
|
421
640
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
recordInbound,
|
|
4
|
+
resolveInbound,
|
|
5
|
+
INBOUND_TTL_MS,
|
|
6
|
+
_resetInboundCacheForTests,
|
|
7
|
+
} from './inbound-cache.js';
|
|
8
|
+
|
|
9
|
+
describe('discord inbound-cache', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
_resetInboundCacheForTests();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.useRealTimers();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('records and resolves an inbound by message id', () => {
|
|
19
|
+
recordInbound({ messageId: 'msg-1', channelId: 'chan-1' });
|
|
20
|
+
expect(resolveInbound('msg-1')).toMatchObject({
|
|
21
|
+
messageId: 'msg-1',
|
|
22
|
+
channelId: 'chan-1',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for unknown keys', () => {
|
|
27
|
+
expect(resolveInbound('unknown')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('expires entries older than INBOUND_TTL_MS on resolve', () => {
|
|
31
|
+
vi.useFakeTimers();
|
|
32
|
+
recordInbound({ messageId: 'msg-1', channelId: 'chan-1' });
|
|
33
|
+
expect(resolveInbound('msg-1')).not.toBeNull();
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(INBOUND_TTL_MS + 1000);
|
|
36
|
+
expect(resolveInbound('msg-1')).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sweeps expired entries on every insert', () => {
|
|
40
|
+
vi.useFakeTimers();
|
|
41
|
+
recordInbound({ messageId: 'msg-1', channelId: 'chan-1' });
|
|
42
|
+
vi.advanceTimersByTime(INBOUND_TTL_MS + 1000);
|
|
43
|
+
recordInbound({ messageId: 'msg-2', channelId: 'chan-2' });
|
|
44
|
+
expect(resolveInbound('msg-1')).toBeNull();
|
|
45
|
+
expect(resolveInbound('msg-2')).not.toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord-side wrapper around the shared inbound-message TTL cache.
|
|
3
|
+
*
|
|
4
|
+
* On every inbound user message, the gateway records `{ messageId, channelId
|
|
5
|
+
* }`. The same `messageId` is sent to the daemon as `externalRef` on the
|
|
6
|
+
* `sendMessage` mutation. When the forwarder later sees `turnStarted` with
|
|
7
|
+
* that `externalRef`, it resolves the channel + message id and starts a
|
|
8
|
+
* Discord thread anchored on the user's message.
|
|
9
|
+
*/
|
|
10
|
+
import { createInboundCache } from '../shared/adapters/inbound-cache.js';
|
|
11
|
+
|
|
12
|
+
export const INBOUND_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
13
|
+
|
|
14
|
+
interface DiscordInboundValue {
|
|
15
|
+
channelId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const cache = createInboundCache<DiscordInboundValue>(INBOUND_TTL_MS);
|
|
19
|
+
|
|
20
|
+
export interface DiscordInboundRecord {
|
|
21
|
+
messageId: string;
|
|
22
|
+
channelId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function recordInbound(entry: DiscordInboundRecord): void {
|
|
26
|
+
cache.record(entry.messageId, { channelId: entry.channelId });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveInbound(messageId: string): DiscordInboundRecord | null {
|
|
30
|
+
const value = cache.resolve(messageId);
|
|
31
|
+
return value ? { messageId, channelId: value.channelId } : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Test hook: drop all cached records. */
|
|
35
|
+
export function _resetInboundCacheForTests(): void {
|
|
36
|
+
cache.reset();
|
|
37
|
+
}
|