@nanhara/hara 0.122.1 → 0.122.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,24 @@ 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.3 — 2026-07-14 — standalone runtime recovery
9
+
10
+ - **Bun-compiled standalone binaries no longer assume `SharedArrayBuffer` exists at module startup.**
11
+ The short synchronous waits used by cross-process stores now prefer `Bun.sleepSync`, lazily use
12
+ `Atomics.wait` only on Node when its primitives exist, and retain a bounded last-resort delay. This
13
+ fixes the macOS arm64 `ReferenceError` that could make the 0.122.2 standalone binary exit before
14
+ even printing `--version`; npm/Node and GHCR users were unaffected. CI now compiles and executes a
15
+ native standalone binary on Linux and macOS, and the release job executes its native Linux asset
16
+ before publishing, so a merely successful cross-compile is no longer accepted as runtime proof.
17
+
18
+ ## 0.122.2 — 2026-07-14 — reproducible container release
19
+
20
+ - **The Docker build now includes the guarded runtime entry before normalizing package modes.** This fixes the
21
+ `runtime-bootstrap.cjs` `ENOENT` that affected only the `v0.122.1` GHCR image job; its npm package and
22
+ standalone binaries were already published and remain valid. CI now builds and starts the complete runtime
23
+ image before a tag can be released, while old Node installations continue to receive the explicit 22.12+
24
+ upgrade instruction.
25
+
8
26
  ## 0.122.1 — 2026-07-14 — protected files and explicit extension trust
9
27
 
10
28
  - **The npm CLI now requires Node.js 22.12+.** Node 20 is end-of-life, so Hara no longer makes a security or
@@ -23,9 +23,9 @@ import { getValidQwenAuth } from "../providers/qwen-oauth.js";
23
23
  import { resolvePlatform } from "../providers/registry.js";
24
24
  import { validateAgainstSchema } from "../agent/structured.js";
25
25
  import { terminateSubprocessTree } from "../security/subprocess-env.js";
26
+ import { sleepSync } from "../sync-sleep.js";
26
27
  const FILE = () => join(homedir(), ".hara", "flows-pending.json");
27
28
  const LOCK = () => FILE() + ".lock";
28
- const sleepCell = new Int32Array(new SharedArrayBuffer(4));
29
29
  const STORE_LOCK_ATTEMPTS = 500;
30
30
  const STORE_LOCK_WAIT_MS = 10;
31
31
  const PENDING_STATUSES = new Set(["pending", "executing", "done", "rejected", "failed", "expired"]);
@@ -151,7 +151,7 @@ function withStoreLock(fn) {
151
151
  continue;
152
152
  }
153
153
  }
154
- Atomics.wait(sleepCell, 0, 0, STORE_LOCK_WAIT_MS);
154
+ sleepSync(STORE_LOCK_WAIT_MS);
155
155
  continue;
156
156
  }
157
157
  const candidate = { pid: process.pid, token: randomUUID() };
@@ -190,7 +190,7 @@ function withStoreLock(fn) {
190
190
  }
191
191
  }
192
192
  }
193
- Atomics.wait(sleepCell, 0, 0, STORE_LOCK_WAIT_MS);
193
+ sleepSync(STORE_LOCK_WAIT_MS);
194
194
  }
195
195
  if (!claim)
196
196
  throw new Error("flows pending store is busy");
@@ -8,6 +8,7 @@ import { createHash, randomBytes } from "node:crypto";
8
8
  import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
+ import { sleepSync } from "../sync-sleep.js";
11
12
  /** Idle window before a chat auto-rotates to a FRESH session (hours). A WeChat/Feishu chat is one
12
13
  * endless surface — without this, days-old context piles onto every new ask and the agent answers
13
14
  * from stale state (the "gateway answers with old context" report). Default 8h: overnight gaps start
@@ -23,7 +24,6 @@ const dir = () => join(haraDir(), "gateway");
23
24
  const file = () => join(dir(), "chats.json");
24
25
  const lockFile = () => `${file()}.lock`;
25
26
  const reclaimFile = () => `${lockFile()}.reclaim`;
26
- const waitCell = new Int32Array(new SharedArrayBuffer(4));
27
27
  const LOCK_WAIT_MS = 5_000;
28
28
  function errorCode(error) {
29
29
  return typeof error === "object" && error !== null && "code" in error
@@ -114,7 +114,7 @@ function acquireLock() {
114
114
  }
115
115
  if (Date.now() >= deadline)
116
116
  throw new Error(`Timed out waiting for gateway session store lock: ${lockFile()}`);
117
- Atomics.wait(waitCell, 0, 0, 10);
117
+ sleepSync(10);
118
118
  continue;
119
119
  }
120
120
  try {
@@ -157,7 +157,7 @@ function acquireLock() {
157
157
  if (Date.now() >= deadline) {
158
158
  throw new Error(`Timed out waiting for gateway session store lock: ${lockFile()}`);
159
159
  }
160
- Atomics.wait(waitCell, 0, 0, 10);
160
+ sleepSync(10);
161
161
  }
162
162
  }
163
163
  }
@@ -5,8 +5,8 @@ import { isAbsolute, join, resolve } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { loadRoles, loadGlobalRoles } from "./roles.js";
8
+ import { sleepSync } from "../sync-sleep.js";
8
9
  const PROJECT_NAME = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
9
- const sleepCell = new Int32Array(new SharedArrayBuffer(4));
10
10
  const LOCK_ATTEMPTS = 500;
11
11
  const LOCK_WAIT_MS = 10;
12
12
  function haraDir() {
@@ -101,7 +101,7 @@ function withProjectsLock(fn) {
101
101
  continue;
102
102
  }
103
103
  }
104
- Atomics.wait(sleepCell, 0, 0, LOCK_WAIT_MS);
104
+ sleepSync(LOCK_WAIT_MS);
105
105
  continue;
106
106
  }
107
107
  const candidate = { pid: process.pid, token: randomUUID() };
@@ -132,7 +132,7 @@ function withProjectsLock(fn) {
132
132
  rmSync(reclaim, { force: true });
133
133
  }
134
134
  }
135
- Atomics.wait(sleepCell, 0, 0, LOCK_WAIT_MS);
135
+ sleepSync(LOCK_WAIT_MS);
136
136
  }
137
137
  if (!claim)
138
138
  throw new Error("projects registry is busy; retry the operation");
@@ -0,0 +1,46 @@
1
+ // Cross-runtime synchronous short wait for the tiny retry delays used by file locks.
2
+ //
3
+ // Bun standalone binaries do not guarantee that SharedArrayBuffer exists, so never construct one at
4
+ // module evaluation time. Prefer Bun's native primitive, use Atomics only in Node, and retain a bounded
5
+ // last resort so a missing/disabled runtime primitive cannot make lock acquisition crash or spin forever.
6
+ const MAX_SHORT_WAIT_MS = 100;
7
+ const MAX_FALLBACK_SPINS = 5_000_000;
8
+ let nodeWaitCell;
9
+ function boundedDelay(milliseconds) {
10
+ if (!Number.isFinite(milliseconds) || milliseconds <= 0)
11
+ return 0;
12
+ return Math.min(MAX_SHORT_WAIT_MS, Math.ceil(milliseconds));
13
+ }
14
+ /** Block for a small, bounded retry delay without assuming SharedArrayBuffer is available. */
15
+ export function sleepSync(milliseconds) {
16
+ const delay = boundedDelay(milliseconds);
17
+ if (delay === 0)
18
+ return;
19
+ const runtime = globalThis;
20
+ if (typeof runtime.Bun?.sleepSync === "function") {
21
+ try {
22
+ runtime.Bun.sleepSync(delay);
23
+ return;
24
+ }
25
+ catch {
26
+ // A partially implemented Bun runtime still gets the bounded fallback below.
27
+ }
28
+ }
29
+ // Atomics.wait is a dependable synchronous sleep on Node's main thread. Do not use this branch in
30
+ // Bun: a compiled Bun executable may expose part of the Node compatibility surface without SAB.
31
+ if (!runtime.Bun && typeof runtime.SharedArrayBuffer === "function" && typeof runtime.Atomics?.wait === "function") {
32
+ try {
33
+ nodeWaitCell ??= new Int32Array(new runtime.SharedArrayBuffer(4));
34
+ runtime.Atomics.wait(nodeWaitCell, 0, 0, delay);
35
+ return;
36
+ }
37
+ catch {
38
+ nodeWaitCell = undefined;
39
+ // Some embedders disable blocking Atomics even when the globals exist. Fall through safely.
40
+ }
41
+ }
42
+ const deadline = Date.now() + delay;
43
+ for (let spins = 0; spins < MAX_FALLBACK_SPINS && Date.now() < deadline; spins++) {
44
+ // Intentionally empty: both time and iteration ceilings bound this compatibility fallback.
45
+ }
46
+ }
@@ -6,9 +6,9 @@ import { basename, join, resolve } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { registerTool } from "./registry.js";
9
+ import { sleepSync } from "../sync-sleep.js";
9
10
  const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10
11
  const TASK_STATUSES = new Set(["pending", "in_progress", "done"]);
11
- const sleepCell = new Int32Array(new SharedArrayBuffer(4));
12
12
  const LOCK_ATTEMPTS = 500;
13
13
  const LOCK_WAIT_MS = 10;
14
14
  /** Resolve aliases/symlinks before deriving the key. A missing path still gets a stable absolute key so
@@ -113,7 +113,7 @@ function withTaskLock(cwd, fn) {
113
113
  continue;
114
114
  }
115
115
  }
116
- Atomics.wait(sleepCell, 0, 0, LOCK_WAIT_MS);
116
+ sleepSync(LOCK_WAIT_MS);
117
117
  continue;
118
118
  }
119
119
  const candidate = { pid: process.pid, token: randomUUID() };
@@ -144,7 +144,7 @@ function withTaskLock(cwd, fn) {
144
144
  rmSync(reclaim, { force: true });
145
145
  }
146
146
  }
147
- Atomics.wait(sleepCell, 0, 0, LOCK_WAIT_MS);
147
+ sleepSync(LOCK_WAIT_MS);
148
148
  }
149
149
  if (!claim)
150
150
  throw new Error("task store is busy; retry the operation");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.122.1",
3
+ "version": "0.122.3",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "runtime-bootstrap.cjs"