Files
openclaw/src/node-host/worker-support.ts
Peter Steinberger f167089d61 feat(nodes): continue Claude catalog sessions on paired nodes via streaming CLI agent runs (#105833)
* feat(gateway): stream node.invoke progress with idle timeouts and cancel

* feat(node-host): opt-in approval-gated claude cli agent run command

* feat(agents): run node-placed claude-cli turns through streaming node invoke

* feat(anthropic): continue claude catalog sessions on advertising nodes

* docs(nodes): document paired-node claude agent runs and continuation

* chore(plugin-sdk): admit spawn-invocation helper into surface budget

* fix(nodes): harden streaming invoke and node run policy from review findings

* test(node-host): prove multi-token tool-policy argv fails closed

* fix(nodes): allowlist cancel event for apps and break node-host type cycle

* chore(protocol): regenerate Swift gateway models for node invoke progress

* chore(node-host): drop test-only export surface flagged by deadcode ratchet

* style(node-host): merge invoke-types imports

* refactor(nodes): split node agent run modules to satisfy the LOC ratchet

* chore(protocol): regenerate android gateway models and sync export baseline

* chore(plugin-sdk): admit spawn-invocation helper and inherited deprecated drift into surface budget

* chore(plugin-sdk): tolerate inherited agent-core deprecated export

* fix(anthropic): drop node param parser orphaned by upstream catalog split

* ci: sync unused-export baseline with healed main

* test(agents): adopt renamed cli backend prepare helper

* style(node-host): format claude run spawn invocation

* ci: retrigger checks

* chore(plugin-sdk): pin surface budgets to healed-base counts

* fix(protocol): restore untyped lazy validators after upstream style change
2026-07-13 07:39:22 -07:00

86 lines
2.7 KiB
TypeScript

import type { GatewayClientRequestOptions } from "../gateway/client.js";
import type { NodeHostClient } from "./client.js";
export type NodeHostWorkerGatewayResponse =
| { type: "gateway-response"; id: string; ok: true; result: unknown }
| { type: "gateway-response"; id: string; ok: false; error: string };
type PendingGatewayRequest = {
resolve: (value: unknown) => void;
reject: (error: Error) => void;
timer: NodeJS.Timeout;
};
export class NodeHostWorkerBridgeClient implements NodeHostClient {
private nextRequestId = 1;
private readonly pending = new Map<string, PendingGatewayRequest>();
constructor(private readonly writeMessage: (message: unknown) => void) {}
async request<T = Record<string, unknown>>(
method: string,
params?: unknown,
opts?: GatewayClientRequestOptions,
): Promise<T> {
if (method === "node.invoke.result") {
this.writeMessage({ type: "invoke-result", result: params ?? {} });
return {} as T;
}
if (method === "node.invoke.progress") {
// Unreachable while the embedded app worker never enables agent runs
// (the only progress emitter); the Mac host bridge has no case for it.
this.writeMessage({ type: "invoke-progress", progress: params ?? {} });
return {} as T;
}
if (method === "node.event") {
this.writeMessage({ type: "node-event", event: params ?? {} });
return {} as T;
}
const id = `gateway-${this.nextRequestId++}`;
const timeoutMs = Math.max(1, opts?.timeoutMs ?? 15_000);
const response = new Promise<unknown>((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`Gateway request timed out: ${method}`));
}, timeoutMs);
this.pending.set(id, { resolve, reject, timer });
});
this.writeMessage({ type: "gateway-request", id, method, params: params ?? {}, timeoutMs });
return (await response) as T;
}
handleResponse(message: NodeHostWorkerGatewayResponse): boolean {
const pending = this.pending.get(message.id);
if (!pending) {
return false;
}
this.pending.delete(message.id);
clearTimeout(pending.timer);
if (message.ok) {
pending.resolve(message.result);
} else {
pending.reject(new Error(message.error));
}
return true;
}
close(): void {
for (const pending of this.pending.values()) {
clearTimeout(pending.timer);
pending.reject(new Error("node-host worker stopped"));
}
this.pending.clear();
}
}
export async function stopNodeHostWorkerFromSignal(
input: { close(): void },
stop: (exitCode: number) => Promise<void>,
exitCode: number,
): Promise<void> {
const stopped = stop(exitCode);
input.close();
await stopped;
}