{"record":{"id":"47cdccadcb8ec4e3","repo":"sipeed/picoclaw","slug":"process-hook-q-closed-while-waiting-for-s","errorCode":null,"errorMessage":"process hook %q closed while waiting for %s","messagePattern":"process hook %q closed while waiting for (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/agent/hook_process.go","lineNumber":358,"sourceCode":"\t}\n\tif params != nil {\n\t\tbody, err := json.Marshal(params)\n\t\tif err != nil {\n\t\t\tph.removePending(id)\n\t\t\treturn err\n\t\t}\n\t\tmsg.Params = body\n\t}\n\n\tif err := ph.send(ctx, msg); err != nil {\n\t\tph.removePending(id)\n\t\treturn err\n\t}\n\n\tselect {\n\tcase resp, ok := <-respCh:\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"process hook %q closed while waiting for %s\", ph.name, method)\n\t\t}\n\t\tif resp.Error != nil {\n\t\t\treturn fmt.Errorf(\"process hook %q %s failed: %s\", ph.name, method, resp.Error.Message)\n\t\t}\n\t\tif out != nil && len(resp.Result) > 0 {\n\t\t\tif err := json.Unmarshal(resp.Result, out); err != nil {\n\t\t\t\treturn fmt.Errorf(\"decode process hook %q %s result: %w\", ph.name, method, err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase <-ctx.Done():\n\t\tph.removePending(id)\n\t\treturn ctx.Err()\n\t}\n}\n\nfunc (ph *ProcessHook) send(ctx context.Context, msg processHookRPCMessage) error {\n\tbody, err := json.Marshal(msg)","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/agent/hook_process.go#L340-L376","documentation":"Returned by ProcessHook.call when the pending-response channel was closed while a request was outstanding: the hook's read loop exited (subprocess died, stdout closed, or Close() called) and cleaned up pending calls before the response for `method` arrived. The request was sent but can never be answered by this hook instance.","triggerScenarios":"The hook process crashes, exits, or is closed between send() of the JSON-RPC request and delivery of the response — e.g. the hook binary hits a panic, exits after handling a previous method, or the runtime is shutting down mid-RPC.","commonSituations":"Hook script that calls sys.exit/os.Exit after one request; hook crashing on a specific method's payload; hook killed by the isolation layer or an external supervisor; parent agent shutting down during a tool-approval call.","solutions":["Check the hook process's stderr/logs to find why it exited (panic, exit code, unhandled input)","Ensure the hook stays alive across requests: a stdio JSON-RPC hook must loop reading newline-delimited messages, never exit after one","Guard against malformed payloads that crash the hook (validate params server-side in the hook)","If shutdown-triggered, cancel the calling context before closing hooks so RPCs stop gracefully"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"func isHookClosedWhileWaiting(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"closed while waiting for\")\n}","tryCatchPattern":"err := ph.CallApproveTool(ctx, payload)\nif err != nil {\n    if isHookClosedWhileWaiting(err) {\n        // process died mid-RPC: recreate hook and retry once\n        ph2, mkErr := NewProcessHook(ctx, ph.Name(), ph.Options())\n        if mkErr != nil { return err }\n        defer ph2.Close()\n        return ph2.CallApproveTool(ctx, payload)\n    }\n    return err\n}","preventionTips":["Keep hook processes alive across requests (loop on stdin, never exit after one message)","Capture hook stderr so crashes are diagnosable","Retry only after recreating the hook — the old instance can never answer"],"tags":["hooks","rpc","process-crash","lifecycle","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}