adding AI clone (track 2) making Omi respond to people on my behalf using my apps like telegram/whatsapp/imessage using beeper - #11067
adding AI clone (track 2) making Omi respond to people on my behalf using my apps like telegram/whatsapp/imessage using beeper#11067abdolotfy wants to merge 13 commits into
Conversation
…sing my apps like telegram/whatsapp/imessage using beeper
There was a problem hiding this comment.
24 issues found across 35 files
Confidence score: 2/5
- In
desktop/windows/src/main/aiClone/autoReplyPolicy.tsanddesktop/windows/src/renderer/src/components/chat/AiCloneDraftHost.tsx, untrusted inbound text can steer draft generation and then bypass a narrow safety phrase gate, allowing unintended sensitive commitments to be auto-sent. Harden prompt construction and replace phrase matching with a broader policy classifier/denylist before any send path. - In
desktop/windows/src/main/aiClone/chatMonitor.tsanddesktop/windows/src/main/aiClone/beeperClient.ts, cursor and pagination behavior can skip messages under load or oldest-first responses, which can cause indefinite missed inbound events and silent automation gaps. Make cursor advancement contiguous to inspected messages and paginate/search in a way that guarantees newest unseen messages are reached. - In
desktop/windows/src/renderer/src/components/settings/tabs/AiCloneTab.tsx,desktop/windows/src/main/aiClone/draftStore.ts, anddesktop/windows/src/renderer/src/components/chat/AiCloneDraftHost.tsx, overlapping async approvals/model calls can duplicate sends or reorder replies, producing confusing or incorrect outbound behavior. Add per-draft claim/in-flight state and serialize submission order for drafts discovered in the same poll. - In
desktop/windows/src/main/ipc/aiClone.tsanddesktop/windows/src/renderer/src/hooks/useAiClone.ts, in-flight work and stale responses can outlive disconnects or refresh races, and persona/message history is broadcast too broadly to renderer windows. Gate all async results on a connection/request generation and restrict events to the specific renderer hostingAiCloneDraftHost.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="desktop/windows/src/renderer/src/pages/Settings.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/pages/Settings.tsx:29">
P2: Opening any Settings tab now triggers Beeper-related IPC calls and keeps a 15-second drafts poll running for the entire Settings page lifetime, despite `AiCloneTab` describing the poll as active while its tab is open. Passing active state into the data/polling effects, or otherwise pausing them for hidden panels, would avoid unnecessary work and background requests.</violation>
</file>
<file name="desktop/windows/src/main/aiClone/beeperTokenStore.ts">
<violation number="1" location="desktop/windows/src/main/aiClone/beeperTokenStore.ts:59">
P2: A process crash or power loss during `set()` can leave `beeper-token.json` truncated; `readFile()` then treats the credential as absent and the user must reconnect. Atomic temp-file replacement would keep the previous valid token until the new ciphertext is fully written.</violation>
<violation number="2" location="desktop/windows/src/main/aiClone/beeperTokenStore.ts:63">
P2: A transient unlink failure can make Disconnect reject before the cached Beeper client is cleared and polling is stopped, leaving AI Clone active. Best-effort deletion or a `try/finally` around the caller's in-memory shutdown would preserve disconnect safety when the file cannot be removed.</violation>
</file>
<file name="desktop/windows/src/main/aiClone/autoReplyPolicy.ts">
<violation number="1" location="desktop/windows/src/main/aiClone/autoReplyPolicy.ts:39">
P1: Sensitive auto-send drafts can bypass this gate when they use wording outside the narrow phrase list, including contract or ordinary money commitments; the same problem occurs when the draft agrees to a sensitive incoming request without repeating its keywords. Expanding the safety policy or failing closed for uncertain drafts would prevent the only pre-`sendMessage` guard from treating these as ordinary replies.</violation>
</file>
<file name="desktop/windows/src/main/aiClone/chatMonitor.ts">
<violation number="1" location="desktop/windows/src/main/aiClone/chatMonitor.ts:54">
P1: High-volume chats can permanently lose messages because the cursor jumps past messages omitted by the 20-message fetch window. Pagination or a cursor that advances only through the contiguous portion actually inspected would avoid skipping the overflow.</violation>
<violation number="2" location="desktop/windows/src/main/aiClone/chatMonitor.ts:59">
P2: A chat whose initial poll is empty never gets a cursor, so the first message that arrives afterward is discarded by the next “first-ever poll” branch. The first empty poll should persist an initialization boundary (for example the poll time, or an explicit initialized state) so only messages present before opt-in are skipped.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/settings/tabs/AiCloneTab.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/settings/tabs/AiCloneTab.tsx:82">
P1: Rapid repeated clicks on Send can send the same reply more than once: this handler remains active until the async IPC call completes, while main removes the draft only after sending. Tracking per-draft submission state and disabling Send (and other conflicting actions) until completion would prevent duplicate messages.</violation>
</file>
<file name="desktop/windows/src/main/aiClone/beeperClient.test.ts">
<violation number="1" location="desktop/windows/src/main/aiClone/beeperClient.test.ts:16">
P2: The vi.mock factory for '@beeper/desktop-api' closes over module-level consts (accountsList, chatsSearch, messagesList, messagesSend), but vi.mock is hoisted above those declarations in Vitest, so the factory can hit a TDZ "Cannot access ... before initialization" at runtime. The repo's existing tests that reference mocks inside factories consistently wrap them in vi.hoisted (e.g. useRecorder.startup.test.tsx, usePushToTalk.test.tsx). Consider hoisting these fns (e.g. const accountsList = vi.hoisted(() => vi.fn())) for consistency and robustness.</violation>
</file>
<file name="desktop/windows/src/main/aiClone/personaDraftPrompt.ts">
<violation number="1" location="desktop/windows/src/main/aiClone/personaDraftPrompt.ts:71">
P2: Review-queue drafts marked `[NEEDS_INPUT]` can be sent verbatim to the contact when the user clicks Send without editing. Stripping the internal marker/note or requiring a recipient-ready replacement before approval would prevent this internal control text from leaving the app.</violation>
</file>
<file name="desktop/windows/src/main/aiClone/beeperClient.ts">
<violation number="1" location="desktop/windows/src/main/aiClone/beeperClient.ts:90">
P3: `limit = 0` is not honored: `takeUpTo` consumes and returns one item because it checks the threshold only after `out.push(item)`. A pre-loop non-positive-limit guard would keep both adapter methods' public `limit` parameters predictable.</violation>
<violation number="2" location="desktop/windows/src/main/aiClone/beeperClient.ts:126">
P1: New inbound messages can be missed indefinitely if Beeper returns `messages.list` oldest-first, because this code truncates the auto-paginated stream before reaching newer pages. Using the chat-scoped `messages.search({ chatIDs: [chatID], limit })` path or an explicitly verified newest-page/direction request would make recency an enforced property rather than an assumption.</violation>
</file>
<file name="desktop/windows/src/main/codingAgent/agentSelection.ts">
<violation number="1" location="desktop/windows/src/main/codingAgent/agentSelection.ts:29">
P2: Text-only requests such as “write a summary” or “create a reply” can be routed away from an otherwise suitable agent because ordinary content verbs are classified as tool-heavy. Matching contextual file/command intent rather than bare verbs would avoid these false-positive capability penalties.</violation>
<violation number="2" location="desktop/windows/src/main/codingAgent/agentSelection.ts:59">
P2: Long-running prompts are ranked by native-resume support, but this execution path never reopens or resumes a task, so the signal changes agent selection without providing resumability. The ranking should be tied to an actual persisted-resume flow, or this criterion should be removed until that flow exists.</violation>
<violation number="3" location="desktop/windows/src/main/codingAgent/agentSelection.ts:67">
P3: The `artifactEmission` nudge can never add a score: every adapter in the capability matrix marks `artifactEmission` as `unsupported`, so `status === 'required'` is never true for the candidate set this module ranks. The line is dead relative to the current matrix. If this was intended as future-proofing, a comment noting that would help; otherwise dropping it keeps the scoring honest and easier to follow.</violation>
</file>
<file name="desktop/windows/src/renderer/src/hooks/useAiClone.ts">
<violation number="1" location="desktop/windows/src/renderer/src/hooks/useAiClone.ts:12">
P2: The settings UI can display stale connection, chat, or draft state when a refresh overlaps a mutation or the drafts poll. Tracking a request generation (or cancelling/ignoring superseded responses) before each state write would ensure only the latest refresh updates the queue and connection UI.</violation>
</file>
<file name="desktop/windows/src/main/codingAgent/taskRunner.ts">
<violation number="1" location="desktop/windows/src/main/codingAgent/taskRunner.ts:55">
P2: Unnamed delegation never reaches this ranking in the user-facing chat flow: `useChat` converts the first connected agent into `agentId`, which `candidateAgents` treats as an explicit choice and preserves first. As a result, the new task-aware selection cannot affect automatic agent choice; leaving `agentId` undefined for unnamed requests or moving selection into the task runner would make the ranking effective.</violation>
</file>
<file name="desktop/windows/src/renderer/src/components/chat/AiCloneDraftHost.tsx">
<violation number="1" location="desktop/windows/src/renderer/src/components/chat/AiCloneDraftHost.tsx:17">
P1: When one poll finds multiple inbound messages, auto-replies can be sent in the wrong order: each model call starts concurrently and the faster newer draft may reach `aiCloneSubmitDraft` before an older draft. Serializing drafts per chat (or otherwise preserving the poll order) would keep the conversation sequence intact.</violation>
<violation number="2" location="desktop/windows/src/renderer/src/components/chat/AiCloneDraftHost.tsx:26">
P1: An inbound sender can potentially steer the model into generating an unintended auto-reply because this line submits untrusted message text in a flattened user prompt, after which `autoReplyPolicy` may send the output automatically. Keeping instructions in a real system message and clearly isolating/validating quoted message content would reduce this prompt-injection path.</violation>
</file>
<file name="desktop/windows/src/main/ipc/aiClone.ts">
<violation number="1" location="desktop/windows/src/main/ipc/aiClone.ts:72">
P2: Private persona and message history are delivered to every BrowserWindow, including hidden auxiliary renderers that do not need to draft replies. Route this event only to the renderer hosting `AiCloneDraftHost` to reduce unnecessary sensitive-data exposure.</violation>
<violation number="2" location="desktop/windows/src/main/ipc/aiClone.ts:131">
P2: Group-chat prompts misattribute every participant’s message to the chat title, which can make the model misunderstand who said what and draft the wrong reply. Preserve sender identity (or explicitly label unknown participants) when constructing history and incoming context.</violation>
<violation number="3" location="desktop/windows/src/main/ipc/aiClone.ts:208">
P1: Disconnecting does not stop work already in flight, so a message can still be broadcast or sent after the user disconnects. Track a connection generation or cancellation signal and check it before broadcasting and sending.</violation>
</file>
<file name="desktop/windows/src/main/aiClone/draftStore.ts">
<violation number="1" location="desktop/windows/src/main/aiClone/draftStore.ts:33">
P2: The draft file is treated as `PendingDraft[]` without validating its array entries, so a valid-but-corrupt queue (for example `[null]`) can make `remove` throw or expose malformed drafts to the UI. Runtime schema validation/filtering of each record before returning it would make queue recovery safe.</violation>
<violation number="2" location="desktop/windows/src/main/aiClone/draftStore.ts:40">
P2: A crash or power loss during `add` or `remove` can truncate `ai-clone-drafts.json`; the next read then silently returns an empty queue, and a subsequent write can overwrite all pending drafts. An atomic temp-file/rename write, such as the repository’s `atomicWriteFileSync`, would preserve the persisted review queue.</violation>
<violation number="3" location="desktop/windows/src/main/aiClone/draftStore.ts:61">
P1: A user can send the same queued draft twice by triggering overlapping approvals: `get()` does not claim the row, and both approval handlers can send before either removes it. A persistent claim/in-flight state or serialized, idempotent approval flow would prevent duplicate Beeper messages.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| * exhaustive — a false negative just means an extra review-queue item next | ||
| * time this list is extended, a false positive costs one tap in the UI. */ | ||
| const SENSITIVE_HINTS = | ||
| /\b(password|ssn|social security|wire transfer|bank account|routing number|credit card number|breaking up|break up with|divorc\w*|lawsuit|diagnos\w*|prescription|i quit|i'?m quitting|got fired|laid off|layoff)\b/i |
There was a problem hiding this comment.
P1: Sensitive auto-send drafts can bypass this gate when they use wording outside the narrow phrase list, including contract or ordinary money commitments; the same problem occurs when the draft agrees to a sensitive incoming request without repeating its keywords. Expanding the safety policy or failing closed for uncertain drafts would prevent the only pre-sendMessage guard from treating these as ordinary replies.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/aiClone/autoReplyPolicy.ts, line 39:
<comment>Sensitive auto-send drafts can bypass this gate when they use wording outside the narrow phrase list, including contract or ordinary money commitments; the same problem occurs when the draft agrees to a sensitive incoming request without repeating its keywords. Expanding the safety policy or failing closed for uncertain drafts would prevent the only pre-`sendMessage` guard from treating these as ordinary replies.</comment>
<file context>
@@ -0,0 +1,53 @@
+ * exhaustive — a false negative just means an extra review-queue item next
+ * time this list is extended, a false positive costs one tap in the UI. */
+const SENSITIVE_HINTS =
+ /\b(password|ssn|social security|wire transfer|bank account|routing number|credit card number|breaking up|break up with|divorc\w*|lawsuit|diagnos\w*|prescription|i quit|i'?m quitting|got fired|laid off|layoff)\b/i
+
+export function looksSensitive(text: string): boolean {
</file context>
| ): NewInboundResult { | ||
| const sorted = [...messages].sort((a, b) => a.timestamp - b.timestamp) | ||
| const newest = sorted[sorted.length - 1] | ||
| const cursor = { latestMessageId: newest?.id, latestTimestamp: newest?.timestamp } |
There was a problem hiding this comment.
P1: High-volume chats can permanently lose messages because the cursor jumps past messages omitted by the 20-message fetch window. Pagination or a cursor that advances only through the contiguous portion actually inspected would avoid skipping the overflow.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/aiClone/chatMonitor.ts, line 54:
<comment>High-volume chats can permanently lose messages because the cursor jumps past messages omitted by the 20-message fetch window. Pagination or a cursor that advances only through the contiguous portion actually inspected would avoid skipping the overflow.</comment>
<file context>
@@ -0,0 +1,70 @@
+): NewInboundResult {
+ const sorted = [...messages].sort((a, b) => a.timestamp - b.timestamp)
+ const newest = sorted[sorted.length - 1]
+ const cursor = { latestMessageId: newest?.id, latestTimestamp: newest?.timestamp }
+
+ if (lastSeenTimestamp === undefined) {
</file context>
| } | ||
|
|
||
| private writeFile(data: StoredFile): void { | ||
| writeFileSync(this.filePath, JSON.stringify(data), 'utf8') |
There was a problem hiding this comment.
P2: A crash or power loss during add or remove can truncate ai-clone-drafts.json; the next read then silently returns an empty queue, and a subsequent write can overwrite all pending drafts. An atomic temp-file/rename write, such as the repository’s atomicWriteFileSync, would preserve the persisted review queue.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/aiClone/draftStore.ts, line 40:
<comment>A crash or power loss during `add` or `remove` can truncate `ai-clone-drafts.json`; the next read then silently returns an empty queue, and a subsequent write can overwrite all pending drafts. An atomic temp-file/rename write, such as the repository’s `atomicWriteFileSync`, would preserve the persisted review queue.</comment>
<file context>
@@ -0,0 +1,63 @@
+ }
+
+ private writeFile(data: StoredFile): void {
+ writeFileSync(this.filePath, JSON.stringify(data), 'utf8')
+ }
+
</file context>
| if (lastSeenTimestamp === undefined) { | ||
| // First-ever poll for this chat: establish the cursor, draft nothing | ||
| // retroactively. | ||
| return { newMessages: [], ...cursor } |
There was a problem hiding this comment.
P2: A chat whose initial poll is empty never gets a cursor, so the first message that arrives afterward is discarded by the next “first-ever poll” branch. The first empty poll should persist an initialization boundary (for example the poll time, or an explicit initialized state) so only messages present before opt-in are skipped.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/aiClone/chatMonitor.ts, line 59:
<comment>A chat whose initial poll is empty never gets a cursor, so the first message that arrives afterward is discarded by the next “first-ever poll” branch. The first empty poll should persist an initialization boundary (for example the poll time, or an explicit initialized state) so only messages present before opt-in are skipped.</comment>
<file context>
@@ -0,0 +1,70 @@
+ if (lastSeenTimestamp === undefined) {
+ // First-ever poll for this chat: establish the cursor, draft nothing
+ // retroactively.
+ return { newMessages: [], ...cursor }
+ }
+
</file context>
| if (!existsSync(this.filePath)) return [] | ||
| try { | ||
| const raw = JSON.parse(readFileSync(this.filePath, 'utf8')) as StoredFile | ||
| return Array.isArray(raw) ? raw : [] |
There was a problem hiding this comment.
P2: The draft file is treated as PendingDraft[] without validating its array entries, so a valid-but-corrupt queue (for example [null]) can make remove throw or expose malformed drafts to the UI. Runtime schema validation/filtering of each record before returning it would make queue recovery safe.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/aiClone/draftStore.ts, line 33:
<comment>The draft file is treated as `PendingDraft[]` without validating its array entries, so a valid-but-corrupt queue (for example `[null]`) can make `remove` throw or expose malformed drafts to the UI. Runtime schema validation/filtering of each record before returning it would make queue recovery safe.</comment>
<file context>
@@ -0,0 +1,63 @@
+ if (!existsSync(this.filePath)) return []
+ try {
+ const raw = JSON.parse(readFileSync(this.filePath, 'utf8')) as StoredFile
+ return Array.isArray(raw) ? raw : []
+ } catch {
+ return []
</file context>
| * fetching more pages than needed. */ | ||
| async function takeUpTo<T>(iterable: AsyncIterable<T>, limit: number): Promise<T[]> { | ||
| const out: T[] = [] | ||
| for await (const item of iterable) { |
There was a problem hiding this comment.
P3: limit = 0 is not honored: takeUpTo consumes and returns one item because it checks the threshold only after out.push(item). A pre-loop non-positive-limit guard would keep both adapter methods' public limit parameters predictable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/aiClone/beeperClient.ts, line 90:
<comment>`limit = 0` is not honored: `takeUpTo` consumes and returns one item because it checks the threshold only after `out.push(item)`. A pre-loop non-positive-limit guard would keep both adapter methods' public `limit` parameters predictable.</comment>
<file context>
@@ -0,0 +1,135 @@
+ * fetching more pages than needed. */
+async function takeUpTo<T>(iterable: AsyncIterable<T>, limit: number): Promise<T[]> {
+ const out: T[] = []
+ for await (const item of iterable) {
+ out.push(item)
+ if (out.length >= limit) break
</file context>
| // tool-heavy task) — they never fire on their own and never outweigh a | ||
| // primary signal. | ||
| if (expectations.modelSwitching.status === 'required') score += 0.5 | ||
| if (expectations.artifactEmission.status === 'required') score += 0.5 |
There was a problem hiding this comment.
P3: The artifactEmission nudge can never add a score: every adapter in the capability matrix marks artifactEmission as unsupported, so status === 'required' is never true for the candidate set this module ranks. The line is dead relative to the current matrix. If this was intended as future-proofing, a comment noting that would help; otherwise dropping it keeps the scoring honest and easier to follow.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At desktop/windows/src/main/codingAgent/agentSelection.ts, line 67:
<comment>The `artifactEmission` nudge can never add a score: every adapter in the capability matrix marks `artifactEmission` as `unsupported`, so `status === 'required'` is never true for the candidate set this module ranks. The line is dead relative to the current matrix. If this was intended as future-proofing, a comment noting that would help; otherwise dropping it keeps the scoring honest and easier to follow.</comment>
<file context>
@@ -0,0 +1,85 @@
+ // tool-heavy task) — they never fire on their own and never outweigh a
+ // primary signal.
+ if (expectations.modelSwitching.status === 'required') score += 0.5
+ if (expectations.artifactEmission.status === 'required') score += 0.5
+
+ return score
</file context>
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks for putting this together — the direction is interesting, and I appreciate the tests around the new AI Clone path.
I’m going to request changes before this can move forward. This feature can send messages as the user through Beeper, so the safety/privacy bar is much higher than for a normal desktop feature. The main blockers I see on this head are:
- The Beeper token, per-chat settings, and queued drafts are stored at the install/userData level and are not tied to the signed-in Omi account or cleared as part of account switch/sign-out teardown. That can expose a previous user’s messaging account/drafts to the next Omi account on the same machine.
aiClone:setChatMode/persisted settings are not runtime-validated, anddecideReplyActiontreats any value other thanoff/draftas auto-send. Unknown or corrupted state needs to fail closed tooff, never to sending.- The draft prompt is flattened into one string before calling the model, so untrusted chat/persona/history content can compete with the intended system instructions. For an auto-send path, please preserve instruction boundaries and treat remote messages as quoted data.
- Polling advances the cursor before draft generation/submission succeeds and has no in-flight/cancellation guard. Failures, overlapping polls, or disconnects can skip messages permanently or duplicate/continue sends after the user’s intent changed.
- Draft approval/auto-send paths are not idempotent against overlapping submits/clicks, so the same draft/message can be sent more than once.
Please address these before maintainer review: scope/clear all AI Clone state with the authenticated account lifecycle, validate all IPC and persisted modes, fail closed for ambiguous send decisions, make cursor advancement acknowledge successful processing, add in-flight/cancel guards, and make sends/draft approvals idempotent. I’d also like a human maintainer/product review before merge because auto-replying as the user is a sensitive product surface.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
… composite cursor
… explicit wipe path
|
Thanks for the detailed review — addressed all five points below. Some required more digging than expected; noting where a fix took more than one attempt to get right.
Happy to keep closing out the remaining partial items, or take a checkpoint here if you'd rather review what's landed so far. |
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks for the follow-up fixes here — several of the earlier lifecycle and idempotency issues are moving in the right direction. I still need to keep this in changes-requested because the remaining gaps are on the path that can send messages as the user.
Blocking items on this head:
- The auto-send safety gate is still too narrow for this risk level.
decideReplyActiononly looks for a small set of sensitive words in the generated draft, so an incoming message can ask for a sensitive commitment and the model can produce a short agreement like “sounds good” or “I agree” that bypasses the gate. For auto-send, please fail closed when either the incoming request or the draft is uncertain/sensitive, or route through a stronger explicit policy check beforesendMessage. - The ordinary Disconnect path is not fail-safe.
aiClone:disconnectcallstokenStore().clear()before bumping the session generation and stopping polling; if the file removal throws, the IPC call rejects before the in-memory client/poll loop is invalidated. The shutdown/cancellation state should be updated in afinally-style path so a failed token-file delete cannot leave AI Clone active. - Multiple inbound messages from the same poll are still drafted/submitted concurrently in
AiCloneDraftHost. In an auto-send chat, a newer reply can be sent before an older one if its model call returns first. Please serialize processing per chat or otherwise preserve the poll/message order before any send path.
This should remain human maintainer/product reviewed before merge: auto-replying through Beeper as the user is a sensitive product surface, and the dependency/lockfile addition should also get normal dependency review.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
What changed and why
Adds Track 2's AI Clone feature: Omi can draft, or in trusted chats auto-send,
replies to inbound messages on the user's behalf. Connects through Beeper
Desktop's local API rather than integrating each messaging network directly,
so WhatsApp/Telegram/iMessage/etc. are all covered through one connection.
Product invariants affected
None. Checked against docs/product/invariants/ — no changed path matches a
locked invariant's path globs (chat-continuity.md's globs cover
agentKernel/**, ipc/mainChat.ts, and hooks/useChat.ts specifically; this
feature adds a separate draft/send path that doesn't touch any of those).
How it was verified
npx vitest run src/main/aiClone src/main/ipc/aiClone.test.ts src/renderer/src/components/chat/AiCloneDraftHost.test.tsx src/renderer/src/components/settings/tabs/AiCloneTab.test.tsx— 63 testspassing across 10 files
npx tsc --noEmit -p tsconfig.node.json/-p tsconfig.web.json— no newerrors introduced
Beeper Desktop instance: connected a real access token, listed real chats,
set a chat to "Draft for review," sent a live test message from another
device, confirmed a draft appeared in the review queue within the poll
interval, and sent it from the UI.
messages.list's exact ordering guarantee fromBeeper's API is assumed newest-first per its documentation; this held in
manual testing but wasn't stress-tested against a chat with high message
volume or out-of-order delivery.
Tests
Feature — core path and main error paths both covered:
beeperClient.test.ts— API client mapping and error propagation on a badtoken
chatMonitor.test.ts— new-message detection against a cursorautoReplyPolicy.test.ts— send/queue/skip decision, including thesensitive-topic and needs-input gates that override auto-send
ipc/aiClone.test.ts— end-to-end poll → detect → broadcast, and thesubmit-draft policy routing
AiCloneDraftHost.test.tsx— draft generation and graceful handling of afailed model call
AiCloneTab.test.tsx— connect flow (success and failure), mode changes,draft approve/dismiss
Failure class (fixes)
Failure-Class: none
New guards (only when adding a check or ratchet)
None added.
Scoped cleanups (optional)
None.