@nanhara/hara 0.129.0 → 0.130.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 +14 -0
- package/dist/serve/protocol.js +1 -1
- package/dist/serve/server.js +19 -5
- package/dist/serve/task-events.js +10 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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.130.0 — 2026-07-20 — ordered task-state delivery
|
|
9
|
+
|
|
10
|
+
- Typed `event.task_state` notifications now carry a server-stream identity and a positive,
|
|
11
|
+
monotonically increasing sequence. Desktop and other authenticated Serve clients can reject
|
|
12
|
+
duplicated or stale lifecycle transitions instead of letting a late event overwrite the current
|
|
13
|
+
task, approval, checkpoint, or completion state.
|
|
14
|
+
- Ordering spans every session and every resume within one `hara serve` process. A server restart
|
|
15
|
+
creates a new stream identity, so clients can accept the fresh sequence without confusing it with
|
|
16
|
+
an older connection.
|
|
17
|
+
- Conversation streaming remains independent from execution state, and task event payloads retain
|
|
18
|
+
the same redacted ambient-data boundary. Existing protocol-v1 clients remain compatible because
|
|
19
|
+
the ordering fields are additive.
|
|
20
|
+
- Upgrade with `npm i -g @nanhara/hara@0.130.0`.
|
|
21
|
+
|
|
8
22
|
## 0.129.0 — 2026-07-20 — durable revisions and workspace recovery
|
|
9
23
|
|
|
10
24
|
- Authenticated Serve clients can commit a user-selected file as a new immutable Artifact revision with
|
package/dist/serve/protocol.js
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
// Server → client notifications (all carry sessionId):
|
|
43
43
|
// event.text / event.reasoning {delta} · event.tool {name,preview} · event.diff {text}
|
|
44
44
|
// event.notice {text} · event.turn_end {reply,usage,error?} · approval.request {approvalId,question}
|
|
45
|
-
// event.task_state {version,taskId,turnId,objective,state,taskStatus,phase,checkpoint,…}
|
|
45
|
+
// event.task_state {version,streamId,sequence,taskId,turnId,objective,state,taskStatus,phase,checkpoint,…}
|
|
46
46
|
// authoritative execution plane; clients feature-detect it via capabilities.events
|
|
47
47
|
export const PROTOCOL_VERSION = 1;
|
|
48
48
|
/** JSON-RPC error codes: standard ones plus hara-specific (-320xx). */
|
package/dist/serve/server.js
CHANGED
|
@@ -32,7 +32,7 @@ import { disposeTodoScope, onTodosChange, restoreTodos, serializeTodos } from ".
|
|
|
32
32
|
import { INTERJECT_PREFIX, disposeReminderScope } from "../agent/reminders.js";
|
|
33
33
|
import { SessionHub, realStore } from "./sessions.js";
|
|
34
34
|
import { parseFrame, rpcResult, rpcError, rpcNotify, ERR, PROTOCOL_VERSION } from "./protocol.js";
|
|
35
|
-
import { taskLifecycleEvent } from "./task-events.js";
|
|
35
|
+
import { taskLifecycleEvent, } from "./task-events.js";
|
|
36
36
|
import { readModelContextFileSync } from "../fs-read.js";
|
|
37
37
|
import { optionalPosixOpenFlag } from "../fs-open-flags.js";
|
|
38
38
|
import { sameOpenedFileIdentity } from "../fs-identity.js";
|
|
@@ -295,6 +295,7 @@ export async function startServe(opts, deps) {
|
|
|
295
295
|
// Physical provider/tool work can outlive its logical timeout. Keep a process-level ledger independent
|
|
296
296
|
// of SessionHub membership so detach/delete cannot make an updater believe the old engine is quiescent.
|
|
297
297
|
const activeOperations = new Set();
|
|
298
|
+
let taskEventSequence = 0;
|
|
298
299
|
let closing = false;
|
|
299
300
|
let closePromise = null;
|
|
300
301
|
const trackActiveOperation = (operation) => {
|
|
@@ -358,10 +359,23 @@ export async function startServe(opts, deps) {
|
|
|
358
359
|
if (ws.readyState === ws.OPEN)
|
|
359
360
|
ws.send(frame);
|
|
360
361
|
};
|
|
362
|
+
const nextTaskEventCursor = () => {
|
|
363
|
+
const sequence = taskEventSequence + 1;
|
|
364
|
+
if (!Number.isSafeInteger(sequence)) {
|
|
365
|
+
throw new Error("task lifecycle event sequence exhausted");
|
|
366
|
+
}
|
|
367
|
+
return { streamId: instanceId, sequence };
|
|
368
|
+
};
|
|
369
|
+
const publishTaskState = (event) => {
|
|
370
|
+
// Commit the cursor immediately before the synchronous broadcast. Dedupe paths never consume one,
|
|
371
|
+
// while every published event has a unique position in this server-wide stream.
|
|
372
|
+
taskEventSequence = event.sequence;
|
|
373
|
+
broadcast("event.task_state", { ...event });
|
|
374
|
+
};
|
|
361
375
|
const broadcastTaskState = (session, activity) => {
|
|
362
376
|
if (!session.task)
|
|
363
377
|
return;
|
|
364
|
-
|
|
378
|
+
publishTaskState(taskLifecycleEvent(session.meta.id, session.task, session.meta.todos ?? [], activity, nextTaskEventCursor()));
|
|
365
379
|
};
|
|
366
380
|
// Discovery file — the desktop shell reads this to find the running server (like a pid/port file).
|
|
367
381
|
const discoveryDir = join(deps.discoveryHome ?? homedir(), ".hara");
|
|
@@ -448,13 +462,13 @@ export async function startServe(opts, deps) {
|
|
|
448
462
|
const emitTaskState = (activity, todos = s.meta.todos ?? []) => {
|
|
449
463
|
if (!s.task)
|
|
450
464
|
return;
|
|
451
|
-
const event = taskLifecycleEvent(sessionId, s.task, todos, activity);
|
|
452
|
-
const { at: _at, ...stableEvent } = event;
|
|
465
|
+
const event = taskLifecycleEvent(sessionId, s.task, todos, activity, nextTaskEventCursor());
|
|
466
|
+
const { at: _at, streamId: _streamId, sequence: _sequence, ...stableEvent } = event;
|
|
453
467
|
const signature = JSON.stringify(stableEvent);
|
|
454
468
|
if (signature === lastTaskStateSignature)
|
|
455
469
|
return;
|
|
456
470
|
lastTaskStateSignature = signature;
|
|
457
|
-
|
|
471
|
+
publishTaskState(event);
|
|
458
472
|
};
|
|
459
473
|
broadcast("event.turn_start", { sessionId, taskId: s.task.id, turnId: s.task.turnId });
|
|
460
474
|
emitTaskState({ state: "running", phase: "starting" });
|
|
@@ -5,7 +5,14 @@ function bounded(value, max) {
|
|
|
5
5
|
}
|
|
6
6
|
/** Build the single task-state shape consumed by Desktop, IDE clients, and the companion. Conversation
|
|
7
7
|
* streaming remains separate; this event is the authoritative execution/status plane. */
|
|
8
|
-
export function taskLifecycleEvent(sessionId, task, todos = [], activity, at = new Date().toISOString()) {
|
|
8
|
+
export function taskLifecycleEvent(sessionId, task, todos = [], activity, cursor, at = new Date().toISOString()) {
|
|
9
|
+
const streamId = cursor.streamId.trim();
|
|
10
|
+
if (!streamId || streamId.length > 128) {
|
|
11
|
+
throw new Error("task lifecycle streamId must contain 1-128 characters");
|
|
12
|
+
}
|
|
13
|
+
if (!Number.isSafeInteger(cursor.sequence) || cursor.sequence <= 0) {
|
|
14
|
+
throw new Error("task lifecycle sequence must be a positive safe integer");
|
|
15
|
+
}
|
|
9
16
|
const current = todos.find((todo) => todo.status === "in_progress")
|
|
10
17
|
?? todos.find((todo) => todo.status === "pending");
|
|
11
18
|
const detail = bounded(activity.detail, 500);
|
|
@@ -22,6 +29,8 @@ export function taskLifecycleEvent(sessionId, task, todos = [], activity, at = n
|
|
|
22
29
|
: task.status;
|
|
23
30
|
return {
|
|
24
31
|
version: TASK_LIFECYCLE_EVENT_VERSION,
|
|
32
|
+
streamId,
|
|
33
|
+
sequence: cursor.sequence,
|
|
25
34
|
sessionId,
|
|
26
35
|
taskId: task.id,
|
|
27
36
|
turnId: task.turnId,
|