@dorokuma/herdsman-pi 0.6.4 → 0.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorokuma/herdsman-pi",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Pi extension bridge for Herdsman agent history. Forked from @ryonakae/shepherd.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,5 +1,5 @@
1
1
  import { createConnection, type Socket } from "node:net";
2
- import { JsonLineDecoder } from "@/shared/json-lines.js";
2
+ import { JsonLineDecoder } from "./shared/json-lines.js";
3
3
 
4
4
  export type AgentEventWireRecord = {
5
5
  agentId?: string | null;
package/src/index.ts CHANGED
@@ -45,6 +45,7 @@ type PiPresence = {
45
45
 
46
46
  type ConnectionStateResponse = {
47
47
  changed?: boolean;
48
+ ackedEventId?: number;
48
49
  context?: AgentWorkspaceContextSnapshot | null;
49
50
  events?: AgentEventWireRecord[];
50
51
  presence: PiPresence;
@@ -223,6 +224,11 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
223
224
  state.runActive = false;
224
225
  };
225
226
 
227
+ const pruneAcknowledgedEvents = (ackedEventId: number | undefined) => {
228
+ if (ackedEventId === undefined) return;
229
+ state.pendingEvents = state.pendingEvents.filter((event) => event.id > ackedEventId);
230
+ };
231
+
226
232
  const applyOwnerContext = (response: ConnectionStateResponse) => {
227
233
  state.latestContext = isLocalOwner(response) ? response.context ?? undefined : undefined;
228
234
  };
@@ -448,6 +454,8 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
448
454
  applyOwnerContext(response);
449
455
  setHerdsmanUi(ctx);
450
456
  addPendingEvents(response.events ?? [], ctx);
457
+ pruneAcknowledgedEvents(response.state?.ackedEventId ?? response.ackedEventId);
458
+ setHerdsmanUi(ctx);
451
459
  scheduleWake(ctx);
452
460
  };
453
461
 
@@ -530,9 +538,11 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
530
538
  ? {
531
539
  ...snapshot,
532
540
  agents: snapshot.agents.filter((agent) => {
533
- const nextAgent = next?.agents.find((candidate) => candidate.paneId === agent.paneId);
534
- if (!nextAgent) return false;
535
- return !agent.id || !nextAgent.id || agent.id === nextAgent.id;
541
+ const nextAgents = next?.agents.filter((candidate) => candidate.paneId === agent.paneId) ?? [];
542
+ if (nextAgents.length === 0) return false;
543
+ return nextAgents.some(
544
+ (nextAgent) => !agent.id || !nextAgent.id || agent.id === nextAgent.id,
545
+ );
536
546
  }),
537
547
  }
538
548
  : undefined;
@@ -762,10 +772,25 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
762
772
 
763
773
  for (const event of batch.events) {
764
774
  try {
765
- await state.client.request("agent.notifications.ack", { eventId: event.id });
775
+ const ackResponse = (await state.client.request("agent.notifications.ack", {
776
+ eventId: event.id,
777
+ })) as { ackedEventId?: number; state?: { ackedEventId?: number } };
778
+ pruneAcknowledgedEvents(ackResponse?.ackedEventId ?? ackResponse?.state?.ackedEventId);
766
779
  state.pendingEvents = state.pendingEvents.filter((pending) => pending.id !== event.id);
767
780
  setHerdsmanUi(ctx);
768
- } catch {
781
+ } catch (error) {
782
+ if (
783
+ error instanceof Error &&
784
+ /no longer pending|invalidated/i.test(error.message)
785
+ ) {
786
+ state.pendingEvents = state.pendingEvents.filter((pending) => pending.id > event.id);
787
+ state.failedWakeThroughEventId = Math.max(
788
+ state.failedWakeThroughEventId,
789
+ ...batch.events.map((batchEvent) => batchEvent.id),
790
+ );
791
+ setHerdsmanUi(ctx);
792
+ continue;
793
+ }
769
794
  // Keep the event pending so the next Herdsman round retries the acknowledgement.
770
795
  // Acknowledgement failure must not block delivery of the main turn.
771
796
  ctx.ui.notify?.(
@@ -0,0 +1,46 @@
1
+ export function encodeJsonLine(value: unknown): string {
2
+ const encoded = JSON.stringify(value);
3
+
4
+ if (encoded === undefined) {
5
+ throw new TypeError("JSON Lines values must be JSON-serializable");
6
+ }
7
+
8
+ return `${encoded}\n`;
9
+ }
10
+
11
+ export class JsonLineFrameTooLargeError extends Error {
12
+ constructor(readonly maxBytes: number) {
13
+ super(`JSON Lines frame exceeds maximum size of ${maxBytes} bytes`);
14
+ this.name = "JsonLineFrameTooLargeError";
15
+ }
16
+ }
17
+
18
+ export class JsonLineDecoder {
19
+ #buffer = "";
20
+ readonly #maxBytes: number;
21
+
22
+ constructor(maxBytes = 1024 * 1024) {
23
+ if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
24
+ throw new RangeError("JSON Lines maximum frame size must be a positive integer");
25
+ }
26
+ this.#maxBytes = maxBytes;
27
+ }
28
+
29
+ push(chunk: string): unknown[] {
30
+ this.#buffer += chunk;
31
+ if (Buffer.byteLength(this.#buffer, "utf8") > this.#maxBytes) {
32
+ throw new JsonLineFrameTooLargeError(this.#maxBytes);
33
+ }
34
+
35
+ const lines = this.#buffer.split("\n");
36
+ this.#buffer = lines.pop() ?? "";
37
+ return lines.filter((line) => line.length > 0).map((line) => JSON.parse(line));
38
+ }
39
+
40
+ flush(): unknown[] {
41
+ if (this.#buffer.length === 0) return [];
42
+ const line = this.#buffer;
43
+ this.#buffer = "";
44
+ return [JSON.parse(line)];
45
+ }
46
+ }