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 +30 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/memory.ts +34 -2
- package/src/ai/model-manager.ts +51 -17
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
package/src/agent/memory.ts
CHANGED
|
@@ -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 =
|
|
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);
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -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
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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([
|
|
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, { ...
|
|
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 {
|