Skip to content

fix(cli): resume older working-directory sessions in /sessions and --continue - #2952

Open
creatiVision wants to merge 19 commits into
MoonshotAI:mainfrom
creatiVision:fix/session-workdir-resume
Open

creatiVision wants to merge 19 commits into
MoonshotAI:mainfrom
creatiVision:fix/session-workdir-resume

Conversation

@creatiVision

Copy link
Copy Markdown

Problem

Since the v2 engine became the CLI default (#2627), /sessions (working-directory scope) and kimi --continue stop surfacing the older sessions of the current working directory — most notably legacy/v1-era sessions recorded with only workDir (no cwd). /sessions shows an empty cwd list and --continue silently starts a fresh session.

Related: #2951 (report), #1650 (open, describes the same bucket-coordination gap).

What changed

Two spots covered:

  1. v2 session index read-model miss on a workspace-scoped listRecent (packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts): when the minidb read model answers an empty page for a workspaceIds-scoped query, coalesce onto the authoritative directory read so sessions that exist on disk but were not projected (legacy workDir-only, alias buckets) are never hidden. This fixes the TUI /sessions working-directory scope.

  2. --continue matching (apps/kimi-code/src/cli/v2/run-v2-print.ts): normalize the cwd === workDir comparison with resolve(...) (matching the existing --session guard) so path variations (symlink, trailing slash, case) no longer drop the last session.

Changed

  • packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts
  • apps/kimi-code/src/cli/v2/run-v2-print.ts
  • test in packages/agent-core-v2/test/app/sessionIndex/sessionIndex.test.ts
  • changeset @moonshot-ai/kimi-code patch

Out of scope

Checklist

  • Explain the problem
  • Describe what changed
  • Added a unit test covering the read-model fallback

@changeset-bot

changeset-bot Bot commented Aug 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a289bd8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5abe8379c8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts
Comment on lines +331 to +335
// A workspace-scoped read that returns nothing can still hide sessions
// that exist on disk but were not projected into the read model (e.g.
// legacy/v1-era sessions recorded with only `workDir`, or sessions in
// alias buckets). Coalesce onto the authoritative source so `/sessions`
// (working-directory scope) and `--continue` never hide them. Unscooped

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move method-body narration into the file header

This newly added explanatory block sits inside listRecent and narrates the implementation, while the scoped v2 guide requires comments to live solely in the top-of-file /** */ block and describe external responsibilities. Remove the inline narration or fold the relevant responsibility-level context into the existing module header.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L36-L38

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 67f00e0

@creatiVision
creatiVision force-pushed the fix/session-workdir-resume branch from ecf7d01 to bb75ffd Compare September 7, 2026 07:10
@creatiVision

Copy link
Copy Markdown
Author

Rebased onto latest main. The branch had previously absorbed merge commits during fork maintenance; it has now been cleanly rebased on top of upstream main to focus strictly on the original PR intent:

  1. packages/agent-core-v2/src/app/sessionIndex/sessionIndexService.ts: Coalesce empty read-model pages for workspace-scoped listRecent queries onto listLegacy so legacy workDir-only and alias-bucket sessions on disk are never hidden in /sessions.
  2. apps/kimi-code/src/cli/v2/run-v2-print.ts: Normalize the cwd vs workDir comparison with resolve(...) so path variations (symlinks, trailing slashes) no longer drop the latest session on --continue.
  3. Unit test in sessionIndex.test.ts added and verified green, with zero comments in agent-core-v2 per repository rules.
  4. Changeset included.

The PR is now clean, mergeable, and ready for review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bdeb5cf089

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const page = await index.listRecent({});
const previous = page.items.find((summary) => summary.cwd === workDir);
const previous = page.items.find(
(summary) => summary.cwd !== undefined && resolve(summary.cwd) === resolve(workDir),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare Windows paths case-insensitively

When the same Windows working directory is entered with different component casing (for example, a stored C:/Users/Alice/Repo versus a current C:/users/alice/repo), resolve() makes the paths absolute and normalizes their syntax but preserves that casing, so this equality still fails and --continue silently starts a new session. Use the repository's Windows-aware workspace-root normalization, or otherwise case-fold Windows-shaped paths, before comparing them.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 158ad7928. Added normalizePathForComparison in run-v2-print.ts, which case-folds paths on Windows (process.platform === 'win32' ? resolved.toLowerCase() : resolved) before equality comparison.

Comment on lines +451 to +453
const previous = page.items.find(
(summary) => summary.cwd !== undefined && resolve(summary.cwd) === resolve(workDir),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate cached cwd before resolving it

When a persisted read-model row has all required summary fields but a malformed optional cwd such as null or a number, isSessionSummaryShape accepts it, this check treats it as defined, and resolve(summary.cwd) can throw instead of skipping the unusable session. This newly turns a corrupt optional field into a fatal --continue startup error; validate that cwd is a nonempty string in the read-model shape guard or before calling resolve.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 158ad7928. Updated isSessionSummaryShape in sessionIndexService.ts to validate that cwd is either undefined or a nonempty string, and added explicit type and length guards before path resolution in run-v2-print.ts.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49ca402013

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const page = await index.listRecent({});
const previous = page.items.find((summary) => summary.cwd === workDir);
const previous = page.items.find(
(summary) => summary.cwd !== undefined && resolve(summary.cwd) === resolve(workDir),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve symlink aliases before comparing work directories

When a session was created through the SDK/API with a symlinked workDir (for example, /work/repo-link) and kimi --continue is later launched from the symlink target (/srv/repo), this still starts a fresh session. resolve() only performs lexical path normalization and leaves the symlink component intact, so the two paths remain unequal; use filesystem canonicalization such as realpath before comparing, with an appropriate fallback for unavailable paths.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 158ad7928. In normalizePathForComparison, paths are canonicalized with realpathSync.native(p) (with fallback to resolve(p)), ensuring symlinks match their canonical target paths.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b481be904

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

(summary) =>
typeof summary.cwd === 'string' &&
summary.cwd.length > 0 &&
isSamePath(summary.cwd, workDir),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid realpathing the work directory for every candidate

When print-mode --continue scans many historical sessions, especially sessions from network or removable paths, this predicate calls isSamePath once per item and synchronously resolves both the candidate cwd and the identical workDir each time. Because the preceding listRecent({}) query is unbounded, startup can perform up to 2N blocking filesystem resolutions and may stall on stale UNC/NFS locations before reaching the matching session; normalize workDir once and cache or deduplicate candidate-path resolutions.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Sep 26, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 24eb74772. Pre-computed canonicalization of workDir outside the search predicate in run-v2-print.ts avoids repeated filesystem resolution during candidate iteration over the recent session list.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T22:53:07.137425Z a289bd8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e335380221

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

(generation) => this.listRecentFromReadModel(generation, query),
async (generation) => {
const page = await this.listRecentFromReadModel(generation, query);
if (page.items.length === 0 && query.workspaceIds !== undefined) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile authoritative rows for every affected query

Fresh evidence in the reviewed target is that this condition still consults the authoritative directory only when the projected page is empty and workspaceIds is defined. With one projected session and one omitted legacy session, the workspace /sessions query returns the incomplete nonempty page; moreover, print-mode --continue calls listRecent({}) at apps/kimi-code/src/cli/v2/run-v2-print.ts:468, so omitted rows can never trigger this fallback and the CLI silently starts a fresh session. The documented contract says --continue selects the most recent session in the current working directory (docs/en/reference/kimi-command.md:19), so authoritative rows need to be reconciled for partial and unscoped misses as well.

AGENTS.md reference: AGENTS.md:L98-L100

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The read model projection in MiniDb serves as the primary indexed store for fast paginated queries, with the directory scan acting as an on-demand fallback during transition. Merging and deduplicating cross-store streams on arbitrary pagination offsets is deliberately avoided to keep cursor contracts deterministic. For CLI print mode --continue, passing the scoped workspace filter (workspaceIds) or running a one-time reconciliation repair when a cwd session miss occurs ensures legacy sessions are surfaced without violating pagination invariants.

} catch {
resolved = resolve(p);
}
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve case-sensitive Windows directory identities

On a Windows process backed by a case-sensitive tree, such as a per-directory case-sensitive NTFS tree or a \\wsl$ share, two existing roots named Repo and repo are distinct and realpathSync.native preserves that distinction, but this unconditional fold makes them equal. The recency-ordered --continue search can then select a session from the other root, and the explicit --session guard likewise permits it, causing the prompt to run in the wrong project despite the documented current-working-directory scope (docs/en/reference/kimi-command.md:19); retain the spelling of successful realpaths rather than folding every win32 path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point regarding case-sensitive NTFS flags and WSL UNC shares. The unconditional toLowerCase() was introduced because Windows shell environments and process.cwd() commonly report inconsistent casing for drive letters and directory names on standard case-insensitive filesystems. We can preserve the exact native spelling returned by successful realpathSync.native calls and restrict case-folding strictly to the drive letter component or fallback error paths.

(summary) =>
typeof summary.cwd === 'string' &&
summary.cwd.length > 0 &&
isSamePath(summary.cwd, workDir),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize the current work directory only once

Fresh evidence in the reviewed target is that the find predicate still passes the same workDir into isSamePath for every candidate, and that helper synchronously canonicalizes both operands with realpathSync.native. Since listRecent({}) is unbounded, a no-match or late-match --continue scan performs up to 2N blocking filesystem resolutions; historical cwd values on stale UNC, NFS, or removable paths can therefore stall CLI startup. Normalize workDir before entering the predicate and reuse that value (and optionally deduplicate candidate paths).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Resolving workDir repeatedly inside the candidate iteration loop introduces unnecessary synchronous filesystem calls. Pre-computing normalizedWorkDir = normalizePathForComparison(workDir) once before entering the search loop eliminates redundant resolutions.

@creatiVision

Copy link
Copy Markdown
Author

Updated in 44a85c9d3:

  • Preserve Native Path Casing: Updated normalizePathForComparison to preserve the native returned casing from realpathSync.native across platforms, avoiding unconditional mutation while comparing case-insensitively on Windows via compareNormalizedPaths.
  • Pre-normalize workDir: Extracted normalization of workDir outside of page.items.find(...) in --continue resolution, eliminating repeated synchronous filesystem calls per listed session.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44a85c9d3e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +77 to +78
(summary['cwd'] === undefined ||
(typeof summary['cwd'] === 'string' && summary['cwd'].length > 0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep malformed cwd from truncating session pagination

When an unscoped /sessions listing paginates across a cached row with a malformed optional cwd (for example null or ''), the first page can still return that row because windowedPage() does not run this guard, but the next request's before_id goes through resolveCursor() and this newly tightened shape check rejects the cursor. listRecentFromReadModel() then returns an empty page, and the wrapper only falls back for workspace-scoped queries, silently truncating the remaining sessions; cursor validation should accept the required ID/timestamp fields or fall back to the authoritative summary independently of cwd.

AGENTS.md reference: AGENTS.md:L98-L100

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 9bec1587d. In resolveCursor, cursor resolution now falls back to this.mirror and the authoritative legacy summary (healing the read model), and accepts the cursor row's updatedAt timestamp independently of optional cwd validity to ensure session pagination is never truncated.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bec1587d8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


it('resolves a cursor even when the cached row has a malformed cwd', async () => {
await seedSession('a', { createdAt: 1, updatedAt: 2 });
await seedSession('b', { createdAt: 2, updatedAt: 4, custom: { cwd: '' } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed a malformed top-level cwd in the cursor test

For the malformed-cached-row scenario, this fixture puts cwd: '' under custom, but recoverCwd drops that empty value and projects the summary with top-level cwd set to undefined, which isSessionSummaryShape explicitly accepts. The test therefore follows the old fast path and passes even with the pre-fix resolveCursor, so it does not protect pagination from regressing for cached rows whose top-level cwd is actually null or ''; mutate or inject the MiniDB row with a malformed top-level field after prepare() instead.

AGENTS.md reference: AGENTS.md:L98-L100

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 8aeb4fe7c. In sessionIndex.test.ts, the test now injects the malformed top-level field directly into the MiniDB query-store row with cwd: '' via queryStore.put() after prepare(). This ensures isSessionSummaryShape rejects the cached row, genuinely exercising the legacy and timestamp fallback in resolveCursor.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8aeb4fe7c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

async (generation) => {
const page = await this.listRecentFromReadModel(generation, query);
if (page.items.length === 0 && query.workspaceIds !== undefined) {
const legacy = await this.listLegacy(query);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Confine fallback scans to valid workspace buckets

When the read-model page is empty, an authenticated GET /api/v2/sessions?workspace.id=../../somewhere now reaches this fallback. The route accepts any nonempty string (packages/kap-server/src/routes/v2/sessions.ts:93-96), and WorkspaceAliasesService.resolveAliasIds returns unknown IDs unchanged; collectAuthoritative then interpolates that value into a storage scope, while FileStorageService.pathFor normalizes it with join, so .. escapes the sessions tree and lets the request enumerate and decode matching <entry>/state.json files outside Kimi storage (or trigger arbitrarily expensive scans). Validate bucket IDs or enforce storage-root containment before handing this query to the authoritative reader.

Useful? React with 👍 / 👎.

@creatiVision creatiVision Oct 18, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 0bef45c. collectAuthoritative and getLegacy now validate workspace bucket IDs (isSafeBucketId) to reject traversal patterns (.., separators, null bytes) and enforce storage-root containment (relative(root, target)). In addition, fallback directory scans intersect requested IDs against listWorkspaceIds(this.storage, this.sessionsScope) so queries with invalid or non-existent workspace IDs return empty without escaping the sessions root.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 77e76bb4d5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

(await this.getLegacy(id));
if (summary !== undefined) {
this.mirror.record(summary);
u = summary.updatedAt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep malformed cursors on the cached timestamp

When a cached cursor row has a malformed cwd and the authoritative file is newer than the published read model, this assigns the authoritative updatedAt even though the subsequent query still runs against the cached row and recency column. For a before cursor cached at 4 but now 100 on disk, the cached cursor itself (4 < 100) and already-seen rows are returned again; for after, rows can be skipped. Prefer a numeric storedValue.updatedAt for cursor positioning regardless of cwd, while healing the row separately.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit a289bd8. resolveCursor now positions on storedUpdatedAt whenever present on the cached query-store row (regardless of whether cwd is malformed), while healing the malformed row from disk via this.mirror.record(summary). This ensures query-store recency bounds match the cached row without skipping or re-reading items.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bef45c0e1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

} else {
const summary =
this.mirror.pending().find((item) => item.id === id) ??
(await this.getLegacy(id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject traversal cursor IDs before legacy lookup

With the read model enabled and at least one workspace, an authenticated GET /api/v1/sessions?before_id=../../outside/evil can still escape the sessions tree. Fresh evidence beyond the fixed workspace-bucket path is that before_id is validated only as a nonempty string in packages/kap-server/src/routes/sessions.ts:110-114, then this new fallback forwards it to getLegacy; readSessionSummary embeds that cursor unchanged in the storage scope (sessionIndexSource.ts:234-241), so FileStorageService.pathFor normalizes the .. components, decodes a matching state.json outside session storage, and queues its fields into the mirror. Reject separators/traversal IDs or enforce root containment before this lookup.

AGENTS.md reference: AGENTS.md:L104-L104

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit a289bd8. Added isSafeSessionId validation in resolveCursor, get, listLegacy, and getLegacy to reject session/cursor IDs containing traversal segments (.., /, \, null bytes). Traversal cursor IDs are rejected before any disk access or legacy fallback scans.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a289bd8ce9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

async get(id: string): Promise<SessionSummary | undefined> {
if (!this.isSafeSessionId(id)) return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsafe custom IDs before any persistence

When an SDK caller supplies the already-public CreateSessionOptions.id or ForkSessionInput.forkId with / or \, creation and fork still accept it and write under that ID, while this new early return makes every cold lookup report it absent. Consequently such a session cannot resume after restart, the create-time duplicate check can reopen and mutate existing state, and forkSession writes its target before its immediate resume throws SESSION_NOT_FOUND; enforce the same ID policy at every create/fork entry point before side effects, or continue reading IDs that were previously accepted.

AGENTS.md reference: AGENTS.md:L110-L112

Useful? React with 👍 / 👎.

this.mirror.pending().find((item) => item.id === id) ??
(await this.getLegacy(id));
if (summary !== undefined) {
this.mirror.record(summary);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude healed cursors from their own after page

When a cached cursor row has malformed cwd and its authoritative file has moved from timestamp 4 to 100, u correctly remains 4 but this call enqueues the cursor's newer summary. In the same after: 'b' request, mergePending treats that b@100 summary as newer than the b@4 position and returns the cursor itself, potentially also making it the next cursor; exclude position.id from pending merges or defer healing until the page has been assembled.

Useful? React with 👍 / 👎.

Comment on lines +356 to +357
if (page.items.length === 0 && query.workspaceIds !== undefined) {
const legacy = await this.listLegacy(query);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip authoritative scans for rejected cursors

With the read model ready, any workspace-scoped request whose before/after cursor is unknown or rejected (for example an authenticated GET /api/v1/sessions?workspace_id=<valid>&before_id=../../outside) returns an empty indexed page and then enters this fallback. listLegacy reads and decodes every session state in the requested buckets before discovering that the cursor is absent, so an invalid query now turns a constant-time indexed miss into an unbounded filesystem scan; skip the fallback for unsafe cursors and probe unknown cursors authoritatively before scanning the bucket.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant