@0xmaxma/claude-gateway 1.3.24 → 1.3.31
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 +172 -50
- package/config.template.json +6 -2
- package/dist/agent/incident-store.d.ts +89 -0
- package/dist/agent/incident-store.d.ts.map +1 -0
- package/dist/agent/incident-store.js +299 -0
- package/dist/agent/incident-store.js.map +1 -0
- package/dist/agent/incident.d.ts +156 -0
- package/dist/agent/incident.d.ts.map +1 -0
- package/dist/agent/incident.js +177 -0
- package/dist/agent/incident.js.map +1 -0
- package/dist/agent/recovery-executor.d.ts +117 -0
- package/dist/agent/recovery-executor.d.ts.map +1 -0
- package/dist/agent/recovery-executor.js +168 -0
- package/dist/agent/recovery-executor.js.map +1 -0
- package/dist/agent/recovery-policy.d.ts +97 -0
- package/dist/agent/recovery-policy.d.ts.map +1 -0
- package/dist/agent/recovery-policy.js +164 -0
- package/dist/agent/recovery-policy.js.map +1 -0
- package/dist/agent/runner.d.ts +44 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +272 -2
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/safe-mode.d.ts +61 -0
- package/dist/agent/safe-mode.d.ts.map +1 -0
- package/dist/agent/safe-mode.js +102 -0
- package/dist/agent/safe-mode.js.map +1 -0
- package/dist/agent/triage.d.ts +94 -0
- package/dist/agent/triage.d.ts.map +1 -0
- package/dist/agent/triage.js +209 -0
- package/dist/agent/triage.js.map +1 -0
- package/dist/agent/turn-trace.d.ts +120 -0
- package/dist/agent/turn-trace.d.ts.map +1 -0
- package/dist/agent/turn-trace.js +122 -0
- package/dist/agent/turn-trace.js.map +1 -0
- package/dist/api/gateway-router.d.ts +21 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +58 -17
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-pending-senders.d.ts +7 -2
- package/dist/api/line-pending-senders.d.ts.map +1 -1
- package/dist/api/line-pending-senders.js +7 -2
- package/dist/api/line-pending-senders.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +563 -172
- package/dist/api/router.js.map +1 -1
- package/dist/api/wizard-state.d.ts +1 -4
- package/dist/api/wizard-state.d.ts.map +1 -1
- package/dist/api/wizard-state.js.map +1 -1
- package/dist/config/migrator.d.ts +4 -0
- package/dist/config/migrator.d.ts.map +1 -1
- package/dist/config/migrator.js +60 -3
- package/dist/config/migrator.js.map +1 -1
- package/dist/discord/receiver.d.ts +1 -0
- package/dist/discord/receiver.d.ts.map +1 -1
- package/dist/discord/receiver.js +24 -6
- package/dist/discord/receiver.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts +18 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +61 -1
- package/dist/session/process.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +59 -0
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/control-channel.d.ts +74 -0
- package/dist/shell/control-channel.d.ts.map +1 -0
- package/dist/shell/control-channel.js +114 -0
- package/dist/shell/control-channel.js.map +1 -0
- package/dist/telegram/receiver.d.ts +1 -0
- package/dist/telegram/receiver.d.ts.map +1 -1
- package/dist/telegram/receiver.js +18 -6
- package/dist/telegram/receiver.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +103 -2
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/tools/discord/access.ts +136 -20
- package/mcp/tools/discord/client.ts +3 -1
- package/mcp/tools/discord/module.ts +75 -4
- package/mcp/tools/discord/skills/access/SKILL.md +66 -8
- package/mcp/tools/discord/skills/configure/SKILL.md +11 -1
- package/mcp/tools/discord/types.ts +21 -2
- package/mcp/tools/skills/handlers.ts +4 -2
- package/mcp/tools/telegram/dedup.ts +4 -1
- package/mcp/tools/telegram/module.ts +5 -14
- package/mcp/tools/telegram/pure.ts +169 -31
- package/mcp/tools/telegram/receiver-server.ts +271 -78
- package/mcp/tools/telegram/skills/access/SKILL.md +93 -23
- package/mcp/tools/telegram/skills/configure/SKILL.md +31 -26
- package/mcp/tools/telegram/typing.ts +124 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -293,6 +293,52 @@ Controls the Claude subprocess backend for all non-app agents.
|
|
|
293
293
|
|
|
294
294
|
This setting is hot-reloadable — new sessions pick it up without a restart.
|
|
295
295
|
|
|
296
|
+
### `gateway.selfHealing.autoRecover`
|
|
297
|
+
|
|
298
|
+
Opt-in self-healing for the turn-trace watchdog (Epic #195). When a turn stalls, the gateway always detects it, logs a scrubbed incident, and notifies the affected chat. This flag additionally controls whether the gateway may *act* on a stall.
|
|
299
|
+
|
|
300
|
+
| Value | Behaviour |
|
|
301
|
+
|-------|-----------|
|
|
302
|
+
| `false` *(default)* | Detection + incident logging + notification only — no automatic action |
|
|
303
|
+
| `true` | The watchdog may run a whitelisted recovery for a stalled turn: a keystroke into the TUI (esc / enter / arrow / menu selection), a session restart, a reversible safe-mode fallback to the headless backend, and — after a successful unblock — a guarded resend of the last message (only if the turn produced no output, so it is never double-submitted) |
|
|
304
|
+
|
|
305
|
+
Recovery actions are clamped to a per-stage whitelist and a per-turn budget, and any local triage treats the on-screen text as untrusted data validated against a closed schema. Safe-mode auto-fallback on a hard PTY failure is independent of this flag (it is always reversible and never presses keys). In-memory only — a gateway restart re-reads your real config.
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"gateway": {
|
|
310
|
+
"selfHealing": {
|
|
311
|
+
"autoRecover": true
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### `gateway.bind`
|
|
318
|
+
|
|
319
|
+
Network interface the HTTP/WebSocket server binds to. Defaults to `127.0.0.1` (localhost-only), so the dashboard and API are **not** exposed to the local network out of the box. Set to `0.0.0.0` to listen on all interfaces (for example when a containerized reverse proxy needs to reach the gateway). The `GATEWAY_BIND` environment variable, when set, takes precedence over this field.
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"gateway": {
|
|
324
|
+
"bind": "127.0.0.1"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
> **⚠️ Upgrade note:** the default bind changed from `0.0.0.0` to `127.0.0.1` (configVersion 1.0.13). To avoid silently cutting off external access, the config migrator is **behavior-preserving**: whenever it upgrades a config that never set `gateway.bind`, it pins `bind` to `0.0.0.0` and logs a one-time warning, so a deployment that was reachable from another host stays reachable. This applies to *any* upgraded config with no `bind` key — including one already stamped `1.0.13` that never received a bind (an earlier version gated this on `< 1.0.13` and left such configs stuck on the `127.0.0.1` default). New installs (no prior config, so no migration runs) keep the secure `127.0.0.1` default. If you *want* localhost-only after upgrading, set `gateway.bind` to `127.0.0.1` explicitly (or the `GATEWAY_BIND` env var).
|
|
330
|
+
|
|
331
|
+
### Terminal Viewer — interactive terminal mode
|
|
332
|
+
|
|
333
|
+
The dashboard's **Terminal Viewer** opens read-only (a live mirror of the PTY). A toggle in the top-right of the viewer switches it into an **interactive terminal**: keystrokes typed into the panel — printable characters, Enter, arrows, Ctrl-combos, Esc — are streamed into the live PTY, and the panel title changes to reflect the active mode. This is a per-browser client-side choice (Issue #201); there is no server config flag to enable it.
|
|
334
|
+
|
|
335
|
+
Because interactive mode turns a read-only view into a remote-write surface, access is protected upstream rather than by a feature flag:
|
|
336
|
+
|
|
337
|
+
- **Authentication** — the WebSocket requires a valid dashboard ticket or API key.
|
|
338
|
+
- **`gateway.bind`** — the gateway binds to `127.0.0.1` (localhost) by default, so the dashboard is not reachable from the network out of the box. Expose a non-loopback bind (`0.0.0.0`) **only** behind a trusted authenticating reverse proxy.
|
|
339
|
+
|
|
340
|
+
Inbound frames are always bounded (text-only, size-capped) and are dropped for headless sessions (no PTY).
|
|
341
|
+
|
|
296
342
|
### `gateway.api.keys`
|
|
297
343
|
|
|
298
344
|
Each key has a `key` string (supports `${ENV_VAR}` interpolation), an optional `description`, and an `agents` field — either an array of agent IDs or `"*"` for full access. Keys support both `Authorization: Bearer` and `X-Api-Key` headers.
|
|
@@ -485,7 +531,7 @@ The gateway proxies `/app/:name/:portName/*` to the app containers. Two env vars
|
|
|
485
531
|
|
|
486
532
|
| Env var | Default | Description |
|
|
487
533
|
|---------|---------|-------------|
|
|
488
|
-
| `GATEWAY_BIND` | `
|
|
534
|
+
| `GATEWAY_BIND` | `127.0.0.1` | Gateway HTTP listen address. Overrides the `gateway.bind` config field when set. Defaults to localhost-only; set to `0.0.0.0` when a **containerized** reverse proxy (Caddy, nginx in Docker) needs to reach the gateway across container boundaries. A **host-network** proxy (Traefik on host) can keep the localhost default. |
|
|
489
535
|
| `DOCKER_HOST` | _(system default)_ | Docker socket/TCP address. When set to `tcp://host:port` (e.g. DinD), the gateway automatically uses the host extracted from `DOCKER_HOST` to proxy to app containers instead of `127.0.0.1`. |
|
|
490
536
|
|
|
491
537
|
Example Caddyfile for apps behind Caddy in Docker:
|
|
@@ -733,20 +779,28 @@ When the config schema changes (new fields added in `config.template.json`), the
|
|
|
733
779
|
|
|
734
780
|
## Pairing New Users
|
|
735
781
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
2.
|
|
741
|
-
3. Approve it:
|
|
782
|
+
New agents default to `dmPolicy: "allowlist"` with the orthogonal `pairing`
|
|
783
|
+
toggle **on**, so pairing works out of the box — no setup needed.
|
|
784
|
+
|
|
785
|
+
1. Ask the user to DM the bot — they receive a 6-character pairing code
|
|
786
|
+
2. Approve it:
|
|
742
787
|
```bash
|
|
743
|
-
npm run pair -- --agent=alfred --code=abc123
|
|
788
|
+
npm run pair -- --agent=alfred --code=abc123 --channel=discord
|
|
744
789
|
```
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
790
|
+
(omit `--channel` or use `--channel=telegram` for Telegram)
|
|
791
|
+
3. The bot confirms pairing within 5 seconds
|
|
792
|
+
4. Lock down after everyone is paired (optional) — turn the pairing toggle off
|
|
793
|
+
so unknown senders are dropped silently (the base policy is already
|
|
794
|
+
`allowlist`):
|
|
749
795
|
```
|
|
796
|
+
/gateway:discord-access dm-pairing off # Discord
|
|
797
|
+
/telegram:access pairing off # Telegram
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
`pairing` is an **orthogonal on/off toggle**, not a `dmPolicy` value: the base
|
|
801
|
+
policy stays `open` | `allowlist` | `disabled`, and pairing layers on top of
|
|
802
|
+
`allowlist`. A legacy `access.json` with `"dmPolicy": "pairing"` is migrated
|
|
803
|
+
automatically on read to `{ dmPolicy: "allowlist", pairing: true }`.
|
|
750
804
|
|
|
751
805
|
To manage channels (add/remove Telegram or Discord) on an existing agent:
|
|
752
806
|
```bash
|
|
@@ -755,78 +809,135 @@ make update-agent # choose "Manage channels"
|
|
|
755
809
|
|
|
756
810
|
---
|
|
757
811
|
|
|
812
|
+
## Channel Conditions & Limitations
|
|
813
|
+
|
|
814
|
+
Each channel gates inbound messages in two tiers — **DM/1:1** and **group** — and
|
|
815
|
+
each has platform-level conditions that must be met *before* the gateway ever
|
|
816
|
+
sees a message. If those aren't met the bot looks online but stays silent.
|
|
817
|
+
|
|
818
|
+
| Channel | Scope | Message reaches the bot when… | Access gate | Answers in group when… |
|
|
819
|
+
|---------|-------|-------------------------------|-------------|------------------------|
|
|
820
|
+
| **Telegram** | DM | always (long-polling) | `dmPolicy` + `pairing` → `allowFrom` | — |
|
|
821
|
+
| | Group | bot is **Admin**, or **Privacy Mode is OFF** + re-added; otherwise only `/cmd`, @mentions, replies | `groupPolicy` + `groupAllowlist` | `requireMention` false, or @mentioned/replied |
|
|
822
|
+
| **Discord** | DM | **Message Content Intent** enabled | `dmPolicy` + `pairing` → `allowFrom` | — |
|
|
823
|
+
| | Guild | **Message Content Intent** + **View Channel** + **Read Message History** | `groupPolicy` + `guildAllowlist` (+ optional `channelAllowlist`/`roleAllowlist`) | `requireMention` false, or @mentioned/replied |
|
|
824
|
+
| **LINE** | 1:1 | webhook delivered (valid signature) | `dmPolicy` | — |
|
|
825
|
+
| | Group/Room | webhook delivered + bot is a member | `groupPolicy` + `groupAllowlist` | `requireMention` false, or **native** @mention |
|
|
826
|
+
|
|
827
|
+
**Telegram limits**
|
|
828
|
+
- Exactly one process may poll a bot token — a second poller causes `409 Conflict`.
|
|
829
|
+
- Bot **commands are DM-only**; in groups they're silently dropped.
|
|
830
|
+
- Group **Privacy Mode is ON by default** — see [Telegram Groups](#telegram-groups). Admin status bypasses it; a Privacy-Mode change only applies after the bot is removed and re-added.
|
|
831
|
+
- Pairing codes: DM knocks reply the code privately; group knocks post the code in the group (needs a message that actually reaches the bot, i.e. Admin/Privacy-off).
|
|
832
|
+
|
|
833
|
+
**Discord limits**
|
|
834
|
+
- **MESSAGE CONTENT INTENT** is a privileged intent — without it message text arrives empty, so the bot can neither answer nor pair. Enable it in the Developer Portal.
|
|
835
|
+
- The bot needs channel permissions **View Channel**, **Read Message History**, **Send Messages** (+ **Create Public Threads** / **Send Messages in Threads** if `DISCORD_AUTO_THREAD=true`).
|
|
836
|
+
- `channelAllowlist` / `roleAllowlist` are backend-only filters (no web UI) applied after the guild gate.
|
|
837
|
+
|
|
838
|
+
**LINE limits**
|
|
839
|
+
- Inbound arrives via the Express **webhook**, not polling; the signature is verified over the **exact raw bytes**. Front it with the bun CORS proxy (see `/tunnel`) — never point cloudflared straight at the gateway, or chunked bodies break the signature and webhooks are dropped.
|
|
840
|
+
- Group/room `requireMention` uses LINE's **native mention** only (`mention.mentionees[].isSelf`). Typing the bot's name as plain text does **not** count, and `@All` does **not** count as a bot mention.
|
|
841
|
+
- Delivery is **reply-token-first (free) → push fallback (metered)**. The single-use reply token lives only ~1 min; after that, replies consume the OA's monthly push quota.
|
|
842
|
+
- Max **5 message objects** per reply/push request (the gateway auto-chunks to fit).
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
758
846
|
## Telegram Groups
|
|
759
847
|
|
|
760
|
-
The bot can respond in Telegram groups and supergroups.
|
|
848
|
+
The bot can respond in Telegram groups and supergroups. A group must be in the
|
|
849
|
+
agent's `groupAllowlist` before the bot will answer there.
|
|
850
|
+
|
|
851
|
+
### Delivery gotcha: Privacy Mode (read this first)
|
|
761
852
|
|
|
762
|
-
|
|
853
|
+
Telegram bots ship with **Privacy Mode ON** (`getMe` returns
|
|
854
|
+
`can_read_all_group_messages: false`). A privacy-mode bot only *receives*, inside
|
|
855
|
+
a group:
|
|
763
856
|
|
|
764
|
-
|
|
857
|
+
- messages that start with `/` (commands),
|
|
858
|
+
- messages that @mention the bot's username, and
|
|
859
|
+
- replies to the bot's own messages.
|
|
765
860
|
|
|
766
|
-
|
|
861
|
+
Everything else is filtered by Telegram **before it reaches the gateway** — the
|
|
862
|
+
bot looks online but never sees the message, so it can neither answer nor mint a
|
|
863
|
+
pairing code. On top of that, bot commands (`/start`, `/status`, …) are
|
|
864
|
+
**DM-only**: the receiver silently drops them in groups so pairing codes can't
|
|
865
|
+
leak to other members. Net effect in a default-privacy group: a plain message is
|
|
866
|
+
invisible and a command is dropped, so nothing happens.
|
|
767
867
|
|
|
768
|
-
|
|
868
|
+
Do one of these so the bot actually receives group messages:
|
|
769
869
|
|
|
770
|
-
|
|
870
|
+
- **Promote the bot to Admin in the group (easiest).** An admin bot receives
|
|
871
|
+
every message regardless of Privacy Mode — no BotFather change, no re-add. Any
|
|
872
|
+
admin role works, even the most restricted.
|
|
873
|
+
- **Disable Privacy Mode**, then **remove and re-add the bot** to the group (the
|
|
874
|
+
new setting only applies on re-join): [@BotFather](https://t.me/BotFather) →
|
|
875
|
+
`/setprivacy` → pick the bot → **Disable**.
|
|
876
|
+
|
|
877
|
+
### Register the group
|
|
878
|
+
|
|
879
|
+
Once the bot can receive group messages, add the group to `groupAllowlist` one of
|
|
880
|
+
two ways.
|
|
881
|
+
|
|
882
|
+
**Option A — pairing code (recommended).** With `groupPolicy: "allowlist"` and
|
|
883
|
+
`pairing: true` (both defaults), send any message in the group. The bot replies
|
|
884
|
+
with a 6-character code. Approve it from a gateway agent session:
|
|
771
885
|
|
|
772
|
-
Alternatively, send a message in the group and visit:
|
|
773
886
|
```
|
|
774
|
-
|
|
887
|
+
/telegram:access pair <code>
|
|
775
888
|
```
|
|
776
|
-
Look for `"chat":{"id": ...}` in the result.
|
|
777
889
|
|
|
778
|
-
|
|
890
|
+
That adds the group id to `groupAllowlist` (the code also lands in the agent's
|
|
891
|
+
`pending` as a `"kind": "group"` entry).
|
|
779
892
|
|
|
780
|
-
|
|
893
|
+
**Option B — edit `access.json` directly.** Get the group id by forwarding any
|
|
894
|
+
group message to [@userinfobot](https://t.me/userinfobot) — a negative number
|
|
895
|
+
like `-1001234567890` — then edit:
|
|
781
896
|
|
|
782
897
|
```
|
|
783
898
|
~/.claude-gateway/agents/<your-agent-id>/workspace/.telegram-state/access.json
|
|
784
899
|
```
|
|
785
900
|
|
|
786
|
-
Add the group under `"groups"`:
|
|
787
|
-
|
|
788
901
|
```json
|
|
789
902
|
{
|
|
790
903
|
"dmPolicy": "allowlist",
|
|
904
|
+
"pairing": true,
|
|
791
905
|
"allowFrom": ["..."],
|
|
792
|
-
"
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
"allowFrom": []
|
|
796
|
-
}
|
|
797
|
-
}
|
|
906
|
+
"groupPolicy": "allowlist",
|
|
907
|
+
"groupAllowlist": ["-1001234567890"],
|
|
908
|
+
"requireMention": true
|
|
798
909
|
}
|
|
799
910
|
```
|
|
800
911
|
|
|
801
|
-
|
|
802
|
-
|
|
912
|
+
`access.json` is re-read on every inbound message — changes take effect
|
|
913
|
+
immediately, no restart.
|
|
803
914
|
|
|
804
|
-
|
|
915
|
+
### Mention gate
|
|
805
916
|
|
|
806
|
-
|
|
917
|
+
`requireMention` is a single top-level boolean (default `true`):
|
|
807
918
|
|
|
808
|
-
|
|
919
|
+
- `true` — the bot answers in an allowlisted group only when @mentioned or
|
|
920
|
+
replied to. This relies on Telegram delivering the @mention; if the bot ignores
|
|
921
|
+
mentions, make it an Admin (see above).
|
|
922
|
+
- `false` — the bot answers **every** message in an allowlisted group. This only
|
|
923
|
+
does anything if the bot can *see* every message, i.e. you also promoted it to
|
|
924
|
+
Admin or disabled Privacy Mode.
|
|
809
925
|
|
|
810
|
-
|
|
926
|
+
Toggle it with `/telegram:access group mention <on|off>`.
|
|
811
927
|
|
|
812
|
-
> **
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
1. Open [@BotFather](https://t.me/BotFather)
|
|
819
|
-
2. Send `/setprivacy`
|
|
820
|
-
3. Select your bot
|
|
821
|
-
4. Choose **Disable**
|
|
822
|
-
|
|
823
|
-
This is a bot-level setting — it applies to all groups the bot joins. If @mention-only is fine, skip this step and keep `"requireMention": true`.
|
|
928
|
+
> **Legacy schema note:** older docs showed a per-group `"groups": { "<id>": {…} }`
|
|
929
|
+
> map. That form is still accepted and auto-migrated on read to the flat
|
|
930
|
+
> `groupAllowlist` + top-level `requireMention` shown above, but new setups should
|
|
931
|
+
> use the flat schema. A per-group member restriction from the old schema is
|
|
932
|
+
> preserved under `legacyGroupAllowFrom`; there is no command to edit it.
|
|
824
933
|
|
|
825
934
|
---
|
|
826
935
|
|
|
827
936
|
## Telegram Commands
|
|
828
937
|
|
|
829
|
-
|
|
938
|
+
Bot commands are **DM-only** — sent in a group they are silently ignored (this
|
|
939
|
+
keeps pairing codes and session state from leaking to other members). Once
|
|
940
|
+
paired, the following commands are available in a private chat:
|
|
830
941
|
|
|
831
942
|
**Session management**
|
|
832
943
|
|
|
@@ -905,6 +1016,17 @@ npm run typecheck
|
|
|
905
1016
|
- Ensure no other process is polling the same bot token (causes 409 Conflict)
|
|
906
1017
|
- Only `TelegramReceiver` polls Telegram — MCP session subprocesses run in `SEND_ONLY` mode (no polling)
|
|
907
1018
|
|
|
1019
|
+
**Bot silent in a Telegram group**
|
|
1020
|
+
- The group must be in `groupAllowlist` — see [Telegram Groups](#telegram-groups). An empty `pending` after messaging usually means the message never reached the bot.
|
|
1021
|
+
- Most common cause: **Privacy Mode** (default ON). A non-admin bot only receives commands, @mentions, and replies in groups — a plain message needed to mint the pairing code is filtered by Telegram. Promote the bot to Admin, or disable Privacy Mode in BotFather and re-add it.
|
|
1022
|
+
- `/start` and other commands are dropped in groups by design — use a normal message (or an @mention) to trigger the pairing code.
|
|
1023
|
+
- If `requireMention: true`, the bot only answers when @mentioned or replied to.
|
|
1024
|
+
|
|
1025
|
+
**Bot silent in a Discord server (guild)**
|
|
1026
|
+
- Enable the **MESSAGE CONTENT INTENT** in the Discord Developer Portal (Bot settings) — without it the bot receives events but empty message text, so it can't respond or pair.
|
|
1027
|
+
- The guild must be in `guildAllowlist` (`groupPolicy: allowlist`), and the bot needs **View Channel** + **Read Message History** in that channel.
|
|
1028
|
+
- If `requireMention: true`, the bot only answers when @mentioned or replied to.
|
|
1029
|
+
|
|
908
1030
|
**Session loses memory after restart**
|
|
909
1031
|
- History is persisted in `~/.claude-gateway/agents/<id>/sessions/<chat_id>.jsonl`
|
|
910
1032
|
- If the file is missing, the session starts fresh (no error)
|
package/config.template.json
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
"claude.dangerouslySkipPermissions"
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
|
-
"configVersion": "1.0.
|
|
13
|
+
"configVersion": "1.0.14",
|
|
14
14
|
"gateway": {
|
|
15
15
|
"logDir": "~/.claude-gateway/logs",
|
|
16
16
|
"timezone": "Asia/Bangkok",
|
|
17
|
+
"bind": "127.0.0.1",
|
|
17
18
|
"models": [
|
|
18
19
|
{ "id": "claude-fable-5[1m]", "label": "Fable 5 (1M)", "alias": "fable[1m]", "contextWindow": 1000000 },
|
|
19
20
|
{ "id": "claude-opus-4-8[1m]", "label": "Opus 4.8 (1M)", "alias": "opus[1m]", "contextWindow": 1000000 },
|
|
@@ -44,7 +45,10 @@
|
|
|
44
45
|
"cleanupHour": 0,
|
|
45
46
|
"cleanupTimezone": "UTC"
|
|
46
47
|
},
|
|
47
|
-
"headless": true
|
|
48
|
+
"headless": true,
|
|
49
|
+
"selfHealing": {
|
|
50
|
+
"autoRecover": false
|
|
51
|
+
}
|
|
48
52
|
},
|
|
49
53
|
"agents": [
|
|
50
54
|
{
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incident store (Epic #195, Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Persists turn-trace stall incidents as on-disk bundles under
|
|
5
|
+
* `<dir>/<id>/`, deduplicates repeats by fingerprint within a rolling window,
|
|
6
|
+
* escalates according to the pure rules in incident.ts, and prunes old bundles.
|
|
7
|
+
*
|
|
8
|
+
* All IO is funnelled through an injected `fs` and `now()` so the store is
|
|
9
|
+
* fully unit-testable against an in-memory filesystem (no real disk, no real
|
|
10
|
+
* clock). The pure decision logic (fingerprint, escalation, scrub, digest)
|
|
11
|
+
* lives in incident.ts; this file only orchestrates it and touches disk.
|
|
12
|
+
*
|
|
13
|
+
* A bundle is captured BEFORE any recovery mutates state (Phase 3 recovery runs
|
|
14
|
+
* after record() returns), so evidence reflects the wedged turn, not its
|
|
15
|
+
* aftermath. Every text artifact is scrubbed on the way in.
|
|
16
|
+
*/
|
|
17
|
+
import type { TurnIncident, TurnIncidentEvidence } from './turn-trace';
|
|
18
|
+
import { type EscalationConfig, type EscalationDecision, type EscalationLevel, type IncidentManifest, type DigestSummary, type RecoveryOutcome } from './incident';
|
|
19
|
+
/** Minimal filesystem surface the store needs. Node's `fs` satisfies it. */
|
|
20
|
+
export interface IncidentFsApi {
|
|
21
|
+
mkdirSync(path: string, opts: {
|
|
22
|
+
recursive: boolean;
|
|
23
|
+
}): void;
|
|
24
|
+
writeFileSync(path: string, data: string): void;
|
|
25
|
+
readFileSync(path: string, enc: 'utf8'): string;
|
|
26
|
+
existsSync(path: string): boolean;
|
|
27
|
+
readdirSync(path: string): string[];
|
|
28
|
+
rmSync(path: string, opts: {
|
|
29
|
+
recursive: boolean;
|
|
30
|
+
force: boolean;
|
|
31
|
+
}): void;
|
|
32
|
+
}
|
|
33
|
+
export interface IncidentStoreDeps {
|
|
34
|
+
/** Base directory for incident bundles (e.g. ~/.claude-gateway/incidents). */
|
|
35
|
+
dir: string;
|
|
36
|
+
fs: IncidentFsApi;
|
|
37
|
+
/** Clock injection — epoch ms. */
|
|
38
|
+
now: () => number;
|
|
39
|
+
/** Transport name recorded on each incident ('telegram' | 'discord' | ...). */
|
|
40
|
+
channel: string;
|
|
41
|
+
/** Gateway version, recorded for context (not part of the fingerprint). */
|
|
42
|
+
gatewayVersion: string;
|
|
43
|
+
/** Lazily resolves the Claude CLI version (part of the fingerprint). */
|
|
44
|
+
getCliVersion: () => string;
|
|
45
|
+
/** Retention: bundles whose lastAt is older than this are pruned (ms). */
|
|
46
|
+
retentionMs?: number;
|
|
47
|
+
/** Retention: hard cap on bundle count (oldest pruned first). */
|
|
48
|
+
maxIncidents?: number;
|
|
49
|
+
/** Escalation thresholds. */
|
|
50
|
+
escalation?: EscalationConfig;
|
|
51
|
+
/** Max occurrence samples retained per incident. */
|
|
52
|
+
maxSamples?: number;
|
|
53
|
+
}
|
|
54
|
+
/** Outcome of recording one stall occurrence. */
|
|
55
|
+
export interface RecordResult {
|
|
56
|
+
id: string;
|
|
57
|
+
fingerprint: string;
|
|
58
|
+
occurrences: number;
|
|
59
|
+
escalation: EscalationDecision;
|
|
60
|
+
/** True when this created a fresh bundle (vs. folding into an existing one). */
|
|
61
|
+
isNew: boolean;
|
|
62
|
+
manifest: IncidentManifest;
|
|
63
|
+
}
|
|
64
|
+
export interface IncidentStore {
|
|
65
|
+
record(incident: TurnIncident, evidence?: TurnIncidentEvidence): RecordResult;
|
|
66
|
+
/** Load a manifest by id, or null if absent/corrupt. */
|
|
67
|
+
get(id: string): IncidentManifest | null;
|
|
68
|
+
/** Record that the user was notified at a level (dedupes re-notifying). */
|
|
69
|
+
markNotified(id: string, level: EscalationLevel, at?: number): void;
|
|
70
|
+
/**
|
|
71
|
+
* Append a recovery outcome to an incident bundle (Phase 3b). The runner
|
|
72
|
+
* executes recovery and returns the outcome; the receiver persists it here so
|
|
73
|
+
* the bundle records what was attempted and whether it worked. Capped like
|
|
74
|
+
* samples so a flapping stall cannot grow the manifest unbounded.
|
|
75
|
+
*/
|
|
76
|
+
appendRecovery(id: string, outcome: RecoveryOutcome): void;
|
|
77
|
+
/** Link a filed GitHub issue to an incident fingerprint. */
|
|
78
|
+
linkIssue(id: string, issueNumber: number): void;
|
|
79
|
+
/** Mark an incident resolved (stops it folding new occurrences). */
|
|
80
|
+
resolve(id: string): void;
|
|
81
|
+
/** Prune bundles past retention / over the count cap. Returns pruned ids. */
|
|
82
|
+
prune(): string[];
|
|
83
|
+
/** Digest summary over the last `sinceMs`. */
|
|
84
|
+
digest(sinceMs: number): DigestSummary;
|
|
85
|
+
/** All manifests currently on disk (unsorted). */
|
|
86
|
+
list(): IncidentManifest[];
|
|
87
|
+
}
|
|
88
|
+
export declare function createIncidentStore(deps: IncidentStoreDeps): IncidentStore;
|
|
89
|
+
//# sourceMappingURL=incident-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"incident-store.d.ts","sourceRoot":"","sources":["../../src/agent/incident-store.ts"],"names":[],"mappings":"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"}
|