Merge pull request #1738 from entireio/fix/1398-hook-stdin-hang-windows · Entire
Merge pull request #1738 from entireio/fix/1398-hook-stdin-hang-windows
aacb4d6→main
Fix agent hooks hanging on stdin EOF on Windows/Git Bash (#1398)
Changes
- cmd/entire/cli
- agent
- copilotcli
- Mlifecycle.go+5/-3
- Mevent.go+61/-8
- Aevent_test.go+127
- external
- Mexternal.go+7/-2
- pi
- Mlifecycle.go+5/-11
- copilotcli
- Mhooks.go+6/-18
- agent
168 unmodified lines
// Stream one JSON value rather than io.ReadAll so the hook never blocks
// waiting for stdin EOF that some agents don't send on Windows (issue #1398).
func (c *CopilotCLIAgent) readHookEnvelope(stdin io.Reader) (*hookEnvelope, error) {
data, err := io.ReadAll(stdin)
if err != nil {
return nil, fmt.Errorf("failed to read hook input: %w", err)
}
return parseHookEnvelope(data)
}
func ReadAndParseHookInput[T any](stdin io.Reader) (*T, error) {
data, err := io.ReadAll(stdin)
if err != nil {
return nil, fmt.Errorf("failed to read hook input: %w", err)
}
if len(data) == 0 {
return nil, errors.New("empty hook input")
}
var result T
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("failed to parse hook input: %w", err)
}
return &result, nil
}
Test cases
TestReadAndParseHookInput_ReturnsBeforeEOF
func TestReadAndParseHookInput_ReturnsBeforeEOF(t *testing.T) {
t.Parallel()
pr, pw := io.Pipe()
go func() {
if _, err := pw.Write([]byte(`{"session_id":"s1","transcript_path":"/t.jsonl"}`)); err != nil {
_ = pw.CloseWithError(err)
}
}()
// ... rest of the test code
}
TestReadHookInputRawLimited_ReturnsBeforeEOF
func TestReadHookInputRawLimited_ReturnsBeforeEOF(t *testing.T) {
// ... setup and execution
}
TestReadAndParseHookInput_MalformedJSON
func TestReadAndParseHookInput_MalformedJSON(t *testing.T) {
_, err := ReadAndParseHookInput[hookInput](strings.NewReader(`{"session_id": INVALID}`))
if err == nil {
... handle error
}
}
Additional Functions
func parseSubagentCheckpointHookInput(r io.Reader) (*SubagentCheckpointHookInput, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("failed to read input: %w", err)
}
if len(data) == 0 {
return nil, errors.New("empty input")
}
var input SubagentCheckpointHookInput
if err := json.Unmarshal(data, &input); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return &input, nil
}