Fix string errors in `.ib.broker` relay

Normalize structured and raw IB errors without indexing strings as
message dictionaries.

Deats,
- match each error form before request-id handling;
- parse `[code N]` and `[Errno N]` prefixes without crashing;
- cover raw errors through `deliver_trade_events()`.

Prompt-IO: ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
wkt/fsp_backfill_sync
Gud Boi 2026-08-28 02:52:12 -04:00
parent 1606122b45
commit 9115d24da0
4 changed files with 196 additions and 24 deletions

View File

@ -0,0 +1,35 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: pid-469264
timestamp: 2026-08-28T06:31:26Z
git_ref: 1606122b
scope: code
substantive: true
raw_file: 20260828T063126Z_1606122b_prompt_io.raw.md
---
## Prompt
Review the staged IB broker patch before committing it. After the review
identified a deterministic error-parser crash, continue with the available
next steps while leaving the typed EMS worktree and Ruff change untouched.
## Response summary
Repaired bracketed IB API error parsing by splitting on the matched error
prefix. Added a deterministic regression that routes bracketed and plain
strings through the real trade-event relay and verifies their EMS error
messages.
## Files changed
- `piker/brokers/ib/broker.py` - safely extracts bracketed API error codes.
- `tests/test_ib_broker.py` - covers bracketed and plain error relay inputs.
## Human edits
The human required review before the root commit, accepted the identified
parser defect as the next work item, and explicitly authorized continuation.
The pre-existing staged IB refactor is human-authored; the agent applied only
the parser repair and focused regression described above.

View File

@ -0,0 +1,30 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-28T06:31:26Z
git_ref: fix_symcache_search
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The human asked for a review of the root checkout's staged IB patch before
committing it. The review found that bracketed API error strings call
`str.partition()` without its required separator. The human then instructed
the agent to continue with available next steps, or ask only if unsure.
## Response
> `git diff HEAD~1..HEAD -- piker/brokers/ib/broker.py`
The generated parser repair uses the recognized `[code ` or `[Errno ` prefix
to extract the numeric code without changing the surrounding event relay.
> `git diff HEAD~1..HEAD -- tests/test_ib_broker.py`
The generated regression drives bracketed and plain strings through
`deliver_trade_events()` and verifies that one `BrokerdError` reaches EMS
with the expected reason and unknown request ID.
The typed EMS worktree and the unrelated Ruff policy change were left
untouched. No commit was created.

View File

@ -52,7 +52,6 @@ from ib_async.objects import (
from piker import config
from piker.log import (
get_logger,
get_console_log,
)
from piker.types import Struct
from piker.accounting import (
@ -1429,30 +1428,38 @@ async def deliver_trade_events(
# NOTE: see impl deats in
# `Client.inline_errors()::push_err()`
err: dict|str = item
is_msg: bool = False
# std case, never relay errors for non-order-control
# related issues.
# https://interactivebrokers.github.io/tws-api/message_codes.html
if isinstance(err, dict):
code: int = err['error_code']
reason: str = err['reason']
reqid: str = str(err['reqid'])
match err:
case dict():
# if isinstance(err, dict):
code: int = err['error_code']
reason: str = err['reason']
reqid: str = str(err['reqid'])
is_msg = True
# XXX, sometimes you'll get just a `str` of the form,
# '[code 104] connection failed' or something..
elif isinstance(err, str):
code_part, _, reason = err.rpartition(']')
if code_part:
for prefix_patt in [
'[Errno ',
'[code ',
]:
code_part, _, code = code_part.partition()
if code:
code = int(code)
break
case str():
# XXX, sometimes you'll get just a `str` of the form,
# '[code 104] connection failed' or something..
# elif isinstance(err, str):
code: int|str = '<NA-err-code>'
code_part, _, reason = err.rpartition(']')
if code_part:
for prefix_patt in [
'[Errno ',
'[code ',
]:
_, prefix, code_str = (
code_part.partition(prefix_patt)
)
if prefix:
code = int(code_str)
break
reqid: str = '<unknown>'
reqid: str = '<unknown>'
# "Warning:" msg codes,
# https://interactivebrokers.github.io/tws-api/message_codes.html#warning_codes
@ -1497,16 +1504,17 @@ async def deliver_trade_events(
)
continue
if err['reqid'] == -1:
if (
is_msg
and
err['reqid'] == -1
):
log.error(
f'TWS external order error ??\n'
f'{pformat(err)}\n'
)
flow: dict = dict(
flows.get(reqid)
or {}
)
flow: dict = dict(flows.get(reqid) or {})
# TODO: we don't want to relay data feed / lookup errors
# so we need some further filtering logic here..

View File

@ -0,0 +1,99 @@
'''
IB broker event-relay regressions.
'''
from collections.abc import AsyncIterator
from typing import Any
import pytest
import trio
from piker.brokers.ib.broker import deliver_trade_events
from piker.clearing import OrderDialogs
from piker.clearing._messages import BrokerdError
@pytest.mark.parametrize(
('error', 'expected_reason'),
[
(
'[code 104] connection failed',
' connection failed',
),
(
'[Errno 111] Connect call failed',
' Connect call failed',
),
(
'plain API error',
'plain API error',
),
],
)
def test_trade_error_strings_are_relayed(
error: str,
expected_reason: str,
) -> None:
'''
Raw IB API error strings must not kill the trade-event relay.
``Client.inline_errors()`` can emit bracketed ``[code N]`` and
``[Errno N]`` strings instead of structured error dictionaries.
The string branch called ``str.partition()`` without a separator,
raising ``TypeError`` before EMS received the error and
terminating all later trade-event delivery. Feed both documented
bracket forms and an unprefixed control through
``deliver_trade_events()``. The exact emitted reason and unknown
request ID prove each event crossed the real relay path without
crashing or being misclassified.
'''
class FakeEmsStream:
'''
Collect messages sent to the EMS stream.
'''
def __init__(self) -> None:
'''
Initialize the captured message sequence.
'''
self.messages: list[Any] = []
async def send(self, msg: Any) -> None:
'''
Capture one relayed broker message.
'''
self.messages.append(msg)
async def events() -> AsyncIterator[tuple[str, str]]:
'''
Emit the raw IB error under test.
'''
yield 'error', error
async def main() -> list[Any]:
'''
Drive the broker relay to input exhaustion.
'''
ems_stream = FakeEmsStream()
await deliver_trade_events(
events(),
ems_stream,
accounts_def={},
proxies={},
ledgers={},
tables={},
flows=OrderDialogs(_flows={}),
)
return ems_stream.messages
messages = trio.run(main)
assert len(messages) == 1
msg = messages[0]
assert isinstance(msg, BrokerdError)
assert msg.reqid == '<unknown>'
assert msg.reason == expected_reason