@nanhara/hara 0.105.0 → 0.106.0

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,14 @@ 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.106.0 — gateway session hygiene
9
+
10
+ - **Idle chats auto-rotate to a fresh thread.** A WeChat/Feishu chat is one endless surface — days-old
11
+ context used to pile onto every new ask and the agent answered from stale state. Now a chat idle
12
+ past **8 hours** (HARA_GATEWAY_IDLE_HOURS to tune; 0 disables) starts the next message on a fresh
13
+ session, with a one-time notice carrying `/resume <id>` — the old thread persists, nothing is lost.
14
+ Same-afternoon follow-ups continue as before.
15
+
8
16
  ## 0.105.0 — fan-outs synthesize before acting
9
17
 
10
18
  - **Synthesis nudge** (the last adopted item from the Claude Code internals study — their KN5
@@ -245,6 +245,10 @@ export async function runGateway(opts) {
245
245
  }
246
246
  }
247
247
  const ctx = chatContext(adapter.name, m.chatId, cwd); // this chat's current { cwd, sessionId }
248
+ if (ctx.rotatedFrom) {
249
+ // Idle auto-rotation just happened (session hygiene): tell the user ONCE, with the escape hatch.
250
+ await adapter.send(m.chatId, `🧵 fresh thread (chat was idle) — /resume ${ctx.rotatedFrom.slice(-18)} continues the previous one`);
251
+ }
248
252
  const cmd = parseCommand(m.text);
249
253
  if (cmd) {
250
254
  if (cmd.cmd === "help")
@@ -8,6 +8,16 @@ import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
10
10
  import { createHash } from "node:crypto";
11
+ /** Idle window before a chat auto-rotates to a FRESH session (hours). A WeChat/Feishu chat is one
12
+ * endless surface — without this, days-old context piles onto every new ask and the agent answers
13
+ * from stale state (the "gateway answers with old context" report). Default 8h: overnight gaps start
14
+ * clean, a same-afternoon follow-up continues. HARA_GATEWAY_IDLE_HOURS tunes it; 0 disables. */
15
+ export function idleRotationMs() {
16
+ const raw = Number(process.env.HARA_GATEWAY_IDLE_HOURS ?? 8);
17
+ if (!Number.isFinite(raw) || raw <= 0)
18
+ return 0; // 0/garbage → disabled
19
+ return raw * 3_600_000;
20
+ }
11
21
  const dir = () => join(homedir(), ".hara", "gateway");
12
22
  const file = () => join(dir(), "chats.json");
13
23
  const mapKey = (platform, chatId) => `${platform}:${chatId}`;
@@ -37,17 +47,27 @@ export function chatContext(platform, chatId, defaultCwd) {
37
47
  const m = load();
38
48
  const k = mapKey(platform, chatId);
39
49
  const e = m[k];
50
+ const now = Date.now();
40
51
  if (!e) {
41
- const fresh = { cwd: defaultCwd, sessionId: deriveId(platform, chatId, defaultCwd, 0), fork: 0 };
52
+ const fresh = { cwd: defaultCwd, sessionId: deriveId(platform, chatId, defaultCwd, 0), fork: 0, lastUsed: now };
42
53
  m[k] = fresh;
43
54
  save(m);
44
55
  return { cwd: fresh.cwd, sessionId: fresh.sessionId, voice: false };
45
56
  }
46
- if (!e.cwd) {
57
+ if (!e.cwd)
47
58
  e.cwd = defaultCwd; // migrate an old (pre-cwd) entry, preserving its existing sessionId
48
- save(m);
59
+ // Session hygiene: a chat idle past the window rotates to a fresh thread (same mechanics as /new).
60
+ // The OLD id is returned so the gateway can offer "/resume <id>" — nothing is lost, sessions persist.
61
+ const idleMs = idleRotationMs();
62
+ let rotatedFrom;
63
+ if (idleMs > 0 && typeof e.lastUsed === "number" && now - e.lastUsed > idleMs && e.sessionId) {
64
+ rotatedFrom = e.sessionId;
65
+ e.fork = (e.fork ?? 0) + 1;
66
+ e.sessionId = deriveId(platform, chatId, e.cwd, e.fork);
49
67
  }
50
- return { cwd: e.cwd, sessionId: e.sessionId, voice: !!e.voice };
68
+ e.lastUsed = now;
69
+ save(m);
70
+ return { cwd: e.cwd, sessionId: e.sessionId, voice: !!e.voice, ...(rotatedFrom ? { rotatedFrom } : {}) };
51
71
  }
52
72
  /** `/voice` — toggle whether this chat's replies are spoken (TTS audio); returns the new state. */
53
73
  export function toggleVoice(platform, chatId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.105.0",
3
+ "version": "0.106.0",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "dist/index.js"