jeo-code 0.8.39 → 0.8.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/notify/daemon-control.ts +65 -4
- package/src/commands/launch/route-slash.ts +25 -14
- package/src/commands/launch.ts +1 -1
- package/src/tui/components/slash.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ 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.40] - 2026-07-21
|
|
10
|
+
_User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."_
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`/route on save` / `/route off save` / bare `/route save`** (`src/commands/launch/route-slash.ts`) — persists `routing.enabled` to `~/.jeo/config.json` (mirrors `/model save`'s existing session-vs-persistent split), so routing preference now survives a restart without re-toggling every session. The bare form persists whatever is currently effective (an active session override, else the config's existing value); the `on`/`off save` forms also set the session-local override in the same call. All three carefully merge into the existing `routing` object rather than replacing it, so previously configured `tiers`/`confidenceThreshold`/`crossProviderPool` are never clobbered.
|
|
14
|
+
|
|
15
|
+
### Verified
|
|
16
|
+
- Live (unmocked), with real credentials and a real config copy: `/route on save` wrote `{"routing":{"enabled":true}}` to disk; a fresh `jeo --tmux` process launched against that same config dir immediately reported `routing: on (this session)` via `/route status` with zero re-toggling.
|
|
17
|
+
- `test/route-slash.test.ts` — 28 pass (was 22): 6 new tests cover `on save`/`off save`/bare `save`, config-field preservation (tiers/confidenceThreshold untouched), and confirming a plain `/route on` (no `save`) never touches disk.
|
|
18
|
+
- Full `bun test` — 3130 pass / 4 pre-existing unrelated flakes (browser screenshot/close timing, LSP definition timing, a perf-threshold test) across 307 files, all 4 confirmed passing in isolated re-runs.
|
|
19
|
+
- `bun run typecheck` — no errors.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Telegram daemon lock PID-reuse safety** (`src/agent/notify/daemon-control.ts`, gjc #2786 parity: "restore macOS daemon signaling (kill(2) + start-time recheck)") — `isPidAlive`'s bare `kill(pid, 0)` cannot distinguish a live daemon from an unrelated process that later reused the same pid after the daemon crashed; reproduced live (`isPidAlive(process.pid)` on an arbitrary unrelated process reports "alive"). New `isLockOwnerAlive` cross-checks the recorded `startedAt` against the OS-reported real process start time (`ps -o etime=`, parsed and diffed with a 10s tolerance) before trusting a lock's pid — used by `acquireDaemonLock`, `daemonStatus`, `startDaemon`, and (most importantly) `stopDaemon`, which previously could have sent a real `SIGTERM` to a completely unrelated process. Falls back to the old existence-only check whenever the OS lookup itself is unavailable (Windows, `ps` missing) — never a regression there.
|
|
23
|
+
- `test/notify-daemon-control.test.ts` — 22 pass (was 19): 3 new tests directly reproduce PID reuse (mismatched `startedAt` on a live pid reports not-alive; `stopDaemon` refuses to signal the mismatched pid and clears the lock instead, verified the unrelated live process survives).
|
|
24
|
+
- `test/daemon-command.test.ts` — fixed to use a real process start time instead of an arbitrary placeholder timestamp (the new stricter check correctly flagged the old fixture as a mismatch).
|
|
25
|
+
|
|
9
26
|
## [0.8.39] - 2026-07-21
|
|
10
27
|
_User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)_
|
|
11
28
|
|
package/README.ja.md
CHANGED
|
@@ -283,11 +283,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
283
283
|
## 変更履歴 (Changelog)
|
|
284
284
|
|
|
285
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
|
|
286
287
|
- **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
|
|
287
288
|
- **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
288
289
|
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
289
290
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
290
|
-
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
291
291
|
|
|
292
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
293
293
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -284,11 +284,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
284
284
|
## 변경 이력 (Changelog)
|
|
285
285
|
|
|
286
286
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
287
|
+
- **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
|
|
287
288
|
- **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
|
|
288
289
|
- **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
289
290
|
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
290
291
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
291
|
-
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
292
292
|
|
|
293
293
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
294
294
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -291,11 +291,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
291
291
|
## Changelog
|
|
292
292
|
|
|
293
293
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
294
|
+
- **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
|
|
294
295
|
- **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
|
|
295
296
|
- **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
296
297
|
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
297
298
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
298
|
-
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
299
299
|
|
|
300
300
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
301
301
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -283,11 +283,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
283
283
|
## 更新日志 (Changelog)
|
|
284
284
|
|
|
285
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.40]** (2026-07-21) — User report (paraphrased): "the /route command doesn't work, fix it." Investigated live before assuming a defect — booted a real `jeo --tmux` session with real Anthropic OAuth credentials from the actual `~/.jeo/config.json` and drove `/route status|on|off|why|history|bogus` plus real end-to-end routed turns. Every subcommand and the routing engine itself (heuristic tiering, `roles.smol` resolution, live provider call, per-turn "thinking" card showing the routed model) worked correctly, including a real stale-catalog edge case (`roles.smol: "claude-haiku-4-5"`, removed from jeo's own catalog in an earlier curation pass, still being accepted directly by the Anthropic API). The one genuine, reproducible gap: `/route on` is SESSION-LOCAL ONLY — `config.routing.enabled` in `~/.jeo/config.json` is never written, so a user has to retype `/route on` at the start of every single session, and (separately, working as designed but easy to miss) an explicit `--model`/`/model` pin silently blocks routing from ever engaging unless `/route on` is run to override it that session. The "settings don't stick" experience is the most likely reading of "doesn't work."
|
|
286
287
|
- **[0.8.39]** (2026-07-21) — User report (paraphrased): "when I copy and paste content from elsewhere, it doesn't need to show every line — like gjc — update it so the composer stays a reasonable size and shows a clear indicator when width/length isn't enough." Investigated live before assuming intent: pasting a large multi-line block into the composer already caps the visible box height and scrolls to the caret, but the scrolled-off content above/below was flagged with a bare `…` — no indication of HOW MUCH text was hidden. (Checked gjc's own actual paste-handling source directly: it does not have a general text-paste-collapse feature either — only image-path detection — so this fix targets the concrete, reproducible UX gap found live, not a specific gjc feature to port.)
|
|
287
288
|
- **[0.8.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
288
289
|
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
289
290
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
290
|
-
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
291
291
|
|
|
292
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
293
293
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -25,6 +25,67 @@ export function isPidAlive(pid: number): boolean {
|
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
export function parseEtimeToMs(raw: string): number | undefined {
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed) return undefined;
|
|
31
|
+
const dayMatch = trimmed.match(/^(\d+)-(.+)$/);
|
|
32
|
+
let days = 0;
|
|
33
|
+
let rest = trimmed;
|
|
34
|
+
if (dayMatch) {
|
|
35
|
+
days = Number(dayMatch[1]);
|
|
36
|
+
rest = dayMatch[2]!;
|
|
37
|
+
}
|
|
38
|
+
const parts = rest.split(":").map(Number);
|
|
39
|
+
if (parts.length < 2 || parts.length > 3 || parts.some(n => !Number.isFinite(n))) return undefined;
|
|
40
|
+
const [h, m, s] = parts.length === 3 ? parts : [0, parts[0]!, parts[1]!];
|
|
41
|
+
return ((days * 24 + h!) * 60 + m!) * 60 * 1000 + s! * 1000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Best-effort actual OS process start time in epoch ms, via `ps -o etime=`
|
|
45
|
+
* (POSIX-portable across macOS/Linux `ps`; unsupported on Windows, where
|
|
46
|
+
* there is no equivalent zero-dependency lookup). Returns `undefined` when
|
|
47
|
+
* the lookup is unavailable or unparseable — callers MUST treat that as
|
|
48
|
+
* "cannot verify" and fall back to the existence-only check, not as "dead". */
|
|
49
|
+
export async function processStartTimeMs(pid: number): Promise<number | undefined> {
|
|
50
|
+
if (process.platform === "win32") return undefined;
|
|
51
|
+
const output = await new Promise<string | undefined>(resolve => {
|
|
52
|
+
let child: ReturnType<typeof nodeSpawn>;
|
|
53
|
+
try {
|
|
54
|
+
child = nodeSpawn("ps", ["-o", "etime=", "-p", String(pid)], { stdio: ["ignore", "pipe", "ignore"] });
|
|
55
|
+
} catch {
|
|
56
|
+
resolve(undefined);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let buf = "";
|
|
60
|
+
child.stdout?.on("data", d => { buf += d; });
|
|
61
|
+
child.on("error", () => resolve(undefined));
|
|
62
|
+
child.on("close", code => resolve(code === 0 ? buf : undefined));
|
|
63
|
+
});
|
|
64
|
+
if (output === undefined) return undefined;
|
|
65
|
+
const ms = parseEtimeToMs(output);
|
|
66
|
+
return ms === undefined ? undefined : Date.now() - ms;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `etime` is truncated to whole seconds and adds a little spawn/poll jitter;
|
|
70
|
+
* genuine PID reuse shows up as a gap of minutes/hours/days, never a few
|
|
71
|
+
* seconds, so a generous-but-tight tolerance loses no real detections. */
|
|
72
|
+
const START_TIME_TOLERANCE_MS = 10_000;
|
|
73
|
+
|
|
74
|
+
/** Stronger alternative to a bare `isPidAlive(lock.pid)`: also cross-checks
|
|
75
|
+
* the recorded `startedAt` against the OS-reported actual process start
|
|
76
|
+
* time to rule out PID reuse — a dead daemon's pid can be reassigned by the
|
|
77
|
+
* OS to a completely unrelated process before the next status/stop check
|
|
78
|
+
* runs, which `kill(pid, 0)` alone cannot distinguish (gjc #2786 parity:
|
|
79
|
+
* "restore macOS daemon signaling (kill(2) + start-time recheck)"). Falls
|
|
80
|
+
* back to the existence-only result whenever the OS lookup itself is
|
|
81
|
+
* unavailable (Windows, `ps` missing, permission denied) — never a
|
|
82
|
+
* regression versus the old behavior in that case. */
|
|
83
|
+
export async function isLockOwnerAlive(lock: DaemonLockInfo): Promise<boolean> {
|
|
84
|
+
if (!isPidAlive(lock.pid)) return false;
|
|
85
|
+
const real = await processStartTimeMs(lock.pid);
|
|
86
|
+
if (real === undefined) return true;
|
|
87
|
+
return Math.abs(real - lock.startedAt) <= START_TIME_TOLERANCE_MS;
|
|
88
|
+
}
|
|
28
89
|
|
|
29
90
|
export async function readDaemonLock(): Promise<DaemonLockInfo | undefined> {
|
|
30
91
|
try {
|
|
@@ -43,7 +104,7 @@ export async function readDaemonLock(): Promise<DaemonLockInfo | undefined> {
|
|
|
43
104
|
export async function acquireDaemonLock(): Promise<{ release: () => Promise<void> } | undefined> {
|
|
44
105
|
await fs.mkdir(notifyDir(), { recursive: true, mode: 0o700 });
|
|
45
106
|
const existing = await readDaemonLock();
|
|
46
|
-
if (existing && existing.pid !== process.pid &&
|
|
107
|
+
if (existing && existing.pid !== process.pid && (await isLockOwnerAlive(existing))) return undefined;
|
|
47
108
|
const info: DaemonLockInfo = { pid: process.pid, startedAt: Date.now() };
|
|
48
109
|
await fs.writeFile(notifyDaemonLockPath(), JSON.stringify(info), { mode: 0o600 });
|
|
49
110
|
return {
|
|
@@ -71,7 +132,7 @@ export async function isNotifyConfigured(): Promise<boolean> {
|
|
|
71
132
|
export async function daemonStatus(): Promise<DaemonStatus> {
|
|
72
133
|
const [lock, configured] = await Promise.all([readDaemonLock(), isNotifyConfigured()]);
|
|
73
134
|
if (!lock) return { configured, running: false, stale: false };
|
|
74
|
-
const alive =
|
|
135
|
+
const alive = await isLockOwnerAlive(lock);
|
|
75
136
|
return { configured, running: alive, stale: !alive, pid: lock.pid, startedAt: lock.startedAt };
|
|
76
137
|
}
|
|
77
138
|
|
|
@@ -115,7 +176,7 @@ async function waitFor(predicate: () => Promise<boolean>, timeoutMs: number, ste
|
|
|
115
176
|
|
|
116
177
|
export async function startDaemon(spawnImpl: SpawnLike = defaultSpawn): Promise<{ ok: boolean; pid?: number; message: string }> {
|
|
117
178
|
const existing = await readDaemonLock();
|
|
118
|
-
if (existing &&
|
|
179
|
+
if (existing && (await isLockOwnerAlive(existing))) {
|
|
119
180
|
return { ok: true, pid: existing.pid, message: `daemon already running (pid ${existing.pid})` };
|
|
120
181
|
}
|
|
121
182
|
// Check BEFORE spawning: an unconfigured daemon exits almost immediately (see
|
|
@@ -138,7 +199,7 @@ export async function startDaemon(spawnImpl: SpawnLike = defaultSpawn): Promise<
|
|
|
138
199
|
|
|
139
200
|
export async function stopDaemon(): Promise<{ ok: boolean; message: string }> {
|
|
140
201
|
const lock = await readDaemonLock();
|
|
141
|
-
if (!lock || !
|
|
202
|
+
if (!lock || !(await isLockOwnerAlive(lock))) {
|
|
142
203
|
await fs.unlink(notifyDaemonLockPath()).catch(() => {});
|
|
143
204
|
return { ok: true, message: "daemon was not running" };
|
|
144
205
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `/route` slash-command handler extracted from launch.ts.
|
|
3
3
|
*
|
|
4
|
-
* Handles `/route [status|on|off|why|history]` (default/`status`: report
|
|
5
|
-
* effective session routing state + the last decision, if any; `on`/`off`:
|
|
4
|
+
* Handles `/route [status|on|off|why|history|save]` (default/`status`: report
|
|
5
|
+
* the effective session routing state + the last decision, if any; `on`/`off`:
|
|
6
6
|
* toggle a SESSION-LOCAL override of `config.routing.enabled`; `why`: explain
|
|
7
7
|
* the last routing decision in detail; `history [n]`: print the last `n`
|
|
8
8
|
* (default 10) recorded decisions, one per line). This block shares mutable
|
|
@@ -11,14 +11,16 @@
|
|
|
11
11
|
* values in via an explicit context object and reads back a result object
|
|
12
12
|
* instead of this function closing over REPL state directly.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* `on`/`off` alone are SESSION-LOCAL only (mirrors `/thinking`), same as
|
|
15
|
+
* before — but `on save`/`off save`/bare `save` now persist `routing.enabled`
|
|
16
|
+
* to `~/.jeo/config.json` (mirrors `/model save`'s explicit opt-in for
|
|
17
|
+
* persistence), so a user who wants routing on by default no longer has to
|
|
18
|
+
* remember to type `/route on` at the start of every session.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import type { RouteDecision } from "../../agent/prompt-router";
|
|
21
22
|
import type { RouteHistoryEntry } from "../../agent/route-history";
|
|
23
|
+
import { saveConfigPatch } from "../../agent/state";
|
|
22
24
|
|
|
23
25
|
export interface RouteSlashCtx {
|
|
24
26
|
sessionRouteOverride: boolean | undefined;
|
|
@@ -44,7 +46,7 @@ export interface RouteSlashResult {
|
|
|
44
46
|
lines: string[];
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
const USAGE = "Usage: /route [status|on|off|why|history [n]]";
|
|
49
|
+
const USAGE = "Usage: /route [status|on|off|why|history [n]|save|on save|off save]";
|
|
48
50
|
|
|
49
51
|
function isRealDecision(decision: RouteDecision | { note: string } | null): decision is RouteDecision {
|
|
50
52
|
return !!decision && "model" in decision;
|
|
@@ -82,11 +84,11 @@ function formatHistory(entries: RouteHistoryEntry[], n: number): string[] {
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
|
-
* Handle `/route [status|on|off|why|history]`. Extracted for the same
|
|
86
|
-
* as `/model`: shares REPL-local routing state with `runTurn` via an
|
|
87
|
+
* Handle `/route [status|on|off|why|history|save]`. Extracted for the same
|
|
88
|
+
* reason as `/model`: shares REPL-local routing state with `runTurn` via an
|
|
87
89
|
* explicit ctx/result object rather than closing over it.
|
|
88
90
|
*/
|
|
89
|
-
export function runRouteSlash(input: string, ctx: RouteSlashCtx): RouteSlashResult {
|
|
91
|
+
export async function runRouteSlash(input: string, ctx: RouteSlashCtx): Promise<RouteSlashResult> {
|
|
90
92
|
const rest = input.slice("/route".length).trim();
|
|
91
93
|
const [sub, arg] = rest.split(/\s+/).filter(Boolean);
|
|
92
94
|
const effective = ctx.sessionRouteOverride ?? ctx.routingConfigEnabled;
|
|
@@ -111,12 +113,21 @@ export function runRouteSlash(input: string, ctx: RouteSlashCtx): RouteSlashResu
|
|
|
111
113
|
|
|
112
114
|
|
|
113
115
|
|
|
114
|
-
if (sub === "on") {
|
|
115
|
-
|
|
116
|
+
if (sub === "on" || sub === "off") {
|
|
117
|
+
const enabled = sub === "on";
|
|
118
|
+
if (arg === "save") {
|
|
119
|
+
await saveConfigPatch(raw => ({ routing: { ...raw.routing, enabled } }));
|
|
120
|
+
return { sessionRouteOverride: enabled, lines: [`routing: ${enabled ? "on" : "off"} (this session) — saved to ~/.jeo/config.json`] };
|
|
121
|
+
}
|
|
122
|
+
return { sessionRouteOverride: enabled, lines: [`routing: ${enabled ? "on" : "off"} (this session)`] };
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
// Bare `/route save` persists whatever is CURRENTLY effective (a prior
|
|
126
|
+
// session-local `on`/`off`, or — if neither was toggled this session —
|
|
127
|
+
// the config's existing value) rather than requiring `/route on save`.
|
|
128
|
+
if (sub === "save") {
|
|
129
|
+
await saveConfigPatch(raw => ({ routing: { ...raw.routing, enabled: effective } }));
|
|
130
|
+
return { lines: [`routing: ${effective ? "on" : "off"} — saved to ~/.jeo/config.json`] };
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
if (sub === "why") {
|
package/src/commands/launch.ts
CHANGED
|
@@ -4810,7 +4810,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4810
4810
|
}
|
|
4811
4811
|
if (input.startsWith("/route") && (input === "/route" || input[6] === " ")) {
|
|
4812
4812
|
const cfgNow = await readGlobalConfig();
|
|
4813
|
-
const routeResult = runRouteSlash(input, {
|
|
4813
|
+
const routeResult = await runRouteSlash(input, {
|
|
4814
4814
|
sessionRouteOverride,
|
|
4815
4815
|
routingConfigEnabled: !!cfgNow.routing?.enabled,
|
|
4816
4816
|
lastRouteDecision,
|
|
@@ -43,7 +43,7 @@ export const SLASH_COMMAND_DETAILS: readonly SlashCommandInfo[] = [
|
|
|
43
43
|
{ command: "/logout", usage: "/logout <anthropic|openai|gemini|antigravity>", description: "Remove the stored OAuth token for a provider", group: "models" },
|
|
44
44
|
{ command: "/roles", usage: "/roles [tier model]", description: "Show or set model role tiers (smol/slow/plan)", group: "models" },
|
|
45
45
|
{ command: "/thinking", usage: "/thinking [level]", description: "Show or set thinking budget (low/medium/high/xhigh)", group: "models" },
|
|
46
|
-
{ command: "/route", usage: "/route [on|off|why|history [n]]", description: "Show/toggle prompt-based model routing for this session, explain the last routing decision, or list recent routing history", group: "models" },
|
|
46
|
+
{ command: "/route", usage: "/route [on|off|why|history [n]|save|on save|off save]", description: "Show/toggle prompt-based model routing for this session, explain the last routing decision, or list recent routing history (add save to persist on/off)", group: "models" },
|
|
47
47
|
{ command: "/agents", usage: "/agents [edit|role] [model|#N|thinking L|maxSteps N|reset]", description: "List subagent roles; use /agents edit for the interactive picker or pin role model/settings", group: "subagents" },
|
|
48
48
|
{ command: "/subagent", usage: "/subagent [role]", description: "Show the current subagent composition (per-role model · thinking · steps); alias of /agents", group: "subagents" },
|
|
49
49
|
{ command: "/view", usage: "/view <file> [a-b]", description: "Render a file with line numbers + light highlight", group: "code" },
|