Skip to content

Implement agent session resume and relaunch recovery#20

Merged
trydis merged 10 commits intomainfrom
session-resume
Mar 13, 2026
Merged

Implement agent session resume and relaunch recovery#20
trydis merged 10 commits intomainfrom
session-resume

Conversation

@trydis
Copy link
Owner

@trydis trydis commented Mar 13, 2026

Capture and persist Claude and Codex session identities so Shellraiser can relaunch active agent sessions instead of falling back to a plain shell.

  • store resumable session metadata in surface state
  • resume only active sessions on relaunch
  • fall back to a normal shell when Claude resume artifacts are missing
  • preserve resume eligibility during app quit/terminal teardown
  • fix Codex session discovery when rollout timestamps include fractions
  • add coverage for resume commands, persistence, quit handling, and wrappers

Summary by CodeRabbit

  • New Features

    • Graceful application shutdown that prepares and saves workspace state before quitting.
    • Session resumption support for Claude and Codex agents, including transcript-aware resume behavior.
    • Improved agent lifecycle tracking with new session and exit phases.
  • Bug Fixes

    • Better handling of child process exits to close or preserve surfaces correctly depending on termination state.
    • Ensure termination path notifies workspace manager and preserves state.
  • Tests

    • Expanded test coverage for session handling, resume logic, termination, and surface lifecycle.

Capture and persist Claude and Codex session identities so Shellraiser
can relaunch active agent sessions instead of falling back to a plain
shell.

- store resumable session metadata in surface state
- resume only active sessions on relaunch
- fall back to a normal shell when Claude resume artifacts are missing
- preserve resume eligibility during app quit/terminal teardown
- fix Codex session discovery when rollout timestamps include fractions
- add coverage for resume commands, persistence, quit handling, and wrappers
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds session lifecycle and resume support for Claude/Codex agents: new agent event phases, per-surface session fields, resume eligibility, workspace termination preparation, centralized child-exit handling, and runtime/launcher changes to resume agent sessions.

Changes

Cohort / File(s) Summary
App Termination & Scripting
Sources/Shellraiser/App/ShellraiserApp.swift, Sources/Shellraiser/Infrastructure/AppleScript/ShellraiserScriptingController.swift, Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift, Sources/Shellraiser/Services/Workspaces/WorkspaceManager+WorkspaceLifecycle.swift
Adds confirmQuit test seam in app delegate, exposes WorkspaceManager.prepareForTermination(), wires NSApplication.willTerminateNotification to prepareForTermination(), and provides scripting test helpers (resetForTesting / prepareForTermination).
Agent Runtime & Events
Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift, Sources/Shellraiser/Infrastructure/Agents/CompletionModels.swift
Introduces init(rootURL:fileManager:), replaces hard-coded runtime paths with instance-rooted paths, expands AgentActivityPhase with .session and .exited, parses additional payload types (claudeCode/codex), emits session events, and adds session/exit handling and monitoring hooks.
Workspace Completion Handling
Sources/Shellraiser/Services/Workspaces/WorkspaceManager+Completions.swift
Handles new .session and .exited AgentActivityEvent cases, parsing session identity and updating surface sessionId/transcriptPath and resume eligibility via surface manager APIs.
Surface Model & Resume Flags
Sources/Shellraiser/Models/SurfaceModel.swift
Adds transcriptPath: String and shouldResumeSession: Bool to SurfaceModel, updates CodingKeys, initializer, encoding/decoding, and default factory.
Ghostty Runtime Launching
Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
Adds surface-aware launchCommand overload, supports resumableExecutable tuples (command+args), escaped executable invocation, and threads surfaceModel into environment merging to enable resume-based launches.
Surface State Management
Sources/Shellraiser/Services/Workspaces/WorkspaceSurfaceManager+SurfaceState.swift
Adds setSessionIdentity and setResumeEligibility with normalization helpers and change-only persistence logic to store sessionId, transcriptPath, and resume eligibility per surface.
Surface Exit Centralization
Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift, Sources/Shellraiser/Services/Workspaces/WorkspaceManager+SurfaceOperations.swift
Replaces inline child-exit close logic with a new WorkspaceManager.handleSurfaceChildExit(workspaceId:surfaceId:) method that resolves the containing pane and closes the surface unless terminating.
Tests & Test Support
Tests/ShellraiserTests/*, Tests/ShellraiserTests/WorkspaceTestSupport.swift
Extensive test additions and updates: agent event parsing, AgentRuntimeBridge wrapper/helper content, Ghostty resume behavior, app termination wiring, WorkspaceManager completion/session scenarios, surface-state persistence, and test helpers now accept transcriptPath and shouldResumeSession. (Many new/updated test files.)

Sequence Diagram

sequenceDiagram
    participant Agent as Agent (Claude/Codex)
    participant Bridge as AgentRuntimeBridge
    participant Manager as WorkspaceManager
    participant Surface as SurfaceModel
    participant Runtime as GhosttyRuntime

    Agent->>Bridge: emit activity (hook-session / session / exited)
    Bridge->>Bridge: parse payload (session_id, transcript_path) / map phases
    Bridge->>Manager: handleAgentActivityEvent(phase:.session/ .exited, payload)
    Manager->>Surface: setSessionIdentity(sessionId, transcriptPath)
    Manager->>Surface: setResumeEligibility(shouldResumeSession)
    Note over Manager,Surface: state persisted only on change
    Runtime->>Runtime: launchCommand(for: surfaceModel)
    Runtime->>Surface: check shouldResumeSession & transcriptPath
    alt resumable executable available
        Runtime->>Agent: start agent with resume command/args
    else fallback
        Runtime->>Agent: start shell launch (/bin/zsh)
    end
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement agent session resume and relaunch recovery' clearly and directly summarizes the main objective of the PR, which is to enable agent session resumption across app relaunches.
Docstring Coverage ✅ Passed Docstring coverage is 96.97% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch session-resume
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
Sources/Shellraiser/Models/SurfaceModel.swift (1)

41-42: Consider providing a default for shouldResumeSession.

transcriptPath has a default (""), but shouldResumeSession does not. For consistency and to reduce call-site verbosity, consider defaulting to false:

🧹 Proposed fix
         transcriptPath: String = "",
-        shouldResumeSession: Bool,
+        shouldResumeSession: Bool = false,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Shellraiser/Models/SurfaceModel.swift` around lines 41 - 42, The
initializer parameter shouldResumeSession currently has no default while
transcriptPath does; update the SurfaceModel initializer (or the SurfaceModel
init signature) to give shouldResumeSession a default value (false) so callers
can omit it and maintain consistency with transcriptPath; locate the init or
struct declaration that contains the parameters transcriptPath: String = "" and
shouldResumeSession: Bool and change shouldResumeSession to have a default of
false.
Sources/Shellraiser/Services/Workspaces/WorkspaceManager+WorkspaceLifecycle.swift (1)

1-1: Unused import.

The AppKit import appears unnecessary for this extension—only Foundation types are used here.

🧹 Proposed fix
-import AppKit
 import Foundation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+WorkspaceLifecycle.swift
at line 1, Remove the unnecessary AppKit import at the top of
WorkspaceManager+WorkspaceLifecycle.swift; this file/extension only uses
Foundation types, so delete the "import AppKit" line (keep or add "import
Foundation" only if the file needs it) to eliminate the unused dependency in the
WorkspaceManager workspace lifecycle extension.
Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift (1)

102-106: Tighten the Claude-resume assertion to avoid false positives.

Consider also asserting that the command does not include /bin/zsh, so the test guarantees resume path selection (not mixed/fallback invocation).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift` around lines 102 -
106, The test currently only checks for "claude" and "--resume" separately which
can yield false positives; update the GhosttyRuntime.launchCommand(for: surface,
terminalConfig: surface.terminalConfig) assertions to verify that the "--resume"
flag is associated with the Claude invocation (e.g., assert the command string
contains the substring pattern "claude" followed by "--resume" or use a regex to
ensure order), and add an assertion that the command does not contain "/bin/zsh"
(XCTAssertFalse(command.contains("/bin/zsh"))) to guarantee the resume path was
selected rather than a zsh fallback; keep references to
GhosttyRuntime.launchCommand, surface, and terminalConfig when locating the
test.
Tests/ShellraiserTests/ShellraiserAppTests.swift (1)

11-12: Isolate shared scripting-controller state between tests.

Both tests mutate ShellraiserScriptingController.shared and leave it installed. Adding teardown/reset logic would prevent cross-test coupling and improve suite reliability.

Also applies to: 24-25

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/ShellraiserTests/ShellraiserAppTests.swift` around lines 11 - 12, Tests
are leaving global ShellraiserScriptingController.shared installed which causes
cross-test coupling; update the test class (ShellraiserAppTests) to isolate
state by adding teardown/reset logic that uninstalls or resets
ShellraiserScriptingController.shared after each test (e.g., implement
tearDown() to call the controller's uninstall/reset method or reassign a fresh
instance) and/or ensure setUp() initializes a clean shared controller before
calls to ShellraiserScriptingController.shared.install(workspaceManager:) used
alongside ShellraiserAppDelegate so the shared singleton is not mutated across
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift`:
- Around line 451-475: The current poller picks the last rollout-*.jsonl file by
pathname and only checks cwd and a second-precision timestamp, which can cause
one surface to pick up another surface's session_id; update the logic (around
session_file, first_line, extract_codex_session_timestamp,
timestamp_is_at_or_after, and the block that extracts session_id and calls
"$helper_path" codex "$surface_id" session "$session_id") to also verify the
rollout record belongs to the current surface before accepting it—for example,
parse and compare the surface identifier (or another unique surface-scoped
field) from first_line against the local surface_id (and/or use a
higher-precision timestamp if available); only when both cwd, surface_id (and
timestamp) match should you extract session_id and resume the session.
- Around line 487-503: The background monitor started by monitor_codex_session
(spawned in the subshell that creates stamp_file) can outlive the real Codex
process and append a late session event; capture its PID right after starting it
(e.g., monitor_pid="$!"), then after the real Codex run returns and before
calling "$helper" codex "$surface" exited, remove the stamp_file to signal the
monitor to stop and wait for the monitor to exit (wait "$monitor_pid"),
optionally killing it if it doesn't terminate within a short timeout; reference
the stamp_file and monitor_codex_session invocation and use $! to obtain the
background PID and wait/kill to ensure the monitor has stopped before emitting
the exited event.

In `@Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift`:
- Around line 125-133: The willTerminateNotification handler creates an async
Task which can let the app exit before prepareForTermination() (and its save)
finishes; change the observer block to call prepareForTermination() directly on
the main thread (synchronously) instead of spawning Task—i.e., in the
NotificationCenter callback (where willTerminateObserver is set) invoke
self?.prepareForTermination() directly (WorkspaceManager is `@MainActor` and the
observer uses queue: .main), and remove the Task { `@MainActor` ... } wrapper to
ensure termination work completes before shutdown.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceSurfaceManager`+SurfaceState.swift:
- Around line 16-21: When building resolvedTranscriptPath inside
WorkspaceSurfaceManager+SurfaceState (the block using
normalizedTranscriptPath(for:agentType, transcriptPath:)), ensure that when
transcriptPath is nil you explicitly clear any previously stored Claude
transcript for that surface instead of leaving the old value in place: set
resolvedTranscriptPath to nil and update the surface’s stored transcriptPath
(surface.transcriptPath) to nil whenever the incoming transcriptPath is nil or
when sessionId changes, mirroring the same fix for the second occurrence noted
(lines 37-43).

In `@Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift`:
- Around line 50-61: These tests mutate files produced by
AgentRuntimeBridge.shared, which causes flakiness when tests run in parallel;
change the tests to use a test-local AgentRuntimeBridge rooted in a unique temp
directory instead of the shared singleton: add (or use) an initializer or
factory on AgentRuntimeBridge that accepts a root/bin directory (e.g.,
AgentRuntimeBridge(rootURL:)), construct a temp directory with
FileManager.uniqueTemporaryDirectory() inside each test, instantiate
AgentRuntimeBridge(rootURL: tempDir), call prepareRuntimeSupport() on that
instance and read the wrapper files from instance.binDirectory, and ensure the
temp directory is removed in test cleanup; apply the same change to the other
test block referenced (lines 77-85).

---

Nitpick comments:
In `@Sources/Shellraiser/Models/SurfaceModel.swift`:
- Around line 41-42: The initializer parameter shouldResumeSession currently has
no default while transcriptPath does; update the SurfaceModel initializer (or
the SurfaceModel init signature) to give shouldResumeSession a default value
(false) so callers can omit it and maintain consistency with transcriptPath;
locate the init or struct declaration that contains the parameters
transcriptPath: String = "" and shouldResumeSession: Bool and change
shouldResumeSession to have a default of false.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceManager`+WorkspaceLifecycle.swift:
- Line 1: Remove the unnecessary AppKit import at the top of
WorkspaceManager+WorkspaceLifecycle.swift; this file/extension only uses
Foundation types, so delete the "import AppKit" line (keep or add "import
Foundation" only if the file needs it) to eliminate the unused dependency in the
WorkspaceManager workspace lifecycle extension.

In `@Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift`:
- Around line 102-106: The test currently only checks for "claude" and
"--resume" separately which can yield false positives; update the
GhosttyRuntime.launchCommand(for: surface, terminalConfig:
surface.terminalConfig) assertions to verify that the "--resume" flag is
associated with the Claude invocation (e.g., assert the command string contains
the substring pattern "claude" followed by "--resume" or use a regex to ensure
order), and add an assertion that the command does not contain "/bin/zsh"
(XCTAssertFalse(command.contains("/bin/zsh"))) to guarantee the resume path was
selected rather than a zsh fallback; keep references to
GhosttyRuntime.launchCommand, surface, and terminalConfig when locating the
test.

In `@Tests/ShellraiserTests/ShellraiserAppTests.swift`:
- Around line 11-12: Tests are leaving global
ShellraiserScriptingController.shared installed which causes cross-test
coupling; update the test class (ShellraiserAppTests) to isolate state by adding
teardown/reset logic that uninstalls or resets
ShellraiserScriptingController.shared after each test (e.g., implement
tearDown() to call the controller's uninstall/reset method or reassign a fresh
instance) and/or ensure setUp() initializes a clean shared controller before
calls to ShellraiserScriptingController.shared.install(workspaceManager:) used
alongside ShellraiserAppDelegate so the shared singleton is not mutated across
tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 66f0eb9a-fb55-49b1-bf4d-6d1401af5f81

📥 Commits

Reviewing files that changed from the base of the PR and between f3b30b5 and 3f15844.

📒 Files selected for processing (22)
  • Sources/Shellraiser/App/ShellraiserApp.swift
  • Sources/Shellraiser/Features/WorkspaceDetail/PaneLeafView.swift
  • Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift
  • Sources/Shellraiser/Infrastructure/Agents/CompletionModels.swift
  • Sources/Shellraiser/Infrastructure/AppleScript/ShellraiserScriptingController.swift
  • Sources/Shellraiser/Infrastructure/Ghostty/GhosttyRuntime.swift
  • Sources/Shellraiser/Models/SurfaceModel.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+Completions.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+SurfaceOperations.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+WorkspaceLifecycle.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceSurfaceManager+SurfaceState.swift
  • Tests/ShellraiserTests/AgentCompletionEventTests.swift
  • Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift
  • Tests/ShellraiserTests/AgentTypeAndWorkspaceModelTests.swift
  • Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift
  • Tests/ShellraiserTests/PaneNodeModelOperationsTests.swift
  • Tests/ShellraiserTests/ShellraiserAppTests.swift
  • Tests/ShellraiserTests/SurfaceModelTests.swift
  • Tests/ShellraiserTests/WorkspaceManagerCompletionTests.swift
  • Tests/ShellraiserTests/WorkspaceSurfaceManagerSurfaceStateTests.swift
  • Tests/ShellraiserTests/WorkspaceTestSupport.swift

Comment on lines +16 to +21
let resolvedTranscriptPath: String?
if let transcriptPath {
resolvedTranscriptPath = normalizedTranscriptPath(for: agentType, transcriptPath: transcriptPath)
} else {
resolvedTranscriptPath = nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear the stored Claude transcript when a session update doesn't provide one.

If transcriptPath is nil, the previous Claude surface.transcriptPath survives even when sessionId changes. GhosttyRuntime uses that path as the resume gate, so a later claudeCode:session update without a transcript can still trigger a resume attempt instead of correctly falling back to a shell.

🛠️ Suggested fix
-        let resolvedTranscriptPath: String?
-        if let transcriptPath {
-            resolvedTranscriptPath = normalizedTranscriptPath(for: agentType, transcriptPath: transcriptPath)
-        } else {
-            resolvedTranscriptPath = nil
-        }
+        let resolvedTranscriptPath = normalizedTranscriptPath(
+            for: agentType,
+            transcriptPath: transcriptPath ?? ""
+        )
@@
                 switch agentType {
                 case .claudeCode:
-                    if let resolvedTranscriptPath,
-                       surface.transcriptPath != resolvedTranscriptPath {
+                    if surface.transcriptPath != resolvedTranscriptPath {
                         surface.transcriptPath = resolvedTranscriptPath
                         didChange = true
                     }

Also applies to: 37-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/Shellraiser/Services/Workspaces/WorkspaceSurfaceManager`+SurfaceState.swift
around lines 16 - 21, When building resolvedTranscriptPath inside
WorkspaceSurfaceManager+SurfaceState (the block using
normalizedTranscriptPath(for:agentType, transcriptPath:)), ensure that when
transcriptPath is nil you explicitly clear any previously stored Claude
transcript for that surface instead of leaving the old value in place: set
resolvedTranscriptPath to nil and update the surface’s stored transcriptPath
(surface.transcriptPath) to nil whenever the incoming transcriptPath is nil or
when sessionId changes, mirroring the same fix for the second occurrence noted
(lines 37-43).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift`:
- Around line 202-214: The guard for hook-session is ineffective because payload
is constructed with printf '%s\n%s' (so it's never empty); update the logic that
handles the "hook-session" branch (the hook-session case that defines
hook_payload, compact_payload, session_id, transcript_path, and payload) to
either (a) build payload only when session_id (or transcript_path, as required)
is non-empty, or (b) keep payload as-is but change the later guard to check the
extracted session_id (and/or transcript_path) emptiness instead of payload;
ensure the runtime uses the session_id variable (not the newline-only payload)
to decide whether to exit for phase="session".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 65861813-e475-423f-a2d5-abb42833cb89

📥 Commits

Reviewing files that changed from the base of the PR and between 3f15844 and e89f7fa.

📒 Files selected for processing (10)
  • Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift
  • Sources/Shellraiser/Infrastructure/AppleScript/ShellraiserScriptingController.swift
  • Sources/Shellraiser/Models/SurfaceModel.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager+WorkspaceLifecycle.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceSurfaceManager+SurfaceState.swift
  • Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift
  • Tests/ShellraiserTests/GhosttyRuntimeCommandTests.swift
  • Tests/ShellraiserTests/ShellraiserAppTests.swift
  • Tests/ShellraiserTests/WorkspaceSurfaceManagerSurfaceStateTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Sources/Shellraiser/Infrastructure/AppleScript/ShellraiserScriptingController.swift
  • Sources/Shellraiser/Services/Workspaces/WorkspaceManager.swift
  • Tests/ShellraiserTests/ShellraiserAppTests.swift

Comment on lines +202 to +214
claudeCode:hook-session)
hook_payload="$(cat)"
compact_payload="$(printf '%s' "$hook_payload" | tr -d '\n')"
session_id="$(printf '%s' "$compact_payload" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | tr '[:upper:]' '[:lower:]' | sed -n '1p')"
transcript_path="$(printf '%s' "$compact_payload" | sed -n 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p')"
payload="$(printf '%s\n%s' "$session_id" "$transcript_path")"
phase="session"
;;
esac

if [ "$phase" = "session" ] && [ -z "$payload" ]; then
exit 0
fi
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Session payload emptiness guard is ineffective for hook-session events.

On Line 207, payload is always at least a newline for hook-session (printf '%s\n%s' ...), so the Line 212 guard won’t suppress missing session_id cases. That can still emit a malformed session event.

💡 Suggested fix
-            payload="$(printf '%s\n%s' "$session_id" "$transcript_path")"
-            phase="session"
+            [ -n "$session_id" ] || exit 0
+            payload="$(printf '%s\n%s' "$session_id" "$transcript_path")"
+            phase="session"
             ;;
         esac

-        if [ "$phase" = "session" ] && [ -z "$payload" ]; then
+        if [ "$phase" = "session" ] && [ -z "$payload" ]; then
             exit 0
         fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
claudeCode:hook-session)
hook_payload="$(cat)"
compact_payload="$(printf '%s' "$hook_payload" | tr -d '\n')"
session_id="$(printf '%s' "$compact_payload" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | tr '[:upper:]' '[:lower:]' | sed -n '1p')"
transcript_path="$(printf '%s' "$compact_payload" | sed -n 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p')"
payload="$(printf '%s\n%s' "$session_id" "$transcript_path")"
phase="session"
;;
esac
if [ "$phase" = "session" ] && [ -z "$payload" ]; then
exit 0
fi
claudeCode:hook-session)
hook_payload="$(cat)"
compact_payload="$(printf '%s' "$hook_payload" | tr -d '\n')"
session_id="$(printf '%s' "$compact_payload" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | tr '[:upper:]' '[:lower:]' | sed -n '1p')"
transcript_path="$(printf '%s' "$compact_payload" | sed -n 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p')"
[ -n "$session_id" ] || exit 0
payload="$(printf '%s\n%s' "$session_id" "$transcript_path")"
phase="session"
;;
esac
if [ "$phase" = "session" ] && [ -z "$payload" ]; then
exit 0
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift` around
lines 202 - 214, The guard for hook-session is ineffective because payload is
constructed with printf '%s\n%s' (so it's never empty); update the logic that
handles the "hook-session" branch (the hook-session case that defines
hook_payload, compact_payload, session_id, transcript_path, and payload) to
either (a) build payload only when session_id (or transcript_path, as required)
is non-empty, or (b) keep payload as-is but change the later guard to check the
extracted session_id (and/or transcript_path) emptiness instead of payload;
ensure the runtime uses the session_id variable (not the newline-only payload)
to decide whether to exit for phase="session".

@trydis trydis merged commit 0271617 into main Mar 13, 2026
2 checks passed
@trydis trydis deleted the session-resume branch March 15, 2026 22:00
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