@roll-agent/client-node 0.2.1 → 0.3.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/README.md +72 -6
- package/dist/index.d.ts +37 -8
- package/dist/index.js +1 -1
- package/dist/runtime-event-recovery.d.ts +127 -0
- package/dist/runtime-event-recovery.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# `@roll-agent/client-node`
|
|
2
2
|
|
|
3
3
|
面向 Node.js、Electron 主进程和本地 Companion 的 Roll Runtime Protocol v1 客户端。
|
|
4
|
-
|
|
4
|
+
客户端优先协商 `"1.3"`,并在 `client.capabilities.set` ACK 完成后才结束连接;旧 Runtime
|
|
5
|
+
继续精确回落到 `"1.2"` / `"1.1"` / `"1.0"`。
|
|
5
6
|
|
|
6
7
|
它负责启动或连接 Runtime、初始化协商、JSON-RPC 请求、流式事件、协议校验、未知 Turn
|
|
7
8
|
结果跟踪,以及可等待的有界子进程关闭。Renderer 不应直接持有该客户端或子进程句柄。
|
|
@@ -13,6 +14,12 @@
|
|
|
13
14
|
`RollNodeClient.start()` 默认执行 `roll runtime serve --stdio`。宿主必须确保 `roll` 命令
|
|
14
15
|
已安装,或通过 `command` 指向随应用分发的 Runtime;这仍然只建立本地 stdio 连接。
|
|
15
16
|
|
|
17
|
+
默认本地 `maxFrameBytes` 为 `17 MiB`。广告 Runtime Protocol `"1.3"` 表示 Client 能接收
|
|
18
|
+
至少 `17 MiB` 的单个 Runtime→Client NDJSON 帧;若调用方显式配置更低上限,Client 会从
|
|
19
|
+
初始化广告中移除 `"1.3"`,但仍可协商旧版本。初始化后的 Client→Runtime 出站上限仍是
|
|
20
|
+
`min(client.maxFrameBytes, initialize.limits.maxFrameBytes)`;当前 Runtime 默认接收入站帧
|
|
21
|
+
上限为 `4 MiB`,不会因为 Client 的 17 MiB 入站能力而扩大。
|
|
22
|
+
|
|
16
23
|
## 安装
|
|
17
24
|
|
|
18
25
|
```bash
|
|
@@ -59,10 +66,23 @@ try {
|
|
|
59
66
|
`onTurnOutcomeUnknown()` 作为本地活动状态的收敛信号:停止 Working 状态、结束本地 waiter、
|
|
60
67
|
保留部分输出,随后在健康连接上或重连后读取 Snapshot。未知副作用不能自动重放。
|
|
61
68
|
|
|
69
|
+
初始化后的 `request()` 会按 negotiated version 校验参数与结果。返回类型由各版本 Schema
|
|
70
|
+
派生为 union;读取 1.2+ Snapshot 的 `pendingInteractions` 或 1.3 Snapshot 的 `eventCursor`
|
|
71
|
+
前,可分别用 `"pendingInteractions" in snapshot` / `"eventCursor" in snapshot` 缩窄。
|
|
72
|
+
1.2 / 1.1 / 1.0 Snapshot 保持冻结形状。
|
|
73
|
+
|
|
62
74
|
## Runtime→Client Request
|
|
63
75
|
|
|
64
|
-
- `
|
|
65
|
-
|
|
76
|
+
- `"1.3"` / `"1.2"` 没有强制 Handler;使用默认帧预算时,即使没有 Handler 也会广告
|
|
77
|
+
`["1.3","1.2","1.0"]`,并以 revision 1 ACK 空能力集合;只有 `userInput.request` 时
|
|
78
|
+
仍保持该集合,存在 `approval.request` 时才同时广告要求该 Handler 的 1.1;
|
|
79
|
+
- 显式把 `maxFrameBytes` 设为低于 `17 MiB` 时不会广告 `"1.3"`,因为该预算无法保证接收
|
|
80
|
+
最大 durable event frame;`"1.2"` / `"1.1"` / `"1.0"` 的回落规则保持不变;
|
|
81
|
+
- `connect()` / `start()` 只有在 `"1.3"` / `"1.2"` 初始 capability ACK 后才 resolve,ACK 前不会把
|
|
82
|
+
Runtime Server Request 投递给 Handler;
|
|
83
|
+
- ACK 的 accepted methods 可以为空或为请求集的任意子集,并按 Runtime registry 排序;Client
|
|
84
|
+
按集合语义应用结果。只有 revision 不匹配或出现未请求 method 才是协议违例;未被接受的
|
|
85
|
+
method 若仍被 Runtime 请求,Client 返回 `-32601`;
|
|
66
86
|
- `"1.1"` 的 `approval.request` handler 是唯一审批写入路径,
|
|
67
87
|
`approval.required` 只是只读 View Event;
|
|
68
88
|
- Handler context 提供 `requestId` 和 `AbortSignal`。Runtime cancel、Client shutdown
|
|
@@ -70,17 +90,63 @@ try {
|
|
|
70
90
|
- 用户拒绝必须返回正常 `{ decision: "reject" }`;Handler 缺失、抛错或返回非法结果会
|
|
71
91
|
让 Runtime 以系统失败终止当前 Turn,不会伪装成 `user_rejected`;
|
|
72
92
|
- `approval.resolved` 用于关闭或更新审批 View;
|
|
93
|
+
- 已协商 `"1.3"` / `"1.2"` 后,新增或撤销 Handler 会串行发送递增 revision;同 method 的 Handler
|
|
94
|
+
替换只在本地原子完成,不产生无意义 revision,旧 disposer 不会误删替代者;
|
|
95
|
+
- 撤销 `"1.3"` / `"1.2"` 能力会立即 abort 该 method 的未决 Interaction,并抑制迟到 Response;
|
|
96
|
+
`runtime.serverRequest.cancel` 使用 `interactionId`,不会与 JSON-RPC request id 混用;
|
|
97
|
+
- `userInput.request` 可通过构造选项或实例方法 `onUserInputRequest()` 注册 typed named
|
|
98
|
+
Handler。用户按 Esc 或关闭表单应返回正常 `{ status: "cancelled", reason? }`,不能 throw;
|
|
99
|
+
完整提交值只返回给当前 Tool 调用,不会出现在 Runtime Event 或 Snapshot;
|
|
73
100
|
- 已协商 `"1.0"` 后不能通过 `registerServerRequestHandler()` 动态升级;应在
|
|
74
101
|
`start()` / `connect()` 时传入初始 handler;
|
|
75
|
-
- 已协商 `"1.1"`
|
|
102
|
+
- 已协商 `"1.1"` 后仍可原子替换 handler;旧 handler 的 disposer 不会误删替代者。
|
|
76
103
|
如果卸载当前版本的必需 handler,Client 会把连接视为协议能力失效并执行有界关闭,
|
|
77
104
|
不会继续以 `"1.1"` 返回 `Method not found`。
|
|
78
105
|
|
|
79
106
|
未显式传入 `clientVersion` 时,初始化信息直接读取当前安装的
|
|
80
107
|
`@roll-agent/client-node` 包版本,不需要在源码与 `package.json` 之间手工同步。
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
|
|
109
|
+
## Runtime Event 恢复(Protocol 1.3)
|
|
110
|
+
|
|
111
|
+
`createEventRecovery()` 返回当前连接唯一的 `RuntimeEventRecoveryManager`。它按 Thread claim
|
|
112
|
+
事件,在 `runtime.events.resume` 的 replay→live barrier 前缓冲并按 opaque cursor 排序,按
|
|
113
|
+
`eventId` 去重,并串行调用 durable event handler。只有 handler 成功后 checkpoint 才推进。
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const recovery = client.createEventRecovery();
|
|
117
|
+
|
|
118
|
+
const result = await recovery.resumeThread({
|
|
119
|
+
threadId,
|
|
120
|
+
checkpoint: persistedCheckpoint,
|
|
121
|
+
applySnapshot: async (snapshot, { reason }) => {
|
|
122
|
+
await replaceLocalThreadView(snapshot, reason);
|
|
123
|
+
},
|
|
124
|
+
onDurableEvent: async (event, nextCheckpoint) => {
|
|
125
|
+
// 应把事件应用与 checkpoint 持久化放在同一事务或等价的原子边界内。
|
|
126
|
+
await applyAndCheckpoint(event, nextCheckpoint);
|
|
127
|
+
},
|
|
128
|
+
onEphemeralEvent: (event) => renderLiveDelta(event),
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- 没有 checkpoint、Runtime instance 已变化、cursor 过期或出现 gap 时,Manager 会先应用
|
|
133
|
+
Snapshot,再从 Snapshot 的 `eventCursor` 发起第二次 resume;空日志 cursor 明确使用
|
|
134
|
+
`null`,不能伪造 cursor;
|
|
135
|
+
- 1.3 fallback 请求的是 byte-bounded recovery Snapshot;`applySnapshot` context 的
|
|
136
|
+
`recoveryProjection` 为 `true`,Snapshot 内 timeline 与 pending arrays 故意为空。宿主应把它
|
|
137
|
+
当作 checkpoint 收敛投影,完整 timeline 另发普通 `thread.snapshot` 分页读取;
|
|
138
|
+
- 恢复阶段的 ephemeral event 会丢弃,进入 live 后才交给 `onEphemeralEvent`;durable
|
|
139
|
+
replay 不会重新触发 Approval、User Input、Tool 或 Turn;
|
|
140
|
+
- claimed Thread 不再同时投递给 raw `onEvent()`。不要让同一 Thread 同时走两套状态应用
|
|
141
|
+
路径;未 claim 的 Thread 仍正常投递给 `onEvent()`;
|
|
142
|
+
- 默认缓冲上限为 10,000 条且不超过 32 MiB;越界会回退 Snapshot 收敛;
|
|
143
|
+
- Protocol 1.2 / 1.1 / 1.0 明确返回 `mode: "snapshot-only"`,只应用 Snapshot,绝不发送
|
|
144
|
+
`runtime.events.resume`,其 `recoveryProjection` context 为 `false`,也不提供无损 replay 保证;
|
|
145
|
+
- Manager 只属于当前连接。重连后创建新 Client,把已持久化 checkpoint 传给新的 Manager;
|
|
146
|
+
不要重放旧 Approval、User Input、Tool 或 Turn 请求。
|
|
147
|
+
|
|
148
|
+
当前 `stdio` Transport 不重放普通 JSON-RPC Request。只有 Protocol 1.3 durable Runtime Event
|
|
149
|
+
使用上述恢复通道;断线后的交互状态仍以 Snapshot 收敛。
|
|
84
150
|
|
|
85
151
|
## 文档
|
|
86
152
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
|
-
import {
|
|
2
|
+
import { RuntimeEventRecoveryManager, type RuntimeEventRecoveryManagerOptions } from "./runtime-event-recovery.ts";
|
|
3
|
+
import { RUNTIME_SERVER_REQUEST_METHODS, type InitializeResult, type JsonRpcId, type LatestRuntimeMethod, type RuntimeEventEnvelope, type RuntimeMethod, type RuntimeMethodInput, type RuntimeMethodResultForVersion, type RuntimeProtocolErrorDataForVersion, type RuntimeProtocolVersion, type RuntimeServerRequestMethod, type RuntimeServerRequestParamsForSupportedVersions, type RuntimeServerRequestResultForSupportedVersions, type TurnId } from "@roll-agent/protocol";
|
|
4
|
+
export { DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES, DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS, RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS, RuntimeEventRecoveryError, RuntimeEventRecoveryManager, type RuntimeDurableEventEnvelope, type RuntimeEphemeralEventEnvelope, type RuntimeEventRecoveryBridge, type RuntimeEventRecoveryCheckpoint, type RuntimeEventRecoveryErrorContext, type RuntimeEventRecoveryManagerOptions, type RuntimeEventRecoveryMode, type RuntimeEventRecoverySnapshot, type RuntimeEventRecoverySnapshotContext, type RuntimeEventRecoverySnapshotReason, type RuntimeEventRecoveryStartResult, type RuntimeEventRecoveryThreadOptions, } from "./runtime-event-recovery.ts";
|
|
3
5
|
export interface RuntimeClientTransport {
|
|
4
6
|
readonly stdin: Writable;
|
|
5
7
|
readonly stdout: Readable;
|
|
@@ -18,10 +20,14 @@ export interface RuntimeServerRequestContext {
|
|
|
18
20
|
readonly requestId: JsonRpcId;
|
|
19
21
|
readonly signal: AbortSignal;
|
|
20
22
|
}
|
|
21
|
-
export type
|
|
23
|
+
export type RuntimeServerRequestHandlerParams<TMethod extends RuntimeServerRequestMethod> = RuntimeServerRequestParamsForSupportedVersions<TMethod>;
|
|
24
|
+
export type RuntimeServerRequestHandler<TMethod extends RuntimeServerRequestMethod> = (params: RuntimeServerRequestHandlerParams<TMethod>, context: RuntimeServerRequestContext) => RuntimeServerRequestResultForSupportedVersions<TMethod> | Promise<RuntimeServerRequestResultForSupportedVersions<TMethod>>;
|
|
22
25
|
export type RuntimeServerRequestHandlers = {
|
|
23
26
|
readonly [TMethod in RuntimeServerRequestMethod]?: RuntimeServerRequestHandler<TMethod>;
|
|
24
27
|
};
|
|
28
|
+
export type UserInputRequestHandlerParams = RuntimeServerRequestHandlerParams<typeof RUNTIME_SERVER_REQUEST_METHODS.userInputRequest>;
|
|
29
|
+
export type UserInputRequestHandler = RuntimeServerRequestHandler<typeof RUNTIME_SERVER_REQUEST_METHODS.userInputRequest>;
|
|
30
|
+
export type RuntimeClientMethodResult<TMethod extends RuntimeMethod> = RuntimeMethodResultForVersion<RuntimeProtocolVersion, TMethod>;
|
|
25
31
|
export interface RollNodeClientOptions {
|
|
26
32
|
readonly cwd: string;
|
|
27
33
|
readonly command?: string;
|
|
@@ -37,6 +43,7 @@ export interface RollNodeClientOptions {
|
|
|
37
43
|
readonly readRetryDelayMs?: number;
|
|
38
44
|
readonly shutdownOptions?: RuntimeShutdownOptions;
|
|
39
45
|
readonly serverRequestHandlers?: RuntimeServerRequestHandlers;
|
|
46
|
+
readonly onUserInputRequest?: UserInputRequestHandler;
|
|
40
47
|
}
|
|
41
48
|
export interface ConnectRuntimeClientOptions {
|
|
42
49
|
readonly transport: RuntimeClientTransport;
|
|
@@ -50,7 +57,9 @@ export interface ConnectRuntimeClientOptions {
|
|
|
50
57
|
readonly readRetryDelayMs?: number;
|
|
51
58
|
readonly shutdownOptions?: RuntimeShutdownOptions;
|
|
52
59
|
readonly serverRequestHandlers?: RuntimeServerRequestHandlers;
|
|
60
|
+
readonly onUserInputRequest?: UserInputRequestHandler;
|
|
53
61
|
}
|
|
62
|
+
type ClientRuntimeProtocolErrorData = RuntimeProtocolErrorDataForVersion<RuntimeProtocolVersion>;
|
|
54
63
|
export interface RuntimeClientExit {
|
|
55
64
|
readonly code: number | null;
|
|
56
65
|
readonly signal: NodeJS.Signals | null;
|
|
@@ -58,11 +67,11 @@ export interface RuntimeClientExit {
|
|
|
58
67
|
}
|
|
59
68
|
export declare class RollRpcError extends Error {
|
|
60
69
|
readonly code: number;
|
|
61
|
-
readonly data:
|
|
70
|
+
readonly data: ClientRuntimeProtocolErrorData | undefined;
|
|
62
71
|
constructor(error: {
|
|
63
72
|
readonly code: number;
|
|
64
73
|
readonly message: string;
|
|
65
|
-
readonly data?:
|
|
74
|
+
readonly data?: ClientRuntimeProtocolErrorData;
|
|
66
75
|
});
|
|
67
76
|
}
|
|
68
77
|
export declare class RollRuntimeExitedError extends Error {
|
|
@@ -71,9 +80,9 @@ export declare class RollRuntimeExitedError extends Error {
|
|
|
71
80
|
constructor(code: number | null, signal: NodeJS.Signals | null);
|
|
72
81
|
}
|
|
73
82
|
export declare class RollRequestTimeoutError extends Error {
|
|
74
|
-
readonly method:
|
|
83
|
+
readonly method: LatestRuntimeMethod;
|
|
75
84
|
readonly timeoutMs: number;
|
|
76
|
-
constructor(method:
|
|
85
|
+
constructor(method: LatestRuntimeMethod, timeoutMs: number);
|
|
77
86
|
}
|
|
78
87
|
export declare class RollRequestFrameTooLargeError extends Error {
|
|
79
88
|
readonly frameBytes: number;
|
|
@@ -96,7 +105,7 @@ export declare class RollUncorrelatedRpcError extends RollRpcError {
|
|
|
96
105
|
constructor(error: {
|
|
97
106
|
readonly code: number;
|
|
98
107
|
readonly message: string;
|
|
99
|
-
readonly data?:
|
|
108
|
+
readonly data?: ClientRuntimeProtocolErrorData;
|
|
100
109
|
});
|
|
101
110
|
}
|
|
102
111
|
export declare class RollNodeClient {
|
|
@@ -109,8 +118,11 @@ export declare class RollNodeClient {
|
|
|
109
118
|
private readonly activeTurns;
|
|
110
119
|
private readonly unknownTurns;
|
|
111
120
|
private readonly serverRequestHandlers;
|
|
121
|
+
private readonly serverRequestHandlerCapabilityRevisions;
|
|
112
122
|
private readonly advertisedProtocolVersions;
|
|
113
123
|
private readonly inFlightServerRequests;
|
|
124
|
+
private readonly inFlightServerRequestsByInteractionId;
|
|
125
|
+
private acknowledgedServerRequestMethods;
|
|
114
126
|
private readonly onTurnOutcomeUnknown;
|
|
115
127
|
private readonly requestTimeoutMs;
|
|
116
128
|
private readonly maxReadRetries;
|
|
@@ -119,7 +131,10 @@ export declare class RollNodeClient {
|
|
|
119
131
|
private readonly outputReader;
|
|
120
132
|
private readonly stderrReader;
|
|
121
133
|
private requestId;
|
|
134
|
+
private capabilityRevision;
|
|
135
|
+
private acknowledgedCapabilityRevision;
|
|
122
136
|
private writeQueue;
|
|
137
|
+
private capabilitySyncTail;
|
|
123
138
|
private readonly exitPromise;
|
|
124
139
|
private resolveExit;
|
|
125
140
|
private exited;
|
|
@@ -130,14 +145,26 @@ export declare class RollNodeClient {
|
|
|
130
145
|
private connectionFailure;
|
|
131
146
|
private exitResult;
|
|
132
147
|
private shutdownPromise;
|
|
148
|
+
private eventRecoveryManager;
|
|
133
149
|
private constructor();
|
|
134
150
|
static start(options: RollNodeClientOptions): Promise<RollNodeClient>;
|
|
135
151
|
static connect(options: ConnectRuntimeClientOptions): Promise<RollNodeClient>;
|
|
152
|
+
private initializeServerRequestCapabilities;
|
|
153
|
+
private queueServerRequestCapabilitySync;
|
|
154
|
+
private requestClientCapabilitiesSet;
|
|
155
|
+
private acceptClientCapabilitiesSetResult;
|
|
156
|
+
private observeServerRequestCapabilitySync;
|
|
136
157
|
onEvent(listener: (event: RuntimeEventEnvelope) => void): () => void;
|
|
158
|
+
/** Creates the single per-connection manager for durable Runtime Event recovery. */
|
|
159
|
+
createEventRecovery(options?: RuntimeEventRecoveryManagerOptions): RuntimeEventRecoveryManager;
|
|
137
160
|
onExit(listener: (exit: RuntimeClientExit) => void): () => void;
|
|
138
161
|
getInitializationResult(): InitializeResult;
|
|
139
162
|
registerServerRequestHandler<TMethod extends RuntimeServerRequestMethod>(method: TMethod, handler: RuntimeServerRequestHandler<TMethod>): () => void;
|
|
140
|
-
|
|
163
|
+
/** Registers the typed Runtime Protocol 1.3/1.2 userInput.request handler. */
|
|
164
|
+
onUserInputRequest(handler: UserInputRequestHandler): () => void;
|
|
165
|
+
private requestEventRecoverySnapshot;
|
|
166
|
+
private requestRuntimeEventsResume;
|
|
167
|
+
request<TMethod extends RuntimeMethod>(method: TMethod, input: RuntimeMethodInput<TMethod>): Promise<RuntimeClientMethodResult<TMethod>>;
|
|
141
168
|
private requestOnce;
|
|
142
169
|
getOutcomeUnknownTurnIds(): readonly TurnId[];
|
|
143
170
|
close(): void;
|
|
@@ -149,6 +176,8 @@ export declare class RollNodeClient {
|
|
|
149
176
|
private handleServerRequest;
|
|
150
177
|
private observeServerRequestResponse;
|
|
151
178
|
private dispatchServerRequest;
|
|
179
|
+
private retireServerRequest;
|
|
180
|
+
private retireServerRequestsForMethod;
|
|
152
181
|
private isServerRequestActive;
|
|
153
182
|
private writeServerRequestError;
|
|
154
183
|
private acceptInitializationResult;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as e}from"node:child_process";import{once as t}from"node:events";import{createInterface as r}from"node:readline";import i from"../package.json"with{type:"json"};import{RUNTIME_EVENT_NOTIFICATION as o,RUNTIME_METHODS as s,RUNTIME_SERVER_REQUEST_CANCEL_NOTIFICATION as n,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as l,getRuntimeProtocolCapabilities as a,isRuntimeServerRequestMethod as u,isRuntimeServerRequestMethodRequired as c,parseRuntimeMethodParams as d,parseRuntimeMethodResult as h,parseRuntimeServerRequestParams as R,parseRuntimeServerRequestResult as m,runtimeEventEnvelopeSchema as v,runtimeProtocolErrorDataSchema as f,runtimeServerRequestCancelParamsSchema as w}from"@roll-agent/protocol";const p=4194304,g=1,E=100,y=3e4,q=3e4,x=1e4,P=5e3;function S(e){if("object"!=typeof e||null===e||!("version"in e)||"string"!=typeof e.version||0===e.version.trim().length)throw new Error("@roll-agent/client-node package metadata is missing a valid version");return e.version}const T=S(i),M={methodNotFound:-32601,invalidParams:-32602,internalError:-32603},F=new Set([s.threadList,s.threadSnapshot,s.threadCapabilities,s.operationGet]);function V(e,t){return a(e).requiredServerRequestMethods.every(e=>"function"==typeof t[e])}export class RollRpcError extends Error{code;data;constructor(e){super(e.message),this.name="RollRpcError",this.code=e.code,this.data=e.data}}export class RollRuntimeExitedError extends Error{code;signal;constructor(e,t){super(`Roll Runtime exited before the request completed (code=${String(e)}, signal=${String(t)})`),this.name="RollRuntimeExitedError",this.code=e,this.signal=t}}export class RollRequestTimeoutError extends Error{method;timeoutMs;constructor(e,t){super(`Roll Runtime request "${e}" timed out after ${String(t)} ms`),this.name="RollRequestTimeoutError",this.method=e,this.timeoutMs=t}}export class RollRequestFrameTooLargeError extends Error{frameBytes;maxFrameBytes;constructor(e,t){super(`Roll Runtime request frame is ${String(e)} bytes, exceeding the ${String(t)} byte limit`),this.name="RollRequestFrameTooLargeError",this.frameBytes=e,this.maxFrameBytes=t}}export class RollRuntimeClosingError extends Error{constructor(){super("Roll Runtime client is shutting down"),this.name="RollRuntimeClosingError"}}export class RollRuntimeShutdownTimeoutError extends Error{gracefulTimeoutMs;terminateTimeoutMs;forceKillTimeoutMs;constructor(e,t,r){super(`Roll Runtime did not exit within ${String(e+t+r)} ms`),this.name="RollRuntimeShutdownTimeoutError",this.gracefulTimeoutMs=e,this.terminateTimeoutMs=t,this.forceKillTimeoutMs=r}}export class RollProtocolViolationError extends Error{constructor(e,t){super(e,void 0===t?void 0:{cause:t}),this.name="RollProtocolViolationError"}}export class RollUncorrelatedRpcError extends RollRpcError{constructor(e){super(e),this.name="RollUncorrelatedRpcError"}}function b(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function O(e){return"string"==typeof e||"number"==typeof e&&Number.isFinite(e)}function C(e,t){if(!Number.isInteger(e)||e<=0)throw new Error(`${t} must be a positive integer`);return e}function k(e={}){return{gracefulTimeoutMs:C(e.gracefulTimeoutMs??q,"gracefulTimeoutMs"),terminateTimeoutMs:C(e.terminateTimeoutMs??x,"terminateTimeoutMs"),forceKillTimeoutMs:C(e.forceKillTimeoutMs??P,"forceKillTimeoutMs")}}function N(e,t){if(!b(e)||"number"!=typeof e.code||!Number.isInteger(e.code)||"string"!=typeof e.message)throw new RollProtocolViolationError(`Roll Runtime returned an invalid error for ${t}`);const r=void 0===e.data?void 0:f.safeParse(e.data);if(void 0!==r&&!r.success)throw new RollProtocolViolationError(`Roll Runtime returned invalid error data for ${t}`,r.error);return{code:e.code,message:e.message,...!0===r?.success?{data:r.data}:{}}}function z(e,t){return new Promise((r,i)=>{const o=setTimeout(()=>r(void 0),t);e.then(e=>{clearTimeout(o),r(e)},e=>{clearTimeout(o),i(e instanceof Error?e:new Error(String(e)))})})}function I(e){return{stdin:e.stdin,stdout:e.stdout,stderr:e.stderr,onExit(t){let r=!1;const i=(e,i)=>{r||(r=!0,t(e,i))};e.once("close",i),e.once("error",()=>i(null,null))},close(){e.stdin.end()},terminate(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGTERM")},forceClose(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGKILL")}}}export class RollNodeClient{transport;maxFrameBytes;outboundMaxFrameBytes;pending=new Map;listeners=new Set;exitListeners=new Set;activeTurns=new Set;unknownTurns=new Set;serverRequestHandlers;advertisedProtocolVersions;inFlightServerRequests=new Map;onTurnOutcomeUnknown;requestTimeoutMs;maxReadRetries;readRetryDelayMs;defaultShutdownOptions;outputReader;stderrReader;requestId=0;writeQueue=Promise.resolve();exitPromise;resolveExit;exited=!1;closing=!1;outputReaderClosed=!1;stderrReaderClosed=!1;initializationResult;connectionFailure;exitResult;shutdownPromise;constructor(e){if(this.transport=e.transport,this.maxFrameBytes=e.maxFrameBytes??p,this.outboundMaxFrameBytes=this.maxFrameBytes,this.onTurnOutcomeUnknown=e.onTurnOutcomeUnknown,this.requestTimeoutMs=e.requestTimeoutMs??y,this.maxReadRetries=e.maxReadRetries??1,this.readRetryDelayMs=e.readRetryDelayMs??E,this.defaultShutdownOptions=k(e.shutdownOptions),this.serverRequestHandlers={...e.serverRequestHandlers},this.advertisedProtocolVersions=l.filter(e=>V(e,this.serverRequestHandlers)),!Number.isInteger(this.maxFrameBytes)||this.maxFrameBytes<=0)throw new Error("maxFrameBytes must be a positive integer");if(!Number.isInteger(this.requestTimeoutMs)||this.requestTimeoutMs<=0)throw new Error("requestTimeoutMs must be a positive integer");if(!Number.isInteger(this.maxReadRetries)||this.maxReadRetries<0)throw new Error("maxReadRetries must be a non-negative integer");if(!Number.isInteger(this.readRetryDelayMs)||this.readRetryDelayMs<0)throw new Error("readRetryDelayMs must be a non-negative integer");this.exitPromise=new Promise(e=>{this.resolveExit=e}),this.outputReader=r({input:e.transport.stdout}),this.outputReader.on("line",e=>this.handleLine(e)),this.stderrReader=void 0===e.transport.stderr?void 0:r({input:e.transport.stderr}),this.stderrReader?.on("line",t=>{try{e.onStderr?.(t)}catch{}}),this.transport.onExit((e,t)=>this.handleExit(e,t))}static async start(t){if(0===t.cwd.trim().length)throw new Error("RollNodeClient requires an explicit non-empty cwd");const r=e(t.command??"roll",[...t.args??["runtime","serve","--stdio"]],{cwd:t.cwd,env:t.env??process.env,shell:!1,stdio:"pipe"});return await RollNodeClient.connect({transport:I(r),...void 0!==t.clientName?{clientName:t.clientName}:{},...void 0!==t.clientVersion?{clientVersion:t.clientVersion}:{},...void 0!==t.onStderr?{onStderr:t.onStderr}:{},...void 0!==t.onTurnOutcomeUnknown?{onTurnOutcomeUnknown:t.onTurnOutcomeUnknown}:{},...void 0!==t.maxFrameBytes?{maxFrameBytes:t.maxFrameBytes}:{},...void 0!==t.requestTimeoutMs?{requestTimeoutMs:t.requestTimeoutMs}:{},...void 0!==t.maxReadRetries?{maxReadRetries:t.maxReadRetries}:{},...void 0!==t.readRetryDelayMs?{readRetryDelayMs:t.readRetryDelayMs}:{},...void 0!==t.shutdownOptions?{shutdownOptions:t.shutdownOptions}:{},...void 0!==t.serverRequestHandlers?{serverRequestHandlers:t.serverRequestHandlers}:{}})}static async connect(e){let t;try{return t=new RollNodeClient(e),t.initializationResult=await t.request(s.initialize,{protocolVersions:[...t.advertisedProtocolVersions],client:{name:e.clientName??"@roll-agent/client-node",version:e.clientVersion??T}}),t.acceptInitializationResult(t.initializationResult),t}catch(r){throw void 0===t?e.transport.close():await t.shutdown().catch(()=>{}),r}}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}onExit(e){const t=this.exitResult;return void 0!==t?(queueMicrotask(()=>{try{e(t)}catch{}}),()=>{}):(this.exitListeners.add(e),()=>{this.exitListeners.delete(e)})}getInitializationResult(){if(void 0===this.initializationResult)throw new Error("Roll Runtime client has not completed initialization");return this.initializationResult}registerServerRequestHandler(e,t){if(!u(e))throw new Error(`Unknown Runtime server request method: ${String(e)}`);const r=this.initializationResult?.protocolVersion;if(void 0!==r&&!a(r).serverRequests)throw new Error(`Cannot register a Runtime server request handler after Protocol ${r} was negotiated; pass serverRequestHandlers to RollNodeClient.connect() or RollNodeClient.start()`);return this.serverRequestHandlers[e]=t,()=>{if(this.serverRequestHandlers[e]!==t)return;const r=this.initializationResult?.protocolVersion;void 0!==r&&c(r,e)?this.failProtocol(new RollProtocolViolationError(`Cannot unregister required Runtime server request handler "${e}" while Protocol ${r} is active`)):delete this.serverRequestHandlers[e]}}async request(e,t){if(void 0!==this.connectionFailure)throw this.connectionFailure;if(this.exited)throw this.exitResult?.error??new RollRuntimeExitedError(null,null);if(this.closing)throw new RollRuntimeClosingError;if(e===s.initialize&&void 0!==this.initializationResult)throw new Error("Roll Runtime client has already completed initialization");const r=d(e,t),i=e===s.turnStart?d(s.turnStart,r).turnId:void 0;void 0!==i&&this.activeTurns.add(i);let o=0;for(;;)try{return await this.requestOnce(e,r)}catch(t){if(!F.has(e)||!(t instanceof RollRpcError)||!0!==t.data?.retryable||o>=this.maxReadRetries)throw void 0!==i&&(t instanceof RollRpcError&&!(t instanceof RollUncorrelatedRpcError)||t instanceof RollRequestFrameTooLargeError?this.activeTurns.delete(i):this.markTurnOutcomeUnknown(i)),t;o+=1,await new Promise(e=>{setTimeout(e,this.readRetryDelayMs)})}}async requestOnce(e,t){const r=this.requestId;this.requestId+=1;const i=new Promise((t,i)=>{const o=setTimeout(()=>{this.pending.delete(r)&&i(new RollRequestTimeoutError(e,this.requestTimeoutMs))},this.requestTimeoutMs);this.pending.set(r,{method:e,resolve:t,reject:i,timer:o})});try{await this.write({jsonrpc:"2.0",id:r,method:e,params:t});const o=await i;try{return h(e,o)}catch(t){const r=new RollProtocolViolationError(`Roll Runtime returned an invalid result for "${e}"`,t);throw this.failProtocol(r),r}}catch(e){const t=this.pending.get(r);throw void 0!==t&&(clearTimeout(t.timer),this.pending.delete(r)),e}}getOutcomeUnknownTurnIds(){return[...this.unknownTurns]}close(){this.shutdown().catch(()=>{})}shutdown(e){const t=this.exitResult;return void 0!==t?Promise.resolve(t):(this.shutdownPromise??=this.performShutdown(void 0===e?this.defaultShutdownOptions:k(e)),this.shutdownPromise)}async performShutdown(e){const{gracefulTimeoutMs:t,terminateTimeoutMs:r,forceKillTimeoutMs:i}=e;this.closing=!0,this.abortServerRequests(new RollRuntimeClosingError),this.transport.close();const o=await z(this.exitPromise,t);if(void 0!==o)return o;if(void 0!==this.transport.terminate){this.transport.terminate();const e=await z(this.exitPromise,r);if(void 0!==e)return e}if(void 0===this.transport.forceClose){const e=new RollRuntimeShutdownTimeoutError(t,r,i);throw this.finishExit(e,null,null),this.closeReaders(),e}this.transport.forceClose();const s=await z(this.exitPromise,i);if(void 0!==s)return s;const n=new RollRuntimeShutdownTimeoutError(t,r,i);throw this.finishExit(n,null,null),this.closeReaders(),n}async write(e,r){const i=JSON.stringify(e),o=Buffer.byteLength(i,"utf8");if(o>this.outboundMaxFrameBytes)throw new RollRequestFrameTooLargeError(o,this.outboundMaxFrameBytes);const s=`${i}\n`;this.writeQueue=this.writeQueue.then(async()=>{this.closing||void 0!==this.connectionFailure||this.exited||!1===r?.()||this.transport.stdin.write(s)||await t(this.transport.stdin,"drain")});try{await this.writeQueue}catch(e){const t=new RollProtocolViolationError("Failed to write to the Roll Runtime transport",e);throw this.failProtocol(t),t}}handleLine(e){if(Buffer.byteLength(e,"utf8")>this.maxFrameBytes)return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime frame exceeds ${String(this.maxFrameBytes)} bytes`));let t;try{t=JSON.parse(e)}catch(e){return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted invalid JSON",e))}if(!b(t)||"2.0"!==t.jsonrpc)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid JSON-RPC frame"));const r=Object.hasOwn(t,"method"),i=Object.hasOwn(t,"id"),o=Object.hasOwn(t,"result"),n=Object.hasOwn(t,"error");if(r)return"string"!=typeof t.method||o||n?void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid JSON-RPC request or notification")):i?O(t.id)?void this.handleServerRequest(t.id,t.method,t.params):void this.failProtocol(new RollProtocolViolationError("Roll Runtime request has an invalid id")):void this.handleNotification(t.method,t.params);if(!i)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response is missing an id"));if(null===t.id){if(!n||o)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response with a null id must contain exactly one error"));try{this.failConnection(new RollUncorrelatedRpcError(N(t.error,"an uncorrelated request")))}catch(e){this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError("Roll Runtime returned an invalid uncorrelated error",e))}return}if(!O(t.id))return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response has an invalid id"));const l=this.pending.get(t.id);if(void 0!==l)if(n!==o){if(n){let e;try{e=new RollRpcError(N(t.error,`"${l.method}"`))}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError(`Roll Runtime returned an invalid error for "${l.method}"`,e))}return this.pending.delete(t.id),clearTimeout(l.timer),void l.reject(e)}if(l.method===s.initialize)try{this.acceptInitializationResult(h(s.initialize,t.result))}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError('Roll Runtime returned an invalid result for "initialize"',e))}this.pending.delete(t.id),clearTimeout(l.timer),l.resolve(t.result)}else this.failProtocol(new RollProtocolViolationError(`Roll Runtime response for "${l.method}" must contain exactly one result or error`))}handleNotification(e,t){if(e===o){const e=v.safeParse(t);return e.success?void 0===this.initializationResult||e.data.protocolVersion!==this.initializationResult.protocolVersion?void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted runtime.event outside the negotiated Protocol")):void this.handleEvent(e.data):void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid runtime.event",e.error))}if(e===n){if(void 0===this.initializationResult||!a(this.initializationResult.protocolVersion).serverRequests)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted runtime.serverRequest.cancel outside a negotiated server-request capability"));const e=w.safeParse(t);if(!e.success)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid runtime.serverRequest.cancel",e.error));const r=this.inFlightServerRequests.get(e.data.serverRequestId);return void(void 0!==r&&(this.inFlightServerRequests.delete(e.data.serverRequestId),r.controller.abort(new Error(e.data.reason))))}this.failProtocol(new RollProtocolViolationError(`Roll Runtime emitted an unknown notification: ${e}`))}handleServerRequest(e,t,r){if(this.closing||void 0!==this.connectionFailure||this.exited)return;if(this.inFlightServerRequests.has(e))return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime reused in-flight server request id ${JSON.stringify(e)}`));if(void 0===this.initializationResult||!a(this.initializationResult.protocolVersion).serverRequests||!u(t))return void this.observeServerRequestResponse(this.writeServerRequestError(e,M.methodNotFound,"Method not found"));const i=this.serverRequestHandlers[t];void 0!==i?this.observeServerRequestResponse(this.dispatchServerRequest(e,t,r,i)):this.observeServerRequestResponse(this.writeServerRequestError(e,M.methodNotFound,"Method not found"))}observeServerRequestResponse(e){e.catch(e=>{void 0!==this.connectionFailure||this.exited||this.failProtocol(new RollProtocolViolationError("Failed to respond to Runtime server request",e))})}async dispatchServerRequest(e,t,r,i){const o={controller:new AbortController};this.inFlightServerRequests.set(e,o);try{let s,n,l;try{s=R(t,r)}catch{return void await this.writeServerRequestError(e,M.invalidParams,"Invalid params",()=>this.isServerRequestActive(e,o))}try{n=await i(s,{requestId:e,signal:o.controller.signal})}catch{return void(this.isServerRequestActive(e,o)&&await this.writeServerRequestError(e,M.internalError,"Internal error",()=>this.isServerRequestActive(e,o)))}if(!this.isServerRequestActive(e,o))return;try{l=m(t,n)}catch{return void await this.writeServerRequestError(e,M.internalError,"Internal error",()=>this.isServerRequestActive(e,o))}this.isServerRequestActive(e,o)&&await this.write({jsonrpc:"2.0",id:e,result:l},()=>this.isServerRequestActive(e,o))}finally{this.inFlightServerRequests.get(e)===o&&this.inFlightServerRequests.delete(e)}}isServerRequestActive(e,t){return!t.controller.signal.aborted&&this.inFlightServerRequests.get(e)===t}async writeServerRequestError(e,t,r,i){await this.write({jsonrpc:"2.0",id:e,error:{code:t,message:r}},i)}acceptInitializationResult(e){if(!this.advertisedProtocolVersions.includes(e.protocolVersion))throw new RollProtocolViolationError(`Roll Runtime selected unadvertised Protocol ${e.protocolVersion}`);this.initializationResult=e,this.outboundMaxFrameBytes=Math.min(this.maxFrameBytes,e.limits.maxFrameBytes)}handleEvent(e){void 0===e.turnId||"turn.completed"!==e.event.type&&"turn.cancelled"!==e.event.type&&"turn.failed"!==e.event.type||this.activeTurns.delete(e.turnId);for(const t of this.listeners)try{t(e)}catch{}}handleExit(e,t){const r=this.connectionFailure??new RollRuntimeExitedError(e,t);this.abortServerRequests(r),this.finishExit(r,e,t),this.closeReaders()}abortServerRequests(e){const t=[...this.inFlightServerRequests.values()];this.inFlightServerRequests.clear();for(const r of t)r.controller.abort(e)}failProtocol(e){this.failConnection(e)}failConnection(e){void 0!==this.connectionFailure||this.exited||(this.connectionFailure=e,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.closeOutputReader(),this.shutdown().catch(()=>{}))}closeOutputReader(){this.outputReaderClosed||(this.outputReaderClosed=!0,this.outputReader.close())}closeReaders(){this.closeOutputReader(),void 0===this.stderrReader||this.stderrReaderClosed||(this.stderrReaderClosed=!0,this.stderrReader.close())}finishExit(e,t,r){if(!this.exited){this.exited=!0,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.exitResult={code:t,signal:r,error:e},this.resolveExit(this.exitResult);for(const e of this.exitListeners)try{e(this.exitResult)}catch{}this.exitListeners.clear()}}rejectPending(e){for(const t of this.pending.values())clearTimeout(t.timer),t.reject(e);this.pending.clear()}markActiveTurnsOutcomeUnknown(){for(const e of this.activeTurns)this.markTurnOutcomeUnknown(e);this.activeTurns.clear()}markTurnOutcomeUnknown(e){if(this.activeTurns.delete(e),!this.unknownTurns.has(e)){this.unknownTurns.add(e);try{this.onTurnOutcomeUnknown?.(e)}catch{}}}}
|
|
1
|
+
import{spawn as e}from"node:child_process";import{once as t}from"node:events";import{createInterface as r}from"node:readline";import i from"../package.json"with{type:"json"};import{RuntimeEventRecoveryError as o,RuntimeEventRecoveryManager as n}from"./runtime-event-recovery.js";import{RUNTIME_EVENT_NOTIFICATION as s,RUNTIME_METHODS as l,RUNTIME_SERVER_REQUEST_CANCEL_NOTIFICATION as a,RUNTIME_SERVER_REQUEST_METHODS as u,RUNTIME_V13_MIN_CLIENT_FRAME_BYTES as c,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as d,getRuntimeProtocolCapabilities as h,getRuntimeProtocolRegistry as R,interactionIdSchema as m,isRuntimeServerRequestMethodAvailable as v,isLatestRuntimeServerRequestMethod as w,isRuntimeServerRequestMethodRequired as f,parseRuntimeMethodParams as p,parseRuntimeMethodParamsForVersion as E,parseRuntimeMethodResult as q,parseRuntimeMethodResultForVersion as g,parseRuntimeProtocolErrorDataForVersion as y,parseRuntimeServerRequestCancelParamsForVersion as S,parseRuntimeServerRequestParamsForVersion as x,parseRuntimeServerRequestResultForVersion as P,initializeResultSchema as T,runtimeEventEnvelopeSchema as b}from"@roll-agent/protocol";export{DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES,DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS,RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS,RuntimeEventRecoveryError,RuntimeEventRecoveryManager}from"./runtime-event-recovery.js";const M=c,F=1,I=100,C=3e4,V=3e4,O=1e4,k=5e3;function N(e){if("object"!=typeof e||null===e||!("version"in e)||"string"!=typeof e.version||0===e.version.trim().length)throw new Error("@roll-agent/client-node package metadata is missing a valid version");return e.version}const $=N(i),B={methodNotFound:-32601,invalidParams:-32602,internalError:-32603},z=new Set([l.threadList,l.threadSnapshot,l.threadCapabilities,l.operationGet]);function U(e){return"1.3"===e||"1.2"===e}function j(e,t,r){Object.assign(e,{[t]:r})}function H(e){const t={...e.serverRequestHandlers},r=t[u.userInputRequest];if(void 0!==r&&void 0!==e.onUserInputRequest&&r!==e.onUserInputRequest)throw new Error('Provide userInput.request through either "onUserInputRequest" or "serverRequestHandlers", not both');return void 0!==e.onUserInputRequest&&(t[u.userInputRequest]=e.onUserInputRequest),t}function A(e,t){return h(e).requiredServerRequestMethods.every(e=>"function"==typeof t[e])}function L(e,t,r){if(U(e)){if(!v(e,t))throw new Error(`Runtime Protocol ${e} does not support server request ${t}`);return x(e,t,r)}if(!v(e,t))throw new Error(`Runtime Protocol ${e} does not support server request ${t}`);return x(e,t,r)}function _(e,t,r){return E(e,t,r)}function D(e,t,r){return g(e,t,r)}function J(e,t,r){if(U(e)){if(!v(e,t))throw new Error(`Runtime Protocol ${e} does not support server request ${t}`);return P(e,t,r)}if(!v(e,t))throw new Error(`Runtime Protocol ${e} does not support server request ${t}`);return P(e,t,r)}export class RollRpcError extends Error{code;data;constructor(e){super(e.message),this.name="RollRpcError",this.code=e.code,this.data=e.data}}export class RollRuntimeExitedError extends Error{code;signal;constructor(e,t){super(`Roll Runtime exited before the request completed (code=${String(e)}, signal=${String(t)})`),this.name="RollRuntimeExitedError",this.code=e,this.signal=t}}export class RollRequestTimeoutError extends Error{method;timeoutMs;constructor(e,t){super(`Roll Runtime request "${e}" timed out after ${String(t)} ms`),this.name="RollRequestTimeoutError",this.method=e,this.timeoutMs=t}}export class RollRequestFrameTooLargeError extends Error{frameBytes;maxFrameBytes;constructor(e,t){super(`Roll Runtime request frame is ${String(e)} bytes, exceeding the ${String(t)} byte limit`),this.name="RollRequestFrameTooLargeError",this.frameBytes=e,this.maxFrameBytes=t}}export class RollRuntimeClosingError extends Error{constructor(){super("Roll Runtime client is shutting down"),this.name="RollRuntimeClosingError"}}export class RollRuntimeShutdownTimeoutError extends Error{gracefulTimeoutMs;terminateTimeoutMs;forceKillTimeoutMs;constructor(e,t,r){super(`Roll Runtime did not exit within ${String(e+t+r)} ms`),this.name="RollRuntimeShutdownTimeoutError",this.gracefulTimeoutMs=e,this.terminateTimeoutMs=t,this.forceKillTimeoutMs=r}}export class RollProtocolViolationError extends Error{constructor(e,t){super(e,void 0===t?void 0:{cause:t}),this.name="RollProtocolViolationError"}}export class RollUncorrelatedRpcError extends RollRpcError{constructor(e){super(e),this.name="RollUncorrelatedRpcError"}}function K(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function G(e){if(!K(e))return;const t=m.safeParse(e.interactionId);return t.success?t.data:void 0}function Q(e){return"string"==typeof e||"number"==typeof e&&Number.isFinite(e)}function Y(e,t){if(!Number.isInteger(e)||e<=0)throw new Error(`${t} must be a positive integer`);return e}function X(e={}){return{gracefulTimeoutMs:Y(e.gracefulTimeoutMs??V,"gracefulTimeoutMs"),terminateTimeoutMs:Y(e.terminateTimeoutMs??O,"terminateTimeoutMs"),forceKillTimeoutMs:Y(e.forceKillTimeoutMs??k,"forceKillTimeoutMs")}}function W(e,t,r){if(!K(e)||"number"!=typeof e.code||!Number.isInteger(e.code)||"string"!=typeof e.message)throw new RollProtocolViolationError(`Roll Runtime returned an invalid error for ${t}`);let i;if(void 0!==e.data)try{i=y(r??"1.1",e.data)}catch(e){throw new RollProtocolViolationError(`Roll Runtime returned invalid error data for ${t}`,e)}return{code:e.code,message:e.message,...void 0===i?{}:{data:i}}}function Z(e,t){return new Promise((r,i)=>{const o=setTimeout(()=>r(void 0),t);e.then(e=>{clearTimeout(o),r(e)},e=>{clearTimeout(o),i(e instanceof Error?e:new Error(String(e)))})})}function ee(e){return{stdin:e.stdin,stdout:e.stdout,stderr:e.stderr,onExit(t){let r=!1;const i=(e,i)=>{r||(r=!0,t(e,i))};e.once("close",i),e.once("error",()=>i(null,null))},close(){e.stdin.end()},terminate(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGTERM")},forceClose(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGKILL")}}}export class RollNodeClient{transport;maxFrameBytes;outboundMaxFrameBytes;pending=new Map;listeners=new Set;exitListeners=new Set;activeTurns=new Set;unknownTurns=new Set;serverRequestHandlers;serverRequestHandlerCapabilityRevisions=new Map;advertisedProtocolVersions;inFlightServerRequests=new Map;inFlightServerRequestsByInteractionId=new Map;acknowledgedServerRequestMethods=new Set;onTurnOutcomeUnknown;requestTimeoutMs;maxReadRetries;readRetryDelayMs;defaultShutdownOptions;outputReader;stderrReader;requestId=0;capabilityRevision=0;acknowledgedCapabilityRevision=0;writeQueue=Promise.resolve();capabilitySyncTail=Promise.resolve();exitPromise;resolveExit;exited=!1;closing=!1;outputReaderClosed=!1;stderrReaderClosed=!1;initializationResult;connectionFailure;exitResult;shutdownPromise;eventRecoveryManager;constructor(e){if(this.transport=e.transport,this.maxFrameBytes=e.maxFrameBytes??M,this.outboundMaxFrameBytes=this.maxFrameBytes,this.onTurnOutcomeUnknown=e.onTurnOutcomeUnknown,this.requestTimeoutMs=e.requestTimeoutMs??C,this.maxReadRetries=e.maxReadRetries??1,this.readRetryDelayMs=e.readRetryDelayMs??I,this.defaultShutdownOptions=X(e.shutdownOptions),this.serverRequestHandlers=H(e),this.advertisedProtocolVersions=d.filter(e=>A(e,this.serverRequestHandlers)&&("1.3"!==e||this.maxFrameBytes>=c)),!Number.isInteger(this.maxFrameBytes)||this.maxFrameBytes<=0)throw new Error("maxFrameBytes must be a positive integer");if(!Number.isInteger(this.requestTimeoutMs)||this.requestTimeoutMs<=0)throw new Error("requestTimeoutMs must be a positive integer");if(!Number.isInteger(this.maxReadRetries)||this.maxReadRetries<0)throw new Error("maxReadRetries must be a non-negative integer");if(!Number.isInteger(this.readRetryDelayMs)||this.readRetryDelayMs<0)throw new Error("readRetryDelayMs must be a non-negative integer");this.exitPromise=new Promise(e=>{this.resolveExit=e}),this.outputReader=r({input:e.transport.stdout}),this.outputReader.on("line",e=>this.handleLine(e)),this.stderrReader=void 0===e.transport.stderr?void 0:r({input:e.transport.stderr}),this.stderrReader?.on("line",t=>{try{e.onStderr?.(t)}catch{}}),this.transport.onExit((e,t)=>this.handleExit(e,t))}static async start(t){if(0===t.cwd.trim().length)throw new Error("RollNodeClient requires an explicit non-empty cwd");const r=e(t.command??"roll",[...t.args??["runtime","serve","--stdio"]],{cwd:t.cwd,env:t.env??process.env,shell:!1,stdio:"pipe"});return await RollNodeClient.connect({transport:ee(r),...void 0!==t.clientName?{clientName:t.clientName}:{},...void 0!==t.clientVersion?{clientVersion:t.clientVersion}:{},...void 0!==t.onStderr?{onStderr:t.onStderr}:{},...void 0!==t.onTurnOutcomeUnknown?{onTurnOutcomeUnknown:t.onTurnOutcomeUnknown}:{},...void 0!==t.maxFrameBytes?{maxFrameBytes:t.maxFrameBytes}:{},...void 0!==t.requestTimeoutMs?{requestTimeoutMs:t.requestTimeoutMs}:{},...void 0!==t.maxReadRetries?{maxReadRetries:t.maxReadRetries}:{},...void 0!==t.readRetryDelayMs?{readRetryDelayMs:t.readRetryDelayMs}:{},...void 0!==t.shutdownOptions?{shutdownOptions:t.shutdownOptions}:{},...void 0!==t.serverRequestHandlers?{serverRequestHandlers:t.serverRequestHandlers}:{},...void 0!==t.onUserInputRequest?{onUserInputRequest:t.onUserInputRequest}:{}})}static async connect(e){let t;try{return t=new RollNodeClient(e),t.initializationResult=await t.request(l.initialize,{protocolVersions:[...t.advertisedProtocolVersions],client:{name:e.clientName??"@roll-agent/client-node",version:e.clientVersion??$}}),t.acceptInitializationResult(t.initializationResult),await t.initializeServerRequestCapabilities(),t}catch(r){throw void 0===t?e.transport.close():await t.shutdown().catch(()=>{}),r}}async initializeServerRequestCapabilities(){const e=this.getInitializationResult().protocolVersion;U(e)?await this.queueServerRequestCapabilitySync():this.acknowledgedServerRequestMethods=new Set(R(e).serverRequestMethods.filter(e=>void 0!==this.serverRequestHandlers[e]))}queueServerRequestCapabilitySync(){const e=this.getInitializationResult().protocolVersion;if(!U(e))return Promise.resolve();if(this.capabilityRevision>=Number.MAX_SAFE_INTEGER)return Promise.reject(new RollProtocolViolationError("Runtime Client capability revision was exhausted"));const t=this.capabilityRevision+1;this.capabilityRevision=t;const r=R(e).serverRequestMethods.filter(e=>void 0!==this.serverRequestHandlers[e]);for(const e of r)this.serverRequestHandlerCapabilityRevisions.has(e)||this.serverRequestHandlerCapabilityRevisions.set(e,t);const i=this.capabilitySyncTail.then(async()=>{await this.requestClientCapabilitiesSet(e,t,r)});return this.capabilitySyncTail=i,i}requestClientCapabilitiesSet(e,t,r){if(void 0!==this.connectionFailure)return Promise.reject(this.connectionFailure);if(this.exited)return Promise.reject(this.exitResult?.error??new RollRuntimeExitedError(null,null));if(this.closing)return Promise.reject(new RollRuntimeClosingError);const i=l.clientCapabilitiesSet,o=E(e,i,{revision:t,serverRequestMethods:[...r]});return this.requestOnce(i,o,t=>g(e,i,t),i=>{this.acceptClientCapabilitiesSetResult(e,t,r,i)})}acceptClientCapabilitiesSetResult(e,t,r,i){const o=g(e,l.clientCapabilitiesSet,i);if(o.revision!==t)throw new RollProtocolViolationError(`Roll Runtime acknowledged Client capability revision ${String(o.revision)}; expected ${String(t)}`);const n=new Set(r),s=new Set(o.acceptedServerRequestMethods);for(const e of s)if(!n.has(e))throw new RollProtocolViolationError(`Roll Runtime acknowledged Client capability method "${e}" that was not requested`);this.acknowledgedCapabilityRevision=t,this.acknowledgedServerRequestMethods=s}observeServerRequestCapabilitySync(e){e.catch(e=>{this.failConnection(e instanceof Error?e:new Error(String(e)))})}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}createEventRecovery(e={}){if(void 0!==this.connectionFailure)throw this.connectionFailure;if(this.exited)throw this.exitResult?.error??new RollRuntimeExitedError(null,null);if(this.closing)throw new RollRuntimeClosingError;return void 0===this.eventRecoveryManager||this.eventRecoveryManager.isClosed()?(this.eventRecoveryManager=new n({getInitializationResult:()=>this.getInitializationResult(),requestSnapshot:e=>this.requestEventRecoverySnapshot(e),requestResume:(e,t)=>this.requestRuntimeEventsResume(e,t)},e),this.eventRecoveryManager):this.eventRecoveryManager}onExit(e){const t=this.exitResult;return void 0!==t?(queueMicrotask(()=>{try{e(t)}catch{}}),()=>{}):(this.exitListeners.add(e),()=>{this.exitListeners.delete(e)})}getInitializationResult(){if(void 0===this.initializationResult)throw new Error("Roll Runtime client has not completed initialization");return this.initializationResult}registerServerRequestHandler(e,t){if(void 0!==this.connectionFailure)throw this.connectionFailure;if(this.exited)throw this.exitResult?.error??new RollRuntimeExitedError(null,null);if(this.closing)throw new RollRuntimeClosingError;if(!w(e))throw new Error(`Unknown Runtime server request method: ${String(e)}`);const r=this.initializationResult?.protocolVersion;if(void 0!==r&&!h(r).serverRequests)throw new Error(`Cannot register a Runtime server request handler after Protocol ${r} was negotiated; pass serverRequestHandlers to RollNodeClient.connect() or RollNodeClient.start()`);const i=this.serverRequestHandlers[e];return j(this.serverRequestHandlers,e,t),void 0!==r&&U(r)&&void 0===i&&this.observeServerRequestCapabilitySync(this.queueServerRequestCapabilitySync()),()=>{if(this.serverRequestHandlers[e]!==t)return;const r=this.initializationResult?.protocolVersion;void 0!==r&&f(r,e)?this.failProtocol(new RollProtocolViolationError(`Cannot unregister required Runtime server request handler "${e}" while Protocol ${r} is active`)):(delete this.serverRequestHandlers[e],this.serverRequestHandlerCapabilityRevisions.delete(e),void 0!==r&&U(r)&&(this.retireServerRequestsForMethod(e,new Error(`Runtime Client capability "${e}" was withdrawn`)),this.observeServerRequestCapabilitySync(this.queueServerRequestCapabilitySync())))}}onUserInputRequest(e){return this.registerServerRequestHandler(u.userInputRequest,e)}requestEventRecoverySnapshot(e){if(void 0!==this.connectionFailure)return Promise.reject(this.connectionFailure);if(this.exited)return Promise.reject(this.exitResult?.error??new RollRuntimeExitedError(null,null));if(this.closing)return Promise.reject(new RollRuntimeClosingError);const t=this.getInitializationResult().protocolVersion;if("1.3"!==t)return this.request(l.threadSnapshot,{threadId:e,limit:1});const r=l.threadSnapshot,i=E(t,r,{threadId:e,limit:1,recovery:!0});return this.requestOnce(r,i,e=>g(t,r,e))}requestRuntimeEventsResume(e,t){if(void 0!==this.connectionFailure)return Promise.reject(this.connectionFailure);if(this.exited)return Promise.reject(this.exitResult?.error??new RollRuntimeExitedError(null,null));if(this.closing)return Promise.reject(new RollRuntimeClosingError);if("1.3"!==this.getInitializationResult().protocolVersion)return Promise.reject(new o("runtime.events.resume requires negotiated Runtime Protocol 1.3"));const r=l.runtimeEventsResume,i=E("1.3",r,e);return this.requestOnce(r,i,e=>g("1.3",r,e),e=>{t(g("1.3",r,e))})}async request(e,t){if(void 0!==this.connectionFailure)throw this.connectionFailure;if(this.exited)throw this.exitResult?.error??new RollRuntimeExitedError(null,null);if(this.closing)throw new RollRuntimeClosingError;if(e===l.initialize&&void 0!==this.initializationResult)throw new Error("Roll Runtime client has already completed initialization");const r=this.initializationResult?.protocolVersion,i=void 0===r?p(e,t):_(r,e,t),o=e===l.turnStart?p(l.turnStart,i).turnId:void 0;void 0!==o&&this.activeTurns.add(o);let n=0;for(;;)try{return await this.requestOnce(e,i,t=>void 0===r?e===l.initialize?T.parse(t):q(e,t):D(r,e,t))}catch(t){if(!z.has(e)||!(t instanceof RollRpcError)||!0!==t.data?.retryable||n>=this.maxReadRetries)throw void 0!==o&&(t instanceof RollRpcError&&!(t instanceof RollUncorrelatedRpcError)||t instanceof RollRequestFrameTooLargeError?this.activeTurns.delete(o):this.markTurnOutcomeUnknown(o)),t;n+=1,await new Promise(e=>{setTimeout(e,this.readRetryDelayMs)})}}async requestOnce(e,t,r,i){const o=this.requestId;this.requestId+=1;const n=new Promise((t,r)=>{const n=setTimeout(()=>{this.pending.delete(o)&&r(new RollRequestTimeoutError(e,this.requestTimeoutMs))},this.requestTimeoutMs);this.pending.set(o,{method:e,resolve:t,reject:r,timer:n,...void 0===i?{}:{acceptResult:i}})});try{await this.write({jsonrpc:"2.0",id:o,method:e,params:t});const i=await n;try{return r(i)}catch(t){const r=new RollProtocolViolationError(`Roll Runtime returned an invalid result for "${e}"`,t);throw this.failProtocol(r),r}}catch(e){const t=this.pending.get(o);throw void 0!==t&&(clearTimeout(t.timer),this.pending.delete(o)),e}}getOutcomeUnknownTurnIds(){return[...this.unknownTurns]}close(){this.shutdown().catch(()=>{})}shutdown(e){const t=this.exitResult;return void 0!==t?Promise.resolve(t):(this.shutdownPromise??=this.performShutdown(void 0===e?this.defaultShutdownOptions:X(e)),this.shutdownPromise)}async performShutdown(e){const{gracefulTimeoutMs:t,terminateTimeoutMs:r,forceKillTimeoutMs:i}=e;this.closing=!0,this.abortServerRequests(new RollRuntimeClosingError),this.transport.close();const o=await Z(this.exitPromise,t);if(void 0!==o)return o;if(void 0!==this.transport.terminate){this.transport.terminate();const e=await Z(this.exitPromise,r);if(void 0!==e)return e}if(void 0===this.transport.forceClose){const e=new RollRuntimeShutdownTimeoutError(t,r,i);throw this.finishExit(e,null,null),this.closeReaders(),e}this.transport.forceClose();const n=await Z(this.exitPromise,i);if(void 0!==n)return n;const s=new RollRuntimeShutdownTimeoutError(t,r,i);throw this.finishExit(s,null,null),this.closeReaders(),s}async write(e,r){const i=JSON.stringify(e),o=Buffer.byteLength(i,"utf8");if(o>this.outboundMaxFrameBytes)throw new RollRequestFrameTooLargeError(o,this.outboundMaxFrameBytes);const n=`${i}\n`;this.writeQueue=this.writeQueue.then(async()=>{this.closing||void 0!==this.connectionFailure||this.exited||!1===r?.()||this.transport.stdin.write(n)||await t(this.transport.stdin,"drain")});try{await this.writeQueue}catch(e){const t=new RollProtocolViolationError("Failed to write to the Roll Runtime transport",e);throw this.failProtocol(t),t}}handleLine(e){if(Buffer.byteLength(e,"utf8")>this.maxFrameBytes)return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime frame exceeds ${String(this.maxFrameBytes)} bytes`));let t;try{t=JSON.parse(e)}catch(e){return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted invalid JSON",e))}if(!K(t)||"2.0"!==t.jsonrpc)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid JSON-RPC frame"));const r=Object.hasOwn(t,"method"),i=Object.hasOwn(t,"id"),o=Object.hasOwn(t,"result"),n=Object.hasOwn(t,"error");if(r)return"string"!=typeof t.method||o||n?void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid JSON-RPC request or notification")):i?Q(t.id)?void this.handleServerRequest(t.id,t.method,t.params):void this.failProtocol(new RollProtocolViolationError("Roll Runtime request has an invalid id")):void this.handleNotification(t.method,t.params);if(!i)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response is missing an id"));if(null===t.id){if(!n||o)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response with a null id must contain exactly one error"));try{this.failConnection(new RollUncorrelatedRpcError(W(t.error,"an uncorrelated request",this.initializationResult?.protocolVersion)))}catch(e){this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError("Roll Runtime returned an invalid uncorrelated error",e))}return}if(!Q(t.id))return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response has an invalid id"));const s=this.pending.get(t.id);if(void 0!==s)if(n!==o){if(n){let e;try{e=new RollRpcError(W(t.error,`"${s.method}"`,this.initializationResult?.protocolVersion))}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError(`Roll Runtime returned an invalid error for "${s.method}"`,e))}return this.pending.delete(t.id),clearTimeout(s.timer),void s.reject(e)}if(s.method===l.initialize)try{this.acceptInitializationResult(T.parse(t.result))}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError('Roll Runtime returned an invalid result for "initialize"',e))}if(void 0!==s.acceptResult)try{s.acceptResult(t.result)}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError(`Roll Runtime returned an invalid result for "${s.method}"`,e))}this.pending.delete(t.id),clearTimeout(s.timer),s.resolve(t.result)}else this.failProtocol(new RollProtocolViolationError(`Roll Runtime response for "${s.method}" must contain exactly one result or error`))}handleNotification(e,t){if(e===s){const e=b.safeParse(t);return e.success?void 0===this.initializationResult||e.data.protocolVersion!==this.initializationResult.protocolVersion?void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted runtime.event outside the negotiated Protocol")):void this.handleEvent(e.data):void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid runtime.event",e.error))}if(e===a){const e=this.initializationResult?.protocolVersion;if(void 0===e||!h(e).serverRequests)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted runtime.serverRequest.cancel outside a negotiated server-request capability"));try{if(U(e)){const r=S(e,t),i=this.inFlightServerRequestsByInteractionId.get(r.interactionId);return void(void 0!==i&&this.retireServerRequest(i,new Error(r.reason)))}if("1.1"===e){const r=S(e,t),i=this.inFlightServerRequests.get(r.serverRequestId);return void(void 0!==i&&this.retireServerRequest(i,new Error(r.reason)))}}catch(e){return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid runtime.serverRequest.cancel",e))}return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime emitted runtime.serverRequest.cancel for unsupported Protocol ${e}`))}this.failProtocol(new RollProtocolViolationError(`Roll Runtime emitted an unknown notification: ${e}`))}handleServerRequest(e,t,r){if(this.closing||void 0!==this.connectionFailure||this.exited)return;if(this.inFlightServerRequests.has(e))return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime reused in-flight server request id ${JSON.stringify(e)}`));const i=this.initializationResult?.protocolVersion;if(void 0===i||"1.0"===i||!h(i).serverRequests||!w(t)||!v(i,t))return void this.observeServerRequestResponse(this.writeServerRequestError(e,B.methodNotFound,"Method not found"));const o=this.serverRequestHandlers[t],n=this.serverRequestHandlerCapabilityRevisions.get(t);U(i)&&(!this.acknowledgedServerRequestMethods.has(t)||void 0===n||this.acknowledgedCapabilityRevision<n)||void 0===o?this.observeServerRequestResponse(this.writeServerRequestError(e,B.methodNotFound,"Method not found")):this.observeServerRequestResponse(this.dispatchServerRequest(i,e,t,r,o))}observeServerRequestResponse(e){e.catch(e=>{void 0!==this.connectionFailure||this.exited||this.failProtocol(new RollProtocolViolationError("Failed to respond to Runtime server request",e))})}async dispatchServerRequest(e,t,r,i,o){const n={controller:new AbortController,method:r,requestId:t};this.inFlightServerRequests.set(t,n);try{let s;try{s=L(e,r,i)}catch{return void await this.writeServerRequestError(t,B.invalidParams,"Invalid params",()=>this.isServerRequestActive(t,n))}const l=G(s);if(void 0!==l&&this.inFlightServerRequestsByInteractionId.has(l))return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime reused in-flight interaction id ${JSON.stringify(l)}`));let a,u;void 0!==l&&(n.interactionId=l,this.inFlightServerRequestsByInteractionId.set(l,n),n.controller.signal.addEventListener("abort",()=>{this.inFlightServerRequestsByInteractionId.get(l)===n&&this.inFlightServerRequestsByInteractionId.delete(l)},{once:!0}));try{a=await o(s,{requestId:t,signal:n.controller.signal})}catch{return void(this.isServerRequestActive(t,n)&&await this.writeServerRequestError(t,B.internalError,"Internal error",()=>this.isServerRequestActive(t,n)))}if(!this.isServerRequestActive(t,n))return;try{u=J(e,r,a)}catch{return void await this.writeServerRequestError(t,B.internalError,"Internal error",()=>this.isServerRequestActive(t,n))}this.isServerRequestActive(t,n)&&await this.write({jsonrpc:"2.0",id:t,result:u},()=>this.isServerRequestActive(t,n))}finally{this.retireServerRequest(n)}}retireServerRequest(e,t){this.inFlightServerRequests.get(e.requestId)===e&&this.inFlightServerRequests.delete(e.requestId),void 0!==e.interactionId&&this.inFlightServerRequestsByInteractionId.get(e.interactionId)===e&&this.inFlightServerRequestsByInteractionId.delete(e.interactionId),void 0===t||e.controller.signal.aborted||e.controller.abort(t)}retireServerRequestsForMethod(e,t){for(const r of[...this.inFlightServerRequests.values()])r.method===e&&this.retireServerRequest(r,t)}isServerRequestActive(e,t){return!t.controller.signal.aborted&&this.inFlightServerRequests.get(e)===t}async writeServerRequestError(e,t,r,i){await this.write({jsonrpc:"2.0",id:e,error:{code:t,message:r}},i)}acceptInitializationResult(e){if(!this.advertisedProtocolVersions.includes(e.protocolVersion))throw new RollProtocolViolationError(`Roll Runtime selected unadvertised Protocol ${e.protocolVersion}`);this.initializationResult=e,this.outboundMaxFrameBytes=Math.min(this.maxFrameBytes,e.limits.maxFrameBytes)}handleEvent(e){if(void 0===e.turnId||"turn.completed"!==e.event.type&&"turn.cancelled"!==e.event.type&&"turn.failed"!==e.event.type||this.activeTurns.delete(e.turnId),!0!==this.eventRecoveryManager?.acceptEvent(e))for(const t of this.listeners)try{t(e)}catch{}}handleExit(e,t){const r=this.connectionFailure??new RollRuntimeExitedError(e,t);this.abortServerRequests(r),this.finishExit(r,e,t),this.closeReaders()}abortServerRequests(e){const t=[...this.inFlightServerRequests.values()];this.inFlightServerRequests.clear();for(const r of t)r.controller.abort(e)}failProtocol(e){this.failConnection(e)}failConnection(e){void 0!==this.connectionFailure||this.exited||(this.connectionFailure=e,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.closeOutputReader(),this.shutdown().catch(()=>{}))}closeOutputReader(){this.outputReaderClosed||(this.outputReaderClosed=!0,this.outputReader.close())}closeReaders(){this.closeOutputReader(),void 0===this.stderrReader||this.stderrReaderClosed||(this.stderrReaderClosed=!0,this.stderrReader.close())}finishExit(e,t,r){if(!this.exited){this.exited=!0,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.exitResult={code:t,signal:r,error:e},this.eventRecoveryManager?.acceptClientExit(e),this.resolveExit(this.exitResult);for(const e of this.exitListeners)try{e(this.exitResult)}catch{}this.exitListeners.clear()}}rejectPending(e){for(const t of this.pending.values())clearTimeout(t.timer),t.reject(e);this.pending.clear()}markActiveTurnsOutcomeUnknown(){for(const e of this.activeTurns)this.markTurnOutcomeUnknown(e);this.activeTurns.clear()}markTurnOutcomeUnknown(e){if(this.activeTurns.delete(e),!this.unknownTurns.has(e)){this.unknownTurns.add(e);try{this.onTurnOutcomeUnknown?.(e)}catch{}}}}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { RUNTIME_METHODS, type InitializeResult, type RuntimeEventCursor, type RuntimeEventEnvelope, type RuntimeInstanceId, type RuntimeMethodInputForVersion, type RuntimeMethodResultForVersion, type RuntimeProtocolVersion, type ThreadId } from "@roll-agent/protocol";
|
|
2
|
+
export declare const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS = 10000;
|
|
3
|
+
export declare const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES: number;
|
|
4
|
+
declare const RECOVERY_PHASES: {
|
|
5
|
+
readonly buffering: "buffering";
|
|
6
|
+
readonly draining: "draining";
|
|
7
|
+
readonly live: "live";
|
|
8
|
+
readonly failed: "failed";
|
|
9
|
+
readonly stopped: "stopped";
|
|
10
|
+
};
|
|
11
|
+
declare const RECOVERY_MODES: {
|
|
12
|
+
readonly resumed: "resumed";
|
|
13
|
+
readonly snapshotResumed: "snapshot-resumed";
|
|
14
|
+
readonly snapshotOnly: "snapshot-only";
|
|
15
|
+
};
|
|
16
|
+
export declare const RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS: {
|
|
17
|
+
readonly initial: "initial";
|
|
18
|
+
readonly runtimeRestarted: "runtime-restarted";
|
|
19
|
+
readonly cursorExpired: "cursor-expired";
|
|
20
|
+
readonly cursorGap: "cursor-gap";
|
|
21
|
+
readonly streamGap: "stream-gap";
|
|
22
|
+
readonly bufferOverflow: "buffer-overflow";
|
|
23
|
+
readonly cursorConflict: "cursor-conflict";
|
|
24
|
+
readonly protocolUnsupported: "protocol-unsupported";
|
|
25
|
+
};
|
|
26
|
+
type RuntimeEventRecoveryPhase = (typeof RECOVERY_PHASES)[keyof typeof RECOVERY_PHASES];
|
|
27
|
+
export type RuntimeEventRecoveryMode = (typeof RECOVERY_MODES)[keyof typeof RECOVERY_MODES];
|
|
28
|
+
export type RuntimeEventRecoverySnapshotReason = (typeof RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS)[keyof typeof RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS];
|
|
29
|
+
export type RuntimeDurableEventEnvelope = Extract<RuntimeEventEnvelope, {
|
|
30
|
+
readonly protocolVersion: "1.3";
|
|
31
|
+
readonly durability: "durable";
|
|
32
|
+
}>;
|
|
33
|
+
export type RuntimeEphemeralEventEnvelope = Extract<RuntimeEventEnvelope, {
|
|
34
|
+
readonly protocolVersion: "1.3";
|
|
35
|
+
readonly durability: "ephemeral";
|
|
36
|
+
}>;
|
|
37
|
+
export type RuntimeEventRecoverySnapshot = RuntimeMethodResultForVersion<RuntimeProtocolVersion, typeof RUNTIME_METHODS.threadSnapshot>;
|
|
38
|
+
type RuntimeEventsResumeInput = RuntimeMethodInputForVersion<"1.3", typeof RUNTIME_METHODS.runtimeEventsResume>;
|
|
39
|
+
type RuntimeEventsResumeResult = RuntimeMethodResultForVersion<"1.3", typeof RUNTIME_METHODS.runtimeEventsResume>;
|
|
40
|
+
export interface RuntimeEventRecoveryCheckpoint {
|
|
41
|
+
readonly threadId: ThreadId;
|
|
42
|
+
readonly runtimeInstanceId: RuntimeInstanceId;
|
|
43
|
+
readonly cursor: RuntimeEventCursor | null;
|
|
44
|
+
}
|
|
45
|
+
export interface RuntimeEventRecoverySnapshotContext {
|
|
46
|
+
readonly reason: RuntimeEventRecoverySnapshotReason;
|
|
47
|
+
readonly protocolVersion: RuntimeProtocolVersion;
|
|
48
|
+
/** True only for the byte-bounded Protocol 1.3 checkpoint projection. */
|
|
49
|
+
readonly recoveryProjection: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface RuntimeEventRecoveryErrorContext {
|
|
52
|
+
readonly threadId: ThreadId;
|
|
53
|
+
readonly phase: RuntimeEventRecoveryPhase;
|
|
54
|
+
}
|
|
55
|
+
export interface RuntimeEventRecoveryThreadOptions {
|
|
56
|
+
readonly threadId: ThreadId;
|
|
57
|
+
readonly checkpoint?: RuntimeEventRecoveryCheckpoint;
|
|
58
|
+
readonly applySnapshot: (snapshot: RuntimeEventRecoverySnapshot, context: RuntimeEventRecoverySnapshotContext) => void | Promise<void>;
|
|
59
|
+
readonly onDurableEvent: (event: RuntimeDurableEventEnvelope, checkpoint: RuntimeEventRecoveryCheckpoint) => void | Promise<void>;
|
|
60
|
+
readonly onEphemeralEvent?: (event: RuntimeEphemeralEventEnvelope) => void;
|
|
61
|
+
readonly onError?: (error: Error, context: RuntimeEventRecoveryErrorContext) => void;
|
|
62
|
+
}
|
|
63
|
+
export interface RuntimeEventRecoveryManagerOptions {
|
|
64
|
+
readonly maxBufferedEvents?: number;
|
|
65
|
+
readonly maxBufferedBytes?: number;
|
|
66
|
+
}
|
|
67
|
+
export type RuntimeEventRecoveryStartResult = {
|
|
68
|
+
readonly mode: "resumed" | "snapshot-resumed";
|
|
69
|
+
readonly protocolVersion: "1.3";
|
|
70
|
+
readonly checkpoint: RuntimeEventRecoveryCheckpoint;
|
|
71
|
+
} | {
|
|
72
|
+
readonly mode: "snapshot-only";
|
|
73
|
+
readonly protocolVersion: Exclude<RuntimeProtocolVersion, "1.3">;
|
|
74
|
+
readonly checkpoint: null;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Internal transport bridge supplied by RollNodeClient.
|
|
78
|
+
*
|
|
79
|
+
* It is exported because RuntimeEventRecoveryManager is public, but applications should normally
|
|
80
|
+
* obtain a manager from RollNodeClient.createEventRecovery().
|
|
81
|
+
*/
|
|
82
|
+
export interface RuntimeEventRecoveryBridge {
|
|
83
|
+
readonly getInitializationResult: () => InitializeResult;
|
|
84
|
+
readonly requestSnapshot: (threadId: ThreadId) => Promise<RuntimeEventRecoverySnapshot>;
|
|
85
|
+
readonly requestResume: (input: RuntimeEventsResumeInput, acceptResult: (result: RuntimeEventsResumeResult) => void) => Promise<RuntimeEventsResumeResult>;
|
|
86
|
+
}
|
|
87
|
+
export declare class RuntimeEventRecoveryError extends Error {
|
|
88
|
+
constructor(message: string, options?: ErrorOptions);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Reconciles persisted Runtime events with concurrent live notifications on one Client connection.
|
|
92
|
+
* A claimed Thread never falls through to RollNodeClient.onEvent() before ordering and de-duplication.
|
|
93
|
+
*/
|
|
94
|
+
export declare class RuntimeEventRecoveryManager {
|
|
95
|
+
private readonly bridge;
|
|
96
|
+
private readonly maxBufferedEvents;
|
|
97
|
+
private readonly maxBufferedBytes;
|
|
98
|
+
private readonly states;
|
|
99
|
+
private readonly checkpoints;
|
|
100
|
+
private closed;
|
|
101
|
+
constructor(bridge: RuntimeEventRecoveryBridge, options?: RuntimeEventRecoveryManagerOptions);
|
|
102
|
+
isClosed(): boolean;
|
|
103
|
+
getCheckpoint(threadId: ThreadId): RuntimeEventRecoveryCheckpoint | undefined;
|
|
104
|
+
resumeThread(options: RuntimeEventRecoveryThreadOptions): Promise<RuntimeEventRecoveryStartResult>;
|
|
105
|
+
stopThread(threadId: ThreadId): boolean;
|
|
106
|
+
close(): void;
|
|
107
|
+
/** @internal Called synchronously by RollNodeClient before raw onEvent() fanout. */
|
|
108
|
+
acceptEvent(event: RuntimeEventEnvelope): boolean;
|
|
109
|
+
/** @internal Called when the owning RollNodeClient transport exits. */
|
|
110
|
+
acceptClientExit(error: Error): void;
|
|
111
|
+
private snapshotAndResume;
|
|
112
|
+
private resumeFrom;
|
|
113
|
+
private validateBarrier;
|
|
114
|
+
private prepareBatch;
|
|
115
|
+
private deliverBatch;
|
|
116
|
+
private bufferDurableEvent;
|
|
117
|
+
private takeBuffer;
|
|
118
|
+
private clearBuffer;
|
|
119
|
+
private rememberDelivered;
|
|
120
|
+
private markIntegrityFailure;
|
|
121
|
+
private throwIntegrityError;
|
|
122
|
+
private scheduleLiveDrain;
|
|
123
|
+
private drainLive;
|
|
124
|
+
private assertActive;
|
|
125
|
+
private failState;
|
|
126
|
+
}
|
|
127
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as e}from"node:crypto";import{RUNTIME_ERROR_CODES_V13 as t,RUNTIME_METHODS as r,RUNTIME_V13_DEFAULT_REPLAY_BUFFER_BYTES as n,RUNTIME_V13_MAX_DURABLE_EVENT_RECORDS as i,compareRuntimeEventCursors as o,runtimeEventCursorDistance as s}from"@roll-agent/protocol";export const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS=i;export const DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES=n;const a={buffering:"buffering",draining:"draining",live:"live",failed:"failed",stopped:"stopped"},u={resumed:"resumed",snapshotResumed:"snapshot-resumed",snapshotOnly:"snapshot-only"};export const RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS={initial:"initial",runtimeRestarted:"runtime-restarted",cursorExpired:"cursor-expired",cursorGap:"cursor-gap",streamGap:"stream-gap",bufferOverflow:"buffer-overflow",cursorConflict:"cursor-conflict",protocolUnsupported:"protocol-unsupported"};export class RuntimeEventRecoveryError extends Error{constructor(e,t){super(e,t),this.name="RuntimeEventRecoveryError"}}class RuntimeEventStreamIntegrityError extends RuntimeEventRecoveryError{reason;constructor(e,t,r){super(t,r),this.name="RuntimeEventStreamIntegrityError",this.reason=e}}function c(e,t,r){const n=e??t;if(!Number.isSafeInteger(n)||n<=0)throw new Error(`${r} must be a positive safe integer`);return n}function E(e){return d(e).fingerprint}function d(t){const r=JSON.stringify({...t,sequence:void 0});return{fingerprint:e("sha256").update(r).digest("hex"),bytes:Buffer.byteLength(r,"utf8")}}function h(e){return e instanceof Error?e:new Error(String(e))}function f(e){if("object"==typeof e&&null!==e&&"data"in e&&"object"==typeof e.data&&null!==e.data&&"rollCode"in e.data&&"string"==typeof e.data.rollCode)return e.data.rollCode}function v(e){const r=f(e);return r===t.eventCursorExpired?RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorExpired:r===t.eventCursorGap?RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorGap:void 0}function l(e){if(!("eventCursor"in e))throw new RuntimeEventRecoveryError("Runtime Protocol 1.3 snapshot omitted eventCursor");return e.eventCursor}function p(e){if(!("recoveryProjection"in e)||!0!==e.recoveryProjection)throw new RuntimeEventRecoveryError("Runtime Protocol 1.3 recovery Snapshot omitted the bounded recovery projection marker")}function R(e,t){try{return o(e,t)}catch(e){throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursors are not comparable",{cause:e})}}function m(e,t){try{return s(e,t)}catch(e){throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor distance is not comparable",{cause:e})}}function y(e){return"1.3"===e.protocolVersion&&"durable"===e.durability}function I(e){return"1.3"===e.protocolVersion&&"ephemeral"===e.durability}export class RuntimeEventRecoveryManager{bridge;maxBufferedEvents;maxBufferedBytes;states=new Map;checkpoints=new Map;closed=!1;constructor(e,t={}){this.bridge=e,this.maxBufferedEvents=c(t.maxBufferedEvents,DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_EVENTS,"maxBufferedEvents"),this.maxBufferedBytes=c(t.maxBufferedBytes,DEFAULT_RUNTIME_EVENT_RECOVERY_MAX_BUFFERED_BYTES,"maxBufferedBytes")}isClosed(){return this.closed}getCheckpoint(e){return this.checkpoints.get(e)}async resumeThread(e){if(this.closed)throw new RuntimeEventRecoveryError("Runtime Event recovery manager is closed");if(this.states.has(e.threadId))throw new RuntimeEventRecoveryError(`Runtime Event recovery already owns Thread ${JSON.stringify(e.threadId)}`);if(void 0!==e.checkpoint&&e.checkpoint.threadId!==e.threadId)throw new RuntimeEventRecoveryError("Runtime Event checkpoint belongs to another Thread");const t=this.bridge.getInitializationResult();if("1.3"!==t.protocolVersion){const r=await this.bridge.requestSnapshot(e.threadId);return await e.applySnapshot(r,{reason:RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.protocolUnsupported,protocolVersion:t.protocolVersion,recoveryProjection:!1}),{mode:u.snapshotOnly,protocolVersion:t.protocolVersion,checkpoint:null}}const r={threadId:e.threadId,runtimeInstanceId:t.runtimeInstanceId,cursor:e.checkpoint?.runtimeInstanceId===t.runtimeInstanceId?e.checkpoint.cursor:null},n={options:e,runtimeInstanceId:t.runtimeInstanceId,buffer:[],bufferedByEventId:new Map,bufferedByCursor:new Map,seenByEventId:new Map,seenByCursor:new Map,phase:a.buffering,checkpoint:r,bufferBytes:0,integrityError:void 0,liveDrain:void 0};this.states.set(e.threadId,n),this.checkpoints.set(e.threadId,r);try{let r;if(void 0===e.checkpoint)await this.snapshotAndResume(n,RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.initial),r=u.snapshotResumed;else if(e.checkpoint.runtimeInstanceId!==t.runtimeInstanceId)await this.snapshotAndResume(n,RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.runtimeRestarted),r=u.snapshotResumed;else try{await this.resumeFrom(n,e.checkpoint.cursor),r=u.resumed}catch(e){const t=v(e)??(e instanceof RuntimeEventStreamIntegrityError?e.reason:void 0);if(void 0===t)throw e;await this.snapshotAndResume(n,t),r=u.snapshotResumed}return this.assertActive(n),n.phase=a.live,this.scheduleLiveDrain(n),{mode:r,protocolVersion:"1.3",checkpoint:n.checkpoint}}catch(t){throw this.failState(n,h(t)),this.states.delete(e.threadId),t}}stopThread(e){const t=this.states.get(e);return void 0!==t&&(t.phase=a.stopped,this.states.delete(e),this.clearBuffer(t),!0)}close(){if(!this.closed){this.closed=!0;for(const e of this.states.values())e.phase=a.stopped,this.clearBuffer(e);this.states.clear()}}acceptEvent(e){const t=this.states.get(e.threadId);if(void 0===t)return!1;if(t.phase===a.stopped||t.phase===a.failed)return!0;if(e.runtimeInstanceId!==t.runtimeInstanceId)return this.markIntegrityFailure(t,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event belongs to another Runtime instance")),!0;if(I(e)){if(t.phase===a.live)try{t.options.onEphemeralEvent?.(e)}catch{}return!0}return y(e)?(this.bufferDurableEvent(t,e),t.phase===a.live&&this.scheduleLiveDrain(t),!0):(this.markIntegrityFailure(t,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Claimed Thread received an event outside Runtime Protocol 1.3")),!0)}acceptClientExit(e){for(const t of this.states.values())this.failState(t,e)}async snapshotAndResume(e,t){let r=t;for(let t=0;t<2;t+=1){this.assertActive(e),e.phase=a.buffering;const n=await this.bridge.requestSnapshot(e.options.threadId);this.assertActive(e),p(n),await e.options.applySnapshot(n,{reason:r,protocolVersion:"1.3",recoveryProjection:!0}),this.assertActive(e);const i=l(n);this.clearBuffer(e),e.seenByEventId.clear(),e.seenByCursor.clear(),e.checkpoint={threadId:e.options.threadId,runtimeInstanceId:e.runtimeInstanceId,cursor:i},this.checkpoints.set(e.options.threadId,e.checkpoint);try{return void await this.resumeFrom(e,i)}catch(e){const n=v(e)??(e instanceof RuntimeEventStreamIntegrityError?e.reason:void 0);if(void 0===n||1===t)throw e;r=n}}throw new RuntimeEventRecoveryError("Runtime Event recovery could not converge on a Snapshot")}async resumeFrom(e,t){let r;this.assertActive(e),e.phase=a.buffering,e.integrityError=void 0;const n=await this.bridge.requestResume({threadId:e.options.threadId,afterCursor:t},e=>{r=e});this.assertActive(e);const i=r??n;e.phase=a.draining,this.throwIntegrityError(e);const o=this.takeBuffer(e),s=this.validateBarrier(o,t,i);for(await this.deliverBatch(e,s,t);e.buffer.length>0;){this.throwIntegrityError(e);const t=this.prepareBatch(e,this.takeBuffer(e));await this.deliverBatch(e,t,e.checkpoint.cursor)}this.throwIntegrityError(e)}validateBarrier(e,t,r){const n=this.prepareBatch(void 0,e).filter(e=>null===t||R(e.cursor,t)>0),i=null===r.throughCursor?[]:n.filter(e=>R(e.cursor,r.throughCursor)<=0);if(i.length!==r.replayedCount)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,`Runtime Event replay announced ${String(r.replayedCount)} events but delivered ${String(i.length)}`);if(0===r.replayedCount){if(0!==R(t,r.throughCursor))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay returned an invalid empty barrier")}else{const e=i.at(-1);if(void 0===e||null===r.throughCursor||0!==R(e.cursor,r.throughCursor))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay did not reach its throughCursor barrier");if(m(t,r.throughCursor)!==BigInt(r.replayedCount))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event replay barrier contains a cursor gap")}return n}prepareBatch(e,t){return t.sort((e,t)=>R(e.cursor,t.cursor)),void 0===e?t:t.filter(t=>{const r=e.seenByEventId.get(t.eventId);if(void 0!==r){if(r.cursor!==t.cursor||r.fingerprint!==E(t))throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused an eventId with conflicting content");return!1}const n=e.seenByCursor.get(t.cursor);if(void 0!==n&&n!==t.eventId)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor was reused by another eventId");return!0})}async deliverBatch(e,t,r){let n=r;for(const r of t){this.assertActive(e);const t=m(n,r.cursor);if(t<=0n)continue;if(1n!==t)throw new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.streamGap,"Runtime Event stream contains a cursor gap");const i={threadId:e.options.threadId,runtimeInstanceId:e.runtimeInstanceId,cursor:r.cursor};await e.options.onDurableEvent(r,i),this.assertActive(e),e.checkpoint=i,this.checkpoints.set(e.options.threadId,i),this.rememberDelivered(e,r),n=r.cursor}}bufferDurableEvent(e,t){const{fingerprint:r,bytes:n}=d(t),i=e.seenByEventId.get(t.eventId);if(void 0!==i)return void(i.cursor===t.cursor&&i.fingerprint===r||this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused a delivered eventId with conflicting content")));const o=e.bufferedByEventId.get(t.eventId);if(void 0!==o)return void(o.cursor===t.cursor&&o.fingerprint===r||this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event reused a buffered eventId with conflicting content")));const s=e.bufferedByCursor.get(t.cursor)??e.seenByCursor.get(t.cursor);if(void 0===s||s===t.eventId){if(e.buffer.length+1>this.maxBufferedEvents||e.bufferBytes+n>this.maxBufferedBytes)return this.clearBuffer(e),void this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.bufferOverflow,"Runtime Event recovery buffer exceeded its bounded capacity"));e.buffer.push(t),e.bufferBytes+=n,e.bufferedByEventId.set(t.eventId,{cursor:t.cursor,fingerprint:r}),e.bufferedByCursor.set(t.cursor,t.eventId)}else this.markIntegrityFailure(e,new RuntimeEventStreamIntegrityError(RUNTIME_EVENT_RECOVERY_SNAPSHOT_REASONS.cursorConflict,"Runtime Event cursor was reused by another eventId"))}takeBuffer(e){const t=e.buffer.splice(0);return e.bufferBytes=0,e.bufferedByEventId.clear(),e.bufferedByCursor.clear(),t}clearBuffer(e){e.buffer.splice(0),e.bufferBytes=0,e.bufferedByEventId.clear(),e.bufferedByCursor.clear(),e.integrityError=void 0}rememberDelivered(e,t){for(e.seenByEventId.set(t.eventId,{cursor:t.cursor,fingerprint:E(t)}),e.seenByCursor.set(t.cursor,t.eventId);e.seenByEventId.size>this.maxBufferedEvents;){const t=e.seenByEventId.keys().next().value;if(void 0===t)break;const r=e.seenByEventId.get(t);e.seenByEventId.delete(t),void 0!==r&&e.seenByCursor.delete(r.cursor)}}markIntegrityFailure(e,t){e.integrityError??=t,e.phase===a.live&&this.scheduleLiveDrain(e)}throwIntegrityError(e){if(void 0!==e.integrityError)throw e.integrityError}scheduleLiveDrain(e){void 0!==e.liveDrain||e.phase!==a.live||0===e.buffer.length&&void 0===e.integrityError||(e.liveDrain=this.drainLive(e).finally(()=>{e.liveDrain=void 0,e.phase===a.live&&this.scheduleLiveDrain(e)}))}async drainLive(e){try{for(this.throwIntegrityError(e);e.buffer.length>0;){const t=this.prepareBatch(e,this.takeBuffer(e));await this.deliverBatch(e,t,e.checkpoint.cursor),this.throwIntegrityError(e)}}catch(t){if(t instanceof RuntimeEventStreamIntegrityError)try{return await this.snapshotAndResume(e,t.reason),this.assertActive(e),void(e.phase=a.live)}catch(t){return void this.failState(e,h(t))}this.failState(e,h(t))}}assertActive(e){if(this.closed||this.states.get(e.options.threadId)!==e||e.phase===a.stopped||e.phase===a.failed)throw new RuntimeEventRecoveryError("Runtime Event recovery was stopped")}failState(e,t){if(e.phase!==a.failed&&e.phase!==a.stopped){e.phase=a.failed,this.clearBuffer(e);try{e.options.onError?.(t,{threadId:e.options.threadId,phase:a.failed})}catch{}}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roll-agent/client-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"node": ">=22.6.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@roll-agent/protocol": "0.
|
|
29
|
+
"@roll-agent/protocol": "0.4.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.0.0"
|