diff --git a/ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.md b/ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.md new file mode 100644 index 00000000..3f0d86a6 --- /dev/null +++ b/ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.md @@ -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. diff --git a/ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.raw.md b/ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.raw.md new file mode 100644 index 00000000..5a3512d4 --- /dev/null +++ b/ai/prompt-io/opencode/20260828T063126Z_1606122b_prompt_io.raw.md @@ -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. diff --git a/piker/brokers/ib/broker.py b/piker/brokers/ib/broker.py index 7f788e0e..1fb377f3 100644 --- a/piker/brokers/ib/broker.py +++ b/piker/brokers/ib/broker.py @@ -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 = '' + 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 = '' + reqid: str = '' # "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.. diff --git a/tests/test_ib_broker.py b/tests/test_ib_broker.py new file mode 100644 index 00000000..53a2776a --- /dev/null +++ b/tests/test_ib_broker.py @@ -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 == '' + assert msg.reason == expected_reason