Skip to content

fix(multiplexer): fix daemon startup crash from invalid claude_status JSON - #178

Merged
shepherdjerred merged 2 commits into
mainfrom
fix/multiplexer-daemon-startup
Jan 1, 2026
Merged

fix(multiplexer): fix daemon startup crash from invalid claude_status JSON#178
shepherdjerred merged 2 commits into
mainfrom
fix/multiplexer-daemon-startup

Conversation

@shepherdjerred

Copy link
Copy Markdown
Owner

Summary

  • Fix daemon crash with "expected value at line 1 column 1" error on startup
  • The SQLite migration v3 set DEFAULT 'Unknown' for claude_status, but code expected JSON-encoded "Unknown"
  • Add FromStr impl for ClaudeWorkingStatus to handle both formats
  • Improve error logging throughout daemon initialization

Test plan

  • Build succeeds (cargo build)
  • Daemon starts successfully with existing database containing raw enum values
  • Verified with RUST_LOG=multiplexer=debug cargo run -- daemon

🤖 Generated with Claude Code

… JSON

The daemon was crashing with "expected value at line 1 column 1" because
the SQLite migration v3 set DEFAULT 'Unknown' for claude_status, but the
code tried to parse it as JSON (which requires quoted "Unknown").

Changes:
- Add FromStr impl for ClaudeWorkingStatus to parse raw enum variant names
- Update session row parsing to try JSON first, then fall back to FromStr
- Add detailed error messages showing which field failed and actual value
- Add debug logging around database and session manager initialization
- Improve client error handling for empty/malformed daemon responses
- Add empty output check for gh pr checks in CI poller

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jan 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @shepherdjerred's task in 1m 35s —— View job


Code Review Complete

  • Read repository guidelines (CLAUDE.md)
  • Review changed files and understand the fix
  • Analyze architectural fit and design decisions
  • Check for logic errors and edge cases
  • Verify security considerations
  • Review commit message quality
  • Provide final feedback

Summary

This PR effectively fixes the daemon startup crash caused by a mismatch between the database migration's default value format and the JSON parsing expectations. The root cause analysis is correct, and the solution is sound.

Architectural Fit: ✅ Good

The fix follows good Rust patterns:

  • Using FromStr trait for parsing is idiomatic
  • Fallback strategy (try JSON first, then FromStr) handles migration gracefully
  • Enhanced error messages improve debuggability
  • Logging additions help diagnose initialization issues

Logic & Edge Cases: ⚠️ Minor Issues

1. FromStr implementation is incomplete (packages/multiplexer/src/core/session.rs:249-261)

The FromStr impl only handles the exact variant names, but there's a risk if the database contains:

  • JSON-encoded values like "\"Unknown\"" (double-encoded)
  • Lowercase variants like "unknown"
  • Whitespace padding

The fallback logic in sqlite.rs:535-537 tries JSON first, then FromStr, which means:

  • Valid JSON "Unknown" → serde_json succeeds ✅
  • Raw enum Unknown → serde_json fails, FromStr succeeds ✅
  • But what about mixed cases or malformed data?

Suggestion: Consider making FromStr more defensive:

impl std::str::FromStr for ClaudeWorkingStatus {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim() {  // Handle whitespace
            "Unknown" => Ok(Self::Unknown),
            "Working" => Ok(Self::Working),
            "WaitingApproval" => Ok(Self::WaitingApproval),
            "WaitingInput" => Ok(Self::WaitingInput),
            "Idle" => Ok(Self::Idle),
            // Try case-insensitive as fallback
            s if s.eq_ignore_ascii_case("unknown") => Ok(Self::Unknown),
            s if s.eq_ignore_ascii_case("working") => Ok(Self::Working),
            s if s.eq_ignore_ascii_case("waitingapproval") => Ok(Self::WaitingApproval),
            s if s.eq_ignore_ascii_case("waitinginput") => Ok(Self::WaitingInput),
            s if s.eq_ignore_ascii_case("idle") => Ok(Self::Idle),
            _ => anyhow::bail!("unknown ClaudeWorkingStatus: {}", s),
        }
    }
}

2. Error message truncation could lose critical context (packages/multiplexer/src/api/client.rs:77-80)

Truncating to 200 bytes might cut in the middle of a UTF-8 character or lose important diagnostic info. Consider:

if trimmed.len() > 200 { 
    format!("{}... (truncated {} bytes)", &trimmed[..200], trimmed.len() - 200)
} else { 
    trimmed 
}

Also, using byte indexing &trimmed[..200] can panic on UTF-8 boundaries. Use character-based truncation:

trimmed.chars().take(200).collect::<String>()

3. Migration doesn't fix existing bad data (packages/multiplexer/src/store/sqlite.rs:230-278)

Migration v3 adds the column with DEFAULT 'Unknown', but doesn't fix existing rows that might have corrupt data. If any rows exist from a failed migration or manual edit, they'll still cause parse errors on daemon startup.

Suggestion: Add a data cleanup step in the migration:

// After adding the column, normalize any existing data
sqlx::query(
    "UPDATE sessions SET claude_status = '\"Unknown\"' WHERE claude_status = 'Unknown'"
)
.execute(pool)
.await?;

Security: ✅ Good

No security concerns. The error messages appropriately truncate potentially large inputs to prevent log injection or DoS through massive error messages.

Design: ✅ Good with one suggestion

The dual-format parsing approach is pragmatic for handling the migration issue. However, this creates permanent technical debt - the codebase will forever support both formats.

Long-term consideration: Add a follow-up migration (v4) that:

  1. Reads all sessions
  2. Normalizes claude_status to JSON format
  3. Removes the FromStr fallback logic

This would clean up the code and prevent future confusion. Not critical for this PR, but worth tracking.

Commit Message: ✅ Excellent

The commit message is exemplary:

  • Clear one-line summary
  • Detailed explanation of root cause
  • Lists all changes made
  • Explains "why" not just "what"

Testing: ⚠️ Missing unit tests

The PR description shows manual testing, but there are no unit tests for:

  • FromStr implementation with various inputs
  • The fallback parsing logic in TryFrom<SessionRow>
  • Edge cases (empty strings, whitespace, invalid variants)

Suggestion: Add tests in packages/multiplexer/src/core/session.rs:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_claude_working_status_from_str() {
        assert_eq!("Unknown".parse::<ClaudeWorkingStatus>().unwrap(), ClaudeWorkingStatus::Unknown);
        assert_eq!("Working".parse::<ClaudeWorkingStatus>().unwrap(), ClaudeWorkingStatus::Working);
        assert!("invalid".parse::<ClaudeWorkingStatus>().is_err());
    }
}

Other Observations

Good additions:

  • Empty output check in CI poller (packages/multiplexer/src/ci/poller.rs:90-93) prevents parse errors
  • Detailed error context in TryFrom<SessionRow> makes debugging much easier
  • Debug logging in daemon initialization helps trace startup issues

Minor style note: The error messages are very verbose (which is good for debugging!), but consider using structured logging fields instead of string interpolation for better log aggregation:

tracing::error!(
    session_name = %row.name,
    field = "claude_status", 
    value = %row.claude_status,
    error = %e,
    "Failed to parse session field"
);

Recommendation

Approve with minor suggestions. The core fix is solid and addresses the immediate issue. The suggestions above would make it more robust, but they're not blockers.

Priority fixes (if you want to address them):

  1. Make FromStr handle whitespace (.trim())
  2. Fix UTF-8 safety in client.rs truncation
  3. Add basic unit tests for FromStr

Nice-to-haves:
4. Case-insensitive fallback in FromStr
5. Data cleanup in migration
6. Structured logging fields


@github-actions

github-actions Bot commented Jan 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @shepherdjerred's task in 1m 39s —— View job


Code Review Complete ✓

  • Read repository guidelines (CLAUDE.md)
  • Review changed files and understand the fix
  • Analyze architectural fit and design decisions
  • Check for logic errors and edge cases
  • Verify security considerations
  • Review commit message quality
  • Provide final feedback

Summary

This PR fixes a daemon startup crash caused by a mismatch between the database migration and the deserialization code. The fix is solid and well-implemented, with one major issue that needs addressing.

Critical Issue

🚨 Unrelated changes in this PR - The PR includes bun-decompile changes (LICENSE, README, code modifications) that are completely unrelated to the multiplexer daemon fix described in the PR title and description. This violates good PR hygiene:

Recommendation: Remove the unrelated bun-decompile changes from this PR. Either:

  1. Revert commit ee6762d8 from this branch, OR
  2. Create a separate PR for the bun-decompile changes

Multiplexer Fix Analysis

The core multiplexer fix (commit 634e2841) is well-designed and addresses the root cause effectively.

Architectural Fit ✓

The solution follows Rust idioms correctly:

  • Implements FromStr trait for ClaudeWorkingStatus at packages/multiplexer/src/core/session.rs:249-262
  • Uses fallback parsing pattern: serde_json::from_str().or_else(|_| row.claude_status.parse()) at sqlite.rs:536-538
  • Backwards compatible - handles both old (raw enum) and new (JSON) formats

Design Decisions ✓

Strong points:

  • The FromStr implementation is simple, maintainable, and matches the enum variant names exactly
  • Error messages now include context (session name, field name, actual value) - excellent for debugging
  • Debug logging added at key initialization points (sqlite.rs:32-38, server.rs:64-88)
  • Client error handling improved with detailed output truncation (client.rs:75-82)

Minor observation:

  • The fallback pattern means the code will accept raw enum values forever. This is fine for backwards compatibility, but consider documenting this in a code comment to explain why both formats are supported.

Logic & Edge Cases ✓

Handles edge cases well:

  • Empty daemon responses checked (client.rs:66-73)
  • Empty gh pr checks output handled (poller.rs:90-93)
  • Session row parsing errors logged with row index for debugging (sqlite.rs:293-300)

Potential edge case (very minor):

  • The FromStr implementation doesn't handle case variations (e.g., "unknown" vs "Unknown"). This is probably fine since the migration uses exact case, but worth noting.

Security ✓

No security concerns. The changes are defensive:

  • Better error handling prevents information leakage (truncates long responses)
  • No new attack surface introduced
  • Logging improvements help with security monitoring

Commit Messages ✓

Commit 634e2841 has an excellent commit message:

  • Clear, descriptive title following conventional commits format
  • Explains the root cause ("migration v3 set DEFAULT 'Unknown'" vs "code expected JSON")
  • Lists all changes made
  • Includes test verification
  • Proper attribution

Commit ee6762d8 ("fix: decompiler") has a vague message and doesn't belong in this PR.

Code Quality Notes

Excellent improvements:

  • Error messages at sqlite.rs:515, 518, 521, 524, 530, 538, 545 now include the session name, field name, and actual invalid value - this makes debugging production issues much easier
  • Consistent use of structured logging with tracing macros
  • Response validation in client.rs prevents silent failures

Minor style note:

  • The error logging format is consistent and readable throughout

Final Verdict

The multiplexer daemon fix is excellent work - well-designed, thoroughly tested, and production-ready. However, the PR needs cleanup to remove unrelated bun-decompile changes before merging.

Recommendation: Merge after removing the unrelated commit.


@shepherdjerred
shepherdjerred merged commit b8160fb into main Jan 1, 2026
3 checks passed
@shepherdjerred
shepherdjerred deleted the fix/multiplexer-daemon-startup branch January 1, 2026 03:55
martindotpy pushed a commit to martindotpy/astro-opengraph-images that referenced this pull request Apr 5, 2026
… JSON (shepherdjerred#178)

* fix(multiplexer): fix daemon startup crash from invalid claude_status JSON

The daemon was crashing with "expected value at line 1 column 1" because
the SQLite migration v3 set DEFAULT 'Unknown' for claude_status, but the
code tried to parse it as JSON (which requires quoted "Unknown").

Changes:
- Add FromStr impl for ClaudeWorkingStatus to parse raw enum variant names
- Update session row parsing to try JSON first, then fall back to FromStr
- Add detailed error messages showing which field failed and actual value
- Add debug logging around database and session manager initialization
- Improve client error handling for empty/malformed daemon responses
- Add empty output check for gh pr checks in CI poller

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: decompiler

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
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