jeo-code 0.6.37 → 0.6.39

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 CHANGED
@@ -6,6 +6,36 @@ 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.39] - 2026-06-21
10
+ _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
+
12
+ ### Fixed
13
+ - **A long "thinking" phase no longer trips a false `stream idle for <ms>ms (no chunk)` retry.** Reasoning/thinking deltas are routed to `onReasoning` and never yielded as a stream chunk, so a model that streams thought tokens for longer than the idle window (default 120s) before emitting visible text looked stalled and got retried — discarding the in-progress reasoning. The per-chunk idle watchdog now treats reasoning activity as a heartbeat (`lastActivityAt`) and re-arms while thinking is actively streaming; it aborts only a genuinely silent stream. The `JEO_STREAM_IDLE_MS` override still covers models that reason fully server-side and stream no thought tokens at all.
14
+
15
+ ### Verified
16
+ - `bun run typecheck` clean; full suite **1752 pass / 0 fail** (216 files), including the new `retryableStream` reasoning-heartbeat tests in `test/round-b.test.ts` (a silent-but-thinking stream stays alive; the watchdog still fires once reasoning activity stops).
17
+ - `scripts/mem-probe.ts` (2000 turns × 40 tools): no long-term leak (per-turn slope **−525 bytes/turn**, exit-listeners flat at 1, heap returns to baseline).
18
+ - `scripts/tmux-verify.sh battery`: **6/6 PASSED** on a fresh `jeo --tmux` boot (boot, `/help`, unknown `$skill`, `/agents`, `$ultragoal`, unresolved `/command`).
19
+
20
+ ## [0.6.38] - 2026-06-21
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)._
22
+
23
+ ### Fixed
24
+ - **A text-only distill fallback no longer loses the turn's learnings.** When the model returns no extractable JSON, `distillSessionMemory` used to write the raw text to a single-doc `MEMORY.md` blob — but `memoryPromptSection` injects that blob ONLY when the concept bundle is empty (concepts always win), so once any concept existed the fallback's learnings were recorded yet never injected. Now, if a concept bundle already exists, the dead blob is suppressed and the prior concepts are kept as the durable memory (`skipped: "distill produced no JSON; kept existing concept bundle (legacy blob suppressed)"`).
25
+ - **A lingering legacy `MEMORY.md` can no longer break OKF conformance.** A frontmatter-less single-doc `MEMORY.md` coexisting with the concept bundle made `validateBundle` non-conformant (`error: concept document missing YAML frontmatter block`) while the loader silently skipped it — an inconsistency that left the bundle permanently invalid (`migrateLegacyMemory` only runs on the explicit `jeo memory-migrate`). Distill now feeds the legacy doc's content into the concept-merge context so its learnings are absorbed, then archives it to `MEMORY.md.bak` on a successful JSON distill — off the active read path, with rollback preserved.
26
+ - **Concept enumeration skips non-concept blobs.** `existingConcepts` now skips any `.md` without YAML frontmatter, so a stray `MEMORY.md` can never be mis-parsed as a concept during the merge.
27
+
28
+ ### Changed
29
+ - **Distill prompt enforces concept granularity.** The merge prompt now forbids a catch-all "Project Memory Bundle" mega-concept and instructs one concept per distinct fact/command/gotcha/preference, so the bundle grows as discrete, cross-linkable concepts.
30
+ - **Per-session injection budget raised** from 3,000 to 5,000 chars (`MEMORY_INJECT_MAX_CHARS`) so a healthy multi-concept bundle is injected in full.
31
+
32
+ ### Verified
33
+ - `bun run typecheck` clean; full suite `1751 pass / 0 fail` (216 files).
34
+ - Memory/OKF suites `57 pass / 0 fail` across `memory`, `memory-okf`, `memory-distill-okf`, `memory-migration-okf`, `memory-search-okf`, `memory-graph-okf`.
35
+ - Live bundle proof: `validateBundle` flips `false (error: MEMORY.md)` → `true (no issues)` after the legacy archive, injection block intact (3,220 chars); bundle now holds discrete facts/commands/gotchas/preferences concepts + a valid `index.md`.
36
+ - `scripts/mem-probe.ts` (2000 turns × 40 tools) reports no long-term leak (−570 bytes/turn, exit-listeners flat at 1); `scripts/tmux-verify.sh battery` passes 6/6 on a fresh `jeo --tmux` boot.
37
+
38
+
9
39
  ## [0.6.37] - 2026-06-20
10
40
  _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._
11
41
 
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.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
+ - **[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).
203
205
  - **[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.
204
206
  - **[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.
205
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.
206
- - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
207
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
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.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
+ - **[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).
203
205
  - **[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.
204
206
  - **[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.
205
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.
206
- - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
207
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -200,11 +200,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
200
200
  ## Changelog
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
+ - **[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
+ - **[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).
203
205
  - **[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.
204
206
  - **[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.
205
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.
206
- - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
207
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- 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.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
+ - **[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).
203
205
  - **[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.
204
206
  - **[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.
205
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.
206
- - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
207
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.37",
3
+ "version": "0.6.39",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -22,7 +22,7 @@ import { buildConceptGraph, expandByGraph, lintConceptGraph, type GraphLintRepor
22
22
  /** On-disk document cap — the distill prompt instructs the model to stay under it. */
23
23
  export const MEMORY_MAX_CHARS = 6_000;
24
24
  /** Per-session prompt injection budget. */
25
- export const MEMORY_INJECT_MAX_CHARS = 3_000;
25
+ export const MEMORY_INJECT_MAX_CHARS = 5_000;
26
26
  /** Transcript slice fed to the distill call. */
27
27
  const TRANSCRIPT_MAX_CHARS = 12_000;
28
28
  /** A session shorter than this has nothing durable to learn. */
@@ -560,6 +560,7 @@ export async function distillSessionMemory(
560
560
  try {
561
561
  const content = await fs.readFile(file, "utf-8");
562
562
  const parsed = parseConcept(content);
563
+ if (!parsed.hasFrontmatter) continue; // skip MEMORY.md and other legacy blobs
563
564
  existingConcepts.push({
564
565
  type: parsed.frontmatter.type,
565
566
  title: parsed.frontmatter.title || "",
@@ -573,6 +574,11 @@ export async function distillSessionMemory(
573
574
  } catch {}
574
575
  }
575
576
  } catch {}
577
+ // A lingering single-doc MEMORY.md (legacy, or a prior text-only bootstrap
578
+ // fallback) is NOT a concept document — loadConcepts ignores it and it breaks
579
+ // OKF conformance. Feed its content into the merge context so its learnings
580
+ // are absorbed into concepts, then archive it on a successful JSON distill.
581
+ const legacyDoc = await loadMemory(cwd);
576
582
 
577
583
  const prompt: Message[] = [
578
584
  {
@@ -583,12 +589,21 @@ export async function distillSessionMemory(
583
589
  "Keep ONLY what helps future sessions in THIS repository: repo facts (structure, conventions, key files), " +
584
590
  "commands that work (build/test/run), gotchas (failures and their fixes), and user preferences. " +
585
591
  "Drop session-specific noise (one-off tasks, transient errors, conversational detail). " +
592
+ "CRITICAL RULES for concept granularity:\n" +
593
+ " 1. Create ONE concept per distinct fact/command/gotcha/preference — never combine multiple learnings into a single concept.\n" +
594
+ " 2. NEVER create a catch-all 'Project Memory Bundle' or similar mega-concept that lists many things. Split it.\n" +
595
+ " 3. Each Command concept covers exactly one command or workflow (e.g., 'bun test', NOT 'All bun commands').\n" +
596
+ " 4. Each Gotcha covers exactly one failure mode and its fix.\n" +
597
+ " 5. Each UserPreference covers exactly one observable preference.\n" +
598
+ " 6. RepoFact concepts describe structure/conventions — split by area (e.g., 'CLI entrypoint', 'Test setup').\n" +
599
+ " 7. If an existing concept is a mega-concept (lists many things), REPLACE it with properly split granular concepts.\n" +
600
+ " 8. Keep each concept body under 400 chars.\n\n" +
586
601
  "You must output a JSON object with a single key \"concepts\", which is an array of concept objects. " +
587
602
  "Each concept object must have the following fields:\n" +
588
603
  " - \"type\": one of \"RepoFact\", \"Command\", \"Gotcha\", \"UserPreference\"\n" +
589
604
  " - \"title\": a short, descriptive title (e.g., \"Bun test runner\")\n" +
590
605
  " - \"description\": a brief one-line summary of the concept\n" +
591
- " - \"body\": the detailed markdown content/body of the concept\n" +
606
+ " - \"body\": the detailed markdown content/body of the concept (≤400 chars)\n" +
592
607
  " - \"tags\": an array of string tags (optional)\n" +
593
608
  " - \"confidence\": one of \"high\", \"medium\", \"low\" (optional)\n" +
594
609
  " - \"links\": an array of other concept paths/IDs this concept links to (optional)\n\n" +
@@ -598,6 +613,7 @@ export async function distillSessionMemory(
598
613
  role: "user",
599
614
  content:
600
615
  `Existing concepts:\n${JSON.stringify(existingConcepts, null, 2)}\n\n` +
616
+ (legacyDoc ? `Legacy single-doc memory to fold into concepts:\n${legacyDoc}\n\n` : "") +
601
617
  `Session transcript (tail):\n${transcriptTail(history)}`
602
618
  }
603
619
  ];
@@ -695,9 +711,25 @@ export async function distillSessionMemory(
695
711
  if (updatedConcepts.length > 0) {
696
712
  await updateLog(bundleDir, updatedConcepts);
697
713
  }
714
+ // Concepts now own the durable memory and the legacy blob (if any) was
715
+ // folded into the merge above. Archive a lingering MEMORY.md off the active
716
+ // read path so it can never break OKF conformance or shadow the bundle.
717
+ if (legacyDoc) {
718
+ await fs.rename(memoryFilePath(cwd), `${memoryFilePath(cwd)}.bak`).catch(() => {});
719
+ }
698
720
  await cleanupStalePendingFiles(bundleDir);
699
721
  return { updated: true };
700
722
  } else {
723
+ // JSON extraction failed (text-only models often wrap or drop the JSON).
724
+ // The single-doc MEMORY.md is a BOOTSTRAP fallback only: it is injected by
725
+ // memoryPromptSection ONLY when the OKF concept bundle is empty. Writing it
726
+ // while concepts already exist would (a) be silently ignored at injection
727
+ // (concepts win) — losing the learning — and (b) break OKF conformance
728
+ // (MEMORY.md has no frontmatter). So when a bundle exists, do NOT clobber
729
+ // it with a dead blob; keep the prior concepts as the durable memory.
730
+ if (existingConcepts.length > 0) {
731
+ return { updated: false, skipped: "distill produced no JSON; kept existing concept bundle (legacy blob suppressed)" };
732
+ }
701
733
  const doc = distilled.trim().slice(0, MEMORY_MAX_CHARS);
702
734
  if (!doc) return { updated: false, skipped: "model returned an empty document" };
703
735
  const file = memoryFilePath(cwd);
@@ -434,30 +434,49 @@ export interface StreamIdleOptions {
434
434
  * Non-interactive contexts opt in (JEO_STREAM_MAX_MS) so a slow-drip stream
435
435
  * (one token every idleMs-ε) cannot run unbounded. */
436
436
  deadlineAt?: number;
437
+ /** Epoch-ms of the most recent stream ACTIVITY, including reasoning/thinking deltas
438
+ * that are routed to onReasoning and never yielded as a chunk. A long "thinking"
439
+ * phase that streams thought tokens keeps bumping this, so the per-chunk idle
440
+ * watchdog re-arms instead of falsely aborting an actively-reasoning stream. Absent
441
+ * → only a yielded chunk counts as activity (legacy behaviour). */
442
+ lastActivityAt?: () => number;
437
443
  onIdle?: () => void;
438
444
  }
439
445
 
440
- /** `iter.next()`, racing the per-chunk idle timeout AND (when set) the overall deadline. */
446
+ /** `iter.next()`, racing a per-chunk idle watchdog AND (when set) the overall deadline.
447
+ * The watchdog re-arms while reasoning activity (idle.lastActivityAt) keeps advancing,
448
+ * so a model that streams thinking tokens for longer than idleMs before emitting visible
449
+ * text is NOT mistaken for a stalled stream — only a genuinely silent stream aborts. */
441
450
  async function nextMaybeIdle(iter: AsyncIterator<string>, idle?: StreamIdleOptions): Promise<IteratorResult<string>> {
442
451
  if (!idle) return iter.next();
443
- const remaining = idle.deadlineAt !== undefined ? idle.deadlineAt - Date.now() : Infinity;
444
- if (remaining <= 0) {
445
- idle.onIdle?.();
446
- throw new Error(`stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted`);
447
- }
448
- const waitMs = Math.min(idle.idleMs, remaining);
449
- const deadlineFires = remaining < idle.idleMs;
452
+ const next = iter.next();
453
+ // Baseline: this wait began now; reasoning deltas during it move lastActivityAt forward.
454
+ const startedAt = Date.now();
455
+ const lastActivity = () => Math.max(startedAt, idle.lastActivityAt?.() ?? 0);
450
456
  let timer: ReturnType<typeof setTimeout> | undefined;
451
- const timeout = new Promise<never>((_, reject) => {
452
- timer = setTimeout(() => {
453
- idle.onIdle?.();
454
- reject(new Error(deadlineFires
455
- ? `stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted`
456
- : `stream idle for ${idle.idleMs}ms (no chunk) — provider sent no token within the idle window (load or long thinking); retrying. Raise JEO_STREAM_IDLE_MS or lower the thinking level if this persists.`));
457
- }, waitMs);
457
+ const watchdog = new Promise<never>((_, reject) => {
458
+ const arm = () => {
459
+ const now = Date.now();
460
+ const overallRemaining = idle.deadlineAt !== undefined ? idle.deadlineAt - now : Infinity;
461
+ if (overallRemaining <= 0) {
462
+ idle.onIdle?.();
463
+ reject(new Error(`stream exceeded the overall deadline (JEO_STREAM_MAX_MS) — slow-drip stream aborted`));
464
+ return;
465
+ }
466
+ const idleRemaining = idle.idleMs - (now - lastActivity());
467
+ const wait = Math.min(idleRemaining, overallRemaining);
468
+ if (wait <= 0) {
469
+ // overallRemaining is > 0 here (pre-checked), so wait<=0 ⟺ the idle window lapsed.
470
+ idle.onIdle?.();
471
+ reject(new Error(`stream idle for ${idle.idleMs}ms (no chunk) — provider sent no token within the idle window (load or long thinking); retrying. Raise JEO_STREAM_IDLE_MS or lower the thinking level if this persists.`));
472
+ return;
473
+ }
474
+ timer = setTimeout(arm, wait);
475
+ };
476
+ arm();
458
477
  });
459
478
  try {
460
- return await Promise.race([iter.next(), timeout]);
479
+ return await Promise.race([next, watchdog]);
461
480
  } finally {
462
481
  if (timer) clearTimeout(timer);
463
482
  }
@@ -512,15 +531,30 @@ export function createModelManager(): ModelManager {
512
531
  // JEO_STREAM_MAX_MS opts in to an OVERALL deadline (round-14): non-interactive
513
532
  // runs can bound a slow-drip stream the per-chunk idle alone never catches.
514
533
  let attempt: AbortController | null = null;
534
+ // Heartbeat: reasoning/thinking deltas are routed to onReasoning and NEVER yielded
535
+ // as a chunk, so a model that "thinks" (streams thought tokens) for longer than the
536
+ // idle window would otherwise look stalled and trip a false idle-stall retry. Wrap
537
+ // onReasoning/onReasoningStart to stamp lastActivityAt; the idle watchdog re-arms
538
+ // while thinking is actively streaming and only aborts a genuinely silent stream.
539
+ let lastActivityAt = Date.now();
540
+ const bump = () => { lastActivityAt = Date.now(); };
541
+ const userOnReasoning = callOptions.onReasoning;
542
+ const userOnReasoningStart = callOptions.onReasoningStart;
543
+ const liveOptions: CallOptions = {
544
+ ...callOptions,
545
+ onReasoning: (delta: string) => { bump(); userOnReasoning?.(delta); },
546
+ onReasoningStart: () => { bump(); userOnReasoningStart?.(); },
547
+ };
515
548
  const makeIter = () => {
516
549
  attempt = new AbortController();
517
550
  const signal = composeAbort(callOptions.signal, attempt.signal);
518
- return streamFn(messages, { ...callOptions, signal }, credential)[Symbol.asyncIterator]();
551
+ return streamFn(messages, { ...liveOptions, signal }, credential)[Symbol.asyncIterator]();
519
552
  };
520
553
  const maxMs = streamMaxMs();
521
554
  yield* retryableStream(makeIter, retry, {
522
555
  idleMs: streamIdleMs(),
523
556
  ...(maxMs !== undefined ? { deadlineAt: Date.now() + maxMs } : {}),
557
+ lastActivityAt: () => lastActivityAt,
524
558
  onIdle: () => attempt?.abort(),
525
559
  });
526
560
  } else {