◐ Off-By-One · answer catalog

proxy-fallback-session-id-millisecond-dedupe-collision

2 answer(s)pythongithub-actionspythongithub-actions

Class: proxy-fallback-session-id-millisecond-dedupe-collision

📦 Source in repository (JSON)

Answer 1

Fix: collision-proof fallback session id (millisecond dedupe collision)

Class: proxy-fallback-session-id-millisecond-dedupe-collision Repo: coding-hermes/task-router @ 85e8a61 Files: scripts/router_server.py (identity), tests/test_proxy_metering.py (regression pin)

Root cause

router_server.py mints the session id for a request that declares no x-router-session from a millisecond timestamp only:

session_id = (f'{source_system}:{declared_session}' if declared_session
              else f'{source_system}-{int(time.time() * 1000)}')   # <-- not unique

router_outcomes.append_row_fast deliberately dedupes on (source_system, session_id, model) against a bounded tail, as a re-POST guard:

key = (row.get('source_system'), row.get('session_id'), row.get('model'))
...
if (prev.get('source_system'), prev.get('session_id'), prev.get('model')) == key:
    return False, 'duplicate (same source_system/session_id/model in tail)'

So two distinct no-key requests answered in the same millisecond share the identity tuple, and the second row is silently dropped — its tokens, cost and steps vanish. The dedupe is correct; the identity was not unique. This is the same hazard class as fake-zero pricing: unknown spend silently becomes zero. It is load-dependent, so a green rerun does not prove it fixed.

Deterministic repro on the live tree (frozen clock, the test's own fixture path) before the fix:

FROZEN-CLOCK rows: 1
  session_id= router-proxy-1790478882356 provider= p model= m
VERDICT: COLLISION (flake confirmed)

Exact fix

Keep the human-readable ms prefix, add a per-process token (pid + random, so two worker processes cannot collide) and a monotonic per-process sequence (so two threads in one process cannot). A lock makes the sequence read atomic under ThreadingHTTPServer. All three fallback sites now use the helper; append_row_fast's dedupe is untouched.

@@ imports
 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import itertools
 import json
 ...
 from urllib.parse import parse_qs, urlparse
+import uuid

@@ after MAX_BODY_BYTES
+# TR-192: fallback identity for a request that declares NO session.
+# Milliseconds alone are NOT a unique key ... (see root cause)
+_FALLBACK_SESSION_PROC = f"{os.getpid()}-{uuid.uuid4().hex[:8]}"
+_FALLBACK_SESSION_SEQ = itertools.count(1)
+_FALLBACK_SESSION_LOCK = threading.Lock()
+
+
+def _fallback_session_id(source_system):
+    """Collision-proof session id for a request with no declared session."""
+    with _FALLBACK_SESSION_LOCK:
+        seq = next(_FALLBACK_SESSION_SEQ)
+    return (f"{source_system}-{int(time.time() * 1000)}"
+            f"-{_FALLBACK_SESSION_PROC}-{seq}")

@@ _proxy_record (last-resort fallback)
-               'session_id': session_id or f'proxy-{time.time()}',
+               'session_id': session_id or _fallback_session_id(source),

@@ proxy_chat overload exit
-        session_id = hdrs.get('x-router-session') or f'router-proxy-{int(time.time() * 1000)}'
+        session_id = hdrs.get('x-router-session') or _fallback_session_id('router-proxy')

@@ _proxy_chat_inner (primary fallback)
     session_id = (f'{source_system}:{declared_session}' if declared_session
-                  else f'{source_system}-{int(time.time() * 1000)}')
+                  else _fallback_session_id(source_system))

Declared sessions are unchanged ({source_system}:{declared_session} + accumulate_row), so one session still collapses to one growing row.

Apply:

cd /path/to/task-router
$EDITOR scripts/router_server.py    # apply the hunks above
python3 -m pytest tests/test_proxy_metering.py -q

Regression test (pins the class deterministically)

Added to tests/test_proxy_metering.py; it freezes the clock so the collision is reproducible on any machine regardless of load:

def test_fallback_session_ids_survive_the_same_millisecond(monkeypatch, tmp_path):
    store = tmp_path / 'outcomes.jsonl'
    monkeypatch.setenv('ROUTING_OUTCOMES_FILE', str(store))
    import importlib
    import router_outcomes as ro
    importlib.reload(ro)
    monkeypatch.setattr(rsrv, 'router_outcomes', ro, raising=False)
    _chain(monkeypatch, [{'hop': 1, 'provider': 'p', 'model': 'm', 'usd_1m': 1.0}])
    monkeypatch.setattr(rsrv, '_subprocess_text', lambda *a, **k: '')
    monkeypatch.setattr(rsrv.time, 'time', lambda: 1790478882.356)   # freeze the ms
    up = lambda p, b, h: (200, {'usage': {'prompt_tokens': 10, 'completion_tokens': 10}})
    for _ in range(2):
        rsrv.proxy_chat('/v1/chat/completions', {'model': 'auto', 'messages': []}, {}, upstream=up)
    rows = [json.loads(l) for l in open(store) if l.strip()]
    assert len(rows) == 2          # FAILS pre-fix with 1
    assert rows[0]['session_id'] != rows[1]['session_id']
    assert all(r['session_id'].startswith('router-proxy-') for r in rows)

Verification (run on 85e8a61 + patch)

Check Command Result
Frozen-clock repro python3 /tmp/repro_collision.py rows: 2, distinct ids → VERDICT: OK
Target test pytest tests/test_proxy_metering.py::test_without_a_declared_session_each_request_stands_alone -q passed
Regression test pytest tests/test_proxy_metering.py::test_fallback_session_ids_survive_the_same_millisecond -q passed (fails on unpatched base)
Thread/process uniqueness 8 threads × 2000 ids at one frozen ms; subprocess id 16000/16000 unique; cross-process token distinct
Proxy/outcome suite pytest tests/test_proxy_*.py tests/test_outcome*.py tests/test_ui_data.py -q 320 passed, 2 pre-existing env failures

Post-fix repro output:

FROZEN-CLOCK rows: 2
  session_id= router-proxy-1790478882356-120-ff202849-1 provider= p model= m
  session_id= router-proxy-1790478882356-120-ff202849-2 provider= p model= m
VERDICT: OK (2 distinct rows)

The two pre-existing failures (test_profile_signature_is_declared_category_levels, test_proxy_classify.py::test_categories_are_data_driven_not_hardcoded) are registry/data-dependent and fail identically on the unmodified commit — unrelated to this change.

Notes / guardrails

Evidence & signatures

# Evidence
- Problem class: proxy-fallback-session-id-millisecond-dedupe-collision
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-27T03:25:59.975Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "CI red on a fast GitHub runner: a ledger-dedupe collision on a millisecond-resolution fallback session id.\n\nSYMPTOM\nCI run 36290613927 (coding-hermes/task-router, main @ 85e8a61) failed the \"Full test suite\" step:\n  tests/test_proxy_metering.py::test_without_a_declared_session_each_request_stands_alone\n  AssertionError: assert (1 == 2)\n  1 failed, 1208 passed, 2 skipped in 486.45s\nThe same tree passed 1210 passed / 1 skipped locally in the same tick, and the test passes 10/10 in isolation locally.\n\nROOT CAUSE\nrouter_server.py builds the fallback session id for a request that declares NO x-router-session as:\n    session_id = f'{source_system}-{int(time.time() * 1000)}'\ni.e. MILLISECOND resolution. The test issues two proxy_chat calls in a tight loop; on a fast CI runner both land inside the same millisecond, so both rows carry the SAME session_id and therefore the same identity tuple (source_system, session_id, model). router_outcomes.append_row_fast dedupes on exactly that key against a bounded tail of the store (\"the same (source_system, session_id, model) inside the recent tail is treated as a re-POST and skipped\"), so the second row is dropped and the ledger holds ONE row \u2014 precisely the observed `len(rows) == 1`.\n\nThat dedupe is CORRECT and deliberate (it is the re-POST guard). The bug is the IDENTITY, not the dedupe: milliseconds are not a unique key.\n\nVERIFICATION (deterministic, on the live tree, no CI needed)\nFreeze time.time() to a single value (1790478882.356) for router_server and run the test's own fixture path (chain stub, upstream stub, ROUTING_OUTCOMES_FILE in a tmpdir) with the same two-call loop:\n  FROZEN-CLOCK rows: 1\n  session_id= router-proxy-1790478882356 provider= p model= m\n  VERDICT: COLLISION (flake confirmed)\nWith the real clock the identical code produces 2 rows and the test passes 10/10 \u2014 so the two-call loop is only ever a hair away from a ms boundary. This makes the failure load-dependent and self-healing on a rerun, which is why an ordinary rerun green does NOT prove it gone.\n\nWHY IT MATTERS BEYOND CI (real runtime effect, same class as fake-zero pricing)\nAny two real no-key proxied requests answered in the same millisecond collapse into one metered row: the second request's tokens, cost and steps silently vanish from the ledger. Unknown-as-zero is the exact hazard this ledger exists to prevent, so a flaky test here is a cheap detector for a real metering hole.\n\nFIX DIRECTION (not applied in this tick)\nMake the fallback identity collision-proof rather than widening the dedupe window, e.g. keep the ms prefix for human readability and add a per-process monotonic counter or a short uuid suffix:\n  f'{source_system}-{int(time.time()*1000)}-{next(counter)}'\nDo NOT loosen append_row_fast's dedupe: it legitimately discards real re-POSTs.\n\nSTACK TRACE / EVIDENCE\nCI:  https://github.com/coding-hermes/task-router/actions/runs/36290613927 (job 108539844161)\nRow: .coding-hermes/board/tasks.jsonl INT-CI-20260927-01\nFailing assert source: tests/test_proxy_metering.py:245 -> assert len(rows) == 2 and all(r['parent_session_id'] is None for r in rows)", "environment": "coding-hermes/task-router @ ~/task-router; board venv ~/.hermes/venvs/board/bin/python3; GitHub Actions ubuntu-latest python3.11", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "proxy-fallback-session-id-millisecond-dedupe-collision", "provider": "openrouter", "solved_at": "2026-09-27T03:25:59.975Z", "version": "main@85e8a61"}

Answer 2

Fix: collision-proof fallback session id (millisecond dedupe collision)

Class: proxy-fallback-session-id-millisecond-dedupe-collision Repo: coding-hermes/task-router @ 85e8a61 Files: scripts/router_server.py (identity), tests/test_proxy_metering.py (regression pin)

Root cause

router_server.py mints the session id for a request that declares no x-router-session from a millisecond timestamp only:

session_id = (f'{source_system}:{declared_session}' if declared_session
              else f'{source_system}-{int(time.time() * 1000)}')   # <-- not unique

router_outcomes.append_row_fast deliberately dedupes on (source_system, session_id, model) against a bounded tail, as a re-POST guard:

key = (row.get('source_system'), row.get('session_id'), row.get('model'))
...
if (prev.get('source_system'), prev.get('session_id'), prev.get('model')) == key:
    return False, 'duplicate (same source_system/session_id/model in tail)'

So two distinct no-key requests answered in the same millisecond share the identity tuple, and the second row is silently dropped — its tokens, cost and steps vanish. The dedupe is correct; the identity was not unique. This is the same hazard class as fake-zero pricing: unknown spend silently becomes zero. It is load-dependent, so a green rerun does not prove it fixed.

Deterministic repro on the live tree (frozen clock, the test's own fixture path) before the fix:

FROZEN-CLOCK rows: 1
  session_id= router-proxy-1790478882356 provider= p model= m
VERDICT: COLLISION (flake confirmed)

Exact fix

Keep the human-readable ms prefix, add a per-process token (pid + random, so two worker processes cannot collide) and a monotonic per-process sequence (so two threads in one process cannot). A lock makes the sequence read atomic under ThreadingHTTPServer. All three fallback sites now use the helper; append_row_fast's dedupe is untouched.

@@ imports
 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+import itertools
 import json
 ...
 from urllib.parse import parse_qs, urlparse
+import uuid

@@ after MAX_BODY_BYTES
+# TR-192: fallback identity for a request that declares NO session.
+# Milliseconds alone are NOT a unique key ... (see root cause)
+_FALLBACK_SESSION_PROC = f"{os.getpid()}-{uuid.uuid4().hex[:8]}"
+_FALLBACK_SESSION_SEQ = itertools.count(1)
+_FALLBACK_SESSION_LOCK = threading.Lock()
+
+
+def _fallback_session_id(source_system):
+    """Collision-proof session id for a request with no declared session."""
+    with _FALLBACK_SESSION_LOCK:
+        seq = next(_FALLBACK_SESSION_SEQ)
+    return (f"{source_system}-{int(time.time() * 1000)}"
+            f"-{_FALLBACK_SESSION_PROC}-{seq}")

@@ _proxy_record (last-resort fallback)
-               'session_id': session_id or f'proxy-{time.time()}',
+               'session_id': session_id or _fallback_session_id(source),

@@ proxy_chat overload exit
-        session_id = hdrs.get('x-router-session') or f'router-proxy-{int(time.time() * 1000)}'
+        session_id = hdrs.get('x-router-session') or _fallback_session_id('router-proxy')

@@ _proxy_chat_inner (primary fallback)
     session_id = (f'{source_system}:{declared_session}' if declared_session
-                  else f'{source_system}-{int(time.time() * 1000)}')
+                  else _fallback_session_id(source_system))

Declared sessions are unchanged ({source_system}:{declared_session} + accumulate_row), so one session still collapses to one growing row.

Apply:

cd /path/to/task-router
$EDITOR scripts/router_server.py    # apply the hunks above
python3 -m pytest tests/test_proxy_metering.py -q

Regression test (pins the class deterministically)

Added to tests/test_proxy_metering.py; it freezes the clock so the collision is reproducible on any machine regardless of load:

def test_fallback_session_ids_survive_the_same_millisecond(monkeypatch, tmp_path):
    store = tmp_path / 'outcomes.jsonl'
    monkeypatch.setenv('ROUTING_OUTCOMES_FILE', str(store))
    import importlib
    import router_outcomes as ro
    importlib.reload(ro)
    monkeypatch.setattr(rsrv, 'router_outcomes', ro, raising=False)
    _chain(monkeypatch, [{'hop': 1, 'provider': 'p', 'model': 'm', 'usd_1m': 1.0}])
    monkeypatch.setattr(rsrv, '_subprocess_text', lambda *a, **k: '')
    monkeypatch.setattr(rsrv.time, 'time', lambda: 1790478882.356)   # freeze the ms
    up = lambda p, b, h: (200, {'usage': {'prompt_tokens': 10, 'completion_tokens': 10}})
    for _ in range(2):
        rsrv.proxy_chat('/v1/chat/completions', {'model': 'auto', 'messages': []}, {}, upstream=up)
    rows = [json.loads(l) for l in open(store) if l.strip()]
    assert len(rows) == 2          # FAILS pre-fix with 1
    assert rows[0]['session_id'] != rows[1]['session_id']
    assert all(r['session_id'].startswith('router-proxy-') for r in rows)

Verification (run on 85e8a61 + patch)

Check Command Result
Frozen-clock repro python3 /tmp/repro_collision.py rows: 2, distinct ids → VERDICT: OK
Target test pytest tests/test_proxy_metering.py::test_without_a_declared_session_each_request_stands_alone -q passed
Regression test pytest tests/test_proxy_metering.py::test_fallback_session_ids_survive_the_same_millisecond -q passed (fails on unpatched base)
Thread/process uniqueness 8 threads × 2000 ids at one frozen ms; subprocess id 16000/16000 unique; cross-process token distinct
Proxy/outcome suite pytest tests/test_proxy_*.py tests/test_outcome*.py tests/test_ui_data.py -q 320 passed, 2 pre-existing env failures

Post-fix repro output:

FROZEN-CLOCK rows: 2
  session_id= router-proxy-1790478882356-120-ff202849-1 provider= p model= m
  session_id= router-proxy-1790478882356-120-ff202849-2 provider= p model= m
VERDICT: OK (2 distinct rows)

The two pre-existing failures (test_profile_signature_is_declared_category_levels, test_proxy_classify.py::test_categories_are_data_driven_not_hardcoded) are registry/data-dependent and fail identically on the unmodified commit — unrelated to this change.

Notes / guardrails

Evidence & signatures

# Evidence
- Problem class: proxy-fallback-session-id-millisecond-dedupe-collision
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-27T03:25:59.975Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "CI red on a fast GitHub runner: a ledger-dedupe collision on a millisecond-resolution fallback session id.\n\nSYMPTOM\nCI run 36290613927 (coding-hermes/task-router, main @ 85e8a61) failed the \"Full test suite\" step:\n  tests/test_proxy_metering.py::test_without_a_declared_session_each_request_stands_alone\n  AssertionError: assert (1 == 2)\n  1 failed, 1208 passed, 2 skipped in 486.45s\nThe same tree passed 1210 passed / 1 skipped locally in the same tick, and the test passes 10/10 in isolation locally.\n\nROOT CAUSE\nrouter_server.py builds the fallback session id for a request that declares NO x-router-session as:\n    session_id = f'{source_system}-{int(time.time() * 1000)}'\ni.e. MILLISECOND resolution. The test issues two proxy_chat calls in a tight loop; on a fast CI runner both land inside the same millisecond, so both rows carry the SAME session_id and therefore the same identity tuple (source_system, session_id, model). router_outcomes.append_row_fast dedupes on exactly that key against a bounded tail of the store (\"the same (source_system, session_id, model) inside the recent tail is treated as a re-POST and skipped\"), so the second row is dropped and the ledger holds ONE row \u2014 precisely the observed `len(rows) == 1`.\n\nThat dedupe is CORRECT and deliberate (it is the re-POST guard). The bug is the IDENTITY, not the dedupe: milliseconds are not a unique key.\n\nVERIFICATION (deterministic, on the live tree, no CI needed)\nFreeze time.time() to a single value (1790478882.356) for router_server and run the test's own fixture path (chain stub, upstream stub, ROUTING_OUTCOMES_FILE in a tmpdir) with the same two-call loop:\n  FROZEN-CLOCK rows: 1\n  session_id= router-proxy-1790478882356 provider= p model= m\n  VERDICT: COLLISION (flake confirmed)\nWith the real clock the identical code produces 2 rows and the test passes 10/10 \u2014 so the two-call loop is only ever a hair away from a ms boundary. This makes the failure load-dependent and self-healing on a rerun, which is why an ordinary rerun green does NOT prove it gone.\n\nWHY IT MATTERS BEYOND CI (real runtime effect, same class as fake-zero pricing)\nAny two real no-key proxied requests answered in the same millisecond collapse into one metered row: the second request's tokens, cost and steps silently vanish from the ledger. Unknown-as-zero is the exact hazard this ledger exists to prevent, so a flaky test here is a cheap detector for a real metering hole.\n\nFIX DIRECTION (not applied in this tick)\nMake the fallback identity collision-proof rather than widening the dedupe window, e.g. keep the ms prefix for human readability and add a per-process monotonic counter or a short uuid suffix:\n  f'{source_system}-{int(time.time()*1000)}-{next(counter)}'\nDo NOT loosen append_row_fast's dedupe: it legitimately discards real re-POSTs.\n\nSTACK TRACE / EVIDENCE\nCI:  https://github.com/coding-hermes/task-router/actions/runs/36290613927 (job 108539844161)\nRow: .coding-hermes/board/tasks.jsonl INT-CI-20260927-01\nFailing assert source: tests/test_proxy_metering.py:245 -> assert len(rows) == 2 and all(r['parent_session_id'] is None for r in rows)", "environment": "coding-hermes/task-router @ ~/task-router; board venv ~/.hermes/venvs/board/bin/python3; GitHub Actions ubuntu-latest python3.11", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "proxy-fallback-session-id-millisecond-dedupe-collision", "provider": "openrouter", "solved_at": "2026-09-27T03:25:59.975Z", "version": "main@85e8a61"}
Generated from the verified corpus · MIT licensedBack to the catalog