Skip to content

feat(bin): add courier spool pickup for iMessage conversations - #17

Merged
Mauryanx merged 15 commits into
mainfrom
fm/fm-courier-inbound-pickup
Oct 9, 2026
Merged

Mauryanx merged 15 commits into
mainfrom
fm/fm-courier-inbound-pickup

Conversation

@Mauryanx

@Mauryanx Mauryanx commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Intent

The captain chose option (a) on 2026-10-08 ("yes go with a"): the courier writes the captain's inbound iMessages only into its own inbound spool, and Firstmate picks them up from there as itself - replacing the cross-account write path that the VM rehearsal showed cannot work in the sandbox (findings C2-C4: C2, inside the courier's private PID namespace the host Firstmate session-lock owner PID is invisible, so the transport fails with "conversation owner is gone"; C3, Firstmate's state/.lock and state/voice-conversation/policy.json are written 0600 by the owner but the courier transport needed to read both; C4, Firstmate's fm_shared_interface.py fchown/setxattr ACL template copying fails with EINVAL inside the courier's one-UID user namespace, so any cross-UID metadata copy fails there). Security goal: no cross-account writes in either direction; the courier never sees or touches Firstmate, Firstmate never touches the courier's credential. Latency goal: the pickup adds under a second on top of the Linq poll. Owner-only inbound, automatic owner replies, polls, attachments and reactions must keep working exactly as today. Smallest durable change, the way Kun builds Firstmate.

What Changed

  • Add opt-in courier spool pickup that files inbound iMessages as Firstmate, with validation, durable deduplication, ordered capture and retries.
  • Route replies, polls and reaction stages through Firstmate-owned outbox requests, track delivery receipts and question answers, and preserve attachment notices; remove the shared writable conversation interface and ACL-copying helper.
  • Document pickup configuration and operation, add offline regression coverage, and register it with the test runner.

Risk Assessment

🚨 High: Recipient replacement can transfer private replies without an established disclosure policy, and delivery delays can silently lose poll answers or break reply ordering.

Testing

Three targeted CLI suites and the syscall-audited manual exchange passed after correcting fixture setup; transcripts and product records were retained, disposable homes were removed, and the accepted real-Linq/two-account limitation remains untested.

  • Live validation: ✅ go - 17 of 18 scenarios driven live against the product
Scenario Result Live Evidence
Start pickup without opt-in, without an explicit home, with a writable policy, or alongside another instance; each unsafe invocation is refused. ✅ pass live Targeted CLI transcript:2; Manual CLI exchange and guard refusals:2,7,8.
Publish ordered inbound records and restart pickup; each message becomes exactly one conversation turn and wake with its transcript and attachment reference preserved. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:3; captured-input artifact preserves the attachment reference.
Submit duplicate, malformed, oversized, incorrectly permissioned and linked records; pickup refuses them and continues to subsequent valid records. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:5,6.
Encounter transient message or vote I/O errors and restart; the cursor remains unchanged and capture retries, while vanished records are passed. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:4,6,9.
Accept a message and publish owner replies; numbered questions carry native poll options, and sent receipts advance playback and monotonic reaction stages. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:7,13,22; outgoing-reply and reaction artifacts.
Vote on a watched question after restart; the first nonempty choice carries its question binding, empty selections preserve the watch, and unrelated or expired votes are ignored. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:8,10,11,33; lifetime cases execute the CLI with an advanced clock.
Answer before or alongside the question receipt, then send unrelated text and late progress; the original follower remains linked and reaches done without stage regression. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:12–19.
Publish successive questions, leaving the second queued or refused; an answer to the first retains its binding and completes the original message. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:20,21.
Send unrelated text while a question is queued, publication fails, or delivery is refused; the undelivered question does not acquire that text's completion chain. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:26–29,32.
Return unknown, pending or unsupported receipt results; later replies stay ordered until a contract terminal receipt, and refused polls lose their vote watch. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:24,25,30,31.
Consume an outgoing request and lose its receipt; pickup republishes identical bytes with the same ID at the bounded retry interval. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:23; retry timing uses the CLI with an advanced clock.
Submit empty text, missing attachments, unavailable-session input or a wire-valid 16001-character transcript; normal notices and failed acknowledgements occur, with no synthetic vote reaction. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:34–37.
Publish records while pickup runs continuously; every sampled record reaches the wake queue in under one second. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:38,39: 0.826, 0.581, 0.646, 0.653 and 0.622 seconds.
Block the conversation transport during capture; pickup sends one failure notice before its failed stage within the pickup deadline. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:40,41: failed stage after 85.8 seconds.
Run pickup against read-only inbound and receipt directories; courier files remain unchanged, credential and ledger files are never opened, and outgoing requests are Firstmate-owned 0640 files. ✅ pass live Observed courier file operations; captured-exchange metadata comparisons; Manual CLI exchange and guard refusals:6.
Restart the owner conversation session and attempt unauthorized publications; durable recovery works and session, principal and destination restrictions remain enforced. ✅ pass live tests/fm-inbox-conversation.test.sh; Targeted CLI transcript:47,50–54.
Drain concurrently appended wakes and interrupt handling; durable records remain available until post-handling acknowledgement. ✅ pass live tests/fm-wake-queue.test.sh; Targeted CLI transcript:73,132; cases launch the real wake-drain executable.
Exchange real captain iMessages, media and votes through an isolated courier with separate Unix accounts; sender filtering, delivery and both account boundaries hold. ⏸️ untested no Drove real Firstmate entry points against disposable courier wire records and audited file operations, but those fixtures cannot establish actual courier/Linq behavior. Attempting a disposable user na…
Evidence: Targeted CLI transcript

Source: Targeted CLI transcript

FM_TEST_BEGIN 2026-10-09T07:29:17Z 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
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.826 0.581 0.646 0.653 0.622
PASS: each record reaches the wake queue in under a second while the pickup runs
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
FM_TEST_END 2026-10-09T07:33:38Z tests/fm-courier-pickup.test.sh exit=0 duration_ms=261037 gate_skip=false
FM_TEST_BEGIN 2026-10-09T07:33:38Z 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-09T07:35:49Z tests/fm-inbox-conversation.test.sh exit=0 duration_ms=130618 gate_skip=false
FM_TEST_BEGIN 2026-10-09T07:35:49Z tests/fm-wake-queue.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - an abandoned same-process lock hold is reclaimed; a parent's live hold is not
ok - without BASHPID a subshell cannot release or reclaim its parent lock and owns its own hold
ok - bounded acquire hands ownership to the waiting caller after contention
ok - presentation lock waits are bounded and retriable without weakening acknowledgement atomicity
ok - malformed presentation locks report acquire failure instead of contention
ok - foreign secondmate queue alerts once per no-progress episode without age-only or cascade noise
ok - declared external-wait pause rows do not feed secondmate wake-loop escalation
ok - a reprovisioned queue generation starts a fresh no-progress interval
ok - an active turn defers the secondmate stall escalation without cancelling it
ok - a long-lived mate mid-turn is not a stall, but a queue frozen past the busy bound still alarms
ok - secondmate stall markers reject symlinks without touching their targets
ok - stall publication acknowledgement closes the pre-marker crash window
ok - empty prefix mate cleanup preserves another mate's stall receipt
ok - self-announced appends suppress only their own bytes and fail toward waking
ok - historical annotations replay nothing already announced and keep everything new
ok - concurrent append plus drain preserves durable records through acknowledgement
ok - signal written while no watcher runs is caught on next run
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 4131989.1791531504.pPZePK
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 6s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  After draining queued wakes, repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
ok - stale wake is queued before suppressor state is advanced
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 4139633.1791531512.pYxntK
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 5s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  After draining queued wakes, repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
ok - a not-provably-working stale wake is queued before its suppressor is advanced
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 4144774.1791531519.M7408S
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 registered custom check(s), but no live watcher process holds this home lock (last beat: 3s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  After draining queued wakes, repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.
ok - registered custom check output is queued before cadence suppression
ok - concurrent drains replay until one post-handling acknowledgement consumes records
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 4 --recovery-generation 4150887.1791531529.Sg1t5Y
ok - drain collapses obvious duplicate heartbeat and signal records
ok - drain asserts watcher liveness: warns on a lapse, stays silent for a live watcher with a fresh beacon
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 8 --recovery-generation 4156045.1791531536.HTLPWe
ok - structural signal enrichment is separate, deduped, home-local, and tier-zero for other wakes
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 13 --recovery-generation 4159658.1791531541.Rxvhjl
ok - every readable unread status line is annotated in full while invalid status files preserve their raw wakes
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 4166424.1791531550.rPpO90
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 2 --recovery-generation 4166424.1791531550.rPpO90
ok - slow annotation releases the append lock and a deleted status file fails open
ok - a branch-actor scoped ack never swallows an unacked main-owned row, and main's later drain sees exactly what remains
ok - main drain and acknowledgement exclude an active branch grant
ok - a branch-held row raises no queued-wake warning for main, and the same row is presented and acknowledged once the grant clears
ok - a queue that cannot be counted keeps the queued-wake alarm up
ok - structurally unusable rows are retired by main alone, leaving every remaining row presentable and acknowledgeable
ok - branch grant cannot take a row already claimed by main
ok - actor ownership filtering precedes same-key deduplication
ok - main reclaims rows granted to an exited branch owner
ok - a branch-actor drain with no eligible-row snapshot refuses loudly instead of draining nothing
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation existing
ok - wake append publishes atomic recovery evidence before durable rows
ok - wake drain: generation-less legacy wakes are adopted and acknowledged
ok - wake drain: a stale acknowledgement cannot retire or consume a newer recovery episode
ok - wake drain: an acknowledgement that consumes nothing says so and names the exact command for the current wake
ok - wake drain: a branch acknowledgement that consumes nothing names the exact command for its granted wake
ok - wake drain: recovery acknowledgement failures are explicit and retryable
ok - interruptions preserve durable rows until post-handling acknowledgement
FM_TEST_END 2026-10-09T07:40:41Z tests/fm-wake-queue.test.sh exit=0 duration_ms=292100 gate_skip=false
FM_TEST_SUMMARY total=3 failed=0 skipped_gate=0 duration_ms=684075
FM_TEST_SUMMARY_FAMILY family=standalone count=1 duration_ms=261037 failed=0
FM_TEST_SUMMARY_FAMILY family=unclassified count=1 duration_ms=130618 failed=0
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=292100 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-wake-queue.test.sh duration_ms=292100
FM_TEST_SLOWEST rank=2 script=tests/fm-courier-pickup.test.sh duration_ms=261037
FM_TEST_SLOWEST rank=3 script=tests/fm-inbox-conversation.test.sh duration_ms=130618
Evidence: Captured input, outgoing reply and reaction records

Source: Captured input, outgoing reply and reaction records

{
  "accepted_input": {
    "authenticated_principal": "captain",
    "committed_transcript": "Please acknowledge the isolated pickup.\n[attachment: /srv/courier/inbox/media/fixture.txt (text/plain, 21 bytes)]",
    "conversation_id": "text",
    "correction_of": null,
    "created_at": "2026-10-09T01:00:00Z",
    "previous_turn_id": null,
    "question_binding": null,
    "request_id": "imsg-req-manual-1",
    "revision": 1,
    "turn_id": "imsg-manual-1"
  },
  "outgoing_reply": {
    "attachments": [],
    "channel": "imessage",
    "id": "fm-577f0c70954198e6d4db5f6db2ef6661dd94c95f0b5ef998",
    "purpose": "reply",
    "text": "The isolated pickup and attachment reference reached Firstmate.",
    "to": "+12025550101"
  },
  "outbox_mode": "0o640",
  "stages": [
    {
      "id": "st-000000000001-25351b9eb4076ca4",
      "message_id": "manual-1",
      "stage": "filed"
    },
    {
      "id": "st-000000000002-11da8e064f6d83a4",
      "message_id": "manual-1",
      "stage": "working"
    },
    {
      "id": "st-000000000003-3cb159b8822d9c90",
      "message_id": "manual-1",
      "stage": "done"
    }
  ],
  "courier_files_unchanged": {
    "private/ledger.json": {
      "sha256": "cc8bf0a9b5a4ebf37d8a8667a197fc48fbba5d90043914347589a5bf9682a0fa",
      "mode": "0o600",
      "uid": 1100,
      "inode": 451677,
      "mtime_ns": 1791531205500010950
    },
    "srv/courier/inbound/000000000001-message-manual-1.json": {
      "sha256": "f71d625ba2bfc081c5b7b423f1545d46534fee37f1915e5f8b358cb9b6646aff",
      "mode": "0o640",
      "uid": 1100,
      "inode": 451679,
      "mtime_ns": 1791531205500193067
    },
    "srv/courier/inbox/media/fixture.txt": {
      "sha256": "e28269a4b5c06f06a2e9c007894acf11a576b63fb4b9aaf2aec5792bb900469a",
      "mode": "0o600",
      "uid": 1100,
      "inode": 451678,
      "mtime_ns": 1791531205500079153
    },
    "etc/courier/policy.toml": {
      "sha256": "7116b3e3554e9f0460d3df8c1cd1839d97d481eb05bf775be30d27d00fdd5487",
      "mode": "0o600",
      "uid": 1100,
      "inode": 451675,
      "mtime_ns": 1791531205499858755
    },
    "etc/courier/credential.json": {
      "sha256": "9f7f4d62fd474731eb5ca8944b41eadd135d06916c024294674ed76706b7f38c",
      "mode": "0o600",
      "uid": 1100,
      "inode": 451676,
      "mtime_ns": 1791531205499940960
    }
  },
  "receipt_unchanged": {
    "srv/courier/inbox/result-fm-577f0c70954198e6d4db5f6db2ef6661dd94c95f0b5ef998.manual.json": {
      "sha256": "f048e95f3487940c63412b539b2b1e797b0371fb331a5b9a9b22aae4cd835a41",
      "mode": "0o640",
      "uid": 1100,
      "inode": 452168,
      "mtime_ns": 1791531214188684553
    }
  },
  "file_syscall_traces": [
    "pickup-capture.strace",
    "pickup-policy-refusal.strace",
    "pickup-reply.strace",
    "pickup-settle.strace"
  ]
}
Evidence: Manual CLI exchange and guard refusals

Source: Manual CLI exchange and guard refusals

capture: fm-courier-pickup: filed manual-1 as request imsg-req-manual-1
policy-refusal: fm-courier-pickup: courier outbox unavailable: courier policy is not root-owned and protected
reply: 
settle: 
CLI exchange: {"transcript": "Please acknowledge the isolated pickup.\n[attachment: /srv/courier/inbox/media/fixture.txt (text/plain, 21 bytes)]", "outgoing_reply": {"attachments": [], "channel": "imessage", "id": "fm-577f0c70954198e6d4db5f6db2ef6661dd94c95f0b5ef998", "purpose": "reply", "text": "The isolated pickup and attachment reference reached Firstmate.", "to": "+12025550101"}, "outbox_mode": "0o640", "stages": [{"id": "st-000000000001-25351b9eb4076ca4", "message_id": "manual-1", "stage": "filed"}, {"id": "st-000000000002-11da8e064f6d83a4", "message_id": "manual-1", "stage": "working"}, {"id": "st-000000000003-3cb159b8822d9c90", "message_id": "manual-1", "stage": "done"}]}
PASS: file syscall traces show no credential, ledger or media opens; read-only inbound and receipts are unchanged
second pickup: exit=1; fm-courier-pickup: another pickup holds this home
missing home: exit=1; fm-courier-pickup: explicit FM_HOME is required
Evidence: Observed courier file operations

Source: Observed courier file operations

Firstmate pickup: observed courier file operations from real CLI executions.
Each trace includes child processes. All credentials and records belong to disposable fixtures.
Full traces are retained alongside this excerpt.

pickup-capture.strace
3869766 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbound", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbound>
3869766 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbound", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbound>
3869766 openat(4<<disposable-courier-root>/srv/courier/inbound>, "000000000001-message-manual-1.json", O_RDONLY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC) = 5<<disposable-courier-root>/srv/courier/inbound/000000000001-message-manual-1.json>
3869766 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbound", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbound>
3869766 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/outbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/outbox>
3869766 openat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-75a48dbfb7eb2d18bbd1022a50bf52b3.tmp", O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC, 0640) = 5<<disposable-courier-root>/srv/courier/outbox/.fm-pickup-75a48dbfb7eb2d18bbd1022a50bf52b3.tmp>
3869766 renameat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-75a48dbfb7eb2d18bbd1022a50bf52b3.tmp", 4<<disposable-courier-root>/srv/courier/outbox>, "st-000000000001-25351b9eb4076ca4.stage.json") = 0
3869766 unlinkat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-75a48dbfb7eb2d18bbd1022a50bf52b3.tmp", 0) = -1 ENOENT (No such file or directory)

pickup-policy-refusal.strace
3874356 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbound", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbound>
3874356 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbox>
3874356 newfstatat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/etc/courier/policy.toml", {st_mode=S_IFREG|0660, st_size=42, ...}, AT_SYMLINK_NOFOLLOW) = 0
3874356 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/outbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/outbox>
3874356 openat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-70f0a621b7032a1a68f801cc56d3202f.tmp", O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC, 0640) = 5<<disposable-courier-root>/srv/courier/outbox/.fm-pickup-70f0a621b7032a1a68f801cc56d3202f.tmp>
3874356 renameat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-70f0a621b7032a1a68f801cc56d3202f.tmp", 4<<disposable-courier-root>/srv/courier/outbox>, "st-000000000002-11da8e064f6d83a4.stage.json") = 0
3874356 unlinkat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-70f0a621b7032a1a68f801cc56d3202f.tmp", 0) = -1 ENOENT (No such file or directory)

pickup-reply.strace
3875321 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbox>
3875321 newfstatat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/etc/courier/policy.toml", {st_mode=S_IFREG|0600, st_size=42, ...}, AT_SYMLINK_NOFOLLOW) = 0
3875321 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/etc/courier/policy.toml", O_RDONLY|O_CLOEXEC) = 4<<disposable-courier-root>/etc/courier/policy.toml>
3875321 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/outbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/outbox>
3875321 openat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-e7b247b6ef8779207e31f708b29c695d.tmp", O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC, 0640) = 5<<disposable-courier-root>/srv/courier/outbox/.fm-pickup-e7b247b6ef8779207e31f708b29c695d.tmp>
3875321 renameat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-e7b247b6ef8779207e31f708b29c695d.tmp", 4<<disposable-courier-root>/srv/courier/outbox>, "fm-577f0c70954198e6d4db5f6db2ef6661dd94c95f0b5ef998.json") = 0
3875321 unlinkat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-e7b247b6ef8779207e31f708b29c695d.tmp", 0) = -1 ENOENT (No such file or directory)
3875321 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbox>
3875321 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbound", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbound>
3875321 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbox>

pickup-settle.strace
3875802 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbox>
3875802 openat(4<<disposable-courier-root>/srv/courier/inbox>, "result-fm-577f0c70954198e6d4db5f6db2ef6661dd94c95f0b5ef998.manual.json", O_RDONLY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC) = 5<<disposable-courier-root>/srv/courier/inbox/result-fm-577f0c70954198e6d4db5f6db2ef6661dd94c95f0b5ef998.manual.json>
3875802 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/inbound", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/inbound>
3875802 openat(AT_FDCWD<~/.no-mistakes/worktrees/8352e211db7e/01M4FHFHD0EJJR9Z226W7BADVC>, "<disposable-courier-root>/srv/courier/outbox", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = 4<<disposable-courier-root>/srv/courier/outbox>
3875802 openat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-676a6531b23b2fa48c681d5f379db123.tmp", O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC, 0640) = 5<<disposable-courier-root>/srv/courier/outbox/.fm-pickup-676a6531b23b2fa48c681d5f379db123.tmp>
3875802 renameat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-676a6531b23b2fa48c681d5f379db123.tmp", 4<<disposable-courier-root>/srv/courier/outbox>, "st-000000000003-3cb159b8822d9c90.stage.json") = 0
3875802 unlinkat(4<<disposable-courier-root>/srv/courier/outbox>, ".fm-pickup-676a6531b23b2fa48c681d5f379db123.tmp", 0) = -1 ENOENT (No such file or directory)
Evidence: Disposable user-namespace capability check

Source: Disposable user-namespace capability check

unshare: write failed /proc/self/uid_map: Operation not permitted

unshare: write failed /proc/self/uid_map: Operation not permitted

Pipeline

Updates from git push no-mistakes

... (13 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

⚠️ **Review** - 1 error

🔧 Fix applied.
13 issues (10 errors, 3 warnings) still open:

  • 🚨 bin/fm-courier-pickup.py:562 - Queued replies can disclose the previous captain's conversation to a replacement recipient. Claim a private reply for captain A, leave it queued behind another text, then change policy.toml to captain B: send() reads B's number and publishes A's reply to B. The courier authorizes that request as a current-captain send. Related sites: bin/fm-courier-pickup.py:220-224 reuses the conversation binding without recipient identity; :283-291 reads the current recipient; :497-502 queues text without its authorized recipient; :549-552 also accepts receipts by ID without matching content digest. The source does not establish that recipient replacement authorizes transferring an existing conversation. Decide that policy; preventing transfer requires durable recipient binding, so the remedy needs authorization.
  • 🚨 bin/fm-courier-pickup.py:539 - The vote watch starts when the question is claimed, before it is delivered. If the courier is unavailable for over a day, replies() deletes the watch while the question remains queued; after recovery the courier sends a fresh poll, but its votes are silently discarded. Related sites: bin/fm-courier-pickup.py:514-515 expires the watch; :537-539 registers it at claim time; :566 publishes the question later; :569-584 settles delivery without starting the watch; :377-379 ignores votes after removal. Keep undelivered question watches and start their expiry when the poll's sent receipt is confirmed, matching the direct bridge's delivery-based watch.
  • ⚠️ bin/fm-courier-pickup.py:574 - The 300-second consumed-without-receipt fallback can release later reply portions while the first remains deliverable. The courier consumes reply A, records an unknown provider result, and continues retrying it; pickup then drops A after 300 seconds and publishes B. B can arrive first, followed by A after recovery, and A's eventual receipt is never applied. Related sites: bin/fm-courier-pickup.py:98 defines the timeout; :550-552 ignores nonterminal receipts; :571-575 advances the queue; :583-586 skips the abandoned reply's stage. The courier's daemon.py ACTIVE/advance paths retain and retry unknown jobs. No intent requirement needs this blind advancement component; remove it and retain ordering until a terminal receipt settles the request.
  • ⚠️ bin/fm-courier-pickup.py:366 - The latest change deliberately changes failure-reaction ordering despite the required criterion that 'reactions must keep working exactly as today.' The hunk calls move(identity, 'failed') immediately after queuing NOT_TEXT. If an earlier text is awaiting its receipt, the failure notice remains queued while signal() publishes the failed tapback. The direct bridge advances this mark only after sending the notice, and the referenced courier wire likewise specifies failed after notification. Related sites: bin/fm-courier-pickup.py:421-423 applies the same change after capture exhaustion; :571-573 holds queued notices; :624 independently publishes stages; tests/fm-courier-pickup-cases.py:321 and :338 require early failure stages. Obtain an explicit amendment permitting early failure reactions, or preserve notice-before-reaction ordering.
  • 🚨 bin/fm-courier-pickup.py:574 - Round 1's R9 fix leaves a permanent receipt-loss sibling: publish reply A, stop pickup before it observes A's sent receipt, and restart after the courier prunes receipts one day later. A remains published in pickup state, so send() never republishes it and blocks every later reply indefinitely. The courier explicitly supports same-ID resubmission to regenerate pruned receipts (receipt contract). Related sites: bin/fm-courier-pickup.py:501 persists publication state; :564 limits publication to the first attempt; :572-574 waits indefinitely; :575 requires a terminal receipt to advance. Preserve strict ordering and recover the receipt using the existing request ID. The remedy adds a receipt-recovery retry path, which needs authorization.
  • 🚨 bin/fm-courier-pickup.py:537 - The criterion 'automatic owner replies, polls, attachments and reactions must keep working exactly as today' conflicts with combining question text and its optional poll through self.owe(..., offered) at :537-538. If question text succeeds but Linq refuses native polls with 402/403, the courier records unknown and retries the poll indefinitely; pickup blocks every subsequent answer. The direct bridge drops the refused poll and continues (existing behavior); the courier propagates poll errors into unknown (courier send). Round 1's R9 fix makes this blockage permanent. Related sites: bin/fm-courier-pickup.py:567-568 emits the combined request; :554 excludes unknown receipts; :573-574 blocks later texts; :585-586 also withholds the question reaction. Restore independent settlement of confirmed question text and best-effort poll failure through the courier contract, preserving strict reply ordering.
  • 🚨 bin/fm-courier-pickup.py:587 - The R11/R12 fix round treats every unknown poll-bearing receipt as confirmed question-text delivery. But the courier emits unknown when the text send itself fails: notify.py:347 propagates that failure through daemon.py:152-154. If question A fails before delivery, pickup removes A, records completed playback, and releases B while the courier continues retrying A; B can arrive first. Related sites: bin/fm-courier-pickup.py:560-563 selects unknown; :590 removes the request; :593 starts its watch; :596-597 records completion; :600-601 advances its reaction. The approved rule assumes confirmed text and an uncertain poll, which this receipt cannot distinguish. The remedy needs authorization to extend the courier receipt with confirmed-text evidence or amend the approved unknown-settlement rule.
  • 🚨 bin/fm-courier-pickup.py:480 - The earlier R5/R6 flag fix introduces an association before a question is published. Let ordinary reply X wait on an unknown receipt, then claim question A behind it. A immediately awaits an answer. When unrelated text C arrives, answered() links A to C even though the captain has never received A's question. C's eventual final answer marks A done without an answer to A. Related sites: bin/fm-courier-pickup.py:435 links every captured text; :539-543 flags and queues the unpublished question; :458 preserves the erroneous link; :469-474 propagates C's terminal stage; :600-601 cannot correct A when its question settles. At the shared answered boundary, exclude questions still queued and unpublished when associating ordinary text answers, while preserving the awaiting flag, established followers, and receipt-independent handling of published questions.
  • 🚨 bin/fm-courier-pickup.py:481 - The R13/R14 fix round leaves unsent-question association siblings. Claim question A, then let outbox publication fail: send() persists published before publish() succeeds, so answered() considers A eligible and links an unrelated text C to it. Alternatively, a terminal denied-limit receipt removes A from the queue while retaining awaiting_answer, producing the same association. C's final answer then marks A done although the captain never received its question. Related sites: bin/fm-courier-pickup.py:545 sets the claim-time flag; :588-590 records publication before it succeeds; :594 and :607-608 discard refused questions without retiring answer eligibility; :437 associates captured texts; :471-476 propagates their completion. Record successful publication only after publish returns, and retire answer eligibility on terminal non-delivery before discarding the question, preserving established follows links and monotonic stages.
  • 🚨 bin/fm-courier-pickup.py:363 - A temporary I/O failure permanently drops valid inbound messages or votes. For example, listing finds sequence 41, but reading it raises EIO; this handler persists cursor=41 without filing it. After recovery or restart, the cursor filter excludes that record forever. Related sites: bin/fm-courier-pickup.py:336 opens the spool; :160-167 opens, inspects and reads records; :365-366 advances and saves the cursor; :351 excludes skipped sequences. Distinguish terminal record refusals from operational OSErrors, and propagate operational failures to the existing tick handler at :630 so the cursor remains unchanged and the next tick retries.
  • 🚨 bin/fm-courier-pickup.py:205 - The added len(record[&#39;transcript&#39;]) &gt; 16000 refusal changes the required existing inbound behavior. A valid 0640 courier record containing 16,001 ASCII characters fits the wire's 128-KiB limit, but pickup rejects it and permanently advances its cursor. The direct bridge attempts capture and sends FAILURE when the transport refuses; pickup never sends that notice or its failed acknowledgement. Related sites: bin/fm-courier-pickup.py:369-373 passes the refused record; :404-440 contains the bypassed failure handling. This contradicts the criterion that owner-only inbound must “keep working exactly as today.” Remove the pickup-only character cap, retaining the byte limit and existing capture failure handling. Sources: inbound contract, direct bridge.
  • ⚠️ bin/fm-courier-pickup.py:121 - Simplification: the added terminal-result aliases failed and refused exceed the courier contract; its terminal failures are denied, denied-limit, and closed. No intent requirement needs this extra acceptance path. Remove those two aliases. Related sites: bin/fm-courier-pickup.py:571 recognizes them as terminal; :599-603 discards their queued text and retires question eligibility; :616 removes their poll watch. Source: courier terminal results.
  • 🚨 bin/fm-courier-pickup.py:487 - The R14/R15 fix rounds (bd1cb6b/b008343) left a multi-question sibling. Deliver Q1, then queue Q2 on the same request behind an unknown receipt. A text answer to Q1 is captured, but Q2 puts the original message in this unpublished set, preventing its follower link; the answer's final reply never marks the original done. Alternatively, refusing Q2 clears Q1's answer eligibility, so a valid Q1 vote loses the same link. The transport permits successive nonfinal questions with distinct bindings. Related sites: bin/fm-courier-pickup.py:490-493 filters text answers; :550-558 shares the awaiting flag across questions; :601-602 clears it on refusal; :393-400 still accepts Q1's vote; :443 resolves captured answers; :476-483 propagates completion only through established links. Preserve eligibility for an earlier published question when excluding or retiring a later one, using existing mark, queue and watch evidence.

🔧 Fix applied.
1 error still open:

  • 🚨 bin/fm-courier-pickup.py:562 - Queued replies can disclose the previous captain's conversation to a replacement recipient. Claim a private reply for captain A, leave it queued behind another text, then change policy.toml to captain B: send() reads B's number and publishes A's reply to B. The courier authorizes that request as a current-captain send. Related sites: bin/fm-courier-pickup.py:220-224 reuses the conversation binding without recipient identity; :283-291 reads the current recipient; :497-502 queues text without its authorized recipient; :549-552 also accepts receipts by ID without matching content digest. The source does not establish that recipient replacement authorizes transferring an existing conversation. Decide that policy; preventing transfer requires durable recipient binding, so the remedy needs authorization.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 17 of 18 scenarios driven live against the product
Scenario Result Live Evidence
Start pickup without opt-in, without an explicit home, with a writable policy, or alongside another instance; each unsafe invocation is refused. ✅ pass live Targeted CLI transcript:2; Manual CLI exchange and guard refusals:2,7,8.
Publish ordered inbound records and restart pickup; each message becomes exactly one conversation turn and wake with its transcript and attachment reference preserved. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:3; captured-input artifact preserves the attachment reference.
Submit duplicate, malformed, oversized, incorrectly permissioned and linked records; pickup refuses them and continues to subsequent valid records. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:5,6.
Encounter transient message or vote I/O errors and restart; the cursor remains unchanged and capture retries, while vanished records are passed. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:4,6,9.
Accept a message and publish owner replies; numbered questions carry native poll options, and sent receipts advance playback and monotonic reaction stages. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:7,13,22; outgoing-reply and reaction artifacts.
Vote on a watched question after restart; the first nonempty choice carries its question binding, empty selections preserve the watch, and unrelated or expired votes are ignored. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:8,10,11,33; lifetime cases execute the CLI with an advanced clock.
Answer before or alongside the question receipt, then send unrelated text and late progress; the original follower remains linked and reaches done without stage regression. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:12–19.
Publish successive questions, leaving the second queued or refused; an answer to the first retains its binding and completes the original message. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:20,21.
Send unrelated text while a question is queued, publication fails, or delivery is refused; the undelivered question does not acquire that text's completion chain. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:26–29,32.
Return unknown, pending or unsupported receipt results; later replies stay ordered until a contract terminal receipt, and refused polls lose their vote watch. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:24,25,30,31.
Consume an outgoing request and lose its receipt; pickup republishes identical bytes with the same ID at the bounded retry interval. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:23; retry timing uses the CLI with an advanced clock.
Submit empty text, missing attachments, unavailable-session input or a wire-valid 16001-character transcript; normal notices and failed acknowledgements occur, with no synthetic vote reaction. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:34–37.
Publish records while pickup runs continuously; every sampled record reaches the wake queue in under one second. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:38,39: 0.826, 0.581, 0.646, 0.653 and 0.622 seconds.
Block the conversation transport during capture; pickup sends one failure notice before its failed stage within the pickup deadline. ✅ pass live tests/fm-courier-pickup.test.sh; Targeted CLI transcript:40,41: failed stage after 85.8 seconds.
Run pickup against read-only inbound and receipt directories; courier files remain unchanged, credential and ledger files are never opened, and outgoing requests are Firstmate-owned 0640 files. ✅ pass live Observed courier file operations; captured-exchange metadata comparisons; Manual CLI exchange and guard refusals:6.
Restart the owner conversation session and attempt unauthorized publications; durable recovery works and session, principal and destination restrictions remain enforced. ✅ pass live tests/fm-inbox-conversation.test.sh; Targeted CLI transcript:47,50–54.
Drain concurrently appended wakes and interrupt handling; durable records remain available until post-handling acknowledgement. ✅ pass live tests/fm-wake-queue.test.sh; Targeted CLI transcript:73,132; cases launch the real wake-drain executable.
Exchange real captain iMessages, media and votes through an isolated courier with separate Unix accounts; sender filtering, delivery and both account boundaries hold. ⏸️ untested no Drove real Firstmate entry points against disposable courier wire records and audited file operations, but those fixtures cannot establish actual courier/Linq behavior. Attempting a disposable user na…
  • TMPDIR="$PWD/.test-phase-tmp" PYTHONDONTWRITEBYTECODE=1 bin/fm-test-run.sh --jobs 1 tests/fm-courier-pickup.test.sh tests/fm-inbox-conversation.test.sh tests/fm-wake-queue.test.sh
  • Ran ~/.no-mistakes/evidence/01M4FHFHD0EJJR9Z226W7BADVC/manual-pickup-check.py through a disposable Bash session-owner fixture; corrected fixture shell lifetime and umask, then reran successfully.
  • Traced real bin/fm-courier-pickup.py once processes using strace -f -qq -yy -e trace=%file,%creds; captured public CLI input, outbox requests, stages and unchanged courier-file metadata.
  • unshare --user --map-root-user -- true — UID-map permission denied.
  • Checked completed transcripts for failures and skips, removed disposable test scratch, and verified git status --short was clean.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The courier now publishes each owner message and native-poll vote as an
immutable record in its own /srv/courier/inbound spool instead of running
fm-inbox.sh across accounts inside its sandbox, which the VM rehearsal
showed cannot work. bin/fm-courier-pickup.py is Firstmate's side of that
wire: it reads the spool read-only as Firstmate, refuses malformed or
duplicate records, files each message into the conversation transport
exactly as the direct iMessage bridge does (same turn and request ids,
transcript, attachment notices, bound poll votes), keeps its cursor in
Firstmate's own state, and answers only through Firstmate-owned outbox
requests: replies, numbered-question polls and ordered, never-backwards
stage requests. It is off unless FM_NOTIFY_COURIER=1, so the direct bridge
stays the only path until activation. Measured pickup, rename to queued
wake: 0.60-0.74 s.

With no cross-account writer left, retire the staged shared interface
(fm_shared_interface.py and its ACL/template copying in the conversation
transport and the wake library, including the permission-denied liveness
and rejected-template lock plumbing that existed only for that peer) and
its tests.
Match the courier's ids-only ledger and its final stage ordering wording.
@Mauryanx Mauryanx closed this Oct 9, 2026
@Mauryanx Mauryanx reopened this Oct 9, 2026
@Mauryanx Mauryanx closed this Oct 9, 2026
@Mauryanx Mauryanx reopened this Oct 9, 2026
…dline

The courier wire (firstmate-voice PR 47) requires the filed or failed stage within PICKUP_DEADLINE=120 s of reading a message. A message that is not filed is now staged failed as soon as it is given up instead of after the courier's receipt for its notice, stage publication no longer waits behind an unsendable text, transport calls are bounded at 10 s, and replies are not read while a capture waits to retry, so the worst case is about 75 s (measured 75.6 s with a hung transport).
@Mauryanx Mauryanx changed the title feat(bin): add Firstmate-owned courier iMessage pickup feat(bin): add courier spool pickup for iMessage conversations Oct 9, 2026
@Mauryanx
Mauryanx merged commit 92c1d08 into main Oct 9, 2026
20 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