|
| 1 | +# Case Study: Issue #82 - Listening Mode Should Be Enabled by Default |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +**Issue:** [link-assistant/agent#82](https://github.com/link-assistant/agent/issues/82) |
| 6 | +**Reported By:** @konard |
| 7 | +**Date:** 2025-12-20 |
| 8 | +**Type:** Bug / Enhancement |
| 9 | +**Related PR:** [#79](https://github.com/link-assistant/agent/pull/79) (stdin handling improvements) |
| 10 | + |
| 11 | +## Problem Statement |
| 12 | + |
| 13 | +The user reports several issues with the current stdin handling behavior: |
| 14 | + |
| 15 | +1. **Single-message mode:** Agent only processes the first message from stdin, then exits. User cannot send additional messages during or after the agent processes the first message. |
| 16 | + |
| 17 | +2. **Process exits after first response:** When using `echo '{"message":"hi"}' | agent`, after the agent responds, the user typed "hi2" but the input was returned to the shell instead of being processed by the agent. |
| 18 | + |
| 19 | +3. **Status message is shown in compact JSON:** The status message `{"type":"status",...}` is output as a single line, which the user expects to be pretty-printed. |
| 20 | + |
| 21 | +4. **Missing `--always-accept-stdin` option:** Need an option to keep accepting stdin even after the agent finishes its work. |
| 22 | + |
| 23 | +5. **Missing option to toggle JSON pretty printing:** By default, JSON should be pretty-printed, with an option to disable for program-to-program communication. |
| 24 | + |
| 25 | +## Timeline of Events (Reconstructed) |
| 26 | + |
| 27 | +``` |
| 28 | +1. User runs: echo '{"message":"hi"}' | agent |
| 29 | +
|
| 30 | +2. Agent receives input via stdin pipe |
| 31 | +
|
| 32 | +3. Agent outputs status message to stderr: |
| 33 | + {"type":"status","mode":"stdin-stream",...} |
| 34 | +
|
| 35 | +4. Agent reads stdin until EOF (which arrives after echo completes) |
| 36 | +
|
| 37 | +5. Agent processes the message: {"message":"hi"} |
| 38 | +
|
| 39 | +6. Agent outputs JSON response to stdout (pretty-printed): |
| 40 | + { |
| 41 | + "type": "step_start", |
| 42 | + ... |
| 43 | + } |
| 44 | + { |
| 45 | + "type": "text", |
| 46 | + ... |
| 47 | + "text": "Hi! How can I help you today?" |
| 48 | + } |
| 49 | + { |
| 50 | + "type": "step_finish", |
| 51 | + ... |
| 52 | + } |
| 53 | +
|
| 54 | +7. Agent exits after session becomes idle |
| 55 | +
|
| 56 | +8. User types "hi2" but it goes to the shell (zsh) because agent has already exited |
| 57 | +
|
| 58 | +9. Shell interprets "hi2" as a command, fails with "command not found" |
| 59 | +``` |
| 60 | + |
| 61 | +## Root Cause Analysis |
| 62 | + |
| 63 | +### Root Cause 1: Single-Shot Stdin Reading |
| 64 | + |
| 65 | +**Location:** `src/index.js:81-129` (`readStdinWithTimeout` function) |
| 66 | + |
| 67 | +**Problem:** The current implementation reads stdin until EOF is received. When input comes from a pipe (`echo ... | agent`), EOF is sent as soon as the echo command completes, before the agent even starts processing. |
| 68 | + |
| 69 | +```javascript |
| 70 | +function readStdinWithTimeout(timeout = null) { |
| 71 | + return new Promise((resolve) => { |
| 72 | + // ... |
| 73 | + const onEnd = () => { |
| 74 | + cleanup(); |
| 75 | + resolve(data); // Resolves on EOF, ending stdin reading |
| 76 | + }; |
| 77 | + // ... |
| 78 | + }); |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +**Impact:** The agent processes only the first batch of input, then exits. There's no mechanism to continue reading stdin for additional messages. |
| 83 | + |
| 84 | +### Root Cause 2: No Continuous Input Mode |
| 85 | + |
| 86 | +**Location:** `src/index.js:752-790` |
| 87 | + |
| 88 | +**Problem:** The main function reads stdin once, processes it, and runs the agent. There's no loop to continue reading and processing additional messages. |
| 89 | + |
| 90 | +```javascript |
| 91 | +// Read stdin with optional timeout |
| 92 | +const input = await readStdinWithTimeout(timeout); |
| 93 | +const trimmedInput = input.trim(); |
| 94 | +// ... |
| 95 | +// Run agent mode (single execution, then exit) |
| 96 | +await runAgentMode(argv, request); |
| 97 | +``` |
| 98 | + |
| 99 | +**Impact:** Even though `InputQueue` class exists with continuous reading capabilities (in `src/cli/input-queue.js`), it's not being utilized in the main flow. |
| 100 | + |
| 101 | +### Root Cause 3: Status Message Uses `console.error` with Compact JSON |
| 102 | + |
| 103 | +**Location:** `src/index.js:136-138` |
| 104 | + |
| 105 | +**Problem:** The `outputStatus` function outputs compact JSON to stderr: |
| 106 | + |
| 107 | +```javascript |
| 108 | +function outputStatus(status) { |
| 109 | + console.error(JSON.stringify(status)); // No pretty printing |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +**Impact:** Status messages are hard to read for humans while debugging or in interactive use. |
| 114 | + |
| 115 | +### Root Cause 4: Missing CLI Options |
| 116 | + |
| 117 | +**Location:** `src/index.js:557-696` |
| 118 | + |
| 119 | +**Problem:** The following options are missing: |
| 120 | + |
| 121 | +- `--always-accept-stdin` - to keep accepting input even after agent finishes |
| 122 | +- `--compact-json` (or similar) - to explicitly control JSON pretty-printing |
| 123 | + |
| 124 | +**Impact:** Users cannot configure the agent behavior to match their needs. |
| 125 | + |
| 126 | +### Root Cause 5: Event Output Already Pretty-Prints (Inconsistency) |
| 127 | + |
| 128 | +**Location:** `src/json-standard/index.ts:50-60` |
| 129 | + |
| 130 | +**Current Behavior:** |
| 131 | + |
| 132 | +```typescript |
| 133 | +export function serializeOutput( |
| 134 | + event: OpenCodeEvent | ClaudeEvent, |
| 135 | + standard: JsonStandard |
| 136 | +): string { |
| 137 | + if (standard === 'claude') { |
| 138 | + return JSON.stringify(event) + EOL; // Compact for claude |
| 139 | + } |
| 140 | + return JSON.stringify(event, null, 2) + EOL; // Pretty for opencode |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +The main JSON output IS already pretty-printed for the "opencode" standard. However, status messages (which go to stderr) are NOT pretty-printed. |
| 145 | + |
| 146 | +## Proposed Solutions |
| 147 | + |
| 148 | +### Solution 1: Implement Continuous Stdin Reading Mode |
| 149 | + |
| 150 | +Modify the stdin handling to continuously read and process messages: |
| 151 | + |
| 152 | +1. Use `createContinuousStdinReader` from `src/cli/input-queue.js` |
| 153 | +2. Process messages as they arrive via the queue |
| 154 | +3. Keep the session alive between messages |
| 155 | +4. Exit only on EOF (end of stdin), SIGINT (Ctrl+C), or explicit exit command |
| 156 | + |
| 157 | +### Solution 2: Add `--always-accept-stdin` Option |
| 158 | + |
| 159 | +Add a new CLI option that: |
| 160 | + |
| 161 | +- When enabled (default: true), continuously accepts stdin input |
| 162 | +- When disabled, processes only the first message and exits |
| 163 | +- Pairs with `--no-always-accept-stdin` for programmatic use |
| 164 | + |
| 165 | +### Solution 3: Add `--compact-json` Option |
| 166 | + |
| 167 | +Add a CLI option to control JSON output formatting: |
| 168 | + |
| 169 | +- When enabled, output compact JSON (for machine consumption) |
| 170 | +- When disabled (default), output pretty-printed JSON |
| 171 | +- Affects both stderr (status) and stdout (events) |
| 172 | + |
| 173 | +### Solution 4: Pretty-Print Status Messages by Default |
| 174 | + |
| 175 | +Modify `outputStatus` to respect the JSON formatting setting: |
| 176 | + |
| 177 | +```javascript |
| 178 | +function outputStatus(status, compact = false) { |
| 179 | + const json = compact |
| 180 | + ? JSON.stringify(status) |
| 181 | + : JSON.stringify(status, null, 2); |
| 182 | + console.error(json); |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +### Solution 5: Support Multi-Turn Conversations |
| 187 | + |
| 188 | +Extend the session handling to: |
| 189 | + |
| 190 | +1. Keep the session open after the first response |
| 191 | +2. Queue incoming messages and process them sequentially |
| 192 | +3. Allow the AI to maintain context across messages |
| 193 | +4. Handle concurrent message sending gracefully |
| 194 | + |
| 195 | +## Implementation Priority |
| 196 | + |
| 197 | +1. **High:** Continuous stdin reading with multi-message support |
| 198 | +2. **High:** Add `--always-accept-stdin` option (with sensible default) |
| 199 | +3. **Medium:** Add `--compact-json` option |
| 200 | +4. **Medium:** Pretty-print status messages |
| 201 | +5. **Low:** Update README.md and help text |
| 202 | + |
| 203 | +## References |
| 204 | + |
| 205 | +- Issue: https://github.com/link-assistant/agent/issues/82 |
| 206 | +- Related PR: https://github.com/link-assistant/agent/pull/79 |
| 207 | +- Input Queue Implementation: `src/cli/input-queue.js` |
| 208 | +- JSON Formatting: `src/json-standard/index.ts` |
0 commit comments