@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/dist/index.js CHANGED
@@ -1,195 +1,243 @@
1
1
  import {
2
+ CLIENT_VERSION,
2
3
  Client,
4
+ ConnectionError,
5
+ DEFAULT_CLIENT_CAPABILITIES,
6
+ DEFAULT_DELIVERABLE_PHASES,
7
+ DaemonError,
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,
45
+ INTENT_HINT_EMBED,
46
+ INTENT_HINT_IMAGE_TO_TEXT,
47
+ INTENT_HINT_OCR,
48
+ INTENT_HINT_TEXT_COMPLETION,
49
+ LOOP_ASSISTANT_OUTPUT_PHASES,
50
+ PROTO_VERSION,
51
+ REMOVED_INTENT_HINTS,
52
+ ReconnectError,
53
+ STREAM_END,
54
+ StaleLoopError,
55
+ TimeoutError,
56
+ VerbosityTier,
57
+ classifyEventVerbosity,
58
+ connectionInitEnvelope,
3
59
  decodeMessage,
4
60
  defaultConfig,
61
+ disconnectCauseName,
62
+ disconnectEnvelope,
5
63
  encodeMessage,
6
64
  extractSootheLoopID,
65
+ inboundNeedsDeliveryAck,
66
+ isCompletionEvent,
67
+ isSubagentProgressEvent,
68
+ isTurnEndCustomData,
69
+ isTurnProgressChunk,
70
+ isValidVerbosityLevel,
7
71
  loadConfigFromEnv,
8
72
  newLoopInputMessage,
9
73
  newLoopNewMessage,
10
74
  newLoopSubscribeMessage,
11
75
  newRequestID,
12
- splitWirePayload
13
- } from "./chunk-OMAC7LA7.js";
76
+ notificationEnvelope,
77
+ parseNamespace,
78
+ pingEnvelope,
79
+ pongEnvelope,
80
+ requestEnvelope,
81
+ shouldShow,
82
+ splitWirePayload,
83
+ subscribeEnvelope,
84
+ unsubscribeEnvelope,
85
+ validateLoopInputIntentHint
86
+ } from "./chunk-U6RMINYV.js";
14
87
 
15
- // src/errors.ts
16
- var ConnectionError = class extends Error {
17
- url;
18
- attempt;
19
- cause;
20
- constructor(url, attempt, cause) {
21
- super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
22
- this.name = "ConnectionError";
23
- this.url = url;
24
- this.attempt = attempt;
25
- this.cause = cause;
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
+ }
26
104
  }
27
- };
28
- var DaemonError = class extends Error {
29
- code;
30
- /** The daemon's error message text. */
31
- daemonMessage;
32
- constructor(code, message) {
33
- super(`daemon error [${code}]: ${message}`);
34
- this.name = "DaemonError";
35
- this.code = code;
36
- this.daemonMessage = message;
105
+ await client.subscribe(
106
+ "loop_events",
107
+ { loop_id: loopId, verbosity: cfg.verbosityLevel },
108
+ cfg.subscriptionTimeout
109
+ );
110
+ return loopId;
111
+ }
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
+ }
37
126
  }
38
- };
39
- var TimeoutError = class extends Error {
40
- operation;
41
- duration;
42
- constructor(operation, duration) {
43
- super(`timeout after ${duration} waiting for ${operation}`);
44
- this.name = "TimeoutError";
45
- this.operation = operation;
46
- this.duration = duration;
127
+ throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
128
+ }
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
+ }
145
+ }
47
146
  }
48
- };
49
-
50
- // src/verbosity.ts
51
- var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
52
- VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
53
- VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
54
- VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
55
- VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
56
- VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
57
- return VerbosityTier2;
58
- })(VerbosityTier || {});
59
- var verbosityLevelValues = {
60
- quiet: 0,
61
- normal: 1,
62
- debug: 3
63
- };
64
- function shouldShow(tier, verbosity) {
65
- if (tier === 99 /* Internal */) {
66
- return false;
147
+ throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
148
+ }
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
+ }
67
166
  }
68
- const level = verbosityLevelValues[verbosity] ?? 1;
69
- return tier <= level;
167
+ throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
70
168
  }
71
- function isValidVerbosityLevel(s) {
72
- return s in verbosityLevelValues;
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));
181
+ }
182
+ throw new Error(
183
+ `failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
184
+ );
73
185
  }
74
186
 
75
- // src/events.ts
76
- var EventPlanCreated = "soothe.cognition.plan.created";
77
- var EventExploreStarted = "soothe.subagent.explore.started";
78
- var EventExploreMilestone = "soothe.subagent.explore.milestone";
79
- var EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
80
- var EventExploreCompleted = "soothe.subagent.explore.completed";
81
- var EventTacitusStarted = "soothe.subagent.tacitus.started";
82
- var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
83
- var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
84
- var EventReplayComplete = "replay_complete";
85
- var EventLoopReattachedWire = "loop_reattached";
86
- var EventToolStarted = "soothe.tool.execution.started";
87
- var EventToolCompleted = "soothe.tool.execution.completed";
88
- var EventToolError = "soothe.tool.execution.error";
89
- var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
90
- var EventToolCallUpdatesBatch = "tool_call_updates_batch";
91
- var EventAgentLoopStarted = "soothe.cognition.agent_loop.started";
92
- var EventAgentLoopIterated = "soothe.cognition.agent_loop.iterated";
93
- var EventAgentLoopCompleted = "soothe.cognition.agent_loop.completed";
94
- var EventAgentLoopReasoned = "soothe.cognition.agent_loop.reasoned";
95
- var EventMessageReceived = "soothe.protocol.message.received";
96
- var EventMessageSent = "soothe.protocol.message.sent";
97
- var EventFinalReport = "soothe.output.autonomous.final_report.reported";
98
- var EventGeneralFailed = "soothe.error.general.failed";
99
- function parseNamespace(ns) {
100
- const parts = splitNamespace(ns);
101
- if (parts.length < 4 || parts[0] !== "soothe") {
102
- return null;
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();
103
196
  }
104
- if (parts[1] === "internal") {
105
- return null;
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
+ }
106
205
  }
107
- return { domain: parts[1], component: parts[2], action: parts[3] };
108
- }
109
- function splitNamespace(ns) {
110
- const parts = [];
111
- let start = 0;
112
- for (let i = 0; i < ns.length; i++) {
113
- if (ns[i] === ".") {
114
- parts.push(ns.slice(start, i));
115
- start = i + 1;
116
- }
117
- }
118
- parts.push(ns.slice(start));
119
- return parts;
120
- }
121
- function classifyEventVerbosity(eventTypeOrNamespace) {
122
- const parsed = parseNamespace(eventTypeOrNamespace);
123
- if (!parsed) {
124
- return classifyByEventTypeString(eventTypeOrNamespace);
125
- }
126
- return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
127
- }
128
- function classifyByDomainAndComponent(domain, _component, full) {
129
- switch (domain) {
130
- case "cognition":
131
- return 1 /* Normal */;
132
- case "protocol":
133
- return 2 /* Detailed */;
134
- case "tool":
135
- return 99 /* Internal */;
136
- case "subagent":
137
- return classifySubagentEvent(full);
138
- case "output":
139
- case "error":
140
- return 0 /* Quiet */;
141
- default:
142
- return 1 /* Normal */;
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
+ );
143
211
  }
144
- }
145
- function classifySubagentEvent(full) {
146
- const parsed = parseNamespace(full);
147
- if (!parsed) return 1 /* Normal */;
148
- switch (parsed.action) {
149
- case "started":
150
- case "completed":
151
- return 1 /* Normal */;
152
- default:
153
- return 2 /* Detailed */;
212
+ async jobCreate(goal, workspace = "") {
213
+ const params = { goal };
214
+ if (workspace) params.workspace = workspace;
215
+ return this.request("job_create", params);
154
216
  }
155
- }
156
- function classifyByEventTypeString(eventType) {
157
- if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
158
- return 0 /* Quiet */;
217
+ async jobStatus(jobId) {
218
+ return this.request("job_status", { job_id: jobId });
159
219
  }
160
- if (eventType === EventToolStarted) {
161
- return 99 /* Internal */;
220
+ async jobCancel(jobId) {
221
+ return this.request("job_cancel", { job_id: jobId });
162
222
  }
163
- return 1 /* Normal */;
164
- }
165
- function isCompletionEvent(eventType) {
166
- return eventType.endsWith(".completed") || eventType.endsWith(".failed") || eventType === EventGeneralFailed;
167
- }
168
- function isSubagentProgressEvent(eventType) {
169
- const parsed = parseNamespace(eventType);
170
- if (!parsed || parsed.domain !== "subagent") {
171
- return false;
223
+ async cronAdd(text, priority = 0) {
224
+ const params = { text };
225
+ if (priority > 0) params.priority = priority;
226
+ return this.request("cron_add", params);
172
227
  }
173
- return parsed.action === "started" || parsed.action === "completed";
174
- }
175
- var ESSENTIAL_EVENT_TYPES = /* @__PURE__ */ new Set([
176
- EventAgentLoopStarted,
177
- EventAgentLoopCompleted,
178
- EventAgentLoopReasoned,
179
- EventPlanCreated,
180
- EventExploreStarted,
181
- EventExploreCompleted,
182
- EventTacitusStarted,
183
- EventTacitusCompleted,
184
- EventGeneralFailed
185
- ]);
228
+ async cronList(status = "") {
229
+ const params = {};
230
+ if (status) params.status = status;
231
+ return this.request("cron_list", params);
232
+ }
233
+ };
186
234
 
187
235
  // src/helpers.ts
188
236
  async function checkDaemonStatus(client, timeout) {
189
- return client.requestResponse({ type: "daemon_status" }, "daemon_status_response", timeout ?? 5e3);
237
+ return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
190
238
  }
191
239
  async function isDaemonLive(wsURL, timeout) {
192
- const { Client: Client2 } = await import("./client-QS23U6WX.js");
240
+ const { Client: Client2 } = await import("./client-UNPC32NQ.js");
193
241
  const t = timeout ?? 5e3;
194
242
  const client = new Client2(wsURL, defaultConfig());
195
243
  try {
@@ -207,131 +255,1885 @@ async function isDaemonLive(wsURL, timeout) {
207
255
  }
208
256
  }
209
257
  async function requestDaemonShutdown(client, timeout) {
210
- const resp = await client.requestResponse({ type: "daemon_shutdown" }, "shutdown_ack", timeout ?? 1e4);
258
+ const resp = await client.requestResponse(
259
+ "daemon_shutdown",
260
+ {},
261
+ "daemon_shutdown",
262
+ timeout ?? 1e4
263
+ );
211
264
  if (resp.status !== "acknowledged") {
212
265
  throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
213
266
  }
214
267
  }
215
268
  async function fetchSkillsCatalog(client, timeout) {
216
- const resp = await client.requestResponse({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
269
+ const resp = await client.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
217
270
  const skillsRaw = resp.skills;
218
271
  if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
219
272
  return skillsRaw.filter((s) => typeof s === "object" && s !== null);
220
273
  }
221
274
  async function fetchConfigSection(client, section, timeout) {
222
- const resp = await client.requestResponse({ type: "config_get", section }, "config_get_response", timeout ?? 5e3);
275
+ const resp = await client.requestResponse(
276
+ "config_get",
277
+ { section },
278
+ "config_get",
279
+ timeout ?? 5e3
280
+ );
223
281
  const sec = resp[section];
224
282
  if (sec && typeof sec === "object") {
225
283
  return sec;
226
284
  }
227
285
  return resp;
228
286
  }
229
-
230
- // src/session.ts
231
- async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
232
- const cfg = config ?? defaultConfig();
233
- await client.sendMessage({ type: "daemon_ready" });
234
- await waitDaemonReady(client, cfg.daemonReadyTimeout);
235
- let loopId = (resumeLoopId ?? "").trim();
236
- if (!loopId) {
237
- const newResp = await client.requestResponse(
238
- newLoopNewMessage(loopNew),
239
- "loop_new_response",
240
- cfg.loopStatusTimeout
287
+ async function requestDaemonConfigReload(client, timeout) {
288
+ return client.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
289
+ }
290
+ async function fetchLoopHistory(client, loopID, timeout) {
291
+ return client.requestResponse(
292
+ "loop_history_fetch",
293
+ { loop_id: loopID },
294
+ "loop_history_fetch",
295
+ timeout ?? 15e3
296
+ );
297
+ }
298
+ async function authenticate(client, accessKey, secretKey, timeout) {
299
+ return client.requestResponse(
300
+ "auth",
301
+ { access_key: accessKey, secret_key: secretKey },
302
+ "auth",
303
+ timeout ?? 15e3
304
+ );
305
+ }
306
+ async function refreshAuthToken(client, refreshToken, timeout) {
307
+ return client.requestResponse(
308
+ "auth_refresh",
309
+ { refresh_token: refreshToken },
310
+ "auth_refresh",
311
+ timeout ?? 15e3
312
+ );
313
+ }
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
324
+ );
325
+ }
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));
334
+ }
335
+ if (!client.isConnected()) {
336
+ throw new Error("Timed out waiting for daemon handshake");
337
+ }
338
+ return await fn(client);
339
+ } finally {
340
+ client.close();
341
+ }
342
+ }
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
241
371
  );
242
- loopId = String(newResp.loop_id ?? "").trim();
243
- if (!loopId) {
244
- throw new Error("loop_new_response missing loop_id");
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" };
245
376
  }
377
+ if (msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("dial")) {
378
+ return { error: `Connection error: ${msg}` };
379
+ }
380
+ return { error: msg };
246
381
  }
247
- const subResp = await client.requestResponse(
248
- { type: "loop_subscribe", loop_id: loopId, verbosity: cfg.verbosityLevel },
249
- "loop_subscribe_response",
250
- cfg.subscriptionTimeout
251
- );
252
- if (subResp.success === false) {
253
- throw new Error(String(subResp.message ?? "loop_subscribe failed"));
382
+ }
383
+
384
+ // src/appkit/broadcaster.ts
385
+ var SUBSCRIBER_QUEUE_CAP = 100;
386
+ var SSEBroadcaster = class {
387
+ subscribers = /* @__PURE__ */ new Map();
388
+ nextSubID = 0;
389
+ /** Creates an empty broadcaster. */
390
+ constructor() {
254
391
  }
255
- return loopId;
392
+ /**
393
+ * Registers a new subscriber channel for a session id. Returns an async
394
+ * iterable the subscriber reads events from. Unsubscribe via
395
+ * `unsubscribe()` or `close()`.
396
+ */
397
+ subscribe(sessionID) {
398
+ const subID = String(this.nextSubID++);
399
+ const sub = { queue: [], waiters: [], closed: false };
400
+ let subs = this.subscribers.get(sessionID);
401
+ if (!subs) {
402
+ subs = /* @__PURE__ */ new Map();
403
+ this.subscribers.set(sessionID, subs);
404
+ }
405
+ subs.set(subID, sub);
406
+ const iterable = {
407
+ [Symbol.asyncIterator]() {
408
+ return {
409
+ next() {
410
+ if (sub.queue.length > 0) {
411
+ return Promise.resolve({ value: sub.queue.shift(), done: false });
412
+ }
413
+ if (sub.closed) {
414
+ return Promise.resolve({ value: void 0, done: true });
415
+ }
416
+ return new Promise((resolve) => {
417
+ sub.waiters.push((ev) => {
418
+ if (ev === null) {
419
+ resolve({ value: void 0, done: true });
420
+ } else {
421
+ resolve({ value: ev, done: false });
422
+ }
423
+ });
424
+ });
425
+ }
426
+ };
427
+ }
428
+ };
429
+ return { iterable, id: subID };
430
+ }
431
+ /** Removes a subscriber by id and closes its iterable. Safe if unknown. */
432
+ unsubscribe(sessionID, subID) {
433
+ const subs = this.subscribers.get(sessionID);
434
+ if (!subs) return;
435
+ const sub = subs.get(subID);
436
+ if (!sub) return;
437
+ sub.closed = true;
438
+ for (const w of sub.waiters) w(null);
439
+ sub.waiters = [];
440
+ subs.delete(subID);
441
+ if (subs.size === 0) this.subscribers.delete(sessionID);
442
+ }
443
+ /**
444
+ * Sends an event to all subscribers for a session id. Non-blocking: a full
445
+ * subscriber queue is skipped (drop-on-full) so one slow consumer cannot
446
+ * block the others.
447
+ */
448
+ broadcast(sessionID, event) {
449
+ const subs = this.subscribers.get(sessionID);
450
+ if (!subs) return;
451
+ for (const sub of subs.values()) {
452
+ if (sub.closed) continue;
453
+ if (sub.waiters.length > 0) {
454
+ const w = sub.waiters.shift();
455
+ w(event);
456
+ } else if (sub.queue.length < SUBSCRIBER_QUEUE_CAP) {
457
+ sub.queue.push(event);
458
+ }
459
+ }
460
+ }
461
+ /** Closes all subscribers for a session id and removes the entry. */
462
+ close(sessionID) {
463
+ const subs = this.subscribers.get(sessionID);
464
+ if (!subs) return;
465
+ for (const sub of subs.values()) {
466
+ sub.closed = true;
467
+ for (const w of sub.waiters) w(null);
468
+ sub.waiters = [];
469
+ }
470
+ this.subscribers.delete(sessionID);
471
+ }
472
+ /** Closes every subscriber channel across all sessions. */
473
+ closeAll() {
474
+ for (const [sessionID, subs] of this.subscribers) {
475
+ for (const sub of subs.values()) {
476
+ sub.closed = true;
477
+ for (const w of sub.waiters) w(null);
478
+ sub.waiters = [];
479
+ }
480
+ this.subscribers.delete(sessionID);
481
+ }
482
+ }
483
+ };
484
+
485
+ // src/appkit/thinking_step.ts
486
+ var MAX_THINKING_STEP_RUNES = 280;
487
+ var DEFAULT_THINKING_STEP_EVENTS = /* @__PURE__ */ new Set([
488
+ "soothe.cognition.plan.step.started",
489
+ "soothe.cognition.plan.step.completed",
490
+ "soothe.cognition.plan.step.failed",
491
+ "soothe.lifecycle.iteration.started",
492
+ "soothe.agent.loop.step.started",
493
+ "soothe.agent.loop.started",
494
+ "soothe.cognition.plan.batch.started",
495
+ "soothe.cognition.plan.created",
496
+ "soothe.cognition.goal.created",
497
+ "soothe.tool.execution.started"
498
+ ]);
499
+ function extractThinkingStep(eventType, data, allow) {
500
+ if (!eventType || !data) return ["", false];
501
+ const et = eventType.trim();
502
+ if (!et) return ["", false];
503
+ const allowlist = allow ?? DEFAULT_THINKING_STEP_EVENTS;
504
+ if (!allowlist.has(et)) return ["", false];
505
+ let line = "";
506
+ switch (et) {
507
+ case "soothe.cognition.plan.step.started":
508
+ line = formatPlanStepLine(data, "");
509
+ break;
510
+ case "soothe.cognition.plan.step.completed":
511
+ line = formatPlanStepLine(data, "done");
512
+ break;
513
+ case "soothe.cognition.plan.step.failed": {
514
+ const stepID = strField(data, "step_id");
515
+ const errMsg = strField(data, "error");
516
+ if (stepID && errMsg) line = `Step ${stepID} failed: ${errMsg}`;
517
+ else if (stepID) line = `Step ${stepID} failed`;
518
+ else if (errMsg) line = `Step failed: ${errMsg}`;
519
+ break;
520
+ }
521
+ case "soothe.agent.loop.step.started":
522
+ line = formatAgentStepLine(data, "");
523
+ break;
524
+ case "soothe.cognition.plan.batch.started": {
525
+ const n = data["parallel_count"];
526
+ if (typeof n === "number" && n > 0) line = `Running ${Math.floor(n)} steps in parallel`;
527
+ break;
528
+ }
529
+ case "soothe.cognition.plan.created":
530
+ case "soothe.agent.loop.started": {
531
+ const g = strField(data, "goal");
532
+ if (g) line = "Goal: " + g;
533
+ break;
534
+ }
535
+ case "soothe.cognition.goal.created": {
536
+ const g = strField(data, "friendly_message", "description");
537
+ if (g) line = "Goal: " + g;
538
+ break;
539
+ }
540
+ case "soothe.lifecycle.iteration.started": {
541
+ const g = strField(data, "goal_description");
542
+ if (g) line = "Iteration: " + g;
543
+ break;
544
+ }
545
+ case "soothe.tool.execution.started": {
546
+ const name = strField(data, "tool_name", "name");
547
+ if (name) line = "Tool: " + name;
548
+ break;
549
+ }
550
+ default:
551
+ return ["", false];
552
+ }
553
+ line = line.trim();
554
+ if (!line) return ["", false];
555
+ const runes = [...line];
556
+ if (runes.length > MAX_THINKING_STEP_RUNES) {
557
+ line = runes.slice(0, MAX_THINKING_STEP_RUNES).join("") + "\u2026";
558
+ }
559
+ return [line, true];
256
560
  }
257
- async function waitDaemonReady(client, timeout) {
258
- const deadline = Date.now() + timeout;
259
- while (Date.now() < deadline) {
260
- const remaining = deadline - Date.now();
261
- if (remaining <= 0) break;
262
- const ev = await client.readEventWithTimeout(remaining);
263
- if (ev === null) break;
264
- if (ev.type === "daemon_ready") {
265
- if (ev.state === "ready") return;
266
- throw new Error(
267
- `daemon not ready: state=${JSON.stringify(ev.state)} message=${JSON.stringify(ev.message ?? "")}`
561
+ function formatPlanStepLine(data, suffix) {
562
+ const stepID = strField(data, "step_id");
563
+ const desc = strField(data, "description");
564
+ if (stepID && suffix) return `Step ${stepID}: ${suffix}`;
565
+ if (stepID && desc) return `Step ${stepID}: ${desc}`;
566
+ if (stepID) return `Step ${stepID}`;
567
+ if (desc && suffix) return `Step: ${suffix}`;
568
+ if (desc) return `Step: ${desc}`;
569
+ if (suffix) return "Step: " + suffix;
570
+ return "";
571
+ }
572
+ function formatAgentStepLine(data, suffix) {
573
+ const stepID = strField(data, "step_id");
574
+ const desc = strField(data, "description");
575
+ if (stepID && desc) return `Step ${stepID}: ${desc}`;
576
+ if (desc) return suffix ? `Step: ${suffix}` : `Step: ${desc}`;
577
+ if (stepID) return `Step ${stepID}`;
578
+ return "";
579
+ }
580
+ function strField(data, ...keys) {
581
+ for (const key of keys) {
582
+ const v = data[key];
583
+ if (typeof v === "string") {
584
+ const s = v.trim();
585
+ if (s) return s;
586
+ }
587
+ }
588
+ return "";
589
+ }
590
+
591
+ // src/appkit/classifier.ts
592
+ var ChatEventTerminal = /* @__PURE__ */ ((ChatEventTerminal2) => {
593
+ ChatEventTerminal2[ChatEventTerminal2["Continue"] = 0] = "Continue";
594
+ ChatEventTerminal2[ChatEventTerminal2["DeliverableComplete"] = 1] = "DeliverableComplete";
595
+ ChatEventTerminal2[ChatEventTerminal2["FailedComplete"] = 2] = "FailedComplete";
596
+ return ChatEventTerminal2;
597
+ })(ChatEventTerminal || {});
598
+ var EVENT_LOOP_HISTORY_REPLAYED = "soothe.lifecycle.loop.history.replayed";
599
+ var EventClassifier = class {
600
+ deliverablePhases;
601
+ minDeliverableRunes;
602
+ thinkingStepEvents;
603
+ treatStatusIdleAsComplete;
604
+ constructor(cfg) {
605
+ if (!cfg.deliverablePhases) {
606
+ throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
607
+ }
608
+ this.deliverablePhases = cfg.deliverablePhases;
609
+ this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
610
+ this.thinkingStepEvents = cfg.thinkingStepEvents;
611
+ this.treatStatusIdleAsComplete = Boolean(cfg.treatStatusIdleAsComplete);
612
+ }
613
+ /**
614
+ * Inspects one decoded event and returns its outcome. `accumulated` is the
615
+ * running assistant text so far, used to pick the final reply when a
616
+ * deliverable event arrives.
617
+ */
618
+ classify(msg, accumulated) {
619
+ return this.processChatEvent(msg, accumulated);
620
+ }
621
+ /**
622
+ * Reports whether a persisted completion_event is user-facing. Uses the
623
+ * configured deliverable phase set; recognizes the protocol output namespace
624
+ * and final_report component as deliverable.
625
+ */
626
+ isDeliverableCompletionEvent(eventType) {
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
+ }
635
+ if (eventType === EventFinalReport) return true;
636
+ if (eventType.startsWith("soothe.protocol.message.")) {
637
+ const phase = eventType.slice("soothe.protocol.message.".length);
638
+ return this.isDeliverableLoopPhase(phase);
639
+ }
640
+ return eventType.includes("soothe.output") && eventType.includes("responded");
641
+ }
642
+ isDeliverableLoopPhase(phase) {
643
+ return this.deliverablePhases.has(phase);
644
+ }
645
+ deliverableResult(content, completionEvent) {
646
+ return { content, terminal: 1 /* DeliverableComplete */, completionEvent };
647
+ }
648
+ continueResult(content) {
649
+ return { content, terminal: 0 /* Continue */ };
650
+ }
651
+ failedResult(err) {
652
+ return { terminal: 2 /* FailedComplete */, err };
653
+ }
654
+ /** Reports whether trimmed assistant text is long enough to persist as final. */
655
+ isSubstantiveAssistantReply(content) {
656
+ return [...content.trim()].length >= this.minDeliverableRunes;
657
+ }
658
+ /**
659
+ * Picks the user-visible reply for a completed query. Only a deliverable
660
+ * terminal result with a recognized completion event yields a final reply.
661
+ */
662
+ resolveDeliverableFinalContent(eventResult, _accumulated) {
663
+ if (eventResult.terminal !== 1 /* DeliverableComplete */) return ["", false];
664
+ if (!this.isDeliverableCompletionEvent(eventResult.completionEvent ?? "")) return ["", false];
665
+ const final = (eventResult.content ?? "").trim();
666
+ if (final) return [final, true];
667
+ return ["", false];
668
+ }
669
+ /** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
670
+ processChatEvent(msg, accumulated) {
671
+ if (!msg || typeof msg !== "object") {
672
+ return { terminal: 0 /* Continue */ };
673
+ }
674
+ const m = msg;
675
+ const typ = m.type;
676
+ if (typ === "next") {
677
+ return this.classifyNextEnvelope(m, accumulated);
678
+ }
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
+ }
686
+ return { terminal: 0 /* Continue */ };
687
+ }
688
+ if (typ === "error") {
689
+ const errObj = m.error ?? {};
690
+ const code = typeof errObj.code === "number" ? errObj.code : -32603;
691
+ return this.failedResult(
692
+ new DaemonError(code, errObj.message ?? "daemon error", errObj.data)
693
+ );
694
+ }
695
+ if (typ === "event") {
696
+ return this.classifyEventPayload(
697
+ m.namespace ?? null,
698
+ m.mode ?? "",
699
+ m.data
268
700
  );
269
701
  }
702
+ return { terminal: 0 /* Continue */ };
270
703
  }
271
- throw new Error(`timeout after ${timeout}ms waiting for daemon_ready (state=ready)`);
704
+ /** Classifies a `next` envelope by projecting its payload. */
705
+ classifyNextEnvelope(env, accumulated) {
706
+ const payload = env.payload ?? {};
707
+ const innerData = payload.data;
708
+ if (innerData && typeof innerData === "object") {
709
+ const innerType = innerData.type ?? "";
710
+ if (innerType === "status") {
711
+ return this.processChatEvent(innerData, accumulated);
712
+ }
713
+ const innerMode = innerData.mode ?? "";
714
+ if (innerMode) {
715
+ return this.classifyEventPayload(
716
+ innerData.namespace ?? payload.namespace ?? null,
717
+ innerMode,
718
+ innerData.data
719
+ );
720
+ }
721
+ }
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
+ }
728
+ if (mode) {
729
+ return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
730
+ }
731
+ return { terminal: 0 /* Continue */ };
732
+ }
733
+ /**
734
+ * Classifies an event payload by (namespace, mode, phase). `data` may be a
735
+ * map or an array of messages (mode="messages").
736
+ */
737
+ classifyEventPayload(namespace, mode, data) {
738
+ const ns = namespaceToString(namespace);
739
+ const dataMap = normalizeEventData(data);
740
+ if (dataMap) {
741
+ let dataType2 = ns;
742
+ const dt2 = dataMap["type"];
743
+ if (typeof dt2 === "string" && dt2) dataType2 = dt2;
744
+ if (dataType2 === EVENT_LOOP_HISTORY_REPLAYED) {
745
+ return { terminal: 0 /* Continue */ };
746
+ }
747
+ const [step, ok] = extractThinkingStep(dataType2, dataMap, this.thinkingStepEvents);
748
+ if (ok) {
749
+ return { thinkingStep: step, terminal: 0 /* Continue */ };
750
+ }
751
+ }
752
+ if (mode === "messages") {
753
+ const result = this.classifyMessagesMode(data, ns);
754
+ if (result) return result;
755
+ }
756
+ if (!dataMap) {
757
+ return { terminal: 0 /* Continue */ };
758
+ }
759
+ let dataType = ns;
760
+ const dt = dataMap["type"];
761
+ if (typeof dt === "string" && dt) dataType = dt;
762
+ let completionEvent = dataType;
763
+ if (!completionEvent) completionEvent = ns;
764
+ if (isNamespaceMatch(ns, dataType, "soothe.output") || isNamespaceMatch(ns, dataType, "responded")) {
765
+ const [content, ok] = extractContentFromData(dataMap);
766
+ if (ok) {
767
+ if (this.isFinalOutputEvent(dataType, ns)) {
768
+ return this.deliverableResult(content, completionEvent);
769
+ }
770
+ return this.continueResult(content);
771
+ }
772
+ }
773
+ if (isNamespaceMatch(ns, dataType, "agent_loop.completed") || isNamespaceMatch(ns, dataType, "agent_loop.reasoned") || isNamespaceMatch(ns, dataType, "loop.completed")) {
774
+ const [content, ok] = extractContentFromData(dataMap);
775
+ if (ok) return this.continueResult(content);
776
+ }
777
+ if (isNamespaceMatch(ns, dataType, "final_report")) {
778
+ const [content, ok] = extractContentFromData(dataMap);
779
+ if (ok) return this.deliverableResult(content, completionEvent);
780
+ }
781
+ if (dataType.includes("soothe.error.") || ns.includes("soothe.error.")) {
782
+ const errType = dataType || ns;
783
+ const msg = dataMap["message"];
784
+ if (typeof msg === "string" && msg) {
785
+ return this.failedResult(new Error(`${errType}: ${msg}`));
786
+ }
787
+ const [content, ok] = extractContentFromData(dataMap);
788
+ if (ok) return this.failedResult(new Error(`${errType}: ${content}`));
789
+ return this.failedResult(new Error(errType));
790
+ }
791
+ if (isNamespaceMatch(ns, dataType, "stream") || isNamespaceMatch(ns, dataType, "progress") || isNamespaceMatch(ns, dataType, "tool_call_updates_batch") || isNamespaceMatch(ns, dataType, "soothe.stream.tool_call.update")) {
792
+ const delta = dataMap["delta"];
793
+ if (typeof delta === "string") return this.continueResult(delta);
794
+ }
795
+ if (isNamespaceMatch(ns, dataType, "heartbeat") || isNamespaceMatch(ns, dataType, "system.daemon") || isNamespaceMatch(ns, dataType, "agent_loop.started") || isNamespaceMatch(ns, dataType, "intent.classified")) {
796
+ return { terminal: 0 /* Continue */ };
797
+ }
798
+ return { terminal: 0 /* Continue */ };
799
+ }
800
+ /** Classifies a mode="messages" payload (array of message objects). */
801
+ classifyMessagesMode(data, _ns) {
802
+ const items = Array.isArray(data) ? data : null;
803
+ if (!items || items.length === 0) return null;
804
+ const first = items[0];
805
+ if (!first || typeof first !== "object") return null;
806
+ const [msgType, rawContent, phase, hasPayload] = firstMessagePayload(data);
807
+ if (hasPayload && rawContent && isStreamingMessageType(msgType)) {
808
+ return this.continueResult(rawContent);
809
+ }
810
+ const loopMsg = loopAIMessage(data);
811
+ if (loopMsg) {
812
+ const content = loopMsg.content;
813
+ if (content) {
814
+ if (isStreamingMessageType(loopMsg.type)) {
815
+ return this.continueResult(content);
816
+ }
817
+ if (this.isDeliverableLoopPhase(loopMsg.phase) && this.isSubstantiveAssistantReply(content)) {
818
+ return this.deliverableResult(content, "soothe.protocol.message." + loopMsg.phase);
819
+ }
820
+ return this.continueResult(content);
821
+ }
822
+ }
823
+ const [directContent, directOk] = this.messagesModeAssistantContent(data);
824
+ if (directOk && this.isSubstantiveAssistantReply(directContent)) {
825
+ return this.deliverableResult(directContent, "soothe.protocol.message.direct_model");
826
+ }
827
+ if (hasPayload && rawContent) {
828
+ if (isTerminalMessageType(msgType) || msgType === "") {
829
+ if (this.isDeliverableLoopPhase(phase) && this.isSubstantiveAssistantReply(rawContent)) {
830
+ return this.deliverableResult(rawContent, "soothe.protocol.message." + phase);
831
+ }
832
+ return this.continueResult(rawContent);
833
+ }
834
+ return this.continueResult(rawContent);
835
+ }
836
+ return null;
837
+ }
838
+ /**
839
+ * Extracts plain assistant text from mode="messages" events that carry a
840
+ * terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
841
+ * before phase tagging; prefer deliverablePhases including text_completion).
842
+ */
843
+ messagesModeAssistantContent(data) {
844
+ if (!Array.isArray(data) || data.length === 0) return ["", false];
845
+ const msgMap = data[0];
846
+ if (!msgMap || typeof msgMap !== "object") return ["", false];
847
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
848
+ if (phase) return ["", false];
849
+ const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
850
+ if (msgType && !isTerminalMessageType(msgType)) return ["", false];
851
+ const content = extractContentFromMessage(msgMap).trim();
852
+ if (!content) return ["", false];
853
+ return [content, true];
854
+ }
855
+ /** soothe output/responded events that carry user-facing final text. */
856
+ isFinalOutputEvent(dataType, ns) {
857
+ const combined = dataType + " " + ns;
858
+ if (combined.includes("final_report")) return true;
859
+ for (const phase of this.deliverablePhases) {
860
+ if (combined.includes(phase)) return true;
861
+ }
862
+ return false;
863
+ }
864
+ };
865
+ function isStreamingMessageType(msgType) {
866
+ return msgType === "AIMessageChunk" || msgType === "ai_chunk" || msgType === "message_chunk";
272
867
  }
273
- async function waitLoopStatusWithID(client, timeout) {
274
- const deadline = Date.now() + timeout;
275
- while (Date.now() < deadline) {
276
- const remaining = deadline - Date.now();
277
- if (remaining <= 0) break;
278
- const ev = await client.readEventWithTimeout(remaining);
279
- if (ev === null) break;
280
- if (ev.type === "error") {
281
- const errResp = ev;
282
- throw new Error(`daemon error: ${errResp.code}: ${errResp.message}`);
868
+ function isTerminalMessageType(msgType) {
869
+ return msgType === "AIMessage" || msgType === "ai" || msgType === "assistant";
870
+ }
871
+ function firstMessagePayload(data) {
872
+ if (!Array.isArray(data) || data.length === 0) return ["", "", "", false];
873
+ const msgMap = data[0];
874
+ if (!msgMap || typeof msgMap !== "object") return ["", "", "", false];
875
+ const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
876
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase : "";
877
+ const content = extractContentFromMessage(msgMap);
878
+ return [msgType, content, phase, true];
879
+ }
880
+ function loopAIMessage(data) {
881
+ if (!Array.isArray(data) || data.length === 0) return null;
882
+ const msgMap = data[0];
883
+ if (!msgMap || typeof msgMap !== "object") return null;
884
+ const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
885
+ if (!phase) return null;
886
+ const type = typeof msgMap.type === "string" ? msgMap.type : "";
887
+ const content = extractContentFromMessage(msgMap);
888
+ return { type, content, phase };
889
+ }
890
+ function extractContentFromMessage(msgMap) {
891
+ const c = msgMap.content;
892
+ if (typeof c === "string" && c) return c;
893
+ if (Array.isArray(c) && c.length > 0) {
894
+ let b = "";
895
+ for (const item of c) {
896
+ if (typeof item === "string") {
897
+ b += item;
898
+ continue;
899
+ }
900
+ if (item && typeof item === "object") {
901
+ const blk = item;
902
+ const t = blk.text;
903
+ if (typeof t === "string") b += t;
904
+ }
283
905
  }
284
- if (ev.type === "status") {
285
- const status = ev;
286
- const lid = status.loop_id;
287
- if (lid && lid !== "") {
288
- return status;
906
+ return b;
907
+ }
908
+ const blocks = msgMap.content_blocks;
909
+ if (Array.isArray(blocks) && blocks.length > 0) {
910
+ let b = "";
911
+ for (const blk of blocks) {
912
+ if (blk && typeof blk === "object") {
913
+ const m = blk;
914
+ const t = m.text;
915
+ if (typeof t === "string") b += t;
289
916
  }
290
917
  }
918
+ return b;
291
919
  }
292
- throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
920
+ return "";
293
921
  }
294
- async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
295
- const deadline = Date.now() + timeout;
296
- while (Date.now() < deadline) {
297
- const remaining = deadline - Date.now();
298
- if (remaining <= 0) break;
299
- const ev = await client.readEventWithTimeout(remaining);
300
- if (ev === null) break;
301
- if (ev.type === "loop_subscribe_response" && ev.success === true) {
302
- if (String(ev.loop_id ?? "") === wantLoopID) return;
922
+ function extractContentFromData(data) {
923
+ for (const key of [
924
+ "final_stdout_message",
925
+ "completion_summary",
926
+ "content",
927
+ "text",
928
+ "response",
929
+ "output",
930
+ "message",
931
+ "report"
932
+ ]) {
933
+ const val = data[key];
934
+ if (typeof val === "string" && val) return [val, true];
935
+ }
936
+ const nested = data.data;
937
+ if (nested && typeof nested === "object") {
938
+ const nm = nested;
939
+ for (const key of [
940
+ "final_stdout_message",
941
+ "completion_summary",
942
+ "content",
943
+ "text",
944
+ "response",
945
+ "output",
946
+ "message",
947
+ "report"
948
+ ]) {
949
+ const val = nm[key];
950
+ if (typeof val === "string" && val) return [val, true];
303
951
  }
304
- if (ev.type === "subscription_confirmed") {
305
- const lid = String(ev.loop_id ?? "");
306
- if (lid === wantLoopID) return;
952
+ }
953
+ return ["", false];
954
+ }
955
+ function isNamespaceMatch(ns, dataType, pattern) {
956
+ return dataType.includes(pattern) || ns.includes(pattern);
957
+ }
958
+ function namespaceToString(namespace) {
959
+ if (typeof namespace === "string") return namespace;
960
+ if (Array.isArray(namespace)) return namespace.filter((s) => typeof s === "string").join(".");
961
+ return "";
962
+ }
963
+ function normalizeEventData(data) {
964
+ if (data == null) return null;
965
+ if (typeof data === "object" && !Array.isArray(data)) {
966
+ return data;
967
+ }
968
+ if (typeof data === "string") {
969
+ try {
970
+ const m = JSON.parse(data);
971
+ if (m && typeof m === "object" && !Array.isArray(m)) return m;
972
+ } catch {
973
+ return null;
307
974
  }
308
975
  }
309
- throw new Error(`timeout after ${timeout}ms waiting for subscription_confirmed`);
976
+ return null;
310
977
  }
311
- async function connectWithRetries(client, maxRetries, retryDelay) {
312
- const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
313
- const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
314
- let lastErr = null;
315
- for (let attempt = 0; attempt < retries; attempt++) {
978
+
979
+ // src/appkit/query_gate.ts
980
+ var ErrQueryBusy = class extends Error {
981
+ constructor() {
982
+ super("appkit: query already in progress for session");
983
+ this.name = "ErrQueryBusy";
984
+ }
985
+ };
986
+ var QueryGate = class {
987
+ active = /* @__PURE__ */ new Map();
988
+ /** Constructs an empty gate. */
989
+ constructor() {
990
+ }
991
+ /**
992
+ * Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
993
+ * already in flight. `abort` is the AbortController for the query's timeout
994
+ * context. `sendCancel` is the daemon-cancel sender; it is invoked from
995
+ * `cancel()` on a detached 10s timeout.
996
+ */
997
+ acquire(sessionID, abort, sendCancel) {
998
+ if (this.active.has(sessionID)) {
999
+ throw new ErrQueryBusy();
1000
+ }
1001
+ this.active.set(sessionID, { abort, sendCancel });
1002
+ }
1003
+ /**
1004
+ * Cooperatively stops a running query for sessionID. Sends the daemon cancel
1005
+ * (on a detached 10s-timeout abort so caller cancellation cannot block the
1006
+ * wire send) BEFORE aborting the local context. Returns silently if no query
1007
+ * is in flight (intent already satisfied).
1008
+ */
1009
+ async cancel(sessionID) {
1010
+ const state = this.active.get(sessionID);
1011
+ if (!state) return;
1012
+ this.active.delete(sessionID);
1013
+ if (state.sendCancel) {
1014
+ const detached = new AbortController();
1015
+ const timer = setTimeout(() => detached.abort(), 1e4);
1016
+ try {
1017
+ await state.sendCancel(detached.signal);
1018
+ } catch {
1019
+ } finally {
1020
+ clearTimeout(timer);
1021
+ }
1022
+ }
1023
+ state.abort.abort();
1024
+ }
1025
+ /**
1026
+ * Clears the gate for sessionID without sending a daemon cancel. Call when a
1027
+ * query completes normally (success or local failure) so the next turn can
1028
+ * acquire.
1029
+ */
1030
+ release(sessionID) {
1031
+ this.active.delete(sessionID);
1032
+ }
1033
+ /** Reports whether a query is in flight for sessionID. */
1034
+ isActive(sessionID) {
1035
+ return this.active.has(sessionID);
1036
+ }
1037
+ };
1038
+
1039
+ // src/appkit/client.ts
1040
+ function defaultClientFactory() {
1041
+ return (url, config) => {
1042
+ return new Client(url, config ?? defaultConfig());
1043
+ };
1044
+ }
1045
+ function defaultBootstrapFunc() {
1046
+ return async (client, workspaceID, userID, config) => {
1047
+ const c = client;
1048
+ const opts = {
1049
+ client_workspace: workspaceID,
1050
+ user_id: userID,
1051
+ client_workspace_id: workspaceID
1052
+ };
1053
+ return bootstrapLoopSession(c, "", config, opts);
1054
+ };
1055
+ }
1056
+
1057
+ // src/appkit/pool.ts
1058
+ var ErrPoolExhausted = class extends Error {
1059
+ constructor() {
1060
+ super("appkit: connection pool exhausted");
1061
+ this.name = "ErrPoolExhausted";
1062
+ }
1063
+ };
1064
+ function defaultPoolConfig() {
1065
+ return {
1066
+ poolSize: 1e3,
1067
+ queryTimeout: 30 * 60 * 1e3,
1068
+ connectionTimeout: 3e4,
1069
+ maxIdleTime: 10 * 60 * 1e3,
1070
+ healthCheckInterval: 3e4
1071
+ };
1072
+ }
1073
+ var PooledConn = class {
1074
+ slotID;
1075
+ client;
1076
+ eventStream = null;
1077
+ streamController = null;
1078
+ sessionID = "";
1079
+ loopID = "";
1080
+ workspaceID = "";
1081
+ lastUsed = 0;
1082
+ constructor(slotID, client) {
1083
+ this.slotID = slotID;
1084
+ this.client = client;
1085
+ }
1086
+ /** Reports whether the underlying client signalled a drop. */
1087
+ isDisconnected() {
1088
+ return this.client.isDisconnected();
1089
+ }
1090
+ isConnected() {
1091
+ return this.client.isConnected() && !this.isDisconnected();
1092
+ }
1093
+ getLoopID() {
1094
+ return this.loopID;
1095
+ }
1096
+ };
1097
+ var ConnectionPool = class {
1098
+ cfg;
1099
+ scfg;
1100
+ factory;
1101
+ bootstrap;
1102
+ store;
1103
+ pool = [];
1104
+ activeSlots = /* @__PURE__ */ new Map();
1105
+ nextSlotID = 1;
1106
+ url;
1107
+ /**
1108
+ * Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
1109
+ * defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
1110
+ * factory/bootstrap fall back to the defaults.
1111
+ */
1112
+ constructor(url, store, cfg, scfg, factory) {
1113
+ this.cfg = cfg ?? defaultPoolConfig();
1114
+ this.scfg = scfg ?? defaultConfig();
1115
+ this.factory = factory ?? defaultClientFactory();
1116
+ this.bootstrap = defaultBootstrapFunc();
1117
+ this.store = store;
1118
+ this.url = url;
1119
+ for (let i = 0; i < this.cfg.poolSize; i++) {
1120
+ this.pool.push(new PooledConn(this.nextSlotID++, this.factory(url, this.scfg)));
1121
+ }
1122
+ }
1123
+ /** Overrides the loop bootstrap function (useful for test fakes). */
1124
+ withBootstrap(f) {
1125
+ if (f) this.bootstrap = f;
1126
+ return this;
1127
+ }
1128
+ /**
1129
+ * Returns a live connection for sessionID, reusing an active slot or
1130
+ * bootstrapping/reattaching as needed. The caller must call `release()`
1131
+ * when done with the connection (a turn completes or the session is reset).
1132
+ */
1133
+ async acquire(sessionID, workspaceID, userID, _signal) {
1134
+ const existing = this.activeSlots.get(sessionID);
1135
+ if (existing) {
1136
+ if (existing.isDisconnected() || !existing.isConnected()) {
1137
+ await this.release(sessionID);
1138
+ } else {
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
+ }
1148
+ }
1149
+ }
1150
+ const conn = this.pool.pop();
1151
+ if (!conn) throw new ErrPoolExhausted();
1152
+ this.activeSlots.set(sessionID, conn);
1153
+ const { loopID, ok } = await this.store.getLoopIDForSession(sessionID).catch(() => ({ loopID: "", ok: false }));
1154
+ let finalLoopID = "";
316
1155
  try {
317
- await client.connect();
318
- return;
1156
+ if (!ok || !loopID) {
1157
+ await conn.client.connect();
1158
+ finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
1159
+ await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
1160
+ });
1161
+ } else {
1162
+ try {
1163
+ await this.resumeAndReattach(conn, loopID);
1164
+ finalLoopID = loopID;
1165
+ } catch {
1166
+ await conn.client.connect();
1167
+ finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
1168
+ await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
1169
+ });
1170
+ }
1171
+ }
319
1172
  } catch (err) {
320
- lastErr = err;
1173
+ await this.release(sessionID);
1174
+ throw err;
321
1175
  }
322
- await new Promise((resolve) => setTimeout(resolve, delay));
1176
+ conn.sessionID = sessionID;
1177
+ conn.loopID = finalLoopID;
1178
+ conn.workspaceID = workspaceID;
1179
+ conn.lastUsed = Date.now();
1180
+ await this.store.updateLastUsed(sessionID).catch(() => {
1181
+ });
1182
+ return conn;
1183
+ }
1184
+ /** Tears down the connection for sessionID and returns the slot. */
1185
+ async release(sessionID) {
1186
+ const conn = this.activeSlots.get(sessionID);
1187
+ if (!conn) return;
1188
+ this.activeSlots.delete(sessionID);
1189
+ if (conn.streamController) {
1190
+ conn.streamController.abort();
1191
+ conn.streamController = null;
1192
+ }
1193
+ try {
1194
+ conn.client.close();
1195
+ } catch {
1196
+ }
1197
+ conn.sessionID = "";
1198
+ conn.loopID = "";
1199
+ conn.eventStream = null;
1200
+ this.pool.push(new PooledConn(this.nextSlotID++, this.factory(this.url, this.scfg)));
1201
+ }
1202
+ /**
1203
+ * Tears down the connection for sessionID so the next acquire bootstraps
1204
+ * fresh. The store should archive the loop id so getLoopIDForSession returns
1205
+ * false next time.
1206
+ */
1207
+ async resetSession(sessionID) {
1208
+ await this.release(sessionID);
1209
+ }
1210
+ /** Gracefully shuts down all active connections. */
1211
+ stop() {
1212
+ for (const [sid, conn] of this.activeSlots) {
1213
+ if (conn.streamController) conn.streamController.abort();
1214
+ try {
1215
+ conn.client.close();
1216
+ } catch {
1217
+ }
1218
+ this.activeSlots.delete(sid);
1219
+ }
1220
+ }
1221
+ /** Stats snapshot for observability. */
1222
+ stats() {
1223
+ return { active: this.activeSlots.size, idle: this.pool.length };
1224
+ }
1225
+ /** Bootstrap a fresh loop and start the reader. */
1226
+ async bootstrapNew(conn, workspaceID, userID) {
1227
+ const loopID = await this.bootstrap(conn.client, workspaceID, userID, this.scfg);
1228
+ this.startReader(conn);
1229
+ return loopID;
1230
+ }
1231
+ /** Reconnect + reattach an existing loop, then start the reader. */
1232
+ async resumeAndReattach(conn, loopID) {
1233
+ await conn.client.connect();
1234
+ try {
1235
+ await conn.client.reattachAndProbe(loopID);
1236
+ } catch (err) {
1237
+ if (err instanceof StaleLoopError) throw err;
1238
+ throw err;
1239
+ }
1240
+ this.startReader(conn);
323
1241
  }
324
- throw new Error(`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`);
1242
+ /** Starts a receiveMessages generator and stores the stream + controller. */
1243
+ startReader(conn) {
1244
+ const controller = new AbortController();
1245
+ conn.streamController = controller;
1246
+ conn.eventStream = conn.client.receiveMessages(controller.signal);
1247
+ }
1248
+ };
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
+ };
325
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
+
1332
+ // src/appkit/turn_runner.ts
1333
+ var ErrQueryTimeout = class extends Error {
1334
+ constructor() {
1335
+ super("appkit: query timeout");
1336
+ this.name = "ErrQueryTimeout";
1337
+ }
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 */;
1352
+ function inputMessageForLoop(text, loopID, attachments, opts) {
1353
+ const msg = { type: "loop_input", content: text };
1354
+ if (loopID) msg.loop_id = loopID;
1355
+ if (attachments && attachments.length > 0) msg.attachments = attachments;
1356
+ if (opts) {
1357
+ if (opts.intentHint?.trim()) {
1358
+ const hintError = validateLoopInputIntentHint(opts.intentHint);
1359
+ if (hintError) {
1360
+ throw new Error(hintError);
1361
+ }
1362
+ msg.intent_hint = opts.intentHint.trim();
1363
+ }
1364
+ if (opts.preferredSubagent?.trim()) msg.preferred_subagent = opts.preferredSubagent.trim();
1365
+ if (opts.responseSchema && Object.keys(opts.responseSchema).length > 0) {
1366
+ msg.response_schema = opts.responseSchema;
1367
+ }
1368
+ if (opts.responseSchemaName?.trim()) msg.response_schema_name = opts.responseSchemaName.trim();
1369
+ if (opts.responseSchemaStrict !== void 0)
1370
+ msg.response_schema_strict = opts.responseSchemaStrict;
1371
+ }
1372
+ return msg;
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
+ }
1381
+ var TurnRunner = class {
1382
+ pool;
1383
+ gate;
1384
+ classifier;
1385
+ store;
1386
+ broadcaster;
1387
+ cfg;
1388
+ buildInput = inputMessageForLoop;
1389
+ onComplete = null;
1390
+ onError = null;
1391
+ constructor(pool, gate, classifier, store, broadcaster, cfg) {
1392
+ this.pool = pool;
1393
+ this.gate = gate;
1394
+ this.classifier = classifier;
1395
+ this.store = store;
1396
+ this.broadcaster = broadcaster;
1397
+ this.cfg = {
1398
+ ...cfg,
1399
+ queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3
1400
+ };
1401
+ }
1402
+ withInputBuilder(f) {
1403
+ if (f) this.buildInput = f;
1404
+ return this;
1405
+ }
1406
+ withOnComplete(f) {
1407
+ this.onComplete = f;
1408
+ return this;
1409
+ }
1410
+ withOnError(f) {
1411
+ this.onError = f;
1412
+ return this;
1413
+ }
1414
+ async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
1415
+ let conn;
1416
+ try {
1417
+ conn = await this.pool.acquire(sessionID, workspaceID, userID, signal);
1418
+ } catch (err) {
1419
+ await this.persistFailed(sessionID, "", err);
1420
+ this.broadcastError(sessionID, err);
1421
+ this.onError?.(sessionID, "", err);
1422
+ throw err;
1423
+ }
1424
+ const loopID = conn.getLoopID();
1425
+ const timeoutController = new AbortController();
1426
+ const timeoutMs = this.cfg.queryTimeout;
1427
+ const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
1428
+ const sendCancel = async (detachedSignal) => {
1429
+ await this.sendLoopCancel(detachedSignal, conn, loopID);
1430
+ };
1431
+ try {
1432
+ this.gate.acquire(sessionID, timeoutController, sendCancel);
1433
+ } catch (err) {
1434
+ clearTimeout(timer);
1435
+ await this.pool.release(sessionID);
1436
+ await this.persistFailed(sessionID, loopID, err);
1437
+ this.broadcastError(sessionID, err);
1438
+ this.onError?.(sessionID, loopID, err);
1439
+ throw err;
1440
+ }
1441
+ let idleTimer = null;
1442
+ const clearIdle = () => {
1443
+ if (idleTimer) {
1444
+ clearTimeout(idleTimer);
1445
+ idleTimer = null;
1446
+ }
1447
+ };
1448
+ try {
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);
1454
+ try {
1455
+ await conn.client.sendMessage(inputMsg);
1456
+ } catch (err) {
1457
+ await this.persistFailed(sessionID, loopID, err);
1458
+ this.broadcastError(sessionID, err);
1459
+ this.onError?.(sessionID, loopID, err);
1460
+ throw err;
1461
+ }
1462
+ const eventStream = conn.eventStream;
1463
+ if (!eventStream) {
1464
+ const err = new Error(`missing event stream for session ${sessionID} (loop ${loopID})`);
1465
+ await this.persistFailed(sessionID, loopID, err);
1466
+ this.broadcastError(sessionID, err);
1467
+ this.onError?.(sessionID, loopID, err);
1468
+ throw err;
1469
+ }
1470
+ let assistantContent = "";
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();
1489
+ const abortRace = new Promise((resolve) => {
1490
+ const onTimeout = () => resolve("timeout");
1491
+ timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
1492
+ if (signal) {
1493
+ const onCaller = () => resolve("caller");
1494
+ signal.addEventListener("abort", onCaller, { once: true });
1495
+ }
1496
+ });
1497
+ const iterator = eventStream[Symbol.asyncIterator]();
1498
+ while (true) {
1499
+ const next = iterator.next();
1500
+ const raced = await Promise.race([
1501
+ next.then((res2) => ({ tag: "msg", res: res2 })),
1502
+ abortRace.then((tag) => ({ tag })),
1503
+ idleRace.then((tag) => ({ tag }))
1504
+ ]);
1505
+ if ("tag" in raced && raced.tag !== "msg") {
1506
+ if (raced.tag === "caller" || signal?.aborted) {
1507
+ clearIdle();
1508
+ const err = new Error("aborted");
1509
+ await this.persistFailed(sessionID, loopID, err);
1510
+ this.broadcastError(sessionID, err);
1511
+ this.onError?.(sessionID, loopID, err);
1512
+ throw err;
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();
1530
+ await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
1531
+ });
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;
1542
+ }
1543
+ const res = raced.res;
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
+ }
1556
+ const err = new Error("event stream closed");
1557
+ await this.persistFailed(sessionID, loopID, err);
1558
+ this.broadcastError(sessionID, err);
1559
+ this.onError?.(sessionID, loopID, err);
1560
+ throw err;
1561
+ }
1562
+ idleRace = armIdle();
1563
+ const msg = res.value;
1564
+ const eventResult = this.classifier.classify(msg, assistantContent);
1565
+ if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
1566
+ clearIdle();
1567
+ await this.persistFailed(sessionID, loopID, eventResult.err);
1568
+ this.broadcastError(sessionID, eventResult.err);
1569
+ this.onError?.(sessionID, loopID, eventResult.err);
1570
+ throw eventResult.err;
1571
+ }
1572
+ const step = (eventResult.thinkingStep ?? "").trim();
1573
+ if (step) this.broadcastThinkingStep(sessionID, step);
1574
+ if (eventResult.content) {
1575
+ if (eventResult.content.startsWith(assistantContent)) {
1576
+ assistantContent = eventResult.content;
1577
+ } else {
1578
+ assistantContent += eventResult.content;
1579
+ }
1580
+ }
1581
+ const [final, deliverable] = this.classifier.resolveDeliverableFinalContent(
1582
+ eventResult,
1583
+ assistantContent
1584
+ );
1585
+ if (deliverable) {
1586
+ clearIdle();
1587
+ await this.completeTurn(
1588
+ sessionID,
1589
+ loopID,
1590
+ final,
1591
+ startedAt,
1592
+ eventResult.completionEvent ?? ""
1593
+ );
1594
+ return;
1595
+ }
1596
+ }
1597
+ } finally {
1598
+ clearIdle();
1599
+ clearTimeout(timer);
1600
+ this.gate.release(sessionID);
1601
+ }
1602
+ }
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
+ }
1619
+ async sendLoopCancel(_signal, conn, loopID) {
1620
+ const lid = (loopID ?? "").trim();
1621
+ if (!conn || !lid) return;
1622
+ const cancelMsg = { type: "command_request", command: "cancel", loop_id: lid };
1623
+ await conn.client.sendMessage(cancelMsg);
1624
+ }
1625
+ async persistResponse(sessionID, loopID, content, startedAt, completionEvent) {
1626
+ const msg = {
1627
+ role: "assistant",
1628
+ content,
1629
+ metadata: {
1630
+ started_at: startedAt,
1631
+ completed_at: Date.now(),
1632
+ duration_ms: Date.now() - startedAt,
1633
+ status: "completed",
1634
+ completion_event: completionEvent,
1635
+ deliverable: true
1636
+ }
1637
+ };
1638
+ await this.store.appendMessage(sessionID, msg).catch(() => {
1639
+ });
1640
+ }
1641
+ async persistFailed(sessionID, _loopID, err) {
1642
+ const msg = {
1643
+ role: "error",
1644
+ content: err.message,
1645
+ metadata: { status: "failed", error_message: err.message }
1646
+ };
1647
+ await this.store.appendMessage(sessionID, msg).catch(() => {
1648
+ });
1649
+ }
1650
+ broadcastThinkingStep(sessionID, step) {
1651
+ if (!this.broadcaster) return;
1652
+ this.broadcaster.broadcast(sessionID, { type: "delta", data: step + "\n" });
1653
+ }
1654
+ broadcastComplete(sessionID, content) {
1655
+ this.broadcaster?.broadcast(sessionID, { type: "complete", data: content });
1656
+ }
1657
+ broadcastError(sessionID, err) {
1658
+ this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
1659
+ }
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
+ };
326
2109
  export {
2110
+ CLIENT_VERSION,
2111
+ ChatEventTerminal,
327
2112
  Client,
2113
+ CommandClient,
328
2114
  ConnectionError,
2115
+ ConnectionPool,
2116
+ DEFAULT_CLIENT_CAPABILITIES,
2117
+ DEFAULT_DELIVERABLE_PHASES,
2118
+ DEFAULT_POST_IDLE_DRAIN_MS,
2119
+ DEFAULT_THINKING_STEP_EVENTS,
329
2120
  DaemonError,
330
- ESSENTIAL_EVENT_TYPES,
331
- EventAgentLoopCompleted,
332
- EventAgentLoopIterated,
333
- EventAgentLoopReasoned,
334
- EventAgentLoopStarted,
2121
+ DaemonSession,
2122
+ DisconnectCause,
2123
+ ErrIdleTimeout,
2124
+ ErrPoolExhausted,
2125
+ ErrQueryBusy,
2126
+ ErrQueryTimeout,
2127
+ EventAutopilotGoalCompleted,
2128
+ EventAutopilotGoalCreated,
2129
+ EventAutopilotGoalProgress,
2130
+ EventAutopilotGoalStatus,
2131
+ EventAutopilotWorkerAssigned,
2132
+ EventAutopilotWorkerUnassigned,
2133
+ EventCardCreated,
2134
+ EventCardReplayBegin,
2135
+ EventCardReplayEnd,
2136
+ EventClassifier,
335
2137
  EventExploreCompleted,
336
2138
  EventExploreMilestone,
337
2139
  EventExploreStarted,
@@ -343,6 +2145,14 @@ export {
343
2145
  EventMessageSent,
344
2146
  EventPlanCreated,
345
2147
  EventReplayComplete,
2148
+ EventStrangeLoopCompleted,
2149
+ EventStrangeLoopContextCompacted,
2150
+ EventStrangeLoopPlanDecision,
2151
+ EventStrangeLoopReasoned,
2152
+ EventStrangeLoopStarted,
2153
+ EventStrangeLoopStepCompleted,
2154
+ EventStrangeLoopStepQueued,
2155
+ EventStrangeLoopStepStarted,
346
2156
  EventStreamToolCallUpdate,
347
2157
  EventTacitusCompleted,
348
2158
  EventTacitusGatherSummary,
@@ -351,31 +2161,76 @@ export {
351
2161
  EventToolCompleted,
352
2162
  EventToolError,
353
2163
  EventToolStarted,
2164
+ INTENT_HINT_EMBED,
2165
+ INTENT_HINT_IMAGE_TO_TEXT,
2166
+ INTENT_HINT_OCR,
2167
+ INTENT_HINT_TEXT_COMPLETION,
2168
+ LOOP_ASSISTANT_OUTPUT_PHASES,
2169
+ PROTO_VERSION,
2170
+ PooledConn,
2171
+ QueryGate,
2172
+ REMOVED_INTENT_HINTS,
2173
+ ReconnectError,
2174
+ SSEBroadcaster,
2175
+ STREAM_END,
2176
+ StaleLoopError,
2177
+ StreamCloseFail,
2178
+ StreamCloseSoftComplete,
354
2179
  TimeoutError,
2180
+ TimeoutPolicy,
2181
+ TurnEventStats,
2182
+ TurnRunner,
355
2183
  VerbosityTier,
2184
+ authenticate,
356
2185
  bootstrapLoopSession,
357
2186
  checkDaemonStatus,
358
2187
  classifyEventVerbosity,
2188
+ compactAttachments,
2189
+ compactImageAttachment,
359
2190
  connectWithRetries,
2191
+ connectedWebsocket,
2192
+ connectionInitEnvelope,
360
2193
  decodeMessage,
361
2194
  defaultConfig,
2195
+ defaultPoolConfig,
2196
+ disconnectCauseName,
2197
+ disconnectEnvelope,
362
2198
  encodeMessage,
363
2199
  extractSootheLoopID,
2200
+ extractThinkingStep,
364
2201
  fetchConfigSection,
2202
+ fetchLoopCards,
2203
+ fetchLoopHistory,
2204
+ fetchLoopMessages,
365
2205
  fetchSkillsCatalog,
2206
+ idleTimeoutForTurn,
2207
+ inboundNeedsDeliveryAck,
2208
+ inputMessageForLoop,
366
2209
  isCompletionEvent,
367
2210
  isDaemonLive,
368
2211
  isSubagentProgressEvent,
2212
+ isTurnEndCustomData,
2213
+ isTurnProgressChunk,
369
2214
  isValidVerbosityLevel,
370
2215
  loadConfigFromEnv,
371
2216
  newLoopInputMessage,
372
2217
  newLoopNewMessage,
373
2218
  newLoopSubscribeMessage,
374
2219
  newRequestID,
2220
+ notificationEnvelope,
375
2221
  parseNamespace,
2222
+ pingEnvelope,
2223
+ pongEnvelope,
2224
+ protocol1Rpc,
2225
+ refreshAuthToken,
2226
+ requestDaemonConfigReload,
376
2227
  requestDaemonShutdown,
2228
+ requestEnvelope,
377
2229
  shouldShow,
378
2230
  splitWirePayload,
2231
+ subscribeEnvelope,
2232
+ unsubscribeEnvelope,
2233
+ validateLoopInputIntentHint,
379
2234
  waitDaemonReady,
380
2235
  waitLoopStatusWithID,
381
2236
  waitSubscriptionConfirmed