Summary
Uploading a file fails with:
Malformed OFISH capability probe output: unsupported status line
The OFISH capability probe runs, but OfishCapabilityParser rejects the response. This happens against a self-hosted OpenCode 1.18.13 server using a custom OpenAI-compatible provider (9router → gemini flash medium) with the default build agent.
Expected
The probe returns the caps ... line followed by ### 200 ok (or ### 501 caps_missing ...), capabilities are cached, and file upload proceeds.
Actual
Upload is blocked. The error originates from OfishCapabilityParser.parse in app/src/main/java/dev/blazelight/p4oc/data/files/ofish/OfishCapabilityParser.kt, the else branch:
else -> OfishProbeResult.Failed("Malformed OFISH capability probe output: unsupported status line")
This means a caps line and a ### status line were both found and ordered correctly, but the final status line was neither ### 200 ok nor ### 501 caps_missing ....
Root-cause hypothesis
OfishShellOutputExtractor.extract (OfishCapabilityProbe.kt) concatenates all message parts into one blob:
message.parts.forEach { part ->
part.text?.takeIf { it.isNotBlank() }?.let { appendLine(it) }
part.state?.output?.takeIf { it.isNotBlank() }?.let { appendLine(it) }
part.state?.raw?.takeIf { it.isNotBlank() }?.let { appendLine(it) }
part.state?.error?.takeIf { it.isNotBlank() }?.let { appendLine(it) }
}
Then OfishCapabilityParser picks the status line with indexOfLast { it.startsWith("### ") }. On servers where the model appends assistant text after running the shell command — and LLMs routinely emit markdown level-3 headings like ### Note / ### Explanation — the last ### line is the model's prose, not the shell trailer, so parsing falls into the else branch.
Note the probe already has a clean precedent for this: extractMutationSegment deliberately treats shell tool state as authoritative and only falls back to text parts when no state contains the expected marker. The capability probe does not follow that pattern.
Suggested fix
- In
OfishCapabilityProbe.probe(), prioritize state.output / state.raw (and ideally only the shell tool's state) over part.text — mirroring extractMutationSegment — so model prose can never shadow the shell trailer.
- Optionally make the parser more robust by matching only real FISH-style trailer lines, e.g.
Regex("^### \\d{3}"), instead of any line starting with ### , and/or validate that the status line is the last non-empty line of the output.
Environment
- P4OC latest main (at time of report)
- Server: OpenCode 1.18.13 (
opencode serve), custom OpenAI-compatible provider (9router), agent build, model gemini flash medium
- Android app performing upload via SAF
Additional context
This may also be the same class of issue as the "malformed output" fragility in OfishMutationParser when non-shell prose gets interleaved — worth hardening both parsers against trailing model narrative.
Summary
Uploading a file fails with:
The OFISH capability probe runs, but
OfishCapabilityParserrejects the response. This happens against a self-hosted OpenCode 1.18.13 server using a custom OpenAI-compatible provider (9router → gemini flash medium) with the defaultbuildagent.Expected
The probe returns the
caps ...line followed by### 200 ok(or### 501 caps_missing ...), capabilities are cached, and file upload proceeds.Actual
Upload is blocked. The error originates from
OfishCapabilityParser.parseinapp/src/main/java/dev/blazelight/p4oc/data/files/ofish/OfishCapabilityParser.kt, theelsebranch:This means a
capsline and a###status line were both found and ordered correctly, but the final status line was neither### 200 oknor### 501 caps_missing ....Root-cause hypothesis
OfishShellOutputExtractor.extract(OfishCapabilityProbe.kt) concatenates all message parts into one blob:message.parts.forEach { part -> part.text?.takeIf { it.isNotBlank() }?.let { appendLine(it) } part.state?.output?.takeIf { it.isNotBlank() }?.let { appendLine(it) } part.state?.raw?.takeIf { it.isNotBlank() }?.let { appendLine(it) } part.state?.error?.takeIf { it.isNotBlank() }?.let { appendLine(it) } }Then
OfishCapabilityParserpicks the status line withindexOfLast { it.startsWith("### ") }. On servers where the model appends assistant text after running the shell command — and LLMs routinely emit markdown level-3 headings like### Note/### Explanation— the last###line is the model's prose, not the shell trailer, so parsing falls into theelsebranch.Note the probe already has a clean precedent for this:
extractMutationSegmentdeliberately treats shell tool state as authoritative and only falls back to text parts when no state contains the expected marker. The capability probe does not follow that pattern.Suggested fix
OfishCapabilityProbe.probe(), prioritizestate.output/state.raw(and ideally only the shell tool's state) overpart.text— mirroringextractMutationSegment— so model prose can never shadow the shell trailer.Regex("^### \\d{3}"), instead of any line starting with###, and/or validate that the status line is the last non-empty line of the output.Environment
opencode serve), custom OpenAI-compatible provider (9router), agentbuild, modelgemini flash mediumAdditional context
This may also be the same class of issue as the "malformed output" fragility in
OfishMutationParserwhen non-shell prose gets interleaved — worth hardening both parsers against trailing model narrative.