@mirasoth/soothe-client 0.2.1 → 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-AQZACDIC.js → chunk-U6RMINYV.js} +489 -5
- package/dist/chunk-U6RMINYV.js.map +1 -0
- package/dist/client-UNPC32NQ.js +7 -0
- package/dist/index.cjs +1402 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +268 -99
- package/dist/index.d.ts +268 -99
- package/dist/index.js +939 -258
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/dist/chunk-AQZACDIC.js.map +0 -1
- package/dist/client-CB6WKQYW.js +0 -7
- /package/dist/{client-CB6WKQYW.js.map → client-UNPC32NQ.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -6,17 +6,55 @@ import {
|
|
|
6
6
|
DEFAULT_DELIVERABLE_PHASES,
|
|
7
7
|
DaemonError,
|
|
8
8
|
DisconnectCause,
|
|
9
|
+
EventAutopilotGoalCompleted,
|
|
10
|
+
EventAutopilotGoalCreated,
|
|
11
|
+
EventAutopilotGoalProgress,
|
|
12
|
+
EventAutopilotGoalStatus,
|
|
13
|
+
EventAutopilotWorkerAssigned,
|
|
14
|
+
EventAutopilotWorkerUnassigned,
|
|
15
|
+
EventCardCreated,
|
|
16
|
+
EventCardReplayBegin,
|
|
17
|
+
EventCardReplayEnd,
|
|
18
|
+
EventExploreCompleted,
|
|
19
|
+
EventExploreMilestone,
|
|
20
|
+
EventExploreStarted,
|
|
21
|
+
EventExploreStepCompleted,
|
|
22
|
+
EventFinalReport,
|
|
23
|
+
EventGeneralFailed,
|
|
24
|
+
EventLoopReattachedWire,
|
|
25
|
+
EventMessageReceived,
|
|
26
|
+
EventMessageSent,
|
|
27
|
+
EventPlanCreated,
|
|
28
|
+
EventReplayComplete,
|
|
29
|
+
EventStrangeLoopCompleted,
|
|
30
|
+
EventStrangeLoopContextCompacted,
|
|
31
|
+
EventStrangeLoopPlanDecision,
|
|
32
|
+
EventStrangeLoopReasoned,
|
|
33
|
+
EventStrangeLoopStarted,
|
|
34
|
+
EventStrangeLoopStepCompleted,
|
|
35
|
+
EventStrangeLoopStepQueued,
|
|
36
|
+
EventStrangeLoopStepStarted,
|
|
37
|
+
EventStreamToolCallUpdate,
|
|
38
|
+
EventTacitusCompleted,
|
|
39
|
+
EventTacitusGatherSummary,
|
|
40
|
+
EventTacitusStarted,
|
|
41
|
+
EventToolCallUpdatesBatch,
|
|
42
|
+
EventToolCompleted,
|
|
43
|
+
EventToolError,
|
|
44
|
+
EventToolStarted,
|
|
9
45
|
INTENT_HINT_EMBED,
|
|
10
46
|
INTENT_HINT_IMAGE_TO_TEXT,
|
|
11
47
|
INTENT_HINT_OCR,
|
|
12
48
|
INTENT_HINT_TEXT_COMPLETION,
|
|
13
49
|
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
14
|
-
Multiplexer,
|
|
15
50
|
PROTO_VERSION,
|
|
16
51
|
REMOVED_INTENT_HINTS,
|
|
17
52
|
ReconnectError,
|
|
53
|
+
STREAM_END,
|
|
18
54
|
StaleLoopError,
|
|
19
55
|
TimeoutError,
|
|
56
|
+
VerbosityTier,
|
|
57
|
+
classifyEventVerbosity,
|
|
20
58
|
connectionInitEnvelope,
|
|
21
59
|
decodeMessage,
|
|
22
60
|
defaultConfig,
|
|
@@ -24,168 +62,182 @@ import {
|
|
|
24
62
|
disconnectEnvelope,
|
|
25
63
|
encodeMessage,
|
|
26
64
|
extractSootheLoopID,
|
|
65
|
+
inboundNeedsDeliveryAck,
|
|
66
|
+
isCompletionEvent,
|
|
67
|
+
isSubagentProgressEvent,
|
|
68
|
+
isTurnEndCustomData,
|
|
69
|
+
isTurnProgressChunk,
|
|
70
|
+
isValidVerbosityLevel,
|
|
27
71
|
loadConfigFromEnv,
|
|
28
72
|
newLoopInputMessage,
|
|
29
73
|
newLoopNewMessage,
|
|
30
74
|
newLoopSubscribeMessage,
|
|
31
75
|
newRequestID,
|
|
32
76
|
notificationEnvelope,
|
|
77
|
+
parseNamespace,
|
|
33
78
|
pingEnvelope,
|
|
34
79
|
pongEnvelope,
|
|
35
80
|
requestEnvelope,
|
|
81
|
+
shouldShow,
|
|
36
82
|
splitWirePayload,
|
|
37
83
|
subscribeEnvelope,
|
|
38
84
|
unsubscribeEnvelope,
|
|
39
85
|
validateLoopInputIntentHint
|
|
40
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-U6RMINYV.js";
|
|
41
87
|
|
|
42
|
-
// src/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return false;
|
|
88
|
+
// src/session.ts
|
|
89
|
+
async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
|
|
90
|
+
const cfg = config ?? defaultConfig();
|
|
91
|
+
let loopId = (resumeLoopId ?? "").trim();
|
|
92
|
+
if (!loopId) {
|
|
93
|
+
const env = newLoopNewMessage(loopNew);
|
|
94
|
+
const newResp = await client.requestResponse(
|
|
95
|
+
env.method,
|
|
96
|
+
env.params ?? {},
|
|
97
|
+
"loop_new",
|
|
98
|
+
cfg.loopStatusTimeout
|
|
99
|
+
);
|
|
100
|
+
loopId = String(newResp.loop_id ?? "").trim();
|
|
101
|
+
if (!loopId) {
|
|
102
|
+
throw new Error("loop_new response missing loop_id");
|
|
103
|
+
}
|
|
59
104
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
105
|
+
await client.subscribe(
|
|
106
|
+
"loop_events",
|
|
107
|
+
{ loop_id: loopId, verbosity: cfg.verbosityLevel },
|
|
108
|
+
cfg.subscriptionTimeout
|
|
109
|
+
);
|
|
110
|
+
return loopId;
|
|
65
111
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
var EventCardReplayEnd = "card.replay_end";
|
|
81
|
-
var EventToolStarted = "soothe.tool.execution.started";
|
|
82
|
-
var EventToolCompleted = "soothe.tool.execution.completed";
|
|
83
|
-
var EventToolError = "soothe.tool.execution.error";
|
|
84
|
-
var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
|
|
85
|
-
var EventToolCallUpdatesBatch = "tool_call_updates_batch";
|
|
86
|
-
var EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
|
|
87
|
-
var EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
|
|
88
|
-
var EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
|
|
89
|
-
var EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
|
|
90
|
-
var EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
|
|
91
|
-
var EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
|
|
92
|
-
var EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
|
|
93
|
-
var EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
|
|
94
|
-
var EventMessageReceived = "soothe.protocol.message.received";
|
|
95
|
-
var EventMessageSent = "soothe.protocol.message.sent";
|
|
96
|
-
var EventFinalReport = "soothe.output.autonomous.final_report.reported";
|
|
97
|
-
var EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
|
|
98
|
-
var EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
|
|
99
|
-
var EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
|
|
100
|
-
var EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
|
|
101
|
-
var EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
|
|
102
|
-
var EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
|
|
103
|
-
var EventGeneralFailed = "soothe.error.general.failed";
|
|
104
|
-
function parseNamespace(ns) {
|
|
105
|
-
const parts = splitNamespace(ns);
|
|
106
|
-
if (parts.length < 4 || parts[0] !== "soothe") {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
if (parts[1] === "internal") {
|
|
110
|
-
return null;
|
|
112
|
+
async function waitDaemonReady(client, timeout) {
|
|
113
|
+
if (client.isConnected()) return;
|
|
114
|
+
const deadline = Date.now() + timeout;
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
const remaining = deadline - Date.now();
|
|
117
|
+
if (remaining <= 0) break;
|
|
118
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
119
|
+
if (ev === null) break;
|
|
120
|
+
if (ev.type === "connection_ack") {
|
|
121
|
+
const result = ev.result ?? {};
|
|
122
|
+
const state = result.readiness_state;
|
|
123
|
+
if (state === "ready") return;
|
|
124
|
+
throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
|
|
125
|
+
}
|
|
111
126
|
}
|
|
112
|
-
|
|
127
|
+
throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
|
|
113
128
|
}
|
|
114
|
-
function
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
async function waitLoopStatusWithID(client, timeout) {
|
|
130
|
+
const deadline = Date.now() + timeout;
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
const remaining = deadline - Date.now();
|
|
133
|
+
if (remaining <= 0) break;
|
|
134
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
135
|
+
if (ev === null) break;
|
|
136
|
+
if (ev.type === "error") {
|
|
137
|
+
const errObj = ev.error ?? {};
|
|
138
|
+
throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
|
|
139
|
+
}
|
|
140
|
+
if (ev.type === "status") {
|
|
141
|
+
const lid = ev.loop_id;
|
|
142
|
+
if (lid && lid !== "") {
|
|
143
|
+
return ev;
|
|
144
|
+
}
|
|
121
145
|
}
|
|
122
146
|
}
|
|
123
|
-
|
|
124
|
-
return parts;
|
|
147
|
+
throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
|
|
125
148
|
}
|
|
126
|
-
function
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
149
|
+
async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
|
|
150
|
+
const deadline = Date.now() + timeout;
|
|
151
|
+
while (Date.now() < deadline) {
|
|
152
|
+
const remaining = deadline - Date.now();
|
|
153
|
+
if (remaining <= 0) break;
|
|
154
|
+
const ev = await client.readEventWithTimeout(remaining);
|
|
155
|
+
if (ev === null) break;
|
|
156
|
+
if (ev.type === "next") {
|
|
157
|
+
const payload = ev.payload ?? {};
|
|
158
|
+
const lid = String(payload.loop_id ?? "");
|
|
159
|
+
if (lid === wantLoopID && payload.success === true) return;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ev.type === "error") {
|
|
163
|
+
const errObj = ev.error ?? {};
|
|
164
|
+
throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
|
|
165
|
+
}
|
|
130
166
|
}
|
|
131
|
-
|
|
167
|
+
throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
|
|
132
168
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case "output":
|
|
146
|
-
case "error":
|
|
147
|
-
return 0 /* Quiet */;
|
|
148
|
-
default:
|
|
149
|
-
return 1 /* Normal */;
|
|
169
|
+
async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
170
|
+
const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
|
|
171
|
+
const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
|
|
172
|
+
let lastErr = null;
|
|
173
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
174
|
+
try {
|
|
175
|
+
await client.connect();
|
|
176
|
+
return;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
lastErr = err;
|
|
179
|
+
}
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
150
181
|
}
|
|
182
|
+
throw new Error(
|
|
183
|
+
`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
|
|
184
|
+
);
|
|
151
185
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
186
|
+
|
|
187
|
+
// src/command_client.ts
|
|
188
|
+
var CommandClient = class {
|
|
189
|
+
url;
|
|
190
|
+
timeoutMs;
|
|
191
|
+
config;
|
|
192
|
+
constructor(url, opts) {
|
|
193
|
+
this.url = url;
|
|
194
|
+
this.timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e4;
|
|
195
|
+
this.config = opts?.config ?? defaultConfig();
|
|
161
196
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
async withClient(fn) {
|
|
198
|
+
const client = new Client(this.url, this.config);
|
|
199
|
+
try {
|
|
200
|
+
await connectWithRetries(client, 5, 250);
|
|
201
|
+
return await fn(client);
|
|
202
|
+
} finally {
|
|
203
|
+
client.close();
|
|
204
|
+
}
|
|
166
205
|
}
|
|
167
|
-
|
|
168
|
-
|
|
206
|
+
/** Generic one-shot RPC. */
|
|
207
|
+
async request(method, params = {}) {
|
|
208
|
+
return this.withClient(
|
|
209
|
+
(client) => client.requestResponse(method, params, void 0, this.timeoutMs)
|
|
210
|
+
);
|
|
169
211
|
}
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
function isSubagentProgressEvent(eventType) {
|
|
176
|
-
const parsed = parseNamespace(eventType);
|
|
177
|
-
if (!parsed || parsed.domain !== "subagent") {
|
|
178
|
-
return false;
|
|
212
|
+
async jobCreate(goal, workspace = "") {
|
|
213
|
+
const params = { goal };
|
|
214
|
+
if (workspace) params.workspace = workspace;
|
|
215
|
+
return this.request("job_create", params);
|
|
179
216
|
}
|
|
180
|
-
|
|
181
|
-
}
|
|
217
|
+
async jobStatus(jobId) {
|
|
218
|
+
return this.request("job_status", { job_id: jobId });
|
|
219
|
+
}
|
|
220
|
+
async jobCancel(jobId) {
|
|
221
|
+
return this.request("job_cancel", { job_id: jobId });
|
|
222
|
+
}
|
|
223
|
+
async cronAdd(text, priority = 0) {
|
|
224
|
+
const params = { text };
|
|
225
|
+
if (priority > 0) params.priority = priority;
|
|
226
|
+
return this.request("cron_add", params);
|
|
227
|
+
}
|
|
228
|
+
async cronList(status = "") {
|
|
229
|
+
const params = {};
|
|
230
|
+
if (status) params.status = status;
|
|
231
|
+
return this.request("cron_list", params);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
182
234
|
|
|
183
235
|
// src/helpers.ts
|
|
184
236
|
async function checkDaemonStatus(client, timeout) {
|
|
185
237
|
return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
|
|
186
238
|
}
|
|
187
239
|
async function isDaemonLive(wsURL, timeout) {
|
|
188
|
-
const { Client: Client2 } = await import("./client-
|
|
240
|
+
const { Client: Client2 } = await import("./client-UNPC32NQ.js");
|
|
189
241
|
const t = timeout ?? 5e3;
|
|
190
242
|
const client = new Client2(wsURL, defaultConfig());
|
|
191
243
|
try {
|
|
@@ -259,104 +311,74 @@ async function refreshAuthToken(client, refreshToken, timeout) {
|
|
|
259
311
|
timeout ?? 15e3
|
|
260
312
|
);
|
|
261
313
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"loop_new",
|
|
273
|
-
cfg.loopStatusTimeout
|
|
274
|
-
);
|
|
275
|
-
loopId = String(newResp.loop_id ?? "").trim();
|
|
276
|
-
if (!loopId) {
|
|
277
|
-
throw new Error("loop_new response missing loop_id");
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
await client.subscribe(
|
|
281
|
-
"loop_events",
|
|
282
|
-
{ loop_id: loopId, verbosity: cfg.verbosityLevel },
|
|
283
|
-
cfg.subscriptionTimeout
|
|
314
|
+
async function fetchLoopCards(client, loopID, timeout) {
|
|
315
|
+
return client.fetchLoopCards(loopID, timeout);
|
|
316
|
+
}
|
|
317
|
+
async function fetchLoopMessages(client, loopID, opts) {
|
|
318
|
+
return client.getLoopMessages(
|
|
319
|
+
loopID,
|
|
320
|
+
opts?.limit,
|
|
321
|
+
opts?.offset,
|
|
322
|
+
opts?.includeEvents,
|
|
323
|
+
opts?.timeout
|
|
284
324
|
);
|
|
285
|
-
return loopId;
|
|
286
325
|
}
|
|
287
|
-
async function
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (ev.type === "connection_ack") {
|
|
296
|
-
const result = ev.result ?? {};
|
|
297
|
-
const state = result.readiness_state;
|
|
298
|
-
if (state === "ready") return;
|
|
299
|
-
throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
|
|
326
|
+
async function connectedWebsocket(wsUrl, fn, timeoutMs = 3e4) {
|
|
327
|
+
const { Client: Client2 } = await import("./client-UNPC32NQ.js");
|
|
328
|
+
const client = new Client2(wsUrl, defaultConfig());
|
|
329
|
+
const deadline = Date.now() + timeoutMs;
|
|
330
|
+
try {
|
|
331
|
+
await client.connect();
|
|
332
|
+
while (!client.isConnected() && Date.now() < deadline) {
|
|
333
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
300
334
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
async function waitLoopStatusWithID(client, timeout) {
|
|
305
|
-
const deadline = Date.now() + timeout;
|
|
306
|
-
while (Date.now() < deadline) {
|
|
307
|
-
const remaining = deadline - Date.now();
|
|
308
|
-
if (remaining <= 0) break;
|
|
309
|
-
const ev = await client.readEventWithTimeout(remaining);
|
|
310
|
-
if (ev === null) break;
|
|
311
|
-
if (ev.type === "error") {
|
|
312
|
-
const errObj = ev.error ?? {};
|
|
313
|
-
throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
|
|
314
|
-
}
|
|
315
|
-
if (ev.type === "status") {
|
|
316
|
-
const lid = ev.loop_id;
|
|
317
|
-
if (lid && lid !== "") {
|
|
318
|
-
return ev;
|
|
319
|
-
}
|
|
335
|
+
if (!client.isConnected()) {
|
|
336
|
+
throw new Error("Timed out waiting for daemon handshake");
|
|
320
337
|
}
|
|
338
|
+
return await fn(client);
|
|
339
|
+
} finally {
|
|
340
|
+
client.close();
|
|
321
341
|
}
|
|
322
|
-
throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
|
|
323
342
|
}
|
|
324
|
-
async function
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
343
|
+
async function protocol1Rpc(wsUrl, method, params = null, opts = {}) {
|
|
344
|
+
const mode = opts.mode ?? "request";
|
|
345
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
346
|
+
try {
|
|
347
|
+
return await connectedWebsocket(
|
|
348
|
+
wsUrl,
|
|
349
|
+
async (client) => {
|
|
350
|
+
if (mode === "notify") {
|
|
351
|
+
await client.notify(method, params ?? {});
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
if (mode === "subscribe") {
|
|
355
|
+
const subId = await client.subscribe(
|
|
356
|
+
method,
|
|
357
|
+
params ?? {},
|
|
358
|
+
timeoutMs
|
|
359
|
+
);
|
|
360
|
+
return { subscription_id: subId };
|
|
361
|
+
}
|
|
362
|
+
const result = await client.requestResponse(
|
|
363
|
+
method,
|
|
364
|
+
params ?? {},
|
|
365
|
+
method,
|
|
366
|
+
timeoutMs
|
|
367
|
+
);
|
|
368
|
+
return result && typeof result === "object" ? result : { result };
|
|
369
|
+
},
|
|
370
|
+
timeoutMs
|
|
371
|
+
);
|
|
372
|
+
} catch (exc) {
|
|
373
|
+
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
374
|
+
if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
|
|
375
|
+
return { error: "Timed out waiting for daemon response" };
|
|
340
376
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
345
|
-
const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
|
|
346
|
-
const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
|
|
347
|
-
let lastErr = null;
|
|
348
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
349
|
-
try {
|
|
350
|
-
await client.connect();
|
|
351
|
-
return;
|
|
352
|
-
} catch (err) {
|
|
353
|
-
lastErr = err;
|
|
377
|
+
if (msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("dial")) {
|
|
378
|
+
return { error: `Connection error: ${msg}` };
|
|
354
379
|
}
|
|
355
|
-
|
|
380
|
+
return { error: msg };
|
|
356
381
|
}
|
|
357
|
-
throw new Error(
|
|
358
|
-
`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
|
|
359
|
-
);
|
|
360
382
|
}
|
|
361
383
|
|
|
362
384
|
// src/appkit/broadcaster.ts
|
|
@@ -578,6 +600,7 @@ var EventClassifier = class {
|
|
|
578
600
|
deliverablePhases;
|
|
579
601
|
minDeliverableRunes;
|
|
580
602
|
thinkingStepEvents;
|
|
603
|
+
treatStatusIdleAsComplete;
|
|
581
604
|
constructor(cfg) {
|
|
582
605
|
if (!cfg.deliverablePhases) {
|
|
583
606
|
throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
|
|
@@ -585,6 +608,7 @@ var EventClassifier = class {
|
|
|
585
608
|
this.deliverablePhases = cfg.deliverablePhases;
|
|
586
609
|
this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
|
|
587
610
|
this.thinkingStepEvents = cfg.thinkingStepEvents;
|
|
611
|
+
this.treatStatusIdleAsComplete = Boolean(cfg.treatStatusIdleAsComplete);
|
|
588
612
|
}
|
|
589
613
|
/**
|
|
590
614
|
* Inspects one decoded event and returns its outcome. `accumulated` is the
|
|
@@ -601,6 +625,13 @@ var EventClassifier = class {
|
|
|
601
625
|
*/
|
|
602
626
|
isDeliverableCompletionEvent(eventType) {
|
|
603
627
|
if (!eventType) return false;
|
|
628
|
+
switch (eventType) {
|
|
629
|
+
case "status.idle":
|
|
630
|
+
case "idle_timeout":
|
|
631
|
+
case "query_timeout":
|
|
632
|
+
case "stream_closed":
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
604
635
|
if (eventType === EventFinalReport) return true;
|
|
605
636
|
if (eventType.startsWith("soothe.protocol.message.")) {
|
|
606
637
|
const phase = eventType.slice("soothe.protocol.message.".length);
|
|
@@ -636,16 +667,22 @@ var EventClassifier = class {
|
|
|
636
667
|
return ["", false];
|
|
637
668
|
}
|
|
638
669
|
/** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
|
|
639
|
-
processChatEvent(msg,
|
|
670
|
+
processChatEvent(msg, accumulated) {
|
|
640
671
|
if (!msg || typeof msg !== "object") {
|
|
641
672
|
return { terminal: 0 /* Continue */ };
|
|
642
673
|
}
|
|
643
674
|
const m = msg;
|
|
644
675
|
const typ = m.type;
|
|
645
676
|
if (typ === "next") {
|
|
646
|
-
return this.classifyNextEnvelope(m);
|
|
677
|
+
return this.classifyNextEnvelope(m, accumulated);
|
|
647
678
|
}
|
|
648
|
-
if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack"
|
|
679
|
+
if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack") {
|
|
680
|
+
return { terminal: 0 /* Continue */ };
|
|
681
|
+
}
|
|
682
|
+
if (typ === "status") {
|
|
683
|
+
if (this.treatStatusIdleAsComplete && String(m.state ?? "").trim().toLowerCase() === "idle" && this.isSubstantiveAssistantReply(accumulated)) {
|
|
684
|
+
return this.deliverableResult(accumulated.trim(), "status.idle");
|
|
685
|
+
}
|
|
649
686
|
return { terminal: 0 /* Continue */ };
|
|
650
687
|
}
|
|
651
688
|
if (typ === "error") {
|
|
@@ -665,10 +702,14 @@ var EventClassifier = class {
|
|
|
665
702
|
return { terminal: 0 /* Continue */ };
|
|
666
703
|
}
|
|
667
704
|
/** Classifies a `next` envelope by projecting its payload. */
|
|
668
|
-
classifyNextEnvelope(env) {
|
|
705
|
+
classifyNextEnvelope(env, accumulated) {
|
|
669
706
|
const payload = env.payload ?? {};
|
|
670
707
|
const innerData = payload.data;
|
|
671
708
|
if (innerData && typeof innerData === "object") {
|
|
709
|
+
const innerType = innerData.type ?? "";
|
|
710
|
+
if (innerType === "status") {
|
|
711
|
+
return this.processChatEvent(innerData, accumulated);
|
|
712
|
+
}
|
|
672
713
|
const innerMode = innerData.mode ?? "";
|
|
673
714
|
if (innerMode) {
|
|
674
715
|
return this.classifyEventPayload(
|
|
@@ -679,6 +720,11 @@ var EventClassifier = class {
|
|
|
679
720
|
}
|
|
680
721
|
}
|
|
681
722
|
const mode = payload.mode ?? "";
|
|
723
|
+
if (mode === "status" || mode === "") {
|
|
724
|
+
if (typeof payload.state === "string" || innerData && "state" in (innerData ?? {})) {
|
|
725
|
+
return this.processChatEvent({ type: "status", ...innerData ?? payload }, accumulated);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
682
728
|
if (mode) {
|
|
683
729
|
return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
|
|
684
730
|
}
|
|
@@ -1090,10 +1136,15 @@ var ConnectionPool = class {
|
|
|
1090
1136
|
if (existing.isDisconnected() || !existing.isConnected()) {
|
|
1091
1137
|
await this.release(sessionID);
|
|
1092
1138
|
} else {
|
|
1093
|
-
existing.lastUsed
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1139
|
+
const idleTooLong = this.cfg.maxIdleTime > 0 && existing.lastUsed > 0 && Date.now() - existing.lastUsed > this.cfg.maxIdleTime;
|
|
1140
|
+
if (idleTooLong) {
|
|
1141
|
+
await this.release(sessionID);
|
|
1142
|
+
} else {
|
|
1143
|
+
existing.lastUsed = Date.now();
|
|
1144
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
1145
|
+
});
|
|
1146
|
+
return existing;
|
|
1147
|
+
}
|
|
1097
1148
|
}
|
|
1098
1149
|
}
|
|
1099
1150
|
const conn = this.pool.pop();
|
|
@@ -1196,6 +1247,88 @@ var ConnectionPool = class {
|
|
|
1196
1247
|
}
|
|
1197
1248
|
};
|
|
1198
1249
|
|
|
1250
|
+
// src/appkit/attachments.ts
|
|
1251
|
+
function compactDefaults(opts) {
|
|
1252
|
+
return {
|
|
1253
|
+
maxDim: opts?.maxDim && opts.maxDim > 0 ? opts.maxDim : 768,
|
|
1254
|
+
quality: opts?.jpegQuality && opts.jpegQuality > 0 ? opts.jpegQuality : 85
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
var sharpLoader = null;
|
|
1258
|
+
async function loadSharp() {
|
|
1259
|
+
if (!sharpLoader) {
|
|
1260
|
+
sharpLoader = (async () => {
|
|
1261
|
+
try {
|
|
1262
|
+
const m = await Function('return import("sharp")')();
|
|
1263
|
+
return m;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
})();
|
|
1268
|
+
}
|
|
1269
|
+
return sharpLoader;
|
|
1270
|
+
}
|
|
1271
|
+
async function compactImageAttachment(mimeType, dataB64, opts) {
|
|
1272
|
+
if (!dataB64 || !mimeType.startsWith("image/")) {
|
|
1273
|
+
return [mimeType, dataB64];
|
|
1274
|
+
}
|
|
1275
|
+
let raw;
|
|
1276
|
+
try {
|
|
1277
|
+
raw = Buffer.from(dataB64, "base64");
|
|
1278
|
+
} catch {
|
|
1279
|
+
return [mimeType, dataB64];
|
|
1280
|
+
}
|
|
1281
|
+
if (raw.length === 0) return [mimeType, dataB64];
|
|
1282
|
+
const sharpMod = await loadSharp();
|
|
1283
|
+
if (!sharpMod) return [mimeType, dataB64];
|
|
1284
|
+
const { maxDim, quality } = compactDefaults(opts);
|
|
1285
|
+
try {
|
|
1286
|
+
const img = sharpMod.default(raw, { failOn: "none" });
|
|
1287
|
+
const meta = await img.metadata();
|
|
1288
|
+
const w = meta.width ?? 0;
|
|
1289
|
+
const h = meta.height ?? 0;
|
|
1290
|
+
if (w <= 0 || h <= 0 || w <= maxDim && h <= maxDim) {
|
|
1291
|
+
return [mimeType, dataB64];
|
|
1292
|
+
}
|
|
1293
|
+
let nw = w;
|
|
1294
|
+
let nh = h;
|
|
1295
|
+
if (w >= h) {
|
|
1296
|
+
if (w > maxDim) {
|
|
1297
|
+
nw = maxDim;
|
|
1298
|
+
nh = Math.max(1, Math.round(h * maxDim / w));
|
|
1299
|
+
}
|
|
1300
|
+
} else if (h > maxDim) {
|
|
1301
|
+
nh = maxDim;
|
|
1302
|
+
nw = Math.max(1, Math.round(w * maxDim / h));
|
|
1303
|
+
}
|
|
1304
|
+
const resized = img.resize(nw, nh, { fit: "fill" });
|
|
1305
|
+
if (mimeType === "image/png") {
|
|
1306
|
+
const buf2 = await resized.png().toBuffer();
|
|
1307
|
+
return [mimeType, buf2.toString("base64")];
|
|
1308
|
+
}
|
|
1309
|
+
const buf = await resized.jpeg({ quality }).toBuffer();
|
|
1310
|
+
return ["image/jpeg", buf.toString("base64")];
|
|
1311
|
+
} catch {
|
|
1312
|
+
return [mimeType, dataB64];
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async function compactAttachments(atts, opts) {
|
|
1316
|
+
if (!atts.length) return atts;
|
|
1317
|
+
const out = [];
|
|
1318
|
+
for (const att of atts) {
|
|
1319
|
+
const cp = { ...att };
|
|
1320
|
+
const mime = typeof cp.mime_type === "string" ? cp.mime_type : "";
|
|
1321
|
+
const data = typeof cp.data === "string" ? cp.data : "";
|
|
1322
|
+
if (mime && data) {
|
|
1323
|
+
const [outMime, outData] = await compactImageAttachment(mime, data, opts);
|
|
1324
|
+
cp.mime_type = outMime;
|
|
1325
|
+
cp.data = outData;
|
|
1326
|
+
}
|
|
1327
|
+
out.push(cp);
|
|
1328
|
+
}
|
|
1329
|
+
return out;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1199
1332
|
// src/appkit/turn_runner.ts
|
|
1200
1333
|
var ErrQueryTimeout = class extends Error {
|
|
1201
1334
|
constructor() {
|
|
@@ -1203,6 +1336,19 @@ var ErrQueryTimeout = class extends Error {
|
|
|
1203
1336
|
this.name = "ErrQueryTimeout";
|
|
1204
1337
|
}
|
|
1205
1338
|
};
|
|
1339
|
+
var ErrIdleTimeout = class extends Error {
|
|
1340
|
+
constructor() {
|
|
1341
|
+
super("appkit: idle timeout");
|
|
1342
|
+
this.name = "ErrIdleTimeout";
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
var TimeoutPolicy = /* @__PURE__ */ ((TimeoutPolicy2) => {
|
|
1346
|
+
TimeoutPolicy2[TimeoutPolicy2["Fail"] = 0] = "Fail";
|
|
1347
|
+
TimeoutPolicy2[TimeoutPolicy2["SoftComplete"] = 1] = "SoftComplete";
|
|
1348
|
+
return TimeoutPolicy2;
|
|
1349
|
+
})(TimeoutPolicy || {});
|
|
1350
|
+
var StreamCloseFail = 0 /* Fail */;
|
|
1351
|
+
var StreamCloseSoftComplete = 1 /* SoftComplete */;
|
|
1206
1352
|
function inputMessageForLoop(text, loopID, attachments, opts) {
|
|
1207
1353
|
const msg = { type: "loop_input", content: text };
|
|
1208
1354
|
if (loopID) msg.loop_id = loopID;
|
|
@@ -1225,6 +1371,13 @@ function inputMessageForLoop(text, loopID, attachments, opts) {
|
|
|
1225
1371
|
}
|
|
1226
1372
|
return msg;
|
|
1227
1373
|
}
|
|
1374
|
+
function idleTimeoutForTurn(cfg, hasAttachments) {
|
|
1375
|
+
const idle = cfg.idleTimeout ?? 0;
|
|
1376
|
+
if (idle <= 0) return 0;
|
|
1377
|
+
const floor = cfg.minIdleTimeoutWithAttachments ?? 0;
|
|
1378
|
+
if (hasAttachments && floor > 0 && idle < floor) return floor;
|
|
1379
|
+
return idle;
|
|
1380
|
+
}
|
|
1228
1381
|
var TurnRunner = class {
|
|
1229
1382
|
pool;
|
|
1230
1383
|
gate;
|
|
@@ -1235,39 +1388,29 @@ var TurnRunner = class {
|
|
|
1235
1388
|
buildInput = inputMessageForLoop;
|
|
1236
1389
|
onComplete = null;
|
|
1237
1390
|
onError = null;
|
|
1238
|
-
/**
|
|
1239
|
-
* Constructs a TurnRunner. pool, gate, classifier, and store are required;
|
|
1240
|
-
* broadcaster may be null.
|
|
1241
|
-
*/
|
|
1242
1391
|
constructor(pool, gate, classifier, store, broadcaster, cfg) {
|
|
1243
1392
|
this.pool = pool;
|
|
1244
1393
|
this.gate = gate;
|
|
1245
1394
|
this.classifier = classifier;
|
|
1246
1395
|
this.store = store;
|
|
1247
1396
|
this.broadcaster = broadcaster;
|
|
1248
|
-
this.cfg = {
|
|
1397
|
+
this.cfg = {
|
|
1398
|
+
...cfg,
|
|
1399
|
+
queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3
|
|
1400
|
+
};
|
|
1249
1401
|
}
|
|
1250
|
-
/** Overrides the loop_input payload builder. */
|
|
1251
1402
|
withInputBuilder(f) {
|
|
1252
1403
|
if (f) this.buildInput = f;
|
|
1253
1404
|
return this;
|
|
1254
1405
|
}
|
|
1255
|
-
/** Sets a completion hook (runs inline on success). */
|
|
1256
1406
|
withOnComplete(f) {
|
|
1257
1407
|
this.onComplete = f;
|
|
1258
1408
|
return this;
|
|
1259
1409
|
}
|
|
1260
|
-
/** Sets an error hook (runs inline on failure). */
|
|
1261
1410
|
withOnError(f) {
|
|
1262
1411
|
this.onError = f;
|
|
1263
1412
|
return this;
|
|
1264
1413
|
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Runs one query turn. The response is broadcast via the SSE broadcaster and
|
|
1267
|
-
* persisted via the SessionStore; it is not returned to the caller (SSE
|
|
1268
|
-
* subscribers receive it). Resolves on success; rejects on failure
|
|
1269
|
-
* (ErrQueryTimeout, AbortError, or a daemon/processing error).
|
|
1270
|
-
*/
|
|
1271
1414
|
async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
|
|
1272
1415
|
let conn;
|
|
1273
1416
|
try {
|
|
@@ -1295,13 +1438,19 @@ var TurnRunner = class {
|
|
|
1295
1438
|
this.onError?.(sessionID, loopID, err);
|
|
1296
1439
|
throw err;
|
|
1297
1440
|
}
|
|
1441
|
+
let idleTimer = null;
|
|
1442
|
+
const clearIdle = () => {
|
|
1443
|
+
if (idleTimer) {
|
|
1444
|
+
clearTimeout(idleTimer);
|
|
1445
|
+
idleTimer = null;
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1298
1448
|
try {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
);
|
|
1449
|
+
let atts = attachments ?? void 0;
|
|
1450
|
+
if (this.cfg.compactAttachmentsBeforeSend && atts && atts.length > 0) {
|
|
1451
|
+
atts = await compactAttachments(atts, this.cfg.compactImageOpts);
|
|
1452
|
+
}
|
|
1453
|
+
const inputMsg = this.buildInput(message, loopID, atts, opts ?? void 0);
|
|
1305
1454
|
try {
|
|
1306
1455
|
await conn.client.sendMessage(inputMsg);
|
|
1307
1456
|
} catch (err) {
|
|
@@ -1320,6 +1469,23 @@ var TurnRunner = class {
|
|
|
1320
1469
|
}
|
|
1321
1470
|
let assistantContent = "";
|
|
1322
1471
|
const startedAt = Date.now();
|
|
1472
|
+
const idleForTurn = idleTimeoutForTurn(this.cfg, (attachments?.length ?? 0) > 0);
|
|
1473
|
+
let idleReject = null;
|
|
1474
|
+
const armIdle = () => {
|
|
1475
|
+
clearIdle();
|
|
1476
|
+
idleReject = null;
|
|
1477
|
+
if (idleForTurn <= 0) {
|
|
1478
|
+
return new Promise(() => {
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
return new Promise((resolve) => {
|
|
1482
|
+
idleReject = () => resolve("idle");
|
|
1483
|
+
idleTimer = setTimeout(() => {
|
|
1484
|
+
idleReject?.();
|
|
1485
|
+
}, idleForTurn);
|
|
1486
|
+
});
|
|
1487
|
+
};
|
|
1488
|
+
let idleRace = armIdle();
|
|
1323
1489
|
const abortRace = new Promise((resolve) => {
|
|
1324
1490
|
const onTimeout = () => resolve("timeout");
|
|
1325
1491
|
timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
|
|
@@ -1333,34 +1499,71 @@ var TurnRunner = class {
|
|
|
1333
1499
|
const next = iterator.next();
|
|
1334
1500
|
const raced = await Promise.race([
|
|
1335
1501
|
next.then((res2) => ({ tag: "msg", res: res2 })),
|
|
1336
|
-
abortRace.then((tag) => ({ tag }))
|
|
1502
|
+
abortRace.then((tag) => ({ tag })),
|
|
1503
|
+
idleRace.then((tag) => ({ tag }))
|
|
1337
1504
|
]);
|
|
1338
1505
|
if ("tag" in raced && raced.tag !== "msg") {
|
|
1339
1506
|
if (raced.tag === "caller" || signal?.aborted) {
|
|
1507
|
+
clearIdle();
|
|
1340
1508
|
const err = new Error("aborted");
|
|
1341
1509
|
await this.persistFailed(sessionID, loopID, err);
|
|
1342
1510
|
this.broadcastError(sessionID, err);
|
|
1343
1511
|
this.onError?.(sessionID, loopID, err);
|
|
1344
1512
|
throw err;
|
|
1345
1513
|
}
|
|
1514
|
+
if (raced.tag === "idle") {
|
|
1515
|
+
clearIdle();
|
|
1516
|
+
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
1517
|
+
});
|
|
1518
|
+
await this.finishTimeout(
|
|
1519
|
+
sessionID,
|
|
1520
|
+
loopID,
|
|
1521
|
+
assistantContent,
|
|
1522
|
+
startedAt,
|
|
1523
|
+
new ErrIdleTimeout(),
|
|
1524
|
+
"idle_timeout",
|
|
1525
|
+
this.cfg.onIdleTimeout ?? 0 /* Fail */
|
|
1526
|
+
);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
clearIdle();
|
|
1346
1530
|
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
1347
1531
|
});
|
|
1348
|
-
await this.
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1532
|
+
await this.finishTimeout(
|
|
1533
|
+
sessionID,
|
|
1534
|
+
loopID,
|
|
1535
|
+
assistantContent,
|
|
1536
|
+
startedAt,
|
|
1537
|
+
new ErrQueryTimeout(),
|
|
1538
|
+
"query_timeout",
|
|
1539
|
+
this.cfg.onQueryTimeout ?? 0 /* Fail */
|
|
1540
|
+
);
|
|
1541
|
+
return;
|
|
1352
1542
|
}
|
|
1353
1543
|
const res = raced.res;
|
|
1354
1544
|
if (res.done) {
|
|
1545
|
+
clearIdle();
|
|
1546
|
+
if ((this.cfg.onStreamClose ?? 0 /* Fail */) === 1 /* SoftComplete */ && assistantContent.trim() !== "") {
|
|
1547
|
+
await this.completeTurn(
|
|
1548
|
+
sessionID,
|
|
1549
|
+
loopID,
|
|
1550
|
+
assistantContent,
|
|
1551
|
+
startedAt,
|
|
1552
|
+
"stream_closed"
|
|
1553
|
+
);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1355
1556
|
const err = new Error("event stream closed");
|
|
1356
1557
|
await this.persistFailed(sessionID, loopID, err);
|
|
1357
1558
|
this.broadcastError(sessionID, err);
|
|
1358
1559
|
this.onError?.(sessionID, loopID, err);
|
|
1359
1560
|
throw err;
|
|
1360
1561
|
}
|
|
1562
|
+
idleRace = armIdle();
|
|
1361
1563
|
const msg = res.value;
|
|
1362
1564
|
const eventResult = this.classifier.classify(msg, assistantContent);
|
|
1363
1565
|
if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
|
|
1566
|
+
clearIdle();
|
|
1364
1567
|
await this.persistFailed(sessionID, loopID, eventResult.err);
|
|
1365
1568
|
this.broadcastError(sessionID, eventResult.err);
|
|
1366
1569
|
this.onError?.(sessionID, loopID, eventResult.err);
|
|
@@ -1380,25 +1583,39 @@ var TurnRunner = class {
|
|
|
1380
1583
|
assistantContent
|
|
1381
1584
|
);
|
|
1382
1585
|
if (deliverable) {
|
|
1383
|
-
|
|
1384
|
-
await this.
|
|
1586
|
+
clearIdle();
|
|
1587
|
+
await this.completeTurn(
|
|
1385
1588
|
sessionID,
|
|
1386
1589
|
loopID,
|
|
1387
1590
|
final,
|
|
1388
1591
|
startedAt,
|
|
1389
1592
|
eventResult.completionEvent ?? ""
|
|
1390
1593
|
);
|
|
1391
|
-
this.broadcastComplete(sessionID, final);
|
|
1392
|
-
this.onComplete?.(sessionID, loopID, final, eventResult.completionEvent ?? "", elapsedMs);
|
|
1393
1594
|
return;
|
|
1394
1595
|
}
|
|
1395
1596
|
}
|
|
1396
1597
|
} finally {
|
|
1598
|
+
clearIdle();
|
|
1397
1599
|
clearTimeout(timer);
|
|
1398
1600
|
this.gate.release(sessionID);
|
|
1399
1601
|
}
|
|
1400
1602
|
}
|
|
1401
|
-
|
|
1603
|
+
async finishTimeout(sessionID, loopID, content, startedAt, failErr, completionEvent, policy) {
|
|
1604
|
+
if (policy === 1 /* SoftComplete */ && content.trim() !== "") {
|
|
1605
|
+
await this.completeTurn(sessionID, loopID, content, startedAt, completionEvent);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
await this.persistFailed(sessionID, loopID, failErr);
|
|
1609
|
+
this.broadcastError(sessionID, failErr);
|
|
1610
|
+
this.onError?.(sessionID, loopID, failErr);
|
|
1611
|
+
throw failErr;
|
|
1612
|
+
}
|
|
1613
|
+
async completeTurn(sessionID, loopID, final, startedAt, completionEvent) {
|
|
1614
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1615
|
+
await this.persistResponse(sessionID, loopID, final, startedAt, completionEvent);
|
|
1616
|
+
this.broadcastComplete(sessionID, final);
|
|
1617
|
+
this.onComplete?.(sessionID, loopID, final, completionEvent, elapsedMs);
|
|
1618
|
+
}
|
|
1402
1619
|
async sendLoopCancel(_signal, conn, loopID) {
|
|
1403
1620
|
const lid = (loopID ?? "").trim();
|
|
1404
1621
|
if (!conn || !lid) return;
|
|
@@ -1441,17 +1658,469 @@ var TurnRunner = class {
|
|
|
1441
1658
|
this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
|
|
1442
1659
|
}
|
|
1443
1660
|
};
|
|
1661
|
+
|
|
1662
|
+
// src/appkit/chunk_filter.ts
|
|
1663
|
+
var MSG_PAIR_LEN = 2;
|
|
1664
|
+
function updatesChunkIsNoop(data) {
|
|
1665
|
+
if (!data || typeof data !== "object") return true;
|
|
1666
|
+
return !("__interrupt__" in data);
|
|
1667
|
+
}
|
|
1668
|
+
function wireBody(msg) {
|
|
1669
|
+
for (const key of ["kwargs", "data"]) {
|
|
1670
|
+
const nested = msg[key];
|
|
1671
|
+
if (nested && typeof nested === "object") return nested;
|
|
1672
|
+
}
|
|
1673
|
+
return msg;
|
|
1674
|
+
}
|
|
1675
|
+
function dictHasToolInvocation(msg) {
|
|
1676
|
+
const body = wireBody(msg);
|
|
1677
|
+
if (body.tool_calls || body.tool_call_chunks) return true;
|
|
1678
|
+
for (const key of ["content", "content_blocks"]) {
|
|
1679
|
+
const raw = body[key];
|
|
1680
|
+
if (Array.isArray(raw)) {
|
|
1681
|
+
for (const item of raw) {
|
|
1682
|
+
if (item && typeof item === "object" && ["tool_call", "tool_call_chunk", "tool_use"].includes(
|
|
1683
|
+
String(item.type ?? "")
|
|
1684
|
+
)) {
|
|
1685
|
+
return true;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
function plainText(msg) {
|
|
1693
|
+
const body = wireBody(msg);
|
|
1694
|
+
const content = body.content ?? msg.content;
|
|
1695
|
+
if (typeof content === "string") return content;
|
|
1696
|
+
if (Array.isArray(content)) {
|
|
1697
|
+
const parts = [];
|
|
1698
|
+
for (const block of content) {
|
|
1699
|
+
if (typeof block === "string") parts.push(block);
|
|
1700
|
+
else if (block && typeof block === "object") {
|
|
1701
|
+
const text = block.text;
|
|
1702
|
+
if (typeof text === "string") parts.push(text);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return parts.join("");
|
|
1706
|
+
}
|
|
1707
|
+
return "";
|
|
1708
|
+
}
|
|
1709
|
+
function messageChunkIsNonActionable(data) {
|
|
1710
|
+
if (!Array.isArray(data) || data.length !== MSG_PAIR_LEN) return false;
|
|
1711
|
+
const msg = data[0];
|
|
1712
|
+
if (msg === null || msg === void 0) return true;
|
|
1713
|
+
if (!msg || typeof msg !== "object") return false;
|
|
1714
|
+
const m = msg;
|
|
1715
|
+
const body = wireBody(m);
|
|
1716
|
+
const raw = String(body.type ?? m.type ?? "");
|
|
1717
|
+
if (raw === "tool" || raw === "ToolMessage" || raw.endsWith("ToolMessage")) return false;
|
|
1718
|
+
if (dictHasToolInvocation(m)) return false;
|
|
1719
|
+
if (body.phase || m.phase) return false;
|
|
1720
|
+
return !plainText(m).trim();
|
|
1721
|
+
}
|
|
1722
|
+
function shouldDropStreamChunkEarly(_namespace, mode, data) {
|
|
1723
|
+
if (mode === "updates") return updatesChunkIsNoop(data);
|
|
1724
|
+
if (mode === "messages") return messageChunkIsNonActionable(data);
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/appkit/events.ts
|
|
1729
|
+
function unwrapNext(event) {
|
|
1730
|
+
if (!event || typeof event !== "object") return event;
|
|
1731
|
+
if (event.type !== "next") return event;
|
|
1732
|
+
const payload = event.payload;
|
|
1733
|
+
if (!payload || typeof payload !== "object") return event;
|
|
1734
|
+
const data = payload.data;
|
|
1735
|
+
return data && typeof data === "object" ? data : event;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/appkit/observability.ts
|
|
1739
|
+
var TurnEventStats = class {
|
|
1740
|
+
total = 0;
|
|
1741
|
+
messages = 0;
|
|
1742
|
+
updates = 0;
|
|
1743
|
+
custom = 0;
|
|
1744
|
+
skipped = 0;
|
|
1745
|
+
filteredEarly = 0;
|
|
1746
|
+
toolCalls = 0;
|
|
1747
|
+
toolResults = 0;
|
|
1748
|
+
textChunks = 0;
|
|
1749
|
+
heartbeatsDropped = 0;
|
|
1750
|
+
postIdleDrained = 0;
|
|
1751
|
+
inboundDropped = 0;
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
// src/appkit/daemon_session.ts
|
|
1755
|
+
var DEFAULT_POST_IDLE_DRAIN_MS = 500;
|
|
1756
|
+
var DaemonSession = class {
|
|
1757
|
+
wsUrl;
|
|
1758
|
+
workspace;
|
|
1759
|
+
streamDelivery;
|
|
1760
|
+
client;
|
|
1761
|
+
rpcClient;
|
|
1762
|
+
loopId = null;
|
|
1763
|
+
readBusy = false;
|
|
1764
|
+
rpcBusy = false;
|
|
1765
|
+
rpcConnected = false;
|
|
1766
|
+
streaming = false;
|
|
1767
|
+
postIdleDrainDeadlineMs;
|
|
1768
|
+
closed = false;
|
|
1769
|
+
earlyDropFn;
|
|
1770
|
+
statsFactory;
|
|
1771
|
+
config;
|
|
1772
|
+
turnEventStats;
|
|
1773
|
+
lastTurnEndState = null;
|
|
1774
|
+
lastTurnCancellationSeen = false;
|
|
1775
|
+
lastTurnErrorMessage = null;
|
|
1776
|
+
constructor(wsUrl, opts = {}) {
|
|
1777
|
+
this.wsUrl = wsUrl;
|
|
1778
|
+
this.workspace = opts.workspace;
|
|
1779
|
+
this.streamDelivery = opts.streamDelivery ?? "adaptive";
|
|
1780
|
+
this.config = opts.config ?? defaultConfig();
|
|
1781
|
+
this.client = new Client(wsUrl, this.config);
|
|
1782
|
+
this.rpcClient = new Client(wsUrl, this.config);
|
|
1783
|
+
this.postIdleDrainDeadlineMs = opts.postIdleDrainDeadlineMs && opts.postIdleDrainDeadlineMs > 0 ? opts.postIdleDrainDeadlineMs : DEFAULT_POST_IDLE_DRAIN_MS;
|
|
1784
|
+
this.earlyDropFn = opts.earlyDropFn ?? shouldDropStreamChunkEarly;
|
|
1785
|
+
this.statsFactory = opts.statsFactory ?? (() => new TurnEventStats());
|
|
1786
|
+
this.turnEventStats = this.statsFactory();
|
|
1787
|
+
}
|
|
1788
|
+
get streamClient() {
|
|
1789
|
+
return this.client;
|
|
1790
|
+
}
|
|
1791
|
+
get rpcSideClient() {
|
|
1792
|
+
return this.rpcClient;
|
|
1793
|
+
}
|
|
1794
|
+
get activeLoopId() {
|
|
1795
|
+
return this.loopId;
|
|
1796
|
+
}
|
|
1797
|
+
resolveStreamDeliveryMode() {
|
|
1798
|
+
const delivery = this.streamDelivery;
|
|
1799
|
+
if (typeof delivery === "function") return String(delivery() || "adaptive");
|
|
1800
|
+
return String(delivery || "adaptive");
|
|
1801
|
+
}
|
|
1802
|
+
get streamDeliveryMode() {
|
|
1803
|
+
return this.resolveStreamDeliveryMode();
|
|
1804
|
+
}
|
|
1805
|
+
shouldDrop(namespace, mode, data) {
|
|
1806
|
+
return Boolean(this.earlyDropFn(namespace, mode, data));
|
|
1807
|
+
}
|
|
1808
|
+
async connect(resumeLoopId) {
|
|
1809
|
+
await connectWithRetries(this.client);
|
|
1810
|
+
return this.bootstrapLoop(resumeLoopId ?? null);
|
|
1811
|
+
}
|
|
1812
|
+
async bootstrapLoop(resumeLoopId) {
|
|
1813
|
+
const loopNew = this.workspace ? { client_workspace: this.workspace, workspace: this.workspace } : void 0;
|
|
1814
|
+
const loopId = await bootstrapLoopSession(this.client, resumeLoopId, this.config, loopNew);
|
|
1815
|
+
this.loopId = loopId;
|
|
1816
|
+
return { type: "status", loop_id: loopId, state: "ready" };
|
|
1817
|
+
}
|
|
1818
|
+
async newLoop() {
|
|
1819
|
+
return this.bootstrapLoop(null);
|
|
1820
|
+
}
|
|
1821
|
+
async switchLoop(loopId) {
|
|
1822
|
+
return this.bootstrapLoop(loopId);
|
|
1823
|
+
}
|
|
1824
|
+
async ensureConnected() {
|
|
1825
|
+
if (this.client.isConnected() && !this.client.isDisconnected()) return;
|
|
1826
|
+
let resumeLoopId = this.loopId;
|
|
1827
|
+
if (this.rpcConnected) {
|
|
1828
|
+
this.rpcClient.close();
|
|
1829
|
+
this.rpcConnected = false;
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
await this.client.reconnect();
|
|
1833
|
+
} catch {
|
|
1834
|
+
this.client.close();
|
|
1835
|
+
await connectWithRetries(this.client);
|
|
1836
|
+
}
|
|
1837
|
+
if (resumeLoopId) {
|
|
1838
|
+
try {
|
|
1839
|
+
await this.client.reattachAndProbe(resumeLoopId);
|
|
1840
|
+
this.loopId = resumeLoopId;
|
|
1841
|
+
return;
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
if (!(err instanceof StaleLoopError)) throw err;
|
|
1844
|
+
resumeLoopId = null;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
await this.bootstrapLoop(resumeLoopId);
|
|
1848
|
+
}
|
|
1849
|
+
async close() {
|
|
1850
|
+
if (this.closed) return;
|
|
1851
|
+
this.closed = true;
|
|
1852
|
+
this.client.close();
|
|
1853
|
+
this.rpcClient.close();
|
|
1854
|
+
this.rpcConnected = false;
|
|
1855
|
+
}
|
|
1856
|
+
async detach() {
|
|
1857
|
+
if (!this.client.isConnected()) return;
|
|
1858
|
+
try {
|
|
1859
|
+
await this.client.notify("disconnect", {});
|
|
1860
|
+
} catch {
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async sendTurn(text, options) {
|
|
1864
|
+
if (!this.loopId) throw new Error("No active loop session");
|
|
1865
|
+
await this.client.sendInput(text, {
|
|
1866
|
+
loopID: this.loopId,
|
|
1867
|
+
autonomous: options?.autonomous,
|
|
1868
|
+
maxIterations: options?.maxIterations,
|
|
1869
|
+
subagent: options?.preferredSubagent,
|
|
1870
|
+
model: options?.model,
|
|
1871
|
+
modelParams: options?.modelParams,
|
|
1872
|
+
attachments: options?.attachments,
|
|
1873
|
+
clarificationMode: options?.clarificationMode,
|
|
1874
|
+
clarificationAnswer: options?.clarificationAnswer,
|
|
1875
|
+
intentHint: options?.intentHint
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
async cancelActiveTurn() {
|
|
1879
|
+
await this.client.notify("slash_command", { cmd: "/cancel" });
|
|
1880
|
+
}
|
|
1881
|
+
async *drainStreamEventsAfterIdle(expectedLoopId) {
|
|
1882
|
+
const deadline = Date.now() + this.postIdleDrainDeadlineMs;
|
|
1883
|
+
let exp = expectedLoopId;
|
|
1884
|
+
while (Date.now() < deadline) {
|
|
1885
|
+
const event = await this.client.readEventWithTimeout(250);
|
|
1886
|
+
if (!event) break;
|
|
1887
|
+
let frame = event;
|
|
1888
|
+
let eventType = String(frame.type ?? "");
|
|
1889
|
+
if (eventType === "next") {
|
|
1890
|
+
frame = unwrapNext(frame) ?? frame;
|
|
1891
|
+
eventType = String(frame.type ?? "");
|
|
1892
|
+
}
|
|
1893
|
+
const eventLoopId = frame.loop_id;
|
|
1894
|
+
if (exp && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== exp) {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
if (eventType === "error") {
|
|
1898
|
+
const errObj = frame.error ?? {};
|
|
1899
|
+
throw new Error(String(errObj.message || frame.message || "daemon error"));
|
|
1900
|
+
}
|
|
1901
|
+
if (eventType === "status") {
|
|
1902
|
+
const loopEv = frame.loop_id;
|
|
1903
|
+
if (typeof loopEv === "string" && loopEv) {
|
|
1904
|
+
this.loopId = loopEv;
|
|
1905
|
+
exp = loopEv;
|
|
1906
|
+
}
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (eventType !== "event") continue;
|
|
1910
|
+
const data = frame.data;
|
|
1911
|
+
const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
|
|
1912
|
+
const mode = String(frame.mode ?? "");
|
|
1913
|
+
if (this.shouldDrop(namespace, mode, data)) {
|
|
1914
|
+
this.turnEventStats.filteredEarly += 1;
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
this.turnEventStats.postIdleDrained += 1;
|
|
1918
|
+
yield [namespace, mode, data];
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async withRpcLock(fn) {
|
|
1922
|
+
while (this.rpcBusy) {
|
|
1923
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
1924
|
+
}
|
|
1925
|
+
this.rpcBusy = true;
|
|
1926
|
+
try {
|
|
1927
|
+
return await fn();
|
|
1928
|
+
} finally {
|
|
1929
|
+
this.rpcBusy = false;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
async ensureRpcConnected() {
|
|
1933
|
+
if (this.rpcConnected && this.rpcClient.isConnected()) return;
|
|
1934
|
+
await connectWithRetries(this.rpcClient);
|
|
1935
|
+
this.rpcConnected = true;
|
|
1936
|
+
}
|
|
1937
|
+
async listLoops(_limit = 20) {
|
|
1938
|
+
return this.withRpcLock(async () => {
|
|
1939
|
+
await this.ensureRpcConnected();
|
|
1940
|
+
return this.rpcClient.listLoops(15e3);
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
async fetchLoopCards(loopId) {
|
|
1944
|
+
const lid = String(loopId || "").trim();
|
|
1945
|
+
if (!lid) return { cards: [], seq: 0, contextTokens: 0, success: false };
|
|
1946
|
+
return this.withRpcLock(async () => {
|
|
1947
|
+
await this.ensureRpcConnected();
|
|
1948
|
+
try {
|
|
1949
|
+
const resp = await this.rpcClient.fetchLoopCards(lid, 3e4);
|
|
1950
|
+
const rawCards = resp.cards;
|
|
1951
|
+
return {
|
|
1952
|
+
cards: Array.isArray(rawCards) ? rawCards : [],
|
|
1953
|
+
seq: Number(resp.seq ?? 0),
|
|
1954
|
+
contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
|
|
1955
|
+
success: true
|
|
1956
|
+
};
|
|
1957
|
+
} catch {
|
|
1958
|
+
return { cards: [], seq: 0, contextTokens: 0, success: false };
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
async fetchLoopHistory(loopId) {
|
|
1963
|
+
const lid = String(loopId || "").trim();
|
|
1964
|
+
if (!lid) {
|
|
1965
|
+
return { goals: [], liveCards: [], liveGoalIndex: null, contextTokens: 0, success: false };
|
|
1966
|
+
}
|
|
1967
|
+
return this.withRpcLock(async () => {
|
|
1968
|
+
await this.ensureRpcConnected();
|
|
1969
|
+
try {
|
|
1970
|
+
const resp = await this.rpcClient.fetchLoopHistory(lid, 3e4);
|
|
1971
|
+
const liveGoalIndex = resp.live_goal_index;
|
|
1972
|
+
return {
|
|
1973
|
+
goals: Array.isArray(resp.goals) ? resp.goals : [],
|
|
1974
|
+
liveCards: Array.isArray(resp.live_cards) ? resp.live_cards : [],
|
|
1975
|
+
liveGoalIndex: typeof liveGoalIndex === "number" ? liveGoalIndex : null,
|
|
1976
|
+
contextTokens: typeof resp.context_tokens === "number" && resp.context_tokens >= 0 ? resp.context_tokens : 0,
|
|
1977
|
+
success: Boolean(resp.success ?? true)
|
|
1978
|
+
};
|
|
1979
|
+
} catch {
|
|
1980
|
+
return {
|
|
1981
|
+
goals: [],
|
|
1982
|
+
liveCards: [],
|
|
1983
|
+
liveGoalIndex: null,
|
|
1984
|
+
contextTokens: 0,
|
|
1985
|
+
success: false
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
async fetchConversationLog(loopId, opts = {}) {
|
|
1991
|
+
const lid = String(loopId || "").trim();
|
|
1992
|
+
if (!lid) return [];
|
|
1993
|
+
return this.withRpcLock(async () => {
|
|
1994
|
+
await this.ensureRpcConnected();
|
|
1995
|
+
const resp = await this.rpcClient.getLoopMessages(
|
|
1996
|
+
lid,
|
|
1997
|
+
opts.limit ?? 100,
|
|
1998
|
+
opts.offset ?? 0,
|
|
1999
|
+
opts.includeEvents ?? false
|
|
2000
|
+
);
|
|
2001
|
+
const raw = resp.messages;
|
|
2002
|
+
if (!Array.isArray(raw)) return [];
|
|
2003
|
+
return raw.filter((m) => !!m && typeof m === "object");
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
async *iterTurnChunks(opts = {}) {
|
|
2007
|
+
this.turnEventStats = this.statsFactory();
|
|
2008
|
+
this.lastTurnEndState = null;
|
|
2009
|
+
this.lastTurnCancellationSeen = false;
|
|
2010
|
+
this.lastTurnErrorMessage = null;
|
|
2011
|
+
let queryStarted = false;
|
|
2012
|
+
let expectedLoopId = this.loopId;
|
|
2013
|
+
let streamPayloadSeen = false;
|
|
2014
|
+
let turnProgressSeen = false;
|
|
2015
|
+
this.streaming = true;
|
|
2016
|
+
const absoluteDeadline = opts.maxWaitMs !== void 0 && opts.maxWaitMs > 0 ? Date.now() + opts.maxWaitMs : null;
|
|
2017
|
+
this.client.peelStalePendingControlEvents();
|
|
2018
|
+
while (this.readBusy) {
|
|
2019
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
2020
|
+
}
|
|
2021
|
+
this.readBusy = true;
|
|
2022
|
+
try {
|
|
2023
|
+
while (true) {
|
|
2024
|
+
if (absoluteDeadline !== null && Date.now() >= absoluteDeadline) {
|
|
2025
|
+
throw new Error(
|
|
2026
|
+
`Turn timed out after ${opts.maxWaitMs}ms (loop=${expectedLoopId ?? "?"})`
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
const event = await this.client.readEvent();
|
|
2030
|
+
if (!event) {
|
|
2031
|
+
if (queryStarted && !this.client.isConnectionAlive()) {
|
|
2032
|
+
this.lastTurnEndState = "connection_lost";
|
|
2033
|
+
throw new Error("Daemon connection lost");
|
|
2034
|
+
}
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
let frame = event;
|
|
2038
|
+
let eventType = String(frame.type ?? "");
|
|
2039
|
+
if (eventType === "next") {
|
|
2040
|
+
frame = unwrapNext(frame) ?? frame;
|
|
2041
|
+
eventType = String(frame.type ?? "");
|
|
2042
|
+
}
|
|
2043
|
+
const eventLoopId = frame.loop_id;
|
|
2044
|
+
if (expectedLoopId && typeof eventLoopId === "string" && eventLoopId && eventLoopId !== expectedLoopId) {
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
if (eventType === "error") {
|
|
2048
|
+
const errObj = frame.error ?? {};
|
|
2049
|
+
throw new Error(String(errObj.message || frame.message || "daemon error"));
|
|
2050
|
+
}
|
|
2051
|
+
if (eventType === "status") {
|
|
2052
|
+
const loopEv = frame.loop_id;
|
|
2053
|
+
if (typeof loopEv === "string" && loopEv) {
|
|
2054
|
+
this.loopId = loopEv;
|
|
2055
|
+
expectedLoopId = loopEv;
|
|
2056
|
+
}
|
|
2057
|
+
const state = String(frame.state ?? "");
|
|
2058
|
+
if (state === "running") {
|
|
2059
|
+
queryStarted = true;
|
|
2060
|
+
} else if (queryStarted && state === "stopped") {
|
|
2061
|
+
this.lastTurnEndState = state;
|
|
2062
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
2063
|
+
break;
|
|
2064
|
+
} else if (queryStarted && state === "idle") {
|
|
2065
|
+
if (!streamPayloadSeen && !this.lastTurnCancellationSeen) continue;
|
|
2066
|
+
this.lastTurnEndState = state;
|
|
2067
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
if (eventType === "command_response") {
|
|
2073
|
+
const content = String(frame.content ?? "");
|
|
2074
|
+
if (content.includes("Cancellation requested")) {
|
|
2075
|
+
this.lastTurnCancellationSeen = true;
|
|
2076
|
+
}
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (eventType !== "event") continue;
|
|
2080
|
+
const data = frame.data;
|
|
2081
|
+
const namespace = Array.isArray(frame.namespace) ? frame.namespace : [];
|
|
2082
|
+
const mode = String(frame.mode ?? "");
|
|
2083
|
+
if (this.shouldDrop(namespace, mode, data)) {
|
|
2084
|
+
this.turnEventStats.filteredEarly += 1;
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
if (mode === "custom" && isTurnEndCustomData(data)) {
|
|
2088
|
+
if (!queryStarted || !turnProgressSeen) continue;
|
|
2089
|
+
}
|
|
2090
|
+
streamPayloadSeen = true;
|
|
2091
|
+
if (isTurnProgressChunk(mode, data)) turnProgressSeen = true;
|
|
2092
|
+
yield [namespace, mode, data];
|
|
2093
|
+
if (mode === "custom" && isTurnEndCustomData(data)) {
|
|
2094
|
+
const customType = String(data.type ?? "").trim();
|
|
2095
|
+
this.lastTurnEndState = customType === STREAM_END ? "stream_end" : "completed";
|
|
2096
|
+
yield* this.drainStreamEventsAfterIdle(expectedLoopId);
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
} catch (exc) {
|
|
2101
|
+
this.lastTurnErrorMessage = String(exc);
|
|
2102
|
+
throw exc;
|
|
2103
|
+
} finally {
|
|
2104
|
+
this.streaming = false;
|
|
2105
|
+
this.readBusy = false;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
1444
2109
|
export {
|
|
1445
2110
|
CLIENT_VERSION,
|
|
1446
2111
|
ChatEventTerminal,
|
|
1447
2112
|
Client,
|
|
2113
|
+
CommandClient,
|
|
1448
2114
|
ConnectionError,
|
|
1449
2115
|
ConnectionPool,
|
|
1450
2116
|
DEFAULT_CLIENT_CAPABILITIES,
|
|
1451
2117
|
DEFAULT_DELIVERABLE_PHASES,
|
|
2118
|
+
DEFAULT_POST_IDLE_DRAIN_MS,
|
|
1452
2119
|
DEFAULT_THINKING_STEP_EVENTS,
|
|
1453
2120
|
DaemonError,
|
|
2121
|
+
DaemonSession,
|
|
1454
2122
|
DisconnectCause,
|
|
2123
|
+
ErrIdleTimeout,
|
|
1455
2124
|
ErrPoolExhausted,
|
|
1456
2125
|
ErrQueryBusy,
|
|
1457
2126
|
ErrQueryTimeout,
|
|
@@ -1497,26 +2166,31 @@ export {
|
|
|
1497
2166
|
INTENT_HINT_OCR,
|
|
1498
2167
|
INTENT_HINT_TEXT_COMPLETION,
|
|
1499
2168
|
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
1500
|
-
Multiplexer,
|
|
1501
2169
|
PROTO_VERSION,
|
|
1502
2170
|
PooledConn,
|
|
1503
2171
|
QueryGate,
|
|
1504
2172
|
REMOVED_INTENT_HINTS,
|
|
1505
2173
|
ReconnectError,
|
|
1506
2174
|
SSEBroadcaster,
|
|
2175
|
+
STREAM_END,
|
|
1507
2176
|
StaleLoopError,
|
|
2177
|
+
StreamCloseFail,
|
|
2178
|
+
StreamCloseSoftComplete,
|
|
1508
2179
|
TimeoutError,
|
|
2180
|
+
TimeoutPolicy,
|
|
2181
|
+
TurnEventStats,
|
|
1509
2182
|
TurnRunner,
|
|
1510
2183
|
VerbosityTier,
|
|
1511
2184
|
authenticate,
|
|
1512
2185
|
bootstrapLoopSession,
|
|
1513
2186
|
checkDaemonStatus,
|
|
1514
2187
|
classifyEventVerbosity,
|
|
2188
|
+
compactAttachments,
|
|
2189
|
+
compactImageAttachment,
|
|
1515
2190
|
connectWithRetries,
|
|
2191
|
+
connectedWebsocket,
|
|
1516
2192
|
connectionInitEnvelope,
|
|
1517
2193
|
decodeMessage,
|
|
1518
|
-
defaultBootstrapFunc,
|
|
1519
|
-
defaultClientFactory,
|
|
1520
2194
|
defaultConfig,
|
|
1521
2195
|
defaultPoolConfig,
|
|
1522
2196
|
disconnectCauseName,
|
|
@@ -1525,12 +2199,18 @@ export {
|
|
|
1525
2199
|
extractSootheLoopID,
|
|
1526
2200
|
extractThinkingStep,
|
|
1527
2201
|
fetchConfigSection,
|
|
2202
|
+
fetchLoopCards,
|
|
1528
2203
|
fetchLoopHistory,
|
|
2204
|
+
fetchLoopMessages,
|
|
1529
2205
|
fetchSkillsCatalog,
|
|
2206
|
+
idleTimeoutForTurn,
|
|
2207
|
+
inboundNeedsDeliveryAck,
|
|
1530
2208
|
inputMessageForLoop,
|
|
1531
2209
|
isCompletionEvent,
|
|
1532
2210
|
isDaemonLive,
|
|
1533
2211
|
isSubagentProgressEvent,
|
|
2212
|
+
isTurnEndCustomData,
|
|
2213
|
+
isTurnProgressChunk,
|
|
1534
2214
|
isValidVerbosityLevel,
|
|
1535
2215
|
loadConfigFromEnv,
|
|
1536
2216
|
newLoopInputMessage,
|
|
@@ -1541,6 +2221,7 @@ export {
|
|
|
1541
2221
|
parseNamespace,
|
|
1542
2222
|
pingEnvelope,
|
|
1543
2223
|
pongEnvelope,
|
|
2224
|
+
protocol1Rpc,
|
|
1544
2225
|
refreshAuthToken,
|
|
1545
2226
|
requestDaemonConfigReload,
|
|
1546
2227
|
requestDaemonShutdown,
|