@nanhara/hara 0.70.0 → 0.95.2
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/CHANGELOG.md +395 -0
- package/README.md +7 -2
- package/dist/agent/compact.js +10 -0
- package/dist/agent/context-report.js +33 -0
- package/dist/agent/failover.js +53 -0
- package/dist/agent/loop.js +120 -13
- package/dist/agent/rewind.js +20 -0
- package/dist/agent/route.js +47 -0
- package/dist/checkpoints.js +91 -0
- package/dist/config.js +33 -9
- package/dist/context/subdir-hints.js +76 -0
- package/dist/cron/runner.js +7 -4
- package/dist/exec/jobs.js +82 -0
- package/dist/gateway/dingtalk.js +209 -0
- package/dist/gateway/discord.js +164 -0
- package/dist/gateway/feishu.js +144 -0
- package/dist/gateway/matrix.js +188 -0
- package/dist/gateway/mattermost.js +206 -0
- package/dist/gateway/serve.js +339 -0
- package/dist/gateway/sessions.js +89 -0
- package/dist/gateway/signal.js +220 -0
- package/dist/gateway/slack.js +190 -0
- package/dist/gateway/telegram.js +115 -0
- package/dist/gateway/tmux-routes.js +135 -0
- package/dist/gateway/tts.js +100 -0
- package/dist/gateway/wecom.js +383 -0
- package/dist/gateway/weixin.js +590 -0
- package/dist/index.js +1073 -120
- package/dist/org-fleet/enroll.js +28 -5
- package/dist/plugins/plugins.js +49 -1
- package/dist/profile/profile.js +436 -0
- package/dist/providers/anthropic.js +28 -1
- package/dist/providers/openai.js +16 -1
- package/dist/sandbox.js +15 -12
- package/dist/security/external-content.js +57 -0
- package/dist/security/permissions.js +211 -0
- package/dist/session/session-model.js +36 -0
- package/dist/statusbar.js +12 -3
- package/dist/tools/builtin.js +49 -2
- package/dist/tools/external_agent.js +118 -0
- package/dist/tools/send.js +35 -0
- package/dist/tools/skill.js +6 -2
- package/dist/tools/todo.js +65 -8
- package/dist/tools/web.js +4 -3
- package/dist/tui/App.js +241 -30
- package/dist/tui/run.js +36 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,401 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.94.1 — unreleased (gateway: relay is on-inbound, not a noisy push)
|
|
9
|
+
|
|
10
|
+
- **Fix the bind output relay to be quiet + platform-correct.** 0.94.0's continuous 3s timer-push flooded chat
|
|
11
|
+
(a message every few seconds) and hit iLink's `ret=-2` (its bot model is passive-reply, no continuous push).
|
|
12
|
+
Replaced with **on-inbound relay**: when you message a bound/registered pane, the daemon captures the pane,
|
|
13
|
+
injects your text, waits ~3s, and replies **once** with the session's NEW output (`🖥 <pane>\n<delta>`). One
|
|
14
|
+
reply per message — no spam, and it fits iLink's "one inbound → one reply" model. Send `?` to peek again.
|
|
15
|
+
- `tmux-routes.ts`: `pickPaneForReply()` (pick+consume, no inject); the timer loop is gone.
|
|
16
|
+
- `serve.ts` onMessage: capture-before → inject → settle → reply with `outputDelta`.
|
|
17
|
+
|
|
18
|
+
## 0.94.0 — unreleased (gateway: bind output relay — two-way remote terminal)
|
|
19
|
+
|
|
20
|
+
- **Output relay for `hara remote bind`** — the daemon now polls each bound tmux pane and pushes its NEW output
|
|
21
|
+
back to chat once it settles, so a session you drive from your phone is **two-way**: your replies inject in
|
|
22
|
+
(existing), and you SEE the session's output come back (`🖥 <pane>\n<delta>`). Only bound (`bind`) panes; the
|
|
23
|
+
first sighting is baselined (no dump of the pre-existing screen); best-effort send (iLink cold-push may drop a
|
|
24
|
+
message when the chat is cold). Pure `outputDelta` (append / unchanged / scroll-anchor / tail) unit-tested.
|
|
25
|
+
- `src/gateway/tmux-routes.ts`: `boundRoutes`, `capturePane`, `outputDelta`.
|
|
26
|
+
- `src/gateway/serve.ts`: a 3s relay loop in `runGateway` (capture → settle → send delta), cleared on abort.
|
|
27
|
+
- Caveat: a full-screen TUI session (e.g. Claude Code's ink UI) relays the rendered frame; a non-TUI /
|
|
28
|
+
`HARA_TUI=0` / `-p`-style session relays cleaner. 285 tests.
|
|
29
|
+
## 0.93.0 — unreleased (hara remote — universal chat-driven HITL for any tmux session)
|
|
30
|
+
|
|
31
|
+
- **`hara remote`** — a first-class, agent-agnostic command so ANY terminal session in tmux (Claude Code, codex,
|
|
32
|
+
hara, a plain REPL) can be driven from chat, not just via the Claude-Code wechat-send skill:
|
|
33
|
+
- `hara remote ask "<q>"` — register this pane (one-shot) + push the question to WeChat; your reply injects back.
|
|
34
|
+
- `hara remote bind` — **persistent bind**: every WeChat reply injects into this pane until `unbind` / `/detach`
|
|
35
|
+
(drive a whole session from your phone while you're out, many messages, not just one).
|
|
36
|
+
- `hara remote unbind` · `hara remote status`.
|
|
37
|
+
- Route store gains `mode: "once" | "bind"`; the daemon consumes "once" routes but keeps "bind" ones
|
|
38
|
+
(`pickRoute`/`deliverToTmux`). New `/detach` chat command unbinds all persistent panes from your phone.
|
|
39
|
+
- Generic by design: the injection is just `tmux send-keys`, so it works for any program reading stdin in the
|
|
40
|
+
pane — the agent only needs to call `hara remote ask` when it wants your input (the confirm-loop pattern).
|
|
41
|
+
- 284 tests (+ bind-mode `pickRoute`); persistent-bind verified live (two replies → same pane, route persists).
|
|
42
|
+
|
|
43
|
+
## 0.92.0 — unreleased (gateway: reply-into-tmux — two-way HITL for any running session)
|
|
44
|
+
|
|
45
|
+
- **Reply routing into an already-running tmux session** — a session you started yourself (Claude Code / codex /
|
|
46
|
+
hara, in tmux) can ping you on WeChat and, while you're away, your reply gets **injected back into that exact
|
|
47
|
+
session** so it continues. No need to launch it under a supervisor or use a blocking tool — borrows the ccgram
|
|
48
|
+
keystroke-injection pattern (the only way to retrofit a session the daemon doesn't own).
|
|
49
|
+
- `src/gateway/tmux-routes.ts`: a route store (`~/.hara/gateway/tmux-routes.json`) + `tmux send-keys` injection.
|
|
50
|
+
The gateway daemon, on an owner reply, injects it into the **oldest live registered pane** (`deliverToTmux`),
|
|
51
|
+
one-shot per ask, dead panes pruned. Pure `pickRoute` + `paneAlive` (list-panes membership) unit-tested.
|
|
52
|
+
- `serve.ts` `onMessage`: a non-slash reply is routed to a waiting pane (and the task isn't re-run) — owner-gated
|
|
53
|
+
by the existing allowlist; only panes that **opted in** are ever touched.
|
|
54
|
+
- The `wechat-send` skill gains `--ask`: send a question + register this tmux pane for the reply.
|
|
55
|
+
- Live-verified the injection path (send-keys → pane received the line); fixed `paneAlive` (display-message was
|
|
56
|
+
too lenient → use `list-panes -a` membership). 283 tests.
|
|
57
|
+
|
|
58
|
+
## 0.91.0 — unreleased (external_agent: delegate to Claude Code / Codex)
|
|
59
|
+
|
|
60
|
+
- **`external_agent` tool** — hand a self-contained task to an EXTERNAL coding agent (**`claude`** / **`codex`**)
|
|
61
|
+
running headless in the current dir, and get its result back. Zero new deps — drives each agent's native
|
|
62
|
+
headless flag (`claude -p … --output-format text --permission-mode …`, `codex exec … --cd … --sandbox …`)
|
|
63
|
+
over `node:child_process`, not openclaw's heavier ACP/acpx stack. Pick the best engine per task.
|
|
64
|
+
- **Gated**: `kind:"exec"` → inherits the approval flow; and because read-only fan-out sub-agents only get the
|
|
65
|
+
`READONLY_TOOLS` allow-list, this privileged tool is **never** exposed to them.
|
|
66
|
+
- **Trust tiers** `externalAgentTrust` / `HARA_EXTERNAL_AGENT_TRUST` = `off | gated (default) | full`. `gated`
|
|
67
|
+
runs the external agent in its safe sub-mode (`claude --permission-mode plan/acceptEdits`,
|
|
68
|
+
`codex --sandbox read-only/workspace-write`); the dangerous bypass/full-access sub-modes are only reachable at
|
|
69
|
+
`full`. Backend allow-list (claude/codex), timeout + output cap. Pure `buildExternalArgv` unit-tested.
|
|
70
|
+
|
|
71
|
+
## 0.90.0 — unreleased (gateway: WeCom + Signal → 10 platforms)
|
|
72
|
+
|
|
73
|
+
- **WeCom (企业微信)** — connects out to WeCom's AI-Bot WebSocket gateway (no public webhook). `HARA_WECOM_BOT_ID`
|
|
74
|
+
+ `HARA_WECOM_SECRET`. Two-way: inbound text + images (incl. AES-decrypted attachments → `~/.hara/wecom/media`),
|
|
75
|
+
outbound text/image/file. Zero new deps (native WebSocket + node:crypto).
|
|
76
|
+
- **Signal** — talks to a local **signal-cli** daemon (JSON-RPC). `HARA_SIGNAL_RPC_URL` + `HARA_SIGNAL_NUMBER`.
|
|
77
|
+
Inbound text + image attachments, outbound text/file; phone numbers redacted in logs. signal-cli is an external
|
|
78
|
+
daemon the user runs (documented). Zero new npm deps.
|
|
79
|
+
- Pure parsers `parseWecomMessage` / `parseSignalMessage` unit-tested. docs/gateway.md + README updated to 10
|
|
80
|
+
platforms. Ported from the openclaw + hermes adapters.
|
|
81
|
+
|
|
82
|
+
## 0.89.0 — unreleased (gateway: Slack · Mattermost · Matrix · DingTalk + docs)
|
|
83
|
+
|
|
84
|
+
- **Four more platforms**, all zero-new-dep (native WebSocket / `fetch`), same `ChatAdapter` seam:
|
|
85
|
+
- **Slack** — Socket Mode (connects out, no public URL). `HARA_SLACK_APP_TOKEN` (xapp-) + `HARA_SLACK_BOT_TOKEN`
|
|
86
|
+
(xoxb-). Inbound text + image files (downloaded via the bot token); outbound text + file upload.
|
|
87
|
+
- **Mattermost** — v4 WebSocket + REST. `HARA_MATTERMOST_URL` + `HARA_MATTERMOST_TOKEN` (bot/PAT). Two-way images.
|
|
88
|
+
- **Matrix** — `/sync` long-poll. `HARA_MATRIX_HOMESERVER` + `HARA_MATRIX_TOKEN` + `HARA_MATRIX_USER_ID`.
|
|
89
|
+
Two-way images; **unencrypted rooms only in v1** (no E2EE).
|
|
90
|
+
- **DingTalk (钉钉)** — Stream Mode (connects out). `HARA_DINGTALK_CLIENT_ID` + `HARA_DINGTALK_CLIENT_SECRET`.
|
|
91
|
+
Text in/out via the per-message `sessionWebhook`; **v1: no file send, inbound images arrive as `[图片]`**.
|
|
92
|
+
- Each platform's wire parser (`parseSlackEvent` / `parseMattermostPost` / `parseMatrixEvent` / `parseMxc` /
|
|
93
|
+
`parseDingtalkMessage`) is pure + unit-tested.
|
|
94
|
+
- **Docs** — new **[docs/gateway.md](docs/gateway.md)** documents all 8 platforms (Telegram · WeChat · Discord ·
|
|
95
|
+
Feishu/Lark · Slack · Mattermost · Matrix · DingTalk): a capabilities table, common config
|
|
96
|
+
(`HARA_GATEWAY_ALLOWED`, `--cwd`, slash commands, two-way images), and per-platform setup. Linked from README.
|
|
97
|
+
- Ported by studying the openclaw + hermes adapter implementations; live-tested per platform as tokens become
|
|
98
|
+
available.
|
|
99
|
+
|
|
100
|
+
## 0.88.0 — unreleased (gateway: Feishu/Lark adapter)
|
|
101
|
+
|
|
102
|
+
- **Feishu/Lark** — `hara gateway --platform feishu` via the official `@larksuiteoapi/node-sdk` (the one new
|
|
103
|
+
dependency): a **WSClient long-connection** for inbound (no public webhook needed — fits the local daemon) and
|
|
104
|
+
the REST Client for outbound. Inbound text / rich-text post / image / file / audio (media downloaded to
|
|
105
|
+
`~/.hara/feishu/media` → the agent SEES images); outbound text + `sendFile` (image upload → image message,
|
|
106
|
+
else file upload → file message) so `send_file` / `/send` work. Creds via `HARA_FEISHU_APP_ID` /
|
|
107
|
+
`HARA_FEISHU_APP_SECRET` (+ `HARA_FEISHU_DOMAIN=lark` for larksuite.com), users via `HARA_GATEWAY_ALLOWED`
|
|
108
|
+
(open_id). v1 = p2p DMs; group support is a fast-follow. `parseFeishuContent` / `flattenPost` are pure + tested.
|
|
109
|
+
|
|
110
|
+
## 0.87.0 — unreleased (gateway: Discord adapter)
|
|
111
|
+
|
|
112
|
+
- **Discord** — `hara gateway --platform discord` connects to the Discord gateway over Node's native global
|
|
113
|
+
WebSocket (zero new dep on Node ≥ 22): HELLO→heartbeat→IDENTIFY, dispatches MESSAGE_CREATE, auto-reconnects.
|
|
114
|
+
Inbound text + image attachments (downloaded to `~/.hara/discord/media` → the agent SEES them); outbound text
|
|
115
|
+
(2000-char chunks) and `sendFile` (multipart) so `send_file` / `/send` work. Token via `HARA_DISCORD_TOKEN`,
|
|
116
|
+
users via `HARA_GATEWAY_ALLOWED` (Discord user ids). Needs the bot's privileged Message Content Intent.
|
|
117
|
+
Same `ChatAdapter` seam as Telegram/WeChat — all cross-platform gateway logic worked unchanged.
|
|
118
|
+
|
|
119
|
+
## 0.86.1 — unreleased (gateway: Telegram reaches image parity with WeChat)
|
|
120
|
+
|
|
121
|
+
- **Telegram two-way images** — `parseTelegramUpdate` now accepts photo messages (caption or a `[图片]` marker)
|
|
122
|
+
and the receive loop downloads the largest photo to `~/.hara/telegram/media` → `InboundMsg.images`, so the
|
|
123
|
+
agent sees it just like on WeChat. Added `sendFile` (sendPhoto for images, sendDocument otherwise) so `send_file`
|
|
124
|
+
and `/send` work on Telegram too. All the cross-platform gateway plumbing (send_file tool, in-chat system
|
|
125
|
+
context, stuck-guard, `-p` image attach/describe) was already platform-agnostic — only the adapter changed.
|
|
126
|
+
|
|
127
|
+
## 0.86.0 — unreleased (gateway: the agent SEES inbound images)
|
|
128
|
+
|
|
129
|
+
- **Inbound image understanding** — a photo you send in chat now reaches the model as a real image, not just a
|
|
130
|
+
`[图片: /path]` text breadcrumb. The WeChat adapter routes downloaded images into `InboundMsg.images`; the
|
|
131
|
+
gateway forwards their paths to the headless run (`HARA_GATEWAY_IMAGES`); the `-p` handler attaches them inline
|
|
132
|
+
for a vision-capable main model, or describes them via the configured `visionModel` sidecar and folds the
|
|
133
|
+
description into the message for a text-only model (e.g. glm-5 main + qwen3.7-plus vision). Together with 0.85's
|
|
134
|
+
`send_file`, the chat is now two-way for images. Verified live: glm-5 correctly described a sent picture via the
|
|
135
|
+
qwen3.7-plus sidecar.
|
|
136
|
+
|
|
137
|
+
## 0.85.0 — unreleased (gateway: agent sends files in conversation + stuck-guard)
|
|
138
|
+
|
|
139
|
+
- **`send_file` tool** — the agent can now deliver a file/image to the chat *conversationally* ("生成 X 发我"),
|
|
140
|
+
not just via the manual `/send` command. Self-gates on `HARA_GATEWAY`, so it appears only inside `hara gateway`.
|
|
141
|
+
It queues the path to a per-message outbox file; the daemon drains it after the headless run and delivers each
|
|
142
|
+
file via the platform adapter (`sendMediaFile` — images inline, others as attachments). This closes the gap
|
|
143
|
+
where hara, asked to "send an image", would generate it fine but then try to UI-automate the desktop WeChat
|
|
144
|
+
client with the `computer` tool (wrong surface, and blind without a vision model) and silently deliver nothing.
|
|
145
|
+
- **In-chat system context** — when running under the gateway, the agent is told it's in a `${platform}` chat:
|
|
146
|
+
deliver files via `send_file` (the only channel that reaches the peer), do **not** drive the desktop client /
|
|
147
|
+
`computer` tool, and never claim a file was sent unless `send_file` succeeded.
|
|
148
|
+
- **Stuck-guard (gateway only)** — once per run, if the agent keeps repeating one non-read tool (≥5×) or acts
|
|
149
|
+
blind (≥2 screenshots it can't read), a one-shot self-check nudge is injected so it steps back and picks a
|
|
150
|
+
working path instead of grinding forever (no human there to hit Esc). Off outside the gateway.
|
|
151
|
+
|
|
152
|
+
## 0.84.0 — unreleased (gateway: file + image send/receive)
|
|
153
|
+
|
|
154
|
+
- **Receive** — a file / image / (untranscribed) voice you send is now downloaded + AES-decrypted to a local
|
|
155
|
+
file under `~/.hara/weixin/media/`, and hara is handed a `[图片|文件 name|语音: /path]` reference so it can
|
|
156
|
+
read/process it (images → a vision-capable model). Ported iLink's inbound media path (CDN download →
|
|
157
|
+
AES-128-ECB decrypt) including the image `aeskey` hex-hack, the `encrypt_query_param` vs `encrypted_query_param`
|
|
158
|
+
naming, the conditional PKCS7 strip, the CDN-host SSRF allowlist, and a 128 MiB cap. Media-only messages
|
|
159
|
+
(no text) are now processed too.
|
|
160
|
+
- **Send** — `sendMediaFile` sends any local file: images go **inline** (`image_item`), everything else
|
|
161
|
+
(zip / pdf / doc / audio / …) as a **file attachment** (`file_item`) carrying the filename. New `/send <path>`
|
|
162
|
+
command (absolute / `~` / relative to the chat's dir). Voice replies + `/say` now ride the same generic path.
|
|
163
|
+
- `ChatAdapter.sendAudio?` → **`sendFile?`** (generic seam). Live-validated end-to-end: ZIP attachment + inline
|
|
164
|
+
image delivered. 265 tests.
|
|
165
|
+
|
|
166
|
+
## 0.83.0 — unreleased (gateway: voice replies — pluggable TTS, /voice + /say)
|
|
167
|
+
|
|
168
|
+
- The gateway can now **reply with voice** — a WeChat audio-file attachment (iLink's native voice bubble is
|
|
169
|
+
unreliable). `/voice` toggles spoken replies for a chat (each reply is also sent as audio); `/say <text>`
|
|
170
|
+
speaks one message. Ported iLink's media-upload path (getuploadurl → AES-128-ECB → CDN → `file_item`) into
|
|
171
|
+
`weixin.ts` byte-exact (incl. the dual AES-key encoding: hex in getuploadurl, base64-of-hex in sendmessage)
|
|
172
|
+
+ live-validated end-to-end.
|
|
173
|
+
- **Pluggable TTS** (`src/gateway/tts.ts`) — config-driven via env, nothing vendor-hardcoded, mirrors the video
|
|
174
|
+
project's provider design; both API and local:
|
|
175
|
+
- `say` (default) — local macOS, ~0.5s, Chinese voices (Tingting…), zero config → m4a.
|
|
176
|
+
- `openai` — any OpenAI-compatible `/audio/speech` endpoint (point `HARA_TTS_BASE_URL` at Aliyun DashScope or
|
|
177
|
+
a local TTS server); reuses the existing `openai` dep, no new dependency.
|
|
178
|
+
- `cmd` — a configurable local command (point `HARA_TTS_CMD` at VoxCPM or any local TTS; text on stdin).
|
|
179
|
+
- Select via `HARA_TTS_PROVIDER` (+ `HARA_TTS_VOICE`/`MODEL`/`BASE_URL`/`API_KEY`/`CMD`); a failed API
|
|
180
|
+
provider falls back to local `say`. New optional `ChatAdapter.sendAudio?` seam (weixin implements it).
|
|
181
|
+
|
|
182
|
+
## 0.82.2 — unreleased (gateway: fix the voice tag that made hara disclaim)
|
|
183
|
+
|
|
184
|
+
- 0.82.1's bare `[voice message]` prefix backfired — hara read it as raw audio to process and replied "I can't
|
|
185
|
+
handle voice," even though the message was already transcribed text (verified: the transcription reaches hara
|
|
186
|
+
correctly). Replaced with an explicit note ("…already transcribed to text below — just reply to it normally,
|
|
187
|
+
you don't have or need the audio") so hara answers the content instead of disclaiming.
|
|
188
|
+
|
|
189
|
+
## 0.82.1 — unreleased (gateway: tag transcribed WeChat voice messages)
|
|
190
|
+
|
|
191
|
+
- WeChat **voice input already works** — iLink transcribes voice server-side (`voice_item.text`) and the gateway
|
|
192
|
+
reads it. But the text reached hara unlabeled, so when the spoken words referenced "this voice," hara replied
|
|
193
|
+
as if it only got text. Now a transcribed voice message is tagged `[voice message] <text>`, so hara knows the
|
|
194
|
+
input came from voice. (Sending voice back is deferred — iLink's native voice bubble is unreliable per the
|
|
195
|
+
Hermes reference; only audio-file attachments are possible, and that needs the media-upload + TTS path.)
|
|
196
|
+
|
|
197
|
+
## 0.82.0 — unreleased (gateway: roam projects + threads from chat — /cd, /pwd, project-scoped /sessions)
|
|
198
|
+
|
|
199
|
+
- A chat can now **switch working directory at runtime**, and its session follows: `/cd <dir>` (absolute, `~`,
|
|
200
|
+
or relative to the current dir) moves the chat into that project and opens that project's own thread; `/pwd`
|
|
201
|
+
shows the current dir + thread; `/sessions` lists the **current dir's** threads; `/new` forks the current
|
|
202
|
+
dir's thread; `/resume <id>` jumps to a thread and adopts its dir so it runs in the right place. Each
|
|
203
|
+
(chat, dir) pair gets its own stable, resumable session id (`<platform>-<chatId>-<cwdTag>[-fork]`) — so one
|
|
204
|
+
gateway roams across projects while keeping per-project history, and switching back resumes. Foundation for
|
|
205
|
+
a future desktop client.
|
|
206
|
+
- The chat-session store (`~/.hara/gateway/chats.json`) is now cwd-aware (`chatContext`/`chatCd`); old entries
|
|
207
|
+
migrate in place (keep their existing thread). Tested.
|
|
208
|
+
|
|
209
|
+
## 0.81.6 — unreleased (headless `-p --resume`: auto-compact long sessions)
|
|
210
|
+
|
|
211
|
+
- The `hara -p … --resume <id>` / `--continue` path (used by the chat gateway and cron) now runs
|
|
212
|
+
`maybeAutoCompact` before saving, so a long chat/cron thread **auto-compacts** (summarizes old turns)
|
|
213
|
+
instead of growing until it overflows the model's context window. Silent in headless mode (no notify) so
|
|
214
|
+
nothing leaks into a captured reply. Opt-out via `autoCompact: false` / `HARA_AUTO_COMPACT=0`. Previously
|
|
215
|
+
auto-compaction was wired only into the interactive/TUI loops, so gateway/cron sessions could overflow.
|
|
216
|
+
|
|
217
|
+
## 0.81.5 — unreleased (gateway: default workspace = ~/.hara/workspace — dir-free, Hermes-style)
|
|
218
|
+
|
|
219
|
+
- `hara gateway` (no `--cwd`) now operates in a dedicated **`~/.hara/workspace`** (created + seeded on first
|
|
220
|
+
run) instead of the launch directory — so the chat bot is dir-free + safe-by-default (like Hermes' own
|
|
221
|
+
`~/.hermes`), never landing full-auto on whatever repo you happened to launch from. `--cwd <dir>` still
|
|
222
|
+
targets a real project.
|
|
223
|
+
- This only sets where *files* are created. hara's **global memory (`~/.hara/memory`) and roles
|
|
224
|
+
(`~/.hara/roles` + B-end `~/.hara/org-roles`) are cwd-independent and always loaded**, so a chat session
|
|
225
|
+
shares the same global brain as the terminal CLI regardless of workspace. `~/.hara/` is hara's home:
|
|
226
|
+
memory · roles · sessions · skills · checkpoints · config · per-platform gateway state.
|
|
227
|
+
|
|
228
|
+
## 0.81.4 — unreleased (gateway: clean chat replies — strip MCP logs + token footer)
|
|
229
|
+
|
|
230
|
+
- The chat gateway scraped the `hara -p` subprocess output and sent it verbatim, so WeChat/Telegram replies
|
|
231
|
+
were wrapped in CLI chrome — `mcp: … → N tool(s)`, `mcp: … failed …`, and the `model · ↑N ↓N tok` footer.
|
|
232
|
+
New `cleanReply()` strips those, leaving just the assistant's answer. (Tool/diff streaming for multi-step
|
|
233
|
+
tasks is untouched; this only removes the always-present MCP-startup + token-footer noise.) Tested.
|
|
234
|
+
|
|
235
|
+
## 0.81.3 — unreleased (fix: gateway/cron subprocess spawn when hara runs via the `hara` bin symlink)
|
|
236
|
+
|
|
237
|
+
- `selfArgv()` (used by the chat gateway and the cron tick to spawn a fresh `hara` per task) **dropped the
|
|
238
|
+
script path** when hara was invoked via the installed `hara` bin symlink: that path has no `.js` extension,
|
|
239
|
+
so the old extension-based heuristic returned just `[node]`, making the spawn `node -p <text> --approval …`
|
|
240
|
+
— which node rejected with `bad option: --approval` (the gateway replied with that error instead of running).
|
|
241
|
+
Now keyed on whether `execPath` is node (the real compiled-binary discriminator): it re-invokes the entry
|
|
242
|
+
(`dist/index.js` **or** the bin symlink) under node, and only re-invokes `execPath` directly for a true
|
|
243
|
+
single-binary build. Test covers all three cases.
|
|
244
|
+
|
|
245
|
+
## 0.81.2 — unreleased (gateway: `--cwd` flag to point at a workspace without `cd`)
|
|
246
|
+
|
|
247
|
+
- `hara gateway [--platform …] --cwd <dir>` sets the directory hara operates in for each incoming message,
|
|
248
|
+
so you can launch the daemon against a chosen workspace without `cd`-ing first (resolved to an absolute
|
|
249
|
+
path). Defaults to the current dir as before. Recommended pattern: a dedicated safe scratch dir (e.g.
|
|
250
|
+
`~/work/projects/tools/hara`) rather than a sensitive repo, since each message runs `--approval full-auto`.
|
|
251
|
+
|
|
252
|
+
## 0.81.1 — unreleased (WeChat gateway: auto-allowlist the bot owner)
|
|
253
|
+
|
|
254
|
+
- `hara gateway --platform weixin` now **auto-allows the bot owner** (the iLink `user_id` from login — i.e.
|
|
255
|
+
whoever scanned the QR), since on a personal-WeChat bot that id equals the `from_user_id` on your own
|
|
256
|
+
messages. Removes the "run once with an empty allowlist to discover your wxid, then re-run" dance — just
|
|
257
|
+
log in and run the daemon. Additional ids can still be added via `HARA_GATEWAY_ALLOWED`. (Validated the full
|
|
258
|
+
receive→reply round-trip against the live iLink server.)
|
|
259
|
+
|
|
260
|
+
## 0.81.0 — unreleased (WeChat (iLink) gateway adapter — drive your local hara from personal WeChat)
|
|
261
|
+
|
|
262
|
+
- **`hara gateway --platform weixin`** adds **WeChat (personal)** as a second chat channel, via Tencent's
|
|
263
|
+
official iLink bot API (`ilinkai.weixin.qq.com`, no ban risk) — the same `ChatAdapter` seam as Telegram,
|
|
264
|
+
so per-chat resumable sessions / allowlist / `/new` `/sessions` `/resume` all carry over. Text DMs (v1):
|
|
265
|
+
the bot reads & edits files and runs bash in the gateway's cwd, and replies in WeChat.
|
|
266
|
+
- **`hara gateway --platform weixin --login`** — interactive QR login (scan with WeChat); saves
|
|
267
|
+
`{account_id, token, base_url}` to `~/.hara/weixin/creds.json`. The per-peer `context_token` and the
|
|
268
|
+
long-poll cursor are persisted, so replies route correctly and a restart resumes mid-stream. Handles
|
|
269
|
+
iLink's `-14` / stale-`-2` session-expiry (tokenless retry on send; re-login prompt on poll). Built-in
|
|
270
|
+
fetch; the login QR renders via the **optional** `qrcode-terminal` dep (falls back to printing the URL).
|
|
271
|
+
No crypto needed on the text path.
|
|
272
|
+
- The allowlist now logs the sender id of unauthorized messages, so you can discover your WeChat id to add
|
|
273
|
+
to `HARA_GATEWAY_ALLOWED`. 256 tests.
|
|
274
|
+
|
|
275
|
+
## 0.80.0 — unreleased (chat gateway — drive your local hara from Telegram; + headless session continuity)
|
|
276
|
+
|
|
277
|
+
- **`hara gateway`** (opt-in daemon, hara's first long-running process) lets you drive your **local** hara from
|
|
278
|
+
a chat app — **Telegram** first ("message your bot → hara reads/edits files, runs bash, replies"). Each chat
|
|
279
|
+
is a **continuous, resumable session**: `/new` forks a fresh thread · `/sessions` lists · `/resume <id>`
|
|
280
|
+
jumps to one — backed by the stored sessions. Access is **allowlist-gated** (`HARA_GATEWAY_ALLOWED` user ids;
|
|
281
|
+
empty = nobody, never wide-open); token from `HARA_TELEGRAM_TOKEN`. Generic `ChatAdapter` shape → WeChat-iLink
|
|
282
|
+
/ Feishu are same-interface fast-follows. **Zero new dep** (built-in `fetch` long-poll). `src/gateway/` + tests.
|
|
283
|
+
- **`hara -p "<task>" --resume <id>` / `--continue`** now does **headless session continuity** — loads the
|
|
284
|
+
session, appends the prompt, runs, saves it back (a `--resume <id>` with no match is created with that id);
|
|
285
|
+
plain `hara -p` stays stateless. Useful for cron, scripts, and the gateway's per-chat threads.
|
|
286
|
+
- This is the chat layer of the multi-terminal plan; the ACP server (for a custom App / editors) is deferred
|
|
287
|
+
until that App exists. 249 tests.
|
|
288
|
+
|
|
289
|
+
## 0.79.0 — unreleased (app-level failover — retry an errored turn on a fallback model)
|
|
290
|
+
|
|
291
|
+
- **`fallbackModel`** (opt-in): when a turn ends in a *recoverable* provider error — overload (529/503),
|
|
292
|
+
rate-limit (429), timeout, transient 5xx, or context-overflow — hara retries it **once on the fallback
|
|
293
|
+
model** instead of dying (a different model may not be overloaded / may have a larger window). `auth` errors
|
|
294
|
+
and user interrupts are never auto-retried. The SDK's transient `maxRetries:4` still handles the first line;
|
|
295
|
+
this is the app layer for what's left. `fallbackBaseURL`/`fallbackApiKey` default to the primary's.
|
|
296
|
+
- Errors now also carry a short **actionable hint** by kind (auth → check key · overloaded → set fallbackModel
|
|
297
|
+
· context-overflow → /compact). Classification handles DashScope/GLM/Qwen **Chinese** error strings too. New
|
|
298
|
+
`src/agent/failover.ts` (pure classify + decide, fully tested); runAgent just executes the decision, guarded
|
|
299
|
+
to one retry. Tests (244 total). Completes the Sprint-2 spine (⑤ rewind + ⑥ failover).
|
|
300
|
+
|
|
301
|
+
## 0.78.0 — unreleased (file-state checkpoints — shadow-git "undo the agent's edits")
|
|
302
|
+
|
|
303
|
+
- **Durable file checkpoints** complete the rewind story (beyond the edit-only in-memory undo, which missed
|
|
304
|
+
`bash`-made changes). Before each turn, hara snapshots the whole working tree into a **shadow git repo** kept
|
|
305
|
+
OUTSIDE the project (`~/.hara/checkpoints/<hash>`, `GIT_DIR` there + `GIT_WORK_TREE` = project root) — so it
|
|
306
|
+
captures everything (incl. `bash`), **never touches your real `.git`/index**, and the model never sees it.
|
|
307
|
+
**`/checkpoint`** lists them; **`/checkpoint restore <n>`** reverts files to one.
|
|
308
|
+
- **Safe by construction**: restore snapshots the current state first (so it's undoable) and only reverts
|
|
309
|
+
changed/deleted files — it **never deletes files created since** the checkpoint. Heavy/derived dirs
|
|
310
|
+
(`node_modules`, `.git`, `dist`, …) are excluded; only the first snapshot is a full scan (git's index makes
|
|
311
|
+
the rest incremental). Default on; opt out with `fileCheckpoints:false` / `HARA_CHECKPOINTS=0`.
|
|
312
|
+
`src/checkpoints.ts` + tests (241 total).
|
|
313
|
+
|
|
314
|
+
## 0.77.0 — unreleased (/rewind — fork the conversation back to an earlier turn)
|
|
315
|
+
|
|
316
|
+
- New **`/rewind`** — `/rewind` lists recent user turns; `/rewind <n>` forks the conversation back to before
|
|
317
|
+
one, so when the agent goes down a wrong path you can snip back to a good turn and re-steer, instead of
|
|
318
|
+
`/clear` (lose everything) or living with a poisoned context (codex's backtrack). **Conversation only —
|
|
319
|
+
file edits are NOT reverted** (durable file-state checkpoints via shadow-git are the planned heavier
|
|
320
|
+
follow-up). Both UIs; pure in-memory + session store. `src/agent/rewind.ts` + tests (239 total).
|
|
321
|
+
|
|
322
|
+
## 0.76.0 — unreleased (/context — see what's filling the context window)
|
|
323
|
+
|
|
324
|
+
- New **`/context`** command: a token-spend breakdown of the conversation — which tool's output, assistant
|
|
325
|
+
text, and your messages are using the window (biggest first, with the share of the model's window) — so on a
|
|
326
|
+
long session you can see *why* you're near the limit, not just the `ctx%` number. Pairs with auto-compaction.
|
|
327
|
+
chars/4 estimate, zero-dep, both UIs. `src/agent/context-report.ts` + tests (237 total).
|
|
328
|
+
|
|
329
|
+
## 0.75.0 — unreleased (lazy subdirectory AGENTS.md / CLAUDE.md — monorepo-local conventions reach the model)
|
|
330
|
+
|
|
331
|
+
- When a tool touches a directory not seen yet this session, hara loads that directory's **`AGENTS.md` /
|
|
332
|
+
`CLAUDE.md`** (the local conventions for that package) and appends it to the tool result — so in a monorepo,
|
|
333
|
+
`packages/api/AGENTS.md` or `growth/CLAUDE.md` reaches the model exactly when work moves there, not just the
|
|
334
|
+
root doc loaded at startup. Each directory loads once per session; only dirs **under cwd** (startup already
|
|
335
|
+
covers cwd→root); paths are taken from a tool's `path` or path-like tokens in a `bash` command. Zero-dep,
|
|
336
|
+
additive (never removes context). `src/context/subdir-hints.ts` + tests (235 total).
|
|
337
|
+
|
|
338
|
+
## 0.74.1 — unreleased (bash output keeps head + tail, not just head)
|
|
339
|
+
|
|
340
|
+
- Long command output (build / test logs) is now truncated **keeping both the head and the tail** instead of
|
|
341
|
+
only the first 100k chars — so the model still sees the **end**, where the error/result usually is (plain
|
|
342
|
+
head-truncation cut exactly the part that matters). `read_file` truncation is unchanged. New `capHeadTail`
|
|
343
|
+
used on `bash` success + failure output; + test (233 total).
|
|
344
|
+
|
|
345
|
+
## 0.74.0 — unreleased (auto-compaction — summarize before the context overflows, like Claude Code)
|
|
346
|
+
|
|
347
|
+
- **Auto-compaction**: when a turn fills the model's context past ~85%, hara now **summarizes the conversation
|
|
348
|
+
and continues automatically** (a "✻ Auto-compacting conversation…" notice) instead of only warning — so a
|
|
349
|
+
long session no longer dead-ends at the context limit. Opt out with `autoCompact: false` (or
|
|
350
|
+
`HARA_AUTO_COMPACT=0`); below the threshold the ≥80% warning still shows. Works in the TUI **and** the classic
|
|
351
|
+
REPL.
|
|
352
|
+
- The summarize-and-replace logic is now a single shared `compactConversation` (manual `/compact` and auto both
|
|
353
|
+
use it — no drift); it keeps the working-memory notes that survive the wipe and resets the context gauge off
|
|
354
|
+
the (small) summary. New `src/agent/compact.ts` (`shouldAutoCompact` trigger) + test (232 total).
|
|
355
|
+
- The two distinct controls (unchanged, just clarified): **`/compact`** summarizes → replaces history to free
|
|
356
|
+
tokens while keeping the thread; **`/clear`** (= `/reset`) wipes the conversation for a fresh start.
|
|
357
|
+
|
|
358
|
+
## 0.73.0 — unreleased (Sprint 2: background shell jobs — run dev servers / watchers without blocking)
|
|
359
|
+
|
|
360
|
+
- **`bash {background: true}`** starts a long-lived command (dev server, `tsc --watch`, a long build) as a
|
|
361
|
+
background job and returns a job id immediately — the agent keeps working instead of blocking on a command
|
|
362
|
+
that never exits. New **`job` tool** (`list` / `tail` / `kill`) manages them; output is captured to a capped
|
|
363
|
+
tail buffer. Background jobs reuse `bash`'s exact **sandbox write-confinement** (shared `shellCommand`) and
|
|
364
|
+
pass the same permission gate when started; they're the agent's own children and are **terminated when hara
|
|
365
|
+
exits** (no orphaned dev servers). `src/exec/jobs.ts` + tests (231 total). First slice of the exec-subsystem
|
|
366
|
+
gap from the 4-expert analysis — persistent/interactive PTY + docker/ssh backends come next.
|
|
367
|
+
|
|
368
|
+
## 0.72.0 — unreleased (per-turn model routing: strong model for code, cheap/general for trivial turns)
|
|
369
|
+
|
|
370
|
+
- **Opt-in per-turn model routing** — the answer to "use a coding model for real work, a cheap/general model
|
|
371
|
+
for trivial chat" *without* splitting hara into two tools. Set `routeModel` (+ optional `routeBaseURL` /
|
|
372
|
+
`routeApiKey`, which default to the primary's) and each turn routes by its latest user message: trivial,
|
|
373
|
+
non-coding turns (short · single-line · no code / URL · no action keyword) go to `routeModel`; anything with
|
|
374
|
+
a coding/action signal stays on the primary `model`. **Conservative by design** — a coding tool errs toward
|
|
375
|
+
the strong model, so routing only fires on clear Q&A / chit-chat.
|
|
376
|
+
- Implemented as a transparent provider wrapper at the single main-chat entry (`withRouting`), so every
|
|
377
|
+
interactive / `-p` / TUI turn gets it while role / review / sub-agent providers stay untouched. The decision
|
|
378
|
+
reads the last `role:"user"` message (tool results are `role:"tool"`), so it's stable across a turn's tool
|
|
379
|
+
rounds. New `src/agent/route.ts` + tests. Env: `HARA_ROUTE_MODEL` / `HARA_ROUTE_BASE_URL` / `HARA_ROUTE_API_KEY`.
|
|
380
|
+
|
|
381
|
+
## 0.71.0 — unreleased (Sprint 1: governance + safety — command permissions · untrusted-content wrapping · structured compaction)
|
|
382
|
+
|
|
383
|
+
Three zero-dep items distilled from a 4-expert (codex/cc-haha/hermes/openclaw) C-end gap analysis; all reinforce hara's governance moat.
|
|
384
|
+
|
|
385
|
+
- **Command-level permission rules for `bash`** (`~/.hara/permissions.json` + project `.hara/permissions.json`):
|
|
386
|
+
an `allow` / `deny` + read-only-autorun policy that **composes with** approval modes. A **deny** rule blocks a
|
|
387
|
+
command even in `--full-auto`; an **allow** rule (or a recognized **read-only** command — `ls`/`grep`/`git
|
|
388
|
+
status`…) auto-runs even in `suggest` mode — ending the "confirm every command vs. unguarded full-auto" false
|
|
389
|
+
choice. Commands are canonicalized (unwrap `bash -lc`, strip `NODE_ENV=…`/`timeout` wrappers) so approving
|
|
390
|
+
`npm test` once sticks across phrasings; a compound command (`&&`/`||`/`;`/`|`) takes its **strictest** part's
|
|
391
|
+
decision; anything unparseable (`$()`/backticks/unbalanced quotes) fails **closed** to a prompt. New
|
|
392
|
+
`hara permissions` (list · `--init [--project]`). `src/security/permissions.ts` + tests.
|
|
393
|
+
- **Untrusted-content wrapping** for `web_fetch` / `web_search`: external page/search text is wrapped in a
|
|
394
|
+
"treat as DATA, not instructions" notice with a **random per-call boundary id**, and homoglyph / zero-width
|
|
395
|
+
tricks (fullwidth/CJK/math angle brackets, ZWSP/BOM/soft-hyphen) are defanged so a hostile page can't forge
|
|
396
|
+
the boundary or smuggle hidden instructions — closing the realistic indirect-prompt-injection vector for an
|
|
397
|
+
agent that holds `bash`. `src/security/external-content.ts` + tests.
|
|
398
|
+
- **Structured `/compact` template**: replaces the one-line summary with a 6-section brief (goal · key decisions
|
|
399
|
+
· files & code · errors & fixes · current state · next step) that **quotes the user's most recent request
|
|
400
|
+
verbatim** and drafts in an `<analysis>` scratchpad first — so resuming after a compaction doesn't drift or
|
|
401
|
+
drop the error→fix history.
|
|
402
|
+
|
|
8
403
|
## 0.70.0 — unreleased (B-end: devices pull their governed org-role bundle)
|
|
9
404
|
|
|
10
405
|
- An enrolled device now **syncs its digital-employee roles from hara-control** — `GET {gateway}/v1/roles`
|
package/README.md
CHANGED
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
**A coding agent CLI that runs like an engineering org.**
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
> Think "Claude Code, but it operates as a configurable, governed *organization* of role-agents" —
|
|
6
8
|
> with routing boundaries, a dispatcher, a single source-of-truth data layer, human-in-the-loop
|
|
7
9
|
> approvals, and cron autonomy.
|
|
8
10
|
|
|
9
|
-
🚧 **v0.
|
|
11
|
+
🚧 **v0.89** · TypeScript · local-first · Apache-2.0
|
|
10
12
|
|
|
11
13
|
**Highlights**
|
|
12
14
|
- **An org, not just an agent** — `hara org "<task>"` routes work to the role that *owns* it; `hara plan "<task>"` decomposes a task into a verified DAG of atoms (frame → atomize → sequence → execute → **verify gate**), and `hara plan --parallel` runs independent atoms concurrently.
|
|
15
|
+
- **Drive it from chat** — `hara gateway` runs your local hara from **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk · WeCom · Signal** (10 platforms), with **two-way images** (send a photo → it sees it; ask for a file → it sends one), per-chat resumable sessions, and project roaming. Connects out — no public webhook. See **[docs/gateway.md](docs/gateway.md)**.
|
|
13
16
|
- **Real terminal UX** — an **ink TUI**: bottom-pinned input box, **plan mode** (read-only → propose a plan → approve → execute), selectable approvals with "don't ask again", windowed reasoning, **paste images** (Ctrl+V) for vision models, light/dark theme.
|
|
14
17
|
- **Persistent memory + self-evolution** — `memory_*` tools over global/project `MEMORY.md`; the agent recalls before acting, **proactively saves** durable facts, and grows its own playbooks (a lexical guard screens what it writes). Inspect/consolidate it with **`hara memory show`** and **`hara memory distill`** (promote recent daily logs → durable memory). Lexical-first by design — semantic search is opt-in, never required.
|
|
15
18
|
- **Multi-provider, all streamed** — Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) with live Markdown + visible reasoning.
|
|
19
|
+
- **Delegate to other agents** — the **`external_agent`** tool hands a self-contained task to **Claude Code** or **Codex** running headless, and returns the result — so you pick the best engine per task. Gated by approval, trust-tiered, and never exposed to read-only sub-agents.
|
|
16
20
|
- **Solid coding core** — `edit_file` / `apply_patch` (atomic multi-file) with colored diffs · `grep`/`glob`/`ls`/`codebase_search` (lexical + optional semantic search over the repo) /`web_fetch` · fuzzy `@file` · `/undo` · `/compact` · **Esc-to-interrupt** · parallel sub-agents · MCP client · macOS sandbox.
|
|
17
21
|
|
|
18
22
|
Track it: https://github.com/hara-cli/hara · https://hara.run
|
|
@@ -240,10 +244,11 @@ turn, or **`/undo`** to revert the last edit. In-session **`/diff`**, **`/review
|
|
|
240
244
|
- **Project context**: auto-loads `AGENTS.md` (the cross-tool standard) walking up to the repo root; `hara init` writes one by analyzing the repo.
|
|
241
245
|
- **`@file` mentions**: attach file contents to a message (`@path`); Tab-completes with a **fuzzy** matcher over the project (subdirs, git-tracked + untracked) — `@idx` → `src/index.ts`. `@<dir>` loads a directory listing, `@src/`+Tab drills into a folder, and mistyped tool/file paths get a "did you mean" suggestion.
|
|
242
246
|
- **Multi-provider**: Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) — **all streamed live**.
|
|
247
|
+
- **Chat gateway**: drive your local hara from a chat app — **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk**. The daemon connects out (no public webhook), with per-chat sessions, project roaming (`/cd`), and **two-way images** (send a photo → it sees it; ask for a file → it sends one). Setup per platform: **[docs/gateway.md](docs/gateway.md)**.
|
|
243
248
|
|
|
244
249
|
### Roadmap
|
|
245
250
|
|
|
246
|
-
**Shipped:** ink TUI · plan mode · persistent memory + self-evolution · atomization planner · parallel plan atoms · **multi-role review chains** · parallel sub-agents · MCP client *and* server · **scheduled tasks (`hara cron`)** · **single-binary distribution** · **Docker image** · `/compact` context management.
|
|
251
|
+
**Shipped:** ink TUI · plan mode · persistent memory + self-evolution · atomization planner · parallel plan atoms · **multi-role review chains** · parallel sub-agents · MCP client *and* server · **scheduled tasks (`hara cron`)** · **chat gateway (8 platforms, two-way images)** · **single-binary distribution** · **Docker image** · `/compact` context management.
|
|
247
252
|
**Next:** SSOT data authority · an enterprise control-plane (fleet + central token management).
|
|
248
253
|
|
|
249
254
|
## Security
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Auto-compaction trigger — the small, testable decision behind "compact the conversation before it overflows"
|
|
2
|
+
// (à la Claude Code's auto-compact). The actual summarize-and-replace I/O lives in index.ts (compactConversation),
|
|
3
|
+
// which reuses the manual /compact path; this just decides *when* to fire.
|
|
4
|
+
/** Auto-compact once the last turn used ≥ this % of the model's context window. */
|
|
5
|
+
export const AUTO_COMPACT_PCT = 85;
|
|
6
|
+
/** Whether to auto-compact now: enabled, the history is substantial enough to be worth summarizing, and the
|
|
7
|
+
* last turn filled the context past the threshold (so the NEXT turn would risk overflow). */
|
|
8
|
+
export function shouldAutoCompact(ctxPct, historyLen, autoCompact, threshold = AUTO_COMPACT_PCT) {
|
|
9
|
+
return autoCompact && historyLen >= 4 && ctxPct >= threshold;
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { contextWindow } from "../statusbar.js";
|
|
2
|
+
const est = (s) => Math.ceil((s?.length ?? 0) / 4);
|
|
3
|
+
/** Estimate token spend by category (your messages / assistant / each tool's output) across the history. */
|
|
4
|
+
export function analyzeContext(history) {
|
|
5
|
+
const by = new Map();
|
|
6
|
+
const add = (k, n) => {
|
|
7
|
+
by.set(k, (by.get(k) ?? 0) + n);
|
|
8
|
+
};
|
|
9
|
+
for (const m of history) {
|
|
10
|
+
if (m.role === "user")
|
|
11
|
+
add("your messages", est(m.content));
|
|
12
|
+
else if (m.role === "assistant")
|
|
13
|
+
add("assistant", est(m.text));
|
|
14
|
+
else if (m.role === "tool")
|
|
15
|
+
for (const r of m.results)
|
|
16
|
+
add(`tool: ${r.name}`, est(r.content));
|
|
17
|
+
}
|
|
18
|
+
const total = [...by.values()].reduce((a, b) => a + b, 0);
|
|
19
|
+
const rows = [...by.entries()]
|
|
20
|
+
.map(([label, tokens]) => ({ label, tokens, pct: Math.round((tokens / (total || 1)) * 100) }))
|
|
21
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
22
|
+
return { total, rows };
|
|
23
|
+
}
|
|
24
|
+
/** Human-readable breakdown, biggest first, with the share of the model's window used + a trim hint. */
|
|
25
|
+
export function formatContextReport(history, model) {
|
|
26
|
+
const { total, rows } = analyzeContext(history);
|
|
27
|
+
if (!rows.length)
|
|
28
|
+
return "Context is empty.";
|
|
29
|
+
const pctWin = Math.min(99, Math.round((total / contextWindow(model)) * 100));
|
|
30
|
+
const lines = rows.slice(0, 8).map((r) => ` ${String(r.pct).padStart(3)}% ${r.label} (~${r.tokens.toLocaleString()} tok)`);
|
|
31
|
+
const tip = pctWin >= 80 ? "\n → near the limit; /compact to summarize or /clear to reset." : "";
|
|
32
|
+
return `Context ~${total.toLocaleString()} tok (~${pctWin}% of ${model}'s window):\n${lines.join("\n")}${tip}`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// App-level failover — what to do when a provider turn ENDS in an error (the SDK already retried transient
|
|
2
|
+
// 429/5xx via maxRetries; this handles what's left). Two recoveries: a context-overflow → compact + retry,
|
|
3
|
+
// and a persistent/overloaded error → retry once on a configured FALLBACK model. The decision is pure +
|
|
4
|
+
// tested here; runAgent just executes it (and guards each recovery to once, so no retry loops).
|
|
5
|
+
/** Classify a provider error from its message (+ HTTP status if known). Message patterns include the
|
|
6
|
+
* Chinese strings DashScope/GLM/Qwen return, since hara targets those endpoints. */
|
|
7
|
+
export function classifyError(msg, status) {
|
|
8
|
+
const m = (msg || "").toLowerCase();
|
|
9
|
+
if (m === "interrupted")
|
|
10
|
+
return "interrupted";
|
|
11
|
+
if (status === 401 || status === 403 || /unauthor|invalid api key|invalid.*key|forbidden|permission denied|无效|鉴权/.test(m))
|
|
12
|
+
return "auth";
|
|
13
|
+
if (status === 429 || /rate.?limit|too many requests|\b429\b|请求过于频繁|限流/.test(m))
|
|
14
|
+
return "rate_limit";
|
|
15
|
+
if (status === 529 || status === 503 || /overload|capacity|service unavailable|temporarily unavailable|\b503\b|\b529\b|繁忙|过载/.test(m))
|
|
16
|
+
return "overloaded";
|
|
17
|
+
if (/context length|context window|maximum context|maximum.*token|too long|reduce the length|超过最大长度|上下文长度|输入过长/.test(m))
|
|
18
|
+
return "context_overflow";
|
|
19
|
+
if (/timeout|timed out|etimedout|econnreset|socket hang up|network/.test(m))
|
|
20
|
+
return "timeout";
|
|
21
|
+
if (typeof status === "number" && status >= 500)
|
|
22
|
+
return "transient";
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
// Error kinds where retrying on a DIFFERENT model can plausibly help (it may not be overloaded / may differ).
|
|
26
|
+
const FALLBACKABLE = new Set(["overloaded", "rate_limit", "timeout", "transient", "context_overflow", "unknown"]);
|
|
27
|
+
/** Decide the recovery for an errored turn: retry once on the fallback model, or fail. Never auto-recovers
|
|
28
|
+
* `auth` (a config problem) or `interrupted` (the user). Context-overflow IS fallback-able — a
|
|
29
|
+
* larger-context fallback model may fit (and preemptive auto-compaction already prevents most overflows). */
|
|
30
|
+
export function failoverAction(kind, s) {
|
|
31
|
+
if (kind === "interrupted" || kind === "auth")
|
|
32
|
+
return "fail";
|
|
33
|
+
if (s.hasFallback && !s.triedFallback && FALLBACKABLE.has(kind))
|
|
34
|
+
return "fallback";
|
|
35
|
+
return "fail";
|
|
36
|
+
}
|
|
37
|
+
/** A short actionable hint appended to the surfaced error message. */
|
|
38
|
+
export function errorHint(kind) {
|
|
39
|
+
switch (kind) {
|
|
40
|
+
case "auth":
|
|
41
|
+
return " — check your API key / auth (try `hara setup`)";
|
|
42
|
+
case "rate_limit":
|
|
43
|
+
return " — rate-limited; wait a moment, or set `fallbackModel` to auto-switch";
|
|
44
|
+
case "overloaded":
|
|
45
|
+
return " — provider overloaded; set `fallbackModel` to auto-switch on errors";
|
|
46
|
+
case "context_overflow":
|
|
47
|
+
return " — context too long; `/compact` (or enable `autoCompact`)";
|
|
48
|
+
case "timeout":
|
|
49
|
+
return " — network timeout; check connectivity";
|
|
50
|
+
default:
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|