@hybridaione/hybridclaw 0.2.8 → 0.2.12

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 (207) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/README.md +16 -355
  3. package/config.example.json +1 -1
  4. package/container/.dockerignore +16 -0
  5. package/container/package-lock.json +2 -2
  6. package/container/package.json +11 -2
  7. package/container/src/index.ts +9 -21
  8. package/container/src/model-retry.ts +19 -0
  9. package/container/src/tools.ts +51 -16
  10. package/dist/channels/discord/runtime.d.ts.map +1 -1
  11. package/dist/channels/discord/runtime.js +83 -18
  12. package/dist/channels/discord/runtime.js.map +1 -1
  13. package/dist/channels/discord/send-permissions.d.ts.map +1 -1
  14. package/dist/channels/discord/send-permissions.js.map +1 -1
  15. package/dist/channels/discord/tool-actions.d.ts +7 -1
  16. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  17. package/dist/channels/discord/tool-actions.js +561 -39
  18. package/dist/channels/discord/tool-actions.js.map +1 -1
  19. package/dist/channels/prompt-adapters.d.ts.map +1 -1
  20. package/dist/channels/prompt-adapters.js +3 -1
  21. package/dist/channels/prompt-adapters.js.map +1 -1
  22. package/dist/cli.js +4 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +2 -2
  26. package/dist/config.js.map +1 -1
  27. package/dist/container-runner.d.ts.map +1 -1
  28. package/dist/container-runner.js +31 -7
  29. package/dist/container-runner.js.map +1 -1
  30. package/dist/container-setup.d.ts.map +1 -1
  31. package/dist/container-setup.js +109 -12
  32. package/dist/container-setup.js.map +1 -1
  33. package/dist/db.d.ts +1 -0
  34. package/dist/db.d.ts.map +1 -1
  35. package/dist/db.js +20 -1
  36. package/dist/db.js.map +1 -1
  37. package/dist/gateway-client.d.ts +3 -2
  38. package/dist/gateway-client.d.ts.map +1 -1
  39. package/dist/gateway-client.js +10 -0
  40. package/dist/gateway-client.js.map +1 -1
  41. package/dist/gateway-service.d.ts.map +1 -1
  42. package/dist/gateway-service.js +34 -4
  43. package/dist/gateway-service.js.map +1 -1
  44. package/dist/gateway-types.d.ts +11 -0
  45. package/dist/gateway-types.d.ts.map +1 -1
  46. package/dist/gateway-types.js.map +1 -1
  47. package/dist/gateway.js +59 -18
  48. package/dist/gateway.js.map +1 -1
  49. package/dist/health.d.ts.map +1 -1
  50. package/dist/health.js +135 -11
  51. package/dist/health.js.map +1 -1
  52. package/dist/logger.js +1 -1
  53. package/dist/logger.js.map +1 -1
  54. package/dist/onboarding.d.ts.map +1 -1
  55. package/dist/onboarding.js +2 -2
  56. package/dist/onboarding.js.map +1 -1
  57. package/dist/prompt-hooks.d.ts.map +1 -1
  58. package/dist/prompt-hooks.js +17 -13
  59. package/dist/prompt-hooks.js.map +1 -1
  60. package/dist/runtime-config.d.ts.map +1 -1
  61. package/dist/runtime-config.js +205 -6
  62. package/dist/runtime-config.js.map +1 -1
  63. package/dist/scheduled-task-runner.d.ts.map +1 -1
  64. package/dist/scheduled-task-runner.js +1 -1
  65. package/dist/scheduled-task-runner.js.map +1 -1
  66. package/dist/scheduler.d.ts +1 -0
  67. package/dist/scheduler.d.ts.map +1 -1
  68. package/dist/scheduler.js +19 -11
  69. package/dist/scheduler.js.map +1 -1
  70. package/dist/silent-reply-stream.d.ts +6 -0
  71. package/dist/silent-reply-stream.d.ts.map +1 -0
  72. package/dist/silent-reply-stream.js +51 -0
  73. package/dist/silent-reply-stream.js.map +1 -0
  74. package/dist/silent-reply.d.ts +5 -0
  75. package/dist/silent-reply.d.ts.map +1 -0
  76. package/dist/silent-reply.js +31 -0
  77. package/dist/silent-reply.js.map +1 -0
  78. package/dist/tui.js +158 -5
  79. package/dist/tui.js.map +1 -1
  80. package/dist/types.d.ts +1 -0
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/types.js.map +1 -1
  83. package/package.json +28 -4
  84. package/.github/workflows/ci.yml +0 -70
  85. package/.github/workflows/pages.yml +0 -41
  86. package/.husky/pre-commit +0 -1
  87. package/CONTRIBUTING.md +0 -33
  88. package/biome.json +0 -35
  89. package/dist/colors.d.ts +0 -18
  90. package/dist/colors.d.ts.map +0 -1
  91. package/dist/colors.js +0 -27
  92. package/dist/colors.js.map +0 -1
  93. package/dist/discord-stream.d.ts +0 -32
  94. package/dist/discord-stream.d.ts.map +0 -1
  95. package/dist/discord-stream.js +0 -196
  96. package/dist/discord-stream.js.map +0 -1
  97. package/dist/discord.basic.test.d.ts +0 -2
  98. package/dist/discord.basic.test.d.ts.map +0 -1
  99. package/dist/discord.basic.test.js +0 -38
  100. package/dist/discord.basic.test.js.map +0 -1
  101. package/dist/discord.d.ts +0 -6
  102. package/dist/discord.d.ts.map +0 -1
  103. package/dist/discord.js +0 -4
  104. package/dist/discord.js.map +0 -1
  105. package/dist/gateway-service.media-routing.test.d.ts +0 -2
  106. package/dist/gateway-service.media-routing.test.d.ts.map +0 -1
  107. package/dist/gateway-service.media-routing.test.js +0 -29
  108. package/dist/gateway-service.media-routing.test.js.map +0 -1
  109. package/dist/git-commit.d.ts +0 -2
  110. package/dist/git-commit.d.ts.map +0 -1
  111. package/dist/git-commit.js +0 -63
  112. package/dist/git-commit.js.map +0 -1
  113. package/dist/hatch.d.ts +0 -7
  114. package/dist/hatch.d.ts.map +0 -1
  115. package/dist/hatch.js +0 -99
  116. package/dist/hatch.js.map +0 -1
  117. package/dist/index.d.ts +0 -2
  118. package/dist/index.d.ts.map +0 -1
  119. package/dist/index.js +0 -60
  120. package/dist/index.js.map +0 -1
  121. package/dist/token-efficiency.basic.test.d.ts +0 -2
  122. package/dist/token-efficiency.basic.test.d.ts.map +0 -1
  123. package/dist/token-efficiency.basic.test.js +0 -29
  124. package/dist/token-efficiency.basic.test.js.map +0 -1
  125. package/src/agent.ts +0 -86
  126. package/src/audit-cli.ts +0 -327
  127. package/src/audit-events.ts +0 -201
  128. package/src/audit-trail.ts +0 -428
  129. package/src/channels/discord/attachments.ts +0 -336
  130. package/src/channels/discord/debounce.ts +0 -25
  131. package/src/channels/discord/delivery.ts +0 -150
  132. package/src/channels/discord/human-delay.ts +0 -48
  133. package/src/channels/discord/inbound.ts +0 -260
  134. package/src/channels/discord/mentions.ts +0 -154
  135. package/src/channels/discord/presence.ts +0 -148
  136. package/src/channels/discord/prompt-adapter.ts +0 -34
  137. package/src/channels/discord/rate-limiter.ts +0 -58
  138. package/src/channels/discord/reactions.ts +0 -211
  139. package/src/channels/discord/runtime.ts +0 -2259
  140. package/src/channels/discord/send-permissions.ts +0 -196
  141. package/src/channels/discord/stream.ts +0 -294
  142. package/src/channels/discord/tool-actions.ts +0 -1390
  143. package/src/channels/discord/typing.ts +0 -140
  144. package/src/channels/prompt-adapters.ts +0 -55
  145. package/src/chunk.ts +0 -161
  146. package/src/cli.ts +0 -911
  147. package/src/config.ts +0 -407
  148. package/src/container-runner.ts +0 -549
  149. package/src/container-setup.ts +0 -317
  150. package/src/conversation.ts +0 -82
  151. package/src/db.ts +0 -2718
  152. package/src/delegation-manager.ts +0 -49
  153. package/src/env.ts +0 -36
  154. package/src/gateway-client.ts +0 -241
  155. package/src/gateway-service.ts +0 -2708
  156. package/src/gateway-types.ts +0 -137
  157. package/src/gateway.ts +0 -850
  158. package/src/health.ts +0 -403
  159. package/src/heartbeat.ts +0 -419
  160. package/src/hybridai-bots.ts +0 -57
  161. package/src/hybridai-models.ts +0 -158
  162. package/src/instruction-approval-audit.ts +0 -90
  163. package/src/instruction-integrity.ts +0 -197
  164. package/src/ipc.ts +0 -181
  165. package/src/logger.ts +0 -29
  166. package/src/memory-consolidation.ts +0 -41
  167. package/src/memory-service.ts +0 -606
  168. package/src/mount-security.ts +0 -261
  169. package/src/observability-ingest.ts +0 -888
  170. package/src/onboarding.ts +0 -719
  171. package/src/proactive-policy.ts +0 -46
  172. package/src/prompt-hooks.ts +0 -372
  173. package/src/runtime-config.ts +0 -1883
  174. package/src/scheduled-task-runner.ts +0 -256
  175. package/src/scheduler.ts +0 -819
  176. package/src/session-export.ts +0 -196
  177. package/src/session-maintenance.ts +0 -367
  178. package/src/session-transcripts.ts +0 -54
  179. package/src/side-effects.ts +0 -97
  180. package/src/skills-guard.ts +0 -1584
  181. package/src/skills.ts +0 -1036
  182. package/src/token-efficiency.ts +0 -266
  183. package/src/tui.ts +0 -604
  184. package/src/types.ts +0 -429
  185. package/src/update.ts +0 -445
  186. package/src/workspace.ts +0 -223
  187. package/tests/approval-policy.test.ts +0 -335
  188. package/tests/channel-message-tool-hints.test.ts +0 -64
  189. package/tests/container.message-tool-normalization.test.ts +0 -204
  190. package/tests/discord.basic.test.ts +0 -304
  191. package/tests/discord.human-presence.test.ts +0 -85
  192. package/tests/discord.send-permissions.test.ts +0 -171
  193. package/tests/discord.tool-action-aliases.test.ts +0 -28
  194. package/tests/discord.tool-actions-channel-lookup.test.ts +0 -166
  195. package/tests/discord.tool-actions-member-lookup.test.ts +0 -186
  196. package/tests/discord.tool-actions-send.test.ts +0 -439
  197. package/tests/gateway-service.media-routing.test.ts +0 -39
  198. package/tests/hybridai-client.test.ts +0 -112
  199. package/tests/hybridai-models.test.ts +0 -46
  200. package/tests/memory-service.test.ts +0 -1114
  201. package/tests/token-efficiency.basic.test.ts +0 -38
  202. package/tests/token-usage.cache.test.ts +0 -128
  203. package/tsconfig.json +0 -18
  204. package/vitest.e2e.config.ts +0 -17
  205. package/vitest.integration.config.ts +0 -17
  206. package/vitest.live.config.ts +0 -18
  207. package/vitest.unit.config.ts +0 -24
package/CHANGELOG.md CHANGED
@@ -2,7 +2,84 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- No unreleased changes.
5
+ ### Added
6
+
7
+ ### Changed
8
+
9
+ ### Fixed
10
+
11
+ ## [0.2.12](https://github.com/HybridAIOne/hybridclaw/tree/v0.2.12)
12
+
13
+ ### Added
14
+
15
+ - **Automatic container publishing**: Added release-tag GitHub Actions publishing to Docker Hub (`hybridaione/hybridclaw-agent`) plus GHCR mirror (`ghcr.io/<org>/hybridclaw-agent`) with versioned tags (`vX.Y.Z`) and stable `latest` updates.
16
+ - **Container build context guardrails**: Added `container/.dockerignore` and included it in npm package files so local secrets/artifacts are excluded from image build context.
17
+
18
+ ### Changed
19
+
20
+ - **Runtime data default location**: Runtime config and data now default to `~/.hybridclaw` (`config.json`, `data/hybridclaw.db`, audit/session artifacts) to match home-directory workspace best practices.
21
+ - **Container bootstrap pull order**: Container readiness now pulls prebuilt images from Docker Hub first (`v<app-version>`, then `latest`) with GHCR fallback before local build.
22
+ - **README scope cleanup**: Reduced README to user-facing install/runtime guidance and moved maintainer/developer internals to `CONTRIBUTING.md`.
23
+ - **Container build script behavior**: `npm run build:container` now runs `docker build` directly without requiring host TypeScript tooling.
24
+
25
+ ### Fixed
26
+
27
+ - **First-run migration completeness**: Startup now migrates legacy `./config.json` and `./data` into `~/.hybridclaw`, archives legacy files, and stores migration backups under `~/.hybridclaw/migration-backups/` on conflicts.
28
+ - **Install-root write issues**: Container image fingerprint state now persists under `~/.hybridclaw/container-image-state` (with legacy state fallback) instead of package install directories.
29
+ - **Duplicate Discord `/status` slash entries**: Slash command registration now keeps `status`/`approve` global-only and removes stale guild-scoped duplicates to avoid duplicate command entries in guild channels.
30
+
31
+ ## [0.2.11](https://github.com/HybridAIOne/hybridclaw/tree/v0.2.11)
32
+
33
+ ### Added
34
+
35
+ - **Model default controls across TUI/Discord**: Added `model default [name]` command support in gateway/TUI plus a Discord `/model` slash command (`info`, `default`) with configured model choices.
36
+ - **Local proactive reminder delivery path**: Added queued proactive pull API (`GET /api/proactive/pull`) and TUI polling so scheduler/heartbeat reminders reliably surface in `tui` channels.
37
+ - **Scheduler timestamp regression test**: Added coverage for legacy SQLite second-precision timestamps and interval due-time regression handling.
38
+
39
+ ### Changed
40
+
41
+ - **Cron tool reminder contract**: Cron `add` now accepts prompt aliases (`prompt`/`message`/`text`), supports relative one-shot scheduling via `at_seconds`, and documents prompt-as-instruction semantics for future model runs.
42
+ - **Scheduler prompt framing**: Scheduled model turns now explicitly instruct execution of the provided instruction without follow-up questions.
43
+
44
+ ### Fixed
45
+
46
+ - **SQLite timestamp interpretation drift**: Scheduler now normalizes legacy `YYYY-MM-DD HH:MM:SS` task timestamps as UTC, preventing immediate re-fire bugs on interval tasks after timezone conversion.
47
+ - **Silent reply normalization edge case**: API/stream silent-token replacement now emits `Message sent.` only for real `message` send actions and otherwise falls back to the latest successful tool result.
48
+
49
+ ## [0.2.10](https://github.com/HybridAIOne/hybridclaw/tree/v0.2.10)
50
+
51
+ ### Added
52
+
53
+ - **Model retry policy helpers + tests**: Added shared model stream-fallback/retry predicates with dedicated unit coverage for retryable/non-retryable HybridAI error classes.
54
+ - **Message tool schema regression test**: Added explicit schema test coverage to enforce valid `components` parameter structure for the `message` tool definition.
55
+
56
+ ### Changed
57
+
58
+ - **Stream failure fallback behavior**: Container model-call flow now applies stream-to-non-stream fallback policy through centralized retry helpers for consistent error classification.
59
+
60
+ ### Fixed
61
+
62
+ - **HybridAI function schema rejection**: Fixed `message` tool `components` schema by defining `items` for the array variant, resolving `invalid_function_parameters` 400 failures.
63
+ - **HybridAI 500 handling robustness**: Streamed 5xx API failures now trigger the non-stream fallback path before hard-failing the turn.
64
+
65
+ ## [0.2.9](https://github.com/HybridAIOne/hybridclaw/tree/v0.2.9)
66
+
67
+ ### Added
68
+
69
+ - **Release bundle guard scripts**: Added root and container `release:check` scripts that validate `npm pack --dry-run` contents and fail on forbidden files (tests, source, CI/config artifacts).
70
+ - **Dry-run publish helpers**: Added `publish:dry` scripts for root and container package smoke checks before publish.
71
+
72
+ ### Changed
73
+
74
+ - **NPM package allowlists**: Added explicit `files` allowlists for root and container packages so publish output is limited to runtime assets and docs/templates/skills that HybridClaw loads at runtime.
75
+ - **Prepack gating**: Root and container packages now run clean build + release bundle validation during `prepack`.
76
+ - **CI packaging checks**: CI now runs root/container release bundle checks to catch publish-regression changes on PRs and pushes.
77
+ - **Silent reply token handling**: Centralized `__MESSAGE_SEND_HANDLED__` parsing/cleanup, added streaming prefix buffering for Discord/API output paths, and aligned prompt token constants with shared silent-reply utilities.
78
+ - **CLI build output mode**: Root `build` script now enforces executable mode on `dist/cli.js` after TypeScript compilation.
79
+
80
+ ### Fixed
81
+
82
+ - **Silent token leakage in streams/history**: Streaming token fragments are now suppressed until divergence/confirmation, trailing silent tokens are stripped from mixed replies, and silent assistant placeholders are filtered from conversation history before model calls.
6
83
 
7
84
  ## [0.2.8](https://github.com/HybridAIOne/hybridclaw/tree/v0.2.8)
8
85
 
package/README.md CHANGED
@@ -11,16 +11,13 @@ npm install -g @hybridaione/hybridclaw
11
11
  hybridclaw onboarding
12
12
  ```
13
13
 
14
- Latest release: [v0.2.8](https://github.com/HybridAIOne/hybridclaw/releases/tag/v0.2.8)
14
+ Latest release: [v0.2.12](https://github.com/HybridAIOne/hybridclaw/releases/tag/v0.2.12)
15
15
 
16
- ## Release highlights (v0.2.8)
16
+ ## Release highlights (v0.2.12)
17
17
 
18
- - Discord `message` tool now supports richer actions (`send`, `read`, `member-info`, `channel-info`, `react`, `quote-reply`, `edit`, `delete`, `pin`, `unpin`, `thread-create`, `thread-reply`).
19
- - Send intent handling is more natural: aliases like `dm`, `post`, `reply`, `respond`, `history`, `fetch`, `lookup`, and `whois` normalize automatically.
20
- - Single-call DM routing is now supported for user targets (`user`/`username`, `@mentions`, IDs), including inline resolution when no explicit `channelId` is provided.
21
- - Message-tool descriptions now include intent phrases and enumerate configured channels so isolated/cron runs can discover available Discord targets.
22
- - Discord send governance gained runtime allowlist controls via `discord.sendPolicy` plus channel/guild/user/role checks.
23
- - Lookup failures now return structured errors with disambiguation candidates and optional `resolveAmbiguous=best` auto-pick behavior.
18
+ - Runtime config/data now default to `~/.hybridclaw`, with automatic migration from legacy `./config.json` and `./data`.
19
+ - HybridClaw now auto-pulls prebuilt runtime images (Docker Hub first, GHCR fallback) before trying a local build.
20
+ - Discord slash command registration now removes duplicate guild `/status` entries and keeps global-only commands clean.
24
21
 
25
22
  ## HybridAI Advantage
26
23
 
@@ -54,7 +51,7 @@ hybridclaw onboarding
54
51
  # 3) open /register in browser (optional) and confirm in terminal
55
52
  # 4) open /login?next=/admin_api_keys in browser and get an API key
56
53
  # 5) paste API key (or URL containing it) back into the CLI
57
- # 6) choose the default bot (saved to config.json) and save secrets to `.env`
54
+ # 6) choose the default bot (saved to ~/.hybridclaw/config.json) and save secrets to `.env`
58
55
 
59
56
  # Start gateway backend (default)
60
57
  hybridclaw gateway
@@ -77,341 +74,20 @@ Runtime model:
77
74
  - If `DISCORD_TOKEN` is set, Discord runs inside gateway automatically.
78
75
  - `hybridclaw tui` is a thin client that connects to the gateway.
79
76
  - `hybridclaw gateway` and `hybridclaw tui` validate the container image at startup.
80
- - If the image is missing, it is built automatically.
81
- - Default rebuild policy is `if-stale`: when tracked container sources changed since last build, the image is rebuilt automatically.
82
- - Policy override (optional): env `HYBRIDCLAW_CONTAINER_REBUILD=if-stale|always|never`.
83
-
84
- HybridClaw best-in-class capabilities:
85
-
86
- - explicit trust-model acceptance during onboarding (recorded in `config.json`)
87
- - typed `config.json` runtime settings with defaults, validation, and hot reload
88
- - formal prompt hook orchestration (`bootstrap`, `memory`, `safety`, `proactivity`)
89
- - layered memory substrate: structured KV, semantic memory, typed knowledge graph entities/relations, canonical cross-channel sessions, and usage event persistence
90
- - lightweight DB evolution and concurrency hardening via `PRAGMA user_version` migrations, `journal_mode=WAL`, and `busy_timeout=5000`
91
- - Discord conversational UX: edit-in-place streaming responses, fence-safe chunking beyond Discord's 2000-char limit, phase-aware typing/reactions, adaptive debounce batching, per-user rate limits, health-driven self-presence, reply-chain-aware context, concise attachment-first screenshot replies, and humanized pacing (time-of-day slowdown, cooldown scaling, selective silence, read-without-reply, startup staggering)
92
- - token-efficient context assembly: per-message history truncation, hard history budgets with head/tail preservation, and head/tail truncation for oversized bootstrap files
93
- - runtime self-awareness in prompts: exact HybridClaw version/date, model, and runtime host metadata injected each turn for reliable "what version/model are you?" answers
94
- - proactive runtime layer with active-hours gating, push delegation (`single`/`parallel`/`chain`), depth-aware tool policy, and retry controls
95
- - trusted-coworker approval model for tool execution: Green (`just run`), Yellow (`narrate + 5s interrupt window`), Red (`explicit approval`) with `yes` (once), `yes for session`, `yes for agent`, and explicit deny (`no`, also `4`) plus pinned-red protections
96
- - structured audit trail: append-only hash-chained wire logs (`data/audit/<session>/wire.jsonl`) with tamper-evident immutability, normalized SQLite audit tables, and verification/search CLI commands
97
- - observability export: incremental `events:batch` forwarding with durable cursor tracking and bot-scoped ingest token lifecycle via `ingest-token:ensure`
98
- - model token telemetry in audit/observability events (`model.usage`) with API usage + deterministic fallback estimates
99
- - built-in usage aggregation (`usage summary|daily|monthly|model`) plus JSONL session exports (`export session [sessionId]`) for cost/debug visibility
100
- - gateway lifecycle controls: managed + unmanaged restart/stop flows with graceful shutdown fallback paths
101
- - instruction-integrity approval flow: core instruction docs (`AGENTS.md`, `SECURITY.md`, `TRUST_MODEL.md`) are hash-verified against a local approved baseline before TUI start
77
+ - On first run, HybridClaw automatically prepares that image (pulls a prebuilt image first, then falls back to local build if needed).
78
+ - If container setup fails, run `npm run build:container` in the project root and retry.
102
79
 
103
80
  ## Configuration
104
81
 
105
- HybridClaw uses typed runtime config in `config.json` (auto-created on first run).
106
-
107
- - Start from `config.example.json` (reference)
108
- - Runtime watches `config.json` and hot-reloads most settings (model defaults, heartbeat, prompt hooks, limits, etc.)
109
- - `discord.guildMembersIntent` enables richer guild member context and better `@name` mention resolution in replies (requires enabling **Server Members Intent** in Discord Developer Portal)
110
- - `discord.presenceIntent` enables Discord presence events (requires enabling **Presence Intent** in Discord Developer Portal)
111
- - `discord.respondToAllMessages` is a global fallback for open-policy guild channels without explicit mode config (`false` mention-gated, `true` free-response)
112
- - `discord.commandMode` controls command access: `public` (any user can run slash/`!claw` commands) or `restricted` (only allowlisted users can run slash/`!claw` commands)
113
- - `discord.commandAllowedUserIds` is the allowlist used when `discord.commandMode` is `restricted`
114
- - `discord.commandUserId` is a legacy single-user allowlist alias; when set without `commandMode`, runtime treats command access as `restricted` for backward compatibility
115
- - `discord.commandsOnly` optional hard mode: if `true`, the bot ignores non-`!claw` messages and only accepts prefixed commands (still subject to `discord.commandMode`)
116
- - `discord.groupPolicy` controls guild channel scope: `open` (default, mention-first unless a channel is set to `free`), `allowlist`, or `disabled`
117
- - `discord.freeResponseChannels` is a Hermes-style channel ID list that gets free-response behavior while other channels remain mention-gated
118
- - `discord.textChunkLimit` controls Discord message chunk size (default `2000`)
119
- - `discord.maxLinesPerMessage` controls max lines per Discord chunk (default `17`)
120
- - `discord.humanDelay` controls natural delays between multi-part messages (`off|natural|custom`)
121
- - `discord.typingMode` controls typing indicator lifecycle (`instant|thinking|streaming|never`)
122
- - `discord.presence.*` enables dynamic self-presence health states (healthy/degraded/exhausted mapped to `online|idle|dnd`, plus maintenance `invisible` during shutdown)
123
- - `discord.lifecycleReactions.*` enables phase emoji transitions (`queued|thinking|toolUse|streaming|done|error`)
124
- - approval policy layer is configured via `.hybridclaw/policy.yaml` (`approval.pinned_red`, `workspace_fence`, pending/timeout controls, audit toggles)
125
- - `discord.ackReaction`, `discord.ackReactionScope`, and `discord.removeAckAfterReply` control acknowledgment reaction behavior
126
- - `discord.debounceMs` controls default inbound debounce; channel overrides can tune noisy channels
127
- - `discord.rateLimitPerUser` and `discord.rateLimitExemptRoles` enforce per-user sliding-window limits
128
- - `discord.suppressPatterns` blocks auto-reply triggers for suppression terms (case-insensitive)
129
- - `discord.maxConcurrentPerChannel` limits concurrent in-flight runs per channel
130
- - `discord.guilds.<guildId>.defaultMode` sets that guild's fallback mode in `open` policy (`mention` recommended)
131
- - `discord.guilds.<guildId>.channels.<channelId>.*` supports per-channel mode and behavior overrides (`mode`, `typingMode`, `debounceMs`, `ackReaction*`, `humanDelay`, `rateLimitPerUser`, `suppressPatterns`, `maxConcurrentPerChannel`)
132
- - `scheduler.jobs[]` defines config-backed proactive jobs with `schedule.kind` (`cron|every|at`), `action.kind` (`agent_turn|system_event`), and delivery targets (`channel|last-channel|webhook`)
133
- - `scheduler.jobs[].name` / `scheduler.jobs[].description` add optional human-readable labels for status/log output; runtime status persists `nextRunAt`
134
- - Config scheduler job metadata (last status, consecutive errors, one-shot completion) persists atomically in `data/scheduler-jobs-state.json`
135
- - Config scheduler jobs auto-disable after repeated failures (5 consecutive errors) and one-shot jobs retry on a bounded interval until successful
136
- - `memory.decayRate` and `memory.consolidationIntervalHours` control semantic-memory consolidation intensity/cadence
137
- - `sessionCompaction.tokenBudget` and `sessionCompaction.budgetRatio` tune compaction token budgeting behavior
138
- - Built-in Discord humanization behaviors include night/weekend pacing, post-exchange cooldown scaling (after 5+ exchanges, reset after 20 minutes idle), selective silence in active free-mode channels, short-ack read reactions, and reconnect staggered dequeue
139
- - Per-guild/per-channel mode takes precedence over `discord.respondToAllMessages`
140
- - Discord slash commands: `/status`, `/approve [view|yes|session|agent|no] [approval_id]`, `/channel-mode <off|mention|free>`, and `/channel-policy <open|allowlist|disabled>` (ephemeral replies)
141
- - `skills.extraDirs` adds additional enterprise/shared skill roots (lowest precedence tier)
142
- - `proactive.*` controls autonomous behavior (`activeHours`, `delegation`, `autoRetry`, `ralph`)
143
- - `proactive.ralph.maxIterations` enables Ralph loop (`0` off, `-1` unlimited, `>0` extra autonomous iterations before forcing completion)
144
- - TUI/Gateway command: `ralph on|off|set <n>|info` (`0` off, `-1` unlimited, `1-64` extra iterations)
145
- - `observability.*` controls push ingest into HybridAI (`events:batch` endpoint, batching, identity metadata)
146
- - Some settings require restart to fully apply (for example HTTP bind host/port)
147
- - Default bot is configured via `hybridai.defaultChatbotId` in `config.json`
148
- - `hybridai.maxTokens` sets the default completion budget per model call (default `4096`)
149
-
150
- Secrets remain in `.env`:
151
-
152
- - `HYBRIDAI_API_KEY` (required)
153
- - `DISCORD_TOKEN` (optional)
154
- - `WEB_API_TOKEN` and `GATEWAY_API_TOKEN` (optional API auth hardening)
155
- - observability ingest token is auto-managed via `POST /api/v1/agent-observability/ingest-token:ensure` and cached locally
156
-
157
- Trust-model acceptance is stored in `config.json` under `security.*` and is required before runtime starts.
158
-
159
- See [TRUST_MODEL.md](./TRUST_MODEL.md) for onboarding acceptance policy and [SECURITY.md](./SECURITY.md) for technical security guidelines.
160
-
161
- ## Audit Trail
162
-
163
- HybridClaw records a forensic audit trail by default:
164
-
165
- - append-only per-session wire logs in `data/audit/<session>/wire.jsonl`
166
- - SHA-256 hash chaining (`_prevHash` -> `_hash`) for tamper-evident immutability
167
- - normalized query tables in SQLite (`audit_events`, `approvals`)
168
- - policy denials captured as approval/authorization events (for example blocked commands)
169
-
170
- Useful commands:
171
-
172
- - `hybridclaw audit recent 50`
173
- - `hybridclaw audit search "tool.call" 50`
174
- - `hybridclaw audit approvals 50 --denied`
175
- - `hybridclaw audit verify <sessionId>`
176
- - `hybridclaw audit instructions`
177
- - `hybridclaw audit instructions --approve`
178
-
179
- Instruction approval notes:
180
-
181
- - local baseline file: `data/audit/instruction-hashes.json`
182
- - `hybridclaw audit instructions` fails when instruction files differ from the approved baseline
183
- - `hybridclaw audit instructions --approve` updates the local approved baseline
184
- - `hybridclaw tui` performs this check before startup and prompts for approval when files changed
185
- - instruction approval actions are audit logged (`approval.request` / `approval.response`, action `instruction:approve`)
186
-
187
- ## Observability Push
188
-
189
- HybridClaw can forward structured audit records to HybridAI's ingest API:
190
-
191
- - endpoint: `POST /api/v1/agent-observability/events:batch`
192
- - source: local `audit_events` table (ordered by `id`)
193
- - transport: bearer ingest token auto-fetched via `POST /api/v1/agent-observability/ingest-token:ensure` using `HYBRIDAI_API_KEY`
194
- - delivery: incremental batches with persisted cursor (`observability_offsets` table), max 1000 events and max 2,000,000-byte payload per request
195
- - token handling: token cache is stored locally in SQLite (`observability_ingest_tokens`) and automatically refreshed on ingest auth failures
196
- - token visibility: `model.usage` payloads include `promptTokens`, `completionTokens`, `totalTokens`, plus estimated and API-native counters for accuracy/coverage
197
-
198
- Config keys (in `config.json`):
199
-
200
- - `observability.enabled` (`true` by default)
201
- - `observability.baseUrl` (for example `https://hybridai.one`)
202
- - `observability.ingestPath` (`/api/v1/agent-observability/events:batch`)
203
- - `observability.botId` (defaults to `hybridai.defaultChatbotId` when empty)
204
- - `observability.agentId`, `observability.label`, `observability.environment`
205
- - `observability.flushIntervalMs`, `observability.batchMaxEvents`
206
-
207
- Runtime diagnostics:
208
-
209
- - local status endpoint `GET /api/status` includes an `observability` block (enabled/running/paused, cursor, last success/failure timestamps)
210
-
211
- ## Agent workspace
212
-
213
- Each agent gets a persistent workspace with markdown files that shape its personality and memory:
214
-
215
- | File | Purpose |
216
- |------|---------|
217
- | `SOUL.md` | Personality, tone, identity |
218
- | `IDENTITY.md` | Name, avatar, emoji |
219
- | `USER.md` | Info about the human |
220
- | `MEMORY.md` | Persistent memory across sessions |
221
- | `AGENTS.md` | Workspace conventions and rules |
222
- | `TOOLS.md` | Environment-specific notes |
223
- | `HEARTBEAT.md` | Periodic tasks |
224
- | `BOOT.md` | Startup instructions |
225
-
226
- Templates in `templates/` are copied to new agent workspaces on first run.
227
- Historical turn logs are mirrored into `<workspace>/.session-transcripts/*.jsonl` for `session_search`.
228
-
229
- ## Skills
230
-
231
- HybridClaw supports `SKILL.md`-based skills (`<skill-name>/SKILL.md`).
232
-
233
- ### Where to put skills
234
-
235
- You can place skills in:
236
-
237
- - any directory listed in `config.skills.extraDirs[]` (enterprise/shared)
238
- - bundled package skills (`<hybridclaw install>/skills/<skill-name>/SKILL.md`)
239
- - `$CODEX_HOME/skills/<skill-name>/SKILL.md` or `~/.codex/skills/<skill-name>/SKILL.md`
240
- - `~/.claude/skills/<skill-name>/SKILL.md`
241
- - `~/.agents/skills/<skill-name>/SKILL.md`
242
- - `./.agents/skills/<skill-name>/SKILL.md` (project)
243
- - `./skills/<skill-name>/SKILL.md` (workspace)
244
-
245
- Load precedence is:
246
-
247
- - `extra < bundled < codex < claude < agents-personal < agents-project < workspace`
248
- - skills are merged by `name`; higher-precedence sources override lower-precedence ones
249
-
250
- Security scanning is trust-aware:
251
-
252
- - `bundled` sources are treated as `builtin` and not scanned
253
- - `workspace` sources (`./skills/`, `./.agents/skills/`) are scanned; `caution` is allowed, `dangerous` is blocked
254
- - `personal` sources (`~/.codex/skills/`, `~/.claude/skills/`, `~/.agents/skills/`) are scanned and blocked on `caution`/`dangerous`
255
- - scanner includes Hermes-derived regex checks, structural limits (50 files, 1MB total, 256KB/file, binary/symlink checks), invisible-unicode detection, and mtime+content-hash cache reuse
256
-
257
- ### Required format
258
-
259
- Each skill must be a folder with a `SKILL.md` file and frontmatter:
260
-
261
- ```markdown
262
- ---
263
- name: repo-orientation
264
- description: Quickly map an unfamiliar repository and identify where a requested feature should be implemented.
265
- user-invocable: true
266
- disable-model-invocation: false
267
- always: false
268
- requires:
269
- bins: [docker, git]
270
- env: [GITHUB_TOKEN]
271
- metadata:
272
- hybridclaw:
273
- tags: [devops, docker]
274
- related_skills: [kubernetes]
275
- ---
276
-
277
- # Repo Orientation
278
- ...instructions...
279
- ```
280
-
281
- Supported frontmatter keys:
282
-
283
- - `name` (required)
284
- - `description` (required)
285
- - `user-invocable` (optional, default `true`)
286
- - `disable-model-invocation` (optional, default `false`)
287
- - `always` (optional, default `false`; embeds full skill body in the system prompt up to `maxAlwaysChars=10000`, then demotes to summary)
288
- - `requires.bins` / `requires.env` (optional; skill is excluded unless requirements are met)
289
- - `metadata.hybridclaw.tags` / `metadata.hybridclaw.related_skills` (optional metadata namespace)
82
+ HybridClaw creates `~/.hybridclaw/config.json` on first run and hot-reloads most runtime settings.
290
83
 
291
- ### Using skills
292
-
293
- Skills are listed to the model as metadata (`name`, `description`, `location`), and the model reads `SKILL.md` on demand with the `read` tool. Skills with `always: true` are embedded directly in the system prompt.
294
-
295
- Prompt embedding modes:
296
-
297
- - `Always`: `always: true` embeds full body in `<skill_always ...>` (budgeted by `maxAlwaysChars=10000`)
298
- - `Summary`: default mode, emits only XML metadata under `<available_skills>`
299
- - `Hidden`: `disable-model-invocation: true` excludes the skill from model prompt metadata (still invocable by slash command when `user-invocable: true`)
300
-
301
- Explicit invocation is supported via:
302
-
303
- - `/skill <name> [input]`
304
- - `/skill:<name> [input]`
305
- - `/<name> [input]` (when `user-invocable: true`; command names are sanitized to lowercase `a-z0-9-`, max 32 chars, with `-2`/`-3` dedup and built-in command-name blocking)
306
-
307
- Example skill in this repo:
308
-
309
- - `skills/repo-orientation/SKILL.md`
310
- - `skills/current-time/SKILL.md`
311
- - `skills/personality/SKILL.md`
312
- - `skills/skill-creator/SKILL.md`
313
-
314
- ### Personality switching skill
315
-
316
- HybridClaw includes a command-only personality skill that updates the active persona contract in `SOUL.md`.
317
-
318
- - List current/available persona: `/personality` (or `/personality list`)
319
- - Activate persona: `/personality <name>`
320
- - Reset to default persona: `/personality reset`
321
-
322
- The skill writes/updates a managed block in `SOUL.md`:
323
-
324
- - `## Active personality`
325
- - `Name: ...`
326
- - `Definition: ...` (copied from the selected profile in `skills/personality/SKILL.md`)
327
- - `Rules: ...` (runtime style/behavior constraints)
328
-
329
- Notes:
330
-
331
- - The personality skill is intentionally command-only (`always: false`, `disable-model-invocation: true`) to avoid adding per-turn prompt overhead.
332
- - Profiles are defined in `skills/personality/SKILL.md` and currently include 25 switchable personas (expert, style, and role personas).
333
-
334
- ## Agent tools
335
-
336
- The agent has access to these sandboxed tools inside the container:
337
-
338
- - `read` / `write` / `edit` / `delete` — file operations
339
- - `glob` / `grep` — file search
340
- - `bash` — shell command execution
341
- - `memory` — durable memory files (`MEMORY.md`, `USER.md`, `memory/YYYY-MM-DD.md`)
342
- - `session_search` — search/summarize historical sessions from transcript archives
343
- - `delegate` — push-based background subagent tasks (`single`, `parallel`, `chain`) with auto-announced completion (no polling)
344
- - `web_fetch` — plain HTTP fetch + extraction for static/read-only content (docs, articles, READMEs, JSON/text APIs, direct files)
345
- - `browser_*` (optional) — full browser automation for JS-rendered or interactive pages (`navigate`, `snapshot`, `click`, `type`, `upload`, `press`, `scroll`, `back`, `screenshot`, `pdf`, `close`)
346
-
347
- `delegate` mode examples:
348
-
349
- - single: `{ "prompt": "Audit auth middleware and list risks", "label": "auth-audit" }`
350
- - parallel: `{ "mode": "parallel", "label": "module-audit", "tasks": [{ "prompt": "Scan api/" }, { "prompt": "Scan ui/" }] }`
351
- - chain: `{ "mode": "chain", "label": "implement-flow", "chain": [{ "prompt": "Scout the payment module" }, { "prompt": "Plan changes from: {previous}" }, { "prompt": "Implement based on: {previous}" }] }`
352
-
353
- Browser tooling notes:
354
-
355
- - Routing default: prefer `web_fetch` first for read-only retrieval.
356
- - Use browser tools for SPAs/web apps/auth flows/interaction tasks, or when `web_fetch` returns escalation hints (`javascript_required`, `spa_shell_only`, `empty_extraction`, `boilerplate_only`, `bot_blocked`).
357
- - Cost profile: browser calls are typically ~10-100x slower/more expensive than `web_fetch`.
358
- - Browser read flow: after `browser_navigate`, use `browser_snapshot` with `mode="full"` to extract content, then `browser_scroll` + `browser_snapshot` for additional lazy-loaded sections.
359
- - `browser_pdf` is for export artifacts, not text extraction.
360
-
361
- - The shipped container image preinstalls `agent-browser` and Chromium (Playwright).
362
- - You can override the binary via `AGENT_BROWSER_BIN` if needed.
363
- - User-directed authenticated browser-flow testing is supported (including filling/submitting login forms on the requested site).
364
- - Browser auth/session state now persists per HybridClaw session by default via a dedicated profile directory under `/workspace/.hybridclaw-runtime/browser-profiles`.
365
- - Session cookies/localStorage are also auto-saved/restored via `agent-browser` session-state files.
366
- - Optional overrides: `BROWSER_PERSIST_PROFILE=false` (disable profile persistence), `BROWSER_PERSIST_SESSION_STATE=false` (disable state file persistence), `BROWSER_PROFILE_ROOT=/path` (custom profile root), `BROWSER_CDP_URL=ws://...` (force CDP attachment to an existing browser).
367
- - Structured audit logs redact sensitive browser/tool arguments (password/token/secret fields and typed form text).
368
- - Navigation to private/loopback hosts is blocked by default (set `BROWSER_ALLOW_PRIVATE_NETWORK=true` to override).
369
- - Screenshot/PDF outputs are constrained to `/workspace/.browser-artifacts`.
370
-
371
- HybridClaw also supports automatic session compaction with pre-compaction memory flush:
372
-
373
- - when a session gets long, old turns are summarized into `session_summary`
374
- - before compaction, the agent gets a `memory`-only flush turn to persist durable notes
375
- - each `(agent_id, user_id)` pair also maintains a canonical cross-channel session for continuity across channels
376
- - canonical context injection includes compacted summary + recent cross-channel messages (excluding the current live session)
377
- - compaction writes JSONL exports to `<workspace>/.session-exports/` for human-readable debugging
378
-
379
- System prompt assembly is handled by a formal hook pipeline:
380
-
381
- - `bootstrap` hook (workspace bootstrap + skills metadata)
382
- - `memory` hook (session summary)
383
- - `safety` hook (runtime guardrails / trust-model constraints)
384
- - `proactivity` hook (memory capture, session recall, delegation behavior)
385
-
386
- Hook toggles live in `config.json` under `promptHooks`.
387
-
388
- ## Testing
389
-
390
- Run checks locally:
391
-
392
- ```bash
393
- # Typecheck only (no emit)
394
- npm run typecheck
395
-
396
- # Strict TS lint gate (unused locals/params)
397
- npm run lint
398
-
399
- # Unit tests (default `npm test`)
400
- npm run test:unit
401
-
402
- # Scoped suites (ready for dedicated tests)
403
- npm run test:integration
404
- npm run test:e2e
405
- npm run test:live
406
- ```
407
-
408
- Test layout and scopes:
409
-
410
- - tests live under `tests/` (not `src/`)
411
- - unit tests: `tests/**/*.test.ts` (excluding `*.integration|*.e2e|*.live`)
412
- - integration tests: `tests/**/*.integration.test.ts`
413
- - e2e tests: `tests/**/*.e2e.test.ts`
414
- - live tests: `tests/**/*.live.test.ts`
84
+ - Start from `config.example.json` (reference).
85
+ - Runtime data is stored in `~/.hybridclaw/` by default (`config.json`, `data/hybridclaw.db`, audit/session files).
86
+ - On upgrade, legacy `./config.json` and `./data` are migrated to `~/.hybridclaw` automatically; backups are kept in `~/.hybridclaw/migration-backups/` when needed.
87
+ - Keep secrets in `.env` (`HYBRIDAI_API_KEY` required, `DISCORD_TOKEN` optional).
88
+ - Trust-model acceptance is stored in `~/.hybridclaw/config.json` under `security.*` and is required before runtime starts.
89
+ - See [TRUST_MODEL.md](./TRUST_MODEL.md) for onboarding acceptance policy and [SECURITY.md](./SECURITY.md) for technical security guidelines.
90
+ - For advanced configuration, audit/observability details, skills internals, agent tools, and developer docs, see [CONTRIBUTING.md](./CONTRIBUTING.md).
415
91
 
416
92
  ## Commands
417
93
 
@@ -444,18 +120,3 @@ In Discord, use `!claw help` to see all commands. Key ones:
444
120
  - `!claw schedule add "<cron>" <prompt>` — Add cron scheduled task
445
121
  - `!claw schedule add at "<ISO time>" <prompt>` — Add one-shot task
446
122
  - `!claw schedule add every <ms> <prompt>` — Add interval task
447
-
448
- ## Project structure
449
-
450
- ```
451
- src/gateway.ts Core runtime entrypoint (DB, scheduler, heartbeat, HTTP API)
452
- src/tui.ts Terminal adapter (thin client to gateway)
453
- src/channels/discord/runtime.ts Discord runtime integration and message transport
454
- src/channels/discord/*.ts Discord responsibility modules (inbound, delivery, mentions, attachments, tools, stream)
455
- src/gateway-service.ts Core shared agent/session logic used by gateway API
456
- src/gateway-client.ts HTTP client used by thin clients (e.g. TUI)
457
- tests/ Vitest suites (unit/integration/e2e/live scopes)
458
- container/src/ Agent code (tools, HybridAI client, IPC)
459
- templates/ Workspace bootstrap files
460
- data/ Runtime data (gitignored): SQLite DB, sessions, agent workspaces
461
- ```
@@ -92,7 +92,7 @@
92
92
  "webApiToken": "",
93
93
  "gatewayBaseUrl": "http://127.0.0.1:9090",
94
94
  "gatewayApiToken": "",
95
- "dbPath": "data/hybridclaw.db",
95
+ "dbPath": "~/.hybridclaw/data/hybridclaw.db",
96
96
  "logLevel": "info"
97
97
  },
98
98
  "observability": {
@@ -0,0 +1,16 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.*
5
+ .npmrc
6
+ npm-debug.log*
7
+ yarn-error.log*
8
+ *.pem
9
+ *.key
10
+ *.crt
11
+ *.p12
12
+ *.pfx
13
+ *.kubeconfig
14
+ *.sqlite
15
+ *.db
16
+ *.log
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hybridclaw-agent",
3
- "version": "0.2.8",
3
+ "version": "0.2.12",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "hybridclaw-agent",
9
- "version": "0.2.8",
9
+ "version": "0.2.12",
10
10
  "dependencies": {
11
11
  "@mozilla/readability": "^0.6.0",
12
12
  "agent-browser": "^0.15.1",
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "hybridclaw-agent",
3
- "version": "0.2.8",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
+ "main": "dist/index.js",
6
+ "files": [
7
+ "dist/",
8
+ "Dockerfile"
9
+ ],
5
10
  "scripts": {
11
+ "clean": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});\"",
6
12
  "build": "tsc",
7
- "lint": "tsc --noEmit --noUnusedLocals --noUnusedParameters"
13
+ "lint": "tsc --noEmit --noUnusedLocals --noUnusedParameters",
14
+ "prepack": "npm run clean && npm run build && npm run release:check",
15
+ "release:check": "node ./scripts/release-check.mjs",
16
+ "publish:dry": "npm publish --dry-run"
8
17
  },
9
18
  "dependencies": {
10
19
  "@mozilla/readability": "^0.6.0",
@@ -8,12 +8,12 @@ import {
8
8
  runAfterToolHooks,
9
9
  runBeforeToolHooks,
10
10
  } from './extensions.js';
11
- import {
12
- callHybridAI,
13
- callHybridAIStream,
14
- HybridAIRequestError,
15
- } from './hybridai-client.js';
11
+ import { callHybridAI, callHybridAIStream } from './hybridai-client.js';
16
12
  import { waitForInput, writeOutput } from './ipc.js';
13
+ import {
14
+ isRetryableModelError,
15
+ shouldFallbackFromStreamError,
16
+ } from './model-retry.js';
17
17
  import {
18
18
  accumulateApiUsage,
19
19
  createTokenUsageStats,
@@ -305,16 +305,6 @@ function emitStreamDelta(delta: string): void {
305
305
  console.error(`[stream] ${payload}`);
306
306
  }
307
307
 
308
- function isRetryableError(err: unknown): boolean {
309
- if (err instanceof HybridAIRequestError) {
310
- return err.status === 429 || (err.status >= 500 && err.status <= 504);
311
- }
312
- const message = err instanceof Error ? err.message : String(err);
313
- return /fetch failed|network|socket|timeout|timed out|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(
314
- message,
315
- );
316
- }
317
-
318
308
  function inferToolError(result: string, blockedReason: string | null): boolean {
319
309
  if (blockedReason) return true;
320
310
  return /\b(error|failed|denied|forbidden|timed out|timeout|exception|invalid)\b/i.test(
@@ -538,11 +528,7 @@ async function callHybridAIWithRetry(params: {
538
528
  maxTokens,
539
529
  );
540
530
  } catch (streamErr) {
541
- const fallbackEligible =
542
- streamErr instanceof HybridAIRequestError &&
543
- streamErr.status >= 400 &&
544
- streamErr.status < 500 &&
545
- streamErr.status !== 429;
531
+ const fallbackEligible = shouldFallbackFromStreamError(streamErr);
546
532
  if (!fallbackEligible) throw streamErr;
547
533
  response = await callHybridAI(
548
534
  baseUrl,
@@ -575,7 +561,9 @@ async function callHybridAIWithRetry(params: {
575
561
  return response;
576
562
  } catch (err) {
577
563
  const retryable =
578
- RETRY_ENABLED && isRetryableError(err) && attempt < RETRY_MAX_ATTEMPTS;
564
+ RETRY_ENABLED &&
565
+ isRetryableModelError(err) &&
566
+ attempt < RETRY_MAX_ATTEMPTS;
579
567
  await emitRuntimeEvent({
580
568
  event: retryable ? 'model_retry' : 'model_error',
581
569
  attempt,
@@ -0,0 +1,19 @@
1
+ import { HybridAIRequestError } from './hybridai-client.js';
2
+
3
+ const TRANSIENT_NETWORK_ERROR_RE =
4
+ /fetch failed|network|socket|timeout|timed out|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i;
5
+
6
+ export function shouldFallbackFromStreamError(error: unknown): boolean {
7
+ if (!(error instanceof HybridAIRequestError)) return false;
8
+ // Keep 429 on retry/backoff path; fallback does not help throttling.
9
+ if (error.status === 429) return false;
10
+ return error.status >= 400 && error.status <= 599;
11
+ }
12
+
13
+ export function isRetryableModelError(error: unknown): boolean {
14
+ if (error instanceof HybridAIRequestError) {
15
+ return error.status === 429 || (error.status >= 500 && error.status <= 504);
16
+ }
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ return TRANSIENT_NETWORK_ERROR_RE.test(message);
19
+ }