Skip to content

connector-runner: heartbeat attribution converges after restart + receive-liveness health surface + image HEALTHCHECKs (#2153, carved out of #2208) - #2355

Open
eumemic wants to merge 19 commits into
masterfrom
dev-pipeline/issue-2153-runner
Open

eumemic wants to merge 19 commits into
masterfrom
dev-pipeline/issue-2153-runner

Conversation

@eumemic

@eumemic eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Carve-out of #2208

Carried

  • packages/aios-connector-http/**: runner heartbeat attribution, receive-liveness health surface, healthcheck module, and package tests.
  • Dockerfile HEALTHCHECK changes for echo-http, signal, slack, sms, telegram, and whatsapp: these only invoke python -m aios_connector_http.healthcheck.
  • connectors/matrix/healthcheck.py: the existing matrix Dockerfile invokes this exact path.

Dropped

  • Every per-connector connector.py edit (Signal, Slack, SMS, Telegram, WhatsApp, Matrix): these are contested readiness-semantics changes and are not needed by the runner health surface.
  • Connector tests added/modified to exercise those dropped edits: signal/tests/test_envelope_hardening.py, slack/tests/test_serve_connection.py, whatsapp/tests/test_transport_health.py, and matrix/tests/test_deployment.py.
  • Harness/config changes outside the runner package: separate concern from this carve-out.

Framing check

All carried Docker HEALTHCHECK commands resolve without any dropped connector implementation: six invoke the carried aios_connector_http.healthcheck module; Matrix invokes the carried connectors/matrix/healthcheck.py, which delegates to that module and probes its existing appservice endpoint. Every existing connector suite passes against the carved-out runner.

Checks

uv run pytest packages/aios-connector-http -q
174 passed in 3.09s

uv run ruff check packages/aios-connector-http connectors
All checks passed!

uv run mypy packages/aios-connector-http/aios_connector_http packages/aios-connector-http/tests
Success: no issues found in 16 source files

uv run pytest connectors/signal/tests -q
178 passed in 3.47s

uv run pytest connectors/slack/tests -q
85 passed in 4.79s

uv run pytest connectors/sms/tests -q
35 passed in 4.53s

uv run pytest connectors/telegram/tests -q
100 passed in 2.06s

uv run pytest connectors/whatsapp/tests -q
157 passed in 8.75s

uv run pytest connectors/matrix/tests -q
44 passed, 1 skipped, 37 warnings in 8.04s

@eumemic eumemic added the pipeline:v2 Owned by the dev-pipeline RECONCILER (not the v1 monolith). Reconciler ONLY touches v2 items. label Sep 4, 2026
@eumemic-bot

eumemic-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: fail

  1. Property: Every active connection in each connector image using the heartbeat healthcheck must transition to healthy after its inbound transport is actually able to receive, while remaining unhealthy before that point.
    • Location: packages/aios-connector-http/aios_connector_http/runner.py:398 (and the serve_connection overrides in Signal, Slack, SMS, Telegram, WhatsApp, and Matrix)
    • Failing test: For each shipped connector override, create/discover a connection, drive serve_connection through successful transport initialization, run a heartbeat iteration, and assert that the connection ID is in healthy_connection_ids and that aios_connector_http.healthcheck.main() exits successfully. This is red on the current head: all six overrides bypass the base implementation and none calls mark_transport_ready, so their state remains starting forever and every newly added Docker HEALTHCHECK remains unhealthy even while the connector is receiving.
    • Suggested remedy: Have each inbound connector signal readiness at the point where its actual listener/webhook/poller is established, and add connector-level health transition tests.

Package tests, Ruff, and mypy pass, but they do not exercise the newly enabled image healthchecks against the concrete connector overrides.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Verdict: NEEDS-CHANGES

  1. High — concrete connector transports never become healthy (packages/aios-connector-http/aios_connector_http/runner.py:398; Signal, Slack, SMS, Telegram, WhatsApp, and Matrix serve_connection overrides)

_ConnectionState now starts as starting, and only mark_transport_ready() changes it to serving. Every shipped inbound connector overrides the base serve_connection, bypasses line 398, and never calls that method. I executed the real SMS registration/receive path: the listener successfully registered conn_1, but the heartbeat reported ([], ["conn_1"]) and healthcheck.main() exited 1. Thus the newly enabled image HEALTHCHECK remains unhealthy while the transport can receive.

Add mark_transport_ready(connection_id) at each connector's actual readiness boundary and connector-level tests that drive real initialization and assert both per-connection attribution and successful probe exit.

Evidence: reviewed live head 7221d65cf8a2e8919218c31a5f52bb9cce428935; package suite 174 passed; Ruff and mypy passed. The focused replacement/unlink refusal tests also passed (3 passed).

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Canonical review record (reconciler-written, company#383)

Verdict: NEEDS-CHANGES at 7221d65cf8a2e8919218c31a5f52bb9cce428935.

  1. BLOCKING — F1-CONCRETE-TRANSPORT-READINESS (packages/aios-connector-http/aiosconnectorhttp/runner.py:398; connectors/{signal,slack,sms,telegram,whatsapp,matrix}//connector.py serveconnection overrides). New connection state defaults to starting, and only marktransportready changes it to serving. All six inbound connector overrides bypass the base implementation and never call that method, so their Docker healthchecks remain unhealthy after transport initialization. Property: Every active connection using the heartbeat healthcheck becomes healthy after its real inbound transport can receive, while remaining unhealthy before readiness.
    At live head 7221d65cf8a2e8919218c31a5f52bb9cce428935, `/tmp/test_pr2355_degraded.py` passed while asserting the real SMS listener was registered, the heartbeat still classified conn_1 unhealthy, and the probe exited 1. The package's 174 tests, Ruff, and mypy passed, demonstrating existing checks miss this integration failure.
    

Canonical record written by the reconciler from the reviewer's structured return (company#383); the reviewer's own prose comment is discussion.

@eumemic-bot

eumemic-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: fail

  1. Property: Every active connection in each connector image using the heartbeat healthcheck must transition to healthy after its inbound transport is actually able to receive, while remaining unhealthy before that point.
    • Location: connectors/slack/src/aios_slack/connector.py:245-257; connectors/telegram/src/aios_telegram/connector.py:206-216
    • Failing test: For Slack and Telegram, register a _ConnectionState, drive _run_socket through successful socket_client.connect() or _run_polling through successful updater.start_polling(), then run a heartbeat iteration and assert the connection ID appears in healthy_connection_ids. This remains red: both methods now receive connection_id but never call mark_transport_ready, so the state remains starting and the image healthcheck stays unhealthy while the transport is receiving.
    • Suggested remedy: Signal transport readiness after each receive transport successfully starts, and add connector-level tests for the unhealthy-before/healthy-after transition.

The prior finding is fixed for Signal, SMS, WhatsApp, and Matrix, but remains violated for Slack and Telegram. Package tests passed (174 passed), as did the Slack (85 passed) and Telegram (100 passed) suites; those suites do not cover this health transition.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Verdict: NEEDS-CHANGES at aa78c8b4d18896aadbf19f517534b8f9ced5e545.

  1. High — F1-CONCRETE-TRANSPORT-READINESS (partially fixed) (connectors/slack/src/aios_slack/connector.py:245, connectors/telegram/src/aios_telegram/connector.py:206)

Property: Every active connection in each connector image using the heartbeat healthcheck must transition to healthy after its inbound transport is actually able to receive, while remaining unhealthy before that point.

Signal, SMS, WhatsApp, and Matrix now mark readiness. Slack and Telegram pass connection_id into their transport starters but never mark readiness after connect() / start_polling() succeeds, so both remain starting while able to receive. I executed both transport startup paths with their real connector methods and observed successful startup followed by serve_status == "starting"; the focused regression test failed twice.

Failing test / replay: from a fresh checkout of this SHA, create test_pr2355_readiness_review.py with the fixture in the structured review replay, then run uv run pytest test_pr2355_readiness_review.py -q; both Slack and Telegram cases fail (starting != serving). Mark each connection ready only after its receive transport has successfully started, and retain pre-start/failure assertions.

Executed checks: package heartbeat/runner tests 108 passed; all six connector suites 599 passed, 1 skipped; Ruff passed; mypy passed; malformed/stale health reads and mixed/all-unhealthy attribution tests passed. Destructive guard negatives for replacement-inode unlink and stale-reclaim races passed (4 passed). The live PR head exactly matched the requested SHA.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Canonical review record (reconciler-written, company#383)

Verdict: NEEDS-CHANGES at aa78c8b4d18896aadbf19f517534b8f9ced5e545.

  1. BLOCKING — F1-CONCRETE-TRANSPORT-READINESS (connectors/slack/src/aiosslack/connector.py:245; connectors/telegram/src/aiostelegram/connector.py:206). Slack successfully completes socketclient.connect and Telegram successfully completes updater.startpolling, but neither transport path transitions its registered connection from starting to serving. Their heartbeat therefore continues to classify a connection as unhealthy after its inbound receiver is operational. Property: Every active connection in each connector image using the heartbeat healthcheck must transition to healthy after its inbound transport is actually able to receive, while remaining unhealthy before that point.
    At exact live/requested SHA aa78c8b4d18896aadbf19f517534b8f9ced5e545, the real SlackConnector._run_socket and TelegramConnector._run_polling methods were executed with successful async transport starts. Both focused cases failed at the post-start assertion with `AssertionError: assert 'starting' == 'serving'`.
    

Canonical record written by the reconciler from the reviewer's structured return (company#383); the reviewer's own prose comment is discussion.

@eumemic-bot

eumemic-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: pass at 0e75dfd60334a108b24be3f9d2a2f06694ad17a2.

The standing transport-readiness property is now satisfied: Slack marks the connection serving only after socket_client.connect() completes, Telegram does so only after updater.start_polling() completes, and the previously corrected Signal, SMS, WhatsApp, and Matrix readiness boundaries remain present. Startup and failed transport initialization remain fail-closed.

Reviewed all 18 changed files. Checks executed: connector HTTP package 174 passed; all six connector suites 599 passed, 1 skipped; Ruff passed; mypy passed.

@eumemic-bot

eumemic-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: pass at deb7ce210a2f984301f77c5fd3f83cb1d57f4467.

The standing transport-readiness property is now satisfied: Slack marks the connection serving only after Socket Mode connects, and Telegram does so only after polling starts; failure and pre-start behavior remain fail-closed. The other shipped inbound connectors also mark readiness at their receive boundary.

Reviewed the complete 19-file diff. The connector-http suite passed on rerun (174 passed), Slack passed (85 passed), Telegram passed (102 passed), Ruff passed, and mypy passed. One initial cold-environment package run timed out in an existing one-second reconnect test while rendering exception logs; the focused test passed five consecutive reruns and the complete package rerun passed.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Verdict: NEEDS-CHANGES

  1. High — cleanup can unlink an operator replacement (packages/aios-connector-http/aios_connector_http/runner.py:1312-1315)

_remove_owned_heartbeat checks the pathname inode in one asyncio.to_thread(path.stat) call and later unlinks by pathname in a second asyncio.to_thread(path.unlink) call. A replacement installed between those calls is therefore deleted. I executed that exact interleaving at reviewed SHA deb7ce210a2f984301f77c5fd3f83cb1d57f4467; the replacement did not survive and the assertion failed. This contradicts the method's stated replacement-inode safety and is a destructive guard failure.

Suggested remedy: Do not perform this check-then-unlink sequence. The normal run() shutdown already deliberately leaves the heartbeat to become stale; remove the unused destructive helper, or redesign cleanup so it cannot unlink by a pathname whose identity can change. Add a deterministic test that interposes replacement specifically after the identity check but before unlink.

Replay

git clone https://github.com/eumemic/aios.git /tmp/aios-2355
cd /tmp/aios-2355
git checkout deb7ce210a2f984301f77c5fd3f83cb1d57f4467
cat >/tmp/repro.py <<'PY'
import asyncio, tempfile
from pathlib import Path
from aios_connector_http.runner import HttpConnector
class C(HttpConnector): connector='probe'
async def main():
 c=C(base_url='http://example.test', token='token')
 with tempfile.TemporaryDirectory() as d:
  p=Path(d)/'alive'; p.write_text('owned'); old=p.open('rb')
  s=p.stat(); c._heartbeat_owned=True; c._heartbeat_identity=(s.st_dev,s.st_ino)
  real=asyncio.to_thread; calls=0
  async def interposed(fn,*args,**kwargs):
   nonlocal calls
   calls += 1
   if calls == 2: p.unlink(); p.write_text('operator replacement')
   return await real(fn,*args,**kwargs)
  asyncio.to_thread=interposed
  try: await c._remove_owned_heartbeat(p)
  finally: asyncio.to_thread=real; old.close()
  print({'replacement_survives':p.exists(),'to_thread_calls':calls})
  assert p.exists(), 'unsafe cleanup unlinked replacement installed after identity check'
asyncio.run(main())
PY
uv run python /tmp/repro.py

The prior readiness finding is fixed: all six concrete inbound connector overrides now call mark_transport_ready; connector/package suites, Ruff, and mypy passed. I also executed malformed/stale/missing heartbeat fail-closed paths, all-unhealthy and mixed-sibling attribution, stale-debris reclaim, and existing pre-check replacement refusal tests via the package suite.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Canonical review record (reconciler-written, company#383)

Verdict: NEEDS-CHANGES at deb7ce210a2f984301f77c5fd3f83cb1d57f4467.

  1. BLOCKING — heartbeat-cleanup-toctou-unlink (packages/aios-connector-http/aiosconnectorhttp/runner.py:1312-1315). removeownedheartbeat checks inode identity with path.stat, then performs path.unlink in a separate worker call. A replacement installed between those operations passes the earlier check and is deleted by the later pathname-based unlink. Property: Heartbeat cleanup must never unlink an operator replacement inode.
    At deb7ce210a2f984301f77c5fd3f83cb1d57f4467, `uv run python /tmp/repro_heartbeat_unlink_race.py` exited 1, printed `{'replacement_survives': False, 'to_thread_calls': 2}`, and raised `AssertionError: unsafe cleanup unlinked replacement installed after identity check`.
    

Canonical record written by the reconciler from the reviewer's structured return (company#383); the reviewer's own prose comment is discussion.

Path identity validation and unlink cannot be made atomic portably, so retain stale-on-shutdown behavior and only relinquish ownership. This guarantees cleanup cannot unlink an operator replacement inode.
@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Verdict: MERGE-SAFE

The prior readiness property is satisfied on 642487061b3d413b777ab36bebdb8c1870f777e8: Signal, Slack, SMS, Telegram, WhatsApp, and Matrix now mark each connection ready only after their concrete receive path is established. Executed the package heartbeat/degraded-path suite (including malformed/missing/stale heartbeat rejection, all-unhealthy and mixed-sibling attribution, stale-file reclamation, replacement-inode refusal, and pre-readiness failure), all six connector suites, Ruff, and mypy. Results: package 174 passed; Signal 178 passed; Slack 85 passed; SMS 35 passed; Telegram 102 passed; WhatsApp 157 passed; Matrix 44 passed/1 skipped; Ruff and mypy passed. Live PR head matched the reviewed SHA. No blocking findings.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Canonical review record (reconciler-written, company#383)

Verdict: MERGE-SAFE at 642487061b3d413b777ab36bebdb8c1870f777e8.

No blocking findings at this head.

Canonical record written by the reconciler from the reviewer's structured return (company#383); the reviewer's own prose comment is discussion.

@eumemic-bot

eumemic-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: fail at 642487061b3d413b777ab36bebdb8c1870f777e8.

  1. Property: Whenever a heartbeat is made fresh, its content must already be a valid snapshot of the current authoritative connection health; readers must never observe a fresh empty or previous-process payload.
    • Location: packages/aios-connector-http/aios_connector_http/runner.py, _heartbeat_loop healthy claim path and _claim_heartbeat
    • Failing test: Start with an authoritative snapshot containing a serving connection and no heartbeat, run exactly one heartbeat-loop iteration, then assert read_connection_health() reports that connection and healthcheck.main() succeeds. This is red because the first iteration creates and touches an empty inode, and only the next iteration (up to five seconds later) writes the payload. The stale-reclaim variant is also red: seed a stale valid payload naming an old connection, run one healthy iteration with a different current connection, and observe that the old payload is made fresh and accepted.
    • Suggested remedy: Publish the current payload before or atomically with the freshness transition on initial claim and stale recovery, and add one-iteration tests for both a missing heartbeat and stale prior-process content.

The standing transport-readiness and non-destructive-cleanup properties are satisfied on this head. Package tests passed (174 passed); Ruff and mypy passed. The attempted combined connector-suite invocation was not evaluable because pytest collided on duplicate tests.conftest module names; this finding is directly covered by package-level behavior.

@eumemic-bot

eumemic-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: fail at 15c2f106cc5789d5a1546d51741b8fc695ce9207.

  1. Property: Whenever a heartbeat is made fresh, its content must already be a valid snapshot of the current authoritative connection health; readers must never observe a fresh empty or previous-process payload.
    • Location: packages/aios-connector-http/aios_connector_http/runner.py:1390-1399
    • Failing test: With an authoritative serving connection and no heartbeat, run exactly one heartbeat-loop iteration and assert that the fresh file reports that connection and passes healthcheck.main(). Also seed stale prior-process content, run one healthy iteration, and assert the newly fresh file reports only current state. Both remain red because the healthy claim calls _claim_heartbeat(path) without the current payload, creating a fresh empty file or refreshing stale old content until the next iteration.
    • Suggested remedy: Ensure initial and stale-recovery claims publish the current validated payload before or atomically with making the heartbeat fresh.

The only change since the prior failing review at 642487061b3d413b777ab36bebdb8c1870f777e8 formats Slack and Telegram methods, so this standing property remains violated. The connector-http suite passed (174 passed), Ruff passed, and mypy passed. The transport-readiness and non-destructive-cleanup properties remain satisfied.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Verdict: NEEDS-CHANGES at 15c2f106cc5789d5a1546d51741b8fc695ce9207. Live PR head matched the reviewed SHA.

  1. High — first healthy claim publishes a fresh invalid/stale payload (packages/aios-connector-http/aios_connector_http/runner.py:1390-1399)

The healthy, unowned branch calls _claim_heartbeat(path) without the current payload. For a missing path this creates and freshens an empty file; for stale crash debris it freshens the previous process's payload. Readers therefore see a fresh heartbeat before its content represents the current authoritative connection state. The payload is corrected only on a later loop iteration (normally five seconds later). I executed a one-iteration missing-file case with a current serving connection: read_connection_health() returned ([], []), and the assertion expecting (['current'], []) failed.

Suggested remedy: Pass the current payload into the healthy claim and ensure claim publishes the validated bytes before or atomically with making the inode fresh. Add deterministic one-iteration tests for both missing-file and stale-debris healthy claims.

Replay

git clone https://github.com/eumemic/aios.git /tmp/aios-2355
cd /tmp/aios-2355
git checkout 15c2f106cc5789d5a1546d51741b8fc695ce9207
cat >/tmp/repro_first_publish.py <<'PY'
import asyncio,json,os,tempfile,time
from pathlib import Path
from unittest.mock import patch
from aios_connector_http.runner import HttpConnector,_ConnectionState
from aios_connector_http.healthcheck import read_connection_health
class C(HttpConnector): connector='probe'
async def one(c,p):
 done=asyncio.Event()
 async def stop(_): done.set(); await asyncio.Event().wait()
 with patch('aios_connector_http.runner.asyncio.sleep',stop):
  t=asyncio.create_task(c._heartbeat_loop(p)); await done.wait(); t.cancel()
  try: await t
  except asyncio.CancelledError: pass
async def case(stale):
 with tempfile.TemporaryDirectory() as d:
  p=Path(d)/'alive'
  if stale:
   p.write_text(json.dumps({'healthy_connection_ids':['old'],'unhealthy_connection_ids':[]})); old=time.time()-3600; os.utime(p,(old,old))
  c=C(base_url='http://x',token='x'); c._discovery_cursor=1
  c._connections['current']=_ConnectionState('current','a',serve_status='serving')
  await one(c,p)
  got=read_connection_health(p); print(stale,got,p.read_text())
  assert got==(['current'],[]), f'first fresh publish retained invalid/stale payload: {got}'
async def run():
 for stale in (False,True): await case(stale)
asyncio.run(run())
PY
uv run python /tmp/repro_first_publish.py

Executed package healthcheck/runner tests: 108 passed, including malformed/missing/stale fail-closed paths, all-unhealthy and mixed-sibling attribution, stale reclaim, and destructive replacement-refusal guards.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Canonical review record (reconciler-written, company#383)

Verdict: NEEDS-CHANGES at 15c2f106cc5789d5a1546d51741b8fc695ce9207.

  1. BLOCKING — heartbeat-first-fresh-publish-stale-content (packages/aios-connector-http/aiosconnectorhttp/runner.py:1390-1399). The healthy unowned branch invokes claimheartbeatpath without payload. A missing path is created and freshened with empty content, while stale crash debris is freshened with prior-process content. Current payload is not written until the next loop iteration. Property: Whenever a heartbeat is made fresh, its content must already be a valid snapshot of current authoritative connection health.
    At 15c2f106cc5789d5a1546d51741b8fc695ce9207, `uv run python /tmp/repro_first_publish.py` exited 1, printed `False ([], [])`, and raised `AssertionError: first fresh publish retained invalid/stale payload: ([], [])`.
    

Canonical record written by the reconciler from the reviewer's structured return (company#383); the reviewer's own prose comment is discussion.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Verdict: NEEDS-CHANGES at 5e64d46e783faeb543c753471e1b369202662669.

  1. High — staging cleanup can unlink an operator replacement (packages/aios-connector-http/aios_connector_http/runner.py:1240-1242)

Property: Heartbeat publication and reclamation must never unlink an inode that the process did not create, including replacements installed at an internal staging pathname.

_claim_heartbeat retains only the temporary pathname, not its inode identity. Its finally block unconditionally unlinks that pathname. I executed an interleaving that replaces the staging pathname after os.link publishes the prepared inode; the claim succeeds, then finally deletes the replacement. Output was {'claim_succeeded': True, 'heartbeat': '{\"healthy_connection_ids\": [], \"unhealthy_connection_ids\": []}', 'replacement_survives': False} followed by AssertionError.

Failing test: Interpose os.link; after the real link succeeds, unlink its source and create an operator replacement at that source pathname. After _claim_heartbeat returns, assert the replacement still exists.

Suggested remedy: Track the created staging inode and only clean it through a mechanism that cannot act on a pathname replacement; add the deterministic interleaving as a regression test.

Replay

git clone https://github.com/eumemic/aios.git /tmp/aios-2355
cd /tmp/aios-2355
git checkout 5e64d46e783faeb543c753471e1b369202662669
cat >/tmp/repro_temp_unlink.py <<'PY'
import os, tempfile
from pathlib import Path
from aios_connector_http.runner import HttpConnector
class C(HttpConnector): connector='probe'
with tempfile.TemporaryDirectory() as d:
    path=Path(d)/'alive'
    real_link=os.link
    staged=None
    def interposed(source, destination, *args, **kwargs):
        global staged
        real_link(source, destination, *args, **kwargs)
        staged=Path(source)
        staged.unlink()
        staged.write_text('operator replacement')
    os.link=interposed
    try:
        identity=C._claim_heartbeat(path, b'{\"healthy_connection_ids\": [], \"unhealthy_connection_ids\": []}', True)
    finally:
        os.link=real_link
    print({'claim_succeeded': identity is not None, 'heartbeat': path.read_text(), 'replacement_survives': staged.exists()})
    assert staged.exists(), 'unsafe staging cleanup unlinked a replacement inode'
PY
uv run python /tmp/repro_temp_unlink.py

Standing properties were rechecked: concrete connector transport readiness, non-destructive public-heartbeat cleanup, and valid current payload before freshness are covered by passing executed tests. I also executed malformed/missing/stale fail-closed behavior, all-unhealthy and mixed-sibling attribution, stale reclamation/refusal races, all connector suites, Ruff, and mypy. Live head matched the reviewed SHA.

@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Canonical review record (reconciler-written, company#383)

Verdict: NEEDS-CHANGES at 5e64d46e783faeb543c753471e1b369202662669.

  1. BLOCKING — heartbeat-staging-path-unlink-race (packages/aios-connector-http/aiosconnectorhttp/runner.py:1240-1242). claimheartbeat retains only the temporary pathname and unconditionally unlinks it in finally. A replacement installed at that pathname after publication is consequently deleted. Property: Heartbeat publication and reclamation must never unlink an inode that the process did not create, including replacements installed at an internal staging pathname.
    At 5e64d46e783faeb543c753471e1b369202662669, `uv run python /tmp/repro_temp_unlink.py` exited 1, printed `{'claim_succeeded': True, 'heartbeat': '{"healthy_connection_ids": [], "unhealthy_connection_ids": []}', 'replacement_survives': False}`, and raised `AssertionError: unsafe staging cleanup unlinked a replacement inode`.
    

Canonical record written by the reconciler from the reviewer's structured return (company#383); the reviewer's own prose comment is discussion.

@eumemic-bot

eumemic-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: fail at 5e64d46e783faeb543c753471e1b369202662669.

  1. Property: Heartbeat publication and reclamation must never unlink an inode that the process did not create, including replacements installed at an internal staging pathname.
    • Location: packages/aios-connector-http/aios_connector_http/runner.py:1237-1242
    • Failing test: Interpose os.link; after the real link publishes the prepared inode, replace its source pathname, then assert the replacement survives _claim_heartbeat. The focused reproduction fails because the unconditional finally unlink deletes the replacement.
    • Suggested remedy: Make staging cleanup conditional on the pathname still naming the process-created staging inode, without introducing another check-then-unlink race; retain a deterministic replacement-interleaving regression test.

The standing transport-readiness, public-heartbeat cleanup, and first-fresh-snapshot properties remain satisfied.

An inode identity check cannot make a later pathname unlink safe, so the suggested identity-tracked cleanup would retain the TOCTOU race. Leave hidden staging links/debris in place rather than risk deleting an operator replacement, and cover the post-link replacement interleaving.
@eumemic

eumemic commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Verdict: NEEDS-CHANGES at bc2c9f7c167bb1c8a7caf70951763299d95c4a22.

  1. High — every successful heartbeat claim leaks its staging pathname (packages/aios-connector-http/aios_connector_http/runner.py:1237-1245)

_claim_heartbeat publishes with os.link(temporary_path, path) and then deliberately never removes temporary_path. Consequently even the ordinary missing-file claim leaves both alive and a hidden .alive.* hard link (link count 2). Each container/process restart or stale reclaim leaves another persistent inode/path containing connection attribution; no out-of-band reclaimer exists in this change. Repeated restart churn can therefore accumulate unbounded files/inodes. I executed a normal healthy claim and the assertion that only the public heartbeat remained failed with entries=['.alive.…', 'alive'].

The replacement-race guard should not be weakened: cleanup must remove only a staging name proven still to identify the inode created by this invocation, without pathname TOCTOU (or use a publication design that does not require an undeletable staging link). Add a normal-claim test asserting no hidden staging entry remains, plus replacement-at-staging refusal coverage.

Replay

git clone https://github.com/eumemic/aios.git /tmp/aios-2355
cd /tmp/aios-2355
git checkout bc2c9f7c167bb1c8a7caf70951763299d95c4a22
uv run python - <<'PY'
import tempfile
from pathlib import Path
from aios_connector_http.runner import HttpConnector
class C(HttpConnector): connector = 'probe'
with tempfile.TemporaryDirectory() as d:
    p = Path(d) / 'alive'
    c = C(base_url='http://example.test', token='token')
    identity = c._claim_heartbeat(
        p, b'{"healthy_connection_ids":["c"],"unhealthy_connection_ids":[]}', True
    )
    entries = sorted(x.name for x in Path(d).iterdir())
    print({'identity': identity, 'entries': entries, 'link_count': p.stat().st_nlink})
    assert len(entries) == 1, f'claim leaked staging pathname: {entries}'
PY

Executed at the live head: package heartbeat/runner tests (111 passed), all six connector suites plus Matrix (601 passed, 1 skipped), Ruff, and mypy. Degraded-path coverage included malformed/missing/stale heartbeat refusal, all-unhealthy and mixed-sibling attribution, first-publication snapshots, transport-start failures, stale reclaim, public-path replacement refusal, rollback replacement refusal, and staging-path replacement preservation. The prior fresh-empty/stale-payload, transport-readiness, and destructive-cleanup findings are fixed.

The healthcheck reader (`_parse_connection_health`) reads the heartbeat
without acquiring the writer `flock`, so the in-place `ftruncate(fd, 0)` +
`os.write` in `_refresh_heartbeat` made a fresh but EMPTY/partial heartbeat
externally visible during every refresh. A concurrent `read_connection_health`
/ `healthcheck.main()` could observe `([], [])` and the probe exit 1 even
though the connection was healthy. This violated the standing requirement that
any fresh heartbeat already contain a complete, structurally valid snapshot.

Publish atomically instead: populate a fresh `O_TMPFILE` inode with the whole
snapshot and `RENAME_EXCHANGE` it into the heartbeat path. An unlocked reader
can then only ever observe the prior complete snapshot or the new complete
snapshot -- never a torn one.

Over-correction guard: a degenerate "never write / never truncate" fix would
also make the torn-read test pass. test_refresh_still_replaces_content_positive_control
asserts an uncontended refresh really lands the new bytes; the mixed-state
test asserts a healthy sibling still publishes.

The suggested remedy (publish refresh content so readers see a complete
snapshot) is correct as far as it goes, but naively swapping in a new inode on
every refresh AMPLIFIES a pre-existing revocation gap: ownership was tracked as
(st_dev, st_ino) only, and atomic publication frees the old inode whose st_ino
the filesystem can immediately recycle. A paused former owner whose recycled
number matched could then resume -- the standing "revoke superseded owner"
property. Remedy-as-stated does NOT achieve that standing property. So ownership
now also carries a per-generation nonce (an xattr stamped on every published
inode); the identity-checked refresh compares it, revoking a stale owner even
when the inode number is recycled. The stale-recovery test now asserts that
property directly (a former owner cannot refresh) rather than the flaky
inode-number-inequality proxy it relied on.

connector-http: 183 passed; ruff + ruff format + mypy clean.
@eumemic

eumemic commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Fix round did not complete

Fix committed at 5b72a84eac and byte-verified, but CI is unknown (still running at 5b72a84: e2e (docker), integration, unit, e2e (non-docker), lint), so the needs:fix/* label stays. Clearing it is gated on a green head, never on the fix looking done.

Framing check: The reviewer's framing is ACCURATE but NARROWER than reality. The torn-read defect is exactly as described (in-place ftruncate+write at runner.py:1322-1328, unlocked reader) and reproduces. However, the suggested remedy is INCOMPLETE: the obvious way to 'publish so readers see only complete snapshots' is to swap in a fresh inode, and doing that naively silently breaks a DIFFERENT standing property (revoke superseded owner, established by commit 0ccc831) because freeing the owned inode lets tmpfs recycle its st_ino and a (st_dev,st_ino)-only ownership check can be defeated by a paused former owner. I verified this is a real, measured amplification (collisions made the standing recovery test fail ~1/5) and that a weaker version pre-existed on the original head (3/40). The fix therefore had to be wider than the finding: atomic publication PLUS an inode-reuse-proof ownership nonce.

Unverified: I did NOT run the full multi-package 'all connector suites' (reviewer cited 601 passed, 1 skipped) — only the aios-connector-http package suite (183 passed). I did NOT run the GitHub Actions CI jobs themselves to green; I only verified via REST that runs were CREATED and queued/in_progress for the exact head SHA 5b72a84 (Code Validation run 4528, eumemic-bot review, Build signal connector image, Migration head check) — their PASS/FAIL conclusions are pending and UNKNOWN. The ownership-nonce hardening relies on filesystem xattr support (user.* namespace); it is present on the sandbox tmpfs and the code degrades gracefully to (st_dev,st_ino)-only when setxattr/getxattr raise OSError, but I did NOT test the container image's actual heartbeat filesystem (e.g. /var/run) for xattr support, so on a filesystem without xattrs the revocation hardening silently falls back to the pre-existing (no-worse) behaviour. I did NOT exercise the O_TMPFILE-unavailable branch on a real non-supporting filesystem (refresh returns None → relinquish) beyond code inspection. Concurrency reproduction used threads within one process, not true multi-process contention.

Attempt 3/3. Automatic retries are TERMINATED; escalated to needs:human/fixround.

@eumemic-bot

eumemic-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Verdict: fail (one blocking convention finding; behaviour is otherwise sound)

Reviewed at head 5b72a84eac7241d42c8d3eff4a323df1f279f5eb, clone at /mnt/review.

What I verified (green)

  • uv run pytest packages/aios-connector-http -q → 183 passed.
  • Connector suites all pass: signal 178, slack 85, sms 35, telegram 102, whatsapp 157, matrix 44 passed / 1 skipped.
  • uv run ruff check packages/aios-connector-http connectors → clean.
  • uv run mypy packages/aios-connector-http/aios_connector_http (the CI-covered source set) → clean, 8 files.
  • Heartbeat state machine is coherent: starting → serving (via mark_transport_ready) → stopped on clean exit, → restarting on failure; the heartbeat loop treats only serving as healthy and fail-closes (content published, mtime frozen) when every active transport is down. Atomic O_TMPFILE+RENAME_EXCHANGE publication, nonce-based ownership revocation, and replacement-safety are all exercised by tests. Matrix healthcheck.py correctly short-circuits on SystemExit(1) from check_connector_heartbeat() before the appservice probe. All seven connector Dockerfiles define a HEALTHCHECK (enforced by test_every_connector_image_defines_a_healthcheck).

Blocking finding

1. Newly-added test modules do not type-check under the project's strict mypy config.

  • property: uv run mypy packages/aios-connector-http/tests must report success (no errors), consistent with the project's strict = true mypy configuration and with this PR's own stated check ("Success: no issues found in 16 source files"). It currently reports 13 errors.
  • The API return/param type of _claim_heartbeat/_refresh_heartbeat changed to a 3-tuple (st_dev, st_ino, nonce: bytes | None), but the new test helpers still assume a 2-tuple:
    • tests/test_heartbeat_refresh_atomicity.py:33 _claim annotated -> tuple[int, int] (returns a 3-tuple); lines 72/110/136 pass that 2-tuple into _refresh_heartbeat (expects 3-tuple); lines 69/70/74 reference runner.os / runner.os.write (Module ... does not explicitly export attribute "os" / has no attribute "write") with unused type: ignore comments.
    • tests/test_healthcheck.py:210 assigns a 2-tuple into a tuple[int,int,bytes|None]|None variable; :219 then does a non-overlapping equality check against it.
  • failing_check: uv run mypy packages/aios-connector-http/tests (RED now: 13 errors in 2 files). Note: CI's mypy invocation in .github/workflows/code-validation.yml:246 does not include this tests directory, so CI will not catch it — but the PR body asserts this exact command passes, and it does not.
  • suggested_remedy (advisory): update the helper signatures/annotations to the 3-tuple identity, thread the nonce through, and drop the now-unused type: ignore comments; for the runner.os patching, patch os.write via a supported handle rather than the module's re-exported os.
  • location: packages/aios-connector-http/tests/test_heartbeat_refresh_atomicity.py:33,69-74,110,136; packages/aios-connector-http/tests/test_healthcheck.py:210,219.

Non-blocking notes

  • The PR description's "Dropped" section states every per-connector connector.py edit and the slack/matrix test edits were removed from this carve-out, but the diff still contains them (matrix/signal/slack/sms/telegram/whatsapp connector.py, slack/tests/test_serve_connection.py, telegram/tests/test_transport_readiness.py). The code is correct and tested; the description is simply inconsistent with the diff. Worth reconciling the PR body so reviewers aren't misled about scope.

@eumemic eumemic added the paused label Sep 5, 2026
@eumemic

eumemic commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Paused — deliberate, not stranded.

This PR carries needs:fix/review-findings and is waiting on the fixround driver, which the CEO seat disabled at 2026-09-05 02:51Z under an explicit chairman directive ("cancel all this stuff" / "reduce lanes to 0") during an Anthropic 5h rate-cap blowout. All 8 lane launchers are off, so no machine will pick up this fix round until the freeze lifts.

Applying paused so the gate-owner watchdog stops reporting it as an unowned fix round — the condition is real but intended, and a true-shaped alarm pointing at a deliberate state is how a real alarm becomes furniture.

This label has no expiry of its own, so the unpause obligation is recorded in the seat's freeze ledger against the same deadline as the other freeze suppressions (2026-09-06T18:00Z). When the lanes are re-armed, remove paused from this PR — do not leave it silently invisible.

@eumemic

eumemic commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Disposition: HOLD — blocked on a deliberately disabled lane, not stranded.

Checked this PR's live state rather than its label:

  • The review verdict at the current head is a genuine open finding (not a stale gate label pointing at a resolved condition), so the needs:fix/review-findings gate is correct.
  • The fix is owned by the fixround driver, and fixround-driver-aios / fixround-driver-company are both enabled=false — disabled 2026-09-05 02:51Z under the chairman's "cancel all this stuff" / "reduce lanes to 0" directive during the rate-cap event, and still off under the 2026-09-07 pipeline shutdown.

So no machine will pick this up until the lanes are re-armed. The condition is real and intended.

Why this is being re-stated: a paused label was applied here on 2026-09-05 for exactly this reason, and it is no longer on the PR — so the gate-owner watchdog resumed reporting it as an idle unowned fix round. Re-applying it. A true-shaped alarm pointing at a deliberate state is how a real alarm becomes furniture, and this one has now cried twice.

Unblock condition (unchanged): when the lanes are re-armed — fix first, then triggers, workflow last — remove paused and let the fixround driver take the round. Nothing here needs the chairman.

@eumemic

eumemic commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

This PR is suppressed ON PURPOSE until 2026-09-21 — here is the reasoning

The paused label silences every gate-owner finding on this PR, and the label has no expiry of its own. A deadline check fired today because the previous declared deadline passed. I extended it deliberately to 2026-09-21; recording why here so the state is legible from the PR itself rather than only from a script comment.

Why it stays paused

The pipeline freeze is still in force, verified from state rather than history:

  • design-pipeline workflow archived_at = 2026-09-07T06:24:25Z
  • design-sweep-15m, design-sweep-company, design-sweep-console — all disabled at consecutive_failures=5 (read account-wide; this session's own trigger list cannot see other sessions' triggers)

Re-arming is blocked on a per-child spend ceiling (aios#2396). Note the correction on that issue: the per-child wall-clock bound already existed and was not the gap — prod carried 10800s, wider than the runaways it was supposed to stop. The consumption ceiling is the real missing piece.

Why unpausing now would be worse than leaving it

This PR carries needs:fix/review-findings and needs:human/fixround. The fix-round drivers that service those labels (fixround-driver-aios, fixround-driver-company) are themselves disabled under the same freeze.

So lifting the pause would not start any work. It would re-expose findings into a queue with no worker — producing recurring alarms nobody can act on, which is precisely how a true alarm decays into furniture. The same mechanism is already visible in aios#2428, where stale dispatched labels hold a floodgate shut and the starvation gets reported as correct backpressure.

What this is not

Not a judgement that this PR is fine. Its findings are unreviewed and its state is unknown to me. It is invisible on purpose, and that purpose has an expiry date attached so the invisibility cannot become permanent by default.

If the freeze outlives 2026-09-21, the right move is another deliberate extension with fresh evidence — not a longer deadline chosen to make the reminder fire less often.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs:fix/review-findings needs:human/fixround paused pipeline:v2 Owned by the dev-pipeline RECONCILER (not the v1 monolith). Reconciler ONLY touches v2 items. review-fix-cycles:2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant