{"record":{"id":"606616e625834b4e","repo":"iOfficeAI/OfficeCLI","slug":"protocol-mismatch-606616","errorCode":"protocol_mismatch","errorMessage":"Format-handler plugin '{_plugin.Manifest.Name}' wrote non-JSON to stdout (a JSONL envelope was expected): {ex.Message}. First chars: \"{Truncate(line, 80)}\". This is a plugin bug — diagnostic output must go to stderr or --log-file, not stdout.","messagePattern":"Format-handler plugin '(.+?)' wrote non-JSON to stdout \\(a JSONL envelope was expected\\): (.+?)\\. First chars: \"(.+?)\"\\. This is a plugin bug — diagnostic output must go to stderr or --log-file, not stdout\\.","errorType":"exception","errorClass":"CliException","httpStatus":null,"severity":"error","filePath":"src/officecli/Core/Plugins/FormatHandlerSession.cs","lineNumber":235,"sourceCode":"\n                // Any protocol-shape failure poisons the session: §6.7 lists\n                // \"malformed reply\" as a broken-state trigger, so we mark\n                // _broken before throwing so the next Send fast-fails instead\n                // of trying to write into a session whose protocol invariants\n                // are gone. We also catch JsonException explicitly: the raw\n                // System.Text.Json message (\"'d' is an invalid start of a\n                // value...\") is opaque to users — wrap it in a clear\n                // `protocol_mismatch` envelope that names the plugin and\n                // shows a preview of what it actually wrote.\n                JsonObject? reply;\n                try\n                {\n                    reply = JsonNode.Parse(line)?.AsObject();\n                }\n                catch (JsonException ex)\n                {\n                    _broken = true;\n                    throw new CliException(\n                        $\"Format-handler plugin '{_plugin.Manifest.Name}' wrote non-JSON to stdout (a JSONL envelope was expected): {ex.Message}. \" +\n                        $\"First chars: \\\"{Truncate(line, 80)}\\\". This is a plugin bug — diagnostic output must go to stderr or --log-file, not stdout.\")\n                    { Code = \"protocol_mismatch\" };\n                }\n\n                if (reply is null)\n                {\n                    _broken = true;\n                    throw new CliException(\n                        $\"Format-handler plugin '{_plugin.Manifest.Name}' reply is not a JSON object. First chars: \\\"{Truncate(line, 80)}\\\".\")\n                    { Code = \"protocol_mismatch\" };\n                }\n\n                var replyType = reply[\"msg_type\"]?.GetValue<string>() ?? \"\";\n                if (replyType == \"ok\")\n                    return reply[\"result\"];\n                if (replyType == \"error\")\n                {","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/iOfficeAI/OfficeCLI/blob/1ced45e900782c5083ed550ddf328ee974e425e7/src/officecli/Core/Plugins/FormatHandlerSession.cs#L217-L253","documentation":"Thrown when the plugin's stdout reply line is not valid JSON — System.Text.Json's JsonNode.Parse throws a JsonException. The raw parser message (e.g. \"'d' is an invalid start of a value\") is opaque, so this wrapper names the plugin, shows the first 80 characters of what was actually written, and explains that stdout is a JSONL-only channel. The session is marked _broken because a protocol-shape failure poisons the framing contract.","triggerScenarios":"FormatHandlerSession.SendRaw reads a line that starts with a non-JSON character or contains malformed JSON. JsonNode.Parse(line) throws inside the try block at line 228-231. Typical cause: the plugin printed a stack trace, a progress message, a banner, or a bare string to stdout.","commonSituations":"Plugin uses a logging library (Python logging, log4j, serde) configured with a StreamHandler/ConsoleAppender on stdout by default. Plugin prints a version banner or 'Starting...' line on startup. Plugin's framework (e.g. argparse, click) echoes to stdout instead of stderr. A crash traceback lands on stdout before the reply.","solutions":["Inspect the 'First chars' preview in the error message to identify exactly what was written to stdout.","Redirect the plugin's logging framework to stderr: Python `logging.basicConfig(stream=sys.stderr)`, Node `console.error(...)`, etc.","Add a `--log-file` flag to the plugin so verbose output goes to a file, not stdout/stderr.","Test the plugin in isolation: pipe its stdout through `jq .` to confirm every line is a valid JSON object."],"exampleFix":"// before: Python plugin logs to stdout by default\nimport logging\nlogging.basicConfig(level=logging.DEBUG)  # defaults to stderr, but...\n# ...or worse:\nprint(f\"Processing slide {n}\")  # goes to stdout, breaks framing\n\n// after\nimport logging, sys\nlogging.basicConfig(level=logging.DEBUG, stream=sys.stderr)\nsys.stderr.write(f\"Processing slide {n}\\n\")","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try\n{\n    var result = session.Send(msgType, command, args, props);\n}\ncatch (CliException ex) when (ex.Code == \"protocol_mismatch\")\n{\n    // The 'First chars' preview in ex.Message shows what the plugin wrote to stdout.\n    // The session is broken — respawn it after fixing the plugin's output routing.\n    session.Dispose();\n    throw;  // or respawn + retry after plugin fix\n}","preventionTips":["Configure plugin logging to stderr from the start: Python logging.basicConfig(stream=sys.stderr).","Run plugin stdout through `jq .` in a test harness to verify every line is a JSON object.","Never use print()/printf/console.log for diagnostics in a plugin — use stderr equivalents.","Add a `--log-file` option to the plugin for verbose output."],"tags":["plugin","protocol","json","stdout","jsonl"],"backgroundTag":null,"analyzedSha":"1ced45e900782c5083ed550ddf328ee974e425e7","analyzedAt":"2026-08-13T13:01:07.193Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}