@dorokuma/herdsman-pi 0.6.3 → 0.6.4
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 +1 -1
- package/src/daemon-client.ts +44 -26
- package/src/index.ts +12 -15
package/package.json
CHANGED
package/src/daemon-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createConnection, type Socket } from "node:net";
|
|
2
|
+
import { JsonLineDecoder } from "@/shared/json-lines.js";
|
|
2
3
|
|
|
3
4
|
export type AgentEventWireRecord = {
|
|
4
5
|
agentId?: string | null;
|
|
@@ -99,8 +100,9 @@ export class ReconnectingDaemonClient {
|
|
|
99
100
|
readonly #pending = new Map<string, PendingRequest>();
|
|
100
101
|
readonly #reconnectDelaysMs: readonly number[];
|
|
101
102
|
readonly #socketPath: string;
|
|
102
|
-
#
|
|
103
|
+
#decoder = new JsonLineDecoder();
|
|
103
104
|
#generation = 0;
|
|
105
|
+
#incompatible = false;
|
|
104
106
|
#nextId = 1;
|
|
105
107
|
#reconnectAttempt = 0;
|
|
106
108
|
#reconnectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -116,6 +118,13 @@ export class ReconnectingDaemonClient {
|
|
|
116
118
|
queueMicrotask(() => this.#connect());
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
resetForSession(): void {
|
|
122
|
+
if (this.#state === "closed") return;
|
|
123
|
+
this.#incompatible = false;
|
|
124
|
+
this.#reconnectAttempt = 0;
|
|
125
|
+
this.#connect();
|
|
126
|
+
}
|
|
127
|
+
|
|
119
128
|
close(): void {
|
|
120
129
|
if (this.#state === "closed") return;
|
|
121
130
|
this.#state = "closed";
|
|
@@ -143,11 +152,11 @@ export class ReconnectingDaemonClient {
|
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
#connect(): void {
|
|
146
|
-
if (this.#state === "closed" || this.#
|
|
155
|
+
if (this.#state === "closed" || this.#incompatible) return;
|
|
147
156
|
this.#state = "connecting";
|
|
148
157
|
this.#generation += 1;
|
|
149
158
|
const generation = this.#generation;
|
|
150
|
-
this.#
|
|
159
|
+
this.#decoder = new JsonLineDecoder();
|
|
151
160
|
const socket = createConnection(this.#socketPath);
|
|
152
161
|
this.#socket = socket;
|
|
153
162
|
let disconnected = false;
|
|
@@ -177,11 +186,12 @@ export class ReconnectingDaemonClient {
|
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
#handleDisconnect(error: Error, generation: number): void {
|
|
180
|
-
if (
|
|
189
|
+
if (this.#state === "closed" || this.#incompatible) return;
|
|
181
190
|
this.#state = "idle";
|
|
182
191
|
this.#socket = undefined;
|
|
183
192
|
this.#rejectAll(error);
|
|
184
193
|
this.onDisconnected?.(error);
|
|
194
|
+
if (this.#incompatible) return;
|
|
185
195
|
const delay =
|
|
186
196
|
this.#reconnectDelaysMs[
|
|
187
197
|
Math.min(this.#reconnectAttempt, this.#reconnectDelaysMs.length - 1)
|
|
@@ -194,31 +204,39 @@ export class ReconnectingDaemonClient {
|
|
|
194
204
|
}
|
|
195
205
|
|
|
196
206
|
#handleData(chunk: string): void {
|
|
197
|
-
this.#
|
|
198
|
-
|
|
199
|
-
while (newline >= 0) {
|
|
200
|
-
const line = this.#buffer.slice(0, newline).trim();
|
|
201
|
-
this.#buffer = this.#buffer.slice(newline + 1);
|
|
202
|
-
newline = this.#buffer.indexOf("\n");
|
|
203
|
-
if (!line) continue;
|
|
204
|
-
const message = JSON.parse(line) as RpcMessage;
|
|
205
|
-
if (
|
|
206
|
-
message.method === "agent.event" ||
|
|
207
|
-
message.method === "agent.context.changed" ||
|
|
208
|
-
message.method === "agent.orchestrator.changed"
|
|
209
|
-
) {
|
|
210
|
-
this.onStreamMessage?.(message as DaemonStreamMessage);
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (message.id === undefined) continue;
|
|
214
|
-
const pending = this.#pending.get(String(message.id));
|
|
215
|
-
if (!pending) continue;
|
|
216
|
-
this.#pending.delete(String(message.id));
|
|
217
|
-
if (message.error) pending.reject(new Error(message.error.message ?? "Herdsman RPC failed"));
|
|
218
|
-
else pending.resolve(message.result);
|
|
207
|
+
for (const message of this.#decoder.push(chunk)) {
|
|
208
|
+
this.#handleMessage(message as RpcMessage);
|
|
219
209
|
}
|
|
220
210
|
}
|
|
221
211
|
|
|
212
|
+
#handleMessage(message: RpcMessage): void {
|
|
213
|
+
if (
|
|
214
|
+
message.method === "agent.event" ||
|
|
215
|
+
message.method === "agent.context.changed" ||
|
|
216
|
+
message.method === "agent.orchestrator.changed"
|
|
217
|
+
) {
|
|
218
|
+
this.onStreamMessage?.(message as DaemonStreamMessage);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (message.id === undefined) return;
|
|
222
|
+
const pending = this.#pending.get(String(message.id));
|
|
223
|
+
if (!pending) return;
|
|
224
|
+
this.#pending.delete(String(message.id));
|
|
225
|
+
if (message.error) {
|
|
226
|
+
const error = new Error(message.error.message ?? "Herdsman RPC failed");
|
|
227
|
+
pending.reject(error);
|
|
228
|
+
if (/unknown|not found|unsupported|method/i.test(error.message)) {
|
|
229
|
+
const incompatible = new Error("Herdsman daemon version is incompatible; restart the session");
|
|
230
|
+
this.#incompatible = true;
|
|
231
|
+
this.#state = "idle";
|
|
232
|
+
this.#socket?.destroy();
|
|
233
|
+
this.#socket = undefined;
|
|
234
|
+
this.#rejectAll(incompatible);
|
|
235
|
+
this.onDisconnected?.(incompatible);
|
|
236
|
+
}
|
|
237
|
+
} else pending.resolve(message.result);
|
|
238
|
+
}
|
|
239
|
+
|
|
222
240
|
#rejectAll(error: Error): void {
|
|
223
241
|
for (const pending of this.#pending.values()) pending.reject(error);
|
|
224
242
|
this.#pending.clear();
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export type HerdsmanDaemonClient = {
|
|
|
56
56
|
onConnected: (() => Promise<void> | void) | undefined;
|
|
57
57
|
onDisconnected: ((error: Error) => void) | undefined;
|
|
58
58
|
onStreamMessage: ((message: DaemonStreamMessage) => void) | undefined;
|
|
59
|
+
resetForSession?(): void;
|
|
59
60
|
request(method: string, params: unknown): Promise<unknown>;
|
|
60
61
|
};
|
|
61
62
|
|
|
@@ -352,11 +353,10 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
352
353
|
const loseRole = (ctx: PiContext | undefined, options: { abort?: boolean } = {}) => {
|
|
353
354
|
if (state.deliveredBatch) {
|
|
354
355
|
state.deliveredBatch.invalidated = true;
|
|
355
|
-
|
|
356
|
-
if (lastEventId !== undefined) {
|
|
356
|
+
if (options.abort) {
|
|
357
357
|
state.failedWakeThroughEventId = Math.max(
|
|
358
358
|
state.failedWakeThroughEventId,
|
|
359
|
-
|
|
359
|
+
...state.deliveredBatch.events.map((event) => event.id),
|
|
360
360
|
);
|
|
361
361
|
}
|
|
362
362
|
if (
|
|
@@ -571,6 +571,11 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
571
571
|
})
|
|
572
572
|
.catch((error) => {
|
|
573
573
|
state.connected = false;
|
|
574
|
+
const incompatibleMessage =
|
|
575
|
+
error instanceof Error && /incompatible/i.test(error.message)
|
|
576
|
+
? error.message
|
|
577
|
+
: undefined;
|
|
578
|
+
if (incompatibleMessage) ctx.ui.notify?.(incompatibleMessage, "error");
|
|
574
579
|
markDisconnected(ctx);
|
|
575
580
|
throw error;
|
|
576
581
|
})
|
|
@@ -645,6 +650,7 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
645
650
|
}
|
|
646
651
|
state.client?.close();
|
|
647
652
|
const client = options.clientFactory?.() ?? new ReconnectingDaemonClient({ socketPath: defaultSocketPath() });
|
|
653
|
+
client.resetForSession?.();
|
|
648
654
|
state.client = client;
|
|
649
655
|
client.onConnected = () => registerPresence(ctx);
|
|
650
656
|
client.onDisconnected = () => {
|
|
@@ -731,13 +737,6 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
731
737
|
const stillOwner =
|
|
732
738
|
state.isOrchestrator && state.currentScope?.terminalId === batch.ownerTerminalId;
|
|
733
739
|
const failBatch = () => {
|
|
734
|
-
const lastEventId = batch.events.at(-1)?.id;
|
|
735
|
-
if (lastEventId !== undefined) {
|
|
736
|
-
state.failedWakeThroughEventId = Math.max(
|
|
737
|
-
state.failedWakeThroughEventId,
|
|
738
|
-
lastEventId,
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
740
|
ctx.ui.notify?.(
|
|
742
741
|
"Herdsman couldn’t acknowledge agent updates · updates remain pending",
|
|
743
742
|
"warning",
|
|
@@ -773,11 +772,9 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
|
|
|
773
772
|
"Herdsman couldn’t acknowledge agent updates · updates remain pending",
|
|
774
773
|
"warning",
|
|
775
774
|
);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
failedEventId,
|
|
780
|
-
);
|
|
775
|
+
// Keep the event pending so a later delivery can retry it. A server-side
|
|
776
|
+
// rejection may mean the pane was invalidated; advancing the wake cursor
|
|
777
|
+
// here would make a reconnect skip a still-pending event.
|
|
781
778
|
break;
|
|
782
779
|
}
|
|
783
780
|
}
|