Skip to content

Commit 40bc013

Browse files
konardclaude
andcommitted
feat: Enable continuous listening mode by default
- Add --always-accept-stdin flag (default: true) for continuous input mode - Add --compact-json flag for machine-to-machine communication - Pretty-print status messages to stderr by default - Implement continuous stdin reader that processes multiple messages - Keep session alive between messages for multi-turn conversations - Handle SIGINT gracefully for clean shutdown - Extract continuous mode logic to src/cli/continuous-mode.js Fixes #82 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 95faf2f commit 40bc013

7 files changed

Lines changed: 959 additions & 79 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ Your prepared working directory: /tmp/gh-issue-solver-1766275120416
1212

1313
Proceed.
1414

15-
Run timestamp: 2025-12-20T23:58:41.858Z
15+
Run timestamp: 2025-12-20T23:58:41.858Z

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ Stdin Mode Options:
218218
--no-interactive Only accept JSON input
219219
--auto-merge-queued-messages Merge rapidly arriving lines (default: true)
220220
--no-auto-merge-queued-messages Treat each line as separate message
221+
--always-accept-stdin Keep accepting input after agent finishes (default: true)
222+
--no-always-accept-stdin Single-message mode - exit after first response
223+
--compact-json Output compact JSON for program-to-program use
221224

222225
--help Show help
223226
--version Show version number
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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`
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"author": {
3+
"id": "MDQ6VXNlcjE0MzE5MDQ=",
4+
"is_bot": false,
5+
"login": "konard",
6+
"name": "Konstantin Diachenko"
7+
},
8+
"body": "As we did in https://github.com/link-assistant/agent/pull/79, we should now support interactive output from the user, it should be until the agent finishes, or CTRL+C is pressed.\n\nAnd also we need to make sure we support additional messages by default while agent is working.\n\n```\nkonard@MacBook-Pro-Konstantin ~ % agent \nagent [command] [options]\n\nCommands:\n agent auth manage credentials\n\nOptions:\n --version Show version number [boolean]\n --model Model to use in format providerID/modelID\n [string] [default: \"opencode/grok-code\"]\n --json-standard JSON output format standard: \"opencode\"\n (default) or \"claude\" (experimental)\n [string] [choices: \"opencode\", \"claude\"] [default: \"opencode\"]\n --system-message Full override of the system message [string]\n --system-message-file Full override of the system message from\n file [string]\n --append-system-message Append to the default system message[string]\n --append-system-message-file Append to the default system message from\n file [string]\n --server Run in server mode (default)\n [boolean] [default: true]\n --verbose Enable verbose mode to debug API requests\n (shows system prompt, token counts, etc.)\n [boolean] [default: false]\n --dry-run Simulate operations without making actual\n API calls or package installations (useful\n for testing) [boolean] [default: false]\n --use-existing-claude-oauth Use existing Claude OAuth credentials from\n ~/.claude/.credentials.json (from Claude\n Code CLI) [boolean] [default: false]\n -p, --prompt Prompt message to send directly (bypasses\n stdin reading) [string]\n --disable-stdin Disable stdin streaming mode (requires\n --prompt or shows help)\n [boolean] [default: false]\n --stdin-stream-timeout Optional timeout in milliseconds for stdin\n reading (default: no timeout) [number]\n --auto-merge-queued-messages Enable auto-merging of rapidly arriving\n input lines into single messages (default:\n true) [boolean] [default: true]\n --interactive Enable interactive mode to accept manual\n input as plain text strings (default: true).\n Use --no-interactive to only accept JSON\n input. [boolean] [default: true]\n --help Show help [boolean]\n```\n\nThis text is read - it should not be red.\n\nAt the moment we get this behaviour, which may make it it impossible for us to send input by default. As `--interactive` is on by default, it should just work.\n\nAlso we need to add option to `--always-accept-stdin` (to make sure we accept stdin, even after the agent finished work).\n\n\n```\nkonard@MacBook-Pro-Konstantin ~ % echo '{\"message\":\"hi\"}' | agent\n{\"type\":\"status\",\"mode\":\"stdin-stream\",\"message\":\"Agent CLI in stdin listening mode. Accepts JSON and plain text input.\",\"hint\":\"Press CTRL+C to exit. Use --help for options.\",\"acceptedFormats\":[\"JSON object with \\\"message\\\" field\",\"Plain text\"],\"options\":{\"interactive\":true,\"autoMergeQueuedMessages\":true}}\nhi2\n{\n \"type\": \"step_start\",\n \"timestamp\": 1766274627602,\n \"sessionID\": \"ses_4c1d40d0fffeyyI2fwcpmiHwjR\",\n \"part\": {\n \"id\": \"prt_b3e2bf80f001R3DDsJ78f2ASmk\",\n \"sessionID\": \"ses_4c1d40d0fffeyyI2fwcpmiHwjR\",\n \"messageID\": \"msg_b3e2bf31c0013vnM84DKjTwj39\",\n \"type\": \"step-start\"\n }\n}\n{\n \"type\": \"text\",\n \"timestamp\": 1766274628640,\n \"sessionID\": \"ses_4c1d40d0fffeyyI2fwcpmiHwjR\",\n \"part\": {\n \"id\": \"prt_b3e2bfbb1001J6SYOBXpDpEgl2\",\n \"sessionID\": \"ses_4c1d40d0fffeyyI2fwcpmiHwjR\",\n \"messageID\": \"msg_b3e2bf31c0013vnM84DKjTwj39\",\n \"type\": \"text\",\n \"text\": \"Hi! How can I help you today?\",\n \"time\": {\n \"start\": 1766274628639,\n \"end\": 1766274628639\n }\n }\n}\n{\n \"type\": \"step_finish\",\n \"timestamp\": 1766274628644,\n \"sessionID\": \"ses_4c1d40d0fffeyyI2fwcpmiHwjR\",\n \"part\": {\n \"id\": \"prt_b3e2bfc22001rm1tFYGnaquEFj\",\n \"sessionID\": \"ses_4c1d40d0fffeyyI2fwcpmiHwjR\",\n \"messageID\": \"msg_b3e2bf31c0013vnM84DKjTwj39\",\n \"type\": \"step-finish\",\n \"reason\": \"stop\",\n \"cost\": 0,\n \"tokens\": {\n \"input\": 3,\n \"output\": 9,\n \"reasoning\": 123,\n \"cache\": {\n \"read\": 8704,\n \"write\": 0\n }\n }\n }\n}\nkonard@MacBook-Pro-Konstantin ~ % hi2\nzsh: command not found: hi2\n```\n\nAlso looks like we don't really support sending any messages after the first one by default, that must be fixed also.\n\n```\n{\"type\":\"status\",\"mode\":\"stdin-stream\",\"message\":\"Agent CLI in stdin listening mode. Accepts JSON and plain text input.\",\"hint\":\"Press CTRL+C to exit. Use --help for options.\",\"acceptedFormats\":[\"JSON object with \\\"message\\\" field\",\"Plain text\"],\"options\":{\"interactive\":true,\"autoMergeQueuedMessages\":true}}\n```\n\nIs also read, but it should not be.\n\nAlso by default we should pretty print json, and there should be an option to disable pretty printing for easy program to program interaction if we need it. At least it is missing in help, if we have such option we need to make sure it is present in README.md and help, if we don't have it we should add it.\n\nPlease download all logs and data related about the issue to this repository, make sure we compile that data to `./docs/case-studies/issue-{id}` folder, and use it to do deep case study analysis (also make sure to search online for additional facts and data), in which we will reconstruct timeline/sequence of events, find root causes of the problem, and propose possible solutions.",
9+
"comments": [],
10+
"createdAt": "2025-12-20T23:57:59Z",
11+
"labels": [
12+
{
13+
"id": "LA_kwDOQYTy3M8AAAACQHoi-w",
14+
"name": "bug",
15+
"description": "Something isn't working",
16+
"color": "d73a4a"
17+
}
18+
],
19+
"number": 82,
20+
"state": "OPEN",
21+
"title": "Listening mode should be enabled by default",
22+
"updatedAt": "2025-12-20T23:58:04Z"
23+
}

docs/stdin-mode.md

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,47 @@ printf 'Line 1\nLine 2\nLine 3' | agent --no-auto-merge-queued-messages
110110

111111
**Default:** Auto-merge is enabled (`--auto-merge-queued-messages`)
112112

113+
### `--always-accept-stdin / --no-always-accept-stdin`
114+
115+
Control whether the agent keeps listening for input after processing each message.
116+
117+
```bash
118+
# Default: continuous listening mode - keeps accepting input until EOF or Ctrl+C
119+
echo '{"message":"hi"}' | agent
120+
121+
# Single-message mode - exit after first response (legacy behavior)
122+
echo '{"message":"hi"}' | agent --no-always-accept-stdin
123+
```
124+
125+
In continuous mode (default):
126+
127+
- The agent keeps the session alive between messages
128+
- You can send multiple messages and they will be queued
129+
- The agent maintains conversation context across messages
130+
- Press Ctrl+C or send EOF to exit
131+
132+
**Default:** Always accept stdin is enabled (`--always-accept-stdin`)
133+
134+
### `--compact-json`
135+
136+
Output compact JSON (single line) instead of pretty-printed JSON.
137+
138+
```bash
139+
# Default: pretty-printed JSON output
140+
echo "hi" | agent
141+
142+
# Compact JSON for program-to-program communication
143+
echo "hi" | agent --compact-json
144+
```
145+
146+
Compact mode is useful for:
147+
148+
- Parsing output with tools like `jq`
149+
- Reducing bandwidth in automated pipelines
150+
- Machine-to-machine communication
151+
152+
**Default:** Pretty-printed JSON (compact-json is disabled)
153+
113154
## Input Formats
114155

115156
### Plain Text
@@ -146,36 +187,46 @@ Becomes a single message: "First line\nSecond line\nThird line"
146187

147188
## Status Messages
148189

149-
When entering stdin listening mode, the CLI outputs a JSON status message:
190+
When entering stdin listening mode, the CLI outputs a JSON status message (now pretty-printed by default):
150191

151192
```json
152193
{
153194
"type": "status",
154195
"mode": "stdin-stream",
155-
"message": "Agent CLI in stdin listening mode. Accepts JSON and plain text input.",
196+
"message": "Agent CLI in continuous listening mode. Accepts JSON and plain text input.",
156197
"hint": "Press CTRL+C to exit. Use --help for options.",
157198
"acceptedFormats": ["JSON object with \"message\" field", "Plain text"],
158199
"options": {
159200
"interactive": true,
160-
"autoMergeQueuedMessages": true
201+
"autoMergeQueuedMessages": true,
202+
"alwaysAcceptStdin": true,
203+
"compactJson": false
161204
}
162205
}
163206
```
164207

165208
This helps programmatic consumers understand the CLI's current mode.
166209

210+
With `--compact-json`, status messages are output as single-line JSON:
211+
212+
```text
213+
{"type":"status","mode":"stdin-stream","message":"Agent CLI in continuous listening mode. Accepts JSON and plain text input.","hint":"Press CTRL+C to exit. Use --help for options.","acceptedFormats":["JSON object with \"message\" field","Plain text"],"options":{"interactive":true,"autoMergeQueuedMessages":true,"alwaysAcceptStdin":true,"compactJson":true}}
214+
```
215+
167216
## Behavior Matrix
168217

169-
| Scenario | Behavior |
170-
| ---------------------------------------- | ------------------------------------ |
171-
| `agent` in terminal (TTY) | Shows help, exits |
172-
| `echo "hi" \| agent` | Processes plain text |
173-
| `echo '{"message":"hi"}' \| agent` | Processes JSON |
174-
| `agent -p "hello"` | Processes prompt directly |
175-
| `agent --disable-stdin` | Shows error, suggests -p |
176-
| `agent --no-interactive` with plain text | Rejects, shows error |
177-
| `agent --no-interactive` with JSON | Processes normally |
178-
| `agent` with stdin open (non-TTY) | Outputs status JSON, waits for input |
218+
| Scenario | Behavior |
219+
| --------------------------------------------- | --------------------------------------------- |
220+
| `agent` in terminal (TTY) | Shows help, exits |
221+
| `echo "hi" \| agent` | Continuous mode - waits for more input |
222+
| `echo '{"message":"hi"}' \| agent` | Continuous mode - waits for more input |
223+
| `agent -p "hello"` | Processes prompt directly, exits |
224+
| `agent --disable-stdin` | Shows error, suggests -p |
225+
| `agent --no-interactive` with plain text | Rejects, shows error |
226+
| `agent --no-interactive` with JSON | Processes normally |
227+
| `agent` with stdin open (non-TTY) | Outputs status JSON, waits for input |
228+
| `echo "hi" \| agent --no-always-accept-stdin` | Single-message mode - exits after response |
229+
| `agent --compact-json` | All JSON output is single-line (NDJSON style) |
179230

180231
## Examples
181232

0 commit comments

Comments
 (0)