From d7fe28bcbf0b8e0efb6f57cc227ba9f556b53c0b Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Sat, 11 Jul 2026 00:50:17 -0700 Subject: [PATCH] fix(coding-agent): rejected unknown continued session ids Treated a UUID passed after --continue as an explicit session target so a missing id cannot fall back to the latest persisted session. Signed-off-by: Christian Stewart --- packages/coding-agent/CHANGELOG.md | 3 + packages/coding-agent/src/main.ts | 15 +++++ .../main-session-resolution-error.test.ts | 63 ++++++++++++++++++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9b021d8d6..30df29cc8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -67,6 +67,9 @@ - Fixed native Windows binary compatibility on older Windows 10 CPUs by building the `omp-windows-x64.exe` release asset with a baseline x64 runtime instead of AVX2. (#5172) - Fixed `GenerateImage` rejecting OpenAI Codex-compatible proxy bearer keys when the token does not expose a `chatgpt-account-id`. (#5174) - Fixed context promotion documentation to accurately reflect the `contextPromotionTarget` runtime behavior and `contextPromotion.enabled` default. (#5163) +### Fixed + +- Fixed `--continue ` falling back to an unrelated latest session when the requested session does not exist. ## [16.4.3] - 2026-07-11 diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 0b2b119d9..1b5d6ec3d 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -634,6 +634,14 @@ async function getChangelogForDisplay(parsed: Args): Promise return undefined; } +const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +function positionalContinueSessionId(parsed: Args): string | undefined { + if (!parsed.continue || parsed.resume || parsed.fork || parsed.messages.length !== 1) return undefined; + const message = parsed.messages[0]?.trim(); + return message && SESSION_ID_ARG_RE.test(message) ? message : undefined; +} + /** Resolves CLI session flags into an existing, forked, in-memory, or cancelled session manager. */ export async function createSessionManager( parsed: Args, @@ -663,6 +671,13 @@ export async function createSessionManager( if (parsed.noSession) { return SessionManager.inMemory(); } + const continueSessionArg = positionalContinueSessionId(parsed); + if (continueSessionArg) { + parsed.resume = continueSessionArg; + parsed.continue = false; + parsed.messages = []; + } + if (typeof parsed.resume === "string") { const sessionArg = parsed.resume; if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) { diff --git a/packages/coding-agent/test/main-session-resolution-error.test.ts b/packages/coding-agent/test/main-session-resolution-error.test.ts index 2b3009b36..f79a31a9d 100644 --- a/packages/coding-agent/test/main-session-resolution-error.test.ts +++ b/packages/coding-agent/test/main-session-resolution-error.test.ts @@ -6,14 +6,19 @@ * `[Uncaught Exception]`. */ import { describe, expect, it, vi } from "bun:test"; +import * as fsp from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; import type { Args } from "@oh-my-pi/pi-coding-agent/cli/args"; import type { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { createSessionManager, SessionResolutionError, writeStartupNotice } from "@oh-my-pi/pi-coding-agent/main"; import * as sessionListingModule from "@oh-my-pi/pi-coding-agent/session/session-listing"; +import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; -function buildResumeArgs(resume: string): Args { +function buildResumeArgs(resume: string, sessionDir?: string): Args { return { resume, + sessionDir, messages: [], fileArgs: [], unknownFlags: new Map(), @@ -21,6 +26,17 @@ function buildResumeArgs(resume: string): Args { }; } +function buildContinueArgs(message: string, sessionDir?: string): Args { + return { + continue: true, + sessionDir, + messages: [message], + fileArgs: [], + unknownFlags: new Map(), + unrecognizedFlags: [], + }; +} + function buildForkArgs(fork: string, noSession = false): Args { return { fork, @@ -107,6 +123,51 @@ describe("createSessionManager — missing session (#2084)", () => { } }); + it("rejects --resume with unknown id instead of falling back to latest persisted session", async () => { + const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-resume-unknown-id-")); + const sessionDir = path.join(cwd, "sessions"); + const missingId = "019ea530-ffff-7000-8000-000000000000"; + try { + const latest = SessionManager.create(cwd, sessionDir); + latest.appendMessage({ role: "user", content: "newer persisted session", timestamp: Date.now() }); + await latest.rewriteEntries(); + const latestSessionId = latest.getSessionId(); + expect(latestSessionId).not.toBe(missingId); + + await expect( + createSessionManager(buildResumeArgs(missingId, sessionDir), cwd, stubSettings), + ).rejects.toMatchObject({ + name: "SessionResolutionError", + message: `Session "${missingId}" not found.`, + hint: expect.stringContaining("omp --resume"), + }); + } finally { + await fsp.rm(cwd, { recursive: true, force: true }); + } + }); + + it("rejects --continue followed by an unknown session id instead of falling back to latest", async () => { + const cwd = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-continue-unknown-id-")); + const sessionDir = path.join(cwd, "sessions"); + const missingId = "019ea530-ffff-7000-8000-000000000000"; + try { + const latest = SessionManager.create(cwd, sessionDir); + latest.appendMessage({ role: "user", content: "latest should not be resumed", timestamp: Date.now() }); + await latest.rewriteEntries(); + expect(latest.getSessionId()).not.toBe(missingId); + + await expect( + createSessionManager(buildContinueArgs(missingId, sessionDir), cwd, stubSettings), + ).rejects.toMatchObject({ + name: "SessionResolutionError", + message: `Session "${missingId}" not found.`, + hint: expect.stringContaining("omp --resume"), + }); + } finally { + await fsp.rm(cwd, { recursive: true, force: true }); + } + }); + it("rejects --fork with SessionResolutionError carrying a usage hint", async () => { vi.spyOn(sessionListingModule, "resolveResumableSession").mockResolvedValue(undefined); try {