Skip to content

fix: classify gateway recovery epochs in Python - #102

Closed
Pigbibi wants to merge 2 commits into
mainfrom
codex/ibkr-gateway-recovery-python-classifier-v1
Closed

fix: classify gateway recovery epochs in Python#102
Pigbibi wants to merge 2 commits into
mainfrom
codex/ibkr-gateway-recovery-python-classifier-v1

Conversation

@Pigbibi

@Pigbibi Pigbibi commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Scope

  • replace the recovery log classifier with a stdlib Python bounded snapshot parser
  • scope terminal events to the inspected container identity and explicit StartedAt epoch
  • preserve restart/recreate stages; no deployment or live action

Safety contract

  • old container events are ignored; replacement ID plus StartedAt is required
  • terminal authentication outcomes are sticky; generic progress cannot clear them
  • malformed, missing-source, special-file, or over-limit input fails closed without a progress extension
  • second-precision file events in the replacement StartedAt second are conservatively included; otherwise they fail closed

Validation

  • all tests/*.sh
  • python3 tests/test_gateway_recovery_classifier.py (11 tests)
  • python3 -m py_compile 2fa_bot.py scripts/*.py
  • python3 -m compileall -q 2fa_bot.py scripts tests/test_gateway_recovery_classifier.py
  • bash -n scripts/*.sh tests/*.sh container_overrides/*.sh
  • actionlint .github/workflows/ci.yml
  • focused shellcheck --severity=warning ...
  • git diff --cached --check

No Scheduler, Cloud Run, trading, account, secret, deploy, endpoint, restart, or recreate actions were performed.

Review policy: preflight then at most one concentrated closure; no merge/rerun/poll.

Co-Authored-By: Codex <noreply@openai.com>
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🤖 Codex PR Review

🚫 Merge blocked: 2 serious issue(s) found in high-risk files

⚖️ Codex Review Arbitration

🚫 block: The current findings remain valid. In scripts/recover_ib_gateway_ready.sh, snapshot_not_before() returns now - IB_GATEWAY_RECOVERY_INITIAL_SNAPSHOT_WINDOW_SECONDS for the initial stage and does not clamp that bound to the current container StartedAt. capture_epoch_decision() then tails /home/ibgateway/Jts/launcher.log and /home/ibgateway/2fa.log from the current container and prefixes every emitted file line with the current epoch_container_id. In scripts/gateway_recovery_classifier.py, Epoch.event_lower_bound uses event_not_before when provided, and _is_in_epoch() accepts file events at or after that lower bound even if they predate started_at. So if those files survive container replacement, pre-start lines from the previous container can be misclassified as current-epoch progress or terminal evidence during the initial stage. Separately, wait_for_ready_with_progress() invokes capture_epoch_decision() on every extension, which re-execs classify_gateway_recovery_snapshot.sh; gateway_recovery_classifier.py:main() creates a fresh RecoveryEpochMachine(epoch) for each invocation, so the sticky terminal behavior explicitly modeled and unit-tested in test_terminal_is_sticky_after_later_progress is not preserved across loop iterations. Because snapshot_not_before() is recomputed each initial-stage iteration, previously observed terminal evidence can age out of later snapshots instead of remaining sticky for the whole epoch assessment. These findings do not contradict the prior blocking finding; re-running classification after failed waits and preserving epoch-scoped terminal state are compatible requirements.

🚫 Blocking Issues

These issues must be fixed before this PR can be merged:

1. 🟠 [HIGH] Logic in scripts/recover_ib_gateway_ready.sh

For the initial stage, snapshot_not_before() uses now - IB_GATEWAY_RECOVERY_INITIAL_SNAPSHOT_WINDOW_SECONDS instead of clamping to the current container's StartedAt. The file-log collector then prefixes every launcher.log/2fa.log line with the current container ID, so if those logs survive across container replacement (for example via a persisted home directory), entries written before the current container started are still treated as current-epoch evidence. That violates the PR's safety contract and can incorrectly return progress or terminal based on the previous container's login attempt. (line 78)

Suggestion: Clamp the lower bound to max(epoch_started_at, now-window) for the initial stage, or make the classifier reject any file event earlier than epoch_started_at even when a broader recency window is used.

2. 🟠 [HIGH] Logic in scripts/recover_ib_gateway_ready.sh

Terminal auth outcomes are not actually sticky across progress extensions. wait_for_ready_with_progress() invokes the Python classifier as a fresh one-shot process on every loop iteration, and on the initial stage the lower bound slides forward each time via snapshot_not_before(). A terminal event seen in an earlier snapshot can therefore age out of later snapshots, after which a newer progress marker will trigger another extension instead of preserving the terminal decision for the epoch. (line 167)

Suggestion: Keep per-epoch terminal state in the shell loop, or keep a fixed lower bound for the whole initial-stage assessment once the first snapshot is taken so previously observed terminal evidence cannot disappear on later iterations.


Review by Codex PR Review bot • PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: efea8091bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/recover_ib_gateway_ready.sh Outdated
SH
}
if ! decision="$({
if ! docker logs --timestamps --since "${epoch_started_at}" "${epoch_container_id}" 2>&1 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore a recency bound before extending recovery

For the initial health-check path, this reads docker logs from the container's StartedAt, which may be days old for an already-running gateway. If that long-lived container later loses API readiness, any startup-era line such as IBC: Login attempt still makes the classifier return progress, so wait_for_ready_with_progress spends the configured progress extensions before restarting/recreating instead of treating the stale progress as irrelevant; the previous implementation bounded this with IB_GATEWAY_RECOVERY_PROGRESS_WINDOW_SECONDS.

Useful? React with 👍 / 👎.

Comment thread scripts/recover_ib_gateway_ready.sh Outdated
exit 42
fi
if [ -f "${log_path}" ]; then
cat "${log_path}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound file logs before feeding the classifier

This now streams the entire launcher and 2FA log files into a classifier that rejects snapshots over 512 events or 256 KiB. On a restarted-but-not-recreated container, those files keep historical entries from prior attempts, so once either log grows past the limit the classifier returns invalid before it can consider fresh current-epoch progress, disabling the progress extension path even during an active login/config flow.

Useful? React with 👍 / 👎.

Comment on lines +25 to +30
PROGRESS_MARKER = re.compile(
r"IBC: (Starting Gateway|Login attempt|Second Factor Authentication|Login has completed|"
r"Configuration tasks completed|Found Gateway main window|Getting config dialog|Getting main window)|"
r"Authentication window found|Auto-fill submitted|Passed token authentication|"
r"Authentication completed|Security code:",
re.IGNORECASE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep post-login dialog dismissal as progress

When the only fresh evidence is the 2FA helper clearing a post-login dialog, the classifier now returns none because this marker list omits the existing Dismissing post-login dialog log emitted by dismiss_dialog in 2fa_bot.py. Those dialogs can block Gateway/API readiness after authentication, and the previous recovery regex treated this as progress, so this path can now refuse an extension and proceed to restart/recreate while the helper is actively unblocking login.

Useful? React with 👍 / 👎.

Comment thread scripts/gateway_recovery_classifier.py Outdated
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import StrEnum

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid requiring host Python 3.11 for recovery

On deployment hosts where python3 is older than 3.11, such as Ubuntu 22.04's default Python 3.10, this import fails before the classifier can return a decision. The wrapper defaults to the host python3, and recovery treats that invocation failure as invalid, so login/config progress extensions are disabled even though the rest of these recovery scripts do not install or require Python 3.11.

Useful? React with 👍 / 👎.

Comment thread scripts/recover_ib_gateway_ready.sh Outdated
Comment on lines +122 to +123
decision="$(capture_epoch_decision \
"${epoch_container_id}" "${epoch_started_at}" "${old_container_id}" "${replacement_identity}")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-check the epoch before classifying logs

If the gateway process exits during wait_for_ready, Docker's restart: always policy restarts the same container ID with a new .State.StartedAt, but this still classifies logs using the pre-wait epoch captured before that restart. In that case the snapshot includes events from a dead container run, so stale terminal/progress lines can drive the extension or recovery decision for the wrong epoch instead of re-inspecting and scoping to the currently running attempt.

Useful? React with 👍 / 👎.

Co-Authored-By: Codex <noreply@openai.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2179e03ba2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if [ "${MOCK_SCENARIO}" = terminal ] && [ "$count" -ge 2 ]; then
printf '%s\n' '2026-07-15T16:38:02.000000000Z IBC closing because login has not completed'
else
printf '%s\n' '2026-07-15T16:38:01.000000000Z IBC: Login attempt'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Generate recent log timestamps in the runtime test

When this CI test runs more than 420 seconds after the hard-coded 2026-07-15T16:38:01Z log entry, the recovery script's default IB_GATEWAY_RECOVERY_INITIAL_SNAPSHOT_WINDOW_SECONDS=420 filters this mocked progress out during the initial stage, so the drift scenario never reaches the expected epoch-change path. I ran bash tests/test_gateway_recovery_runtime_contract.sh on July 15, 2026 at 19:29 UTC and it failed for exactly this reason; since .github/workflows/ci.yml now invokes this script, the workflow will stay red unless these timestamps are derived from the test's current time or the window is controlled.

Useful? React with 👍 / 👎.

Comment on lines +112 to +113
if ! docker logs --timestamps --since "${event_not_before}" "${epoch_container_id}" 2>&1 \
| sed "s/^/D\\t${epoch_container_id}\\t/"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound docker logs before classifying progress

When the container emits more than 512 docker-log lines during the current wait window, this unbounded stream exceeds MAX_SNAPSHOT_EVENTS in parse_protocol_snapshot, causing the classifier to return invalid before the recovery code can use any legitimate IBC: Login attempt/auth progress in the same snapshot. The previous grep-based docker-log check could still find progress in a chatty startup, but this path now refuses the extension, so cap/tail or prefilter the docker source before feeding the bounded classifier.

Useful? React with 👍 / 👎.

Comment on lines +79 to +80
def event_lower_bound(self) -> tuple[datetime, int]:
return parse_rfc3339_nanos(self.event_not_before or self.started_at)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp event lower bound to the epoch start

When the initial-stage container has restarted within the 420-second snapshot window, event_not_before can be earlier than the current .State.StartedAt, and this property then lets docker/file log lines from the previous process epoch count as current progress. For example, a fresh restart at 16:39 with a retained 16:38:30 IBC: Login attempt line is classified as progress, so recovery can extend instead of acting on the stalled current epoch; the lower bound should be the later of the recency cutoff and started_at.

Useful? React with 👍 / 👎.

@Pigbibi

Pigbibi commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Closed under main-authorized permanent freeze. Preserved CI evidence: required Run shell tests failed at head 2179e03. Any follow-up must use a fresh origin/main reslice.

@Pigbibi Pigbibi closed this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant