@nanhara/hara 0.122.3 → 0.122.5

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 (61) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +15 -2
  3. package/SECURITY.md +4 -0
  4. package/dist/agent/limits.js +51 -0
  5. package/dist/agent/loop.js +367 -112
  6. package/dist/agent/repeat-guard.js +49 -12
  7. package/dist/checkpoints.js +9 -0
  8. package/dist/cli.js +2 -1
  9. package/dist/config.js +10 -2
  10. package/dist/context/agents-md.js +21 -2
  11. package/dist/context/mentions.js +84 -1
  12. package/dist/context/workspace-scope.js +49 -0
  13. package/dist/cron/deliver.js +61 -38
  14. package/dist/cron/runner.js +423 -65
  15. package/dist/cron/schedule.js +23 -7
  16. package/dist/cron/store.js +709 -43
  17. package/dist/fs-walk.js +279 -7
  18. package/dist/fs-write.js +9 -0
  19. package/dist/gateway/feishu.js +351 -64
  20. package/dist/gateway/flows-pending.js +28 -14
  21. package/dist/gateway/flows.js +306 -31
  22. package/dist/gateway/outbound-files.js +20 -6
  23. package/dist/gateway/runtime-state.js +1485 -0
  24. package/dist/gateway/serve.js +550 -242
  25. package/dist/gateway/telegram.js +279 -29
  26. package/dist/gateway/tts.js +182 -38
  27. package/dist/hooks.js +22 -22
  28. package/dist/index.js +466 -158
  29. package/dist/memory/store.js +8 -5
  30. package/dist/org/planner.js +11 -6
  31. package/dist/process-identity.js +52 -0
  32. package/dist/providers/bounded-turn.js +42 -0
  33. package/dist/recall.js +69 -1
  34. package/dist/runtime.js +24 -0
  35. package/dist/sandbox.js +54 -4
  36. package/dist/search/embed.js +13 -2
  37. package/dist/search/hybrid.js +6 -3
  38. package/dist/search/semindex.js +87 -5
  39. package/dist/security/guardian.js +3 -15
  40. package/dist/security/permissions.js +11 -0
  41. package/dist/serve/server.js +70 -42
  42. package/dist/serve/sessions.js +4 -3
  43. package/dist/tools/agent.js +1 -1
  44. package/dist/tools/ask_user.js +5 -1
  45. package/dist/tools/builtin.js +5 -2
  46. package/dist/tools/codebase.js +67 -18
  47. package/dist/tools/computer.js +149 -68
  48. package/dist/tools/cron.js +72 -18
  49. package/dist/tools/edit.js +3 -2
  50. package/dist/tools/external_agent.js +66 -12
  51. package/dist/tools/memory.js +25 -7
  52. package/dist/tools/patch.js +11 -2
  53. package/dist/tools/registry.js +16 -1
  54. package/dist/tools/search.js +68 -9
  55. package/dist/tools/send.js +1 -1
  56. package/dist/tools/skill.js +1 -1
  57. package/dist/tools/web.js +43 -13
  58. package/dist/tui/App.js +93 -25
  59. package/dist/vision.js +5 -6
  60. package/package.json +3 -3
  61. package/runtime-bootstrap.cjs +22 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,123 @@ 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.122.5 — 2026-07-15 — standalone ambient-config security boundary
9
+
10
+ - **Standalone binaries no longer trust the directory they are launched from.** Bun's runtime `.env` and
11
+ `bunfig.toml` autoloading is now explicitly disabled, together with the already-default-off project
12
+ `package.json` and `tsconfig.json` loaders. A repository can no longer run a `bunfig` preload before Hara's
13
+ permission checks or inject project `.env` values into the standalone/desktop-sidecar process. Users of the
14
+ `0.122.4` standalone binary should upgrade promptly; the npm/Node runtime was not affected by Bun's compiled
15
+ executable autoload path.
16
+ - **The release gate exercises the real boundary.** Native standalone CI and tag publication launch the actual
17
+ binary from a hostile fixture directory containing both a marker-writing preload and a model-changing test
18
+ `.env`; the build fails if either executes or loads. A source-policy regression test also requires all four
19
+ ambient config loaders to remain explicitly disabled and the smoke to stay wired into CI/release.
20
+ - **x64 standalone assets use Bun's baseline CPU target.** Intel macOS, Linux, and Windows sidecar callers no
21
+ longer inherit the modern/AVX assumption from an unqualified x64 target, improving compatibility with older
22
+ Intel machines and Rosetta validation without changing ARM64 builds.
23
+ - **Optional native search stays optional at build time.** TypeScript now has an explicit ambient contract for
24
+ `@zvec/zvec`, so npm can omit the unsupported macOS Intel binding without breaking `npm ci`; runtime loading
25
+ still falls back to the durable JSON search path when the native module is unavailable.
26
+
27
+ ## 0.122.4 — 2026-07-14 — bounded agent lifecycle and chat delivery hardening
28
+
29
+ - **An active agent can no longer renew itself forever.** Every CLI, headless, org, and Desktop/serve turn
30
+ now has a 30-minute total wall-clock deadline and a 64 model/tool-round ceiling, independent of streaming
31
+ activity and per-tool timeouts. A visible warning fires after five minutes or at 75% of the round budget;
32
+ reaching either hard boundary returns an actionable `halted` outcome to the terminal, Desktop protocol, or
33
+ gateway reply. `runTimeoutMs` (friendly `30m`/`90s` values; hard max 2h) and `maxAgentRounds` (hard max 256)
34
+ can raise/lower the bounds but cannot disable them.
35
+ - **Repeated failures now trip a real circuit-breaker.** The third identical failing tool call closes its
36
+ protocol round and stops the run instead of merely asking the model not to repeat itself. A final round cap
37
+ still catches alternating or superficially changing loops.
38
+ - **Cancellation reaches owned work.** Tools receive the combined user/deadline signal; foreground shell and
39
+ external-agent process trees terminate on it, and read-only sub-agents inherit the parent's cancellation
40
+ with tighter 8-minute/24-round limits. Providers/tools that ignore `AbortSignal` are raced at the core, so
41
+ Hara itself still settles on time and never executes a late next tool call. File, memory, skill, and gateway
42
+ outbox writes re-check cancellation immediately before commit, while session ownership remains held until an
43
+ uncooperative tool physically settles, preventing a timed-out write from racing the next turn.
44
+ - **One-shot model helpers are hard-bounded too.** Setup checks, role routing, plan decomposition and
45
+ verification, commit-message generation, session naming, conversation compaction, vision analysis, and the
46
+ security guardian now combine cooperative abort with a hard Promise deadline. A custom provider that ignores
47
+ cancellation can no longer strand the CLI before or after the main agent loop.
48
+ - **Cron can no longer hold its global tick lock forever.** Scheduled jobs keep their 30-minute per-job
49
+ deadline and the whole tick now has a non-renewable 60-minute wall-clock watchdog. Timeout cancellation
50
+ force-stops the owned process tree, always releases the tick lock, records `running`/`timed_out`, duration and
51
+ last error durably, counts timeouts toward `alertAfter`, and lets later due jobs run after an individual job
52
+ timeout (a total-tick timeout stops the remainder). `HARA_CRON_JOB_TIMEOUT_MS` and
53
+ `HARA_CRON_TICK_TIMEOUT_MS` tune the millisecond bounds, with hard maxima of 24 hours and 5 hours; a scheduled
54
+ job is additionally capped by its tick. `hara cron list` now makes active and timed-out work explicit.
55
+ - **Cron timeout notifications survive transport outages and restarts.** Each outcome/threshold alarm is
56
+ committed with terminal state before delivery, keeps one stable idempotency key across bounded per-tick
57
+ retries, and is removed (with alert cooldown recorded) only after confirmed transport success. Each job's
58
+ queue is capped at 64: launches reserve room for outcome + alert, disable before overflow, and recovery may
59
+ compact only an old outcome while marking that loss explicitly; jobs.json also has schema/count/byte limits.
60
+ Configure the failure threshold with `--alert-after 1..1000` or the cron tool's `alertAfter` field.
61
+ - **Cron recovery no longer mistakes PID reuse for a live owner forever.** The tick and synchronous jobs-store
62
+ locks use Linux/macOS process-birth identity; store commits additionally use a conservative renewable lease,
63
+ snapshot CAS, and a token fence.
64
+ Unknown identity formats and live legacy identity-less commit guards fail closed instead of being age-stolen.
65
+ A `running` record beyond the 24-hour hard job maximum plus grace is marked interrupted and disabled even if
66
+ its PID is alive. Recovery never kills or replays the possibly orphaned task; it persists the failure
67
+ notification for operator verification. Manual runs also refuse every unresolved `running` marker; if its
68
+ parent is dead, Hara records an interrupted/disabled state and requires operator inspection before retrying.
69
+ - **Creation-minute cron jobs now use an explicit one-shot due marker.** A job added just after that minute's
70
+ OS tick runs once on the next tick, while disabling it clears the marker so a later enable cannot replay a
71
+ days- or months-old occurrence. Absolute one-shot schedules already in the past are rejected instead of being
72
+ displayed forever as a misleading next run.
73
+ - **`hara resume <id>` now re-enters the correct runtime without blocking terminal input.** The launcher uses
74
+ the same runtime-aware self command as cron/gateway and waits asynchronously with inherited stdio: npm/Node
75
+ keeps its script entry, while a Bun-compiled standalone re-executes only the binary instead of treating its
76
+ virtual/user `argv[1]` as JavaScript. This removes the standalone resume hang/high-CPU/dead-input failure.
77
+ - **The home directory is no longer an implicit repository.** Canonical/real-path checks suppress first-run
78
+ AGENTS generation and reject `hara init`, repo indexing, codebase search, and recursive grep/glob/inventory at
79
+ the Home root (including symlink aliases). Hara shows a `cd /path/to/project` hint; non-recursive `ls`, explicit
80
+ file reads, and explicitly selected child directories remain usable.
81
+ - **Recursive project discovery is streaming and interruptible.** Glob/grep fallback, codebase search, semantic
82
+ indexing, recall, Desktop file lookup, did-you-mean, and `@dir` expansion share file, directory, Dirent, and
83
+ wall-clock bounds from the start of discovery. Cached empty-directory forests and a single extremely wide
84
+ directory can no longer block the event loop past the agent deadline; truncated indexes are never published.
85
+ - **Windows shell and portable-home discovery are deterministic.** Hara probes the conventional Git Bash
86
+ installation paths before falling back to `cmd.exe`, understands MSYS drive paths and UNC paths, and honors an
87
+ explicit `HOME` ahead of `USERPROFILE` so portable/Git-Bash sessions do not silently load another profile.
88
+
89
+ - **Long chat replies now split on natural line/word boundaries without tearing Unicode.** Feishu and
90
+ Telegram bubbles preserve the exact reply while keeping emoji ZWJ sequences, flags, combining marks, CRLF,
91
+ and surrogate pairs intact whenever the platform limit permits.
92
+ - **Only one Hara gateway process may own a configured bot connection at a time.** A credential-scoped,
93
+ crash-safe private lease refuses a second live Feishu/Telegram/etc. connection and reclaims only a
94
+ proven-dead owner, preventing multiple WebSockets from replying to the same event.
95
+ - **Stable inbound message ids are deduplicated across restarts.** Successfully handled Feishu and Telegram
96
+ ids are persisted for one hour in a byte- and count-bounded owner-only store; failures remain retryable,
97
+ concurrent duplicates and startup replays older than 30 seconds are ignored, and different bot credentials
98
+ use isolated stores. State contains ids/timestamps and a one-way connection namespace only—never message
99
+ text, user data, or credentials.
100
+ - **Outbound chat transport is bounded and ordered across adapter instances.** Telegram and Feishu text/file
101
+ requests use real `AbortSignal` cancellation plus a hard 30s/120s ceiling; a credential-scoped, process-wide
102
+ per-chat lane prevents long multipart replies from interleaving with one-shot or flow delivery. A failed or
103
+ timed-out call settles for its caller without freezing shutdown or the inbound retry claim; an ambiguous
104
+ underlying transfer remains quarantined in that live process's lane until it actually settles.
105
+ - **Flow side effects resume from durable receipts.** Each reply, notification target, and pending approval has
106
+ a stable, credential-scoped receipt. Redelivery skips locally completed receipts, pending actions reuse their
107
+ stable opaque id, and idempotency keys are forwarded where supported. A crash after remote acceptance but
108
+ before the local receipt commit can still cause at-least-once delivery on transports without server-side
109
+ deduplication. No-tool model decisions, bounded failure attempts, and partial target/chunk progress also
110
+ survive restarts, so a retry cannot mix a newly generated answer into an older partially delivered flow.
111
+ - **Stable-id gateway events do not re-execute coding or stateful commands on delivery retry.** A private
112
+ started marker is durable before coding, tmux injection, `/new`, `/voice`, `/say`, or `/send`; completed
113
+ reply/file/default-voice bytes are durable before transport. Default voice retries therefore reuse the same
114
+ synthesized bytes instead of rerunning coding or TTS. Speech now has a 60-second default/120-second hard
115
+ deadline, propagates shutdown cancellation to remote requests, and terminates the full local
116
+ `say`/custom-command process tree.
117
+ - **Interrupted gateway outcome markers have an explicit fail-closed recovery path.** A credential-scoped,
118
+ single-instance `hara gateway --recover-outcome ... --confirm-recovery terminalize:<id>` command releases one
119
+ abandoned active slot without rerunning its side effect; a separate exact confirmation can later delete only
120
+ an already-terminal tombstone. Running or unacknowledged completed records cannot be silently erased.
121
+ - Feishu outbound REST now uses native abortable requests with an in-memory, early-refresh tenant-token cache;
122
+ the official SDK remains responsible for the inbound WebSocket/resource surface. Hara resolves mentions
123
+ from event metadata and drains tracked callbacks before releasing the single-instance lease on shutdown.
124
+
8
125
  ## 0.122.3 — 2026-07-14 — standalone runtime recovery
9
126
 
10
127
  - **Bun-compiled standalone binaries no longer assume `SharedArrayBuffer` exists at module startup.**
package/README.md CHANGED
@@ -175,6 +175,11 @@ hara --profile work # use a named profile from ~/.hara/config.json
175
175
  hara -m glm-5 # pick a model
176
176
  ```
177
177
 
178
+ Run Hara from a project directory. When the current directory resolves to your Home root, Hara does not treat
179
+ the whole Home tree as a repository: project init/index and default recursive grep/glob/codebase inventory are
180
+ disabled with a `cd /path/to/project` hint. Non-recursive `ls`, explicitly named files, and explicitly selected
181
+ child directories still work.
182
+
178
183
  For automation, `--schema` accepts inline JSON Schema or a schema file. The model must return through the
179
184
  validated `structured_output` tool; on success stdout contains only the JSON value, while diagnostics go to
180
185
  stderr and missing/invalid output exits non-zero. `--role reviewer` resolves locally, `--role global:reviewer`
@@ -234,16 +239,24 @@ only changed files re-embed (a full repo rebuild that takes ~a minute re-runs in
234
239
  `hara config set computerUse read|click|full` and allowlist apps with `hara config set computerApps "App, …"`. Guarded
235
240
  by the tier, the frontmost-app allowlist, a dangerous-key blocklist, and a once-per-session grant. Screenshots are read via your
236
241
  vision model into **actionable** output — interactive elements + positions (pass `focus` to target what you're after) — so even a text-only main model can click.
237
- **Sessions**: conversations are saved automatically — `-c` / `--resume <id>` to continue, `hara sessions` to list, `hara export [id] [--out file]` to render one as a Markdown transcript.
242
+ **Sessions**: conversations are saved automatically — `-c` / `--resume <id>` or `hara resume <id>` to continue, `hara sessions` to list, `hara export [id] [--out file]` to render one as a Markdown transcript. The `hara resume` launcher preserves terminal input in both npm/Node and standalone-binary installs.
238
243
  **MCP**: add an `mcpServers` map to global config (a reviewed project config additionally needs `HARA_TRUST_PROJECT_CONFIG=1` at launch); their tools appear to the agent as `mcp__<server>__<tool>`. Configured MCP servers, like `external_agent`, are trusted host extensions outside Hara's protected-file boundary. Every interactive tool call requires confirmation (even in `full-auto`), and non-interactive runs disable them by default; reviewed automation can explicitly opt in before launch with `HARA_ALLOW_TRUSTED_EXTENSIONS=1`. hara can also **be** an MCP server — `hara mcp` exposes its read/search tools (esp. **`codebase_search`**) over stdio so other clients (Claude Desktop, Cursor, another hara) can use them; read-only by default (`HARA_MCP_TOOLS` to override).
239
244
  **Vim mode**: `hara config set vimMode true` makes the prompt modal — Esc → normal, `i/a/A/I` insert, `h l 0 $ w b e` motions, `x D C dd cw p` edits. Off by default.
240
- **Scheduled tasks**: `hara cron add "0 9 * * 1-5" "<task>"` (or `"every 30m"`, `"in 2h"`) runs a task on a schedule — each run is a fresh hara session. `hara cron install` wires a per-minute tick into launchd/crontab (no daemon); `--org` routes through the role org. Manage with `hara cron list/run/enable/disable/remove/logs`.
245
+ **Scheduled tasks**: `hara cron add "0 9 * * 1-5" "<task>"` (or `"every 30m"`, `"in 2h"`) runs a task on a schedule — each run is a fresh hara session. `hara cron install` wires a per-minute tick into launchd/crontab (no daemon); `--org` routes through the role org. Manage with `hara cron list/run/enable/disable/remove/logs`. Every job has a 30-minute deadline and the whole sequential tick has a non-renewable 60-minute watchdog: a job timeout kills its process tree, records `timed out` + duration/error, then continues with the next due job; a tick timeout stops the remainder and releases the global lock. Add `--deliver feishu:<chatId>` (or Telegram/webhook) for outcomes and `--alert-after N` for the consecutive-failure 🚨 threshold (default 3). Delivery intent is durable before transport, uses a stable idempotency key, and retries with bounded backoff on later ticks until confirmed. A failed channel cannot grow `jobs.json` forever: each job keeps at most 64 pending effects, reserves outcome/alert room before launch, and disables itself with a visible backlog error when full; restore delivery, let the queue drain, then re-enable it. Tune milliseconds with `HARA_CRON_JOB_TIMEOUT_MS` (hard max 24h) and `HARA_CRON_TICK_TIMEOUT_MS` (hard max 5h); scheduled jobs are also capped by the tick. After upgrading from a version whose tick is already stuck, terminate that specific legacy `hara cron tick` process tree once (or reboot); the next scheduler minute marks over-age state interrupted/disabled and recovers the lock without replaying a possibly orphaned task.
241
246
  **Work coordination**: `todo_write` is the agent's short, session-scoped checklist; it persists with that
242
247
  session and is isolated between simultaneous sub-agents and serve sessions. `task` is the durable project pool
243
248
  for work that outlives a conversation: add/update/list/remove items with `pending|in_progress|done`, an optional
244
249
  owner, and `blockedBy` dependencies. The private, atomic store is shared by concurrent hara processes for the
245
250
  same project and rejects missing/self/cyclic dependencies.
246
251
  **Notifications**: `hara config set notify bell` (terminal bell) or `notify system` (OS notification) pings you when a turn finishes — handy for long runs you've stepped away from. Gated on elapsed time so quick turns stay quiet; off by default.
252
+ **Run limits and loop alarms**: every agent turn has a non-renewable 30-minute total deadline and a 64-round
253
+ model/tool cap. Hara warns after five minutes or at 75% of the round budget, stops the third identical failing
254
+ tool call, and surfaces the final reason in CLI, Desktop, or gateway output. Tune intentional long work with
255
+ `hara config set runTimeoutMs 45m` (1s..2h) and `hara config set maxAgentRounds 96` (1..256), or
256
+ `HARA_RUN_TIMEOUT_MS` / `HARA_MAX_AGENT_ROUNDS`; neither boundary can be disabled. Sub-agents are capped at
257
+ 8 minutes/24 rounds and inherit the parent's cancellation. Auxiliary model work (planning, verification,
258
+ compaction, naming, commit messages, the guardian, and vision) has its own short hard deadline as well, so a
259
+ provider that ignores cancellation cannot strand the CLI outside the main loop.
247
260
  **Hooks**: run your own shell commands around tool calls via a `"hooks"` map in global config; hooks from a reviewed project config require the launch-time `HARA_TRUST_PROJECT_CONFIG=1` opt-in. A **`PreToolUse`** hook can **veto** a call (non-zero exit blocks it; its output becomes the reason the model sees) — gate `bash`, forbid edits outside a path, require a clean tree. A **`PostToolUse`** hook observes (format/lint a file the agent just wrote, log, notify). Each has a `matcher` (regex/literal on the tool name, `*` = all) and gets `{tool, payload}` on stdin + `HARA_TOOL_NAME` in env. Plugins can contribute hooks too.
248
261
  Reviewer/read-only/plan runs and parallel read-only sub-agents suppress both hook phases: PreToolUse and
249
262
  PostToolUse commands are arbitrary shell, so either could otherwise bypass their read-only contract indirectly.
package/SECURITY.md CHANGED
@@ -37,6 +37,10 @@ local user (who already has your shell).
37
37
  execution cannot approve those built-in paths past the check. `HARA_ALLOW_SENSITIVE_FILES=1` is an explicit
38
38
  launch-time exposure switch for one process: it removes the built-in denies and that process's shell
39
39
  protected-read preflight/Seatbelt mask.
40
+ - **Standalone launch configuration is closed.** Released Bun-compiled binaries explicitly disable runtime
41
+ loading of the working directory's `.env`, `bunfig.toml`, `package.json`, and `tsconfig.json`. This prevents
42
+ a project preload or ambient environment file from running/injecting values before Hara's own boundaries.
43
+ Native CI executes each release binary from a hostile fixture directory and fails if either path activates.
40
44
  - **Shell guardrails differ by platform.** With the protected-file policy enabled, shell admission statically
41
45
  rejects literal protected paths and environment-dump commands on every OS. On macOS, Hara additionally
42
46
  applies a Seatbelt read mask to existing
@@ -0,0 +1,51 @@
1
+ /** Hard lifecycle bounds for one agent run. These are deliberately independent of the provider's
2
+ * stream-idle watchdog and each tool's own timeout: activity must not be able to renew a run forever. */
3
+ export const DEFAULT_AGENT_RUN_TIMEOUT_MS = 30 * 60_000;
4
+ export const MAX_AGENT_RUN_TIMEOUT_MS = 2 * 60 * 60_000;
5
+ export const MIN_AGENT_RUN_TIMEOUT_MS = 1_000;
6
+ export const DEFAULT_AGENT_MAX_ROUNDS = 64;
7
+ export const MAX_AGENT_MAX_ROUNDS = 256;
8
+ export function parseAgentRunTimeoutMs(value) {
9
+ if (typeof value === "number")
10
+ return Number.isFinite(value) ? value : undefined;
11
+ if (typeof value !== "string")
12
+ return undefined;
13
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h)?\s*$/i.exec(value);
14
+ if (!match)
15
+ return undefined;
16
+ const amount = Number(match[1]);
17
+ const unit = (match[2] ?? "ms").toLowerCase();
18
+ const multiplier = unit === "h" ? 3_600_000 : unit === "m" ? 60_000 : unit === "s" ? 1_000 : 1;
19
+ const parsed = amount * multiplier;
20
+ return Number.isFinite(parsed) ? parsed : undefined;
21
+ }
22
+ /** Parse a total run deadline. Invalid/zero values cannot disable the safety boundary. Human values such
23
+ * as `30m`, `90s`, and `1h` are accepted in config; HARA_RUN_TIMEOUT_MS also accepts plain milliseconds. */
24
+ export function agentRunTimeoutMs(value = process.env.HARA_RUN_TIMEOUT_MS) {
25
+ const parsed = parseAgentRunTimeoutMs(value);
26
+ if (parsed === undefined || parsed <= 0)
27
+ return DEFAULT_AGENT_RUN_TIMEOUT_MS;
28
+ return Math.max(MIN_AGENT_RUN_TIMEOUT_MS, Math.min(MAX_AGENT_RUN_TIMEOUT_MS, Math.trunc(parsed)));
29
+ }
30
+ /** Maximum provider/tool rounds in one run. This catches active loops that never go silent. */
31
+ export function agentMaxRounds(value = process.env.HARA_MAX_AGENT_ROUNDS) {
32
+ const parsed = typeof value === "number" ? value : Number(value);
33
+ if (!Number.isFinite(parsed) || parsed <= 0)
34
+ return DEFAULT_AGENT_MAX_ROUNDS;
35
+ return Math.max(1, Math.min(MAX_AGENT_MAX_ROUNDS, Math.trunc(parsed)));
36
+ }
37
+ export function formatAgentDuration(ms) {
38
+ if (ms >= 3_600_000 && ms % 3_600_000 === 0)
39
+ return `${ms / 3_600_000}h`;
40
+ if (ms >= 3_600_000)
41
+ return `${Math.round(ms / 360_000) / 10}h`;
42
+ if (ms >= 60_000 && ms % 60_000 === 0)
43
+ return `${ms / 60_000}m`;
44
+ if (ms >= 60_000)
45
+ return `${Math.round(ms / 6_000) / 10}m`;
46
+ if (ms >= 1_000 && ms % 1_000 === 0)
47
+ return `${ms / 1_000}s`;
48
+ if (ms >= 1_000)
49
+ return `${Math.round(ms / 1_000)}s`;
50
+ return `${ms}ms`;
51
+ }