jeo-code 0.8.24 → 0.8.26
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 +39 -3
- package/README.ja.md +4 -2
- package/README.ko.md +4 -2
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/package.json +1 -1
- package/src/agent/config-schema.ts +1 -1
- package/src/agent/engine.ts +13 -4
- package/src/agent/notify/telegram-daemon.ts +11 -3
- package/src/agent/notify/topic-registry.ts +16 -0
- package/src/agent/opik-tracer.ts +2 -2
- package/src/agent/plan.ts +5 -1
- package/src/agent/web-search.ts +1 -1
- package/src/ai/model-catalog.ts +2 -3
- package/src/ai/model-registry.ts +0 -1
- package/src/cli.ts +8 -0
- package/src/commands/launch.ts +9 -1
- package/src/tui/app.ts +14 -4
- package/src/tui/components/footer.ts +3 -1
- package/src/util/broken-pipe.ts +20 -0
- package/src/util/nofile-limit.ts +66 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@ 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.8.26] - 2026-07-14
|
|
10
|
+
_Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed._
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
- **`claude-haiku-4-5` dropped from the exposed model catalog and built-in aliases** (`src/ai/model-catalog.ts`, `src/ai/model-registry.ts`, `src/agent/config-schema.ts`) — jeo's Anthropic catalog now lists only >=4.6-generation models (sonnet-5, fable-5, mythos-5, opus-4-6/4-7/4-8, sonnet-4-6). The `haiku` built-in alias is removed (no >=4.6 Anthropic small-tier model exists to repoint it at); `web-search.ts`'s default search model moves to `claude-sonnet-4-6`. 27 tests across 6 files updated to the resulting cross-provider fallback pool composition (verified against the REAL routing/pool-selection logic, not asserted from inference — e.g. the trivial-tier pool empties to `defaultModel` when only Anthropic is credentialed, `gpt-4.1` now lands in the small-tier tercile bucket for mixed openai+anthropic configs).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **EPIPE/broken-pipe crash on an early-closing reader** (`src/util/broken-pipe.ts`, `src/cli.ts`) — gjc v0.10.1 parity. `jeo --help | head` (or any piped output whose reader stops early) previously dumped a raw `EPIPE: broken pipe, write` stack through jeo's process-level `uncaughtException`/`unhandledRejection` net and exited 1. Both the fatal handler and the dispatch catch block now recognize `EPIPE`/`ERR_STREAM_DESTROYED` and exit quietly with 141 (128+SIGPIPE, matching what the shell itself reports for a SIGPIPE-killed pipeline producer) — no dump, no misleading "error:" line for a condition that isn't a real failure. A genuine (non-pipe) uncaught exception is unaffected and still dumps + exits 1.
|
|
17
|
+
- **Telegram topic-rename retry after a transient `editForumTopic` failure** (`src/agent/notify/topic-registry.ts`'s new `wouldRename`, `src/agent/notify/telegram-daemon.ts`) — the local `TopicRegistry` previously committed a session's new topic name (`applyName`) BEFORE the remote Telegram rename call, so a transient network blip/rate-limit left the registry believing the rename had already applied; the next identical `identity_header` reassertion (next turn, or a session restart) then silently skipped retrying, permanently stranding the remote forum topic at its provisional "session <shortId>" name. The registry commit now happens only AFTER `editForumTopic` confirms success, so a failed attempt stays retry-eligible.
|
|
18
|
+
- **Context-usage meter now anchored to provider-reported tokens, not a client-side estimate** (`src/agent/engine.ts`'s `AgentLoopEvents.onUsage` gains an optional `lastCall` param, `src/tui/app.ts`, `src/tui/components/footer.ts`) — gjc v0.10.1 parity ("make provider-reported usage the SSOT for context tokens/%"). The TUI footer's context-window percentage was seeded ONCE per turn from a ~4-chars/token client-side heuristic and never updated with the real usage every provider adapter already reports per call — the footer meter could silently drift from what the provider (and any billing) actually measures. `onUsage`'s `lastCall` now carries that single call's own reported `inputTokens` (not the cumulative turn total), and the footer overwrites its estimate with it the instant a real response arrives; the pre-call estimate remains visible only for the brief gap before the first response.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **macOS low file-descriptor-limit startup warning** (`src/util/nofile-limit.ts`, wired into `src/commands/launch.ts`) — gjc v0.9.0 parity. BSD's low default `ulimit -n` (256, sometimes 1024) risks opaque downstream `EMFILE` ("too many open files") failures from file watching, the browser tool, or a broad repo scan. jeo now warns once at launch (stderr only — never pollutes one-shot `-p`'s piped stdout reply) when the EFFECTIVE limit (read AFTER Bun's own startup auto-raise-to-hard-limit, which is what actually governs the process — see the doc comment for why this is a more useful signal than a pre-raise shell setting) is below 4096, with concrete `ulimit -n`/`launchctl limit maxfiles` guidance. Linux-only skipped (distro defaults/conventions vary too widely to assume the same fix applies); opt-out via `JEO_SKIP_NOFILE_CHECK=1`.
|
|
22
|
+
|
|
23
|
+
### Ruled out (investigated, no jeo-code action needed)
|
|
24
|
+
- Anthropic OAuth `strict:true` tool-use bug (gjc v0.9.4/v0.9.5 hotfix) — jeo's Anthropic provider never sends a `strict` field on tool definitions; bug class doesn't exist.
|
|
25
|
+
- Compiled-binary `models.json` import crash (gjc v0.9.4 hotfix) — jeo's catalog is a plain hardcoded TS array, not a separate JSON file loaded at runtime; bug class doesn't exist.
|
|
26
|
+
- Invalid `antigravity/gemini-3.1-pro-high` selector (gjc v0.9.3, PR #1891) — jeo independently fixed the SAME root cause (the backend's own `deprecatedModelIds.newModelId` mapping) a day BEFORE gjc's upstream PR, in v0.8.3, via a `BUILTIN_ALIASES` silent redirect to the successor model — a strictly better UX than gjc's filter-and-force-the-`:effort`-suffix-workaround approach, since existing configs keep working with zero user action. jeo's live-discovery `deprecatedModelIds` filtering (`src/ai/model-discovery.ts`) already covers the dynamic-catalog side too, with regression coverage for the real payload shape.
|
|
27
|
+
- Several Telegram-daemon gjc fixes (topic-delete races, bot-mention gating, ask-reply ack/persist ordering, `/rich`-style durable-toggle confirmation, control slash commands, `forum_topic_edited` user-rename preservation, ask-selection acknowledgement) — each independently verified as architecturally inapplicable (jeo never deletes topics; jeo has no group/multi-user chat model, no `ask` tool, no durable in-thread config commands, and no `forum_topic_edited` handling at all) rather than assumed from the gjc PR title. See the session's full investigation notes for per-item evidence.
|
|
28
|
+
|
|
29
|
+
### Verified
|
|
30
|
+
- Full `bun test` — 3075 pass / 0 fail across 304 files (3050 baseline + 25 new: 8 broken-pipe, 9 nofile-limit incl. a real-process E2E proving the wire end-to-end with an isolated no-credential env — never a real billed LLM call, 3 context-usage-ssot, 4 topic-registry `wouldRename`, 1 telegram-daemon rename-retry integration test).
|
|
31
|
+
- `bun run typecheck` — no errors.
|
|
32
|
+
- Live end-to-end (real CLI entrypoint, no mocks): EPIPE fix reproduced BEFORE fixing (raw `EPIPE: broken pipe, write` dump, non-141 exit) and confirmed fixed after (quiet 141 exit, no dump) via a real Bun child process piped into a reader that never reads a byte. nofile-limit warning reproduced against the REAL Bun runtime behavior (Bun raises its own soft `RLIMIT_NOFILE` to the hard limit at startup — confirmed empirically, not assumed — so the repro lowers both soft AND hard limits) and confirmed the warning lands on stderr only, with stdout's piped reply left uncontaminated. Telegram rename-retry fix mutation-tested: the new regression test was run against the pre-fix source via `git stash` and genuinely failed as predicted, then passed once the fix was restored.
|
|
33
|
+
|
|
34
|
+
## [0.8.25] - 2026-07-13
|
|
35
|
+
_Post-audit follow-up: 0.8.24's fixture-repair subagent flagged (but, correctly, did not itself fix — out of its assigned scope) a case-sensitivity bug in the new PlanSchema maker→verifier ordering rule. Fixed._
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- **PlanSchema ordering rule: role matching is now case-insensitive, matching dispatch** (`src/agent/plan.ts`) — the ordering rule's local `READONLY_ROLES`/`VERIFIER_ROLES` lookup compared `step.role?.trim()` directly, while `subagents.ts`'s `normalizeRoleId` (which every actual role DISPATCH goes through) lowercases first. A plan ending in e.g. `role: ARCHITECT` (or any mixed-case role) resolved correctly at runtime but was WRONGLY rejected by the schema as an unverified mutation (`READONLY_ROLES["ARCHITECT"]` is `undefined`, not `true`) — a real, live discrepancy between what `jeo team` would actually execute and what `PlanSchema`/`jeo approve` would accept. Fixed with `.toLowerCase()` at the one lookup site; an uppercase MUTATING role with no trailing verifier is still correctly rejected (verified — case-insensitivity does not widen `READONLY_ROLES` itself).
|
|
39
|
+
|
|
40
|
+
### Verified
|
|
41
|
+
- Full `bun test` — 3050 pass / 0 fail across 301 files (3049 baseline + 1 new dedicated test: mixed/upper-case verifier roles now accept, an upper-case mutating role with no verifier still correctly rejects).
|
|
42
|
+
- `bun run typecheck` — no errors.
|
|
43
|
+
- Live end-to-end (real CLI entrypoint, no mocks) — genuinely reproduced BEFORE fixing, not merely reasoned about: `git stash` the fix, ran `jeo approve` on a plan ending in `role: ARCHITECT` — refused with exit 1, the exact "unverified mutation" message quoted above; `git stash pop` restored the fix, re-ran the SAME plan — approved successfully, exit 0.
|
|
44
|
+
|
|
9
45
|
## [0.8.24] - 2026-07-13
|
|
10
46
|
_Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap._
|
|
11
47
|
|
|
@@ -14,13 +50,13 @@ _Follow-up gap analysis against the same external "self-improving agent system"
|
|
|
14
50
|
- **Evidence gate: independent verifiers must show their work** (`src/agent/task-tool.ts`'s `SubagentRunResult.readOnlyEvidenceCalls`, `src/commands/team.ts`'s `evaluateSubagentResult`, `src/commands/ralplan.ts`'s `runConsensusCriticGate`) — an architect/critic verdict (`[OKAY]`/`Architectural Status: CLEAR`) was text the model could emit with ZERO real inspection; both independent-verifier code paths in jeo (the per-step role gate AND ralplan's separate consensus-critic gate) now count OBSERVED `read`/`search`/`find`/`ast_grep`/`lsp` calls during the run and hard-fail any read-only-role verdict with zero evidence calls — unconditional, never softened by `--strict-mutations`, mirroring `goal-verifier.ts`'s existing `applyEvidenceGate` philosophy applied to the OTHER verifier path in the codebase.
|
|
15
51
|
- **Safety-boundary automatic model fallback** (`src/agent/engine.ts`'s `AgentLoopOptions.safetyFallbackAvailable`/`SAFETY_FALLBACK_TAG`, wired into `src/agent/task-tool.ts` and `src/commands/launch.ts`) — an UNCATEGORIZED refusal (bare `stop_reason=refusal`/`finish_reason=content_filter`/Gemini `SAFETY`, distinct from a `Refusal (<category>)`-shaped deterministic content-policy hit, which is UNTOUCHED and still hard-fails with zero fallback) previously backed off forever on the SAME model. When a genuinely different-PROVIDER candidate is available (same-provider siblings share the same classifier, so they're excluded too — stricter than the rate-limit fallback's same-credential-scope-only exclusion), the engine now bails with a distinctly-tagged doneReason and the caller switches models instead of riding the unbounded backoff — mirrors the already-shipped rate-limit fast-fallback's exact reroute-and-retry shape (never a bail-only predicate with nothing to switch to), reusing its provider-exclusion accumulation across repeated bails. `launch.ts`'s `routeFailureReason`/`equivalentRouteFallback` gained the same provider-level exclusion for the main interactive turn, and the internal tag is stripped from every user-visible surface (never leaks into a reply or a `[route]` notice).
|
|
16
52
|
- **Memory: earned confidence, active staleness tracking** (`src/agent/memory.ts`'s `isConceptStale`, `upsertConceptFile`'s new `opts.verified`, `distillSessionMemory`'s new `opts.sessionVerified`) — a concept's `last_verified` date was written unconditionally on every distillation pass, meaning "verified" was really just "written," never actually earned by a real verification event. `last_verified` is now written ONLY when the caller explicitly marks the session's distillation as verified; `isConceptStale` (pure, no I/O) treats a concept as stale when it has never been verified OR its last verification is older than 30 days (configurable) — 2026 industry pattern: confidence must be earned and staleness must be actively tracked, not read off a passive write-timestamp. Also fixed: `.jeo/` now self-ignores in git on first creation (`ensureJeoGitignore` in `src/agent/state.ts`) — memory/skill-lesson writes under `.jeo/` are agent-local working state, and without this a CI routine (`jeo routine init` + `create-pull-request`) shipped `.jeo/memory/*`/`.jeo/skills/*` churn into every generated PR.
|
|
17
|
-
- **Dynamic Workflows: `eval` tool, an agent-authored orchestration primitive** (`src/agent/eval-tool.ts`, wired into `src/commands/launch.ts`) — `task`'s `tasks[]` batch is ONE declarative, single-stage, parallel-only fan-out with no way to pipe one subagent's output into the next, branch on a result, or run N stages with a barrier between them. `eval {code, timeoutMs?}` lets the agent write an actual async JS function body composing THREE orchestration primitives — `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)` (plus `log`) — with real loops/conditionals/sequencing. Runs in an isolated `Worker` (own OS thread) with a genuinely preemptive wall-clock timeout via `worker.terminate()` — NOT the same-process `AsyncFunction` pattern `browser {run}` uses, which cannot be given a real timeout at all (a synchronous bug blocks jeo's own event loop; `Promise.race` can never preempt it — verified empirically against a `while(true){}` reproduction). `task()` bridges back to the main thread over a `postMessage` RPC (config/credentials never enter the worker); every dispatch — however the script shapes it, including N raw concurrent `task()` calls that bypass `parallel()`/`pipeline()` entirely — is bounded by a real concurrency semaphore at `MAX_FANOUT` (4) in the RPC handler itself, plus a 40-dispatch total-count cap and a 20-item queue cap per `parallel()`/`pipeline()` call. Trust model: full `bash`-tier process access (NOT a security sandbox — `node:vm`/`vm2` offer no real isolation in 2026; documented as such), gated by the identical deep-interview mutation lock; never exposed to a subagent's own toolset (mirrors `task`'s own recursion-prevention exclusion).
|
|
53
|
+
- **Dynamic Workflows: `eval` tool, an agent-authored orchestration primitive** (`src/agent/eval-tool.ts`, wired into `src/commands/launch.ts`) — `task`'s `tasks[]` batch is ONE declarative, single-stage, parallel-only fan-out with no way to pipe one subagent's output into the next, branch on a result, or run N stages with a barrier between them. `eval {code, timeoutMs?}` lets the agent write an actual async JS function body composing THREE orchestration primitives — `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)` (plus `log`) — with real loops/conditionals/sequencing. Runs in an isolated `Worker` (own OS thread) with a genuinely preemptive wall-clock timeout via `worker.terminate()` — NOT the same-process `AsyncFunction` pattern `browser {run}` uses, which cannot be given a real timeout at all (a synchronous bug blocks jeo's own event loop; `Promise.race` can never preempt it — verified empirically against a `while(true){}` reproduction). `task()` bridges back to the main thread over a `postMessage` RPC (config/credentials never enter the worker); every dispatch — however the script shapes it, including N raw concurrent `task()` calls that bypass `parallel()`/`pipeline()` entirely — is bounded by a real concurrency semaphore at `MAX_FANOUT` (4) in the RPC handler itself, plus a 40-dispatch total-count cap and a 20-item queue cap per `parallel()`/`pipeline()` call. A settle-aware abort signal (separate from the timeout/cancel signal) additionally cancels any fire-and-forget `task()` call the script fired without awaiting — whether still QUEUED behind `MAX_FANOUT` other dispatches or already IN-FLIGHT inside `runSubagentOnce` — the instant the tool itself settles, so a zombie subagent dispatch never keeps running invisibly (burning LLM calls whose result is discarded) after the tool has already returned to the model. Trust model: full `bash`-tier process access (NOT a security sandbox — `node:vm`/`vm2` offer no real isolation in 2026; documented as such), gated by the identical deep-interview mutation lock; never exposed to a subagent's own toolset (mirrors `task`'s own recursion-prevention exclusion).
|
|
18
54
|
|
|
19
55
|
### Verified
|
|
20
|
-
- Full `bun test` —
|
|
56
|
+
- Full `bun test` — 3049 pass / 0 fail across 301 files (2997 baseline (post-audit fixture repair for the 20 pre-existing tests broken by the new PlanSchema ordering rule and evidence gate, none weakened — Pattern A: trailing verifier step; Pattern B: a real read-tool-call evidence turn) + 52 new: 8 dedicated PlanSchema ordering-rule tests covering the reject/accept boundary the fixture repairs alone never exercised, 4 safety-fallback engine tests, 3 task-tool safety-reroute tests, 4 launch.ts safety-fallback end-to-end tests, 23 eval-tool tests (including 3 covering the settle-aware fire-and-forget cancellation fix), plus the memory/gitignore additions).
|
|
21
57
|
- `bun run typecheck` — no errors.
|
|
22
58
|
- Live end-to-end (real CLI entrypoint, no mocks): `jeo approve` on a schema-invalid (unverified-mutation) plan refuses with exit code 1 and the exact ordering-rule message; the real (unmocked) `eval` tool composing `pipeline()` → `parallel()` in one script and returning a structured result; a genuine `while(true){}` script terminated by the Worker timeout at ~803ms for an 800ms budget (proving REAL preemption, not a same-thread race that would have hung indefinitely — confirmed the bug is real by first reproducing the hang against a bare `Promise.race`, which required an external OS-level `timeout` to recover from).
|
|
23
|
-
- Each new feature's highest-value contract mutation-tested against the ACTUAL source, not merely reasoned about: the ordering rule (neutered `ctx.addIssue` in `plan.ts`, full suite re-run — exactly the 4 reject-direction tests among the 8 new dedicated ordering-rule tests failed (13 pass / 4 fail), every accept-direction and unrelated test stayed green, confirming the 12 earlier fixture repairs only ever proved the ACCEPT direction and closing that real coverage gap before shipping; restored,
|
|
59
|
+
- Each new feature's highest-value contract mutation-tested against the ACTUAL source, not merely reasoned about: the ordering rule (neutered `ctx.addIssue` in `plan.ts`, full suite re-run — exactly the 4 reject-direction tests among the 8 new dedicated ordering-rule tests failed (13 pass / 4 fail), every accept-direction and unrelated test stayed green, confirming the 12 earlier fixture repairs only ever proved the ACCEPT direction and closing that real coverage gap before shipping; restored, the full suite green again); the evidence gate (zero-evidence verdict correctly blocked, one real read call correctly clears it); the safety fallback (categorized `Refusal (<category>)` NEVER checks `safetyFallbackAvailable`, confirmed via a spy — true-positive path fully untouched); the eval concurrency semaphore (10 raw concurrent `task()` calls bypassing `parallel()`/`pipeline()` entirely still peak at ≤4 in-flight, proven via a real overlapping-delay probe, not a label); the settle-aware fire-and-forget cancellation (a `task()` call fired without `await` is proven to NEVER reach `callLlm`, verified with a 300ms grace-period probe after the tool returns, not a bare synchronous assertion that would trivially pass even if cancellation were completely broken).
|
|
24
60
|
|
|
25
61
|
## [0.8.23] - 2026-07-12
|
|
26
62
|
_Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause._
|
package/README.ja.md
CHANGED
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
- **セーフティバウンダリの自動モデルフォールバック** — 未分類のセーフティ拒否(本物のコンテンツポリシー違反ではなく、分類器の誤検知の可能性)は、同じモデルで永遠に譲歩するのではなく、実際に別プロバイダのモデルへ切り替わるようになりました — 既存のレート制限時の高速フォールバックと同じパターンです。`Refusal (<category>)` 形式の確定的な拒否は影響を受けず、引き続きフォールバックなしでハードフェイルします。
|
|
55
55
|
- **メモリ: 勝ち取った信頼** — コンセプトの検証日付は、あらゆる書き込みではなく、蒸留パスが明示的に検証済みとマークした場合にのみ記録されるようになりました。`isConceptStale` は受動的なタイムスタンプを信頼する代わりに、未検証(または30日超の陳腐化)のコンセプトを再検証が必要なものとして扱います。
|
|
56
56
|
- **ダイナミックワークフロー(`eval` ツール)** — サブエージェントディスパッチの周りに実際の JS 制御フローを書けます: `task(role, taskText, context?)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`log(message)` を組み合わせ、`task` の単一ステージ `tasks[]` バッチでは表現できない逐次/分岐オーケストレーションを構成します。隔離された Worker スレッドで、真のプリエンプティブなタイムアウト(同一プロセス内のレースではなく `worker.terminate()`)を伴って実行され — `bash` と同じフルプロセスの信頼度で、サンドボックスのふりはせず、同じ interview 変更ロックでゲートされます。
|
|
57
|
+
- **壊れた出力パイプで静かに終了** — 早期に読み取りを止めるコマンドへパイプした場合(`jeo --help | head`、消えたリモートピア)、生の `EPIPE` スタックダンプはもう発生しません — シェルが SIGPIPE で終了したパイプラインプロデューサに対して報告するのと同じコード(141)で静かに終了します。本物のクラッシュには影響せず、引き続き明確に表面化します。
|
|
58
|
+
- **macOS の低いファイルディスクリプタ上限警告** — 低い `ulimit -n`(BSD のデフォルト 256/1024)は、ファイル監視、ブラウザツール、広範なリポジトリスキャンで不明瞭な `EMFILE` 失敗を招くおそれがあります — jeo は起動時に一度(stderr のみ、パイプされる `-p` 出力を汚染しません)、具体的な `ulimit`/`launchctl` ガイダンスとともに警告するようになりました。`JEO_SKIP_NOFILE_CHECK=1` でオプトアウトできます。
|
|
57
59
|
|
|
58
60
|
## インストール
|
|
59
61
|
|
|
@@ -281,11 +283,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
281
283
|
## 変更履歴 (Changelog)
|
|
282
284
|
|
|
283
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
287
|
+
- **[0.8.25]** (2026-07-13) — Post-audit follow-up: 0.8.24's fixture-repair subagent flagged (but, correctly, did not itself fix — out of its assigned scope) a case-sensitivity bug in the new PlanSchema maker→verifier ordering rule. Fixed.
|
|
284
288
|
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
285
289
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
286
290
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
287
|
-
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
288
|
-
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
289
291
|
|
|
290
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
291
293
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
- **안전 경계 자동 모델 폴백** — 미분류 안전 거부(실제 콘텐츠 정책 위반이 아니라 분류기의 오탐일 가능성)가 발생하면 이제 같은 모델에서 영원히 물러서는 대신 실제로 다른 프로바이더의 모델로 전환합니다 — 기존 rate-limit 빠른 폴백과 동일한 방식입니다. `Refusal (<category>)` 형태의 결정론적 거부는 영향받지 않고 여전히 폴백 없이 하드 실패합니다.
|
|
55
55
|
- **메모리: 획득한 신뢰** — 개념의 검증 날짜는 이제 모든 쓰기 시점이 아니라 증류 패스가 명시적으로 검증됨으로 표시할 때만 기록됩니다. `isConceptStale`은 수동적인 타임스탬프를 신뢰하는 대신 미검증(또는 30일 초과) 개념을 재검증이 필요한 것으로 처리합니다.
|
|
56
56
|
- **동적 워크플로(`eval` 도구)** — 서브에이전트 디스패치 주변에 실제 JS 제어 흐름을 작성합니다: `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)`, `log(message)`로 `task`의 단일 스테이지 `tasks[]` 배치로는 표현할 수 없는 순차/분기 오케스트레이션을 구성합니다. 격리된 Worker 스레드에서 진정한 선점형 타임아웃(같은 프로세스 내 경쟁이 아닌 `worker.terminate()`)으로 실행되며 — `bash`와 동일한 풀프로세스 신뢰 수준으로, 샌드박스인 척하지 않고, 동일한 인터뷰 변이 락으로 게이트됩니다.
|
|
57
|
+
- **끊긴 출력 파이프에서 조용히 종료** — 읽기를 일찍 멈추는 명령으로 파이프할 때(`jeo --help | head`, 사라진 원격 피어) 더 이상 원시 `EPIPE` 스택을 덤프하지 않습니다 — 셸이 SIGPIPE로 종료된 파이프라인 생산자에게 보고하는 것과 동일한 코드(141)로 조용히 종료됩니다. 실제 크래시는 영향받지 않고 여전히 명확하게 드러납니다.
|
|
58
|
+
- **macOS 낮은 파일 디스크립터 한도 경고** — 낮은 `ulimit -n`(BSD 기본값 256/1024)은 파일 감시, 브라우저 도구, 넓은 리포지토리 스캔에서 불명확한 `EMFILE` 실패를 유발할 수 있습니다 — jeo는 이제 실행 시 한 번(stderr로만, 파이프되는 `-p` 출력은 오염시키지 않음) `ulimit`/`launchctl` 구체적인 안내와 함께 경고합니다. `JEO_SKIP_NOFILE_CHECK=1`로 옵트아웃할 수 있습니다.
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
## 설치
|
|
@@ -282,11 +284,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
282
284
|
## 변경 이력 (Changelog)
|
|
283
285
|
|
|
284
286
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
287
|
+
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
288
|
+
- **[0.8.25]** (2026-07-13) — Post-audit follow-up: 0.8.24's fixture-repair subagent flagged (but, correctly, did not itself fix — out of its assigned scope) a case-sensitivity bug in the new PlanSchema maker→verifier ordering rule. Fixed.
|
|
285
289
|
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
286
290
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
287
291
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
288
|
-
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
289
|
-
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
290
292
|
|
|
291
293
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
292
294
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
|
|
|
59
59
|
- **Safety-boundary automatic model fallback** — an uncategorized safety refusal (a possible classifier false positive, not a genuine content-policy hit) now switches to a genuinely different-provider model instead of backing off forever on the same one — mirrors the existing rate-limit fast-fallback. A `Refusal (<category>)`-shaped deterministic hit is untouched and still hard-fails with zero fallback.
|
|
60
60
|
- **Memory: earned confidence** — a concept's verification date is now written only when a distillation pass is explicitly marked verified, not on every write; `isConceptStale` treats an unverified (or >30-day-stale) concept as needing re-verification instead of trusting a passive timestamp.
|
|
61
61
|
- **Dynamic Workflows (`eval` tool)** — write real JS control flow around subagent dispatch: `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)`, and `log(message)`, composing sequential/branching orchestration that `task`'s single-stage `tasks[]` batch can't express. Runs in an isolated Worker thread with a genuinely preemptive timeout (`worker.terminate()`, not a same-process race) — same full-process trust as `bash`, no sandbox pretense, gated by the same interview mutation lock.
|
|
62
|
+
- **Quiet exit on a broken output pipe** — piping jeo into a command that stops reading early (`jeo --help | head`, a vanished remote peer) no longer dumps a raw `EPIPE` stack; it exits quietly with the same code (141) a shell reports for any SIGPIPE-killed pipeline producer. A genuine crash is unaffected and still surfaces clearly.
|
|
63
|
+
- **macOS low file-descriptor-limit warning** — a low `ulimit -n` (BSD's 256/1024 default) risks opaque `EMFILE` failures from file watching, the browser tool, or a broad repo scan; jeo now warns once at launch (stderr only, never piped `-p` output) with concrete `ulimit`/`launchctl` guidance. Opt out with `JEO_SKIP_NOFILE_CHECK=1`.
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
## Install
|
|
@@ -289,11 +291,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
289
291
|
## Changelog
|
|
290
292
|
|
|
291
293
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
294
|
+
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
295
|
+
- **[0.8.25]** (2026-07-13) — Post-audit follow-up: 0.8.24's fixture-repair subagent flagged (but, correctly, did not itself fix — out of its assigned scope) a case-sensitivity bug in the new PlanSchema maker→verifier ordering rule. Fixed.
|
|
292
296
|
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
293
297
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
294
298
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
295
|
-
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
296
|
-
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
297
299
|
|
|
298
300
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
299
301
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
- **安全边界自动模型回退** — 未分类的安全拒绝(可能是分类器误报,而非真实的内容策略命中)现在会切换到真正不同提供商的模型,而不是在同一个模型上无限退让 — 与现有的 rate-limit 快速回退是同一套模式。`Refusal (<category>)` 这种形态的确定性拒绝不受影响,仍然零回退硬失败。
|
|
55
55
|
- **内存: 应得的信任** — 概念的验证日期现在只在蒸馏阶段被明确标记为已验证时才会写入,而不是每次写入都记录;`isConceptStale` 把未验证(或超过 30 天过期)的概念视为需要重新验证,而不是信任一个被动的时间戳。
|
|
56
56
|
- **动态工作流(`eval` 工具)** — 围绕子代理派遣编写真正的 JS 控制流: `task(role, taskText, context?)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`log(message)`,组合出 `task` 单阶段 `tasks[]` 批次无法表达的顺序/分支编排。运行在隔离的 Worker 线程中,具备真正的抢占式超时(`worker.terminate()`,而非同进程内的竞争) — 与 `bash` 同等的全进程信任级别,不假装沙箱,受同一个 interview 变更锁门控。
|
|
57
|
+
- **在破损输出管道上安静退出** — 当管道输出到提前停止读取的命令时(`jeo --help | head`、消失的远程对端),不再转储原始的 `EPIPE` 堆栈 — 现在会以 shell 对被 SIGPIPE 终止的管道生产者报告的相同退出码(141)安静退出。真正的崩溃不受影响,仍会清晰地暴露出来。
|
|
58
|
+
- **macOS 低文件描述符上限警告** — 较低的 `ulimit -n`(BSD 默认 256/1024)可能导致文件监视、浏览器工具或大范围仓库扫描出现不明的 `EMFILE` 失败 — jeo 现在会在启动时警告一次(仅 stderr,绝不污染被管道的 `-p` 输出),并给出具体的 `ulimit`/`launchctl` 指引。可通过 `JEO_SKIP_NOFILE_CHECK=1` 选择退出。
|
|
57
59
|
|
|
58
60
|
## 安装
|
|
59
61
|
|
|
@@ -281,11 +283,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
281
283
|
## 更新日志 (Changelog)
|
|
282
284
|
|
|
283
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.26]** (2026-07-14) — Model catalog cleanup (drop sub-4.6 Anthropic `claude-haiku-4-5` from the exposed catalog/aliases per user direction) plus a focused gap analysis against `gajae-code`'s (gjc) 0.8.0→0.10.1 release history (~200 PRs) — most of gjc's surface (RPC/ACP/coordinator-mcp/IRC-sidebar/psmux/Windows-team) doesn't exist in jeo's smaller architecture and was ruled out with direct evidence; a few genuinely applicable gaps were found and closed.
|
|
287
|
+
- **[0.8.25]** (2026-07-13) — Post-audit follow-up: 0.8.24's fixture-repair subagent flagged (but, correctly, did not itself fix — out of its assigned scope) a case-sensitivity bug in the new PlanSchema maker→verifier ordering rule. Fixed.
|
|
284
288
|
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
285
289
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
286
290
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
287
|
-
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
288
|
-
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
289
291
|
|
|
290
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
291
293
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -226,7 +226,7 @@ export const ConfigSchema = z
|
|
|
226
226
|
export type ValidatedConfig = z.infer<typeof ConfigSchema>;
|
|
227
227
|
|
|
228
228
|
/** Validate parsed JSON against the config schema. Returns a tagged result, never throws. */
|
|
229
|
-
const BUILTIN_ALIASES = new Set(["fast", "local", "sonnet", "opus", "
|
|
229
|
+
const BUILTIN_ALIASES = new Set(["fast", "local", "sonnet", "opus", "gpt", "flash"]);
|
|
230
230
|
|
|
231
231
|
function normalizeModelId(id: string | undefined, aliases: Record<string, string>): string | undefined {
|
|
232
232
|
if (!id) return id;
|
package/src/agent/engine.ts
CHANGED
|
@@ -238,8 +238,13 @@ export interface AgentLoopEvents {
|
|
|
238
238
|
onToolProgress?(tool: string, partial: string): void;
|
|
239
239
|
/** Transient progress notice (e.g. "rate limited — retrying in Ns"); NOT a terminal error. */
|
|
240
240
|
onNotice?(message: string): void;
|
|
241
|
-
/** Cumulative token usage after each LLM call — drives live usage meters
|
|
242
|
-
|
|
241
|
+
/** Cumulative token usage after each LLM call — drives live usage meters (billing/
|
|
242
|
+
* `/usage` totals). `lastCall` is the SAME provider response's OWN reported tokens
|
|
243
|
+
* (not summed across steps) — since every step resends the full growing history,
|
|
244
|
+
* `lastCall.inputTokens` is the provider's own measurement of the CURRENT context
|
|
245
|
+
* size and is the correct source for a context-window percentage meter (gjc v0.10.1
|
|
246
|
+
* parity: provider-reported usage as SSOT, not a client-side character estimate). */
|
|
247
|
+
onUsage?(usage: { inputTokens: number; outputTokens: number }, lastCall?: { inputTokens: number; outputTokens: number }): void;
|
|
243
248
|
/** Accumulated streamed model response so far — drives the live reasoning view. Only
|
|
244
249
|
* requested when a consumer sets it (the engine streams solely for the TUI). */
|
|
245
250
|
onModelStream?(textSoFar: string): void;
|
|
@@ -506,6 +511,10 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
506
511
|
let stopKind: "steps" | "time" = "steps";
|
|
507
512
|
let step = 1;
|
|
508
513
|
const acc = { inputTokens: 0, outputTokens: 0 };
|
|
514
|
+
// The most recent SINGLE provider call's OWN reported usage (never summed across
|
|
515
|
+
// steps) — each step resends the whole growing history, so this is the provider's
|
|
516
|
+
// own measurement of the CURRENT context size, not a cumulative billing total.
|
|
517
|
+
let lastCallUsage: { inputTokens: number; outputTokens: number } | undefined;
|
|
509
518
|
let sawUsage = false;
|
|
510
519
|
const finish = (r: AgentLoopResult): AgentLoopResult => (sawUsage ? { ...r, usage: { ...acc } } : r);
|
|
511
520
|
// Salvage a spin-stop into a useful answer (C): instead of returning a bare
|
|
@@ -750,7 +759,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
750
759
|
reasoningEffort: opts.reasoningEffort,
|
|
751
760
|
signal: stepSignal,
|
|
752
761
|
sessionKey: opts.sessionKey,
|
|
753
|
-
onUsage: u => {
|
|
762
|
+
onUsage: u => { const inputTokens = u.inputTokens ?? 0, outputTokens = u.outputTokens ?? 0; acc.inputTokens += inputTokens; acc.outputTokens += outputTokens; lastCallUsage = { inputTokens, outputTokens }; sawUsage = true; },
|
|
754
763
|
onToken,
|
|
755
764
|
onReasoning,
|
|
756
765
|
onReasoningStart: ev.onReasoningStart,
|
|
@@ -948,7 +957,7 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
948
957
|
return finish({ done: false, steps: step, doneReason: `Error: ${message}` });
|
|
949
958
|
}
|
|
950
959
|
flushStreams(); // deliver the final (possibly throttled-out) snapshot before parse/commit
|
|
951
|
-
if (sawUsage) ev.onUsage?.({ ...acc });
|
|
960
|
+
if (sawUsage) ev.onUsage?.({ ...acc }, lastCallUsage ? { ...lastCallUsage } : undefined);
|
|
952
961
|
|
|
953
962
|
let invocation: any;
|
|
954
963
|
try {
|
|
@@ -507,13 +507,21 @@ export class TelegramDaemon {
|
|
|
507
507
|
// `getOrCreateTopic` only names a topic on FIRST creation — a later,
|
|
508
508
|
// more-informative identity (the provisional short-id name gets
|
|
509
509
|
// superseded once the real repo/branch is known) needs an explicit
|
|
510
|
-
// rename,
|
|
511
|
-
|
|
510
|
+
// rename, attempted only when the name actually changed (dedup).
|
|
511
|
+
// The LOCAL registry is committed (`applyName`) only AFTER
|
|
512
|
+
// `editForumTopic` confirms success — committing it first would let a
|
|
513
|
+
// transient remote failure leave the registry believing the rename
|
|
514
|
+
// already applied, so the next identical `identity_header`
|
|
515
|
+
// reassertion would silently skip retrying and the remote topic
|
|
516
|
+
// would stay stuck at its provisional name forever.
|
|
517
|
+
if (this.topics.wouldRename(conn.sessionId, name)) {
|
|
512
518
|
try {
|
|
513
519
|
await this.opts.telegram.editForumTopic(this.opts.chatId, topicId, name);
|
|
520
|
+
this.topics.applyName(conn.sessionId, name);
|
|
514
521
|
void this.persistTopics();
|
|
515
522
|
} catch {
|
|
516
|
-
// best-effort — a failed rename just leaves the provisional name
|
|
523
|
+
// best-effort — a failed rename just leaves the provisional name,
|
|
524
|
+
// and (unlike before) leaves the registry retry-eligible too.
|
|
517
525
|
}
|
|
518
526
|
}
|
|
519
527
|
}
|
|
@@ -109,6 +109,22 @@ export class TopicRegistry {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Pure peek: true when applying `name` to `sessionId`'s topic would
|
|
114
|
+
* actually change it (no topic record, or already at that name, both
|
|
115
|
+
* return false). Does NOT mutate — pair with `applyName`, called only
|
|
116
|
+
* AFTER a remote rename (`editForumTopic`) confirms success. Committing
|
|
117
|
+
* the name here unconditionally (as a prior version did) let a transient
|
|
118
|
+
* remote failure leave the LOCAL registry believing the rename had already
|
|
119
|
+
* applied, so the next identical `identity_header` reassertion silently
|
|
120
|
+
* skipped retrying and the remote topic stayed stuck at its provisional
|
|
121
|
+
* name forever.
|
|
122
|
+
*/
|
|
123
|
+
wouldRename(sessionId: string, name: string): boolean {
|
|
124
|
+
const record = this.topics.get(sessionId);
|
|
125
|
+
return !!record && record.name !== name;
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
/**
|
|
113
129
|
* Record the topic's applied title. Returns `true` when it changed (so the
|
|
114
130
|
* caller should `editForumTopic`), `false` when already current or unknown.
|
package/src/agent/opik-tracer.ts
CHANGED
|
@@ -355,9 +355,9 @@ export function wrapEvents(events: AgentLoopEvents | undefined, tracer: OpikTrac
|
|
|
355
355
|
} catch { /* I2 */ }
|
|
356
356
|
base.onToolResult?.(tool, success, output);
|
|
357
357
|
},
|
|
358
|
-
onUsage(usage: { inputTokens: number; outputTokens: number }) {
|
|
358
|
+
onUsage(usage: { inputTokens: number; outputTokens: number }, lastCall?: { inputTokens: number; outputTokens: number }) {
|
|
359
359
|
try { tracer.usage(usage); } catch { /* I2 */ }
|
|
360
|
-
base.onUsage?.(usage);
|
|
360
|
+
base.onUsage?.(usage, lastCall);
|
|
361
361
|
},
|
|
362
362
|
};
|
|
363
363
|
return wrapped;
|
package/src/agent/plan.ts
CHANGED
|
@@ -89,7 +89,11 @@ export const PlanSchema = z.object({
|
|
|
89
89
|
}
|
|
90
90
|
let pendingUnverifiedMutation: string | undefined;
|
|
91
91
|
for (const unit of units) {
|
|
92
|
-
|
|
92
|
+
// .toLowerCase() mirrors subagents.ts's normalizeRoleId (dispatch is
|
|
93
|
+
// case-insensitive) — without it, a plan ending in e.g. `role: ARCHITECT`
|
|
94
|
+
// would resolve correctly at runtime but be WRONGLY rejected here as an
|
|
95
|
+
// unverified mutation (READONLY_ROLES["ARCHITECT"] is undefined, not true).
|
|
96
|
+
const roleIds = unit.steps.map(s => (s.role?.trim().toLowerCase() || DEFAULT_STEP_ROLE));
|
|
93
97
|
const unitMutates = roleIds.some(r => READONLY_ROLES[r] !== true);
|
|
94
98
|
const unitVerifies = roleIds.some(r => VERIFIER_ROLES[r] === true);
|
|
95
99
|
if (unitMutates) {
|
package/src/agent/web-search.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface SearchProviderDef {
|
|
|
65
65
|
|
|
66
66
|
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
67
67
|
const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
|
|
68
|
-
const DEFAULT_SEARCH_MODEL = "claude-haiku-4-5
|
|
68
|
+
const DEFAULT_SEARCH_MODEL = "claude-sonnet-4-6"; // haiku-4-5 dropped from jeo's exposed model list (>=4.6 only); sonnet-4-6 is the cheapest surviving Anthropic model
|
|
69
69
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
70
70
|
const HARD_TIMEOUT_MS = 90_000;
|
|
71
71
|
const SEARCH_SYSTEM_PROMPT =
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -80,7 +80,7 @@ const STD: ThinkLevel[] = ["low", "medium", "high"];
|
|
|
80
80
|
* (`cheapestCredentialed`/`strongestCredentialed`/`compareStrengthAscending`)
|
|
81
81
|
* now tiebreak on `releaseDate` directly rather than relying on position. */
|
|
82
82
|
export const MODEL_CATALOG: readonly CatalogModel[] = [
|
|
83
|
-
// Anthropic (newest first: sonnet-5 > fable-5 > opus-4-8 > opus-4-7 > sonnet-4-6 > opus-4-6
|
|
83
|
+
// Anthropic (newest first: sonnet-5 > fable-5 > opus-4-8 > opus-4-7 > sonnet-4-6 > opus-4-6). 4.5-and-below dropped from jeo's exposed model list per user direction.
|
|
84
84
|
{ canonical: "claude-sonnet-5", provider: "anthropic", providerModel: "claude-sonnet-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
|
|
85
85
|
// Fable 5 = Anthropic's most capable widely-released model (adaptive thinking always on);
|
|
86
86
|
// Mythos 5 is limited-availability (Project Glasswing) but callable by id for approved accounts.
|
|
@@ -92,12 +92,11 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
|
|
|
92
92
|
// The nativizable path still replays signature-only thinking blocks for cross-turn continuity.
|
|
93
93
|
// The 4.6 generation onward is 1M context / 128k sync max output (Anthropic docs:
|
|
94
94
|
// Opus 4.8 & Sonnet 5 comparison table; Opus 4.6/4.7, Sonnet 4.6 share the gen's
|
|
95
|
-
// dateless-snapshot 1M/128k + 300k batch-output beta).
|
|
95
|
+
// dateless-snapshot 1M/128k + 300k batch-output beta).
|
|
96
96
|
{ canonical: "claude-opus-4-8", provider: "anthropic", providerModel: "claude-opus-4-8", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-05" },
|
|
97
97
|
{ canonical: "claude-opus-4-7", provider: "anthropic", providerModel: "claude-opus-4-7", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
|
|
98
98
|
{ canonical: "claude-sonnet-4-6", provider: "anthropic", providerModel: "claude-sonnet-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
|
|
99
99
|
{ canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
|
|
100
|
-
{ canonical: "claude-haiku-4-5", provider: "anthropic", providerModel: "claude-haiku-4-5-20251001", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true, releaseDate: "2025-10" },
|
|
101
100
|
// OpenAI (newest first: gpt-5.6 > gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
|
|
102
101
|
{ canonical: "gpt-5.6", provider: "openai", providerModel: "gpt-5.6", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
|
|
103
102
|
{ canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
|
package/src/ai/model-registry.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { dispatch } from "./cli/runner";
|
|
3
3
|
import { restoreTerminalState } from "./util/terminal-restore";
|
|
4
|
+
import { isBrokenPipeError, BROKEN_PIPE_EXIT_CODE } from "./util/broken-pipe";
|
|
4
5
|
import pkg from "../package.json";
|
|
5
6
|
|
|
6
7
|
const APP_NAME = "jeo";
|
|
@@ -22,8 +23,14 @@ process.title = APP_NAME;
|
|
|
22
23
|
// the REPL has put stdin in raw mode + enabled bracketed paste. Without this the
|
|
23
24
|
// shell is left mute ("error printed, then input is dead"). Restore the terminal
|
|
24
25
|
// SYNCHRONOUSLY, print one clean line, and exit non-zero — never a raw stack dump.
|
|
26
|
+
//
|
|
27
|
+
// A downstream pipe reader that stops early (`jeo --help | head`, a vanished
|
|
28
|
+
// socket peer) makes the NEXT stdout/stderr write throw EPIPE from an async tick
|
|
29
|
+
// outside any user try/catch — that is NOT a real fatal, so it exits quietly
|
|
30
|
+
// (matching the shell's own SIGPIPE exit code) instead of dumping the raw error.
|
|
25
31
|
const fatal = (err: unknown): never => {
|
|
26
32
|
restoreTerminalState();
|
|
33
|
+
if (isBrokenPipeError(err)) process.exit(BROKEN_PIPE_EXIT_CODE);
|
|
27
34
|
const msg = (err as Error)?.message ?? String(err);
|
|
28
35
|
process.stderr.write(`error: ${msg}\n`);
|
|
29
36
|
process.exit(1);
|
|
@@ -37,6 +44,7 @@ try {
|
|
|
37
44
|
} catch (err) {
|
|
38
45
|
// Service-readiness: never surface a raw stack trace to users; clean error + non-zero exit.
|
|
39
46
|
restoreTerminalState();
|
|
47
|
+
if (isBrokenPipeError(err)) process.exit(BROKEN_PIPE_EXIT_CODE);
|
|
40
48
|
process.stderr.write(`error: ${(err as Error)?.message ?? String(err)}\n`);
|
|
41
49
|
process.exit(1);
|
|
42
50
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -37,8 +37,9 @@ import { staticCompletionContext, readlineCompleter, formatCompletionPreview, fo
|
|
|
37
37
|
import { normalizeBaseUrl } from "./setup-helpers";
|
|
38
38
|
import { EVOLUTION_STAGES, animateAsciiArt } from "../tui/components/ascii-art";
|
|
39
39
|
import { getEvolutionTip } from "../tui/components/evolution";
|
|
40
|
-
import { renderWelcome, playWelcomeSweep } from "../tui/components/welcome";
|
|
41
40
|
import { checkForUpdate, readUpdateCache, writeUpdateCache } from "../util/update-check";
|
|
41
|
+
import { checkNofileLimit } from "../util/nofile-limit";
|
|
42
|
+
import { renderWelcome, playWelcomeSweep } from "../tui/components/welcome";
|
|
42
43
|
import { jeoEnv } from "../util/env";
|
|
43
44
|
import { renderUpdateBox } from "../tui/components/update-box";
|
|
44
45
|
import { consumeLaunchWhatsNew } from "../util/whats-new";
|
|
@@ -366,6 +367,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
366
367
|
process.exitCode = 1;
|
|
367
368
|
return;
|
|
368
369
|
}
|
|
370
|
+
// macOS startup preflight: a low soft fd limit (BSD default is often 256-1024)
|
|
371
|
+
// surfaces as an opaque downstream EMFILE ("too many open files") from file
|
|
372
|
+
// watching, the browser tool, or a broad repo scan — warn up front with
|
|
373
|
+
// actionable guidance instead of letting the user hit that later. No-op on
|
|
374
|
+
// non-macOS platforms and opt-out-able via JEO_SKIP_NOFILE_CHECK=1.
|
|
375
|
+
const nofileWarning = checkNofileLimit();
|
|
376
|
+
if (nofileWarning) console.error(nofileWarning);
|
|
369
377
|
|
|
370
378
|
if (flags.worktree) {
|
|
371
379
|
const wt = resolveWorktree(cwd, flags.worktree);
|
package/src/tui/app.ts
CHANGED
|
@@ -453,7 +453,11 @@ export class LaunchTui {
|
|
|
453
453
|
this.draw();
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
/**
|
|
456
|
+
/** Seed the footer's context meter with a client-side character-count ESTIMATE
|
|
457
|
+
* before the turn's first provider call returns (called once, right after the
|
|
458
|
+
* user prompt is appended). The `onUsage` handler below overwrites
|
|
459
|
+
* `contextUsedTokens` with the provider's own reported usage the moment a real
|
|
460
|
+
* response arrives, so this estimate is only ever visible for the pre-call gap. */
|
|
457
461
|
setContextUsage(usedTokens: number, maxTokens?: number): void {
|
|
458
462
|
this.footer.contextUsedTokens = usedTokens;
|
|
459
463
|
this.footer.contextMaxTokens = maxTokens;
|
|
@@ -719,10 +723,16 @@ export class LaunchTui {
|
|
|
719
723
|
this.footer.provider = provider;
|
|
720
724
|
this.draw();
|
|
721
725
|
},
|
|
722
|
-
onUsage: (u: { inputTokens: number; outputTokens: number }) => {
|
|
723
|
-
//
|
|
724
|
-
// (and available to the footer meter).
|
|
726
|
+
onUsage: (u: { inputTokens: number; outputTokens: number }, lastCall?: { inputTokens: number; outputTokens: number }) => {
|
|
727
|
+
// Cumulative turn usage — shown in the final summary/cost meter (unchanged).
|
|
725
728
|
this.turnUsage = u;
|
|
729
|
+
// Context-window meter (gjc v0.10.1 parity): once a provider response reports
|
|
730
|
+
// its OWN usage, that call's `inputTokens` IS the provider's own measurement of
|
|
731
|
+
// the current context size — since every step resends the whole growing
|
|
732
|
+
// history, this is strictly more accurate than the pre-call character-count
|
|
733
|
+
// estimate `setContextUsage` seeded the footer with. Overwrite the estimate the
|
|
734
|
+
// instant real data exists; `contextMaxTokens` (the catalog ceiling) is untouched.
|
|
735
|
+
if (lastCall) this.footer.contextUsedTokens = lastCall.inputTokens;
|
|
726
736
|
this.draw();
|
|
727
737
|
},
|
|
728
738
|
};
|
|
@@ -20,7 +20,9 @@ export interface FooterData {
|
|
|
20
20
|
unicode?: boolean;
|
|
21
21
|
/** Colorize the stage track (default true). Set false for the mono theme. */
|
|
22
22
|
color?: boolean;
|
|
23
|
-
/**
|
|
23
|
+
/** Context usage in tokens. Provider-anchored once a call has returned this turn
|
|
24
|
+
* (the last response's own reported `inputTokens` — gjc v0.10.1 SSOT parity);
|
|
25
|
+
* a client-side character-count estimate only for the brief pre-first-response gap. */
|
|
24
26
|
contextUsedTokens?: number;
|
|
25
27
|
/** Provider/model context window in tokens, when known. */
|
|
26
28
|
contextMaxTokens?: number;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A downstream pipe reader that exits early (e.g. `jeo --help | head -c 10`, or a
|
|
3
|
+
* background socket peer vanishing mid-write) makes the NEXT stdout/stderr write
|
|
4
|
+
* throw EPIPE from an async tick where no user try/catch is in scope — the
|
|
5
|
+
* process-level `uncaughtException`/`unhandledRejection` net in cli.ts then treats
|
|
6
|
+
* it as a genuine fatal error and dumps a raw stack/message. Under Bun a non-TTY
|
|
7
|
+
* stdout is an fs stream over a pipe fd, so this is reachable any time jeo's output
|
|
8
|
+
* is piped into a command that stops reading (`| head`, `| true`, a closed socket).
|
|
9
|
+
*
|
|
10
|
+
* `isBrokenPipeError` lets the process-level handler distinguish this class from a
|
|
11
|
+
* REAL fatal so it can exit quietly instead of dumping. 141 = 128 + SIGPIPE, matching
|
|
12
|
+
* what a shell reports for a SIGPIPE-killed producer in a `foo | head` pipeline —
|
|
13
|
+
* consistent with what the reader side of the SAME pipeline already sees.
|
|
14
|
+
*/
|
|
15
|
+
export const BROKEN_PIPE_EXIT_CODE = 141;
|
|
16
|
+
|
|
17
|
+
export function isBrokenPipeError(err: unknown): boolean {
|
|
18
|
+
const code = (err as { code?: unknown } | null)?.code;
|
|
19
|
+
return code === "EPIPE" || code === "ERR_STREAM_DESTROYED";
|
|
20
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jeoEnv } from "./env";
|
|
2
|
+
|
|
3
|
+
/** Below this soft `ulimit -n`, file watching, the browser tool, image/attachment
|
|
4
|
+
* workflows, and broad repo scans (`grep`/`glob`/`ast_grep` across a large tree)
|
|
5
|
+
* risk opaque downstream EMFILE ("too many open files") failures on macOS — whose
|
|
6
|
+
* default per-process soft limit (256, sometimes 1024) is far below Linux's typical
|
|
7
|
+
* 1024-65536 default. This mirrors macOS's own `launchctl`/`ulimit` guidance
|
|
8
|
+
* threshold, not an arbitrary jeo-specific number. */
|
|
9
|
+
export const LOW_NOFILE_THRESHOLD = 4096;
|
|
10
|
+
|
|
11
|
+
/** Read the CURRENT process's EFFECTIVE `RLIMIT_NOFILE` via `ulimit -n` (POSIX
|
|
12
|
+
* shells expose no other portable way to read this from Bun/Node — there is no
|
|
13
|
+
* `getrlimit` binding in either runtime). Bun raises its own process's soft limit
|
|
14
|
+
* to the hard limit at startup (documented behavior, capped at 1_048_576), so a
|
|
15
|
+
* spawned `sh -c "ulimit -n"` child inherits that ALREADY-RAISED value, not
|
|
16
|
+
* whatever a wrapping shell's `ulimit -n` set before launching jeo — this reads
|
|
17
|
+
* the value that ACTUALLY governs the process's file-descriptor budget, which is
|
|
18
|
+
* arguably more useful than the pre-raise shell setting: it only reads low when
|
|
19
|
+
* even Bun's own auto-raise stayed low, meaning the HARD limit itself (verifiable
|
|
20
|
+
* with the shell's own `ulimit -Hn`) is the real ceiling. Returns `null` when the
|
|
21
|
+
* value can't be determined (non-POSIX shell, `ulimit` unavailable, or a parse
|
|
22
|
+
* failure) so the caller can fail OPEN (never warn on an unknown limit — a
|
|
23
|
+
* false-positive warning on every launch would be worse than the rare missed true
|
|
24
|
+
* positive). */
|
|
25
|
+
export function readSoftNofileLimit(): number | null {
|
|
26
|
+
try {
|
|
27
|
+
const result = Bun.spawnSync(["sh", "-c", "ulimit -n"]);
|
|
28
|
+
if (result.exitCode !== 0) return null;
|
|
29
|
+
const text = result.stdout.toString("utf-8").trim();
|
|
30
|
+
// A "unlimited" soft limit reports as the literal string "unlimited" on some
|
|
31
|
+
// shells — that's the OPPOSITE of the low-limit problem this check exists for.
|
|
32
|
+
if (text === "unlimited") return Number.POSITIVE_INFINITY;
|
|
33
|
+
const n = Number(text);
|
|
34
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Startup preflight: on macOS only (BSD's low default is the specific pain point;
|
|
41
|
+
* Linux distros vary too widely in their own defaults/conventions to assume the
|
|
42
|
+
* same fix applies), warn once when the EFFECTIVE file-descriptor limit (see
|
|
43
|
+
* `readSoftNofileLimit`'s doc comment for what "effective" means once Bun's own
|
|
44
|
+
* startup auto-raise is accounted for) is below `LOW_NOFILE_THRESHOLD`.
|
|
45
|
+
* `JEO_SKIP_NOFILE_CHECK=1` opts out entirely. Returns the warning text (never
|
|
46
|
+
* throws, never exits) so callers choose how/whether to print it — `null` when no
|
|
47
|
+
* warning applies (limit is fine, platform isn't macOS, opted out, or the limit
|
|
48
|
+
* couldn't be determined). */
|
|
49
|
+
export function checkNofileLimit(
|
|
50
|
+
platform: NodeJS.Platform = process.platform,
|
|
51
|
+
env: Record<string, string | undefined> = process.env,
|
|
52
|
+
readLimit: () => number | null = readSoftNofileLimit,
|
|
53
|
+
): string | null {
|
|
54
|
+
if (platform !== "darwin") return null;
|
|
55
|
+
if (jeoEnv("SKIP_NOFILE_CHECK", env) === "1") return null;
|
|
56
|
+
const limit = readLimit();
|
|
57
|
+
if (limit === null || limit >= LOW_NOFILE_THRESHOLD) return null;
|
|
58
|
+
return (
|
|
59
|
+
`warning: file descriptor limit is low (${limit}, recommended >= ${LOW_NOFILE_THRESHOLD}). ` +
|
|
60
|
+
"File watching, the browser tool, and broad repo scans may fail with \"too many open files\". Raise it with:\n" +
|
|
61
|
+
" ulimit -n 4096\n" +
|
|
62
|
+
" sudo launchctl limit maxfiles 4096 65536\n" +
|
|
63
|
+
"(avoid huge values like 2147483646 — macOS commonly rejects or clamps them). " +
|
|
64
|
+
"Set JEO_SKIP_NOFILE_CHECK=1 to silence this check."
|
|
65
|
+
);
|
|
66
|
+
}
|