@nanhara/hara 0.105.0 → 0.107.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 +20 -0
- package/dist/agent/loop.js +3 -1
- package/dist/agent/reminders.js +9 -0
- package/dist/gateway/serve.js +4 -0
- package/dist/gateway/sessions.js +24 -4
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.107.0 — interjection triage: the model is the scheduler, the todo list is the queue
|
|
9
|
+
|
|
10
|
+
- **Mid-task messages get triaged, not blindly folded in.** Typing while hara works always reached
|
|
11
|
+
the model between tool calls (type-ahead steering) — but nothing told it HOW to handle the
|
|
12
|
+
interjection. Now every mid-task message carries a triage contract, backed by a standing policy in
|
|
13
|
+
the system prompt: a **refinement** folds into the current task immediately; a **new independent
|
|
14
|
+
task** goes onto the todo queue (`todo_write`, one-line acknowledgment, current work continues);
|
|
15
|
+
something **urgent** — a bug, "stop", "this first" — finishes the current step safely (no half-done
|
|
16
|
+
edits), re-plans the queue, and switches immediately. Same architecture codex and Claude Code
|
|
17
|
+
landed on: no engine-level priority scheduler — classification is exactly what the model is best
|
|
18
|
+
at, and the todo list (with its live panel + attention refresh) is the task queue.
|
|
19
|
+
|
|
20
|
+
## 0.106.0 — gateway session hygiene
|
|
21
|
+
|
|
22
|
+
- **Idle chats auto-rotate to a fresh thread.** A WeChat/Feishu chat is one endless surface — days-old
|
|
23
|
+
context used to pile onto every new ask and the agent answered from stale state. Now a chat idle
|
|
24
|
+
past **8 hours** (HARA_GATEWAY_IDLE_HOURS to tune; 0 disables) starts the next message on a fresh
|
|
25
|
+
session, with a one-time notice carrying `/resume <id>` — the old thread persists, nothing is lost.
|
|
26
|
+
Same-afternoon follow-ups continue as before.
|
|
27
|
+
|
|
8
28
|
## 0.105.0 — fan-outs synthesize before acting
|
|
9
29
|
|
|
10
30
|
- **Synthesis nudge** (the last adopted item from the Claude Code internals study — their KN5
|
package/dist/agent/loop.js
CHANGED
|
@@ -59,7 +59,9 @@ you can do. When analyzing a project, start wide in ONE batch — manifest (pack
|
|
|
59
59
|
pyproject.toml / go.mod), README, build/CI config — then chase only what the task needs with narrow
|
|
60
60
|
grep/glob; don't read whole large files when a targeted search answers the question. For broad,
|
|
61
61
|
open-ended exploration (more than ~3 searches), spawn \`agent\` sub-agents — several in one response for
|
|
62
|
-
independent questions (role "explore") — each returns conclusions, not dumps.
|
|
62
|
+
independent questions (role "explore") — each returns conclusions, not dumps. Messages the user sends
|
|
63
|
+
mid-task arrive marked as interjections — triage them (refine current / queue as todo / urgent-switch)
|
|
64
|
+
instead of blindly folding everything into the current task; the todo list is your task queue. For a multi-step task, call \`todo_write\` to plan a short checklist and keep it updated as
|
|
63
65
|
you go (one item in_progress at a time) — skip it for trivial one-step tasks. You have a persistent
|
|
64
66
|
memory: use memory_search before answering about prior decisions,
|
|
65
67
|
conventions, or the user's preferences, and memory_write to proactively save durable facts you learn.
|
package/dist/agent/reminders.js
CHANGED
|
@@ -30,6 +30,15 @@ export function wrapReminders(items) {
|
|
|
30
30
|
/** How many tool rounds a checklist may sit untouched (with unfinished items) before the model gets an
|
|
31
31
|
* attention refresh. Reset on every todo_write; re-arms after firing so it nags at most once per N. */
|
|
32
32
|
export const TODO_STALE_ROUNDS = 5;
|
|
33
|
+
/** Prefix for a message the user sent MID-TASK (type-ahead steering). Carries the triage contract
|
|
34
|
+
* inline (self-contained even for role-overridden runs): the model — not the engine — is the
|
|
35
|
+
* scheduler, and the todo list is the task queue (codex/Claude-Code's model too: neither ships an
|
|
36
|
+
* engine-level priority scheduler; classification is exactly what the LLM is best at). */
|
|
37
|
+
export const INTERJECT_PREFIX = "[Sent while you were working on the above — TRIAGE before continuing: " +
|
|
38
|
+
"a refinement/correction of the current task → fold it in now; " +
|
|
39
|
+
"a NEW independent task → todo_write it onto the queue, acknowledge in one line, continue the current task; " +
|
|
40
|
+
"URGENT (a bug, \"stop\", \"this first\") → finish the current step safely (no half-done edits), " +
|
|
41
|
+
"todo_write the re-plan (current task → pending, this → in_progress), and switch to it immediately.]";
|
|
33
42
|
/** Parallel fan-outs at/above this size get a synthesis nudge (CC's KN5 synthesizer, hara-shaped:
|
|
34
43
|
* instead of a dedicated merger agent, the MAIN model is reminded to merge before acting). */
|
|
35
44
|
export const SYNTHESIS_MIN_AGENTS = 3;
|
package/dist/gateway/serve.js
CHANGED
|
@@ -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")
|
package/dist/gateway/sessions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { loadPermissionRules, scaffoldPermissions, globalPermissionsPath, projec
|
|
|
26
26
|
import { routingProvider } from "./agent/route.js";
|
|
27
27
|
import { shouldAutoCompact, COMPACT_SYSTEM, buildFileRestore } from "./agent/compact.js";
|
|
28
28
|
import { recentTouched } from "./agent/touched.js";
|
|
29
|
+
import { INTERJECT_PREFIX } from "./agent/reminders.js";
|
|
29
30
|
import { checkForUpdate } from "./update-check.js";
|
|
30
31
|
import { formatContextReport } from "./agent/context-report.js";
|
|
31
32
|
import { userTurnPreviews, rewindTo } from "./agent/rewind.js";
|
|
@@ -3054,7 +3055,7 @@ program.action(async (opts) => {
|
|
|
3054
3055
|
const attach = !r2.skip && r2.attach?.length ? r2.attach : undefined;
|
|
3055
3056
|
if (!body.trim() && !attach)
|
|
3056
3057
|
continue; // image-only message whose image was skipped → nothing to add
|
|
3057
|
-
out.push({ role: "user", content:
|
|
3058
|
+
out.push({ role: "user", content: `${INTERJECT_PREFIX}\n\n${body}`, ...(attach ? { images: attach } : {}) });
|
|
3058
3059
|
}
|
|
3059
3060
|
return out;
|
|
3060
3061
|
};
|