@alexkroman1/aai 1.4.4 → 1.4.5
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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +7 -0
- package/dist/host/runtime-barrel.js +41 -6
- package/dist/host/s2s.d.ts +6 -0
- package/host/s2s.test.ts +20 -0
- package/host/s2s.ts +27 -2
- package/host/session.test.ts +49 -0
- package/host/session.ts +37 -3
- package/package.json +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @alexkroman1/aai@1.4.
|
|
2
|
+
> @alexkroman1/aai@1.4.5 build /home/runner/work/agent/agent/packages/aai
|
|
3
3
|
> tsdown && tsc -p tsconfig.build.json
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m [34mtsdown v0.21.7[39m powered by [38;2;255;126;23mrolldown v1.0.0-rc.12[39m
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[34mℹ[39m target: [34mnode22[39m
|
|
9
9
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
10
10
|
[34mℹ[39m Build start
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mhost/runtime-barrel.js[22m [
|
|
11
|
+
[34mℹ[39m [2mdist/[22m[1mhost/runtime-barrel.js[22m [2m77.48 kB[22m [2m│ gzip: 23.08 kB[22m
|
|
12
12
|
[34mℹ[39m [2mdist/[22m[1msdk/protocol.js[22m [2m 4.75 kB[22m [2m│ gzip: 1.76 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22m[1mindex.js[22m [2m 2.88 kB[22m [2m│ gzip: 1.24 kB[22m
|
|
14
14
|
[34mℹ[39m [2mdist/[22m[1msdk/manifest-barrel.js[22m [2m 0.36 kB[22m [2m│ gzip: 0.20 kB[22m
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
[34mℹ[39m [2mdist/[22massemblyai-Cxg9eobY.js [2m 0.53 kB[22m [2m│ gzip: 0.35 kB[22m
|
|
23
23
|
[34mℹ[39m [2mdist/[22manthropic-BrUCPKUc.js [2m 0.23 kB[22m [2m│ gzip: 0.18 kB[22m
|
|
24
24
|
[34mℹ[39m [2mdist/[22mcartesia-DwDk2tEu.js [2m 0.22 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
25
|
-
[34mℹ[39m 14 files, total:
|
|
26
|
-
[32m✔[39m Build complete in [
|
|
25
|
+
[34mℹ[39m 14 files, total: 102.30 kB
|
|
26
|
+
[32m✔[39m Build complete in [32m49ms[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @alexkroman1/aai
|
|
2
2
|
|
|
3
|
+
## 1.4.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 07dc8fb: Log raw reply.done arrivals from the S2S service (sid, status) and warn when the S2S socket closes while a reply is still active, so silent drops are visible server-side.
|
|
8
|
+
- 2ca5d1f: Instrument slow reply_done dispatches with warn-level logs (session id, duration, hadTurnPromise) to help diagnose event-loop starvation under load.
|
|
9
|
+
|
|
3
10
|
## 1.4.4
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
@@ -1326,7 +1326,7 @@ function parseS2sMessage(obj) {
|
|
|
1326
1326
|
const result = S2sMessageSchema.safeParse(obj);
|
|
1327
1327
|
return result.success ? result.data : void 0;
|
|
1328
1328
|
}
|
|
1329
|
-
function dispatchS2sMessage(emitter, msg, state) {
|
|
1329
|
+
function dispatchS2sMessage(emitter, msg, state, dispatchCtx) {
|
|
1330
1330
|
switch (msg.type) {
|
|
1331
1331
|
case "session.ready":
|
|
1332
1332
|
emitter.emit("ready", { sessionId: msg.session_id });
|
|
@@ -1369,6 +1369,10 @@ function dispatchS2sMessage(emitter, msg, state) {
|
|
|
1369
1369
|
});
|
|
1370
1370
|
break;
|
|
1371
1371
|
case "reply.done":
|
|
1372
|
+
dispatchCtx.log.info("S2S << reply.done", {
|
|
1373
|
+
...dispatchCtx.sid !== void 0 ? { sid: dispatchCtx.sid } : {},
|
|
1374
|
+
status: msg.status ?? "completed"
|
|
1375
|
+
});
|
|
1372
1376
|
if (msg.status === "interrupted") emitter.emit("event", { type: "cancelled" });
|
|
1373
1377
|
else emitter.emit("event", { type: "reply_done" });
|
|
1374
1378
|
break;
|
|
@@ -1383,12 +1387,16 @@ function dispatchS2sMessage(emitter, msg, state) {
|
|
|
1383
1387
|
}
|
|
1384
1388
|
}
|
|
1385
1389
|
function connectS2s(opts) {
|
|
1386
|
-
const { apiKey, config, createWebSocket, logger: log = consoleLogger } = opts;
|
|
1390
|
+
const { apiKey, config, createWebSocket, logger: log = consoleLogger, sid } = opts;
|
|
1387
1391
|
return new Promise((resolve, reject) => {
|
|
1388
1392
|
log.info("S2S connecting", { url: config.wssUrl });
|
|
1389
1393
|
const ws = createWebSocket(config.wssUrl, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
1390
1394
|
const emitter = createNanoEvents();
|
|
1391
1395
|
const dispatchState = { speechActive: false };
|
|
1396
|
+
const dispatchCtx = sid !== void 0 ? {
|
|
1397
|
+
log,
|
|
1398
|
+
sid
|
|
1399
|
+
} : { log };
|
|
1392
1400
|
let opened = false;
|
|
1393
1401
|
function send(msg) {
|
|
1394
1402
|
if (ws.readyState !== 1) {
|
|
@@ -1468,6 +1476,7 @@ function connectS2s(opts) {
|
|
|
1468
1476
|
}
|
|
1469
1477
|
function logIncoming(obj) {
|
|
1470
1478
|
if (obj.type === "reply.audio" || obj.type === "input.audio") return;
|
|
1479
|
+
if (obj.type === "reply.done") return;
|
|
1471
1480
|
log.info(`S2S << ${obj.type}`);
|
|
1472
1481
|
}
|
|
1473
1482
|
function handleS2sMessage(ev) {
|
|
@@ -1485,7 +1494,7 @@ function connectS2s(opts) {
|
|
|
1485
1494
|
log.warn(`S2S << unrecognised message type: ${obj.type ?? JSON.stringify(raw).slice(0, 200)}`);
|
|
1486
1495
|
return;
|
|
1487
1496
|
}
|
|
1488
|
-
dispatchS2sMessage(emitter, parsed, dispatchState);
|
|
1497
|
+
dispatchS2sMessage(emitter, parsed, dispatchState, dispatchCtx);
|
|
1489
1498
|
}
|
|
1490
1499
|
ws.addEventListener("message", handleS2sMessage);
|
|
1491
1500
|
ws.addEventListener("close", (ev) => {
|
|
@@ -1621,12 +1630,22 @@ function handleReplyCancelled(ctx) {
|
|
|
1621
1630
|
ctx.cancelReply();
|
|
1622
1631
|
ctx.client.event({ type: "cancelled" });
|
|
1623
1632
|
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Warn when the entry-to-emit time for a reply_done dispatch exceeds this.
|
|
1635
|
+
* Tool-less sessions should be sub-millisecond; sessions with pending tools
|
|
1636
|
+
* will legitimately spend time awaiting ctx.turnPromise. We log both (with
|
|
1637
|
+
* `hadTurnPromise`) so event-loop starvation is distinguishable from
|
|
1638
|
+
* genuine tool-call latency.
|
|
1639
|
+
*/
|
|
1640
|
+
const REPLY_DONE_SLOW_THRESHOLD_MS = 50;
|
|
1624
1641
|
function handleReplyDone(ctx) {
|
|
1642
|
+
const startMs = Date.now();
|
|
1625
1643
|
const doneReplyId = ctx.reply.currentReplyId;
|
|
1626
1644
|
if (doneReplyId === null) {
|
|
1627
1645
|
ctx.log.debug("Dropping duplicate reply.done (no active reply)");
|
|
1628
1646
|
return;
|
|
1629
1647
|
}
|
|
1648
|
+
const hadTurnPromise = ctx.turnPromise !== null;
|
|
1630
1649
|
const sendPending = () => {
|
|
1631
1650
|
if (ctx.reply.currentReplyId !== doneReplyId) {
|
|
1632
1651
|
ctx.reply.pendingTools = [];
|
|
@@ -1644,9 +1663,16 @@ function handleReplyDone(ctx) {
|
|
|
1644
1663
|
ctx.client.playAudioDone();
|
|
1645
1664
|
ctx.client.event({ type: "reply_done" });
|
|
1646
1665
|
ctx.reply.currentReplyId = null;
|
|
1666
|
+
const durationMs = Date.now() - startMs;
|
|
1667
|
+
if (durationMs >= REPLY_DONE_SLOW_THRESHOLD_MS) ctx.log.warn("slow reply_done dispatch", {
|
|
1668
|
+
sid: ctx.id,
|
|
1669
|
+
agent: ctx.agent,
|
|
1670
|
+
durationMs,
|
|
1671
|
+
hadTurnPromise
|
|
1672
|
+
});
|
|
1647
1673
|
}
|
|
1648
1674
|
};
|
|
1649
|
-
if (
|
|
1675
|
+
if (hadTurnPromise) ctx.turnPromise?.then(sendPending);
|
|
1650
1676
|
else sendPending();
|
|
1651
1677
|
}
|
|
1652
1678
|
function setupListeners(ctx, handle) {
|
|
@@ -1669,7 +1695,15 @@ function setupListeners(ctx, handle) {
|
|
|
1669
1695
|
handle.close();
|
|
1670
1696
|
});
|
|
1671
1697
|
handle.on("close", (code, reason) => {
|
|
1672
|
-
ctx.
|
|
1698
|
+
const activeReplyId = ctx.reply.currentReplyId;
|
|
1699
|
+
if (activeReplyId !== null) ctx.log.warn("S2S closed with active reply", {
|
|
1700
|
+
sid: ctx.id,
|
|
1701
|
+
agent: ctx.agent,
|
|
1702
|
+
activeReplyId,
|
|
1703
|
+
code,
|
|
1704
|
+
reason
|
|
1705
|
+
});
|
|
1706
|
+
else ctx.log.info("S2S closed", {
|
|
1673
1707
|
code,
|
|
1674
1708
|
reason
|
|
1675
1709
|
});
|
|
@@ -1749,7 +1783,8 @@ function createS2sSession(opts) {
|
|
|
1749
1783
|
apiKey,
|
|
1750
1784
|
config: s2sConfig,
|
|
1751
1785
|
createWebSocket,
|
|
1752
|
-
logger: log
|
|
1786
|
+
logger: log,
|
|
1787
|
+
sid: id
|
|
1753
1788
|
});
|
|
1754
1789
|
if (sessionAbort.signal.aborted || generation !== connectGeneration) {
|
|
1755
1790
|
handle.close();
|
package/dist/host/s2s.d.ts
CHANGED
|
@@ -77,5 +77,11 @@ export type ConnectS2sOptions = {
|
|
|
77
77
|
config: S2SConfig;
|
|
78
78
|
createWebSocket: CreateS2sWebSocket;
|
|
79
79
|
logger?: Logger;
|
|
80
|
+
/**
|
|
81
|
+
* Session id attached to diagnostic log lines (e.g. raw `reply.done`
|
|
82
|
+
* arrivals from the S2S service). Optional; logs omit the field when
|
|
83
|
+
* not provided.
|
|
84
|
+
*/
|
|
85
|
+
sid?: string;
|
|
80
86
|
};
|
|
81
87
|
export declare function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle>;
|
package/host/s2s.test.ts
CHANGED
|
@@ -402,6 +402,26 @@ describe("connectS2s", () => {
|
|
|
402
402
|
expect(handler.mock.calls[0]?.[0]).toEqual({ type: "cancelled" });
|
|
403
403
|
});
|
|
404
404
|
|
|
405
|
+
test("reply.done arrival is logged with sid and status", async () => {
|
|
406
|
+
const { raw, createWebSocket, logger } = createTestS2s();
|
|
407
|
+
const infoSpy = vi.fn();
|
|
408
|
+
logger.info = infoSpy;
|
|
409
|
+
const handle = await connectS2s({
|
|
410
|
+
apiKey: "test-key",
|
|
411
|
+
config: s2sConfig,
|
|
412
|
+
createWebSocket,
|
|
413
|
+
logger,
|
|
414
|
+
sid: "sess-abc",
|
|
415
|
+
});
|
|
416
|
+
handle.on("event", vi.fn());
|
|
417
|
+
|
|
418
|
+
raw.emit("message", Buffer.from(JSON.stringify({ type: "reply.done", status: "completed" })));
|
|
419
|
+
|
|
420
|
+
const arrivalCall = infoSpy.mock.calls.find((c) => c[0] === "S2S << reply.done");
|
|
421
|
+
expect(arrivalCall).toBeDefined();
|
|
422
|
+
expect(arrivalCall?.[1]).toEqual({ sid: "sess-abc", status: "completed" });
|
|
423
|
+
});
|
|
424
|
+
|
|
405
425
|
test("session.error with session_not_found dispatches 'sessionExpired'", async () => {
|
|
406
426
|
const { raw, handle } = await setupHandle();
|
|
407
427
|
const handler = vi.fn();
|
package/host/s2s.ts
CHANGED
|
@@ -86,10 +86,18 @@ export type S2sEvent = ClientEvent & { _interrupted?: boolean };
|
|
|
86
86
|
*/
|
|
87
87
|
type DispatchState = { speechActive: boolean };
|
|
88
88
|
|
|
89
|
+
type DispatchContext = {
|
|
90
|
+
/** Logger used for diagnostic `S2S <<` arrival logs. */
|
|
91
|
+
log: Logger;
|
|
92
|
+
/** Session id threaded through diagnostic logs; omitted when undefined. */
|
|
93
|
+
sid?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
89
96
|
function dispatchS2sMessage(
|
|
90
97
|
emitter: Emitter<S2sEvents>,
|
|
91
98
|
msg: S2sServerMessage,
|
|
92
99
|
state: DispatchState,
|
|
100
|
+
dispatchCtx: DispatchContext,
|
|
93
101
|
): void {
|
|
94
102
|
switch (msg.type) {
|
|
95
103
|
case "session.ready":
|
|
@@ -131,6 +139,13 @@ function dispatchS2sMessage(
|
|
|
131
139
|
});
|
|
132
140
|
break;
|
|
133
141
|
case "reply.done":
|
|
142
|
+
// Log every raw reply.done arrival from the S2S service — one line per
|
|
143
|
+
// event, before any client-facing dedup — so we can cross-check which
|
|
144
|
+
// stalled sessions actually received reply.done for their turn.
|
|
145
|
+
dispatchCtx.log.info("S2S << reply.done", {
|
|
146
|
+
...(dispatchCtx.sid !== undefined ? { sid: dispatchCtx.sid } : {}),
|
|
147
|
+
status: msg.status ?? "completed",
|
|
148
|
+
});
|
|
134
149
|
if (msg.status === "interrupted") {
|
|
135
150
|
emitter.emit("event", { type: "cancelled" });
|
|
136
151
|
} else {
|
|
@@ -192,10 +207,16 @@ export type ConnectS2sOptions = {
|
|
|
192
207
|
config: S2SConfig;
|
|
193
208
|
createWebSocket: CreateS2sWebSocket;
|
|
194
209
|
logger?: Logger;
|
|
210
|
+
/**
|
|
211
|
+
* Session id attached to diagnostic log lines (e.g. raw `reply.done`
|
|
212
|
+
* arrivals from the S2S service). Optional; logs omit the field when
|
|
213
|
+
* not provided.
|
|
214
|
+
*/
|
|
215
|
+
sid?: string;
|
|
195
216
|
};
|
|
196
217
|
|
|
197
218
|
export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
|
|
198
|
-
const { apiKey, config, createWebSocket, logger: log = consoleLogger } = opts;
|
|
219
|
+
const { apiKey, config, createWebSocket, logger: log = consoleLogger, sid } = opts;
|
|
199
220
|
|
|
200
221
|
return new Promise((resolve, reject) => {
|
|
201
222
|
log.info("S2S connecting", { url: config.wssUrl });
|
|
@@ -206,6 +227,7 @@ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
|
|
|
206
227
|
|
|
207
228
|
const emitter = createNanoEvents<S2sEvents>();
|
|
208
229
|
const dispatchState: DispatchState = { speechActive: false };
|
|
230
|
+
const dispatchCtx: DispatchContext = sid !== undefined ? { log, sid } : { log };
|
|
209
231
|
let opened = false;
|
|
210
232
|
|
|
211
233
|
function send(msg: { type: string; [key: string]: unknown }): void {
|
|
@@ -287,6 +309,9 @@ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
|
|
|
287
309
|
function logIncoming(obj: { type?: unknown }): void {
|
|
288
310
|
// reply.audio and input.audio are ~95% of traffic — skip logging.
|
|
289
311
|
if (obj.type === "reply.audio" || obj.type === "input.audio") return;
|
|
312
|
+
// reply.done gets a richer log (sid + status) inside dispatchS2sMessage;
|
|
313
|
+
// skip the generic line here to avoid a duplicate.
|
|
314
|
+
if (obj.type === "reply.done") return;
|
|
290
315
|
log.info(`S2S << ${obj.type}`);
|
|
291
316
|
}
|
|
292
317
|
|
|
@@ -309,7 +334,7 @@ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
|
|
|
309
334
|
);
|
|
310
335
|
return;
|
|
311
336
|
}
|
|
312
|
-
dispatchS2sMessage(emitter, parsed, dispatchState);
|
|
337
|
+
dispatchS2sMessage(emitter, parsed, dispatchState, dispatchCtx);
|
|
313
338
|
}
|
|
314
339
|
|
|
315
340
|
ws.addEventListener("message", handleS2sMessage);
|
package/host/session.test.ts
CHANGED
|
@@ -191,6 +191,55 @@ describe("createS2sSession", () => {
|
|
|
191
191
|
expect(client.audioDoneCount).toBe(1);
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
test("S2S close with active reply logs a warn", async () => {
|
|
195
|
+
const warn = vi.fn();
|
|
196
|
+
const info = vi.fn();
|
|
197
|
+
const logger = { debug: vi.fn(), info, warn, error: vi.fn() };
|
|
198
|
+
const { session, mockHandle } = setup({ logger });
|
|
199
|
+
await session.start();
|
|
200
|
+
|
|
201
|
+
// reply started but not yet done → currentReplyId is non-null
|
|
202
|
+
mockHandle._fire("replyStarted", { replyId: "r-active" });
|
|
203
|
+
mockHandle._fire("close", 1006, "abnormal");
|
|
204
|
+
|
|
205
|
+
expect(warn).toHaveBeenCalledWith(
|
|
206
|
+
"S2S closed with active reply",
|
|
207
|
+
expect.objectContaining({ activeReplyId: "r-active", code: 1006, reason: "abnormal" }),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("S2S clean close (no active reply) logs at info, not warn", async () => {
|
|
212
|
+
const warn = vi.fn();
|
|
213
|
+
const info = vi.fn();
|
|
214
|
+
const logger = { debug: vi.fn(), info, warn, error: vi.fn() };
|
|
215
|
+
const { session, mockHandle } = setup({ logger });
|
|
216
|
+
await session.start();
|
|
217
|
+
|
|
218
|
+
mockHandle._fire("replyStarted", { replyId: "r1" });
|
|
219
|
+
mockHandle._fire("event", { type: "reply_done" });
|
|
220
|
+
mockHandle._fire("close", 1000, "ok");
|
|
221
|
+
|
|
222
|
+
expect(warn).not.toHaveBeenCalledWith("S2S closed with active reply", expect.any(Object));
|
|
223
|
+
expect(info).toHaveBeenCalledWith("S2S closed", expect.objectContaining({ code: 1000 }));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("fast reply_done dispatch does not warn", async () => {
|
|
227
|
+
const warn = vi.fn();
|
|
228
|
+
const logger = {
|
|
229
|
+
debug: vi.fn(),
|
|
230
|
+
info: vi.fn(),
|
|
231
|
+
warn,
|
|
232
|
+
error: vi.fn(),
|
|
233
|
+
};
|
|
234
|
+
const { session, mockHandle } = setup({ logger });
|
|
235
|
+
await session.start();
|
|
236
|
+
|
|
237
|
+
mockHandle._fire("replyStarted", { replyId: "r1" });
|
|
238
|
+
mockHandle._fire("event", { type: "reply_done" });
|
|
239
|
+
|
|
240
|
+
expect(warn).not.toHaveBeenCalledWith("slow reply_done dispatch", expect.any(Object));
|
|
241
|
+
});
|
|
242
|
+
|
|
194
243
|
test("cancelled event emits cancelled", async () => {
|
|
195
244
|
const { session, client, mockHandle } = setup();
|
|
196
245
|
await session.start();
|
package/host/session.ts
CHANGED
|
@@ -179,7 +179,17 @@ function handleReplyCancelled(ctx: S2sSessionCtx): void {
|
|
|
179
179
|
ctx.client.event({ type: "cancelled" });
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Warn when the entry-to-emit time for a reply_done dispatch exceeds this.
|
|
184
|
+
* Tool-less sessions should be sub-millisecond; sessions with pending tools
|
|
185
|
+
* will legitimately spend time awaiting ctx.turnPromise. We log both (with
|
|
186
|
+
* `hadTurnPromise`) so event-loop starvation is distinguishable from
|
|
187
|
+
* genuine tool-call latency.
|
|
188
|
+
*/
|
|
189
|
+
const REPLY_DONE_SLOW_THRESHOLD_MS = 50;
|
|
190
|
+
|
|
182
191
|
function handleReplyDone(ctx: S2sSessionCtx): void {
|
|
192
|
+
const startMs = Date.now();
|
|
183
193
|
const doneReplyId = ctx.reply.currentReplyId;
|
|
184
194
|
// Dedup duplicate reply.done events from the S2S service: once the reply
|
|
185
195
|
// has been fully dispatched (or was never started), currentReplyId is null.
|
|
@@ -187,6 +197,7 @@ function handleReplyDone(ctx: S2sSessionCtx): void {
|
|
|
187
197
|
ctx.log.debug("Dropping duplicate reply.done (no active reply)");
|
|
188
198
|
return;
|
|
189
199
|
}
|
|
200
|
+
const hadTurnPromise = ctx.turnPromise !== null;
|
|
190
201
|
const sendPending = () => {
|
|
191
202
|
if (ctx.reply.currentReplyId !== doneReplyId) {
|
|
192
203
|
ctx.reply.pendingTools = [];
|
|
@@ -204,10 +215,19 @@ function handleReplyDone(ctx: S2sSessionCtx): void {
|
|
|
204
215
|
ctx.client.event({ type: "reply_done" });
|
|
205
216
|
// Mark reply as finished so any repeated reply.done is dropped above.
|
|
206
217
|
ctx.reply.currentReplyId = null;
|
|
218
|
+
const durationMs = Date.now() - startMs;
|
|
219
|
+
if (durationMs >= REPLY_DONE_SLOW_THRESHOLD_MS) {
|
|
220
|
+
ctx.log.warn("slow reply_done dispatch", {
|
|
221
|
+
sid: ctx.id,
|
|
222
|
+
agent: ctx.agent,
|
|
223
|
+
durationMs,
|
|
224
|
+
hadTurnPromise,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
207
227
|
}
|
|
208
228
|
};
|
|
209
|
-
if (
|
|
210
|
-
void ctx.turnPromise
|
|
229
|
+
if (hadTurnPromise) {
|
|
230
|
+
void ctx.turnPromise?.then(sendPending);
|
|
211
231
|
} else {
|
|
212
232
|
sendPending();
|
|
213
233
|
}
|
|
@@ -229,7 +249,20 @@ function setupListeners(ctx: S2sSessionCtx, handle: S2sHandle): void {
|
|
|
229
249
|
handle.close();
|
|
230
250
|
});
|
|
231
251
|
handle.on("close", (code, reason) => {
|
|
232
|
-
ctx.
|
|
252
|
+
const activeReplyId = ctx.reply.currentReplyId;
|
|
253
|
+
if (activeReplyId !== null) {
|
|
254
|
+
// Silent drop — S2S socket closed while the server was still owed a
|
|
255
|
+
// reply. Client stays in waitingForReply=true until a session timeout.
|
|
256
|
+
ctx.log.warn("S2S closed with active reply", {
|
|
257
|
+
sid: ctx.id,
|
|
258
|
+
agent: ctx.agent,
|
|
259
|
+
activeReplyId,
|
|
260
|
+
code,
|
|
261
|
+
reason,
|
|
262
|
+
});
|
|
263
|
+
} else {
|
|
264
|
+
ctx.log.info("S2S closed", { code, reason });
|
|
265
|
+
}
|
|
233
266
|
ctx.s2s = null;
|
|
234
267
|
ctx.cancelReply();
|
|
235
268
|
});
|
|
@@ -319,6 +352,7 @@ export function createS2sSession(opts: S2sSessionOptions): Session {
|
|
|
319
352
|
config: s2sConfig,
|
|
320
353
|
createWebSocket,
|
|
321
354
|
logger: log,
|
|
355
|
+
sid: id,
|
|
322
356
|
});
|
|
323
357
|
if (sessionAbort.signal.aborted || generation !== connectGeneration) {
|
|
324
358
|
handle.close();
|