@nanhara/hara 0.112.1 → 0.112.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,29 @@ 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.112.3 — big file write no longer traps the model in a "params not passed" loop
9
+
10
+ - **Fixed the loop where a model (glm-5 / qwen via DashScope) repeats `write_file` / `bash` with empty or
11
+ `undefined` arguments.** Two problems compounded: the OpenAI-compatible path capped output at
12
+ `max_tokens: 8192`, so a large `write_file` (e.g. a 64-hexagram data file) ran PAST the limit and its
13
+ tool-call-arguments JSON was cut off — and hara then **silently swallowed the unparseable JSON into an
14
+ empty `{}`**. So the tool ran with no path/content (`bash` got `command: undefined` →
15
+ `/bin/sh: undefined: command not found`), the model saw the failure, "fixed" it by calling again, and
16
+ looped — never realizing its *output* had been truncated. Now: (1) `max_tokens` is raised to 32000
17
+ (glm-5/qwen accept it), and (2) **truncated/malformed tool arguments surface as an actionable error**
18
+ ("the model hit its output-length limit mid tool-call — write the file in smaller parts") instead of a
19
+ silent `{}`, so the loop breaks and the fix is obvious.
20
+
21
+ ## 0.112.2 — moving/resizing the window no longer garbles the UI
22
+
23
+ - **Terminal resize no longer stacks the status row + input box into a garble.** ink 6.8's resize
24
+ handler only clears the screen when the terminal gets *narrower*; on a *widen* (or a resize it doesn't
25
+ classify as narrowing) it just re-renders, so the old frame — reflowed at the new width — is never
26
+ erased, and the ~125ms spinner tick stacks a fresh copy each time (the "I moved the window and the UI
27
+ filled up with repeated `waiting for the model…` lines"). hara now hooks the resize event and, on ANY
28
+ resize, resets ink's tracked output (debounced across a drag's burst of events) so the next render
29
+ starts clean and subsequent ticks erase correctly. Not "just the terminal" — a real repaint fix.
30
+
8
31
  ## 0.112.1 — the background-job indicator is now LIVE (even at idle)
9
32
 
10
33
  - **`⚙ N bg running` updates in real time — including when hara is idle.** In 0.112.0 the indicator only
@@ -2,6 +2,35 @@ import OpenAI from "openai";
2
2
  import { imageToBase64 } from "../images.js";
3
3
  import { reasoningParams } from "./reasoning.js";
4
4
  import { resolvePlatform } from "./registry.js";
5
+ /** Assemble streamed tool-call fragments into tool uses. CRITICAL: non-empty arguments that don't parse
6
+ * mean the model was cut off MID tool-call (almost always the output-length limit on a big write_file /
7
+ * bash). Silently substituting `{}` makes the model loop forever — it calls write_file with no path, bash
8
+ * with `command: undefined` (`/bin/sh: undefined: command not found`), sees the failure, and retries,
9
+ * never realizing its OUTPUT was truncated. So we surface it as an actionable error instead. Exported for
10
+ * tests. */
11
+ export function assembleToolCalls(entries, finish) {
12
+ let truncated = false;
13
+ const toolUses = entries
14
+ .filter((t) => t.id && t.name)
15
+ .map((t) => {
16
+ let input = {};
17
+ const raw = (t.args || "").trim();
18
+ if (raw) {
19
+ try {
20
+ input = JSON.parse(raw);
21
+ }
22
+ catch {
23
+ truncated = true;
24
+ }
25
+ }
26
+ return { id: t.id, name: t.name, input };
27
+ });
28
+ if (truncated) {
29
+ const why = finish === "length" ? "the model hit its output-length limit mid tool-call" : "the model emitted malformed tool-call arguments";
30
+ return { toolUses: [], error: `Tool call dropped — ${why}, so its arguments were incomplete. For a large file, write it in smaller parts (several write_file/edit calls) rather than one giant call.` };
31
+ }
32
+ return { toolUses };
33
+ }
5
34
  // Re-exported for callers that still import it from here (the reasoning-family check now lives in reasoning.ts).
6
35
  export { isReasoningModel } from "./reasoning.js";
7
36
  /** Build OpenAI chat-completions messages from neutral history. */
@@ -63,7 +92,7 @@ export function createOpenAIProvider(opts) {
63
92
  const params = {
64
93
  model: opts.model,
65
94
  messages: toOpenAI(system, history),
66
- max_tokens: 8192,
95
+ max_tokens: 32000, // was 8192 — too small: a big write_file's args got truncated → unparseable → loop
67
96
  stream: true,
68
97
  stream_options: { include_usage: true },
69
98
  };
@@ -118,18 +147,9 @@ export function createOpenAIProvider(opts) {
118
147
  return { text: "", toolUses: [], stop: "error", errorMsg: "interrupted" };
119
148
  return { text: "", toolUses: [], stop: "error", errorMsg: `${e?.status ?? ""} ${e?.message ?? e}` };
120
149
  }
121
- const toolUses = [...acc.values()]
122
- .filter((t) => t.id && t.name)
123
- .map((t) => {
124
- let input = {};
125
- try {
126
- input = JSON.parse(t.args || "{}");
127
- }
128
- catch {
129
- input = {};
130
- }
131
- return { id: t.id, name: t.name, input };
132
- });
150
+ const { toolUses, error: argsError } = assembleToolCalls([...acc.values()], finish);
151
+ if (argsError)
152
+ return { text, toolUses: [], stop: "error", errorMsg: argsError, usage };
133
153
  const stop = finish === "tool_calls" || toolUses.length ? "tool_use" : "end";
134
154
  return { text, toolUses, stop, usage };
135
155
  },
package/dist/tui/run.js CHANGED
@@ -6,7 +6,35 @@ import { createElement } from "react";
6
6
  import { App } from "./App.js";
7
7
  export async function runTui(props) {
8
8
  const instance = render(createElement(App, props));
9
- await instance.waitUntilExit();
9
+ // Resize repaint fix. ink 6.8's own resize handler only clears the screen when the terminal gets
10
+ // NARROWER; on a WIDEN (or a resize it doesn't classify as narrowing) it just re-renders, so the old
11
+ // frame — reflowed at the new width — is never erased, and the ~125ms spinner tick stacks a fresh copy
12
+ // each time (the "moved the window and the UI stacked up" garble). We complement it: on ANY resize,
13
+ // clear ink's tracked output so the next render starts clean. Debounced by a microtask so a burst of
14
+ // resize events during a window drag collapses to one clear.
15
+ const out = process.stdout;
16
+ let pending = false;
17
+ const onResize = () => {
18
+ if (pending)
19
+ return;
20
+ pending = true;
21
+ queueMicrotask(() => {
22
+ pending = false;
23
+ try {
24
+ instance.clear();
25
+ }
26
+ catch {
27
+ /* best-effort — never let a repaint fix crash the session */
28
+ }
29
+ });
30
+ };
31
+ out.on("resize", onResize);
32
+ try {
33
+ await instance.waitUntilExit();
34
+ }
35
+ finally {
36
+ out.off("resize", onResize);
37
+ }
10
38
  }
11
39
  // A tiny ink yes/no prompt for pre-TUI confirms (e.g. the first-run "create AGENTS.md?" offer).
12
40
  // MUST be ink, NOT readline: a readline question before the main TUI leaves stdin in a state ink
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.112.1",
3
+ "version": "0.112.3",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "dist/index.js"