jeo-code 0.6.33 → 0.6.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
9
  ## [0.6.33] - 2026-06-19
10
+ ## [0.6.34] - 2026-06-20
11
+ _Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate._
12
+
13
+ ### Added
14
+ - **Sessions remember their model (per-session model selection).** The session JSONL header now carries an optional `model` field: `createSession(cwd, id, model?)` pins it, `updateSessionModel(id, model)` rewrites it in place (no message loss, byte-identical no-op when unchanged), and `loadSession`/`listSessions` restore it. In `launch.ts`, every model change — the `/model` picker, a `model …` action, the OpenAI-compatible-endpoint setter, and live picker selections — is persisted into the active session via a best-effort `persistSessionModel()` (a header-rewrite failure never aborts the turn). On `/resume` (and `--resume`) the session's pinned model is restored unless the CLI explicitly pinned one (`--model`/role/provider wins), so each session can carry its own model independent of the global default. The `--resume` list and the resume picker surface the pinned model (`[provider/model]`).
15
+
16
+ ### Changed
17
+ - **`jeo --tmux` reports a failed attach instead of vanishing.** A nonzero `tmux attach` exit (e.g. `open terminal failed: not a terminal` when stdout isn't a real TTY, a too-small client, or a transient server error) used to be swallowed — jeo returned 0 and left the freshly created session orphaned with no hint. The attach exit code is now surfaced and propagated to `process.exitCode`, and the message is honest about state: it advises `tmux attach -t <session>` only when the session is STILL live, and otherwise reports the session `ended before it could be attached` (so "reattach" is never misleading after an instant inner crash).
18
+ - **tmux session names no longer produce a double dash.** `tmuxSafeNamePart` now trims a trailing dash off the truncated head before appending the disambiguating hash, so a truncation boundary landing right after a `-` yields `name-<hash>` instead of an ugly `name--<hash>`.
19
+ - **`renameSession` shares one header-rewrite path.** Both the rename and the new model-pin go through a single internal `rewriteSessionHeader(id, mutate, cwd)` that locates the JSONL header, applies a mutator (returning `false` to skip the write when nothing changed), and rewrites the file in place — one place for the missing-file/missing-header error handling.
20
+
21
+ ### Verified
22
+ - **No bun memory leak / slowdown.** `scripts/mem-probe.ts` drove 2000–4000 realistic LaunchTui turns: the post-GC heap keeps returning to a flat settled floor (~4.3 MB across turns 200→3400, net **+0.52 MB** vs baseline), with `exit`/`resize`/`SIGINT` process-listener counts stable (no accumulation). The probe's net-growth gate was hardened to measure the **settled floor** (min over the trailing half of samples) rather than the single final sample, since Bun's incremental GC leaves the per-sample heap bimodal — a final sample landing on a transient pre-collection peak was a measurement artifact, not retained memory.
23
+ - **`jeo --tmux` live.** `tmux-verify.sh smoke` OK + `battery` **6/6 PASSED** (boot, `/help`, unknown `$skill`, `/agents`, `$ultragoal`, unresolved `/command`).
24
+ - **Green gates.** `bun run typecheck` clean; `bun test` **1714 pass / 0 fail** (211 files), including the new per-session-model round-trip (`test/session.test.ts`) and tmux attach-failure / double-dash cases (`test/tmux.test.ts`).
25
+
10
26
  _A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification._
11
27
 
12
28
  ### Changed
package/README.ja.md CHANGED
@@ -200,11 +200,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
200
200
  ## 変更履歴 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
203
+ - **[0.6.33]** (2026-06-19)
204
+ - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
204
205
  - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
205
206
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
206
207
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
207
- - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -200,11 +200,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
200
200
  ## 변경 이력 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
203
+ - **[0.6.33]** (2026-06-19)
204
+ - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
204
205
  - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
205
206
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
206
207
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
207
- - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -200,11 +200,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
200
200
  ## Changelog
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
203
+ - **[0.6.33]** (2026-06-19)
204
+ - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
204
205
  - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
205
206
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
206
207
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
207
- - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -200,11 +200,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
200
200
  ## 更新日志 (Changelog)
201
201
 
202
202
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
203
- - **[0.6.33]** (2026-06-19) — A redesigned `jeo` forge mark — a hollow line-board crayfish/eyeglass emblem drawn as thick rounded-corner tubes (no letters, no DNA helix) — that now renders inside compact-scaled forge cards, plus a unified verification directive that adds gjc's test-quality contract, and a fresh `jeo --tmux` no-leak re-verification.
203
+ - **[0.6.33]** (2026-06-19)
204
+ - **[0.6.34]** (2026-06-20) — Per-session model memory — each saved session now remembers the model it was last using and restores it on `/resume` — plus clearer `jeo --tmux` attach diagnostics, a tmux session-name double-dash fix, and a more robust no-leak probe gate.
204
205
  - **[0.6.32]** (2026-06-19) — Anthropic extended thinking is actually enabled now — the request finally sends a `thinking` block (adaptive for Opus/Sonnet 4.6+, budget for older), fixing reasoning on **opus-4-8** — plus a multi-token `/command`·`$skill` trigger highlight that paints every invocation and survives the trailing space, and a fresh `jeo --tmux` no-leak re-verification.
205
206
  - **[0.6.31]** (2026-06-19) — Live "Thinking" indicator for signature-only reasoning models (Anthropic opus-4-7/4-8), a live color cue when a `/command` or `$skill` trigger is recognized in the prompt, and a rich gjc-style `/resume` session picker — plus a fresh `jeo --tmux` no-leak re-verification.
206
207
  - **[0.6.30]** (2026-06-19) — gjc-style intermediate-judgment guard classification extracted from the engine loop, plus a re-verification that `jeo --tmux` does not leak bun memory or slow down.
207
- - **[0.6.29]** (2026-06-19) — Signature-only thinking-block replay (Anthropic opus-4-7/4-8), plus a tmux mouse-flood memory guard confirming `jeo --tmux` does not leak.
208
208
 
209
209
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
210
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.33",
3
+ "version": "0.6.34",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -10,6 +10,9 @@ export interface SessionHeader {
10
10
  timestamp: string;
11
11
  cwd: string;
12
12
  title?: string;
13
+ /** Model id pinned to this session; restored on resume so each session can carry
14
+ * its own model independent of the global default (per-session model selection). */
15
+ model?: string;
13
16
  }
14
17
 
15
18
  export interface SessionEntry {
@@ -35,6 +38,8 @@ export interface SessionSummary {
35
38
  /** Session file size in bytes (for the resume picker's metadata line). */
36
39
  sizeBytes?: number;
37
40
  title?: string;
41
+ /** Model id pinned to this session (header `model`), if any. */
42
+ model?: string;
38
43
  }
39
44
 
40
45
  export const SESSION_VERSION = 1;
@@ -53,7 +58,8 @@ export function sessionPath(id: string, cwd = process.cwd()): string {
53
58
 
54
59
  export async function createSession(
55
60
  cwd = process.cwd(),
56
- id = newSessionId()
61
+ id = newSessionId(),
62
+ model?: string
57
63
  ): Promise<{ id: string; path: string }> {
58
64
  const dir = sessionsDir(cwd);
59
65
  await fs.mkdir(dir, { recursive: true });
@@ -64,6 +70,7 @@ export async function createSession(
64
70
  id,
65
71
  timestamp: new Date().toISOString(),
66
72
  cwd,
73
+ ...(model ? { model } : {}),
67
74
  };
68
75
 
69
76
  const file = sessionPath(id, cwd);
@@ -292,6 +299,7 @@ export async function listSessions(cwd = process.cwd()): Promise<SessionSummary[
292
299
  mtimeMs: stat.mtimeMs,
293
300
  sizeBytes: stat.size,
294
301
  title: header.title,
302
+ model: header.model,
295
303
  });
296
304
  } catch {
297
305
  // Tolerate malformed files (skip them)
@@ -309,10 +317,16 @@ export async function latestSessionId(cwd = process.cwd()): Promise<string | und
309
317
  }
310
318
 
311
319
  /**
312
- * Rename a session by updating the title in its JSONL header.
313
- * Throws a clear Error if the session file does not exist.
320
+ * Locate the session's JSONL header, apply `mutate`, and rewrite the file in place.
321
+ * `mutate` returns false to signal "no change needed" (skips the write). Shared by
322
+ * {@link renameSession} and {@link updateSessionModel}. Throws a clear Error when the
323
+ * session file or its header is missing.
314
324
  */
315
- export async function renameSession(id: string, title: string, cwd = process.cwd()): Promise<void> {
325
+ async function rewriteSessionHeader(
326
+ id: string,
327
+ mutate: (header: SessionHeader) => boolean,
328
+ cwd: string,
329
+ ): Promise<void> {
316
330
  const file = sessionPath(id, cwd);
317
331
  let content: string;
318
332
  try {
@@ -347,11 +361,35 @@ export async function renameSession(id: string, title: string, cwd = process.cwd
347
361
  throw new Error(`Session header missing in session ${id}`);
348
362
  }
349
363
 
350
- header.title = title;
364
+ if (!mutate(header)) return;
351
365
  lines[headerIndex] = JSON.stringify(header);
352
366
  await fs.writeFile(file, lines.join("\n"), "utf8");
353
367
  }
354
368
 
369
+ /**
370
+ * Rename a session by updating the title in its JSONL header.
371
+ * Throws a clear Error if the session file does not exist.
372
+ */
373
+ export async function renameSession(id: string, title: string, cwd = process.cwd()): Promise<void> {
374
+ await rewriteSessionHeader(id, header => {
375
+ header.title = title;
376
+ return true;
377
+ }, cwd);
378
+ }
379
+
380
+ /**
381
+ * Pin a model to a session by updating the `model` field in its JSONL header so a
382
+ * later `/resume` restores it. No-op (no write) when the header already names that
383
+ * model. Throws a clear Error if the session file does not exist.
384
+ */
385
+ export async function updateSessionModel(id: string, model: string, cwd = process.cwd()): Promise<void> {
386
+ await rewriteSessionHeader(id, header => {
387
+ if (header.model === model) return false;
388
+ header.model = model;
389
+ return true;
390
+ }, cwd);
391
+ }
392
+
355
393
  /**
356
394
  * Delete a session file.
357
395
  * Returns false on ENOENT, true on success.
@@ -14,7 +14,11 @@ function hashString(input: string): string {
14
14
  function tmuxSafeNamePart(input: string, max = 32): string {
15
15
  const safe = input.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "value";
16
16
  if (safe.length <= max) return safe;
17
- return `${safe.slice(0, Math.max(1, max - 7))}-${hashString(input)}`;
17
+ // Trim a trailing dash from the truncated head so a boundary landing right
18
+ // after a `-` doesn't produce an ugly `name--<hash>` (double dash). The head
19
+ // is guaranteed non-empty and to start with an alnum (safe is trimmed).
20
+ const head = safe.slice(0, Math.max(1, max - 7)).replace(/-+$/, "") || safe.slice(0, 1);
21
+ return `${head}-${hashString(input)}`;
18
22
  }
19
23
 
20
24
  function tmuxRuntimeSuffix(flags: LaunchFlags): string {
@@ -91,6 +91,7 @@ import {
91
91
  latestSessionId,
92
92
  exportSession,
93
93
  renameSession,
94
+ updateSessionModel,
94
95
  deleteSession,
95
96
  sessionPath,
96
97
  appendCompaction,
@@ -383,8 +384,27 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
383
384
  stdout: "inherit",
384
385
  stderr: "inherit",
385
386
  });
386
- await attach.exited;
387
+ // A nonzero attach exit (e.g. "open terminal failed: not a terminal" when
388
+ // stdout isn't a real TTY, a too-small client, or a transient server error)
389
+ // otherwise vanished: jeo returned 0 and the freshly created session was left
390
+ // orphaned with no hint. Surface it. Only advise reattach when the session is
391
+ // STILL live — if the inner jeo already exited (bad args, instant crash) the
392
+ // session is gone and "reattach" would be misleading.
393
+ const attachCode = await attach.exited;
394
+ if (attachCode !== 0) {
395
+ const alive = Bun.spawnSync([tmuxBin, "has-session", "-t", `=${sessionName}`], {
396
+ stdout: "ignore",
397
+ stderr: "ignore",
398
+ }).exitCode === 0;
399
+ console.error(
400
+ alive
401
+ ? `Error: tmux attach failed (exit ${attachCode}). The session is still running; reattach with: tmux attach -t ${sessionName}`
402
+ : `Error: tmux session ${sessionName} ended before it could be attached (attach exit ${attachCode}).`,
403
+ );
404
+ process.exitCode = attachCode;
405
+ }
387
406
  return;
407
+
388
408
  } else {
389
409
  console.warn("warning: tmux is not available on PATH. Launching directly...");
390
410
  }
@@ -411,7 +431,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
411
431
  }
412
432
  console.log("Saved sessions (newest first):");
413
433
  for (const s of sessions) {
414
- console.log(` ${s.id} ${s.timestamp} (${s.messageCount} msgs) ${s.preview}`);
434
+ console.log(` ${s.id} ${s.timestamp} (${s.messageCount} msgs)${s.model ? ` [${s.model}]` : ""} ${s.preview}`);
415
435
  }
416
436
  console.log("\nResume with: jeo launch --resume <id>");
417
437
  return;
@@ -588,23 +608,37 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
588
608
  const id = flags.resumeId ?? (await latestSessionId(cwd));
589
609
  if (!id) {
590
610
  console.log("No session to resume. Starting a new one.");
591
- sessionId = (await createSession(cwd)).id;
611
+ sessionId = (await createSession(cwd, undefined, sessionModel)).id;
592
612
  } else {
593
613
  try {
594
- const { messages } = await loadSession(id, cwd);
614
+ const { header, messages } = await loadSession(id, cwd);
595
615
  for (const m of messages) history.push(m);
596
616
  sessionId = id;
597
- console.log(`Resumed session ${id} (${messages.length} messages).`);
617
+ // Restore the model this session was last using unless the CLI explicitly
618
+ // pinned one (flags.model/role/provider → initialSessionModel wins).
619
+ if (!initialSessionModel && header.model) sessionModel = header.model;
620
+ const modelNote = sessionModel ? ` · model ${sessionModel}` : "";
621
+ console.log(`Resumed session ${id} (${messages.length} messages).${modelNote}`);
598
622
  } catch (err) {
599
623
  console.log(`Could not resume ${id}: ${(err as Error).message}. Starting fresh.`);
600
- sessionId = (await createSession(cwd)).id;
624
+ sessionId = (await createSession(cwd, undefined, sessionModel)).id;
601
625
  }
602
626
  }
603
627
  } else {
604
- sessionId = (await createSession(cwd)).id;
628
+ sessionId = (await createSession(cwd, undefined, sessionModel)).id;
605
629
  }
606
630
  }
607
631
 
632
+ // Persist the active per-session model into the session header so `/resume` restores
633
+ // it (each session can carry its own model independent of the global default).
634
+ // Best-effort: a header-rewrite failure must never abort the turn.
635
+ const persistSessionModel = async (): Promise<void> => {
636
+ if (flags.noSession || !sessionId || !sessionModel) return;
637
+ try {
638
+ await updateSessionModel(sessionId, sessionModel, cwd);
639
+ } catch { /* best-effort */ }
640
+ };
641
+
608
642
  // `step N/M` display seed: the explicit --max-steps cap, else the dynamic budget's
609
643
  // rolling base — the engine's onBudget event keeps the denominator honest as it grows.
610
644
  const initialStepLimit = flags.maxSteps > 0 ? flags.maxSteps : initialDynamicStepLimit();
@@ -1007,7 +1041,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1007
1041
  history.length = 1;
1008
1042
  if (sessionId && !flags.noSession) {
1009
1043
  try {
1010
- sessionId = (await createSession(cwd)).id;
1044
+ sessionId = (await createSession(cwd, undefined, sessionModel)).id;
1011
1045
  } catch { /* best-effort: in-memory clear already done */ }
1012
1046
  }
1013
1047
  console.log("(history cleared)");
@@ -2317,6 +2351,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2317
2351
  const { resolved, provider } = await describeModel(target);
2318
2352
  const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
2319
2353
  sessionModel = target;
2354
+ await persistSessionModel();
2320
2355
  const defaultThinking = isThinkingLevel(action) ? action : undefined;
2321
2356
  if (defaultThinking) {
2322
2357
  sessionThinking = defaultThinking;
@@ -2873,7 +2908,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2873
2908
  const startFreshSession = async (verb: string): Promise<void> => {
2874
2909
  history.length = 1;
2875
2910
  if (!flags.noSession) {
2876
- sessionId = (await createSession(cwd)).id;
2911
+ sessionId = (await createSession(cwd, undefined, sessionModel)).id;
2877
2912
  advanceSessionBoxColor(); // distinct input-box hue per newly opened session
2878
2913
  console.log(`(${verb} — new session ${sessionId})`);
2879
2914
  } else {
@@ -2916,10 +2951,12 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2916
2951
  const arg = tokens.slice(1).join(" ").trim();
2917
2952
  const applyResume = async (rid: string): Promise<void> => {
2918
2953
  try {
2919
- const { messages } = await loadSession(rid, cwd);
2954
+ const { header, messages } = await loadSession(rid, cwd);
2920
2955
  history.length = 1;
2921
2956
  for (const m of messages) history.push(m);
2922
2957
  sessionId = rid;
2958
+ // Restore the model this session was last using (per-session model).
2959
+ if (header.model) sessionModel = header.model;
2923
2960
  // Seed /retry + reply marker from the last user/assistant turn.
2924
2961
  lastUserInput = ""; lastReply = "";
2925
2962
  for (let k = history.length - 1; k >= 1; k--) {
@@ -3497,6 +3534,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3497
3534
  const qualified = qualifyModelId(modelArg, "openai");
3498
3535
  sessionModel = qualified;
3499
3536
  await saveConfigPatch(raw => rememberModelPatch(raw, qualified));
3537
+ await persistSessionModel();
3500
3538
  console.log(`OpenAI-compatible endpoint set: ${url} · default model ${qualified} — saved to ~/.jeo/config.json.`);
3501
3539
  } else {
3502
3540
  console.log(`OpenAI-compatible endpoint set: ${url} — saved to ~/.jeo/config.json.`);
@@ -4012,6 +4050,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4012
4050
  // MRU persistence: picking a model IS saving it — the newest pick wins
4013
4051
  // as the global default; recents keep the rotation for every session.
4014
4052
  await saveConfigPatch(raw => rememberModelPatch(raw, arg));
4053
+ await persistSessionModel();
4015
4054
  }
4016
4055
  const { resolved, provider } = await describeModel(label);
4017
4056
  const st = statuses.find(s => s.name === provider);