Skip to content

Commit c7eb768

Browse files
committed
test(core): temporary diagnostic for failing agent S2 writes
Logs the S2 append pipeline error (basin, stream, endpoint, message) when a session .out write fails, to capture why the agent's direct-to-S2 writes return an empty stream in CI while succeeding locally. To be reverted.
1 parent 71166f5 commit c7eb768

4 files changed

Lines changed: 84 additions & 4 deletions

File tree

apps/webapp/test/helpers/agentHarness.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ export function runRealChatAgent(opts: RunRealChatAgentOptions): RunningAgent {
112112
) as Promise<void>
113113
).finally(restore);
114114

115+
done.catch((e) => {
116+
const err = e instanceof Error ? e : new Error(String(e));
117+
console.error(
118+
"[AGENT-DIAG] run rejected",
119+
opts.agentId,
120+
runId,
121+
`${err.name}: ${err.message}`,
122+
(err.stack ?? "").split("\n").slice(1, 6).join(" || ")
123+
);
124+
});
125+
115126
return {
116127
done,
117128
close: async () => {

apps/webapp/test/helpers/testChatAgent.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ export const testChatAgent = chat
5555

5656
run: async ({ messages, signal }) => {
5757
const model = locals.get(testChatModelLocal);
58+
console.error(
59+
"[TURN-DIAG] e2e-test-chat run: messages=",
60+
messages?.length,
61+
"hasModel=",
62+
!!model,
63+
"aborted=",
64+
signal?.aborted,
65+
"lastRole=",
66+
messages?.[messages.length - 1]?.role
67+
);
5868
if (!model) {
5969
throw new Error("test model not injected via locals");
6070
}

apps/webapp/test/session-agent.e2e.test.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ afterAll(async () => {
6666
await server?.stop();
6767
}, 120_000);
6868

69+
/**
70+
* A synchronous mock model stream: enqueues every chunk in one tick and closes.
71+
* `simulateReadableStream`'s default 0ms delay still yields a `setTimeout(0)`
72+
* macrotask between chunks, which a loaded CI event loop can starve so the
73+
* later chunks never emit and the turn produces an empty `.out`. Emitting
74+
* synchronously keeps the mock deterministic across environments.
75+
*/
76+
function simStream(chunks: readonly unknown[]): ReadableStream<any> {
77+
return new ReadableStream({
78+
start(controller) {
79+
for (const chunk of chunks) {
80+
controller.enqueue(chunk);
81+
}
82+
controller.close();
83+
},
84+
});
85+
}
86+
6987
function textModel(text: string) {
7088
const chunks = [
7189
{ type: "text-start", id: "t1" },
@@ -81,7 +99,7 @@ function textModel(text: string) {
8199
},
82100
];
83101
return new MockLanguageModelV3({
84-
doStream: async () => ({ stream: simulateReadableStream({ chunks: chunks as never }) }),
102+
doStream: async () => ({ stream: simStream(chunks) }),
85103
});
86104
}
87105

@@ -139,7 +157,7 @@ function echoModel() {
139157
},
140158
},
141159
];
142-
return { stream: simulateReadableStream({ chunks: chunks as never }) };
160+
return { stream: simStream(chunks) };
143161
},
144162
});
145163
}
@@ -239,7 +257,7 @@ function toolCallThenText(opts: {
239257
let idx = 0;
240258
return new MockLanguageModelV3({
241259
doStream: async () => ({
242-
stream: simulateReadableStream({ chunks: (idx++ === 0 ? call1 : call2) as never }),
260+
stream: simStream(idx++ === 0 ? call1 : call2),
243261
}),
244262
});
245263
}
@@ -264,7 +282,7 @@ function sequenceModel(texts: string[]) {
264282
{ type: "text-end", id: "t1" },
265283
FINISH_STOP,
266284
];
267-
return { stream: simulateReadableStream({ chunks: chunks as never }) };
285+
return { stream: simStream(chunks) };
268286
},
269287
});
270288
}
@@ -317,6 +335,18 @@ describe("session agent e2e (real chat.agent loop)", () => {
317335
maxMs: 30_000,
318336
});
319337

338+
const dbg = await fetch(
339+
`${baseUrl}/realtime/v1/sessions/${encodeURIComponent(addressingKey)}/out/records`,
340+
{ headers: { Authorization: `Bearer ${token}` } }
341+
);
342+
console.error(
343+
"[EA1-DIAG] collected parts:",
344+
parts.length,
345+
"readback:",
346+
dbg.status,
347+
(await dbg.text()).slice(0, 700)
348+
);
349+
320350
const text = parts
321351
.filter((p) => p.chunk != null)
322352
.map((p) => JSON.stringify(p.chunk))

packages/core/src/v3/realtimeStreams/streamsWriterV2.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ export class StreamsWriterV2<T = any> implements StreamsWriter {
152152

153153
this.log(`[S2MetadataStream] Starting stream pipeline`);
154154

155+
console.error(
156+
"[S2WRITER-DIAG] pipeline START stream=",
157+
this.options.stream,
158+
"flushMs=",
159+
this.flushIntervalMs
160+
);
161+
let recordCount = 0;
162+
155163
// Convert source stream to AppendRecord format and pipe to S2
156164
await this.serverStream
157165
.pipeThrough(
@@ -166,6 +174,7 @@ export class StreamsWriterV2<T = any> implements StreamsWriter {
166174
controller.error(encoded.error);
167175
return;
168176
}
177+
recordCount++;
169178
controller.enqueue(AppendRecord.string({ body: encoded.body }));
170179
},
171180
})
@@ -182,6 +191,15 @@ export class StreamsWriterV2<T = any> implements StreamsWriter {
182191
// Get final position to verify completion
183192
const lastAcked = session.lastAckedPosition();
184193

194+
console.error(
195+
"[S2WRITER-DIAG] pipeline DONE stream=",
196+
this.options.stream,
197+
"recordsIn=",
198+
recordCount,
199+
"lastAckedEnd=",
200+
JSON.stringify(lastAcked?.end)
201+
);
202+
185203
if (lastAcked?.end) {
186204
this.lastSeqNum = lastAcked.end.seqNum;
187205
this.log(
@@ -193,6 +211,17 @@ export class StreamsWriterV2<T = any> implements StreamsWriter {
193211
this.log("[S2MetadataStream] Stream error occurred but stream was aborted");
194212
return;
195213
}
214+
console.error(
215+
"[S2WRITER-DIAG] pipeline error basin=",
216+
this.options.basin,
217+
"stream=",
218+
this.options.stream,
219+
"endpoint=",
220+
this.options.endpoint,
221+
"err=",
222+
error instanceof Error ? `${error.name}: ${error.message}` : String(error),
223+
(error instanceof Error ? error.stack ?? "" : "").split("\n").slice(1, 5).join(" || ")
224+
);
196225
this.logError("[S2MetadataStream] Error in stream pipeline:", error);
197226
throw error;
198227
}

0 commit comments

Comments
 (0)