@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.
Files changed (92) hide show
  1. package/README.md +172 -50
  2. package/config.template.json +6 -2
  3. package/dist/agent/incident-store.d.ts +89 -0
  4. package/dist/agent/incident-store.d.ts.map +1 -0
  5. package/dist/agent/incident-store.js +299 -0
  6. package/dist/agent/incident-store.js.map +1 -0
  7. package/dist/agent/incident.d.ts +156 -0
  8. package/dist/agent/incident.d.ts.map +1 -0
  9. package/dist/agent/incident.js +177 -0
  10. package/dist/agent/incident.js.map +1 -0
  11. package/dist/agent/recovery-executor.d.ts +117 -0
  12. package/dist/agent/recovery-executor.d.ts.map +1 -0
  13. package/dist/agent/recovery-executor.js +168 -0
  14. package/dist/agent/recovery-executor.js.map +1 -0
  15. package/dist/agent/recovery-policy.d.ts +97 -0
  16. package/dist/agent/recovery-policy.d.ts.map +1 -0
  17. package/dist/agent/recovery-policy.js +164 -0
  18. package/dist/agent/recovery-policy.js.map +1 -0
  19. package/dist/agent/runner.d.ts +44 -0
  20. package/dist/agent/runner.d.ts.map +1 -1
  21. package/dist/agent/runner.js +272 -2
  22. package/dist/agent/runner.js.map +1 -1
  23. package/dist/agent/safe-mode.d.ts +61 -0
  24. package/dist/agent/safe-mode.d.ts.map +1 -0
  25. package/dist/agent/safe-mode.js +102 -0
  26. package/dist/agent/safe-mode.js.map +1 -0
  27. package/dist/agent/triage.d.ts +94 -0
  28. package/dist/agent/triage.d.ts.map +1 -0
  29. package/dist/agent/triage.js +209 -0
  30. package/dist/agent/triage.js.map +1 -0
  31. package/dist/agent/turn-trace.d.ts +120 -0
  32. package/dist/agent/turn-trace.d.ts.map +1 -0
  33. package/dist/agent/turn-trace.js +122 -0
  34. package/dist/agent/turn-trace.js.map +1 -0
  35. package/dist/api/gateway-router.d.ts +21 -0
  36. package/dist/api/gateway-router.d.ts.map +1 -1
  37. package/dist/api/gateway-router.js +58 -17
  38. package/dist/api/gateway-router.js.map +1 -1
  39. package/dist/api/line-pending-senders.d.ts +7 -2
  40. package/dist/api/line-pending-senders.d.ts.map +1 -1
  41. package/dist/api/line-pending-senders.js +7 -2
  42. package/dist/api/line-pending-senders.js.map +1 -1
  43. package/dist/api/router.d.ts.map +1 -1
  44. package/dist/api/router.js +563 -172
  45. package/dist/api/router.js.map +1 -1
  46. package/dist/api/wizard-state.d.ts +1 -4
  47. package/dist/api/wizard-state.d.ts.map +1 -1
  48. package/dist/api/wizard-state.js.map +1 -1
  49. package/dist/config/migrator.d.ts +4 -0
  50. package/dist/config/migrator.d.ts.map +1 -1
  51. package/dist/config/migrator.js +60 -3
  52. package/dist/config/migrator.js.map +1 -1
  53. package/dist/discord/receiver.d.ts +1 -0
  54. package/dist/discord/receiver.d.ts.map +1 -1
  55. package/dist/discord/receiver.js +24 -6
  56. package/dist/discord/receiver.js.map +1 -1
  57. package/dist/index.js +3 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/session/process.d.ts +18 -0
  60. package/dist/session/process.d.ts.map +1 -1
  61. package/dist/session/process.js +61 -1
  62. package/dist/session/process.js.map +1 -1
  63. package/dist/shell/claude-pty-shell.js +59 -0
  64. package/dist/shell/claude-pty-shell.js.map +1 -1
  65. package/dist/shell/control-channel.d.ts +74 -0
  66. package/dist/shell/control-channel.d.ts.map +1 -0
  67. package/dist/shell/control-channel.js +114 -0
  68. package/dist/shell/control-channel.js.map +1 -0
  69. package/dist/telegram/receiver.d.ts +1 -0
  70. package/dist/telegram/receiver.d.ts.map +1 -1
  71. package/dist/telegram/receiver.js +18 -6
  72. package/dist/telegram/receiver.js.map +1 -1
  73. package/dist/types.d.ts +21 -0
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/ui/web-ui.d.ts.map +1 -1
  76. package/dist/ui/web-ui.js +103 -2
  77. package/dist/ui/web-ui.js.map +1 -1
  78. package/mcp/tools/discord/access.ts +136 -20
  79. package/mcp/tools/discord/client.ts +3 -1
  80. package/mcp/tools/discord/module.ts +75 -4
  81. package/mcp/tools/discord/skills/access/SKILL.md +66 -8
  82. package/mcp/tools/discord/skills/configure/SKILL.md +11 -1
  83. package/mcp/tools/discord/types.ts +21 -2
  84. package/mcp/tools/skills/handlers.ts +4 -2
  85. package/mcp/tools/telegram/dedup.ts +4 -1
  86. package/mcp/tools/telegram/module.ts +5 -14
  87. package/mcp/tools/telegram/pure.ts +169 -31
  88. package/mcp/tools/telegram/receiver-server.ts +271 -78
  89. package/mcp/tools/telegram/skills/access/SKILL.md +93 -23
  90. package/mcp/tools/telegram/skills/configure/SKILL.md +31 -26
  91. package/mcp/tools/telegram/typing.ts +124 -0
  92. 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` | `0.0.0.0` | Gateway HTTP listen address. Must be `0.0.0.0` (default) when a **containerized** reverse proxy (Caddy, nginx in Docker) needs to reach the gateway. Set to `127.0.0.1` only if using a **host-network** proxy (Traefik on host) loopback is not reachable across container boundaries. |
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
- 1. Set `dmPolicy` to `pairing` in `access.json`:
737
- ```json
738
- { "dmPolicy": "pairing" }
739
- ```
740
- 2. Ask the user to DM the bot — they receive a 6-character pairing code
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
- 4. The bot confirms pairing within 5 seconds
746
- 5. Lock down after everyone is paired:
747
- ```bash
748
- npm run pair -- --agent=alfred --policy=allowlist
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. Groups must be registered before the bot will respond.
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
- **Step 1 Add the bot to the group as Admin**
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
- Add your bot to the group and **promote it to Admin**. Without admin rights, Telegram does not deliver group messages to the bot — it will appear online but never respond.
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
- Minimum required admin permission: **"Read Messages"** (or any admin roleeven the most restricted works).
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
- **Step 2 Get the group ID**
868
+ Do one of these so the bot actually receives group messages:
769
869
 
770
- Forward any message from the group to [@userinfobot](https://t.me/userinfobot). It will reply with the chat ID a negative number like `-1001234567890`.
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
- https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
887
+ /telegram:access pair <code>
775
888
  ```
776
- Look for `"chat":{"id": ...}` in the result.
777
889
 
778
- **Step 3 Register the group**
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
- Edit the agent's `access.json` directly:
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
- "groups": {
793
- "-1001234567890": {
794
- "requireMention": true,
795
- "allowFrom": []
796
- }
797
- }
906
+ "groupPolicy": "allowlist",
907
+ "groupAllowlist": ["-1001234567890"],
908
+ "requireMention": true
798
909
  }
799
910
  ```
800
911
 
801
- Set `"requireMention": false` if you want the bot to respond to all messages without needing an @mention.
802
- To restrict to specific members only, add their Telegram user IDs to `"allowFrom"`.
912
+ `access.json` is re-read on every inbound message changes take effect
913
+ immediately, no restart.
803
914
 
804
- **Step 4 — Start chatting**
915
+ ### Mention gate
805
916
 
806
- @mention the bot in the group (or reply to one of its messages). Changes to `access.json` take effect immediately — no restart needed.
917
+ `requireMention` is a single top-level boolean (default `true`):
807
918
 
808
- **Managing groups**
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
- Edit `access.json` to add or remove entries from the `"groups"` object. The gateway re-reads the file on every inbound message.
926
+ Toggle it with `/telegram:access group mention <on|off>`.
811
927
 
812
- > **Note:** `/telegram:access` skill is available when running inside a gateway agent session (TELEGRAM_STATE_DIR is set automatically). For standalone terminal use, edit `access.json` directly as shown above.
813
-
814
- **Optional Let the bot read all messages (disable Privacy Mode)**
815
-
816
- By default, Telegram bots in groups only receive messages that start with `/` or directly @mention the bot. If you want the bot to respond to every message without an @mention (and have set `"requireMention": false` in `access.json`), you also need to disable Privacy Mode at the bot level:
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
- Once paired, the following bot commands are available in a private chat:
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)
@@ -10,10 +10,11 @@
10
10
  "claude.dangerouslySkipPermissions"
11
11
  ]
12
12
  },
13
- "configVersion": "1.0.11",
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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"}