Skip to content

fix: harden iMessage pickup timing and binding output - #19

Merged
Mauryanx merged 2 commits into
mainfrom
fm/pickup-clock-and-bind
Oct 9, 2026
Merged

Mauryanx merged 2 commits into
mainfrom
fm/pickup-clock-and-bind

Conversation

@Mauryanx

@Mauryanx Mauryanx commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Intent

Part of the overnight hardening build: smallest durable best-practice fixes, never today-only patches, never over-engineered. The final VM pass (~/dev/firstmate/data/vm-final-pass/report.md) found two small Firstmate-side defects in the courier iMessage pickup before live activation:
P2: bin/fm-courier-pickup.py schedules its reply cadence on the wall clock (around line 233 now=time.time and lines 650-651 self.next_reply = self.now() + REPLY_EVERY). After a backward clock step, no published reply is claimed or sent for the size of the step, with nothing logged (a 10-minute step delayed a reply by over 60 s; a restart sent it at once).
D2: bin/fm_inbox_conversation.py bind (around line 365) returns only conversation_id and credential, but the activation runbook (firstmate-voice docs/imessage-linq.md around lines 553-555 and docs/deploy-hermes.md around line 272) says to write state/imessage/binding.json as {conversation_id, credential, destination} exactly as bind returned them, and the pickup refuses to start without destination.

What Changed

  • Pace reply polling and capture retries with the monotonic clock; retry pending captures immediately after restart while retaining attempt counts.
  • Recover missing receipts immediately when saved publication timestamps are ahead of wall time, reusing identical requests and IDs.
  • Include non-default destinations in conversation bind output so iMessage bindings can be saved directly; add regression cases for clock rollback and binding output.

Risk Assessment

✅ Low: The bounded fixes address both reported defects, preserve transport authorization and default voice compatibility, and include behavioral regression coverage.

Testing

Both targeted suites and isolated real-process CLI checks passed, with pre-change defects reproduced, fixture setup errors corrected, evidence retained, and disposable files removed; clock steps were process-local and delivery was validated at the courier spool boundary.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Save the raw iMessage bind response as binding.json, then start pickup and successfully file an inbound message. ✅ pass live clock-bind-current-final.log:1-5 — raw bind response saved unchanged; pickup exits 0 and files imsg-req-m1.
Repeat binding without changing credentials, reject principal or destination changes, and preserve the default voice binding response shape. ✅ pass live clock-bind-current-final.log:3-5; tests/fm-inbox-conversation.test.sh transcript:9.
Publish a reply after a ten-minute backward wall-clock step; running pickup publishes it to the courier outbox on cadence and advances to done after a sent receipt. ✅ pass live clock-bind-current-final.log:6-7 — outbox reply after 2.224 s; tests/fm-courier-pickup.test.sh transcript:52.
Advance the wall clock ten minutes while pickup runs; reply publication retains its normal cadence and receipt handling completes. ✅ pass live clock-bind-current-final.log:8-9; forward-current.log:1-2 — outbox publication after 2.230 s with the clock advanced after publication committed.
Make capture fail during backward and forward clock steps; pickup preserves five/twenty-second retry delays and publishes one failure notice. ✅ pass live clock-bind-current-final.log:10-15 — retry delays approximately 5.15 s and 20.4 s for both clock directions.
Restart with a pending capture containing a stale pre-upgrade wall-clock deadline; pickup retries immediately and files the message once. ✅ pass live clock-bind-current-final.log:16 — legacy pending capture filed in 1.866 s despite a future wall-clock deadline.
Remove a delivery receipt and step the wall clock backward; pickup republishes the same request bytes and ID without stalling recovery. ✅ pass live tests/fm-courier-pickup.test.sh, targeted-tests.log:35 — pruned-receipt recovery uses byte-identical same-ID republication and survives rollback.
Hold capture indefinitely; pickup publishes one failure notice before its failed stage within the courier's 120-second deadline. ✅ pass live tests/fm-courier-pickup.test.sh, targeted-tests.log:53-54 — failure stage reached after 85.8 s.
Evidence: Targeted CLI suite transcript

Source: Targeted CLI suite transcript

FM_TEST_BEGIN 2026-10-09T11:30:34Z tests/fm-inbox-conversation.test.sh family=unclassified expected_gate_skip=none
PASS: 9 inputs accounted for; 8 single dispatch claims, 1 stale bound input explicitly rejected; 6 replies retained
PASS: capture/accept/publication/playback crash windows, duplicate races and wrong-session refusals
PASS: the newest capture names the live call; a call nobody answered is superseded, a nameless one supersedes nothing
PASS: a redial captured mid-pass is never superseded; the pass restarts and retires the dropped call
PASS: live publication is owner-authored, digest-bound, and ordered; portions surface as waiting speech
PASS: the session holding the lock answers what its predecessor left saved; a lock-less caller is refused
PASS: an ElevenLabs-only home refuses an iMessage reply and records nothing new for a voice conversation
PASS: a v2 policy carries a text destination; each conversation publishes only to the one it is bound to
PASS: a destination withdrawn from the policy can no longer be published to
ok - conversation contract: durable capture, session routing, reply and playback recovery
FM_TEST_END 2026-10-09T11:32:44Z tests/fm-inbox-conversation.test.sh exit=0 duration_ms=130388 gate_skip=false
FM_TEST_BEGIN 2026-10-09T11:32:44Z tests/fm-courier-pickup.test.sh family=standalone expected_gate_skip=none
PASS: without FM_NOTIFY_COURIER=1 the pickup exits 3 and touches nothing
PASS: a spool record becomes exactly one turn and one wake, acknowledged filed; a rescan files nothing again
PASS: an operational message-read error preserves the cursor and files the same record after restart
PASS: duplicate, wrong-mode, mismatched, extra-field, duplicate-key, NaN, symlinked and hard-linked records are refused
PASS: a record that vanishes between listing and open is refused and passed
PASS: accepted turns show working; a numbered question is texted with its poll; its stage follows the sent receipt
PASS: a vote on a question it asked is filed once with that question's binding; other votes are not filed
PASS: an operational vote-read error preserves ordering and captures the original choice after restart
PASS: a question delayed two days keeps its watch across restart and accepts a vote after delivery
PASS: an empty vote leaves the question watch available for a later nonempty vote across restart
PASS: direct and propagated progress preserve question rank; a late original reply preserves its follower link
PASS: a final answer marks done; stages are published in order and never backwards
PASS: an unrelated text preserves the established vote follower and its original done reaction
PASS: a vote answer with together receipt preserves the original done reaction and clears its answer flag
PASS: a text answer with together receipt preserves the original done reaction and clears its answer flag
PASS: a vote answer with delayed receipt preserves the original done reaction and clears its answer flag
PASS: a text answer with delayed receipt preserves the original done reaction and clears its answer flag
PASS: a text answer with refused receipt preserves the original done reaction and clears its answer flag
PASS: a queued second question preserves the first question's text answer binding and done reaction
PASS: a refused second question preserves the first question's vote answer binding and done reaction
PASS: a question without poll options awaits an answer at claim and clears the flag on failure
PASS: a pruned receipt is recovered by byte-identical same-ID republication, limited to once per 60 s, and not stalled by a backward clock step
PASS: an unknown poll receipt holds the queue, playback, reaction and watch until its sent receipt
PASS: unsupported failed and refused receipt results do not settle a question or release later replies
PASS: an unpublished numbered question ignores unrelated text and finishes only after its later answer
PASS: a numbered question whose outbox publication fails stays ineligible until publication succeeds
PASS: an unpublished unnumbered question ignores unrelated text and finishes only after its later answer
PASS: a unnumbered question whose outbox publication fails stays ineligible until publication succeeds
PASS: a consumed text with an unknown result holds later portions past 300 s until a terminal receipt
PASS: a refused poll has no vote watch; a queued failure reaction waits for its own notice receipt
PASS: a refused question retires answer eligibility and does not follow a later unrelated request
PASS: a delivered poll expires after its delivery-based watch lifetime
PASS: an empty message is told NOT_TEXT and marked failed; an unsaved attachment is filed with its notice
PASS: a message that cannot be filed is retried, then told the failure sentence and marked failed
PASS: a 16001-character transcript within the wire byte limit attempts capture and receives the normal failure notice and stage
PASS: a vote that exhausts capture retries sends the failure notice without a synthetic-message reaction
pickup latency (rename to queued wake), seconds: 0.788 0.643 0.589 0.628 0.619
PASS: each record reaches the wake queue in under a second while the pickup runs
PASS: after a backward wall-clock step the running pickup still reads and sends replies on its cadence
slow-failure stage after 85.8 s
PASS: a hung capture sends one failure notice before its failed stage, all within 120 s of pickup
PASS: the spool is left exactly as the courier left it after deleting its vanished record
ok - courier pickup: one turn per record, refusals, replies, polls, stages and latency
ok - courier pickup service: valid unit runs this pickup for its home, refuses unsafe paths
FM_TEST_END 2026-10-09T11:37:27Z tests/fm-courier-pickup.test.sh exit=0 duration_ms=282517 gate_skip=false
FM_TEST_SUMMARY total=2 failed=0 skipped_gate=0 duration_ms=413158
FM_TEST_SUMMARY_FAMILY family=standalone count=1 duration_ms=282517 failed=0
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=130388 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-courier-pickup.test.sh duration_ms=282517
FM_TEST_SLOWEST rank=2 script=tests/fm-inbox-conversation.test.sh duration_ms=130388
Evidence: Live binding, clock-step, retry and restart checks

Source: Live binding, clock-step, retry and restart checks

bind output: {"conversation_id": "text", "credential": "[redacted]", "destination": "imessage"}
pickup startup exit: 0 stderr: fm-courier-pickup: filed m1 as request imsg-req-m1
rebind refused: fm-inbox conversation: conversation already bound to destination imessage
rebind refused: fm-inbox conversation: conversation already bound
PASS: raw iMessage bind JSON starts pickup and files imsg-req-m1; repeat bind is identical, principal/destination changes refused, default voice shape unchanged
wall step=-600s; reply after initial tick=2.224s; after publication=1.924s; outbox={"attachments": [], "channel": "imessage", "id": "fm-5f4c1ad4bf962208e619f344879614c021aebc9f68623a39", "purpose": "reply", "text": "Reply despite clock step -600", "to": "+12025550101"}
PASS: running pickup keeps 2 s reply cadence through wall step -600s and completes after sent receipt
wall step=+600s; reply after initial tick=2.264s; after publication=1.964s; outbox={"attachments": [], "channel": "imessage", "id": "fm-f4c82e92c0896b32c6899eeeb030253fce15a67dee99c0e5", "purpose": "reply", "text": "Reply despite clock step 600", "to": "+12025550101"}
PASS: running pickup keeps 2 s reply cadence through wall step +600s and completes after sent receipt
wall step=-600s; first retry delay=5.144s
second retry delay=20.450s; notice={"attachments": [], "channel": "imessage", "id": "fm-0362da7846fbe608b95d23f30e4081075e80d83c4a762b5f", "purpose": "notice", "text": "I couldn't reach Firstmate.", "to": "+12025550101"}
PASS: capture retries retain 5/20 s backoff through wall step -600s and publish one failure notice
wall step=+600s; first retry delay=5.152s
second retry delay=20.424s; notice={"attachments": [], "channel": "imessage", "id": "fm-0362da7846fbe608b95d23f30e4081075e80d83c4a762b5f", "purpose": "notice", "text": "I couldn't reach Firstmate.", "to": "+12025550101"}
PASS: capture retries retain 5/20 s backoff through wall step +600s and publish one failure notice
PASS: restart retries legacy pending capture immediately (1.866s), ignores stale wall deadline and files exactly imsg-req-m1
Evidence: Pre-change defect reproduction

Source: Pre-change defect reproduction

bind output: {"conversation_id": "text", "credential": "[redacted]"}
pickup startup exit: 1 stderr: fm-courier-pickup: state/imessage/binding.json is not an imessage binding
FAIL: raw-bind AssertionError('raw bind response cannot start pickup')
FAIL: reply-backward AssertionError('clock step stalled reply beyond normal cadence')
wall step=+600s; reply after initial tick=2.781s; after publication=2.481s; outbox={"attachments": [], "channel": "imessage", "id": "fm-f5e7b1cebcea6fc44f80cb0936cdf990ec9743d4b77fd08b", "purpose": "reply", "text": "Reply despite clock step 600", "to": "+12025550101"}
PASS: running pickup keeps 2 s reply cadence through wall step +600s and completes after sent receipt
FAIL: retry-backward AssertionError('clock step stalled first 5 s retry')
wall step=+600s; first retry delay=0.250s
FAIL: retry-forward AssertionError('wall-clock step rushed or stalled 5 s retry')
FAIL: legacy-restart AssertionError('legacy deadline blocked retry after restart')
Evidence: Forward clock step after publication

Source: Forward clock step after publication

wall step=+600s; reply after initial tick=2.230s; after publication=1.930s; outbox={"attachments": [], "channel": "imessage", "id": "fm-ba99ead658a1e6578b19d5445119988904319d840dcdb475", "purpose": "reply", "text": "Reply despite clock step 600", "to": "+12025550101"}
PASS: running pickup keeps 2 s reply cadence through wall step +600s and completes after sent receipt
Evidence: Retained executable scenario driver

Source: Retained executable scenario driver

"""Live CLI check: clock steps are process-local, all product paths are disposable.
Oracle: author intent requires cadence to ignore wall steps and raw bind JSON to
be pickup-ready. Existing CLI contract specifies 2 s polling and 5/20 s retries.
No product modules are imported or product methods called by this driver.
"""
import json
import os
from pathlib import Path
import pwd
import subprocess
import sys
import time

root, temp, owner = Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3]
cli = root / 'bin/fm-inbox.sh'
pickup = root / 'bin/fm-courier-pickup.py'
processes = []
failures = []


def setup(name):
    home, courier = temp / name / 'home', temp / name / 'courier'
    (home / 'state').mkdir(parents=True)
    (home / 'state/.lock').write_text(owner + '\n')
    env = {k: v for k, v in os.environ.items() if not k.startswith('FM_') and k != 'STATE'}
    env.update(FM_HOME=str(home), FM_NOTIFY_COURIER='1', FM_COURIER_ROOT=str(courier),
               FM_COURIER_USER=pwd.getpwuid(os.getuid()).pw_name, PYTHONDONTWRITEBYTECODE='1')
    for name in ('inbound', 'outbox', 'inbox'):
        (courier / 'srv/courier' / name).mkdir(parents=True)
    (courier / 'etc/courier').mkdir(parents=True)
    (courier / 'etc/courier/policy.toml').write_text(
        'version = 1\ndefault = "deny"\nteam = []\n[captain]\nname = "Captain"\nto = "+12025550101"\nchannels = ["imessage"]\n')
    (courier / 'etc/courier/policy.toml').chmod(0o600)
    call(env, 'pilot-init', {'publication_policy': 'owner-authored-v2', 'destinations': ['imessage', 'elevenlabs']})
    binding = call(env, 'bind', {'conversation_id': 'text', 'authenticated_principal': 'captain', 'destination': 'imessage'})
    (home / 'state/imessage').mkdir()
    # For cadence checks alone, repair only the known D2 baseline fixture gap.
    # The separate bind check saves the response untouched and permits no repair.
    (home / 'state/imessage/binding.json').write_text(json.dumps(dict(binding, destination='imessage')))
    return home, courier, env, binding


def call(env, command, payload, code=0):
    r = subprocess.run([str(cli), 'conversation', command], input=json.dumps(payload),
                       env=env, capture_output=True, text=True, timeout=30)
    assert r.returncode == code, (command, r.returncode, r.stderr)
    return json.loads(r.stdout) if code == 0 else r


def once(env):
    r = subprocess.run([sys.executable, str(pickup), 'once'], env=env, capture_output=True, text=True, timeout=30)
    assert r.returncode == 0, r.stderr
    return r


def message(courier, key='m1'):
    row = {'kind': 'courier-inbound', 'version': 1, 'seq': 1, 'type': 'message',
           'published_at': '2026-10-09T01:00:00Z', 'message_id': key, 'chat_id': 'chat-1',
           'created_at': '2026-10-09T01:00:00Z', 'transcript': 'Test the clock', 'attachments': [], 'other_parts': 0}
    path = courier / 'srv/courier/inbound' / ('000000000001-message-' + key + '.json')
    path.write_text(json.dumps(row))
    path.chmod(0o640)


def state(home):
    path = home / 'state/courier-pickup/state.json'
    return json.loads(path.read_text()) if path.exists() else None


def outgoing(courier):
    return [p for p in (courier / 'srv/courier/outbox').glob('*.json') if not p.name.endswith(('.stage.json', '.votes.json'))]


def wait_for(predicate, timeout, label):
    started = time.monotonic()
    while not predicate():
        assert time.monotonic() - started < timeout, label
        time.sleep(0.02)


def start(home, env):
    offset = home / 'wall-offset'
    offset.write_text('0')
    # Run the shipped executable's __main__ in its normal Python runtime. Change
    # only time.time's system-clock input; monotonic and sleep remain real.
    wrapper = ('import pathlib,runpy,sys,time; offset=pathlib.Path(sys.argv.pop(1)); '
               'wall=time.time; time.time=lambda: wall()+float(offset.read_text()); '
               'sys.argv=sys.argv[1:]; runpy.run_path(sys.argv[0],run_name="__main__")')
    err = open(home / 'pickup.stderr', 'w')
    p = subprocess.Popen([sys.executable, '-c', wrapper, str(offset), str(pickup), 'run'],
                         env=env, stdout=subprocess.DEVNULL, stderr=err)
    processes.append((p, err))
    return p, offset


def stop(p):
    p.terminate()
    p.wait(timeout=15)


def binding_case():
    home, courier, env, bound = setup('bind')
    print('bind output:', json.dumps(dict(bound, credential='[redacted]')), flush=True)
    # Write exactly the public response; this is the runbook's activation path.
    (home / 'state/imessage/binding.json').write_text(json.dumps(bound))
    message(courier)
    r = subprocess.run([sys.executable, str(pickup), 'once'], env=env, capture_output=True, text=True, timeout=30)
    print('pickup startup exit:', r.returncode, 'stderr:', r.stderr.strip(), flush=True)
    assert r.returncode == 0, 'raw bind response cannot start pickup'
    assert set(bound) == {'conversation_id', 'credential', 'destination'} and bound['destination'] == 'imessage'
    assert call(env, 'bind', {'conversation_id': 'text', 'authenticated_principal': 'captain', 'destination': 'imessage'}) == bound
    for payload in ({'conversation_id': 'text', 'authenticated_principal': 'captain', 'destination': 'elevenlabs'},
                    {'conversation_id': 'text', 'authenticated_principal': 'other', 'destination': 'imessage'}):
        denied = call(env, 'bind', payload, 2)
        print('rebind refused:', denied.stderr.strip(), flush=True)
    voice = call(env, 'bind', {'conversation_id': 'call', 'authenticated_principal': 'captain'})
    assert set(voice) == {'conversation_id', 'credential'}
    [request] = call(env, 'audit', {'conversation_id': 'text'})['requests']
    assert request['request_id'] == 'imsg-req-m1'
    print('PASS: raw iMessage bind JSON starts pickup and files imsg-req-m1; repeat bind is identical, principal/destination changes refused, default voice shape unchanged', flush=True)


def reply_case(step):
    home, courier, env, bound = setup('reply-' + str(step))
    message(courier)
    once(env)
    call(env, 'accept', {'conversation_id': 'text'})
    p, offset = start(home, env)
    try:
        # A completed initial tick proves it scheduled its next read before the
        # input clock moves. Start with an unpublished request so no old text exists.
        wait_for(lambda: state(home)['marks']['m1']['stage'] == 'working', 5, 'initial running tick')
        initial = time.monotonic()
        time.sleep(0.3)
        published = time.monotonic()
        call(env, 'publish', {'conversation_id': 'text', 'request_id': 'imsg-req-m1', 'response_id': 'clock-answer',
                             'sequence': 1, 'kind': 'answer', 'final': True, 'destination': 'imessage',
                             'speech_text': 'Reply despite clock step ' + str(step)})
        offset.write_text(str(step))
        wait_for(lambda: outgoing(courier), 7, 'clock step stalled reply beyond normal cadence')
        elapsed = time.monotonic() - initial
        [path] = outgoing(courier)
        row = json.loads(path.read_text())
        assert row['text'] == 'Reply despite clock step ' + str(step), row
        print('wall step=%+ds; reply after initial tick=%.3fs; after publication=%.3fs; outbox=%s' %
              (step, elapsed, time.monotonic() - published, json.dumps(row)), flush=True)
        assert 1.4 <= elapsed < 5, 'wall-clock step rushed or stalled 2 s cadence'
        path.unlink()
        receipt = courier / 'srv/courier/inbox' / ('result-' + row['id'] + '.json')
        receipt.write_text(json.dumps({'kind': 'courier-result', 'id': row['id'], 'digest': 'd', 'result': 'sent',
                                      'approval_ref': None, 'idempotency_key': row['id'], 'message_id': 'm'}))
        receipt.chmod(0o640)
        wait_for(lambda: state(home)['marks']['m1']['stage'] == 'done', 5, 'sent receipt not completed')
        print('PASS: running pickup keeps 2 s reply cadence through wall step %+ds and completes after sent receipt' % step, flush=True)
    finally:
        stop(p)


def retry_case(step):
    home, courier, env, bound = setup('retry-' + str(step))
    (home / 'state/.lock').write_text('99999999\n')
    message(courier)
    p, offset = start(home, env)
    try:
        wait_for(lambda: state(home) and state(home)['pending'] and state(home)['pending']['attempts'] == 1,
                 5, 'first capture failure')
        first = time.monotonic()
        offset.write_text(str(step))
        wait_for(lambda: state(home)['pending'] is None or state(home)['pending']['attempts'] >= 2,
                 8, 'clock step stalled first 5 s retry')
        second = time.monotonic()
        print('wall step=%+ds; first retry delay=%.3fs' % (step, second - first), flush=True)
        assert 4.8 <= second - first < 8, 'wall-clock step rushed or stalled 5 s retry'
        wait_for(lambda: outgoing(courier), 24, 'clock step stalled second 20 s retry')
        elapsed = time.monotonic() - second
        [path] = outgoing(courier)
        row = json.loads(path.read_text())
        print('second retry delay=%.3fs; notice=%s' % (elapsed, json.dumps(row)), flush=True)
        assert 19.8 <= elapsed < 24, 'wall-clock step rushed or stalled 20 s retry'
        assert row['text'] == "I couldn't reach Firstmate." and state(home)['pending'] is None
        print('PASS: capture retries retain 5/20 s backoff through wall step %+ds and publish one failure notice' % step, flush=True)
    finally:
        stop(p)


def restart_case():
    home, courier, env, bound = setup('restart')
    (home / 'state/.lock').write_text('99999999\n')
    message(courier)
    once(env)
    persisted = state(home)
    assert persisted['pending']['attempts'] == 1
    # Persisted state is an intentional public recovery contract. Simulate a
    # pre-upgrade process's wall deadline, hours ahead after the rollback.
    persisted['pending']['not_before'] = time.time() + 3600
    (home / 'state/courier-pickup/state.json').write_text(json.dumps(persisted))
    (home / 'state/.lock').write_text(owner + '\n')
    started = time.monotonic()
    once(env)
    assert state(home)['pending'] is None, 'legacy deadline blocked retry after restart'
    [request] = call(env, 'audit', {'conversation_id': 'text'})['requests']
    assert request['request_id'] == 'imsg-req-m1'
    print('PASS: restart retries legacy pending capture immediately (%.3fs), ignores stale wall deadline and files exactly imsg-req-m1' %
          (time.monotonic() - started), flush=True)


try:
    for name, action in [('raw-bind', binding_case), ('reply-backward', lambda: reply_case(-600)),
                         ('reply-forward', lambda: reply_case(600)), ('retry-backward', lambda: retry_case(-600)),
                         ('retry-forward', lambda: retry_case(600)), ('legacy-restart', restart_case)]:
        if len(sys.argv) > 4 and name not in sys.argv[4:]:
            continue
        try:
            action()
        except Exception as exc:
            failures.append(name)
            print('FAIL:', name, repr(exc), flush=True)
finally:
    for p, err in processes:
        if p.poll() is None:
            stop(p)
        err.close()
sys.exit(bool(failures))
Evidence: Validation setup, oracles and evidence boundaries

Source: Validation setup, oracles and evidence boundaries

The checks run the shipped fm-inbox.sh and fm-courier-pickup.py entry points as processes against disposable FM_HOME and courier spool roots inside this worktree.
The shell named codex is the existing suite ownership fixture, not a vendor agent.
Clock steps are injected into time.time in the pickup process before executing its __main__; time.monotonic and sleep remain real.
The test never imports product functions or changes the host clock.
Courier receipt inputs model the external courier boundary; these checks establish Firstmate outbox publication and acknowledgement handling, not network delivery to an iPhone.

Acceptance oracles are the author's raw-bind activation contract and clock-rollback requirement, plus the existing CLI contract of a two-second reply cadence and five/twenty-second capture retries.
clock-bind-current-final.log shows raw-bind startup, preserved default binding shape, refused rebinds, backward/forward reply publication, retry delays, and legacy pending-state restart recovery.
clock-bind-base-final.log reproduces missing destination, backward reply/retry stalls, rushed forward retries, and stale-deadline restart failure using the two changed binaries from the base commit with the same CLI dependencies.
forward-current.log and forward-base.log additionally move the clock only after publication commits, to avoid a clock-triggered empty poll masking reply timing.
The retained clock-bind-driver.py includes that refined ordering and accepts optional scenario names after its root, temp and owner arguments.

Earlier clock-bind-current.log and clock-bind-base.log include setup attempts: the driver initially allowed its lock-owning shell to exit and created group-writable courier policy files.
The driver was corrected to retain its owner shell and use protected policy permissions; final logs supersede those setup attempts.
No tracked source or test files were changed.

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Save the raw iMessage bind response as binding.json, then start pickup and successfully file an inbound message. ✅ pass live clock-bind-current-final.log:1-5 — raw bind response saved unchanged; pickup exits 0 and files imsg-req-m1.
Repeat binding without changing credentials, reject principal or destination changes, and preserve the default voice binding response shape. ✅ pass live clock-bind-current-final.log:3-5; tests/fm-inbox-conversation.test.sh transcript:9.
Publish a reply after a ten-minute backward wall-clock step; running pickup publishes it to the courier outbox on cadence and advances to done after a sent receipt. ✅ pass live clock-bind-current-final.log:6-7 — outbox reply after 2.224 s; tests/fm-courier-pickup.test.sh transcript:52.
Advance the wall clock ten minutes while pickup runs; reply publication retains its normal cadence and receipt handling completes. ✅ pass live clock-bind-current-final.log:8-9; forward-current.log:1-2 — outbox publication after 2.230 s with the clock advanced after publication committed.
Make capture fail during backward and forward clock steps; pickup preserves five/twenty-second retry delays and publishes one failure notice. ✅ pass live clock-bind-current-final.log:10-15 — retry delays approximately 5.15 s and 20.4 s for both clock directions.
Restart with a pending capture containing a stale pre-upgrade wall-clock deadline; pickup retries immediately and files the message once. ✅ pass live clock-bind-current-final.log:16 — legacy pending capture filed in 1.866 s despite a future wall-clock deadline.
Remove a delivery receipt and step the wall clock backward; pickup republishes the same request bytes and ID without stalling recovery. ✅ pass live tests/fm-courier-pickup.test.sh, targeted-tests.log:35 — pruned-receipt recovery uses byte-identical same-ID republication and survives rollback.
Hold capture indefinitely; pickup publishes one failure notice before its failed stage within the courier's 120-second deadline. ✅ pass live tests/fm-courier-pickup.test.sh, targeted-tests.log:53-54 — failure stage reached after 85.8 s.
  • TMPDIR="$PWD/.validation/tmp" PYTHONDONTWRITEBYTECODE=1 bin/fm-test-run.sh --jobs 1 tests/fm-inbox-conversation.test.sh tests/fm-courier-pickup.test.sh
  • PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.validation/tmp" .validation/codex -c 'python3 "$1" "$2" "$3" "$$"; exit "$?"' fixture "$PWD/.validation/clock-bind-driver.py" "$PWD" "$PWD/.validation/manual-current-final"
  • Executed the same scenario driver against workspace-local copies of the two changed binaries from base commit 4b73a9119db78d2bbfab9e3a0ce89bed00e02103; reproduced missing destination and clock-related stalls.
  • Executed the driver selector reply-forward against current and base binaries with the clock step applied after publication committed.
  • Corrected initial disposable fixture ownership and policy-permission setup errors, re-ran affected checks, retained evidence, and removed .validation; git status --short was empty.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…ound destination from bind

The pickup scheduled reply reads and capture retries on the wall clock, so a
backward clock step silently stalled replies for the size of the step. Pace
them on time.monotonic(), keep wall time for record timestamps, and treat a
receipt republish time ahead of now as due.

conversation bind now returns a non-default destination beside
conversation_id and credential, so state/imessage/binding.json can be written
exactly as bind returned it; a default voice binding is unchanged.
@Mauryanx
Mauryanx merged commit 0179b5b into main Oct 9, 2026
19 checks passed
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