Skip to content

Conversation

@starcomo
Copy link

@starcomo starcomo commented Jan 4, 2026

Summary

  • Add error parameter to recoverToolResultMissing and extract tool_use IDs from error message as fallback
  • Add multiple index offsets in findMessageByIndexNeedingThinking to handle API/storage index mismatch
  • Add failedAssistantMsg.info.id as fallback in recoverThinkingBlockOrder

Problem

Session recovery was failing in certain scenarios:

1. tool_result_missing error

When failedAssistantMsg.parts is empty and storage also has no parts, recovery fails even though the tool_use ID is present in the error message itself.

2. thinking_block_order error

API message index differs from storage index (due to system messages), causing findMessageByIndexNeedingThinking to return null.

Solution

  1. tool_result_missing: Use extractToolUseIdsFromError(error) as fallback to extract IDs directly from error message
  2. thinking_block_order:
    • Try multiple index offsets (±5) in findMessageByIndexNeedingThinking
    • Use failedAssistantMsg.info.id directly as additional fallback

Test plan

  • Verify tool_result_missing recovery works when parts are empty
  • Verify thinking_block_order recovery works with index mismatch

Fixes #483


Summary by cubic

Improves session recovery by adding robust fallbacks for tool_result_missing and thinking_block_order errors, so recovery works even with index mismatches or empty parts. This makes recovery more reliable across varied API/storage states.

  • Bug Fixes
    • tool_result_missing: recoverToolResultMissing accepts error and extracts tool_use IDs from the error message when parts are empty.
    • thinking_block_order: findMessageByIndexNeedingThinking tries index offsets (±5) and recoverThinkingBlockOrder falls back to failedAssistantMsg.info.id when needed.

Written for commit c6feac9. Summary will update on new commits.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 4, 2026

All contributors have signed the CLA. Thank you! ✅
Posted by the CLA Assistant Lite bot.

@greptile-apps
Copy link

greptile-apps bot commented Jan 4, 2026

Greptile Summary

This PR enhances session recovery by adding fallback mechanisms for two critical error types: tool_result_missing and thinking_block_order.

Key Changes:

  • tool_result_missing: Added extractToolUseIdsFromError() to extract tool_use IDs directly from error messages when failedAssistantMsg.parts is empty
  • thinking_block_order: Modified findMessageByIndexNeedingThinking() to try multiple index offsets (±1 to ±5) and added failedAssistantMsg.info.id as a direct fallback to handle API/storage index mismatches

Impact:
The changes improve robustness of the session recovery mechanism by providing multiple fallback paths when primary recovery methods fail. This addresses issue #483 where sessions would fail to recover even though the necessary information was available in error messages or alternative sources.

Confidence Score: 4/5

  • This PR is safe to merge with low risk - adds defensive fallback logic without breaking existing functionality
  • Score reflects well-structured fallback mechanisms that improve error recovery. One minor concern about index offset order potentially matching wrong messages in edge cases, but the risk is low given the specific error conditions being handled
  • Pay attention to src/hooks/session-recovery/storage.ts - the index offset order in findMessageByIndexNeedingThinking could potentially match incorrect messages in edge cases with multiple assistant messages

Important Files Changed

Filename Overview
src/hooks/session-recovery/index.ts Added error parameter fallback for tool_result_missing recovery via extractToolUseIdsFromError() regex and direct message ID fallback for thinking_block_order
src/hooks/session-recovery/storage.ts Modified findMessageByIndexNeedingThinking to try multiple index offsets (±1 to ±5) to handle API/storage index mismatches

Sequence Diagram

sequenceDiagram
    participant API as Claude API
    participant Hook as Session Recovery Hook
    participant Index as index.ts
    participant Storage as storage.ts
    
    API->>Hook: Error: tool_result_missing
    Hook->>Index: recoverToolResultMissing(failedMsg, error)
    Index->>Index: extractToolUseIds(failedMsg.parts)
    alt parts empty
        Index->>Index: extractToolUseIdsFromError(error)
        Note over Index: NEW: Regex extracts toolu_* from error message
    end
    Index->>API: Send tool_result parts
    API-->>Hook: Recovery success
    
    API->>Hook: Error: thinking_block_order
    Hook->>Index: recoverThinkingBlockOrder(failedMsg, error)
    Index->>Index: extractMessageIndex(error)
    Index->>Storage: findMessageByIndexNeedingThinking(targetIndex)
    Storage->>Storage: Try offsets: 0, -1, +1, -2, +2, -3, -4, -5
    Note over Storage: NEW: Multiple index offsets
    alt message found
        Storage-->>Index: return messageID
        Index->>Storage: prependThinkingPart(messageID)
    else not found
        Index->>Index: Try failedMsg.info.id directly
        Note over Index: NEW: Direct fallback
        alt direct fallback works
            Index->>Storage: prependThinkingPart(failedMsgId)
        else still not found
            Index->>Storage: findMessagesWithOrphanThinking()
            Index->>Storage: prependThinkingPart(orphanID)
        end
    end
    Storage-->>Index: Recovery success/failure
    Index-->>Hook: Return result
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  1. src/hooks/session-recovery/storage.ts, line 374-383 (link)

    logic: trying offsets without positive offsets first could match wrong message

    the order tries negative offsets (-1, -2, -3, -4, -5) before larger positive offsets (+1, +2), which means if the actual target is at targetIndex + 3, this would incorrectly match an earlier message at targetIndex - 1 or similar

55 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

10 issues found across 55 files

Confidence score: 3/5

  • Writing untrusted PR titles to $GITHUB_OUTPUT via plain echo in .github/workflows/sisyphus-agent.yml risks newline/control-character injection and can corrupt downstream workflow steps.
  • blocking.ts creates a Worker but never guarantees worker.terminate()/port1.close() on exceptions, so leaked resources or hung workers are likely under failure paths.
  • Pay close attention to .github/workflows/sisyphus-agent.yml, src/features/opencode-skill-loader/blocking.ts, src/features/skill-mcp-manager/manager.ts - output handling, cleanup, and idle-client management each need hardening before merge.
Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/features/claude-code-plugin-loader/loader.ts">

<violation number="1" location="src/features/claude-code-plugin-loader/loader.ts:467">
P2: Wrapping synchronous functions in `Promise.resolve()` provides no parallelization benefit. Since `loadPluginCommands`, `loadPluginSkillsAsCommands`, `loadPluginAgents`, and `loadPluginHooksConfigs` are all synchronous, they still execute sequentially when called as arguments. Consider keeping the original sequential pattern for the sync functions and only `await` the async `loadPluginMcpServers`.</violation>
</file>

<file name="src/features/context-injector/injector.ts">

<violation number="1" location="src/features/context-injector/injector.ts:129">
P2: Using `Date.now()` alone for ID generation can cause collisions if called multiple times within the same millisecond. Consider using `crypto.randomUUID()` or combining timestamp with a random component.</violation>
</file>

<file name="src/features/context-injector/injector.test.ts">

<violation number="1" location="src/features/context-injector/injector.test.ts:203">
P2: Test helper `createMockMessage` generates inconsistent mock data: `messageID` in parts doesn&#39;t match `info.id`. The `info.id` includes `Math.random()` while `messageID` uses a separate `Date.now()` call without it, causing a mismatch. Consider storing the generated ID in a variable and reusing it.</violation>
</file>

<file name="src/features/opencode-skill-loader/blocking.ts">

<violation number="1" location="src/features/opencode-skill-loader/blocking.ts:27">
P2: Missing try/finally for resource cleanup. If an exception occurs after the Worker is created (e.g., in `postMessage` or `receiveMessageOnPort`), `worker.terminate()` and `port1.close()` will never be called, causing a resource leak.</violation>
</file>

<file name="src/features/claude-code-command-loader/loader.ts">

<violation number="1" location="src/features/claude-code-command-loader/loader.ts:190">
P2: Template literal has extra leading spaces compared to sync version. Each content line has an unwanted leading space that will cause inconsistent command template output between `loadCommandsFromDir` and `loadCommandsFromDirAsync`.</violation>
</file>

<file name="src/shared/opencode-config-dir.test.ts">

<violation number="1" location="src/shared/opencode-config-dir.test.ts:221">
P2: This assertion is too weak and doesn&#39;t match the test name. The test claims to verify &#39;returns null when no config exists&#39; but the assertion accepts both `null` and `string`, meaning it can never fail. Consider using a deterministic assertion that actually verifies the expected behavior.</violation>
</file>

<file name=".github/workflows/lint-workflows.yml">

<violation number="1" location=".github/workflows/lint-workflows.yml:19">
P2: Avoid downloading and executing remote scripts via `curl | bash` pattern. Consider using the official GitHub Action `rhysd/actionlint-action` instead, which is more secure and maintainable.</violation>
</file>

<file name="src/features/skill-mcp-manager/manager.ts">

<violation number="1" location="src/features/skill-mcp-manager/manager.ts:184">
P2: `disconnectAll()` should also clear `pendingConnections` to fully reset manager state and avoid returning stale in-flight connections.</violation>

<violation number="2" location="src/features/skill-mcp-manager/manager.ts:200">
P2: Handle the Promise returned by `cleanupIdleClients()` in the interval callback to avoid unhandled rejections (and consider preventing overlapping runs if cleanup can take a while).</violation>
</file>

<file name=".github/workflows/sisyphus-agent.yml">

<violation number="1" location=".github/workflows/sisyphus-agent.yml:322">
P1: `TITLE` is written to `$GITHUB_OUTPUT` with a plain `echo &quot;name=$value&quot;`, which is unsafe/brittle for untrusted text and can break or inject outputs if the title contains newlines/control characters. Use the delimiter (`&lt;&lt;EOF`) form (and quote `$GITHUB_OUTPUT`) when setting outputs.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines 467 to 473
const [commands, skills, agents, mcpServers, hooksConfigs] = await Promise.all([
Promise.resolve(loadPluginCommands(plugins)),
Promise.resolve(loadPluginSkillsAsCommands(plugins)),
Promise.resolve(loadPluginAgents(plugins)),
loadPluginMcpServers(plugins),
Promise.resolve(loadPluginHooksConfigs(plugins)),
])
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: Wrapping synchronous functions in Promise.resolve() provides no parallelization benefit. Since loadPluginCommands, loadPluginSkillsAsCommands, loadPluginAgents, and loadPluginHooksConfigs are all synchronous, they still execute sequentially when called as arguments. Consider keeping the original sequential pattern for the sync functions and only await the async loadPluginMcpServers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/claude-code-plugin-loader/loader.ts, line 467:

<comment>Wrapping synchronous functions in `Promise.resolve()` provides no parallelization benefit. Since `loadPluginCommands`, `loadPluginSkillsAsCommands`, `loadPluginAgents`, and `loadPluginHooksConfigs` are all synchronous, they still execute sequentially when called as arguments. Consider keeping the original sequential pattern for the sync functions and only `await` the async `loadPluginMcpServers`.</comment>

<file context>
@@ -464,11 +464,13 @@ export interface PluginComponentsResult {
-  const agents = loadPluginAgents(plugins)
-  const mcpServers = await loadPluginMcpServers(plugins)
-  const hooksConfigs = loadPluginHooksConfigs(plugins)
+  const [commands, skills, agents, mcpServers, hooksConfigs] = await Promise.all([
+    Promise.resolve(loadPluginCommands(plugins)),
+    Promise.resolve(loadPluginSkillsAsCommands(plugins)),
</file context>
Suggested change
const [commands, skills, agents, mcpServers, hooksConfigs] = await Promise.all([
Promise.resolve(loadPluginCommands(plugins)),
Promise.resolve(loadPluginSkillsAsCommands(plugins)),
Promise.resolve(loadPluginAgents(plugins)),
loadPluginMcpServers(plugins),
Promise.resolve(loadPluginHooksConfigs(plugins)),
])
const commands = loadPluginCommands(plugins)
const skills = loadPluginSkillsAsCommands(plugins)
const agents = loadPluginAgents(plugins)
const mcpServers = await loadPluginMcpServers(plugins)
const hooksConfigs = loadPluginHooksConfigs(plugins)
Fix with Cubic

path?: { cwd?: string; root?: string }
}

const syntheticMessageId = `synthetic_ctx_${Date.now()}`
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: Using Date.now() alone for ID generation can cause collisions if called multiple times within the same millisecond. Consider using crypto.randomUUID() or combining timestamp with a random component.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/context-injector/injector.ts, line 129:

<comment>Using `Date.now()` alone for ID generation can cause collisions if called multiple times within the same millisecond. Consider using `crypto.randomUUID()` or combining timestamp with a random component.</comment>

<file context>
@@ -0,0 +1,166 @@
+        path?: { cwd?: string; root?: string }
+      }
+
+      const syntheticMessageId = `synthetic_ctx_${Date.now()}`
+      const syntheticPartId = `synthetic_ctx_part_${Date.now()}`
+      const now = Date.now()
</file context>
Fix with Cubic

{
id: `part_${Date.now()}`,
sessionID,
messageID: `msg_${Date.now()}`,
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: Test helper createMockMessage generates inconsistent mock data: messageID in parts doesn't match info.id. The info.id includes Math.random() while messageID uses a separate Date.now() call without it, causing a mismatch. Consider storing the generated ID in a variable and reusing it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/context-injector/injector.test.ts, line 203:

<comment>Test helper `createMockMessage` generates inconsistent mock data: `messageID` in parts doesn&#39;t match `info.id`. The `info.id` includes `Math.random()` while `messageID` uses a separate `Date.now()` call without it, causing a mismatch. Consider storing the generated ID in a variable and reusing it.</comment>

<file context>
@@ -0,0 +1,292 @@
+      {
+        id: `part_${Date.now()}`,
+        sessionID,
+        messageID: `msg_${Date.now()}`,
+        type: &quot;text&quot; as const,
+        text,
</file context>
Fix with Cubic

const signal = new Int32Array(new SharedArrayBuffer(4))
const { port1, port2 } = new MessageChannel()

const worker = new Worker(new URL("./discover-worker.ts", import.meta.url), {
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: Missing try/finally for resource cleanup. If an exception occurs after the Worker is created (e.g., in postMessage or receiveMessageOnPort), worker.terminate() and port1.close() will never be called, causing a resource leak.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/opencode-skill-loader/blocking.ts, line 27:

<comment>Missing try/finally for resource cleanup. If an exception occurs after the Worker is created (e.g., in `postMessage` or `receiveMessageOnPort`), `worker.terminate()` and `port1.close()` will never be called, causing a resource leak.</comment>

<file context>
@@ -0,0 +1,62 @@
+  const signal = new Int32Array(new SharedArrayBuffer(4))
+  const { port1, port2 } = new MessageChannel()
+  
+  const worker = new Worker(new URL(&quot;./discover-worker.ts&quot;, import.meta.url), {
+    workerData: { signal }
+  })
</file context>
Fix with Cubic

Comment on lines 190 to 196
${body.trim()}
</command-instruction>
<user-request>
$ARGUMENTS
</user-request>`

Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: Template literal has extra leading spaces compared to sync version. Each content line has an unwanted leading space that will cause inconsistent command template output between loadCommandsFromDir and loadCommandsFromDirAsync.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/claude-code-command-loader/loader.ts, line 190:

<comment>Template literal has extra leading spaces compared to sync version. Each content line has an unwanted leading space that will cause inconsistent command template output between `loadCommandsFromDir` and `loadCommandsFromDirAsync`.</comment>

<file context>
@@ -129,3 +130,130 @@ export function loadOpencodeProjectCommands(): Record&lt;string, CommandDefinition&gt;
+      const { data, body } = parseFrontmatter&lt;CommandFrontmatter&gt;(content)
+
+      const wrappedTemplate = `&lt;command-instruction&gt;
+ ${body.trim()}
+ &lt;/command-instruction&gt;
+
</file context>
Suggested change
${body.trim()}
</command-instruction>
<user-request>
$ARGUMENTS
</user-request>`
${body.trim()}
</command-instruction>
<user-request>
$ARGUMENTS
</user-request>`
Fix with Cubic

const result = detectExistingConfigDir("opencode", "1.0.200")

// #then result is either null or a valid string path
expect(result === null || typeof result === "string").toBe(true)
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: This assertion is too weak and doesn't match the test name. The test claims to verify 'returns null when no config exists' but the assertion accepts both null and string, meaning it can never fail. Consider using a deterministic assertion that actually verifies the expected behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/shared/opencode-config-dir.test.ts, line 221:

<comment>This assertion is too weak and doesn&#39;t match the test name. The test claims to verify &#39;returns null when no config exists&#39; but the assertion accepts both `null` and `string`, meaning it can never fail. Consider using a deterministic assertion that actually verifies the expected behavior.</comment>

<file context>
@@ -0,0 +1,224 @@
+      const result = detectExistingConfigDir(&quot;opencode&quot;, &quot;1.0.200&quot;)
+
+      // #then result is either null or a valid string path
+      expect(result === null || typeof result === &quot;string&quot;).toBe(true)
+    })
+  })
</file context>
Fix with Cubic


- name: Install actionlint
run: |
bash <(curl -sSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.10/scripts/download-actionlint.bash)
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: Avoid downloading and executing remote scripts via curl | bash pattern. Consider using the official GitHub Action rhysd/actionlint-action instead, which is more secure and maintainable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/lint-workflows.yml, line 19:

<comment>Avoid downloading and executing remote scripts via `curl | bash` pattern. Consider using the official GitHub Action `rhysd/actionlint-action` instead, which is more secure and maintainable.</comment>

<file context>
@@ -0,0 +1,22 @@
+
+      - name: Install actionlint
+        run: |
+          bash &lt;(curl -sSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.10/scripts/download-actionlint.bash)
+
+      - name: Run actionlint
</file context>
Fix with Cubic


async disconnectAll(): Promise<void> {
for (const [, managed] of this.clients.entries()) {
this.stopCleanupTimer()
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: disconnectAll() should also clear pendingConnections to fully reset manager state and avoid returning stale in-flight connections.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/skill-mcp-manager/manager.ts, line 184:

<comment>`disconnectAll()` should also clear `pendingConnections` to fully reset manager state and avoid returning stale in-flight connections.</comment>

<file context>
@@ -102,26 +164,64 @@ export class SkillMcpManager {
 
   async disconnectAll(): Promise&lt;void&gt; {
-    for (const [, managed] of this.clients.entries()) {
+    this.stopCleanupTimer()
+    const clients = Array.from(this.clients.values())
+    this.clients.clear()
</file context>
Suggested change
this.stopCleanupTimer()
this.stopCleanupTimer()
this.pendingConnections.clear()
Fix with Cubic

private startCleanupTimer(): void {
if (this.cleanupInterval) return
this.cleanupInterval = setInterval(() => {
this.cleanupIdleClients()
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P2: Handle the Promise returned by cleanupIdleClients() in the interval callback to avoid unhandled rejections (and consider preventing overlapping runs if cleanup can take a while).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/skill-mcp-manager/manager.ts, line 200:

<comment>Handle the Promise returned by `cleanupIdleClients()` in the interval callback to avoid unhandled rejections (and consider preventing overlapping runs if cleanup can take a while).</comment>

<file context>
@@ -102,26 +164,64 @@ export class SkillMcpManager {
+  private startCleanupTimer(): void {
+    if (this.cleanupInterval) return
+    this.cleanupInterval = setInterval(() =&gt; {
+      this.cleanupIdleClients()
+    }, 60_000)
+    this.cleanupInterval.unref()
</file context>
Suggested change
this.cleanupIdleClients()
void this.cleanupIdleClients().catch(() => {})
Fix with Cubic

echo "type=issue" >> $GITHUB_OUTPUT
echo "number=${ISSUE_NUM}" >> $GITHUB_OUTPUT
fi
echo "title=${TITLE}" >> $GITHUB_OUTPUT
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 4, 2026

Choose a reason for hiding this comment

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

P1: TITLE is written to $GITHUB_OUTPUT with a plain echo "name=$value", which is unsafe/brittle for untrusted text and can break or inject outputs if the title contains newlines/control characters. Use the delimiter (<<EOF) form (and quote $GITHUB_OUTPUT) when setting outputs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/sisyphus-agent.yml, line 322:

<comment>`TITLE` is written to `$GITHUB_OUTPUT` with a plain `echo &quot;name=$value&quot;`, which is unsafe/brittle for untrusted text and can break or inject outputs if the title contains newlines/control characters. Use the delimiter (`&lt;&lt;EOF`) form (and quote `$GITHUB_OUTPUT`) when setting outputs.</comment>

<file context>
@@ -244,14 +309,17 @@ jobs:
               echo &quot;type=issue&quot; &gt;&gt; $GITHUB_OUTPUT
               echo &quot;number=${ISSUE_NUM}&quot; &gt;&gt; $GITHUB_OUTPUT
             fi
+            echo &quot;title=${TITLE}&quot; &gt;&gt; $GITHUB_OUTPUT
           fi
 
</file context>
Suggested change
echo "title=${TITLE}" >> $GITHUB_OUTPUT
{
echo "title<<EOF"
echo "$TITLE"
echo "EOF"
} >> "$GITHUB_OUTPUT"
Fix with Cubic

Copy link
Collaborator

@junhoyeo junhoyeo left a comment

Choose a reason for hiding this comment

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

@starcomo thanks for opening the PR.

Right now the commit set in this PR is pretty messy and includes unrelated history (it looks like it pulled in existing work). Could you rebase this branch onto the latest master and force-push so the PR contains only your changes?

A good PR is one that introduces only the changes you’re actually making, with a clean and reviewable diff.

…king_block_order

- Add error parameter to recoverToolResultMissing and extract tool_use IDs from error message as fallback
- Add multiple index offsets in findMessageByIndexNeedingThinking to handle API/storage index mismatch
- Add failedAssistantMsg.info.id as fallback in recoverThinkingBlockOrder

Fixes code-yeongyu#483
@starcomo starcomo force-pushed the fix/session-recovery-fallbacks branch from aa1b8bd to c6feac9 Compare January 6, 2026 22:45
@starcomo
Copy link
Author

starcomo commented Jan 6, 2026

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Jan 6, 2026
@code-yeongyu
Copy link
Owner

@sisyphus-dev-ai review ulw

@sisyphus-dev-ai sisyphus-dev-ai added the sisyphus: working Sisyphus is currently working on this label Jan 9, 2026
@sisyphus-dev-ai
Copy link
Collaborator

PR #486 Technical Review - ULTRAWORK MODE

Summary

This PR adds critical fallback mechanisms for session recovery, addressing issue #483. The changes are focused and well-intentioned, but there are several concerns that need attention before merge.

Critical Issues

1. ⚠️ Offset Order Imbalance (Greptile is Correct)

File: src/hooks/session-recovery/storage.ts
Location: findMessageByIndexNeedingThinking() lines 373-381

Issue: The offset search pattern is asymmetric:

const indicesToTry = [
  targetIndex,      // 0
  targetIndex - 1,  // -1
  targetIndex + 1,  // +1
  targetIndex - 2,  // -2
  targetIndex + 2,  // +2
  targetIndex - 3,  // -3  ❌ no +3
  targetIndex - 4,  // -4  ❌ no +4
  targetIndex - 5,  // -5  ❌ no +5
]

Problem: This pattern tries 5 negative offsets but only 2 positive offsets. If the API/storage index mismatch tends to be positive (target at +3, +4, +5), this will fail.

Recommendation: Make the pattern symmetric:

const indicesToTry = [
  targetIndex,
  targetIndex - 1, targetIndex + 1,
  targetIndex - 2, targetIndex + 2,
  targetIndex - 3, targetIndex + 3,
  targetIndex - 4, targetIndex + 4,
  targetIndex - 5, targetIndex + 5,
]

Or better yet, make it configurable or analyze the actual distribution of mismatches to optimize the order.

2. 🟡 Regex Pattern Validation

File: src/hooks/session-recovery/index.ts
Location: extractToolUseIdsFromError() line 127

Current:

const matches = message.match(/toolu_[a-zA-Z0-9]+/g)

Analysis:

  • Pattern looks correct for Anthropic's tool_use ID format (toolu_ prefix + alphanumeric)
  • Using Set to deduplicate is good practice
  • Converts error message to lowercase in getErrorMessage() but regex expects lowercase toolu_

Potential Issue: If Anthropic's error messages use TOOLU_ or mixed case, this will miss them.

Recommendation: Make regex case-insensitive:

const matches = message.match(/toolu_[a-zA-Z0-9]+/gi)  // Note the 'i' flag

3. 🟢 Direct failedMsg.info.id Fallback (Good)

File: src/hooks/session-recovery/index.ts
Location: recoverThinkingBlockOrder() lines 213-221

Analysis: This is a smart fallback. When index extraction fails, trying the failed message ID directly makes sense because:

  1. The error is about this specific message
  2. It's the most likely candidate needing recovery
  3. prependThinkingPart is idempotent (won't break if called twice)

No changes needed - this looks good.

Non-Critical Observations

4. 📝 First-Match Strategy

The offset loop returns on first match:

for (const idx of indicesToTry) {
  if (idx < 0 || idx >= messages.length) continue
  const targetMsg = messages[idx]
  if (targetMsg.role !== "assistant") continue
  // ... checks ...
  if (!firstIsThinking) {
    return targetMsg.id  // ❗ Returns first match
  }
}

Question: What if multiple messages match? Currently returns the first match according to offset order.

Impact: With asymmetric offsets, this biases toward negative indices. With symmetric offsets, this would be more balanced.

Recommendation: Document this behavior. Consider logging which offset was used for debugging.

5. 🔍 Missing Tests

Critical Code Paths Not Covered:

  • extractToolUseIdsFromError() regex extraction
  • Offset fallback logic in findMessageByIndexNeedingThinking()
  • Direct failedMsg.info.id fallback
  • Integration test: recovery with empty parts + error parameter

Recommendation: Add tests before merge.

Git History Check

CLEAN: PR contains only 1 commit with 2 file changes. No merge commits or unrelated history detected.

Note: @junhoyeo's concern about "messy history" may be from viewing the GitHub PR page differently, but technically the commit is clean.

Performance Impact

Minimal:

  • Regex is only run as fallback when parts are empty
  • Offset loop tries max 8 indices (vs 1 before)
  • All changes are in error/recovery paths (not hot path)

Security Concerns

🔒 None identified:

  • Regex doesn't execute code
  • No SQL injection risk
  • No arbitrary file access

Edge Cases to Consider

  1. What if error message contains fake tool_use IDs?
    → Low risk, but could extract wrong IDs if error message includes examples

  2. What if targetIndex is at array boundary (0 or length-1)?
    → Handled correctly with if (idx < 0 || idx >= messages.length) continue

  3. What if all offsets point to non-assistant messages?
    → Falls back to orphan detection (existing behavior)

Recommendations Summary

Must Fix (Before Merge)

  1. Fix offset order: Make symmetric (-5 to +5)
  2. Add 'i' flag to regex: Handle case-insensitive tool IDs
  3. Add tests: Cover new fallback code paths

Should Consider

  1. Add logging for which offset was used (debugging aid)
  2. Document the first-match behavior
  3. Consider metrics: track which fallback path succeeded

Nice to Have

  1. Make offset range configurable
  2. Add telemetry to understand actual mismatch patterns

Verdict

Status: ⚠️ CHANGES REQUESTED

Rationale:

Merge After:

  1. Fix offset symmetry
  2. Add regex 'i' flag
  3. Add basic tests
  4. Re-review

Positive Aspects

✅ Smart fallback design
✅ Minimal performance impact
✅ Clean commit history
✅ Well-scoped changes (2 files only)
✅ Addresses real user pain (#483)


Reviewed in ULTRAWORK MODE by @sisyphus-dev-ai

@sisyphus-dev-ai sisyphus-dev-ai removed the sisyphus: working Sisyphus is currently working on this label Jan 9, 2026
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.

4 participants