@mirasoth/soothe-client 0.1.0 → 0.4.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 +59 -43
- package/dist/chunk-U6RMINYV.js +1883 -0
- package/dist/chunk-U6RMINYV.js.map +1 -0
- package/dist/client-UNPC32NQ.js +7 -0
- package/dist/index.cjs +3677 -584
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1279 -280
- package/dist/index.d.ts +1279 -280
- package/dist/index.js +2098 -243
- package/dist/index.js.map +1 -1
- package/package.json +37 -8
- package/dist/chunk-OMAC7LA7.js +0 -647
- package/dist/chunk-OMAC7LA7.js.map +0 -1
- package/dist/client-QS23U6WX.js +0 -7
- /package/dist/{client-QS23U6WX.js.map → client-UNPC32NQ.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,113 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
function disconnectCauseName(cause) {
|
|
35
|
+
return cause === 1 /* Clean */ ? "clean" : "unclean";
|
|
36
|
+
}
|
|
37
|
+
var ConnectionError, DaemonError, TimeoutError, DisconnectCause, ReconnectError, StaleLoopError;
|
|
38
|
+
var init_errors = __esm({
|
|
39
|
+
"src/errors.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
ConnectionError = class extends Error {
|
|
42
|
+
url;
|
|
43
|
+
attempt;
|
|
44
|
+
cause;
|
|
45
|
+
constructor(url, attempt, cause) {
|
|
46
|
+
super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
|
|
47
|
+
this.name = "ConnectionError";
|
|
48
|
+
this.url = url;
|
|
49
|
+
this.attempt = attempt;
|
|
50
|
+
this.cause = cause;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
DaemonError = class extends Error {
|
|
54
|
+
/** Numeric error code from the RFC-450 §7.3 registry. */
|
|
55
|
+
code;
|
|
56
|
+
/** The daemon's error message text. */
|
|
57
|
+
daemonMessage;
|
|
58
|
+
/** Optional machine-parseable error details. */
|
|
59
|
+
data;
|
|
60
|
+
constructor(code, message, data) {
|
|
61
|
+
super(`daemon error [${code}]: ${message}`);
|
|
62
|
+
this.name = "DaemonError";
|
|
63
|
+
this.code = code;
|
|
64
|
+
this.daemonMessage = message;
|
|
65
|
+
this.data = data;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
TimeoutError = class extends Error {
|
|
69
|
+
operation;
|
|
70
|
+
duration;
|
|
71
|
+
constructor(operation, duration) {
|
|
72
|
+
super(`timeout after ${duration} waiting for ${operation}`);
|
|
73
|
+
this.name = "TimeoutError";
|
|
74
|
+
this.operation = operation;
|
|
75
|
+
this.duration = duration;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
DisconnectCause = /* @__PURE__ */ ((DisconnectCause2) => {
|
|
79
|
+
DisconnectCause2[DisconnectCause2["Unclean"] = 0] = "Unclean";
|
|
80
|
+
DisconnectCause2[DisconnectCause2["Clean"] = 1] = "Clean";
|
|
81
|
+
return DisconnectCause2;
|
|
82
|
+
})(DisconnectCause || {});
|
|
83
|
+
ReconnectError = class extends Error {
|
|
84
|
+
url;
|
|
85
|
+
attempts;
|
|
86
|
+
cause;
|
|
87
|
+
constructor(url, attempts, cause) {
|
|
88
|
+
super(`reconnect to ${url} failed after ${attempts} attempts: ${cause.message}`);
|
|
89
|
+
this.name = "ReconnectError";
|
|
90
|
+
this.url = url;
|
|
91
|
+
this.attempts = attempts;
|
|
92
|
+
this.cause = cause;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
StaleLoopError = class extends Error {
|
|
96
|
+
loopID;
|
|
97
|
+
cause;
|
|
98
|
+
constructor(loopID, cause) {
|
|
99
|
+
const detail = cause ? `: ${cause.message}` : "";
|
|
100
|
+
super(`stale loop ${loopID}: reattach accepted but liveness probe failed${detail}`);
|
|
101
|
+
this.name = "StaleLoopError";
|
|
102
|
+
this.loopID = loopID;
|
|
103
|
+
this.cause = cause;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// src/verbosity.ts
|
|
110
|
+
function shouldShow(tier, verbosity) {
|
|
111
|
+
if (tier === 99 /* Internal */) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const level = verbosityLevelValues[verbosity] ?? 1;
|
|
115
|
+
return tier <= level;
|
|
116
|
+
}
|
|
117
|
+
function isValidVerbosityLevel(s) {
|
|
118
|
+
return s in verbosityLevelValues;
|
|
119
|
+
}
|
|
120
|
+
var VerbosityTier, verbosityLevelValues;
|
|
121
|
+
var init_verbosity = __esm({
|
|
122
|
+
"src/verbosity.ts"() {
|
|
123
|
+
"use strict";
|
|
124
|
+
VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
|
|
125
|
+
VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
|
|
126
|
+
VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
|
|
127
|
+
VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
|
|
128
|
+
VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
|
|
129
|
+
VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
|
|
130
|
+
return VerbosityTier2;
|
|
131
|
+
})(VerbosityTier || {});
|
|
132
|
+
verbosityLevelValues = {
|
|
133
|
+
quiet: 0,
|
|
134
|
+
normal: 1,
|
|
135
|
+
debug: 3
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
33
140
|
// src/config.ts
|
|
34
141
|
function defaultConfig() {
|
|
35
142
|
return {
|
|
@@ -40,7 +147,11 @@ function defaultConfig() {
|
|
|
40
147
|
heartbeatInterval: 3e4,
|
|
41
148
|
daemonReadyTimeout: 2e4,
|
|
42
149
|
loopStatusTimeout: 6e4,
|
|
43
|
-
subscriptionTimeout: 1e4
|
|
150
|
+
subscriptionTimeout: 1e4,
|
|
151
|
+
reconnectMaxAttempts: 10,
|
|
152
|
+
reconnectInitialDelay: 500,
|
|
153
|
+
reconnectMaxDelay: 1e4,
|
|
154
|
+
reattachProbeTimeout: 5e3
|
|
44
155
|
};
|
|
45
156
|
}
|
|
46
157
|
function loadConfigFromEnv() {
|
|
@@ -90,102 +201,89 @@ function decodeMessage(data) {
|
|
|
90
201
|
} catch {
|
|
91
202
|
throw new Error(`invalid JSON: ${data}`);
|
|
92
203
|
}
|
|
204
|
+
if (!parsed || typeof parsed !== "object") return parsed;
|
|
93
205
|
const type = parsed.type;
|
|
94
206
|
if (!type) return parsed;
|
|
95
207
|
switch (type) {
|
|
96
|
-
|
|
97
|
-
case "loop_input":
|
|
98
|
-
return { ...parsed };
|
|
99
|
-
case "command":
|
|
100
|
-
return { ...parsed };
|
|
101
|
-
case "daemon_status":
|
|
102
|
-
return { ...parsed };
|
|
103
|
-
case "daemon_shutdown":
|
|
104
|
-
return { ...parsed };
|
|
105
|
-
case "config_get":
|
|
106
|
-
return { ...parsed };
|
|
107
|
-
case "loop_new":
|
|
108
|
-
return { ...parsed };
|
|
109
|
-
case "loop_subscribe":
|
|
110
|
-
return { ...parsed };
|
|
111
|
-
case "loop_detach":
|
|
112
|
-
return { ...parsed };
|
|
113
|
-
case "loop_list":
|
|
114
|
-
return { ...parsed };
|
|
115
|
-
case "loop_get":
|
|
116
|
-
return { ...parsed };
|
|
117
|
-
case "loop_tree":
|
|
118
|
-
return { ...parsed };
|
|
119
|
-
case "loop_prune":
|
|
120
|
-
return { ...parsed };
|
|
121
|
-
case "loop_delete":
|
|
208
|
+
case "connection_init":
|
|
122
209
|
return { ...parsed };
|
|
123
|
-
case "
|
|
210
|
+
case "connection_ack":
|
|
124
211
|
return { ...parsed };
|
|
125
|
-
case "
|
|
212
|
+
case "request":
|
|
126
213
|
return { ...parsed };
|
|
127
|
-
case "
|
|
214
|
+
case "response":
|
|
128
215
|
return { ...parsed };
|
|
129
|
-
case "
|
|
216
|
+
case "notification":
|
|
130
217
|
return { ...parsed };
|
|
131
|
-
case "
|
|
218
|
+
case "subscribe":
|
|
132
219
|
return { ...parsed };
|
|
133
|
-
|
|
134
|
-
case "event":
|
|
135
|
-
return { ...parsed };
|
|
136
|
-
case "status": {
|
|
137
|
-
const msg = { ...parsed };
|
|
138
|
-
if (!msg.loop_id && parsed.loopId && typeof parsed.loopId === "string") {
|
|
139
|
-
msg.loop_id = parsed.loopId;
|
|
140
|
-
}
|
|
141
|
-
return msg;
|
|
142
|
-
}
|
|
143
|
-
case "subscription_confirmed":
|
|
220
|
+
case "next":
|
|
144
221
|
return { ...parsed };
|
|
145
222
|
case "error":
|
|
146
223
|
return { ...parsed };
|
|
147
|
-
case "
|
|
148
|
-
return { ...parsed };
|
|
149
|
-
case "daemon_status_response":
|
|
150
|
-
return { ...parsed };
|
|
151
|
-
case "shutdown_ack":
|
|
152
|
-
return { ...parsed };
|
|
153
|
-
case "loop_new_response":
|
|
154
|
-
return { ...parsed };
|
|
155
|
-
case "loop_subscribe_response":
|
|
156
|
-
return { ...parsed };
|
|
157
|
-
case "loop_detach_response":
|
|
224
|
+
case "complete":
|
|
158
225
|
return { ...parsed };
|
|
159
|
-
case "
|
|
226
|
+
case "unsubscribe":
|
|
160
227
|
return { ...parsed };
|
|
161
|
-
case "
|
|
228
|
+
case "ping":
|
|
162
229
|
return { ...parsed };
|
|
163
|
-
case "
|
|
230
|
+
case "pong":
|
|
164
231
|
return { ...parsed };
|
|
165
|
-
case "
|
|
232
|
+
case "receipt_response":
|
|
166
233
|
return { ...parsed };
|
|
167
|
-
case "
|
|
234
|
+
case "disconnect":
|
|
168
235
|
return { ...parsed };
|
|
169
|
-
case "
|
|
170
|
-
return { ...parsed };
|
|
171
|
-
case "history_replay":
|
|
172
|
-
return { ...parsed };
|
|
173
|
-
case "history_replay_complete":
|
|
174
|
-
case "replay_complete":
|
|
175
|
-
return { ...parsed };
|
|
176
|
-
case "loop_reattached":
|
|
177
|
-
return { ...parsed };
|
|
178
|
-
case "config_get_response":
|
|
179
|
-
case "invoke_skill_response":
|
|
180
|
-
return parsed;
|
|
181
|
-
case "skills_list_response":
|
|
182
|
-
return { ...parsed };
|
|
183
|
-
case "models_list_response":
|
|
236
|
+
case "status":
|
|
184
237
|
return { ...parsed };
|
|
185
238
|
default:
|
|
186
239
|
return parsed;
|
|
187
240
|
}
|
|
188
241
|
}
|
|
242
|
+
function requestEnvelope(method, params, id) {
|
|
243
|
+
return {
|
|
244
|
+
proto: PROTO_VERSION,
|
|
245
|
+
type: "request",
|
|
246
|
+
method,
|
|
247
|
+
params,
|
|
248
|
+
id: id ?? newRequestID()
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function notificationEnvelope(method, params) {
|
|
252
|
+
return { proto: PROTO_VERSION, type: "notification", method, params };
|
|
253
|
+
}
|
|
254
|
+
function subscribeEnvelope(method, params, id) {
|
|
255
|
+
return {
|
|
256
|
+
proto: PROTO_VERSION,
|
|
257
|
+
type: "subscribe",
|
|
258
|
+
method,
|
|
259
|
+
params,
|
|
260
|
+
id: id ?? newRequestID()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function unsubscribeEnvelope(id) {
|
|
264
|
+
return { proto: PROTO_VERSION, type: "unsubscribe", id };
|
|
265
|
+
}
|
|
266
|
+
function connectionInitEnvelope(opts) {
|
|
267
|
+
return {
|
|
268
|
+
proto: PROTO_VERSION,
|
|
269
|
+
type: "connection_init",
|
|
270
|
+
params: {
|
|
271
|
+
client_version: opts?.client_version ?? CLIENT_VERSION,
|
|
272
|
+
client_name: opts?.client_name ?? "soothe-client-ts",
|
|
273
|
+
accept_proto: opts?.accept_proto ?? [PROTO_VERSION],
|
|
274
|
+
capabilities: opts?.capabilities ?? DEFAULT_CLIENT_CAPABILITIES
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function pingEnvelope() {
|
|
279
|
+
return { proto: PROTO_VERSION, type: "ping" };
|
|
280
|
+
}
|
|
281
|
+
function pongEnvelope() {
|
|
282
|
+
return { proto: PROTO_VERSION, type: "pong" };
|
|
283
|
+
}
|
|
284
|
+
function disconnectEnvelope() {
|
|
285
|
+
return { proto: PROTO_VERSION, type: "disconnect" };
|
|
286
|
+
}
|
|
189
287
|
function splitWirePayload(data) {
|
|
190
288
|
const trimmed = data.trim();
|
|
191
289
|
if (trimmed === "") return [];
|
|
@@ -195,21 +293,25 @@ function splitWirePayload(data) {
|
|
|
195
293
|
function extractSootheLoopID(msg) {
|
|
196
294
|
if (!msg || typeof msg !== "object") return ["", false];
|
|
197
295
|
const m = msg;
|
|
296
|
+
if (m.type === "next") {
|
|
297
|
+
const payload = m.payload;
|
|
298
|
+
if (payload && typeof payload === "object") {
|
|
299
|
+
const data = payload.data;
|
|
300
|
+
if (data && typeof data === "object") {
|
|
301
|
+
const id = data.loop_id;
|
|
302
|
+
if (id && id !== "") return [id, true];
|
|
303
|
+
}
|
|
304
|
+
const pid = payload.loop_id;
|
|
305
|
+
if (pid && pid !== "") return [pid, true];
|
|
306
|
+
}
|
|
307
|
+
return ["", false];
|
|
308
|
+
}
|
|
198
309
|
if (m.type === "status") {
|
|
199
310
|
const id = m.loop_id;
|
|
200
311
|
if (id && id !== "") return [id, true];
|
|
201
312
|
return ["", false];
|
|
202
313
|
}
|
|
203
|
-
|
|
204
|
-
const top = m.loop_id;
|
|
205
|
-
if (top && top !== "") return [top, true];
|
|
206
|
-
const data = m.data;
|
|
207
|
-
if (data && typeof data === "object") {
|
|
208
|
-
const dataId = data["loop_id"] ?? data["loopId"];
|
|
209
|
-
if (dataId && dataId !== "") return [dataId, true];
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const generic = m["loop_id"] ?? m["loopId"];
|
|
314
|
+
const generic = m.loop_id;
|
|
213
315
|
if (generic && generic !== "") return [generic, true];
|
|
214
316
|
return ["", false];
|
|
215
317
|
}
|
|
@@ -217,52 +319,552 @@ function newRequestID() {
|
|
|
217
319
|
return (0, import_node_crypto.randomUUID)();
|
|
218
320
|
}
|
|
219
321
|
function newLoopInputMessage(loopID, content) {
|
|
220
|
-
return {
|
|
221
|
-
request_id: newRequestID(),
|
|
222
|
-
type: "loop_input",
|
|
322
|
+
return notificationEnvelope("loop_input", {
|
|
223
323
|
loop_id: loopID,
|
|
224
324
|
content,
|
|
225
325
|
autonomous: false
|
|
226
|
-
};
|
|
326
|
+
});
|
|
227
327
|
}
|
|
228
328
|
function newLoopNewMessage(opts) {
|
|
229
329
|
const options = typeof opts === "string" ? { client_workspace: opts } : opts ?? {};
|
|
230
330
|
const clientWorkspace = options.client_workspace ?? options.workspace;
|
|
231
|
-
const
|
|
232
|
-
request_id: newRequestID(),
|
|
233
|
-
type: "loop_new"
|
|
234
|
-
};
|
|
331
|
+
const params = {};
|
|
235
332
|
if (clientWorkspace?.trim()) {
|
|
236
|
-
|
|
333
|
+
params.client_workspace = clientWorkspace.trim();
|
|
237
334
|
}
|
|
238
335
|
if (options.user_id?.trim()) {
|
|
239
|
-
|
|
336
|
+
params.user_id = options.user_id.trim();
|
|
240
337
|
}
|
|
241
338
|
if (options.client_workspace_id?.trim()) {
|
|
242
|
-
|
|
339
|
+
params.client_workspace_id = options.client_workspace_id.trim();
|
|
243
340
|
}
|
|
244
341
|
if (options.is_ephemeral) {
|
|
245
|
-
|
|
342
|
+
params.is_ephemeral = true;
|
|
246
343
|
}
|
|
247
|
-
return
|
|
344
|
+
return requestEnvelope("loop_new", params);
|
|
248
345
|
}
|
|
249
346
|
function newLoopSubscribeMessage(loopID, verbosity, streamDelivery) {
|
|
250
|
-
const
|
|
251
|
-
request_id: newRequestID(),
|
|
252
|
-
type: "loop_subscribe",
|
|
253
|
-
loop_id: loopID,
|
|
254
|
-
verbosity
|
|
255
|
-
};
|
|
347
|
+
const params = { loop_id: loopID, verbosity };
|
|
256
348
|
if (streamDelivery) {
|
|
257
|
-
|
|
349
|
+
params.stream_delivery = streamDelivery;
|
|
258
350
|
}
|
|
259
|
-
return
|
|
351
|
+
return subscribeEnvelope("loop_events", params);
|
|
260
352
|
}
|
|
261
|
-
var import_node_crypto;
|
|
353
|
+
var import_node_crypto, PROTO_VERSION, DEFAULT_CLIENT_CAPABILITIES, CLIENT_VERSION;
|
|
262
354
|
var init_protocol = __esm({
|
|
263
355
|
"src/protocol.ts"() {
|
|
264
356
|
"use strict";
|
|
265
357
|
import_node_crypto = require("crypto");
|
|
358
|
+
PROTO_VERSION = "1";
|
|
359
|
+
DEFAULT_CLIENT_CAPABILITIES = ["streaming", "batch", "heartbeat", "receipts"];
|
|
360
|
+
CLIENT_VERSION = "0.4.0";
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// src/intent_hints.ts
|
|
365
|
+
function validateLoopInputIntentHint(hint) {
|
|
366
|
+
const key = hint.trim().toLowerCase();
|
|
367
|
+
if (key === "direct_llm" || key === "quiz") {
|
|
368
|
+
return REMOVED_INTENT_HINT_MESSAGES[key];
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
var INTENT_HINT_TEXT_COMPLETION, INTENT_HINT_IMAGE_TO_TEXT, INTENT_HINT_OCR, INTENT_HINT_EMBED, REMOVED_INTENT_HINTS, REMOVED_INTENT_HINT_MESSAGES, LOOP_ASSISTANT_OUTPUT_PHASES, DEFAULT_DELIVERABLE_PHASES;
|
|
373
|
+
var init_intent_hints = __esm({
|
|
374
|
+
"src/intent_hints.ts"() {
|
|
375
|
+
"use strict";
|
|
376
|
+
INTENT_HINT_TEXT_COMPLETION = "text_completion";
|
|
377
|
+
INTENT_HINT_IMAGE_TO_TEXT = "image_to_text";
|
|
378
|
+
INTENT_HINT_OCR = "ocr";
|
|
379
|
+
INTENT_HINT_EMBED = "embed";
|
|
380
|
+
REMOVED_INTENT_HINTS = ["direct_llm", "quiz"];
|
|
381
|
+
REMOVED_INTENT_HINT_MESSAGES = {
|
|
382
|
+
direct_llm: "intent_hint direct_llm is removed; use text_completion (text-only) or image_to_text (with attachments)",
|
|
383
|
+
quiz: "intent_hint quiz is removed; omit intent_hint and let intake classify the turn"
|
|
384
|
+
};
|
|
385
|
+
LOOP_ASSISTANT_OUTPUT_PHASES = [
|
|
386
|
+
"goal_completion",
|
|
387
|
+
"quiz",
|
|
388
|
+
"autonomous_goal",
|
|
389
|
+
"direct_model",
|
|
390
|
+
"text_completion",
|
|
391
|
+
"image_to_text",
|
|
392
|
+
"ocr",
|
|
393
|
+
"embed",
|
|
394
|
+
"plan_direct"
|
|
395
|
+
];
|
|
396
|
+
DEFAULT_DELIVERABLE_PHASES = /* @__PURE__ */ new Set([
|
|
397
|
+
"quiz",
|
|
398
|
+
"goal_completion",
|
|
399
|
+
"direct_model",
|
|
400
|
+
"text_completion",
|
|
401
|
+
"image_to_text",
|
|
402
|
+
"ocr",
|
|
403
|
+
"embed"
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// src/events.ts
|
|
409
|
+
function parseNamespace(ns) {
|
|
410
|
+
const parts = splitNamespace(ns);
|
|
411
|
+
if (parts.length < 4 || parts[0] !== "soothe") {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
if (parts[1] === "internal") {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
return { domain: parts[1], component: parts[2], action: parts[3] };
|
|
418
|
+
}
|
|
419
|
+
function splitNamespace(ns) {
|
|
420
|
+
const parts = [];
|
|
421
|
+
let start = 0;
|
|
422
|
+
for (let i = 0; i < ns.length; i++) {
|
|
423
|
+
if (ns[i] === ".") {
|
|
424
|
+
parts.push(ns.slice(start, i));
|
|
425
|
+
start = i + 1;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
parts.push(ns.slice(start));
|
|
429
|
+
return parts;
|
|
430
|
+
}
|
|
431
|
+
function classifyEventVerbosity(eventTypeOrNamespace) {
|
|
432
|
+
const parsed = parseNamespace(eventTypeOrNamespace);
|
|
433
|
+
if (!parsed) {
|
|
434
|
+
return classifyByEventTypeString(eventTypeOrNamespace);
|
|
435
|
+
}
|
|
436
|
+
return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
|
|
437
|
+
}
|
|
438
|
+
function classifyByDomainAndComponent(domain, _component, full) {
|
|
439
|
+
switch (domain) {
|
|
440
|
+
case "cognition":
|
|
441
|
+
return 1 /* Normal */;
|
|
442
|
+
case "protocol":
|
|
443
|
+
return 2 /* Detailed */;
|
|
444
|
+
case "tool":
|
|
445
|
+
return 99 /* Internal */;
|
|
446
|
+
case "subagent":
|
|
447
|
+
return classifySubagentEvent(full);
|
|
448
|
+
case "autopilot":
|
|
449
|
+
return 1 /* Normal */;
|
|
450
|
+
case "output":
|
|
451
|
+
case "error":
|
|
452
|
+
return 0 /* Quiet */;
|
|
453
|
+
default:
|
|
454
|
+
return 1 /* Normal */;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function classifySubagentEvent(full) {
|
|
458
|
+
const parsed = parseNamespace(full);
|
|
459
|
+
if (!parsed) return 1 /* Normal */;
|
|
460
|
+
switch (parsed.action) {
|
|
461
|
+
case "started":
|
|
462
|
+
case "completed":
|
|
463
|
+
return 1 /* Normal */;
|
|
464
|
+
default:
|
|
465
|
+
return 2 /* Detailed */;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function classifyByEventTypeString(eventType) {
|
|
469
|
+
if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
|
|
470
|
+
return 0 /* Quiet */;
|
|
471
|
+
}
|
|
472
|
+
if (eventType === EventToolStarted) {
|
|
473
|
+
return 99 /* Internal */;
|
|
474
|
+
}
|
|
475
|
+
return 1 /* Normal */;
|
|
476
|
+
}
|
|
477
|
+
function isCompletionEvent(eventType) {
|
|
478
|
+
return eventType.endsWith(".completed") || eventType.endsWith(".failed") || eventType === EventGeneralFailed;
|
|
479
|
+
}
|
|
480
|
+
function isSubagentProgressEvent(eventType) {
|
|
481
|
+
const parsed = parseNamespace(eventType);
|
|
482
|
+
if (!parsed || parsed.domain !== "subagent") {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
return parsed.action === "started" || parsed.action === "completed";
|
|
486
|
+
}
|
|
487
|
+
var EventPlanCreated, EventExploreStarted, EventExploreMilestone, EventExploreStepCompleted, EventExploreCompleted, EventTacitusStarted, EventTacitusGatherSummary, EventTacitusCompleted, EventReplayComplete, EventLoopReattachedWire, EventCardReplayBegin, EventCardCreated, EventCardReplayEnd, EventToolStarted, EventToolCompleted, EventToolError, EventStreamToolCallUpdate, EventToolCallUpdatesBatch, EventStrangeLoopStarted, EventStrangeLoopCompleted, EventStrangeLoopPlanDecision, EventStrangeLoopReasoned, EventStrangeLoopStepStarted, EventStrangeLoopStepQueued, EventStrangeLoopStepCompleted, EventStrangeLoopContextCompacted, EventMessageReceived, EventMessageSent, EventFinalReport, EventAutopilotGoalStatus, EventAutopilotGoalProgress, EventAutopilotGoalCreated, EventAutopilotGoalCompleted, EventAutopilotWorkerAssigned, EventAutopilotWorkerUnassigned, EventGeneralFailed;
|
|
488
|
+
var init_events = __esm({
|
|
489
|
+
"src/events.ts"() {
|
|
490
|
+
"use strict";
|
|
491
|
+
init_verbosity();
|
|
492
|
+
EventPlanCreated = "soothe.cognition.plan.created";
|
|
493
|
+
EventExploreStarted = "soothe.subagent.explore.started";
|
|
494
|
+
EventExploreMilestone = "soothe.subagent.explore.milestone";
|
|
495
|
+
EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
|
|
496
|
+
EventExploreCompleted = "soothe.subagent.explore.completed";
|
|
497
|
+
EventTacitusStarted = "soothe.subagent.tacitus.started";
|
|
498
|
+
EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
|
|
499
|
+
EventTacitusCompleted = "soothe.subagent.tacitus.completed";
|
|
500
|
+
EventReplayComplete = "replay_complete";
|
|
501
|
+
EventLoopReattachedWire = "loop_reattached";
|
|
502
|
+
EventCardReplayBegin = "card.replay_begin";
|
|
503
|
+
EventCardCreated = "card.created";
|
|
504
|
+
EventCardReplayEnd = "card.replay_end";
|
|
505
|
+
EventToolStarted = "soothe.tool.execution.started";
|
|
506
|
+
EventToolCompleted = "soothe.tool.execution.completed";
|
|
507
|
+
EventToolError = "soothe.tool.execution.error";
|
|
508
|
+
EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
|
|
509
|
+
EventToolCallUpdatesBatch = "tool_call_updates_batch";
|
|
510
|
+
EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
|
|
511
|
+
EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
|
|
512
|
+
EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
|
|
513
|
+
EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
|
|
514
|
+
EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
|
|
515
|
+
EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
|
|
516
|
+
EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
|
|
517
|
+
EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
|
|
518
|
+
EventMessageReceived = "soothe.protocol.message.received";
|
|
519
|
+
EventMessageSent = "soothe.protocol.message.sent";
|
|
520
|
+
EventFinalReport = "soothe.output.autonomous.final_report.reported";
|
|
521
|
+
EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
|
|
522
|
+
EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
|
|
523
|
+
EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
|
|
524
|
+
EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
|
|
525
|
+
EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
|
|
526
|
+
EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
|
|
527
|
+
EventGeneralFailed = "soothe.error.general.failed";
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// src/multiplexer.ts
|
|
532
|
+
var Multiplexer;
|
|
533
|
+
var init_multiplexer = __esm({
|
|
534
|
+
"src/multiplexer.ts"() {
|
|
535
|
+
"use strict";
|
|
536
|
+
init_errors();
|
|
537
|
+
Multiplexer = class {
|
|
538
|
+
rpcs = /* @__PURE__ */ new Map();
|
|
539
|
+
subs = /* @__PURE__ */ new Map();
|
|
540
|
+
receipts = /* @__PURE__ */ new Map();
|
|
541
|
+
/**
|
|
542
|
+
* Installs a pending RPC wait keyed by `id`. Returns the pending call and an
|
|
543
|
+
* unregister function that MUST be called when the wait ends (success,
|
|
544
|
+
* timeout, or cancel) to avoid leaks. If a late response arrives after the
|
|
545
|
+
* caller has unregistered, it is dropped (log-and-drop) — no leak.
|
|
546
|
+
*/
|
|
547
|
+
registerRPC(id) {
|
|
548
|
+
let callResolve;
|
|
549
|
+
let callReject;
|
|
550
|
+
const call = new Promise((resolve, reject) => {
|
|
551
|
+
callResolve = resolve;
|
|
552
|
+
callReject = reject;
|
|
553
|
+
});
|
|
554
|
+
const pending = { resolve: callResolve, reject: callReject };
|
|
555
|
+
this.rpcs.set(id, pending);
|
|
556
|
+
const unregister = () => {
|
|
557
|
+
if (this.rpcs.get(id) === pending) {
|
|
558
|
+
this.rpcs.delete(id);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
return { call, unregister };
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Installs a pending subscription stream keyed by `id`. Returns the stream
|
|
565
|
+
* channel (an async-iterable-like push sink), a `done` signal, and an
|
|
566
|
+
* unregister function. The Client pushes `next`/`complete` frames via
|
|
567
|
+
* `push`; the application reads from the channel.
|
|
568
|
+
*/
|
|
569
|
+
registerSubscription(id) {
|
|
570
|
+
let resolveDone;
|
|
571
|
+
const done = new Promise((resolve) => {
|
|
572
|
+
resolveDone = resolve;
|
|
573
|
+
});
|
|
574
|
+
const pending = {
|
|
575
|
+
push: () => {
|
|
576
|
+
},
|
|
577
|
+
done,
|
|
578
|
+
resolveDone,
|
|
579
|
+
settled: false
|
|
580
|
+
};
|
|
581
|
+
const push = (frame) => {
|
|
582
|
+
if (pending.settled) return;
|
|
583
|
+
pending.push(frame);
|
|
584
|
+
};
|
|
585
|
+
pending.push = () => {
|
|
586
|
+
};
|
|
587
|
+
this.subs.set(id, pending);
|
|
588
|
+
const unregister = () => {
|
|
589
|
+
if (this.subs.get(id) === pending) {
|
|
590
|
+
pending.settled = true;
|
|
591
|
+
this.subs.delete(id);
|
|
592
|
+
resolveDone();
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
return { push, done, unregister };
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Installs a pending receipt wait keyed by `receipt`. Returns an unregister
|
|
599
|
+
* function.
|
|
600
|
+
*/
|
|
601
|
+
registerReceipt(receipt) {
|
|
602
|
+
let resolveWait;
|
|
603
|
+
const wait = new Promise((resolve) => {
|
|
604
|
+
resolveWait = resolve;
|
|
605
|
+
});
|
|
606
|
+
this.receipts.set(receipt, resolveWait);
|
|
607
|
+
const unregister = () => {
|
|
608
|
+
this.receipts.delete(receipt);
|
|
609
|
+
};
|
|
610
|
+
return { wait, unregister };
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Wires a real sink for a registered subscription's `push`. Called by the
|
|
614
|
+
* Client right after `registerSubscription` to install the channel/queue the
|
|
615
|
+
* application reads from.
|
|
616
|
+
*/
|
|
617
|
+
setSubscriptionSink(id, sink) {
|
|
618
|
+
const sub = this.subs.get(id);
|
|
619
|
+
if (sub) sub.push = sink;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Inspects one decoded frame, delivers it to a matching waiter if one
|
|
623
|
+
* exists, and returns `true` (consumed). Returns `false` for frames with no
|
|
624
|
+
* matching waiter — these flow on to the resolver queue / event stream.
|
|
625
|
+
* Safe to call from the message handler.
|
|
626
|
+
*/
|
|
627
|
+
route(frame) {
|
|
628
|
+
if (!frame || typeof frame !== "object") return false;
|
|
629
|
+
const typ = frame.type;
|
|
630
|
+
const id = frame.id;
|
|
631
|
+
if (typ === "response" || typ === "error") {
|
|
632
|
+
if (!id) return false;
|
|
633
|
+
const pc = this.rpcs.get(id);
|
|
634
|
+
if (!pc) return false;
|
|
635
|
+
if (typ === "error") {
|
|
636
|
+
const errObj = frame.error ?? {};
|
|
637
|
+
const code = typeof errObj.code === "number" ? errObj.code : -32603;
|
|
638
|
+
const message = errObj.message ?? "daemon error";
|
|
639
|
+
pc.reject(new DaemonError(code, message, errObj.data));
|
|
640
|
+
} else {
|
|
641
|
+
const result = frame.result ?? frame;
|
|
642
|
+
pc.resolve(result);
|
|
643
|
+
}
|
|
644
|
+
this.rpcs.delete(id);
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
if (typ === "next" || typ === "complete") {
|
|
648
|
+
if (!id) return false;
|
|
649
|
+
const ps = this.subs.get(id);
|
|
650
|
+
if (!ps) return false;
|
|
651
|
+
if (ps.settled) return true;
|
|
652
|
+
ps.push(frame);
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
if (typ === "receipt_response") {
|
|
656
|
+
const rid = frame.receipt;
|
|
657
|
+
if (!rid) return false;
|
|
658
|
+
const ch = this.receipts.get(rid);
|
|
659
|
+
if (!ch) return false;
|
|
660
|
+
ch(frame);
|
|
661
|
+
this.receipts.delete(rid);
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
/** Reports whether an RPC waiter is registered for `id`. */
|
|
667
|
+
hasRPCWaiter(id) {
|
|
668
|
+
return this.rpcs.has(id);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// src/stream_terminal.ts
|
|
675
|
+
function isTurnEndCustomData(data) {
|
|
676
|
+
if (!data || typeof data !== "object") return false;
|
|
677
|
+
const customType = String(data.type ?? "").trim();
|
|
678
|
+
if (!TURN_END_CUSTOM_TYPES.has(customType)) return false;
|
|
679
|
+
if (customType === STREAM_END) {
|
|
680
|
+
const scope = String(data.scope ?? "turn").trim().toLowerCase();
|
|
681
|
+
return scope === "" || scope === "turn";
|
|
682
|
+
}
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
function isTurnProgressChunk(mode, data) {
|
|
686
|
+
if (mode === "messages" || mode === "updates") return true;
|
|
687
|
+
if (mode !== "custom" || !data || typeof data !== "object") return false;
|
|
688
|
+
if (isTurnEndCustomData(data)) return false;
|
|
689
|
+
const customType = String(data.type ?? "").trim();
|
|
690
|
+
if (TURN_PROGRESS_CUSTOM_TYPES.has(customType)) return true;
|
|
691
|
+
if (customType.startsWith("soothe.cognition.strange_loop.step")) return true;
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
function stalePendingFrameLabel(event) {
|
|
695
|
+
const eventType = String(event.type ?? "");
|
|
696
|
+
if (STALE_TURN_PENDING_TYPES.has(eventType)) return eventType;
|
|
697
|
+
if (eventType === "next") {
|
|
698
|
+
const payload = event.payload;
|
|
699
|
+
if (!payload || typeof payload !== "object") return null;
|
|
700
|
+
const p = payload;
|
|
701
|
+
const staleMode = String(p.mode ?? "");
|
|
702
|
+
if (STALE_TURN_PENDING_TYPES.has(staleMode)) return staleMode;
|
|
703
|
+
const inner = p.data;
|
|
704
|
+
if (inner && typeof inner === "object") {
|
|
705
|
+
return stalePendingFrameLabel(inner);
|
|
706
|
+
}
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
if (eventType === "event") {
|
|
710
|
+
const mode = String(event.mode ?? "");
|
|
711
|
+
const data = event.data;
|
|
712
|
+
if (mode === "custom" && isTurnEndCustomData(data)) {
|
|
713
|
+
return String(data.type ?? "").trim();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
function inboundNeedsDeliveryAck(event) {
|
|
719
|
+
const eventType = String(event.type ?? "");
|
|
720
|
+
if (eventType === "complete") return true;
|
|
721
|
+
if (eventType === "next") {
|
|
722
|
+
const payload = event.payload;
|
|
723
|
+
if (!payload || typeof payload !== "object") return false;
|
|
724
|
+
const p = payload;
|
|
725
|
+
const inner = p.data;
|
|
726
|
+
if (!inner || typeof inner !== "object") return false;
|
|
727
|
+
if (String(p.mode ?? "") === "event") {
|
|
728
|
+
return inboundNeedsAckFromEventShape(inner);
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
if (eventType === "event") return inboundNeedsAckFromEventShape(event);
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
function inboundNeedsAckFromEventShape(event) {
|
|
736
|
+
const mode = String(event.mode ?? "");
|
|
737
|
+
const data = event.data;
|
|
738
|
+
if (mode === "custom" && isTurnEndCustomData(data)) return true;
|
|
739
|
+
if (mode === "messages" && Array.isArray(data) && data.length > 0) {
|
|
740
|
+
const body = data[0];
|
|
741
|
+
if (!body || typeof body !== "object") return false;
|
|
742
|
+
const t = String(body.type ?? "");
|
|
743
|
+
return t === STREAM_END || t.includes("stream.end");
|
|
744
|
+
}
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
function extractLoopIdFromInbound(event) {
|
|
748
|
+
const direct = String(event.loop_id ?? "").trim();
|
|
749
|
+
if (direct) return direct;
|
|
750
|
+
if (String(event.type ?? "") !== "next") return "";
|
|
751
|
+
const payload = event.payload;
|
|
752
|
+
if (!payload || typeof payload !== "object") return "";
|
|
753
|
+
const p = payload;
|
|
754
|
+
const fromPayload = String(p.loop_id ?? "").trim();
|
|
755
|
+
if (fromPayload) return fromPayload;
|
|
756
|
+
const inner = p.data;
|
|
757
|
+
if (inner && typeof inner === "object") {
|
|
758
|
+
return String(inner.loop_id ?? "").trim();
|
|
759
|
+
}
|
|
760
|
+
return "";
|
|
761
|
+
}
|
|
762
|
+
var STREAM_END, TURN_END_CUSTOM_TYPES, TURN_PROGRESS_CUSTOM_TYPES, STALE_TURN_PENDING_TYPES;
|
|
763
|
+
var init_stream_terminal = __esm({
|
|
764
|
+
"src/stream_terminal.ts"() {
|
|
765
|
+
"use strict";
|
|
766
|
+
init_events();
|
|
767
|
+
STREAM_END = "soothe.stream.end";
|
|
768
|
+
TURN_END_CUSTOM_TYPES = /* @__PURE__ */ new Set([
|
|
769
|
+
STREAM_END,
|
|
770
|
+
EventStrangeLoopCompleted
|
|
771
|
+
]);
|
|
772
|
+
TURN_PROGRESS_CUSTOM_TYPES = /* @__PURE__ */ new Set([
|
|
773
|
+
EventPlanCreated,
|
|
774
|
+
EventStrangeLoopStepStarted,
|
|
775
|
+
EventStrangeLoopStepQueued,
|
|
776
|
+
EventStrangeLoopStepCompleted
|
|
777
|
+
]);
|
|
778
|
+
STALE_TURN_PENDING_TYPES = /* @__PURE__ */ new Set([
|
|
779
|
+
"connection_ack",
|
|
780
|
+
EventCardReplayBegin,
|
|
781
|
+
EventCardReplayEnd,
|
|
782
|
+
EventCardCreated,
|
|
783
|
+
"complete"
|
|
784
|
+
]);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// src/inbound_priority.ts
|
|
789
|
+
function inboundFrameDropPriority(event) {
|
|
790
|
+
if (!event) return DROP_PRIORITY_CRITICAL;
|
|
791
|
+
let eventType = String(event.type ?? "");
|
|
792
|
+
if (eventType === "event_batch" || eventType === "tool_call_updates_batch") {
|
|
793
|
+
return DROP_PRIORITY_HIGH;
|
|
794
|
+
}
|
|
795
|
+
if (eventType === "next") {
|
|
796
|
+
const payload = event.payload;
|
|
797
|
+
if (payload && typeof payload === "object") {
|
|
798
|
+
const p = payload;
|
|
799
|
+
const innerMode = String(p.mode ?? "");
|
|
800
|
+
const innerData = p.data;
|
|
801
|
+
if (innerMode === "messages") {
|
|
802
|
+
if (messagesWireTerminal(innerData)) return DROP_PRIORITY_CRITICAL;
|
|
803
|
+
if (Array.isArray(innerData) && innerData[0] && typeof innerData[0] === "object") {
|
|
804
|
+
if (String(innerData[0].phase ?? "") === "goal_completion") {
|
|
805
|
+
return DROP_PRIORITY_CRITICAL;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (String(p.type ?? "") === "complete") return DROP_PRIORITY_CRITICAL;
|
|
810
|
+
if (innerData && typeof innerData === "object") {
|
|
811
|
+
return inboundFrameDropPriority(innerData);
|
|
812
|
+
}
|
|
813
|
+
eventType = String(p.type ?? "");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (eventType === "complete" || eventType === "error" || eventType === "connection_ack") {
|
|
817
|
+
return DROP_PRIORITY_CRITICAL;
|
|
818
|
+
}
|
|
819
|
+
if (eventType === "status") {
|
|
820
|
+
const state = String(event.state ?? "");
|
|
821
|
+
if (["idle", "running", "stopped", "detached"].includes(state)) {
|
|
822
|
+
return DROP_PRIORITY_CRITICAL;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (eventType === "event") {
|
|
826
|
+
const mode = String(event.mode ?? "");
|
|
827
|
+
const data = event.data;
|
|
828
|
+
if (mode === "custom") {
|
|
829
|
+
if (isTurnEndCustomData(data)) return DROP_PRIORITY_CRITICAL;
|
|
830
|
+
if (data && typeof data === "object") {
|
|
831
|
+
const customType = String(data.type ?? "");
|
|
832
|
+
if (customType.startsWith("soothe.cognition.")) return DROP_PRIORITY_HIGH;
|
|
833
|
+
if (customType.startsWith("soothe.error.") || customType === "stream_degraded") {
|
|
834
|
+
return DROP_PRIORITY_CRITICAL;
|
|
835
|
+
}
|
|
836
|
+
if (customType === "soothe.ux.stream_tool_wire.tool_call_updates_batch") {
|
|
837
|
+
return DROP_PRIORITY_HIGH;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (mode === "messages") {
|
|
842
|
+
if (messagesWireTerminal(data)) return DROP_PRIORITY_CRITICAL;
|
|
843
|
+
if (Array.isArray(data) && data[0] && typeof data[0] === "object") {
|
|
844
|
+
if (String(data[0].phase ?? "") === "goal_completion") {
|
|
845
|
+
return DROP_PRIORITY_CRITICAL;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return DROP_PRIORITY_NORMAL;
|
|
851
|
+
}
|
|
852
|
+
function messagesWireTerminal(data) {
|
|
853
|
+
if (!Array.isArray(data) || data.length === 0) return false;
|
|
854
|
+
const body = data[0];
|
|
855
|
+
if (!body || typeof body !== "object") return false;
|
|
856
|
+
const t = String(body.type ?? "");
|
|
857
|
+
return t === STREAM_END || t.includes("stream.end");
|
|
858
|
+
}
|
|
859
|
+
var DROP_PRIORITY_CRITICAL, DROP_PRIORITY_HIGH, DROP_PRIORITY_NORMAL, DEFAULT_INBOUND_MAX_SIZE;
|
|
860
|
+
var init_inbound_priority = __esm({
|
|
861
|
+
"src/inbound_priority.ts"() {
|
|
862
|
+
"use strict";
|
|
863
|
+
init_stream_terminal();
|
|
864
|
+
DROP_PRIORITY_CRITICAL = 0;
|
|
865
|
+
DROP_PRIORITY_HIGH = 1;
|
|
866
|
+
DROP_PRIORITY_NORMAL = 2;
|
|
867
|
+
DEFAULT_INBOUND_MAX_SIZE = 2e4;
|
|
266
868
|
}
|
|
267
869
|
});
|
|
268
870
|
|
|
@@ -278,13 +880,39 @@ var init_client = __esm({
|
|
|
278
880
|
import_node_events = require("events");
|
|
279
881
|
import_ws = __toESM(require("ws"), 1);
|
|
280
882
|
init_config();
|
|
883
|
+
init_errors();
|
|
884
|
+
init_multiplexer();
|
|
885
|
+
init_intent_hints();
|
|
886
|
+
init_stream_terminal();
|
|
887
|
+
init_inbound_priority();
|
|
281
888
|
init_protocol();
|
|
282
889
|
Client = class extends import_node_events.EventEmitter {
|
|
283
890
|
url;
|
|
284
891
|
config;
|
|
285
892
|
ws = null;
|
|
286
893
|
messageBuffer = [];
|
|
894
|
+
inboundMaxSize = DEFAULT_INBOUND_MAX_SIZE;
|
|
895
|
+
inboundDroppedCount = 0;
|
|
896
|
+
onStreamDegraded = null;
|
|
287
897
|
resolvers = [];
|
|
898
|
+
// Protocol-1 handshake state (RFC-450 §8.2)
|
|
899
|
+
handshakeComplete = false;
|
|
900
|
+
negotiatedCapabilities = /* @__PURE__ */ new Set();
|
|
901
|
+
protocolVersion = null;
|
|
902
|
+
readinessState = null;
|
|
903
|
+
heartbeatIntervalMs = 0;
|
|
904
|
+
heartbeatTimer = null;
|
|
905
|
+
lastPongMonotonic = 0;
|
|
906
|
+
// Mid-session drop signal (RFC-450 §8.3). The 'disconnected' event is
|
|
907
|
+
// emitted exactly once when the connection drops, carrying a DisconnectCause
|
|
908
|
+
// that distinguishes clean (peer `disconnect`) from unclean (read/write
|
|
909
|
+
// error or missed pong). `disconnFired` guards the once-only delivery.
|
|
910
|
+
disconnFired = false;
|
|
911
|
+
// Pending-request/subscription multiplexer (RFC-629 constraint #1). Routes
|
|
912
|
+
// inbound frames by (type, id) instead of discarding non-matching events.
|
|
913
|
+
mux = new Multiplexer();
|
|
914
|
+
deliveryRecvSeq = /* @__PURE__ */ new Map();
|
|
915
|
+
deliveryAckedSeq = /* @__PURE__ */ new Map();
|
|
288
916
|
constructor(url, config) {
|
|
289
917
|
super();
|
|
290
918
|
this.url = url;
|
|
@@ -293,7 +921,10 @@ var init_client = __esm({
|
|
|
293
921
|
// ---------------------------------------------------------------------------
|
|
294
922
|
// Connection lifecycle
|
|
295
923
|
// ---------------------------------------------------------------------------
|
|
296
|
-
/**
|
|
924
|
+
/**
|
|
925
|
+
* Dials the Soothe daemon WebSocket and completes the protocol-1 handshake
|
|
926
|
+
* (connection_init → connection_ack with readiness_state "ready").
|
|
927
|
+
*/
|
|
297
928
|
connect() {
|
|
298
929
|
return new Promise((resolve, reject) => {
|
|
299
930
|
const ws = new import_ws.default(this.url, {
|
|
@@ -301,9 +932,27 @@ var init_client = __esm({
|
|
|
301
932
|
});
|
|
302
933
|
ws.on("open", () => {
|
|
303
934
|
this.ws = ws;
|
|
304
|
-
|
|
935
|
+
this.disconnFired = false;
|
|
936
|
+
this._lastCause = null;
|
|
937
|
+
this.mux = new Multiplexer();
|
|
938
|
+
this._performHandshake().then((ack) => {
|
|
939
|
+
this.handshakeComplete = true;
|
|
940
|
+
this.readinessState = ack.result?.readiness_state ?? "ready";
|
|
941
|
+
this._startHeartbeat();
|
|
942
|
+
resolve();
|
|
943
|
+
}).catch((err) => {
|
|
944
|
+
this._stopHeartbeat();
|
|
945
|
+
this.ws = null;
|
|
946
|
+
this.handshakeComplete = false;
|
|
947
|
+
try {
|
|
948
|
+
ws.close(1011, "handshake failed");
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
reject(err);
|
|
952
|
+
});
|
|
305
953
|
});
|
|
306
954
|
ws.on("error", (err) => {
|
|
955
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
307
956
|
if (!this.ws) {
|
|
308
957
|
reject(new Error(`soothe dial: ${err.message}`));
|
|
309
958
|
}
|
|
@@ -311,20 +960,44 @@ var init_client = __esm({
|
|
|
311
960
|
ws.on("message", (data) => {
|
|
312
961
|
const text = data.toString();
|
|
313
962
|
for (const frame of splitWirePayload(text)) {
|
|
963
|
+
let msg;
|
|
314
964
|
try {
|
|
315
|
-
|
|
316
|
-
if (msg !== null) {
|
|
317
|
-
this.messageBuffer.push(msg);
|
|
318
|
-
this.emit("message", msg);
|
|
319
|
-
const resolver = this.resolvers.shift();
|
|
320
|
-
if (resolver) resolver(msg);
|
|
321
|
-
}
|
|
965
|
+
msg = decodeMessage(frame);
|
|
322
966
|
} catch {
|
|
967
|
+
continue;
|
|
323
968
|
}
|
|
324
|
-
|
|
969
|
+
if (msg === null) continue;
|
|
970
|
+
const m = msg;
|
|
971
|
+
if (m.type === "ping") {
|
|
972
|
+
this._sendRaw(pongEnvelope());
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
if (m.type === "pong") {
|
|
976
|
+
this.lastPongMonotonic = Date.now();
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (m.type === "disconnect") {
|
|
980
|
+
this._signalDisconnect(1 /* Clean */);
|
|
981
|
+
}
|
|
982
|
+
if (this.mux.route(m)) {
|
|
983
|
+
this._trackInboundDeliveryAck(m);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
this._trackInboundDeliveryAck(m);
|
|
987
|
+
const resolver = this.resolvers.shift();
|
|
988
|
+
if (resolver) {
|
|
989
|
+
resolver(msg);
|
|
990
|
+
} else {
|
|
991
|
+
this.enqueueMessageBuffer(msg);
|
|
992
|
+
}
|
|
993
|
+
this.emit("message", msg);
|
|
994
|
+
}
|
|
325
995
|
});
|
|
326
996
|
ws.on("close", () => {
|
|
327
997
|
this.ws = null;
|
|
998
|
+
this._stopHeartbeat();
|
|
999
|
+
this.handshakeComplete = false;
|
|
1000
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
328
1001
|
this.emit("close");
|
|
329
1002
|
for (const resolver of this.resolvers) {
|
|
330
1003
|
resolver(null);
|
|
@@ -333,18 +1006,219 @@ var init_client = __esm({
|
|
|
333
1006
|
});
|
|
334
1007
|
});
|
|
335
1008
|
}
|
|
336
|
-
/**
|
|
1009
|
+
/** Sends a `disconnect` notification and closes the WebSocket. */
|
|
337
1010
|
close() {
|
|
1011
|
+
this._stopHeartbeat();
|
|
338
1012
|
if (!this.ws) return;
|
|
339
1013
|
try {
|
|
1014
|
+
if (this.ws.readyState === import_ws.default.OPEN) {
|
|
1015
|
+
this.ws.send(JSON.stringify(disconnectEnvelope()));
|
|
1016
|
+
}
|
|
340
1017
|
this.ws.close(1e3, "");
|
|
341
1018
|
} catch {
|
|
342
1019
|
}
|
|
343
1020
|
this.ws = null;
|
|
1021
|
+
this.handshakeComplete = false;
|
|
344
1022
|
}
|
|
345
|
-
/** Returns whether the client has an active WebSocket connection. */
|
|
1023
|
+
/** Returns whether the client has an active, handshaked WebSocket connection. */
|
|
346
1024
|
isConnected() {
|
|
347
|
-
return this.ws !== null && this.ws.readyState === import_ws.default.OPEN;
|
|
1025
|
+
return this.ws !== null && this.ws.readyState === import_ws.default.OPEN && this.handshakeComplete;
|
|
1026
|
+
}
|
|
1027
|
+
// ---------------------------------------------------------------------------
|
|
1028
|
+
// Mid-session drop signal + reconnect/reattach (RFC-450 §8.3, RFC-629 L0)
|
|
1029
|
+
// ---------------------------------------------------------------------------
|
|
1030
|
+
/**
|
|
1031
|
+
* Returns whether the connection has dropped (the `'disconnected'` event has
|
|
1032
|
+
* fired). Pair with the `'disconnected'` event for the signal. Use
|
|
1033
|
+
* `disconnectCause()` to read the cause.
|
|
1034
|
+
*/
|
|
1035
|
+
isDisconnected() {
|
|
1036
|
+
return this.disconnFired;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Returns the cause of the most recent drop, or `null` if the connection has
|
|
1040
|
+
* not dropped. Clean follows a `disconnect` notification (loops keep running
|
|
1041
|
+
* server-side); unclean is a read/write error or missed pong.
|
|
1042
|
+
*/
|
|
1043
|
+
disconnectCause() {
|
|
1044
|
+
if (!this.disconnFired) return null;
|
|
1045
|
+
return this._lastCause ?? 0 /* Unclean */;
|
|
1046
|
+
}
|
|
1047
|
+
_lastCause = null;
|
|
1048
|
+
/**
|
|
1049
|
+
* Delivers the disconnect cause exactly once via the `'disconnected'` event.
|
|
1050
|
+
* Safe to call from any path; subsequent calls are no-ops. Listeners receive
|
|
1051
|
+
* the cause as the event argument.
|
|
1052
|
+
*/
|
|
1053
|
+
_signalDisconnect(cause) {
|
|
1054
|
+
if (this.disconnFired) return;
|
|
1055
|
+
this.disconnFired = true;
|
|
1056
|
+
this._lastCause = cause;
|
|
1057
|
+
try {
|
|
1058
|
+
this.emit("disconnected", cause);
|
|
1059
|
+
} catch {
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Re-dials the daemon and re-handshakes after a connection drop (RFC-450
|
|
1064
|
+
* §8.3). Does not re-establish loop subscriptions; follow with
|
|
1065
|
+
* `reattachAndProbe()` to resume a loop session. The caller should invoke
|
|
1066
|
+
* this after the `'disconnected'` event fires. Reuses the same Client,
|
|
1067
|
+
* resetting the drop signal and multiplexer.
|
|
1068
|
+
*
|
|
1069
|
+
* Performs bounded-retry backoff using the configured reconnect knobs.
|
|
1070
|
+
*/
|
|
1071
|
+
async reconnect() {
|
|
1072
|
+
const maxAttempts = this.config.reconnectMaxAttempts || 10;
|
|
1073
|
+
const initialDelay = this.config.reconnectInitialDelay || 500;
|
|
1074
|
+
const maxDelay = this.config.reconnectMaxDelay || 1e4;
|
|
1075
|
+
let lastErr = null;
|
|
1076
|
+
let delay = initialDelay;
|
|
1077
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1078
|
+
try {
|
|
1079
|
+
await this.connect();
|
|
1080
|
+
return;
|
|
1081
|
+
} catch (err) {
|
|
1082
|
+
lastErr = err;
|
|
1083
|
+
}
|
|
1084
|
+
if (attempt < maxAttempts) {
|
|
1085
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1086
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
throw new ReconnectError(this.url, maxAttempts, lastErr ?? new Error("unknown error"));
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Resumes an existing loop after a reconnect: issues `loop_reattach`,
|
|
1093
|
+
* re-subscribes to `loop_events`, then runs a `loop_get` liveness probe to
|
|
1094
|
+
* detect stale loops that accept the handshake but silently drop input.
|
|
1095
|
+
* Returns a `StaleLoopError` when the probe fails; callers should fall back
|
|
1096
|
+
* to a fresh `loop_new` bootstrap.
|
|
1097
|
+
*
|
|
1098
|
+
* Per RFC-629: connection-level readiness is the handshake's readiness_state
|
|
1099
|
+
* (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
|
|
1100
|
+
* probe.
|
|
1101
|
+
*/
|
|
1102
|
+
async reattachAndProbe(loopID) {
|
|
1103
|
+
if (!loopID || !loopID.trim()) {
|
|
1104
|
+
throw new Error("soothe: reattachAndProbe requires a loop id");
|
|
1105
|
+
}
|
|
1106
|
+
const lid = loopID.trim();
|
|
1107
|
+
const reattachTimeout = this.config.loopStatusTimeout || 15e3;
|
|
1108
|
+
try {
|
|
1109
|
+
await this.requestResponse(
|
|
1110
|
+
"loop_reattach",
|
|
1111
|
+
{ loop_id: lid },
|
|
1112
|
+
"loop_reattach",
|
|
1113
|
+
reattachTimeout
|
|
1114
|
+
);
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
throw new Error(`loop_reattach: ${err.message}`);
|
|
1117
|
+
}
|
|
1118
|
+
const subTimeout = this.config.subscriptionTimeout || 1e4;
|
|
1119
|
+
try {
|
|
1120
|
+
await this.subscribe(
|
|
1121
|
+
"loop_events",
|
|
1122
|
+
{ loop_id: lid, verbosity: this.config.verbosityLevel },
|
|
1123
|
+
subTimeout
|
|
1124
|
+
);
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
throw new Error(`loop events subscription failed: ${err.message}`);
|
|
1127
|
+
}
|
|
1128
|
+
const probeTimeout = this.config.reattachProbeTimeout || 5e3;
|
|
1129
|
+
try {
|
|
1130
|
+
await this.getLoop(lid, probeTimeout);
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
if (err instanceof DaemonError && err.code === -32200) {
|
|
1133
|
+
throw new StaleLoopError(lid, err);
|
|
1134
|
+
}
|
|
1135
|
+
throw new StaleLoopError(lid, err);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// ---------------------------------------------------------------------------
|
|
1139
|
+
// Protocol-1 handshake (RFC-450 §8.2)
|
|
1140
|
+
// ---------------------------------------------------------------------------
|
|
1141
|
+
/** Send connection_init and wait for connection_ack with readiness "ready". */
|
|
1142
|
+
async _performHandshake() {
|
|
1143
|
+
const init = connectionInitEnvelope({
|
|
1144
|
+
client_version: CLIENT_VERSION,
|
|
1145
|
+
client_name: "soothe-client-ts",
|
|
1146
|
+
accept_proto: [PROTO_VERSION],
|
|
1147
|
+
capabilities: DEFAULT_CLIENT_CAPABILITIES
|
|
1148
|
+
});
|
|
1149
|
+
await this.sendMessage(init);
|
|
1150
|
+
const deadline = Date.now() + this.config.daemonReadyTimeout;
|
|
1151
|
+
while (Date.now() < deadline) {
|
|
1152
|
+
const remaining = deadline - Date.now();
|
|
1153
|
+
if (remaining <= 0) break;
|
|
1154
|
+
const ev = await this.readEventWithTimeout(remaining);
|
|
1155
|
+
if (ev === null) {
|
|
1156
|
+
throw new Error("connection closed during handshake");
|
|
1157
|
+
}
|
|
1158
|
+
if (ev.type === "status") {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
if (ev.type !== "connection_ack") {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const ack = ev;
|
|
1165
|
+
const result = ack.result ?? {};
|
|
1166
|
+
const state = result.readiness_state ?? "ready";
|
|
1167
|
+
this.protocolVersion = result.protocol_version ?? PROTO_VERSION;
|
|
1168
|
+
this.negotiatedCapabilities = new Set(result.capabilities ?? []);
|
|
1169
|
+
this.heartbeatIntervalMs = result.heartbeat_interval_ms ?? 0;
|
|
1170
|
+
if (state === "incompatible") {
|
|
1171
|
+
throw new Error(`protocol version incompatible: daemon returned ${this.protocolVersion}`);
|
|
1172
|
+
}
|
|
1173
|
+
if (state === "ready") {
|
|
1174
|
+
return ack;
|
|
1175
|
+
}
|
|
1176
|
+
if (state === "error") {
|
|
1177
|
+
throw new Error("daemon startup failed");
|
|
1178
|
+
}
|
|
1179
|
+
if (state === "degraded") {
|
|
1180
|
+
throw new Error("daemon is degraded");
|
|
1181
|
+
}
|
|
1182
|
+
await this._sleep(50);
|
|
1183
|
+
await this.sendMessage(init);
|
|
1184
|
+
}
|
|
1185
|
+
throw new Error(`timeout after ${this.config.daemonReadyTimeout}ms waiting for connection_ack`);
|
|
1186
|
+
}
|
|
1187
|
+
// ---------------------------------------------------------------------------
|
|
1188
|
+
// Heartbeat (RFC-450 §8.3)
|
|
1189
|
+
// ---------------------------------------------------------------------------
|
|
1190
|
+
_startHeartbeat() {
|
|
1191
|
+
if (!this.negotiatedCapabilities.has("heartbeat")) return;
|
|
1192
|
+
const interval = this.heartbeatIntervalMs;
|
|
1193
|
+
if (interval <= 0) return;
|
|
1194
|
+
this.lastPongMonotonic = Date.now();
|
|
1195
|
+
this.heartbeatTimer = setInterval(() => this._heartbeatTick(interval), interval);
|
|
1196
|
+
}
|
|
1197
|
+
_stopHeartbeat() {
|
|
1198
|
+
if (this.heartbeatTimer) {
|
|
1199
|
+
clearInterval(this.heartbeatTimer);
|
|
1200
|
+
this.heartbeatTimer = null;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
_heartbeatTick(intervalMs) {
|
|
1204
|
+
if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
|
|
1205
|
+
const timeoutMs = Math.max(1e4, intervalMs * 2);
|
|
1206
|
+
const now = Date.now();
|
|
1207
|
+
if (now - (this.lastPongMonotonic || now) > intervalMs + timeoutMs) {
|
|
1208
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
1209
|
+
try {
|
|
1210
|
+
this.ws.close(1001, "heartbeat timeout");
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
this.ws.send(JSON.stringify(pingEnvelope()));
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
_sleep(ms) {
|
|
1221
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
348
1222
|
}
|
|
349
1223
|
// ---------------------------------------------------------------------------
|
|
350
1224
|
// Core messaging
|
|
@@ -352,17 +1226,29 @@ var init_client = __esm({
|
|
|
352
1226
|
/** Serializes msg as JSON and sends it as a WebSocket text frame. */
|
|
353
1227
|
sendMessage(msg) {
|
|
354
1228
|
return new Promise((resolve, reject) => {
|
|
355
|
-
if (!this.ws) {
|
|
1229
|
+
if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
|
|
356
1230
|
reject(new Error("soothe: not connected"));
|
|
357
1231
|
return;
|
|
358
1232
|
}
|
|
359
1233
|
const payload = JSON.stringify(msg);
|
|
360
1234
|
this.ws.send(payload, (err) => {
|
|
361
|
-
if (err)
|
|
362
|
-
|
|
1235
|
+
if (err) {
|
|
1236
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
1237
|
+
reject(err);
|
|
1238
|
+
} else {
|
|
1239
|
+
resolve();
|
|
1240
|
+
}
|
|
363
1241
|
});
|
|
364
1242
|
});
|
|
365
1243
|
}
|
|
1244
|
+
/** Low-level send that does not reject on a missing connection (best-effort). */
|
|
1245
|
+
_sendRaw(msg) {
|
|
1246
|
+
if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
|
|
1247
|
+
try {
|
|
1248
|
+
this.ws.send(JSON.stringify(msg));
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
366
1252
|
/** Returns an async iterable of decoded messages. Ends when connection closes. */
|
|
367
1253
|
async *receiveMessages(signal) {
|
|
368
1254
|
while (true) {
|
|
@@ -395,7 +1281,7 @@ var init_client = __esm({
|
|
|
395
1281
|
if (msg === null) return null;
|
|
396
1282
|
return msg;
|
|
397
1283
|
}
|
|
398
|
-
/** Reads a single event with a timeout. Returns null on timeout or
|
|
1284
|
+
/** Reads a single event with a timeout. Returns null on timeout or close. */
|
|
399
1285
|
readEventWithTimeout(timeout) {
|
|
400
1286
|
if (this.messageBuffer.length > 0) {
|
|
401
1287
|
const msg = this.messageBuffer.shift();
|
|
@@ -415,270 +1301,598 @@ var init_client = __esm({
|
|
|
415
1301
|
this.resolvers.push(resolver);
|
|
416
1302
|
});
|
|
417
1303
|
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Remove stale handshake/terminal frames left in `messageBuffer` before a turn.
|
|
1306
|
+
* Returns labels of removed frames (in order).
|
|
1307
|
+
*/
|
|
1308
|
+
peelStalePendingControlEvents() {
|
|
1309
|
+
if (this.messageBuffer.length === 0) return [];
|
|
1310
|
+
const kept = [];
|
|
1311
|
+
const removed = [];
|
|
1312
|
+
while (this.messageBuffer.length > 0) {
|
|
1313
|
+
const event = this.messageBuffer.shift();
|
|
1314
|
+
const label = stalePendingFrameLabel(event);
|
|
1315
|
+
if (label !== null) {
|
|
1316
|
+
removed.push(label);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
kept.push(event);
|
|
1320
|
+
}
|
|
1321
|
+
this.messageBuffer = kept;
|
|
1322
|
+
return removed;
|
|
1323
|
+
}
|
|
1324
|
+
/** True when the underlying socket is still open (may not be handshaked). */
|
|
1325
|
+
isConnectionAlive() {
|
|
1326
|
+
return this.ws !== null && this.ws.readyState === import_ws.default.OPEN;
|
|
1327
|
+
}
|
|
1328
|
+
/** Override pending buffer cap (tests / tuning). */
|
|
1329
|
+
setInboundMaxSize(n) {
|
|
1330
|
+
if (n > 0) this.inboundMaxSize = n;
|
|
1331
|
+
}
|
|
1332
|
+
/** How many NORMAL-priority frames were dropped under backpressure. */
|
|
1333
|
+
inboundDropped() {
|
|
1334
|
+
return this.inboundDroppedCount;
|
|
1335
|
+
}
|
|
1336
|
+
/** Hook invoked on the first inbound overflow drop. */
|
|
1337
|
+
setStreamDegradedCallback(fn) {
|
|
1338
|
+
this.onStreamDegraded = fn;
|
|
1339
|
+
}
|
|
1340
|
+
enqueueMessageBuffer(msg) {
|
|
1341
|
+
const max = this.inboundMaxSize > 0 ? this.inboundMaxSize : DEFAULT_INBOUND_MAX_SIZE;
|
|
1342
|
+
if (this.messageBuffer.length < max) {
|
|
1343
|
+
this.messageBuffer.push(msg);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const ev = msg;
|
|
1347
|
+
let dropIdx = -1;
|
|
1348
|
+
let dropPri = -1;
|
|
1349
|
+
for (let i = 0; i < this.messageBuffer.length; i++) {
|
|
1350
|
+
const p = inboundFrameDropPriority(this.messageBuffer[i]);
|
|
1351
|
+
if (p > dropPri) {
|
|
1352
|
+
dropPri = p;
|
|
1353
|
+
dropIdx = i;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const incomingPri = inboundFrameDropPriority(ev);
|
|
1357
|
+
if (dropIdx >= 0 && dropPri >= DROP_PRIORITY_NORMAL) {
|
|
1358
|
+
this.messageBuffer.splice(dropIdx, 1);
|
|
1359
|
+
this.messageBuffer.push(msg);
|
|
1360
|
+
this.noteInboundDrop();
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (incomingPri >= DROP_PRIORITY_NORMAL) {
|
|
1364
|
+
this.noteInboundDrop();
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (this.messageBuffer.length > 0) {
|
|
1368
|
+
this.messageBuffer.shift();
|
|
1369
|
+
this.noteInboundDrop();
|
|
1370
|
+
}
|
|
1371
|
+
this.messageBuffer.push(msg);
|
|
1372
|
+
}
|
|
1373
|
+
noteInboundDrop() {
|
|
1374
|
+
this.inboundDroppedCount += 1;
|
|
1375
|
+
if (this.onStreamDegraded && this.inboundDroppedCount === 1) {
|
|
1376
|
+
try {
|
|
1377
|
+
this.onStreamDegraded(1, "inbound_queue_overflow");
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// ---------------------------------------------------------------------------
|
|
1383
|
+
// Protocol-1 RPC primitives (RFC-450 §5/§9)
|
|
1384
|
+
// ---------------------------------------------------------------------------
|
|
1385
|
+
/**
|
|
1386
|
+
* Reads the next frame directly from the live socket (via a resolver),
|
|
1387
|
+
* bypassing `messageBuffer`. Used by RPC waits so that stream events
|
|
1388
|
+
* previously buffered for `readEvent()`/`receiveMessages()` consumers are
|
|
1389
|
+
* not re-cycled through the RPC wait loop (which would stall behind a
|
|
1390
|
+
* continuous subscription stream). Non-RPC frames read here are pushed to
|
|
1391
|
+
* `messageBuffer` for the stream readers.
|
|
1392
|
+
*/
|
|
1393
|
+
readLiveEventWithTimeout(timeout) {
|
|
1394
|
+
if (!this.ws) return Promise.resolve(null);
|
|
1395
|
+
return new Promise((resolve) => {
|
|
1396
|
+
const timer = setTimeout(() => {
|
|
1397
|
+
const idx = this.resolvers.indexOf(resolver);
|
|
1398
|
+
if (idx >= 0) this.resolvers.splice(idx, 1);
|
|
1399
|
+
resolve(null);
|
|
1400
|
+
}, timeout);
|
|
1401
|
+
const resolver = (val) => {
|
|
1402
|
+
clearTimeout(timer);
|
|
1403
|
+
resolve(val);
|
|
1404
|
+
};
|
|
1405
|
+
this.resolvers.push(resolver);
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Sends a `request` envelope and waits for the matching `response` (or
|
|
1410
|
+
* `error`) correlated by `id` (RFC-450 §5/§9). Returns the `result` object.
|
|
1411
|
+
*
|
|
1412
|
+
* Multiplexer-aware (RFC-629 constraint #1): registers a pending RPC wait
|
|
1413
|
+
* keyed by the request id so that, even when a `receiveMessages()` reader
|
|
1414
|
+
* is concurrently active, the matching `response`/`error` is routed to
|
|
1415
|
+
* this caller instead of being discarded or buffered behind a stream.
|
|
1416
|
+
* Non-matching frames are routed to their own waiters by the multiplexer
|
|
1417
|
+
* or flow on to the resolver queue for stream readers.
|
|
1418
|
+
*/
|
|
1419
|
+
async requestResponse(method, params, responseType, timeout = 15e3) {
|
|
1420
|
+
const req = requestEnvelope(method, params);
|
|
1421
|
+
const rid = req.id;
|
|
1422
|
+
const { call, unregister } = this.mux.registerRPC(rid);
|
|
1423
|
+
const label = responseType ?? method;
|
|
1424
|
+
try {
|
|
1425
|
+
await this.sendMessage(req);
|
|
1426
|
+
const result = await this._raceRPC(call, timeout, label);
|
|
1427
|
+
return result;
|
|
1428
|
+
} finally {
|
|
1429
|
+
unregister();
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Races the multiplexer's RPC promise against a timeout and the connection
|
|
1434
|
+
* drop signal. Resolves with the `result` on `response`; rejects with a
|
|
1435
|
+
* `DaemonError` on `error`; rejects with a timeout/close error otherwise.
|
|
1436
|
+
* The disconnect listener is always removed to avoid accumulating handlers.
|
|
1437
|
+
*/
|
|
1438
|
+
async _raceRPC(call, timeout, label) {
|
|
1439
|
+
let timer;
|
|
1440
|
+
let cleanupDisconnect = () => {
|
|
1441
|
+
};
|
|
1442
|
+
const timeoutP = new Promise((_, reject) => {
|
|
1443
|
+
timer = setTimeout(
|
|
1444
|
+
() => reject(new Error(`timeout after ${timeout}ms waiting for ${label}`)),
|
|
1445
|
+
timeout
|
|
1446
|
+
);
|
|
1447
|
+
});
|
|
1448
|
+
const closedP = new Promise((_, reject) => {
|
|
1449
|
+
if (this.disconnFired) {
|
|
1450
|
+
reject(new Error(`connection closed waiting for ${label}`));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const onDisconnect = () => {
|
|
1454
|
+
reject(new Error(`connection closed waiting for ${label}`));
|
|
1455
|
+
};
|
|
1456
|
+
this.once("disconnected", onDisconnect);
|
|
1457
|
+
cleanupDisconnect = () => this.removeListener("disconnected", onDisconnect);
|
|
1458
|
+
});
|
|
1459
|
+
try {
|
|
1460
|
+
return await Promise.race([call, timeoutP, closedP]);
|
|
1461
|
+
} finally {
|
|
1462
|
+
if (timer) clearTimeout(timer);
|
|
1463
|
+
cleanupDisconnect();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Sends a pre-built envelope (e.g. `unsubscribe`) that carries an `id` and
|
|
1468
|
+
* waits for the matching `response`/`error`. Used for envelope types that
|
|
1469
|
+
* are not `request` (e.g. `unsubscribe` → `autopilot_unsubscribe`) but still
|
|
1470
|
+
* expect a correlated response from the daemon.
|
|
1471
|
+
*/
|
|
1472
|
+
async _requestResponseForEnvelope(env, label, timeout) {
|
|
1473
|
+
const rid = env.id;
|
|
1474
|
+
const { call, unregister } = this.mux.registerRPC(rid);
|
|
1475
|
+
try {
|
|
1476
|
+
await this.sendMessage(env);
|
|
1477
|
+
return await this._raceRPC(call, timeout, label);
|
|
1478
|
+
} finally {
|
|
1479
|
+
unregister();
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
/** Sends a fire-and-forget `notification` envelope (no response expected). */
|
|
1483
|
+
notify(method, params) {
|
|
1484
|
+
return this.sendMessage(notificationEnvelope(method, params));
|
|
1485
|
+
}
|
|
1486
|
+
_trackInboundDeliveryAck(event) {
|
|
1487
|
+
if (String(event.type ?? "") === "event_batch") {
|
|
1488
|
+
const events = event.events;
|
|
1489
|
+
if (Array.isArray(events)) {
|
|
1490
|
+
for (const sub of events) {
|
|
1491
|
+
if (sub && typeof sub === "object") {
|
|
1492
|
+
this._trackInboundDeliveryAck(sub);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (!inboundNeedsDeliveryAck(event)) return;
|
|
1499
|
+
const loopId = extractLoopIdFromInbound(event);
|
|
1500
|
+
if (!loopId) return;
|
|
1501
|
+
const next = (this.deliveryRecvSeq.get(loopId) ?? 0) + 1;
|
|
1502
|
+
this.deliveryRecvSeq.set(loopId, next);
|
|
1503
|
+
void this._sendDeliveryAck(loopId, next);
|
|
1504
|
+
}
|
|
1505
|
+
async _sendDeliveryAck(loopId, seq) {
|
|
1506
|
+
const acked = this.deliveryAckedSeq.get(loopId) ?? 0;
|
|
1507
|
+
if (seq <= acked) return;
|
|
1508
|
+
this.deliveryAckedSeq.set(loopId, seq);
|
|
1509
|
+
if (!this.isConnected()) return;
|
|
1510
|
+
try {
|
|
1511
|
+
await this.notify("delivery_ack", { loop_id: loopId, seq });
|
|
1512
|
+
} catch {
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Starts a subscription stream. Returns the subscription `id` for later
|
|
1517
|
+
* correlation and `unsubscribe()`. Stream events arrive as `next` frames
|
|
1518
|
+
* carrying the same `id`.
|
|
1519
|
+
*/
|
|
1520
|
+
async subscribe(method, params, timeout = 5e3) {
|
|
1521
|
+
const req = subscribeEnvelope(method, params);
|
|
1522
|
+
const subId = req.id;
|
|
1523
|
+
await this.sendMessage(req);
|
|
1524
|
+
const deadline = Date.now() + timeout;
|
|
1525
|
+
while (Date.now() < deadline) {
|
|
1526
|
+
const remaining = deadline - Date.now();
|
|
1527
|
+
if (remaining <= 0) break;
|
|
1528
|
+
const ev = await this.readLiveEventWithTimeout(remaining);
|
|
1529
|
+
if (ev === null) break;
|
|
1530
|
+
const evId = ev.id;
|
|
1531
|
+
if (evId !== subId) {
|
|
1532
|
+
this.enqueueMessageBuffer(ev);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
const typ = ev.type;
|
|
1536
|
+
if (typ === "error") {
|
|
1537
|
+
const errObj = ev.error ?? {};
|
|
1538
|
+
throw new DaemonError(
|
|
1539
|
+
errObj.code ?? -32603,
|
|
1540
|
+
errObj.message ?? "subscription rejected",
|
|
1541
|
+
errObj.data
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
if (typ === "next" || typ === "complete") {
|
|
1545
|
+
this.messageBuffer.unshift(ev);
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return subId;
|
|
1550
|
+
}
|
|
1551
|
+
/** Cancels an active subscription by id. */
|
|
1552
|
+
unsubscribe(subscriptionId) {
|
|
1553
|
+
return this.sendMessage(unsubscribeEnvelope(subscriptionId));
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Reads the next stream event from a subscription. For `next` frames the
|
|
1557
|
+
* `payload` is returned; for `complete`/`error` the full envelope is
|
|
1558
|
+
* returned so the caller can inspect termination.
|
|
1559
|
+
*/
|
|
1560
|
+
async next() {
|
|
1561
|
+
const ev = await this.readEvent();
|
|
1562
|
+
if (ev === null) return null;
|
|
1563
|
+
if (ev.type === "next") {
|
|
1564
|
+
return ev.payload ?? {};
|
|
1565
|
+
}
|
|
1566
|
+
return ev;
|
|
1567
|
+
}
|
|
418
1568
|
// ---------------------------------------------------------------------------
|
|
419
1569
|
// High-level API methods (Loop-first, RFC-503)
|
|
420
1570
|
// ---------------------------------------------------------------------------
|
|
421
|
-
/** Sends user input to the daemon (loop_input; requires loopID). */
|
|
1571
|
+
/** Sends user input to the daemon (loop_input notification; requires loopID). */
|
|
422
1572
|
sendInput(text, options) {
|
|
423
1573
|
const loopId = (options?.loopID ?? "").trim();
|
|
424
1574
|
if (!loopId) {
|
|
425
1575
|
return Promise.reject(new Error("sendInput requires options.loopID"));
|
|
426
1576
|
}
|
|
427
|
-
const
|
|
428
|
-
type: "loop_input",
|
|
1577
|
+
const params = {
|
|
429
1578
|
loop_id: loopId,
|
|
430
1579
|
content: text,
|
|
431
1580
|
autonomous: options?.autonomous ?? false
|
|
432
1581
|
};
|
|
433
|
-
if (options?.maxIterations !== void 0)
|
|
434
|
-
if (options?.subagent)
|
|
435
|
-
if (options?.
|
|
436
|
-
if (options?.
|
|
437
|
-
if (options?.
|
|
438
|
-
if (options?.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
1582
|
+
if (options?.maxIterations !== void 0) params.max_iterations = options.maxIterations;
|
|
1583
|
+
if (options?.subagent) params.preferred_subagent = options.subagent;
|
|
1584
|
+
if (options?.model) params.model = options.model;
|
|
1585
|
+
if (options?.modelParams) params.model_params = options.modelParams;
|
|
1586
|
+
if (options?.attachments) params.attachments = options.attachments;
|
|
1587
|
+
if (options?.intentHint) {
|
|
1588
|
+
const hintError = validateLoopInputIntentHint(options.intentHint);
|
|
1589
|
+
if (hintError) {
|
|
1590
|
+
return Promise.reject(new Error(hintError));
|
|
1591
|
+
}
|
|
1592
|
+
params.intent_hint = options.intentHint;
|
|
1593
|
+
}
|
|
1594
|
+
if (options?.responseSchema) params.response_schema = options.responseSchema;
|
|
1595
|
+
if (options?.responseSchemaName) params.response_schema_name = options.responseSchemaName;
|
|
1596
|
+
if (options?.responseSchemaStrict !== void 0)
|
|
1597
|
+
params.response_schema_strict = options.responseSchemaStrict;
|
|
1598
|
+
if (options?.clarificationMode) params.clarification_mode = options.clarificationMode;
|
|
1599
|
+
if (options?.clarificationAnswer) params.clarification_answer = true;
|
|
1600
|
+
if (options?.clarificationAnswers) params.clarification_answers = options.clarificationAnswers;
|
|
1601
|
+
return this.notify("loop_input", params);
|
|
1602
|
+
}
|
|
1603
|
+
/** Sends a slash command to the daemon (slash_command notification). */
|
|
442
1604
|
sendCommand(cmd) {
|
|
443
|
-
return this.
|
|
1605
|
+
return this.notify("slash_command", { cmd });
|
|
444
1606
|
}
|
|
445
1607
|
// ---------------------------------------------------------------------------
|
|
446
1608
|
// Loop lifecycle methods (RFC-503)
|
|
447
1609
|
// ---------------------------------------------------------------------------
|
|
448
|
-
/** Requests the daemon to create a new
|
|
1610
|
+
/** Requests the daemon to create a new StrangeLoop and waits for the response. */
|
|
449
1611
|
sendLoopNew(opts) {
|
|
450
1612
|
return this.sendMessage(newLoopNewMessage(opts));
|
|
451
1613
|
}
|
|
452
|
-
/** Subscribes to events for a loop. */
|
|
453
|
-
sendLoopSubscribe(loopID, verbosity, streamDelivery) {
|
|
454
|
-
|
|
455
|
-
if (streamDelivery) {
|
|
456
|
-
msg.stream_delivery = streamDelivery;
|
|
457
|
-
}
|
|
458
|
-
return this.sendMessage(msg);
|
|
459
|
-
}
|
|
460
|
-
/** Detaches from a loop (keeps loop running). */
|
|
461
|
-
sendLoopDetach(loopID, requestID) {
|
|
462
|
-
return this.sendMessage({
|
|
463
|
-
type: "loop_detach",
|
|
1614
|
+
/** Subscribes to events for a loop (subscribe → loop_events). */
|
|
1615
|
+
async sendLoopSubscribe(loopID, verbosity, streamDelivery) {
|
|
1616
|
+
await this.subscribe("loop_events", {
|
|
464
1617
|
loop_id: loopID,
|
|
465
|
-
|
|
1618
|
+
verbosity,
|
|
1619
|
+
stream_delivery: streamDelivery
|
|
466
1620
|
});
|
|
467
1621
|
}
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
return this.sendMessage(
|
|
1622
|
+
/** Detaches from a loop (unsubscribe by subscription id). */
|
|
1623
|
+
sendLoopDetach(loopID) {
|
|
1624
|
+
return this.sendMessage(unsubscribeEnvelope(loopID));
|
|
471
1625
|
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
return this.sendMessage(
|
|
1626
|
+
/** Notifies the daemon that this client is leaving (disconnect notification). */
|
|
1627
|
+
sendDetach() {
|
|
1628
|
+
return this.sendMessage(disconnectEnvelope());
|
|
475
1629
|
}
|
|
476
1630
|
/** Requests daemon status check. */
|
|
477
|
-
sendDaemonStatus(
|
|
478
|
-
return this.sendMessage({
|
|
479
|
-
type: "daemon_status",
|
|
480
|
-
request_id: requestID ?? newRequestID()
|
|
481
|
-
});
|
|
1631
|
+
sendDaemonStatus() {
|
|
1632
|
+
return this.sendMessage(requestEnvelope("daemon_status", {}));
|
|
482
1633
|
}
|
|
483
1634
|
/** Requests daemon shutdown. */
|
|
484
|
-
sendDaemonShutdown(
|
|
485
|
-
return this.sendMessage({
|
|
486
|
-
type: "daemon_shutdown",
|
|
487
|
-
request_id: requestID ?? newRequestID()
|
|
488
|
-
});
|
|
1635
|
+
sendDaemonShutdown() {
|
|
1636
|
+
return this.sendMessage(requestEnvelope("daemon_shutdown", {}));
|
|
489
1637
|
}
|
|
490
1638
|
/** Requests a config section from the daemon. */
|
|
491
|
-
sendConfigGet(section
|
|
492
|
-
return this.sendMessage({
|
|
493
|
-
type: "config_get",
|
|
494
|
-
section,
|
|
495
|
-
request_id: requestID ?? newRequestID()
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
// ---------------------------------------------------------------------------
|
|
499
|
-
// Loop management RPC methods (RFC-504)
|
|
500
|
-
// ---------------------------------------------------------------------------
|
|
501
|
-
/** Requests the persisted loop list. */
|
|
502
|
-
sendLoopList(filter, limit, requestID) {
|
|
503
|
-
const msg = {
|
|
504
|
-
type: "loop_list",
|
|
505
|
-
request_id: requestID ?? newRequestID()
|
|
506
|
-
};
|
|
507
|
-
if (filter) msg.filter = filter;
|
|
508
|
-
if (limit !== void 0) msg.limit = limit;
|
|
509
|
-
return this.sendMessage(msg);
|
|
510
|
-
}
|
|
511
|
-
/** Requests detailed loop metadata. */
|
|
512
|
-
sendLoopGet(loopID, verbose, requestID) {
|
|
513
|
-
const msg = {
|
|
514
|
-
type: "loop_get",
|
|
515
|
-
loop_id: loopID,
|
|
516
|
-
request_id: requestID ?? newRequestID()
|
|
517
|
-
};
|
|
518
|
-
if (verbose) msg.verbose = verbose;
|
|
519
|
-
return this.sendMessage(msg);
|
|
520
|
-
}
|
|
521
|
-
/** Requests loop tree visualization. */
|
|
522
|
-
sendLoopTree(loopID, format, requestID) {
|
|
523
|
-
const msg = {
|
|
524
|
-
type: "loop_tree",
|
|
525
|
-
loop_id: loopID,
|
|
526
|
-
request_id: requestID ?? newRequestID()
|
|
527
|
-
};
|
|
528
|
-
if (format) msg.format = format;
|
|
529
|
-
return this.sendMessage(msg);
|
|
530
|
-
}
|
|
531
|
-
/** Requests pruning of old failed branches. */
|
|
532
|
-
sendLoopPrune(loopID, retentionDays, dryRun, requestID) {
|
|
533
|
-
const msg = {
|
|
534
|
-
type: "loop_prune",
|
|
535
|
-
loop_id: loopID,
|
|
536
|
-
request_id: requestID ?? newRequestID()
|
|
537
|
-
};
|
|
538
|
-
if (retentionDays !== void 0) msg.retention_days = retentionDays;
|
|
539
|
-
if (dryRun !== void 0) msg.dry_run = dryRun;
|
|
540
|
-
return this.sendMessage(msg);
|
|
541
|
-
}
|
|
542
|
-
/** Requests loop deletion. */
|
|
543
|
-
sendLoopDelete(loopID, requestID) {
|
|
544
|
-
return this.sendMessage({
|
|
545
|
-
type: "loop_delete",
|
|
546
|
-
loop_id: loopID,
|
|
547
|
-
request_id: requestID ?? newRequestID()
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
/** Requests reattachment to a loop with history replay. */
|
|
551
|
-
sendLoopReattach(loopID, requestID) {
|
|
552
|
-
return this.sendMessage({
|
|
553
|
-
type: "loop_reattach",
|
|
554
|
-
loop_id: loopID,
|
|
555
|
-
request_id: requestID ?? newRequestID()
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
// ---------------------------------------------------------------------------
|
|
559
|
-
// Skills and models
|
|
560
|
-
// ---------------------------------------------------------------------------
|
|
561
|
-
/** Requests the skills catalog (RFC-400). */
|
|
562
|
-
sendSkillsList(requestID) {
|
|
563
|
-
return this.sendMessage({
|
|
564
|
-
type: "skills_list",
|
|
565
|
-
request_id: requestID ?? newRequestID()
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
/** Requests the models catalog (RFC-400). */
|
|
569
|
-
sendModelsList(requestID) {
|
|
570
|
-
return this.sendMessage({
|
|
571
|
-
type: "models_list",
|
|
572
|
-
request_id: requestID ?? newRequestID()
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
/** Invokes a skill on the daemon (RFC-400). */
|
|
576
|
-
sendInvokeSkill(skill, args, requestID) {
|
|
577
|
-
const msg = {
|
|
578
|
-
type: "invoke_skill",
|
|
579
|
-
skill,
|
|
580
|
-
request_id: requestID ?? newRequestID()
|
|
581
|
-
};
|
|
582
|
-
if (args) msg.args = args;
|
|
583
|
-
return this.sendMessage(msg);
|
|
584
|
-
}
|
|
585
|
-
// ---------------------------------------------------------------------------
|
|
586
|
-
// Request-Response pattern
|
|
587
|
-
// ---------------------------------------------------------------------------
|
|
588
|
-
/** Sends a request with a unique request_id and waits for a matching response. */
|
|
589
|
-
async requestResponse(payload, responseType, timeout) {
|
|
590
|
-
const rid = newRequestID();
|
|
591
|
-
payload.request_id = rid;
|
|
592
|
-
await this.sendMessage(payload);
|
|
593
|
-
const deadline = Date.now() + timeout;
|
|
594
|
-
while (Date.now() < deadline) {
|
|
595
|
-
const remaining = deadline - Date.now();
|
|
596
|
-
if (remaining <= 0) break;
|
|
597
|
-
const ev = await this.readEventWithTimeout(remaining);
|
|
598
|
-
if (ev === null) {
|
|
599
|
-
break;
|
|
600
|
-
}
|
|
601
|
-
const evRid = ev.request_id;
|
|
602
|
-
if (evRid !== rid) continue;
|
|
603
|
-
const typ = ev.type;
|
|
604
|
-
if (typ === "error") {
|
|
605
|
-
const msg = ev.message ?? "unknown error";
|
|
606
|
-
throw new Error(`daemon error: ${msg}`);
|
|
607
|
-
}
|
|
608
|
-
if (typ === responseType) {
|
|
609
|
-
return ev;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
throw new Error(`timeout after ${timeout}ms waiting for ${responseType}`);
|
|
1639
|
+
sendConfigGet(section) {
|
|
1640
|
+
return this.sendMessage(requestEnvelope("config_get", { section }));
|
|
613
1641
|
}
|
|
614
1642
|
// ---------------------------------------------------------------------------
|
|
615
|
-
// Convenience RPC methods
|
|
1643
|
+
// Convenience RPC methods (blocking request/response)
|
|
616
1644
|
// ---------------------------------------------------------------------------
|
|
617
1645
|
/** Requests the skills catalog and waits for the response. */
|
|
618
1646
|
listSkills(timeout) {
|
|
619
|
-
return this.requestResponse(
|
|
1647
|
+
return this.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
|
|
620
1648
|
}
|
|
621
1649
|
/** Requests the models catalog and waits for the response. */
|
|
622
1650
|
listModels(timeout) {
|
|
623
|
-
return this.requestResponse(
|
|
1651
|
+
return this.requestResponse("models_list", {}, "models_list", timeout ?? 15e3);
|
|
624
1652
|
}
|
|
625
|
-
/** Invokes a skill on the daemon host and receives echo
|
|
1653
|
+
/** Invokes a skill on the daemon host and receives echo. */
|
|
626
1654
|
invokeSkill(skill, args, timeout) {
|
|
627
|
-
|
|
1655
|
+
const params = { skill, args: args ?? "" };
|
|
1656
|
+
return this.requestResponse("invoke_skill", params, "invoke_skill", timeout ?? 12e4);
|
|
628
1657
|
}
|
|
629
1658
|
/** Requests loop list and waits for response. */
|
|
630
|
-
listLoops(timeout) {
|
|
631
|
-
|
|
1659
|
+
listLoops(timeout, workspace) {
|
|
1660
|
+
const params = {};
|
|
1661
|
+
if (workspace) params.filter = { workspace };
|
|
1662
|
+
return this.requestResponse("loop_list", params, "loop_list", timeout ?? 15e3);
|
|
632
1663
|
}
|
|
633
1664
|
/** Requests loop details and waits for response. */
|
|
634
1665
|
getLoop(loopID, timeout) {
|
|
635
|
-
return this.requestResponse(
|
|
1666
|
+
return this.requestResponse("loop_get", { loop_id: loopID }, "loop_get", timeout ?? 15e3);
|
|
636
1667
|
}
|
|
637
1668
|
/** Requests loop tree and waits for response. */
|
|
638
1669
|
getLoopTree(loopID, timeout) {
|
|
639
|
-
return this.requestResponse(
|
|
1670
|
+
return this.requestResponse("loop_tree", { loop_id: loopID }, "loop_tree", timeout ?? 15e3);
|
|
640
1671
|
}
|
|
641
1672
|
/** Requests loop deletion and waits for response. */
|
|
642
1673
|
deleteLoop(loopID, timeout) {
|
|
643
|
-
return this.requestResponse(
|
|
1674
|
+
return this.requestResponse(
|
|
1675
|
+
"loop_delete",
|
|
1676
|
+
{ loop_id: loopID },
|
|
1677
|
+
"loop_delete",
|
|
1678
|
+
timeout ?? 15e3
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
/** Requests persisted conversation/activity rows. */
|
|
1682
|
+
sendLoopMessages(loopID, limit, offset, includeEvents) {
|
|
1683
|
+
const params = { loop_id: loopID };
|
|
1684
|
+
if (limit !== void 0) params.limit = limit;
|
|
1685
|
+
if (offset !== void 0) params.offset = offset;
|
|
1686
|
+
if (includeEvents) params.include_events = true;
|
|
1687
|
+
return this.sendMessage(requestEnvelope("loop_messages", params));
|
|
1688
|
+
}
|
|
1689
|
+
/** Requests LangGraph checkpoint channel values. */
|
|
1690
|
+
sendLoopStateGet(loopID) {
|
|
1691
|
+
return this.sendMessage(requestEnvelope("loop_state_get", { loop_id: loopID }));
|
|
1692
|
+
}
|
|
1693
|
+
/** Applies partial checkpoint values. */
|
|
1694
|
+
sendLoopStateUpdate(loopID, values, asNode) {
|
|
1695
|
+
const params = { loop_id: loopID, values };
|
|
1696
|
+
if (asNode) params.as_node = asNode;
|
|
1697
|
+
return this.sendMessage(requestEnvelope("loop_state_update", params));
|
|
1698
|
+
}
|
|
1699
|
+
/** Requests display card ledger snapshot. */
|
|
1700
|
+
sendLoopCardsFetch(loopID) {
|
|
1701
|
+
return this.sendMessage(requestEnvelope("loop_cards_fetch", { loop_id: loopID }));
|
|
1702
|
+
}
|
|
1703
|
+
/** Requests the full loop history (RFC-631). */
|
|
1704
|
+
sendLoopHistoryFetch(loopID) {
|
|
1705
|
+
return this.sendMessage(requestEnvelope("loop_history_fetch", { loop_id: loopID }));
|
|
1706
|
+
}
|
|
1707
|
+
/** Requests MCP server status. */
|
|
1708
|
+
sendMCPStatus() {
|
|
1709
|
+
return this.sendMessage(requestEnvelope("mcp_status", {}));
|
|
1710
|
+
}
|
|
1711
|
+
/** Requests daemon config reload. */
|
|
1712
|
+
sendConfigReload() {
|
|
1713
|
+
return this.sendMessage(requestEnvelope("config_reload", {}));
|
|
1714
|
+
}
|
|
1715
|
+
/** Submits credentials for daemon-side authentication. */
|
|
1716
|
+
sendAuth(accessKey, secretKey) {
|
|
1717
|
+
return this.sendMessage(
|
|
1718
|
+
requestEnvelope("auth", { access_key: accessKey, secret_key: secretKey })
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
/** Refreshes the daemon-side auth token. */
|
|
1722
|
+
sendAuthRefresh(refreshToken) {
|
|
1723
|
+
return this.sendMessage(requestEnvelope("auth_refresh", { refresh_token: refreshToken }));
|
|
1724
|
+
}
|
|
1725
|
+
/** Requests persisted messages and waits for response. */
|
|
1726
|
+
getLoopMessages(loopID, limit, offset, includeEvents, timeout) {
|
|
1727
|
+
const params = { loop_id: loopID };
|
|
1728
|
+
if (limit !== void 0) params.limit = limit;
|
|
1729
|
+
if (offset !== void 0) params.offset = offset;
|
|
1730
|
+
if (includeEvents) params.include_events = true;
|
|
1731
|
+
return this.requestResponse("loop_messages", params, "loop_messages", timeout ?? 15e3);
|
|
1732
|
+
}
|
|
1733
|
+
/** Requests loop state and waits for response. */
|
|
1734
|
+
getLoopState(loopID, timeout) {
|
|
1735
|
+
return this.requestResponse(
|
|
1736
|
+
"loop_state_get",
|
|
1737
|
+
{ loop_id: loopID },
|
|
1738
|
+
"loop_state_get",
|
|
1739
|
+
timeout ?? 15e3
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
/** Updates loop state and waits for response. */
|
|
1743
|
+
updateLoopState(loopID, values, asNode, timeout) {
|
|
1744
|
+
const params = { loop_id: loopID, values };
|
|
1745
|
+
if (asNode) params.as_node = asNode;
|
|
1746
|
+
return this.requestResponse(
|
|
1747
|
+
"loop_state_update",
|
|
1748
|
+
params,
|
|
1749
|
+
"loop_state_update",
|
|
1750
|
+
timeout ?? 15e3
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
/** Requests display cards and waits for response. */
|
|
1754
|
+
fetchLoopCards(loopID, timeout) {
|
|
1755
|
+
return this.requestResponse(
|
|
1756
|
+
"loop_cards_fetch",
|
|
1757
|
+
{ loop_id: loopID },
|
|
1758
|
+
"loop_cards_fetch",
|
|
1759
|
+
timeout ?? 15e3
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
/** Requests MCP status and waits for response. */
|
|
1763
|
+
getMCPStatus(timeout) {
|
|
1764
|
+
return this.requestResponse("mcp_status", {}, "mcp_status", timeout ?? 15e3);
|
|
1765
|
+
}
|
|
1766
|
+
/** Requests loop history and waits for response. */
|
|
1767
|
+
fetchLoopHistory(loopID, timeout) {
|
|
1768
|
+
return this.requestResponse(
|
|
1769
|
+
"loop_history_fetch",
|
|
1770
|
+
{ loop_id: loopID },
|
|
1771
|
+
"loop_history_fetch",
|
|
1772
|
+
timeout ?? 15e3
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
/** Requests daemon config reload and waits for response. */
|
|
1776
|
+
reloadConfig(timeout) {
|
|
1777
|
+
return this.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
|
|
1778
|
+
}
|
|
1779
|
+
/** Submits credentials for daemon-side authentication and waits for response. */
|
|
1780
|
+
authenticate(accessKey, secretKey, timeout) {
|
|
1781
|
+
return this.requestResponse(
|
|
1782
|
+
"auth",
|
|
1783
|
+
{ access_key: accessKey, secret_key: secretKey },
|
|
1784
|
+
"auth",
|
|
1785
|
+
timeout ?? 15e3
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
/** Refreshes the daemon-side auth token and waits for response. */
|
|
1789
|
+
refreshAuthToken(refreshToken, timeout) {
|
|
1790
|
+
return this.requestResponse(
|
|
1791
|
+
"auth_refresh",
|
|
1792
|
+
{ refresh_token: refreshToken },
|
|
1793
|
+
"auth_refresh",
|
|
1794
|
+
timeout ?? 15e3
|
|
1795
|
+
);
|
|
644
1796
|
}
|
|
645
1797
|
// ---------------------------------------------------------------------------
|
|
646
|
-
//
|
|
1798
|
+
// RFC-228 Job IPC methods
|
|
1799
|
+
// ---------------------------------------------------------------------------
|
|
1800
|
+
/** Creates an autopilot job and waits for the response. */
|
|
1801
|
+
createJob(goal, verificationRules, workspace, timeout) {
|
|
1802
|
+
const params = { goal };
|
|
1803
|
+
if (verificationRules) params.verification_rules = verificationRules;
|
|
1804
|
+
if (workspace) params.workspace = workspace;
|
|
1805
|
+
return this.requestResponse("job_create", params, "job_create", timeout ?? 15e3);
|
|
1806
|
+
}
|
|
1807
|
+
/** Queries job status and waits for the response. */
|
|
1808
|
+
getJobStatus(jobId, timeout) {
|
|
1809
|
+
return this.requestResponse("job_status", { job_id: jobId }, "job_status", timeout ?? 15e3);
|
|
1810
|
+
}
|
|
1811
|
+
/** Pauses a running job. */
|
|
1812
|
+
pauseJob(jobId, timeout) {
|
|
1813
|
+
return this.requestResponse("job_pause", { job_id: jobId }, "job_pause", timeout ?? 15e3);
|
|
1814
|
+
}
|
|
1815
|
+
/** Resumes a paused job. */
|
|
1816
|
+
resumeJob(jobId, timeout) {
|
|
1817
|
+
return this.requestResponse("job_resume", { job_id: jobId }, "job_resume", timeout ?? 15e3);
|
|
1818
|
+
}
|
|
1819
|
+
/** Cancels a job. */
|
|
1820
|
+
cancelJob(jobId, timeout) {
|
|
1821
|
+
return this.requestResponse("job_cancel", { job_id: jobId }, "job_cancel", timeout ?? 15e3);
|
|
1822
|
+
}
|
|
1823
|
+
/** Requests the DAG visualization for a job. */
|
|
1824
|
+
getJobDag(jobId, timeout) {
|
|
1825
|
+
return this.requestResponse("job_dag", { job_id: jobId }, "job_dag", timeout ?? 15e3);
|
|
1826
|
+
}
|
|
1827
|
+
/** Sends guidance to a job or specific goal. */
|
|
1828
|
+
sendJobGuidance(jobId, text, goalId, timeout) {
|
|
1829
|
+
const params = { job_id: jobId, content: text };
|
|
1830
|
+
if (goalId) params.goal_id = goalId;
|
|
1831
|
+
return this.requestResponse("job_guidance", params, "job_guidance", timeout ?? 3e4);
|
|
1832
|
+
}
|
|
1833
|
+
/** Subscribes to autopilot worker events. */
|
|
1834
|
+
autopilotSubscribe(timeout) {
|
|
1835
|
+
return this.subscribe("autopilot_events", {}, timeout ?? 15e3);
|
|
1836
|
+
}
|
|
1837
|
+
/** Unsubscribes from autopilot worker events. */
|
|
1838
|
+
autopilotUnsubscribe(timeout) {
|
|
1839
|
+
const req = unsubscribeEnvelope(newRequestID());
|
|
1840
|
+
return this._requestResponseForEnvelope(req, "autopilot_unsubscribe", timeout ?? 15e3);
|
|
1841
|
+
}
|
|
647
1842
|
// ---------------------------------------------------------------------------
|
|
648
|
-
|
|
1843
|
+
// RFC-229 Cron IPC methods
|
|
1844
|
+
// ---------------------------------------------------------------------------
|
|
1845
|
+
/** Creates a scheduled job from natural language. */
|
|
1846
|
+
cronAdd(text, priority, timeout) {
|
|
1847
|
+
const params = { text };
|
|
1848
|
+
if (priority !== void 0) params.priority = priority;
|
|
1849
|
+
return this.requestResponse(
|
|
1850
|
+
"cron_add",
|
|
1851
|
+
params,
|
|
1852
|
+
"cron_add",
|
|
1853
|
+
timeout ?? 3e4
|
|
1854
|
+
// Longer timeout for NL extraction
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
/** Lists scheduled jobs. */
|
|
1858
|
+
cronList(status, timeout) {
|
|
1859
|
+
const params = {};
|
|
1860
|
+
if (status !== void 0) params.status = status;
|
|
1861
|
+
return this.requestResponse("cron_list", params, "cron_list", timeout ?? 15e3);
|
|
1862
|
+
}
|
|
1863
|
+
/** Shows a specific scheduled job. */
|
|
1864
|
+
cronShow(jobId, timeout) {
|
|
1865
|
+
return this.requestResponse("cron_show", { job_id: jobId }, "cron_show", timeout ?? 15e3);
|
|
1866
|
+
}
|
|
1867
|
+
/** Cancels a scheduled job. */
|
|
1868
|
+
cronCancel(jobId, timeout) {
|
|
1869
|
+
return this.requestResponse("cron_cancel", { job_id: jobId }, "cron_cancel", timeout ?? 15e3);
|
|
1870
|
+
}
|
|
1871
|
+
// ---------------------------------------------------------------------------
|
|
1872
|
+
// Wait helpers
|
|
1873
|
+
/**
|
|
1874
|
+
* Waits for the connection_ack to report readiness (already done in
|
|
1875
|
+
* connect(); kept for callers that reconnect manually). Resolves
|
|
1876
|
+
* immediately if the handshake is already complete.
|
|
1877
|
+
*/
|
|
649
1878
|
async waitForDaemonReady(timeout) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
while (Date.now() < deadline) {
|
|
653
|
-
const remaining = deadline - Date.now();
|
|
654
|
-
if (remaining <= 0) break;
|
|
655
|
-
const ev = await this.readEventWithTimeout(remaining);
|
|
656
|
-
if (ev === null) break;
|
|
657
|
-
if (ev.type !== "daemon_ready") continue;
|
|
658
|
-
if (ev.state === "ready") return ev;
|
|
659
|
-
const msg = ev.message ?? `daemon state is ${ev.state}`;
|
|
660
|
-
throw new Error(`daemon not ready: ${msg}`);
|
|
1879
|
+
if (this.handshakeComplete) {
|
|
1880
|
+
return { readiness_state: this.readinessState ?? "ready" };
|
|
661
1881
|
}
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
/** Waits for subscription confirmation matching loop id. */
|
|
665
|
-
async waitForSubscriptionConfirmed(loopID, _verbosity, timeout) {
|
|
666
|
-
const t = timeout ?? 5e3;
|
|
1882
|
+
const t = timeout ?? 1e4;
|
|
667
1883
|
const deadline = Date.now() + t;
|
|
668
1884
|
while (Date.now() < deadline) {
|
|
669
1885
|
const remaining = deadline - Date.now();
|
|
670
1886
|
if (remaining <= 0) break;
|
|
671
1887
|
const ev = await this.readEventWithTimeout(remaining);
|
|
672
1888
|
if (ev === null) break;
|
|
673
|
-
if (ev.type
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const lid = String(ev.loop_id ?? "");
|
|
679
|
-
if (lid === loopID) return;
|
|
1889
|
+
if (ev.type !== "connection_ack") continue;
|
|
1890
|
+
const result = ev.result ?? {};
|
|
1891
|
+
const state = result.readiness_state;
|
|
1892
|
+
if (state === "ready") return ev;
|
|
1893
|
+
throw new Error(`daemon not ready: state=${state ?? "unknown"}`);
|
|
680
1894
|
}
|
|
681
|
-
throw new Error(`timeout after ${t}ms waiting for
|
|
1895
|
+
throw new Error(`timeout after ${t}ms waiting for connection_ack`);
|
|
682
1896
|
}
|
|
683
1897
|
};
|
|
684
1898
|
}
|
|
@@ -687,14 +1901,33 @@ var init_client = __esm({
|
|
|
687
1901
|
// src/index.ts
|
|
688
1902
|
var index_exports = {};
|
|
689
1903
|
__export(index_exports, {
|
|
1904
|
+
CLIENT_VERSION: () => CLIENT_VERSION,
|
|
1905
|
+
ChatEventTerminal: () => ChatEventTerminal,
|
|
690
1906
|
Client: () => Client,
|
|
1907
|
+
CommandClient: () => CommandClient,
|
|
691
1908
|
ConnectionError: () => ConnectionError,
|
|
1909
|
+
ConnectionPool: () => ConnectionPool,
|
|
1910
|
+
DEFAULT_CLIENT_CAPABILITIES: () => DEFAULT_CLIENT_CAPABILITIES,
|
|
1911
|
+
DEFAULT_DELIVERABLE_PHASES: () => DEFAULT_DELIVERABLE_PHASES,
|
|
1912
|
+
DEFAULT_POST_IDLE_DRAIN_MS: () => DEFAULT_POST_IDLE_DRAIN_MS,
|
|
1913
|
+
DEFAULT_THINKING_STEP_EVENTS: () => DEFAULT_THINKING_STEP_EVENTS,
|
|
692
1914
|
DaemonError: () => DaemonError,
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1915
|
+
DaemonSession: () => DaemonSession,
|
|
1916
|
+
DisconnectCause: () => DisconnectCause,
|
|
1917
|
+
ErrIdleTimeout: () => ErrIdleTimeout,
|
|
1918
|
+
ErrPoolExhausted: () => ErrPoolExhausted,
|
|
1919
|
+
ErrQueryBusy: () => ErrQueryBusy,
|
|
1920
|
+
ErrQueryTimeout: () => ErrQueryTimeout,
|
|
1921
|
+
EventAutopilotGoalCompleted: () => EventAutopilotGoalCompleted,
|
|
1922
|
+
EventAutopilotGoalCreated: () => EventAutopilotGoalCreated,
|
|
1923
|
+
EventAutopilotGoalProgress: () => EventAutopilotGoalProgress,
|
|
1924
|
+
EventAutopilotGoalStatus: () => EventAutopilotGoalStatus,
|
|
1925
|
+
EventAutopilotWorkerAssigned: () => EventAutopilotWorkerAssigned,
|
|
1926
|
+
EventAutopilotWorkerUnassigned: () => EventAutopilotWorkerUnassigned,
|
|
1927
|
+
EventCardCreated: () => EventCardCreated,
|
|
1928
|
+
EventCardReplayBegin: () => EventCardReplayBegin,
|
|
1929
|
+
EventCardReplayEnd: () => EventCardReplayEnd,
|
|
1930
|
+
EventClassifier: () => EventClassifier,
|
|
698
1931
|
EventExploreCompleted: () => EventExploreCompleted,
|
|
699
1932
|
EventExploreMilestone: () => EventExploreMilestone,
|
|
700
1933
|
EventExploreStarted: () => EventExploreStarted,
|
|
@@ -706,6 +1939,14 @@ __export(index_exports, {
|
|
|
706
1939
|
EventMessageSent: () => EventMessageSent,
|
|
707
1940
|
EventPlanCreated: () => EventPlanCreated,
|
|
708
1941
|
EventReplayComplete: () => EventReplayComplete,
|
|
1942
|
+
EventStrangeLoopCompleted: () => EventStrangeLoopCompleted,
|
|
1943
|
+
EventStrangeLoopContextCompacted: () => EventStrangeLoopContextCompacted,
|
|
1944
|
+
EventStrangeLoopPlanDecision: () => EventStrangeLoopPlanDecision,
|
|
1945
|
+
EventStrangeLoopReasoned: () => EventStrangeLoopReasoned,
|
|
1946
|
+
EventStrangeLoopStarted: () => EventStrangeLoopStarted,
|
|
1947
|
+
EventStrangeLoopStepCompleted: () => EventStrangeLoopStepCompleted,
|
|
1948
|
+
EventStrangeLoopStepQueued: () => EventStrangeLoopStepQueued,
|
|
1949
|
+
EventStrangeLoopStepStarted: () => EventStrangeLoopStepStarted,
|
|
709
1950
|
EventStreamToolCallUpdate: () => EventStreamToolCallUpdate,
|
|
710
1951
|
EventTacitusCompleted: () => EventTacitusCompleted,
|
|
711
1952
|
EventTacitusGatherSummary: () => EventTacitusGatherSummary,
|
|
@@ -714,220 +1955,247 @@ __export(index_exports, {
|
|
|
714
1955
|
EventToolCompleted: () => EventToolCompleted,
|
|
715
1956
|
EventToolError: () => EventToolError,
|
|
716
1957
|
EventToolStarted: () => EventToolStarted,
|
|
1958
|
+
INTENT_HINT_EMBED: () => INTENT_HINT_EMBED,
|
|
1959
|
+
INTENT_HINT_IMAGE_TO_TEXT: () => INTENT_HINT_IMAGE_TO_TEXT,
|
|
1960
|
+
INTENT_HINT_OCR: () => INTENT_HINT_OCR,
|
|
1961
|
+
INTENT_HINT_TEXT_COMPLETION: () => INTENT_HINT_TEXT_COMPLETION,
|
|
1962
|
+
LOOP_ASSISTANT_OUTPUT_PHASES: () => LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
1963
|
+
PROTO_VERSION: () => PROTO_VERSION,
|
|
1964
|
+
PooledConn: () => PooledConn,
|
|
1965
|
+
QueryGate: () => QueryGate,
|
|
1966
|
+
REMOVED_INTENT_HINTS: () => REMOVED_INTENT_HINTS,
|
|
1967
|
+
ReconnectError: () => ReconnectError,
|
|
1968
|
+
SSEBroadcaster: () => SSEBroadcaster,
|
|
1969
|
+
STREAM_END: () => STREAM_END,
|
|
1970
|
+
StaleLoopError: () => StaleLoopError,
|
|
1971
|
+
StreamCloseFail: () => StreamCloseFail,
|
|
1972
|
+
StreamCloseSoftComplete: () => StreamCloseSoftComplete,
|
|
717
1973
|
TimeoutError: () => TimeoutError,
|
|
1974
|
+
TimeoutPolicy: () => TimeoutPolicy,
|
|
1975
|
+
TurnEventStats: () => TurnEventStats,
|
|
1976
|
+
TurnRunner: () => TurnRunner,
|
|
718
1977
|
VerbosityTier: () => VerbosityTier,
|
|
1978
|
+
authenticate: () => authenticate,
|
|
719
1979
|
bootstrapLoopSession: () => bootstrapLoopSession,
|
|
720
1980
|
checkDaemonStatus: () => checkDaemonStatus,
|
|
721
1981
|
classifyEventVerbosity: () => classifyEventVerbosity,
|
|
1982
|
+
compactAttachments: () => compactAttachments,
|
|
1983
|
+
compactImageAttachment: () => compactImageAttachment,
|
|
722
1984
|
connectWithRetries: () => connectWithRetries,
|
|
1985
|
+
connectedWebsocket: () => connectedWebsocket,
|
|
1986
|
+
connectionInitEnvelope: () => connectionInitEnvelope,
|
|
723
1987
|
decodeMessage: () => decodeMessage,
|
|
724
1988
|
defaultConfig: () => defaultConfig,
|
|
1989
|
+
defaultPoolConfig: () => defaultPoolConfig,
|
|
1990
|
+
disconnectCauseName: () => disconnectCauseName,
|
|
1991
|
+
disconnectEnvelope: () => disconnectEnvelope,
|
|
725
1992
|
encodeMessage: () => encodeMessage,
|
|
726
1993
|
extractSootheLoopID: () => extractSootheLoopID,
|
|
1994
|
+
extractThinkingStep: () => extractThinkingStep,
|
|
727
1995
|
fetchConfigSection: () => fetchConfigSection,
|
|
1996
|
+
fetchLoopCards: () => fetchLoopCards,
|
|
1997
|
+
fetchLoopHistory: () => fetchLoopHistory,
|
|
1998
|
+
fetchLoopMessages: () => fetchLoopMessages,
|
|
728
1999
|
fetchSkillsCatalog: () => fetchSkillsCatalog,
|
|
2000
|
+
idleTimeoutForTurn: () => idleTimeoutForTurn,
|
|
2001
|
+
inboundNeedsDeliveryAck: () => inboundNeedsDeliveryAck,
|
|
2002
|
+
inputMessageForLoop: () => inputMessageForLoop,
|
|
729
2003
|
isCompletionEvent: () => isCompletionEvent,
|
|
730
2004
|
isDaemonLive: () => isDaemonLive,
|
|
731
2005
|
isSubagentProgressEvent: () => isSubagentProgressEvent,
|
|
2006
|
+
isTurnEndCustomData: () => isTurnEndCustomData,
|
|
2007
|
+
isTurnProgressChunk: () => isTurnProgressChunk,
|
|
732
2008
|
isValidVerbosityLevel: () => isValidVerbosityLevel,
|
|
733
2009
|
loadConfigFromEnv: () => loadConfigFromEnv,
|
|
734
2010
|
newLoopInputMessage: () => newLoopInputMessage,
|
|
735
2011
|
newLoopNewMessage: () => newLoopNewMessage,
|
|
736
2012
|
newLoopSubscribeMessage: () => newLoopSubscribeMessage,
|
|
737
2013
|
newRequestID: () => newRequestID,
|
|
2014
|
+
notificationEnvelope: () => notificationEnvelope,
|
|
738
2015
|
parseNamespace: () => parseNamespace,
|
|
2016
|
+
pingEnvelope: () => pingEnvelope,
|
|
2017
|
+
pongEnvelope: () => pongEnvelope,
|
|
2018
|
+
protocol1Rpc: () => protocol1Rpc,
|
|
2019
|
+
refreshAuthToken: () => refreshAuthToken,
|
|
2020
|
+
requestDaemonConfigReload: () => requestDaemonConfigReload,
|
|
739
2021
|
requestDaemonShutdown: () => requestDaemonShutdown,
|
|
2022
|
+
requestEnvelope: () => requestEnvelope,
|
|
740
2023
|
shouldShow: () => shouldShow,
|
|
741
2024
|
splitWirePayload: () => splitWirePayload,
|
|
2025
|
+
subscribeEnvelope: () => subscribeEnvelope,
|
|
2026
|
+
unsubscribeEnvelope: () => unsubscribeEnvelope,
|
|
2027
|
+
validateLoopInputIntentHint: () => validateLoopInputIntentHint,
|
|
742
2028
|
waitDaemonReady: () => waitDaemonReady,
|
|
743
2029
|
waitLoopStatusWithID: () => waitLoopStatusWithID,
|
|
744
2030
|
waitSubscriptionConfirmed: () => waitSubscriptionConfirmed
|
|
745
2031
|
});
|
|
746
2032
|
module.exports = __toCommonJS(index_exports);
|
|
2033
|
+
init_errors();
|
|
2034
|
+
init_verbosity();
|
|
2035
|
+
init_config();
|
|
2036
|
+
init_protocol();
|
|
2037
|
+
init_intent_hints();
|
|
2038
|
+
init_events();
|
|
2039
|
+
init_client();
|
|
747
2040
|
|
|
748
|
-
// src/
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
attempt;
|
|
752
|
-
cause;
|
|
753
|
-
constructor(url, attempt, cause) {
|
|
754
|
-
super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
|
|
755
|
-
this.name = "ConnectionError";
|
|
756
|
-
this.url = url;
|
|
757
|
-
this.attempt = attempt;
|
|
758
|
-
this.cause = cause;
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
var DaemonError = class extends Error {
|
|
762
|
-
code;
|
|
763
|
-
/** The daemon's error message text. */
|
|
764
|
-
daemonMessage;
|
|
765
|
-
constructor(code, message) {
|
|
766
|
-
super(`daemon error [${code}]: ${message}`);
|
|
767
|
-
this.name = "DaemonError";
|
|
768
|
-
this.code = code;
|
|
769
|
-
this.daemonMessage = message;
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
var TimeoutError = class extends Error {
|
|
773
|
-
operation;
|
|
774
|
-
duration;
|
|
775
|
-
constructor(operation, duration) {
|
|
776
|
-
super(`timeout after ${duration} waiting for ${operation}`);
|
|
777
|
-
this.name = "TimeoutError";
|
|
778
|
-
this.operation = operation;
|
|
779
|
-
this.duration = duration;
|
|
780
|
-
}
|
|
781
|
-
};
|
|
2041
|
+
// src/command_client.ts
|
|
2042
|
+
init_client();
|
|
2043
|
+
init_config();
|
|
782
2044
|
|
|
783
|
-
// src/
|
|
784
|
-
var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
|
|
785
|
-
VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
|
|
786
|
-
VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
|
|
787
|
-
VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
|
|
788
|
-
VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
|
|
789
|
-
VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
|
|
790
|
-
return VerbosityTier2;
|
|
791
|
-
})(VerbosityTier || {});
|
|
792
|
-
var verbosityLevelValues = {
|
|
793
|
-
quiet: 0,
|
|
794
|
-
normal: 1,
|
|
795
|
-
debug: 3
|
|
796
|
-
};
|
|
797
|
-
function shouldShow(tier, verbosity) {
|
|
798
|
-
if (tier === 99 /* Internal */) {
|
|
799
|
-
return false;
|
|
800
|
-
}
|
|
801
|
-
const level = verbosityLevelValues[verbosity] ?? 1;
|
|
802
|
-
return tier <= level;
|
|
803
|
-
}
|
|
804
|
-
function isValidVerbosityLevel(s) {
|
|
805
|
-
return s in verbosityLevelValues;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// src/index.ts
|
|
2045
|
+
// src/session.ts
|
|
809
2046
|
init_config();
|
|
810
2047
|
init_protocol();
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
var EventToolCallUpdatesBatch = "tool_call_updates_batch";
|
|
828
|
-
var EventAgentLoopStarted = "soothe.cognition.agent_loop.started";
|
|
829
|
-
var EventAgentLoopIterated = "soothe.cognition.agent_loop.iterated";
|
|
830
|
-
var EventAgentLoopCompleted = "soothe.cognition.agent_loop.completed";
|
|
831
|
-
var EventAgentLoopReasoned = "soothe.cognition.agent_loop.reasoned";
|
|
832
|
-
var EventMessageReceived = "soothe.protocol.message.received";
|
|
833
|
-
var EventMessageSent = "soothe.protocol.message.sent";
|
|
834
|
-
var EventFinalReport = "soothe.output.autonomous.final_report.reported";
|
|
835
|
-
var EventGeneralFailed = "soothe.error.general.failed";
|
|
836
|
-
function parseNamespace(ns) {
|
|
837
|
-
const parts = splitNamespace(ns);
|
|
838
|
-
if (parts.length < 4 || parts[0] !== "soothe") {
|
|
839
|
-
return null;
|
|
840
|
-
}
|
|
841
|
-
if (parts[1] === "internal") {
|
|
842
|
-
return null;
|
|
2048
|
+
init_errors();
|
|
2049
|
+
async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
|
|
2050
|
+
const cfg = config ?? defaultConfig();
|
|
2051
|
+
let loopId = (resumeLoopId ?? "").trim();
|
|
2052
|
+
if (!loopId) {
|
|
2053
|
+
const env = newLoopNewMessage(loopNew);
|
|
2054
|
+
const newResp = await client.requestResponse(
|
|
2055
|
+
env.method,
|
|
2056
|
+
env.params ?? {},
|
|
2057
|
+
"loop_new",
|
|
2058
|
+
cfg.loopStatusTimeout
|
|
2059
|
+
);
|
|
2060
|
+
loopId = String(newResp.loop_id ?? "").trim();
|
|
2061
|
+
if (!loopId) {
|
|
2062
|
+
throw new Error("loop_new response missing loop_id");
|
|
2063
|
+
}
|
|
843
2064
|
}
|
|
844
|
-
|
|
2065
|
+
await client.subscribe(
|
|
2066
|
+
"loop_events",
|
|
2067
|
+
{ loop_id: loopId, verbosity: cfg.verbosityLevel },
|
|
2068
|
+
cfg.subscriptionTimeout
|
|
2069
|
+
);
|
|
2070
|
+
return loopId;
|
|
845
2071
|
}
|
|
846
|
-
function
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
2072
|
+
async function waitDaemonReady(client, timeout) {
|
|
2073
|
+
if (client.isConnected()) return;
|
|
2074
|
+
const deadline = Date.now() + timeout;
|
|
2075
|
+
while (Date.now() < deadline) {
|
|
2076
|
+
const remaining = deadline - Date.now();
|
|
2077
|
+
if (remaining <= 0) break;
|
|
2078
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
2079
|
+
if (ev === null) break;
|
|
2080
|
+
if (ev.type === "connection_ack") {
|
|
2081
|
+
const result = ev.result ?? {};
|
|
2082
|
+
const state = result.readiness_state;
|
|
2083
|
+
if (state === "ready") return;
|
|
2084
|
+
throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
|
|
853
2085
|
}
|
|
854
2086
|
}
|
|
855
|
-
|
|
856
|
-
return parts;
|
|
2087
|
+
throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
|
|
857
2088
|
}
|
|
858
|
-
function
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
2089
|
+
async function waitLoopStatusWithID(client, timeout) {
|
|
2090
|
+
const deadline = Date.now() + timeout;
|
|
2091
|
+
while (Date.now() < deadline) {
|
|
2092
|
+
const remaining = deadline - Date.now();
|
|
2093
|
+
if (remaining <= 0) break;
|
|
2094
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
2095
|
+
if (ev === null) break;
|
|
2096
|
+
if (ev.type === "error") {
|
|
2097
|
+
const errObj = ev.error ?? {};
|
|
2098
|
+
throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
|
|
2099
|
+
}
|
|
2100
|
+
if (ev.type === "status") {
|
|
2101
|
+
const lid = ev.loop_id;
|
|
2102
|
+
if (lid && lid !== "") {
|
|
2103
|
+
return ev;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
862
2106
|
}
|
|
863
|
-
|
|
2107
|
+
throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
|
|
864
2108
|
}
|
|
865
|
-
function
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
2109
|
+
async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
|
|
2110
|
+
const deadline = Date.now() + timeout;
|
|
2111
|
+
while (Date.now() < deadline) {
|
|
2112
|
+
const remaining = deadline - Date.now();
|
|
2113
|
+
if (remaining <= 0) break;
|
|
2114
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
2115
|
+
if (ev === null) break;
|
|
2116
|
+
if (ev.type === "next") {
|
|
2117
|
+
const payload = ev.payload ?? {};
|
|
2118
|
+
const lid = String(payload.loop_id ?? "");
|
|
2119
|
+
if (lid === wantLoopID && payload.success === true) return;
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
if (ev.type === "error") {
|
|
2123
|
+
const errObj = ev.error ?? {};
|
|
2124
|
+
throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
|
|
2125
|
+
}
|
|
880
2126
|
}
|
|
2127
|
+
throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
|
|
881
2128
|
}
|
|
882
|
-
function
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
2129
|
+
async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
2130
|
+
const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
|
|
2131
|
+
const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
|
|
2132
|
+
let lastErr = null;
|
|
2133
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
2134
|
+
try {
|
|
2135
|
+
await client.connect();
|
|
2136
|
+
return;
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
lastErr = err;
|
|
2139
|
+
}
|
|
2140
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
891
2141
|
}
|
|
2142
|
+
throw new Error(
|
|
2143
|
+
`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
|
|
2144
|
+
);
|
|
892
2145
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
2146
|
+
|
|
2147
|
+
// src/command_client.ts
|
|
2148
|
+
var CommandClient = class {
|
|
2149
|
+
url;
|
|
2150
|
+
timeoutMs;
|
|
2151
|
+
config;
|
|
2152
|
+
constructor(url, opts) {
|
|
2153
|
+
this.url = url;
|
|
2154
|
+
this.timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e4;
|
|
2155
|
+
this.config = opts?.config ?? defaultConfig();
|
|
896
2156
|
}
|
|
897
|
-
|
|
898
|
-
|
|
2157
|
+
async withClient(fn) {
|
|
2158
|
+
const client = new Client(this.url, this.config);
|
|
2159
|
+
try {
|
|
2160
|
+
await connectWithRetries(client, 5, 250);
|
|
2161
|
+
return await fn(client);
|
|
2162
|
+
} finally {
|
|
2163
|
+
client.close();
|
|
2164
|
+
}
|
|
899
2165
|
}
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
function isSubagentProgressEvent(eventType) {
|
|
906
|
-
const parsed = parseNamespace(eventType);
|
|
907
|
-
if (!parsed || parsed.domain !== "subagent") {
|
|
908
|
-
return false;
|
|
2166
|
+
/** Generic one-shot RPC. */
|
|
2167
|
+
async request(method, params = {}) {
|
|
2168
|
+
return this.withClient(
|
|
2169
|
+
(client) => client.requestResponse(method, params, void 0, this.timeoutMs)
|
|
2170
|
+
);
|
|
909
2171
|
}
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
2172
|
+
async jobCreate(goal, workspace = "") {
|
|
2173
|
+
const params = { goal };
|
|
2174
|
+
if (workspace) params.workspace = workspace;
|
|
2175
|
+
return this.request("job_create", params);
|
|
2176
|
+
}
|
|
2177
|
+
async jobStatus(jobId) {
|
|
2178
|
+
return this.request("job_status", { job_id: jobId });
|
|
2179
|
+
}
|
|
2180
|
+
async jobCancel(jobId) {
|
|
2181
|
+
return this.request("job_cancel", { job_id: jobId });
|
|
2182
|
+
}
|
|
2183
|
+
async cronAdd(text, priority = 0) {
|
|
2184
|
+
const params = { text };
|
|
2185
|
+
if (priority > 0) params.priority = priority;
|
|
2186
|
+
return this.request("cron_add", params);
|
|
2187
|
+
}
|
|
2188
|
+
async cronList(status = "") {
|
|
2189
|
+
const params = {};
|
|
2190
|
+
if (status) params.status = status;
|
|
2191
|
+
return this.request("cron_list", params);
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
926
2194
|
|
|
927
2195
|
// src/helpers.ts
|
|
928
2196
|
init_config();
|
|
929
2197
|
async function checkDaemonStatus(client, timeout) {
|
|
930
|
-
return client.requestResponse(
|
|
2198
|
+
return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
|
|
931
2199
|
}
|
|
932
2200
|
async function isDaemonLive(wsURL, timeout) {
|
|
933
2201
|
const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
@@ -948,134 +2216,1906 @@ async function isDaemonLive(wsURL, timeout) {
|
|
|
948
2216
|
}
|
|
949
2217
|
}
|
|
950
2218
|
async function requestDaemonShutdown(client, timeout) {
|
|
951
|
-
const resp = await client.requestResponse(
|
|
2219
|
+
const resp = await client.requestResponse(
|
|
2220
|
+
"daemon_shutdown",
|
|
2221
|
+
{},
|
|
2222
|
+
"daemon_shutdown",
|
|
2223
|
+
timeout ?? 1e4
|
|
2224
|
+
);
|
|
952
2225
|
if (resp.status !== "acknowledged") {
|
|
953
2226
|
throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
|
|
954
2227
|
}
|
|
955
2228
|
}
|
|
956
2229
|
async function fetchSkillsCatalog(client, timeout) {
|
|
957
|
-
const resp = await client.requestResponse(
|
|
2230
|
+
const resp = await client.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
|
|
958
2231
|
const skillsRaw = resp.skills;
|
|
959
2232
|
if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
|
|
960
2233
|
return skillsRaw.filter((s) => typeof s === "object" && s !== null);
|
|
961
2234
|
}
|
|
962
2235
|
async function fetchConfigSection(client, section, timeout) {
|
|
963
|
-
const resp = await client.requestResponse(
|
|
2236
|
+
const resp = await client.requestResponse(
|
|
2237
|
+
"config_get",
|
|
2238
|
+
{ section },
|
|
2239
|
+
"config_get",
|
|
2240
|
+
timeout ?? 5e3
|
|
2241
|
+
);
|
|
964
2242
|
const sec = resp[section];
|
|
965
2243
|
if (sec && typeof sec === "object") {
|
|
966
2244
|
return sec;
|
|
967
2245
|
}
|
|
968
2246
|
return resp;
|
|
969
2247
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
if (!loopId) {
|
|
980
|
-
const newResp = await client.requestResponse(
|
|
981
|
-
newLoopNewMessage(loopNew),
|
|
982
|
-
"loop_new_response",
|
|
983
|
-
cfg.loopStatusTimeout
|
|
984
|
-
);
|
|
985
|
-
loopId = String(newResp.loop_id ?? "").trim();
|
|
986
|
-
if (!loopId) {
|
|
987
|
-
throw new Error("loop_new_response missing loop_id");
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
const subResp = await client.requestResponse(
|
|
991
|
-
{ type: "loop_subscribe", loop_id: loopId, verbosity: cfg.verbosityLevel },
|
|
992
|
-
"loop_subscribe_response",
|
|
993
|
-
cfg.subscriptionTimeout
|
|
2248
|
+
async function requestDaemonConfigReload(client, timeout) {
|
|
2249
|
+
return client.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
|
|
2250
|
+
}
|
|
2251
|
+
async function fetchLoopHistory(client, loopID, timeout) {
|
|
2252
|
+
return client.requestResponse(
|
|
2253
|
+
"loop_history_fetch",
|
|
2254
|
+
{ loop_id: loopID },
|
|
2255
|
+
"loop_history_fetch",
|
|
2256
|
+
timeout ?? 15e3
|
|
994
2257
|
);
|
|
995
|
-
if (subResp.success === false) {
|
|
996
|
-
throw new Error(String(subResp.message ?? "loop_subscribe failed"));
|
|
997
|
-
}
|
|
998
|
-
return loopId;
|
|
999
2258
|
}
|
|
1000
|
-
async function
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
2259
|
+
async function authenticate(client, accessKey, secretKey, timeout) {
|
|
2260
|
+
return client.requestResponse(
|
|
2261
|
+
"auth",
|
|
2262
|
+
{ access_key: accessKey, secret_key: secretKey },
|
|
2263
|
+
"auth",
|
|
2264
|
+
timeout ?? 15e3
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
async function refreshAuthToken(client, refreshToken, timeout) {
|
|
2268
|
+
return client.requestResponse(
|
|
2269
|
+
"auth_refresh",
|
|
2270
|
+
{ refresh_token: refreshToken },
|
|
2271
|
+
"auth_refresh",
|
|
2272
|
+
timeout ?? 15e3
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
async function fetchLoopCards(client, loopID, timeout) {
|
|
2276
|
+
return client.fetchLoopCards(loopID, timeout);
|
|
2277
|
+
}
|
|
2278
|
+
async function fetchLoopMessages(client, loopID, opts) {
|
|
2279
|
+
return client.getLoopMessages(
|
|
2280
|
+
loopID,
|
|
2281
|
+
opts?.limit,
|
|
2282
|
+
opts?.offset,
|
|
2283
|
+
opts?.includeEvents,
|
|
2284
|
+
opts?.timeout
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
async function connectedWebsocket(wsUrl, fn, timeoutMs = 3e4) {
|
|
2288
|
+
const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
2289
|
+
const client = new Client2(wsUrl, defaultConfig());
|
|
2290
|
+
const deadline = Date.now() + timeoutMs;
|
|
2291
|
+
try {
|
|
2292
|
+
await client.connect();
|
|
2293
|
+
while (!client.isConnected() && Date.now() < deadline) {
|
|
2294
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
1012
2295
|
}
|
|
2296
|
+
if (!client.isConnected()) {
|
|
2297
|
+
throw new Error("Timed out waiting for daemon handshake");
|
|
2298
|
+
}
|
|
2299
|
+
return await fn(client);
|
|
2300
|
+
} finally {
|
|
2301
|
+
client.close();
|
|
1013
2302
|
}
|
|
1014
|
-
throw new Error(`timeout after ${timeout}ms waiting for daemon_ready (state=ready)`);
|
|
1015
2303
|
}
|
|
1016
|
-
async function
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
2304
|
+
async function protocol1Rpc(wsUrl, method, params = null, opts = {}) {
|
|
2305
|
+
const mode = opts.mode ?? "request";
|
|
2306
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2307
|
+
try {
|
|
2308
|
+
return await connectedWebsocket(
|
|
2309
|
+
wsUrl,
|
|
2310
|
+
async (client) => {
|
|
2311
|
+
if (mode === "notify") {
|
|
2312
|
+
await client.notify(method, params ?? {});
|
|
2313
|
+
return {};
|
|
2314
|
+
}
|
|
2315
|
+
if (mode === "subscribe") {
|
|
2316
|
+
const subId = await client.subscribe(
|
|
2317
|
+
method,
|
|
2318
|
+
params ?? {},
|
|
2319
|
+
timeoutMs
|
|
2320
|
+
);
|
|
2321
|
+
return { subscription_id: subId };
|
|
2322
|
+
}
|
|
2323
|
+
const result = await client.requestResponse(
|
|
2324
|
+
method,
|
|
2325
|
+
params ?? {},
|
|
2326
|
+
method,
|
|
2327
|
+
timeoutMs
|
|
2328
|
+
);
|
|
2329
|
+
return result && typeof result === "object" ? result : { result };
|
|
2330
|
+
},
|
|
2331
|
+
timeoutMs
|
|
2332
|
+
);
|
|
2333
|
+
} catch (exc) {
|
|
2334
|
+
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
2335
|
+
if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
|
|
2336
|
+
return { error: "Timed out waiting for daemon response" };
|
|
1026
2337
|
}
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1029
|
-
const lid = status.loop_id;
|
|
1030
|
-
if (lid && lid !== "") {
|
|
1031
|
-
return status;
|
|
1032
|
-
}
|
|
2338
|
+
if (msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("dial")) {
|
|
2339
|
+
return { error: `Connection error: ${msg}` };
|
|
1033
2340
|
}
|
|
2341
|
+
return { error: msg };
|
|
1034
2342
|
}
|
|
1035
|
-
throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
|
|
1036
2343
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
2344
|
+
|
|
2345
|
+
// src/index.ts
|
|
2346
|
+
init_stream_terminal();
|
|
2347
|
+
|
|
2348
|
+
// src/appkit/broadcaster.ts
|
|
2349
|
+
var SUBSCRIBER_QUEUE_CAP = 100;
|
|
2350
|
+
var SSEBroadcaster = class {
|
|
2351
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
2352
|
+
nextSubID = 0;
|
|
2353
|
+
/** Creates an empty broadcaster. */
|
|
2354
|
+
constructor() {
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Registers a new subscriber channel for a session id. Returns an async
|
|
2358
|
+
* iterable the subscriber reads events from. Unsubscribe via
|
|
2359
|
+
* `unsubscribe()` or `close()`.
|
|
2360
|
+
*/
|
|
2361
|
+
subscribe(sessionID) {
|
|
2362
|
+
const subID = String(this.nextSubID++);
|
|
2363
|
+
const sub = { queue: [], waiters: [], closed: false };
|
|
2364
|
+
let subs = this.subscribers.get(sessionID);
|
|
2365
|
+
if (!subs) {
|
|
2366
|
+
subs = /* @__PURE__ */ new Map();
|
|
2367
|
+
this.subscribers.set(sessionID, subs);
|
|
1046
2368
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
2369
|
+
subs.set(subID, sub);
|
|
2370
|
+
const iterable = {
|
|
2371
|
+
[Symbol.asyncIterator]() {
|
|
2372
|
+
return {
|
|
2373
|
+
next() {
|
|
2374
|
+
if (sub.queue.length > 0) {
|
|
2375
|
+
return Promise.resolve({ value: sub.queue.shift(), done: false });
|
|
2376
|
+
}
|
|
2377
|
+
if (sub.closed) {
|
|
2378
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
2379
|
+
}
|
|
2380
|
+
return new Promise((resolve) => {
|
|
2381
|
+
sub.waiters.push((ev) => {
|
|
2382
|
+
if (ev === null) {
|
|
2383
|
+
resolve({ value: void 0, done: true });
|
|
2384
|
+
} else {
|
|
2385
|
+
resolve({ value: ev, done: false });
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
return { iterable, id: subID };
|
|
2394
|
+
}
|
|
2395
|
+
/** Removes a subscriber by id and closes its iterable. Safe if unknown. */
|
|
2396
|
+
unsubscribe(sessionID, subID) {
|
|
2397
|
+
const subs = this.subscribers.get(sessionID);
|
|
2398
|
+
if (!subs) return;
|
|
2399
|
+
const sub = subs.get(subID);
|
|
2400
|
+
if (!sub) return;
|
|
2401
|
+
sub.closed = true;
|
|
2402
|
+
for (const w of sub.waiters) w(null);
|
|
2403
|
+
sub.waiters = [];
|
|
2404
|
+
subs.delete(subID);
|
|
2405
|
+
if (subs.size === 0) this.subscribers.delete(sessionID);
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Sends an event to all subscribers for a session id. Non-blocking: a full
|
|
2409
|
+
* subscriber queue is skipped (drop-on-full) so one slow consumer cannot
|
|
2410
|
+
* block the others.
|
|
2411
|
+
*/
|
|
2412
|
+
broadcast(sessionID, event) {
|
|
2413
|
+
const subs = this.subscribers.get(sessionID);
|
|
2414
|
+
if (!subs) return;
|
|
2415
|
+
for (const sub of subs.values()) {
|
|
2416
|
+
if (sub.closed) continue;
|
|
2417
|
+
if (sub.waiters.length > 0) {
|
|
2418
|
+
const w = sub.waiters.shift();
|
|
2419
|
+
w(event);
|
|
2420
|
+
} else if (sub.queue.length < SUBSCRIBER_QUEUE_CAP) {
|
|
2421
|
+
sub.queue.push(event);
|
|
2422
|
+
}
|
|
1050
2423
|
}
|
|
1051
2424
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
await client.connect();
|
|
1061
|
-
return;
|
|
1062
|
-
} catch (err) {
|
|
1063
|
-
lastErr = err;
|
|
2425
|
+
/** Closes all subscribers for a session id and removes the entry. */
|
|
2426
|
+
close(sessionID) {
|
|
2427
|
+
const subs = this.subscribers.get(sessionID);
|
|
2428
|
+
if (!subs) return;
|
|
2429
|
+
for (const sub of subs.values()) {
|
|
2430
|
+
sub.closed = true;
|
|
2431
|
+
for (const w of sub.waiters) w(null);
|
|
2432
|
+
sub.waiters = [];
|
|
1064
2433
|
}
|
|
1065
|
-
|
|
2434
|
+
this.subscribers.delete(sessionID);
|
|
1066
2435
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
2436
|
+
/** Closes every subscriber channel across all sessions. */
|
|
2437
|
+
closeAll() {
|
|
2438
|
+
for (const [sessionID, subs] of this.subscribers) {
|
|
2439
|
+
for (const sub of subs.values()) {
|
|
2440
|
+
sub.closed = true;
|
|
2441
|
+
for (const w of sub.waiters) w(null);
|
|
2442
|
+
sub.waiters = [];
|
|
2443
|
+
}
|
|
2444
|
+
this.subscribers.delete(sessionID);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
// src/appkit/classifier.ts
|
|
2450
|
+
init_errors();
|
|
2451
|
+
init_events();
|
|
2452
|
+
|
|
2453
|
+
// src/appkit/thinking_step.ts
|
|
2454
|
+
var MAX_THINKING_STEP_RUNES = 280;
|
|
2455
|
+
var DEFAULT_THINKING_STEP_EVENTS = /* @__PURE__ */ new Set([
|
|
2456
|
+
"soothe.cognition.plan.step.started",
|
|
2457
|
+
"soothe.cognition.plan.step.completed",
|
|
2458
|
+
"soothe.cognition.plan.step.failed",
|
|
2459
|
+
"soothe.lifecycle.iteration.started",
|
|
2460
|
+
"soothe.agent.loop.step.started",
|
|
2461
|
+
"soothe.agent.loop.started",
|
|
2462
|
+
"soothe.cognition.plan.batch.started",
|
|
2463
|
+
"soothe.cognition.plan.created",
|
|
2464
|
+
"soothe.cognition.goal.created",
|
|
2465
|
+
"soothe.tool.execution.started"
|
|
2466
|
+
]);
|
|
2467
|
+
function extractThinkingStep(eventType, data, allow) {
|
|
2468
|
+
if (!eventType || !data) return ["", false];
|
|
2469
|
+
const et = eventType.trim();
|
|
2470
|
+
if (!et) return ["", false];
|
|
2471
|
+
const allowlist = allow ?? DEFAULT_THINKING_STEP_EVENTS;
|
|
2472
|
+
if (!allowlist.has(et)) return ["", false];
|
|
2473
|
+
let line = "";
|
|
2474
|
+
switch (et) {
|
|
2475
|
+
case "soothe.cognition.plan.step.started":
|
|
2476
|
+
line = formatPlanStepLine(data, "");
|
|
2477
|
+
break;
|
|
2478
|
+
case "soothe.cognition.plan.step.completed":
|
|
2479
|
+
line = formatPlanStepLine(data, "done");
|
|
2480
|
+
break;
|
|
2481
|
+
case "soothe.cognition.plan.step.failed": {
|
|
2482
|
+
const stepID = strField(data, "step_id");
|
|
2483
|
+
const errMsg = strField(data, "error");
|
|
2484
|
+
if (stepID && errMsg) line = `Step ${stepID} failed: ${errMsg}`;
|
|
2485
|
+
else if (stepID) line = `Step ${stepID} failed`;
|
|
2486
|
+
else if (errMsg) line = `Step failed: ${errMsg}`;
|
|
2487
|
+
break;
|
|
2488
|
+
}
|
|
2489
|
+
case "soothe.agent.loop.step.started":
|
|
2490
|
+
line = formatAgentStepLine(data, "");
|
|
2491
|
+
break;
|
|
2492
|
+
case "soothe.cognition.plan.batch.started": {
|
|
2493
|
+
const n = data["parallel_count"];
|
|
2494
|
+
if (typeof n === "number" && n > 0) line = `Running ${Math.floor(n)} steps in parallel`;
|
|
2495
|
+
break;
|
|
2496
|
+
}
|
|
2497
|
+
case "soothe.cognition.plan.created":
|
|
2498
|
+
case "soothe.agent.loop.started": {
|
|
2499
|
+
const g = strField(data, "goal");
|
|
2500
|
+
if (g) line = "Goal: " + g;
|
|
2501
|
+
break;
|
|
2502
|
+
}
|
|
2503
|
+
case "soothe.cognition.goal.created": {
|
|
2504
|
+
const g = strField(data, "friendly_message", "description");
|
|
2505
|
+
if (g) line = "Goal: " + g;
|
|
2506
|
+
break;
|
|
2507
|
+
}
|
|
2508
|
+
case "soothe.lifecycle.iteration.started": {
|
|
2509
|
+
const g = strField(data, "goal_description");
|
|
2510
|
+
if (g) line = "Iteration: " + g;
|
|
2511
|
+
break;
|
|
2512
|
+
}
|
|
2513
|
+
case "soothe.tool.execution.started": {
|
|
2514
|
+
const name = strField(data, "tool_name", "name");
|
|
2515
|
+
if (name) line = "Tool: " + name;
|
|
2516
|
+
break;
|
|
2517
|
+
}
|
|
2518
|
+
default:
|
|
2519
|
+
return ["", false];
|
|
2520
|
+
}
|
|
2521
|
+
line = line.trim();
|
|
2522
|
+
if (!line) return ["", false];
|
|
2523
|
+
const runes = [...line];
|
|
2524
|
+
if (runes.length > MAX_THINKING_STEP_RUNES) {
|
|
2525
|
+
line = runes.slice(0, MAX_THINKING_STEP_RUNES).join("") + "\u2026";
|
|
2526
|
+
}
|
|
2527
|
+
return [line, true];
|
|
2528
|
+
}
|
|
2529
|
+
function formatPlanStepLine(data, suffix) {
|
|
2530
|
+
const stepID = strField(data, "step_id");
|
|
2531
|
+
const desc = strField(data, "description");
|
|
2532
|
+
if (stepID && suffix) return `Step ${stepID}: ${suffix}`;
|
|
2533
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
2534
|
+
if (stepID) return `Step ${stepID}`;
|
|
2535
|
+
if (desc && suffix) return `Step: ${suffix}`;
|
|
2536
|
+
if (desc) return `Step: ${desc}`;
|
|
2537
|
+
if (suffix) return "Step: " + suffix;
|
|
2538
|
+
return "";
|
|
2539
|
+
}
|
|
2540
|
+
function formatAgentStepLine(data, suffix) {
|
|
2541
|
+
const stepID = strField(data, "step_id");
|
|
2542
|
+
const desc = strField(data, "description");
|
|
2543
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
2544
|
+
if (desc) return suffix ? `Step: ${suffix}` : `Step: ${desc}`;
|
|
2545
|
+
if (stepID) return `Step ${stepID}`;
|
|
2546
|
+
return "";
|
|
2547
|
+
}
|
|
2548
|
+
function strField(data, ...keys) {
|
|
2549
|
+
for (const key of keys) {
|
|
2550
|
+
const v = data[key];
|
|
2551
|
+
if (typeof v === "string") {
|
|
2552
|
+
const s = v.trim();
|
|
2553
|
+
if (s) return s;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
return "";
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/appkit/classifier.ts
|
|
2560
|
+
var ChatEventTerminal = /* @__PURE__ */ ((ChatEventTerminal2) => {
|
|
2561
|
+
ChatEventTerminal2[ChatEventTerminal2["Continue"] = 0] = "Continue";
|
|
2562
|
+
ChatEventTerminal2[ChatEventTerminal2["DeliverableComplete"] = 1] = "DeliverableComplete";
|
|
2563
|
+
ChatEventTerminal2[ChatEventTerminal2["FailedComplete"] = 2] = "FailedComplete";
|
|
2564
|
+
return ChatEventTerminal2;
|
|
2565
|
+
})(ChatEventTerminal || {});
|
|
2566
|
+
var EVENT_LOOP_HISTORY_REPLAYED = "soothe.lifecycle.loop.history.replayed";
|
|
2567
|
+
var EventClassifier = class {
|
|
2568
|
+
deliverablePhases;
|
|
2569
|
+
minDeliverableRunes;
|
|
2570
|
+
thinkingStepEvents;
|
|
2571
|
+
treatStatusIdleAsComplete;
|
|
2572
|
+
constructor(cfg) {
|
|
2573
|
+
if (!cfg.deliverablePhases) {
|
|
2574
|
+
throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
|
|
2575
|
+
}
|
|
2576
|
+
this.deliverablePhases = cfg.deliverablePhases;
|
|
2577
|
+
this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
|
|
2578
|
+
this.thinkingStepEvents = cfg.thinkingStepEvents;
|
|
2579
|
+
this.treatStatusIdleAsComplete = Boolean(cfg.treatStatusIdleAsComplete);
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* Inspects one decoded event and returns its outcome. `accumulated` is the
|
|
2583
|
+
* running assistant text so far, used to pick the final reply when a
|
|
2584
|
+
* deliverable event arrives.
|
|
2585
|
+
*/
|
|
2586
|
+
classify(msg, accumulated) {
|
|
2587
|
+
return this.processChatEvent(msg, accumulated);
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Reports whether a persisted completion_event is user-facing. Uses the
|
|
2591
|
+
* configured deliverable phase set; recognizes the protocol output namespace
|
|
2592
|
+
* and final_report component as deliverable.
|
|
2593
|
+
*/
|
|
2594
|
+
isDeliverableCompletionEvent(eventType) {
|
|
2595
|
+
if (!eventType) return false;
|
|
2596
|
+
switch (eventType) {
|
|
2597
|
+
case "status.idle":
|
|
2598
|
+
case "idle_timeout":
|
|
2599
|
+
case "query_timeout":
|
|
2600
|
+
case "stream_closed":
|
|
2601
|
+
return true;
|
|
2602
|
+
}
|
|
2603
|
+
if (eventType === EventFinalReport) return true;
|
|
2604
|
+
if (eventType.startsWith("soothe.protocol.message.")) {
|
|
2605
|
+
const phase = eventType.slice("soothe.protocol.message.".length);
|
|
2606
|
+
return this.isDeliverableLoopPhase(phase);
|
|
2607
|
+
}
|
|
2608
|
+
return eventType.includes("soothe.output") && eventType.includes("responded");
|
|
2609
|
+
}
|
|
2610
|
+
isDeliverableLoopPhase(phase) {
|
|
2611
|
+
return this.deliverablePhases.has(phase);
|
|
2612
|
+
}
|
|
2613
|
+
deliverableResult(content, completionEvent) {
|
|
2614
|
+
return { content, terminal: 1 /* DeliverableComplete */, completionEvent };
|
|
2615
|
+
}
|
|
2616
|
+
continueResult(content) {
|
|
2617
|
+
return { content, terminal: 0 /* Continue */ };
|
|
2618
|
+
}
|
|
2619
|
+
failedResult(err) {
|
|
2620
|
+
return { terminal: 2 /* FailedComplete */, err };
|
|
2621
|
+
}
|
|
2622
|
+
/** Reports whether trimmed assistant text is long enough to persist as final. */
|
|
2623
|
+
isSubstantiveAssistantReply(content) {
|
|
2624
|
+
return [...content.trim()].length >= this.minDeliverableRunes;
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Picks the user-visible reply for a completed query. Only a deliverable
|
|
2628
|
+
* terminal result with a recognized completion event yields a final reply.
|
|
2629
|
+
*/
|
|
2630
|
+
resolveDeliverableFinalContent(eventResult, _accumulated) {
|
|
2631
|
+
if (eventResult.terminal !== 1 /* DeliverableComplete */) return ["", false];
|
|
2632
|
+
if (!this.isDeliverableCompletionEvent(eventResult.completionEvent ?? "")) return ["", false];
|
|
2633
|
+
const final = (eventResult.content ?? "").trim();
|
|
2634
|
+
if (final) return [final, true];
|
|
2635
|
+
return ["", false];
|
|
2636
|
+
}
|
|
2637
|
+
/** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
|
|
2638
|
+
processChatEvent(msg, accumulated) {
|
|
2639
|
+
if (!msg || typeof msg !== "object") {
|
|
2640
|
+
return { terminal: 0 /* Continue */ };
|
|
2641
|
+
}
|
|
2642
|
+
const m = msg;
|
|
2643
|
+
const typ = m.type;
|
|
2644
|
+
if (typ === "next") {
|
|
2645
|
+
return this.classifyNextEnvelope(m, accumulated);
|
|
2646
|
+
}
|
|
2647
|
+
if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack") {
|
|
2648
|
+
return { terminal: 0 /* Continue */ };
|
|
2649
|
+
}
|
|
2650
|
+
if (typ === "status") {
|
|
2651
|
+
if (this.treatStatusIdleAsComplete && String(m.state ?? "").trim().toLowerCase() === "idle" && this.isSubstantiveAssistantReply(accumulated)) {
|
|
2652
|
+
return this.deliverableResult(accumulated.trim(), "status.idle");
|
|
2653
|
+
}
|
|
2654
|
+
return { terminal: 0 /* Continue */ };
|
|
2655
|
+
}
|
|
2656
|
+
if (typ === "error") {
|
|
2657
|
+
const errObj = m.error ?? {};
|
|
2658
|
+
const code = typeof errObj.code === "number" ? errObj.code : -32603;
|
|
2659
|
+
return this.failedResult(
|
|
2660
|
+
new DaemonError(code, errObj.message ?? "daemon error", errObj.data)
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
if (typ === "event") {
|
|
2664
|
+
return this.classifyEventPayload(
|
|
2665
|
+
m.namespace ?? null,
|
|
2666
|
+
m.mode ?? "",
|
|
2667
|
+
m.data
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
return { terminal: 0 /* Continue */ };
|
|
2671
|
+
}
|
|
2672
|
+
/** Classifies a `next` envelope by projecting its payload. */
|
|
2673
|
+
classifyNextEnvelope(env, accumulated) {
|
|
2674
|
+
const payload = env.payload ?? {};
|
|
2675
|
+
const innerData = payload.data;
|
|
2676
|
+
if (innerData && typeof innerData === "object") {
|
|
2677
|
+
const innerType = innerData.type ?? "";
|
|
2678
|
+
if (innerType === "status") {
|
|
2679
|
+
return this.processChatEvent(innerData, accumulated);
|
|
2680
|
+
}
|
|
2681
|
+
const innerMode = innerData.mode ?? "";
|
|
2682
|
+
if (innerMode) {
|
|
2683
|
+
return this.classifyEventPayload(
|
|
2684
|
+
innerData.namespace ?? payload.namespace ?? null,
|
|
2685
|
+
innerMode,
|
|
2686
|
+
innerData.data
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
const mode = payload.mode ?? "";
|
|
2691
|
+
if (mode === "status" || mode === "") {
|
|
2692
|
+
if (typeof payload.state === "string" || innerData && "state" in (innerData ?? {})) {
|
|
2693
|
+
return this.processChatEvent({ type: "status", ...innerData ?? payload }, accumulated);
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
if (mode) {
|
|
2697
|
+
return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
|
|
2698
|
+
}
|
|
2699
|
+
return { terminal: 0 /* Continue */ };
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Classifies an event payload by (namespace, mode, phase). `data` may be a
|
|
2703
|
+
* map or an array of messages (mode="messages").
|
|
2704
|
+
*/
|
|
2705
|
+
classifyEventPayload(namespace, mode, data) {
|
|
2706
|
+
const ns = namespaceToString(namespace);
|
|
2707
|
+
const dataMap = normalizeEventData(data);
|
|
2708
|
+
if (dataMap) {
|
|
2709
|
+
let dataType2 = ns;
|
|
2710
|
+
const dt2 = dataMap["type"];
|
|
2711
|
+
if (typeof dt2 === "string" && dt2) dataType2 = dt2;
|
|
2712
|
+
if (dataType2 === EVENT_LOOP_HISTORY_REPLAYED) {
|
|
2713
|
+
return { terminal: 0 /* Continue */ };
|
|
2714
|
+
}
|
|
2715
|
+
const [step, ok] = extractThinkingStep(dataType2, dataMap, this.thinkingStepEvents);
|
|
2716
|
+
if (ok) {
|
|
2717
|
+
return { thinkingStep: step, terminal: 0 /* Continue */ };
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
if (mode === "messages") {
|
|
2721
|
+
const result = this.classifyMessagesMode(data, ns);
|
|
2722
|
+
if (result) return result;
|
|
2723
|
+
}
|
|
2724
|
+
if (!dataMap) {
|
|
2725
|
+
return { terminal: 0 /* Continue */ };
|
|
2726
|
+
}
|
|
2727
|
+
let dataType = ns;
|
|
2728
|
+
const dt = dataMap["type"];
|
|
2729
|
+
if (typeof dt === "string" && dt) dataType = dt;
|
|
2730
|
+
let completionEvent = dataType;
|
|
2731
|
+
if (!completionEvent) completionEvent = ns;
|
|
2732
|
+
if (isNamespaceMatch(ns, dataType, "soothe.output") || isNamespaceMatch(ns, dataType, "responded")) {
|
|
2733
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2734
|
+
if (ok) {
|
|
2735
|
+
if (this.isFinalOutputEvent(dataType, ns)) {
|
|
2736
|
+
return this.deliverableResult(content, completionEvent);
|
|
2737
|
+
}
|
|
2738
|
+
return this.continueResult(content);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
if (isNamespaceMatch(ns, dataType, "agent_loop.completed") || isNamespaceMatch(ns, dataType, "agent_loop.reasoned") || isNamespaceMatch(ns, dataType, "loop.completed")) {
|
|
2742
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2743
|
+
if (ok) return this.continueResult(content);
|
|
2744
|
+
}
|
|
2745
|
+
if (isNamespaceMatch(ns, dataType, "final_report")) {
|
|
2746
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2747
|
+
if (ok) return this.deliverableResult(content, completionEvent);
|
|
2748
|
+
}
|
|
2749
|
+
if (dataType.includes("soothe.error.") || ns.includes("soothe.error.")) {
|
|
2750
|
+
const errType = dataType || ns;
|
|
2751
|
+
const msg = dataMap["message"];
|
|
2752
|
+
if (typeof msg === "string" && msg) {
|
|
2753
|
+
return this.failedResult(new Error(`${errType}: ${msg}`));
|
|
2754
|
+
}
|
|
2755
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2756
|
+
if (ok) return this.failedResult(new Error(`${errType}: ${content}`));
|
|
2757
|
+
return this.failedResult(new Error(errType));
|
|
2758
|
+
}
|
|
2759
|
+
if (isNamespaceMatch(ns, dataType, "stream") || isNamespaceMatch(ns, dataType, "progress") || isNamespaceMatch(ns, dataType, "tool_call_updates_batch") || isNamespaceMatch(ns, dataType, "soothe.stream.tool_call.update")) {
|
|
2760
|
+
const delta = dataMap["delta"];
|
|
2761
|
+
if (typeof delta === "string") return this.continueResult(delta);
|
|
2762
|
+
}
|
|
2763
|
+
if (isNamespaceMatch(ns, dataType, "heartbeat") || isNamespaceMatch(ns, dataType, "system.daemon") || isNamespaceMatch(ns, dataType, "agent_loop.started") || isNamespaceMatch(ns, dataType, "intent.classified")) {
|
|
2764
|
+
return { terminal: 0 /* Continue */ };
|
|
2765
|
+
}
|
|
2766
|
+
return { terminal: 0 /* Continue */ };
|
|
2767
|
+
}
|
|
2768
|
+
/** Classifies a mode="messages" payload (array of message objects). */
|
|
2769
|
+
classifyMessagesMode(data, _ns) {
|
|
2770
|
+
const items = Array.isArray(data) ? data : null;
|
|
2771
|
+
if (!items || items.length === 0) return null;
|
|
2772
|
+
const first = items[0];
|
|
2773
|
+
if (!first || typeof first !== "object") return null;
|
|
2774
|
+
const [msgType, rawContent, phase, hasPayload] = firstMessagePayload(data);
|
|
2775
|
+
if (hasPayload && rawContent && isStreamingMessageType(msgType)) {
|
|
2776
|
+
return this.continueResult(rawContent);
|
|
2777
|
+
}
|
|
2778
|
+
const loopMsg = loopAIMessage(data);
|
|
2779
|
+
if (loopMsg) {
|
|
2780
|
+
const content = loopMsg.content;
|
|
2781
|
+
if (content) {
|
|
2782
|
+
if (isStreamingMessageType(loopMsg.type)) {
|
|
2783
|
+
return this.continueResult(content);
|
|
2784
|
+
}
|
|
2785
|
+
if (this.isDeliverableLoopPhase(loopMsg.phase) && this.isSubstantiveAssistantReply(content)) {
|
|
2786
|
+
return this.deliverableResult(content, "soothe.protocol.message." + loopMsg.phase);
|
|
2787
|
+
}
|
|
2788
|
+
return this.continueResult(content);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const [directContent, directOk] = this.messagesModeAssistantContent(data);
|
|
2792
|
+
if (directOk && this.isSubstantiveAssistantReply(directContent)) {
|
|
2793
|
+
return this.deliverableResult(directContent, "soothe.protocol.message.direct_model");
|
|
2794
|
+
}
|
|
2795
|
+
if (hasPayload && rawContent) {
|
|
2796
|
+
if (isTerminalMessageType(msgType) || msgType === "") {
|
|
2797
|
+
if (this.isDeliverableLoopPhase(phase) && this.isSubstantiveAssistantReply(rawContent)) {
|
|
2798
|
+
return this.deliverableResult(rawContent, "soothe.protocol.message." + phase);
|
|
2799
|
+
}
|
|
2800
|
+
return this.continueResult(rawContent);
|
|
2801
|
+
}
|
|
2802
|
+
return this.continueResult(rawContent);
|
|
2803
|
+
}
|
|
2804
|
+
return null;
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Extracts plain assistant text from mode="messages" events that carry a
|
|
2808
|
+
* terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
|
|
2809
|
+
* before phase tagging; prefer deliverablePhases including text_completion).
|
|
2810
|
+
*/
|
|
2811
|
+
messagesModeAssistantContent(data) {
|
|
2812
|
+
if (!Array.isArray(data) || data.length === 0) return ["", false];
|
|
2813
|
+
const msgMap = data[0];
|
|
2814
|
+
if (!msgMap || typeof msgMap !== "object") return ["", false];
|
|
2815
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
2816
|
+
if (phase) return ["", false];
|
|
2817
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
2818
|
+
if (msgType && !isTerminalMessageType(msgType)) return ["", false];
|
|
2819
|
+
const content = extractContentFromMessage(msgMap).trim();
|
|
2820
|
+
if (!content) return ["", false];
|
|
2821
|
+
return [content, true];
|
|
2822
|
+
}
|
|
2823
|
+
/** soothe output/responded events that carry user-facing final text. */
|
|
2824
|
+
isFinalOutputEvent(dataType, ns) {
|
|
2825
|
+
const combined = dataType + " " + ns;
|
|
2826
|
+
if (combined.includes("final_report")) return true;
|
|
2827
|
+
for (const phase of this.deliverablePhases) {
|
|
2828
|
+
if (combined.includes(phase)) return true;
|
|
2829
|
+
}
|
|
2830
|
+
return false;
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
function isStreamingMessageType(msgType) {
|
|
2834
|
+
return msgType === "AIMessageChunk" || msgType === "ai_chunk" || msgType === "message_chunk";
|
|
2835
|
+
}
|
|
2836
|
+
function isTerminalMessageType(msgType) {
|
|
2837
|
+
return msgType === "AIMessage" || msgType === "ai" || msgType === "assistant";
|
|
2838
|
+
}
|
|
2839
|
+
function firstMessagePayload(data) {
|
|
2840
|
+
if (!Array.isArray(data) || data.length === 0) return ["", "", "", false];
|
|
2841
|
+
const msgMap = data[0];
|
|
2842
|
+
if (!msgMap || typeof msgMap !== "object") return ["", "", "", false];
|
|
2843
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
2844
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase : "";
|
|
2845
|
+
const content = extractContentFromMessage(msgMap);
|
|
2846
|
+
return [msgType, content, phase, true];
|
|
2847
|
+
}
|
|
2848
|
+
function loopAIMessage(data) {
|
|
2849
|
+
if (!Array.isArray(data) || data.length === 0) return null;
|
|
2850
|
+
const msgMap = data[0];
|
|
2851
|
+
if (!msgMap || typeof msgMap !== "object") return null;
|
|
2852
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
2853
|
+
if (!phase) return null;
|
|
2854
|
+
const type = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
2855
|
+
const content = extractContentFromMessage(msgMap);
|
|
2856
|
+
return { type, content, phase };
|
|
2857
|
+
}
|
|
2858
|
+
function extractContentFromMessage(msgMap) {
|
|
2859
|
+
const c = msgMap.content;
|
|
2860
|
+
if (typeof c === "string" && c) return c;
|
|
2861
|
+
if (Array.isArray(c) && c.length > 0) {
|
|
2862
|
+
let b = "";
|
|
2863
|
+
for (const item of c) {
|
|
2864
|
+
if (typeof item === "string") {
|
|
2865
|
+
b += item;
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
if (item && typeof item === "object") {
|
|
2869
|
+
const blk = item;
|
|
2870
|
+
const t = blk.text;
|
|
2871
|
+
if (typeof t === "string") b += t;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
return b;
|
|
2875
|
+
}
|
|
2876
|
+
const blocks = msgMap.content_blocks;
|
|
2877
|
+
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
2878
|
+
let b = "";
|
|
2879
|
+
for (const blk of blocks) {
|
|
2880
|
+
if (blk && typeof blk === "object") {
|
|
2881
|
+
const m = blk;
|
|
2882
|
+
const t = m.text;
|
|
2883
|
+
if (typeof t === "string") b += t;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
return b;
|
|
2887
|
+
}
|
|
2888
|
+
return "";
|
|
2889
|
+
}
|
|
2890
|
+
function extractContentFromData(data) {
|
|
2891
|
+
for (const key of [
|
|
2892
|
+
"final_stdout_message",
|
|
2893
|
+
"completion_summary",
|
|
2894
|
+
"content",
|
|
2895
|
+
"text",
|
|
2896
|
+
"response",
|
|
2897
|
+
"output",
|
|
2898
|
+
"message",
|
|
2899
|
+
"report"
|
|
2900
|
+
]) {
|
|
2901
|
+
const val = data[key];
|
|
2902
|
+
if (typeof val === "string" && val) return [val, true];
|
|
2903
|
+
}
|
|
2904
|
+
const nested = data.data;
|
|
2905
|
+
if (nested && typeof nested === "object") {
|
|
2906
|
+
const nm = nested;
|
|
2907
|
+
for (const key of [
|
|
2908
|
+
"final_stdout_message",
|
|
2909
|
+
"completion_summary",
|
|
2910
|
+
"content",
|
|
2911
|
+
"text",
|
|
2912
|
+
"response",
|
|
2913
|
+
"output",
|
|
2914
|
+
"message",
|
|
2915
|
+
"report"
|
|
2916
|
+
]) {
|
|
2917
|
+
const val = nm[key];
|
|
2918
|
+
if (typeof val === "string" && val) return [val, true];
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
return ["", false];
|
|
2922
|
+
}
|
|
2923
|
+
function isNamespaceMatch(ns, dataType, pattern) {
|
|
2924
|
+
return dataType.includes(pattern) || ns.includes(pattern);
|
|
2925
|
+
}
|
|
2926
|
+
function namespaceToString(namespace) {
|
|
2927
|
+
if (typeof namespace === "string") return namespace;
|
|
2928
|
+
if (Array.isArray(namespace)) return namespace.filter((s) => typeof s === "string").join(".");
|
|
2929
|
+
return "";
|
|
2930
|
+
}
|
|
2931
|
+
function normalizeEventData(data) {
|
|
2932
|
+
if (data == null) return null;
|
|
2933
|
+
if (typeof data === "object" && !Array.isArray(data)) {
|
|
2934
|
+
return data;
|
|
2935
|
+
}
|
|
2936
|
+
if (typeof data === "string") {
|
|
2937
|
+
try {
|
|
2938
|
+
const m = JSON.parse(data);
|
|
2939
|
+
if (m && typeof m === "object" && !Array.isArray(m)) return m;
|
|
2940
|
+
} catch {
|
|
2941
|
+
return null;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
return null;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// src/appkit/query_gate.ts
|
|
2948
|
+
var ErrQueryBusy = class extends Error {
|
|
2949
|
+
constructor() {
|
|
2950
|
+
super("appkit: query already in progress for session");
|
|
2951
|
+
this.name = "ErrQueryBusy";
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
var QueryGate = class {
|
|
2955
|
+
active = /* @__PURE__ */ new Map();
|
|
2956
|
+
/** Constructs an empty gate. */
|
|
2957
|
+
constructor() {
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
|
|
2961
|
+
* already in flight. `abort` is the AbortController for the query's timeout
|
|
2962
|
+
* context. `sendCancel` is the daemon-cancel sender; it is invoked from
|
|
2963
|
+
* `cancel()` on a detached 10s timeout.
|
|
2964
|
+
*/
|
|
2965
|
+
acquire(sessionID, abort, sendCancel) {
|
|
2966
|
+
if (this.active.has(sessionID)) {
|
|
2967
|
+
throw new ErrQueryBusy();
|
|
2968
|
+
}
|
|
2969
|
+
this.active.set(sessionID, { abort, sendCancel });
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Cooperatively stops a running query for sessionID. Sends the daemon cancel
|
|
2973
|
+
* (on a detached 10s-timeout abort so caller cancellation cannot block the
|
|
2974
|
+
* wire send) BEFORE aborting the local context. Returns silently if no query
|
|
2975
|
+
* is in flight (intent already satisfied).
|
|
2976
|
+
*/
|
|
2977
|
+
async cancel(sessionID) {
|
|
2978
|
+
const state = this.active.get(sessionID);
|
|
2979
|
+
if (!state) return;
|
|
2980
|
+
this.active.delete(sessionID);
|
|
2981
|
+
if (state.sendCancel) {
|
|
2982
|
+
const detached = new AbortController();
|
|
2983
|
+
const timer = setTimeout(() => detached.abort(), 1e4);
|
|
2984
|
+
try {
|
|
2985
|
+
await state.sendCancel(detached.signal);
|
|
2986
|
+
} catch {
|
|
2987
|
+
} finally {
|
|
2988
|
+
clearTimeout(timer);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
state.abort.abort();
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Clears the gate for sessionID without sending a daemon cancel. Call when a
|
|
2995
|
+
* query completes normally (success or local failure) so the next turn can
|
|
2996
|
+
* acquire.
|
|
2997
|
+
*/
|
|
2998
|
+
release(sessionID) {
|
|
2999
|
+
this.active.delete(sessionID);
|
|
3000
|
+
}
|
|
3001
|
+
/** Reports whether a query is in flight for sessionID. */
|
|
3002
|
+
isActive(sessionID) {
|
|
3003
|
+
return this.active.has(sessionID);
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
|
|
3007
|
+
// src/appkit/pool.ts
|
|
3008
|
+
init_errors();
|
|
3009
|
+
init_config();
|
|
3010
|
+
|
|
3011
|
+
// src/appkit/client.ts
|
|
3012
|
+
init_config();
|
|
3013
|
+
init_client();
|
|
3014
|
+
function defaultClientFactory() {
|
|
3015
|
+
return (url, config) => {
|
|
3016
|
+
return new Client(url, config ?? defaultConfig());
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
function defaultBootstrapFunc() {
|
|
3020
|
+
return async (client, workspaceID, userID, config) => {
|
|
3021
|
+
const c = client;
|
|
3022
|
+
const opts = {
|
|
3023
|
+
client_workspace: workspaceID,
|
|
3024
|
+
user_id: userID,
|
|
3025
|
+
client_workspace_id: workspaceID
|
|
3026
|
+
};
|
|
3027
|
+
return bootstrapLoopSession(c, "", config, opts);
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// src/appkit/pool.ts
|
|
3032
|
+
var ErrPoolExhausted = class extends Error {
|
|
3033
|
+
constructor() {
|
|
3034
|
+
super("appkit: connection pool exhausted");
|
|
3035
|
+
this.name = "ErrPoolExhausted";
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
function defaultPoolConfig() {
|
|
3039
|
+
return {
|
|
3040
|
+
poolSize: 1e3,
|
|
3041
|
+
queryTimeout: 30 * 60 * 1e3,
|
|
3042
|
+
connectionTimeout: 3e4,
|
|
3043
|
+
maxIdleTime: 10 * 60 * 1e3,
|
|
3044
|
+
healthCheckInterval: 3e4
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
var PooledConn = class {
|
|
3048
|
+
slotID;
|
|
3049
|
+
client;
|
|
3050
|
+
eventStream = null;
|
|
3051
|
+
streamController = null;
|
|
3052
|
+
sessionID = "";
|
|
3053
|
+
loopID = "";
|
|
3054
|
+
workspaceID = "";
|
|
3055
|
+
lastUsed = 0;
|
|
3056
|
+
constructor(slotID, client) {
|
|
3057
|
+
this.slotID = slotID;
|
|
3058
|
+
this.client = client;
|
|
3059
|
+
}
|
|
3060
|
+
/** Reports whether the underlying client signalled a drop. */
|
|
3061
|
+
isDisconnected() {
|
|
3062
|
+
return this.client.isDisconnected();
|
|
3063
|
+
}
|
|
3064
|
+
isConnected() {
|
|
3065
|
+
return this.client.isConnected() && !this.isDisconnected();
|
|
3066
|
+
}
|
|
3067
|
+
getLoopID() {
|
|
3068
|
+
return this.loopID;
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
var ConnectionPool = class {
|
|
3072
|
+
cfg;
|
|
3073
|
+
scfg;
|
|
3074
|
+
factory;
|
|
3075
|
+
bootstrap;
|
|
3076
|
+
store;
|
|
3077
|
+
pool = [];
|
|
3078
|
+
activeSlots = /* @__PURE__ */ new Map();
|
|
3079
|
+
nextSlotID = 1;
|
|
3080
|
+
url;
|
|
3081
|
+
/**
|
|
3082
|
+
* Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
|
|
3083
|
+
* defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
|
|
3084
|
+
* factory/bootstrap fall back to the defaults.
|
|
3085
|
+
*/
|
|
3086
|
+
constructor(url, store, cfg, scfg, factory) {
|
|
3087
|
+
this.cfg = cfg ?? defaultPoolConfig();
|
|
3088
|
+
this.scfg = scfg ?? defaultConfig();
|
|
3089
|
+
this.factory = factory ?? defaultClientFactory();
|
|
3090
|
+
this.bootstrap = defaultBootstrapFunc();
|
|
3091
|
+
this.store = store;
|
|
3092
|
+
this.url = url;
|
|
3093
|
+
for (let i = 0; i < this.cfg.poolSize; i++) {
|
|
3094
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(url, this.scfg)));
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
/** Overrides the loop bootstrap function (useful for test fakes). */
|
|
3098
|
+
withBootstrap(f) {
|
|
3099
|
+
if (f) this.bootstrap = f;
|
|
3100
|
+
return this;
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Returns a live connection for sessionID, reusing an active slot or
|
|
3104
|
+
* bootstrapping/reattaching as needed. The caller must call `release()`
|
|
3105
|
+
* when done with the connection (a turn completes or the session is reset).
|
|
3106
|
+
*/
|
|
3107
|
+
async acquire(sessionID, workspaceID, userID, _signal) {
|
|
3108
|
+
const existing = this.activeSlots.get(sessionID);
|
|
3109
|
+
if (existing) {
|
|
3110
|
+
if (existing.isDisconnected() || !existing.isConnected()) {
|
|
3111
|
+
await this.release(sessionID);
|
|
3112
|
+
} else {
|
|
3113
|
+
const idleTooLong = this.cfg.maxIdleTime > 0 && existing.lastUsed > 0 && Date.now() - existing.lastUsed > this.cfg.maxIdleTime;
|
|
3114
|
+
if (idleTooLong) {
|
|
3115
|
+
await this.release(sessionID);
|
|
3116
|
+
} else {
|
|
3117
|
+
existing.lastUsed = Date.now();
|
|
3118
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
3119
|
+
});
|
|
3120
|
+
return existing;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
const conn = this.pool.pop();
|
|
3125
|
+
if (!conn) throw new ErrPoolExhausted();
|
|
3126
|
+
this.activeSlots.set(sessionID, conn);
|
|
3127
|
+
const { loopID, ok } = await this.store.getLoopIDForSession(sessionID).catch(() => ({ loopID: "", ok: false }));
|
|
3128
|
+
let finalLoopID = "";
|
|
3129
|
+
try {
|
|
3130
|
+
if (!ok || !loopID) {
|
|
3131
|
+
await conn.client.connect();
|
|
3132
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
3133
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
3134
|
+
});
|
|
3135
|
+
} else {
|
|
3136
|
+
try {
|
|
3137
|
+
await this.resumeAndReattach(conn, loopID);
|
|
3138
|
+
finalLoopID = loopID;
|
|
3139
|
+
} catch {
|
|
3140
|
+
await conn.client.connect();
|
|
3141
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
3142
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
} catch (err) {
|
|
3147
|
+
await this.release(sessionID);
|
|
3148
|
+
throw err;
|
|
3149
|
+
}
|
|
3150
|
+
conn.sessionID = sessionID;
|
|
3151
|
+
conn.loopID = finalLoopID;
|
|
3152
|
+
conn.workspaceID = workspaceID;
|
|
3153
|
+
conn.lastUsed = Date.now();
|
|
3154
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
3155
|
+
});
|
|
3156
|
+
return conn;
|
|
3157
|
+
}
|
|
3158
|
+
/** Tears down the connection for sessionID and returns the slot. */
|
|
3159
|
+
async release(sessionID) {
|
|
3160
|
+
const conn = this.activeSlots.get(sessionID);
|
|
3161
|
+
if (!conn) return;
|
|
3162
|
+
this.activeSlots.delete(sessionID);
|
|
3163
|
+
if (conn.streamController) {
|
|
3164
|
+
conn.streamController.abort();
|
|
3165
|
+
conn.streamController = null;
|
|
3166
|
+
}
|
|
3167
|
+
try {
|
|
3168
|
+
conn.client.close();
|
|
3169
|
+
} catch {
|
|
3170
|
+
}
|
|
3171
|
+
conn.sessionID = "";
|
|
3172
|
+
conn.loopID = "";
|
|
3173
|
+
conn.eventStream = null;
|
|
3174
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(this.url, this.scfg)));
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Tears down the connection for sessionID so the next acquire bootstraps
|
|
3178
|
+
* fresh. The store should archive the loop id so getLoopIDForSession returns
|
|
3179
|
+
* false next time.
|
|
3180
|
+
*/
|
|
3181
|
+
async resetSession(sessionID) {
|
|
3182
|
+
await this.release(sessionID);
|
|
3183
|
+
}
|
|
3184
|
+
/** Gracefully shuts down all active connections. */
|
|
3185
|
+
stop() {
|
|
3186
|
+
for (const [sid, conn] of this.activeSlots) {
|
|
3187
|
+
if (conn.streamController) conn.streamController.abort();
|
|
3188
|
+
try {
|
|
3189
|
+
conn.client.close();
|
|
3190
|
+
} catch {
|
|
3191
|
+
}
|
|
3192
|
+
this.activeSlots.delete(sid);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
/** Stats snapshot for observability. */
|
|
3196
|
+
stats() {
|
|
3197
|
+
return { active: this.activeSlots.size, idle: this.pool.length };
|
|
3198
|
+
}
|
|
3199
|
+
/** Bootstrap a fresh loop and start the reader. */
|
|
3200
|
+
async bootstrapNew(conn, workspaceID, userID) {
|
|
3201
|
+
const loopID = await this.bootstrap(conn.client, workspaceID, userID, this.scfg);
|
|
3202
|
+
this.startReader(conn);
|
|
3203
|
+
return loopID;
|
|
3204
|
+
}
|
|
3205
|
+
/** Reconnect + reattach an existing loop, then start the reader. */
|
|
3206
|
+
async resumeAndReattach(conn, loopID) {
|
|
3207
|
+
await conn.client.connect();
|
|
3208
|
+
try {
|
|
3209
|
+
await conn.client.reattachAndProbe(loopID);
|
|
3210
|
+
} catch (err) {
|
|
3211
|
+
if (err instanceof StaleLoopError) throw err;
|
|
3212
|
+
throw err;
|
|
3213
|
+
}
|
|
3214
|
+
this.startReader(conn);
|
|
3215
|
+
}
|
|
3216
|
+
/** Starts a receiveMessages generator and stores the stream + controller. */
|
|
3217
|
+
startReader(conn) {
|
|
3218
|
+
const controller = new AbortController();
|
|
3219
|
+
conn.streamController = controller;
|
|
3220
|
+
conn.eventStream = conn.client.receiveMessages(controller.signal);
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
|
|
3224
|
+
// src/appkit/attachments.ts
|
|
3225
|
+
function compactDefaults(opts) {
|
|
3226
|
+
return {
|
|
3227
|
+
maxDim: opts?.maxDim && opts.maxDim > 0 ? opts.maxDim : 768,
|
|
3228
|
+
quality: opts?.jpegQuality && opts.jpegQuality > 0 ? opts.jpegQuality : 85
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
var sharpLoader = null;
|
|
3232
|
+
async function loadSharp() {
|
|
3233
|
+
if (!sharpLoader) {
|
|
3234
|
+
sharpLoader = (async () => {
|
|
3235
|
+
try {
|
|
3236
|
+
const m = await Function('return import("sharp")')();
|
|
3237
|
+
return m;
|
|
3238
|
+
} catch {
|
|
3239
|
+
return null;
|
|
3240
|
+
}
|
|
3241
|
+
})();
|
|
3242
|
+
}
|
|
3243
|
+
return sharpLoader;
|
|
3244
|
+
}
|
|
3245
|
+
async function compactImageAttachment(mimeType, dataB64, opts) {
|
|
3246
|
+
if (!dataB64 || !mimeType.startsWith("image/")) {
|
|
3247
|
+
return [mimeType, dataB64];
|
|
3248
|
+
}
|
|
3249
|
+
let raw;
|
|
3250
|
+
try {
|
|
3251
|
+
raw = Buffer.from(dataB64, "base64");
|
|
3252
|
+
} catch {
|
|
3253
|
+
return [mimeType, dataB64];
|
|
3254
|
+
}
|
|
3255
|
+
if (raw.length === 0) return [mimeType, dataB64];
|
|
3256
|
+
const sharpMod = await loadSharp();
|
|
3257
|
+
if (!sharpMod) return [mimeType, dataB64];
|
|
3258
|
+
const { maxDim, quality } = compactDefaults(opts);
|
|
3259
|
+
try {
|
|
3260
|
+
const img = sharpMod.default(raw, { failOn: "none" });
|
|
3261
|
+
const meta = await img.metadata();
|
|
3262
|
+
const w = meta.width ?? 0;
|
|
3263
|
+
const h = meta.height ?? 0;
|
|
3264
|
+
if (w <= 0 || h <= 0 || w <= maxDim && h <= maxDim) {
|
|
3265
|
+
return [mimeType, dataB64];
|
|
3266
|
+
}
|
|
3267
|
+
let nw = w;
|
|
3268
|
+
let nh = h;
|
|
3269
|
+
if (w >= h) {
|
|
3270
|
+
if (w > maxDim) {
|
|
3271
|
+
nw = maxDim;
|
|
3272
|
+
nh = Math.max(1, Math.round(h * maxDim / w));
|
|
3273
|
+
}
|
|
3274
|
+
} else if (h > maxDim) {
|
|
3275
|
+
nh = maxDim;
|
|
3276
|
+
nw = Math.max(1, Math.round(w * maxDim / h));
|
|
3277
|
+
}
|
|
3278
|
+
const resized = img.resize(nw, nh, { fit: "fill" });
|
|
3279
|
+
if (mimeType === "image/png") {
|
|
3280
|
+
const buf2 = await resized.png().toBuffer();
|
|
3281
|
+
return [mimeType, buf2.toString("base64")];
|
|
3282
|
+
}
|
|
3283
|
+
const buf = await resized.jpeg({ quality }).toBuffer();
|
|
3284
|
+
return ["image/jpeg", buf.toString("base64")];
|
|
3285
|
+
} catch {
|
|
3286
|
+
return [mimeType, dataB64];
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
async function compactAttachments(atts, opts) {
|
|
3290
|
+
if (!atts.length) return atts;
|
|
3291
|
+
const out = [];
|
|
3292
|
+
for (const att of atts) {
|
|
3293
|
+
const cp = { ...att };
|
|
3294
|
+
const mime = typeof cp.mime_type === "string" ? cp.mime_type : "";
|
|
3295
|
+
const data = typeof cp.data === "string" ? cp.data : "";
|
|
3296
|
+
if (mime && data) {
|
|
3297
|
+
const [outMime, outData] = await compactImageAttachment(mime, data, opts);
|
|
3298
|
+
cp.mime_type = outMime;
|
|
3299
|
+
cp.data = outData;
|
|
3300
|
+
}
|
|
3301
|
+
out.push(cp);
|
|
3302
|
+
}
|
|
3303
|
+
return out;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// src/appkit/turn_runner.ts
|
|
3307
|
+
init_intent_hints();
|
|
3308
|
+
var ErrQueryTimeout = class extends Error {
|
|
3309
|
+
constructor() {
|
|
3310
|
+
super("appkit: query timeout");
|
|
3311
|
+
this.name = "ErrQueryTimeout";
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
var ErrIdleTimeout = class extends Error {
|
|
3315
|
+
constructor() {
|
|
3316
|
+
super("appkit: idle timeout");
|
|
3317
|
+
this.name = "ErrIdleTimeout";
|
|
3318
|
+
}
|
|
3319
|
+
};
|
|
3320
|
+
var TimeoutPolicy = /* @__PURE__ */ ((TimeoutPolicy2) => {
|
|
3321
|
+
TimeoutPolicy2[TimeoutPolicy2["Fail"] = 0] = "Fail";
|
|
3322
|
+
TimeoutPolicy2[TimeoutPolicy2["SoftComplete"] = 1] = "SoftComplete";
|
|
3323
|
+
return TimeoutPolicy2;
|
|
3324
|
+
})(TimeoutPolicy || {});
|
|
3325
|
+
var StreamCloseFail = 0 /* Fail */;
|
|
3326
|
+
var StreamCloseSoftComplete = 1 /* SoftComplete */;
|
|
3327
|
+
function inputMessageForLoop(text, loopID, attachments, opts) {
|
|
3328
|
+
const msg = { type: "loop_input", content: text };
|
|
3329
|
+
if (loopID) msg.loop_id = loopID;
|
|
3330
|
+
if (attachments && attachments.length > 0) msg.attachments = attachments;
|
|
3331
|
+
if (opts) {
|
|
3332
|
+
if (opts.intentHint?.trim()) {
|
|
3333
|
+
const hintError = validateLoopInputIntentHint(opts.intentHint);
|
|
3334
|
+
if (hintError) {
|
|
3335
|
+
throw new Error(hintError);
|
|
3336
|
+
}
|
|
3337
|
+
msg.intent_hint = opts.intentHint.trim();
|
|
3338
|
+
}
|
|
3339
|
+
if (opts.preferredSubagent?.trim()) msg.preferred_subagent = opts.preferredSubagent.trim();
|
|
3340
|
+
if (opts.responseSchema && Object.keys(opts.responseSchema).length > 0) {
|
|
3341
|
+
msg.response_schema = opts.responseSchema;
|
|
3342
|
+
}
|
|
3343
|
+
if (opts.responseSchemaName?.trim()) msg.response_schema_name = opts.responseSchemaName.trim();
|
|
3344
|
+
if (opts.responseSchemaStrict !== void 0)
|
|
3345
|
+
msg.response_schema_strict = opts.responseSchemaStrict;
|
|
3346
|
+
}
|
|
3347
|
+
return msg;
|
|
3348
|
+
}
|
|
3349
|
+
function idleTimeoutForTurn(cfg, hasAttachments) {
|
|
3350
|
+
const idle = cfg.idleTimeout ?? 0;
|
|
3351
|
+
if (idle <= 0) return 0;
|
|
3352
|
+
const floor = cfg.minIdleTimeoutWithAttachments ?? 0;
|
|
3353
|
+
if (hasAttachments && floor > 0 && idle < floor) return floor;
|
|
3354
|
+
return idle;
|
|
3355
|
+
}
|
|
3356
|
+
var TurnRunner = class {
|
|
3357
|
+
pool;
|
|
3358
|
+
gate;
|
|
3359
|
+
classifier;
|
|
3360
|
+
store;
|
|
3361
|
+
broadcaster;
|
|
3362
|
+
cfg;
|
|
3363
|
+
buildInput = inputMessageForLoop;
|
|
3364
|
+
onComplete = null;
|
|
3365
|
+
onError = null;
|
|
3366
|
+
constructor(pool, gate, classifier, store, broadcaster, cfg) {
|
|
3367
|
+
this.pool = pool;
|
|
3368
|
+
this.gate = gate;
|
|
3369
|
+
this.classifier = classifier;
|
|
3370
|
+
this.store = store;
|
|
3371
|
+
this.broadcaster = broadcaster;
|
|
3372
|
+
this.cfg = {
|
|
3373
|
+
...cfg,
|
|
3374
|
+
queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
withInputBuilder(f) {
|
|
3378
|
+
if (f) this.buildInput = f;
|
|
3379
|
+
return this;
|
|
3380
|
+
}
|
|
3381
|
+
withOnComplete(f) {
|
|
3382
|
+
this.onComplete = f;
|
|
3383
|
+
return this;
|
|
3384
|
+
}
|
|
3385
|
+
withOnError(f) {
|
|
3386
|
+
this.onError = f;
|
|
3387
|
+
return this;
|
|
3388
|
+
}
|
|
3389
|
+
async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
|
|
3390
|
+
let conn;
|
|
3391
|
+
try {
|
|
3392
|
+
conn = await this.pool.acquire(sessionID, workspaceID, userID, signal);
|
|
3393
|
+
} catch (err) {
|
|
3394
|
+
await this.persistFailed(sessionID, "", err);
|
|
3395
|
+
this.broadcastError(sessionID, err);
|
|
3396
|
+
this.onError?.(sessionID, "", err);
|
|
3397
|
+
throw err;
|
|
3398
|
+
}
|
|
3399
|
+
const loopID = conn.getLoopID();
|
|
3400
|
+
const timeoutController = new AbortController();
|
|
3401
|
+
const timeoutMs = this.cfg.queryTimeout;
|
|
3402
|
+
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
3403
|
+
const sendCancel = async (detachedSignal) => {
|
|
3404
|
+
await this.sendLoopCancel(detachedSignal, conn, loopID);
|
|
3405
|
+
};
|
|
3406
|
+
try {
|
|
3407
|
+
this.gate.acquire(sessionID, timeoutController, sendCancel);
|
|
3408
|
+
} catch (err) {
|
|
3409
|
+
clearTimeout(timer);
|
|
3410
|
+
await this.pool.release(sessionID);
|
|
3411
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
3412
|
+
this.broadcastError(sessionID, err);
|
|
3413
|
+
this.onError?.(sessionID, loopID, err);
|
|
3414
|
+
throw err;
|
|
3415
|
+
}
|
|
3416
|
+
let idleTimer = null;
|
|
3417
|
+
const clearIdle = () => {
|
|
3418
|
+
if (idleTimer) {
|
|
3419
|
+
clearTimeout(idleTimer);
|
|
3420
|
+
idleTimer = null;
|
|
3421
|
+
}
|
|
3422
|
+
};
|
|
3423
|
+
try {
|
|
3424
|
+
let atts = attachments ?? void 0;
|
|
3425
|
+
if (this.cfg.compactAttachmentsBeforeSend && atts && atts.length > 0) {
|
|
3426
|
+
atts = await compactAttachments(atts, this.cfg.compactImageOpts);
|
|
3427
|
+
}
|
|
3428
|
+
const inputMsg = this.buildInput(message, loopID, atts, opts ?? void 0);
|
|
3429
|
+
try {
|
|
3430
|
+
await conn.client.sendMessage(inputMsg);
|
|
3431
|
+
} catch (err) {
|
|
3432
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
3433
|
+
this.broadcastError(sessionID, err);
|
|
3434
|
+
this.onError?.(sessionID, loopID, err);
|
|
3435
|
+
throw err;
|
|
3436
|
+
}
|
|
3437
|
+
const eventStream = conn.eventStream;
|
|
3438
|
+
if (!eventStream) {
|
|
3439
|
+
const err = new Error(`missing event stream for session ${sessionID} (loop ${loopID})`);
|
|
3440
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
3441
|
+
this.broadcastError(sessionID, err);
|
|
3442
|
+
this.onError?.(sessionID, loopID, err);
|
|
3443
|
+
throw err;
|
|
3444
|
+
}
|
|
3445
|
+
let assistantContent = "";
|
|
3446
|
+
const startedAt = Date.now();
|
|
3447
|
+
const idleForTurn = idleTimeoutForTurn(this.cfg, (attachments?.length ?? 0) > 0);
|
|
3448
|
+
let idleReject = null;
|
|
3449
|
+
const armIdle = () => {
|
|
3450
|
+
clearIdle();
|
|
3451
|
+
idleReject = null;
|
|
3452
|
+
if (idleForTurn <= 0) {
|
|
3453
|
+
return new Promise(() => {
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
return new Promise((resolve) => {
|
|
3457
|
+
idleReject = () => resolve("idle");
|
|
3458
|
+
idleTimer = setTimeout(() => {
|
|
3459
|
+
idleReject?.();
|
|
3460
|
+
}, idleForTurn);
|
|
3461
|
+
});
|
|
3462
|
+
};
|
|
3463
|
+
let idleRace = armIdle();
|
|
3464
|
+
const abortRace = new Promise((resolve) => {
|
|
3465
|
+
const onTimeout = () => resolve("timeout");
|
|
3466
|
+
timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
|
|
3467
|
+
if (signal) {
|
|
3468
|
+
const onCaller = () => resolve("caller");
|
|
3469
|
+
signal.addEventListener("abort", onCaller, { once: true });
|
|
3470
|
+
}
|
|
3471
|
+
});
|
|
3472
|
+
const iterator = eventStream[Symbol.asyncIterator]();
|
|
3473
|
+
while (true) {
|
|
3474
|
+
const next = iterator.next();
|
|
3475
|
+
const raced = await Promise.race([
|
|
3476
|
+
next.then((res2) => ({ tag: "msg", res: res2 })),
|
|
3477
|
+
abortRace.then((tag) => ({ tag })),
|
|
3478
|
+
idleRace.then((tag) => ({ tag }))
|
|
3479
|
+
]);
|
|
3480
|
+
if ("tag" in raced && raced.tag !== "msg") {
|
|
3481
|
+
if (raced.tag === "caller" || signal?.aborted) {
|
|
3482
|
+
clearIdle();
|
|
3483
|
+
const err = new Error("aborted");
|
|
3484
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
3485
|
+
this.broadcastError(sessionID, err);
|
|
3486
|
+
this.onError?.(sessionID, loopID, err);
|
|
3487
|
+
throw err;
|
|
3488
|
+
}
|
|
3489
|
+
if (raced.tag === "idle") {
|
|
3490
|
+
clearIdle();
|
|
3491
|
+
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
3492
|
+
});
|
|
3493
|
+
await this.finishTimeout(
|
|
3494
|
+
sessionID,
|
|
3495
|
+
loopID,
|
|
3496
|
+
assistantContent,
|
|
3497
|
+
startedAt,
|
|
3498
|
+
new ErrIdleTimeout(),
|
|
3499
|
+
"idle_timeout",
|
|
3500
|
+
this.cfg.onIdleTimeout ?? 0 /* Fail */
|
|
3501
|
+
);
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
clearIdle();
|
|
3505
|
+
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
3506
|
+
});
|
|
3507
|
+
await this.finishTimeout(
|
|
3508
|
+
sessionID,
|
|
3509
|
+
loopID,
|
|
3510
|
+
assistantContent,
|
|
3511
|
+
startedAt,
|
|
3512
|
+
new ErrQueryTimeout(),
|
|
3513
|
+
"query_timeout",
|
|
3514
|
+
this.cfg.onQueryTimeout ?? 0 /* Fail */
|
|
3515
|
+
);
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
const res = raced.res;
|
|
3519
|
+
if (res.done) {
|
|
3520
|
+
clearIdle();
|
|
3521
|
+
if ((this.cfg.onStreamClose ?? 0 /* Fail */) === 1 /* SoftComplete */ && assistantContent.trim() !== "") {
|
|
3522
|
+
await this.completeTurn(
|
|
3523
|
+
sessionID,
|
|
3524
|
+
loopID,
|
|
3525
|
+
assistantContent,
|
|
3526
|
+
startedAt,
|
|
3527
|
+
"stream_closed"
|
|
3528
|
+
);
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
const err = new Error("event stream closed");
|
|
3532
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
3533
|
+
this.broadcastError(sessionID, err);
|
|
3534
|
+
this.onError?.(sessionID, loopID, err);
|
|
3535
|
+
throw err;
|
|
3536
|
+
}
|
|
3537
|
+
idleRace = armIdle();
|
|
3538
|
+
const msg = res.value;
|
|
3539
|
+
const eventResult = this.classifier.classify(msg, assistantContent);
|
|
3540
|
+
if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
|
|
3541
|
+
clearIdle();
|
|
3542
|
+
await this.persistFailed(sessionID, loopID, eventResult.err);
|
|
3543
|
+
this.broadcastError(sessionID, eventResult.err);
|
|
3544
|
+
this.onError?.(sessionID, loopID, eventResult.err);
|
|
3545
|
+
throw eventResult.err;
|
|
3546
|
+
}
|
|
3547
|
+
const step = (eventResult.thinkingStep ?? "").trim();
|
|
3548
|
+
if (step) this.broadcastThinkingStep(sessionID, step);
|
|
3549
|
+
if (eventResult.content) {
|
|
3550
|
+
if (eventResult.content.startsWith(assistantContent)) {
|
|
3551
|
+
assistantContent = eventResult.content;
|
|
3552
|
+
} else {
|
|
3553
|
+
assistantContent += eventResult.content;
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
const [final, deliverable] = this.classifier.resolveDeliverableFinalContent(
|
|
3557
|
+
eventResult,
|
|
3558
|
+
assistantContent
|
|
3559
|
+
);
|
|
3560
|
+
if (deliverable) {
|
|
3561
|
+
clearIdle();
|
|
3562
|
+
await this.completeTurn(
|
|
3563
|
+
sessionID,
|
|
3564
|
+
loopID,
|
|
3565
|
+
final,
|
|
3566
|
+
startedAt,
|
|
3567
|
+
eventResult.completionEvent ?? ""
|
|
3568
|
+
);
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
} finally {
|
|
3573
|
+
clearIdle();
|
|
3574
|
+
clearTimeout(timer);
|
|
3575
|
+
this.gate.release(sessionID);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
async finishTimeout(sessionID, loopID, content, startedAt, failErr, completionEvent, policy) {
|
|
3579
|
+
if (policy === 1 /* SoftComplete */ && content.trim() !== "") {
|
|
3580
|
+
await this.completeTurn(sessionID, loopID, content, startedAt, completionEvent);
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
await this.persistFailed(sessionID, loopID, failErr);
|
|
3584
|
+
this.broadcastError(sessionID, failErr);
|
|
3585
|
+
this.onError?.(sessionID, loopID, failErr);
|
|
3586
|
+
throw failErr;
|
|
3587
|
+
}
|
|
3588
|
+
async completeTurn(sessionID, loopID, final, startedAt, completionEvent) {
|
|
3589
|
+
const elapsedMs = Date.now() - startedAt;
|
|
3590
|
+
await this.persistResponse(sessionID, loopID, final, startedAt, completionEvent);
|
|
3591
|
+
this.broadcastComplete(sessionID, final);
|
|
3592
|
+
this.onComplete?.(sessionID, loopID, final, completionEvent, elapsedMs);
|
|
3593
|
+
}
|
|
3594
|
+
async sendLoopCancel(_signal, conn, loopID) {
|
|
3595
|
+
const lid = (loopID ?? "").trim();
|
|
3596
|
+
if (!conn || !lid) return;
|
|
3597
|
+
const cancelMsg = { type: "command_request", command: "cancel", loop_id: lid };
|
|
3598
|
+
await conn.client.sendMessage(cancelMsg);
|
|
3599
|
+
}
|
|
3600
|
+
async persistResponse(sessionID, loopID, content, startedAt, completionEvent) {
|
|
3601
|
+
const msg = {
|
|
3602
|
+
role: "assistant",
|
|
3603
|
+
content,
|
|
3604
|
+
metadata: {
|
|
3605
|
+
started_at: startedAt,
|
|
3606
|
+
completed_at: Date.now(),
|
|
3607
|
+
duration_ms: Date.now() - startedAt,
|
|
3608
|
+
status: "completed",
|
|
3609
|
+
completion_event: completionEvent,
|
|
3610
|
+
deliverable: true
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3616
|
+
async persistFailed(sessionID, _loopID, err) {
|
|
3617
|
+
const msg = {
|
|
3618
|
+
role: "error",
|
|
3619
|
+
content: err.message,
|
|
3620
|
+
metadata: { status: "failed", error_message: err.message }
|
|
3621
|
+
};
|
|
3622
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
broadcastThinkingStep(sessionID, step) {
|
|
3626
|
+
if (!this.broadcaster) return;
|
|
3627
|
+
this.broadcaster.broadcast(sessionID, { type: "delta", data: step + "\n" });
|
|
3628
|
+
}
|
|
3629
|
+
broadcastComplete(sessionID, content) {
|
|
3630
|
+
this.broadcaster?.broadcast(sessionID, { type: "complete", data: content });
|
|
3631
|
+
}
|
|
3632
|
+
broadcastError(sessionID, err) {
|
|
3633
|
+
this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
|
|
3637
|
+
// src/appkit/daemon_session.ts
|
|
3638
|
+
init_client();
|
|
3639
|
+
init_config();
|
|
3640
|
+
init_errors();
|
|
3641
|
+
init_stream_terminal();
|
|
3642
|
+
|
|
3643
|
+
// src/appkit/chunk_filter.ts
|
|
3644
|
+
var MSG_PAIR_LEN = 2;
|
|
3645
|
+
function updatesChunkIsNoop(data) {
|
|
3646
|
+
if (!data || typeof data !== "object") return true;
|
|
3647
|
+
return !("__interrupt__" in data);
|
|
3648
|
+
}
|
|
3649
|
+
function wireBody(msg) {
|
|
3650
|
+
for (const key of ["kwargs", "data"]) {
|
|
3651
|
+
const nested = msg[key];
|
|
3652
|
+
if (nested && typeof nested === "object") return nested;
|
|
3653
|
+
}
|
|
3654
|
+
return msg;
|
|
3655
|
+
}
|
|
3656
|
+
function dictHasToolInvocation(msg) {
|
|
3657
|
+
const body = wireBody(msg);
|
|
3658
|
+
if (body.tool_calls || body.tool_call_chunks) return true;
|
|
3659
|
+
for (const key of ["content", "content_blocks"]) {
|
|
3660
|
+
const raw = body[key];
|
|
3661
|
+
if (Array.isArray(raw)) {
|
|
3662
|
+
for (const item of raw) {
|
|
3663
|
+
if (item && typeof item === "object" && ["tool_call", "tool_call_chunk", "tool_use"].includes(
|
|
3664
|
+
String(item.type ?? "")
|
|
3665
|
+
)) {
|
|
3666
|
+
return true;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
return false;
|
|
3672
|
+
}
|
|
3673
|
+
function plainText(msg) {
|
|
3674
|
+
const body = wireBody(msg);
|
|
3675
|
+
const content = body.content ?? msg.content;
|
|
3676
|
+
if (typeof content === "string") return content;
|
|
3677
|
+
if (Array.isArray(content)) {
|
|
3678
|
+
const parts = [];
|
|
3679
|
+
for (const block of content) {
|
|
3680
|
+
if (typeof block === "string") parts.push(block);
|
|
3681
|
+
else if (block && typeof block === "object") {
|
|
3682
|
+
const text = block.text;
|
|
3683
|
+
if (typeof text === "string") parts.push(text);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
return parts.join("");
|
|
3687
|
+
}
|
|
3688
|
+
return "";
|
|
3689
|
+
}
|
|
3690
|
+
function messageChunkIsNonActionable(data) {
|
|
3691
|
+
if (!Array.isArray(data) || data.length !== MSG_PAIR_LEN) return false;
|
|
3692
|
+
const msg = data[0];
|
|
3693
|
+
if (msg === null || msg === void 0) return true;
|
|
3694
|
+
if (!msg || typeof msg !== "object") return false;
|
|
3695
|
+
const m = msg;
|
|
3696
|
+
const body = wireBody(m);
|
|
3697
|
+
const raw = String(body.type ?? m.type ?? "");
|
|
3698
|
+
if (raw === "tool" || raw === "ToolMessage" || raw.endsWith("ToolMessage")) return false;
|
|
3699
|
+
if (dictHasToolInvocation(m)) return false;
|
|
3700
|
+
if (body.phase || m.phase) return false;
|
|
3701
|
+
return !plainText(m).trim();
|
|
3702
|
+
}
|
|
3703
|
+
function shouldDropStreamChunkEarly(_namespace, mode, data) {
|
|
3704
|
+
if (mode === "updates") return updatesChunkIsNoop(data);
|
|
3705
|
+
if (mode === "messages") return messageChunkIsNonActionable(data);
|
|
3706
|
+
return false;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
// src/appkit/events.ts
|
|
3710
|
+
function unwrapNext(event) {
|
|
3711
|
+
if (!event || typeof event !== "object") return event;
|
|
3712
|
+
if (event.type !== "next") return event;
|
|
3713
|
+
const payload = event.payload;
|
|
3714
|
+
if (!payload || typeof payload !== "object") return event;
|
|
3715
|
+
const data = payload.data;
|
|
3716
|
+
return data && typeof data === "object" ? data : event;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/appkit/observability.ts
|
|
3720
|
+
var TurnEventStats = class {
|
|
3721
|
+
total = 0;
|
|
3722
|
+
messages = 0;
|
|
3723
|
+
updates = 0;
|
|
3724
|
+
custom = 0;
|
|
3725
|
+
skipped = 0;
|
|
3726
|
+
filteredEarly = 0;
|
|
3727
|
+
toolCalls = 0;
|
|
3728
|
+
toolResults = 0;
|
|
3729
|
+
textChunks = 0;
|
|
3730
|
+
heartbeatsDropped = 0;
|
|
3731
|
+
postIdleDrained = 0;
|
|
3732
|
+
inboundDropped = 0;
|
|
3733
|
+
};
|
|
3734
|
+
|
|
3735
|
+
// src/appkit/daemon_session.ts
|
|
3736
|
+
var DEFAULT_POST_IDLE_DRAIN_MS = 500;
|
|
3737
|
+
var DaemonSession = class {
|
|
3738
|
+
wsUrl;
|
|
3739
|
+
workspace;
|
|
3740
|
+
streamDelivery;
|
|
3741
|
+
client;
|
|
3742
|
+
rpcClient;
|
|
3743
|
+
loopId = null;
|
|
3744
|
+
readBusy = false;
|
|
3745
|
+
rpcBusy = false;
|
|
3746
|
+
rpcConnected = false;
|
|
3747
|
+
streaming = false;
|
|
3748
|
+
postIdleDrainDeadlineMs;
|
|
3749
|
+
closed = false;
|
|
3750
|
+
earlyDropFn;
|
|
3751
|
+
statsFactory;
|
|
3752
|
+
config;
|
|
3753
|
+
turnEventStats;
|
|
3754
|
+
lastTurnEndState = null;
|
|
3755
|
+
lastTurnCancellationSeen = false;
|
|
3756
|
+
lastTurnErrorMessage = null;
|
|
3757
|
+
constructor(wsUrl, opts = {}) {
|
|
3758
|
+
this.wsUrl = wsUrl;
|
|
3759
|
+
this.workspace = opts.workspace;
|
|
3760
|
+
this.streamDelivery = opts.streamDelivery ?? "adaptive";
|
|
3761
|
+
this.config = opts.config ?? defaultConfig();
|
|
3762
|
+
this.client = new Client(wsUrl, this.config);
|
|
3763
|
+
this.rpcClient = new Client(wsUrl, this.config);
|
|
3764
|
+
this.postIdleDrainDeadlineMs = opts.postIdleDrainDeadlineMs && opts.postIdleDrainDeadlineMs > 0 ? opts.postIdleDrainDeadlineMs : DEFAULT_POST_IDLE_DRAIN_MS;
|
|
3765
|
+
this.earlyDropFn = opts.earlyDropFn ?? shouldDropStreamChunkEarly;
|
|
3766
|
+
this.statsFactory = opts.statsFactory ?? (() => new TurnEventStats());
|
|
3767
|
+
this.turnEventStats = this.statsFactory();
|
|
3768
|
+
}
|
|
3769
|
+
get streamClient() {
|
|
3770
|
+
return this.client;
|
|
3771
|
+
}
|
|
3772
|
+
get rpcSideClient() {
|
|
3773
|
+
return this.rpcClient;
|
|
3774
|
+
}
|
|
3775
|
+
get activeLoopId() {
|
|
3776
|
+
return this.loopId;
|
|
3777
|
+
}
|
|
3778
|
+
resolveStreamDeliveryMode() {
|
|
3779
|
+
const delivery = this.streamDelivery;
|
|
3780
|
+
if (typeof delivery === "function") return String(delivery() || "adaptive");
|
|
3781
|
+
return String(delivery || "adaptive");
|
|
3782
|
+
}
|
|
3783
|
+
get streamDeliveryMode() {
|
|
3784
|
+
return this.resolveStreamDeliveryMode();
|
|
3785
|
+
}
|
|
3786
|
+
shouldDrop(namespace, mode, data) {
|
|
3787
|
+
return Boolean(this.earlyDropFn(namespace, mode, data));
|
|
3788
|
+
}
|
|
3789
|
+
async connect(resumeLoopId) {
|
|
3790
|
+
await connectWithRetries(this.client);
|
|
3791
|
+
return this.bootstrapLoop(resumeLoopId ?? null);
|
|
3792
|
+
}
|
|
3793
|
+
async bootstrapLoop(resumeLoopId) {
|
|
3794
|
+
const loopNew = this.workspace ? { client_workspace: this.workspace, workspace: this.workspace } : void 0;
|
|
3795
|
+
const loopId = await bootstrapLoopSession(this.client, resumeLoopId, this.config, loopNew);
|
|
3796
|
+
this.loopId = loopId;
|
|
3797
|
+
return { type: "status", loop_id: loopId, state: "ready" };
|
|
3798
|
+
}
|
|
3799
|
+
async newLoop() {
|
|
3800
|
+
return this.bootstrapLoop(null);
|
|
3801
|
+
}
|
|
3802
|
+
async switchLoop(loopId) {
|
|
3803
|
+
return this.bootstrapLoop(loopId);
|
|
3804
|
+
}
|
|
3805
|
+
async ensureConnected() {
|
|
3806
|
+
if (this.client.isConnected() && !this.client.isDisconnected()) return;
|
|
3807
|
+
let resumeLoopId = this.loopId;
|
|
3808
|
+
if (this.rpcConnected) {
|
|
3809
|
+
this.rpcClient.close();
|
|
3810
|
+
this.rpcConnected = false;
|
|
3811
|
+
}
|
|
3812
|
+
try {
|
|
3813
|
+
await this.client.reconnect();
|
|
3814
|
+
} catch {
|
|
3815
|
+
this.client.close();
|
|
3816
|
+
await connectWithRetries(this.client);
|
|
3817
|
+
}
|
|
3818
|
+
if (resumeLoopId) {
|
|
3819
|
+
try {
|
|
3820
|
+
await this.client.reattachAndProbe(resumeLoopId);
|
|
3821
|
+
this.loopId = resumeLoopId;
|
|
3822
|
+
return;
|
|
3823
|
+
} catch (err) {
|
|
3824
|
+
if (!(err instanceof StaleLoopError)) throw err;
|
|
3825
|
+
resumeLoopId = null;
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
await this.bootstrapLoop(resumeLoopId);
|
|
3829
|
+
}
|
|
3830
|
+
async close() {
|
|
3831
|
+
if (this.closed) return;
|
|
3832
|
+
this.closed = true;
|
|
3833
|
+
this.client.close();
|
|
3834
|
+
this.rpcClient.close();
|
|
3835
|
+
this.rpcConnected = false;
|
|
3836
|
+
}
|
|
3837
|
+
async detach() {
|
|
3838
|
+
if (!this.client.isConnected()) return;
|
|
3839
|
+
try {
|
|
3840
|
+
await this.client.notify("disconnect", {});
|
|
3841
|
+
} catch {
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
async sendTurn(text, options) {
|
|
3845
|
+
if (!this.loopId) throw new Error("No active loop session");
|
|
3846
|
+
await this.client.sendInput(text, {
|
|
3847
|
+
loopID: this.loopId,
|
|
3848
|
+
autonomous: options?.autonomous,
|
|
3849
|
+
maxIterations: options?.maxIterations,
|
|
3850
|
+
subagent: options?.preferredSubagent,
|
|
3851
|
+
model: options?.model,
|
|
3852
|
+
modelParams: options?.modelParams,
|
|
3853
|
+
attachments: options?.attachments,
|
|
3854
|
+
clarificationMode: options?.clarificationMode,
|
|
3855
|
+
clarificationAnswer: options?.clarificationAnswer,
|
|
3856
|
+
intentHint: options?.intentHint
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
async cancelActiveTurn() {
|
|
3860
|
+
await this.client.notify("slash_command", { cmd: "/cancel" });
|
|
3861
|
+
}
|
|
3862
|
+
async *drainStreamEventsAfterIdle(expectedLoopId) {
|
|
3863
|
+
const deadline = Date.now() + this.postIdleDrainDeadlineMs;
|
|
3864
|
+
let exp = expectedLoopId;
|
|
3865
|
+
while (Date.now() < deadline) {
|
|
3866
|
+
const event = await this.client.readEventWithTimeout(250);
|
|
3867
|
+
if (!event) break;
|
|
3868
|
+
let frame = event;
|
|
3869
|
+
let eventType = String(frame.type ?? "");
|
|
3870
|
+
if (eventType === "next") {
|
|
3871
|
+
frame = unwrapNext(frame) ?? frame;
|
|
3872
|
+
eventType = String(frame.type ?? "");
|
|
3873
|
+
}
|
|
3874
|
+
const eventLoopId = frame.loop_id;
|
|
3875
|
+
if (exp && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== exp) {
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
if (eventType === "error") {
|
|
3879
|
+
const errObj = frame.error ?? {};
|
|
3880
|
+
throw new Error(String(errObj.message || frame.message || "daemon error"));
|
|
3881
|
+
}
|
|
3882
|
+
if (eventType === "status") {
|
|
3883
|
+
const loopEv = frame.loop_id;
|
|
3884
|
+
if (typeof loopEv === "string" && loopEv) {
|
|
3885
|
+
this.loopId = loopEv;
|
|
3886
|
+
exp = loopEv;
|
|
3887
|
+
}
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
if (eventType !== "event") continue;
|
|
3891
|
+
const data = frame.data;
|
|
3892
|
+
const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
|
|
3893
|
+
const mode = String(frame.mode ?? "");
|
|
3894
|
+
if (this.shouldDrop(namespace, mode, data)) {
|
|
3895
|
+
this.turnEventStats.filteredEarly += 1;
|
|
3896
|
+
continue;
|
|
3897
|
+
}
|
|
3898
|
+
this.turnEventStats.postIdleDrained += 1;
|
|
3899
|
+
yield [namespace, mode, data];
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
async withRpcLock(fn) {
|
|
3903
|
+
while (this.rpcBusy) {
|
|
3904
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
3905
|
+
}
|
|
3906
|
+
this.rpcBusy = true;
|
|
3907
|
+
try {
|
|
3908
|
+
return await fn();
|
|
3909
|
+
} finally {
|
|
3910
|
+
this.rpcBusy = false;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
async ensureRpcConnected() {
|
|
3914
|
+
if (this.rpcConnected && this.rpcClient.isConnected()) return;
|
|
3915
|
+
await connectWithRetries(this.rpcClient);
|
|
3916
|
+
this.rpcConnected = true;
|
|
3917
|
+
}
|
|
3918
|
+
async listLoops(_limit = 20) {
|
|
3919
|
+
return this.withRpcLock(async () => {
|
|
3920
|
+
await this.ensureRpcConnected();
|
|
3921
|
+
return this.rpcClient.listLoops(15e3);
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
async fetchLoopCards(loopId) {
|
|
3925
|
+
const lid = String(loopId || "").trim();
|
|
3926
|
+
if (!lid) return { cards: [], seq: 0, contextTokens: 0, success: false };
|
|
3927
|
+
return this.withRpcLock(async () => {
|
|
3928
|
+
await this.ensureRpcConnected();
|
|
3929
|
+
try {
|
|
3930
|
+
const resp = await this.rpcClient.fetchLoopCards(lid, 3e4);
|
|
3931
|
+
const rawCards = resp.cards;
|
|
3932
|
+
return {
|
|
3933
|
+
cards: Array.isArray(rawCards) ? rawCards : [],
|
|
3934
|
+
seq: Number(resp.seq ?? 0),
|
|
3935
|
+
contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
|
|
3936
|
+
success: true
|
|
3937
|
+
};
|
|
3938
|
+
} catch {
|
|
3939
|
+
return { cards: [], seq: 0, contextTokens: 0, success: false };
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3942
|
+
}
|
|
3943
|
+
async fetchLoopHistory(loopId) {
|
|
3944
|
+
const lid = String(loopId || "").trim();
|
|
3945
|
+
if (!lid) {
|
|
3946
|
+
return { goals: [], liveCards: [], liveGoalIndex: null, contextTokens: 0, success: false };
|
|
3947
|
+
}
|
|
3948
|
+
return this.withRpcLock(async () => {
|
|
3949
|
+
await this.ensureRpcConnected();
|
|
3950
|
+
try {
|
|
3951
|
+
const resp = await this.rpcClient.fetchLoopHistory(lid, 3e4);
|
|
3952
|
+
const liveGoalIndex = resp.live_goal_index;
|
|
3953
|
+
return {
|
|
3954
|
+
goals: Array.isArray(resp.goals) ? resp.goals : [],
|
|
3955
|
+
liveCards: Array.isArray(resp.live_cards) ? resp.live_cards : [],
|
|
3956
|
+
liveGoalIndex: typeof liveGoalIndex === "number" ? liveGoalIndex : null,
|
|
3957
|
+
contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
|
|
3958
|
+
success: Boolean(resp.success ?? true)
|
|
3959
|
+
};
|
|
3960
|
+
} catch {
|
|
3961
|
+
return {
|
|
3962
|
+
goals: [],
|
|
3963
|
+
liveCards: [],
|
|
3964
|
+
liveGoalIndex: null,
|
|
3965
|
+
contextTokens: 0,
|
|
3966
|
+
success: false
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
async fetchConversationLog(loopId, opts = {}) {
|
|
3972
|
+
const lid = String(loopId || "").trim();
|
|
3973
|
+
if (!lid) return [];
|
|
3974
|
+
return this.withRpcLock(async () => {
|
|
3975
|
+
await this.ensureRpcConnected();
|
|
3976
|
+
const resp = await this.rpcClient.getLoopMessages(
|
|
3977
|
+
lid,
|
|
3978
|
+
opts.limit ?? 100,
|
|
3979
|
+
opts.offset ?? 0,
|
|
3980
|
+
opts.includeEvents ?? false
|
|
3981
|
+
);
|
|
3982
|
+
const raw = resp.messages;
|
|
3983
|
+
if (!Array.isArray(raw)) return [];
|
|
3984
|
+
return raw.filter((m) => !!m && typeof m === "object");
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
async *iterTurnChunks(opts = {}) {
|
|
3988
|
+
this.turnEventStats = this.statsFactory();
|
|
3989
|
+
this.lastTurnEndState = null;
|
|
3990
|
+
this.lastTurnCancellationSeen = false;
|
|
3991
|
+
this.lastTurnErrorMessage = null;
|
|
3992
|
+
let queryStarted = false;
|
|
3993
|
+
let expectedLoopId = this.loopId;
|
|
3994
|
+
let streamPayloadSeen = false;
|
|
3995
|
+
let turnProgressSeen = false;
|
|
3996
|
+
this.streaming = true;
|
|
3997
|
+
const absoluteDeadline = opts.maxWaitMs !== void 0 && opts.maxWaitMs > 0 ? Date.now() + opts.maxWaitMs : null;
|
|
3998
|
+
this.client.peelStalePendingControlEvents();
|
|
3999
|
+
while (this.readBusy) {
|
|
4000
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
4001
|
+
}
|
|
4002
|
+
this.readBusy = true;
|
|
4003
|
+
try {
|
|
4004
|
+
while (true) {
|
|
4005
|
+
if (absoluteDeadline !== null && Date.now() >= absoluteDeadline) {
|
|
4006
|
+
throw new Error(
|
|
4007
|
+
`Turn timed out after ${opts.maxWaitMs}ms (loop=${expectedLoopId ?? "?"})`
|
|
4008
|
+
);
|
|
4009
|
+
}
|
|
4010
|
+
const event = await this.client.readEvent();
|
|
4011
|
+
if (!event) {
|
|
4012
|
+
if (queryStarted && !this.client.isConnectionAlive()) {
|
|
4013
|
+
this.lastTurnEndState = "connection_lost";
|
|
4014
|
+
throw new Error("Daemon connection lost");
|
|
4015
|
+
}
|
|
4016
|
+
break;
|
|
4017
|
+
}
|
|
4018
|
+
let frame = event;
|
|
4019
|
+
let eventType = String(frame.type ?? "");
|
|
4020
|
+
if (eventType === "next") {
|
|
4021
|
+
frame = unwrapNext(frame) ?? frame;
|
|
4022
|
+
eventType = String(frame.type ?? "");
|
|
4023
|
+
}
|
|
4024
|
+
const eventLoopId = frame.loop_id;
|
|
4025
|
+
if (expectedLoopId && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== expectedLoopId) {
|
|
4026
|
+
continue;
|
|
4027
|
+
}
|
|
4028
|
+
if (eventType === "error") {
|
|
4029
|
+
const errObj = frame.error ?? {};
|
|
4030
|
+
throw new Error(String(errObj.message || frame.message || "daemon error"));
|
|
4031
|
+
}
|
|
4032
|
+
if (eventType === "status") {
|
|
4033
|
+
const loopEv = frame.loop_id;
|
|
4034
|
+
if (typeof loopEv === "string" && loopEv) {
|
|
4035
|
+
this.loopId = loopEv;
|
|
4036
|
+
expectedLoopId = loopEv;
|
|
4037
|
+
}
|
|
4038
|
+
const state = String(frame.state ?? "");
|
|
4039
|
+
if (state === "running") {
|
|
4040
|
+
queryStarted = true;
|
|
4041
|
+
} else if (queryStarted && state === "stopped") {
|
|
4042
|
+
this.lastTurnEndState = state;
|
|
4043
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
4044
|
+
break;
|
|
4045
|
+
} else if (queryStarted && state === "idle") {
|
|
4046
|
+
if (!streamPayloadSeen && !this.lastTurnCancellationSeen) continue;
|
|
4047
|
+
this.lastTurnEndState = state;
|
|
4048
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
4049
|
+
break;
|
|
4050
|
+
}
|
|
4051
|
+
continue;
|
|
4052
|
+
}
|
|
4053
|
+
if (eventType === "command_response") {
|
|
4054
|
+
const content = String(frame.content ?? "");
|
|
4055
|
+
if (content.includes("Cancellation requested")) {
|
|
4056
|
+
this.lastTurnCancellationSeen = true;
|
|
4057
|
+
}
|
|
4058
|
+
continue;
|
|
4059
|
+
}
|
|
4060
|
+
if (eventType !== "event") continue;
|
|
4061
|
+
const data = frame.data;
|
|
4062
|
+
const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
|
|
4063
|
+
const mode = String(frame.mode ?? "");
|
|
4064
|
+
if (this.shouldDrop(namespace, mode, data)) {
|
|
4065
|
+
this.turnEventStats.filteredEarly += 1;
|
|
4066
|
+
continue;
|
|
4067
|
+
}
|
|
4068
|
+
if (mode === "custom" && isTurnEndCustomData(data)) {
|
|
4069
|
+
if (!queryStarted || !turnProgressSeen) continue;
|
|
4070
|
+
}
|
|
4071
|
+
streamPayloadSeen = true;
|
|
4072
|
+
if (isTurnProgressChunk(mode, data)) turnProgressSeen = true;
|
|
4073
|
+
yield [namespace, mode, data];
|
|
4074
|
+
if (mode === "custom" && isTurnEndCustomData(data)) {
|
|
4075
|
+
const customType = String(data.type ?? "").trim();
|
|
4076
|
+
this.lastTurnEndState = customType === STREAM_END ? "stream_end" : "completed";
|
|
4077
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
4078
|
+
break;
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
} catch (exc) {
|
|
4082
|
+
this.lastTurnErrorMessage = String(exc);
|
|
4083
|
+
throw exc;
|
|
4084
|
+
} finally {
|
|
4085
|
+
this.streaming = false;
|
|
4086
|
+
this.readBusy = false;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
};
|
|
1069
4090
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1070
4091
|
0 && (module.exports = {
|
|
4092
|
+
CLIENT_VERSION,
|
|
4093
|
+
ChatEventTerminal,
|
|
1071
4094
|
Client,
|
|
4095
|
+
CommandClient,
|
|
1072
4096
|
ConnectionError,
|
|
4097
|
+
ConnectionPool,
|
|
4098
|
+
DEFAULT_CLIENT_CAPABILITIES,
|
|
4099
|
+
DEFAULT_DELIVERABLE_PHASES,
|
|
4100
|
+
DEFAULT_POST_IDLE_DRAIN_MS,
|
|
4101
|
+
DEFAULT_THINKING_STEP_EVENTS,
|
|
1073
4102
|
DaemonError,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
4103
|
+
DaemonSession,
|
|
4104
|
+
DisconnectCause,
|
|
4105
|
+
ErrIdleTimeout,
|
|
4106
|
+
ErrPoolExhausted,
|
|
4107
|
+
ErrQueryBusy,
|
|
4108
|
+
ErrQueryTimeout,
|
|
4109
|
+
EventAutopilotGoalCompleted,
|
|
4110
|
+
EventAutopilotGoalCreated,
|
|
4111
|
+
EventAutopilotGoalProgress,
|
|
4112
|
+
EventAutopilotGoalStatus,
|
|
4113
|
+
EventAutopilotWorkerAssigned,
|
|
4114
|
+
EventAutopilotWorkerUnassigned,
|
|
4115
|
+
EventCardCreated,
|
|
4116
|
+
EventCardReplayBegin,
|
|
4117
|
+
EventCardReplayEnd,
|
|
4118
|
+
EventClassifier,
|
|
1079
4119
|
EventExploreCompleted,
|
|
1080
4120
|
EventExploreMilestone,
|
|
1081
4121
|
EventExploreStarted,
|
|
@@ -1087,6 +4127,14 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
|
1087
4127
|
EventMessageSent,
|
|
1088
4128
|
EventPlanCreated,
|
|
1089
4129
|
EventReplayComplete,
|
|
4130
|
+
EventStrangeLoopCompleted,
|
|
4131
|
+
EventStrangeLoopContextCompacted,
|
|
4132
|
+
EventStrangeLoopPlanDecision,
|
|
4133
|
+
EventStrangeLoopReasoned,
|
|
4134
|
+
EventStrangeLoopStarted,
|
|
4135
|
+
EventStrangeLoopStepCompleted,
|
|
4136
|
+
EventStrangeLoopStepQueued,
|
|
4137
|
+
EventStrangeLoopStepStarted,
|
|
1090
4138
|
EventStreamToolCallUpdate,
|
|
1091
4139
|
EventTacitusCompleted,
|
|
1092
4140
|
EventTacitusGatherSummary,
|
|
@@ -1095,31 +4143,76 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
|
1095
4143
|
EventToolCompleted,
|
|
1096
4144
|
EventToolError,
|
|
1097
4145
|
EventToolStarted,
|
|
4146
|
+
INTENT_HINT_EMBED,
|
|
4147
|
+
INTENT_HINT_IMAGE_TO_TEXT,
|
|
4148
|
+
INTENT_HINT_OCR,
|
|
4149
|
+
INTENT_HINT_TEXT_COMPLETION,
|
|
4150
|
+
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
4151
|
+
PROTO_VERSION,
|
|
4152
|
+
PooledConn,
|
|
4153
|
+
QueryGate,
|
|
4154
|
+
REMOVED_INTENT_HINTS,
|
|
4155
|
+
ReconnectError,
|
|
4156
|
+
SSEBroadcaster,
|
|
4157
|
+
STREAM_END,
|
|
4158
|
+
StaleLoopError,
|
|
4159
|
+
StreamCloseFail,
|
|
4160
|
+
StreamCloseSoftComplete,
|
|
1098
4161
|
TimeoutError,
|
|
4162
|
+
TimeoutPolicy,
|
|
4163
|
+
TurnEventStats,
|
|
4164
|
+
TurnRunner,
|
|
1099
4165
|
VerbosityTier,
|
|
4166
|
+
authenticate,
|
|
1100
4167
|
bootstrapLoopSession,
|
|
1101
4168
|
checkDaemonStatus,
|
|
1102
4169
|
classifyEventVerbosity,
|
|
4170
|
+
compactAttachments,
|
|
4171
|
+
compactImageAttachment,
|
|
1103
4172
|
connectWithRetries,
|
|
4173
|
+
connectedWebsocket,
|
|
4174
|
+
connectionInitEnvelope,
|
|
1104
4175
|
decodeMessage,
|
|
1105
4176
|
defaultConfig,
|
|
4177
|
+
defaultPoolConfig,
|
|
4178
|
+
disconnectCauseName,
|
|
4179
|
+
disconnectEnvelope,
|
|
1106
4180
|
encodeMessage,
|
|
1107
4181
|
extractSootheLoopID,
|
|
4182
|
+
extractThinkingStep,
|
|
1108
4183
|
fetchConfigSection,
|
|
4184
|
+
fetchLoopCards,
|
|
4185
|
+
fetchLoopHistory,
|
|
4186
|
+
fetchLoopMessages,
|
|
1109
4187
|
fetchSkillsCatalog,
|
|
4188
|
+
idleTimeoutForTurn,
|
|
4189
|
+
inboundNeedsDeliveryAck,
|
|
4190
|
+
inputMessageForLoop,
|
|
1110
4191
|
isCompletionEvent,
|
|
1111
4192
|
isDaemonLive,
|
|
1112
4193
|
isSubagentProgressEvent,
|
|
4194
|
+
isTurnEndCustomData,
|
|
4195
|
+
isTurnProgressChunk,
|
|
1113
4196
|
isValidVerbosityLevel,
|
|
1114
4197
|
loadConfigFromEnv,
|
|
1115
4198
|
newLoopInputMessage,
|
|
1116
4199
|
newLoopNewMessage,
|
|
1117
4200
|
newLoopSubscribeMessage,
|
|
1118
4201
|
newRequestID,
|
|
4202
|
+
notificationEnvelope,
|
|
1119
4203
|
parseNamespace,
|
|
4204
|
+
pingEnvelope,
|
|
4205
|
+
pongEnvelope,
|
|
4206
|
+
protocol1Rpc,
|
|
4207
|
+
refreshAuthToken,
|
|
4208
|
+
requestDaemonConfigReload,
|
|
1120
4209
|
requestDaemonShutdown,
|
|
4210
|
+
requestEnvelope,
|
|
1121
4211
|
shouldShow,
|
|
1122
4212
|
splitWirePayload,
|
|
4213
|
+
subscribeEnvelope,
|
|
4214
|
+
unsubscribeEnvelope,
|
|
4215
|
+
validateLoopInputIntentHint,
|
|
1123
4216
|
waitDaemonReady,
|
|
1124
4217
|
waitLoopStatusWithID,
|
|
1125
4218
|
waitSubscriptionConfirmed
|