jeo-code 0.6.39 → 0.6.40
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/CHANGELOG.md +7 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +2 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/ai/model-manager.ts +21 -8
- package/src/ai/providers/anthropic.ts +1 -1
- package/src/ai/providers/antigravity.ts +1 -1
- package/src/ai/providers/gemini.ts +2 -2
- package/src/ai/providers/ollama.ts +1 -1
- package/src/ai/providers/openai-responses.ts +1 -1
- package/src/ai/providers/openai.ts +1 -1
- package/src/ai/sse.ts +16 -3
- package/src/ai/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.6.40] - 2026-06-21
|
|
10
|
+
_Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **A connected-but-quiet stream no longer trips a false `stream idle for <ms>ms (no chunk)` retry — wire-level heartbeat.** The v0.6.39 fix only counted reasoning/thinking deltas as activity, so a model that reasons server-side and streams NO thought tokens (only SSE keepalive/`ping` events, e.g. Anthropic `event: ping`, or events that never become a chunk) still looked stalled and got aborted+retried past the idle window. The SSE reader (`readLines`/`readSse`) now fires an `onStreamActivity` heartbeat on ANY bytes received from the provider — including keepalive comments and events filtered out before becoming a chunk or reasoning delta — threaded through every provider (`anthropic`, `openai`, `openai-responses`, `gemini`, `antigravity`, `ollama`). The idle watchdog re-arms while ANY wire activity advances and aborts only a genuinely dead socket (zero bytes for the idle window).
|
|
14
|
+
- **The default stream-idle window is now 300s (was 120s) so a silent local backend no longer false-aborts and stops the turn.** Root cause of the recurring `stream idle … (no chunk)` halt for local/slow providers: a backend like Ollama / llama.cpp emits ZERO bytes (no keepalive) during model load + prompt-eval before the first token, which on modest hardware or a large context routinely exceeds 120s — the wire heartbeat has nothing to fire on, the watchdog aborts an alive-but-quiet generation, each retry re-incurs the same slow first byte, the attempt budget exhausts, and the turn stops. The cap now only bites a genuinely dead stream after 5 minutes; `JEO_STREAM_IDLE_MS` overrides it and Ctrl-C remains the interactive escape.
|
|
15
|
+
|
|
9
16
|
## [0.6.39] - 2026-06-21
|
|
10
17
|
_A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6)._
|
|
11
18
|
|
package/README.ja.md
CHANGED
|
@@ -200,11 +200,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
200
200
|
## 変更履歴 (Changelog)
|
|
201
201
|
|
|
202
202
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
203
|
+
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
203
204
|
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
204
205
|
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
205
206
|
- **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
206
207
|
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
207
|
-
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -200,11 +200,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
200
200
|
## 변경 이력 (Changelog)
|
|
201
201
|
|
|
202
202
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
203
|
+
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
203
204
|
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
204
205
|
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
205
206
|
- **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
206
207
|
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
207
|
-
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -183,6 +183,7 @@ JEO_TUI_ALT_SCREEN=1 # legacy alt-screen turn (default: inline scroll
|
|
|
183
183
|
JEO_STEP_BASE=24 # dynamic step budget: rolling base
|
|
184
184
|
JEO_STEP_HARD_CAP=600 # absolute termination guarantee
|
|
185
185
|
JEO_STREAM_MAX_MS=300000 # opt-in overall stream deadline (default off; bounds slow-drip streams)
|
|
186
|
+
JEO_STREAM_IDLE_MS=300000 # per-chunk idle cap (default 300s); raise for slow/local backends silent before first token
|
|
186
187
|
JEO_TOOL_OUTPUT_MAX=4000 # model-visible tool output cap (full output spills to artifacts)
|
|
187
188
|
```
|
|
188
189
|
|
|
@@ -200,11 +201,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
200
201
|
## Changelog
|
|
201
202
|
|
|
202
203
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
204
|
+
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
203
205
|
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
204
206
|
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
205
207
|
- **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
206
208
|
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
207
|
-
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
208
209
|
|
|
209
210
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
211
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -200,11 +200,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
200
200
|
## 更新日志 (Changelog)
|
|
201
201
|
|
|
202
202
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
203
|
+
- **[0.6.40]** (2026-06-21) — Wire-level stream heartbeat: ANY bytes from the provider (SSE keepalive/ping, filtered events) now re-arm the idle watchdog — a connected-but-quiet stream can no longer trip a false `stream idle` retry. Default idle window raised to 300s so Ollama/llama.cpp model-load silence no longer exhausts retries and stops the turn.
|
|
203
204
|
- **[0.6.39]** (2026-06-21) — A long "thinking" phase no longer trips a false stream-idle retry: reasoning/thinking deltas now act as a stream heartbeat, so a model that streams thought tokens past the idle window before any visible text is no longer mistaken for a stalled stream and retried (which discarded the in-progress reasoning). Re-verified leak-free (`mem-probe`, 2000 turns, −525 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
204
205
|
- **[0.6.38]** (2026-06-21) — The OKF concept-bundle memory no longer silently drops what it learns: a legacy single-doc `MEMORY.md` (or a text-only distill fallback) can no longer shadow the concept bundle, break OKF conformance, or lose a turn's learnings — its content is folded into the concept merge and the stale blob is archived. Re-verified leak-free (`mem-probe`, 2000 turns, −570 bytes/turn slope) with a fresh `jeo --tmux` boot battery (6/6).
|
|
205
206
|
- **[0.6.37]** (2026-06-20) — Two dead-end fixes: the boxed prompt's ↑/↓ now recalls input history on a soft-wrapped one-liner (only a genuine multi-line draft gets in-box caret nav), and every terminating Spec-first stage (deep-interview, ralplan, team) now surfaces a user-visible answer instead of silently stalling — re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
206
207
|
- **[0.6.36]** (2026-06-20) — When `jeo --tmux` flips the mouse on so you can drag-select, the drag now actually lands on the system clipboard — the in-session tmux profile sets `set-clipboard on` + a local `copy-command` on the CURRENT session only — plus `/help` documents the drag-to-copy and the Shift/Option-drag escape hatch, re-verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot.
|
|
207
|
-
- **[0.6.35]** (2026-06-20) — The prompt's Ctrl+C now clears a non-empty input box on the first press and only exits on the next press of an empty box; plus app-driven system-clipboard copy (OSC 52 + local tool, tmux-aware), drag-and-drop image attachment, a Ctrl-L prompt re-anchor, and a SIGCONT resume repaint — verified leak-free (`mem-probe`) with a fresh `jeo --tmux` boot check.
|
|
208
208
|
|
|
209
209
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
210
210
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/ai/model-manager.ts
CHANGED
|
@@ -390,9 +390,16 @@ const DEFAULT_CALL_TIMEOUT_MS = 120_000;
|
|
|
390
390
|
/** Per-chunk idle cap for streaming: a stream that emits NOTHING for this long is
|
|
391
391
|
* aborted, but a healthy long generation (chunks keep arriving) runs unbounded —
|
|
392
392
|
* unlike a single wall-clock cap that would kill a long-but-active stream.
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
|
|
393
|
+
* Set to 300s (not 120s): the wire-level heartbeat already re-arms this watchdog on
|
|
394
|
+
* ANY bytes (keepalive/ping) from remote providers, so this cap now only bites the
|
|
395
|
+
* GENUINELY-silent case — chiefly local backends (Ollama / llama.cpp) whose model
|
|
396
|
+
* load + prompt-eval before the first token emits zero bytes and no keepalive, and
|
|
397
|
+
* can easily exceed 120s on modest hardware or a large context. A too-tight cap there
|
|
398
|
+
* aborts an alive-but-quiet generation, retries re-incur the same slow first byte, the
|
|
399
|
+
* attempt budget exhausts, and the turn STOPS — the exact false-failure this guards.
|
|
400
|
+
* Opt-in override via JEO_STREAM_IDLE_MS for reasoning/local workloads whose silent
|
|
401
|
+
* phase runs longer still; Ctrl-C remains the interactive escape for a truly dead one. */
|
|
402
|
+
const STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
396
403
|
|
|
397
404
|
/** Combine two abort signals into one. Preserves BOTH even when `AbortSignal.any`
|
|
398
405
|
* is unavailable (manual fallback), so neither the caller's cancel nor the timeout
|
|
@@ -531,11 +538,13 @@ export function createModelManager(): ModelManager {
|
|
|
531
538
|
// JEO_STREAM_MAX_MS opts in to an OVERALL deadline (round-14): non-interactive
|
|
532
539
|
// runs can bound a slow-drip stream the per-chunk idle alone never catches.
|
|
533
540
|
let attempt: AbortController | null = null;
|
|
534
|
-
// Heartbeat:
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
// onReasoning
|
|
538
|
-
//
|
|
541
|
+
// Heartbeat: a long server-side "thinking" phase emits no yielded chunk, so the
|
|
542
|
+
// idle watchdog would otherwise look stalled and trip a false idle-stall retry.
|
|
543
|
+
// Two heartbeat sources bump lastActivityAt: (1) reasoning/thinking deltas routed
|
|
544
|
+
// to onReasoning, and (2) onStreamActivity — the wire-level heartbeat fired on ANY
|
|
545
|
+
// bytes from the provider stream (SSE keepalive/ping comments, events that never
|
|
546
|
+
// become a chunk). The watchdog re-arms while ANY activity advances and aborts only
|
|
547
|
+
// a genuinely dead stream (zero bytes for the idle window).
|
|
539
548
|
let lastActivityAt = Date.now();
|
|
540
549
|
const bump = () => { lastActivityAt = Date.now(); };
|
|
541
550
|
const userOnReasoning = callOptions.onReasoning;
|
|
@@ -544,6 +553,10 @@ export function createModelManager(): ModelManager {
|
|
|
544
553
|
...callOptions,
|
|
545
554
|
onReasoning: (delta: string) => { bump(); userOnReasoning?.(delta); },
|
|
546
555
|
onReasoningStart: () => { bump(); userOnReasoningStart?.(); },
|
|
556
|
+
// Wire-level heartbeat: ANY bytes from the provider stream (SSE keepalive/ping
|
|
557
|
+
// comments, events that never become a chunk) stamp activity, so the idle
|
|
558
|
+
// watchdog re-arms for a connected-but-quiet stream and aborts only a dead one.
|
|
559
|
+
onStreamActivity: bump,
|
|
547
560
|
};
|
|
548
561
|
const makeIter = () => {
|
|
549
562
|
attempt = new AbortController();
|
|
@@ -410,7 +410,7 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
410
410
|
// + signature_delta(signature). Accumulate per index and emit one ReasoningArtifact per
|
|
411
411
|
// block on stream end so the signed thought can be replayed (gajae continuity).
|
|
412
412
|
const thinkBlocks = new Map<number, { text: string; signature?: string }>();
|
|
413
|
-
for await (const data of readSse(response.body)) {
|
|
413
|
+
for await (const data of readSse(response.body, options.onStreamActivity)) {
|
|
414
414
|
let evt: {
|
|
415
415
|
type?: string;
|
|
416
416
|
index?: number;
|
|
@@ -280,7 +280,7 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
280
280
|
let yielded = false;
|
|
281
281
|
let usage: CcaUsage | undefined;
|
|
282
282
|
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
283
|
-
for await (const data of readSse(response.body)) {
|
|
283
|
+
for await (const data of readSse(response.body, options.onStreamActivity)) {
|
|
284
284
|
let chunk: CcaChunk;
|
|
285
285
|
try { chunk = JSON.parse(data); } catch { continue; }
|
|
286
286
|
const thought = thoughtOf(chunk);
|
|
@@ -259,7 +259,7 @@ async function* ccaTurn(messages: Message[], options: CallOptions, credential: C
|
|
|
259
259
|
let lastEmptyReason: string | undefined;
|
|
260
260
|
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
261
261
|
const seenSigs = new Set<string>();
|
|
262
|
-
for await (const data of readSse(response.body)) {
|
|
262
|
+
for await (const data of readSse(response.body, options.onStreamActivity)) {
|
|
263
263
|
let chunk: CcaChunk;
|
|
264
264
|
try {
|
|
265
265
|
chunk = JSON.parse(data);
|
|
@@ -334,7 +334,7 @@ export const geminiAdapter: ProviderAdapter = {
|
|
|
334
334
|
let lastEmptyReason: string | undefined;
|
|
335
335
|
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
336
336
|
const seenSigs = new Set<string>();
|
|
337
|
-
for await (const data of readSse(response.body)) {
|
|
337
|
+
for await (const data of readSse(response.body, options.onStreamActivity)) {
|
|
338
338
|
let chunk: GeminiChunk;
|
|
339
339
|
try {
|
|
340
340
|
chunk = JSON.parse(data);
|
|
@@ -64,7 +64,7 @@ export const ollamaAdapter: ProviderAdapter = {
|
|
|
64
64
|
let doneReason: string | undefined;
|
|
65
65
|
// Route inline <think>…</think> (local reasoning models) to the reasoning channel.
|
|
66
66
|
const think = createThinkSplitter(options.onReasoning);
|
|
67
|
-
for await (const line of readLines(response.body)) {
|
|
67
|
+
for await (const line of readLines(response.body, options.onStreamActivity)) {
|
|
68
68
|
let chunk: { message?: { content?: string; thinking?: string }; done?: boolean; done_reason?: string; prompt_eval_count?: number; eval_count?: number; total_duration?: number };
|
|
69
69
|
try {
|
|
70
70
|
chunk = JSON.parse(line);
|
|
@@ -279,7 +279,7 @@ export async function* codexResponsesStream(
|
|
|
279
279
|
let yieldedAny = false;
|
|
280
280
|
let incompleteReason: string | undefined;
|
|
281
281
|
const toolAcc = new Map<number, { name: string; args: string }>();
|
|
282
|
-
for await (const data of readSse(response.body)) {
|
|
282
|
+
for await (const data of readSse(response.body, options.onStreamActivity)) {
|
|
283
283
|
const ev = parseResponsesEvent(data);
|
|
284
284
|
if (ev.reasoningDelta) options.onReasoning?.(ev.reasoningDelta);
|
|
285
285
|
if (ev.reasoningItem) options.onReasoningArtifact?.({ provider: "openai", model: options.model, itemId: ev.reasoningItem.id, encrypted: ev.reasoningItem.encrypted });
|
|
@@ -201,7 +201,7 @@ export const openaiAdapter: ProviderAdapter = {
|
|
|
201
201
|
// visible answer and onto the reasoning channel. No-op for models that never emit it.
|
|
202
202
|
const think = createThinkSplitter(options.onReasoning);
|
|
203
203
|
const toolAcc = new Map<number, { name: string; args: string }>();
|
|
204
|
-
for await (const data of readSse(response.body)) {
|
|
204
|
+
for await (const data of readSse(response.body, options.onStreamActivity)) {
|
|
205
205
|
let chunk: { choices?: { delta?: OpenAIDelta; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
206
206
|
try {
|
|
207
207
|
chunk = JSON.parse(data);
|
package/src/ai/sse.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export async function* readLines(
|
|
1
|
+
export async function* readLines(
|
|
2
|
+
stream: ReadableStream<Uint8Array>,
|
|
3
|
+
onActivity?: () => void,
|
|
4
|
+
): AsyncGenerator<string> {
|
|
2
5
|
const reader = stream.getReader();
|
|
3
6
|
const decoder = new TextDecoder("utf-8");
|
|
4
7
|
let buffer = "";
|
|
@@ -8,6 +11,13 @@ export async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGener
|
|
|
8
11
|
if (done) {
|
|
9
12
|
break;
|
|
10
13
|
}
|
|
14
|
+
// Wire-level heartbeat: ANY bytes from the server (including SSE keepalive/ping
|
|
15
|
+
// comments and events that never become a yielded chunk) mark the stream as alive,
|
|
16
|
+
// so the idle watchdog re-arms instead of falsely aborting a connected-but-quiet
|
|
17
|
+
// stream (e.g. a model reasoning server-side that emits only ping events).
|
|
18
|
+
if (value && value.length > 0) {
|
|
19
|
+
onActivity?.();
|
|
20
|
+
}
|
|
11
21
|
buffer += decoder.decode(value, { stream: true });
|
|
12
22
|
const parts = buffer.split("\n");
|
|
13
23
|
buffer = parts.pop() ?? "";
|
|
@@ -34,8 +44,11 @@ export async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGener
|
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
export async function* readSse(
|
|
38
|
-
|
|
47
|
+
export async function* readSse(
|
|
48
|
+
stream: ReadableStream<Uint8Array>,
|
|
49
|
+
onActivity?: () => void,
|
|
50
|
+
): AsyncGenerator<string> {
|
|
51
|
+
for await (const line of readLines(stream, onActivity)) {
|
|
39
52
|
if (line.startsWith("data:")) {
|
|
40
53
|
let data = line.slice(5);
|
|
41
54
|
if (data.startsWith(" ")) {
|
package/src/ai/types.ts
CHANGED
|
@@ -125,6 +125,12 @@ export interface CallOptions {
|
|
|
125
125
|
* thoughtSignature / reasoning item id+encrypted). Separate from `onReasoning` (display
|
|
126
126
|
* text) because these arrive on different SSE events and are opaque replay data. */
|
|
127
127
|
onReasoningArtifact?: (artifact: ReasoningArtifact) => void;
|
|
128
|
+
/** Internal wire-activity heartbeat: fired on ANY bytes received from the provider
|
|
129
|
+
* stream — including SSE keepalive/ping comments and events that never become a
|
|
130
|
+
* yielded chunk or reasoning delta. Set by the manager's streaming path so the idle
|
|
131
|
+
* watchdog treats a connected-but-quiet stream (e.g. a model reasoning server-side
|
|
132
|
+
* that emits only ping events) as alive. NOT forwarded to user callbacks. */
|
|
133
|
+
onStreamActivity?: () => void;
|
|
128
134
|
/** NATIVE tool-calling: function declarations the model may call. Present only on the
|
|
129
135
|
* main agent step (never the prose wrap-up). Adapters with `supportsNativeTools` send
|
|
130
136
|
* these on the wire and re-serialize the structured tool call back into the engine's
|