While developing PPT App, we found that the local anna-app dev harness still behaves differently from the published size contract for synchronous tools.invoke results.
Protocol frames larger than 64 KiB cause serious failures: large requests terminate the Python bridge process, preventing even subsequent small requests from running; large responses crash the plugin response reader task and incorrectly report executa process exited. Neither case returns a size-limit error that the application can handle.
Using an isolated minimal plugin, we reproduced these failures in the runtimes pinned by both our project’s current CLI and the latest CLI available as of September 23, 2026. We traced them to two asyncio readers using the default 64 KiB limit. We would appreciate confirmation and alignment of the platform and local harness transport contracts, along with fixes for the process and task lifecycle failures caused by oversized frames.
Background: the published synchronous result limit is approximately 4 MiB
We previously reported the platform’s silent truncation of tools.invoke results in this thread:
The official reply confirmed that the truncation issue had been fixed and explicitly stated a 4 MiB limit for the full synchronous tools.invoke result, with result_too_large returned when the limit is exceeded.
The current “Result size and integrity” section of the App UI Host API documentation further states:
~4 MiB for the full serialized result (payload + envelopes), bounded by the NATS transport
max_payload(4 194 304 bytes)
The same section also explicitly states:
There is no intermediate per-string-field limit — payloads such as a 100 KB HTML document or a 200 KB base64 data URL pass through unchanged as long as the whole result fits the channel cap.
Our understanding is that a synchronous invoke result, including its protocol envelopes, should be returned intact when it fits within the approximately 4 MiB channel budget. Exceeding that budget should produce an explicit, machine-readable error. The local development environment should support the same contract; otherwise, local testing cannot validate data transfers that the platform permits.
Several distinct limits need to be kept separate:
| Scope | Limit in the current official documentation |
|---|---|
Full synchronous tools.invoke result, including envelopes |
Approximately 4 MiB |
tools.invokeAsync result |
256 KB |
tools.invokeAsync arguments |
64 KB |
| A single APS KV value | 64 KB by default / advisory; the hard cap depends on environment configuration |
The last two entries are documented in the tools.* detailed reference and the APS documentation, respectively. They should not be interpreted as a universal 64 KB limit on synchronous invoke results.
We are not inferring that synchronous request arguments support 4 MiB merely because synchronous results do. We would also appreciate a separate, explicit size contract for synchronous request arguments. Whatever that request limit is, exceeding it should not terminate the entire bridge.
Test environment and methodology
- Operating system: macOS.
- Node.js: v24.19.0.
- Python: 3.10.15.
- Test date: September 23, 2026.
| CLI version | Pinned anna-app-runtime-local |
anna-app-core in the test environment |
|---|---|---|
| 0.1.47, currently used by our project | 0.2.0a21 | 0.18.1 |
| 0.1.53, the latest npm release when checked | 0.2.0a23 | 0.20.0 |
Both version combinations reproduced the issues below.
To isolate the failures from PPT application logic, we used a standalone plugin that only exchanges valid JSON-RPC and returns synthetic ASCII content. We tested:
- The published package’s
ExecutaPool, inspecting the response reader task, child process exit status, and plugin PID on subsequent calls. - The published package’s native Python bridge, using
executas.register→session.create→session.call(ns="tools", method="invoke")to exercise the actual dispatcher and error mapping. PythonBridgefrom both published CLI versions, verifying the Node-side errors and subsequent call behavior after the bridge exits.
These tests used the components the harness actually depends on, without PPT rendering, network uploads, or APS. We did not test the remote platform’s actual 4 MiB boundary in this investigation. This report concerns independently reproducible local transport failures and their inconsistency with the published contract.
Issue 1: large requests terminate the Python bridge and break subsequent calls
When a complete JSON-RPC request line sent from Node to the Python bridge exceeds 65,536 bytes, the bridge exits with code 1 instead of returning a structured error.
All sizes below are the UTF-8 byte length of the complete JSON-RPC line, excluding the trailing LF newline:
| Request frame size | Observed result |
|---|---|
| 65,535 bytes | Succeeds; subsequent ping and small invoke calls work |
| 65,536 bytes | Succeeds; subsequent ping and small invoke calls work |
| 65,537 bytes | No JSON-RPC response; bridge exits with code 1 |
| 102,400 bytes | Also terminates the bridge |
The actual CLI-side error is:
python bridge exited (code=1 signal=-)
Even a very small subsequent request then fails with:
python bridge not running
The failure affects more than the current tool call: once the shared bridge is unavailable, subsequent calls cannot proceed either. We observed no automatic recovery in these tests.
The boundary applies to the entire protocol frame, not just the application string. In our minimal request, a 65,536-byte frame contained only 65,331 bytes of content; the remainder consisted of session, method, argument, and JSON envelope fields. Application content can therefore hit this boundary before reaching 64 KiB itself.
Identified cause
The non-Windows branch of _iter_stdin_lines() in anna_app_runtime_local/bridge.py uses:
reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(reader)
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
while True:
line = await reader.readline()
No explicit limit is set, so asyncio’s default 65,536-byte limit applies. An oversized line triggers LimitOverrunError, which readline() then converts into:
ValueError: Separator is found, but chunk is longer than limit
The exception propagates out of the stdin reader loop without being converted into a protocol error, ultimately terminating the bridge. The Node side marks the bridge as closed and continues rejecting calls.
We tested this on macOS. Linux uses the same source branch but has not been tested here. Windows has a different stdin implementation, so these findings do not establish that the request-side failure is identical on Windows.
Issue 2: large responses crash the reader task, falsely report plugin exit, and leave the old process running
A different failure occurs when a complete JSON-RPC response line sent from Executa to the Python bridge exceeds the same boundary:
| Response frame size, excluding trailing LF | Observed result |
|---|---|
| 64,000 / 65,535 / 65,536 bytes | Returned intact; subsequent calls reuse the same plugin PID |
| 65,537 bytes | tool_failed / executa process exited |
| 100 KiB / 200 KiB / 1 MiB / 4 MiB | Same failure |
The 100 KiB, 200 KiB, and 1 MiB responses are clearly below the documented approximately 4 MiB channel budget, yet already fail in the local harness.
The actual bridge and dispatcher return:
{
"ok": false,
"error": {
"code": "tool_failed",
"message": "executa process exited",
"details": {}
}
}
Further inspection showed that the error message does not accurately describe the process state:
- The plugin stdout reader task has terminated with
ValueError. - The harness has set the corresponding
_ExecutaProcess.closedtotrue. - The original plugin process is still alive, with
proc.returncodeequal toNone. - The bridge itself still responds to ping.
- The next small invoke succeeds, but starts a new plugin PID; the old plugin process remains alive and has not been reaped.
This should therefore not be attributed simply to the tool crashing. The harness’s response reader task fails, incorrectly reports a process exit, and replaces the process record on the next call.
Identified cause
In anna_app_runtime_local/executa.py, the call to asyncio.create_subprocess_exec() does not set an explicit limit for the stdout/stderr readers. _reader_loop() then calls:
line = await ep.proc.stdout.readline()
An oversized line terminates the reader task with an exception. Its finally block runs the following code for both normal EOF and reader exceptions:
ep.closed = True
for fut in ep.pending.values():
if not fut.done():
fut.set_exception(_PluginError("executa process exited"))
ep.pending.clear()
This hides the actual line-length error and returns neither result_too_large nor information about the actual size. Based on this code, other calls waiting for responses from the same plugin may also fail together; we have not separately conducted a concurrency stress test.
On the next call, the harness sees closed, starts a new process, and overwrites the pool entry without first terminating and reaping the old process. We confirmed that the old process remained alive in the minimal plugin experiment.
The relevant bridge.py and executa.py files are each identical across the two runtime versions, which also explains why upgrading to CLI 0.1.53 still reproduces the failures.
Controlled experiment: increasing the reader limit restores moderately sized responses
In an isolated diagnostic process, we temporarily set only the subprocess reader limit to limit=8*1024*1024, without modifying installed dependencies. Calling the same minimal plugin again produced the following results:
- Response frames of 65,537 bytes, 100 KiB, 200 KiB, 1 MiB, and 4 MiB were all read intact.
- The response reader task remained operational.
- Subsequent small calls continued reusing the same plugin PID.
This further confirms that the 64 KiB failure comes from the harness’s default reader limit.
Increasing the buffer was a diagnostic experiment, not a complete fix. A production implementation still needs to distinguish the underlying readable protocol frame size from each channel’s actual budget, account for envelope overhead, and return explicit errors when limits are exceeded. The request and response readers also need to be fixed separately.
Impact on application development and responsiveness
PPT App transfers page HTML, preview data, and structured content through tool calls. To accommodate the current local environment, we use a 48 KiB inline budget and have implemented compression, upload references, and chunked reads for some text transfers.
These measures work around some failures, but add compression, upload, download URL retrieval, download, and repeated-call overhead. For page previews, page switching, and editing feedback, repeated transport round trips directly affect the user experience and increase implementation and debugging costs.
If approximately 4 MiB synchronous results were supported consistently by both the platform and the local harness, many moderately sized payloads could be transferred in a single invoke, potentially making the app substantially more responsive. Content that needs durable storage, or files that actually exceed the channel limit, would still be appropriate for APS or Host Upload.
The more serious issue today is the failure behavior: developers receive bridge-exit or tool-process-exit errors instead of a size-limit error they can handle. Applications cannot easily distinguish tool failures from transport failures, or reliably switch to reference-based transfer and continue after the failure.
Expected behavior and questions for the platform
- Confirm a consistent synchronous result contract. Is the documented approximately 4 MiB limit also the intended target for the local harness? Please clarify the serialization method, which envelopes count toward the limit, and the recommended usable payload budget.
- Document synchronous request argument limits separately. Please distinguish synchronous requests, synchronous results, asynchronous requests, and asynchronous results, with exact byte limits and the corresponding errors.
- Fix both local transport directions. Please review Node → bridge stdin, Executa → bridge stdout, and reverse-RPC frames passing through these readers, so that neither reader remains a hidden bottleneck.
- Fail explicitly on oversized data while keeping the runtime usable. Return machine-readable errors with the actual size and allowed maximum. Do not terminate the entire bridge or misreport reader exceptions as plugin exits. Valid small requests should continue working after a failed call.
- Fix subprocess lifecycle handling. When a reader task fails, handle pending calls appropriately, preserve the actual cause, and correctly reap or recover the associated process rather than leaving the old process running.
- Add boundary regressions and identify the fixed versions. Cover 65,535 / 65,536 / 65,537 bytes, 100/200 KiB, values near and above 4 MiB, Chinese text and JSON escaping, Base64, and recovery after failure. Publish the runtime fix and update the CLI’s pinned runtime version.
Thank you for the earlier fix to the 32,000-character truncation issue. We hope this follow-up can bring the local harness and platform into alignment on transport capacity, size limits, and failure behavior, so developers can safely use synchronous results larger than 64 KiB according to the documentation, without discovering hidden limits through failures that disrupt local development.