@nanhara/hara 0.109.2 → 0.109.3

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 CHANGED
@@ -5,6 +5,19 @@ 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.109.3 — an empty model response no longer looks like a hang
9
+
10
+ - **A turn that comes back with no text and no tool calls is no longer silently dropped.** The agent
11
+ loop treated an empty completion as "done" and returned with zero feedback — so after e.g. a "继续"
12
+ the box just sat there, idle, looking frozen for hours (CPU 0%, waiting at the prompt) when really
13
+ the turn had vanished. It now **retries once** with a nudge (an empty response is usually a transient
14
+ hiccup), and if it's still empty says so plainly (`✻ empty response — nothing to do. Rephrase…`)
15
+ instead of disappearing. Matches how Claude Code / codex refuse to end on a blank turn.
16
+ - **Closed a matching spin:** a `tool_use` stop reason carrying an *empty* tool list used to push an
17
+ empty tool round and re-request in a loop; it's now bounded by the same single-retry guard. (The
18
+ 120s stall-watchdog already covered a genuinely dead/silent socket — this covers the case where the
19
+ request *succeeds* but returns nothing.)
20
+
8
21
  ## 0.109.2 — long paste no longer freezes the input box
9
22
 
10
23
  - **The input box now draws a bottom-anchored viewport, not every wrapped row.** A long multi-line
@@ -97,6 +97,7 @@ export async function runAgent(history, opts) {
97
97
  const permRules = loadPermissionRules(ctx.cwd); // command-level allow/ask/deny policy for the bash tool
98
98
  let activeProvider = provider; // may switch to a fallback model on a recoverable error (app-failover)
99
99
  let triedFallback = false;
100
+ let emptyRetried = false; // one-shot: a genuinely empty model turn gets a single nudge before we give up
100
101
  // Stuck/loop guard — only in headless chat (`hara gateway`), where a wrong approach can grind forever with
101
102
  // nobody to hit Esc (e.g. screenshots it can't read). Once per run, when the agent keeps repeating one
102
103
  // non-read tool or acting blind, we inject a reflection nudge so it steps back instead of spinning.
@@ -281,7 +282,37 @@ export async function runAgent(history, opts) {
281
282
  }
282
283
  return;
283
284
  }
284
- if (r.stop !== "tool_use")
285
+ // Empty-turn guard. The model returned nothing actionable — no text AND no tool calls (a blank
286
+ // completion, or a "tool_use" stop with an empty tool list). Silently returning here leaves the
287
+ // user at a dead prompt with ZERO feedback: it reads as a 15-hour hang when really the turn just
288
+ // vanished. Retry ONCE with a nudge (usually a transient hiccup), then, if still empty, say so
289
+ // plainly and end — never loop forever, never disappear. (Claude Code / codex both guard this.)
290
+ if (!r.text.trim() && r.toolUses.length === 0) {
291
+ if (!emptyRetried) {
292
+ emptyRetried = true;
293
+ history.pop(); // drop the empty assistant turn before re-asking
294
+ history.push({ role: "user", content: "(Your previous response was empty. Continue the task now — take the next concrete step with a tool, or reply with text. Do not return an empty response.)" });
295
+ if (!opts.quiet) {
296
+ const note = "✻ empty response — retrying once…";
297
+ if (sink)
298
+ sink.notice(note);
299
+ else
300
+ out(c.dim(`${note}\n`));
301
+ }
302
+ continue;
303
+ }
304
+ const note = "✻ the model returned an empty response — nothing to do. Rephrase your request, or press Enter to try again.";
305
+ if (!opts.quiet) {
306
+ if (sink)
307
+ sink.notice(note);
308
+ else
309
+ out(c.dim(`${note}\n`));
310
+ }
311
+ return;
312
+ }
313
+ // A "tool_use" stop with text but no tools (rare) has nothing to execute — end after showing the text
314
+ // rather than pushing an empty tool round and re-requesting in a loop.
315
+ if (r.stop !== "tool_use" || r.toolUses.length === 0)
285
316
  return;
286
317
  const plans = [];
287
318
  // Extra (per-run) tools win over the registry so a run-scoped tool can't be shadowed by a global one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.109.2",
3
+ "version": "0.109.3",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "dist/index.js"