Skip to content

Commit fda856e

Browse files
authored
Merge pull request #1459 from link-assistant/issue-1458-95ae87b62e25
fix: interactive mode comment output - shell quoting, queue deadlock, duplicate init
2 parents 55795d6 + ef852e6 commit fda856e

13 files changed

+15943
-34
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@link-assistant/hive-mind': patch
3+
---
4+
5+
Fix interactive mode PR comment output: use stdin for GitHub API calls to prevent shell quoting corruption, flush comment queue before tool result timeout to prevent stuck "Waiting for result..." comments, and guard against duplicate session started comments from late system.init events
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Case Study: Interactive Mode Comment Output Issues (#1458)
2+
3+
## Overview
4+
5+
Investigation of 89+ broken PR comments during an interactive Claude session on
6+
`xlab2016/space_db_private/pull/20`. Three distinct failure modes were identified,
7+
each with a different root cause.
8+
9+
**Source data:**
10+
11+
- PR: `xlab2016/space_db_private/pull/20` (339 total comments)
12+
- Solution draft log: 51,196 lines (from gist)
13+
- Representative samples saved in this directory as JSON files
14+
15+
---
16+
17+
## Issue 1: Empty/Corrupted Markdown Fields (27 comments)
18+
19+
### Symptoms
20+
21+
Comments displayed with literal empty fields like:
22+
23+
```
24+
**Pattern:** ''
25+
**File:** ''
26+
**Path:** ''
27+
```
28+
29+
Bodies contained literal single-quote characters (char code 39) wrapping field values.
30+
31+
### Root Cause
32+
33+
`command-stream`'s `quote()` function wraps values in shell single quotes:
34+
`'value'` → which requires a shell to interpret. However, `gh api -f body=...`
35+
passed via `execFile` (which bypasses the shell) received the quotes as literal
36+
characters instead of shell metacharacters.
37+
38+
The comment body was correctly constructed in JavaScript, but corrupted during
39+
the GitHub API call because `command-stream`'s tagged template literal produced
40+
shell-quoted strings that were never shell-interpreted.
41+
42+
### Fix
43+
44+
Replaced `command-stream`'s `$` template literal calls with direct `execFileAsync`
45+
using `--input -` to pass the JSON body via stdin. This completely bypasses shell
46+
quoting and ensures the body arrives at the GitHub API exactly as constructed.
47+
48+
---
49+
50+
## Issue 2: Comments Not Updated With Results (61 comments)
51+
52+
### Symptoms
53+
54+
Tool use comments permanently showed "⏳ Waiting for result..." instead of being
55+
edited with the tool's output when the result arrived.
56+
57+
### Root Cause
58+
59+
Deadlock between `handleToolResult` and `processQueue`:
60+
61+
1. `handleToolResult` awaits `commentIdPromise` (resolved when the tool_use
62+
comment is posted and gets a comment ID)
63+
2. `commentIdPromise` is resolved by `processQueue` processing the comment queue
64+
3. `processQueue` runs in `processEvent`'s `finally` block
65+
4. But `processEvent` is blocked because `handleToolResult` is still awaiting
66+
67+
This created a circular dependency. After 30 seconds, the timeout fired and
68+
`handleToolResult` posted a separate "result" comment instead of editing the
69+
original. Evidence from logs:
70+
71+
```
72+
21:55:38 - Tool use queued for comment
73+
21:56:08 - "Timeout waiting for tool use comment, posting result separately"
74+
```
75+
76+
### Fix
77+
78+
Added explicit queue flushing in `handleToolResult` before waiting for the
79+
comment ID promise. This breaks the deadlock by processing any pending queue
80+
items (including the tool_use comment) immediately.
81+
82+
---
83+
84+
## Issue 3: Duplicate Session Started Comment (1 comment)
85+
86+
### Symptoms
87+
88+
A second "🚀 Interactive session started" comment appeared at `22:40:53`,
89+
well after the session was already active (first init at `21:54:53`).
90+
91+
### Root Cause
92+
93+
A `task_notification` event for a background dotnet SDK installation arrived
94+
at `22:40:52` (after the `result` event at `22:40:37`). This late notification
95+
triggered Claude CLI to emit a second `system.init` event with the same session
96+
ID at `22:40:53`.
97+
98+
Timeline from logs:
99+
100+
```
101+
21:54:53 - First system.init (session starts)
102+
22:40:37 - result event (session effectively ends)
103+
22:40:52 - Late task_notification (background dotnet install)
104+
22:40:53 - Second system.init (same session ID - causes duplicate comment)
105+
```
106+
107+
### Fix
108+
109+
Added a guard in `handleSystemInit` that checks if `state.sessionId` is already
110+
set. If a session is already initialized, the duplicate `system.init` event is
111+
silently ignored (with verbose logging for diagnostics).
112+
113+
---
114+
115+
## Files Changed
116+
117+
- `src/interactive-mode.lib.mjs` — All three fixes applied
118+
- `tests/test-interactive-mode.mjs` — Updated test mocks for new `execFileAsync` pattern
119+
- `src/unicode-sanitization.lib.mjs` — No changes (already correct from #1324)
120+
121+
## Testing
122+
123+
All 91 tests pass after the fixes, including existing regression tests for
124+
unicode sanitization (#1324) and agent task events (#1450).
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[
2+
{
3+
"id": 4104682726,
4+
"created_at": "2026-03-21T22:40:53Z",
5+
"updated_at": "2026-03-21T22:40:53Z",
6+
"body": "## 🚀 Interactive session started\n\n| Property | Value |\n|----------|-------|\n| **Session ID** | `2024f25c-03c6-4eb4-b7ed-0b2847653bba` |\n| **Model** | `claude-sonnet-4-6` |\n| **Claude Code Version** | `2.1.81` |\n| **Permission Mode** | `bypassPermissions` |\n| **Working Directory** | `/tmp/gh-issue-solver-1774130096902` |\n| **Available Tools** | `Task`, `TaskOutput`, `Bash`, `Glob`, `Grep`, `ExitPlanMode`, `Read`, `Edit`, `Write`, `NotebookEdit`, `WebFetch`, `TodoWrite`, `WebSearch`, `TaskStop`, `AskUserQuestion`, `Skill`, `EnterPlanMode`, `EnterWorktree`, `ExitWorktree`, `CronCreate`, `CronDelete`, `CronList`, `ToolSearch` |\n| **MCP Servers** | `claude.ai Google Calendar` (needs-auth), `claude.ai Gmail` (needs-auth) |\n| **Slash Commands** | `/update-config`, `/debug`, `/simplify`, `/batch`, `/loop`, `/claude-api`, `/compact`, `/context`, `/cost`, `/heapdump`, `/init`, `/pr-comments`, `/release-notes`, `/review`, `/security-review`, `/extra-usage`, `/insights` |\n| **Agents** | `general-purpose`, `statusline-setup`, `Explore`, `Plan` |\n\n---\n\n<details>\n<summary>📄 Raw JSON</summary>\n\n```json\n[\n {\n \"type\": \"system\",\n \"subtype\": \"init\",\n \"cwd\": \"/tmp/gh-issue-solver-1774130096902\",\n \"session_id\": \"2024f25c-03c6-4eb4-b7ed-0b2847653bba\",\n \"tools\": [\n \"Task\",\n \"TaskOutput\",\n \"Bash\",\n \"Glob\",\n \"Grep\",\n \"ExitPlanMode\",\n \"Read\",\n \"Edit\",\n \"Write\",\n \"NotebookEdit\",\n \"WebFetch\",\n \"TodoWrite\",\n \"WebSearch\",\n \"TaskStop\",\n \"AskUserQuestion\",\n \"Skill\",\n \"EnterPlanMode\",\n \"EnterWorktree\",\n \"ExitWorktree\",\n \"CronCreate\",\n \"CronDelete\",\n \"CronList\",\n \"ToolSearch\"\n ],\n \"mcp_servers\": [\n {\n \"name\": \"claude.ai Google Calendar\",\n \"status\": \"needs-auth\"\n },\n {\n \"name\": \"claude.ai Gmail\",\n \"status\": \"needs-auth\"\n }\n ],\n \"model\": \"claude-sonnet-4-6\",\n \"permissionMode\": \"bypassPermissions\",\n \"slash_commands\": [\n \"update-config\",\n \"debug\",\n \"simplify\",\n \"batch\",\n \"loop\",\n \"claude-api\",\n \"compact\",\n \"context\",\n \"cost\",\n \"heapdump\",\n \"init\",\n \"pr-comments\",\n \"release-notes\",\n \"review\",\n \"security-review\",\n \"extra-usage\",\n \"insights\"\n ],\n \"apiKeySource\": \"none\",\n \"claude_code_version\": \"2.1.81\",\n \"output_style\": \"default\",\n \"agents\": [\n \"general-purpose\",\n \"statusline-setup\",\n \"Explore\",\n \"Plan\"\n ],\n \"skills\": [\n \"update-config\",\n \"debug\",\n \"simplify\",\n \"batch\",\n \"loop\",\n \"claude-api\"\n ],\n \"plugins\": [],\n \"uuid\": \"4af5eeb6-e2b5-46e8-b64f-165455a16779\",\n \"fast_mode_state\": \"off\"\n }\n]\n```\n\n</details>"
7+
}
8+
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[
2+
{
3+
"id": 4104659088,
4+
"created_at": "2026-03-21T22:33:51Z",
5+
"updated_at": "2026-03-21T22:33:51Z",
6+
"body": "## 🤖 Agent task: wget -q https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh && chmod +x /tmp/dotnet-install.sh && /tmp/dotnet-install.sh --version 9.0.100 --install-dir /tmp/dotnet9 > /tmp/dotnet_install.log 2>&1; echo \"Exit: $?\"\n\n| Property | Value |\n|----------|-------|\n| **Task ID** | `bk8qj36e3` |\n| **Type** | `local_bash` |\n| **Status** | ⏳ Running... |\n\n\n---\n\n<details>\n<summary>📄 Raw JSON</summary>\n\n```json\n[\n {\n \"type\": \"system\",\n \"subtype\": \"task_started\",\n \"task_id\": \"bk8qj36e3\",\n \"tool_use_id\": \"toolu_01WVXmWkiUzVQiNTKHhPYdYp\",\n \"description\": \"wget -q https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh && chmod +x /tmp/dotnet-install.sh && /tmp/dotnet-install.sh --version 9.0.100 --install-dir /tmp/dotnet9 > /tmp/dotnet_install.log 2>&1; echo \\\"Exit: $?\\\"\",\n \"task_type\": \"local_bash\",\n \"uuid\": \"b3953022-c072-4943-abe7-b651a56a5eef\",\n \"session_id\": \"2024f25c-03c6-4eb4-b7ed-0b2847653bba\"\n }\n]\n```\n\n</details>"
7+
},
8+
{
9+
"id": 4104661257,
10+
"created_at": "2026-03-21T22:34:20Z",
11+
"updated_at": "2026-03-21T22:34:21Z",
12+
"body": "'## 🔎 Grep tool use\n\n**Pattern:** \n**Path:** \n\n<details open>\n<summary>📤 Output (✅ success)</summary>\n\n\n\n</details>\n\n---\n\n<details>\n<summary>📄 Raw JSON</summary>\n\n\n\n</details>'"
13+
},
14+
{
15+
"id": 4104662013,
16+
"created_at": "2026-03-21T22:34:32Z",
17+
"updated_at": "2026-03-21T22:34:33Z",
18+
"body": "'## 🔎 Grep tool use\n\n**Pattern:** \n**Path:** \n\n<details open>\n<summary>📤 Output (✅ success)</summary>\n\n\n\n</details>\n\n---\n\n<details>\n<summary>📄 Raw JSON</summary>\n\n\n\n</details>'"
19+
},
20+
{
21+
"id": 4104663699,
22+
"created_at": "2026-03-21T22:35:02Z",
23+
"updated_at": "2026-03-21T22:35:03Z",
24+
"body": "'## 📖 Read tool use\n\n**File:** \n**Range:** offset=338, limit=60\n\n<details open>\n<summary>📤 Output (✅ success)</summary>\n\n\n\n</details>\n\n---\n\n<details>\n<summary>📄 Raw JSON</summary>\n\n\n\n</details>'"
25+
}
26+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"id": 4104659844,
4+
"created_at": "2026-03-21T22:34:03Z",
5+
"updated_at": "2026-03-21T22:34:03Z",
6+
"body": "## 📖 Read tool use\n\n**File:** `/tmp/gh-issue-solver-1774130096902/src/Libs/Magic.Kernel/Devices/Streams/ClawStreamDevice.cs`\n\n_⏳ Waiting for result..._\n\n---\n\n<details>\n<summary>📄 Raw JSON</summary>\n\n```json\n[\n {\n \"type\": \"assistant\",\n \"message\": {\n \"model\": \"claude-sonnet-4-6\",\n \"id\": \"msg_01AAfDNY2uhUojtFyWK53J1y\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"content\": [\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01Ka24aQ9hDWQnyicRJUJQLr\",\n \"name\": \"Read\",\n \"input\": {\n \"file_path\": \"/tmp/gh-issue-solver-1774130096902/src/Libs/Magic.Kernel/Devices/Streams/ClawStreamDevice.cs\"\n },\n \"caller\": {\n \"type\": \"direct\"\n }\n }\n ],\n \"stop_reason\": null,\n \"stop_sequence\": null,\n \"usage\": {\n \"input_tokens\": 1,\n \"cache_creation_input_tokens\": 297,\n \"cache_read_input_tokens\": 67583,\n \"cache_creation\": {\n \"ephemeral_5m_input_tokens\": 0,\n \"ephemeral_1h_input_tokens\": 297\n },\n \"output_tokens\": 1,\n \"service_tier\": \"standard\",\n \"inference_geo\": \"not_available\"\n },\n \"context_management\": null\n },\n \"parent_tool_use_id\": null,\n \"session_id\": \"2024f25c-03c6-4eb4-b7ed-0b2847653bba\",\n \"uuid\": \"0e64635d-204c-4123-a0cd-050359faf057\"\n }\n]\n```\n\n</details>"
7+
},
8+
{
9+
"id": 4104662688,
10+
"created_at": "2026-03-21T22:34:44Z",
11+
"updated_at": "2026-03-21T22:34:44Z",
12+
"body": "## 🔎 Grep tool use\n\n**Pattern:** `case Opcodes.GetObj|GetObjAsync|ExecuteGetObj`\n**Path:** `/tmp/gh-issue-solver-1774130096902/src/Libs/Magic.Kernel/Interpretation/Interpreter.cs`\n\n_⏳ Waiting for result..._\n\n---\n\n<details>\n<summary>📄 Raw JSON</summary>\n\n```json\n[\n {\n \"type\": \"assistant\",\n \"message\": {\n \"model\": \"claude-sonnet-4-6\",\n \"id\": \"msg_01J2Rnbj3F2YBaQwJTrPVhXQ\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"content\": [\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_01RqbZy4ZLu81R4rsHbhNPio\",\n \"name\": \"Grep\",\n \"input\": {\n \"pattern\": \"case Opcodes.GetObj|GetObjAsync|ExecuteGetObj\",\n \"path\": \"/tmp/gh-issue-solver-1774130096902/src/Libs/Magic.Kernel/Interpretation/Interpreter.cs\",\n \"output_mode\": \"content\",\n \"context\": 15,\n \"head_limit\": 50\n },\n \"caller\": {\n \"type\": \"direct\"\n }\n }\n ],\n \"stop_reason\": null,\n \"stop_sequence\": null,\n \"usage\": {\n \"input_tokens\": 1,\n \"cache_creation_input_tokens\": 1364,\n \"cache_read_input_tokens\": 78387,\n \"cache_creation\": {\n \"ephemeral_5m_input_tokens\": 0,\n \"ephemeral_1h_input_tokens\": 1364\n },\n \"output_tokens\": 1,\n \"service_tier\": \"standard\",\n \"inference_geo\": \"not_available\"\n },\n \"context_management\": null\n },\n \"parent_tool_use_id\": null,\n \"session_id\": \"2024f25c-03c6-4eb4-b7ed-0b2847653bba\",\n \"uuid\": \"3924e669-9666-4db0-b2af-2c11f9d89a05\"\n }\n]\n```\n\n</details>"
13+
}
14+
]

0 commit comments

Comments
 (0)