Skip to content

feat(desktop): confirm send_message from the Messages history - #21325

Open
sauranshbhardwaj wants to merge 11 commits into
BasedHardware:mainfrom
sauranshbhardwaj:feat/desktop-send-message-check
Open

sauranshbhardwaj wants to merge 11 commits into
BasedHardware:mainfrom
sauranshbhardwaj:feat/desktop-send-message-check

Conversation

@sauranshbhardwaj

@sauranshbhardwaj sauranshbhardwaj commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What changed and why

Today send_message reports "status": "sent" as soon as Messages accepts the send. Nothing checks whether Messages actually recorded the message as sent or delivered, so a message Messages marked "Not Delivered" still reads as sent to the model and to the person.

This PR makes send_message check its own send in the Messages history and report what it finds. It is piece 2 of #21162, in its narrow form: it checks only messages Omi sends through send_message. Drafts the person sends themselves are not checked. It builds on the device tool surface from #20938.

The check reuses MessagesReaderService, the reader behind read_message_history and list_message_chats. It adds two reads to that reader, a baseline row id and the outgoing rows after it, and it shares one chat lookup with read_message_history. There is no second reader.

Results

status When ok Ledger
delivered The one new matching row without an error is marked delivered. true succeeded
sent That row is marked sent, not yet delivered, when the check ends. true succeeded
not_delivered Every new matching row carries an error. false (reason: not_delivered) failed
unconfirmed No new matching row, a row not yet marked sent, two identical new rows, a recipient that matches two different stored numbers, or a history that could not be read (for example, no Full Disk Access). true, with outcome_unknown: true outcome_unknown
cancelled The tool call was stopped before Messages was asked to send. Nothing was sent. false (reason: cancelled) failed
  • unconfirmed is an unknown outcome, never a failure and never a success. The result has no error key, and it carries outcome_unknown: true, which makes the kernel record the invocation as outcome_unknown (error code executor_reported_outcome_unknown) instead of succeeded. Later operation receipts therefore never describe an unconfirmed send as done. Only send_message can lower its own success this way, and only with the exact boolean. An unconfirmed_reason says why: no_matching_row, not_yet_sent, ambiguous, ambiguous_recipient, history_unreadable or check_stopped.
  • Every result has a summary that says in plain words what happened. For unconfirmed it says the message may or may not have been sent, not to send it again, and to ask the person to check Messages first.
  • When an error row came first and another attempt then went through (for example an iMessage error and then the text-message fallback), the result is delivered or sent with succeeded_after_error: true, and the summary says so.
  • A send that times out is checked the same way. The result carries send_timed_out: true and the checked status, instead of the old "may or may not have been sent" error. Its service is only what the history shows (the service of the row that confirmed it, otherwise empty), and attached_file is empty because whether an attachment went is unknown.
  • Omi never claims sent without a row marked sent.
  • The send is never retried or repeated, whatever the history shows. A stop that arrives before Messages is asked to send means nothing is sent, and the executor returns cancelled with "The send was stopped before Messages was asked to send it. Nothing was sent." instead of raw error text. When the stop also ends the owner-bound run, the kernel's existing owner-changed result is what reaches the model, as for every physical tool.

How the check finds Omi's message

Before sending, the check records the newest message.ROWID. Afterwards a row counts only when all of these hold:

  • its row id is above that baseline, so no message that existed before the send can count;
  • it is outgoing (is_from_me = 1);
  • it is in a direct chat with the recipient: a chat whose only participant is that handle. A person can have an iMessage chat and a text-message chat, and both count. Group chats, other people's chats and the same person's email chat do not;
  • when first seen, it is dated no more than 1 second before the send started. The row id baseline already excludes older rows, so this only stops an old message that syncs in from another device during the window from counting. A row that matched once is then tracked by row id, so a later change to its date cannot drop it;
  • its text equals what was sent after normalising: line endings unified, runs of spaces and tabs collapsed, each line and the whole text trimmed. The body is read from text, or from attributedBody when text is empty, as the reader already does.

The check decides over every new matching row. Rows with an error are failed attempts. Exactly one row without an error is Omi's message. Two or more rows without an error mean an identical message went to the same person at the same moment, and the result is unconfirmed / ambiguous.

Accepted limit. An identical message sent to the same person from another of the person's devices (their phone, say) within about a second of Omi's send lands above the baseline, inside the tolerance, and can confirm Omi's send even if Omi's own send wrote nothing. That needs the same text to the same person in the same second, and it is the residual this design accepts.

Handle matching. Contacts gives "(267) 361-8369" while Messages stores "+12673618369". Two phone numbers name the same handle only when:

  • both are international (+, 00, or 011 with a North American region) with equal digits, or both national with equal digits; or
  • one is a national number that, after an optional trunk prefix, is the other international number without a real calling code.

The trunk prefixes come from a small table: "0" in most places, "8" in Russia, Kazakhstan, Tajikistan and Turkmenistan, "8 0" in Belarus, "8" or "0" in Lithuania, "06" in Hungary and "1" in North America.

Which calling codes count. Messages resolves a national number through the sending account, not the Mac's region:

  • When the chat's sending account is a phone number (chat.account_login "P:+1…"), only that account's calling code counts.
  • Otherwise every code in the ITU table counts. The Mac's region is one of them, but it is not preferred. Letting the region pick would turn a two-way match into a confident wrong answer, which is what the live test showed: a Mac set to India texting a US number typed nationally ("2673618369") read as unconfirmed, because the region rule tried +91 while Messages had sent to +1.
  • Stores without the account_login column fall back to every code.

If the request matches two different stored numbers, the check does not choose between them: it reports unconfirmed / ambiguous_recipient. Examples: "+12673618369" and "+912673618369" both present with no phone account to decide, or, in Italy, "06 1234 5678" against both "+390612345678" and "+39612345678". Two different international numbers never match, so "+14155550123" and "+914155550123" stay two people.

Extensions ("ext. 45", ";45", "x45") and a "(0)" inside an international number ("+44 (0) 7700 900123") are ignored. Argentine mobiles written nationally with the "15" mobile prefix are not rewritten to the international "9" form, so they read unconfirmed. Email addresses compare without case.

The calling codes come from a table of ITU calling codes in MessagingHandle.swift, checked against every ISO region macOS knows and tested for duplicate regions. Regions with no calling code of their own (uninhabited territories, "001") have none.

What changed for read_message_history. It now uses the same lookup. Before, it matched the stored handle string exactly. Now it also matches the other spellings of the same number (national and international forms, by the rule above) and emails without case. So:

  • a handle spelled the Contacts way finds the chat, including a national number behind a foreign calling code;
  • an exact request such as "+14155550123" can read a more recently active direct chat stored as "4155550123";
  • "sam@example.com" reads the "Sam@Example.com" chat.

The direct chat still wins over a group. Approval is still keyed on the requested handle string.

The window

The check reads every half second, for up to 5 seconds after the send returns. It stops early in one case only: when the reads have shown the same single delivered row, and no error row, for at least 1 second. At the half-second interval that is three agreeing reads (at 0, 0.5 and 1 s), so an online recipient answers in about 1 to 3 seconds. Everything else reads the whole window: a sent-but-not-delivered row, an error (a text-message fallback may follow), a row not yet sent, or no row. A second identical row before the delivery settles makes the result ambiguous. A failed read breaks a run of agreeing reads.

Messages writes the outgoing row when it accepts the send. It sets is_sent once the service takes the message, usually within a second for iMessage. It sets is_delivered when the recipient's device acknowledges it, usually a second or two later when that device is online. A recipient whose device is offline gets sent, which is true. Errors that Messages records only after a long service timeout show as sent, which was true when the check ended.

A failed read during the window (a locked store, a checkpoint) is retried on the next tick. The outcome is history_unreadable only if no read after the send works. The reader also waits up to 0.25 s for a lock before failing. The check runs off the main actor: the reads go through the reader actor, and the wait is an async sleep. A stopped tool call ends the check with unconfirmed / check_stopped.

History columns used

The new reads use columns the reader already relies on, plus four: message.is_sent, message.is_delivered, message.error and message.service. They also use message.ROWID, is_from_me, date, text and attributedBody, plus chat_message_join, chat_handle_join and handle.id. message.date is nanoseconds since 2001-01-01 UTC, which the reader already converts. If a future Messages schema drops a column, the read fails as store_read_failed and the result is unconfirmed.

Privacy

  • Before the send, the check reads only the newest row id. After the send, it reads only rows newer than that, in that one person's direct chats.
  • Text comparison happens inside the reader. Only the row id, service and delivery state leave it, never the text or the handle. The handle comparison runs inside SQLite, so only matching chat ids come back.
  • The log line records only the outcome code, the reason code, whether the send timed out or followed an error, the number of checks and the time waited. It never records the text, the handle or row contents. The existing argument redaction for send_message is unchanged.

Model guidance

The send_message guidance in omi-tool-manifest.ts now tells the model to report the status in plain words, never to send again on its own, to ask the person to check Messages before anything is sent again when the result is unconfirmed, and to send again after not_delivered only if the person asks. The generated surfaces and the fixture are regenerated with scripts/generate-tool-surfaces.mjs.

Product invariants affected

  • INV-INT-1 (product/invariants/integrations.md) is named because MessagesReaderService.swift matches **/*ReaderService*. The result comes from a deterministic read of the store, covered by fixture tests. Matching a person's chats depends on the chat's sending account and on a table of calling codes and trunk prefixes. Where that cannot decide between two stored numbers, the check reads unconfirmed rather than guessing.
  • INV-AGENT-* (product/invariants/agent-control-plane.md), Authority: desktop/macos/agent/src/runtime/** changes, and the ledger is affected. A Swift executor result that carries outcome_unknown: true now completes its dispatched invocation as outcome_unknown (error code executor_reported_outcome_unknown) instead of succeeded, through finalizedToolInvocationLedgerOutcome in relay-tool-result.ts and completeToolInvocation. The flag is honoured only for send_message and only as the exact boolean true. It can only lower a success to unknown. A failure stays a failure, and capability checks run exactly as before completion. Policy and approval are unchanged, and send_message stays never_auto_retry. Guard test: run-tool-capability.test.ts covers completion as outcome_unknown under live authority; relay-tool-result.test.ts and conversation-operations.test.ts cover the mapping and the receipt.
  • INV-CHAT-1 (product/invariants/chat-continuity.md) is named for the same glob and is unaffected: the result is an ordinary tool result in the kernel's invocation ledger, and no transcript or second store is added.

How it was verified

  • xcrun swift build -c debug --package-path Desktop --build-tests: passes.
  • Focused tests, all passing:
    • MessageSendConfirmationTests (52), MessagingHandleTests (21), SendMessageResultTests (8) and LockedCounterTests (2);
    • KnowledgeGraphPaginationTests (11) and RealtimeHubResumeOnceGateTests, which now use the shared counter.
  • scripts/test-tool-surfaces.sh: generate-tool-surfaces --check reports all outputs match; agent vitest 72 files, 905 tests passed (including the ledger tests); pi-mono-extension 143 passed. tsc --noEmit is clean.
  • swift-format lint and SwiftLint (0 violations) are clean; the desktop test-quality ratchet is at baseline; desktop-flow-lint passes.
  • Whole Swift suite at fb32c6628a (swift test --skip SubscriptionEntitlementServiceTests in desktop/macos/Desktop): 7959 tests, 10 skipped, 2 failures, both known and unrelated to this change: DesktopAutomationWindowPresentationTests.testQuietPresentationAlsoParksNonactivatingOverlayPanels (display geometry) and RecentScreensMenuContentTests.testDayOldFrameShowsACalendarDateNotAClockTime (an Indian locale date format).
  • Mutation testing: last run at the previous round, before the account-based matching. Of 46 Swift mutants of the check, the reader, the matcher and the result, all 46 were killed, and so were 3 TypeScript mutants of the ledger mapping. It was not rerun for the matching change in fb32c6628a; that change is covered by the new tests listed below.
  • The pre-push gate, run locally against upstream at fb32c66 with this body: PRE_PUSH_BASE_REMOTE=upstream OMI_PR_BODY_FILE=<this body> scripts/pre-push-singleflight upstream. It reports "PR preflight passed: 33 checks" and "Bounded pre-push gate passed".

Packaged-app check (to run by hand). On a packaged build of this branch with Full Disk Access and Automation for Messages granted:

  1. Ask Omi: "Send an iMessage to saying: testing the send check".

  2. Approve the send.

  3. After a second or two, expect the reply to say the message was sent and delivered. The tool result shows "status": "delivered".

  4. Optional: turn off Full Disk Access for the build, relaunch it, and send again. Expect "status": "unconfirmed" with history_unreadable, and a reply asking you to check Messages before anything is sent again. The message is still sent once.

  5. Optional, if you have a number that is not on iMessage: send to it with the service left unset. If Messages records an iMessage error and then sends a text message, expect delivered or sent with succeeded_after_error: true. Whether real Messages writes that error row first has not been observed yet.

  6. If your Mac's region differs from your number's country (for example an Indian locale and a US number), ask Omi to text your own number typed without the country code. Expect delivered.

A person ran step 1 on an earlier build of this branch (256163fe5c), on a Mac with an Indian locale, texting their own US number typed nationally. Messages delivered it (the thread showed "Read"), but the check reported unconfirmed / no_matching_row, because the region rule tried +91. The account-based matching above fixes that, and the case is now a test. Rerun on the current build (fb32c6628a, packaged together with the approval-card stack so send_message can run): same Mac, same number typed nationally, Allow Once on the card, and the reply said it was marked delivered. The log line was send_message outcome=delivered reason=none timed_out=false after_error=false checks=5 waited_ms=2377. Steps 4 to 6 have not been run.

Tests

MessagesHistoryFixture (in Tests/Support) builds a store shaped like the Messages history inside each test. Like the real store, it runs in WAL mode, keeps one handle row per address and service, and gives one person separate iMessage and text-message chats. It encodes attributedBody the way the reader decodes it, including bodies of 128 bytes or more. The real reader is pointed at it. The fake send writes the rows Messages would write. An injected clock and sleeper drive the poll, so no test sleeps on the wall clock.

MessageSendConfirmationTests covers:

  • Outcomes:
    • delivered;
    • a clean delivery stopping after it has held for a second (3 checks, 1000 ms) and reporting its service;
    • a delivery that arrives late still waiting a settled second;
    • sent but not delivered, and no row, reading the whole window;
    • delivery marked during the poll;
    • error only as not_delivered after the full window;
    • an error followed later by a text-message success as delivered with succeeded_after_error, service "SMS", after the full window;
    • an error beside a sent text message as sent, and beside an unsent row as unconfirmed;
    • a row not yet marked sent as unconfirmed, never sent.
  • Several new rows: delivered beside sent, two delivered, identical rows in the iMessage and text-message chats, and a second identical row arriving before the delivery settles are all ambiguous. A recipient that matches two different stored numbers (region IT, "06 1234 5678" against "+390612345678" and "+39612345678") is ambiguous_recipient.
  • Only this send's rows count:
    • an older identical row;
    • an identical row written half a second before the send;
    • three identical sends in a row, each judged only by its own row;
    • an old row that syncs in during the window;
    • a row half a second early counts, one past the tolerance does not, and the tolerance is pinned at 1 second or less;
    • a row that matched and is then re-dated stays counted.
  • Which chats count: the same person's text-message chat counts. These do not: another person's chat with identical text (+91… beside +1…), the same person's email chat, a group chat with the recipient, and an incoming identical row. A handle spelled the Contacts way finds the chat.
  • National numbers and the sending account:
    • a Mac set to India sending to a US number typed nationally confirms against the +1 chat;
    • the same request with both a +1 and a +91 chat present is ambiguous_recipient;
    • a phone sending account ("P:+1…") decides between them and confirms.
  • Texting yourself: the incoming self-copy (is_from_me = 0) is ignored, so the outgoing row decides. An incoming copy alone never confirms.
  • Text: line-ending and whitespace normalisation; different text; attributedBody-only bodies of 9, 127, 128, 180 and 520 bytes, including emoji.
  • Unreadable history:
    • a missing store gives history_unreadable with no checks and still sends once;
    • a failed read mid-poll is retried and the send confirms;
    • every post-send read failing gives history_unreadable.
  • Never retried, and stopping:
    • a failed send is not retried, and only the baseline read runs;
    • a stop during the baseline read sends nothing;
    • a stop between polls ends the check through the loop's own cancellation check, with reads that ignore cancellation;
    • a stop during the sleep, or surfacing from a read, ends the check;
    • a timed-out send is still checked and sent once, while other send errors are not treated as timeouts.
  • Probe: DeviceToolsProbe.sendCheckDetail runs the check against the absent store with a stand-in send and a clock that moves only with its sleeps, and reports unconfirmed / history_unreadable with one send. It runs nothing real. The device-tool-surface e2e flow asserts the same keys and covers the new files. The flow itself was not run, because that needs a running app.

MessagingHandleTests covers:

  • international pairs never matching: +1 vs +91, +1 vs +61, +44 vs +244, and two international forms compared through the region;
  • a phone sending account limiting national numbers to its code: with a US account, an 8-digit number does not match Hong Kong, Singapore or US numbers, and "(415) 555-0123" does not match +91…;
  • AU 04… never matching Bermuda +1441…;
  • without a phone account, any real calling code counting: the live India/US case, the same digits behind +91 (hence the ambiguity rule), and no match through a code that does not exist;
  • reading the account's calling code from "P:+…" logins, and none from email or national logins;
  • real matches: a US national number vs +1, an Indian 0-prefixed number vs +91 with the region set to IN (and not with US), UK, Italian landlines that keep their 0, 00 dialling, emails without case, and short codes;
  • trunk prefixes other than 0: Russia "8", Belarus "8 0", Lithuania "8" and "0", Hungary "06", North America "1", and "8" not being a trunk prefix behind a UK account;
  • North American "011" dialling, and "011" staying national in Japan;
  • "+44 (0) …" spellings, and extensions written "ext.", ";" and "x";
  • the documented Argentine "15" limit;
  • the Italian two-number case and the distinct-address check behind ambiguous_recipient;
  • the calling-code table, including Ascension (+247), no code for "001", and no region listed twice;
  • read_message_history finding the chat the send check uses, still preferring the direct chat, reading the more recent chat stored in national form for an exact international request, and matching emails without case;
  • a locked store making the reader wait out its busy timeout instead of failing at once.

SendMessageResultTests covers:

  • ok, status, reason, outcome_unknown and unconfirmed_reason for each outcome and all six reasons;
  • the plain-words summaries and succeeded_after_error;
  • timed-out results: ok true when confirmed or unknown, send_timed_out, and a service that is only what the history showed, with no attachment claimed;
  • a stop before the send giving cancelled with "Nothing was sent" and no raw error text, and other failures keeping their reasons without raw text.

LockedCounterTests covers the shared counter. KnowledgeGraphPaginationTests and RealtimeHubResumeOnceGateTests now use it in place of their private copies and still pass.

Agent vitest:

  • relay-tool-result.test.ts:
    • an unconfirmed send becomes outcome_unknown and keeps its summary;
    • delivered stays succeeded and a failure stays failed;
    • a non-boolean flag is ignored;
    • another tool's flag is ignored.
  • run-tool-capability.test.ts covers completion as outcome_unknown under live authority.
  • conversation-operations.test.ts covers the receipt.

UI evidence

UI-Evidence: none -- tool result, ledger status and model guidance only; no UI changes

Failure class (fixes)

Failure-Class: none

View guided diff

After Omi sends a message, read the outgoing row back from the Messages history through the existing reader and report delivered, sent, not_delivered or unconfirmed. Only rows in the recipient's direct chat created after the send started count, rows that existed before the send are excluded, the check polls for at most five seconds, and the send is never retried. Unknown outcomes are reported as unconfirmed rather than failed, and only the outcome code and timing are logged.
Update the send_message manifest guidance so the model reports delivered, sent, not_delivered and unconfirmed in plain words, never sends again on its own, and asks the person to check Messages before anything is sent again when the outcome is unconfirmed. Regenerate the tool surfaces.
The hermetic device tools probe now runs the send check with a stand-in send against the absent Messages store and reports an unconfirmed, history_unreadable outcome with exactly one send. The device-tool-surface flow asserts it and covers MessageSendConfirmation.swift.
…decide over the whole window

Two phone numbers now match only when their digits are equal or one is the international form of the other's national number in the Mac's region, and read_message_history and the send check share one chat lookup. The check records the newest message row before sending, counts only rows past it dated at most a second before the send, reads for the whole window and decides over every new row, so an iMessage error followed by a text-message success reads as sent and two identical new rows stay ambiguous. A stop before the send sends nothing, a failed read is retried on the next tick, the reader waits out a brief lock, and a timed-out send is checked instead of reported as a failure. The test fixture now runs in WAL mode with one handle per service, a text-message chat beside the iMessage chat, long attributed bodies and other people's chats, and the probe's send check no longer runs anything real.
…ool ledger

A Swift executor result that carries outcome_unknown true now completes its invocation as outcome_unknown with error code executor_reported_outcome_unknown instead of succeeded, so later operation receipts never describe an unconfirmed send as done. The flag can only lower a success to unknown. The model still receives the full result and its plain-words summary.
A row half a second early must count, which fails if the clock tolerance is removed, and two international numbers must never match through the Mac's region.
The probe's clock was fixed, so a check that ever reached the poll would never reach its deadline. It now moves half a second per sleep and still never waits.
…ing for the send check

The check now stops once the reads have shown one delivered row and no error row for a second, and reads the whole window otherwise. A row that matched is tracked by row id, so a later date change cannot drop it. Handles match national and international spellings through the region's trunk prefixes, North American 011 dialling, a bracketed 0 and extensions, never across forms without a region, and a recipient matching two different stored numbers reads as ambiguous. A stop before the send returns cancelled with nothing sent, a timed-out send reports only the service the history shows, the calling-code table gains the missing ITU regions, and two private test counters use the shared one.
… ledger

The ledger honours outcome_unknown only from send_message and only as the exact boolean true.
The settled second restarts when the delivered row changes or a read fails, and a Sapporo number written 011 stays national in Japan.
…or the send check

A live send from a Mac set to India to a US number typed nationally read as unconfirmed, because the region rule tried +91 while Messages had resolved the number to +1 through the account. A national number now matches an international handle when it equals that handle without a real calling code: only the sending account's code when chat.account_login is a phone number, otherwise any code in the table. A request that matches two different stored numbers stays ambiguous, two international numbers never match, and the incoming copy of a text to yourself never counts. The calling-code table is now data and is checked for duplicate regions.
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @sauranshbhardwaj — this is careful work, and the test coverage is exceptional. I reviewed the full diff; notes below.

What stands out well:

  • MessageSendConfirmation.swift — the send runs exactly once on every path, and unconfirmed is modeled as an unknown outcome rather than a failure. The pre-send row-id baseline, tracked row IDs surviving date changes, and the settled-delivery window are all the right shape. MessageSendConfirmationTests (~720 lines) against a real-shaped SQLite fixture with an injected clock covers the iMessage-error-then-SMS-fallback, ambiguity, tolerance-window, and cancellation paths genuinely.
  • relay-tool-result.ts / tool-invocation-ledger.ts — EXECUTOR_OUTCOME_UNKNOWN_TOOLS is scoped to send_message only, the flag can only lower a success to unknown, and only the exact boolean is honored (tested against "true", 1, "yes", {}).
  • MessagingHandle.swift + docs/device-tool-surface.md — the calling-code table, trunk-prefix handling, and never-guess-on-ambiguity rule are documented with their accepted limits stated plainly.
  • DeviceToolsProbe.sendCheckDetail — the stand-in send asserting sendCheckSends: 1 against the absent store keeps the e2e probe non-sending; good design.
  • The LockedCounter extraction into OmiSupport cleanly de-duplicates two private test copies.

Two things I'd want a maintainer to sign off on before merge:

  1. New ledger terminal state. tool-invocation-ledger.ts / kernel-core.ts / run-tool-capability.ts widen every tool-invocation completion from succeeded | failed to include outcome_unknown, recorded with error_code = executor_reported_outcome_unknown. The implementation is tightly scoped, but this is an architecture decision for the kernel's contract with every future tool, not just send_message — worth a maintainer deciding whether unknown outcomes belong in the shared ledger versus staying tool-local.

  2. Behavior change to read_message_history. chatsForHandle replaces the exact-handle SQL match, so a request like +141****0123 can now resolve to a differently-stored spelling of the same number (e.g. 4155550123) and prefers the most recently active direct chat. The new matching is well tested and docs/device-tool-surface.md states the change plainly, but reading a different chat than the literal handle requested is a user-visible behavior change to an existing tool, so it deserves a product decision, not just a correctness check.

Minor observations: MessagesSenderService.sendTimedOut no longer surfaces as allServicesFailed — timed-out sends are checked from history instead, which is consistent with the never-send-twice goal; busyMode = .timeout(0.25) on the reader is a sensible tolerance for WAL checkpoints; the generated manifest digests in GeneratedToolExecutors.swift are updated in step with the manifest bullet changes.

Both items above need a human maintainer decision before this can merge; the implementation quality itself is not in question.

This feedback is automated, posted on behalf of the Omi maintainers.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge positive-signal Automation verified a genuine fix/quality contribution macOS labels Oct 12, 2026
@sauranshbhardwaj

Copy link
Copy Markdown
Contributor Author

Thanks for the review. On the two sign-off points:

  1. Ledger state. outcome_unknown already exists on main: it's in ToolInvocationStatus (tool-invocation-ledger.ts:8), and the ledger already moves dispatched invocations into it when an outcome is lost, for example on restart (:179, :219). What this PR adds is one more way in. A completing executor can now report it, and only send_message may do that, and only to lower a success. The contract with other tools doesn't change. If you'd rather keep that tool-local, I can record the unknown outcome in send_message's own result only and leave the completion path as it was.
  2. read_message_history. Fair. If you'd rather the existing tool keep its exact-handle match, I'll split it: the send check keeps the wider same-number matching it needs (Contacts and Messages spell numbers differently), and read_message_history goes back to the literal handle. It's a small change. Say which you prefer.

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

macOS needs-maintainer-review Needs a human maintainer to sign off before merge positive-signal Automation verified a genuine fix/quality contribution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants