Skip to content

fix(sessions): treat session.json without sessionId as absent (#228) - #234

Merged
TerrysPOV merged 6 commits into
moazbuilds:masterfrom
rudi193-cmd:fix/issue-228-missing-session-id
Jul 19, 2026
Merged

fix(sessions): treat session.json without sessionId as absent (#228)#234
TerrysPOV merged 6 commits into
moazbuilds:masterfrom
rudi193-cmd:fix/issue-228-missing-session-id

Conversation

@rudi193-cmd

Copy link
Copy Markdown
Contributor

Fixes #228. Corrupted session.json without sessionId is treated as absent; runner bootstraps instead of crashing on sessionId.slice. Tests: tests/sessions-missing-id.test.ts

@rudi193-cmd

Copy link
Copy Markdown
Contributor Author

Note on the failing claude-review check: this is the same workflow credential failure as #233, before review runs:

ANTHROPIC_API_KEY:
Failed to authenticate. API Error: 401 Invalid authentication credentials

The patch verification passed locally before push:

node --experimental-strip-types --test tests/sessions-missing-id.test.ts
# 3 pass, 0 fail

So this PR is blocked on the repo's Claude Code Review workflow credentials / rerun, not on a code failure from this change.

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code review

Found 1 issue:

  1. peekThreadSession is the one remaining read path that doesn't go through hasValidSessionId. A corrupted sessions.json thread row — exactly the corruption class issue #228 reports — passes the if (!session) check in the Telegram/Discord /status and /context handlers, then crashes on session.sessionId.slice(0, 8) (or the ${session.sessionId}.jsonl interpolation), reproducing the same Cannot read properties of undefined (reading 'slice') this PR is meant to eliminate.

peekThreadSession returns the raw record without validation:

https://github.com/moazbuilds/claudeclaw/blob/404ba91b0c1b19411382558a17cd945336520be3/src/sessionManager.ts#L106-L112

Vulnerable callers:

if (command === "/status") {
const session = sessionKey ? await peekThreadSession(sessionKey) : await peekSession();
const settings = getSettings();
if (!session) {
await sendMessage(config.token, chatId, "📊 No active session.", threadId);
return;
}
const lines = [
"📊 **Session Status**",
`Session: \`${session.sessionId.slice(0, 8)}\``,
`Turns: ${session.turnCount ?? 0}`,
`Model: ${settings.model || "default"}`,
`Security: ${settings.security.level}`,

const isGuildCmd = !!interaction.guild_id && !!interaction.channel_id;
const session = isGuildCmd
? await peekThreadSession(interaction.channel_id!)
: await peekSession();
if (!session) {
await respondToInteraction(interaction, { content: "No active session." });
return;
}
const home = homedir();
const projectSlug = process.cwd().replace(/\//g, "-");
const jsonlPath = `${home}/.claude/projects/${projectSlug}/${session.sessionId}.jsonl`;
if (!existsSync(jsonlPath)) {
await respondToInteraction(interaction, { content: "Conversation file not found." });
return;

Quickest fix: route peekThreadSession through the same hasValidSessionId guard getThreadSession now uses (lines 41-45 of sessionManager.ts), so it returns null for corrupted rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
@rudi193-cmd

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 5a6e633.

peekThreadSession now routes through the same hasValidSessionId guard as getThreadSession, so corrupted sessions.json thread rows return null instead of reaching callers that call session.sessionId.slice(...).

Added a regression test in tests/sessions-missing-id.test.ts.

node --experimental-strip-types --test tests/sessions-missing-id.test.ts
# 4 pass, 0 fail

Ready for another look when you have a moment.

@rudi193-cmd

Copy link
Copy Markdown
Contributor Author

@TerrysPOV friendly re-review ping — peekThreadSession now routes through the same hasValidSessionId guard as getThreadSession (5a6e633), so corrupted sessions.json thread rows return null. claude-review is green and the branch is mergeable. Re-requested your review; happy to adjust if anything else stands out.

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code review

peekThreadSession blocker is resolved in 5a6e633 — thanks.

Found 1 remaining issue (same class):

  1. listThreadSessions is the last unfiltered thread read path. A corrupted sessions.json row will be returned as-is, and the /status thread-sessions loop in Discord crashes on ts.sessionId.slice(0, 8) — same TypeError signature this PR is meant to eliminate.

listThreadSessions returns the raw map values without hasValidSessionId:

/** List all active thread sessions. */
export async function listThreadSessions(): Promise<ThreadSession[]> {
const data = await loadSessions();
return Object.values(data.threads);
}

Crash site:

if (threadSessions.length > 0) {
lines.push("", `**Thread Sessions:** ${threadSessions.length}`);
for (const ts of threadSessions.slice(0, 5)) {
lines.push(` Thread \`${ts.threadId.slice(0, 8)}\` → Session \`${ts.sessionId.slice(0, 8)}\` (${ts.turnCount} turns)`);
}
if (threadSessions.length > 5) {
lines.push(` ... and ${threadSessions.length - 5} more`);

Quickest fix, mirroring the peekThreadSession shape in 5a6e633:

return Object.values(data.threads).filter(hasValidSessionId);

(Sister mutators incrementThreadTurn / markThreadCompactWarned only bump fields and bail on !session, so they don't crash — they just mutate corrupted rows. Worth a follow-up but not blocking.)

listThreadSessions was the last thread read path returning rows
without going through hasValidSessionId. A sessions.json row missing
sessionId would reach the Discord /status thread-sessions loop and
crash on ts.sessionId.slice(0, 8) — the same TypeError class moazbuilds#234
eliminates. Apply the existing guard. (review follow-up, moazbuilds#234)
@rudi193-cmd

Copy link
Copy Markdown
Contributor Author

Resolved in a9eb7cb. listThreadSessions now filters through hasValidSessionId, so a corrupted sessions.json row can no longer reach the /status thread-sessions loop and crash on ts.sessionId.slice(0, 8). That was the last unfiltered thread read path — getThreadSession, peekThreadSession, and listThreadSessions are now all gated. Thanks for the catch.

Required by plugin-version-guard / marketplace-version-guard — master
advanced to 1.0.39 after moazbuilds#233 merged, matching this branch. (moazbuilds#234)
@TerrysPOV

Copy link
Copy Markdown
Collaborator

@claude

@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude encountered an error after 2s —— View job


I'll analyze this and get back to you.

TerrysPOV added a commit to rudi193-cmd/claudeclaw that referenced this pull request Jul 19, 2026
Maintainer commit — master reached 1.0.40 via moazbuilds#233, so the branch's
1.0.40 bump was a no-op. Re-bump to 1.0.41 to satisfy
plugin-version-guard and marketplace-version-guard for PR moazbuilds#234.

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The listThreadSessions blocker from the prior review is resolved — Object.values(data.threads).filter(hasValidSessionId) in a9eb7cb. I traced the full crash class and it's now complete: every path that dereferences .sessionId (Discord/Telegram/Slack /status + /context, runner, rotation, web UI) is sourced from a validated read, and the web-UI readers stay safe via their parallel UUID_RE.test() guard.

Maintainer-committed the version bump correction in c5e6343: the branch's 1.0.40 bump had become a no-op after #233 landed master at 1.0.40, so I re-bumped to 1.0.41 to clear plugin-version-guard / marketplace-version-guard. Please run bun run bump:plugin-version + bun run bump:marketplace-version yourself on future PRs — and rebase if another version bump lands before this merges.

Approving.

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved

@TerrysPOV
TerrysPOV force-pushed the fix/issue-228-missing-session-id branch from c5e6343 to 54854a9 Compare July 19, 2026 08:40

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving on the current head 54854a9 after reverting my erroneous 1.0.41 bump.

Context: the code fix is complete — the listThreadSessions guard (a9eb7cb) closes the last unvalidated thread-session read path, and I traced the full crash class to confirm no unguarded .sessionId dereference remains. The author's 1.0.40 bump already satisfies plugin-version-guard / marketplace-version-guard (they compare against this PR's base at 1.0.39, and both ran green at this SHA). My earlier extra bump to 1.0.41 was a misdiagnosis that introduced a merge conflict against master's 1.0.40; I've force-pushed it out, so the branch is back to a clean, mergeable state.

Approving.

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.

Crash loop: TypeError when session.json exists but sessionId is missing

2 participants